diff --git a/test/replay/app_replay_e2e_test.go b/test/replay/app_replay_e2e_test.go index 81c523aa4..ba4625323 100644 --- a/test/replay/app_replay_e2e_test.go +++ b/test/replay/app_replay_e2e_test.go @@ -1,630 +1,14 @@ package replay import ( - "bytes" - "context" - "encoding/json" - "fmt" - "path/filepath" "testing" - "time" - "github.com/btcsuite/btcd/chaincfg" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/testutil/testdata" - - "cosmossdk.io/log" - "cosmossdk.io/math" - "github.com/babylonlabs-io/babylon/app" - babylonApp "github.com/babylonlabs-io/babylon/app" - appkeepers "github.com/babylonlabs-io/babylon/app/keepers" - "github.com/babylonlabs-io/babylon/test/e2e/initialization" - btclighttypes "github.com/babylonlabs-io/babylon/x/btclightclient/types" - bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" - dbmc "github.com/cometbft/cometbft-db" - abci "github.com/cometbft/cometbft/abci/types" - cs "github.com/cometbft/cometbft/consensus" - cmtcrypto "github.com/cometbft/cometbft/crypto" - cometlog "github.com/cometbft/cometbft/libs/log" - "github.com/cometbft/cometbft/mempool" - cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" - "github.com/cometbft/cometbft/proxy" - sm "github.com/cometbft/cometbft/state" - "github.com/cometbft/cometbft/store" - cmttypes "github.com/cometbft/cometbft/types" - dbm "github.com/cosmos/cosmos-db" - "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/client/tx" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - "github.com/cosmos/cosmos-sdk/server" - servertypes "github.com/cosmos/cosmos-sdk/server/types" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/tx/signing" - xauthsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" - gogoprotoio "github.com/cosmos/gogoproto/io" - "github.com/otiai10/copy" "github.com/stretchr/testify/require" ) -var validatorConfig = &initialization.NodeConfig{ - Name: "initValidator", - Pruning: "default", - PruningKeepRecent: "0", - PruningInterval: "0", - SnapshotInterval: 1500, - SnapshotKeepRecent: 2, - IsValidator: true, -} - -const ( - chainID = initialization.ChainAID - testPartSize = 65536 - - defaultGasLimit = 500000 - defaultFee = 500000 - epochLength = 10 -) - -var ( - defaultFeeCoin = sdk.NewCoin("ubbn", math.NewInt(defaultFee)) - BtcParams = &chaincfg.SimNetParams - CovenantSKs, _, CovenantQuorum = bstypes.DefaultCovenantCommittee() -) - -func getGenDoc( - t *testing.T, nodeDir string) (map[string]json.RawMessage, *genutiltypes.AppGenesis) { - path := filepath.Join(nodeDir, "config", "genesis.json") - fmt.Printf("path to gendoc: %s\n", path) - - genState, appGenesis, err := genutiltypes.GenesisStateFromGenFile(path) - require.NoError(t, err) - return genState, appGenesis -} - -type AppOptionsMap map[string]interface{} - -func (m AppOptionsMap) Get(key string) interface{} { - v, ok := m[key] - if !ok { - return interface{}(nil) - } - - return v -} - -func NewAppOptionsWithFlagHome(homePath string) servertypes.AppOptions { - return AppOptionsMap{ - flags.FlagHome: homePath, - "btc-config.network": "simnet", - "pruning": "nothing", - "chain-id": chainID, - "app-db-backend": "memdb", - } -} - -func getBlockId(t *testing.T, block *cmttypes.Block) cmttypes.BlockID { - bps, err := block.MakePartSet(testPartSize) - require.NoError(t, err) - return cmttypes.BlockID{Hash: block.Hash(), PartSetHeader: bps.Header()} -} - -type FinalizedBlock struct { - Height uint64 - ID cmttypes.BlockID - Block *cmttypes.Block -} - -type BabylonAppDriver struct { - App *app.BabylonApp - PrivSigner *appkeepers.PrivSigner - DriverAccountPrivKey cryptotypes.PrivKey - DriverAccountSeqNr uint64 - DriverAccountAccNr uint64 - BlockExec *sm.BlockExecutor - BlockStore *store.BlockStore - StateStore sm.Store - NodeDir string - ValidatorAddress []byte - FinalizedBlocks []FinalizedBlock - LastState sm.State -} - -// Inititializes Babylon driver for block creation -func NewBabylonAppDriver( - t *testing.T, - dir string, - copyDir string, -) *BabylonAppDriver { - chain, err := initialization.InitChain( - chainID, - dir, - []*initialization.NodeConfig{validatorConfig}, - 3*time.Minute, - 1*time.Minute, - 1, - []*btclighttypes.BTCHeaderInfo{}, - ) - require.NoError(t, err) - require.NotNil(t, chain) - - _, doc := getGenDoc(t, chain.Nodes[0].ConfigDir) - fmt.Printf("config dir is path %s\n", chain.Nodes[0].ConfigDir) - - if copyDir != "" { - // Copy dir is needed as otherwise - err := copy.Copy(chain.Nodes[0].ConfigDir, copyDir) - fmt.Printf("copying %s to %s\n", chain.Nodes[0].ConfigDir, copyDir) - - require.NoError(t, err) - } - - genDoc, err := doc.ToGenesisDoc() - require.NoError(t, err) - - state, err := sm.MakeGenesisState(genDoc) - require.NoError(t, err) - - stateStore := sm.NewStore(dbmc.NewMemDB(), sm.StoreOptions{ - DiscardABCIResponses: false, - }) - - if err := stateStore.Save(state); err != nil { - panic(err) - } - - signer, err := appkeepers.InitPrivSigner(chain.Nodes[0].ConfigDir) - require.NoError(t, err) - require.NotNil(t, signer) - signerValAddress := signer.WrappedPV.GetAddress() - fmt.Printf("signer val address: %s\n", signerValAddress.String()) - - appOptions := NewAppOptionsWithFlagHome(chain.Nodes[0].ConfigDir) - baseAppOptions := server.DefaultBaseappOptions(appOptions) - tmpApp := babylonApp.NewBabylonApp( - log.NewNopLogger(), - dbm.NewMemDB(), - nil, - true, - map[int64]bool{}, - 0, - signer, - appOptions, - babylonApp.EmptyWasmOpts, - baseAppOptions..., - ) - - cmtApp := server.NewCometABCIWrapper(tmpApp) - procxyCons := proxy.NewMultiAppConn( - proxy.NewLocalClientCreator(cmtApp), - proxy.NopMetrics(), - ) - err = procxyCons.Start() - require.NoError(t, err) - - blockStore := store.NewBlockStore(dbmc.NewMemDB()) - - blockExec := sm.NewBlockExecutor( - stateStore, - cometlog.TestingLogger(), - procxyCons.Consensus(), - &mempool.NopMempool{}, - sm.EmptyEvidencePool{}, - blockStore, - ) - require.NotNil(t, blockExec) - - hs := cs.NewHandshaker( - stateStore, - state, - blockStore, - genDoc, - ) - - require.NotNil(t, hs) - hs.SetLogger(cometlog.TestingLogger()) - err = hs.Handshake(procxyCons) - require.NoError(t, err) - - state, err = stateStore.Load() - require.NoError(t, err) - require.NotNil(t, state) - validatorAddress, _ := state.Validators.GetByIndex(0) - - validatorPrivKey := secp256k1.PrivKey{ - Key: chain.Nodes[0].PrivateKey, - } - - return &BabylonAppDriver{ - App: tmpApp, - PrivSigner: signer, - DriverAccountPrivKey: &validatorPrivKey, - // Driver account always start from 1, as we executed tx for creating validator - // in genesis block - DriverAccountSeqNr: 1, - DriverAccountAccNr: 0, - BlockExec: blockExec, - BlockStore: blockStore, - StateStore: stateStore, - NodeDir: chain.Nodes[0].ConfigDir, - ValidatorAddress: validatorAddress, - FinalizedBlocks: []FinalizedBlock{}, - LastState: state.Copy(), - } -} - -func (d *BabylonAppDriver) GetLastFinalizedBlock() *FinalizedBlock { - if len(d.FinalizedBlocks) == 0 { - return nil - } - - return &d.FinalizedBlocks[len(d.FinalizedBlocks)-1] -} - -type senderInfo struct { - privKey cryptotypes.PrivKey - sequenceNumber uint64 - accountNumber uint64 -} - -func createTx( - t *testing.T, - txConfig client.TxConfig, - senderInfo *senderInfo, - gas uint64, - fee sdk.Coin, - msgs ...sdk.Msg, -) []byte { - txBuilder := txConfig.NewTxBuilder() - txBuilder.SetGasLimit(gas) - txBuilder.SetFeeAmount(sdk.NewCoins(fee)) - txBuilder.SetMsgs(msgs...) - - sigV2 := signing.SignatureV2{ - PubKey: senderInfo.privKey.PubKey(), - Data: &signing.SingleSignatureData{ - SignMode: signing.SignMode(txConfig.SignModeHandler().DefaultMode()), - Signature: nil, - }, - Sequence: senderInfo.sequenceNumber, - } - - err := txBuilder.SetSignatures(sigV2) - require.NoError(t, err) - - signerData := xauthsigning.SignerData{ - ChainID: chainID, - AccountNumber: senderInfo.accountNumber, - Sequence: senderInfo.sequenceNumber, - } - - sigV2, err = tx.SignWithPrivKey( - context.Background(), - signing.SignMode(txConfig.SignModeHandler().DefaultMode()), - signerData, - txBuilder, - senderInfo.privKey, - txConfig, - senderInfo.sequenceNumber, - ) - require.NoError(t, err) - - err = txBuilder.SetSignatures(sigV2) - require.NoError(t, err) - - txBytes, err := txConfig.TxEncoder()(txBuilder.GetTx()) - require.NoError(t, err) - - return txBytes -} - -func (d *BabylonAppDriver) CreateTx( - t *testing.T, - senderInfo *senderInfo, - gas uint64, - fee sdk.Coin, - msgs ...sdk.Msg, -) []byte { - return createTx(t, d.App.TxConfig(), senderInfo, gas, fee, msgs...) -} - -// SendTxWithMessagesSuccess sends tx with msgs to the mempool and asserts that -// execution was successful -func (d *BabylonAppDriver) SendTxWithMessagesSuccess( - t *testing.T, - senderInfo *senderInfo, - gas uint64, - fee sdk.Coin, - msgs ...sdk.Msg, -) { - txBytes := d.CreateTx(t, senderInfo, gas, fee, msgs...) - - result, err := d.App.CheckTx(&abci.RequestCheckTx{ - Tx: txBytes, - Type: abci.CheckTxType_New, - }) - require.NoError(t, err) - require.Equal(t, result.Code, uint32(0)) -} - -func signVoteExtension( - t *testing.T, - veBytes []byte, - height uint64, - valPrivKey cmtcrypto.PrivKey, -) []byte { - cve := cmtproto.CanonicalVoteExtension{ - Extension: veBytes, - Height: int64(height), - Round: int64(0), - ChainId: chainID, - } - - var cveBuffer bytes.Buffer - err := gogoprotoio.NewDelimitedWriter(&cveBuffer).WriteMsg(&cve) - require.NoError(t, err) - cveBytes := cveBuffer.Bytes() - extensionSig, err := valPrivKey.Sign(cveBytes) - require.NoError(t, err) - - return extensionSig -} - -func (d *BabylonAppDriver) GenerateNewBlock(t *testing.T) *abci.ResponseFinalizeBlock { - if len(d.FinalizedBlocks) == 0 { - extCommitFirsBlock := &cmttypes.ExtendedCommit{} - block1, err := d.BlockExec.CreateProposalBlock( - context.Background(), - 1, - d.LastState, - extCommitFirsBlock, - d.ValidatorAddress, - ) - require.NoError(t, err) - require.NotNil(t, block1) - - accepted, err := d.BlockExec.ProcessProposal(block1, d.LastState) - require.NoError(t, err) - require.True(t, accepted) - - block1ID := getBlockId(t, block1) - state, err := d.BlockExec.ApplyVerifiedBlock(d.LastState, block1ID, block1) - require.NoError(t, err) - require.NotNil(t, state) - - d.FinalizedBlocks = append(d.FinalizedBlocks, FinalizedBlock{ - Height: 1, - ID: block1ID, - Block: block1, - }) - d.LastState = state.Copy() - - lastResponse, err := d.StateStore.LoadFinalizeBlockResponse(1) - require.NoError(t, err) - require.NotNil(t, lastResponse) - return lastResponse - } else { - lastFinalizedBlock := d.GetLastFinalizedBlock() - - var extension []byte - - if lastFinalizedBlock.Height > 1 { - ext, err := d.BlockExec.ExtendVote( - context.Background(), - &cmttypes.Vote{ - BlockID: lastFinalizedBlock.ID, - Height: int64(lastFinalizedBlock.Height), - }, - lastFinalizedBlock.Block, - d.LastState, - ) - require.NoError(t, err) - extension = ext - } else { - extension = []byte{} - } - - extensionSig := signVoteExtension( - t, - extension, - lastFinalizedBlock.Height, - d.PrivSigner.WrappedPV.GetValPrivKey(), - ) - - // We are adding invalid signatures here as we are not validating them in - // ApplyBlock - extCommitSig := cmttypes.ExtendedCommitSig{ - CommitSig: cmttypes.CommitSig{ - BlockIDFlag: cmttypes.BlockIDFlagCommit, - ValidatorAddress: d.ValidatorAddress, - Timestamp: time.Now().Add(1 * time.Second), - Signature: []byte("test"), - }, - Extension: extension, - ExtensionSignature: extensionSig, - } - - oneValExtendedCommit := &cmttypes.ExtendedCommit{ - Height: int64(lastFinalizedBlock.Height), - Round: 0, - BlockID: lastFinalizedBlock.ID, - ExtendedSignatures: []cmttypes.ExtendedCommitSig{ - extCommitSig, - }, - } - - block1, err := d.BlockExec.CreateProposalBlock( - context.Background(), - int64(lastFinalizedBlock.Height)+1, - d.LastState, - oneValExtendedCommit, - d.ValidatorAddress, - ) - require.NoError(t, err) - require.NotNil(t, block1) - - // it is here as it is good sanity check for all babylon custom validations - accepted, err := d.BlockExec.ProcessProposal(block1, d.LastState) - require.NoError(t, err) - require.True(t, accepted) - - block1ID := getBlockId(t, block1) - state, err := d.BlockExec.ApplyVerifiedBlock(d.LastState, block1ID, block1) - require.NoError(t, err) - require.NotNil(t, state) - - d.FinalizedBlocks = append(d.FinalizedBlocks, FinalizedBlock{ - Height: lastFinalizedBlock.Height + 1, - ID: block1ID, - Block: block1, - }) - d.LastState = state.Copy() - - lastResponse, err := d.StateStore.LoadFinalizeBlockResponse(state.LastBlockHeight) - require.NoError(t, err) - require.NotNil(t, lastResponse) - return lastResponse - } -} - -func (d *BabylonAppDriver) GenerateNewBlockAssertExecutionSuccess( - t *testing.T, - expectedTxNumber int, -) { - response := d.GenerateNewBlock(t) - - require.Equal(t, len(response.TxResults), expectedTxNumber) - for _, tx := range response.TxResults { - require.Equal(t, tx.Code, uint32(0)) - } -} - -func (d *BabylonAppDriver) GetDriverAccountAddress() sdk.AccAddress { - return sdk.AccAddress(d.DriverAccountPrivKey.PubKey().Address()) -} - -func (d *BabylonAppDriver) GetDriverAccountSenderInfo() *senderInfo { - return &senderInfo{ - privKey: d.DriverAccountPrivKey, - sequenceNumber: d.DriverAccountSeqNr, - accountNumber: d.DriverAccountAccNr, - } -} - -// SendTxWithMsgsFromDriverAccount sends tx with msgs from driver account and asserts that -// execution was successful. It assumes that there will only be one tx in the block. -func (d *BabylonAppDriver) SendTxWithMsgsFromDriverAccount( - t *testing.T, - msgs ...sdk.Msg) { - d.SendTxWithMessagesSuccess( - t, - d.GetDriverAccountSenderInfo(), - defaultGasLimit, - defaultFeeCoin, - msgs..., - ) - d.GenerateNewBlockAssertExecutionSuccess(t, 1) - - d.DriverAccountSeqNr++ -} - -type BlockReplayer struct { - BlockExec *sm.BlockExecutor - LastState sm.State -} - -func NewBlockReplayer(t *testing.T, nodeDir string) *BlockReplayer { - _, doc := getGenDoc(t, nodeDir) - - genDoc, err := doc.ToGenesisDoc() - require.NoError(t, err) - - state, err := sm.MakeGenesisState(genDoc) - require.NoError(t, err) - - stateStore := sm.NewStore(dbmc.NewMemDB(), sm.StoreOptions{ - DiscardABCIResponses: false, - }) - - if err := stateStore.Save(state); err != nil { - panic(err) - } - - signer, err := appkeepers.InitPrivSigner(nodeDir) - require.NoError(t, err) - require.NotNil(t, signer) - signerValAddress := signer.WrappedPV.GetAddress() - fmt.Printf("signer val address: %s\n", signerValAddress.String()) - - appOptions := NewAppOptionsWithFlagHome(nodeDir) - baseAppOptions := server.DefaultBaseappOptions(appOptions) - tmpApp := babylonApp.NewBabylonApp( - log.NewNopLogger(), - dbm.NewMemDB(), - nil, - true, - map[int64]bool{}, - 0, - signer, - appOptions, - babylonApp.EmptyWasmOpts, - baseAppOptions..., - ) - - cmtApp := server.NewCometABCIWrapper(tmpApp) - procxyCons := proxy.NewMultiAppConn( - proxy.NewLocalClientCreator(cmtApp), - proxy.NopMetrics(), - ) - err = procxyCons.Start() - require.NoError(t, err) - - blockStore := store.NewBlockStore(dbmc.NewMemDB()) - - blockExec := sm.NewBlockExecutor( - stateStore, - cometlog.TestingLogger(), - procxyCons.Consensus(), - &mempool.NopMempool{}, - sm.EmptyEvidencePool{}, - blockStore, - ) - require.NotNil(t, blockExec) - - hs := cs.NewHandshaker( - stateStore, - state, - blockStore, - genDoc, - ) - - require.NotNil(t, hs) - hs.SetLogger(cometlog.TestingLogger()) - err = hs.Handshake(procxyCons) - require.NoError(t, err) - - state, err = stateStore.Load() - require.NoError(t, err) - require.NotNil(t, state) - - return &BlockReplayer{ - BlockExec: blockExec, - LastState: state, - } -} - -func (r *BlockReplayer) ReplayBlocks(t *testing.T, blocks []FinalizedBlock) { - for _, block := range blocks { - blockID := getBlockId(t, block.Block) - state, err := r.BlockExec.ApplyVerifiedBlock(r.LastState, blockID, block.Block) - require.NoError(t, err) - require.NotNil(t, state) - r.LastState = state.Copy() - } -} - func TestReplayBlocks(t *testing.T) { driverTempDir := t.TempDir() replayerTempDir := t.TempDir() diff --git a/test/replay/btcstaking_test.go b/test/replay/btcstaking_test.go new file mode 100644 index 000000000..6179af49b --- /dev/null +++ b/test/replay/btcstaking_test.go @@ -0,0 +1,113 @@ +package replay + +import ( + "math/rand" + "testing" + "time" + + "github.com/babylonlabs-io/babylon/testutil/datagen" + bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" + "github.com/stretchr/testify/require" +) + +// TestEpochFinalization checks whether we can finalize some epochs +func TestEpochFinalization(t *testing.T) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + driverTempDir := t.TempDir() + replayerTempDir := t.TempDir() + driver := NewBabylonAppDriver(t, driverTempDir, replayerTempDir) + // first finalize at least one block + driver.GenerateNewBlock(t) + epochingParams := driver.GetEpochingParams() + + epoch1 := driver.GetEpoch() + require.Equal(t, epoch1.EpochNumber, uint64(1)) + + for i := 0; i < int(epochingParams.EpochInterval); i++ { + driver.GenerateNewBlock(t) + } + + epoch2 := driver.GetEpoch() + require.Equal(t, epoch2.EpochNumber, uint64(2)) + + driver.FinializeCkptForEpoch(r, t, epoch1.EpochNumber) +} + +func FuzzCreatingAndActivatingDelegations(f *testing.F) { + datagen.AddRandomSeedsToFuzzer(f, 3) + + f.Fuzz(func(t *testing.T, seed int64) { + t.Parallel() + r := rand.New(rand.NewSource(seed)) + numFinalityProviders := uint32(datagen.RandomInRange(r, 3, 7)) + numDelegationsPerFinalityProvider := uint32(datagen.RandomInRange(r, 20, 30)) + + driverTempDir := t.TempDir() + replayerTempDir := t.TempDir() + driver := NewBabylonAppDriver(t, driverTempDir, replayerTempDir) + // first finalize at least one block + driver.GenerateNewBlock(t) + stakingParams := driver.GetBTCStakingParams(t) + + fpInfos := GenerateNFinalityProviders(r, t, numFinalityProviders, driver.GetDriverAccountAddress()) + + // register all finality providers + for _, fpInfo := range fpInfos { + driver.SendTxWithMsgsFromDriverAccount(t, fpInfo.MsgCreateFinalityProvider) + } + + // register all delegations + var allDelegationInfos []*datagen.CreateDelegationInfo + for _, fpInfo := range fpInfos { + delInfos := GenerateNBTCDelegationsForFinalityProvider( + r, + t, + numDelegationsPerFinalityProvider, + driver.GetDriverAccountAddress(), + fpInfo, + stakingParams, + ) + allDelegationInfos = append(allDelegationInfos, delInfos...) + msgs := DelegationInfosToCreateBTCDelegationMsgs(delInfos) + driver.SendTxWithMsgsFromDriverAccount(t, msgs...) + } + + allDelegations := driver.GetAllBTCDelegations(t) + require.Equal(t, uint32(len(allDelegations)), numFinalityProviders*numDelegationsPerFinalityProvider) + + // add all covenant signatures + for _, delInfo := range allDelegationInfos { + driver.SendTxWithMsgsFromDriverAccount(t, MsgsToSdkMsg(delInfo.MsgAddCovenantSigs)...) + } + + allVerifiedDelegations := driver.GetVerifiedBTCDelegations(t) + require.Equal(t, uint32(len(allVerifiedDelegations)), numFinalityProviders*numDelegationsPerFinalityProvider) + + stakingTransactions := DelegationInfosToBTCTx(allDelegationInfos) + blockWithProofs := driver.GenBlockWithTransactions( + r, + t, + stakingTransactions, + ) + // make staking txs k-deep + driver.ExtendBTCLcWithNEmptyBlocks(r, t, 10) + + for i, stakingTx := range stakingTransactions { + driver.SendTxWithMsgsFromDriverAccount(t, &bstypes.MsgAddBTCDelegationInclusionProof{ + Signer: driver.GetDriverAccountAddress().String(), + StakingTxHash: stakingTx.TxHash().String(), + StakingTxInclusionProof: bstypes.NewInclusionProofFromSpvProof(blockWithProofs.Proofs[i+1]), + }) + } + + activeDelegations := driver.GetActiveBTCDelegations(t) + require.Equal(t, uint32(len(activeDelegations)), numFinalityProviders*numDelegationsPerFinalityProvider) + + // Replay all the blocks from driver and check appHash + replayer := NewBlockReplayer(t, replayerTempDir) + replayer.ReplayBlocks(t, driver.FinalizedBlocks) + // after replay we should have the same apphash + require.Equal(t, driver.LastState.LastBlockHeight, replayer.LastState.LastBlockHeight) + require.Equal(t, driver.LastState.AppHash, replayer.LastState.AppHash) + }) +} diff --git a/test/replay/driver.go b/test/replay/driver.go new file mode 100644 index 000000000..c822d914d --- /dev/null +++ b/test/replay/driver.go @@ -0,0 +1,889 @@ +package replay + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + goMath "math" + "math/rand" + "path/filepath" + "testing" + "time" + + "github.com/babylonlabs-io/babylon/btctxformatter" + bbn "github.com/babylonlabs-io/babylon/types" + btckckpttypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" + ckpttypes "github.com/babylonlabs-io/babylon/x/checkpointing/types" + et "github.com/babylonlabs-io/babylon/x/epoching/types" + + "cosmossdk.io/log" + "cosmossdk.io/math" + "github.com/babylonlabs-io/babylon/app" + babylonApp "github.com/babylonlabs-io/babylon/app" + appkeepers "github.com/babylonlabs-io/babylon/app/keepers" + "github.com/babylonlabs-io/babylon/test/e2e/initialization" + "github.com/babylonlabs-io/babylon/testutil/datagen" + btclighttypes "github.com/babylonlabs-io/babylon/x/btclightclient/types" + bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" + dbmc "github.com/cometbft/cometbft-db" + abci "github.com/cometbft/cometbft/abci/types" + cs "github.com/cometbft/cometbft/consensus" + cmtcrypto "github.com/cometbft/cometbft/crypto" + cometlog "github.com/cometbft/cometbft/libs/log" + "github.com/cometbft/cometbft/mempool" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cometbft/cometbft/proxy" + sm "github.com/cometbft/cometbft/state" + "github.com/cometbft/cometbft/store" + cmttypes "github.com/cometbft/cometbft/types" + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/server" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + xauthsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + gogoprotoio "github.com/cosmos/gogoproto/io" + "github.com/otiai10/copy" + "github.com/stretchr/testify/require" +) + +var validatorConfig = &initialization.NodeConfig{ + Name: "initValidator", + Pruning: "default", + PruningKeepRecent: "0", + PruningInterval: "0", + SnapshotInterval: 1500, + SnapshotKeepRecent: 2, + IsValidator: true, +} + +const ( + chainID = initialization.ChainAID + testPartSize = 65536 + + defaultGasLimit = 5000000 + defaultFee = 500000 + epochLength = 10 +) + +var ( + defaultFeeCoin = sdk.NewCoin("ubbn", math.NewInt(defaultFee)) + BtcParams = &chaincfg.SimNetParams + covenantSKs, _, CovenantQuorum = bstypes.DefaultCovenantCommittee() +) + +func getGenDoc( + t *testing.T, nodeDir string) (map[string]json.RawMessage, *genutiltypes.AppGenesis) { + path := filepath.Join(nodeDir, "config", "genesis.json") + fmt.Printf("path to gendoc: %s\n", path) + + genState, appGenesis, err := genutiltypes.GenesisStateFromGenFile(path) + require.NoError(t, err) + return genState, appGenesis +} + +func MsgsToSdkMsg[M sdk.Msg](msgs []M) []sdk.Msg { + sdkMsgs := make([]sdk.Msg, len(msgs)) + for i, msg := range msgs { + sdkMsgs[i] = msg + } + return sdkMsgs +} + +type AppOptionsMap map[string]interface{} + +func (m AppOptionsMap) Get(key string) interface{} { + v, ok := m[key] + if !ok { + return interface{}(nil) + } + + return v +} + +func NewAppOptionsWithFlagHome(homePath string) servertypes.AppOptions { + return AppOptionsMap{ + flags.FlagHome: homePath, + "btc-config.network": "simnet", + "pruning": "nothing", + "chain-id": chainID, + "app-db-backend": "memdb", + } +} + +func getBlockId(t *testing.T, block *cmttypes.Block) cmttypes.BlockID { + bps, err := block.MakePartSet(testPartSize) + require.NoError(t, err) + return cmttypes.BlockID{Hash: block.Hash(), PartSetHeader: bps.Header()} +} + +type FinalizedBlock struct { + Height uint64 + ID cmttypes.BlockID + Block *cmttypes.Block +} + +type BabylonAppDriver struct { + App *app.BabylonApp + PrivSigner *appkeepers.PrivSigner + DriverAccountPrivKey cryptotypes.PrivKey + DriverAccountSeqNr uint64 + DriverAccountAccNr uint64 + BlockExec *sm.BlockExecutor + BlockStore *store.BlockStore + StateStore sm.Store + NodeDir string + ValidatorAddress []byte + FinalizedBlocks []FinalizedBlock + LastState sm.State +} + +// Inititializes Babylon driver for block creation +func NewBabylonAppDriver( + t *testing.T, + dir string, + copyDir string, +) *BabylonAppDriver { + chain, err := initialization.InitChain( + chainID, + dir, + []*initialization.NodeConfig{validatorConfig}, + 3*time.Minute, + 1*time.Minute, + 1, + []*btclighttypes.BTCHeaderInfo{}, + ) + require.NoError(t, err) + require.NotNil(t, chain) + + _, doc := getGenDoc(t, chain.Nodes[0].ConfigDir) + fmt.Printf("config dir is path %s\n", chain.Nodes[0].ConfigDir) + + if copyDir != "" { + // Copy dir is needed as otherwise + err := copy.Copy(chain.Nodes[0].ConfigDir, copyDir) + fmt.Printf("copying %s to %s\n", chain.Nodes[0].ConfigDir, copyDir) + + require.NoError(t, err) + } + + genDoc, err := doc.ToGenesisDoc() + require.NoError(t, err) + + state, err := sm.MakeGenesisState(genDoc) + require.NoError(t, err) + + stateStore := sm.NewStore(dbmc.NewMemDB(), sm.StoreOptions{ + DiscardABCIResponses: false, + }) + + if err := stateStore.Save(state); err != nil { + panic(err) + } + + signer, err := appkeepers.InitPrivSigner(chain.Nodes[0].ConfigDir) + require.NoError(t, err) + require.NotNil(t, signer) + signerValAddress := signer.WrappedPV.GetAddress() + fmt.Printf("signer val address: %s\n", signerValAddress.String()) + + appOptions := NewAppOptionsWithFlagHome(chain.Nodes[0].ConfigDir) + baseAppOptions := server.DefaultBaseappOptions(appOptions) + tmpApp := babylonApp.NewBabylonApp( + log.NewNopLogger(), + dbm.NewMemDB(), + nil, + true, + map[int64]bool{}, + 0, + signer, + appOptions, + babylonApp.EmptyWasmOpts, + baseAppOptions..., + ) + + cmtApp := server.NewCometABCIWrapper(tmpApp) + procxyCons := proxy.NewMultiAppConn( + proxy.NewLocalClientCreator(cmtApp), + proxy.NopMetrics(), + ) + err = procxyCons.Start() + require.NoError(t, err) + + blockStore := store.NewBlockStore(dbmc.NewMemDB()) + + blockExec := sm.NewBlockExecutor( + stateStore, + cometlog.TestingLogger(), + procxyCons.Consensus(), + &mempool.NopMempool{}, + sm.EmptyEvidencePool{}, + blockStore, + ) + require.NotNil(t, blockExec) + + hs := cs.NewHandshaker( + stateStore, + state, + blockStore, + genDoc, + ) + + require.NotNil(t, hs) + hs.SetLogger(cometlog.TestingLogger()) + err = hs.Handshake(procxyCons) + require.NoError(t, err) + + state, err = stateStore.Load() + require.NoError(t, err) + require.NotNil(t, state) + validatorAddress, _ := state.Validators.GetByIndex(0) + + validatorPrivKey := secp256k1.PrivKey{ + Key: chain.Nodes[0].PrivateKey, + } + + return &BabylonAppDriver{ + App: tmpApp, + PrivSigner: signer, + DriverAccountPrivKey: &validatorPrivKey, + // Driver account always start from 1, as we executed tx for creating validator + // in genesis block + DriverAccountSeqNr: 1, + DriverAccountAccNr: 0, + BlockExec: blockExec, + BlockStore: blockStore, + StateStore: stateStore, + NodeDir: chain.Nodes[0].ConfigDir, + ValidatorAddress: validatorAddress, + FinalizedBlocks: []FinalizedBlock{}, + LastState: state.Copy(), + } +} + +func (d *BabylonAppDriver) GetLastFinalizedBlock() *FinalizedBlock { + if len(d.FinalizedBlocks) == 0 { + return nil + } + + return &d.FinalizedBlocks[len(d.FinalizedBlocks)-1] +} + +func (d *BabylonAppDriver) GetContextForLastFinalizedBlock() sdk.Context { + lastFinalizedBlock := d.GetLastFinalizedBlock() + return d.App.NewUncachedContext(false, *lastFinalizedBlock.Block.Header.ToProto()) +} + +type senderInfo struct { + privKey cryptotypes.PrivKey + sequenceNumber uint64 + accountNumber uint64 +} + +func createTx( + t *testing.T, + txConfig client.TxConfig, + senderInfo *senderInfo, + gas uint64, + fee sdk.Coin, + msgs ...sdk.Msg, +) []byte { + txBuilder := txConfig.NewTxBuilder() + txBuilder.SetGasLimit(gas) + txBuilder.SetFeeAmount(sdk.NewCoins(fee)) + err := txBuilder.SetMsgs(msgs...) + require.NoError(t, err) + + sigV2 := signing.SignatureV2{ + PubKey: senderInfo.privKey.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: signing.SignMode(txConfig.SignModeHandler().DefaultMode()), + Signature: nil, + }, + Sequence: senderInfo.sequenceNumber, + } + + err = txBuilder.SetSignatures(sigV2) + require.NoError(t, err) + + signerData := xauthsigning.SignerData{ + ChainID: chainID, + AccountNumber: senderInfo.accountNumber, + Sequence: senderInfo.sequenceNumber, + } + + sigV2, err = tx.SignWithPrivKey( + context.Background(), + signing.SignMode(txConfig.SignModeHandler().DefaultMode()), + signerData, + txBuilder, + senderInfo.privKey, + txConfig, + senderInfo.sequenceNumber, + ) + require.NoError(t, err) + + err = txBuilder.SetSignatures(sigV2) + require.NoError(t, err) + + txBytes, err := txConfig.TxEncoder()(txBuilder.GetTx()) + require.NoError(t, err) + + return txBytes +} + +func (d *BabylonAppDriver) CreateTx( + t *testing.T, + senderInfo *senderInfo, + gas uint64, + fee sdk.Coin, + msgs ...sdk.Msg, +) []byte { + return createTx(t, d.App.TxConfig(), senderInfo, gas, fee, msgs...) +} + +// SendTxWithMessagesSuccess sends tx with msgs to the mempool and asserts that +// execution was successful +func (d *BabylonAppDriver) SendTxWithMessagesSuccess( + t *testing.T, + senderInfo *senderInfo, + gas uint64, + fee sdk.Coin, + msgs ...sdk.Msg, +) { + txBytes := d.CreateTx(t, senderInfo, gas, fee, msgs...) + + result, err := d.App.CheckTx(&abci.RequestCheckTx{ + Tx: txBytes, + Type: abci.CheckTxType_New, + }) + require.NoError(t, err) + require.Equal(t, result.Code, uint32(0)) +} + +func signVoteExtension( + t *testing.T, + veBytes []byte, + height uint64, + valPrivKey cmtcrypto.PrivKey, +) []byte { + cve := cmtproto.CanonicalVoteExtension{ + Extension: veBytes, + Height: int64(height), + Round: int64(0), + ChainId: chainID, + } + + var cveBuffer bytes.Buffer + err := gogoprotoio.NewDelimitedWriter(&cveBuffer).WriteMsg(&cve) + require.NoError(t, err) + cveBytes := cveBuffer.Bytes() + extensionSig, err := valPrivKey.Sign(cveBytes) + require.NoError(t, err) + + return extensionSig +} + +func (d *BabylonAppDriver) GenerateNewBlock(t *testing.T) *abci.ResponseFinalizeBlock { + if len(d.FinalizedBlocks) == 0 { + extCommitFirsBlock := &cmttypes.ExtendedCommit{} + block1, err := d.BlockExec.CreateProposalBlock( + context.Background(), + 1, + d.LastState, + extCommitFirsBlock, + d.ValidatorAddress, + ) + require.NoError(t, err) + require.NotNil(t, block1) + + accepted, err := d.BlockExec.ProcessProposal(block1, d.LastState) + require.NoError(t, err) + require.True(t, accepted) + + block1ID := getBlockId(t, block1) + state, err := d.BlockExec.ApplyVerifiedBlock(d.LastState, block1ID, block1) + require.NoError(t, err) + require.NotNil(t, state) + + d.FinalizedBlocks = append(d.FinalizedBlocks, FinalizedBlock{ + Height: 1, + ID: block1ID, + Block: block1, + }) + d.LastState = state.Copy() + + lastResponse, err := d.StateStore.LoadFinalizeBlockResponse(1) + require.NoError(t, err) + require.NotNil(t, lastResponse) + return lastResponse + } else { + lastFinalizedBlock := d.GetLastFinalizedBlock() + + var extension []byte + + if lastFinalizedBlock.Height > 1 { + ext, err := d.BlockExec.ExtendVote( + context.Background(), + &cmttypes.Vote{ + BlockID: lastFinalizedBlock.ID, + Height: int64(lastFinalizedBlock.Height), + }, + lastFinalizedBlock.Block, + d.LastState, + ) + require.NoError(t, err) + extension = ext + } else { + extension = []byte{} + } + + extensionSig := signVoteExtension( + t, + extension, + lastFinalizedBlock.Height, + d.PrivSigner.WrappedPV.GetValPrivKey(), + ) + + // We are adding invalid signatures here as we are not validating them in + // ApplyBlock + extCommitSig := cmttypes.ExtendedCommitSig{ + CommitSig: cmttypes.CommitSig{ + BlockIDFlag: cmttypes.BlockIDFlagCommit, + ValidatorAddress: d.ValidatorAddress, + Timestamp: time.Now().Add(1 * time.Second), + Signature: []byte("test"), + }, + Extension: extension, + ExtensionSignature: extensionSig, + } + + oneValExtendedCommit := &cmttypes.ExtendedCommit{ + Height: int64(lastFinalizedBlock.Height), + Round: 0, + BlockID: lastFinalizedBlock.ID, + ExtendedSignatures: []cmttypes.ExtendedCommitSig{ + extCommitSig, + }, + } + + block1, err := d.BlockExec.CreateProposalBlock( + context.Background(), + int64(lastFinalizedBlock.Height)+1, + d.LastState, + oneValExtendedCommit, + d.ValidatorAddress, + ) + require.NoError(t, err) + require.NotNil(t, block1) + + // it is here as it is good sanity check for all babylon custom validations + accepted, err := d.BlockExec.ProcessProposal(block1, d.LastState) + require.NoError(t, err) + require.True(t, accepted) + + block1ID := getBlockId(t, block1) + state, err := d.BlockExec.ApplyVerifiedBlock(d.LastState, block1ID, block1) + require.NoError(t, err) + require.NotNil(t, state) + + d.FinalizedBlocks = append(d.FinalizedBlocks, FinalizedBlock{ + Height: lastFinalizedBlock.Height + 1, + ID: block1ID, + Block: block1, + }) + d.LastState = state.Copy() + + lastResponse, err := d.StateStore.LoadFinalizeBlockResponse(state.LastBlockHeight) + require.NoError(t, err) + require.NotNil(t, lastResponse) + return lastResponse + } +} + +func (d *BabylonAppDriver) GenerateNewBlockAssertExecutionSuccess( + t *testing.T, +) { + response := d.GenerateNewBlock(t) + + for _, tx := range response.TxResults { + require.Equal(t, tx.Code, uint32(0)) + } +} + +func (d *BabylonAppDriver) GetDriverAccountAddress() sdk.AccAddress { + return sdk.AccAddress(d.DriverAccountPrivKey.PubKey().Address()) +} + +func (d *BabylonAppDriver) GetDriverAccountSenderInfo() *senderInfo { + return &senderInfo{ + privKey: d.DriverAccountPrivKey, + sequenceNumber: d.DriverAccountSeqNr, + accountNumber: d.DriverAccountAccNr, + } +} + +func (d *BabylonAppDriver) GetBTCLCTip() (*wire.BlockHeader, uint32) { + tipInfo := d.App.BTCLightClientKeeper.GetTipInfo(d.GetContextForLastFinalizedBlock()) + return tipInfo.Header.ToBlockHeader(), tipInfo.Height +} + +func blocksWithProofsToHeaderBytes(blocks []*datagen.BlockWithProofs) []bbn.BTCHeaderBytes { + headerBytes := []bbn.BTCHeaderBytes{} + for _, block := range blocks { + headerBytes = append(headerBytes, bbn.NewBTCHeaderBytesFromBlockHeader(&block.Block.Header)) + } + return headerBytes +} + +func (d *BabylonAppDriver) ExtendBTCLcWithNEmptyBlocks( + r *rand.Rand, + t *testing.T, + n uint32, +) (*wire.BlockHeader, uint32) { + tip, _ := d.GetBTCLCTip() + blocks := datagen.GenNEmptyBlocks(r, uint64(n), tip) + headers := blocksWithProofsToHeaderBytes(blocks) + + d.SendTxWithMsgsFromDriverAccount(t, &btclighttypes.MsgInsertHeaders{ + Signer: d.GetDriverAccountAddress().String(), + Headers: headers, + }) + + newTip, newTipHeight := d.GetBTCLCTip() + return newTip, newTipHeight +} + +func (d *BabylonAppDriver) GenBlockWithTransactions( + r *rand.Rand, + t *testing.T, + txs []*wire.MsgTx, +) *datagen.BlockWithProofs { + tip, _ := d.GetBTCLCTip() + block := datagen.GenRandomBtcdBlockWithTransactions(r, txs, tip) + headers := blocksWithProofsToHeaderBytes([]*datagen.BlockWithProofs{block}) + + d.SendTxWithMsgsFromDriverAccount(t, &btclighttypes.MsgInsertHeaders{ + Signer: d.GetDriverAccountAddress().String(), + Headers: headers, + }) + + return block +} + +func (d *BabylonAppDriver) getDelegationWithStatus(t *testing.T, status bstypes.BTCDelegationStatus) []*bstypes.BTCDelegationResponse { + pagination := &query.PageRequest{} + pagination.Limit = goMath.MaxUint32 + + delegations, err := d.App.BTCStakingKeeper.BTCDelegations(d.GetContextForLastFinalizedBlock(), &bstypes.QueryBTCDelegationsRequest{ + Status: status, + Pagination: pagination, + }) + require.NoError(t, err) + return delegations.BtcDelegations +} + +func (d *BabylonAppDriver) GetAllBTCDelegations(t *testing.T) []*bstypes.BTCDelegationResponse { + return d.getDelegationWithStatus(t, bstypes.BTCDelegationStatus_ANY) +} + +func (d *BabylonAppDriver) GetVerifiedBTCDelegations(t *testing.T) []*bstypes.BTCDelegationResponse { + return d.getDelegationWithStatus(t, bstypes.BTCDelegationStatus_VERIFIED) +} + +func (d *BabylonAppDriver) GetActiveBTCDelegations(t *testing.T) []*bstypes.BTCDelegationResponse { + return d.getDelegationWithStatus(t, bstypes.BTCDelegationStatus_ACTIVE) +} + +func (d *BabylonAppDriver) GetBTCStakingParams(t *testing.T) *bstypes.Params { + params := d.App.BTCStakingKeeper.GetParams(d.GetContextForLastFinalizedBlock()) + return ¶ms +} + +func (d *BabylonAppDriver) GetEpochingParams() et.Params { + return d.App.EpochingKeeper.GetParams(d.GetContextForLastFinalizedBlock()) +} + +func (d *BabylonAppDriver) GetEpoch() *et.Epoch { + return d.App.EpochingKeeper.GetEpoch(d.GetContextForLastFinalizedBlock()) +} + +func (d *BabylonAppDriver) GetCheckpoint( + t *testing.T, + epochNumber uint64, +) *ckpttypes.RawCheckpointWithMeta { + checkpoint, err := d.App.CheckpointingKeeper.GetRawCheckpoint(d.GetContextForLastFinalizedBlock(), epochNumber) + require.NoError(t, err) + return checkpoint +} + +func (d *BabylonAppDriver) GetLastFinalizedEpoch() uint64 { + return d.App.CheckpointingKeeper.GetLastFinalizedEpoch(d.GetContextForLastFinalizedBlock()) +} + +func (d *BabylonAppDriver) GenCkptForEpoch(r *rand.Rand, t *testing.T, epochNumber uint64) { + ckptWithMeta := d.GetCheckpoint(t, epochNumber) + subAddress := d.GetDriverAccountAddress() + subAddressBytes := subAddress.Bytes() + + rawCkpt, err := ckpttypes.FromRawCkptToBTCCkpt(ckptWithMeta.Ckpt, subAddressBytes) + require.NoError(t, err) + + tagBytes, err := hex.DecodeString(initialization.BabylonOpReturnTag) + require.NoError(t, err) + + data1, data2 := btctxformatter.MustEncodeCheckpointData( + btctxformatter.BabylonTag(tagBytes), + btctxformatter.CurrentVersion, + rawCkpt, + ) + + tx1 := datagen.CreatOpReturnTransaction(r, data1) + tx2 := datagen.CreatOpReturnTransaction(r, data2) + + blockWithProofs := d.GenBlockWithTransactions(r, t, []*wire.MsgTx{tx1, tx2}) + + msg := btckckpttypes.MsgInsertBTCSpvProof{ + Submitter: subAddress.String(), + Proofs: blockWithProofs.Proofs[1:], + } + + d.SendTxWithMsgsFromDriverAccount(t, &msg) +} + +func (d *BabylonAppDriver) FinializeCkptForEpoch(r *rand.Rand, t *testing.T, epochNumber uint64) { + lastFinalizedEpoch := d.GetLastFinalizedEpoch() + require.Equal(t, lastFinalizedEpoch+1, epochNumber) + + btckptParams := d.GetBTCCkptParams(t) + d.GenCkptForEpoch(r, t, epochNumber) + + _, _ = d.ExtendBTCLcWithNEmptyBlocks(r, t, btckptParams.CheckpointFinalizationTimeout) + + lastFinalizedEpoch = d.GetLastFinalizedEpoch() + require.Equal(t, lastFinalizedEpoch, epochNumber) +} + +func (d *BabylonAppDriver) GetBTCCkptParams(t *testing.T) btckckpttypes.Params { + return d.App.BtcCheckpointKeeper.GetParams(d.GetContextForLastFinalizedBlock()) +} + +// SendTxWithMsgsFromDriverAccount sends tx with msgs from driver account and asserts that +// SendTxWithMsgsFromDriverAccount sends tx with msgs from driver account and asserts that +// execution was successful. It assumes that there will only be one tx in the block. +func (d *BabylonAppDriver) SendTxWithMsgsFromDriverAccount( + t *testing.T, + msgs ...sdk.Msg) { + d.SendTxWithMessagesSuccess( + t, + d.GetDriverAccountSenderInfo(), + defaultGasLimit, + defaultFeeCoin, + msgs..., + ) + + result := d.GenerateNewBlock(t) + + for _, rs := range result.TxResults { + // our checkpoint transactions have 0 gas wanted, skip them to avoid confusing the + // tests + if rs.GasWanted == 0 { + continue + } + + // all executions should be successful + require.Equal(t, rs.Code, uint32(0)) + } + + d.DriverAccountSeqNr++ +} + +type BlockReplayer struct { + BlockExec *sm.BlockExecutor + LastState sm.State +} + +func NewBlockReplayer(t *testing.T, nodeDir string) *BlockReplayer { + _, doc := getGenDoc(t, nodeDir) + + genDoc, err := doc.ToGenesisDoc() + require.NoError(t, err) + + state, err := sm.MakeGenesisState(genDoc) + require.NoError(t, err) + + stateStore := sm.NewStore(dbmc.NewMemDB(), sm.StoreOptions{ + DiscardABCIResponses: false, + }) + + if err := stateStore.Save(state); err != nil { + panic(err) + } + + signer, err := appkeepers.InitPrivSigner(nodeDir) + require.NoError(t, err) + require.NotNil(t, signer) + signerValAddress := signer.WrappedPV.GetAddress() + fmt.Printf("signer val address: %s\n", signerValAddress.String()) + + appOptions := NewAppOptionsWithFlagHome(nodeDir) + baseAppOptions := server.DefaultBaseappOptions(appOptions) + tmpApp := babylonApp.NewBabylonApp( + log.NewNopLogger(), + dbm.NewMemDB(), + nil, + true, + map[int64]bool{}, + 0, + signer, + appOptions, + babylonApp.EmptyWasmOpts, + baseAppOptions..., + ) + + cmtApp := server.NewCometABCIWrapper(tmpApp) + procxyCons := proxy.NewMultiAppConn( + proxy.NewLocalClientCreator(cmtApp), + proxy.NopMetrics(), + ) + err = procxyCons.Start() + require.NoError(t, err) + + blockStore := store.NewBlockStore(dbmc.NewMemDB()) + + blockExec := sm.NewBlockExecutor( + stateStore, + cometlog.TestingLogger(), + procxyCons.Consensus(), + &mempool.NopMempool{}, + sm.EmptyEvidencePool{}, + blockStore, + ) + require.NotNil(t, blockExec) + + hs := cs.NewHandshaker( + stateStore, + state, + blockStore, + genDoc, + ) + + require.NotNil(t, hs) + hs.SetLogger(cometlog.TestingLogger()) + err = hs.Handshake(procxyCons) + require.NoError(t, err) + + state, err = stateStore.Load() + require.NoError(t, err) + require.NotNil(t, state) + + return &BlockReplayer{ + BlockExec: blockExec, + LastState: state, + } +} + +func (r *BlockReplayer) ReplayBlocks(t *testing.T, blocks []FinalizedBlock) { + for _, block := range blocks { + blockID := getBlockId(t, block.Block) + state, err := r.BlockExec.ApplyVerifiedBlock(r.LastState, blockID, block.Block) + require.NoError(t, err) + require.NotNil(t, state) + r.LastState = state.Copy() + } +} + +type FinalityProviderInfo struct { + MsgCreateFinalityProvider *bstypes.MsgCreateFinalityProvider + BTCPrivateKey *btcec.PrivateKey + BabylonAddress sdk.AccAddress +} + +func GenerateNFinalityProviders( + r *rand.Rand, + t *testing.T, + n uint32, + senderAddress sdk.AccAddress, +) []*FinalityProviderInfo { + var infos []*FinalityProviderInfo + for i := uint32(0); i < n; i++ { + prv, _, err := datagen.GenRandomBTCKeyPair(r) + require.NoError(t, err) + msg, err := datagen.GenRandomCreateFinalityProviderMsgWithBTCBabylonSKs( + r, + prv, + senderAddress, + ) + require.NoError(t, err) + + infos = append(infos, &FinalityProviderInfo{ + MsgCreateFinalityProvider: msg, + BTCPrivateKey: prv, + BabylonAddress: senderAddress, + }) + } + + return infos +} + +func GenerateNBTCDelegationsForFinalityProvider( + r *rand.Rand, + t *testing.T, + n uint32, + senderAddress sdk.AccAddress, + fpInfo *FinalityProviderInfo, + params *bstypes.Params, +) []*datagen.CreateDelegationInfo { + var delInfos []*datagen.CreateDelegationInfo + + for i := uint32(0); i < n; i++ { + // TODO this slow due the key generation + stakerPrv, _, err := datagen.GenRandomBTCKeyPair(r) + require.NoError(t, err) + delInfo := datagen.GenRandomMsgCreateBtcDelegationAndMsgAddCovenantSignatures( + r, + t, + BtcParams, + senderAddress, + []bbn.BIP340PubKey{*fpInfo.MsgCreateFinalityProvider.BtcPk}, + stakerPrv, + covenantSKs, + params, + ) + delInfos = append(delInfos, delInfo) + } + + return delInfos +} + +func DelegationInfosToCreateBTCDelegationMsgs( + delInfos []*datagen.CreateDelegationInfo, +) []sdk.Msg { + msgs := []sdk.Msg{} + for _, delInfo := range delInfos { + msgs = append(msgs, delInfo.MsgCreateBTCDelegation) + } + return msgs +} + +func DelegationInfosToBTCTx( + delInfos []*datagen.CreateDelegationInfo, +) []*wire.MsgTx { + txs := []*wire.MsgTx{} + for _, delInfo := range delInfos { + txs = append(txs, delInfo.StakingTx) + } + return txs +} diff --git a/testutil/datagen/btc_blockchain.go b/testutil/datagen/btc_blockchain.go index 3e3ee42ec..64c9c1fe6 100644 --- a/testutil/datagen/btc_blockchain.go +++ b/testutil/datagen/btc_blockchain.go @@ -1,10 +1,14 @@ package datagen import ( + "bytes" "math/big" "math/rand" + "time" "github.com/babylonlabs-io/babylon/btctxformatter" + bbn "github.com/babylonlabs-io/babylon/types" + btcctypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -56,6 +60,97 @@ func GenRandomBtcdBlock(r *rand.Rand, numBabylonTxs int, prevHash *chainhash.Has return block, rawCkpt } +// BTC block with proofs of each tx. Index of txs in the block is the same as the index of proofs. +type BlockWithProofs struct { + Block *wire.MsgBlock + Proofs []*btcctypes.BTCSpvProof +} + +func GenRandomBtcdBlockWithTransactions( + r *rand.Rand, + txs []*wire.MsgTx, + prevHeader *wire.BlockHeader, +) *BlockWithProofs { + // we allways add coinbase tx to the block, to make sure merkle root is different + // every time + coinbaseTx := createCoinbaseTx(r.Int31(), &chaincfg.SimNetParams) + msgTxs := []*wire.MsgTx{coinbaseTx} + msgTxs = append(msgTxs, txs...) + + // calculate correct Merkle root + merkleRoot := calcMerkleRoot(msgTxs) + // don't apply any difficulty + difficulty, _ := new(big.Int).SetString("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) + workBits := blockchain.BigToCompact(difficulty) + + header := GenRandomBtcdHeader(r) + header.MerkleRoot = merkleRoot + header.Bits = workBits + if prevHeader != nil { + prevHash := prevHeader.BlockHash() + header.PrevBlock = prevHash + header.Timestamp = prevHeader.Timestamp.Add(time.Minute * 10) + } + // find a nonce that satisfies difficulty + SolveBlock(header) + + var txBytes [][]byte + for _, tx := range msgTxs { + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + _ = tx.Serialize(buf) + txBytes = append(txBytes, buf.Bytes()) + } + + var proofs []*btcctypes.BTCSpvProof + + for i, _ := range msgTxs { + headerBytes := bbn.NewBTCHeaderBytesFromBlockHeader(header) + proof, err := btcctypes.SpvProofFromHeaderAndTransactions(&headerBytes, txBytes, uint(i)) + if err != nil { + panic(err) + } + + proofs = append(proofs, proof) + } + + block := &wire.MsgBlock{ + Header: *header, + Transactions: msgTxs, + } + return &BlockWithProofs{ + Block: block, + Proofs: proofs, + } +} + +func GenChainFromListsOfTransactions( + r *rand.Rand, + txs [][]*wire.MsgTx, + initHeader *wire.BlockHeader) []*BlockWithProofs { + if initHeader == nil { + panic("initHeader is required") + } + + if len(txs) == 0 { + panic("txs is required") + } + + var parentHeader *wire.BlockHeader = initHeader + var createdBlocks []*BlockWithProofs = []*BlockWithProofs{} + for _, txs := range txs { + msgBlock := GenRandomBtcdBlockWithTransactions(r, txs, parentHeader) + parentHeader = &msgBlock.Block.Header + createdBlocks = append(createdBlocks, msgBlock) + } + + return createdBlocks +} + +func GenNEmptyBlocks(r *rand.Rand, n uint64, prevHeader *wire.BlockHeader) []*BlockWithProofs { + var txs [][]*wire.MsgTx = make([][]*wire.MsgTx, n) + return GenChainFromListsOfTransactions(r, txs, prevHeader) +} + // GenRandomBtcdBlockchainWithBabylonTx generates a blockchain of `n` blocks, in which each block has some probability of including some Babylon txs // Specifically, each block // - has `oneTxThreshold` probability of including 1 Babylon tx that does not has any match diff --git a/testutil/datagen/btcstaking.go b/testutil/datagen/btcstaking.go index bd2eef8ff..a39da55c0 100644 --- a/testutil/datagen/btcstaking.go +++ b/testutil/datagen/btcstaking.go @@ -56,7 +56,11 @@ func GenRandomDescription(r *rand.Rand) *stakingtypes.Description { return &stakingtypes.Description{Moniker: GenRandomHexStr(r, 10)} } -func GenRandomFinalityProviderWithBTCBabylonSKs(r *rand.Rand, btcSK *btcec.PrivateKey, fpAddr sdk.AccAddress) (*bstypes.FinalityProvider, error) { +func GenRandomFinalityProviderWithBTCBabylonSKs( + r *rand.Rand, + btcSK *btcec.PrivateKey, + fpAddr sdk.AccAddress, +) (*bstypes.FinalityProvider, error) { // commission commission := GenRandomCommission(r) // description @@ -78,6 +82,24 @@ func GenRandomFinalityProviderWithBTCBabylonSKs(r *rand.Rand, btcSK *btcec.Priva }, nil } +func GenRandomCreateFinalityProviderMsgWithBTCBabylonSKs( + r *rand.Rand, + btcSK *btcec.PrivateKey, + fpAddr sdk.AccAddress, +) (*bstypes.MsgCreateFinalityProvider, error) { + fp, err := GenRandomFinalityProviderWithBTCBabylonSKs(r, btcSK, fpAddr) + if err != nil { + return nil, err + } + return &bstypes.MsgCreateFinalityProvider{ + Addr: fp.Addr, + Description: fp.Description, + Commission: fp.Commission, + BtcPk: fp.BtcPk, + Pop: fp.Pop, + }, nil +} + // TODO: accommodate presign unbonding flow func GenRandomBTCDelegation( r *rand.Rand, @@ -221,6 +243,174 @@ func GenRandomBTCDelegation( return del, nil } +type CreateDelegationInfo struct { + MsgCreateBTCDelegation *bstypes.MsgCreateBTCDelegation + MsgAddCovenantSigs []*bstypes.MsgAddCovenantSigs + StakingTxHash string + StakingTx *wire.MsgTx +} + +// GenRandomMsgCreateBtcDelegation generates a random MsgCreateBTCDelegation message +// valid for the given parameters. +func GenRandomMsgCreateBtcDelegationAndMsgAddCovenantSignatures( + r *rand.Rand, + t *testing.T, + btcNet *chaincfg.Params, + stakerAddr sdk.AccAddress, + fpBTCPKs []bbn.BIP340PubKey, + delSK *btcec.PrivateKey, + covenantSKs []*btcec.PrivateKey, + params *bstypes.Params, +) *CreateDelegationInfo { + require.Positive(t, params.CovenantQuorum) + require.Positive(t, len(fpBTCPKs)) + require.Positive(t, len(covenantSKs)) + require.Equal(t, len(params.CovenantPks), len(covenantSKs)) + + delPK := delSK.PubKey() + + delBTCPK := bbn.NewBIP340PubKeyFromBTCPK(delPK) + // list of finality provider PKs + fpPKs, err := bbn.NewBTCPKsFromBIP340PKs(fpBTCPKs) + require.NoError(t, err) + var covenantPks []*btcec.PublicKey + for _, sk := range covenantSKs { + covenantPks = append(covenantPks, sk.PubKey()) + } + + stakingTime := RandomInRange( + r, + int(params.MinStakingTimeBlocks), + int(params.MaxStakingTimeBlocks), + ) + + totalSat := RandomInRange( + r, + // add 10000 just in case + int(params.MinStakingValueSat)+10000, + int(params.MaxStakingValueSat), + ) + + // staking/slashing tx + stakingSlashingInfo := GenBTCStakingSlashingInfo( + r, + t, + btcNet, + delSK, + fpPKs, + covenantPks, + params.CovenantQuorum, + uint16(stakingTime), + int64(totalSat), + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + ) + + slashingPathSpendInfo, err := stakingSlashingInfo.StakingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + // delegator pre-signs slashing tx + delegatorSig, err := stakingSlashingInfo.SlashingTx.Sign( + stakingSlashingInfo.StakingTx, + StakingOutIdx, + slashingPathSpendInfo.GetPkScriptPath(), + delSK, + ) + require.NoError(t, err) + + serializedStakingTx, err := bbn.SerializeBTCTx(stakingSlashingInfo.StakingTx) + require.NoError(t, err) + + stkTxHash := stakingSlashingInfo.StakingTx.TxHash() + unbondingValue := uint64(totalSat) - uint64(params.UnbondingFeeSat) + + unbondingSlashingInfo := GenBTCUnbondingSlashingInfo( + r, + t, + btcNet, + delSK, + fpPKs, + covenantPks, + params.CovenantQuorum, + wire.NewOutPoint(&stkTxHash, StakingOutIdx), + uint16(params.UnbondingTimeBlocks), + int64(unbondingValue), + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + ) + + unbondingTxBytes, err := bbn.SerializeBTCTx(unbondingSlashingInfo.UnbondingTx) + require.NoError(t, err) + delSlashingTxSig, err := unbondingSlashingInfo.GenDelSlashingTxSig(delSK) + require.NoError(t, err) + + pop, err := bstypes.NewPoPBTC(sdk.MustAccAddressFromBech32(stakerAddr.String()), delSK) + require.NoError(t, err) + + msg := &bstypes.MsgCreateBTCDelegation{ + StakerAddr: stakerAddr.String(), + Pop: pop, + BtcPk: delBTCPK, + FpBtcPkList: fpBTCPKs, + StakingTime: uint32(stakingTime), + StakingValue: int64(totalSat), + StakingTx: serializedStakingTx, + // By default it is nil it is up to the caller to set it + StakingTxInclusionProof: nil, + SlashingTx: stakingSlashingInfo.SlashingTx, + DelegatorSlashingSig: delegatorSig, + UnbondingValue: int64(unbondingValue), + UnbondingTime: params.UnbondingTimeBlocks, + UnbondingTx: unbondingTxBytes, + UnbondingSlashingTx: unbondingSlashingInfo.SlashingTx, + DelegatorUnbondingSlashingSig: delSlashingTxSig, + } + + // covenant pre-signs slashing tx for staking tx + covenantSigs, err := GenCovenantAdaptorSigs( + covenantSKs, + fpPKs, + stakingSlashingInfo.StakingTx, + slashingPathSpendInfo.GetPkScriptPath(), + stakingSlashingInfo.SlashingTx, + ) + require.NoError(t, err) + + // covenant pre-signs slashing tx and unbonding tx + unbondingPathSpendInfo, err := stakingSlashingInfo.StakingInfo.UnbondingPathSpendInfo() + require.NoError(t, err) + covUnbondingSlashingSigs, covUnbondingSigs, err := unbondingSlashingInfo.GenCovenantSigs( + covenantSKs, + fpPKs, + stakingSlashingInfo.StakingTx, + unbondingPathSpendInfo.GetPkScriptPath(), + ) + require.NoError(t, err) + + msgs := make([]*bstypes.MsgAddCovenantSigs, len(covenantPks)) + + for i := 0; i < len(covenantPks); i++ { + msgAddCovenantSig := &bstypes.MsgAddCovenantSigs{ + Signer: stakerAddr.String(), + Pk: covenantSigs[i].CovPk, + StakingTxHash: stkTxHash.String(), + SlashingTxSigs: covenantSigs[i].AdaptorSigs, + UnbondingTxSig: covUnbondingSigs[i].Sig, + SlashingUnbondingTxSigs: covUnbondingSlashingSigs[i].AdaptorSigs, + } + msgs[i] = msgAddCovenantSig + } + + return &CreateDelegationInfo{ + MsgCreateBTCDelegation: msg, + MsgAddCovenantSigs: msgs, + StakingTxHash: stkTxHash.String(), + StakingTx: stakingSlashingInfo.StakingTx, + } +} + type TestStakingSlashingInfo struct { StakingTx *wire.MsgTx SlashingTx *bstypes.BTCSlashingTx diff --git a/testutil/datagen/datagen.go b/testutil/datagen/datagen.go index fcc096212..02fddaa01 100644 --- a/testutil/datagen/datagen.go +++ b/testutil/datagen/datagen.go @@ -49,3 +49,8 @@ func ValidHex(hexStr string, length int) bool { } return true } + +// RandomInRange returns a random integer in the range [min, max). +func RandomInRange(r *rand.Rand, min, max int) int { + return r.Intn(max-min) + min +}