From 11f6c1c51b89fd4e1d2ff1ea7164eabb60cedfe0 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Tue, 29 Mar 2022 10:26:49 +0200 Subject: [PATCH 1/7] docs(cmd/gossamer): Metrics port fixed from `9090` to `9876` (#2433) --- cmd/gossamer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/gossamer/README.md b/cmd/gossamer/README.md index ec6cba7d00..8b22332b3b 100644 --- a/cmd/gossamer/README.md +++ b/cmd/gossamer/README.md @@ -188,7 +188,7 @@ defined in [lib/runtime/wasmer/exports.go](../../lib/runtime/wasmer/exports.go). Gossamer publishes telemetry data and also includes an embedded Prometheus server that reports metrics. The metrics capabilities are defined in the [dot/metrics](../../dot/metrics) package and build on [the metrics library that is included with Go Ethereum](https://github.com/ethereum/go-ethereum/blob/master/metrics/README.md). -The default listening address for Prometheus metrics is `localhost:9090`, and Gossamer allows the user to configure this parameter with the +The default listening address for Prometheus metrics is `localhost:9876`, and Gossamer allows the user to configure this parameter with the `--metrics-address` command-line parameter. The Gossamer telemetry server publishes telemetry data that is compatible with [Polkadot Telemetry](https://github.com/paritytech/substrate-telemetry) and [its helpful UI](https://telemetry.polkadot.io/). From 19a2b4e36d8f46b062aa1b139edc514f12f119f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 14:56:44 -0400 Subject: [PATCH 2/7] chore(deps): bump google.golang.org/protobuf from 1.27.1 to 1.28.0 (#2440) Bumps [google.golang.org/protobuf](https://github.com/protocolbuffers/protobuf-go) from 1.27.1 to 1.28.0. - [Release notes](https://github.com/protocolbuffers/protobuf-go/releases) - [Changelog](https://github.com/protocolbuffers/protobuf-go/blob/master/release.bash) - [Commits](https://github.com/protocolbuffers/protobuf-go/compare/v1.27.1...v1.28.0) --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 04e823cfa7..2c9f0177b6 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/wasmerio/go-ext-wasm v0.3.2-0.20200326095750-0a32be6068ec golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 - google.golang.org/protobuf v1.27.1 + google.golang.org/protobuf v1.28.0 ) require ( diff --git a/go.sum b/go.sum index f9326a0968..65f71bcae7 100644 --- a/go.sum +++ b/go.sum @@ -1793,8 +1793,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From e700d5e4d7caad23471167f3fea3a090bd2c2773 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Thu, 31 Mar 2022 12:05:44 +0200 Subject: [PATCH 3/7] chore(tests): refactor tests/utils/request_utils.go (#2385) - Inject context to `PostRPC` and `PostRPCWithRetry` - Swap method and url arguments for `PostRPC` and `PostRPCWithRetry` - Remove some global variables - Wrap all possible errors - `PostRPCWithRetry` retries until context is canceled - Keep 1s timeout for RPC calls --- tests/rpc/rpc_00_test.go | 6 +- tests/rpc/rpc_01-system_test.go | 6 +- tests/rpc/rpc_02-author_test.go | 6 +- tests/rpc/rpc_03-chain_test.go | 7 ++- tests/rpc/rpc_04-offchain_test.go | 6 +- tests/rpc/rpc_05-state_test.go | 31 +++++++++-- tests/rpc/rpc_06-engine_test.go | 6 +- tests/rpc/rpc_07-payment_test.go | 6 +- tests/rpc/rpc_08-contracts_test.go | 6 +- tests/rpc/rpc_09-babe_test.go | 6 +- tests/rpc/system_integration_test.go | 8 ++- tests/stress/grandpa_test.go | 35 +++++++++--- tests/stress/helpers.go | 66 +++++++++++++++------- tests/stress/network_test.go | 9 ++- tests/stress/stress_test.go | 77 +++++++++++++++++++------- tests/sync/sync_test.go | 12 +++- tests/utils/chain.go | 49 ++++++++++------ tests/utils/common.go | 18 ------ tests/utils/dev.go | 22 +++++--- tests/utils/framework.go | 6 +- tests/utils/gossamer_utils.go | 22 ++++++-- tests/utils/request_utils.go | 83 +++++++++++++++++----------- tests/utils/system.go | 8 ++- 23 files changed, 354 insertions(+), 147 deletions(-) diff --git a/tests/rpc/rpc_00_test.go b/tests/rpc/rpc_00_test.go index 368d66f847..e1b9b462e0 100644 --- a/tests/rpc/rpc_00_test.go +++ b/tests/rpc/rpc_00_test.go @@ -4,6 +4,7 @@ package rpc import ( + "context" "fmt" "os" "reflect" @@ -38,13 +39,14 @@ type testCase struct { skip bool } -func getResponse(t *testing.T, test *testCase) interface{} { +func getResponse(ctx context.Context, t *testing.T, test *testCase) interface{} { if test.skip { t.Skip("RPC endpoint not yet implemented") return nil } - respBody, err := utils.PostRPC(test.method, utils.NewEndpoint(currentPort), test.params) + endpoint := utils.NewEndpoint(currentPort) + respBody, err := utils.PostRPC(ctx, endpoint, test.method, test.params) require.NoError(t, err) target := reflect.New(reflect.TypeOf(test.expected)).Interface() diff --git a/tests/rpc/rpc_01-system_test.go b/tests/rpc/rpc_01-system_test.go index 1599a88116..bf43135990 100644 --- a/tests/rpc/rpc_01-system_test.go +++ b/tests/rpc/rpc_01-system_test.go @@ -4,6 +4,7 @@ package rpc import ( + "context" "testing" "time" @@ -97,7 +98,10 @@ func TestSystemRPC(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - target := getResponse(t, test) + ctx := context.Background() + getResponseCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + target := getResponse(getResponseCtx, t, test) switch v := target.(type) { case *modules.SystemHealthResponse: diff --git a/tests/rpc/rpc_02-author_test.go b/tests/rpc/rpc_02-author_test.go index 07ab8da9d9..9db0aabddd 100644 --- a/tests/rpc/rpc_02-author_test.go +++ b/tests/rpc/rpc_02-author_test.go @@ -5,6 +5,7 @@ package rpc import ( "bytes" + "context" "fmt" "testing" "time" @@ -139,7 +140,10 @@ func TestAuthorRPC(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - _ = getResponse(t, test) + ctx := context.Background() + getResponseCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + _ = getResponse(getResponseCtx, t, test) }) } diff --git a/tests/rpc/rpc_03-chain_test.go b/tests/rpc/rpc_03-chain_test.go index 6276b24fa8..31111ccb5d 100644 --- a/tests/rpc/rpc_03-chain_test.go +++ b/tests/rpc/rpc_03-chain_test.go @@ -4,6 +4,7 @@ package rpc import ( + "context" "log" "testing" "time" @@ -72,7 +73,11 @@ func TestChainRPC(t *testing.T) { test.params = "[\"" + chainBlockHeaderHash + "\"]" } - target := getResponse(t, test) + ctx := context.Background() + + getResponseCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + target := getResponse(getResponseCtx, t, test) switch v := target.(type) { case *modules.ChainBlockHeaderResponse: diff --git a/tests/rpc/rpc_04-offchain_test.go b/tests/rpc/rpc_04-offchain_test.go index 52cabb20bc..b31dba704c 100644 --- a/tests/rpc/rpc_04-offchain_test.go +++ b/tests/rpc/rpc_04-offchain_test.go @@ -4,6 +4,7 @@ package rpc import ( + "context" "testing" "time" @@ -43,7 +44,10 @@ func TestOffchainRPC(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - _ = getResponse(t, test) + ctx := context.Background() + getResponseCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + _ = getResponse(getResponseCtx, t, test) }) } diff --git a/tests/rpc/rpc_05-state_test.go b/tests/rpc/rpc_05-state_test.go index cd948c32b1..bb8920dacd 100644 --- a/tests/rpc/rpc_05-state_test.go +++ b/tests/rpc/rpc_05-state_test.go @@ -4,6 +4,7 @@ package rpc import ( + "context" "fmt" "testing" "time" @@ -33,7 +34,11 @@ func TestStateRPCResponseValidation(t *testing.T) { time.Sleep(time.Second) // give server a second to start - blockHash, err := utils.GetBlockHash(t, nodes[0], "") + ctx := context.Background() + + getBlockHashCtx, cancel := context.WithTimeout(ctx, time.Second) + blockHash, err := utils.GetBlockHash(getBlockHashCtx, t, nodes[0], "") + cancel() require.NoError(t, err) testCases := []*testCase{ @@ -118,7 +123,10 @@ func TestStateRPCResponseValidation(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - _ = getResponse(t, test) + ctx := context.Background() + getResponseCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + _ = getResponse(getResponseCtx, t, test) }) } @@ -142,7 +150,11 @@ func TestStateRPCAPI(t *testing.T) { time.Sleep(5 * time.Second) // Wait for block production - blockHash, err := utils.GetBlockHash(t, nodes[0], "") + ctx := context.Background() + + getBlockHashCtx, cancel := context.WithTimeout(ctx, time.Second) + blockHash, err := utils.GetBlockHash(getBlockHashCtx, t, nodes[0], "") + cancel() require.NoError(t, err) const ( @@ -319,7 +331,11 @@ func TestStateRPCAPI(t *testing.T) { // Cases for valid block hash in RPC params for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - respBody, err := utils.PostRPC(test.method, utils.NewEndpoint(nodes[0].RPCPort), test.params) + ctx := context.Background() + postRPCCtx, cancel := context.WithTimeout(ctx, time.Second) + endpoint := utils.NewEndpoint(nodes[0].RPCPort) + respBody, err := utils.PostRPC(postRPCCtx, endpoint, test.method, test.params) + cancel() require.NoError(t, err) require.Contains(t, string(respBody), test.expected) @@ -351,7 +367,12 @@ func TestRPCStructParamUnmarshal(t *testing.T) { params: `[["0xf2794c22e353e9a839f12faab03a911bf68967d635641a7087e53f2bff1ecad3c6756fee45ec79ead60347fffb770bcdf0ec74da701ab3d6495986fe1ecc3027"],"0xa32c60dee8647b07435ae7583eb35cee606209a595718562dd4a486a07b6de15", null]`, //nolint:lll } t.Run(test.description, func(t *testing.T) { - respBody, err := utils.PostRPC(test.method, utils.NewEndpoint(nodes[0].RPCPort), test.params) + ctx := context.Background() + + postRPCCtx, cancel := context.WithTimeout(ctx, time.Second) + endpoint := utils.NewEndpoint(nodes[0].RPCPort) + respBody, err := utils.PostRPC(postRPCCtx, endpoint, test.method, test.params) + cancel() require.NoError(t, err) require.NotContains(t, string(respBody), "json: cannot unmarshal") fmt.Println(string(respBody)) diff --git a/tests/rpc/rpc_06-engine_test.go b/tests/rpc/rpc_06-engine_test.go index 688bffb192..4f5a3020ab 100644 --- a/tests/rpc/rpc_06-engine_test.go +++ b/tests/rpc/rpc_06-engine_test.go @@ -4,6 +4,7 @@ package rpc import ( + "context" "testing" "time" @@ -38,7 +39,10 @@ func TestEngineRPC(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - _ = getResponse(t, test) + ctx := context.Background() + getResponseCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + _ = getResponse(getResponseCtx, t, test) }) } diff --git a/tests/rpc/rpc_07-payment_test.go b/tests/rpc/rpc_07-payment_test.go index 1318028d93..8639ff417d 100644 --- a/tests/rpc/rpc_07-payment_test.go +++ b/tests/rpc/rpc_07-payment_test.go @@ -4,6 +4,7 @@ package rpc import ( + "context" "testing" "time" @@ -33,7 +34,10 @@ func TestPaymentRPC(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - _ = getResponse(t, test) + ctx := context.Background() + getResponseCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + _ = getResponse(getResponseCtx, t, test) }) } diff --git a/tests/rpc/rpc_08-contracts_test.go b/tests/rpc/rpc_08-contracts_test.go index 8f16fea724..21b33d1283 100644 --- a/tests/rpc/rpc_08-contracts_test.go +++ b/tests/rpc/rpc_08-contracts_test.go @@ -4,6 +4,7 @@ package rpc import ( + "context" "testing" "time" @@ -38,7 +39,10 @@ func TestContractsRPC(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - _ = getResponse(t, test) + ctx := context.Background() + getResponseCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + _ = getResponse(getResponseCtx, t, test) }) } diff --git a/tests/rpc/rpc_09-babe_test.go b/tests/rpc/rpc_09-babe_test.go index 38b62ebb5a..0f8318ffea 100644 --- a/tests/rpc/rpc_09-babe_test.go +++ b/tests/rpc/rpc_09-babe_test.go @@ -4,6 +4,7 @@ package rpc import ( + "context" "testing" "time" @@ -33,7 +34,10 @@ func TestBabeRPC(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - _ = getResponse(t, test) + ctx := context.Background() + getResponseCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + _ = getResponse(getResponseCtx, t, test) }) } diff --git a/tests/rpc/system_integration_test.go b/tests/rpc/system_integration_test.go index 70871cdf19..809e172b28 100644 --- a/tests/rpc/system_integration_test.go +++ b/tests/rpc/system_integration_test.go @@ -4,6 +4,8 @@ package rpc import ( + "context" + "fmt" "reflect" "strconv" "testing" @@ -53,7 +55,11 @@ func TestStableNetworkRPC(t *testing.T) { for _, test := range testsCases { t.Run(test.description, func(t *testing.T) { - respBody, err := utils.PostRPC(test.method, "http://"+utils.HOSTNAME+":"+utils.PORT, "{}") + ctx := context.Background() + + endpoint := fmt.Sprintf("http://%s:%s", utils.HOSTNAME, utils.PORT) + const params = "{}" + respBody, err := utils.PostRPC(ctx, endpoint, test.method, params) require.NoError(t, err) target := reflect.New(reflect.TypeOf(test.expected)).Interface() diff --git a/tests/stress/grandpa_test.go b/tests/stress/grandpa_test.go index 704236a58e..a98935619b 100644 --- a/tests/stress/grandpa_test.go +++ b/tests/stress/grandpa_test.go @@ -4,6 +4,7 @@ package stress import ( + "context" "os" "testing" "time" @@ -25,11 +26,16 @@ func TestStress_Grandpa_OneAuthority(t *testing.T) { time.Sleep(time.Second * 10) - compareChainHeadsWithRetry(t, nodes) - prev, _ := compareFinalizedHeads(t, nodes) + ctx := context.Background() + + const getChainHeadTimeout = time.Second + compareChainHeadsWithRetry(ctx, t, nodes, getChainHeadTimeout) + + const getFinalizedHeadTimeout = time.Second + prev, _ := compareFinalizedHeads(ctx, t, nodes, getFinalizedHeadTimeout) time.Sleep(time.Second * 10) - curr, _ := compareFinalizedHeads(t, nodes) + curr, _ := compareFinalizedHeads(ctx, t, nodes, getFinalizedHeadTimeout) require.NotEqual(t, prev, curr) } @@ -48,9 +54,13 @@ func TestStress_Grandpa_ThreeAuthorities(t *testing.T) { require.Len(t, errList, 0) }() + ctx := context.Background() + numRounds := 5 for i := 1; i < numRounds+1; i++ { - fin, err := compareFinalizedHeadsWithRetry(t, nodes, uint64(i)) + const getFinalizedHeadByRoundTimeout = time.Second + fin, err := compareFinalizedHeadsWithRetry(ctx, t, + nodes, uint64(i), getFinalizedHeadByRoundTimeout) require.NoError(t, err) t.Logf("finalised hash in round %d: %s", i, fin) } @@ -70,9 +80,13 @@ func TestStress_Grandpa_SixAuthorities(t *testing.T) { require.Len(t, errList, 0) }() + ctx := context.Background() + numRounds := 10 for i := 1; i < numRounds+1; i++ { - fin, err := compareFinalizedHeadsWithRetry(t, nodes, uint64(i)) + const getFinalizedHeadByRoundTimeout = time.Second + fin, err := compareFinalizedHeadsWithRetry(ctx, t, nodes, + uint64(i), getFinalizedHeadByRoundTimeout) require.NoError(t, err) t.Logf("finalised hash in round %d: %s", i, fin) } @@ -95,9 +109,13 @@ func TestStress_Grandpa_NineAuthorities(t *testing.T) { require.Len(t, errList, 0) }() + ctx := context.Background() + numRounds := 3 for i := 1; i < numRounds+1; i++ { - fin, err := compareFinalizedHeadsWithRetry(t, nodes, uint64(i)) + const getFinalizedHeadByRoundTimeout = time.Second + fin, err := compareFinalizedHeadsWithRetry(ctx, t, nodes, + uint64(i), getFinalizedHeadByRoundTimeout) require.NoError(t, err) t.Logf("finalised hash in round %d: %s", i, fin) } @@ -129,9 +147,12 @@ func TestStress_Grandpa_CatchUp(t *testing.T) { require.NoError(t, err) nodes = append(nodes, node) + ctx := context.Background() + numRounds := 10 for i := 1; i < numRounds+1; i++ { - fin, err := compareFinalizedHeadsWithRetry(t, nodes, uint64(i)) + const getFinalizedHeadByRoundTimeout = time.Second + fin, err := compareFinalizedHeadsWithRetry(ctx, t, nodes, uint64(i), getFinalizedHeadByRoundTimeout) require.NoError(t, err) t.Logf("finalised hash in round %d: %s", i, fin) } diff --git a/tests/stress/helpers.go b/tests/stress/helpers.go index 163253fffc..3f2a9c7f40 100644 --- a/tests/stress/helpers.go +++ b/tests/stress/helpers.go @@ -26,15 +26,18 @@ var ( // compareChainHeads calls getChainHead for each node in the array // it returns a map of chainHead hashes to node key names, and an error if the hashes don't all match -func compareChainHeads(t *testing.T, nodes []*utils.Node) (map[common.Hash][]string, error) { - hashes := make(map[common.Hash][]string) +func compareChainHeads(ctx context.Context, t *testing.T, nodes []*utils.Node, + getChainHeadTimeout time.Duration) (hashes map[common.Hash][]string, err error) { + hashes = make(map[common.Hash][]string) for _, node := range nodes { - header := utils.GetChainHead(t, node) + getChainHeadCtx, cancel := context.WithTimeout(ctx, getChainHeadTimeout) + header := utils.GetChainHead(getChainHeadCtx, t, node) + cancel() + logger.Infof("got header with hash %s from node with key %s", header.Hash(), node.Key) hashes[header.Hash()] = append(hashes[header.Hash()], node.Key) } - var err error if len(hashes) != 1 { err = errChainHeadMismatch } @@ -43,17 +46,26 @@ func compareChainHeads(t *testing.T, nodes []*utils.Node) (map[common.Hash][]str } // compareChainHeadsWithRetry calls compareChainHeads, retrying up to maxRetries times if it errors. -func compareChainHeadsWithRetry(t *testing.T, nodes []*utils.Node) error { +func compareChainHeadsWithRetry(ctx context.Context, t *testing.T, nodes []*utils.Node, + getChainHeadTimeout time.Duration) error { var hashes map[common.Hash][]string var err error for i := 0; i < maxRetries; i++ { - hashes, err = compareChainHeads(t, nodes) + hashes, err = compareChainHeads(ctx, t, nodes, getChainHeadTimeout) if err == nil { break } - time.Sleep(time.Second) + timer := time.NewTimer(time.Second) + select { + case <-timer.C: + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return err // last error + } } if err != nil { @@ -81,7 +93,7 @@ func compareBlocksByNumber(ctx context.Context, t *testing.T, nodes []*utils.Nod } for { // retry until context gets canceled - result.hash, result.err = utils.GetBlockHash(t, node, num) + result.hash, result.err = utils.GetBlockHash(ctx, t, node, num) if err := ctx.Err(); err != nil { result.err = err @@ -123,15 +135,18 @@ func compareBlocksByNumber(ctx context.Context, t *testing.T, nodes []*utils.Nod // compareFinalizedHeads calls getFinalizedHeadByRound for each node in the array // it returns a map of finalisedHead hashes to node key names, and an error if the hashes don't all match -func compareFinalizedHeads(t *testing.T, nodes []*utils.Node) (map[common.Hash][]string, error) { - hashes := make(map[common.Hash][]string) +func compareFinalizedHeads(ctx context.Context, t *testing.T, nodes []*utils.Node, + getFinalizedHeadTimeout time.Duration) (hashes map[common.Hash][]string, err error) { + hashes = make(map[common.Hash][]string) for _, node := range nodes { - hash := utils.GetFinalizedHead(t, node) + getFinalizedHeadCtx, cancel := context.WithTimeout(ctx, getFinalizedHeadTimeout) + hash := utils.GetFinalizedHead(getFinalizedHeadCtx, t, node) + cancel() + logger.Infof("got finalised head with hash %s from node with key %s", hash, node.Key) hashes[hash] = append(hashes[hash], node.Key) } - var err error if len(hashes) == 0 { err = errNoFinalizedBlock } @@ -145,10 +160,15 @@ func compareFinalizedHeads(t *testing.T, nodes []*utils.Node) (map[common.Hash][ // compareFinalizedHeadsByRound calls getFinalizedHeadByRound for each node in the array // it returns a map of finalisedHead hashes to node key names, and an error if the hashes don't all match -func compareFinalizedHeadsByRound(t *testing.T, nodes []*utils.Node, round uint64) (map[common.Hash][]string, error) { - hashes := make(map[common.Hash][]string) +func compareFinalizedHeadsByRound(ctx context.Context, t *testing.T, nodes []*utils.Node, + round uint64, getFinalizedHeadByRoundTimeout time.Duration) ( + hashes map[common.Hash][]string, err error) { + hashes = make(map[common.Hash][]string) for _, node := range nodes { - hash, err := utils.GetFinalizedHeadByRound(t, node, round) + getFinalizedHeadByRoundCtx, cancel := context.WithTimeout(ctx, getFinalizedHeadByRoundTimeout) + hash, err := utils.GetFinalizedHeadByRound(getFinalizedHeadByRoundCtx, t, node, round) + cancel() + if err != nil { return nil, err } @@ -157,7 +177,6 @@ func compareFinalizedHeadsByRound(t *testing.T, nodes []*utils.Node, round uint6 hashes[hash] = append(hashes[hash], node.Key) } - var err error if len(hashes) == 0 { err = errNoFinalizedBlock } @@ -171,12 +190,14 @@ func compareFinalizedHeadsByRound(t *testing.T, nodes []*utils.Node, round uint6 // compareFinalizedHeadsWithRetry calls compareFinalizedHeadsByRound, retrying up to maxRetries times if it errors. // it returns the finalised hash if it succeeds -func compareFinalizedHeadsWithRetry(t *testing.T, nodes []*utils.Node, round uint64) (common.Hash, error) { +func compareFinalizedHeadsWithRetry(ctx context.Context, t *testing.T, + nodes []*utils.Node, round uint64, + getFinalizedHeadByRoundTimeout time.Duration) ( + hash common.Hash, err error) { var hashes map[common.Hash][]string - var err error for i := 0; i < maxRetries; i++ { - hashes, err = compareFinalizedHeadsByRound(t, nodes, round) + hashes, err = compareFinalizedHeadsByRound(ctx, t, nodes, round, getFinalizedHeadByRoundTimeout) if err == nil { break } @@ -199,8 +220,11 @@ func compareFinalizedHeadsWithRetry(t *testing.T, nodes []*utils.Node, round uin return common.Hash{}, nil } -func getPendingExtrinsics(t *testing.T, node *utils.Node) []string { - respBody, err := utils.PostRPC(utils.AuthorPendingExtrinsics, utils.NewEndpoint(node.RPCPort), "[]") +func getPendingExtrinsics(ctx context.Context, t *testing.T, node *utils.Node) []string { + endpoint := utils.NewEndpoint(node.RPCPort) + method := utils.AuthorPendingExtrinsics + const params = "[]" + respBody, err := utils.PostRPC(ctx, endpoint, method, params) require.NoError(t, err) exts := new(modules.PendingExtrinsicsResponse) diff --git a/tests/stress/network_test.go b/tests/stress/network_test.go index b8115c9cf0..31c3a3bc0b 100644 --- a/tests/stress/network_test.go +++ b/tests/stress/network_test.go @@ -4,6 +4,7 @@ package stress import ( + "context" "testing" "time" @@ -27,8 +28,14 @@ func TestNetwork_MaxPeers(t *testing.T) { // wait for nodes to connect time.Sleep(time.Second * 10) + ctx := context.Background() + for i, node := range nodes { - peers := utils.GetPeers(t, node) + const getPeersTimeout = time.Second + getPeersCtx, cancel := context.WithTimeout(ctx, getPeersTimeout) + peers := utils.GetPeers(getPeersCtx, t, node) + cancel() + t.Logf("node %d: peer count=%d", i, len(peers)) require.LessOrEqual(t, len(peers), 5) } diff --git a/tests/stress/stress_test.go b/tests/stress/stress_test.go index 91d8e1bdbd..830acd1076 100644 --- a/tests/stress/stress_test.go +++ b/tests/stress/stress_test.go @@ -142,7 +142,10 @@ func TestSync_Basic(t *testing.T) { require.Len(t, errList, 0) }() - err = compareChainHeadsWithRetry(t, nodes) + ctx := context.Background() + const getChainHeadTimeout = time.Second + + err = compareChainHeadsWithRetry(ctx, t, nodes, getChainHeadTimeout) require.NoError(t, err) } @@ -162,16 +165,24 @@ func TestSync_MultipleEpoch(t *testing.T) { time.Sleep(time.Second * 10) - slotDuration := utils.SlotDuration(t, nodes[0]) - epochLength := utils.EpochLength(t, nodes[0]) + ctx := context.Background() + + slotDurationCtx, cancel := context.WithTimeout(ctx, time.Second) + slotDuration := utils.SlotDuration(slotDurationCtx, t, nodes[0]) + cancel() + + epochLengthCtx, cancel := context.WithTimeout(ctx, time.Second) + epochLength := utils.EpochLength(epochLengthCtx, t, nodes[0]) + cancel() // Wait for epoch to pass time.Sleep(time.Duration(uint64(slotDuration.Nanoseconds()) * epochLength)) - ctx := context.Background() - // Just checking that everythings operating as expected - header := utils.GetChainHead(t, nodes[0]) + getChainHeadCtx, cancel := context.WithTimeout(ctx, time.Second) + header := utils.GetChainHead(getChainHeadCtx, t, nodes[0]) + cancel() + currentHeight := header.Number for i := uint(0); i < currentHeight; i++ { t.Log("comparing...", i) @@ -235,8 +246,12 @@ func TestSync_Bench(t *testing.T) { false, true) require.NoError(t, err) + ctx := context.Background() + for { - header, err := utils.GetChainHeadWithError(t, alice) + getChainHeadCtx, cancel := context.WithTimeout(ctx, time.Second) + header, err := utils.GetChainHeadWithError(getChainHeadCtx, t, alice) + cancel() if err != nil { continue } @@ -248,7 +263,10 @@ func TestSync_Bench(t *testing.T) { time.Sleep(3 * time.Second) } - err = utils.PauseBABE(t, alice) + pauseBabeCtx, cancel := context.WithTimeout(ctx, time.Second) + err = utils.PauseBABE(pauseBabeCtx, alice) + cancel() + require.NoError(t, err) t.Log("BABE paused") @@ -274,7 +292,10 @@ func TestSync_Bench(t *testing.T) { t.Fatal("did not sync") } - head, err := utils.GetChainHeadWithError(t, bob) + getChainHeadCtx, getChainHeadCancel := context.WithTimeout(ctx, time.Second) + head, err := utils.GetChainHeadWithError(getChainHeadCtx, t, bob) + getChainHeadCancel() + if err != nil { continue } @@ -297,8 +318,6 @@ func TestSync_Bench(t *testing.T) { // assert block is correct t.Log("comparing block...", numBlocks) - ctx := context.Background() - const compareTimeout = 5 * time.Second compareCtx, cancel := context.WithTimeout(ctx, compareTimeout) @@ -449,7 +468,12 @@ func TestSync_SubmitExtrinsic(t *testing.T) { extEnc, err := types.EncodeToHexString(ext) require.NoError(t, err) - prevHeader := utils.GetChainHead(t, nodes[idx]) // get starting header so that we can lookup blocks by number later + ctx := context.Background() + + // get starting header so that we can lookup blocks by number later + getChainHeadCtx, getChainHeadCancel := context.WithTimeout(ctx, time.Second) + prevHeader := utils.GetChainHead(getChainHeadCtx, t, nodes[idx]) + getChainHeadCancel() // Send the extrinsic hash, err := api.RPC.Author.SubmitExtrinsic(ext) @@ -460,7 +484,10 @@ func TestSync_SubmitExtrinsic(t *testing.T) { // wait until there's no more pending extrinsics for i := 0; i < maxRetries; i++ { - exts := getPendingExtrinsics(t, nodes[idx]) + getPendingExtsCtx, getPendingExtsCancel := context.WithTimeout(ctx, time.Second) + exts := getPendingExtrinsics(getPendingExtsCtx, t, nodes[idx]) + getPendingExtsCancel() + if len(exts) == 0 { break } @@ -468,7 +495,9 @@ func TestSync_SubmitExtrinsic(t *testing.T) { time.Sleep(time.Second) } - header := utils.GetChainHead(t, nodes[idx]) + getChainHeadCtx, cancel := context.WithTimeout(ctx, time.Second) + header := utils.GetChainHead(getChainHeadCtx, t, nodes[idx]) + cancel() // search from child -> parent blocks for extrinsic var ( @@ -477,7 +506,10 @@ func TestSync_SubmitExtrinsic(t *testing.T) { ) for i := 0; i < maxRetries; i++ { - block := utils.GetBlock(t, nodes[idx], header.ParentHash) + getBlockCtx, getBlockCancel := context.WithTimeout(ctx, time.Second) + block := utils.GetBlock(getBlockCtx, t, nodes[idx], header.ParentHash) + getBlockCancel() + if block == nil { // couldn't get block, increment retry counter continue @@ -511,8 +543,6 @@ func TestSync_SubmitExtrinsic(t *testing.T) { require.True(t, included) - ctx := context.Background() - const compareTimeout = 5 * time.Second compareCtx, cancel := context.WithTimeout(ctx, compareTimeout) @@ -708,12 +738,21 @@ func TestStress_SecondarySlotProduction(t *testing.T) { secondaryPlainCount := 0 secondaryVRFCount := 0 + ctx := context.Background() + for i := 1; i < 10; i++ { fmt.Printf("%d iteration\n", i) - hash, err := utils.GetBlockHash(t, nodes[0], fmt.Sprintf("%d", i)) + + getBlockHashCtx, cancel := context.WithTimeout(ctx, time.Second) + hash, err := utils.GetBlockHash(getBlockHashCtx, t, nodes[0], fmt.Sprintf("%d", i)) + cancel() + require.NoError(t, err) - block := utils.GetBlock(t, nodes[0], hash) + getBlockCtx, cancel := context.WithTimeout(ctx, time.Second) + block := utils.GetBlock(getBlockCtx, t, nodes[0], hash) + cancel() + header := block.Header preDigestItem := header.Digest.Types[0] diff --git a/tests/sync/sync_test.go b/tests/sync/sync_test.go index 2c8d94299a..ff9512b8f2 100644 --- a/tests/sync/sync_test.go +++ b/tests/sync/sync_test.go @@ -4,6 +4,7 @@ package sync import ( + "context" "fmt" "log" "os" @@ -62,11 +63,20 @@ func TestMain(m *testing.M) { // this starts nodes and runs RPC calls (which loads db) func TestCalls(t *testing.T) { + ctx := context.Background() + err := framework.StartNodes(t) require.Len(t, err, 0) for _, call := range tests { time.Sleep(call.delay) - _, err := framework.CallRPC(call.nodeIdx, call.method, call.params) + + const callRPCTimeout = time.Second + callRPCCtx, cancel := context.WithTimeout(ctx, callRPCTimeout) + + _, err := framework.CallRPC(callRPCCtx, call.nodeIdx, call.method, call.params) + + cancel() + require.NoError(t, err) } diff --git a/tests/utils/chain.go b/tests/utils/chain.go index 299e3808d4..3b707b7ff5 100644 --- a/tests/utils/chain.go +++ b/tests/utils/chain.go @@ -4,9 +4,11 @@ package utils import ( + "context" "fmt" "strconv" "testing" + "time" "github.com/ChainSafe/gossamer/dot/rpc/modules" "github.com/ChainSafe/gossamer/dot/types" @@ -15,8 +17,10 @@ import ( ) // GetChainHead calls the endpoint chain_getHeader to get the latest chain head -func GetChainHead(t *testing.T, node *Node) *types.Header { - respBody, err := PostRPC(ChainGetHeader, NewEndpoint(node.RPCPort), "[]") +func GetChainHead(ctx context.Context, t *testing.T, node *Node) *types.Header { + endpoint := NewEndpoint(node.RPCPort) + const params = "[]" + respBody, err := PostRPC(ctx, endpoint, ChainGetHeader, params) require.NoError(t, err) header := new(modules.ChainBlockHeaderResponse) @@ -27,8 +31,10 @@ func GetChainHead(t *testing.T, node *Node) *types.Header { } // GetChainHeadWithError calls the endpoint chain_getHeader to get the latest chain head -func GetChainHeadWithError(t *testing.T, node *Node) (*types.Header, error) { - respBody, err := PostRPC(ChainGetHeader, NewEndpoint(node.RPCPort), "[]") +func GetChainHeadWithError(ctx context.Context, t *testing.T, node *Node) (*types.Header, error) { + endpoint := NewEndpoint(node.RPCPort) + const params = "[]" + respBody, err := PostRPC(ctx, endpoint, ChainGetHeader, params) require.NoError(t, err) header := new(modules.ChainBlockHeaderResponse) @@ -40,12 +46,14 @@ func GetChainHeadWithError(t *testing.T, node *Node) (*types.Header, error) { return headerResponseToHeader(t, header), nil } -// GetBlockHash calls the endpoint chain_getBlockHash to get the latest chain head -func GetBlockHash(t *testing.T, node *Node, num string) (common.Hash, error) { - respBody, err := PostRPCWithRetry(ChainGetBlockHash, NewEndpoint(node.RPCPort), "["+num+"]", 5) - if err != nil { - return common.Hash{}, err - } +// GetBlockHash calls the endpoint chain_getBlockHash to get the latest chain head. +// It will block until a response is received or the context gets canceled. +func GetBlockHash(ctx context.Context, t *testing.T, node *Node, num string) (common.Hash, error) { + endpoint := NewEndpoint(node.RPCPort) + params := "[" + num + "]" + const requestWait = time.Second + respBody, err := PostRPCWithRetry(ctx, endpoint, ChainGetBlockHash, params, requestWait) + require.NoError(t, err) var hash string err = DecodeRPC(t, respBody, &hash) @@ -56,8 +64,11 @@ func GetBlockHash(t *testing.T, node *Node, num string) (common.Hash, error) { } // GetFinalizedHead calls the endpoint chain_getFinalizedHead to get the latest finalised head -func GetFinalizedHead(t *testing.T, node *Node) common.Hash { - respBody, err := PostRPC(ChainGetFinalizedHead, NewEndpoint(node.RPCPort), "[]") +func GetFinalizedHead(ctx context.Context, t *testing.T, node *Node) common.Hash { + endpoint := NewEndpoint(node.RPCPort) + method := ChainGetFinalizedHead + const params = "[]" + respBody, err := PostRPC(ctx, endpoint, method, params) require.NoError(t, err) var hash string @@ -68,9 +79,12 @@ func GetFinalizedHead(t *testing.T, node *Node) common.Hash { // GetFinalizedHeadByRound calls the endpoint chain_getFinalizedHeadByRound to get the finalised head at a given round // TODO: add setID, hard-coded at 1 for now -func GetFinalizedHeadByRound(t *testing.T, node *Node, round uint64) (common.Hash, error) { +func GetFinalizedHeadByRound(ctx context.Context, t *testing.T, node *Node, round uint64) (common.Hash, error) { p := strconv.Itoa(int(round)) - respBody, err := PostRPC(ChainGetFinalizedHeadByRound, NewEndpoint(node.RPCPort), "["+p+",1]") + endpoint := NewEndpoint(node.RPCPort) + method := ChainGetFinalizedHeadByRound + params := "[" + p + ",1]" + respBody, err := PostRPC(ctx, endpoint, method, params) require.NoError(t, err) var hash string @@ -83,8 +97,11 @@ func GetFinalizedHeadByRound(t *testing.T, node *Node, round uint64) (common.Has } // GetBlock calls the endpoint chain_getBlock -func GetBlock(t *testing.T, node *Node, hash common.Hash) *types.Block { - respBody, err := PostRPC(ChainGetBlock, NewEndpoint(node.RPCPort), "[\""+hash.String()+"\"]") +func GetBlock(ctx context.Context, t *testing.T, node *Node, hash common.Hash) *types.Block { + endpoint := NewEndpoint(node.RPCPort) + method := ChainGetBlock + params := fmt.Sprintf(`["%s"]`, hash) + respBody, err := PostRPC(ctx, endpoint, method, params) require.NoError(t, err) block := new(modules.ChainBlockResponse) diff --git a/tests/utils/common.go b/tests/utils/common.go index 5bd9812612..7948583c4b 100644 --- a/tests/utils/common.go +++ b/tests/utils/common.go @@ -5,10 +5,7 @@ package utils import ( "encoding/json" - "net" - "net/http" "os" - "time" ) var ( @@ -25,21 +22,6 @@ var ( // NETWORK_SIZE is the value for the environnent variable NETWORK_SIZE. NETWORK_SIZE = os.Getenv("NETWORK_SIZE") //nolint:revive - - // ContentTypeJSON is the JSON header application/json. - ContentTypeJSON = "application/json" - dialTimeout = 60 * time.Second - httpClientTimeout = 120 * time.Second - - transport = &http.Transport{ - Dial: (&net.Dialer{ - Timeout: dialTimeout, - }).Dial, - } - httpClient = &http.Client{ - Transport: transport, - Timeout: httpClientTimeout, - } ) // ServerResponse wraps the RPC response diff --git a/tests/utils/dev.go b/tests/utils/dev.go index 1b16b4603f..f59f09391a 100644 --- a/tests/utils/dev.go +++ b/tests/utils/dev.go @@ -4,6 +4,7 @@ package utils import ( + "context" "encoding/binary" "strconv" "testing" @@ -14,14 +15,19 @@ import ( ) // PauseBABE calls the endpoint dev_control with the params ["babe", "stop"] -func PauseBABE(t *testing.T, node *Node) error { - _, err := PostRPC(DevControl, NewEndpoint(node.RPCPort), "[\"babe\", \"stop\"]") +func PauseBABE(ctx context.Context, node *Node) error { + endpoint := NewEndpoint(node.RPCPort) + const params = `["babe", "stop"]` + _, err := PostRPC(ctx, endpoint, DevControl, params) return err } // SlotDuration Calls dev endpoint for slot duration -func SlotDuration(t *testing.T, node *Node) time.Duration { - slotDuration, err := PostRPC("dev_slotDuration", NewEndpoint(node.RPCPort), "[]") +func SlotDuration(ctx context.Context, t *testing.T, node *Node) time.Duration { + endpoint := NewEndpoint(node.RPCPort) + const method = "dev_slotDuration" + const params = "[]" + slotDuration, err := PostRPC(ctx, endpoint, method, params) if err != nil { require.NoError(t, err) @@ -38,9 +44,11 @@ func SlotDuration(t *testing.T, node *Node) time.Duration { } // EpochLength Calls dev endpoint for epoch length -func EpochLength(t *testing.T, node *Node) uint64 { - epochLength, err := PostRPC("dev_epochLength", NewEndpoint(node.RPCPort), "[]") - +func EpochLength(ctx context.Context, t *testing.T, node *Node) uint64 { + endpoint := NewEndpoint(node.RPCPort) + const method = "dev_epochLength" + const params = "[]" + epochLength, err := PostRPC(ctx, endpoint, method, params) if err != nil { require.NoError(t, err) } diff --git a/tests/utils/framework.go b/tests/utils/framework.go index 696ea0975e..dbdd41e662 100644 --- a/tests/utils/framework.go +++ b/tests/utils/framework.go @@ -4,6 +4,7 @@ package utils import ( + "context" "fmt" "os" "strconv" @@ -58,12 +59,13 @@ func (fw *Framework) KillNodes(t *testing.T) []error { } // CallRPC call RPC method with given params for node at idx -func (fw *Framework) CallRPC(idx int, method, params string) (respJSON interface{}, err error) { +func (fw *Framework) CallRPC(ctx context.Context, idx int, method, params string) ( + respJSON interface{}, err error) { if idx >= len(fw.nodes) { return nil, fmt.Errorf("node index greater than quantity of nodes") } node := fw.nodes[idx] - respBody, err := PostRPC(method, NewEndpoint(node.RPCPort), params) + respBody, err := PostRPC(ctx, NewEndpoint(node.RPCPort), method, params) if err != nil { return nil, err } diff --git a/tests/utils/gossamer_utils.go b/tests/utils/gossamer_utils.go index ec36d3009d..73f1d408be 100644 --- a/tests/utils/gossamer_utils.go +++ b/tests/utils/gossamer_utils.go @@ -5,6 +5,7 @@ package utils import ( "bufio" + "context" "fmt" "io" "os" @@ -196,10 +197,21 @@ func startGossamer(t *testing.T, node *Node, websocket bool) error { } }() + ctx := context.Background() + var started bool for i := 0; i < maxRetries; i++ { time.Sleep(time.Second * 5) - if err = checkNodeStarted(t, "http://"+HOSTNAME+":"+node.RPCPort); err == nil { + + const checkNodeStartedTimeout = time.Second + checkNodeCtx, cancel := context.WithTimeout(ctx, checkNodeStartedTimeout) + + addr := fmt.Sprintf("http://%s:%s", HOSTNAME, node.RPCPort) + err = checkNodeStarted(checkNodeCtx, t, addr) + + cancel() + + if err == nil { started = true break } @@ -239,10 +251,10 @@ func RunGossamer(t *testing.T, idx int, basepath, genesis, config string, websoc } // checkNodeStarted check if gossamer node is started -func checkNodeStarted(t *testing.T, gossamerHost string) error { - method := "system_health" - - respBody, err := PostRPC(method, gossamerHost, "{}") +func checkNodeStarted(ctx context.Context, t *testing.T, gossamerHost string) error { + const method = "system_health" + const params = "{}" + respBody, err := PostRPC(ctx, gossamerHost, method, params) if err != nil { return err } diff --git a/tests/utils/request_utils.go b/tests/utils/request_utils.go index 2068c09d49..eff8b8c52a 100644 --- a/tests/utils/request_utils.go +++ b/tests/utils/request_utils.go @@ -21,54 +21,73 @@ import ( "github.com/stretchr/testify/require" ) -// PostRPC utils for sending payload to endpoint and getting []byte back -func PostRPC(method, host, params string) ([]byte, error) { - data := []byte(`{"jsonrpc":"2.0","method":"` + method + `","params":` + params + `,"id":1}`) - buf := &bytes.Buffer{} - _, err := buf.Write(data) +// PostRPC sends a payload using the method, host and params string given. +// It returns the response bytes and an eventual error. +func PostRPC(ctx context.Context, endpoint, method, params string) (data []byte, err error) { + requestBody := fmt.Sprintf(`{"jsonrpc":"2.0","method":"%s","params":%s,"id":1}`, method, params) + requestBuffer := bytes.NewBuffer([]byte(requestBody)) + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, requestBuffer) if err != nil { - return nil, err + return nil, fmt.Errorf("cannot create HTTP request: %w", err) } - r, err := http.NewRequest("POST", host, buf) + const contentType = "application/json" + request.Header.Set("Content-Type", contentType) + request.Header.Set("Accept", contentType) + + response, err := http.DefaultClient.Do(request) if err != nil { - return nil, err + return nil, fmt.Errorf("cannot do HTTP request: %w", err) } - r.Header.Set("Content-Type", ContentTypeJSON) - r.Header.Set("Accept", ContentTypeJSON) - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - r = r.WithContext(ctx) + data, err = io.ReadAll(response.Body) + if err != nil { + _ = response.Body.Close() + return nil, fmt.Errorf("cannot read HTTP response body: %w", err) + } - resp, err := httpClient.Do(r) + err = response.Body.Close() if err != nil { - return nil, err - } else if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("status code not OK") + return nil, fmt.Errorf("cannot close HTTP response body: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + return data, nil +} - respBody, err := io.ReadAll(resp.Body) +// PostRPCWithRetry repeatitively calls `PostRPC` repeatitively +// until it succeeds within the requestWait duration or returns +// the last error if the context is canceled. +func PostRPCWithRetry(ctx context.Context, endpoint, method, params string, + requestWait time.Duration) (data []byte, err error) { + try := 0 + for { + try++ - return respBody, err + postRPCCtx, postRPCCancel := context.WithTimeout(ctx, requestWait) -} + data, err := PostRPC(postRPCCtx, endpoint, method, params) -// PostRPCWithRetry is a wrapper around `PostRPC` that calls it `retry` number of times. -func PostRPCWithRetry(method, host, params string, retry int) ([]byte, error) { - count := 0 - for { - resp, err := PostRPC(method, host, params) - if err == nil || count >= retry { - return resp, err + if err == nil { + postRPCCancel() + return data, nil + } + + // wait for full requestWait duration or main context cancelation + <-postRPCCtx.Done() + postRPCCancel() + + if ctx.Err() != nil { + break } - time.Sleep(200 * time.Millisecond) - count++ } + + totalTime := time.Duration(try) * requestWait + tryWord := "try" + if try > 1 { + tryWord = "tries" + } + return nil, fmt.Errorf("after %d %s totalling %s: %w", try, tryWord, totalTime, err) } // DecodeRPC will decode []body into target interface diff --git a/tests/utils/system.go b/tests/utils/system.go index 3810cbe49e..2bd697bfac 100644 --- a/tests/utils/system.go +++ b/tests/utils/system.go @@ -4,6 +4,7 @@ package utils import ( + "context" "testing" "github.com/ChainSafe/gossamer/dot/rpc/modules" @@ -12,8 +13,11 @@ import ( ) // GetPeers calls the endpoint system_peers -func GetPeers(t *testing.T, node *Node) []common.PeerInfo { - respBody, err := PostRPC("system_peers", NewEndpoint(node.RPCPort), "[]") +func GetPeers(ctx context.Context, t *testing.T, node *Node) []common.PeerInfo { + endpoint := NewEndpoint(node.RPCPort) + const method = "system_peers" + const params = "[]" + respBody, err := PostRPC(ctx, endpoint, method, params) require.NoError(t, err) resp := new(modules.SystemPeersResponse) From ce3c10cc11a117c0738125e4e946ea11ac26b677 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Fri, 1 Apr 2022 12:31:23 +0200 Subject: [PATCH 4/7] fix(ci): caching of Go caches (#2451) --- .github/workflows/build.yml | 32 ++++++++++++------------- .github/workflows/code-cov.yml | 16 ++++++------- .github/workflows/integration-tests.yml | 17 +++++++------ .github/workflows/unit-tests.yml | 17 +++++++------ 4 files changed, 40 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb8269c69e..033288ddb9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,28 +30,28 @@ jobs: platform: [macos-latest, ubuntu-latest] runs-on: ${{ matrix.platform }} steps: - - id: go-cache-paths - run: | - echo "::set-output name=go-build::$(go env GOCACHE)" - echo "::set-output name=go-mod::$(go env GOMODCACHE)" - uses: actions/setup-go@v2 with: go-version: 1.18 stable: true check-latest: true + - name: Set cache variables + id: go-cache-paths + run: | + echo "::set-output name=go-build::$(go env GOCACHE)" + echo "::set-output name=go-mod::$(go env GOMODCACHE)" + - uses: actions/checkout@v3 - # cache go build cache - - name: Cache go modules + - name: Go build cache uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-go-build - # cache go mod cache - - name: Cache go modules + - name: Go modules cache uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} @@ -65,28 +65,28 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - id: go-cache-paths - run: | - echo "::set-output name=go-build::$(go env GOCACHE)" - echo "::set-output name=go-mod::$(go env GOMODCACHE)" - uses: actions/setup-go@v2 with: go-version: 1.18 stable: true check-latest: true + - name: Set cache variables + id: go-cache-paths + run: | + echo "::set-output name=go-build::$(go env GOCACHE)" + echo "::set-output name=go-mod::$(go env GOMODCACHE)" + - uses: actions/checkout@v3 - # cache go build cache - - name: Cache go modules + - name: Go build cache uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-go-build - # cache go mod cache - - name: Cache go modules + - name: Go modules cache uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} diff --git a/.github/workflows/code-cov.yml b/.github/workflows/code-cov.yml index faba86bf30..58586c3ffd 100644 --- a/.github/workflows/code-cov.yml +++ b/.github/workflows/code-cov.yml @@ -24,28 +24,28 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - id: go-cache-paths - run: | - echo "::set-output name=go-build::$(go env GOCACHE)" - echo "::set-output name=go-mod::$(go env GOMODCACHE)" - uses: actions/setup-go@v2 with: go-version: 1.18 stable: true check-latest: true + - name: Set cache variables + id: go-cache-paths + run: | + echo "::set-output name=go-build::$(go env GOCACHE)" + echo "::set-output name=go-mod::$(go env GOMODCACHE)" + - uses: actions/checkout@v3 - # cache go build cache - - name: Cache go modules + - name: Go build cache uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-go-build - # cache go mod cache - - name: Cache go modules + - name: Go modules cache uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index b608341cb0..fc51cf9ab9 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -29,29 +29,28 @@ jobs: ] runs-on: ubuntu-latest steps: - - id: go-cache-paths - run: | - echo "::set-output name=go-build::$(go env GOCACHE)" - echo "::set-output name=go-mod::$(go env GOMODCACHE)" - - uses: actions/setup-go@v2 with: go-version: 1.18 stable: true check-latest: true + - name: Set cache variables + id: go-cache-paths + run: | + echo "::set-output name=go-build::$(go env GOCACHE)" + echo "::set-output name=go-mod::$(go env GOMODCACHE)" + - uses: actions/checkout@v3 - # cache go build cache - - name: Cache go modules + - name: Go build cache uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-go-build - # cache go mod cache - - name: Cache go modules + - name: Go modules cache uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index af1dd8dc78..b5f023e7ee 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -24,29 +24,28 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - id: go-cache-paths - run: | - echo "::set-output name=go-build::$(go env GOCACHE)" - echo "::set-output name=go-mod::$(go env GOMODCACHE)" - - uses: actions/setup-go@v2 with: go-version: 1.18 stable: true check-latest: true + - name: Set cache variables + id: go-cache-paths + run: | + echo "::set-output name=go-build::$(go env GOCACHE)" + echo "::set-output name=go-mod::$(go env GOMODCACHE)" + - uses: actions/checkout@v3 - # cache go build cache - - name: Cache go modules + - name: Go build cache uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-build }} key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-go-build - # cache go mod cache - - name: Cache go modules + - name: Go modules cache uses: actions/cache@v3 with: path: ${{ steps.go-cache-paths.outputs.go-mod }} From e991cc84246be4bcb6807da82c2aa19473211569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ecl=C3=A9sio=20Junior?= Date: Fri, 1 Apr 2022 10:16:26 -0400 Subject: [PATCH 5/7] fix(dot/digest): BABE NextEpochData and NextConfigData should be set on finalization (#2339) * feat: include substrate docker image * chore: add substrate docker images * chore: formatting and add depends on yaml key * chore: add datadog stuff again * chore: use alice peer id * chore: split substrate docker compose services into another file * chore: enable prometheus external and add datadog * chore: keep substrate ports same as gossamer node ports * chore: expose rpc and ws calls in substrate docker file * chore: use same dns name and p2p id * chore: use same key for both alice gossamer and substrate nodes * chore: fix dd golang cmd chore: keep both cross-client and gssmr chain genesis raw chore: mistakes * Update devnet/README.md Co-authored-by: Quentin McGaw * Update devnet/README.md Co-authored-by: Quentin McGaw * chore: update `substrate_alice` readme info * Update devnet/README.md Co-authored-by: Quentin McGaw * chore: add `/tcp` to expose ports * chore: adjust args and envs * chore: pin prometheus version, add read only volume and add trailing new line * chore: improve README.md * chore: add port 7001 explicitly * chore: remove prometheus `--config.file` flag Co-authored-by: Quentin McGaw * chore: use env to chain This is needed since ARG cannot be used inside ENTRYPOINT * update substrate_bob.Dockerfile * chore: add datadog-agent start to gssmr alice node * chore: add comment about different but same node keys * chore: IncrementSetID to return the nextSetID * chore: digest HandleDigests check !ok first * feat: store babe next epoch data only at finalised blocks * feat: include substrate docker image * chore: add substrate docker images * chore: formatting and add depends on yaml key * chore: add datadog stuff again * chore: use alice peer id * chore: split substrate docker compose services into another file * chore: enable prometheus external and add datadog * chore: keep substrate ports same as gossamer node ports * chore: expose rpc and ws calls in substrate docker file * chore: use same dns name and p2p id * chore: use same key for both alice gossamer and substrate nodes * chore: fix dd golang cmd chore: keep both cross-client and gssmr chain genesis raw chore: mistakes * Update devnet/README.md Co-authored-by: Quentin McGaw * Update devnet/README.md Co-authored-by: Quentin McGaw * chore: update `substrate_alice` readme info * chore: add `/tcp` to expose ports * Update devnet/README.md Co-authored-by: Quentin McGaw * chore: adjust args and envs * chore: pin prometheus version, add read only volume and add trailing new line * chore: improve README.md * chore: add port 7001 explicitly * chore: add datadog-agent start to gssmr alice node * chore: remove prometheus `--config.file` flag Co-authored-by: Quentin McGaw * chore: use env to chain This is needed since ARG cannot be used inside ENTRYPOINT * update substrate_bob.Dockerfile * chore: add comment about different but same node keys * chore: IncrementSetID to return the nextSetID * chore: digest HandleDigests check !ok first * feat: store babe next epoch data only at finalised blocks * chore: resolve conflicts * chore: remove unused * chore: rename function and change log message * chore: fix babe next epoch data/config while not finalized chain * chore: allow forks validate blocks using the right epoch data * chore: add exported comments and improve VerifyBlock comment * chore: fix lint, removing shadowing * chore: promising integration test to cover babe digest handler * chore: integration test added to babe verify block * chore: needs improvent at chain finalization * chore: integration test done! * chore: fix lint * chore: addressing comments at digest pkg * chore: wrap errors at granpda pkg and fix lll at verify_integration_test * chore: improve comments * chore: improving state/epoch tests * chore: return wrapped error at `babe digest finalization` * chore: improve testing * chore: improve exported comment Co-authored-by: Quentin McGaw * chore: improve exported comment Co-authored-by: Quentin McGaw * chore: improve error decription Co-authored-by: Quentin McGaw * chore: improve error description * chore: reorg `HasConfigData` and `HasEpochData` * chore: add two mutexes for protect `nextEpoch` and `nextConfig` * chore: improve exported comment * chore: change `GetEpochData` to check in the memory if database doesnt return * chore: scope specific variables * unneeded `\n` * chore: fix unit tests * chore: git add fix exported func name * chore: fix tests * chore: remove DigestHandler from core pkg * chore: get current epoch at next epoch data and next config data * chore: check `err != nil` before check `err is not ErrKeyNotFound` * chore: rename error var name * chore: flip babe digest finalization * chore: improve exported comment * chore: separate not related conditions * chore: remove DigestHandler from `dot/core` pkg * chore: changing `String()` method owner * chore: change function name to `persistBABEDigestsForNextEpoch` * chore: fix typo * chore: remove `nilnil` lint warn * chore: remove unused core.DigestHandler mocks * chore: fix * chore: unexport functions * chore: fix integration tests Co-authored-by: Quentin McGaw --- dot/core/errors.go | 3 - dot/core/helpers_test.go | 5 - dot/core/interface.go | 7 +- dot/core/messages_integration_test.go | 8 - dot/core/mock_core_test.go | 37 +- dot/core/service.go | 10 - dot/core/service_integration_test.go | 7 - dot/core/service_test.go | 36 +- dot/digest/digest.go | 126 ++++--- dot/digest/digest_test.go | 94 +++-- dot/digest/interface.go | 8 +- dot/node.go | 2 +- dot/rpc/http_test.go | 7 - dot/rpc/modules/author_integration_test.go | 3 - dot/rpc/modules/system_integration_test.go | 3 - dot/services.go | 3 +- dot/services_integration_test.go | 25 +- dot/state/block.go | 8 +- dot/state/block_finalisation.go | 1 + dot/state/epoch.go | 292 +++++++++++++++- dot/state/epoch_test.go | 387 ++++++++++++++++++++- dot/state/grandpa.go | 14 +- dot/state/grandpa_test.go | 8 +- dot/types/digest.go | 2 +- lib/babe/epoch.go | 4 +- lib/babe/epoch_integration_test.go | 2 +- lib/babe/epoch_test.go | 6 +- lib/babe/mock_state_test.go | 16 +- lib/babe/state.go | 7 +- lib/babe/verify.go | 19 +- lib/babe/verify_integration_test.go | 298 ++++++++++++++++ lib/babe/verify_test.go | 30 +- lib/grandpa/grandpa_test.go | 2 +- lib/grandpa/message_handler_test.go | 15 +- 34 files changed, 1202 insertions(+), 293 deletions(-) diff --git a/dot/core/errors.go b/dot/core/errors.go index 76471f196d..10364b8499 100644 --- a/dot/core/errors.go +++ b/dot/core/errors.go @@ -44,9 +44,6 @@ var ( // ErrEmptyRuntimeCode is returned when the storage :code is empty ErrEmptyRuntimeCode = errors.New("new :code is empty") - // ErrNilDigestHandler is returned when the DigestHandler interface is nil - ErrNilDigestHandler = errors.New("cannot have nil DigestHandler") - errNilCodeSubstitutedState = errors.New("cannot have nil CodeSubstitutedStat") ) diff --git a/dot/core/helpers_test.go b/dot/core/helpers_test.go index 3a2fdcdcbf..48a2f82499 100644 --- a/dot/core/helpers_test.go +++ b/dot/core/helpers_test.go @@ -7,7 +7,6 @@ import ( "path/filepath" "testing" - "github.com/ChainSafe/gossamer/dot/digest" "github.com/ChainSafe/gossamer/dot/network" "github.com/ChainSafe/gossamer/dot/state" "github.com/ChainSafe/gossamer/internal/log" @@ -32,10 +31,6 @@ func NewTestService(t *testing.T, cfg *Config) *Service { cfg = &Config{} } - if cfg.DigestHandler == nil { - cfg.DigestHandler = &digest.Handler{} // only for nil check in NewService - } - if cfg.Keystore == nil { cfg.Keystore = keystore.NewGlobalKeystore() kp, err := sr25519.GenerateKeypair() diff --git a/dot/core/interface.go b/dot/core/interface.go index 54d5cc46b1..aadf9b908a 100644 --- a/dot/core/interface.go +++ b/dot/core/interface.go @@ -17,7 +17,7 @@ import ( "github.com/ChainSafe/gossamer/lib/transaction" ) -//go:generate mockgen -destination=mock_core_test.go -package $GOPACKAGE . BlockState,StorageState,TransactionState,Network,EpochState,CodeSubstitutedState,DigestHandler +//go:generate mockgen -destination=mock_core_test.go -package $GOPACKAGE . BlockState,StorageState,TransactionState,Network,EpochState,CodeSubstitutedState // BlockState interface for block state methods type BlockState interface { @@ -87,8 +87,3 @@ type CodeSubstitutedState interface { LoadCodeSubstitutedBlockHash() common.Hash StoreCodeSubstitutedBlockHash(hash common.Hash) error } - -// DigestHandler is the interface for the consensus digest handler -type DigestHandler interface { - HandleDigests(header *types.Header) -} diff --git a/dot/core/messages_integration_test.go b/dot/core/messages_integration_test.go index d38ed2fe3f..c20d6453be 100644 --- a/dot/core/messages_integration_test.go +++ b/dot/core/messages_integration_test.go @@ -78,10 +78,6 @@ func TestService_HandleBlockProduced(t *testing.T) { Keystore: keystore.NewGlobalKeystore(), } - digestHandler := NewMockDigestHandler(ctrl) - digestHandler.EXPECT().HandleDigests(gomock.AssignableToTypeOf(new(types.Header))) - cfg.DigestHandler = digestHandler - s := NewTestService(t, cfg) err := s.Start() require.NoError(t, err) @@ -137,9 +133,6 @@ func TestService_HandleTransactionMessage(t *testing.T) { telemetryMock := NewMockClient(ctrl) telemetryMock.EXPECT().SendMessage(gomock.Any()).AnyTimes() - digestHandler := NewMockDigestHandler(ctrl) - digestHandler.EXPECT().HandleDigests(gomock.AssignableToTypeOf(new(types.Header))) - net := NewMockNetwork(ctrl) net.EXPECT().GossipMessage(gomock.AssignableToTypeOf(new(network.TransactionMessage))).AnyTimes() net.EXPECT().IsSynced().Return(true).AnyTimes() @@ -151,7 +144,6 @@ func TestService_HandleTransactionMessage(t *testing.T) { cfg := &Config{ Keystore: ks, TransactionState: state.NewTransactionState(telemetryMock), - DigestHandler: digestHandler, Network: net, } diff --git a/dot/core/mock_core_test.go b/dot/core/mock_core_test.go index 7cfa4e471a..ee0d44a027 100644 --- a/dot/core/mock_core_test.go +++ b/dot/core/mock_core_test.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ChainSafe/gossamer/dot/core (interfaces: BlockState,StorageState,TransactionState,Network,EpochState,CodeSubstitutedState,DigestHandler) +// Source: github.com/ChainSafe/gossamer/dot/core (interfaces: BlockState,StorageState,TransactionState,Network,EpochState,CodeSubstitutedState) // Package core is a generated GoMock package. package core @@ -803,38 +803,3 @@ func (mr *MockCodeSubstitutedStateMockRecorder) StoreCodeSubstitutedBlockHash(ar mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreCodeSubstitutedBlockHash", reflect.TypeOf((*MockCodeSubstitutedState)(nil).StoreCodeSubstitutedBlockHash), arg0) } - -// MockDigestHandler is a mock of DigestHandler interface. -type MockDigestHandler struct { - ctrl *gomock.Controller - recorder *MockDigestHandlerMockRecorder -} - -// MockDigestHandlerMockRecorder is the mock recorder for MockDigestHandler. -type MockDigestHandlerMockRecorder struct { - mock *MockDigestHandler -} - -// NewMockDigestHandler creates a new mock instance. -func NewMockDigestHandler(ctrl *gomock.Controller) *MockDigestHandler { - mock := &MockDigestHandler{ctrl: ctrl} - mock.recorder = &MockDigestHandlerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockDigestHandler) EXPECT() *MockDigestHandlerMockRecorder { - return m.recorder -} - -// HandleDigests mocks base method. -func (m *MockDigestHandler) HandleDigests(arg0 *types.Header) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "HandleDigests", arg0) -} - -// HandleDigests indicates an expected call of HandleDigests. -func (mr *MockDigestHandlerMockRecorder) HandleDigests(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleDigests", reflect.TypeOf((*MockDigestHandler)(nil).HandleDigests), arg0) -} diff --git a/dot/core/service.go b/dot/core/service.go index e267b5a051..bd41a71af5 100644 --- a/dot/core/service.go +++ b/dot/core/service.go @@ -50,7 +50,6 @@ type Service struct { storageState StorageState transactionState TransactionState net Network - digestHandler DigestHandler // map of code substitutions keyed by block hash codeSubstitute map[common.Hash]string @@ -71,7 +70,6 @@ type Config struct { Network Network Keystore *keystore.GlobalKeystore Runtime runtime.Instance - DigestHandler DigestHandler CodeSubstitutes map[common.Hash]string CodeSubstitutedState CodeSubstitutedState @@ -96,10 +94,6 @@ func NewService(cfg *Config) (*Service, error) { return nil, ErrNilNetwork } - if cfg.DigestHandler == nil { - return nil, ErrNilDigestHandler - } - if cfg.CodeSubstitutedState == nil { return nil, errNilCodeSubstitutedState } @@ -121,7 +115,6 @@ func NewService(cfg *Config) (*Service, error) { blockAddCh: blockAddCh, codeSubstitute: cfg.CodeSubstitutes, codeSubstitutedState: cfg.CodeSubstitutedState, - digestHandler: cfg.DigestHandler, } return srv, nil @@ -218,9 +211,6 @@ func (s *Service) handleBlock(block *types.Block, state *rtstorage.TrieState) er logger.Debugf("imported block %s and stored state trie with root %s", block.Header.Hash(), state.MustRoot()) - // handle consensus digests - s.digestHandler.HandleDigests(&block.Header) - rt, err := s.blockState.GetRuntime(&block.Header.ParentHash) if err != nil { return err diff --git a/dot/core/service_integration_test.go b/dot/core/service_integration_test.go index d6f860f2a1..a1f76705bb 100644 --- a/dot/core/service_integration_test.go +++ b/dot/core/service_integration_test.go @@ -167,10 +167,6 @@ func TestAnnounceBlock(t *testing.T) { Network: net, } - digestHandler := NewMockDigestHandler(ctrl) - digestHandler.EXPECT().HandleDigests(gomock.AssignableToTypeOf(new(types.Header))) - cfg.DigestHandler = digestHandler - s := NewTestService(t, cfg) err := s.Start() require.NoError(t, err) @@ -583,9 +579,6 @@ func TestService_GetRuntimeVersion(t *testing.T) { func TestService_HandleSubmittedExtrinsic(t *testing.T) { cfg := &Config{} ctrl := gomock.NewController(t) - digestHandler := NewMockDigestHandler(ctrl) - digestHandler.EXPECT().HandleDigests(gomock.AssignableToTypeOf(new(types.Header))) - cfg.DigestHandler = digestHandler net := NewMockNetwork(ctrl) net.EXPECT().GossipMessage(gomock.AssignableToTypeOf(new(network.TransactionMessage))) diff --git a/dot/core/service_test.go b/dot/core/service_test.go index 47eeae82f1..3a178bfc83 100644 --- a/dot/core/service_test.go +++ b/dot/core/service_test.go @@ -273,6 +273,7 @@ func Test_Service_handleCodeSubstitution(t *testing.T) { func Test_Service_handleBlock(t *testing.T) { t.Parallel() + execTest := func(t *testing.T, s *Service, block *types.Block, trieState *rtstorage.TrieState, expErr error) { err := s.handleBlock(block, trieState) assert.ErrorIs(t, err, expErr) @@ -280,6 +281,7 @@ func Test_Service_handleBlock(t *testing.T) { assert.EqualError(t, err, expErr.Error()) } } + t.Run("nil input", func(t *testing.T) { t.Parallel() service := &Service{} @@ -366,13 +368,10 @@ func Test_Service_handleBlock(t *testing.T) { mockBlockState := NewMockBlockState(ctrl) mockBlockState.EXPECT().AddBlock(&block).Return(blocktree.ErrBlockExists) mockBlockState.EXPECT().GetRuntime(&block.Header.ParentHash).Return(nil, errTestDummyError) - mockDigestHandler := NewMockDigestHandler(ctrl) - mockDigestHandler.EXPECT().HandleDigests(&block.Header) service := &Service{ - storageState: mockStorageState, - blockState: mockBlockState, - digestHandler: mockDigestHandler, + storageState: mockStorageState, + blockState: mockBlockState, } execTest(t, service, &block, trieState, errTestDummyError) }) @@ -396,13 +395,10 @@ func Test_Service_handleBlock(t *testing.T) { mockBlockState.EXPECT().GetRuntime(&block.Header.ParentHash).Return(runtimeMock, nil) mockBlockState.EXPECT().HandleRuntimeChanges(trieState, runtimeMock, block.Header.Hash()). Return(errTestDummyError) - mockDigestHandler := NewMockDigestHandler(ctrl) - mockDigestHandler.EXPECT().HandleDigests(&block.Header) service := &Service{ - storageState: mockStorageState, - blockState: mockBlockState, - digestHandler: mockDigestHandler, + storageState: mockStorageState, + blockState: mockBlockState, } execTest(t, service, &block, trieState, errTestDummyError) }) @@ -425,14 +421,11 @@ func Test_Service_handleBlock(t *testing.T) { mockBlockState.EXPECT().AddBlock(&block).Return(blocktree.ErrBlockExists) mockBlockState.EXPECT().GetRuntime(&block.Header.ParentHash).Return(runtimeMock, nil) mockBlockState.EXPECT().HandleRuntimeChanges(trieState, runtimeMock, block.Header.Hash()).Return(nil) - mockDigestHandler := NewMockDigestHandler(ctrl) - mockDigestHandler.EXPECT().HandleDigests(&block.Header) service := &Service{ - storageState: mockStorageState, - blockState: mockBlockState, - digestHandler: mockDigestHandler, - ctx: context.Background(), + storageState: mockStorageState, + blockState: mockBlockState, + ctx: context.Background(), } execTest(t, service, &block, trieState, nil) }) @@ -488,17 +481,14 @@ func Test_Service_HandleBlockProduced(t *testing.T) { mockBlockState.EXPECT().AddBlock(&block).Return(blocktree.ErrBlockExists) mockBlockState.EXPECT().GetRuntime(&block.Header.ParentHash).Return(runtimeMock, nil) mockBlockState.EXPECT().HandleRuntimeChanges(trieState, runtimeMock, block.Header.Hash()).Return(nil) - mockDigestHandler := NewMockDigestHandler(ctrl) - mockDigestHandler.EXPECT().HandleDigests(&block.Header) mockNetwork := NewMockNetwork(ctrl) mockNetwork.EXPECT().GossipMessage(msg) service := &Service{ - storageState: mockStorageState, - blockState: mockBlockState, - digestHandler: mockDigestHandler, - net: mockNetwork, - ctx: context.Background(), + storageState: mockStorageState, + blockState: mockBlockState, + net: mockNetwork, + ctx: context.Background(), } execTest(t, service, &block, trieState, nil) }) diff --git a/dot/digest/digest.go b/dot/digest/digest.go index 1e8794a34c..ad17d166d9 100644 --- a/dot/digest/digest.go +++ b/dot/digest/digest.go @@ -6,7 +6,9 @@ package digest import ( "context" "errors" + "fmt" + "github.com/ChainSafe/gossamer/dot/state" "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/internal/log" "github.com/ChainSafe/gossamer/lib/services" @@ -15,6 +17,8 @@ import ( var ( _ services.Service = &Handler{} + + ErrDefineNextEpoch = errors.New("cannot define next epoch data and config") ) // Handler is used to handle consensus messages and relevant authority updates to BABE and GRANDPA @@ -118,12 +122,14 @@ func (h *Handler) NextGrandpaAuthorityChange() (next uint) { func (h *Handler) HandleDigests(header *types.Header) { for i, d := range header.Digest.Types { val, ok := d.Value().(types.ConsensusDigest) - if ok { - err := h.handleConsensusDigest(&val, header) - if err != nil { - h.logger.Errorf("cannot handle digests for block number %d, index %d, digest %s: %s", - header.Number, i, d.Value(), err) - } + if !ok { + continue + } + + err := h.handleConsensusDigest(&val, header) + if err != nil { + h.logger.Errorf("cannot handle digest for block number %d, index %d, digest %s: %s", + header.Number, i, d.Value(), err) } } } @@ -175,15 +181,35 @@ func (h *Handler) handleGrandpaConsensusDigest(digest scale.VaryingDataType, hea } func (h *Handler) handleBabeConsensusDigest(digest scale.VaryingDataType, header *types.Header) error { + headerHash := header.Hash() + switch val := digest.Value().(type) { case types.NextEpochData: - h.logger.Debugf("handling BABENextEpochData data: %v", digest) - return h.handleNextEpochData(val, header) + currEpoch, err := h.epochState.GetEpochForBlock(header) + if err != nil { + return fmt.Errorf("cannot get epoch for block %d (%s): %w", + header.Number, headerHash, err) + } + + nextEpoch := currEpoch + 1 + h.epochState.StoreBABENextEpochData(nextEpoch, headerHash, val) + h.logger.Debugf("stored BABENextEpochData data: %v for hash: %s to epoch: %d", digest, headerHash, nextEpoch) + return nil + case types.BABEOnDisabled: return h.handleBABEOnDisabled(val, header) + case types.NextConfigData: - h.logger.Debugf("handling BABENextConfigData data: %v", digest) - return h.handleNextConfigData(val, header) + currEpoch, err := h.epochState.GetEpochForBlock(header) + if err != nil { + return fmt.Errorf("cannot get epoch for block %d (%s): %w", + header.Number, headerHash, err) + } + + nextEpoch := currEpoch + 1 + h.epochState.StoreBABENextConfigData(nextEpoch, headerHash, val) + h.logger.Debugf("stored BABENextConfigData data: %v for hash: %s to epoch: %d", digest, headerHash, nextEpoch) + return nil } return errors.New("invalid consensus digest data") @@ -197,6 +223,7 @@ func (h *Handler) handleBlockImport(ctx context.Context) { continue } + h.HandleDigests(&block.Header) err := h.handleGrandpaChangesOnImport(block.Header.Number) if err != nil { h.logger.Errorf("failed to handle grandpa changes on block import: %s", err) @@ -215,7 +242,12 @@ func (h *Handler) handleBlockFinalisation(ctx context.Context) { continue } - err := h.handleGrandpaChangesOnFinalization(info.Header.Number) + err := h.persistBABEDigestsForNextEpoch(&info.Header) + if err != nil { + h.logger.Errorf("failed to store babe next epoch digest: %s", err) + } + + err = h.handleGrandpaChangesOnFinalization(info.Header.Number) if err != nil { h.logger.Errorf("failed to handle grandpa changes on block finalisation: %s", err) } @@ -225,6 +257,35 @@ func (h *Handler) handleBlockFinalisation(ctx context.Context) { } } +// persistBABEDigestsForNextEpoch is called only when a block is finalised +// and defines the correct next epoch data and next config data. +func (h *Handler) persistBABEDigestsForNextEpoch(finalizedHeader *types.Header) error { + currEpoch, err := h.epochState.GetEpochForBlock(finalizedHeader) + if err != nil { + return fmt.Errorf("cannot get epoch for block %d (%s): %w", + finalizedHeader.Number, finalizedHeader.Hash(), err) + } + + nextEpoch := currEpoch + 1 + err = h.epochState.FinalizeBABENextEpochData(nextEpoch) + if err != nil && !errors.Is(err, state.ErrEpochNotInMemory) { + return fmt.Errorf("cannot finalize babe next epoch data for block number %d (%s): %w", + finalizedHeader.Number, finalizedHeader.Hash(), err) + } + + err = h.epochState.FinalizeBABENextConfigData(nextEpoch) + if err == nil { + return nil + } else if errors.Is(err, state.ErrEpochNotInMemory) { + return fmt.Errorf("%w: %s", ErrDefineNextEpoch, err) + } + + // the epoch state does not contains any information about the next epoch + return fmt.Errorf("cannot finalize babe next config data for block number %d (%s): %w", + finalizedHeader.Number, finalizedHeader.Hash(), err) + +} + func (h *Handler) handleGrandpaChangesOnImport(num uint) error { resume := h.grandpaResume if resume != nil && num >= resume.atBlock { @@ -233,17 +294,12 @@ func (h *Handler) handleGrandpaChangesOnImport(num uint) error { fc := h.grandpaForcedChange if fc != nil && num >= fc.atBlock { - err := h.grandpaState.IncrementSetID() + curr, err := h.grandpaState.IncrementSetID() if err != nil { return err } h.grandpaForcedChange = nil - curr, err := h.grandpaState.GetCurrentSetID() - if err != nil { - return err - } - h.logger.Debugf("incremented grandpa set id %d", curr) } @@ -258,17 +314,12 @@ func (h *Handler) handleGrandpaChangesOnFinalization(num uint) error { sc := h.grandpaScheduledChange if sc != nil && num >= sc.atBlock { - err := h.grandpaState.IncrementSetID() + curr, err := h.grandpaState.IncrementSetID() if err != nil { return err } h.grandpaScheduledChange = nil - curr, err := h.grandpaState.GetCurrentSetID() - if err != nil { - return err - } - h.logger.Debugf("incremented grandpa set id %d", curr) } @@ -381,32 +432,3 @@ func (h *Handler) handleBABEOnDisabled(_ types.BABEOnDisabled, _ *types.Header) h.logger.Debug("handling BABEOnDisabled") return nil } - -func (h *Handler) handleNextEpochData(act types.NextEpochData, header *types.Header) error { - currEpoch, err := h.epochState.GetEpochForBlock(header) - if err != nil { - return err - } - - // set EpochState epoch data for upcoming epoch - data, err := act.ToEpochData() - if err != nil { - return err - } - - h.logger.Debugf("setting data for block number %d and epoch %d with data: %v", - header.Number, currEpoch+1, data) - return h.epochState.SetEpochData(currEpoch+1, data) -} - -func (h *Handler) handleNextConfigData(config types.NextConfigData, header *types.Header) error { - currEpoch, err := h.epochState.GetEpochForBlock(header) - if err != nil { - return err - } - - h.logger.Debugf("setting BABE config data for block number %d and epoch %d with data: %v", - header.Number, currEpoch+1, config.ToConfigData()) - // set EpochState config data for upcoming epoch - return h.epochState.SetConfigData(currEpoch+1, config.ToConfigData()) -} diff --git a/dot/digest/digest_test.go b/dot/digest/digest_test.go index 7d15248d6e..dcb0d8633b 100644 --- a/dot/digest/digest_test.go +++ b/dot/digest/digest_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + "context" + "github.com/ChainSafe/gossamer/dot/state" "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/internal/log" @@ -23,7 +25,7 @@ import ( //go:generate mockgen -destination=mock_telemetry_test.go -package $GOPACKAGE github.com/ChainSafe/gossamer/dot/telemetry Client -func newTestHandler(t *testing.T) *Handler { +func newTestHandler(t *testing.T) (*Handler, *state.Service) { testDatadirPath := t.TempDir() ctrl := gomock.NewController(t) @@ -49,11 +51,11 @@ func newTestHandler(t *testing.T) *Handler { dh, err := NewHandler(log.Critical, stateSrvc.Block, stateSrvc.Epoch, stateSrvc.Grandpa) require.NoError(t, err) - return dh + return dh, stateSrvc } func TestHandler_GrandpaScheduledChange(t *testing.T) { - handler := newTestHandler(t) + handler, _ := newTestHandler(t) handler.Start() defer handler.Stop() @@ -112,7 +114,7 @@ func TestHandler_GrandpaScheduledChange(t *testing.T) { } func TestHandler_GrandpaForcedChange(t *testing.T) { - handler := newTestHandler(t) + handler, _ := newTestHandler(t) handler.Start() defer handler.Stop() @@ -161,7 +163,7 @@ func TestHandler_GrandpaForcedChange(t *testing.T) { } func TestHandler_GrandpaPauseAndResume(t *testing.T) { - handler := newTestHandler(t) + handler, _ := newTestHandler(t) handler.Start() defer handler.Stop() @@ -224,7 +226,7 @@ func TestHandler_GrandpaPauseAndResume(t *testing.T) { } func TestNextGrandpaAuthorityChange_OneChange(t *testing.T) { - handler := newTestHandler(t) + handler, _ := newTestHandler(t) handler.Start() defer handler.Stop() @@ -264,7 +266,7 @@ func TestNextGrandpaAuthorityChange_OneChange(t *testing.T) { } func TestNextGrandpaAuthorityChange_MultipleChanges(t *testing.T) { - handler := newTestHandler(t) + handler, _ := newTestHandler(t) handler.Start() defer handler.Stop() @@ -337,7 +339,7 @@ func TestNextGrandpaAuthorityChange_MultipleChanges(t *testing.T) { } func TestHandler_HandleBABEOnDisabled(t *testing.T) { - handler := newTestHandler(t) + handler, _ := newTestHandler(t) header := &types.Header{ Number: 1, } @@ -377,16 +379,13 @@ func createHeaderWithPreDigest(t *testing.T, slotNumber uint64) *types.Header { require.NoError(t, err) return &types.Header{ + Number: 1, Digest: digest, } } func TestHandler_HandleNextEpochData(t *testing.T) { - expData := common.MustHexToBytes("0x0108d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01000000000000008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4801000000000000004d58630000000000000000000000000000000000000000000000000000000000") //nolint:lll - - handler := newTestHandler(t) - handler.Start() - defer handler.Stop() + expectedDigestBytes := common.MustHexToBytes("0x0108d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01000000000000008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4801000000000000004d58630000000000000000000000000000000000000000000000000000000000") //nolint:lll keyring, err := keystore.NewSr25519Keyring() require.NoError(t, err) @@ -401,17 +400,19 @@ func TestHandler_HandleNextEpochData(t *testing.T) { Weight: 1, } - var digest = types.NewBabeConsensusDigest() - err = digest.Set(types.NextEpochData{ + nextEpochData := types.NextEpochData{ Authorities: []types.AuthorityRaw{authA, authB}, Randomness: [32]byte{77, 88, 99}, - }) + } + + digest := types.NewBabeConsensusDigest() + err = digest.Set(nextEpochData) require.NoError(t, err) data, err := scale.Marshal(digest) require.NoError(t, err) - require.Equal(t, expData, data) + require.Equal(t, expectedDigestBytes, data) d := &types.ConsensusDigest{ ConsensusEngineID: types.BabeEngineID, @@ -419,11 +420,33 @@ func TestHandler_HandleNextEpochData(t *testing.T) { } header := createHeaderWithPreDigest(t, 10) + handler, stateSrv := newTestHandler(t) err = handler.handleConsensusDigest(d, header) require.NoError(t, err) - stored, err := handler.epochState.(*state.EpochState).GetEpochData(1) + const targetEpoch = 1 + + blockHeaderKey := append([]byte("hdr"), header.Hash().ToBytes()...) + blockHeaderKey = append([]byte("block"), blockHeaderKey...) + err = stateSrv.DB().Put(blockHeaderKey, []byte{}) + require.NoError(t, err) + + handler.finalised = make(chan *types.FinalisationInfo, 1) + + const timeout = time.Second + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + handler.finalised <- &types.FinalisationInfo{ + Header: *header, + Round: 1, + SetID: 1, + } + + handler.handleBlockFinalisation(ctx) + + stored, err := handler.epochState.(*state.EpochState).GetEpochData(targetEpoch, nil) require.NoError(t, err) act, ok := digest.Value().(types.NextEpochData) @@ -437,16 +460,14 @@ func TestHandler_HandleNextEpochData(t *testing.T) { } func TestHandler_HandleNextConfigData(t *testing.T) { - handler := newTestHandler(t) - handler.Start() - defer handler.Stop() - var digest = types.NewBabeConsensusDigest() - err := digest.Set(types.NextConfigData{ + nextConfigData := types.NextConfigData{ C1: 1, C2: 8, SecondarySlots: 1, - }) + } + + err := digest.Set(nextConfigData) require.NoError(t, err) data, err := scale.Marshal(digest) @@ -459,15 +480,38 @@ func TestHandler_HandleNextConfigData(t *testing.T) { header := createHeaderWithPreDigest(t, 10) + handler, stateSrv := newTestHandler(t) + err = handler.handleConsensusDigest(d, header) require.NoError(t, err) + const targetEpoch = 1 + + blockHeaderKey := append([]byte("hdr"), header.Hash().ToBytes()...) + blockHeaderKey = append([]byte("block"), blockHeaderKey...) + err = stateSrv.DB().Put(blockHeaderKey, []byte{}) + require.NoError(t, err) + + handler.finalised = make(chan *types.FinalisationInfo, 1) + + const timeout = time.Second + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + handler.finalised <- &types.FinalisationInfo{ + Header: *header, + Round: 1, + SetID: 1, + } + + handler.handleBlockFinalisation(ctx) + act, ok := digest.Value().(types.NextConfigData) if !ok { t.Fatal() } - stored, err := handler.epochState.(*state.EpochState).GetConfigData(1) + stored, err := handler.epochState.(*state.EpochState).GetConfigData(targetEpoch, nil) require.NoError(t, err) require.Equal(t, act.ToConfigData(), stored) } diff --git a/dot/digest/interface.go b/dot/digest/interface.go index 75087fec67..22c2fda1e5 100644 --- a/dot/digest/interface.go +++ b/dot/digest/interface.go @@ -5,6 +5,7 @@ package digest import ( "github.com/ChainSafe/gossamer/dot/types" + "github.com/ChainSafe/gossamer/lib/common" "github.com/ChainSafe/gossamer/lib/grandpa" ) @@ -22,12 +23,17 @@ type EpochState interface { GetEpochForBlock(header *types.Header) (uint64, error) SetEpochData(epoch uint64, info *types.EpochData) error SetConfigData(epoch uint64, info *types.ConfigData) error + + StoreBABENextEpochData(epoch uint64, hash common.Hash, nextEpochData types.NextEpochData) + StoreBABENextConfigData(epoch uint64, hash common.Hash, nextEpochData types.NextConfigData) + FinalizeBABENextEpochData(epoch uint64) error + FinalizeBABENextConfigData(epoch uint64) error } // GrandpaState is the interface for the state.GrandpaState type GrandpaState interface { SetNextChange(authorities []grandpa.Voter, number uint) error - IncrementSetID() error + IncrementSetID() (newSetID uint64, err error) SetNextPause(number uint) error SetNextResume(number uint) error GetCurrentSetID() (uint64, error) diff --git a/dot/node.go b/dot/node.go index 0d3a626087..bb5344b063 100644 --- a/dot/node.go +++ b/dot/node.go @@ -287,7 +287,7 @@ func NewNode(cfg *Config, ks *keystore.GlobalKeystore) (*Node, error) { } nodeSrvcs = append(nodeSrvcs, dh) - coreSrvc, err := createCoreService(cfg, ks, stateSrvc, networkSrvc, dh) + coreSrvc, err := createCoreService(cfg, ks, stateSrvc, networkSrvc) if err != nil { return nil, fmt.Errorf("failed to create core service: %s", err) } diff --git a/dot/rpc/http_test.go b/dot/rpc/http_test.go index 800846e0d6..da5865586f 100644 --- a/dot/rpc/http_test.go +++ b/dot/rpc/http_test.go @@ -366,7 +366,6 @@ func externalIP() (string, error) { } //go:generate mockgen -destination=mock_telemetry_test.go -package $GOPACKAGE github.com/ChainSafe/gossamer/dot/telemetry Client -//go:generate mockgen -destination=mock_digesthandler_test.go -package $GOPACKAGE github.com/ChainSafe/gossamer/dot/core DigestHandler //go:generate mockgen -destination=mock_network_test.go -package $GOPACKAGE github.com/ChainSafe/gossamer/dot/core Network func newCoreServiceTest(t *testing.T) *core.Service { @@ -407,12 +406,6 @@ func newCoreServiceTest(t *testing.T) *core.Service { CodeSubstitutedState: stateSrvc.Base, } - digestHandler := NewMockDigestHandler(ctrl) - digestHandler.EXPECT(). - HandleDigests(gomock.AssignableToTypeOf(new(types.Header))). - AnyTimes() - cfg.DigestHandler = digestHandler - cfg.Keystore = keystore.NewGlobalKeystore() kp, err := sr25519.GenerateKeypair() require.NoError(t, err) diff --git a/dot/rpc/modules/author_integration_test.go b/dot/rpc/modules/author_integration_test.go index f90e484493..9930ece237 100644 --- a/dot/rpc/modules/author_integration_test.go +++ b/dot/rpc/modules/author_integration_test.go @@ -721,13 +721,11 @@ func setupStateAndPopulateTrieState(t *testing.T, basepath string, } //go:generate mockgen -destination=mock_code_substituted_state_test.go -package modules github.com/ChainSafe/gossamer/dot/core CodeSubstitutedState -//go:generate mockgen -destination=mock_digest_handler_test.go -package modules github.com/ChainSafe/gossamer/dot/core DigestHandler func newAuthorModule(t *testing.T, integrationTestController *integrationTestController) *AuthorModule { t.Helper() codeSubstitutedStateMock := NewMockCodeSubstitutedState(nil) - digestHandlerMock := NewMockDigestHandler(nil) cfg := &core.Config{ TransactionState: integrationTestController.stateSrv.Transaction, @@ -736,7 +734,6 @@ func newAuthorModule(t *testing.T, integrationTestController *integrationTestCon Network: integrationTestController.network, Keystore: integrationTestController.keystore, CodeSubstitutedState: codeSubstitutedStateMock, - DigestHandler: digestHandlerMock, } core2test, err := core.NewService(cfg) diff --git a/dot/rpc/modules/system_integration_test.go b/dot/rpc/modules/system_integration_test.go index 695227a132..cbd129b7ae 100644 --- a/dot/rpc/modules/system_integration_test.go +++ b/dot/rpc/modules/system_integration_test.go @@ -375,8 +375,6 @@ func newCoreService(t *testing.T, srvc *state.Service) *core.Service { gomock.AssignableToTypeOf(new(network.TransactionMessage))). AnyTimes() - digestHandlerMock := NewMockDigestHandler(nil) - cfg := &core.Config{ Runtime: rt, Keystore: ks, @@ -386,7 +384,6 @@ func newCoreService(t *testing.T, srvc *state.Service) *core.Service { EpochState: srvc.Epoch, Network: mocknet, CodeSubstitutedState: srvc.Base, - DigestHandler: digestHandlerMock, } s, err := core.NewService(cfg) diff --git a/dot/services.go b/dot/services.go index 49c2c1f03a..0cdb74a278 100644 --- a/dot/services.go +++ b/dot/services.go @@ -229,7 +229,7 @@ func createBABEService(cfg *Config, st *state.Service, ks keystore.Keystore, // createCoreService creates the core service from the provided core configuration func createCoreService(cfg *Config, ks *keystore.GlobalKeystore, - st *state.Service, net *network.Service, dh *digest.Handler) ( + st *state.Service, net *network.Service) ( *core.Service, error) { logger.Debug("creating core service" + asAuthority(cfg.Core.Roles == types.AuthorityRole) + @@ -254,7 +254,6 @@ func createCoreService(cfg *Config, ks *keystore.GlobalKeystore, TransactionState: st.Transaction, Keystore: ks, Network: net, - DigestHandler: dh, CodeSubstitutes: codeSubs, CodeSubstitutedState: st.Base, } diff --git a/dot/services_integration_test.go b/dot/services_integration_test.go index f00bd55ca7..c6511ecd6c 100644 --- a/dot/services_integration_test.go +++ b/dot/services_integration_test.go @@ -59,10 +59,7 @@ func TestCreateCoreService(t *testing.T) { networkSrvc := &network.Service{} - dh, err := createDigestHandler(cfg.Log.DigestLvl, stateSrvc) - require.NoError(t, err) - - coreSrvc, err := createCoreService(cfg, ks, stateSrvc, networkSrvc, dh) + coreSrvc, err := createCoreService(cfg, ks, stateSrvc, networkSrvc) require.NoError(t, err) require.NotNil(t, coreSrvc) } @@ -105,10 +102,7 @@ func TestCreateSyncService(t *testing.T) { ver, err := createBlockVerifier(stateSrvc) require.NoError(t, err) - dh, err := createDigestHandler(cfg.Log.DigestLvl, stateSrvc) - require.NoError(t, err) - - coreSrvc, err := createCoreService(cfg, ks, stateSrvc, &network.Service{}, dh) + coreSrvc, err := createCoreService(cfg, ks, stateSrvc, &network.Service{}) require.NoError(t, err) _, err = newSyncService(cfg, stateSrvc, &grandpa.Service{}, ver, coreSrvc, &network.Service{}, nil) @@ -162,10 +156,7 @@ func TestCreateRPCService(t *testing.T) { err = loadRuntime(cfg, ns, stateSrvc, ks, networkSrvc) require.NoError(t, err) - dh, err := createDigestHandler(cfg.Log.DigestLvl, stateSrvc) - require.NoError(t, err) - - coreSrvc, err := createCoreService(cfg, ks, stateSrvc, networkSrvc, dh) + coreSrvc, err := createCoreService(cfg, ks, stateSrvc, networkSrvc) require.NoError(t, err) sysSrvc, err := createSystemService(&cfg.System, stateSrvc) @@ -208,10 +199,7 @@ func TestCreateBABEService(t *testing.T) { err = loadRuntime(cfg, ns, stateSrvc, ks, &network.Service{}) require.NoError(t, err) - dh, err := createDigestHandler(cfg.Log.DigestLvl, stateSrvc) - require.NoError(t, err) - - coreSrvc, err := createCoreService(cfg, ks, stateSrvc, &network.Service{}, dh) + coreSrvc, err := createCoreService(cfg, ks, stateSrvc, &network.Service{}) require.NoError(t, err) bs, err := createBABEService(cfg, stateSrvc, ks.Babe, coreSrvc, nil) @@ -312,10 +300,7 @@ func TestNewWebSocketServer(t *testing.T) { err = loadRuntime(cfg, ns, stateSrvc, ks, networkSrvc) require.NoError(t, err) - dh, err := createDigestHandler(cfg.Log.DigestLvl, stateSrvc) - require.NoError(t, err) - - coreSrvc, err := createCoreService(cfg, ks, stateSrvc, networkSrvc, dh) + coreSrvc, err := createCoreService(cfg, ks, stateSrvc, networkSrvc) require.NoError(t, err) sysSrvc, err := createSystemService(&cfg.System, stateSrvc) diff --git a/dot/state/block.go b/dot/state/block.go index b3da60f1c1..0ce071a6e4 100644 --- a/dot/state/block.go +++ b/dot/state/block.go @@ -183,7 +183,8 @@ func (bs *BlockState) GenesisHash() common.Hash { return bs.genesisHash } -// HasHeader returns if the db contains a header with the given hash +// HasHeader returns true if the hash is part of the unfinalised blocks in-memory or +// persisted in the database. func (bs *BlockState) HasHeader(hash common.Hash) (bool, error) { if bs.unfinalisedBlocks.getBlock(hash) != nil { return true, nil @@ -192,6 +193,11 @@ func (bs *BlockState) HasHeader(hash common.Hash) (bool, error) { return bs.db.Has(headerKey(hash)) } +// HasHeaderInDatabase returns true if the database contains a header with the given hash +func (bs *BlockState) HasHeaderInDatabase(hash common.Hash) (bool, error) { + return bs.db.Has(headerKey(hash)) +} + // GetHeader returns a BlockHeader for a given hash func (bs *BlockState) GetHeader(hash common.Hash) (header *types.Header, err error) { header = bs.unfinalisedBlocks.getBlockHeader(hash) diff --git a/dot/state/block_finalisation.go b/dot/state/block_finalisation.go index e5adbc049e..d7677bebc4 100644 --- a/dot/state/block_finalisation.go +++ b/dot/state/block_finalisation.go @@ -117,6 +117,7 @@ func (bs *BlockState) GetHighestFinalisedHeader() (*types.Header, error) { func (bs *BlockState) SetFinalisedHash(hash common.Hash, round, setID uint64) error { bs.Lock() defer bs.Unlock() + has, _ := bs.HasHeader(hash) if !has { return fmt.Errorf("cannot finalise unknown block %s", hash) diff --git a/dot/state/epoch.go b/dot/state/epoch.go index a4b7d3c2c6..d77ae8fc32 100644 --- a/dot/state/epoch.go +++ b/dot/state/epoch.go @@ -7,13 +7,21 @@ import ( "encoding/binary" "errors" "fmt" + "sync" "time" "github.com/ChainSafe/chaindb" "github.com/ChainSafe/gossamer/dot/types" + "github.com/ChainSafe/gossamer/lib/common" "github.com/ChainSafe/gossamer/pkg/scale" ) +var ( + ErrEpochNotInMemory = errors.New("epoch not found in memory map") + errHashNotInMemory = errors.New("hash not found in memory map") + errHashNotPersisted = errors.New("hash with next epoch not found in database") +) + var ( epochPrefix = "epoch" epochLengthKey = []byte("epochlength") @@ -45,6 +53,14 @@ type EpochState struct { blockState *BlockState epochLength uint64 // measured in slots skipToEpoch uint64 + + nextEpochDataLock sync.RWMutex + // nextEpochData follows the format map[epoch]map[block hash]next epoch data + nextEpochData map[uint64]map[common.Hash]types.NextEpochData + + nextConfigDataLock sync.RWMutex + // nextConfigData follows the format map[epoch]map[block hash]next config data + nextConfigData map[uint64]map[common.Hash]types.NextConfigData } // NewEpochStateFromGenesis returns a new EpochState given information for the first epoch, fetched from the runtime @@ -68,10 +84,12 @@ func NewEpochStateFromGenesis(db chaindb.Database, blockState *BlockState, } s := &EpochState{ - baseState: NewBaseState(db), - blockState: blockState, - db: epochDB, - epochLength: genesisConfig.EpochLength, + baseState: NewBaseState(db), + blockState: blockState, + db: epochDB, + epochLength: genesisConfig.EpochLength, + nextEpochData: make(map[uint64]map[common.Hash]types.NextEpochData), + nextConfigData: make(map[uint64]map[common.Hash]types.NextConfigData), } auths, err := types.BABEAuthorityRawToAuthority(genesisConfig.GenesisAuthorities) @@ -126,11 +144,13 @@ func NewEpochState(db chaindb.Database, blockState *BlockState) (*EpochState, er } return &EpochState{ - baseState: baseState, - blockState: blockState, - db: chaindb.NewTable(db, epochPrefix), - epochLength: epochLength, - skipToEpoch: skipToEpoch, + baseState: baseState, + blockState: blockState, + db: chaindb.NewTable(db, epochPrefix), + epochLength: epochLength, + skipToEpoch: skipToEpoch, + nextEpochData: make(map[uint64]map[common.Hash]types.NextEpochData), + nextConfigData: make(map[uint64]map[common.Hash]types.NextConfigData), }, nil } @@ -220,8 +240,32 @@ func (s *EpochState) SetEpochData(epoch uint64, info *types.EpochData) error { return s.db.Put(epochDataKey(epoch), enc) } -// GetEpochData returns the epoch data for a given epoch -func (s *EpochState) GetEpochData(epoch uint64) (*types.EpochData, error) { +// GetEpochData returns the epoch data for a given epoch persisted in database +// otherwise will try to get the data from the in-memory map using the header +// if the header params is nil then it will search only in database +func (s *EpochState) GetEpochData(epoch uint64, header *types.Header) (*types.EpochData, error) { + epochData, err := s.getEpochDataInDatabase(epoch) + if err == nil && epochData != nil { + return epochData, nil + } + + if err != nil && !errors.Is(err, chaindb.ErrKeyNotFound) { + return nil, fmt.Errorf("failed to get epoch data from database: %w", err) + } else if header == nil { + // if no header is given then skip the lookup in-memory + return epochData, nil + } + + epochData, err = s.getEpochDataInMemory(epoch, header) + if err != nil { + return nil, fmt.Errorf("failed to get epoch data from memory: %w", err) + } + + return epochData, nil +} + +// getEpochDataInDatabase returns the epoch data for a given epoch persisted in database +func (s *EpochState) getEpochDataInDatabase(epoch uint64) (*types.EpochData, error) { enc, err := s.db.Get(epochDataKey(epoch)) if err != nil { return nil, err @@ -236,6 +280,32 @@ func (s *EpochState) GetEpochData(epoch uint64) (*types.EpochData, error) { return raw.ToEpochData() } +// getEpochDataInMemory retrieves the right epoch data that belongs to the header parameter +func (s *EpochState) getEpochDataInMemory(epoch uint64, header *types.Header) (*types.EpochData, error) { + s.nextEpochDataLock.RLock() + defer s.nextEpochDataLock.RUnlock() + + atEpoch, has := s.nextEpochData[epoch] + if !has { + return nil, fmt.Errorf("%w: %d", ErrEpochNotInMemory, epoch) + } + + headerHash := header.Hash() + + for hash, value := range atEpoch { + isDescendant, err := s.blockState.IsDescendantOf(hash, headerHash) + if err != nil { + return nil, fmt.Errorf("cannot verify the ancestry: %w", err) + } + + if isDescendant { + return value.ToEpochData() + } + } + + return nil, fmt.Errorf("%w: %s", errHashNotInMemory, headerHash) +} + // GetLatestEpochData returns the EpochData for the current epoch func (s *EpochState) GetLatestEpochData() (*types.EpochData, error) { curr, err := s.GetCurrentEpoch() @@ -243,12 +313,25 @@ func (s *EpochState) GetLatestEpochData() (*types.EpochData, error) { return nil, err } - return s.GetEpochData(curr) + return s.GetEpochData(curr, nil) } // HasEpochData returns whether epoch data exists for a given epoch func (s *EpochState) HasEpochData(epoch uint64) (bool, error) { - return s.db.Has(epochDataKey(epoch)) + has, err := s.db.Has(epochDataKey(epoch)) + if err == nil && has { + return has, nil + } + + if !errors.Is(chaindb.ErrKeyNotFound, err) { + return false, fmt.Errorf("cannot check database for epoch key %d: %w", epoch, err) + } + + s.nextEpochDataLock.Lock() + defer s.nextEpochDataLock.Unlock() + + _, has = s.nextEpochData[epoch] + return has, nil } // SetConfigData sets the BABE config data for a given epoch @@ -272,8 +355,32 @@ func (s *EpochState) setLatestConfigData(epoch uint64) error { return s.db.Put(latestConfigDataKey, buf) } -// GetConfigData returns the BABE config data for a given epoch -func (s *EpochState) GetConfigData(epoch uint64) (*types.ConfigData, error) { +// GetConfigData returns the config data for a given epoch persisted in database +// otherwise tries to get the data from the in-memory map using the header. +// If the header params is nil then it will search only in the database +func (s *EpochState) GetConfigData(epoch uint64, header *types.Header) (*types.ConfigData, error) { + configData, err := s.getConfigDataInDatabase(epoch) + if err == nil && configData != nil { + return configData, nil + } + + if err != nil && !errors.Is(err, chaindb.ErrKeyNotFound) { + return nil, fmt.Errorf("failed to get config data from database: %w", err) + } else if header == nil { + // if no header is given then skip the lookup in-memory + return configData, nil + } + + configData, err = s.getConfigDataInMemory(epoch, header) + if err != nil { + return nil, fmt.Errorf("failed to get config data from memory: %w", err) + } + + return configData, nil +} + +// getConfigDataInDatabase returns the BABE config data for a given epoch persisted in database +func (s *EpochState) getConfigDataInDatabase(epoch uint64) (*types.ConfigData, error) { enc, err := s.db.Get(configDataKey(epoch)) if err != nil { return nil, err @@ -288,6 +395,32 @@ func (s *EpochState) GetConfigData(epoch uint64) (*types.ConfigData, error) { return info, nil } +// getConfigDataInMemory retrieves the BABE config data for a given epoch that belongs to the header parameter +func (s *EpochState) getConfigDataInMemory(epoch uint64, header *types.Header) (*types.ConfigData, error) { + s.nextConfigDataLock.RLock() + defer s.nextConfigDataLock.RUnlock() + + atEpoch, has := s.nextConfigData[epoch] + if !has { + return nil, fmt.Errorf("%w: %d", ErrEpochNotInMemory, epoch) + } + + headerHash := header.Hash() + + for hash, value := range atEpoch { + isDescendant, err := s.blockState.IsDescendantOf(hash, headerHash) + if err != nil { + return nil, fmt.Errorf("cannot verify the ancestry: %w", err) + } + + if isDescendant { + return value.ToConfigData(), nil + } + } + + return nil, fmt.Errorf("%w: %s", errHashNotInMemory, headerHash) +} + // GetLatestConfigData returns the most recently set ConfigData func (s *EpochState) GetLatestConfigData() (*types.ConfigData, error) { b, err := s.db.Get(latestConfigDataKey) @@ -296,12 +429,25 @@ func (s *EpochState) GetLatestConfigData() (*types.ConfigData, error) { } epoch := binary.LittleEndian.Uint64(b) - return s.GetConfigData(epoch) + return s.GetConfigData(epoch, nil) } // HasConfigData returns whether config data exists for a given epoch func (s *EpochState) HasConfigData(epoch uint64) (bool, error) { - return s.db.Has(configDataKey(epoch)) + has, err := s.db.Has(configDataKey(epoch)) + if err == nil && has { + return has, nil + } + + if err != nil && !errors.Is(chaindb.ErrKeyNotFound, err) { + return false, fmt.Errorf("cannot check database for epoch key %d: %w", epoch, err) + } + + s.nextConfigDataLock.Lock() + defer s.nextConfigDataLock.Unlock() + + _, has = s.nextConfigData[epoch] + return has, nil } // GetStartSlotForEpoch returns the first slot in the given epoch. @@ -365,3 +511,115 @@ func (s *EpochState) SkipVerify(header *types.Header) (bool, error) { return false, nil } + +// StoreBABENextEpochData stores the types.NextEpochData under epoch and hash keys +func (s *EpochState) StoreBABENextEpochData(epoch uint64, hash common.Hash, nextEpochData types.NextEpochData) { + s.nextEpochDataLock.Lock() + defer s.nextEpochDataLock.Unlock() + + _, has := s.nextEpochData[epoch] + if !has { + s.nextEpochData[epoch] = make(map[common.Hash]types.NextEpochData) + } + s.nextEpochData[epoch][hash] = nextEpochData +} + +// StoreBABENextConfigData stores the types.NextConfigData under epoch and hash keys +func (s *EpochState) StoreBABENextConfigData(epoch uint64, hash common.Hash, nextConfigData types.NextConfigData) { + s.nextConfigDataLock.Lock() + defer s.nextConfigDataLock.Unlock() + + _, has := s.nextConfigData[epoch] + if !has { + s.nextConfigData[epoch] = make(map[common.Hash]types.NextConfigData) + } + s.nextConfigData[epoch][hash] = nextConfigData +} + +// FinalizeBABENextEpochData stores the right types.NextEpochData by +// getting the set of hashes from the received epoch and for each hash +// check if the header is in the database then it's been finalized and +// thus we can also set the corresponding EpochData in the database +func (s *EpochState) FinalizeBABENextEpochData(epoch uint64) error { + s.nextEpochDataLock.Lock() + defer s.nextEpochDataLock.Unlock() + + finalizedNextEpochData, err := lookupForNextEpochPersistedHash(s.nextEpochData, s, epoch) + if err != nil { + return fmt.Errorf("cannot find next epoch data: %w", err) + } + + ed, err := finalizedNextEpochData.ToEpochData() + if err != nil { + return fmt.Errorf("cannot transform epoch data: %w", err) + } + + err = s.SetEpochData(epoch, ed) + if err != nil { + return fmt.Errorf("cannot set epoch data: %w", err) + } + + // remove previous epochs from the memory + for e := range s.nextEpochData { + if e <= epoch { + delete(s.nextEpochData, e) + } + } + + return nil +} + +// FinalizeBABENextConfigData stores the right types.NextConfigData by +// getting the set of hashes from the received epoch and for each hash +// check if the header is in the database then it's been finalized and +// thus we can also set the corresponding NextConfigData in the database +func (s *EpochState) FinalizeBABENextConfigData(epoch uint64) error { + s.nextConfigDataLock.Lock() + defer s.nextConfigDataLock.Unlock() + + finalizedNextConfigData, err := lookupForNextEpochPersistedHash(s.nextConfigData, s, epoch) + if err != nil { + return fmt.Errorf("cannot find next config data: %w", err) + } + + cd := finalizedNextConfigData.ToConfigData() + err = s.SetConfigData(epoch, cd) + if err != nil { + return fmt.Errorf("cannot set config data: %w", err) + } + + // remove previous epochs from the memory + for e := range s.nextConfigData { + if e <= epoch { + delete(s.nextConfigData, e) + } + } + + return nil +} + +// lookupForNextEpochPersistedHash given a specific epoch (the key) will go through the hashes looking +// for a database persisted hash (belonging to the finalized chain) +// which contains the right configuration to be persisted and safely used +func lookupForNextEpochPersistedHash[T types.NextConfigData | types.NextEpochData]( + nextEpochMap map[uint64]map[common.Hash]T, es *EpochState, epoch uint64) (next *T, err error) { + hashes, has := nextEpochMap[epoch] + if !has { + return nil, ErrEpochNotInMemory + } + + for hash, inMemory := range hashes { + persisted, err := es.blockState.HasHeaderInDatabase(hash) + if err != nil { + return nil, fmt.Errorf("failed to check header exists in database: %w", err) + } + + if !persisted { + continue + } + + return &inMemory, nil + } + + return nil, errHashNotPersisted +} diff --git a/dot/state/epoch_test.go b/dot/state/epoch_test.go index 24946c93c4..f3b4247890 100644 --- a/dot/state/epoch_test.go +++ b/dot/state/epoch_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/ChainSafe/gossamer/dot/types" + "github.com/ChainSafe/gossamer/lib/common" "github.com/ChainSafe/gossamer/lib/crypto/sr25519" "github.com/ChainSafe/gossamer/lib/keystore" "github.com/ChainSafe/gossamer/pkg/scale" @@ -72,7 +73,7 @@ func TestEpochState_EpochData(t *testing.T) { err = s.SetEpochData(1, info) require.NoError(t, err) - res, err := s.GetEpochData(1) + res, err := s.GetEpochData(1, nil) require.NoError(t, err) require.Equal(t, info.Randomness, res.Randomness) @@ -127,7 +128,7 @@ func TestEpochState_ConfigData(t *testing.T) { err := s.SetConfigData(1, data) require.NoError(t, err) - ret, err := s.GetConfigData(1) + ret, err := s.GetConfigData(1, nil) require.NoError(t, err) require.Equal(t, data, ret) @@ -224,3 +225,385 @@ func TestEpochState_GetEpochFromTime(t *testing.T) { require.NoError(t, err) require.Equal(t, uint64(99), epoch) } + +type inMemoryNextEpochData struct { + epoch uint64 + hashes []common.Hash + nextEpochDatas []types.NextEpochData +} + +func TestStoreAndFinalizeBabeNextEpochData(t *testing.T) { + /* + * Setup the services: StateService, DigestHandler, EpochState + * and VerificationManager + */ + + keyring, _ := keystore.NewSr25519Keyring() + keyPairs := []*sr25519.Keypair{ + keyring.KeyAlice, keyring.KeyBob, keyring.KeyCharlie, + keyring.KeyDave, keyring.KeyEve, keyring.KeyFerdie, + keyring.KeyGeorge, keyring.KeyHeather, keyring.KeyIan, + } + + authorities := make([]types.AuthorityRaw, len(keyPairs)) + for i, keyPair := range keyPairs { + authorities[i] = types.AuthorityRaw{ + Key: keyPair.Public().(*sr25519.PublicKey).AsBytes(), + } + } + + tests := map[string]struct { + finalizeHash common.Hash + inMemoryEpoch []inMemoryNextEpochData + finalizeEpoch uint64 + expectErr error + shouldRemainInMemory int + }{ + "store_and_finalize_successfully": { + shouldRemainInMemory: 1, + finalizeEpoch: 2, + finalizeHash: common.MustHexToHash("0x68a27df5a52ff2251df2cc8368f7dcefb305a13bb3d89b65c8fb070f23877f2c"), + inMemoryEpoch: []inMemoryNextEpochData{ + { + epoch: 1, + hashes: []common.Hash{ + common.MustHexToHash("0x9da3ce2785da743bfbc13449db7dcb7a69c07ca914276d839abe7bedc6ac8fed"), + common.MustHexToHash("0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"), + common.MustHexToHash("0xc0096358534ec8d21d01d34b836eed476a1c343f8724fa2153dc0725ad797a90"), + }, + nextEpochDatas: []types.NextEpochData{ + { + Authorities: authorities[:3], + Randomness: [32]byte{1}, + }, + { + Authorities: authorities[3:6], + Randomness: [32]byte{2}, + }, + { + Authorities: authorities[6:], + Randomness: [32]byte{3}, + }, + }, + }, + { + epoch: 2, + hashes: []common.Hash{ + common.MustHexToHash("0x5b940c7fc0a1c5a58e4d80c5091dd003303b8f18e90a989f010c1be6f392bed1"), + common.MustHexToHash("0xd380bee22de487a707cbda65dd9d4e2188f736908c42cf390c8919d4f7fc547c"), + common.MustHexToHash("0x68a27df5a52ff2251df2cc8368f7dcefb305a13bb3d89b65c8fb070f23877f2c"), + }, + nextEpochDatas: []types.NextEpochData{ + { + Authorities: authorities[6:], + Randomness: [32]byte{1}, + }, + { + Authorities: authorities[:3], + Randomness: [32]byte{2}, + }, + { + Authorities: authorities[3:6], + Randomness: [32]byte{3}, + }, + }, + }, + { + epoch: 3, + hashes: []common.Hash{ + common.MustHexToHash("0xab5c9230a7dde8bb90a6728ba4a0165423294dac14336b1443f865b796ff682c"), + }, + nextEpochDatas: []types.NextEpochData{ + { + Authorities: authorities[6:], + Randomness: [32]byte{1}, + }, + }, + }, + }, + }, + "cannot_finalize_hash_not_stored": { + shouldRemainInMemory: 1, + finalizeEpoch: 1, + finalizeHash: common.Hash{}, // finalize when the hash does not exists + expectErr: errHashNotPersisted, + inMemoryEpoch: []inMemoryNextEpochData{ + { + epoch: 1, + hashes: []common.Hash{ + common.MustHexToHash("0x9da3ce2785da743bfbc13449db7dcb7a69c07ca914276d839abe7bedc6ac8fed"), + common.MustHexToHash("0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"), + common.MustHexToHash("0xc0096358534ec8d21d01d34b836eed476a1c343f8724fa2153dc0725ad797a90"), + }, + nextEpochDatas: []types.NextEpochData{ + { + Authorities: authorities[:3], + Randomness: [32]byte{1}, + }, + { + Authorities: authorities[3:6], + Randomness: [32]byte{2}, + }, + { + Authorities: authorities[6:], + Randomness: [32]byte{3}, + }, + }, + }, + }, + }, + "cannot_finalize_in_memory_epoch_not_found": { + shouldRemainInMemory: 1, + finalizeEpoch: 3, // try to finalize a epoch that does not exists + finalizeHash: common.Hash{}, + expectErr: ErrEpochNotInMemory, + inMemoryEpoch: []inMemoryNextEpochData{ + { + epoch: 1, + hashes: []common.Hash{ + common.MustHexToHash("0x9da3ce2785da743bfbc13449db7dcb7a69c07ca914276d839abe7bedc6ac8fed"), + common.MustHexToHash("0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"), + common.MustHexToHash("0xc0096358534ec8d21d01d34b836eed476a1c343f8724fa2153dc0725ad797a90"), + }, + nextEpochDatas: []types.NextEpochData{ + { + Authorities: authorities[:3], + Randomness: [32]byte{1}, + }, + { + Authorities: authorities[3:6], + Randomness: [32]byte{2}, + }, + { + Authorities: authorities[6:], + Randomness: [32]byte{3}, + }, + }, + }, + }, + }, + } + + for testName, tt := range tests { + t.Run(testName, func(t *testing.T) { + epochState := newEpochStateFromGenesis(t) + + for _, e := range tt.inMemoryEpoch { + for i, hash := range e.hashes { + epochState.StoreBABENextEpochData(e.epoch, hash, e.nextEpochDatas[i]) + } + } + + require.Len(t, epochState.nextEpochData, len(tt.inMemoryEpoch)) + + expectedNextEpochData := epochState.nextEpochData[tt.finalizeEpoch][tt.finalizeHash] + + err := epochState.blockState.db.Put(headerKey(tt.finalizeHash), []byte{}) + require.NoError(t, err) + + err = epochState.FinalizeBABENextEpochData(tt.finalizeEpoch) + if tt.expectErr != nil { + require.ErrorIs(t, err, tt.expectErr) + } else { + require.NoError(t, err) + + expected, err := expectedNextEpochData.ToEpochData() + require.NoError(t, err) + + gotNextEpochData, err := epochState.GetEpochData(tt.finalizeEpoch, nil) + require.NoError(t, err) + + require.Equal(t, expected, gotNextEpochData) + } + + // should delete previous epochs since the most up to date epoch is stored + require.Len(t, epochState.nextEpochData, tt.shouldRemainInMemory) + }) + } +} + +type inMemotyNextConfighData struct { + epoch uint64 + hashes []common.Hash + nextConfigDatas []types.NextConfigData +} + +func TestStoreAndFinalizeBabeNextConfigData(t *testing.T) { + tests := map[string]struct { + finalizeHash common.Hash + inMemoryEpoch []inMemotyNextConfighData + finalizeEpoch uint64 + expectErr error + shouldRemainInMemory int + }{ + "store_and_finalize_successfully": { + shouldRemainInMemory: 1, + finalizeEpoch: 2, + finalizeHash: common.MustHexToHash("0x68a27df5a52ff2251df2cc8368f7dcefb305a13bb3d89b65c8fb070f23877f2c"), + inMemoryEpoch: []inMemotyNextConfighData{ + { + epoch: 1, + hashes: []common.Hash{ + common.MustHexToHash("0x9da3ce2785da743bfbc13449db7dcb7a69c07ca914276d839abe7bedc6ac8fed"), + common.MustHexToHash("0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"), + common.MustHexToHash("0xc0096358534ec8d21d01d34b836eed476a1c343f8724fa2153dc0725ad797a90"), + }, + nextConfigDatas: []types.NextConfigData{ + { + C1: 1, + C2: 2, + SecondarySlots: 0, + }, + { + C1: 2, + C2: 3, + SecondarySlots: 1, + }, + { + C1: 3, + C2: 4, + SecondarySlots: 0, + }, + }, + }, + { + epoch: 2, + hashes: []common.Hash{ + common.MustHexToHash("0x5b940c7fc0a1c5a58e4d80c5091dd003303b8f18e90a989f010c1be6f392bed1"), + common.MustHexToHash("0xd380bee22de487a707cbda65dd9d4e2188f736908c42cf390c8919d4f7fc547c"), + common.MustHexToHash("0x68a27df5a52ff2251df2cc8368f7dcefb305a13bb3d89b65c8fb070f23877f2c"), + }, + nextConfigDatas: []types.NextConfigData{ + { + C1: 1, + C2: 2, + SecondarySlots: 0, + }, + { + C1: 2, + C2: 3, + SecondarySlots: 1, + }, + { + C1: 3, + C2: 4, + SecondarySlots: 0, + }, + }, + }, + { + epoch: 3, + hashes: []common.Hash{ + common.MustHexToHash("0xab5c9230a7dde8bb90a6728ba4a0165423294dac14336b1443f865b796ff682c"), + }, + nextConfigDatas: []types.NextConfigData{ + { + C1: 1, + C2: 2, + SecondarySlots: 0, + }, + }, + }, + }, + }, + "cannot_finalize_hash_doesnt_exists": { + shouldRemainInMemory: 1, + finalizeEpoch: 1, + finalizeHash: common.Hash{}, // finalize when the hash does not exists + expectErr: errHashNotPersisted, + inMemoryEpoch: []inMemotyNextConfighData{ + { + epoch: 1, + hashes: []common.Hash{ + common.MustHexToHash("0x9da3ce2785da743bfbc13449db7dcb7a69c07ca914276d839abe7bedc6ac8fed"), + common.MustHexToHash("0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"), + common.MustHexToHash("0xc0096358534ec8d21d01d34b836eed476a1c343f8724fa2153dc0725ad797a90"), + }, + nextConfigDatas: []types.NextConfigData{ + { + C1: 1, + C2: 2, + SecondarySlots: 0, + }, + { + C1: 2, + C2: 3, + SecondarySlots: 1, + }, + { + C1: 3, + C2: 4, + SecondarySlots: 0, + }, + }, + }, + }, + }, + "cannot_finalize_in_memory_epoch_not_found": { + shouldRemainInMemory: 1, + finalizeEpoch: 3, // try to finalize a epoch that does not exists + finalizeHash: common.Hash{}, + expectErr: ErrEpochNotInMemory, + inMemoryEpoch: []inMemotyNextConfighData{ + { + epoch: 1, + hashes: []common.Hash{ + common.MustHexToHash("0x9da3ce2785da743bfbc13449db7dcb7a69c07ca914276d839abe7bedc6ac8fed"), + common.MustHexToHash("0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3"), + common.MustHexToHash("0xc0096358534ec8d21d01d34b836eed476a1c343f8724fa2153dc0725ad797a90"), + }, + nextConfigDatas: []types.NextConfigData{ + { + C1: 1, + C2: 2, + SecondarySlots: 0, + }, + { + C1: 2, + C2: 3, + SecondarySlots: 1, + }, + { + C1: 3, + C2: 4, + SecondarySlots: 0, + }, + }, + }, + }, + }, + } + + for testName, tt := range tests { + t.Run(testName, func(t *testing.T) { + epochState := newEpochStateFromGenesis(t) + + for _, e := range tt.inMemoryEpoch { + for i, hash := range e.hashes { + epochState.StoreBABENextConfigData(e.epoch, hash, e.nextConfigDatas[i]) + } + } + + require.Len(t, epochState.nextConfigData, len(tt.inMemoryEpoch)) + + expectedConfigData := epochState.nextConfigData[tt.finalizeEpoch][tt.finalizeHash] + + err := epochState.blockState.db.Put(headerKey(tt.finalizeHash), []byte{}) + require.NoError(t, err) + + err = epochState.FinalizeBABENextConfigData(tt.finalizeEpoch) + if tt.expectErr != nil { + require.ErrorIs(t, err, tt.expectErr) + } else { + require.NoError(t, err) + + gotConfigData, err := epochState.GetConfigData(tt.finalizeEpoch, nil) + require.NoError(t, err) + require.Equal(t, expectedConfigData.ToConfigData(), gotConfigData) + } + + // should delete previous epochs since the most up to date epoch is stored + require.Len(t, epochState.nextConfigData, tt.shouldRemainInMemory) + }) + } +} diff --git a/dot/state/grandpa.go b/dot/state/grandpa.go index 0983e9614d..7b29387e25 100644 --- a/dot/state/grandpa.go +++ b/dot/state/grandpa.go @@ -6,6 +6,7 @@ package state import ( "encoding/binary" "errors" + "fmt" "github.com/ChainSafe/chaindb" "github.com/ChainSafe/gossamer/dot/types" @@ -159,14 +160,19 @@ func (s *GrandpaState) SetNextChange(authorities []types.GrandpaVoter, number ui } // IncrementSetID increments the set ID -func (s *GrandpaState) IncrementSetID() error { +func (s *GrandpaState) IncrementSetID() (newSetID uint64, err error) { currSetID, err := s.GetCurrentSetID() if err != nil { - return err + return 0, fmt.Errorf("cannot get current set ID: %w", err) } - nextSetID := currSetID + 1 - return s.setCurrentSetID(nextSetID) + newSetID = currSetID + 1 + err = s.setCurrentSetID(newSetID) + if err != nil { + return 0, fmt.Errorf("cannot set current set ID: %w", err) + } + + return newSetID, nil } // setSetIDChangeAtBlock sets a set ID change at a certain block diff --git a/dot/state/grandpa_test.go b/dot/state/grandpa_test.go index a22add98fb..20bf45869a 100644 --- a/dot/state/grandpa_test.go +++ b/dot/state/grandpa_test.go @@ -60,10 +60,7 @@ func TestGrandpaState_IncrementSetID(t *testing.T) { gs, err := NewGrandpaStateFromGenesis(db, testAuths) require.NoError(t, err) - err = gs.IncrementSetID() - require.NoError(t, err) - - setID, err := gs.GetCurrentSetID() + setID, err := gs.IncrementSetID() require.NoError(t, err) require.Equal(t, genesisSetID+1, setID) } @@ -88,7 +85,7 @@ func TestGrandpaState_GetSetIDByBlockNumber(t *testing.T) { require.NoError(t, err) require.Equal(t, genesisSetID+1, setID) - err = gs.IncrementSetID() + newSetID, err := gs.IncrementSetID() require.NoError(t, err) setID, err = gs.GetSetIDByBlockNumber(100) @@ -98,6 +95,7 @@ func TestGrandpaState_GetSetIDByBlockNumber(t *testing.T) { setID, err = gs.GetSetIDByBlockNumber(101) require.NoError(t, err) require.Equal(t, genesisSetID+1, setID) + require.Equal(t, genesisSetID+1, newSetID) } func TestGrandpaState_LatestRound(t *testing.T) { diff --git a/dot/types/digest.go b/dot/types/digest.go index 73894c160a..e0796dcca1 100644 --- a/dot/types/digest.go +++ b/dot/types/digest.go @@ -80,7 +80,7 @@ type ConsensusDigest struct { func (d ConsensusDigest) Index() uint { return 4 } // String returns the digest as a string -func (d *ConsensusDigest) String() string { +func (d ConsensusDigest) String() string { return fmt.Sprintf("ConsensusDigest ConsensusEngineID=%s Data=0x%x", d.ConsensusEngineID.ToBytes(), d.Data) } diff --git a/lib/babe/epoch.go b/lib/babe/epoch.go index 7d556767db..379e53be1c 100644 --- a/lib/babe/epoch.go +++ b/lib/babe/epoch.go @@ -100,7 +100,7 @@ func (b *Service) getEpochDataAndStartSlot(epoch uint64) (*epochData, uint64, er return nil, 0, fmt.Errorf("%w: for epoch %d", errNoEpochData, epoch) } - data, err := b.epochState.GetEpochData(epoch) + data, err := b.epochState.GetEpochData(epoch, nil) if err != nil { return nil, 0, fmt.Errorf("cannot get epoch data for epoch %d: %w", epoch, err) } @@ -117,7 +117,7 @@ func (b *Service) getEpochDataAndStartSlot(epoch uint64) (*epochData, uint64, er var cfgData *types.ConfigData if has { - cfgData, err = b.epochState.GetConfigData(epoch) + cfgData, err = b.epochState.GetConfigData(epoch, nil) if err != nil { return nil, 0, fmt.Errorf("cannot get config data for epoch %d: %w", epoch, err) } diff --git a/lib/babe/epoch_integration_test.go b/lib/babe/epoch_integration_test.go index 2b285a2c01..dd2adc3fbc 100644 --- a/lib/babe/epoch_integration_test.go +++ b/lib/babe/epoch_integration_test.go @@ -42,7 +42,7 @@ func TestInitiateEpoch_Epoch1(t *testing.T) { Weight: 1, } - data, err := bs.epochState.GetEpochData(0) + data, err := bs.epochState.GetEpochData(0, nil) require.NoError(t, err) data.Authorities = []types.Authority{auth} err = bs.epochState.SetEpochData(1, data) diff --git a/lib/babe/epoch_test.go b/lib/babe/epoch_test.go index bb2a2b5b77..0e4d62ccfe 100644 --- a/lib/babe/epoch_test.go +++ b/lib/babe/epoch_test.go @@ -91,8 +91,8 @@ func TestBabeService_getEpochDataAndStartSlot(t *testing.T) { Authorities: []types.Authority{*authority}, } - mockEpochState1.EXPECT().GetEpochData(uint64(1)).Return(testEpochData, nil) - mockEpochState2.EXPECT().GetEpochData(uint64(1)).Return(testEpochData, nil) + mockEpochState1.EXPECT().GetEpochData(uint64(1), nil).Return(testEpochData, nil) + mockEpochState2.EXPECT().GetEpochData(uint64(1), nil).Return(testEpochData, nil) mockEpochState1.EXPECT().HasConfigData(uint64(1)).Return(true, nil) mockEpochState2.EXPECT().HasConfigData(uint64(1)).Return(false, nil) @@ -102,7 +102,7 @@ func TestBabeService_getEpochDataAndStartSlot(t *testing.T) { C2: 1, } - mockEpochState1.EXPECT().GetConfigData(uint64(1)).Return(testConfigData, nil) + mockEpochState1.EXPECT().GetConfigData(uint64(1), nil).Return(testConfigData, nil) testLatestConfigData := &types.ConfigData{ C1: 1, diff --git a/lib/babe/mock_state_test.go b/lib/babe/mock_state_test.go index 2e11a41f36..f5bdfd2d91 100644 --- a/lib/babe/mock_state_test.go +++ b/lib/babe/mock_state_test.go @@ -529,18 +529,18 @@ func (m *MockEpochState) EXPECT() *MockEpochStateMockRecorder { } // GetConfigData mocks base method. -func (m *MockEpochState) GetConfigData(arg0 uint64) (*types.ConfigData, error) { +func (m *MockEpochState) GetConfigData(arg0 uint64, arg1 *types.Header) (*types.ConfigData, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetConfigData", arg0) + ret := m.ctrl.Call(m, "GetConfigData", arg0, arg1) ret0, _ := ret[0].(*types.ConfigData) ret1, _ := ret[1].(error) return ret0, ret1 } // GetConfigData indicates an expected call of GetConfigData. -func (mr *MockEpochStateMockRecorder) GetConfigData(arg0 interface{}) *gomock.Call { +func (mr *MockEpochStateMockRecorder) GetConfigData(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfigData", reflect.TypeOf((*MockEpochState)(nil).GetConfigData), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfigData", reflect.TypeOf((*MockEpochState)(nil).GetConfigData), arg0, arg1) } // GetCurrentEpoch mocks base method. @@ -559,18 +559,18 @@ func (mr *MockEpochStateMockRecorder) GetCurrentEpoch() *gomock.Call { } // GetEpochData mocks base method. -func (m *MockEpochState) GetEpochData(arg0 uint64) (*types.EpochData, error) { +func (m *MockEpochState) GetEpochData(arg0 uint64, arg1 *types.Header) (*types.EpochData, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEpochData", arg0) + ret := m.ctrl.Call(m, "GetEpochData", arg0, arg1) ret0, _ := ret[0].(*types.EpochData) ret1, _ := ret[1].(error) return ret0, ret1 } // GetEpochData indicates an expected call of GetEpochData. -func (mr *MockEpochStateMockRecorder) GetEpochData(arg0 interface{}) *gomock.Call { +func (mr *MockEpochStateMockRecorder) GetEpochData(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEpochData", reflect.TypeOf((*MockEpochState)(nil).GetEpochData), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEpochData", reflect.TypeOf((*MockEpochState)(nil).GetEpochData), arg0, arg1) } // GetEpochForBlock mocks base method. diff --git a/lib/babe/state.go b/lib/babe/state.go index 3e59be8aea..5b168e850a 100644 --- a/lib/babe/state.go +++ b/lib/babe/state.go @@ -65,9 +65,12 @@ type EpochState interface { SetCurrentEpoch(epoch uint64) error GetCurrentEpoch() (uint64, error) SetEpochData(uint64, *types.EpochData) error - GetEpochData(epoch uint64) (*types.EpochData, error) + HasEpochData(epoch uint64) (bool, error) - GetConfigData(epoch uint64) (*types.ConfigData, error) + + GetEpochData(epoch uint64, header *types.Header) (*types.EpochData, error) + GetConfigData(epoch uint64, header *types.Header) (*types.ConfigData, error) + HasConfigData(epoch uint64) (bool, error) GetLatestConfigData() (*types.ConfigData, error) GetStartSlotForEpoch(epoch uint64) (uint64, error) diff --git a/lib/babe/verify.go b/lib/babe/verify.go index 459b5e57c9..add58392a4 100644 --- a/lib/babe/verify.go +++ b/lib/babe/verify.go @@ -72,7 +72,7 @@ func (v *VerificationManager) SetOnDisabled(index uint32, header *types.Header) defer v.lock.Unlock() if _, has := v.epochInfo[epoch]; !has { - info, err := v.getVerifierInfo(epoch) + info, err := v.getVerifierInfo(epoch, header) if err != nil { return err } @@ -127,6 +127,7 @@ func (v *VerificationManager) SetOnDisabled(index uint32, header *types.Header) } // VerifyBlock verifies that the block producer for the given block was authorized to produce it. +// It checks the next epoch and config data stored in memory only if it cannot retrieve the data from database // It returns an error if the block is invalid. func (v *VerificationManager) VerifyBlock(header *types.Header) error { var ( @@ -165,7 +166,7 @@ func (v *VerificationManager) VerifyBlock(header *types.Header) error { v.lock.Lock() if info, has = v.epochInfo[epoch]; !has { - info, err = v.getVerifierInfo(epoch) + info, err = v.getVerifierInfo(epoch, header) if err != nil { v.lock.Unlock() // SkipVerify is set to true only in the case where we have imported a state at a given height, @@ -195,13 +196,13 @@ func (v *VerificationManager) VerifyBlock(header *types.Header) error { return verifier.verifyAuthorshipRight(header) } -func (v *VerificationManager) getVerifierInfo(epoch uint64) (*verifierInfo, error) { - epochData, err := v.epochState.GetEpochData(epoch) +func (v *VerificationManager) getVerifierInfo(epoch uint64, header *types.Header) (*verifierInfo, error) { + epochData, err := v.epochState.GetEpochData(epoch, header) if err != nil { return nil, fmt.Errorf("failed to get epoch data for epoch %d: %w", epoch, err) } - configData, err := v.getConfigData(epoch) + configData, err := v.getConfigData(epoch, header) if err != nil { return nil, fmt.Errorf("failed to get config data: %w", err) } @@ -219,16 +220,16 @@ func (v *VerificationManager) getVerifierInfo(epoch uint64) (*verifierInfo, erro }, nil } -func (v *VerificationManager) getConfigData(epoch uint64) (*types.ConfigData, error) { +func (v *VerificationManager) getConfigData(epoch uint64, header *types.Header) (*types.ConfigData, error) { for i := int(epoch); i >= 0; i-- { has, err := v.epochState.HasConfigData(uint64(i)) if err != nil { return nil, err + } else if !has { + continue } - if has { - return v.epochState.GetConfigData(uint64(i)) - } + return v.epochState.GetConfigData(uint64(i), header) } return nil, errNoConfigData diff --git a/lib/babe/verify_integration_test.go b/lib/babe/verify_integration_test.go index 9c1f201aab..48cdd864da 100644 --- a/lib/babe/verify_integration_test.go +++ b/lib/babe/verify_integration_test.go @@ -7,9 +7,14 @@ package babe import ( "errors" + "fmt" "testing" + "time" + "github.com/ChainSafe/chaindb" + "github.com/ChainSafe/gossamer/dot/digest" "github.com/ChainSafe/gossamer/dot/state" + "github.com/ChainSafe/gossamer/dot/telemetry" "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/internal/log" "github.com/ChainSafe/gossamer/lib/common" @@ -427,3 +432,296 @@ func TestVerifyAuthorshipRight_Equivocation(t *testing.T) { err = verifier.verifyAuthorshipRight(&block2.Header) require.Equal(t, ErrProducerEquivocated, err) } + +func TestVerifyForkBlocksWithRespectiveEpochData(t *testing.T) { + /* + * Setup the services: StateService, DigestHandler, EpochState + * and VerificationManager + */ + keyPairs := []*sr25519.Keypair{ + keyring.KeyAlice, keyring.KeyBob, keyring.KeyCharlie, + keyring.KeyDave, keyring.KeyEve, keyring.KeyFerdie, + keyring.KeyGeorge, keyring.KeyHeather, keyring.KeyIan, + } + + authorities := make([]types.AuthorityRaw, len(keyPairs)) + for i, keyPair := range keyPairs { + authorities[i] = types.AuthorityRaw{ + Key: keyPair.Public().(*sr25519.PublicKey).AsBytes(), + } + } + + // starts with only 3 authorities in the authority set + epochBABEConfig := &types.BabeConfiguration{ + SlotDuration: 1000, + EpochLength: 10, + C1: 1, + C2: 4, + GenesisAuthorities: authorities[:3], + Randomness: [32]byte{}, + SecondarySlots: 0, + } + + genesis, trie, genesisHeader := genesis.NewTestGenesisWithTrieAndHeader(t) + + ctrl := gomock.NewController(t) + telemetryMock := NewMockClient(ctrl) + telemetryMock.EXPECT().SendMessage( + telemetry.NewNotifyFinalized( + genesisHeader.Hash(), + fmt.Sprint(genesisHeader.Number), + ), + ) + + stateService := state.NewService(state.Config{ + Path: t.TempDir(), + Telemetry: telemetryMock, + }) + + stateService.UseMemDB() + + err := stateService.Initialise(genesis, genesisHeader, trie) + require.NoError(t, err) + + inMemoryDB, err := chaindb.NewBadgerDB(&chaindb.Config{ + InMemory: true, + DataDir: t.TempDir(), + }) + require.NoError(t, err) + + epochState, err := state.NewEpochStateFromGenesis(inMemoryDB, stateService.Block, epochBABEConfig) + require.NoError(t, err) + + digestHandler, err := digest.NewHandler(log.DoNotChange, stateService.Block, epochState, stateService.Grandpa) + require.NoError(t, err) + + digestHandler.Start() + + verificationManager, err := NewVerificationManager(stateService.Block, epochState) + require.NoError(t, err) + + /* + * lets issue different blocks starting from genesis (a fork) + */ + aliceBlockNextEpoch := types.NextEpochData{ + Authorities: authorities[3:], + } + aliceBlockNextConfigData := types.NextConfigData{ + C1: 9, + C2: 10, + SecondarySlots: 1, + } + aliceBlockHeader := issueConsensusDigestsBlockFromGenesis(t, genesisHeader, keyring.KeyAlice, + stateService, aliceBlockNextEpoch, aliceBlockNextConfigData) + + bobBlockNextEpoch := types.NextEpochData{ + Authorities: authorities[6:], + } + bobBlockNextConfigData := types.NextConfigData{ + C1: 3, + C2: 8, + SecondarySlots: 1, + } + bobBlockHeader := issueConsensusDigestsBlockFromGenesis(t, genesisHeader, keyring.KeyBob, + stateService, bobBlockNextEpoch, bobBlockNextConfigData) + + // wait for digest handleBlockImport goroutine gets the imported + // block, process its digest and store the info at epoch state + time.Sleep(time.Second * 2) + + /* + * Simulate a fork from the genesis file, the fork alice and the fork bob + * contains different digest handlers. + */ + const chainLen = 5 + forkAliceChain := make([]types.Header, chainLen) + forkBobChain := make([]types.Header, chainLen) + + forkAliceLastHeader := aliceBlockHeader + forkBobLastHeader := bobBlockHeader + + for idx := 0; idx < chainLen; idx++ { + forkAliceLastHeader = issueNewBlockFrom(t, forkAliceLastHeader, + keyring.KeyAlice, stateService) + + forkBobLastHeader = issueNewBlockFrom(t, forkBobLastHeader, + keyring.KeyBob, stateService) + + forkAliceChain[idx] = *forkAliceLastHeader + forkBobChain[idx] = *forkBobLastHeader + } + + // verify if each block from the fork alice get the right digest + const epochToTest = 1 + + expectedThreshold, err := CalculateThreshold(aliceBlockNextConfigData.C1, + aliceBlockNextConfigData.C2, len(authorities[3:])) + require.NoError(t, err) + + for _, headerToVerify := range forkAliceChain { + verifierInfo, err := verificationManager.getVerifierInfo(epochToTest, &headerToVerify) + require.NoError(t, err) + + require.Equal(t, len(authorities[3:]), len(verifierInfo.authorities)) + rawAuthorities := make([]types.AuthorityRaw, len(verifierInfo.authorities)) + + for i, auth := range verifierInfo.authorities { + rawAuthorities[i] = *auth.ToRaw() + } + + require.ElementsMatch(t, authorities[3:], rawAuthorities) + require.True(t, verifierInfo.secondarySlots) + require.Equal(t, expectedThreshold, verifierInfo.threshold) + } + + // each block from the fork bob should use the right digest + + expectedThreshold, err = CalculateThreshold(bobBlockNextConfigData.C1, + bobBlockNextConfigData.C2, len(authorities[6:])) + require.NoError(t, err) + + for _, headerToVerify := range forkBobChain { + verifierInfo, err := verificationManager.getVerifierInfo(epochToTest, &headerToVerify) + require.NoError(t, err) + + require.Equal(t, len(authorities[6:]), len(verifierInfo.authorities)) + rawAuthorities := make([]types.AuthorityRaw, len(verifierInfo.authorities)) + + for i, auth := range verifierInfo.authorities { + rawAuthorities[i] = *auth.ToRaw() + } + + // should keep the original authorities + require.ElementsMatch(t, authorities[6:], rawAuthorities) + require.True(t, verifierInfo.secondarySlots) + require.Equal(t, expectedThreshold, verifierInfo.threshold) + } + + telemetryMock.EXPECT().SendMessage( + telemetry.NewNotifyFinalized( + forkBobLastHeader.Hash(), + fmt.Sprint(forkBobLastHeader.Number), + ), + ) + err = stateService.Block.SetFinalisedHash(forkBobLastHeader.Hash(), 1, 1) + require.NoError(t, err) + + // wait for digest handleBlockFinalize goroutine gets the finalized + // block, clean up the in memory data and store the finalized digest in db + time.Sleep(time.Second * 2) + + // as a chain was finalized any block built upon it should use the database stored data + blockUponFinalizedHeader := issueNewBlockFrom(t, forkBobLastHeader, + keyring.KeyBob, stateService) + + verifierInfo, err := verificationManager.getVerifierInfo(epochToTest, blockUponFinalizedHeader) + require.NoError(t, err) + + require.Equal(t, len(authorities[6:]), len(verifierInfo.authorities)) + rawAuthorities := make([]types.AuthorityRaw, len(verifierInfo.authorities)) + + for i, auth := range verifierInfo.authorities { + rawAuthorities[i] = *auth.ToRaw() + } + + // should keep the original authorities + require.ElementsMatch(t, authorities[6:], rawAuthorities) + require.True(t, verifierInfo.secondarySlots) + require.Equal(t, expectedThreshold, verifierInfo.threshold) +} + +// issueConsensusDigestsBlocksFromGenesis will create different +// blocks that contains different consensus messages digests +func issueConsensusDigestsBlockFromGenesis(t *testing.T, genesisHeader *types.Header, + kp *sr25519.Keypair, stateService *state.Service, + nextEpoch types.NextEpochData, nextConfig types.NextConfigData) *types.Header { + t.Helper() + + output, proof, err := kp.VrfSign(makeTranscript(Randomness{}, uint64(0), 0)) + require.NoError(t, err) + + babePrimaryPreDigest := types.BabePrimaryPreDigest{ + SlotNumber: 1, + VRFOutput: output, + VRFProof: proof, + } + + preRuntimeDigest, err := babePrimaryPreDigest.ToPreRuntimeDigest() + require.NoError(t, err) + + babeConsensusDigestNextEpoch := types.NewBabeConsensusDigest() + require.NoError(t, babeConsensusDigestNextEpoch.Set(nextEpoch)) + + babeConsensusDigestNextConfigData := types.NewBabeConsensusDigest() + require.NoError(t, babeConsensusDigestNextConfigData.Set(nextConfig)) + + nextEpochData, err := scale.Marshal(babeConsensusDigestNextEpoch) + require.NoError(t, err) + + nextEpochConsensusDigest := types.ConsensusDigest{ + ConsensusEngineID: types.BabeEngineID, + Data: nextEpochData, + } + + nextConfigData, err := scale.Marshal(babeConsensusDigestNextConfigData) + require.NoError(t, err) + + nextConfigConsensusDigest := types.ConsensusDigest{ + ConsensusEngineID: types.BabeEngineID, + Data: nextConfigData, + } + + digest := types.NewDigest() + require.NoError(t, digest.Add(*preRuntimeDigest, nextEpochConsensusDigest, nextConfigConsensusDigest)) + + headerWhoOwnsNextEpochDigest := &types.Header{ + ParentHash: genesisHeader.Hash(), + Number: 1, + Digest: digest, + } + + err = stateService.Block.AddBlock(&types.Block{ + Header: *headerWhoOwnsNextEpochDigest, + Body: *types.NewBody([]types.Extrinsic{}), + }) + require.NoError(t, err) + + return headerWhoOwnsNextEpochDigest +} + +// issueNewBlockFrom will create and store a new block following a chain +func issueNewBlockFrom(t *testing.T, parentHeader *types.Header, + kp *sr25519.Keypair, stateService *state.Service) *types.Header { + t.Helper() + + output, proof, err := kp.VrfSign(makeTranscript(Randomness{}, uint64(1), 1)) + require.NoError(t, err) + + babePrimaryPreDigest := types.BabePrimaryPreDigest{ + SlotNumber: 1, + VRFOutput: output, + VRFProof: proof, + } + + preRuntimeDigest, err := babePrimaryPreDigest.ToPreRuntimeDigest() + require.NoError(t, err) + + digest := scale.NewVaryingDataTypeSlice(scale.MustNewVaryingDataType( + types.PreRuntimeDigest{})) + + require.NoError(t, digest.Add(*preRuntimeDigest)) + + header := &types.Header{ + ParentHash: parentHeader.Hash(), + Number: parentHeader.Number + 1, + Digest: digest, + } + + err = stateService.Block.AddBlock(&types.Block{ + Header: *header, + Body: *types.NewBody([]types.Extrinsic{}), + }) + require.NoError(t, err) + + return header +} diff --git a/lib/babe/verify_test.go b/lib/babe/verify_test.go index 40cb479883..f6c106ca7c 100644 --- a/lib/babe/verify_test.go +++ b/lib/babe/verify_test.go @@ -737,10 +737,12 @@ func TestVerificationManager_getConfigData(t *testing.T) { mockEpochStateHasErr := NewMockEpochState(ctrl) mockEpochStateGetErr := NewMockEpochState(ctrl) + testHeader := types.NewEmptyHeader() + mockEpochStateEmpty.EXPECT().HasConfigData(uint64(0)).Return(false, nil) mockEpochStateHasErr.EXPECT().HasConfigData(uint64(0)).Return(false, errNoConfigData) mockEpochStateGetErr.EXPECT().HasConfigData(uint64(0)).Return(true, nil) - mockEpochStateGetErr.EXPECT().GetConfigData(uint64(0)).Return(nil, errNoConfigData) + mockEpochStateGetErr.EXPECT().GetConfigData(uint64(0), testHeader).Return(nil, errNoConfigData) vm0, err := NewVerificationManager(mockBlockState, mockEpochStateEmpty) assert.NoError(t, err) @@ -774,7 +776,7 @@ func TestVerificationManager_getConfigData(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := tt.vm - res, err := v.getConfigData(tt.epoch) + res, err := v.getConfigData(tt.epoch, testHeader) if tt.expErr != nil { assert.EqualError(t, err, tt.expErr.Error()) } else { @@ -793,22 +795,24 @@ func TestVerificationManager_getVerifierInfo(t *testing.T) { mockEpochStateThresholdErr := NewMockEpochState(ctrl) mockEpochStateOk := NewMockEpochState(ctrl) - mockEpochStateGetErr.EXPECT().GetEpochData(uint64(0)).Return(nil, errNoConfigData) + testHeader := types.NewEmptyHeader() + + mockEpochStateGetErr.EXPECT().GetEpochData(uint64(0), testHeader).Return(nil, errNoConfigData) - mockEpochStateHasErr.EXPECT().GetEpochData(uint64(0)).Return(&types.EpochData{}, nil) + mockEpochStateHasErr.EXPECT().GetEpochData(uint64(0), testHeader).Return(&types.EpochData{}, nil) mockEpochStateHasErr.EXPECT().HasConfigData(uint64(0)).Return(false, errNoConfigData) - mockEpochStateThresholdErr.EXPECT().GetEpochData(uint64(0)).Return(&types.EpochData{}, nil) + mockEpochStateThresholdErr.EXPECT().GetEpochData(uint64(0), testHeader).Return(&types.EpochData{}, nil) mockEpochStateThresholdErr.EXPECT().HasConfigData(uint64(0)).Return(true, nil) - mockEpochStateThresholdErr.EXPECT().GetConfigData(uint64(0)). + mockEpochStateThresholdErr.EXPECT().GetConfigData(uint64(0), testHeader). Return(&types.ConfigData{ C1: 3, C2: 1, }, nil) - mockEpochStateOk.EXPECT().GetEpochData(uint64(0)).Return(&types.EpochData{}, nil) + mockEpochStateOk.EXPECT().GetEpochData(uint64(0), testHeader).Return(&types.EpochData{}, nil) mockEpochStateOk.EXPECT().HasConfigData(uint64(0)).Return(true, nil) - mockEpochStateOk.EXPECT().GetConfigData(uint64(0)). + mockEpochStateOk.EXPECT().GetConfigData(uint64(0), testHeader). Return(&types.ConfigData{ C1: 1, C2: 3, @@ -856,7 +860,7 @@ func TestVerificationManager_getVerifierInfo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := tt.vm - res, err := v.getVerifierInfo(tt.epoch) + res, err := v.getVerifierInfo(tt.epoch, testHeader) if tt.expErr != nil { assert.EqualError(t, err, tt.expErr.Error()) } else { @@ -905,15 +909,15 @@ func TestVerificationManager_VerifyBlock(t *testing.T) { Return(uint64(0), errGetEpoch) mockEpochStateSkipVerifyErr.EXPECT().GetEpochForBlock(testBlockHeaderEmpty).Return(uint64(1), nil) - mockEpochStateSkipVerifyErr.EXPECT().GetEpochData(uint64(1)).Return(nil, errGetEpochData) + mockEpochStateSkipVerifyErr.EXPECT().GetEpochData(uint64(1), testBlockHeaderEmpty).Return(nil, errGetEpochData) mockEpochStateSkipVerifyErr.EXPECT().SkipVerify(testBlockHeaderEmpty).Return(false, errSkipVerify) mockEpochStateSkipVerifyTrue.EXPECT().GetEpochForBlock(testBlockHeaderEmpty).Return(uint64(1), nil) - mockEpochStateSkipVerifyTrue.EXPECT().GetEpochData(uint64(1)).Return(nil, errGetEpochData) + mockEpochStateSkipVerifyTrue.EXPECT().GetEpochData(uint64(1), testBlockHeaderEmpty).Return(nil, errGetEpochData) mockEpochStateSkipVerifyTrue.EXPECT().SkipVerify(testBlockHeaderEmpty).Return(true, nil) mockEpochStateGetVerifierInfoErr.EXPECT().GetEpochForBlock(testBlockHeaderEmpty).Return(uint64(1), nil) - mockEpochStateGetVerifierInfoErr.EXPECT().GetEpochData(uint64(1)). + mockEpochStateGetVerifierInfoErr.EXPECT().GetEpochData(uint64(1), testBlockHeaderEmpty). Return(nil, errGetEpochData) mockEpochStateGetVerifierInfoErr.EXPECT().SkipVerify(testBlockHeaderEmpty).Return(false, nil) @@ -1065,7 +1069,7 @@ func TestVerificationManager_SetOnDisabled(t *testing.T) { mockEpochStateGetEpochErr.EXPECT().GetEpochForBlock(types.NewEmptyHeader()).Return(uint64(0), errGetEpoch) mockEpochStateGetEpochDataErr.EXPECT().GetEpochForBlock(types.NewEmptyHeader()).Return(uint64(0), nil) - mockEpochStateGetEpochDataErr.EXPECT().GetEpochData(uint64(0)).Return(nil, errGetEpochData) + mockEpochStateGetEpochDataErr.EXPECT().GetEpochData(uint64(0), types.NewEmptyHeader()).Return(nil, errGetEpochData) mockEpochStateIndexLenErr.EXPECT().GetEpochForBlock(types.NewEmptyHeader()).Return(uint64(2), nil) diff --git a/lib/grandpa/grandpa_test.go b/lib/grandpa/grandpa_test.go index da73202e8e..4aaefed652 100644 --- a/lib/grandpa/grandpa_test.go +++ b/lib/grandpa/grandpa_test.go @@ -133,7 +133,7 @@ func TestUpdateAuthorities(t *testing.T) { err = gs.grandpaState.(*state.GrandpaState).SetNextChange(next, 1) require.NoError(t, err) - err = gs.grandpaState.(*state.GrandpaState).IncrementSetID() + _, err = gs.grandpaState.(*state.GrandpaState).IncrementSetID() require.NoError(t, err) err = gs.updateAuthorities() diff --git a/lib/grandpa/message_handler_test.go b/lib/grandpa/message_handler_test.go index 54973774a6..5e30d15a92 100644 --- a/lib/grandpa/message_handler_test.go +++ b/lib/grandpa/message_handler_test.go @@ -619,10 +619,7 @@ func TestMessageHandler_VerifyBlockJustification_WithEquivocatoryVotes(t *testin err = st.Block.AddBlock(block) require.NoError(t, err) - err = st.Grandpa.IncrementSetID() - require.NoError(t, err) - - setID, err := st.Grandpa.GetCurrentSetID() + setID, err := st.Grandpa.IncrementSetID() require.NoError(t, err) require.Equal(t, uint64(1), setID) @@ -664,10 +661,7 @@ func TestMessageHandler_VerifyBlockJustification(t *testing.T) { err = st.Block.AddBlock(block) require.NoError(t, err) - err = st.Grandpa.IncrementSetID() - require.NoError(t, err) - - setID, err := st.Grandpa.GetCurrentSetID() + setID, err := st.Grandpa.IncrementSetID() require.NoError(t, err) require.Equal(t, uint64(1), setID) @@ -721,10 +715,7 @@ func TestMessageHandler_VerifyBlockJustification_invalid(t *testing.T) { err = st.Block.AddBlock(block) require.NoError(t, err) - err = st.Grandpa.IncrementSetID() - require.NoError(t, err) - - setID, err := st.Grandpa.GetCurrentSetID() + setID, err := st.Grandpa.IncrementSetID() require.NoError(t, err) require.Equal(t, uint64(1), setID) From ded3a0b56d03582744ddcf432ca8cbff1d330927 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 18:37:48 -0400 Subject: [PATCH 6/7] chore(deps): bump ansi-regex in /tests/polkadotjs_test (#2447) Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/chalk/ansi-regex/releases) - [Commits](https://github.com/chalk/ansi-regex/compare/v3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: ansi-regex dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/polkadotjs_test/package-lock.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/polkadotjs_test/package-lock.json b/tests/polkadotjs_test/package-lock.json index d5c6f600ea..25fce2c881 100644 --- a/tests/polkadotjs_test/package-lock.json +++ b/tests/polkadotjs_test/package-lock.json @@ -305,9 +305,9 @@ "dev": true }, "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "dev": true }, "ansi-styles": { @@ -502,9 +502,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "is-fullwidth-code-point": { @@ -1371,9 +1371,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "is-fullwidth-code-point": { @@ -1445,9 +1445,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "is-fullwidth-code-point": { From 9efde47003506dff22ca1c96073b9e806a8ac617 Mon Sep 17 00:00:00 2001 From: default Date: Sun, 3 Apr 2022 00:54:02 -0400 Subject: [PATCH 7/7] fix(trie): decode inline child nodes (#2369) --- internal/trie/node/decode.go | 12 +++++ internal/trie/node/decode_test.go | 59 ++++++++++++++++++++++++ internal/trie/node/encode_decode_test.go | 47 ++++++++++++++++++- lib/trie/database.go | 24 ++++++++-- 4 files changed, 137 insertions(+), 5 deletions(-) diff --git a/internal/trie/node/decode.go b/internal/trie/node/decode.go index c67e84e49a..4da4a5bbc2 100644 --- a/internal/trie/node/decode.go +++ b/internal/trie/node/decode.go @@ -105,6 +105,18 @@ func decodeBranch(reader io.Reader, header byte) (branch *Branch, err error) { ErrDecodeChildHash, i, err) } + // Handle inlined leaf nodes. + const hashLength = 32 + if Type(hash[0]>>6) == LeafType && len(hash) < hashLength { + leaf, err := decodeLeaf(bytes.NewReader(hash[1:]), hash[0]) + if err != nil { + return nil, fmt.Errorf("%w: at index %d: %s", + ErrDecodeValue, i, err) + } + branch.Children[i] = leaf + continue + } + branch.Children[i] = &Leaf{ HashDigest: hash, } diff --git a/internal/trie/node/decode_test.go b/internal/trie/node/decode_test.go index c6f683aece..755acbaf44 100644 --- a/internal/trie/node/decode_test.go +++ b/internal/trie/node/decode_test.go @@ -95,6 +95,65 @@ func Test_Decode(t *testing.T) { Dirty: true, }, }, + "branch with two inlined children": { + reader: bytes.NewReader( + []byte{ + 158, // node type 2 (branch w/o value) and key length 30 + // Key data start + 195, 101, 195, 207, 89, 214, + 113, 235, 114, 218, 14, 122, + 65, 19, 196, 16, 2, 80, 95, + 14, 123, 144, 18, 9, 107, + 65, 196, 235, 58, 175, + // Key data end + 148, 127, 110, 164, 41, 8, 0, 0, 104, 95, 15, 31, 5, + 21, 244, 98, 205, 207, 132, 224, 241, 214, 4, 93, 252, + 187, 32, 134, 92, 74, 43, 127, 1, 0, 0, + }, + ), + n: &Branch{ + Key: []byte{ + 12, 3, 6, 5, 12, 3, + 12, 15, 5, 9, 13, 6, + 7, 1, 14, 11, 7, 2, + 13, 10, 0, 14, 7, 10, + 4, 1, 1, 3, 12, 4, + }, + Children: [16]Node{ + nil, nil, nil, nil, + &Leaf{ + Key: []byte{ + 14, 7, 11, 9, 0, 1, + 2, 0, 9, 6, 11, 4, + 1, 12, 4, 14, 11, + 3, 10, 10, 15, 9, + 4, 7, 15, 6, 14, + 10, 4, 2, 9, + }, + Value: []byte{0, 0}, + Dirty: true, + }, + nil, nil, nil, nil, + &Leaf{ + Key: []byte{ + 15, 1, 15, 0, 5, 1, + 5, 15, 4, 6, 2, 12, + 13, 12, 15, 8, 4, + 14, 0, 15, 1, 13, + 6, 0, 4, 5, 13, + 15, 12, 11, 11, + }, + Value: []byte{ + 134, 92, 74, 43, + 127, 1, 0, 0, + }, + Dirty: true, + }, + nil, nil, nil, nil, nil, nil, + }, + Dirty: true, + }, + }, } for name, testCase := range testCases { diff --git a/internal/trie/node/encode_decode_test.go b/internal/trie/node/encode_decode_test.go index f4bb168697..bd59778388 100644 --- a/internal/trie/node/encode_decode_test.go +++ b/internal/trie/node/encode_decode_test.go @@ -43,7 +43,7 @@ func Test_Branch_Encode_Decode(t *testing.T) { Dirty: true, }, }, - "branch with child": { + "branch with child leaf inline": { branchToEncode: &Branch{ Key: []byte{5}, Children: [16]Node{ @@ -57,7 +57,50 @@ func Test_Branch_Encode_Decode(t *testing.T) { Key: []byte{5}, Children: [16]Node{ &Leaf{ - HashDigest: []byte{0x41, 0x9, 0x4, 0xa}, + Key: []byte{9}, + Value: []byte{10}, + Dirty: true, + }, + }, + Dirty: true, + }, + }, + "branch with child leaf hash": { + branchToEncode: &Branch{ + Key: []byte{5}, + Children: [16]Node{ + &Leaf{ + Key: []byte{ + 10, 11, 12, 13, + 14, 15, 16, 17, + 18, 19, 20, 21, + 14, 15, 16, 17, + 10, 11, 12, 13, + 14, 15, 16, 17, + }, + Value: []byte{ + 10, 11, 12, 13, + 14, 15, 16, 17, + 10, 11, 12, 13, + 14, 15, 16, 17, + 10, 11, 12, 13, + }, + }, + }, + }, + branchDecoded: &Branch{ + Key: []byte{5}, + Children: [16]Node{ + &Leaf{ + HashDigest: []byte{ + 2, 18, 48, 30, 98, + 133, 244, 78, 70, + 161, 196, 105, 228, + 190, 159, 228, 199, 29, + 254, 212, 160, 55, 199, + 21, 186, 226, 204, 145, + 132, 5, 39, 204, + }, }, }, Dirty: true, diff --git a/lib/trie/database.go b/lib/trie/database.go index b9822dc47a..273ce9f8d1 100644 --- a/lib/trie/database.go +++ b/lib/trie/database.go @@ -186,6 +186,18 @@ func (t *Trie) load(db chaindb.Database, n Node) error { hash := child.GetHash() + _, isLeaf := child.(*node.Leaf) + if len(hash) == 0 && isLeaf { + // node has already been loaded inline + // just set encoding + hash digest + _, _, err := child.EncodeAndHash(false) + if err != nil { + return err + } + child.SetDirty(false) + continue + } + encodedNode, err := db.Get(hash) if err != nil { return fmt.Errorf("cannot find child node key 0x%x in database: %w", hash, err) @@ -330,12 +342,18 @@ func getFromDB(db chaindb.Database, n Node, key []byte) ( // childIndex is the nibble after the common prefix length in the key being searched. childIndex := key[commonPrefixLength] - childWithHashOnly := branch.Children[childIndex] - if childWithHashOnly == nil { + child := branch.Children[childIndex] + if child == nil { return nil, nil } - childHash := childWithHashOnly.GetHash() + // Child can be either inlined or a hash pointer. + childHash := child.GetHash() + _, isLeaf := child.(*node.Leaf) + if len(childHash) == 0 && isLeaf { + return getFromDB(db, child, key[commonPrefixLength+1:]) + } + encodedChild, err := db.Get(childHash) if err != nil { return nil, fmt.Errorf(