diff --git a/persistence/context.go b/persistence/context.go index b8f22632eb..7effdd1943 100644 --- a/persistence/context.go +++ b/persistence/context.go @@ -81,6 +81,11 @@ func (p *PostgresContext) Commit(proposerAddr, quorumCert []byte) error { return err } + // Commit the tree store updates. This is the grossness I'm talking about. + if err := p.stateTrees.Commit(); err != nil { + return err + } + // Commit the SQL transaction ctx := context.TODO() if err := p.tx.Commit(ctx); err != nil { diff --git a/persistence/test/state_test.go b/persistence/test/state_test.go index d7a7555cde..e950e59b10 100644 --- a/persistence/test/state_test.go +++ b/persistence/test/state_test.go @@ -187,8 +187,103 @@ func TestStateHash_ReplayingRandomTransactionsIsDeterministic(t *testing.T) { } } +func TestStateHash_TreeUpdatesRollback(t *testing.T) { + height := int64(0) + db := NewTestPostgresContext(t, height) + + apps, err := db.GetAllApps(height) + require.NoError(t, err) + app := apps[0] + + addrBz, err := hex.DecodeString(app.GetAddress()) + require.NoError(t, err) + require.NotEmpty(t, addrBz) + + hash1, err := db.ComputeStateHash() + require.NoError(t, err) + + err = db.NewSavePoint() + require.NoError(t, err) + + // Update the app's stake to trigger a change when ComputeStateHash is called + stakeAmount := 42 + stakeAmount += 1 // change the stake amount + stakeAmountStr := strconv.Itoa(stakeAmount) + err = db.SetAppStakeAmount(addrBz, stakeAmountStr) + require.NoError(t, err) + + txBz := []byte("a tx, i am, which set the app stake amount to " + stakeAmountStr) + idxTx := &coreTypes.IndexedTransaction{ + Tx: txBz, + Height: height, + Index: 0, + ResultCode: 0, + Error: "TODO", + SignerAddr: "TODO", + RecipientAddr: "TODO", + MessageType: "TODO", + } + + err = db.IndexTransaction(idxTx) + require.NoError(t, err) + + hash2, err := db.ComputeStateHash() + require.NoError(t, err) + require.NotEmpty(t, hash2) + + err = db.RollbackToSavePoint() + require.NoError(t, err) + + hash3, err := db.ComputeStateHash() + require.NoError(t, err) + require.Equal(t, hash3, hash1) +} + func TestStateHash_TreeUpdatesAreIdempotent(t *testing.T) { - // ADDTEST(#361): Create an issue dedicated to increasing the test coverage for state hashes + height := int64(0) + db := NewTestPostgresContext(t, height) + + apps, err := db.GetAllApps(height) + require.NoError(t, err) + app := apps[0] + + addrBz, err := hex.DecodeString(app.GetAddress()) + require.NoError(t, err) + require.NotEmpty(t, addrBz) + + err = db.NewSavePoint() + require.NoError(t, err) + + // Update the app's stake + stakeAmount := 42 + stakeAmount += 1 // change the stake amount + stakeAmountStr := strconv.Itoa(stakeAmount) + err = db.SetAppStakeAmount(addrBz, stakeAmountStr) + require.NoError(t, err) + + txBz := []byte("a tx, i am, which set the app stake amount to " + stakeAmountStr) + idxTx := &coreTypes.IndexedTransaction{ + Tx: txBz, + Height: height, + Index: 0, + ResultCode: 0, + Error: "TODO", + SignerAddr: "TODO", + RecipientAddr: "TODO", + MessageType: "TODO", + } + + err = db.IndexTransaction(idxTx) + require.NoError(t, err) + + hash1, err := db.ComputeStateHash() + require.NoError(t, err) + require.NotEmpty(t, hash1) + + hash2, err := db.ComputeStateHash() + require.NoError(t, err) + require.NotEmpty(t, hash1) + require.Equal(t, hash1, hash2) } func TestStateHash_TreeUpdatesNegativeTestCase(t *testing.T) { diff --git a/persistence/trees/atomic_test.go b/persistence/trees/atomic_test.go new file mode 100644 index 0000000000..ff94d1ab67 --- /dev/null +++ b/persistence/trees/atomic_test.go @@ -0,0 +1,21 @@ +package trees + +import ( + "testing" + + "github.com/golang/mock/gomock" + mockModules "github.com/pokt-network/pocket/shared/modules/mocks" + "github.com/stretchr/testify/require" +) + +func TestTreeStore_AtomicUpdates(t *testing.T) { + ctrl := gomock.NewController(t) + mockBus := mockModules.NewMockBus(ctrl) + ts := &treeStore{ + bus: mockBus, + txi: mockBus.GetPersistenceModule().GetTxIndexer(), + treeStoreDir: ":memory:", + } + ts.setupTrees() + require.NotEmpty(t, ts) +} diff --git a/persistence/trees/module.go b/persistence/trees/module.go index c0f3afa039..f0e70cf65d 100644 --- a/persistence/trees/module.go +++ b/persistence/trees/module.go @@ -9,10 +9,10 @@ import ( "github.com/pokt-network/smt" ) -var _ modules.Module = &treeStore{} +var _ modules.Module = &TreeStore{} -func (*treeStore) Create(bus modules.Bus, options ...modules.ModuleOption) (modules.Module, error) { - m := &treeStore{} +func (*TreeStore) Create(bus modules.Bus, options ...modules.ModuleOption) (modules.Module, error) { + m := &TreeStore{} bus.RegisterModule(m) @@ -20,8 +20,8 @@ func (*treeStore) Create(bus modules.Bus, options ...modules.ModuleOption) (modu option(m) } - if m.txi == nil { - m.txi = bus.GetPersistenceModule().GetTxIndexer() + if m.TXI == nil { + m.TXI = bus.GetPersistenceModule().GetTxIndexer() } if err := m.setupTrees(); err != nil { @@ -32,13 +32,13 @@ func (*treeStore) Create(bus modules.Bus, options ...modules.ModuleOption) (modu } func Create(bus modules.Bus, options ...modules.ModuleOption) (modules.Module, error) { - return new(treeStore).Create(bus, options...) + return new(TreeStore).Create(bus, options...) } // WithLogger assigns a logger for the tree store func WithLogger(logger *modules.Logger) modules.ModuleOption { return func(m modules.InitializableModule) { - if mod, ok := m.(*treeStore); ok { + if mod, ok := m.(*TreeStore); ok { mod.logger = logger } } @@ -48,9 +48,9 @@ func WithLogger(logger *modules.Logger) modules.ModuleOption { // saves its data. func WithTreeStoreDirectory(path string) modules.ModuleOption { return func(m modules.InitializableModule) { - mod, ok := m.(*treeStore) + mod, ok := m.(*TreeStore) if ok { - mod.treeStoreDir = path + mod.TreeStoreDir = path } } } @@ -58,25 +58,25 @@ func WithTreeStoreDirectory(path string) modules.ModuleOption { // WithTxIndexer assigns a TxIndexer for use during operation. func WithTxIndexer(txi indexer.TxIndexer) modules.ModuleOption { return func(m modules.InitializableModule) { - mod, ok := m.(*treeStore) + mod, ok := m.(*TreeStore) if ok { - mod.txi = txi + mod.TXI = txi } } } -func (t *treeStore) GetModuleName() string { return modules.TreeStoreModuleName } -func (t *treeStore) Start() error { return nil } -func (t *treeStore) Stop() error { return nil } -func (t *treeStore) GetBus() modules.Bus { return t.bus } -func (t *treeStore) SetBus(bus modules.Bus) { t.bus = bus } +func (t *TreeStore) GetModuleName() string { return modules.TreeStoreModuleName } +func (t *TreeStore) Start() error { return nil } +func (t *TreeStore) Stop() error { return nil } +func (t *TreeStore) GetBus() modules.Bus { return t.Bus } +func (t *TreeStore) SetBus(bus modules.Bus) { t.Bus = bus } -func (t *treeStore) setupTrees() error { - if t.treeStoreDir == ":memory:" { +func (t *TreeStore) setupTrees() error { + if t.TreeStoreDir == ":memory:" { return t.setupInMemory() } - nodeStore, err := kvstore.NewKVStore(fmt.Sprintf("%s/%s_nodes", t.treeStoreDir, RootTreeName)) + nodeStore, err := kvstore.NewKVStore(fmt.Sprintf("%s/%s_nodes", t.TreeStoreDir, RootTreeName)) if err != nil { return err } @@ -88,7 +88,7 @@ func (t *treeStore) setupTrees() error { t.merkleTrees = make(map[string]*stateTree, len(stateTreeNames)) for i := 0; i < len(stateTreeNames); i++ { - nodeStore, err := kvstore.NewKVStore(fmt.Sprintf("%s/%s_nodes", t.treeStoreDir, stateTreeNames[i])) + nodeStore, err := kvstore.NewKVStore(fmt.Sprintf("%s/%s_nodes", t.TreeStoreDir, stateTreeNames[i])) if err != nil { return err } @@ -102,7 +102,7 @@ func (t *treeStore) setupTrees() error { return nil } -func (t *treeStore) setupInMemory() error { +func (t *TreeStore) setupInMemory() error { nodeStore := kvstore.NewMemKVStore() t.rootTree = &stateTree{ name: RootTreeName, diff --git a/persistence/trees/trees.go b/persistence/trees/trees.go index a0b8b6dd12..98d3a677cd 100644 --- a/persistence/trees/trees.go +++ b/persistence/trees/trees.go @@ -71,30 +71,30 @@ type stateTree struct { nodeStore kvstore.KVStore } -var _ modules.TreeStoreModule = &treeStore{} +var _ modules.TreeStoreModule = &TreeStore{} -// treeStore stores a set of merkle trees that -// it manages. It fulfills the modules.treeStore interface. +// TreeStore stores a set of merkle trees that +// it manages. It fulfills the modules.TreeStore interface. // * It is responsible for atomic commit or rollback behavior // of the underlying trees by utilizing the lazy loading // functionality provided by the underlying smt library. -type treeStore struct { +type TreeStore struct { base_modules.IntegratableModule logger *modules.Logger - bus modules.Bus - txi indexer.TxIndexer + Bus modules.Bus + TXI indexer.TxIndexer - treeStoreDir string + TreeStoreDir string rootTree *stateTree merkleTrees map[string]*stateTree - prev *Worldstate + Prev *Worldstate } // GetTree returns the name, root hash, and nodeStore for the matching tree tree // stored in the TreeStore. This enables the caller to import the smt and not // change the one stored -func (t *treeStore) GetTree(name string) ([]byte, kvstore.KVStore) { +func (t *TreeStore) GetTree(name string) ([]byte, kvstore.KVStore) { if name == RootTreeName { return t.rootTree.tree.Root(), t.rootTree.nodeStore } @@ -113,10 +113,10 @@ type Worldstate struct { // Update takes a pgx transaction and a height and updates all of the trees in the TreeStore for that height. // It is atomic and handles its own savepoint and rollback creation. -func (t *treeStore) Update(pgtx pgx.Tx, height uint64) (string, error) { +func (t *TreeStore) Update(pgtx pgx.Tx, height uint64) (string, error) { t.logger.Info().Msgf("🌴 updating state trees at height %d", height) - stateHash, err := t.updateMerkleTrees(pgtx, t.txi, height) + stateHash, err := t.updateMerkleTrees(pgtx, t.TXI, height) if err != nil { t.Rollback() return "", fmt.Errorf("failed to update merkle trees: %w", err) @@ -128,7 +128,7 @@ func (t *treeStore) Update(pgtx pgx.Tx, height uint64) (string, error) { // DebugClearAll is used by the debug cli to completely reset all merkle trees. // This should only be called by the debug CLI. // TECHDEBT: Move this into a separate file with a debug build flag to avoid accidental usage in prod -func (t *treeStore) DebugClearAll() error { +func (t *TreeStore) DebugClearAll() error { if err := t.rootTree.nodeStore.ClearAll(); err != nil { return fmt.Errorf("failed to clear root node store: %w", err) } @@ -145,7 +145,7 @@ func (t *treeStore) DebugClearAll() error { // updateMerkleTrees updates all of the merkle trees in order defined by `numMerkleTrees` // * it returns the new state hash capturing the state of all the trees or an error if one occurred -func (t *treeStore) updateMerkleTrees(pgtx pgx.Tx, txi indexer.TxIndexer, height uint64) (string, error) { +func (t *TreeStore) updateMerkleTrees(pgtx pgx.Tx, txi indexer.TxIndexer, height uint64) (string, error) { for treeName := range t.merkleTrees { switch treeName { // Actor Merkle Trees @@ -216,7 +216,7 @@ func (t *treeStore) updateMerkleTrees(pgtx pgx.Tx, txi indexer.TxIndexer, height return t.getStateHash(), nil } -func (t *treeStore) getStateHash() string { +func (t *TreeStore) getStateHash() string { for _, stateTree := range t.merkleTrees { if err := t.rootTree.tree.Update([]byte(stateTree.name), stateTree.tree.Root()); err != nil { log.Fatalf("failed to update root tree with %s tree's hash: %v", stateTree.name, err) @@ -230,7 +230,7 @@ func (t *treeStore) getStateHash() string { } // Commit applies any changes that have been made to the underlying trees -func (t *treeStore) Commit() error { +func (t *TreeStore) Commit() error { for treeName, stateTree := range t.merkleTrees { if err := stateTree.tree.Commit(); err != nil { return fmt.Errorf("failed to commit %s: %w", treeName, err) @@ -240,29 +240,29 @@ func (t *treeStore) Commit() error { } // Savepoint generates a new savepoint for the tree store and saves it internally. -func (t *treeStore) Savepoint() error { +func (t *TreeStore) Savepoint() error { w, err := t.save() if err != nil { return err } - t.prev = w + t.Prev = w return nil } // Rollback intentionally can't return an error because at this point we're out of tricks // to recover from problems. -func (t *treeStore) Rollback() { - if t.prev != nil { - t.load(t.prev) +func (t *TreeStore) Rollback() { + if t.Prev != nil { + t.load(t.Prev) return } t.logger.Fatal().Msgf("rollback called without valid savepoint - this is a bug - treeStore shutting down: %+v", t) } // save serializes the treeStore to a WorldState object with new readers and writers. -func (t *treeStore) save() (*Worldstate, error) { +func (t *TreeStore) save() (*Worldstate, error) { w := &Worldstate{ - TreeStoreDir: t.treeStoreDir, + TreeStoreDir: t.TreeStoreDir, MerkleTrees: map[string]*stateTree{}, } @@ -287,7 +287,7 @@ func (t *treeStore) save() (*Worldstate, error) { return w, nil } -func (t *treeStore) load(w *Worldstate) { +func (t *TreeStore) load(w *Worldstate) { t.merkleTrees = w.MerkleTrees t.rootTree = w.RootTree } @@ -297,7 +297,7 @@ func (t *treeStore) load(w *Worldstate) { //////////////////////// // NB: I think this needs to be done manually for all 4 types. -func (t *treeStore) updateActorsTree(actorType coreTypes.ActorType, actors []*coreTypes.Actor) error { +func (t *TreeStore) updateActorsTree(actorType coreTypes.ActorType, actors []*coreTypes.Actor) error { for _, actor := range actors { bzAddr, err := hex.DecodeString(actor.GetAddress()) if err != nil { @@ -324,7 +324,7 @@ func (t *treeStore) updateActorsTree(actorType coreTypes.ActorType, actors []*co // Account Tree Helpers // ////////////////////////// -func (t *treeStore) updateAccountTrees(accounts []*coreTypes.Account) error { +func (t *TreeStore) updateAccountTrees(accounts []*coreTypes.Account) error { for _, account := range accounts { bzAddr, err := hex.DecodeString(account.GetAddress()) if err != nil { @@ -344,7 +344,7 @@ func (t *treeStore) updateAccountTrees(accounts []*coreTypes.Account) error { return nil } -func (t *treeStore) updatePoolTrees(pools []*coreTypes.Account) error { +func (t *TreeStore) updatePoolTrees(pools []*coreTypes.Account) error { for _, pool := range pools { bzAddr, err := hex.DecodeString(pool.GetAddress()) if err != nil { @@ -368,7 +368,7 @@ func (t *treeStore) updatePoolTrees(pools []*coreTypes.Account) error { // Data Tree Helpers // /////////////////////// -func (t *treeStore) updateTransactionsTree(indexedTxs []*coreTypes.IndexedTransaction) error { +func (t *TreeStore) updateTransactionsTree(indexedTxs []*coreTypes.IndexedTransaction) error { for _, idxTx := range indexedTxs { txBz := idxTx.GetTx() txHash := crypto.SHA3Hash(txBz) @@ -379,7 +379,7 @@ func (t *treeStore) updateTransactionsTree(indexedTxs []*coreTypes.IndexedTransa return nil } -func (t *treeStore) updateParamsTree(params []*coreTypes.Param) error { +func (t *TreeStore) updateParamsTree(params []*coreTypes.Param) error { for _, param := range params { paramBz, err := codec.GetCodec().Marshal(param) paramKey := crypto.SHA3Hash([]byte(param.Name)) @@ -394,7 +394,7 @@ func (t *treeStore) updateParamsTree(params []*coreTypes.Param) error { return nil } -func (t *treeStore) updateFlagsTree(flags []*coreTypes.Flag) error { +func (t *TreeStore) updateFlagsTree(flags []*coreTypes.Flag) error { for _, flag := range flags { flagBz, err := codec.GetCodec().Marshal(flag) flagKey := crypto.SHA3Hash([]byte(flag.Name))