From d851e3cffabad68c03938675c0fb2d7bea848ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nina=20/=20=E1=83=9C=E1=83=98=E1=83=9C=E1=83=90?= Date: Mon, 10 Jun 2024 21:15:51 +0200 Subject: [PATCH] test: consistent appHash between commits (#3513) ## Overview Fixes #2414 ### This test is split up in a few parts: - Execute transactions deterministically on v1 and save the AppHash. https://github.com/celestiaorg/celestia-app/pull/3522 ([includes genesis folder backport from utils](https://github.com/celestiaorg/celestia-app/pull/3520)) - Set up the same testApp environment on main, execute transactions and assert that the AppHash matches the one from v1 ### Verifying the AppHash: If you want to make sure that the expected AppHash is correct, you can run the [non determinism test on v1.x](https://github.com/celestiaorg/celestia-app/pull/3522) that generates it. ### Follow-ups: #3509 https://github.com/celestiaorg/celestia-app/issues/3540 --------- Co-authored-by: Rootul P --- app/test/consistent_apphash_test.go | 173 ++++++++++++++++++++++++++++ test/e2e/testnet/testnet.go | 2 +- test/util/genesis/genesis.go | 49 +++++--- test/util/test_app.go | 151 +++++++++++++++++++++--- 4 files changed, 343 insertions(+), 32 deletions(-) create mode 100644 app/test/consistent_apphash_test.go diff --git a/app/test/consistent_apphash_test.go b/app/test/consistent_apphash_test.go new file mode 100644 index 0000000000..c3133ba98c --- /dev/null +++ b/app/test/consistent_apphash_test.go @@ -0,0 +1,173 @@ +package app_test + +import ( + "fmt" + "testing" + + "github.com/celestiaorg/celestia-app/v2/app" + "github.com/celestiaorg/celestia-app/v2/app/encoding" + "github.com/celestiaorg/celestia-app/v2/pkg/appconsts" + "github.com/celestiaorg/celestia-app/v2/pkg/user" + testutil "github.com/celestiaorg/celestia-app/v2/test/util" + "github.com/celestiaorg/celestia-app/v2/test/util/blobfactory" + "github.com/celestiaorg/go-square/blob" + appns "github.com/celestiaorg/go-square/namespace" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + "github.com/tendermint/tendermint/proto/tendermint/version" +) + +type SdkTx struct { + sdkMsgs []sdk.Msg + txOptions []user.TxOption +} + +type BlobTx struct { + author string + blobs []*blob.Blob + txOptions []user.TxOption +} + +// TestConsistentAppHash executes a set of txs, generates an app hash, +// and compares it against a previously generated hash from the same set of transactions. +// App hashes across different commits should be consistent. +func TestConsistentAppHash(t *testing.T) { + // Expected app hash produced by v1.x - TODO: link to the test producing the hash + expectedAppHash := []byte{9, 208, 117, 101, 108, 61, 146, 58, 26, 190, 199, 124, 76, 178, 84, 74, 54, 159, 76, 187, 2, 169, 128, 87, 70, 78, 8, 192, 28, 144, 116, 117} + + // Initialize testApp + testApp := testutil.NewTestApp() + + enc := encoding.MakeConfig(app.ModuleEncodingRegisters...) + // Create deterministic keys + kr, pubKeys := deterministicKeyRing(enc.Codec) + + recs, err := kr.List() + require.NoError(t, err) + accountNames := make([]string, 0, len(recs)) + + // Get the name of the records + for _, rec := range recs { + accountNames = append(accountNames, rec.Name) + } + + // Apply genesis state to the app. + _, _, err = testutil.SetupDeterministicGenesisState(testApp, pubKeys, 1_000_000_000, app.DefaultInitialConsensusParams()) + require.NoError(t, err) + + // Query keyring account infos + accountInfos := queryAccountInfo(testApp, accountNames, kr) + + // Create accounts for the signer + accounts := make([]*user.Account, 0, len(accountInfos)) + for i, accountInfo := range accountInfos { + account := user.NewAccount(accountNames[i], accountInfo.AccountNum, accountInfo.Sequence) + accounts = append(accounts, account) + } + + // Create a signer with keyring accounts + signer, err := user.NewSigner(kr, enc.TxConfig, testutil.ChainID, app.DefaultInitialVersion, accounts...) + require.NoError(t, err) + + amount := sdk.NewCoins(sdk.NewCoin(app.BondDenom, sdk.NewIntFromUint64(1000))) + + // Create an SDK Tx + sdkTx := SdkTx{ + sdkMsgs: []sdk.Msg{ + banktypes.NewMsgSend(signer.Account(accountNames[0]).Address(), + signer.Account(accountNames[1]).Address(), + amount), + }, + txOptions: blobfactory.DefaultTxOpts(), + } + + // Create a Blob Tx + blobTx := BlobTx{ + author: accountNames[2], + blobs: []*blob.Blob{blob.New(fixedNamespace(), []byte{1}, appconsts.DefaultShareVersion)}, + txOptions: blobfactory.DefaultTxOpts(), + } + + // Create SDK Tx + rawSdkTx, err := signer.CreateTx(sdkTx.sdkMsgs, sdkTx.txOptions...) + require.NoError(t, err) + + // Create Blob Tx + rawBlobTx, _, err := signer.CreatePayForBlobs(blobTx.author, blobTx.blobs, blobTx.txOptions...) + require.NoError(t, err) + + // BeginBlock + header := tmproto.Header{ + Version: version.Consensus{App: 1}, + Height: testApp.LastBlockHeight() + 1, + } + testApp.BeginBlock(abci.RequestBeginBlock{Header: header}) + + // Deliver SDK Tx + resp := testApp.DeliverTx(abci.RequestDeliverTx{Tx: rawSdkTx}) + require.EqualValues(t, 0, resp.Code, resp.Log) + + // Deliver Blob Tx + blob, isBlobTx := blob.UnmarshalBlobTx(rawBlobTx) + require.True(t, isBlobTx) + resp = testApp.DeliverTx(abci.RequestDeliverTx{Tx: blob.Tx}) + require.EqualValues(t, 0, resp.Code, resp.Log) + + // EndBlock + testApp.EndBlock(abci.RequestEndBlock{Height: header.Height}) + + // Commit the state + testApp.Commit() + + // Get the app hash + appHash := testApp.LastCommitID().Hash + + // Require that the app hash is equal to the app hash produced on a different commit + require.Equal(t, expectedAppHash, appHash) +} + +// fixedNamespace returns a hardcoded namespace +func fixedNamespace() appns.Namespace { + return appns.Namespace{ + Version: 0, + ID: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 67, 154, 200, 228, 130, 74, 147, 162, 11}, + } +} + +// deterministicKeyRing returns a deterministic keyring and a list of deterministic public keys +func deterministicKeyRing(cdc codec.Codec) (keyring.Keyring, []types.PubKey) { + mnemonics := []string{ + "great myself congress genuine scale muscle view uncover pipe miracle sausage broccoli lonely swap table foam brand turtle comic gorilla firm mad grunt hazard", + "cheap job month trigger flush cactus chest juice dolphin people limit crunch curious secret object beach shield snake hunt group sketch cousin puppy fox", + "oil suffer bamboo one better attack exist dolphin relief enforce cat asset raccoon lava regret found love certain plunge grocery accuse goat together kiss", + "giraffe busy subject doll jump drama sea daring again club spend toe mind organ real liar permit refuse change opinion donkey job cricket speed", + "fee vapor thing fish fan memory negative raven cram win quantum ozone job mirror shoot sting quiz black apart funny sort cancel friend curtain", + "skin beef review pilot tooth act any alarm there only kick uniform ticket material cereal radar ethics list unlock method coral smooth street frequent", + "ecology scout core guard load oil school effort near alcohol fancy save cereal owner enforce impact sand husband trophy solve amount fish festival sell", + "used describe angle twin amateur pyramid bitter pool fluid wing erode rival wife federal curious drink battle put elbow mandate another token reveal tone", + "reason fork target chimney lift typical fine divorce mixture web robot kiwi traffic stove miss crane welcome camp bless fuel october riot pluck ordinary", + "undo logic mobile modify master force donor rose crumble forget plate job canal waste turn damp sure point deposit hazard quantum car annual churn", + "charge subway treat loop donate place loan want grief leg message siren joy road exclude match empty enforce vote meadow enlist vintage wool involve", + } + kb := keyring.NewInMemory(cdc) + pubKeys := make([]types.PubKey, len(mnemonics)) + for idx, mnemonic := range mnemonics { + rec, err := kb.NewAccount(fmt.Sprintf("account-%d", idx), mnemonic, "", "", hd.Secp256k1) + if err != nil { + panic(err) + } + pubKey, err := rec.GetPubKey() + if err != nil { + panic(err) + } + pubKeys[idx] = pubKey + } + return kb, pubKeys +} diff --git a/test/e2e/testnet/testnet.go b/test/e2e/testnet/testnet.go index 1813392d85..fe6ad5325a 100644 --- a/test/e2e/testnet/testnet.go +++ b/test/e2e/testnet/testnet.go @@ -75,7 +75,7 @@ func (t *Testnet) CreateGenesisNode(version string, selfDelegation, upgradeHeigh if err != nil { return err } - if err := t.genesis.AddValidator(node.GenesisValidator()); err != nil { + if err := t.genesis.NewValidator(node.GenesisValidator()); err != nil { return err } t.nodes = append(t.nodes, node) diff --git a/test/util/genesis/genesis.go b/test/util/genesis/genesis.go index 2292445136..ab2e8a93eb 100644 --- a/test/util/genesis/genesis.go +++ b/test/util/genesis/genesis.go @@ -44,6 +44,21 @@ type Genesis struct { genOps []Modifier } +// Accounts getter +func (g *Genesis) Accounts() []Account { + return g.accounts +} + +// Keyring getter +func (g *Genesis) Keyring() keyring.Keyring { + return g.kr +} + +// Validators getter +func (g *Genesis) Validators() []Validator { + return g.validators +} + // NewDefaultGenesis creates a new default genesis with no accounts or validators. func NewDefaultGenesis() *Genesis { ecfg := encoding.MakeConfig(app.ModuleBasics) @@ -58,29 +73,34 @@ func NewDefaultGenesis() *Genesis { return g } +// WithModifier adds a genesis modifier to the genesis. func (g *Genesis) WithModifiers(ops ...Modifier) *Genesis { g.genOps = append(g.genOps, ops...) return g } +// WithConsensusParams sets the consensus parameters of the genesis. func (g *Genesis) WithConsensusParams(params *tmproto.ConsensusParams) *Genesis { g.ConsensusParams = params return g } +// WithChainID sets the chain ID of the genesis. func (g *Genesis) WithChainID(chainID string) *Genesis { g.ChainID = chainID return g } +// WithGenesisTime sets the genesis time of the genesis. func (g *Genesis) WithGenesisTime(genesisTime time.Time) *Genesis { g.GenesisTime = genesisTime return g } +// WithAccounts adds the given validators to the genesis. func (g *Genesis) WithValidators(vals ...Validator) *Genesis { for _, val := range vals { - err := g.AddValidator(val) + err := g.NewValidator(val) if err != nil { panic(err) } @@ -100,6 +120,7 @@ func (g *Genesis) WithKeyringAccounts(accs ...KeyringAccount) *Genesis { return g } +// AddAccount adds an existing account to the genesis. func (g *Genesis) AddAccount(account Account) error { for _, acc := range g.accounts { if bytes.Equal(acc.PubKey.Bytes(), account.PubKey.Bytes()) { @@ -110,6 +131,7 @@ func (g *Genesis) AddAccount(account Account) error { return nil } +// NewAccount creates a new account and adds it to the genesis. func (g *Genesis) NewAccount(acc KeyringAccount) error { if err := acc.ValidateBasic(); err != nil { return err @@ -139,16 +161,12 @@ func (g *Genesis) NewAccount(acc KeyringAccount) error { return nil } +// AddValidator verifies and adds a given validator to the genesis. func (g *Genesis) AddValidator(val Validator) error { if err := val.ValidateBasic(); err != nil { return err } - // Add the validator's genesis account - if err := g.NewAccount(val.KeyringAccount); err != nil { - return err - } - // Add the validator's genesis transaction gentx, err := val.GenTx(g.ecfg, g.kr, g.ChainID) if err != nil { @@ -161,10 +179,17 @@ func (g *Genesis) AddValidator(val Validator) error { return nil } -func (g *Genesis) Accounts() []Account { - return g.accounts +// Creates a new validator account and adds it to the genesis. +func (g *Genesis) NewValidator(val Validator) error { + // Add the validator's genesis account + if err := g.NewAccount(val.KeyringAccount); err != nil { + return err + } + + return g.AddValidator(val) } +// Export returns the genesis document of the network. func (g *Genesis) Export() (*coretypes.GenesisDoc, error) { gentxs := make([]json.RawMessage, 0, len(g.genTxs)) for _, genTx := range g.genTxs { @@ -186,14 +211,6 @@ func (g *Genesis) Export() (*coretypes.GenesisDoc, error) { ) } -func (g *Genesis) Keyring() keyring.Keyring { - return g.kr -} - -func (g *Genesis) Validators() []Validator { - return g.validators -} - // Validator returns the validator at the given index. False is returned if the // index is out of bounds. func (g *Genesis) Validator(i int) (Validator, bool) { diff --git a/test/util/test_app.go b/test/util/test_app.go index 765b53d326..1fc02f8d06 100644 --- a/test/util/test_app.go +++ b/test/util/test_app.go @@ -11,22 +11,25 @@ import ( "github.com/celestiaorg/celestia-app/v2/pkg/appconsts" v1 "github.com/celestiaorg/celestia-app/v2/pkg/appconsts/v1" v2 "github.com/celestiaorg/celestia-app/v2/pkg/appconsts/v2" + "github.com/celestiaorg/celestia-app/v2/test/util/genesis" "github.com/celestiaorg/celestia-app/v2/test/util/testfactory" "github.com/celestiaorg/celestia-app/v2/test/util/testnode" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/simapp" - "github.com/cosmos/cosmos-sdk/testutil/mock" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/spf13/cast" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/libs/log" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmversion "github.com/tendermint/tendermint/proto/tendermint/version" @@ -73,8 +76,8 @@ func SetupTestAppWithGenesisValSet(cparams *tmproto.ConsensusParams, genAccounts return testApp, kr } -func NewTestAppWithGenesisSet(cparams *tmproto.ConsensusParams, genAccounts ...string) (*app.App, *tmtypes.ValidatorSet, keyring.Keyring) { - // var cache sdk.MultiStorePersistentCache +// NewTestApp creates a new app instance with an empty memDB and a no-op logger. +func NewTestApp() *app.App { // EmptyAppOptions is a stub implementing AppOptions emptyOpts := EmptyAppOptions{} // var anteOpt = func(bapp *baseapp.BaseApp) { bapp.SetAnteHandler(nil) } @@ -82,14 +85,90 @@ func NewTestAppWithGenesisSet(cparams *tmproto.ConsensusParams, genAccounts ...s encCfg := encoding.MakeConfig(app.ModuleEncodingRegisters...) - testApp := app.New( + return app.New( log.NewNopLogger(), db, nil, cast.ToUint(emptyOpts.Get(server.FlagInvCheckPeriod)), encCfg, 0, emptyOpts, ) +} + +// SetupDeterministicGenesisState sets genesis on initialized testApp with the provided arguments. +func SetupDeterministicGenesisState(testApp *app.App, pubKeys []cryptotypes.PubKey, balance int64, cparams *tmproto.ConsensusParams) (keyring.Keyring, []genesis.Account, error) { + // create genesis + gen := genesis.NewDefaultGenesis(). + WithChainID(ChainID). + WithConsensusParams(cparams). + WithGenesisTime(time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC).UTC()) + + // add accounts to genesis + for _, pk := range pubKeys { + err := gen.AddAccount(genesis.Account{ + PubKey: pk, + Balance: balance, + }) + if err != nil { + return nil, nil, err + } + } + + // add validator to genesis + err := AddDeterministicValidatorToGenesis(gen) + if err != nil { + return nil, nil, fmt.Errorf("failed to add validator: %w", err) + } + + genDoc, err := gen.Export() + if err != nil { + return nil, nil, fmt.Errorf("failed to export genesis doc: %w", err) + } + + // initialise test app against genesis + testApp.Info(abci.RequestInfo{}) + + abciParams := &abci.ConsensusParams{ + Block: &abci.BlockParams{ + // choose some value large enough to not bottleneck the max square + // size + MaxBytes: int64(appconsts.DefaultSquareSizeUpperBound*appconsts.DefaultSquareSizeUpperBound) * appconsts.ContinuationSparseShareContentSize, + MaxGas: cparams.Block.MaxGas, + }, + Evidence: &cparams.Evidence, + Validator: &cparams.Validator, + Version: &cparams.Version, + } + + // init chain will set the validator set and initialize the genesis accounts + testApp.InitChain( + abci.RequestInitChain{ + Time: gen.GenesisTime, + Validators: []abci.ValidatorUpdate{}, + ConsensusParams: abciParams, + AppStateBytes: genDoc.AppState, + ChainId: genDoc.ChainID, + }, + ) + // commit genesis changes + testApp.Commit() + testApp.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{ + ChainID: ChainID, + Height: testApp.LastBlockHeight() + 1, + AppHash: testApp.LastCommitID().Hash, + ValidatorsHash: genDoc.ValidatorHash(), + NextValidatorsHash: genDoc.ValidatorHash(), + Version: tmversion.Consensus{ + App: cparams.Version.AppVersion, + }, + }}) + + return gen.Keyring(), gen.Accounts(), nil +} + +// NewTestAppWithGenesisSet initializes a new app with genesis accounts and returns the testApp, validator set and keyring. +func NewTestAppWithGenesisSet(cparams *tmproto.ConsensusParams, genAccounts ...string) (*app.App, *tmtypes.ValidatorSet, keyring.Keyring) { + testApp := NewTestApp() genesisState, valSet, kr := GenesisStateWithSingleValidator(testApp, genAccounts...) stateBytes, err := json.MarshalIndent(genesisState, "", " ") @@ -111,7 +190,7 @@ func NewTestAppWithGenesisSet(cparams *tmproto.ConsensusParams, genAccounts ...s genesisTime := time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC).UTC() - _ = testApp.Info(abci.RequestInfo{}) + testApp.Info(abci.RequestInfo{}) // init chain will set the validator set and initialize the genesis accounts testApp.InitChain( @@ -126,6 +205,48 @@ func NewTestAppWithGenesisSet(cparams *tmproto.ConsensusParams, genAccounts ...s return testApp, valSet, kr } +// AddDeterministicValidatorToGenesis adds a single deterministic validator to the genesis. +func AddDeterministicValidatorToGenesis(g *genesis.Genesis) error { + // hardcoded keys for deterministic account creation + mnemo := "body world north giggle crop reduce height copper damp next verify orphan lens loan adjust inform utility theory now ranch motion opinion crowd fun" + consensusKey := ed25519.GenPrivKeyFromSecret([]byte("12345678901234567890123456389012")) + networkKey := ed25519.GenPrivKeyFromSecret([]byte("12345678901234567890123456786012")) + + val := genesis.Validator{ + KeyringAccount: genesis.KeyringAccount{ + Name: "validator1", + InitialTokens: 1_000_000_000, + }, + Stake: 1_000_000, + ConsensusKey: consensusKey, + NetworkKey: networkKey, + } + + // initialize the validator's genesis account in the keyring + rec, err := g.Keyring().NewAccount(val.Name, mnemo, "", "", hd.Secp256k1) + if err != nil { + return fmt.Errorf("failed to create account: %w", err) + } + + validatorPubKey, err := rec.GetPubKey() + if err != nil { + return fmt.Errorf("failed to get pubkey: %w", err) + } + + // make account from keyring account + account := genesis.Account{ + PubKey: validatorPubKey, + Balance: val.KeyringAccount.InitialTokens, + } + + // add the validator's account to the genesis + if err := g.AddAccount(account); err != nil { + return fmt.Errorf("failed to add account: %w", err) + } + + return g.AddValidator(val) +} + // AddAccount mimics the cli addAccount command, providing an // account with an allocation of to "token" and "tia" tokens in the genesis // state @@ -192,21 +313,20 @@ func AddAccount(addr sdk.AccAddress, appState app.GenesisState, cdc codec.Codec) // GenesisStateWithSingleValidator initializes GenesisState with a single // validator and genesis accounts that also act as delegators. func GenesisStateWithSingleValidator(testApp *app.App, genAccounts ...string) (app.GenesisState, *tmtypes.ValidatorSet, keyring.Keyring) { - privVal := mock.NewPV() - pubKey, err := privVal.GetPubKey() - if err != nil { - panic(err) - } - // create validator set with single validator - validator := tmtypes.NewValidator(pubKey, 1) + validatorPubKey := ed25519.PubKey([]byte("12345678901234567890123456789012")) + validator := tmtypes.NewValidator(validatorPubKey, 1) valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{validator}) - // generate genesis account - senderPrivKey := secp256k1.GenPrivKey() - accs := make([]authtypes.GenesisAccount, 0, len(genAccounts)+1) + // generate sender account + senderPrivKey := secp256k1.GenPrivKeyFromSecret([]byte("09876543210987654321098765432109")) acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), 0, 0) + + // append sender account to genesis accounts + accs := make([]authtypes.GenesisAccount, 0, len(genAccounts)+1) accs = append(accs, acc) + + // genesis accounts and sender account balances balances := make([]banktypes.Balance, 0, len(genAccounts)+1) balances = append(balances, banktypes.Balance{ Address: acc.GetAddress().String(), @@ -214,6 +334,7 @@ func GenesisStateWithSingleValidator(testApp *app.App, genAccounts ...string) (a }) kr, fundedBankAccs, fundedAuthAccs := testnode.FundKeyringAccounts(genAccounts...) + accs = append(accs, fundedAuthAccs...) balances = append(balances, fundedBankAccs...)