From 3d05655b4405f63fedec9fee3e5f49647f809858 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Mon, 20 May 2024 12:40:35 -0300 Subject: [PATCH 01/14] chore: move commands to cli package, avoid more than one file on main package --- cmd/sid/{ => cli}/init.go | 4 ++-- cmd/sid/{ => cli}/start.go | 4 ++-- cmd/sid/main.go | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) rename cmd/sid/{ => cli}/init.go (97%) rename cmd/sid/{ => cli}/start.go (98%) diff --git a/cmd/sid/init.go b/cmd/sid/cli/init.go similarity index 97% rename from cmd/sid/init.go rename to cmd/sid/cli/init.go index a399c60..0b140fc 100644 --- a/cmd/sid/init.go +++ b/cmd/sid/cli/init.go @@ -1,4 +1,4 @@ -package main +package cli import ( "fmt" @@ -13,7 +13,7 @@ import ( const forceFlag = "force" -var initCommand = cli.Command{ +var InitCommand = cli.Command{ Name: "init", Usage: "Initialize the staking indexer home directory.", Flags: []cli.Flag{ diff --git a/cmd/sid/start.go b/cmd/sid/cli/start.go similarity index 98% rename from cmd/sid/start.go rename to cmd/sid/cli/start.go index b532704..eecb4a8 100644 --- a/cmd/sid/start.go +++ b/cmd/sid/cli/start.go @@ -1,4 +1,4 @@ -package main +package cli import ( "fmt" @@ -24,7 +24,7 @@ const ( paramsPathFlag = "params-path" ) -var startCommand = cli.Command{ +var StartCommand = cli.Command{ Name: "start", Usage: "Start the staking-indexer server", Description: "Start the staking-indexer server.", diff --git a/cmd/sid/main.go b/cmd/sid/main.go index f7e2924..6224ab8 100644 --- a/cmd/sid/main.go +++ b/cmd/sid/main.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + sidcli "github.com/babylonchain/staking-indexer/cmd/sid/cli" "github.com/urfave/cli" ) @@ -16,7 +17,7 @@ func main() { app := cli.NewApp() app.Name = "sid" app.Usage = "Staking Indexer Daemon (sid)." - app.Commands = append(app.Commands, startCommand, initCommand) + app.Commands = append(app.Commands, sidcli.StartCommand, sidcli.InitCommand) if err := app.Run(os.Args); err != nil { fatal(err) From f2389e42b7836d755f0af6ef1878ab004e3ab324 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Mon, 20 May 2024 19:36:31 -0300 Subject: [PATCH 02/14] feat: add new CLI to get btc headers to btclightclient format --- cmd/sid/cli/btc_headers.go | 137 ++++++++++++++++++++++++++++++++ cmd/sid/cli/btc_headers_test.go | 74 +++++++++++++++++ cmd/sid/main.go | 2 +- itest/test_manager.go | 12 +-- 4 files changed, 218 insertions(+), 7 deletions(-) create mode 100644 cmd/sid/cli/btc_headers.go create mode 100644 cmd/sid/cli/btc_headers_test.go diff --git a/cmd/sid/cli/btc_headers.go b/cmd/sid/cli/btc_headers.go new file mode 100644 index 0000000..796448b --- /dev/null +++ b/cmd/sid/cli/btc_headers.go @@ -0,0 +1,137 @@ +package cli + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + + babylontypes "github.com/babylonchain/babylon/types" + bbnbtclightclienttypes "github.com/babylonchain/babylon/x/btclightclient/types" + "github.com/babylonchain/staking-indexer/btcclient" + "github.com/babylonchain/staking-indexer/config" + "github.com/babylonchain/staking-indexer/log" + "github.com/babylonchain/staking-indexer/utils" + "github.com/urfave/cli" + + sdkmath "cosmossdk.io/math" +) + +const ( + outputFileFlag = "outputfile-path" +) + +var BtcHeaderCommand = cli.Command{ + Name: "btc-headers", + Usage: "btc-headers 10 15 --outputfile-path ~/myoutput/path/btc-headers.json", + Description: `Get BTC headers "from" and "to" a specific block height.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: homeFlag, + Usage: "The path to the staking indexer home directory", + Value: config.DefaultHomeDir, + }, + cli.StringFlag{ + Name: outputFileFlag, + Usage: "The path to the output file", + Value: filepath.Join(config.DefaultHomeDir, "output-btc-headers.json"), + }, + }, + Action: btcHeaders, +} + +func btcHeaders(ctx *cli.Context) error { + args := ctx.Args() + if len(args) != 2 { + return fmt.Errorf("not enough params, please specify 'from' and 'to' block") + } + + fromStr, toStr := args[0], args[1] + fromBlock, err := strconv.ParseUint(fromStr, 10, 64) + if err != nil { + return fmt.Errorf("unable to parse %s: %w", fromStr, err) + } + + toBlock, err := strconv.ParseUint(toStr, 10, 64) + if err != nil { + return fmt.Errorf("unable to parse %s: %w", toStr, err) + } + + if fromBlock > toBlock { + return fmt.Errorf("the from %d block should be less than to block %d", fromBlock, toBlock) + } + + homePath, err := filepath.Abs(ctx.String(homeFlag)) + if err != nil { + return err + } + homePath = utils.CleanAndExpandPath(homePath) + + cfg, err := config.LoadConfig(homePath) + if err != nil { + return fmt.Errorf("failed to load configuration: %w", err) + } + + logger, err := log.NewRootLoggerWithFile(config.LogFile(homePath), cfg.LogLevel) + if err != nil { + return fmt.Errorf("failed to initialize the logger: %w", err) + } + + btcClient, err := btcclient.NewBTCClient( + cfg.BTCConfig, + logger, + ) + if err != nil { + return fmt.Errorf("failed to initialize the BTC client: %w", err) + } + + btcHeaders, err := BtcHeaderInfo(btcClient, fromBlock, toBlock) + if err != nil { + return fmt.Errorf("failed to get BTC Header blocks: %w", err) + } + + genState := bbnbtclightclienttypes.GenesisState{ + BtcHeaders: btcHeaders, + } + + bz, err := json.MarshalIndent(genState, "", " ") + if err != nil { + return fmt.Errorf("failed to generate json to set to output file %+v: %w", genState, err) + } + + outputFilePath := ctx.String(outputFileFlag) + file, err := os.OpenFile(outputFilePath, os.O_CREATE, os.ModePerm) + if err != nil { + return fmt.Errorf("failed to open output file %s: %w", outputFilePath, err) + } + defer file.Close() + + if _, err := file.Write(bz); err != nil { + return fmt.Errorf("failed to write to output file %s: %w", outputFilePath, err) + } + return nil +} + +// BtcHeaderInfo queries the btc client for (fromBlk ~ toBlk) BTC blocks, converting to BTCHeaderInfo. +func BtcHeaderInfo(btcClient *btcclient.BTCClient, fromBlk, toBlk uint64) ([]*bbnbtclightclienttypes.BTCHeaderInfo, error) { + btcHeaders := make([]*bbnbtclightclienttypes.BTCHeaderInfo, 0, toBlk-fromBlk) + var currenWork = sdkmath.ZeroUint() + + for blkHeight := fromBlk; blkHeight < toBlk; blkHeight++ { + idxBlock, err := btcClient.GetBlockByHeight(blkHeight) + if err != nil { + return nil, fmt.Errorf("failed to get block height %d from BTC client: %w", blkHeight, err) + } + blkHeader := idxBlock.Header + + headerWork := bbnbtclightclienttypes.CalcHeaderWork(blkHeader) + currenWork = bbnbtclightclienttypes.CumulativeWork(headerWork, currenWork) + + headerBytes := babylontypes.NewBTCHeaderBytesFromBlockHeader(blkHeader) + + bbnBtcHeaderInfo := bbnbtclightclienttypes.NewBTCHeaderInfo(&headerBytes, headerBytes.Hash(), blkHeight, ¤Work) + btcHeaders = append(btcHeaders, bbnBtcHeaderInfo) + } + return btcHeaders, nil +} diff --git a/cmd/sid/cli/btc_headers_test.go b/cmd/sid/cli/btc_headers_test.go new file mode 100644 index 0000000..99de723 --- /dev/null +++ b/cmd/sid/cli/btc_headers_test.go @@ -0,0 +1,74 @@ +package cli_test + +import ( + "math/rand" + "testing" + + bbnbtclightclienttypes "github.com/babylonchain/babylon/x/btclightclient/types" + "github.com/babylonchain/staking-indexer/btcclient" + "github.com/babylonchain/staking-indexer/cmd/sid/cli" + e2etest "github.com/babylonchain/staking-indexer/itest" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestBtcHeaders(t *testing.T) { + r := rand.New(rand.NewSource(10)) + blocksPerRetarget := 2016 + genState := bbnbtclightclienttypes.DefaultGenesis() + + initBlocksQnt := r.Intn(15) + blocksPerRetarget + btcd, btcClient := StartBtcClientAndBtcHandler(t, initBlocksQnt) + + // from zero height + infos, err := cli.BtcHeaderInfo(btcClient, 0, uint64(initBlocksQnt)) + require.NoError(t, err) + require.Equal(t, len(infos), initBlocksQnt) + + // should be valid on genesis, start from zero height. + genState.BtcHeaders = infos + require.NoError(t, genState.Validate()) + + generatedBlocksQnt := r.Intn(15) + 2 + btcd.GenerateBlocks(generatedBlocksQnt) + totalBlks := initBlocksQnt + generatedBlocksQnt + + // check from height with interval + fromBlockHeight := blocksPerRetarget - 1 + toBlockHeight := totalBlks - 2 + + infos, err = cli.BtcHeaderInfo(btcClient, uint64(fromBlockHeight), uint64(toBlockHeight)) + require.NoError(t, err) + require.Equal(t, len(infos), int(toBlockHeight-fromBlockHeight)) + + // try to check if it is valid on genesis, should fail is not retarget block. + genState.BtcHeaders = infos + require.EqualError(t, genState.Validate(), "genesis block must be a difficulty adjustment block") + + // from retarget block + infos, err = cli.BtcHeaderInfo(btcClient, uint64(blocksPerRetarget), uint64(totalBlks)) + require.NoError(t, err) + require.Equal(t, len(infos), int(totalBlks-blocksPerRetarget)) + + // check if it is valid on genesis + genState.BtcHeaders = infos + require.NoError(t, genState.Validate()) +} + +func StartBtcClientAndBtcHandler(t *testing.T, generateNBlocks int) (*e2etest.BitcoindTestHandler, *btcclient.BTCClient) { + btcd := e2etest.NewBitcoindHandler(t) + btcd.Start() + _ = btcd.CreateWallet(e2etest.WalletName, e2etest.Passphrase) + + resp := btcd.GenerateBlocks(generateNBlocks) + require.Equal(t, len(resp.Blocks), generateNBlocks) + + cfg := e2etest.DefaultStakingIndexerConfig(t.TempDir()) + btcClient, err := btcclient.NewBTCClient( + cfg.BTCConfig, + zap.NewNop(), + ) + require.NoError(t, err) + + return btcd, btcClient +} diff --git a/cmd/sid/main.go b/cmd/sid/main.go index 6224ab8..6d4a739 100644 --- a/cmd/sid/main.go +++ b/cmd/sid/main.go @@ -17,7 +17,7 @@ func main() { app := cli.NewApp() app.Name = "sid" app.Usage = "Staking Indexer Daemon (sid)." - app.Commands = append(app.Commands, sidcli.StartCommand, sidcli.InitCommand) + app.Commands = append(app.Commands, sidcli.StartCommand, sidcli.InitCommand, sidcli.BtcHeaderCommand) if err := app.Run(os.Args); err != nil { fatal(err) diff --git a/itest/test_manager.go b/itest/test_manager.go index 6b29e9c..1133505 100644 --- a/itest/test_manager.go +++ b/itest/test_manager.go @@ -59,14 +59,14 @@ var ( eventuallyWaitTimeOut = 1 * time.Minute eventuallyPollTime = 500 * time.Millisecond testParamsPath = "test-params.json" - passphrase = "pass" - walletName = "test-wallet" + Passphrase = "pass" + WalletName = "test-wallet" ) func StartManagerWithNBlocks(t *testing.T, n int) *TestManager { h := NewBitcoindHandler(t) h.Start() - _ = h.CreateWallet(walletName, passphrase) + _ = h.CreateWallet(WalletName, Passphrase) resp := h.GenerateBlocks(n) minerAddressDecoded, err := btcutil.DecodeAddress(resp.Address, regtestParams) @@ -80,13 +80,13 @@ func StartManagerWithNBlocks(t *testing.T, n int) *TestManager { } func StartWithBitcoinHandler(t *testing.T, h *BitcoindTestHandler, minerAddress btcutil.Address, dirPath string, startHeight uint64) *TestManager { - cfg := defaultStakingIndexerConfig(dirPath) + cfg := DefaultStakingIndexerConfig(dirPath) logger, err := log.NewRootLoggerWithFile(config.LogFile(dirPath), "debug") require.NoError(t, err) rpcclient, err := rpcclient.New(cfg.BTCConfig.ToConnConfig(), nil) require.NoError(t, err) - err = rpcclient.WalletPassphrase(passphrase, 200) + err = rpcclient.WalletPassphrase(Passphrase, 200) require.NoError(t, err) walletPrivKey, err := rpcclient.DumpPrivKey(minerAddress) require.NoError(t, err) @@ -188,7 +188,7 @@ func ReStartFromHeight(t *testing.T, tm *TestManager, height uint64) *TestManage return restartedTm } -func defaultStakingIndexerConfig(homePath string) *config.Config { +func DefaultStakingIndexerConfig(homePath string) *config.Config { defaultConfig := config.DefaultConfigWithHome(homePath) // both wallet and node are bicoind From a897a6b94395da6dde1c81ace78d2f10cec56a45 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Mon, 20 May 2024 19:55:28 -0300 Subject: [PATCH 03/14] fix: write to output file, inclue last block of 'toBlock' to load as well --- cmd/sid/cli/btc_headers.go | 18 ++++++++++-------- cmd/sid/cli/btc_headers_test.go | 6 +++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cmd/sid/cli/btc_headers.go b/cmd/sid/cli/btc_headers.go index 796448b..11490fa 100644 --- a/cmd/sid/cli/btc_headers.go +++ b/cmd/sid/cli/btc_headers.go @@ -14,6 +14,7 @@ import ( "github.com/babylonchain/staking-indexer/log" "github.com/babylonchain/staking-indexer/utils" "github.com/urfave/cli" + "go.uber.org/zap" sdkmath "cosmossdk.io/math" ) @@ -101,15 +102,16 @@ func btcHeaders(ctx *cli.Context) error { } outputFilePath := ctx.String(outputFileFlag) - file, err := os.OpenFile(outputFilePath, os.O_CREATE, os.ModePerm) - if err != nil { - return fmt.Errorf("failed to open output file %s: %w", outputFilePath, err) + if err := os.WriteFile(outputFilePath, bz, 0644); err != nil { + return fmt.Errorf("failed to write to output %s file %s: %w", bz, outputFilePath, err) } - defer file.Close() - if _, err := file.Write(bz); err != nil { - return fmt.Errorf("failed to write to output file %s: %w", outputFilePath, err) - } + logger.Info( + "Successfully wrote btc headers to file", + zap.Uint64("fromBlock", fromBlock), + zap.Uint64("toBlock", toBlock), + zap.String("outputFile", outputFilePath), + ) return nil } @@ -118,7 +120,7 @@ func BtcHeaderInfo(btcClient *btcclient.BTCClient, fromBlk, toBlk uint64) ([]*bb btcHeaders := make([]*bbnbtclightclienttypes.BTCHeaderInfo, 0, toBlk-fromBlk) var currenWork = sdkmath.ZeroUint() - for blkHeight := fromBlk; blkHeight < toBlk; blkHeight++ { + for blkHeight := fromBlk; blkHeight <= toBlk; blkHeight++ { idxBlock, err := btcClient.GetBlockByHeight(blkHeight) if err != nil { return nil, fmt.Errorf("failed to get block height %d from BTC client: %w", blkHeight, err) diff --git a/cmd/sid/cli/btc_headers_test.go b/cmd/sid/cli/btc_headers_test.go index 99de723..c55f79c 100644 --- a/cmd/sid/cli/btc_headers_test.go +++ b/cmd/sid/cli/btc_headers_test.go @@ -23,7 +23,7 @@ func TestBtcHeaders(t *testing.T) { // from zero height infos, err := cli.BtcHeaderInfo(btcClient, 0, uint64(initBlocksQnt)) require.NoError(t, err) - require.Equal(t, len(infos), initBlocksQnt) + require.Equal(t, len(infos), initBlocksQnt+1) // should be valid on genesis, start from zero height. genState.BtcHeaders = infos @@ -39,7 +39,7 @@ func TestBtcHeaders(t *testing.T) { infos, err = cli.BtcHeaderInfo(btcClient, uint64(fromBlockHeight), uint64(toBlockHeight)) require.NoError(t, err) - require.Equal(t, len(infos), int(toBlockHeight-fromBlockHeight)) + require.Equal(t, len(infos), int(toBlockHeight-fromBlockHeight)+1) // try to check if it is valid on genesis, should fail is not retarget block. genState.BtcHeaders = infos @@ -48,7 +48,7 @@ func TestBtcHeaders(t *testing.T) { // from retarget block infos, err = cli.BtcHeaderInfo(btcClient, uint64(blocksPerRetarget), uint64(totalBlks)) require.NoError(t, err) - require.Equal(t, len(infos), int(totalBlks-blocksPerRetarget)) + require.Equal(t, len(infos), int(totalBlks-blocksPerRetarget)+1) // check if it is valid on genesis genState.BtcHeaders = infos From fab2bf6439951c022dc46a08c5a1968e0777c8d4 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Tue, 21 May 2024 10:23:39 -0300 Subject: [PATCH 04/14] chore: address comments, output file flag, usage/description of command, parameters and func rename to BtcHeaderInfoList --- cmd/sid/cli/btc_headers.go | 23 ++++++++++++----------- cmd/sid/cli/btc_headers_test.go | 6 +++--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/cmd/sid/cli/btc_headers.go b/cmd/sid/cli/btc_headers.go index 11490fa..32a4e44 100644 --- a/cmd/sid/cli/btc_headers.go +++ b/cmd/sid/cli/btc_headers.go @@ -20,13 +20,14 @@ import ( ) const ( - outputFileFlag = "outputfile-path" + outputFileFlag = "output" + defaultOutputFileName = "output-btc-headers.json" ) var BtcHeaderCommand = cli.Command{ Name: "btc-headers", - Usage: "btc-headers 10 15 --outputfile-path ~/myoutput/path/btc-headers.json", - Description: `Get BTC headers "from" and "to" a specific block height.`, + Usage: fmt.Sprintf("btc-headers [from] [to] [--%s=path/to/btc-headers.json]", outputFileFlag), + Description: "Output a range of BTC headers into a JSON file.", Flags: []cli.Flag{ cli.StringFlag{ Name: homeFlag, @@ -36,7 +37,7 @@ var BtcHeaderCommand = cli.Command{ cli.StringFlag{ Name: outputFileFlag, Usage: "The path to the output file", - Value: filepath.Join(config.DefaultHomeDir, "output-btc-headers.json"), + Value: filepath.Join(config.DefaultHomeDir, defaultOutputFileName), }, }, Action: btcHeaders, @@ -45,7 +46,7 @@ var BtcHeaderCommand = cli.Command{ func btcHeaders(ctx *cli.Context) error { args := ctx.Args() if len(args) != 2 { - return fmt.Errorf("not enough params, please specify 'from' and 'to' block") + return fmt.Errorf("not enough params, please specify [from] and [to]") } fromStr, toStr := args[0], args[1] @@ -60,7 +61,7 @@ func btcHeaders(ctx *cli.Context) error { } if fromBlock > toBlock { - return fmt.Errorf("the from %d block should be less than to block %d", fromBlock, toBlock) + return fmt.Errorf("the [from] %d should not be greater than the [to] %d", fromBlock, toBlock) } homePath, err := filepath.Abs(ctx.String(homeFlag)) @@ -87,9 +88,9 @@ func btcHeaders(ctx *cli.Context) error { return fmt.Errorf("failed to initialize the BTC client: %w", err) } - btcHeaders, err := BtcHeaderInfo(btcClient, fromBlock, toBlock) + btcHeaders, err := BtcHeaderInfoList(btcClient, fromBlock, toBlock) if err != nil { - return fmt.Errorf("failed to get BTC Header blocks: %w", err) + return fmt.Errorf("failed to get BTC headers: %w", err) } genState := bbnbtclightclienttypes.GenesisState{ @@ -103,7 +104,7 @@ func btcHeaders(ctx *cli.Context) error { outputFilePath := ctx.String(outputFileFlag) if err := os.WriteFile(outputFilePath, bz, 0644); err != nil { - return fmt.Errorf("failed to write to output %s file %s: %w", bz, outputFilePath, err) + return fmt.Errorf("failed to write to output file %s: %w", outputFilePath, err) } logger.Info( @@ -115,8 +116,8 @@ func btcHeaders(ctx *cli.Context) error { return nil } -// BtcHeaderInfo queries the btc client for (fromBlk ~ toBlk) BTC blocks, converting to BTCHeaderInfo. -func BtcHeaderInfo(btcClient *btcclient.BTCClient, fromBlk, toBlk uint64) ([]*bbnbtclightclienttypes.BTCHeaderInfo, error) { +// BtcHeaderInfoList queries the btc client for (fromBlk ~ toBlk) BTC blocks, converting to BTCHeaderInfo. +func BtcHeaderInfoList(btcClient *btcclient.BTCClient, fromBlk, toBlk uint64) ([]*bbnbtclightclienttypes.BTCHeaderInfo, error) { btcHeaders := make([]*bbnbtclightclienttypes.BTCHeaderInfo, 0, toBlk-fromBlk) var currenWork = sdkmath.ZeroUint() diff --git a/cmd/sid/cli/btc_headers_test.go b/cmd/sid/cli/btc_headers_test.go index c55f79c..8b226f8 100644 --- a/cmd/sid/cli/btc_headers_test.go +++ b/cmd/sid/cli/btc_headers_test.go @@ -21,7 +21,7 @@ func TestBtcHeaders(t *testing.T) { btcd, btcClient := StartBtcClientAndBtcHandler(t, initBlocksQnt) // from zero height - infos, err := cli.BtcHeaderInfo(btcClient, 0, uint64(initBlocksQnt)) + infos, err := cli.BtcHeaderInfoList(btcClient, 0, uint64(initBlocksQnt)) require.NoError(t, err) require.Equal(t, len(infos), initBlocksQnt+1) @@ -37,7 +37,7 @@ func TestBtcHeaders(t *testing.T) { fromBlockHeight := blocksPerRetarget - 1 toBlockHeight := totalBlks - 2 - infos, err = cli.BtcHeaderInfo(btcClient, uint64(fromBlockHeight), uint64(toBlockHeight)) + infos, err = cli.BtcHeaderInfoList(btcClient, uint64(fromBlockHeight), uint64(toBlockHeight)) require.NoError(t, err) require.Equal(t, len(infos), int(toBlockHeight-fromBlockHeight)+1) @@ -46,7 +46,7 @@ func TestBtcHeaders(t *testing.T) { require.EqualError(t, genState.Validate(), "genesis block must be a difficulty adjustment block") // from retarget block - infos, err = cli.BtcHeaderInfo(btcClient, uint64(blocksPerRetarget), uint64(totalBlks)) + infos, err = cli.BtcHeaderInfoList(btcClient, uint64(blocksPerRetarget), uint64(totalBlks)) require.NoError(t, err) require.Equal(t, len(infos), int(totalBlks-blocksPerRetarget)+1) From c6d9de72def9ac034f2a4f1e67ee0624e97202c2 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Tue, 21 May 2024 10:25:19 -0300 Subject: [PATCH 05/14] fix: slice size of btcHeaders --- cmd/sid/cli/btc_headers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sid/cli/btc_headers.go b/cmd/sid/cli/btc_headers.go index 32a4e44..efe300f 100644 --- a/cmd/sid/cli/btc_headers.go +++ b/cmd/sid/cli/btc_headers.go @@ -118,7 +118,7 @@ func btcHeaders(ctx *cli.Context) error { // BtcHeaderInfoList queries the btc client for (fromBlk ~ toBlk) BTC blocks, converting to BTCHeaderInfo. func BtcHeaderInfoList(btcClient *btcclient.BTCClient, fromBlk, toBlk uint64) ([]*bbnbtclightclienttypes.BTCHeaderInfo, error) { - btcHeaders := make([]*bbnbtclightclienttypes.BTCHeaderInfo, 0, toBlk-fromBlk) + btcHeaders := make([]*bbnbtclightclienttypes.BTCHeaderInfo, 0, toBlk-fromBlk+1) var currenWork = sdkmath.ZeroUint() for blkHeight := fromBlk; blkHeight <= toBlk; blkHeight++ { From a1d6a1cbbeb649137b01585feebc96441b8026c4 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Tue, 21 May 2024 10:58:11 -0300 Subject: [PATCH 06/14] chore: moved e2e test of btcheaders to itest dir and create new fuzz test for BtcHeaderInfoList --- cmd/sid/cli/btc_headers.go | 3 +- cmd/sid/cli/btc_headers_test.go | 84 ++++++++++----------------------- itest/e2e_test.go | 47 +++++++++++++++++- itest/test_manager.go | 19 ++++++++ 4 files changed, 93 insertions(+), 60 deletions(-) diff --git a/cmd/sid/cli/btc_headers.go b/cmd/sid/cli/btc_headers.go index efe300f..c6e497a 100644 --- a/cmd/sid/cli/btc_headers.go +++ b/cmd/sid/cli/btc_headers.go @@ -10,6 +10,7 @@ import ( babylontypes "github.com/babylonchain/babylon/types" bbnbtclightclienttypes "github.com/babylonchain/babylon/x/btclightclient/types" "github.com/babylonchain/staking-indexer/btcclient" + "github.com/babylonchain/staking-indexer/btcscanner" "github.com/babylonchain/staking-indexer/config" "github.com/babylonchain/staking-indexer/log" "github.com/babylonchain/staking-indexer/utils" @@ -117,7 +118,7 @@ func btcHeaders(ctx *cli.Context) error { } // BtcHeaderInfoList queries the btc client for (fromBlk ~ toBlk) BTC blocks, converting to BTCHeaderInfo. -func BtcHeaderInfoList(btcClient *btcclient.BTCClient, fromBlk, toBlk uint64) ([]*bbnbtclightclienttypes.BTCHeaderInfo, error) { +func BtcHeaderInfoList(btcClient btcscanner.Client, fromBlk, toBlk uint64) ([]*bbnbtclightclienttypes.BTCHeaderInfo, error) { btcHeaders := make([]*bbnbtclightclienttypes.BTCHeaderInfo, 0, toBlk-fromBlk+1) var currenWork = sdkmath.ZeroUint() diff --git a/cmd/sid/cli/btc_headers_test.go b/cmd/sid/cli/btc_headers_test.go index 8b226f8..6f1fb04 100644 --- a/cmd/sid/cli/btc_headers_test.go +++ b/cmd/sid/cli/btc_headers_test.go @@ -4,71 +4,39 @@ import ( "math/rand" "testing" - bbnbtclightclienttypes "github.com/babylonchain/babylon/x/btclightclient/types" - "github.com/babylonchain/staking-indexer/btcclient" + bbndatagen "github.com/babylonchain/babylon/testutil/datagen" + "github.com/babylonchain/staking-indexer/cmd/sid/cli" - e2etest "github.com/babylonchain/staking-indexer/itest" + "github.com/babylonchain/staking-indexer/testutils/datagen" + "github.com/babylonchain/staking-indexer/testutils/mocks" + + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "go.uber.org/zap" ) -func TestBtcHeaders(t *testing.T) { - r := rand.New(rand.NewSource(10)) - blocksPerRetarget := 2016 - genState := bbnbtclightclienttypes.DefaultGenesis() - - initBlocksQnt := r.Intn(15) + blocksPerRetarget - btcd, btcClient := StartBtcClientAndBtcHandler(t, initBlocksQnt) - - // from zero height - infos, err := cli.BtcHeaderInfoList(btcClient, 0, uint64(initBlocksQnt)) - require.NoError(t, err) - require.Equal(t, len(infos), initBlocksQnt+1) - - // should be valid on genesis, start from zero height. - genState.BtcHeaders = infos - require.NoError(t, genState.Validate()) - - generatedBlocksQnt := r.Intn(15) + 2 - btcd.GenerateBlocks(generatedBlocksQnt) - totalBlks := initBlocksQnt + generatedBlocksQnt +func FuzzBtcHeaders(f *testing.F) { + bbndatagen.AddRandomSeedsToFuzzer(f, 10) - // check from height with interval - fromBlockHeight := blocksPerRetarget - 1 - toBlockHeight := totalBlks - 2 - - infos, err = cli.BtcHeaderInfoList(btcClient, uint64(fromBlockHeight), uint64(toBlockHeight)) - require.NoError(t, err) - require.Equal(t, len(infos), int(toBlockHeight-fromBlockHeight)+1) - - // try to check if it is valid on genesis, should fail is not retarget block. - genState.BtcHeaders = infos - require.EqualError(t, genState.Validate(), "genesis block must be a difficulty adjustment block") - - // from retarget block - infos, err = cli.BtcHeaderInfoList(btcClient, uint64(blocksPerRetarget), uint64(totalBlks)) - require.NoError(t, err) - require.Equal(t, len(infos), int(totalBlks-blocksPerRetarget)+1) - - // check if it is valid on genesis - genState.BtcHeaders = infos - require.NoError(t, genState.Validate()) -} + f.Fuzz(func(t *testing.T, seed int64) { + r := rand.New(rand.NewSource(seed)) + // Generate a random number of blocks + numBlocks := bbndatagen.RandomInt(r, 50) + 30 -func StartBtcClientAndBtcHandler(t *testing.T, generateNBlocks int) (*e2etest.BitcoindTestHandler, *btcclient.BTCClient) { - btcd := e2etest.NewBitcoindHandler(t) - btcd.Start() - _ = btcd.CreateWallet(e2etest.WalletName, e2etest.Passphrase) + chainIndexedBlocks := datagen.GetRandomIndexedBlocks(r, numBlocks) + startHeight := uint64(chainIndexedBlocks[0].Height) + endHeight := uint64(chainIndexedBlocks[len(chainIndexedBlocks)-1].Height) - resp := btcd.GenerateBlocks(generateNBlocks) - require.Equal(t, len(resp.Blocks), generateNBlocks) + ctl := gomock.NewController(t) + mockBtcClient := mocks.NewMockClient(ctl) - cfg := e2etest.DefaultStakingIndexerConfig(t.TempDir()) - btcClient, err := btcclient.NewBTCClient( - cfg.BTCConfig, - zap.NewNop(), - ) - require.NoError(t, err) + for i := 0; i < int(numBlocks); i++ { + idxBlock := chainIndexedBlocks[i] + mockBtcClient.EXPECT().GetBlockByHeight(gomock.Eq(uint64(idxBlock.Height))). + Return(idxBlock, nil).AnyTimes() + } - return btcd, btcClient + infos, err := cli.BtcHeaderInfoList(mockBtcClient, startHeight, endHeight) + require.NoError(t, err) + require.EqualValues(t, len(infos), numBlocks) + }) } diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 8672dae..210797c 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -12,6 +12,7 @@ import ( "github.com/babylonchain/babylon/btcstaking" bbndatagen "github.com/babylonchain/babylon/testutil/datagen" + bbnbtclightclienttypes "github.com/babylonchain/babylon/x/btclightclient/types" queuecli "github.com/babylonchain/staking-queue-client/client" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" @@ -21,6 +22,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/stretchr/testify/require" + "github.com/babylonchain/staking-indexer/cmd/sid/cli" "github.com/babylonchain/staking-indexer/config" "github.com/babylonchain/staking-indexer/testutils" "github.com/babylonchain/staking-indexer/testutils/datagen" @@ -408,6 +410,49 @@ func TestStakingUnbondingLifeCycle(t *testing.T) { tm.CheckNextWithdrawEvent(t, stakingTx.TxHash()) } +func TestBtcHeaders(t *testing.T) { + r := rand.New(rand.NewSource(10)) + blocksPerRetarget := 2016 + genState := bbnbtclightclienttypes.DefaultGenesis() + + initBlocksQnt := r.Intn(15) + blocksPerRetarget + btcd, btcClient := StartBtcClientAndBtcHandler(t, initBlocksQnt) + + // from zero height + infos, err := cli.BtcHeaderInfoList(btcClient, 0, uint64(initBlocksQnt)) + require.NoError(t, err) + require.Equal(t, len(infos), initBlocksQnt+1) + + // should be valid on genesis, start from zero height. + genState.BtcHeaders = infos + require.NoError(t, genState.Validate()) + + generatedBlocksQnt := r.Intn(15) + 2 + btcd.GenerateBlocks(generatedBlocksQnt) + totalBlks := initBlocksQnt + generatedBlocksQnt + + // check from height with interval + fromBlockHeight := blocksPerRetarget - 1 + toBlockHeight := totalBlks - 2 + + infos, err = cli.BtcHeaderInfoList(btcClient, uint64(fromBlockHeight), uint64(toBlockHeight)) + require.NoError(t, err) + require.Equal(t, len(infos), int(toBlockHeight-fromBlockHeight)+1) + + // try to check if it is valid on genesis, should fail is not retarget block. + genState.BtcHeaders = infos + require.EqualError(t, genState.Validate(), "genesis block must be a difficulty adjustment block") + + // from retarget block + infos, err = cli.BtcHeaderInfoList(btcClient, uint64(blocksPerRetarget), uint64(totalBlks)) + require.NoError(t, err) + require.Equal(t, len(infos), int(totalBlks-blocksPerRetarget)+1) + + // check if it is valid on genesis + genState.BtcHeaders = infos + require.NoError(t, genState.Validate()) +} + func buildUnbondingTx( t *testing.T, params *types.GlobalParams, @@ -476,7 +521,7 @@ func buildWithdrawTx( lockTime uint16, lockedAmount btcutil.Amount, ) *wire.MsgTx { - + destAddress, err := btcutil.NewAddressPubKey(stakerPrivKey.PubKey().SerializeCompressed(), regtestParams) require.NoError(t, err) diff --git a/itest/test_manager.go b/itest/test_manager.go index 1133505..50a573f 100644 --- a/itest/test_manager.go +++ b/itest/test_manager.go @@ -21,6 +21,7 @@ import ( "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/signal" "github.com/stretchr/testify/require" + "go.uber.org/zap" "github.com/babylonchain/staking-indexer/btcclient" "github.com/babylonchain/staking-indexer/btcscanner" @@ -79,6 +80,24 @@ func StartManagerWithNBlocks(t *testing.T, n int) *TestManager { return StartWithBitcoinHandler(t, h, minerAddressDecoded, dirPath, 1) } +func StartBtcClientAndBtcHandler(t *testing.T, generateNBlocks int) (*BitcoindTestHandler, *btcclient.BTCClient) { + btcd := NewBitcoindHandler(t) + btcd.Start() + _ = btcd.CreateWallet(WalletName, Passphrase) + + resp := btcd.GenerateBlocks(generateNBlocks) + require.Equal(t, len(resp.Blocks), generateNBlocks) + + cfg := DefaultStakingIndexerConfig(t.TempDir()) + btcClient, err := btcclient.NewBTCClient( + cfg.BTCConfig, + zap.NewNop(), + ) + require.NoError(t, err) + + return btcd, btcClient +} + func StartWithBitcoinHandler(t *testing.T, h *BitcoindTestHandler, minerAddress btcutil.Address, dirPath string, startHeight uint64) *TestManager { cfg := DefaultStakingIndexerConfig(dirPath) logger, err := log.NewRootLoggerWithFile(config.LogFile(dirPath), "debug") From c615845ebe2e38f65c9ab897a7dfba8239ecc398 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Wed, 22 May 2024 08:30:44 -0300 Subject: [PATCH 07/14] chore: rename defaultOutputFileName --- cmd/sid/cli/btc_headers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/sid/cli/btc_headers.go b/cmd/sid/cli/btc_headers.go index c6e497a..86c42eb 100644 --- a/cmd/sid/cli/btc_headers.go +++ b/cmd/sid/cli/btc_headers.go @@ -22,12 +22,12 @@ import ( const ( outputFileFlag = "output" - defaultOutputFileName = "output-btc-headers.json" + defaultOutputFileName = "btc-headers.json" ) var BtcHeaderCommand = cli.Command{ Name: "btc-headers", - Usage: fmt.Sprintf("btc-headers [from] [to] [--%s=path/to/btc-headers.json]", outputFileFlag), + Usage: fmt.Sprintf("btc-head2ers [from] [to] [--%s=path/to/btc-headers.json]", outputFileFlag), Description: "Output a range of BTC headers into a JSON file.", Flags: []cli.Flag{ cli.StringFlag{ From ede037da876219eafb202240955e9deb2d68b9ea Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Wed, 22 May 2024 08:32:21 -0300 Subject: [PATCH 08/14] fix: maxStakingTime to be minStakingTime + 1 --- testutils/datagen/params.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/datagen/params.go b/testutils/datagen/params.go index e97a5cf..4ba8d36 100644 --- a/testutils/datagen/params.go +++ b/testutils/datagen/params.go @@ -49,7 +49,7 @@ func GenerateGlobalParamsVersions(r *rand.Rand, t *testing.T) *types.ParamsVersi // Max Staking Amount should be more than the minimum staking amount maxStakingAmount := btcutil.Amount(r.Int63n(100000)) + minStakingAmount minStakingTime := uint16(r.Intn(1000)) + 1 - maxStakingTime := uint16(r.Intn(10000)) + minStakingTime + maxStakingTime := uint16(r.Intn(10000)) + minStakingTime + 1 // These parameters should be monotonically increasing // The staking cap should be more than the maximum staking amount From 455d6f42c3627bef09f7a086581802d74fc07511 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Wed, 22 May 2024 08:33:10 -0300 Subject: [PATCH 09/14] chore: remove 2 on usage for btc-headers --- cmd/sid/cli/btc_headers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sid/cli/btc_headers.go b/cmd/sid/cli/btc_headers.go index 86c42eb..60b166e 100644 --- a/cmd/sid/cli/btc_headers.go +++ b/cmd/sid/cli/btc_headers.go @@ -27,7 +27,7 @@ const ( var BtcHeaderCommand = cli.Command{ Name: "btc-headers", - Usage: fmt.Sprintf("btc-head2ers [from] [to] [--%s=path/to/btc-headers.json]", outputFileFlag), + Usage: fmt.Sprintf("btc-headers [from] [to] [--%s=path/to/btc-headers.json]", outputFileFlag), Description: "Output a range of BTC headers into a JSON file.", Flags: []cli.Flag{ cli.StringFlag{ From ba3450f8f00cf8f2439cf6d82e54ba0a5c20be23 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Wed, 22 May 2024 08:39:07 -0300 Subject: [PATCH 10/14] chore: add check for idx blocks be the same as BtcHeaderInfo --- cmd/sid/cli/btc_headers_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmd/sid/cli/btc_headers_test.go b/cmd/sid/cli/btc_headers_test.go index 6f1fb04..76fd271 100644 --- a/cmd/sid/cli/btc_headers_test.go +++ b/cmd/sid/cli/btc_headers_test.go @@ -5,6 +5,7 @@ import ( "testing" bbndatagen "github.com/babylonchain/babylon/testutil/datagen" + babylontypes "github.com/babylonchain/babylon/types" "github.com/babylonchain/staking-indexer/cmd/sid/cli" "github.com/babylonchain/staking-indexer/testutils/datagen" @@ -38,5 +39,13 @@ func FuzzBtcHeaders(f *testing.F) { infos, err := cli.BtcHeaderInfoList(mockBtcClient, startHeight, endHeight) require.NoError(t, err) require.EqualValues(t, len(infos), numBlocks) + + for i, info := range infos { + idxBlock := chainIndexedBlocks[i] + headerBytes := babylontypes.NewBTCHeaderBytesFromBlockHeader(idxBlock.Header) + require.Equal(t, info.Header, &headerBytes) + require.EqualValues(t, info.Height, idxBlock.Height) + require.EqualValues(t, info.Hash, headerBytes.Hash()) + } }) } From adf5ca49d3e4104eb978f0e4922701081e55eecf Mon Sep 17 00:00:00 2001 From: Rafael Tenfen Date: Wed, 22 May 2024 11:55:48 -0300 Subject: [PATCH 11/14] fix: test btc header (#97) --- cmd/sid/cli/btc_headers.go | 3 ++- cmd/sid/cli/btc_headers_test.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/sid/cli/btc_headers.go b/cmd/sid/cli/btc_headers.go index 60b166e..d20b53b 100644 --- a/cmd/sid/cli/btc_headers.go +++ b/cmd/sid/cli/btc_headers.go @@ -27,8 +27,9 @@ const ( var BtcHeaderCommand = cli.Command{ Name: "btc-headers", - Usage: fmt.Sprintf("btc-headers [from] [to] [--%s=path/to/btc-headers.json]", outputFileFlag), + Usage: "Output a range of BTC headers into a JSON file.", Description: "Output a range of BTC headers into a JSON file.", + UsageText: fmt.Sprintf("btc-headers [from] [to] [--%s=path/to/btc-headers.json]", outputFileFlag), Flags: []cli.Flag{ cli.StringFlag{ Name: homeFlag, diff --git a/cmd/sid/cli/btc_headers_test.go b/cmd/sid/cli/btc_headers_test.go index 76fd271..9974df7 100644 --- a/cmd/sid/cli/btc_headers_test.go +++ b/cmd/sid/cli/btc_headers_test.go @@ -23,7 +23,7 @@ func FuzzBtcHeaders(f *testing.F) { // Generate a random number of blocks numBlocks := bbndatagen.RandomInt(r, 50) + 30 - chainIndexedBlocks := datagen.GetRandomIndexedBlocks(r, numBlocks) + chainIndexedBlocks := datagen.GetRandomIndexedBlocks(r, bbndatagen.RandomInt(r, 150), numBlocks) startHeight := uint64(chainIndexedBlocks[0].Height) endHeight := uint64(chainIndexedBlocks[len(chainIndexedBlocks)-1].Height) From 51d98f1dbafda2fbbe550599c83331b549d26a6c Mon Sep 17 00:00:00 2001 From: Rafael Tenfen Date: Fri, 24 May 2024 09:39:14 -0300 Subject: [PATCH 12/14] chore: improve btc headers (#99) --- btcclient/client.go | 39 +++++++++++++++++++++++++------ btcscanner/expected_btc_client.go | 2 ++ cmd/sid/cli/btc_headers.go | 3 +-- cmd/sid/cli/btc_headers_test.go | 4 ++-- testutils/mocks/btc_client.go | 16 +++++++++++++ 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/btcclient/client.go b/btcclient/client.go index 0c54401..d95c998 100644 --- a/btcclient/client.go +++ b/btcclient/client.go @@ -56,6 +56,25 @@ func (c *BTCClient) GetTipHeight() (uint64, error) { } func (c *BTCClient) GetBlockByHeight(height uint64) (*types.IndexedBlock, error) { + blockHash, err := c.GetBlockHashByHeight(height) + if err != nil { + return nil, err + } + + callForBlock := func() (*wire.MsgBlock, error) { + return c.client.GetBlock(blockHash) + } + + block, err := clientCallWithRetry(callForBlock, c.logger, c.cfg) + if err != nil { + return nil, fmt.Errorf("failed to get block by hash %s: %w", blockHash.String(), err) + } + + btcTxs := utils.GetWrappedTxs(block) + return types.NewIndexedBlock(int32(height), &block.Header, btcTxs), nil +} + +func (c *BTCClient) GetBlockHashByHeight(height uint64) (*chainhash.Hash, error) { callForBlockHash := func() (*chainhash.Hash, error) { return c.client.GetBlockHash(int64(height)) } @@ -65,19 +84,25 @@ func (c *BTCClient) GetBlockByHeight(height uint64) (*types.IndexedBlock, error) return nil, fmt.Errorf("failed to get block by height %d: %w", height, err) } - callForBlock := func() (*wire.MsgBlock, error) { - return c.client.GetBlock(blockHash) + return blockHash, nil +} + +func (c *BTCClient) GetBlockHeaderByHeight(height uint64) (*wire.BlockHeader, error) { + blockHash, err := c.GetBlockHashByHeight(height) + if err != nil { + return nil, err } - block, err := clientCallWithRetry(callForBlock, c.logger, c.cfg) + callForBlockHeader := func() (*wire.BlockHeader, error) { + return c.client.GetBlockHeader(blockHash) + } + header, err := clientCallWithRetry(callForBlockHeader, c.logger, c.cfg) if err != nil { - return nil, fmt.Errorf("failed to get block by hash %s: %w", blockHash.String(), err) + return nil, fmt.Errorf("failed to get block header by hash %s: %w", blockHash.String(), err) } - btcTxs := utils.GetWrappedTxs(block) - - return types.NewIndexedBlock(int32(height), &block.Header, btcTxs), nil + return header, nil } func clientCallWithRetry[T any]( diff --git a/btcscanner/expected_btc_client.go b/btcscanner/expected_btc_client.go index 75d1951..9865fa8 100644 --- a/btcscanner/expected_btc_client.go +++ b/btcscanner/expected_btc_client.go @@ -2,9 +2,11 @@ package btcscanner import ( "github.com/babylonchain/staking-indexer/types" + "github.com/btcsuite/btcd/wire" ) type Client interface { GetTipHeight() (uint64, error) GetBlockByHeight(height uint64) (*types.IndexedBlock, error) + GetBlockHeaderByHeight(height uint64) (*wire.BlockHeader, error) } diff --git a/cmd/sid/cli/btc_headers.go b/cmd/sid/cli/btc_headers.go index d20b53b..51715ad 100644 --- a/cmd/sid/cli/btc_headers.go +++ b/cmd/sid/cli/btc_headers.go @@ -124,11 +124,10 @@ func BtcHeaderInfoList(btcClient btcscanner.Client, fromBlk, toBlk uint64) ([]*b var currenWork = sdkmath.ZeroUint() for blkHeight := fromBlk; blkHeight <= toBlk; blkHeight++ { - idxBlock, err := btcClient.GetBlockByHeight(blkHeight) + blkHeader, err := btcClient.GetBlockHeaderByHeight(blkHeight) if err != nil { return nil, fmt.Errorf("failed to get block height %d from BTC client: %w", blkHeight, err) } - blkHeader := idxBlock.Header headerWork := bbnbtclightclienttypes.CalcHeaderWork(blkHeader) currenWork = bbnbtclightclienttypes.CumulativeWork(headerWork, currenWork) diff --git a/cmd/sid/cli/btc_headers_test.go b/cmd/sid/cli/btc_headers_test.go index 9974df7..77222c6 100644 --- a/cmd/sid/cli/btc_headers_test.go +++ b/cmd/sid/cli/btc_headers_test.go @@ -32,8 +32,8 @@ func FuzzBtcHeaders(f *testing.F) { for i := 0; i < int(numBlocks); i++ { idxBlock := chainIndexedBlocks[i] - mockBtcClient.EXPECT().GetBlockByHeight(gomock.Eq(uint64(idxBlock.Height))). - Return(idxBlock, nil).AnyTimes() + mockBtcClient.EXPECT().GetBlockHeaderByHeight(gomock.Eq(uint64(idxBlock.Height))). + Return(idxBlock.Header, nil).AnyTimes() } infos, err := cli.BtcHeaderInfoList(mockBtcClient, startHeight, endHeight) diff --git a/testutils/mocks/btc_client.go b/testutils/mocks/btc_client.go index d9dd2ab..e402741 100644 --- a/testutils/mocks/btc_client.go +++ b/testutils/mocks/btc_client.go @@ -9,6 +9,7 @@ import ( types "github.com/babylonchain/staking-indexer/types" gomock "github.com/golang/mock/gomock" + "github.com/btcsuite/btcd/wire" ) // MockClient is a mock of Client interface. @@ -49,6 +50,21 @@ func (mr *MockClientMockRecorder) GetBlockByHeight(height interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByHeight", reflect.TypeOf((*MockClient)(nil).GetBlockByHeight), height) } +// GetBlockHeaderByHeight mocks base method. +func (m *MockClient) GetBlockHeaderByHeight(height uint64) (*wire.BlockHeader, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBlockHeaderByHeight", height) + ret0, _ := ret[0].(*wire.BlockHeader) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlockHeaderByHeight indicates an expected call of GetBlockHeaderByHeight. +func (mr *MockClientMockRecorder) GetBlockHeaderByHeight(height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHeaderByHeight", reflect.TypeOf((*MockClient)(nil).GetBlockHeaderByHeight), height) +} + // GetTipHeight mocks base method. func (m *MockClient) GetTipHeight() (uint64, error) { m.ctrl.T.Helper() From f00593f85e03718f95317c37fc6d7c8c11889795 Mon Sep 17 00:00:00 2001 From: Cirrus Gai Date: Wed, 5 Jun 2024 14:50:14 +0800 Subject: [PATCH 13/14] feat: Add time-based cap (#112) --- doc/staking.md | 1 + indexer/indexer.go | 21 +++- indexer/indexer_test.go | 162 ++----------------------------- itest/e2e_test.go | 188 +++++++----------------------------- itest/test-params.json | 17 ++++ itest/test_manager.go | 37 ++++++- params/params.go | 89 +++++++++-------- params/params_test.go | 26 +++-- testutils/datagen/params.go | 42 ++++++-- types/params.go | 13 +++ 10 files changed, 230 insertions(+), 366 deletions(-) diff --git a/doc/staking.md b/doc/staking.md index 06a9fae..46052fc 100644 --- a/doc/staking.md +++ b/doc/staking.md @@ -35,6 +35,7 @@ Bitcoin activation height. Each version contains the following: "version": , "activation_height": , "staking_cap": , + "cap_height": , "tag": "", "covenant_pks": [ "", diff --git a/indexer/indexer.go b/indexer/indexer.go index e192643..9e92d2e 100644 --- a/indexer/indexer.go +++ b/indexer/indexer.go @@ -823,7 +823,7 @@ func (si *StakingIndexer) ProcessStakingTx( } // check if the staking tvl is overflow with this staking tx - stakingOverflow, err := si.isOverflow(uint64(params.StakingCap)) + stakingOverflow, err := si.isOverflow(height, params) if err != nil { return fmt.Errorf("failed to check the overflow of staking tx: %w", err) } @@ -1090,13 +1090,28 @@ func (si *StakingIndexer) validateStakingTx(params *types.GlobalParams, stakingD return nil } -func (si *StakingIndexer) isOverflow(cap uint64) (bool, error) { +func (si *StakingIndexer) isOverflow(height uint64, params *types.GlobalParams) (bool, error) { + isTimeBased := params.IsTimeBasedCap() + + if isTimeBased && height < params.ActivationHeight { + panic(fmt.Errorf("the transaction height %d should not be lower than the param activation height: %d", + height, params.ActivationHeight)) + } + + if isTimeBased && height > params.CapHeight { + return true, nil + } + + if isTimeBased && height <= params.CapHeight { + return false, nil + } + confirmedTvl, err := si.is.GetConfirmedTvl() if err != nil { return false, fmt.Errorf("failed to get the confirmed TVL: %w", err) } - return confirmedTvl >= cap, nil + return confirmedTvl >= uint64(params.StakingCap), nil } func (si *StakingIndexer) GetConfirmedTvl() (uint64, error) { diff --git a/indexer/indexer_test.go b/indexer/indexer_test.go index ffd4710..588a2bf 100644 --- a/indexer/indexer_test.go +++ b/indexer/indexer_test.go @@ -19,7 +19,6 @@ import ( "github.com/babylonchain/staking-indexer/btcscanner" "github.com/babylonchain/staking-indexer/config" "github.com/babylonchain/staking-indexer/indexer" - "github.com/babylonchain/staking-indexer/indexerstore" "github.com/babylonchain/staking-indexer/testutils" "github.com/babylonchain/staking-indexer/testutils/datagen" "github.com/babylonchain/staking-indexer/testutils/mocks" @@ -84,7 +83,7 @@ func NewTestScenario(r *rand.Rand, t *testing.T, versionedParams *types.ParamsVe // no active staking events created, otherwise, to be an unbonding event if r.Intn(100) < stakingChance || !hasActiveStakingEvent(stakingEvents) { stakingEvent := buildStakingEvent(r, t, height, p) - if checkOverflow && tvl >= p.StakingCap { + if checkOverflow && isOverflow(t, uint64(height), tvl, p) { stakingEvent.IsOverflow = true } else { tvl += stakingEvent.StakingTxData.StakingAmount @@ -555,122 +554,6 @@ func FuzzValidateWithdrawTxFromUnbonding(f *testing.F) { }) } -func FuzzTestOverflow(f *testing.F) { - bbndatagen.AddRandomSeedsToFuzzer(f, 10) - - f.Fuzz(func(t *testing.T, seed int64) { - r := rand.New(rand.NewSource(seed)) - - homePath := filepath.Join(t.TempDir(), "indexer") - cfg := config.DefaultConfigWithHome(homePath) - - sysParamsVersions := datagen.GenerateGlobalParamsVersions(r, t) - - db, err := cfg.DatabaseConfig.GetDbBackend() - require.NoError(t, err) - chainUpdateInfoChan := make(chan *btcscanner.ChainUpdateInfo) - mockBtcScanner := NewMockedBtcScanner(t, chainUpdateInfoChan) - stakingIndexer, err := indexer.NewStakingIndexer(cfg, zap.NewNop(), NewMockedConsumer(t), db, sysParamsVersions, mockBtcScanner) - require.NoError(t, err) - defer func() { - err = db.Close() - require.NoError(t, err) - }() - - // Select the first params versions to play with - params := sysParamsVersions.ParamsVersions[0] - // Accumulate the test data - var stakingTxData []*StakingTxData - // Keep sending staking tx until the staking cap is reached for the very last tx - for { - stakingData, tvl, storedStakingTx, stakingTx := sendStakingTx( - t, r, stakingIndexer, params, stakingTxData, - uint64(params.ActivationHeight)+1, - ) - stakingTxData = append(stakingTxData, &StakingTxData{ - StakingTx: stakingTx, - StakingData: stakingData, - }) - // ~10% chance to trigger unbonding tx on existing staking tx to reduce the tvl - if r.Intn(10) == 0 { - sendUnbondingTx(t, stakingIndexer, params, stakingTxData, r) - } - - // Let's break if the staking tx is overflow - if storedStakingTx.IsOverflow { - require.True(t, tvl > uint64(params.StakingCap)) - break - } - require.True(t, tvl <= uint64(params.StakingCap)) - } - - // Unbond some of the tx so that new staking tx can be processed - for { - sendUnbondingTx(t, stakingIndexer, params, stakingTxData, r) - // Let's break if the tvl is below max staking value - tvl, err := stakingIndexer.GetConfirmedTvl() - require.NoError(t, err) - if tvl < uint64(params.StakingCap) { - break - } - } - - // Send staking tx again so that it will be accepted until overflow again - for { - stakingData, tvl, storedStakingTx, stakingTx := sendStakingTx( - t, r, stakingIndexer, params, stakingTxData, - uint64(params.ActivationHeight)+1, - ) - stakingTxData = append(stakingTxData, &StakingTxData{ - StakingTx: stakingTx, - StakingData: stakingData, - }) - if storedStakingTx.IsOverflow { - require.True(t, tvl > uint64(params.StakingCap)) - break - } - require.True(t, tvl <= uint64(params.StakingCap)) - } - - // Now, let's test the overflow with the second params - secondParam := sysParamsVersions.ParamsVersions[1] - - // Let's send more staking txs until the staking cap is exceeded again - for { - stakingData := datagen.GenerateTestStakingData(t, r, secondParam) - _, stakingTx := datagen.GenerateStakingTxFromTestData(t, r, secondParam, stakingData) - // For a valid tx, its btc height is always larger than the activation height - mockedHeight := uint64(secondParam.ActivationHeight) + 1 - tvl, err := stakingIndexer.GetConfirmedTvl() - require.NoError(t, err) - err = stakingIndexer.ProcessStakingTx( - stakingTx.MsgTx(), - getParsedStakingData(stakingData, stakingTx.MsgTx(), secondParam), - mockedHeight, time.Now(), secondParam) - require.NoError(t, err) - storedStakingTx, err := stakingIndexer.GetStakingTxByHash(stakingTx.Hash()) - require.NoError(t, err) - require.NotNil(t, storedStakingTx) - - stakingTxData = append(stakingTxData, &StakingTxData{ - StakingTx: stakingTx, - StakingData: stakingData, - }) - // ~20% chance to trigger unbonding tx on existing staking tx to reduce the tvl - if r.Intn(5) == 0 { - sendUnbondingTx(t, stakingIndexer, secondParam, stakingTxData, r) - } - - // Let's break if the staking tx is overflow - if storedStakingTx.IsOverflow { - require.True(t, tvl > uint64(secondParam.StakingCap)) - break - } - require.True(t, tvl <= uint64(secondParam.StakingCap)) - } - }) -} - func getParsedStakingData(data *datagen.TestStakingData, tx *wire.MsgTx, params *types.GlobalParams) *btcstaking.ParsedV0StakingTx { return &btcstaking.ParsedV0StakingTx{ StakingOutput: tx.TxOut[0], @@ -709,43 +592,12 @@ func NewMockedBtcScanner(t *testing.T, chainUpdateInfoChan chan *btcscanner.Chai return mockBtcScanner } -// This helper method will randomly select a staking tx from stakingTxData and unbond it -func sendUnbondingTx( - t *testing.T, stakingIndexer *indexer.StakingIndexer, - params *types.GlobalParams, stakingTxData []*StakingTxData, r *rand.Rand, -) { - // select a random staking tx from stakingTxData and unbond it if it is not already unbonded - data := stakingTxData[r.Intn(len(stakingTxData))] - if data.Unbonded { - return +func isOverflow(t *testing.T, height uint64, tvl btcutil.Amount, params *types.GlobalParams) bool { + if params.IsTimeBasedCap() { + require.GreaterOrEqual(t, height, params.ActivationHeight) + + return height > params.CapHeight } - // unbond the staking tx - unbondingTx := datagen.GenerateUnbondingTxFromStaking(t, params, data.StakingData, data.StakingTx.Hash(), 0) - mockedHeight := uint64(params.ActivationHeight) + 1 - err := stakingIndexer.ProcessUnbondingTx( - unbondingTx.MsgTx(), - data.StakingTx.Hash(), - mockedHeight, time.Now(), params) - require.NoError(t, err) - data.Unbonded = true -} -func sendStakingTx( - t *testing.T, r *rand.Rand, stakingIndexer *indexer.StakingIndexer, - params *types.GlobalParams, stakingTxData []*StakingTxData, height uint64, -) (*datagen.TestStakingData, uint64, *indexerstore.StoredStakingTransaction, *btcutil.Tx) { - stakingData := datagen.GenerateTestStakingData(t, r, params) - _, stakingTx := datagen.GenerateStakingTxFromTestData(t, r, params, stakingData) - tvl, err := stakingIndexer.GetConfirmedTvl() - require.NoError(t, err) - err = stakingIndexer.ProcessStakingTx( - stakingTx.MsgTx(), - getParsedStakingData(stakingData, stakingTx.MsgTx(), params), - height, time.Now(), params) - require.NoError(t, err) - storedStakingTx, err := stakingIndexer.GetStakingTxByHash(stakingTx.Hash()) - require.NoError(t, err) - require.NotNil(t, storedStakingTx) - - return stakingData, tvl, storedStakingTx, stakingTx + return tvl >= params.StakingCap } diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 43fdb83..0d40c31 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -15,10 +15,7 @@ import ( bbnbtclightclienttypes "github.com/babylonchain/babylon/x/btclightclient/types" queuecli "github.com/babylonchain/staking-queue-client/client" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/stretchr/testify/require" @@ -26,7 +23,6 @@ import ( "github.com/babylonchain/staking-indexer/config" "github.com/babylonchain/staking-indexer/testutils" "github.com/babylonchain/staking-indexer/testutils/datagen" - "github.com/babylonchain/staking-indexer/types" ) func TestBTCScanner(t *testing.T) { @@ -97,33 +93,14 @@ func TestStakingLifeCycle(t *testing.T) { // TODO: test with multiple system parameters sysParams := tm.VersionedParams.ParamsVersions[0] k := uint64(sysParams.ConfirmationDepth) - testStakingData := datagen.GenerateTestStakingData(t, r, sysParams) - stakingInfo, err := btcstaking.BuildV0IdentifiableStakingOutputs( - sysParams.Tag, - tm.WalletPrivKey.PubKey(), - testStakingData.FinalityProviderKey, - sysParams.CovenantPks, - sysParams.CovenantQuorum, - testStakingData.StakingTime, - testStakingData.StakingAmount, - regtestParams, - ) - require.NoError(t, err) - // send the staking tx and mine blocks - require.NoError(t, err) - stakingTx, err := testutils.CreateTxFromOutputsAndSign( - tm.WalletClient, - []*wire.TxOut{stakingInfo.OpReturnOutput, stakingInfo.StakingOutput}, - 1000, - tm.MinerAddr, - ) - require.NoError(t, err) + // build, send the staking tx and mine blocks + stakingTx, testStakingData, stakingInfo := tm.BuildStakingTx(t, r, sysParams) stakingTxHash := stakingTx.TxHash() tm.SendTxWithNConfirmations(t, stakingTx, int(k)) // check that the staking tx is already stored - tm.WaitForStakingTxStored(t, stakingTxHash) + _ = tm.WaitForStakingTxStored(t, stakingTxHash) // check the staking event is received by the queue tm.CheckNextStakingEvent(t, stakingTxHash) @@ -172,29 +149,9 @@ func TestUnconfirmedTVL(t *testing.T) { k := sysParams.ConfirmationDepth // build staking tx - testStakingData := datagen.GenerateTestStakingData(t, r, sysParams) - stakingInfo, err := btcstaking.BuildV0IdentifiableStakingOutputs( - sysParams.Tag, - tm.WalletPrivKey.PubKey(), - testStakingData.FinalityProviderKey, - sysParams.CovenantPks, - sysParams.CovenantQuorum, - testStakingData.StakingTime, - testStakingData.StakingAmount, - regtestParams, - ) - require.NoError(t, err) - + stakingTx, testStakingData, stakingInfo := tm.BuildStakingTx(t, r, sysParams) // send the staking tx and mine 1 block to trigger // unconfirmed calculation - require.NoError(t, err) - stakingTx, err := testutils.CreateTxFromOutputsAndSign( - tm.WalletClient, - []*wire.TxOut{stakingInfo.OpReturnOutput, stakingInfo.StakingOutput}, - 1000, - tm.MinerAddr, - ) - require.NoError(t, err) tm.SendTxWithNConfirmations(t, stakingTx, 1) tm.CheckNextUnconfirmedEvent(t, 0, uint64(stakingInfo.StakingOutput.Value)) @@ -244,7 +201,6 @@ func TestIndexerRestart(t *testing.T) { // generate valid staking tx data r := rand.New(rand.NewSource(time.Now().UnixNano())) - // TODO: test with multiple system parameters sysParams := tm.VersionedParams.ParamsVersions[0] k := sysParams.ConfirmationDepth testStakingData := datagen.GenerateTestStakingData(t, r, sysParams) @@ -273,7 +229,7 @@ func TestIndexerRestart(t *testing.T) { tm.SendTxWithNConfirmations(t, stakingTx, int(k)) // check that the staking tx is already stored - tm.WaitForStakingTxStored(t, stakingTxHash) + _ = tm.WaitForStakingTxStored(t, stakingTxHash) // check the staking event is received by the queue tm.CheckNextStakingEvent(t, stakingTxHash) @@ -337,7 +293,7 @@ func TestStakingUnbondingLifeCycle(t *testing.T) { tm.SendTxWithNConfirmations(t, stakingTx, int(k)) // check that the staking tx is already stored - tm.WaitForStakingTxStored(t, stakingTxHash) + _ = tm.WaitForStakingTxStored(t, stakingTxHash) // check the staking event is received by the queue tm.CheckNextStakingEvent(t, stakingTxHash) @@ -408,6 +364,36 @@ func TestStakingUnbondingLifeCycle(t *testing.T) { tm.CheckNextWithdrawEvent(t, stakingTx.TxHash()) } +// TestTimeBasedCap tests the case where the time-based cap is applied +func TestTimeBasedCap(t *testing.T) { + // start from the height at which the time-based cap is effective + n := 110 + tm := StartManagerWithNBlocks(t, n, 100) + defer tm.Stop() + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + sysParams := tm.VersionedParams.ParamsVersions[1] + k := uint64(sysParams.ConfirmationDepth) + + // build and send staking tx which should not overflow + stakingTx, _, _ := tm.BuildStakingTx(t, r, sysParams) + tm.SendTxWithNConfirmations(t, stakingTx, int(k)) + storedTx := tm.WaitForStakingTxStored(t, stakingTx.TxHash()) + require.False(t, storedTx.IsOverflow) + + // generate blocks so that the height is out of the cap height + tm.BitcoindHandler.GenerateBlocks(20) + currentHeight, err := tm.BitcoindHandler.GetBlockCount() + require.NoError(t, err) + require.Greater(t, uint64(currentHeight), sysParams.CapHeight) + + // send another staking tx which should be overflow + stakingTx2, _, _ := tm.BuildStakingTx(t, r, sysParams) + tm.SendTxWithNConfirmations(t, stakingTx2, int(k)) + storedTx2 := tm.WaitForStakingTxStored(t, stakingTx2.TxHash()) + require.True(t, storedTx2.IsOverflow) +} + func TestBtcHeaders(t *testing.T) { r := rand.New(rand.NewSource(10)) blocksPerRetarget := 2016 @@ -451,108 +437,6 @@ func TestBtcHeaders(t *testing.T) { require.NoError(t, genState.Validate()) } -func buildUnbondingTx( - t *testing.T, - params *types.GlobalParams, - stakerPrivKey *btcec.PrivateKey, - fpKey *btcec.PublicKey, - stakingAmount btcutil.Amount, - stakingTxHash *chainhash.Hash, - stakingOutputIdx uint32, - unbondingSpendInfo *btcstaking.SpendInfo, - stakingTx *wire.MsgTx, - covPrivKeys []*btcec.PrivateKey, -) *wire.MsgTx { - expectedOutputValue := stakingAmount - params.UnbondingFee - unbondingInfo, err := btcstaking.BuildUnbondingInfo( - stakerPrivKey.PubKey(), - []*btcec.PublicKey{fpKey}, - params.CovenantPks, - params.CovenantQuorum, - params.UnbondingTime, - expectedOutputValue, - regtestParams, - ) - require.NoError(t, err) - - unbondingTx := wire.NewMsgTx(2) - unbondingTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(stakingTxHash, stakingOutputIdx), nil, nil)) - unbondingTx.AddTxOut(unbondingInfo.UnbondingOutput) - - // generate covenant unbonding sigs - unbondingCovSigs := make([]*schnorr.Signature, len(covPrivKeys)) - for i, privKey := range covPrivKeys { - sig, err := btcstaking.SignTxWithOneScriptSpendInputStrict( - unbondingTx, - stakingTx, - stakingOutputIdx, - unbondingSpendInfo.GetPkScriptPath(), - privKey, - ) - require.NoError(t, err) - - unbondingCovSigs[i] = sig - } - - stakerUnbondingSig, err := btcstaking.SignTxWithOneScriptSpendInputFromScript( - unbondingTx, - stakingTx.TxOut[stakingOutputIdx], - stakerPrivKey, - unbondingSpendInfo.RevealedLeaf.Script, - ) - require.NoError(t, err) - - witness, err := unbondingSpendInfo.CreateUnbondingPathWitness(unbondingCovSigs, stakerUnbondingSig) - require.NoError(t, err) - unbondingTx.TxIn[0].Witness = witness - - return unbondingTx -} - -func buildWithdrawTx( - t *testing.T, - stakerPrivKey *btcec.PrivateKey, - fundTxOutput *wire.TxOut, - fundTxHash chainhash.Hash, - fundTxOutputIndex uint32, - fundTxSpendInfo *btcstaking.SpendInfo, - lockTime uint16, - lockedAmount btcutil.Amount, -) *wire.MsgTx { - - destAddress, err := btcutil.NewAddressPubKey(stakerPrivKey.PubKey().SerializeCompressed(), regtestParams) - - require.NoError(t, err) - destAddressScript, err := txscript.PayToAddrScript(destAddress) - require.NoError(t, err) - - // to spend output with relative timelock transaction need to be version two or higher - withdrawTx := wire.NewMsgTx(2) - withdrawTx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&fundTxHash, fundTxOutputIndex), nil, nil)) - withdrawTx.AddTxOut(wire.NewTxOut(int64(lockedAmount.MulF64(0.5)), destAddressScript)) - - // we need to set sequence number before signing, as signing commits to sequence - // number - withdrawTx.TxIn[0].Sequence = uint32(lockTime) - - sig, err := btcstaking.SignTxWithOneScriptSpendInputFromTapLeaf( - withdrawTx, - fundTxOutput, - stakerPrivKey, - fundTxSpendInfo.RevealedLeaf, - ) - - require.NoError(t, err) - - witness, err := fundTxSpendInfo.CreateTimeLockPathWitness(sig) - - require.NoError(t, err) - - withdrawTx.TxIn[0].Witness = witness - - return withdrawTx -} - func getCovenantPrivKeys(t *testing.T) []*btcec.PrivateKey { // private keys of the covenant committee which correspond to the public keys in test-params.json covenantPrivKeysHex := []string{ diff --git a/itest/test-params.json b/itest/test-params.json index 94578ce..3480ddc 100644 --- a/itest/test-params.json +++ b/itest/test-params.json @@ -16,6 +16,23 @@ "max_staking_time": 100, "min_staking_time": 50, "confirmation_depth": 10 + }, + { + "version": 1, + "activation_height": 110, + "cap_height": 120, + "tag": "01020304", + "covenant_pks": [ + "03cecdb3f9b99e0d67e806a9d1abd9d8c7811602dc7653bcb657a3faff29b76047" + ], + "covenant_quorum": 1, + "unbonding_time": 20, + "unbonding_fee": 1000, + "max_staking_amount": 300000, + "min_staking_amount": 3000, + "max_staking_time": 100, + "min_staking_time": 50, + "confirmation_depth": 10 } ] } diff --git a/itest/test_manager.go b/itest/test_manager.go index ca875e4..a396c8e 100644 --- a/itest/test_manager.go +++ b/itest/test_manager.go @@ -3,12 +3,14 @@ package e2etest import ( "encoding/hex" "encoding/json" + "math/rand" "os" "path/filepath" "sync" "testing" "time" + "github.com/babylonchain/babylon/btcstaking" queuecli "github.com/babylonchain/staking-queue-client/client" "github.com/babylonchain/staking-queue-client/queuemngr" "github.com/btcsuite/btcd/btcec/v2" @@ -31,6 +33,8 @@ import ( "github.com/babylonchain/staking-indexer/log" "github.com/babylonchain/staking-indexer/params" "github.com/babylonchain/staking-indexer/server" + "github.com/babylonchain/staking-indexer/testutils" + "github.com/babylonchain/staking-indexer/testutils/datagen" "github.com/babylonchain/staking-indexer/types" ) @@ -226,6 +230,35 @@ func DefaultStakingIndexerConfig(homePath string) *config.Config { return defaultConfig } +func (tm *TestManager) BuildStakingTx( + t *testing.T, + r *rand.Rand, + params *types.GlobalParams, +) (*wire.MsgTx, *datagen.TestStakingData, *btcstaking.IdentifiableStakingInfo) { + testStakingData := datagen.GenerateTestStakingData(t, r, params) + stakingInfo, err := btcstaking.BuildV0IdentifiableStakingOutputs( + params.Tag, + tm.WalletPrivKey.PubKey(), + testStakingData.FinalityProviderKey, + params.CovenantPks, + params.CovenantQuorum, + testStakingData.StakingTime, + testStakingData.StakingAmount, + regtestParams, + ) + require.NoError(t, err) + + stakingTx, err := testutils.CreateTxFromOutputsAndSign( + tm.WalletClient, + []*wire.TxOut{stakingInfo.OpReturnOutput, stakingInfo.StakingOutput}, + 1000, + tm.MinerAddr, + ) + require.NoError(t, err) + + return stakingTx, testStakingData, stakingInfo +} + func (tm *TestManager) SendTxWithNConfirmations(t *testing.T, tx *wire.MsgTx, n int) { txHash, err := tm.WalletClient.SendRawTransaction(tx, true) require.NoError(t, err) @@ -347,7 +380,7 @@ func (tm *TestManager) CheckNextUnconfirmedEvent(t *testing.T, confirmedTvl, tot } } -func (tm *TestManager) WaitForStakingTxStored(t *testing.T, txHash chainhash.Hash) { +func (tm *TestManager) WaitForStakingTxStored(t *testing.T, txHash chainhash.Hash) *indexerstore.StoredStakingTransaction { var storedTx indexerstore.StoredStakingTransaction require.Eventually(t, func() bool { storedStakingTx, err := tm.Si.GetStakingTxByHash(&txHash) @@ -359,6 +392,8 @@ func (tm *TestManager) WaitForStakingTxStored(t *testing.T, txHash chainhash.Has }, eventuallyWaitTimeOut, eventuallyPollTime) require.Equal(t, txHash.String(), storedTx.Tx.TxHash().String()) + + return &storedTx } func (tm *TestManager) WaitForUnbondingTxStored(t *testing.T, txHash chainhash.Hash) { diff --git a/params/params.go b/params/params.go index 0ce7760..90cf0a5 100644 --- a/params/params.go +++ b/params/params.go @@ -1,7 +1,6 @@ package params import ( - "context" "encoding/hex" "encoding/json" "fmt" @@ -46,6 +45,24 @@ func parseConfirmationDepthValue(confirmationDepth uint64) (uint16, error) { return uint16(confirmationDepth), nil } +// either staking cap and cap height should be positive if cap height is positive +func parseCap(stakingCap, capHeight uint64) (btcutil.Amount, uint64, error) { + if stakingCap != 0 && capHeight != 0 { + return 0, 0, fmt.Errorf("only either of staking cap and cap height can be set") + } + + if stakingCap == 0 && capHeight == 0 { + return 0, 0, fmt.Errorf("either of staking cap and cap height must be set") + } + + if stakingCap != 0 { + parsedStakingCap, err := parseBtcValue(stakingCap) + return parsedStakingCap, 0, err + } + + return 0, capHeight, nil +} + func parseBtcValue(value uint64) (btcutil.Amount, error) { if value > math.MaxInt64 { return 0, fmt.Errorf("value %d is too large. Max: %d", value, math.MaxInt64) @@ -102,6 +119,7 @@ type VersionedGlobalParams struct { Version uint64 `json:"version"` ActivationHeight uint64 `json:"activation_height"` StakingCap uint64 `json:"staking_cap"` + CapHeight uint64 `json:"cap_height"` Tag string `json:"tag"` CovenantPks []string `json:"covenant_pks"` CovenantQuorum uint64 `json:"covenant_quorum"` @@ -122,6 +140,7 @@ type ParsedVersionedGlobalParams struct { Version uint64 ActivationHeight uint64 StakingCap btcutil.Amount + CapHeight uint64 Tag []byte CovenantPks []*btcec.PublicKey CovenantQuorum uint32 @@ -161,7 +180,7 @@ func ParseGlobalParams(p *GlobalParams) (*ParsedGlobalParams, error) { } var parsedVersions []*ParsedVersionedGlobalParams - for _, v := range p.Versions { + for i, v := range p.Versions { vCopy := v cv, err := parseVersionedGlobalParams(vCopy) @@ -173,15 +192,21 @@ func ParseGlobalParams(p *GlobalParams) (*ParsedGlobalParams, error) { if len(parsedVersions) > 0 { pv := parsedVersions[len(parsedVersions)-1] + lastStakingCap := FindLastStakingCap(p.Versions[:i]) + if cv.Version != pv.Version+1 { return nil, fmt.Errorf("invalid params with version %d. versions should be monotonically increasing by 1", cv.Version) } - if cv.StakingCap < pv.StakingCap { - return nil, fmt.Errorf("invalid params with version %d. staking cap cannot be decreased in later versions", cv.Version) + if cv.StakingCap != 0 && cv.StakingCap < btcutil.Amount(lastStakingCap) { + return nil, fmt.Errorf("invalid params with version %d. staking cap cannot be decreased in later versions, last non-zero staking cap: %d, got: %d", + cv.Version, lastStakingCap, cv.StakingCap) } if cv.ActivationHeight < pv.ActivationHeight { return nil, fmt.Errorf("invalid params with version %d. activation height cannot be overlapping between earlier and later versions", cv.Version) } + if cv.ActivationHeight <= pv.CapHeight { + return nil, fmt.Errorf("invalid params with version %d. activation height must be greater than the cap height of the previous version", cv.Version) + } } parsedVersions = append(parsedVersions, cv) @@ -192,6 +217,23 @@ func ParseGlobalParams(p *GlobalParams) (*ParsedGlobalParams, error) { }, nil } +// FindLastStakingCap finds the last staking cap that is not zero +// it returns zero if not non-zero value is found +func FindLastStakingCap(prevVersions []*VersionedGlobalParams) uint64 { + numPrevVersions := len(prevVersions) + if len(prevVersions) == 0 { + return 0 + } + + for i := numPrevVersions - 1; i >= 0; i-- { + if prevVersions[i].StakingCap > 0 { + return prevVersions[i].StakingCap + } + } + + return 0 +} + func (lp *GlobalParamsRetriever) VersionedParams() *types.ParamsVersions { return lp.paramsVersions } @@ -276,15 +318,16 @@ func parseVersionedGlobalParams(p *VersionedGlobalParams) (*ParsedVersionedGloba return nil, fmt.Errorf("invalid confirmation_depth: %w", err) } - stakingCap, err := parseBtcValue(p.StakingCap) + stakingCap, capHeight, err := parseCap(p.StakingCap, p.CapHeight) if err != nil { - return nil, fmt.Errorf("invalid staking_cap: %w", err) + return nil, fmt.Errorf("invalid cap: %w", err) } return &ParsedVersionedGlobalParams{ Version: p.Version, ActivationHeight: p.ActivationHeight, StakingCap: stakingCap, + CapHeight: capHeight, Tag: tag, CovenantPks: covenantKeys, CovenantQuorum: quroum, @@ -298,39 +341,6 @@ func parseVersionedGlobalParams(p *VersionedGlobalParams) (*ParsedVersionedGloba }, nil } -func (g *ParsedGlobalParams) getVersionedGlobalParamsByHeight(btcHeight uint64) *ParsedVersionedGlobalParams { - // Iterate the list in reverse (i.e. decreasing ActivationHeight) - // and identify the first element that has an activation height below - // the specified BTC height. - for i := len(g.Versions) - 1; i >= 0; i-- { - paramsVersion := g.Versions[i] - if paramsVersion.ActivationHeight <= btcHeight { - return paramsVersion - } - } - return nil -} - -func (g *ParsedGlobalParams) ParamsByHeight(_ context.Context, height uint64) (*types.GlobalParams, error) { - versionedParams := g.getVersionedGlobalParamsByHeight(height) - if versionedParams == nil { - return nil, fmt.Errorf("no global params for height %d", height) - } - - return &types.GlobalParams{ - CovenantPks: versionedParams.CovenantPks, - CovenantQuorum: versionedParams.CovenantQuorum, - Tag: versionedParams.Tag, - UnbondingTime: versionedParams.UnbondingTime, - UnbondingFee: versionedParams.UnbondingFee, - MaxStakingAmount: versionedParams.MaxStakingAmount, - MinStakingAmount: versionedParams.MinStakingAmount, - MaxStakingTime: versionedParams.MaxStakingTime, - MinStakingTime: versionedParams.MinStakingTime, - ConfirmationDepth: versionedParams.ConfirmationDepth, - }, nil -} - func (g *ParsedGlobalParams) ToGlobalParams() *types.ParamsVersions { versionedParams := make([]*types.GlobalParams, len(g.Versions)) for i, p := range g.Versions { @@ -338,6 +348,7 @@ func (g *ParsedGlobalParams) ToGlobalParams() *types.ParamsVersions { Version: uint16(p.Version), ActivationHeight: p.ActivationHeight, StakingCap: p.StakingCap, + CapHeight: p.CapHeight, Tag: p.Tag, CovenantPks: p.CovenantPks, CovenantQuorum: p.CovenantQuorum, diff --git a/params/params_test.go b/params/params_test.go index f423c04..79b83e0 100644 --- a/params/params_test.go +++ b/params/params_test.go @@ -1,7 +1,6 @@ package params_test import ( - "context" "encoding/hex" "math" "math/rand" @@ -34,6 +33,7 @@ func generateInitParams(t *testing.T, r *rand.Rand) *params.VersionedGlobalParam Version: 0, ActivationHeight: 0, StakingCap: uint64(r.Int63n(int64(initialCapMin)) + int64(initialCapMin)), + CapHeight: 0, Tag: tag, CovenantPks: pks, CovenantQuorum: uint64(quorum), @@ -70,9 +70,17 @@ func genValidGlobalParam( for i := 1; i < int(num); i++ { prev := versions[i-1] next := generateInitParams(t, r) - next.ActivationHeight = prev.ActivationHeight + uint64(r.Int63n(100)+100) + next.ActivationHeight = uint64(math.Max(float64(prev.ActivationHeight), float64(prev.CapHeight))) + uint64(r.Int63n(100)+100) next.Version = prev.Version + 1 - next.StakingCap = prev.StakingCap + uint64(r.Int63n(1000000000)+1) + + // 1/3 chance to have a time-based cap + if r.Intn(3) == 0 { + next.CapHeight = next.ActivationHeight + uint64(r.Int63n(100)+100) + next.StakingCap = 0 + } else { + lastStakingCap := params.FindLastStakingCap(versions[:i]) + next.StakingCap = lastStakingCap + uint64(r.Int63n(1000000000)+1) + } versions = append(versions, next) } @@ -96,6 +104,7 @@ func FuzzParseValidParams(f *testing.F) { require.Equal(t, globalParams.Versions[i].Version, p.Version) require.Equal(t, globalParams.Versions[i].ActivationHeight, p.ActivationHeight) require.Equal(t, globalParams.Versions[i].StakingCap, uint64(p.StakingCap)) + require.Equal(t, globalParams.Versions[i].CapHeight, p.CapHeight) require.Equal(t, globalParams.Versions[i].Tag, hex.EncodeToString(p.Tag)) require.Equal(t, globalParams.Versions[i].CovenantQuorum, uint64(p.CovenantQuorum)) require.Equal(t, globalParams.Versions[i].UnbondingTime, uint64(p.UnbondingTime)) @@ -121,15 +130,18 @@ func FuzzRetrievingParametersByHeight(f *testing.F) { randParameterIndex := r.Intn(numOfParams) randVersionedParams := parsedParams.Versions[randParameterIndex] - // If we are querying exactly by one of the activation height, we shuld always - // retriveve original parameters - params, err := parsedParams.ParamsByHeight(context.Background(), randVersionedParams.ActivationHeight) + // If we are querying exactly by one of the activation height, we should always + // retrieve original parameters + typeGlobalParams := parsedParams.ToGlobalParams() + params, err := typeGlobalParams.GetParamsForBTCHeight(int32(randVersionedParams.ActivationHeight)) require.NoError(t, err) require.NotNil(t, params) require.Equal(t, randVersionedParams.CovenantQuorum, params.CovenantQuorum) require.Equal(t, randVersionedParams.CovenantPks, params.CovenantPks) require.Equal(t, randVersionedParams.Tag, params.Tag) + require.Equal(t, randVersionedParams.StakingCap, params.StakingCap) + require.Equal(t, randVersionedParams.CapHeight, params.CapHeight) require.Equal(t, randVersionedParams.UnbondingTime, params.UnbondingTime) require.Equal(t, randVersionedParams.UnbondingFee, params.UnbondingFee) require.Equal(t, randVersionedParams.MaxStakingAmount, params.MaxStakingAmount) @@ -141,7 +153,7 @@ func FuzzRetrievingParametersByHeight(f *testing.F) { if randParameterIndex > 0 { // If we are querying by a height that is one before the activations height // of the randomly chosen parameter, we should retrieve previous parameters version - params, err := parsedParams.ParamsByHeight(context.Background(), randVersionedParams.ActivationHeight-1) + params, err := typeGlobalParams.GetParamsForBTCHeight(int32(randVersionedParams.ActivationHeight) - 1) require.NoError(t, err) require.NotNil(t, params) paramsBeforeRand := parsedParams.Versions[randParameterIndex-1] diff --git a/testutils/datagen/params.go b/testutils/datagen/params.go index 2a39d55..ef8e484 100644 --- a/testutils/datagen/params.go +++ b/testutils/datagen/params.go @@ -18,7 +18,6 @@ func GenerateGlobalParamsVersions(r *rand.Rand, t *testing.T) *types.ParamsVersi numVersions := uint16(r.Intn(10) + 1) // For now keep the same covenants across versions - // TODO: consider covenants updates here numCovenants := r.Intn(10) + 1 covPks := make([]*btcec.PublicKey, numCovenants) for i := 0; i < numCovenants; i++ { @@ -29,8 +28,10 @@ func GenerateGlobalParamsVersions(r *rand.Rand, t *testing.T) *types.ParamsVersi covQuorum := uint32(r.Intn(numCovenants) + 1) // Keep the same tag across versions - // TODO: consider tag updates tag := []byte{0x01, 0x02, 0x03, 0x04} + // Keep the confirmation depth across versions + // the value should be at least 2 + confirmationDepth := uint16(r.Intn(100) + 2) paramsVersions := &types.ParamsVersions{ ParamsVersions: make([]*types.GlobalParams, 0), @@ -38,8 +39,6 @@ func GenerateGlobalParamsVersions(r *rand.Rand, t *testing.T) *types.ParamsVersi lastStakingCap := btcutil.Amount(0) lastActivationHeight := int32(0) lastCovKeys := make([]*btcec.PublicKey, numCovenants) - // confirmation depth is at least 2 - confirmationDepth := uint16(r.Intn(100) + 2) copy(lastCovKeys, covPks) for version := uint16(0); version <= numVersions; version++ { // These parameters can freely change between versions @@ -52,18 +51,26 @@ func GenerateGlobalParamsVersions(r *rand.Rand, t *testing.T) *types.ParamsVersi minStakingTime := uint16(r.Intn(1000)) + 1 maxStakingTime := uint16(r.Intn(10000)) + minStakingTime + 1 - // These parameters should be monotonically increasing - // The staking cap should be more than the maximum staking amount - stakingCap := btcutil.Amount(r.Int63n(1000000)) + - btcutil.Amount(r.Int63n(100000)) + maxStakingAmount + lastStakingCap - lastStakingCap = stakingCap activationHeight := int32(r.Intn(100)) + lastActivationHeight + 1 lastActivationHeight = activationHeight + + // 1/3 chance to have a time-based cap + var capHeight uint64 + var stakingCap btcutil.Amount + if r.Intn(3) == 0 { + capHeight = uint64(activationHeight) + uint64(r.Int63n(100)+100) + stakingCap = 0 + } else { + lastStakingCap = findLastStakingCap(paramsVersions.ParamsVersions[:int(version)]) + stakingCap = lastStakingCap + btcutil.Amount(r.Int63n(1000000000)+1) + } + rotatedKeys := rotateCovenantPks(lastCovKeys, r, t) copy(lastCovKeys, rotatedKeys) paramsVersions.ParamsVersions = append(paramsVersions.ParamsVersions, &types.GlobalParams{ Version: version, StakingCap: stakingCap, + CapHeight: capHeight, ActivationHeight: uint64(activationHeight), Tag: tag, CovenantPks: rotatedKeys, @@ -102,3 +109,20 @@ func rotateCovenantPks(oldKeys []*btcec.PublicKey, r *rand.Rand, t *testing.T) [ return newKeys } + +// findLastStakingCap finds the last staking cap that is not zero +// it returns zero if not non-zero value is found +func findLastStakingCap(prevVersions []*types.GlobalParams) btcutil.Amount { + numPrevVersions := len(prevVersions) + if len(prevVersions) == 0 { + return 0 + } + + for i := numPrevVersions - 1; i >= 0; i-- { + if prevVersions[i].StakingCap > 0 { + return prevVersions[i].StakingCap + } + } + + return 0 +} diff --git a/types/params.go b/types/params.go index ecc4a9f..b027886 100644 --- a/types/params.go +++ b/types/params.go @@ -15,6 +15,7 @@ type GlobalParams struct { Version uint16 ActivationHeight uint64 StakingCap btcutil.Amount + CapHeight uint64 Tag []byte CovenantPks []*btcec.PublicKey CovenantQuorum uint32 @@ -43,3 +44,15 @@ func (pv *ParamsVersions) GetParamsForBTCHeight(btcHeight int32) (*GlobalParams, } return nil, fmt.Errorf("no version corresponds to the provided BTC height") } + +func (gp *GlobalParams) IsTimeBasedCap() bool { + if gp.StakingCap != 0 && gp.CapHeight != 0 { + panic(fmt.Errorf("only either of staking cap and cap height can be set")) + } + + if gp.StakingCap == 0 && gp.CapHeight == 0 { + panic(fmt.Errorf("either of staking cap and cap height must be set")) + } + + return gp.CapHeight != 0 +} From a3729aa00e9903c36d2818367d8a99b1b405474d Mon Sep 17 00:00:00 2001 From: Cirrus Gai Date: Fri, 7 Jun 2024 16:42:19 +0800 Subject: [PATCH 14/14] chore: Bump parameter parser (#115) --- .circleci/config.yml | 2 +- Dockerfile | 2 +- btcscanner/block_handler.go | 7 +- btcscanner/btc_scanner.go | 29 +--- btcscanner/btc_scanner_test.go | 8 +- cmd/sid/cli/start.go | 4 +- go.mod | 7 +- go.sum | 2 + indexer/indexer.go | 62 +++---- indexer/indexer_test.go | 39 +++-- itest/e2e_test.go | 12 +- itest/test_manager.go | 8 +- params/params.go | 298 +-------------------------------- params/params_test.go | 172 ------------------- testutils/datagen/params.go | 17 +- testutils/datagen/staking.go | 12 +- testutils/mocks/btc_client.go | 2 +- testutils/mocks/btc_scanner.go | 8 +- testutils/utils.go | 4 +- types/params.go | 58 ------- 20 files changed, 116 insertions(+), 637 deletions(-) delete mode 100644 params/params_test.go delete mode 100644 types/params.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 1e46bec..391c654 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,7 +11,7 @@ jobs: resource_class: large steps: - go/install: - version: "1.21.6" + version: "1.22.3" - checkout - run: name: Print Go environment diff --git a/Dockerfile b/Dockerfile index d56c05d..ff80ad8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21.6-alpine as builder +FROM golang:1.22.3-alpine as builder # TARGETPLATFORM should be one of linux/amd64 or linux/arm64. ARG TARGETPLATFORM="linux/amd64" diff --git a/btcscanner/block_handler.go b/btcscanner/block_handler.go index cc577fe..4c21b0f 100644 --- a/btcscanner/block_handler.go +++ b/btcscanner/block_handler.go @@ -126,13 +126,8 @@ func (bs *BtcPoller) handleNewBlock(blockEpoch *notifier.BlockEpoch) error { return fmt.Errorf("failed to add the block %d to cache: %w", ib.Height, err) } - params, err := bs.paramsVersions.GetParamsForBTCHeight(blockEpoch.Height) - if err != nil { - return fmt.Errorf("failed to get parameters for height %d: %w", blockEpoch.Height, err) - } - // try to extract confirmed blocks - confirmedBlocks := bs.unconfirmedBlockCache.TrimConfirmedBlocks(int(params.ConfirmationDepth) - 1) + confirmedBlocks := bs.unconfirmedBlockCache.TrimConfirmedBlocks(int(bs.confirmationDepth) - 1) bs.commitChainUpdate(confirmedBlocks) diff --git a/btcscanner/btc_scanner.go b/btcscanner/btc_scanner.go index ea17791..5286cde 100644 --- a/btcscanner/btc_scanner.go +++ b/btcscanner/btc_scanner.go @@ -15,7 +15,7 @@ import ( var _ BtcScanner = (*BtcPoller)(nil) type BtcScanner interface { - Start(startHeight uint64) error + Start(startHeight, activationHeight uint64) error // ChainUpdateInfoChan receives the chain update info // after bootstrapping or when new block is received @@ -44,7 +44,7 @@ type BtcPoller struct { btcClient Client btcNotifier notifier.ChainNotifier - paramsVersions *types.ParamsVersions + confirmationDepth uint16 // the current tip BTC block confirmedTipBlock *types.IndexedBlock @@ -62,7 +62,7 @@ type BtcPoller struct { } func NewBTCScanner( - paramsVersions *types.ParamsVersions, + confirmationDepth uint16, logger *zap.Logger, btcClient Client, btcNotifier notifier.ChainNotifier, @@ -76,7 +76,7 @@ func NewBTCScanner( logger: logger.With(zap.String("module", "btcscanner")), btcClient: btcClient, btcNotifier: btcNotifier, - paramsVersions: paramsVersions, + confirmationDepth: confirmationDepth, chainUpdateInfoChan: make(chan *ChainUpdateInfo), unconfirmedBlockCache: unconfirmedBlockCache, isSynced: atomic.NewBool(false), @@ -86,12 +86,12 @@ func NewBTCScanner( } // Start starts the scanning process from the last confirmed height + 1 -func (bs *BtcPoller) Start(startHeight uint64) error { +func (bs *BtcPoller) Start(startHeight, activationHeight uint64) error { if bs.isStarted.Swap(true) { return fmt.Errorf("the BTC scanner is already started") } - if err := bs.waitUntilActivation(); err != nil { + if err := bs.waitUntilActivation(activationHeight); err != nil { return err } @@ -110,9 +110,7 @@ func (bs *BtcPoller) Start(startHeight uint64) error { return nil } -func (bs *BtcPoller) waitUntilActivation() error { - activationHeight := bs.paramsVersions.ParamsVersions[0].ActivationHeight - +func (bs *BtcPoller) waitUntilActivation(activationHeight uint64) error { for { tipHeight, err := bs.btcClient.GetTipHeight() if err != nil { @@ -154,11 +152,6 @@ func (bs *BtcPoller) Bootstrap(startHeight uint64) error { return fmt.Errorf("the start height %d is higher than the current tip height %d", startHeight, tipHeight) } - params, err := bs.paramsVersions.GetParamsForBTCHeight(int32(tipHeight)) - if err != nil { - return fmt.Errorf("cannot get the global parameters for height %d", tipHeight) - } - var confirmedBlocks []*types.IndexedBlock for i := startHeight; i <= tipHeight; i++ { ib, err := bs.btcClient.GetBlockByHeight(i) @@ -179,7 +172,7 @@ func (bs *BtcPoller) Bootstrap(startHeight uint64) error { return fmt.Errorf("failed to add the block %d to cache: %w", ib.Height, err) } - tempConfirmedBlocks := bs.unconfirmedBlockCache.TrimConfirmedBlocks(int(params.ConfirmationDepth) - 1) + tempConfirmedBlocks := bs.unconfirmedBlockCache.TrimConfirmedBlocks(int(bs.confirmationDepth) - 1) confirmedBlocks = append(confirmedBlocks, tempConfirmedBlocks...) } @@ -199,12 +192,8 @@ func (bs *BtcPoller) GetUnconfirmedBlocks() ([]*types.IndexedBlock, error) { if tipBlock == nil { return nil, nil } - params, err := bs.paramsVersions.GetParamsForBTCHeight(tipBlock.Height) - if err != nil { - return nil, fmt.Errorf("failed to get params for height %d: %w", tipBlock.Height, err) - } - lastBlocks := bs.unconfirmedBlockCache.GetLastBlocks(int(params.ConfirmationDepth) - 1) + lastBlocks := bs.unconfirmedBlockCache.GetLastBlocks(int(bs.confirmationDepth) - 1) return lastBlocks, nil } diff --git a/btcscanner/btc_scanner_test.go b/btcscanner/btc_scanner_test.go index 2ba5b23..cc8e73b 100644 --- a/btcscanner/btc_scanner_test.go +++ b/btcscanner/btc_scanner_test.go @@ -22,8 +22,8 @@ func FuzzPoller(f *testing.F) { f.Fuzz(func(t *testing.T, seed int64) { r := rand.New(rand.NewSource(seed)) versionedParams := datagen.GenerateGlobalParamsVersions(r, t) - k := uint64(versionedParams.ParamsVersions[0].ConfirmationDepth) - startHeight := versionedParams.ParamsVersions[0].ActivationHeight + k := uint64(versionedParams.Versions[0].ConfirmationDepth) + startHeight := versionedParams.Versions[0].ActivationHeight // Generate a random number of blocks numBlocks := bbndatagen.RandomIntOtherThan(r, 0, 50) + k // make sure we have at least k+1 entry chainIndexedBlocks := datagen.GetRandomIndexedBlocks(r, startHeight, numBlocks) @@ -38,7 +38,7 @@ func FuzzPoller(f *testing.F) { Return(chainIndexedBlocks[i], nil).AnyTimes() } - btcScanner, err := btcscanner.NewBTCScanner(versionedParams, zap.NewNop(), mockBtcClient, &mock.ChainNotifier{}) + btcScanner, err := btcscanner.NewBTCScanner(uint16(k), zap.NewNop(), mockBtcClient, &mock.ChainNotifier{}) require.NoError(t, err) var wg sync.WaitGroup @@ -54,7 +54,7 @@ func FuzzPoller(f *testing.F) { require.Equal(t, bestHeight, updateInfo.TipUnconfirmedBlock.Height) }() - err = btcScanner.Start(startHeight) + err = btcScanner.Start(startHeight, startHeight) require.NoError(t, err) defer func() { err := btcScanner.Stop() diff --git a/cmd/sid/cli/start.go b/cmd/sid/cli/start.go index eecb4a8..ab62892 100644 --- a/cmd/sid/cli/start.go +++ b/cmd/sid/cli/start.go @@ -94,7 +94,9 @@ func start(ctx *cli.Context) error { versionedParams := paramsRetriever.VersionedParams() // create BTC scanner - scanner, err := btcscanner.NewBTCScanner(versionedParams, logger, btcClient, btcNotifier) + // we don't expect the confirmation depth to change across different versions + // so we can always use the first one + scanner, err := btcscanner.NewBTCScanner(versionedParams.Versions[0].ConfirmationDepth, logger, btcClient, btcNotifier) if err != nil { return fmt.Errorf("failed to initialize the BTC scanner: %w", err) } diff --git a/go.mod b/go.mod index ad1426f..a907658 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,14 @@ module github.com/babylonchain/staking-indexer -go 1.21.6 +go 1.22.3 + +toolchain go1.22.4 require ( + cosmossdk.io/math v1.2.0 github.com/avast/retry-go/v4 v4.5.1 github.com/babylonchain/babylon v0.8.6-0.20240426101001-7778c798e236 + github.com/babylonchain/networks/parameters v0.2.0 github.com/babylonchain/staking-queue-client v0.2.1 github.com/btcsuite/btcd v0.24.0 github.com/btcsuite/btcd/btcec/v2 v2.3.2 @@ -40,7 +44,6 @@ require ( cosmossdk.io/depinject v1.0.0-alpha.4 // indirect cosmossdk.io/errors v1.0.1 // indirect cosmossdk.io/log v1.3.1 // indirect - cosmossdk.io/math v1.2.0 // indirect cosmossdk.io/store v1.0.2 // indirect cosmossdk.io/x/circuit v0.1.0 // indirect cosmossdk.io/x/evidence v0.1.0 // indirect diff --git a/go.sum b/go.sum index 81917f8..d167759 100644 --- a/go.sum +++ b/go.sum @@ -283,6 +283,8 @@ github.com/aws/aws-sdk-go v1.44.312/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/babylonchain/babylon v0.8.6-0.20240426101001-7778c798e236 h1:Ydna4VcP56xu1+zdgygqHdSCeMduZjuznVhr4exO5do= github.com/babylonchain/babylon v0.8.6-0.20240426101001-7778c798e236/go.mod h1:lfeASLNJgcUsX7LEns3HRUv0k+MjzcB2q2AMasfz38M= +github.com/babylonchain/networks/parameters v0.2.0 h1:f3e1MwMFm33mIKji7AgQk39aO9YIfbCbbjXtQHEQ99E= +github.com/babylonchain/networks/parameters v0.2.0/go.mod h1:nejhvrL7Iwh5Vunvkg7pnomQZlHnyNzOY9lQaDp6tOA= github.com/babylonchain/staking-queue-client v0.2.1 h1:FKbxUUOoCydAsUoj9XQMIQT1S79ThX9p7vVc5yjWm8c= github.com/babylonchain/staking-queue-client v0.2.1/go.mod h1:mEgA6N3SnwFwGEOsUYr/HdjpKa13Wck08MS7VY/Icvo= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= diff --git a/indexer/indexer.go b/indexer/indexer.go index 9e92d2e..6d80599 100644 --- a/indexer/indexer.go +++ b/indexer/indexer.go @@ -9,6 +9,7 @@ import ( "time" "github.com/babylonchain/babylon/btcstaking" + "github.com/babylonchain/networks/parameters/parser" queuecli "github.com/babylonchain/staking-queue-client/client" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" @@ -30,7 +31,7 @@ type StakingIndexer struct { stopOnce sync.Once consumer consumer.EventConsumer - paramsVersions *types.ParamsVersions + paramsVersions *parser.ParsedGlobalParams cfg *config.Config logger *zap.Logger @@ -48,7 +49,7 @@ func NewStakingIndexer( logger *zap.Logger, consumer consumer.EventConsumer, db kvdb.Backend, - paramsVersions *types.ParamsVersions, + paramsVersions *parser.ParsedGlobalParams, btcScanner btcscanner.BtcScanner, ) (*StakingIndexer, error) { is, err := indexerstore.NewIndexerStore(db) @@ -81,7 +82,7 @@ func (si *StakingIndexer) Start(startHeight uint64) error { return } - if err := si.btcScanner.Start(startHeight); err != nil { + if err := si.btcScanner.Start(startHeight, si.paramsVersions.Versions[0].ActivationHeight); err != nil { startErr = err return } @@ -102,7 +103,7 @@ func (si *StakingIndexer) Start(startHeight uint64) error { // (1) does not handle irrelevant blocks (impossible to have staking tx) // (2) does not miss relevant blocks (possible to have staking tx) func (si *StakingIndexer) ValidateStartHeight(startHeight uint64) error { - baseHeight := si.paramsVersions.ParamsVersions[0].ActivationHeight + baseHeight := si.paramsVersions.Versions[0].ActivationHeight if startHeight < baseHeight { return fmt.Errorf("the start height should not be lower than the earliest activation height %d", baseHeight) } @@ -125,7 +126,7 @@ func (si *StakingIndexer) ValidateStartHeight(startHeight uint64) error { func (si *StakingIndexer) GetStartHeight() uint64 { lastProcessedHeight, err := si.is.GetLastProcessedHeight() if err != nil { - return si.paramsVersions.ParamsVersions[0].ActivationHeight + return si.paramsVersions.Versions[0].ActivationHeight } return lastProcessedHeight + 1 @@ -235,9 +236,9 @@ func (si *StakingIndexer) CalculateTvlInUnconfirmedBlocks(unconfirmedBlocks []*t tvl := btcutil.Amount(0) unconfirmedStakingTxs := make(map[chainhash.Hash]*indexerstore.StoredStakingTransaction) for _, b := range unconfirmedBlocks { - params, err := si.paramsVersions.GetParamsForBTCHeight(b.Height) + params, err := si.getVersionedParams(uint64(b.Height)) if err != nil { - return 0, fmt.Errorf("failed to get params for height %d: %w", b.Height, err) + return 0, err } for _, tx := range b.Txs { @@ -296,11 +297,9 @@ func (si *StakingIndexer) CalculateTvlInUnconfirmedBlocks(unconfirmedBlocks []*t } if spentInputIdx >= 0 { // 3. is a spending tx, check whether it is a valid unbonding tx - paramsFromStakingTxHeight, err := si.paramsVersions.GetParamsForBTCHeight( - int32(stakingTx.InclusionHeight), - ) + paramsFromStakingTxHeight, err := si.getVersionedParams(stakingTx.InclusionHeight) if err != nil { - return 0, fmt.Errorf("failed to get the params for the staking tx height %d: %w", stakingTx.InclusionHeight, err) + return 0, err } isUnbonding, err := si.IsValidUnbondingTx(msgTx, stakingTx, paramsFromStakingTxHeight) if err != nil { @@ -350,7 +349,7 @@ func (si *StakingIndexer) CalculateTvlInUnconfirmedBlocks(unconfirmedBlocks []*t // HandleConfirmedBlock iterates through the tx set of a confirmed block and // parse the staking, unbonding, and withdrawal txs if there are any. func (si *StakingIndexer) HandleConfirmedBlock(b *types.IndexedBlock) error { - params, err := si.paramsVersions.GetParamsForBTCHeight(b.Height) + params, err := si.getVersionedParams(uint64(b.Height)) if err != nil { return err } @@ -430,11 +429,9 @@ func (si *StakingIndexer) handleSpendingUnbondingTransaction( return err } - paramsFromStakingTxHeight, err := si.paramsVersions.GetParamsForBTCHeight( - int32(storedStakingTx.InclusionHeight), - ) + paramsFromStakingTxHeight, err := si.getVersionedParams(storedStakingTx.InclusionHeight) if err != nil { - return fmt.Errorf("failed to get the params for the staking tx height: %w", err) + return err } if err := si.ValidateWithdrawalTxFromUnbonding(tx, storedStakingTx, spendingInputIdx, paramsFromStakingTxHeight); err != nil { @@ -474,11 +471,9 @@ func (si *StakingIndexer) handleSpendingStakingTransaction( timestamp time.Time, ) error { stakingTxHash := stakingTx.Tx.TxHash() - paramsFromStakingTxHeight, err := si.paramsVersions.GetParamsForBTCHeight( - int32(stakingTx.InclusionHeight), - ) + paramsFromStakingTxHeight, err := si.getVersionedParams(stakingTx.InclusionHeight) if err != nil { - return fmt.Errorf("failed to get the params for the staking tx height: %w", err) + return err } // check whether it is a valid unbonding tx @@ -552,7 +547,7 @@ func (si *StakingIndexer) ValidateWithdrawalTxFromStaking( tx *wire.MsgTx, stakingTx *indexerstore.StoredStakingTransaction, spendingInputIdx int, - params *types.GlobalParams, + params *parser.ParsedVersionedGlobalParams, ) error { // re-build the time-lock path script and check whether the script from // the witness matches @@ -591,7 +586,7 @@ func (si *StakingIndexer) ValidateWithdrawalTxFromUnbonding( tx *wire.MsgTx, stakingTx *indexerstore.StoredStakingTransaction, spendingInputIdx int, - params *types.GlobalParams, + params *parser.ParsedVersionedGlobalParams, ) error { // re-build the time-lock path script and check whether the script from // the witness matches @@ -700,7 +695,7 @@ func (si *StakingIndexer) getSpentUnbondingTx(tx *wire.MsgTx) (*indexerstore.Sto // It returns error when (1) it fails to verify the unbonding tx due // to invalid parameters, and (2) the tx spends the unbonding path // but is invalid -func (si *StakingIndexer) IsValidUnbondingTx(tx *wire.MsgTx, stakingTx *indexerstore.StoredStakingTransaction, params *types.GlobalParams) (bool, error) { +func (si *StakingIndexer) IsValidUnbondingTx(tx *wire.MsgTx, stakingTx *indexerstore.StoredStakingTransaction, params *parser.ParsedVersionedGlobalParams) (bool, error) { // 1. an unbonding tx must have exactly one input and output if len(tx.TxIn) != 1 { return false, nil @@ -784,7 +779,7 @@ func (si *StakingIndexer) ProcessStakingTx( tx *wire.MsgTx, stakingData *btcstaking.ParsedV0StakingTx, height uint64, timestamp time.Time, - params *types.GlobalParams, + params *parser.ParsedVersionedGlobalParams, ) error { var ( // whether the staking tx is overflow @@ -921,7 +916,7 @@ func (si *StakingIndexer) ProcessUnbondingTx( tx *wire.MsgTx, stakingTxHash *chainhash.Hash, height uint64, timestamp time.Time, - params *types.GlobalParams, + params *parser.ParsedVersionedGlobalParams, ) error { si.logger.Info("found an unbonding tx", zap.Uint64("height", height), @@ -1003,7 +998,7 @@ func (si *StakingIndexer) processWithdrawTx(tx *wire.MsgTx, stakingTxHash *chain return nil } -func (si *StakingIndexer) tryParseStakingTx(tx *wire.MsgTx, params *types.GlobalParams) (*btcstaking.ParsedV0StakingTx, error) { +func (si *StakingIndexer) tryParseStakingTx(tx *wire.MsgTx, params *parser.ParsedVersionedGlobalParams) (*btcstaking.ParsedV0StakingTx, error) { possible := btcstaking.IsPossibleV0StakingTx(tx, params.Tag) if !possible { return nil, fmt.Errorf("not staking tx") @@ -1061,7 +1056,7 @@ func getTxHex(tx *wire.MsgTx) (string, error) { // validateStakingTx performs the validation checks for the staking tx // such as min and max staking amount and staking time -func (si *StakingIndexer) validateStakingTx(params *types.GlobalParams, stakingData *btcstaking.ParsedV0StakingTx) error { +func (si *StakingIndexer) validateStakingTx(params *parser.ParsedVersionedGlobalParams, stakingData *btcstaking.ParsedV0StakingTx) error { value := btcutil.Amount(stakingData.StakingOutput.Value) // Minimum staking amount check if value < params.MinStakingAmount { @@ -1090,8 +1085,8 @@ func (si *StakingIndexer) validateStakingTx(params *types.GlobalParams, stakingD return nil } -func (si *StakingIndexer) isOverflow(height uint64, params *types.GlobalParams) (bool, error) { - isTimeBased := params.IsTimeBasedCap() +func (si *StakingIndexer) isOverflow(height uint64, params *parser.ParsedVersionedGlobalParams) (bool, error) { + isTimeBased := params.CapHeight != 0 if isTimeBased && height < params.ActivationHeight { panic(fmt.Errorf("the transaction height %d should not be lower than the param activation height: %d", @@ -1117,3 +1112,12 @@ func (si *StakingIndexer) isOverflow(height uint64, params *types.GlobalParams) func (si *StakingIndexer) GetConfirmedTvl() (uint64, error) { return si.is.GetConfirmedTvl() } + +func (si *StakingIndexer) getVersionedParams(height uint64) (*parser.ParsedVersionedGlobalParams, error) { + params := si.paramsVersions.GetVersionedGlobalParamsByHeight(height) + if params == nil { + return nil, fmt.Errorf("the params for height %d does not exist", height) + } + + return params, nil +} diff --git a/indexer/indexer_test.go b/indexer/indexer_test.go index 588a2bf..a7b4e64 100644 --- a/indexer/indexer_test.go +++ b/indexer/indexer_test.go @@ -9,6 +9,7 @@ import ( "github.com/babylonchain/babylon/btcstaking" bbndatagen "github.com/babylonchain/babylon/testutil/datagen" + "github.com/babylonchain/networks/parameters/parser" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -46,7 +47,7 @@ type UnbondingEvent struct { } type TestScenario struct { - VersionedParams *types.ParamsVersions + VersionedParams *parser.ParsedGlobalParams StakingEvents []*StakingEvent UnbondingEvents []*UnbondingEvent Blocks []*types.IndexedBlock @@ -58,8 +59,8 @@ type TestScenario struct { // randomly distributed across blocks. A tx has a given chance (stakingChance/100) // to be a staking tx while the rest to be an unbonding tx, n is the number of // staking txs and unbonding txs -func NewTestScenario(r *rand.Rand, t *testing.T, versionedParams *types.ParamsVersions, stakingChance int, numEvents int, checkOverflow bool) *TestScenario { - startHeight := r.Int31n(1000) + 1 + int32(versionedParams.ParamsVersions[0].ActivationHeight) +func NewTestScenario(r *rand.Rand, t *testing.T, versionedParams *parser.ParsedGlobalParams, stakingChance int, numEvents int, checkOverflow bool) *TestScenario { + startHeight := r.Int31n(1000) + 1 + int32(versionedParams.Versions[0].ActivationHeight) lastEventHeight := startHeight stakingEvents := make([]*StakingEvent, 0) unbondingEvents := make([]*UnbondingEvent, 0) @@ -71,8 +72,8 @@ func NewTestScenario(r *rand.Rand, t *testing.T, versionedParams *types.ParamsVe for i := 0; i < numEvents; i++ { // randomly select a height at which the event should be happening height := lastEventHeight + r.Int31n(3) - p, err := versionedParams.GetParamsForBTCHeight(height) - require.NoError(t, err) + p := versionedParams.GetVersionedGlobalParamsByHeight(uint64(height)) + require.NotNil(t, p) txs, ok := txsPerHeight[height] if !ok { // new height @@ -83,7 +84,7 @@ func NewTestScenario(r *rand.Rand, t *testing.T, versionedParams *types.ParamsVe // no active staking events created, otherwise, to be an unbonding event if r.Intn(100) < stakingChance || !hasActiveStakingEvent(stakingEvents) { stakingEvent := buildStakingEvent(r, t, height, p) - if checkOverflow && isOverflow(t, uint64(height), tvl, p) { + if checkOverflow && isOverflow(uint64(height), tvl, p) { stakingEvent.IsOverflow = true } else { tvl += stakingEvent.StakingTxData.StakingAmount @@ -94,8 +95,8 @@ func NewTestScenario(r *rand.Rand, t *testing.T, versionedParams *types.ParamsVe prevStakingEvent := findActiveStakingEvent(stakingEvents, r) require.NotNil(t, prevStakingEvent) require.False(t, prevStakingEvent.Unbonded) - stakingParams, err := versionedParams.GetParamsForBTCHeight(prevStakingEvent.Height) - require.NoError(t, err) + stakingParams := versionedParams.GetVersionedGlobalParamsByHeight(uint64(prevStakingEvent.Height)) + require.NotNil(t, stakingParams) unbondingEvent := buildUnbondingEvent(prevStakingEvent, height, stakingParams, t) unbondingEvents = append(unbondingEvents, unbondingEvent) prevStakingEvent.Unbonded = true @@ -135,7 +136,7 @@ func NewTestScenario(r *rand.Rand, t *testing.T, versionedParams *types.ParamsVe } } -func buildUnbondingEvent(stakingEvent *StakingEvent, height int32, p *types.GlobalParams, t *testing.T) *UnbondingEvent { +func buildUnbondingEvent(stakingEvent *StakingEvent, height int32, p *parser.ParsedVersionedGlobalParams, t *testing.T) *UnbondingEvent { stakingTxHash := stakingEvent.StakingTx.Hash() unbondingTx := datagen.GenerateUnbondingTxFromStaking(t, p, stakingEvent.StakingTxData, stakingTxHash, 0) @@ -169,7 +170,7 @@ func hasActiveStakingEvent(stakingEvents []*StakingEvent) bool { return false } -func buildStakingEvent(r *rand.Rand, t *testing.T, height int32, p *types.GlobalParams) *StakingEvent { +func buildStakingEvent(r *rand.Rand, t *testing.T, height int32, p *parser.ParsedVersionedGlobalParams) *StakingEvent { stakingData := datagen.GenerateTestStakingData(t, r, p) _, stakingTx := datagen.GenerateStakingTxFromTestData(t, r, p, stakingData) @@ -298,7 +299,7 @@ func FuzzGetStartHeight(f *testing.F) { // 1. no blocks have been processed, the start height should be equal to the base height initialHeight := stakingIndexer.GetStartHeight() - require.Equal(t, sysParams.ParamsVersions[0].ActivationHeight, initialHeight) + require.Equal(t, sysParams.Versions[0].ActivationHeight, initialHeight) err = stakingIndexer.ValidateStartHeight(initialHeight) require.NoError(t, err) @@ -389,7 +390,7 @@ func FuzzVerifyUnbondingTx(f *testing.F) { }() // Select the first params versions to play with - params := sysParamsVersions.ParamsVersions[0] + params := sysParamsVersions.Versions[0] // 1. generate and add a valid staking tx to the indexer stakingData := datagen.GenerateTestStakingData(t, r, params) _, stakingTx := datagen.GenerateStakingTxFromTestData(t, r, params, stakingData) @@ -459,7 +460,7 @@ func FuzzValidateWithdrawTxFromStaking(f *testing.F) { }() // Select the first params versions to play with - params := sysParamsVersions.ParamsVersions[0] + params := sysParamsVersions.Versions[0] // 1. generate and add a valid staking tx to the indexer stakingData := datagen.GenerateTestStakingData(t, r, params) _, stakingTx := datagen.GenerateStakingTxFromTestData(t, r, params, stakingData) @@ -515,7 +516,7 @@ func FuzzValidateWithdrawTxFromUnbonding(f *testing.F) { }() // Select the first params versions to play with - params := sysParamsVersions.ParamsVersions[0] + params := sysParamsVersions.Versions[0] // 1. generate and add a valid staking tx to the indexer stakingData := datagen.GenerateTestStakingData(t, r, params) _, stakingTx := datagen.GenerateStakingTxFromTestData(t, r, params, stakingData) @@ -554,7 +555,7 @@ func FuzzValidateWithdrawTxFromUnbonding(f *testing.F) { }) } -func getParsedStakingData(data *datagen.TestStakingData, tx *wire.MsgTx, params *types.GlobalParams) *btcstaking.ParsedV0StakingTx { +func getParsedStakingData(data *datagen.TestStakingData, tx *wire.MsgTx, params *parser.ParsedVersionedGlobalParams) *btcstaking.ParsedV0StakingTx { return &btcstaking.ParsedV0StakingTx{ StakingOutput: tx.TxOut[0], StakingOutputIdx: 0, @@ -585,17 +586,15 @@ func NewMockedConsumer(t *testing.T) *mocks.MockEventConsumer { func NewMockedBtcScanner(t *testing.T, chainUpdateInfoChan chan *btcscanner.ChainUpdateInfo) *mocks.MockBtcScanner { ctl := gomock.NewController(t) mockBtcScanner := mocks.NewMockBtcScanner(ctl) - mockBtcScanner.EXPECT().Start(gomock.Any()).Return(nil).AnyTimes() + mockBtcScanner.EXPECT().Start(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() mockBtcScanner.EXPECT().ChainUpdateInfoChan().Return(chainUpdateInfoChan).AnyTimes() mockBtcScanner.EXPECT().Stop().Return(nil).AnyTimes() return mockBtcScanner } -func isOverflow(t *testing.T, height uint64, tvl btcutil.Amount, params *types.GlobalParams) bool { - if params.IsTimeBasedCap() { - require.GreaterOrEqual(t, height, params.ActivationHeight) - +func isOverflow(height uint64, tvl btcutil.Amount, params *parser.ParsedVersionedGlobalParams) bool { + if params.CapHeight != 0 { return height > params.CapHeight } diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 0d40c31..65fed23 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -34,7 +34,7 @@ func TestBTCScanner(t *testing.T) { require.NoError(t, err) require.Equal(t, n, count) - k := int(tm.VersionedParams.ParamsVersions[0].ConfirmationDepth) + k := int(tm.VersionedParams.Versions[0].ConfirmationDepth) _ = tm.BitcoindHandler.GenerateBlocks(10) @@ -91,7 +91,7 @@ func TestStakingLifeCycle(t *testing.T) { // generate valid staking tx data r := rand.New(rand.NewSource(time.Now().UnixNano())) // TODO: test with multiple system parameters - sysParams := tm.VersionedParams.ParamsVersions[0] + sysParams := tm.VersionedParams.Versions[0] k := uint64(sysParams.ConfirmationDepth) // build, send the staking tx and mine blocks @@ -145,7 +145,7 @@ func TestUnconfirmedTVL(t *testing.T) { // generate valid staking tx data r := rand.New(rand.NewSource(time.Now().UnixNano())) // TODO: test with multiple system parameters - sysParams := tm.VersionedParams.ParamsVersions[0] + sysParams := tm.VersionedParams.Versions[0] k := sysParams.ConfirmationDepth // build staking tx @@ -201,7 +201,7 @@ func TestIndexerRestart(t *testing.T) { // generate valid staking tx data r := rand.New(rand.NewSource(time.Now().UnixNano())) - sysParams := tm.VersionedParams.ParamsVersions[0] + sysParams := tm.VersionedParams.Versions[0] k := sysParams.ConfirmationDepth testStakingData := datagen.GenerateTestStakingData(t, r, sysParams) stakingInfo, err := btcstaking.BuildV0IdentifiableStakingOutputs( @@ -265,7 +265,7 @@ func TestStakingUnbondingLifeCycle(t *testing.T) { // generate valid staking tx data // TODO: test with multiple system parameters - sysParams := tm.VersionedParams.ParamsVersions[0] + sysParams := tm.VersionedParams.Versions[0] k := uint64(sysParams.ConfirmationDepth) testStakingData := getTestStakingData(t) stakingInfo, err := btcstaking.BuildV0IdentifiableStakingOutputs( @@ -372,7 +372,7 @@ func TestTimeBasedCap(t *testing.T) { defer tm.Stop() r := rand.New(rand.NewSource(time.Now().UnixNano())) - sysParams := tm.VersionedParams.ParamsVersions[1] + sysParams := tm.VersionedParams.Versions[1] k := uint64(sysParams.ConfirmationDepth) // build and send staking tx which should not overflow diff --git a/itest/test_manager.go b/itest/test_manager.go index a396c8e..aeb868b 100644 --- a/itest/test_manager.go +++ b/itest/test_manager.go @@ -11,6 +11,7 @@ import ( "time" "github.com/babylonchain/babylon/btcstaking" + "github.com/babylonchain/networks/parameters/parser" queuecli "github.com/babylonchain/staking-queue-client/client" "github.com/babylonchain/staking-queue-client/queuemngr" "github.com/btcsuite/btcd/btcec/v2" @@ -35,7 +36,6 @@ import ( "github.com/babylonchain/staking-indexer/server" "github.com/babylonchain/staking-indexer/testutils" "github.com/babylonchain/staking-indexer/testutils/datagen" - "github.com/babylonchain/staking-indexer/types" ) type TestManager struct { @@ -55,7 +55,7 @@ type TestManager struct { UnbondingEventChan <-chan queuecli.QueueMessage WithdrawEventChan <-chan queuecli.QueueMessage BtcInfoEventChan <-chan queuecli.QueueMessage - VersionedParams *types.ParamsVersions + VersionedParams *parser.ParsedGlobalParams } // bitcoin params used for testing @@ -131,7 +131,7 @@ func StartWithBitcoinHandler(t *testing.T, h *BitcoindTestHandler, minerAddress require.NoError(t, err) versionedParams := paramsRetriever.VersionedParams() require.NoError(t, err) - scanner, err := btcscanner.NewBTCScanner(versionedParams, logger, btcClient, btcNotifier) + scanner, err := btcscanner.NewBTCScanner(versionedParams.Versions[0].ConfirmationDepth, logger, btcClient, btcNotifier) require.NoError(t, err) // create event consumer @@ -233,7 +233,7 @@ func DefaultStakingIndexerConfig(homePath string) *config.Config { func (tm *TestManager) BuildStakingTx( t *testing.T, r *rand.Rand, - params *types.GlobalParams, + params *parser.ParsedVersionedGlobalParams, ) (*wire.MsgTx, *datagen.TestStakingData, *btcstaking.IdentifiableStakingInfo) { testStakingData := datagen.GenerateTestStakingData(t, r, params) stakingInfo, err := btcstaking.BuildV0IdentifiableStakingOutputs( diff --git a/params/params.go b/params/params.go index 90cf0a5..2f57045 100644 --- a/params/params.go +++ b/params/params.go @@ -1,114 +1,20 @@ package params import ( - "encoding/hex" "encoding/json" - "fmt" - "math" "os" - "github.com/babylonchain/babylon/btcstaking" + "github.com/babylonchain/networks/parameters/parser" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" - - "github.com/babylonchain/staking-indexer/types" ) -func checkPositive(value uint64) error { - if value == 0 { - return fmt.Errorf("value must be positive") - } - return nil -} - -func parseTimeLockValue(timelock uint64) (uint16, error) { - if timelock > math.MaxUint16 { - return 0, fmt.Errorf("timelock value %d is too large. Max: %d", timelock, math.MaxUint16) - } - - if err := checkPositive(timelock); err != nil { - return 0, fmt.Errorf("invalid timelock value: %w", err) - } - - return uint16(timelock), nil -} - -func parseConfirmationDepthValue(confirmationDepth uint64) (uint16, error) { - if confirmationDepth > math.MaxUint16 { - return 0, fmt.Errorf("timelock value %d is too large. Max: %d", confirmationDepth, math.MaxUint16) - } - - if err := checkPositive(confirmationDepth); err != nil { - return 0, fmt.Errorf("invalid confirmation depth value: %w", err) - } - - return uint16(confirmationDepth), nil -} - -// either staking cap and cap height should be positive if cap height is positive -func parseCap(stakingCap, capHeight uint64) (btcutil.Amount, uint64, error) { - if stakingCap != 0 && capHeight != 0 { - return 0, 0, fmt.Errorf("only either of staking cap and cap height can be set") - } - - if stakingCap == 0 && capHeight == 0 { - return 0, 0, fmt.Errorf("either of staking cap and cap height must be set") - } - - if stakingCap != 0 { - parsedStakingCap, err := parseBtcValue(stakingCap) - return parsedStakingCap, 0, err - } - - return 0, capHeight, nil -} - -func parseBtcValue(value uint64) (btcutil.Amount, error) { - if value > math.MaxInt64 { - return 0, fmt.Errorf("value %d is too large. Max: %d", value, math.MaxInt64) - } - - if err := checkPositive(value); err != nil { - return 0, fmt.Errorf("invalid btc value value: %w", err) - } - // retrun amount in satoshis - return btcutil.Amount(value), nil -} - -func parseUint32(value uint64) (uint32, error) { - if value > math.MaxUint32 { - return 0, fmt.Errorf("value %d is too large. Max: %d", value, math.MaxUint32) - } - - if err := checkPositive(value); err != nil { - return 0, fmt.Errorf("invalid value: %w", err) - } - - return uint32(value), nil -} - -// parseCovenantPubKeyFromHex parses public key string to btc public key -// the input should be 33 bytes -func parseCovenantPubKeyFromHex(pkStr string) (*btcec.PublicKey, error) { - pkBytes, err := hex.DecodeString(pkStr) - if err != nil { - return nil, err - } - - pk, err := btcec.ParsePubKey(pkBytes) - if err != nil { - return nil, err - } - - return pk, nil -} - type ParamsRetriever interface { - VersionedParams() *types.ParamsVersions + VersionedParams() *parser.ParsedGlobalParams } type GlobalParamsRetriever struct { - paramsVersions *types.ParamsVersions + paramsVersions *parser.ParsedGlobalParams } type GlobalParams struct { @@ -159,210 +65,20 @@ func NewGlobalParamsRetriever(filePath string) (*GlobalParamsRetriever, error) { return nil, err } - var globalParams GlobalParams + var globalParams parser.GlobalParams err = json.Unmarshal(data, &globalParams) if err != nil { return nil, err } - parsedGlobalParams, err := ParseGlobalParams(&globalParams) - + parsedGlobalParams, err := parser.ParseGlobalParams(&globalParams) if err != nil { return nil, err } - return &GlobalParamsRetriever{paramsVersions: parsedGlobalParams.ToGlobalParams()}, nil -} - -func ParseGlobalParams(p *GlobalParams) (*ParsedGlobalParams, error) { - if len(p.Versions) == 0 { - return nil, fmt.Errorf("global params must have at least one version") - } - var parsedVersions []*ParsedVersionedGlobalParams - - for i, v := range p.Versions { - vCopy := v - cv, err := parseVersionedGlobalParams(vCopy) - - if err != nil { - return nil, fmt.Errorf("invalid params with version %d: %w", vCopy.Version, err) - } - - // Check latest version - if len(parsedVersions) > 0 { - pv := parsedVersions[len(parsedVersions)-1] - - lastStakingCap := FindLastStakingCap(p.Versions[:i]) - - if cv.Version != pv.Version+1 { - return nil, fmt.Errorf("invalid params with version %d. versions should be monotonically increasing by 1", cv.Version) - } - if cv.StakingCap != 0 && cv.StakingCap < btcutil.Amount(lastStakingCap) { - return nil, fmt.Errorf("invalid params with version %d. staking cap cannot be decreased in later versions, last non-zero staking cap: %d, got: %d", - cv.Version, lastStakingCap, cv.StakingCap) - } - if cv.ActivationHeight < pv.ActivationHeight { - return nil, fmt.Errorf("invalid params with version %d. activation height cannot be overlapping between earlier and later versions", cv.Version) - } - if cv.ActivationHeight <= pv.CapHeight { - return nil, fmt.Errorf("invalid params with version %d. activation height must be greater than the cap height of the previous version", cv.Version) - } - } - - parsedVersions = append(parsedVersions, cv) - } - - return &ParsedGlobalParams{ - Versions: parsedVersions, - }, nil + return &GlobalParamsRetriever{paramsVersions: parsedGlobalParams}, nil } -// FindLastStakingCap finds the last staking cap that is not zero -// it returns zero if not non-zero value is found -func FindLastStakingCap(prevVersions []*VersionedGlobalParams) uint64 { - numPrevVersions := len(prevVersions) - if len(prevVersions) == 0 { - return 0 - } - - for i := numPrevVersions - 1; i >= 0; i-- { - if prevVersions[i].StakingCap > 0 { - return prevVersions[i].StakingCap - } - } - - return 0 -} - -func (lp *GlobalParamsRetriever) VersionedParams() *types.ParamsVersions { +func (lp *GlobalParamsRetriever) VersionedParams() *parser.ParsedGlobalParams { return lp.paramsVersions } - -func parseVersionedGlobalParams(p *VersionedGlobalParams) (*ParsedVersionedGlobalParams, error) { - tag, err := hex.DecodeString(p.Tag) - - if err != nil { - return nil, fmt.Errorf("invalid tag: %w", err) - } - - if len(tag) != btcstaking.MagicBytesLen { - return nil, fmt.Errorf("invalid tag length, expected %d, got %d", btcstaking.MagicBytesLen, len(p.Tag)) - } - - if len(p.CovenantPks) == 0 { - return nil, fmt.Errorf("empty covenant public keys") - } - if p.CovenantQuorum > uint64(len(p.CovenantPks)) { - return nil, fmt.Errorf("covenant quorum cannot be more than the amount of covenants") - } - - quroum, err := parseUint32(p.CovenantQuorum) - if err != nil { - return nil, fmt.Errorf("invalid covenant quorum: %w", err) - } - - var covenantKeys []*btcec.PublicKey - for _, covPk := range p.CovenantPks { - pk, err := parseCovenantPubKeyFromHex(covPk) - if err != nil { - return nil, fmt.Errorf("invalid covenant public key %s: %w", covPk, err) - } - - covenantKeys = append(covenantKeys, pk) - } - - maxStakingAmount, err := parseBtcValue(p.MaxStakingAmount) - - if err != nil { - return nil, fmt.Errorf("invalid max_staking_amount: %w", err) - } - - minStakingAmount, err := parseBtcValue(p.MinStakingAmount) - - if err != nil { - return nil, fmt.Errorf("invalid min_staking_amount: %w", err) - } - - if maxStakingAmount <= minStakingAmount { - return nil, fmt.Errorf("max-staking-amount must be larger than min-staking-amount") - } - - ubTime, err := parseTimeLockValue(p.UnbondingTime) - if err != nil { - return nil, fmt.Errorf("invalid unbonding_time: %w", err) - } - - ubFee, err := parseBtcValue(p.UnbondingFee) - if err != nil { - return nil, fmt.Errorf("invalid unbonding_fee: %w", err) - } - - maxStakingTime, err := parseTimeLockValue(p.MaxStakingTime) - if err != nil { - return nil, fmt.Errorf("invalid max_staking_time: %w", err) - } - - minStakingTime, err := parseTimeLockValue(p.MinStakingTime) - if err != nil { - return nil, fmt.Errorf("invalid min_staking_time: %w", err) - } - - // NOTE: Allow config when max-staking-time is equal to min-staking-time, as then - // we can configure a fixed staking time. - if maxStakingTime < minStakingTime { - return nil, fmt.Errorf("max-staking-time must be larger or equalt min-staking-time") - } - - confirmationDepth, err := parseConfirmationDepthValue(p.ConfirmationDepth) - if err != nil { - return nil, fmt.Errorf("invalid confirmation_depth: %w", err) - } - - stakingCap, capHeight, err := parseCap(p.StakingCap, p.CapHeight) - if err != nil { - return nil, fmt.Errorf("invalid cap: %w", err) - } - - return &ParsedVersionedGlobalParams{ - Version: p.Version, - ActivationHeight: p.ActivationHeight, - StakingCap: stakingCap, - CapHeight: capHeight, - Tag: tag, - CovenantPks: covenantKeys, - CovenantQuorum: quroum, - UnbondingTime: ubTime, - UnbondingFee: ubFee, - MaxStakingAmount: maxStakingAmount, - MinStakingAmount: minStakingAmount, - MaxStakingTime: maxStakingTime, - MinStakingTime: minStakingTime, - ConfirmationDepth: confirmationDepth, - }, nil -} - -func (g *ParsedGlobalParams) ToGlobalParams() *types.ParamsVersions { - versionedParams := make([]*types.GlobalParams, len(g.Versions)) - for i, p := range g.Versions { - globalParams := &types.GlobalParams{ - Version: uint16(p.Version), - ActivationHeight: p.ActivationHeight, - StakingCap: p.StakingCap, - CapHeight: p.CapHeight, - Tag: p.Tag, - CovenantPks: p.CovenantPks, - CovenantQuorum: p.CovenantQuorum, - UnbondingTime: p.UnbondingTime, - UnbondingFee: p.UnbondingFee, - MaxStakingAmount: p.MaxStakingAmount, - MinStakingAmount: p.MinStakingAmount, - MaxStakingTime: p.MaxStakingTime, - MinStakingTime: p.MinStakingTime, - ConfirmationDepth: p.ConfirmationDepth, - } - - versionedParams[i] = globalParams - } - - return &types.ParamsVersions{ParamsVersions: versionedParams} -} diff --git a/params/params_test.go b/params/params_test.go deleted file mode 100644 index 79b83e0..0000000 --- a/params/params_test.go +++ /dev/null @@ -1,172 +0,0 @@ -package params_test - -import ( - "encoding/hex" - "math" - "math/rand" - "testing" - - bbndatagen "github.com/babylonchain/babylon/testutil/datagen" - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcutil" - "github.com/stretchr/testify/require" - - "github.com/babylonchain/staking-indexer/params" -) - -var ( - initialCapMin, _ = btcutil.NewAmount(100) - tag = hex.EncodeToString([]byte{0x01, 0x02, 0x03, 0x04}) - quorum = 2 -) - -func generateInitParams(t *testing.T, r *rand.Rand) *params.VersionedGlobalParams { - var pks []string - - for i := 0; i < quorum+1; i++ { - privkey, err := btcec.NewPrivateKey() - require.NoError(t, err) - pks = append(pks, hex.EncodeToString(privkey.PubKey().SerializeCompressed())) - } - - gp := params.VersionedGlobalParams{ - Version: 0, - ActivationHeight: 0, - StakingCap: uint64(r.Int63n(int64(initialCapMin)) + int64(initialCapMin)), - CapHeight: 0, - Tag: tag, - CovenantPks: pks, - CovenantQuorum: uint64(quorum), - UnbondingTime: uint64(r.Int63n(100) + 100), - UnbondingFee: uint64(r.Int63n(100000) + 100000), - MaxStakingAmount: uint64(r.Int63n(100000000) + 100000000), - MinStakingAmount: uint64(r.Int63n(1000000) + 1000000), - MaxStakingTime: math.MaxUint16, - MinStakingTime: uint64(r.Int63n(10000) + 10000), - ConfirmationDepth: uint64(r.Int63n(10) + 1), - } - - return &gp -} - -func genValidGlobalParam( - t *testing.T, - r *rand.Rand, - num uint32, -) *params.GlobalParams { - require.True(t, num > 0) - - initParams := generateInitParams(t, r) - - if num == 1 { - return ¶ms.GlobalParams{ - Versions: []*params.VersionedGlobalParams{initParams}, - } - } - - var versions []*params.VersionedGlobalParams - versions = append(versions, initParams) - - for i := 1; i < int(num); i++ { - prev := versions[i-1] - next := generateInitParams(t, r) - next.ActivationHeight = uint64(math.Max(float64(prev.ActivationHeight), float64(prev.CapHeight))) + uint64(r.Int63n(100)+100) - next.Version = prev.Version + 1 - - // 1/3 chance to have a time-based cap - if r.Intn(3) == 0 { - next.CapHeight = next.ActivationHeight + uint64(r.Int63n(100)+100) - next.StakingCap = 0 - } else { - lastStakingCap := params.FindLastStakingCap(versions[:i]) - next.StakingCap = lastStakingCap + uint64(r.Int63n(1000000000)+1) - } - versions = append(versions, next) - } - - return ¶ms.GlobalParams{ - Versions: versions, - } -} - -// PROPERTY: Every valid global params should be parsed successfully -func FuzzParseValidParams(f *testing.F) { - bbndatagen.AddRandomSeedsToFuzzer(f, 10) - f.Fuzz(func(t *testing.T, seed int64) { - r := rand.New(rand.NewSource(seed)) - numVersions := uint32(r.Int63n(50) + 10) - globalParams := genValidGlobalParam(t, r, numVersions) - parsedParams, err := params.ParseGlobalParams(globalParams) - require.NoError(t, err) - require.NotNil(t, parsedParams) - require.Len(t, parsedParams.Versions, int(numVersions)) - for i, p := range parsedParams.Versions { - require.Equal(t, globalParams.Versions[i].Version, p.Version) - require.Equal(t, globalParams.Versions[i].ActivationHeight, p.ActivationHeight) - require.Equal(t, globalParams.Versions[i].StakingCap, uint64(p.StakingCap)) - require.Equal(t, globalParams.Versions[i].CapHeight, p.CapHeight) - require.Equal(t, globalParams.Versions[i].Tag, hex.EncodeToString(p.Tag)) - require.Equal(t, globalParams.Versions[i].CovenantQuorum, uint64(p.CovenantQuorum)) - require.Equal(t, globalParams.Versions[i].UnbondingTime, uint64(p.UnbondingTime)) - require.Equal(t, globalParams.Versions[i].UnbondingFee, uint64(p.UnbondingFee)) - require.Equal(t, globalParams.Versions[i].MaxStakingAmount, uint64(p.MaxStakingAmount)) - require.Equal(t, globalParams.Versions[i].MinStakingAmount, uint64(p.MinStakingAmount)) - require.Equal(t, globalParams.Versions[i].MaxStakingTime, uint64(p.MaxStakingTime)) - require.Equal(t, globalParams.Versions[i].MinStakingTime, uint64(p.MinStakingTime)) - require.Equal(t, globalParams.Versions[i].ConfirmationDepth, uint64(p.ConfirmationDepth)) - } - }) -} - -func FuzzRetrievingParametersByHeight(f *testing.F) { - bbndatagen.AddRandomSeedsToFuzzer(f, 10) - f.Fuzz(func(t *testing.T, seed int64) { - r := rand.New(rand.NewSource(seed)) - numVersions := uint32(r.Int63n(50) + 10) - globalParams := genValidGlobalParam(t, r, numVersions) - parsedParams, err := params.ParseGlobalParams(globalParams) - require.NoError(t, err) - numOfParams := len(parsedParams.Versions) - randParameterIndex := r.Intn(numOfParams) - randVersionedParams := parsedParams.Versions[randParameterIndex] - - // If we are querying exactly by one of the activation height, we should always - // retrieve original parameters - typeGlobalParams := parsedParams.ToGlobalParams() - params, err := typeGlobalParams.GetParamsForBTCHeight(int32(randVersionedParams.ActivationHeight)) - require.NoError(t, err) - require.NotNil(t, params) - - require.Equal(t, randVersionedParams.CovenantQuorum, params.CovenantQuorum) - require.Equal(t, randVersionedParams.CovenantPks, params.CovenantPks) - require.Equal(t, randVersionedParams.Tag, params.Tag) - require.Equal(t, randVersionedParams.StakingCap, params.StakingCap) - require.Equal(t, randVersionedParams.CapHeight, params.CapHeight) - require.Equal(t, randVersionedParams.UnbondingTime, params.UnbondingTime) - require.Equal(t, randVersionedParams.UnbondingFee, params.UnbondingFee) - require.Equal(t, randVersionedParams.MaxStakingAmount, params.MaxStakingAmount) - require.Equal(t, randVersionedParams.MinStakingAmount, params.MinStakingAmount) - require.Equal(t, randVersionedParams.MaxStakingTime, params.MaxStakingTime) - require.Equal(t, randVersionedParams.MinStakingTime, params.MinStakingTime) - require.Equal(t, randVersionedParams.ConfirmationDepth, params.ConfirmationDepth) - - if randParameterIndex > 0 { - // If we are querying by a height that is one before the activations height - // of the randomly chosen parameter, we should retrieve previous parameters version - params, err := typeGlobalParams.GetParamsForBTCHeight(int32(randVersionedParams.ActivationHeight) - 1) - require.NoError(t, err) - require.NotNil(t, params) - paramsBeforeRand := parsedParams.Versions[randParameterIndex-1] - require.Equal(t, paramsBeforeRand.CovenantQuorum, params.CovenantQuorum) - require.Equal(t, paramsBeforeRand.CovenantPks, params.CovenantPks) - require.Equal(t, paramsBeforeRand.Tag, params.Tag) - require.Equal(t, paramsBeforeRand.UnbondingTime, params.UnbondingTime) - require.Equal(t, paramsBeforeRand.UnbondingFee, params.UnbondingFee) - require.Equal(t, paramsBeforeRand.MaxStakingAmount, params.MaxStakingAmount) - require.Equal(t, paramsBeforeRand.MinStakingAmount, params.MinStakingAmount) - require.Equal(t, paramsBeforeRand.MaxStakingTime, params.MaxStakingTime) - require.Equal(t, paramsBeforeRand.MinStakingTime, params.MinStakingTime) - require.Equal(t, paramsBeforeRand.ConfirmationDepth, params.ConfirmationDepth) - } - }) -} diff --git a/testutils/datagen/params.go b/testutils/datagen/params.go index ef8e484..1530c00 100644 --- a/testutils/datagen/params.go +++ b/testutils/datagen/params.go @@ -4,16 +4,15 @@ import ( "math/rand" "testing" + "github.com/babylonchain/networks/parameters/parser" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/stretchr/testify/require" - - "github.com/babylonchain/staking-indexer/types" ) // GenerateGlobalParamsVersions generate test params and save it in a file // It returns the file path -func GenerateGlobalParamsVersions(r *rand.Rand, t *testing.T) *types.ParamsVersions { +func GenerateGlobalParamsVersions(r *rand.Rand, t *testing.T) *parser.ParsedGlobalParams { // Random number of versions numVersions := uint16(r.Intn(10) + 1) @@ -33,8 +32,8 @@ func GenerateGlobalParamsVersions(r *rand.Rand, t *testing.T) *types.ParamsVersi // the value should be at least 2 confirmationDepth := uint16(r.Intn(100) + 2) - paramsVersions := &types.ParamsVersions{ - ParamsVersions: make([]*types.GlobalParams, 0), + paramsVersions := &parser.ParsedGlobalParams{ + Versions: make([]*parser.ParsedVersionedGlobalParams, 0), } lastStakingCap := btcutil.Amount(0) lastActivationHeight := int32(0) @@ -61,14 +60,14 @@ func GenerateGlobalParamsVersions(r *rand.Rand, t *testing.T) *types.ParamsVersi capHeight = uint64(activationHeight) + uint64(r.Int63n(100)+100) stakingCap = 0 } else { - lastStakingCap = findLastStakingCap(paramsVersions.ParamsVersions[:int(version)]) + lastStakingCap = findLastStakingCap(paramsVersions.Versions[:int(version)]) stakingCap = lastStakingCap + btcutil.Amount(r.Int63n(1000000000)+1) } rotatedKeys := rotateCovenantPks(lastCovKeys, r, t) copy(lastCovKeys, rotatedKeys) - paramsVersions.ParamsVersions = append(paramsVersions.ParamsVersions, &types.GlobalParams{ - Version: version, + paramsVersions.Versions = append(paramsVersions.Versions, &parser.ParsedVersionedGlobalParams{ + Version: uint64(version), StakingCap: stakingCap, CapHeight: capHeight, ActivationHeight: uint64(activationHeight), @@ -112,7 +111,7 @@ func rotateCovenantPks(oldKeys []*btcec.PublicKey, r *rand.Rand, t *testing.T) [ // findLastStakingCap finds the last staking cap that is not zero // it returns zero if not non-zero value is found -func findLastStakingCap(prevVersions []*types.GlobalParams) btcutil.Amount { +func findLastStakingCap(prevVersions []*parser.ParsedVersionedGlobalParams) btcutil.Amount { numPrevVersions := len(prevVersions) if len(prevVersions) == 0 { return 0 diff --git a/testutils/datagen/staking.go b/testutils/datagen/staking.go index 0042051..8ae1163 100644 --- a/testutils/datagen/staking.go +++ b/testutils/datagen/staking.go @@ -6,6 +6,7 @@ import ( "github.com/babylonchain/babylon/btcstaking" bbndatagen "github.com/babylonchain/babylon/testutil/datagen" + "github.com/babylonchain/networks/parameters/parser" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" @@ -14,7 +15,6 @@ import ( "github.com/stretchr/testify/require" "github.com/babylonchain/staking-indexer/indexerstore" - "github.com/babylonchain/staking-indexer/types" ) type TestStakingData struct { @@ -27,7 +27,7 @@ type TestStakingData struct { func GenerateTestStakingData( t *testing.T, r *rand.Rand, - params *types.GlobalParams, + params *parser.ParsedVersionedGlobalParams, ) *TestStakingData { stakerPrivKey, err := btcec.NewPrivateKey() require.NoError(t, err) @@ -46,7 +46,7 @@ func GenerateTestStakingData( } } -func GenerateStakingTxFromTestData(t *testing.T, r *rand.Rand, params *types.GlobalParams, stakingData *TestStakingData) (*btcstaking.IdentifiableStakingInfo, *btcutil.Tx) { +func GenerateStakingTxFromTestData(t *testing.T, r *rand.Rand, params *parser.ParsedVersionedGlobalParams, stakingData *TestStakingData) (*btcstaking.IdentifiableStakingInfo, *btcutil.Tx) { stakingInfo, tx, err := btcstaking.BuildV0IdentifiableStakingOutputsAndTx( params.Tag, stakingData.StakerKey, @@ -73,7 +73,7 @@ func GenerateStakingTxFromTestData(t *testing.T, r *rand.Rand, params *types.Glo return stakingInfo, btcutil.NewTx(tx) } -func GenerateUnbondingTxFromStaking(t *testing.T, params *types.GlobalParams, stakingData *TestStakingData, stakingTxHash *chainhash.Hash, stakingOutputIdx uint32) *btcutil.Tx { +func GenerateUnbondingTxFromStaking(t *testing.T, params *parser.ParsedVersionedGlobalParams, stakingData *TestStakingData, stakingTxHash *chainhash.Hash, stakingOutputIdx uint32) *btcutil.Tx { stakingInfo, err := btcstaking.BuildV0IdentifiableStakingOutputs( params.Tag, stakingData.StakerKey, @@ -109,7 +109,7 @@ func GenerateUnbondingTxFromStaking(t *testing.T, params *types.GlobalParams, st return btcutil.NewTx(unbondingTx) } -func GenerateWithdrawalTxFromStaking(t *testing.T, r *rand.Rand, params *types.GlobalParams, stakingData *TestStakingData, stakingTxHash *chainhash.Hash, stakingOutputIdx uint32) *btcutil.Tx { +func GenerateWithdrawalTxFromStaking(t *testing.T, r *rand.Rand, params *parser.ParsedVersionedGlobalParams, stakingData *TestStakingData, stakingTxHash *chainhash.Hash, stakingOutputIdx uint32) *btcutil.Tx { stakingInfo, err := btcstaking.BuildV0IdentifiableStakingOutputs( params.Tag, stakingData.StakerKey, @@ -140,7 +140,7 @@ func GenerateWithdrawalTxFromStaking(t *testing.T, r *rand.Rand, params *types.G return btcutil.NewTx(withdrawalTx) } -func GenerateWithdrawalTxFromUnbonding(t *testing.T, r *rand.Rand, params *types.GlobalParams, stakingData *TestStakingData, unbondingTxHash *chainhash.Hash) *btcutil.Tx { +func GenerateWithdrawalTxFromUnbonding(t *testing.T, r *rand.Rand, params *parser.ParsedVersionedGlobalParams, stakingData *TestStakingData, unbondingTxHash *chainhash.Hash) *btcutil.Tx { // build and send withdraw tx from the unbonding tx unbondingInfo, err := btcstaking.BuildUnbondingInfo( stakingData.StakerKey, diff --git a/testutils/mocks/btc_client.go b/testutils/mocks/btc_client.go index e402741..c4bd42b 100644 --- a/testutils/mocks/btc_client.go +++ b/testutils/mocks/btc_client.go @@ -8,8 +8,8 @@ import ( reflect "reflect" types "github.com/babylonchain/staking-indexer/types" + wire "github.com/btcsuite/btcd/wire" gomock "github.com/golang/mock/gomock" - "github.com/btcsuite/btcd/wire" ) // MockClient is a mock of Client interface. diff --git a/testutils/mocks/btc_scanner.go b/testutils/mocks/btc_scanner.go index 2aa9acd..283f7f1 100644 --- a/testutils/mocks/btc_scanner.go +++ b/testutils/mocks/btc_scanner.go @@ -93,17 +93,17 @@ func (mr *MockBtcScannerMockRecorder) LastConfirmedHeight() *gomock.Call { } // Start mocks base method. -func (m *MockBtcScanner) Start(startHeight uint64) error { +func (m *MockBtcScanner) Start(startHeight, activationHeight uint64) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Start", startHeight) + ret := m.ctrl.Call(m, "Start", startHeight, activationHeight) ret0, _ := ret[0].(error) return ret0 } // Start indicates an expected call of Start. -func (mr *MockBtcScannerMockRecorder) Start(startHeight interface{}) *gomock.Call { +func (mr *MockBtcScannerMockRecorder) Start(startHeight, activationHeight interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockBtcScanner)(nil).Start), startHeight) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockBtcScanner)(nil).Start), startHeight, activationHeight) } // Stop mocks base method. diff --git a/testutils/utils.go b/testutils/utils.go index a50bfd5..a61a3e8 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/babylonchain/babylon/btcstaking" + "github.com/babylonchain/networks/parameters/parser" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcjson" @@ -22,7 +23,6 @@ import ( "github.com/stretchr/testify/require" "github.com/babylonchain/staking-indexer/config" - "github.com/babylonchain/staking-indexer/types" ) type Utxo struct { @@ -238,7 +238,7 @@ func makeInputSource(utxos []Utxo) txauthor.InputSource { func BuildUnbondingTx( t *testing.T, - params *types.GlobalParams, + params *parser.ParsedVersionedGlobalParams, stakerPrivKey *btcec.PrivateKey, fpKey *btcec.PublicKey, stakingAmount btcutil.Amount, diff --git a/types/params.go b/types/params.go deleted file mode 100644 index b027886..0000000 --- a/types/params.go +++ /dev/null @@ -1,58 +0,0 @@ -package types - -import ( - "fmt" - - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcutil" -) - -type ParamsVersions struct { - ParamsVersions []*GlobalParams -} - -type GlobalParams struct { - Version uint16 - ActivationHeight uint64 - StakingCap btcutil.Amount - CapHeight uint64 - Tag []byte - CovenantPks []*btcec.PublicKey - CovenantQuorum uint32 - UnbondingTime uint16 - UnbondingFee btcutil.Amount - MaxStakingAmount btcutil.Amount - MinStakingAmount btcutil.Amount - MaxStakingTime uint16 - MinStakingTime uint16 - ConfirmationDepth uint16 -} - -// GetParamsForBTCHeight Retrieve the parameters that should take into effect -// for a particular BTC height. -// Makes the assumption that the parameters are ordered by monotonically increasing `ActivationHeight` -// and that there are no overlaps between parameter activations. -func (pv *ParamsVersions) GetParamsForBTCHeight(btcHeight int32) (*GlobalParams, error) { - // Iterate the list in reverse (i.e. decreasing ActivationHeight) - // and identify the first element that has an activation height below - // the specified BTC height. - for i := len(pv.ParamsVersions) - 1; i >= 0; i-- { - paramsVersion := pv.ParamsVersions[i] - if paramsVersion.ActivationHeight <= uint64(btcHeight) { - return paramsVersion, nil - } - } - return nil, fmt.Errorf("no version corresponds to the provided BTC height") -} - -func (gp *GlobalParams) IsTimeBasedCap() bool { - if gp.StakingCap != 0 && gp.CapHeight != 0 { - panic(fmt.Errorf("only either of staking cap and cap height can be set")) - } - - if gp.StakingCap == 0 && gp.CapHeight == 0 { - panic(fmt.Errorf("either of staking cap and cap height must be set")) - } - - return gp.CapHeight != 0 -}