diff --git a/persistence/context.go b/persistence/context.go index f72a41f86..0af52de76 100644 --- a/persistence/context.go +++ b/persistence/context.go @@ -31,7 +31,7 @@ type PostgresContext struct { // Need to simply access them via the bus. blockStore blockstore.BlockStore txIndexer indexer.TxIndexer - stateTrees *stateTrees + stateTrees modules.TreeStore networkId string } @@ -50,7 +50,7 @@ func (p *PostgresContext) RollbackToSavePoint(bytes []byte) error { // IMPROVE(#361): Guarantee the integrity of the state // Full details in the thread from the PR review: https://github.com/pokt-network/pocket/pull/285#discussion_r1018471719 func (p *PostgresContext) ComputeStateHash() (string, error) { - stateHash, err := p.updateMerkleTrees() + stateHash, err := p.stateTrees.Update(p.tx, p.txIndexer, uint64(p.Height)) if err != nil { return "", err } diff --git a/persistence/debug.go b/persistence/debug.go index cd6a70d99..81b075365 100644 --- a/persistence/debug.go +++ b/persistence/debug.go @@ -1,12 +1,10 @@ package persistence import ( - "crypto/sha256" "runtime/debug" "github.com/pokt-network/pocket/persistence/types" "github.com/pokt-network/pocket/shared/messaging" - "github.com/pokt-network/smt" ) // A list of functions to clear data from the DB not associated with protocol actors @@ -118,16 +116,5 @@ func (p *PostgresContext) clearAllSQLState() error { } func (p *PostgresContext) clearAllTreeState() error { - for treeType := merkleTree(0); treeType < numMerkleTrees; treeType++ { - nodeStore := p.stateTrees.nodeStores[treeType] - - if err := nodeStore.ClearAll(); err != nil { - return err - } - - // Needed in order to make sure the root is re-set correctly after clearing - p.stateTrees.merkleTrees[treeType] = smt.NewSparseMerkleTree(nodeStore, sha256.New()) - } - - return nil + return p.stateTrees.DebugClearAll() } diff --git a/persistence/docs/CHANGELOG.md b/persistence/docs/CHANGELOG.md index 0948b2792..38c4080aa 100644 --- a/persistence/docs/CHANGELOG.md +++ b/persistence/docs/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.0.58] - 2023-06-14 + +- Refactors the stateTrees implementation off of the PersistenceContext and into its in module +- Implements the new Update and Commit pattern with the SMT trees in the trees module + ## [0.0.0.57] - 2023-06-06 - Uses ":memory:" to signify when connecting to an in-memory database diff --git a/persistence/gov.go b/persistence/gov.go index e0bbcdb38..5ddec2884 100644 --- a/persistence/gov.go +++ b/persistence/gov.go @@ -8,7 +8,6 @@ import ( "github.com/pokt-network/pocket/logger" "github.com/pokt-network/pocket/persistence/types" "github.com/pokt-network/pocket/runtime/genesis" - coreTypes "github.com/pokt-network/pocket/shared/core/types" "github.com/pokt-network/pocket/shared/modules" ) @@ -156,48 +155,6 @@ func getParamOrFlag[T int | string | []byte](p *PostgresContext, tableName, para return } -func (p *PostgresContext) getParamsUpdated(height int64) ([]*coreTypes.Param, error) { - ctx, tx := p.getCtxAndTx() - // Get all parameters / flags at current height - rows, err := tx.Query(ctx, p.getParamsOrFlagsUpdateAtHeightQuery(types.ParamsTableName, height)) - if err != nil { - return nil, err - } - defer rows.Close() - var paramSlice []*coreTypes.Param // Store returned rows - // Loop over all rows returned and load them into the ParamOrFlag struct array - for rows.Next() { - param := new(coreTypes.Param) - if err := rows.Scan(¶m.Name, ¶m.Value); err != nil { - return nil, err - } - param.Height = height - paramSlice = append(paramSlice, param) - } - return paramSlice, nil -} - -func (p *PostgresContext) getFlagsUpdated(height int64) ([]*coreTypes.Flag, error) { - ctx, tx := p.getCtxAndTx() - // Get all parameters / flags at current height - rows, err := tx.Query(ctx, p.getParamsOrFlagsUpdateAtHeightQuery(types.FlagsTableName, height)) - if err != nil { - return nil, err - } - defer rows.Close() - var flagSlice []*coreTypes.Flag // Store returned rows - // Loop over all rows returned and load them into the ParamOrFlag struct array - for rows.Next() { - flag := new(coreTypes.Flag) - if err := rows.Scan(&flag.Name, &flag.Value, &flag.Enabled); err != nil { - return nil, err - } - flag.Height = height - flagSlice = append(flagSlice, flag) - } - return flagSlice, nil -} - // GetAllParams returns a map of the current latest updated values for all parameters // and their values in the form map[parameterName] = parameterValue func (p *PostgresContext) GetAllParams() ([][]string, error) { @@ -219,15 +176,6 @@ func (p *PostgresContext) GetAllParams() ([][]string, error) { return paramSlice, nil } -func (p *PostgresContext) getParamsOrFlagsUpdateAtHeightQuery(tableName string, height int64) string { - fields := "name,value" - if tableName == types.FlagsTableName { - fields += ",enabled" - } - // Build correct query to get all Params/Flags at certain height ordered by their name values - return fmt.Sprintf("SELECT %s FROM %s WHERE height=%d ORDER BY name ASC", fields, tableName, height) -} - func (p *PostgresContext) getLatestParamsOrFlagsQuery(tableName string) string { fields := "name,value" if tableName == types.FlagsTableName { diff --git a/persistence/module.go b/persistence/module.go index 32a6f51a4..887a85f06 100644 --- a/persistence/module.go +++ b/persistence/module.go @@ -9,6 +9,7 @@ import ( "github.com/pokt-network/pocket/logger" "github.com/pokt-network/pocket/persistence/blockstore" "github.com/pokt-network/pocket/persistence/indexer" + "github.com/pokt-network/pocket/persistence/trees" "github.com/pokt-network/pocket/runtime/configs" "github.com/pokt-network/pocket/runtime/genesis" "github.com/pokt-network/pocket/shared/modules" @@ -36,13 +37,14 @@ type persistenceModule struct { // A key-value store mapping heights to blocks. Needed for block synchronization. blockStore blockstore.BlockStore - // A tx indexer (i.e. key-value store) mapping transaction hashes to transactions. Needed for - // avoiding tx replays attacks, and is also used as the backing database for the transaction - // tx merkle tree. + // txIndexer is a key-value store mapping transaction hashes to `IndexedTransaction` protos. + // It is needed to avoid tx replay attacks and for business logic around transaction validation. + // IMPORTANT: It doubles as the data store for the transaction tree in state tree set. txIndexer indexer.TxIndexer - // A list of all the merkle trees maintained by the persistence module that roll up into the state commitment. - stateTrees *stateTrees + // stateTrees manages all of the merkle trees maintained by the + // persistence module that roll up into the state commitment. + stateTrees modules.TreeStore // Only one write context is allowed at a time writeContext *PostgresContext @@ -102,7 +104,8 @@ func (*persistenceModule) Create(bus modules.Bus, options ...modules.ModuleOptio return nil, err } - stateTrees, err := newStateTrees(persistenceCfg.TreesStoreDir) + // TECHDEBT (#808): Make TreeStore into a full Module + stateTrees, err := trees.NewStateTrees(persistenceCfg.TreesStoreDir) if err != nil { return nil, err } @@ -234,6 +237,10 @@ func (m *persistenceModule) GetTxIndexer() indexer.TxIndexer { return m.txIndexer } +func (m *persistenceModule) GetTreeStore() modules.TreeStore { + return m.stateTrees +} + func (m *persistenceModule) GetNetworkID() string { return m.networkId } diff --git a/persistence/sql/README.md b/persistence/sql/README.md new file mode 100644 index 000000000..b851ef559 --- /dev/null +++ b/persistence/sql/README.md @@ -0,0 +1,35 @@ +# SQL Package + +The SQL package is a library of functions that all take a `pgx.Tx` as an argument and execute SQL queries on it. + +- GetAccountsUpdated +- GetActors +- GetTransactions +- GetPools +- GetAccounts +- GetFlags +- GetParams + +These functions are meant to abstract away the SQL row and error handling for components of the persistence package. + +GetActors is used to get each of the actor types in the system: Applications, Validators, Watchers, and Servicers. + +## Why a whole package? + +If the SQL handling lives in the persistence package, the package submodules will run into import cycle errors when attempting to use the SQL handlers. Putting the SQL helpers into a package inside persistence avoids the import cycle. + +```mermaid +flowchart TD + subgraph pers[persistence] + subgraph p[ without sql package - import cycle error ] + persistenceContext1[PersistenceContext] + treeStore1[TreeStore] + treeStore1 -- Tx --> persistenceContext1 + persistenceContext1 -- Tx --> treeStore1 + end + subgraph p2[ with sql package - refactored ] + persistenceContext -- Tx --> sql + treeStore2 -- Tx --> sql + end + end +``` diff --git a/persistence/sql/sql.go b/persistence/sql/sql.go new file mode 100644 index 000000000..48bbec2d3 --- /dev/null +++ b/persistence/sql/sql.go @@ -0,0 +1,223 @@ +package sql + +import ( + "context" + "encoding/hex" + "fmt" + + "github.com/jackc/pgx/v5" + "github.com/pokt-network/pocket/persistence/indexer" + ptypes "github.com/pokt-network/pocket/persistence/types" + coreTypes "github.com/pokt-network/pocket/shared/core/types" +) + +// actorTypeToSchmeaName maps an ActorType to a PostgreSQL schema name +var actorTypeToSchemaName = map[coreTypes.ActorType]ptypes.ProtocolActorSchema{ + coreTypes.ActorType_ACTOR_TYPE_APP: ptypes.ApplicationActor, + coreTypes.ActorType_ACTOR_TYPE_VAL: ptypes.ValidatorActor, + coreTypes.ActorType_ACTOR_TYPE_FISH: ptypes.FishermanActor, + coreTypes.ActorType_ACTOR_TYPE_SERVICER: ptypes.ServicerActor, +} + +// GetActors is responsible for fetching the actors that have been updated at a given height. +func GetActors( + pgtx pgx.Tx, + actorType coreTypes.ActorType, + height uint64, +) ([]*coreTypes.Actor, error) { + actorSchema, ok := actorTypeToSchemaName[actorType] + if !ok { + return nil, fmt.Errorf("no schema found for actor type: %s", actorType) + } + + // TECHDEBT(#813): Avoid this cast to int64 + query := actorSchema.GetUpdatedAtHeightQuery(int64(height)) + rows, err := pgtx.Query(context.TODO(), query) + if err != nil { + return nil, err + } + defer rows.Close() + + addrs := make([][]byte, 0) + for rows.Next() { + var addr string + if err := rows.Scan(&addr); err != nil { + return nil, err + } + addrBz, err := hex.DecodeString(addr) + if err != nil { + return nil, err + } + addrs = append(addrs, addrBz) + } + + actors := make([]*coreTypes.Actor, len(addrs)) + for i, addr := range addrs { + // TECHDEBT(#813): Avoid this cast to int64 + actor, err := getActor(pgtx, actorSchema, addr, int64(height)) + if err != nil { + return nil, err + } + actors[i] = actor + } + + return actors, nil +} + +// GetAccountsUpdated gets the AccountSchema accounts that have been updated at height +func GetAccountsUpdated( + pgtx pgx.Tx, + acctType ptypes.ProtocolAccountSchema, + height uint64, +) ([]*coreTypes.Account, error) { + accounts := []*coreTypes.Account{} + + // TECHDEBT(#813): Avoid this cast to int64 + query := acctType.GetAccountsUpdatedAtHeightQuery(int64(height)) + rows, err := pgtx.Query(context.TODO(), query) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + acc := new(coreTypes.Account) + if err := rows.Scan(&acc.Address, &acc.Amount); err != nil { + return nil, err + } + accounts = append(accounts, acc) + } + + return accounts, nil +} + +// GetTransactions takes a transaction indexer and returns the transactions for the current height +func GetTransactions(txi indexer.TxIndexer, height uint64) ([]*coreTypes.IndexedTransaction, error) { + // TECHDEBT(#813): Avoid this cast to int64 + indexedTxs, err := txi.GetByHeight(int64(height), false) + if err != nil { + return nil, fmt.Errorf("failed to get transactions by height: %w", err) + } + return indexedTxs, nil +} + +// GetPools returns the pools updated at the given height +func GetPools(pgtx pgx.Tx, height uint64) ([]*coreTypes.Account, error) { + pools, err := GetAccountsUpdated(pgtx, ptypes.Pool, height) + if err != nil { + return nil, fmt.Errorf("failed to get pools: %w", err) + } + return pools, nil +} + +// GetAccounts returns the list of accounts updated at the provided height +func GetAccounts(pgtx pgx.Tx, height uint64) ([]*coreTypes.Account, error) { + accounts, err := GetAccountsUpdated(pgtx, ptypes.Account, height) + if err != nil { + return nil, fmt.Errorf("failed to get accounts: %w", err) + } + return accounts, nil +} + +// GetFlags returns the set of flags updated at the given height +func GetFlags(pgtx pgx.Tx, height uint64) ([]*coreTypes.Flag, error) { + fields := "name,value,enabled" + query := fmt.Sprintf("SELECT %s FROM %s WHERE height=%d ORDER BY name ASC", fields, ptypes.FlagsTableName, height) + rows, err := pgtx.Query(context.TODO(), query) + if err != nil { + return nil, fmt.Errorf("failed to get flags: %w", err) + } + defer rows.Close() + + flagSlice := []*coreTypes.Flag{} + for rows.Next() { + flag := new(coreTypes.Flag) + if err := rows.Scan(&flag.Name, &flag.Value, &flag.Enabled); err != nil { + return nil, err + } + flag.Height = int64(height) + flagSlice = append(flagSlice, flag) + } + + return flagSlice, nil +} + +// GetParams returns the set of params updated at the currented height +func GetParams(pgtx pgx.Tx, height uint64) ([]*coreTypes.Param, error) { + fields := "name,value" + query := fmt.Sprintf("SELECT %s FROM %s WHERE height=%d ORDER BY name ASC", fields, ptypes.ParamsTableName, height) + rows, err := pgtx.Query(context.TODO(), query) + if err != nil { + return nil, err + } + defer rows.Close() + + var paramSlice []*coreTypes.Param + for rows.Next() { + param := new(coreTypes.Param) + if err := rows.Scan(¶m.Name, ¶m.Value); err != nil { + return nil, err + } + param.Height = int64(height) + paramSlice = append(paramSlice, param) + } + + return paramSlice, nil +} + +func getActor(tx pgx.Tx, actorSchema ptypes.ProtocolActorSchema, address []byte, height int64) (actor *coreTypes.Actor, err error) { + ctx := context.TODO() + actor, height, err = getActorFromRow(actorSchema.GetActorType(), tx.QueryRow(ctx, actorSchema.GetQuery(hex.EncodeToString(address), height))) + if err != nil { + return + } + return getChainsForActor(ctx, tx, actorSchema, actor, height) +} + +func getActorFromRow(actorType coreTypes.ActorType, row pgx.Row) (actor *coreTypes.Actor, height int64, err error) { + actor = &coreTypes.Actor{ + ActorType: actorType, + } + err = row.Scan( + &actor.Address, + &actor.PublicKey, + &actor.StakedAmount, + &actor.ServiceUrl, + &actor.Output, + &actor.PausedHeight, + &actor.UnstakingHeight, + &height) + return +} + +func getChainsForActor( + ctx context.Context, + tx pgx.Tx, + actorSchema ptypes.ProtocolActorSchema, + actor *coreTypes.Actor, + height int64, +) (a *coreTypes.Actor, err error) { + if actorSchema.GetChainsTableName() == "" { + return actor, nil + } + rows, err := tx.Query(ctx, actorSchema.GetChainsQuery(actor.Address, height)) + if err != nil { + return actor, err + } + defer rows.Close() + + var chainAddr string + var chainID string + var chainEndHeight int64 + for rows.Next() { + err = rows.Scan(&chainAddr, &chainID, &chainEndHeight) + if err != nil { + return + } + if chainAddr != actor.Address { + return actor, fmt.Errorf("unexpected address %s, expected %s when reading chains", chainAddr, actor.Address) + } + actor.Chains = append(actor.Chains, chainID) + } + return actor, nil +} diff --git a/persistence/state.go b/persistence/state.go deleted file mode 100644 index 7fc6d448a..000000000 --- a/persistence/state.go +++ /dev/null @@ -1,382 +0,0 @@ -package persistence - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "fmt" - - "github.com/pokt-network/pocket/persistence/kvstore" - "github.com/pokt-network/pocket/persistence/types" - "github.com/pokt-network/pocket/shared/codec" - coreTypes "github.com/pokt-network/pocket/shared/core/types" - "github.com/pokt-network/pocket/shared/crypto" - "github.com/pokt-network/smt" -) - -type merkleTree float64 - -type stateTrees struct { - merkleTrees map[merkleTree]*smt.SMT - - // nodeStores are part of the SMT, but references are kept below for convenience - // and debugging purposes - nodeStores map[merkleTree]kvstore.KVStore -} - -// A list of Merkle Trees used to maintain the state hash. -const ( - // IMPORTANT: The order in which these trees are defined is important and strict. It implicitly - // defines the index of the root hash each independent as they are concatenated together - // to generate the state hash. - - // Actor Merkle Trees - appMerkleTree merkleTree = iota - valMerkleTree - fishMerkleTree - servicerMerkleTree - - // Account Merkle Trees - accountMerkleTree - poolMerkleTree - - // Data Merkle Trees - transactionsMerkleTree - paramsMerkleTree - flagsMerkleTree - - // Used for iteration purposes only; see https://stackoverflow.com/a/64178235/768439 as a reference - numMerkleTrees -) - -var merkleTreeToString = map[merkleTree]string{ - appMerkleTree: "app", - valMerkleTree: "val", - fishMerkleTree: "fish", - servicerMerkleTree: "servicer", - - accountMerkleTree: "account", - poolMerkleTree: "pool", - - transactionsMerkleTree: "transactions", - paramsMerkleTree: "params", - flagsMerkleTree: "flags", -} - -var actorTypeToMerkleTreeName = map[coreTypes.ActorType]merkleTree{ - coreTypes.ActorType_ACTOR_TYPE_APP: appMerkleTree, - coreTypes.ActorType_ACTOR_TYPE_VAL: valMerkleTree, - coreTypes.ActorType_ACTOR_TYPE_FISH: fishMerkleTree, - coreTypes.ActorType_ACTOR_TYPE_SERVICER: servicerMerkleTree, -} - -var actorTypeToSchemaName = map[coreTypes.ActorType]types.ProtocolActorSchema{ - coreTypes.ActorType_ACTOR_TYPE_APP: types.ApplicationActor, - coreTypes.ActorType_ACTOR_TYPE_VAL: types.ValidatorActor, - coreTypes.ActorType_ACTOR_TYPE_FISH: types.FishermanActor, - coreTypes.ActorType_ACTOR_TYPE_SERVICER: types.ServicerActor, -} - -var merkleTreeToActorTypeName = map[merkleTree]coreTypes.ActorType{ - appMerkleTree: coreTypes.ActorType_ACTOR_TYPE_APP, - valMerkleTree: coreTypes.ActorType_ACTOR_TYPE_VAL, - fishMerkleTree: coreTypes.ActorType_ACTOR_TYPE_FISH, - servicerMerkleTree: coreTypes.ActorType_ACTOR_TYPE_SERVICER, -} - -func newStateTrees(treesStoreDir string) (*stateTrees, error) { - if treesStoreDir == ":memory:" { - return newMemStateTrees() - } - - stateTrees := &stateTrees{ - merkleTrees: make(map[merkleTree]*smt.SMT, int(numMerkleTrees)), - nodeStores: make(map[merkleTree]kvstore.KVStore, int(numMerkleTrees)), - } - - for tree := merkleTree(0); tree < numMerkleTrees; tree++ { - nodeStore, err := kvstore.NewKVStore(fmt.Sprintf("%s/%s_nodes", treesStoreDir, merkleTreeToString[tree])) - if err != nil { - return nil, err - } - stateTrees.nodeStores[tree] = nodeStore - stateTrees.merkleTrees[tree] = smt.NewSparseMerkleTree(nodeStore, sha256.New()) - } - return stateTrees, nil -} - -func newMemStateTrees() (*stateTrees, error) { - stateTrees := &stateTrees{ - merkleTrees: make(map[merkleTree]*smt.SMT, int(numMerkleTrees)), - nodeStores: make(map[merkleTree]kvstore.KVStore, int(numMerkleTrees)), - } - for tree := merkleTree(0); tree < numMerkleTrees; tree++ { - nodeStore := kvstore.NewMemKVStore() // For testing, `smt.NewSimpleMap()` can be used as well - stateTrees.nodeStores[tree] = nodeStore - stateTrees.merkleTrees[tree] = smt.NewSparseMerkleTree(nodeStore, sha256.New()) - } - return stateTrees, nil -} - -func (p *PostgresContext) updateMerkleTrees() (string, error) { - // Update all the merkle trees - for treeType := merkleTree(0); treeType < numMerkleTrees; treeType++ { - switch treeType { - // Actor Merkle Trees - case appMerkleTree, valMerkleTree, fishMerkleTree, servicerMerkleTree: - actorType, ok := merkleTreeToActorTypeName[treeType] - if !ok { - return "", fmt.Errorf("no actor type found for merkle tree: %v", treeType) - } - if err := p.updateActorsTree(actorType); err != nil { - return "", fmt.Errorf("failed to update actors tree for treeType: %v, actorType: %v - %w", treeType, actorType, err) - } - - // Account Merkle Trees - case accountMerkleTree: - if err := p.updateAccountTrees(); err != nil { - return "", fmt.Errorf("failed to update account trees - %w", err) - } - case poolMerkleTree: - if err := p.updatePoolTrees(); err != nil { - return "", fmt.Errorf("failed to update pool trees - %w", err) - } - - // Data Merkle Trees - case transactionsMerkleTree: - if err := p.updateTransactionsTree(); err != nil { - return "", fmt.Errorf("failed to update transactions tree - %w", err) - } - case paramsMerkleTree: - if err := p.updateParamsTree(); err != nil { - return "", fmt.Errorf("failed to update params tree - %w", err) - } - case flagsMerkleTree: - if err := p.updateFlagsTree(); err != nil { - return "", fmt.Errorf("failed to update flags tree - %w", err) - } - - // Default - default: - p.logger.Fatal().Msgf("Not handled yet in state commitment update. Merkle tree #{%v}", treeType) - } - } - - return p.getStateHash(), nil -} - -func (p *PostgresContext) getStateHash() string { - // Get the root of each Merkle Tree - roots := make([][]byte, 0) - for tree := merkleTree(0); tree < numMerkleTrees; tree++ { - roots = append(roots, p.stateTrees.merkleTrees[tree].Root()) - } - - // Get the state hash - rootsConcat := bytes.Join(roots, []byte{}) - stateHash := sha256.Sum256(rootsConcat) - - // Convert the array to a slice and return it - return hex.EncodeToString(stateHash[:]) -} - -// Actor Tree Helpers - -func (p *PostgresContext) updateActorsTree(actorType coreTypes.ActorType) error { - actors, err := p.getActorsUpdatedAtHeight(actorType, p.Height) - if err != nil { - return err - } - - for _, actor := range actors { - bzAddr, err := hex.DecodeString(actor.GetAddress()) - if err != nil { - return err - } - - actorBz, err := codec.GetCodec().Marshal(actor) - if err != nil { - return err - } - - merkleTreeName, ok := actorTypeToMerkleTreeName[actorType] - if !ok { - return fmt.Errorf("no merkle tree found for actor type: %s", actorType) - } - if err := p.stateTrees.merkleTrees[merkleTreeName].Update(bzAddr, actorBz); err != nil { - return err - } - - // If no errors updating then commit changes to the nodestore - if err := p.stateTrees.merkleTrees[merkleTreeName].Commit(); err != nil { - return err - } - } - - return nil -} - -func (p *PostgresContext) getActorsUpdatedAtHeight(actorType coreTypes.ActorType, height int64) (actors []*coreTypes.Actor, err error) { - actorSchema, ok := actorTypeToSchemaName[actorType] - if !ok { - return nil, fmt.Errorf("no schema found for actor type: %s", actorType) - } - - schemaActors, err := p.GetActorsUpdated(actorSchema, height) - if err != nil { - return nil, err - } - - actors = make([]*coreTypes.Actor, len(schemaActors)) - for i, schemaActor := range schemaActors { - actor := &coreTypes.Actor{ - ActorType: actorType, - Address: schemaActor.Address, - PublicKey: schemaActor.PublicKey, - Chains: schemaActor.Chains, - ServiceUrl: schemaActor.ServiceUrl, - StakedAmount: schemaActor.StakedAmount, - PausedHeight: schemaActor.PausedHeight, - UnstakingHeight: schemaActor.UnstakingHeight, - Output: schemaActor.Output, - } - actors[i] = actor - } - return -} - -// Account Tree Helpers - -func (p *PostgresContext) updateAccountTrees() error { - accounts, err := p.GetAccountsUpdated(p.Height) - if err != nil { - return err - } - - for _, account := range accounts { - bzAddr, err := hex.DecodeString(account.GetAddress()) - if err != nil { - return err - } - - accBz, err := codec.GetCodec().Marshal(account) - if err != nil { - return err - } - - if err := p.stateTrees.merkleTrees[accountMerkleTree].Update(bzAddr, accBz); err != nil { - return err - } - - // If no errors updating then commit changes to the nodestore - if err := p.stateTrees.merkleTrees[accountMerkleTree].Commit(); err != nil { - return err - } - } - - return nil -} - -func (p *PostgresContext) updatePoolTrees() error { - pools, err := p.GetPoolsUpdated(p.Height) - if err != nil { - return err - } - - for _, pool := range pools { - bzAddr, err := hex.DecodeString(pool.GetAddress()) - if err != nil { - return err - } - - accBz, err := codec.GetCodec().Marshal(pool) - if err != nil { - return err - } - - if err := p.stateTrees.merkleTrees[poolMerkleTree].Update(bzAddr, accBz); err != nil { - return err - } - - // If no errors updating then commit changes to the nodestore - if err := p.stateTrees.merkleTrees[poolMerkleTree].Commit(); err != nil { - return err - } - } - - return nil -} - -// Data Tree Helpers - -func (p *PostgresContext) updateTransactionsTree() error { - indexedTxs, err := p.txIndexer.GetByHeight(p.Height, false) - if err != nil { - return err - } - - for _, idxTx := range indexedTxs { - txBz := idxTx.GetTx() - txHash := crypto.SHA3Hash(txBz) - if err := p.stateTrees.merkleTrees[transactionsMerkleTree].Update(txHash, txBz); err != nil { - return err - } - - // If no errors updating then commit changes to the nodestore - if err := p.stateTrees.merkleTrees[transactionsMerkleTree].Commit(); err != nil { - return err - } - } - - return nil -} - -func (p *PostgresContext) updateParamsTree() error { - params, err := p.getParamsUpdated(p.Height) - if err != nil { - return err - } - - for _, param := range params { - paramBz, err := codec.GetCodec().Marshal(param) - paramKey := crypto.SHA3Hash([]byte(param.Name)) - if err != nil { - return err - } - if err := p.stateTrees.merkleTrees[paramsMerkleTree].Update(paramKey, paramBz); err != nil { - return err - } - - // If no errors updating then commit changes to the nodestore - if err := p.stateTrees.merkleTrees[paramsMerkleTree].Commit(); err != nil { - return err - } - } - - return nil -} - -func (p *PostgresContext) updateFlagsTree() error { - flags, err := p.getFlagsUpdated(p.Height) - if err != nil { - return err - } - - for _, flag := range flags { - flagBz, err := codec.GetCodec().Marshal(flag) - flagKey := crypto.SHA3Hash([]byte(flag.Name)) - if err != nil { - return err - } - if err := p.stateTrees.merkleTrees[flagsMerkleTree].Update(flagKey, flagBz); err != nil { - return err - } - - // If no errors updating then commit changes to the nodestore - if err := p.stateTrees.merkleTrees[flagsMerkleTree].Commit(); err != nil { - return err - } - } - - return nil -} diff --git a/persistence/test/account_test.go b/persistence/test/account_test.go index b75694cea..ec121efca 100644 --- a/persistence/test/account_test.go +++ b/persistence/test/account_test.go @@ -8,12 +8,13 @@ import ( "math/rand" "testing" + "github.com/stretchr/testify/require" + "github.com/pokt-network/pocket/persistence" "github.com/pokt-network/pocket/runtime/test_artifacts/keygen" coreTypes "github.com/pokt-network/pocket/shared/core/types" "github.com/pokt-network/pocket/shared/crypto" "github.com/pokt-network/pocket/shared/utils" - "github.com/stretchr/testify/require" ) func FuzzAccountAmount(f *testing.F) { diff --git a/persistence/test/actor_test.go b/persistence/test/actor_test.go index b4d1d00cc..89ff4afb4 100644 --- a/persistence/test/actor_test.go +++ b/persistence/test/actor_test.go @@ -3,8 +3,9 @@ package test import ( "testing" - "github.com/pokt-network/pocket/shared/core/types" "github.com/stretchr/testify/require" + + coreTypes "github.com/pokt-network/pocket/shared/core/types" ) func TestGetAllStakedActors(t *testing.T) { @@ -21,13 +22,13 @@ func TestGetAllStakedActors(t *testing.T) { actualFishermen := 0 for _, actor := range actors { switch actor.ActorType { - case types.ActorType_ACTOR_TYPE_VAL: + case coreTypes.ActorType_ACTOR_TYPE_VAL: actualValidators++ - case types.ActorType_ACTOR_TYPE_SERVICER: + case coreTypes.ActorType_ACTOR_TYPE_SERVICER: actualServicers++ - case types.ActorType_ACTOR_TYPE_APP: + case coreTypes.ActorType_ACTOR_TYPE_APP: actualApplications++ - case types.ActorType_ACTOR_TYPE_FISH: + case coreTypes.ActorType_ACTOR_TYPE_FISH: actualFishermen++ } } diff --git a/persistence/test/application_test.go b/persistence/test/application_test.go index ebf02a200..7e99920d5 100644 --- a/persistence/test/application_test.go +++ b/persistence/test/application_test.go @@ -5,23 +5,24 @@ import ( "log" "testing" + "github.com/stretchr/testify/require" + "github.com/pokt-network/pocket/persistence" - "github.com/pokt-network/pocket/persistence/types" + ptypes "github.com/pokt-network/pocket/persistence/types" coreTypes "github.com/pokt-network/pocket/shared/core/types" "github.com/pokt-network/pocket/shared/crypto" - "github.com/stretchr/testify/require" ) func FuzzApplication(f *testing.F) { fuzzSingleProtocolActor(f, - newTestGenericActor(types.ApplicationActor, newTestApp), - getGenericActor(types.ApplicationActor, getTestApp), - types.ApplicationActor) + newTestGenericActor(ptypes.ApplicationActor, newTestApp), + getGenericActor(ptypes.ApplicationActor, getTestApp), + ptypes.ApplicationActor) } func TestGetApplicationsUpdatedAtHeight(t *testing.T) { getApplicationsUpdatedFunc := func(db *persistence.PostgresContext, height int64) ([]*coreTypes.Actor, error) { - return db.GetActorsUpdated(types.ApplicationActor, height) + return db.GetActorsUpdated(ptypes.ApplicationActor, height) } getAllActorsUpdatedAtHeightTest(t, createAndInsertDefaultTestApp, getApplicationsUpdatedFunc, 1) } diff --git a/persistence/test/fisherman_test.go b/persistence/test/fisherman_test.go index ee3565bf4..a3a245f37 100644 --- a/persistence/test/fisherman_test.go +++ b/persistence/test/fisherman_test.go @@ -5,18 +5,19 @@ import ( "log" "testing" + "github.com/stretchr/testify/require" + "github.com/pokt-network/pocket/persistence" - "github.com/pokt-network/pocket/persistence/types" + ptypes "github.com/pokt-network/pocket/persistence/types" coreTypes "github.com/pokt-network/pocket/shared/core/types" "github.com/pokt-network/pocket/shared/crypto" - "github.com/stretchr/testify/require" ) func FuzzFisherman(f *testing.F) { fuzzSingleProtocolActor(f, - newTestGenericActor(types.FishermanActor, newTestFisherman), - getGenericActor(types.FishermanActor, getTestFisherman), - types.FishermanActor) + newTestGenericActor(ptypes.FishermanActor, newTestFisherman), + getGenericActor(ptypes.FishermanActor, getTestFisherman), + ptypes.FishermanActor) } func TestGetSetFishermanStakeAmount(t *testing.T) { @@ -26,7 +27,7 @@ func TestGetSetFishermanStakeAmount(t *testing.T) { func TestGetFishermanUpdatedAtHeight(t *testing.T) { getFishermanUpdatedFunc := func(db *persistence.PostgresContext, height int64) ([]*coreTypes.Actor, error) { - return db.GetActorsUpdated(types.FishermanActor, height) + return db.GetActorsUpdated(ptypes.FishermanActor, height) } getAllActorsUpdatedAtHeightTest(t, createAndInsertDefaultTestFisherman, getFishermanUpdatedFunc, 1) } diff --git a/persistence/test/generic_test.go b/persistence/test/generic_test.go index 59b86bcbf..557ea091e 100644 --- a/persistence/test/generic_test.go +++ b/persistence/test/generic_test.go @@ -5,14 +5,15 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/require" + "github.com/pokt-network/pocket/persistence" - "github.com/pokt-network/pocket/persistence/types" + ptypes "github.com/pokt-network/pocket/persistence/types" coreTypes "github.com/pokt-network/pocket/shared/core/types" - "github.com/stretchr/testify/require" ) func getGenericActor[T any]( - protocolActorSchema types.ProtocolActorSchema, + protocolActorSchema ptypes.ProtocolActorSchema, getActor func(*persistence.PostgresContext, []byte) (T, error), ) func(*persistence.PostgresContext, string) (*coreTypes.Actor, error) { return func(db *persistence.PostgresContext, address string) (*coreTypes.Actor, error) { @@ -29,7 +30,7 @@ func getGenericActor[T any]( } } -func newTestGenericActor[T any](protocolActorSchema types.ProtocolActorSchema, newActor func() (T, error)) func() (*coreTypes.Actor, error) { +func newTestGenericActor[T any](protocolActorSchema ptypes.ProtocolActorSchema, newActor func() (T, error)) func() (*coreTypes.Actor, error) { return func() (*coreTypes.Actor, error) { actor, err := newActor() if err != nil { @@ -201,7 +202,7 @@ func getAllActorsUpdatedAtHeightTest[T any]( require.Equal(t, 1, len(accs)) } -func getActorValues(_ types.ProtocolActorSchema, actorValue reflect.Value) *coreTypes.Actor { +func getActorValues(_ ptypes.ProtocolActorSchema, actorValue reflect.Value) *coreTypes.Actor { chains := make([]string, 0) if actorValue.FieldByName("Chains").Kind() != 0 { chains = actorValue.FieldByName("Chains").Interface().([]string) diff --git a/persistence/test/setup_test.go b/persistence/test/setup_test.go index f8d549412..70ac0603b 100644 --- a/persistence/test/setup_test.go +++ b/persistence/test/setup_test.go @@ -11,8 +11,11 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" + "github.com/pokt-network/pocket/persistence" - "github.com/pokt-network/pocket/persistence/types" + ptypes "github.com/pokt-network/pocket/persistence/types" "github.com/pokt-network/pocket/runtime" "github.com/pokt-network/pocket/runtime/configs" "github.com/pokt-network/pocket/runtime/test_artifacts" @@ -22,8 +25,6 @@ import ( "github.com/pokt-network/pocket/shared/modules" moduleTypes "github.com/pokt-network/pocket/shared/modules/types" "github.com/pokt-network/pocket/shared/utils" - "github.com/stretchr/testify/require" - "golang.org/x/exp/slices" ) var ( @@ -139,7 +140,7 @@ func fuzzSingleProtocolActor( f *testing.F, newTestActor func() (*coreTypes.Actor, error), getTestActor func(db *persistence.PostgresContext, address string) (*coreTypes.Actor, error), - protocolActorSchema types.ProtocolActorSchema, + protocolActorSchema ptypes.ProtocolActorSchema, ) { // Clear the genesis state. clearAllState() @@ -193,9 +194,9 @@ func fuzzSingleProtocolActor( newStakedTokens = getRandomBigIntString() case 1: switch protocolActorSchema.GetActorSpecificColName() { - case types.ServiceURLCol: + case ptypes.ServiceURLCol: serviceUrl = getRandomServiceURL() - case types.UnusedCol: + case ptypes.UnusedCol: serviceUrl = "" default: t.Error("Unexpected actor specific column name") diff --git a/persistence/test/validator_test.go b/persistence/test/validator_test.go index bc3182d9e..c35b67a07 100644 --- a/persistence/test/validator_test.go +++ b/persistence/test/validator_test.go @@ -5,18 +5,19 @@ import ( "log" "testing" + "github.com/stretchr/testify/require" + "github.com/pokt-network/pocket/persistence" - "github.com/pokt-network/pocket/persistence/types" + ptypes "github.com/pokt-network/pocket/persistence/types" coreTypes "github.com/pokt-network/pocket/shared/core/types" "github.com/pokt-network/pocket/shared/crypto" - "github.com/stretchr/testify/require" ) func FuzzValidator(f *testing.F) { fuzzSingleProtocolActor(f, - newTestGenericActor(types.ValidatorActor, newTestValidator), - getGenericActor(types.ValidatorActor, getTestValidator), - types.ValidatorActor) + newTestGenericActor(ptypes.ValidatorActor, newTestValidator), + getGenericActor(ptypes.ValidatorActor, getTestValidator), + ptypes.ValidatorActor) } func TestGetSetValidatorStakeAmount(t *testing.T) { @@ -26,7 +27,7 @@ func TestGetSetValidatorStakeAmount(t *testing.T) { func TestGetValidatorUpdatedAtHeight(t *testing.T) { getValidatorsUpdatedFunc := func(db *persistence.PostgresContext, height int64) ([]*coreTypes.Actor, error) { - return db.GetActorsUpdated(types.ValidatorActor, height) + return db.GetActorsUpdated(ptypes.ValidatorActor, height) } getAllActorsUpdatedAtHeightTest(t, createAndInsertDefaultTestValidator, getValidatorsUpdatedFunc, 5) } diff --git a/persistence/trees/trees.go b/persistence/trees/trees.go new file mode 100644 index 000000000..1463b901c --- /dev/null +++ b/persistence/trees/trees.go @@ -0,0 +1,368 @@ +// package trees maintains a set of sparse merkle trees +// each backed by the KVStore interface. It offers an atomic +// commit and rollback mechanism for interacting with +// that core resource map of merkle trees. +package trees + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "hash" + + "github.com/jackc/pgx/v5" + + "github.com/pokt-network/pocket/persistence/indexer" + "github.com/pokt-network/pocket/persistence/kvstore" + "github.com/pokt-network/pocket/persistence/sql" + "github.com/pokt-network/pocket/shared/codec" + coreTypes "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/crypto" + "github.com/pokt-network/smt" +) + +var smtTreeHasher hash.Hash = sha256.New() + +var merkleTreeToString = map[merkleTree]string{ + appMerkleTree: "app", + valMerkleTree: "val", + fishMerkleTree: "fish", + servicerMerkleTree: "servicer", + + accountMerkleTree: "account", + poolMerkleTree: "pool", + + transactionsMerkleTree: "transactions", + paramsMerkleTree: "params", + flagsMerkleTree: "flags", +} + +var actorTypeToMerkleTreeName = map[coreTypes.ActorType]merkleTree{ + coreTypes.ActorType_ACTOR_TYPE_APP: appMerkleTree, + coreTypes.ActorType_ACTOR_TYPE_VAL: valMerkleTree, + coreTypes.ActorType_ACTOR_TYPE_FISH: fishMerkleTree, + coreTypes.ActorType_ACTOR_TYPE_SERVICER: servicerMerkleTree, +} + +var merkleTreeToActorTypeName = map[merkleTree]coreTypes.ActorType{ + appMerkleTree: coreTypes.ActorType_ACTOR_TYPE_APP, + valMerkleTree: coreTypes.ActorType_ACTOR_TYPE_VAL, + fishMerkleTree: coreTypes.ActorType_ACTOR_TYPE_FISH, + servicerMerkleTree: coreTypes.ActorType_ACTOR_TYPE_SERVICER, +} + +type merkleTree float64 + +// A list of Merkle Trees used to maintain the state hash. +const ( + // IMPORTANT: The order in which these trees are defined is important and strict. It implicitly + // defines the index of the root hash each independent as they are concatenated together + // to generate the state hash. + + // TECHDEBT(#834): Remove the need for enforced ordering + + // Actor Merkle Trees + appMerkleTree merkleTree = iota + valMerkleTree + fishMerkleTree + servicerMerkleTree + + // Account Merkle Trees + accountMerkleTree + poolMerkleTree + + // Data Merkle Trees + transactionsMerkleTree + paramsMerkleTree + flagsMerkleTree + + // Used for iteration purposes only; see https://stackoverflow.com/a/64178235/768439 as a reference + numMerkleTrees +) + +// 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 { + treeStoreDir string + merkleTrees map[merkleTree]*smt.SMT + nodeStores map[merkleTree]kvstore.KVStore +} + +func (t *treeStore) Update(pgtx pgx.Tx, txi indexer.TxIndexer, height uint64) (string, error) { + return t.updateMerkleTrees(pgtx, txi, height) +} + +// NewStateTrees is the constructor object for a treeStore and initializes and configures a new +// tree for the appropriate type of store, i.e. in-memory vs file system storage. +func NewStateTrees(treesStoreDir string) (*treeStore, error) { + if treesStoreDir == ":memory:" { + return newMemStateTrees() + } + + stateTrees := &treeStore{ + treeStoreDir: treesStoreDir, + merkleTrees: make(map[merkleTree]*smt.SMT, int(numMerkleTrees)), + nodeStores: make(map[merkleTree]kvstore.KVStore, int(numMerkleTrees)), + } + + for tree := merkleTree(0); tree < numMerkleTrees; tree++ { + nodeStore, err := kvstore.NewKVStore(fmt.Sprintf("%s/%s_nodes", treesStoreDir, merkleTreeToString[tree])) + if err != nil { + return nil, err + } + stateTrees.nodeStores[tree] = nodeStore + stateTrees.merkleTrees[tree] = smt.NewSparseMerkleTree(nodeStore, smtTreeHasher) + } + return stateTrees, nil +} + +// 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 { + for treeType := merkleTree(0); treeType < numMerkleTrees; treeType++ { + nodeStore := t.nodeStores[treeType] + if err := nodeStore.ClearAll(); err != nil { + return fmt.Errorf("failed to clear %s node store: %w", merkleTreeToString[treeType], err) + } + t.merkleTrees[treeType] = smt.NewSparseMerkleTree(nodeStore, smtTreeHasher) + } + return nil +} + +// newMemStateTrees creates a new in-memory state tree +func newMemStateTrees() (*treeStore, error) { + stateTrees := &treeStore{ + merkleTrees: make(map[merkleTree]*smt.SMT, int(numMerkleTrees)), + nodeStores: make(map[merkleTree]kvstore.KVStore, int(numMerkleTrees)), + } + for tree := merkleTree(0); tree < numMerkleTrees; tree++ { + nodeStore := kvstore.NewMemKVStore() // For testing, `smt.NewSimpleMap()` can be used as well + stateTrees.nodeStores[tree] = nodeStore + stateTrees.merkleTrees[tree] = smt.NewSparseMerkleTree(nodeStore, smtTreeHasher) + } + return stateTrees, nil +} + +// 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) { + for treeType := merkleTree(0); treeType < numMerkleTrees; treeType++ { + switch treeType { + // Actor Merkle Trees + case appMerkleTree, valMerkleTree, fishMerkleTree, servicerMerkleTree: + actorType, ok := merkleTreeToActorTypeName[treeType] + if !ok { + return "", fmt.Errorf("no actor type found for merkle tree: %v", treeType) + } + + actors, err := sql.GetActors(pgtx, actorType, height) + if err != nil { + return "", fmt.Errorf("failed to get actors at height: %w", err) + } + + if err := t.updateActorsTree(actorType, actors); err != nil { + return "", fmt.Errorf("failed to update actors tree for treeType: %v, actorType: %v - %w", treeType, actorType, err) + } + + // Account Merkle Trees + case accountMerkleTree: + accounts, err := sql.GetAccounts(pgtx, height) + if err != nil { + return "", fmt.Errorf("failed to get accounts: %w", err) + } + if err := t.updateAccountTrees(accounts); err != nil { + return "", fmt.Errorf("failed to update account trees: %w", err) + } + case poolMerkleTree: + pools, err := sql.GetPools(pgtx, height) + if err != nil { + return "", fmt.Errorf("failed to get transactions: %w", err) + } + if err := t.updatePoolTrees(pools); err != nil { + return "", fmt.Errorf("failed to update pool trees - %w", err) + } + + // Data Merkle Trees + case transactionsMerkleTree: + indexedTxs, err := sql.GetTransactions(txi, height) + if err != nil { + return "", fmt.Errorf("failed to get transactions: %w", err) + } + if err := t.updateTransactionsTree(indexedTxs); err != nil { + return "", fmt.Errorf("failed to update transactions: %w", err) + } + case paramsMerkleTree: + params, err := sql.GetParams(pgtx, height) + if err != nil { + return "", fmt.Errorf("failed to get params: %w", err) + } + if err := t.updateParamsTree(params); err != nil { + return "", fmt.Errorf("failed to update params tree: %w", err) + } + case flagsMerkleTree: + flags, err := sql.GetFlags(pgtx, height) + if err != nil { + return "", fmt.Errorf("failed to get flags from transaction: %w", err) + } + if err := t.updateFlagsTree(flags); err != nil { + return "", fmt.Errorf("failed to update flags tree - %w", err) + } + // Default + default: + panic(fmt.Sprintf("not handled in state commitment update. Merkle tree #{%v}", treeType)) + } + } + + if err := t.commit(); err != nil { + return "", fmt.Errorf("failed to commit: %w", err) + } + return t.getStateHash(), nil +} + +func (t *treeStore) commit() error { + for tree := merkleTree(0); tree < numMerkleTrees; tree++ { + if err := t.merkleTrees[tree].Commit(); err != nil { + return fmt.Errorf("failed to commit %s: %w", merkleTreeToString[tree], err) + } + } + return nil +} + +func (t *treeStore) getStateHash() string { + // create an order-matters list of roots + roots := make([][]byte, 0) + for tree := merkleTree(0); tree < numMerkleTrees; tree++ { + roots = append(roots, t.merkleTrees[tree].Root()) + } + + // combine them and hash the result + rootsConcat := bytes.Join(roots, []byte{}) + stateHash := sha256.Sum256(rootsConcat) + + // Convert the array to a slice and return it + // REF: https://stackoverflow.com/questions/28886616/convert-array-to-slice-in-go + return hex.EncodeToString(stateHash[:]) +} + +//////////////////////// +// Actor Tree Helpers // +//////////////////////// + +// NB: I think this needs to be done manually for all 4 types. +func (t *treeStore) updateActorsTree(actorType coreTypes.ActorType, actors []*coreTypes.Actor) error { + for _, actor := range actors { + bzAddr, err := hex.DecodeString(actor.GetAddress()) + if err != nil { + return err + } + + actorBz, err := codec.GetCodec().Marshal(actor) + if err != nil { + return err + } + + merkleTreeName, ok := actorTypeToMerkleTreeName[actorType] + if !ok { + return fmt.Errorf("no merkle tree found for actor type: %s", actorType) + } + if err := t.merkleTrees[merkleTreeName].Update(bzAddr, actorBz); err != nil { + return err + } + } + + return nil +} + +////////////////////////// +// Account Tree Helpers // +////////////////////////// + +func (t *treeStore) updateAccountTrees(accounts []*coreTypes.Account) error { + for _, account := range accounts { + bzAddr, err := hex.DecodeString(account.GetAddress()) + if err != nil { + return err + } + + accBz, err := codec.GetCodec().Marshal(account) + if err != nil { + return err + } + + if err := t.merkleTrees[accountMerkleTree].Update(bzAddr, accBz); err != nil { + return err + } + } + + return nil +} + +func (t *treeStore) updatePoolTrees(pools []*coreTypes.Account) error { + for _, pool := range pools { + bzAddr, err := hex.DecodeString(pool.GetAddress()) + if err != nil { + return err + } + + accBz, err := codec.GetCodec().Marshal(pool) + if err != nil { + return err + } + + if err := t.merkleTrees[poolMerkleTree].Update(bzAddr, accBz); err != nil { + return err + } + } + + return nil +} + +/////////////////////// +// Data Tree Helpers // +/////////////////////// + +func (t *treeStore) updateTransactionsTree(indexedTxs []*coreTypes.IndexedTransaction) error { + for _, idxTx := range indexedTxs { + txBz := idxTx.GetTx() + txHash := crypto.SHA3Hash(txBz) + if err := t.merkleTrees[transactionsMerkleTree].Update(txHash, txBz); err != nil { + return err + } + } + return nil +} + +func (t *treeStore) updateParamsTree(params []*coreTypes.Param) error { + for _, param := range params { + paramBz, err := codec.GetCodec().Marshal(param) + paramKey := crypto.SHA3Hash([]byte(param.Name)) + if err != nil { + return err + } + if err := t.merkleTrees[paramsMerkleTree].Update(paramKey, paramBz); err != nil { + return err + } + } + + return nil +} + +func (t *treeStore) updateFlagsTree(flags []*coreTypes.Flag) error { + for _, flag := range flags { + flagBz, err := codec.GetCodec().Marshal(flag) + flagKey := crypto.SHA3Hash([]byte(flag.Name)) + if err != nil { + return err + } + if err := t.merkleTrees[flagsMerkleTree].Update(flagKey, flagBz); err != nil { + return err + } + } + + return nil +} diff --git a/persistence/trees/trees_test.go b/persistence/trees/trees_test.go new file mode 100644 index 000000000..17c502d1a --- /dev/null +++ b/persistence/trees/trees_test.go @@ -0,0 +1,19 @@ +package trees + +import "testing" + +// TECHDEBT(#836): Tests added in https://github.com/pokt-network/pocket/pull/836 +func TestTreeStore_Update(t *testing.T) { + // TODO: Write test case for the Update method + t.Skip("TODO: Write test case for Update method") +} + +func TestTreeStore_New(t *testing.T) { + // TODO: Write test case for the NewStateTrees function + t.Skip("TODO: Write test case for NewStateTrees function") +} + +func TestTreeStore_DebugClearAll(t *testing.T) { + // TODO: Write test case for the DebugClearAll method + t.Skip("TODO: Write test case for DebugClearAll method") +} diff --git a/runtime/docs/CHANGELOG.md b/runtime/docs/CHANGELOG.md index 98206c2d4..b36289c67 100644 --- a/runtime/docs/CHANGELOG.md +++ b/runtime/docs/CHANGELOG.md @@ -7,12 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.0.43] - 2023-06-14 + +- Rename package import types to coreTypes for consitency and clarity + ## [0.0.0.42] - 2023-06-13 - Append "Hostname" to validator endpoint hostname constants - Promoted string literal to `RandomValidatorEndpointK8SHostname` constant -## [0.0.0.41] - 2023-06-06 +## [0.0.0.41] - 2023-06-07 - Adds fisherman and servicer proto configurations. - Renames actor hostnames diff --git a/runtime/test_artifacts/generator.go b/runtime/test_artifacts/generator.go index 24b80a2df..02241381e 100644 --- a/runtime/test_artifacts/generator.go +++ b/runtime/test_artifacts/generator.go @@ -10,7 +10,6 @@ import ( "github.com/pokt-network/pocket/runtime/configs" "github.com/pokt-network/pocket/runtime/genesis" "github.com/pokt-network/pocket/runtime/test_artifacts/keygen" - "github.com/pokt-network/pocket/shared/core/types" coreTypes "github.com/pokt-network/pocket/shared/core/types" "github.com/pokt-network/pocket/shared/crypto" "google.golang.org/protobuf/types/known/timestamppb" @@ -64,7 +63,7 @@ func WithActors(actors []*coreTypes.Actor, actorKeys []string) func(*genesis.Gen genesis.Accounts = append(genesis.Accounts, newActorAccounts...) for _, actor := range actors { switch actor.ActorType { - case types.ActorType_ACTOR_TYPE_APP: + case coreTypes.ActorType_ACTOR_TYPE_APP: genesis.Applications = append(genesis.Applications, actor) case coreTypes.ActorType_ACTOR_TYPE_VAL: genesis.Validators = append(genesis.Validators, actor) diff --git a/shared/modules/doc/CHANGELOG.md b/shared/modules/doc/CHANGELOG.md index 25c9b9f89..d6d965cce 100644 --- a/shared/modules/doc/CHANGELOG.md +++ b/shared/modules/doc/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.0.15] - 2023-06-14 + +- Defines the TreeStore interface + ## [0.0.0.14] - 2023-06-06 - Adds fisherman, servicer, and validator modules to utility interface. diff --git a/shared/modules/persistence_module.go b/shared/modules/persistence_module.go index 9cb0c837c..65681a5ac 100644 --- a/shared/modules/persistence_module.go +++ b/shared/modules/persistence_module.go @@ -3,6 +3,7 @@ package modules //go:generate mockgen -destination=./mocks/persistence_module_mock.go github.com/pokt-network/pocket/shared/modules PersistenceModule,PersistenceRWContext,PersistenceReadContext,PersistenceWriteContext import ( + "github.com/jackc/pgx/v5" "github.com/pokt-network/pocket/persistence/blockstore" "github.com/pokt-network/pocket/persistence/indexer" "github.com/pokt-network/pocket/runtime/genesis" @@ -21,8 +22,13 @@ type PersistenceModule interface { NewReadContext(height int64) (PersistenceReadContext, error) ReleaseWriteContext() error // The module can maintain many read contexts, but only one write context can exist at a time - // BlockStore operations + // BlockStore maps a block height to an *coreTypes.IndexedTransaction GetBlockStore() blockstore.BlockStore + + // TreeStore manages atomic access to a set of merkle trees + // that compose the state hash. + GetTreeStore() TreeStore + NewWriteContext() PersistenceRWContext // Indexer operations @@ -33,6 +39,17 @@ type PersistenceModule interface { HandleDebugMessage(*messaging.DebugMessage) error } +// TreeStore defines the interface for atomic updates and rollbacks to the internal +// merkle trees that compose the state hash of pocket. +type TreeStore interface { + // Update returns the new state hash for a given height. + // * Update inherits the pgx transaction's read view of the database and builds the trees according to that view. + // TODO(#808): Change interface to `Update(pgtx pgx.Tx, height uint64) (string, error)` + Update(pgtx pgx.Tx, txi indexer.TxIndexer, height uint64) (string, error) + // DebugClearAll completely clears the state of the trees. For debugging purposes only. + DebugClearAll() error +} + // Interface defining the context within which the node can operate with the persistence layer. // Operations in the context of a PersistenceContext are isolated from other operations and // other persistence contexts until committed, enabling parallelizability along other operations.