diff --git a/persistence/docs/PROTOCOL_STATE_HASH.md b/persistence/docs/PROTOCOL_STATE_HASH.md index 80a8df33f..75ffd4886 100644 --- a/persistence/docs/PROTOCOL_STATE_HASH.md +++ b/persistence/docs/PROTOCOL_STATE_HASH.md @@ -23,14 +23,14 @@ This document defines how Pocket V1 takes a snapshot of its world state. An intr ### Infrastructural Components -| Component | Data Type | Implementation Options - Examples | Implementation Selected - Current | Example | Use Case | -| --------------------- | ------------------------------------- | ------------------------------------------------------ | --------------------------------- | ------------------- | -------------------------------------------------------------------------------- | -| Data Tables | SQL Database / Engine | MySQL, SQLite, PostgreSQL | PostgresSQL | Validator SQL Table | Validating & updating information when applying a transaction | -| Merkle Trees | Merkle Trie backed by Key-Value Store | Celestia's SMT, Libra's JMT, Cosmos' IAVL, Verkle Tree | Celestia's SMT | Fisherman Trie | Maintains the state of all account based trees | -| Blocks | Serialization Codec | Amino, Protobuf, Thrift, Avro | Protobuf | Block protobuf | Serialized and inserted into the Block Store | -| Objects (e.g. Actors) | Serialization Codec | Amino, Protobuf, Thrift, Avro | Protobuf | Servicer protobuf | Serialized and inserted into the corresponding Tree | -| Block Store | Key Value Store | LevelDB, BadgerDB, RocksDB, BoltDB | BadgerDb | Block Store | Maintains a key-value store of the blockchain blocks | -| Transaction Indexer | Key Value Store | LevelDB, BadgerDB, RocksDB, BoltDB | BadgerDB | Tx Indexer | Indexes transactions in different ways for fast queries, presence checks, etc... | +| Component | Data Type | Implementation Options - Examples | Implementation Selected - Current | Example | Use Case | +| --------------------- | ------------------------------------- | ------------------------------------------------------ | ----------------------------------- | ------------------- | -------------------------------------------------------------------------------- | +| Data Tables | SQL Database / Engine | MySQL, SQLite, PostgreSQL | PostgresSQL | Validator SQL Table | Validating & updating information when applying a transaction | +| Merkle Trees | Merkle Trie backed by Key-Value Store | Celestia's SMT, Libra's JMT, Cosmos' IAVL, Verkle Tree | Pocket's SMT (Forked from Celestia) | Fisherman Trie | Maintains the state of all account based trees | +| Blocks | Serialization Codec | Amino, Protobuf, Thrift, Avro | Protobuf | Block protobuf | Serialized and inserted into the Block Store | +| Objects (e.g. Actors) | Serialization Codec | Amino, Protobuf, Thrift, Avro | Protobuf | Servicer protobuf | Serialized and inserted into the corresponding Tree | +| Block Store | Key Value Store | LevelDB, BadgerDB, RocksDB, BoltDB | BadgerDb | Block Store | Maintains a key-value store of the blockchain blocks | +| Transaction Indexer | Key Value Store | LevelDB, BadgerDB, RocksDB, BoltDB | BadgerDB | Tx Indexer | Indexes transactions in different ways for fast queries, presence checks, etc... | ### Block Proto @@ -40,8 +40,6 @@ The block protobuf that is serialized and store in the block store can be found An individual Merkle Tree is created for each type of actor, record or data type. Each of these is backed by its own key-value store. -Note that the order in which the trees are defined (found in `persistence/state.go`) is important since it determines how the state hash is computed. _TODO(#361): Consider specifying the oder in a `.proto` `enum` rather than a `.go` `iota`._ - **Actor Merkle Trees**: - Applications @@ -71,8 +69,8 @@ This flow shows the interaction between the `PostgresDB` and `MerkleTrees` liste 3. Serialize each record using the corresponding underlying protobuf 4. Insert the serialized record into the corresponding tree (which is back by a key-value store) 5. Compute the root hash of each tree -6. Aggregate all the root hashes by concatenating them together -7. Compute the new `stateHash` by taking a hash of the concatenated hash list +6. Insert the name of the tree and its root hash into the root tree +7. Compute the new `stateHash` by hex encoding the root tree's root hash ```mermaid sequenceDiagram @@ -89,15 +87,15 @@ sequenceDiagram end P->>+PKV: GetRoot() PKV->>-P: rootHash + P->>P: rootTree.Update(stateTreeName, rootHash) end - P->>P: stateHash = hash(concat(rootHashes)) + P->>P: stateHash = hex(rootTree.Root()) + activate P deactivate P ``` -_IMPORTANT: The order in which the `rootHashes` are concatenated is based on the definition in which the trees are ordered in within `state.go`._ - ## Store Block (Commit) When the `Commit(proposer, quorumCert)` function is invoked, the current context is committed to disk. The `PersistenceContext` does the following: diff --git a/persistence/module.go b/persistence/module.go index c60811846..2fb370617 100644 --- a/persistence/module.go +++ b/persistence/module.go @@ -235,6 +235,10 @@ func (m *persistenceModule) GetTxIndexer() indexer.TxIndexer { return m.txIndexer } +func (m *persistenceModule) GetTreeStore() modules.TreeStoreModule { + return m.stateTrees +} + func (m *persistenceModule) GetNetworkID() string { return m.networkId } diff --git a/persistence/test/state_test.go b/persistence/test/state_test.go index ab13b1215..d7a7555cd 100644 --- a/persistence/test/state_test.go +++ b/persistence/test/state_test.go @@ -42,9 +42,9 @@ func TestStateHash_DeterministicStateWhenUpdatingAppStake(t *testing.T) { // logic changes, these hashes will need to be updated based on the test output. // TODO: Add an explicit updateSnapshots flag to the test to make this more clear. stateHashes := []string{ - "6a30f096c86793de894388aad171e84c5ce766cbd82c5a5d36ca53d6c99ac041", - "c5840401da7028948f6d025867249fb9f9a9e738b36158669a64746e5b4f3ed2", - "209a2bda7e9a4495f45281e726777b33f54ac61868c8d8a059719cb5cefd2f75", + "fb1c1b2da242eb6148884e1f11c184c673963b6fcb59feea4ea51c9840e4b56c", + "9ae40f9fd0864c01760c19d6688d1c7bba5ad746fc3f7123cf071b142c2a302a", + "5c13743e9d29e83cbff1301e3750c7583e365b2c3fd81b643ed8db985f46268f", } stakeAmount := initialStakeAmount diff --git a/persistence/trees/module.go b/persistence/trees/module.go index 22de6a1b1..7942ad60a 100644 --- a/persistence/trees/module.go +++ b/persistence/trees/module.go @@ -44,30 +44,47 @@ func (t *treeStore) setupTrees() error { return t.setupInMemory() } - t.merkleTrees = make(map[merkleTree]*smt.SMT, int(numMerkleTrees)) - t.nodeStores = make(map[merkleTree]kvstore.KVStore, int(numMerkleTrees)) + nodeStore, err := kvstore.NewKVStore(fmt.Sprintf("%s/%s_nodes", t.treeStoreDir, RootTreeName)) + if err != nil { + return err + } + t.rootTree = &stateTree{ + name: RootTreeName, + tree: smt.NewSparseMerkleTree(nodeStore, smtTreeHasher), + nodeStore: nodeStore, + } + t.merkleTrees = make(map[string]*stateTree, len(stateTreeNames)) - for tree := merkleTree(0); tree < numMerkleTrees; tree++ { - nodeStore, err := kvstore.NewKVStore(fmt.Sprintf("%s/%s_nodes", t.treeStoreDir, merkleTreeToString[tree])) + for i := 0; i < len(stateTreeNames); i++ { + nodeStore, err := kvstore.NewKVStore(fmt.Sprintf("%s/%s_nodes", t.treeStoreDir, stateTreeNames[i])) if err != nil { return err } - t.nodeStores[tree] = nodeStore - t.merkleTrees[tree] = smt.NewSparseMerkleTree(nodeStore, smtTreeHasher) + t.merkleTrees[stateTreeNames[i]] = &stateTree{ + name: stateTreeNames[i], + tree: smt.NewSparseMerkleTree(nodeStore, smtTreeHasher), + nodeStore: nodeStore, + } } return nil } func (t *treeStore) setupInMemory() error { - t.merkleTrees = make(map[merkleTree]*smt.SMT, int(numMerkleTrees)) - t.nodeStores = make(map[merkleTree]kvstore.KVStore, int(numMerkleTrees)) - - for tree := merkleTree(0); tree < numMerkleTrees; tree++ { + nodeStore := kvstore.NewMemKVStore() + t.rootTree = &stateTree{ + name: RootTreeName, + tree: smt.NewSparseMerkleTree(nodeStore, smtTreeHasher), + nodeStore: nodeStore, + } + t.merkleTrees = make(map[string]*stateTree, len(stateTreeNames)) + for i := 0; i < len(stateTreeNames); i++ { nodeStore := kvstore.NewMemKVStore() // For testing, `smt.NewSimpleMap()` can be used as well - t.nodeStores[tree] = nodeStore - t.merkleTrees[tree] = smt.NewSparseMerkleTree(nodeStore, smtTreeHasher) + t.merkleTrees[stateTreeNames[i]] = &stateTree{ + name: stateTreeNames[i], + tree: smt.NewSparseMerkleTree(nodeStore, smtTreeHasher), + nodeStore: nodeStore, + } } - return nil } diff --git a/persistence/trees/trees.go b/persistence/trees/trees.go index 196677cd6..62824bc89 100644 --- a/persistence/trees/trees.go +++ b/persistence/trees/trees.go @@ -5,14 +5,13 @@ package trees import ( - "bytes" "crypto/sha256" "encoding/hex" "fmt" "hash" + "log" "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" @@ -28,63 +27,50 @@ import ( // as a package level variable for visibility and internal use. var smtTreeHasher hash.Hash = sha256.New() -var merkleTreeToString = map[merkleTree]string{ - appMerkleTree: "app", - valMerkleTree: "val", - fishMerkleTree: "fish", - servicerMerkleTree: "servicer", - - accountMerkleTree: "account", - poolMerkleTree: "pool", +const ( + RootTreeName = "root" + AppTreeName = "app" + ValTreeName = "val" + FishTreeName = "fish" + ServicerTreeName = "servicer" + AccountTreeName = "account" + PoolTreeName = "pool" + TransactionsTreeName = "transactions" + ParamsTreeName = "params" + FlagsTreeName = "flags" +) - transactionsMerkleTree: "transactions", - paramsMerkleTree: "params", - flagsMerkleTree: "flags", +var actorTypeToMerkleTreeName = map[coreTypes.ActorType]string{ + coreTypes.ActorType_ACTOR_TYPE_APP: AppTreeName, + coreTypes.ActorType_ACTOR_TYPE_VAL: ValTreeName, + coreTypes.ActorType_ACTOR_TYPE_FISH: FishTreeName, + coreTypes.ActorType_ACTOR_TYPE_SERVICER: ServicerTreeName, } -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 merkleTreeNameToActorTypeName = map[string]coreTypes.ActorType{ + AppTreeName: coreTypes.ActorType_ACTOR_TYPE_APP, + ValTreeName: coreTypes.ActorType_ACTOR_TYPE_VAL, + FishTreeName: coreTypes.ActorType_ACTOR_TYPE_FISH, + ServicerTreeName: coreTypes.ActorType_ACTOR_TYPE_SERVICER, } -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, +var stateTreeNames = []string{ + // Actor Trees + AppTreeName, ValTreeName, FishTreeName, ServicerTreeName, + // Account Trees + AccountTreeName, PoolTreeName, + // Data Trees + TransactionsTreeName, ParamsTreeName, FlagsTreeName, } -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 -) +// stateTree is a wrapper around the SMT that contains an identifying +// key alongside the tree and nodeStore that backs the tree +type stateTree struct { + name string + tree *smt.SMT + nodeStore kvstore.KVStore +} -// Ensure treeStore implements TreeStore var _ modules.TreeStoreModule = &treeStore{} // treeStore stores a set of merkle trees that @@ -96,8 +82,21 @@ type treeStore struct { base_modules.IntegratableModule treeStoreDir string - merkleTrees map[merkleTree]*smt.SMT - nodeStores map[merkleTree]kvstore.KVStore + rootTree *stateTree + merkleTrees map[string]*stateTree +} + +// 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) { + if name == RootTreeName { + return t.rootTree.tree.Root(), t.rootTree.nodeStore + } + if tree, ok := t.merkleTrees[name]; ok { + return tree.tree.Root(), tree.nodeStore + } + return nil, nil } // Update takes a transaction and a height and updates @@ -111,12 +110,16 @@ func (t *treeStore) Update(pgtx pgx.Tx, height uint64) (string, error) { // 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 := t.rootTree.nodeStore.ClearAll(); err != nil { + return fmt.Errorf("failed to clear root node store: %w", err) + } + t.rootTree.tree = smt.NewSparseMerkleTree(t.rootTree.nodeStore, smtTreeHasher) + for treeName, stateTree := range t.merkleTrees { + nodeStore := stateTree.nodeStore if err := nodeStore.ClearAll(); err != nil { - return fmt.Errorf("failed to clear %s node store: %w", merkleTreeToString[treeType], err) + return fmt.Errorf("failed to clear %s node store: %w", treeName, err) } - t.merkleTrees[treeType] = smt.NewSparseMerkleTree(nodeStore, smtTreeHasher) + stateTree.tree = smt.NewSparseMerkleTree(nodeStore, smtTreeHasher) } return nil } @@ -124,13 +127,13 @@ 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) { - for treeType := merkleTree(0); treeType < numMerkleTrees; treeType++ { - switch treeType { + for treeName := range t.merkleTrees { + switch treeName { // Actor Merkle Trees - case appMerkleTree, valMerkleTree, fishMerkleTree, servicerMerkleTree: - actorType, ok := merkleTreeToActorTypeName[treeType] + case AppTreeName, ValTreeName, FishTreeName, ServicerTreeName: + actorType, ok := merkleTreeNameToActorTypeName[treeName] if !ok { - return "", fmt.Errorf("no actor type found for merkle tree: %v", treeType) + return "", fmt.Errorf("no actor type found for merkle tree: %s", treeName) } actors, err := sql.GetActors(pgtx, actorType, height) @@ -139,11 +142,11 @@ func (t *treeStore) updateMerkleTrees(pgtx pgx.Tx, txi indexer.TxIndexer, height } 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) + return "", fmt.Errorf("failed to update actors tree for treeType: %s, actorType: %v - %w", treeName, actorType, err) } // Account Merkle Trees - case accountMerkleTree: + case AccountTreeName: accounts, err := sql.GetAccounts(pgtx, height) if err != nil { return "", fmt.Errorf("failed to get accounts: %w", err) @@ -151,7 +154,7 @@ func (t *treeStore) updateMerkleTrees(pgtx pgx.Tx, txi indexer.TxIndexer, height if err := t.updateAccountTrees(accounts); err != nil { return "", fmt.Errorf("failed to update account trees: %w", err) } - case poolMerkleTree: + case PoolTreeName: pools, err := sql.GetPools(pgtx, height) if err != nil { return "", fmt.Errorf("failed to get transactions: %w", err) @@ -161,7 +164,7 @@ func (t *treeStore) updateMerkleTrees(pgtx pgx.Tx, txi indexer.TxIndexer, height } // Data Merkle Trees - case transactionsMerkleTree: + case TransactionsTreeName: indexedTxs, err := sql.GetTransactions(txi, height) if err != nil { return "", fmt.Errorf("failed to get transactions: %w", err) @@ -169,7 +172,7 @@ func (t *treeStore) updateMerkleTrees(pgtx pgx.Tx, txi indexer.TxIndexer, height if err := t.updateTransactionsTree(indexedTxs); err != nil { return "", fmt.Errorf("failed to update transactions: %w", err) } - case paramsMerkleTree: + case ParamsTreeName: params, err := sql.GetParams(pgtx, height) if err != nil { return "", fmt.Errorf("failed to get params: %w", err) @@ -177,7 +180,7 @@ func (t *treeStore) updateMerkleTrees(pgtx pgx.Tx, txi indexer.TxIndexer, height if err := t.updateParamsTree(params); err != nil { return "", fmt.Errorf("failed to update params tree: %w", err) } - case flagsMerkleTree: + case FlagsTreeName: flags, err := sql.GetFlags(pgtx, height) if err != nil { return "", fmt.Errorf("failed to get flags from transaction: %w", err) @@ -187,7 +190,7 @@ func (t *treeStore) updateMerkleTrees(pgtx pgx.Tx, txi indexer.TxIndexer, height } // Default default: - panic(fmt.Sprintf("not handled in state commitment update. Merkle tree #{%v}", treeType)) + log.Fatalf("not handled in state commitment update. Merkle tree: %s", treeName) } } @@ -198,28 +201,21 @@ func (t *treeStore) updateMerkleTrees(pgtx pgx.Tx, txi indexer.TxIndexer, height } 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) + for treeName, stateTree := range t.merkleTrees { + if err := stateTree.tree.Commit(); err != nil { + return fmt.Errorf("failed to commit %s: %w", treeName, 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()) + 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) + } } - - // 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[:]) + return hex.EncodeToString(t.rootTree.tree.Root()) } //////////////////////// @@ -243,7 +239,7 @@ func (t *treeStore) updateActorsTree(actorType coreTypes.ActorType, actors []*co if !ok { return fmt.Errorf("no merkle tree found for actor type: %s", actorType) } - if err := t.merkleTrees[merkleTreeName].Update(bzAddr, actorBz); err != nil { + if err := t.merkleTrees[merkleTreeName].tree.Update(bzAddr, actorBz); err != nil { return err } } @@ -267,7 +263,7 @@ func (t *treeStore) updateAccountTrees(accounts []*coreTypes.Account) error { return err } - if err := t.merkleTrees[accountMerkleTree].Update(bzAddr, accBz); err != nil { + if err := t.merkleTrees[AccountTreeName].tree.Update(bzAddr, accBz); err != nil { return err } } @@ -287,7 +283,7 @@ func (t *treeStore) updatePoolTrees(pools []*coreTypes.Account) error { return err } - if err := t.merkleTrees[poolMerkleTree].Update(bzAddr, accBz); err != nil { + if err := t.merkleTrees[PoolTreeName].tree.Update(bzAddr, accBz); err != nil { return err } } @@ -303,7 +299,7 @@ func (t *treeStore) updateTransactionsTree(indexedTxs []*coreTypes.IndexedTransa for _, idxTx := range indexedTxs { txBz := idxTx.GetTx() txHash := crypto.SHA3Hash(txBz) - if err := t.merkleTrees[transactionsMerkleTree].Update(txHash, txBz); err != nil { + if err := t.merkleTrees[TransactionsTreeName].tree.Update(txHash, txBz); err != nil { return err } } @@ -317,7 +313,7 @@ func (t *treeStore) updateParamsTree(params []*coreTypes.Param) error { if err != nil { return err } - if err := t.merkleTrees[paramsMerkleTree].Update(paramKey, paramBz); err != nil { + if err := t.merkleTrees[ParamsTreeName].tree.Update(paramKey, paramBz); err != nil { return err } } @@ -332,7 +328,7 @@ func (t *treeStore) updateFlagsTree(flags []*coreTypes.Flag) error { if err != nil { return err } - if err := t.merkleTrees[flagsMerkleTree].Update(flagKey, flagBz); err != nil { + if err := t.merkleTrees[FlagsTreeName].tree.Update(flagKey, flagBz); err != nil { return err } } diff --git a/shared/modules/persistence_module.go b/shared/modules/persistence_module.go index 1646c81b1..3160ef391 100644 --- a/shared/modules/persistence_module.go +++ b/shared/modules/persistence_module.go @@ -30,6 +30,9 @@ type PersistenceModule interface { GetTxIndexer() indexer.TxIndexer TransactionExists(transactionHash string) (bool, error) + // TreeStore operations + GetTreeStore() TreeStoreModule + // Debugging / development only HandleDebugMessage(*messaging.DebugMessage) error } diff --git a/shared/modules/treestore_module.go b/shared/modules/treestore_module.go index 5f322cbca..f187b18f2 100644 --- a/shared/modules/treestore_module.go +++ b/shared/modules/treestore_module.go @@ -2,6 +2,7 @@ package modules import ( "github.com/jackc/pgx/v5" + "github.com/pokt-network/pocket/persistence/kvstore" ) const ( @@ -27,4 +28,6 @@ type TreeStoreModule interface { Update(pgtx pgx.Tx, height uint64) (string, error) // DebugClearAll completely clears the state of the trees. For debugging purposes only. DebugClearAll() error + // GetTree returns the specified tree's root and nodeStore in order to be imported elsewhere + GetTree(name string) ([]byte, kvstore.KVStore) } diff --git a/utility/session_test.go b/utility/session_test.go index 497d88668..ff544e508 100644 --- a/utility/session_test.go +++ b/utility/session_test.go @@ -25,7 +25,7 @@ func TestSession_GetSession_SingleFishermanSingleServicerBaseCase(t *testing.T) numFishermen := 1 numServicers := 1 // needs to be manually updated if business logic changes - expectedSessionId := "5acf559f1a3faf3bea7eb692fe51bc1e2e5fb687ede0a6daa7d42399da4aa82b" + expectedSessionId := "7a915e89e4805095150e6826dff161ab2612e766b9e6893e6fe747e20a3abfa8" runtimeCfg, utilityMod, _ := prepareEnvironment(t, 5, numServicers, 1, numFishermen)