diff --git a/.github/workflows/api-swagger-ui.yml b/.github/workflows/api-swagger-ui.yml index 6d5747e9e5..61056baa0f 100644 --- a/.github/workflows/api-swagger-ui.yml +++ b/.github/workflows/api-swagger-ui.yml @@ -1,11 +1,14 @@ -name: Build and Push Swagger-UI to R2 testnet-api-docs.spacemesh.network +name: Build and Push Swagger-UI env: go-version: "1.22" on: - release: - types: [published] + # Allow manually triggering this workflow + workflow_dispatch: + push: + tags: + - '*' jobs: check-version: @@ -42,31 +45,32 @@ jobs: - name: upload to testnet uses: jakejarvis/s3-sync-action@master with: - args: --acl public-read --follow-symlinks --delete + args: --acl public-read --follow-symlinks env: AWS_S3_BUCKET: ${{ secrets.CLOUDFLARE_TESTNET_API_BUCKET }} AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_SECRET_ACCESS_KEY }} SOURCE_DIR: api/release/openapi/swagger/src - DEST_DIR: '/${{ github.event.release.tag_name }}' + DEST_DIR: '${{ github.ref_name }}' AWS_S3_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com - name: update url json file for testnet working-directory: api/release/openapi/swagger/src run: | + mkdir spec && cd spec curl -o spec_urls.json https://testnet-api-docs.spacemesh.network/spec_urls.json - new_url="{\"url\":\"https://testnet-api-docs.spacemesh.network/${{ github.event.release.tag_name }}/api.swagger.json\",\"name\":\"${{ github.event.release.tag_name }}\"}" + new_url="{\"url\":\"https://testnet-api-docs.spacemesh.network/${{ github.ref_name }}/api.swagger.json\",\"name\":\"${{ github.ref_name }}\"}" jq ". += [$new_url]" spec_urls.json > tmp.json && mv tmp.json spec_urls.json - name: upload new testnet json file uses: jakejarvis/s3-sync-action@master with: - args: --acl public-read --follow-symlinks --delete + args: --acl public-read --follow-symlinks env: AWS_S3_BUCKET: ${{ secrets.CLOUDFLARE_TESTNET_API_BUCKET }} AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_SECRET_ACCESS_KEY }} - SOURCE_DIR: api/release/openapi/swagger/src/spec_urls.json + SOURCE_DIR: api/release/openapi/swagger/src/spec DEST_DIR: '' AWS_S3_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d63bf9393a..8264de8ae6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,12 +113,10 @@ jobs: go-version: ${{ env.go-version }} - name: setup env run: make install - - name: staticcheck - run: make staticcheck - name: lint run: make lint-github-action - build-tools: + build: runs-on: ${{ matrix.os }} needs: filter-changes if: ${{ needs.filter-changes.outputs.nondocchanges == 'true' }} @@ -160,6 +158,7 @@ jobs: with: check-latest: true go-version: ${{ env.go-version }} + cache: ${{ runner.arch != 'arm64' }} - name: setup env run: make install - name: build merge-nodes @@ -168,52 +167,6 @@ jobs: run: make gen-p2p-identity - name: build bootstrapper run: make bootstrapper - - build: - runs-on: ${{ matrix.os }} - needs: filter-changes - if: ${{ needs.filter-changes.outputs.nondocchanges == 'true' }} - strategy: - fail-fast: true - matrix: - os: - - ubuntu-22.04 - - ubuntu-latest-arm-8-cores - - macos-13 - - [self-hosted, macOS, ARM64, go-spacemesh] - - windows-2022 - steps: - - name: Add OpenCL support - Ubuntu - if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'ubuntu-latest-arm-8-cores' }} - run: sudo apt-get update -q && sudo apt-get install -qy ocl-icd-opencl-dev libpocl2 - - name: disable Windows Defender - Windows - if: ${{ matrix.os == 'windows-2022' }} - run: | - Set-MpPreference -DisableRealtimeMonitoring $true - - name: Set new git config - Windows - if: ${{ matrix.os == 'windows-2022' }} - run: | - git config --global pack.window 1 - git config --global core.compression 0 - git config --global http.postBuffer 524288000 - - name: checkout - uses: actions/checkout@v4 - with: - ssh-key: ${{ secrets.GH_ACTION_PRIVATE_KEY }} - - uses: extractions/netrc@v2 - with: - machine: github.com - username: ${{ secrets.GH_ACTION_TOKEN_USER }} - password: ${{ secrets.GH_ACTION_TOKEN }} - if: vars.GOPRIVATE - - name: set up go - uses: actions/setup-go@v5 - with: - check-latest: true - go-version: ${{ env.go-version }} - cache: ${{ runner.arch != 'arm64' }} - - name: setup env - run: make install - name: build timeout-minutes: 5 run: make build @@ -228,7 +181,8 @@ jobs: os: - ubuntu-22.04 - ubuntu-latest-arm-8-cores - - macos-13 + # FIXME: reenable the macos runner + # - macos-13 - [self-hosted, macOS, ARM64, go-spacemesh] - windows-2022 steps: @@ -306,7 +260,6 @@ jobs: - filter-changes - quicktests - lint - - build-tools - build - unittests runs-on: ubuntu-22.04 diff --git a/.github/workflows/systest.yml b/.github/workflows/systest.yml index a31d1744de..f2aca2d137 100644 --- a/.github/workflows/systest.yml +++ b/.github/workflows/systest.yml @@ -67,7 +67,7 @@ jobs: id: install uses: azure/setup-kubectl@v4 with: - version: "v1.23.15" + version: "v1.27.16" - name: Setup gcloud authentication uses: google-github-actions/auth@v2 @@ -130,10 +130,8 @@ jobs: go-version-file: "go.mod" - name: Run tests - timeout-minutes: 60 env: test_id: systest-${{ steps.vars.outputs.sha_short }} - label: sanity storage: premium-rwo=10Gi node_selector: cloud.google.com/gke-nodepool=systemtest size: 20 diff --git a/CHANGELOG.md b/CHANGELOG.md index de0dd6a230..ac0a08d403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,74 @@ See [RELEASE](./RELEASE.md) for workflow instructions. +## UNRELEASED + +### Upgrade information + +The command line flag `--scan-malfeasant-atxs` has been removed. All malfeasant ATXs before 1.6.0 have been marked as +such and the node will continue to scan new ATXs for their validity. + +### Highlights + +### Features + +### Improvements + +## Release v1.6.6 + +### Improvements + +* [#6198](https://github.com/spacemeshos/go-spacemesh/pull/6198) Configure default TTL for caching poet's /v1/info + +* [#6199](https://github.com/spacemeshos/go-spacemesh/pull/6199) Cache poet's /v1/pow_params + +## Release v1.6.5 + +### Improvements + +* [#6185](https://github.com/spacemeshos/go-spacemesh/pull/6185) Optimize mempool + +* [#6187](https://github.com/spacemeshos/go-spacemesh/pull/6187) The merge tool now ignores files that are not `.key` + files in the `identities` directory when merging two nodes. + +* [#6128](https://github.com/spacemeshos/go-spacemesh/pull/6128) Reduce logs spam + +## Release v1.6.4 + +### Improvements + +* [#6107](https://github.com/spacemeshos/go-spacemesh/pull/6107) Cache PoET queries between multiple identities on the + same node. This will reduce the number of requests the node makes to the PoET server during the cyclegap. + +* [#6152](https://github.com/spacemeshos/go-spacemesh/pull/6152) Fixed a bug where in rare cases the node would panic + due to the closing of a closed channel in the fetcher. + +* [#6142](https://github.com/spacemeshos/go-spacemesh/pull/6142) Fix node not dropping peers that are broadcasting + invalid ATXs. + +## Release v1.6.3 + +### Improvements + +* [#6137](https://github.com/spacemeshos/go-spacemesh/pull/6137) Fix hanging ATX sync. + +## Release v1.6.2 + +### Improvements + +* [#5793](https://github.com/spacemeshos/go-spacemesh/pull/5793) Reduced hare committee 8x from 400 to 50 to decrease + network traffic caused by Hare. + +* [#6099](https://github.com/spacemeshos/go-spacemesh/pull/6099) Adds new metrics to the API to provide insights into + the performance and behavior of the node's APIs. + +* [#6115](https://github.com/spacemeshos/go-spacemesh/pull/6115) Increase the number of supported ATXs to 8.0 Mio. + +### Features + +* [#6112](https://github.com/spacemeshos/go-spacemesh/pull/6112) Adds vesting, vault, and drain vault contents to the + v2alpha2 Transaction API. Fixes the 'unspecified' transaction type. + ## Release v1.6.1 ### Improvements @@ -637,6 +705,7 @@ and permanent ineligibility for rewards. * [#5494](https://github.com/spacemeshos/go-spacemesh/pull/5494) Make routing discovery more configurable and less spammy by default. + * [#5511](https://github.com/spacemeshos/go-spacemesh/pull/5511) Fix dialing peers on their private IPs, which was causing "portscan" complaints. @@ -646,7 +715,7 @@ and permanent ineligibility for rewards. * [#5470](https://github.com/spacemeshos/go-spacemesh/pull/5470) Fixed a bug in event reporting where the node reports a disconnection from the PoST service as a "PoST failed" event. - Disconnections cannot be avoided completely and do not interrupt the PoST proofing process. As long as the PoST + Disconnections cannot be avoided completely and do not interrupt the PoST proving process. As long as the PoST service reconnects within a reasonable time, the node will continue to operate normally without reporting any errors via the event API. diff --git a/Dockerfile b/Dockerfile index bd1223a342..b1a4bd87ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,7 +59,7 @@ FROM linux AS spacemesh # Finally we copy the statically compiled Go binary. COPY --from=builder /src/build/go-spacemesh /bin/ -COPY --from=builder /src/build/service /bin/ +COPY --from=builder /src/build/post-service /bin/ COPY --from=builder /src/build/libpost.so /bin/ COPY --from=builder /src/build/gen-p2p-identity /bin/ COPY --from=builder /src/build/merge-nodes /bin/ diff --git a/Makefile b/Makefile index c1853c7560..4b9cb3bda5 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,6 @@ COMMIT = $(shell git rev-parse HEAD) BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) GOLANGCI_LINT_VERSION := v1.59.0 -STATICCHECK_VERSION := v0.4.7 GOTESTSUM_VERSION := v1.12.0 GOSCALE_VERSION := v1.2.0 MOCKGEN_VERSION := v0.4.0 @@ -61,7 +60,6 @@ install: go install github.com/spacemeshos/go-scale/scalegen@$(GOSCALE_VERSION) go install go.uber.org/mock/mockgen@$(MOCKGEN_VERSION) go install gotest.tools/gotestsum@$(GOTESTSUM_VERSION) - go install honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION) .PHONY: install build: go-spacemesh get-profiler get-postrs-service @@ -97,7 +95,7 @@ clear-test-cache: .PHONY: clear-test-cache test: get-libs - @$(ULIMIT) CGO_LDFLAGS="$(CGO_TEST_LDFLAGS)" gotestsum -- -race -timeout 8m -p 1 $(UNIT_TESTS) + @$(ULIMIT) CGO_LDFLAGS="$(CGO_TEST_LDFLAGS)" gotestsum -- -race -timeout 8m $(UNIT_TESTS) .PHONY: test generate: get-libs @@ -112,10 +110,6 @@ test-generate: @git diff --name-only --diff-filter=AM --exit-code . || { echo "\nPlease rerun 'make generate' and commit changes.\n"; exit 1; } .PHONY: test-generate -staticcheck: get-libs - @$(ULIMIT) CGO_LDFLAGS="$(CGO_TEST_LDFLAGS)" staticcheck ./... -.PHONY: staticcheck - test-tidy: # Working directory must be clean, or this test would be destructive git diff --quiet || (echo "\033[0;31mWorking directory not clean!\033[0m" && git --no-pager diff && exit 1) @@ -145,7 +139,7 @@ lint-github-action: get-libs .PHONY: lint-github-action cover: get-libs - @$(ULIMIT) CGO_LDFLAGS="$(CGO_TEST_LDFLAGS)" go test -coverprofile=cover.out -timeout 0 -p 1 -coverpkg=./... $(UNIT_TESTS) + @$(ULIMIT) CGO_LDFLAGS="$(CGO_TEST_LDFLAGS)" go test -coverprofile=cover.out -timeout 30m -coverpkg=./... $(UNIT_TESTS) .PHONY: cover list-versions: diff --git a/Makefile-libs.Inc b/Makefile-libs.Inc index 03c0ad332e..7f80ae516b 100644 --- a/Makefile-libs.Inc +++ b/Makefile-libs.Inc @@ -50,21 +50,21 @@ else endif endif -POSTRS_SETUP_REV = 0.7.8 +POSTRS_SETUP_REV = 0.7.11 POSTRS_SETUP_ZIP = libpost-$(platform)-v$(POSTRS_SETUP_REV).zip POSTRS_SETUP_URL_ZIP ?= https://github.com/spacemeshos/post-rs/releases/download/v$(POSTRS_SETUP_REV)/$(POSTRS_SETUP_ZIP) POSTRS_PROFILER_ZIP = profiler-$(platform)-v$(POSTRS_SETUP_REV).zip POSTRS_PROFILER_URL ?= https://github.com/spacemeshos/post-rs/releases/download/v$(POSTRS_SETUP_REV)/$(POSTRS_PROFILER_ZIP) -POSTRS_SERVICE_ZIP = service-$(platform)-v$(POSTRS_SETUP_REV).zip +POSTRS_SERVICE_ZIP = post-service-$(platform)-v$(POSTRS_SETUP_REV).zip POSTRS_SERVICE_URL ?= https://github.com/spacemeshos/post-rs/releases/download/v$(POSTRS_SETUP_REV)/$(POSTRS_SERVICE_ZIP) ifeq ($(platform), windows) POSTRS_SETUP_LIBS = post.h post.dll POSTRS_PROFILER_BIN = profiler.exe - POSTRS_SERVICE_BIN = service.exe + POSTRS_SERVICE_BIN = post-service.exe else ifeq ($(platform), $(filter $(platform), macos macos-m1)) POSTRS_SETUP_LIBS = post.h libpost.dylib @@ -73,7 +73,7 @@ else endif POSTRS_PROFILER_BIN = profiler - POSTRS_SERVICE_BIN = service + POSTRS_SERVICE_BIN = post-service endif BINDIR_POSTRS_SETUP_LIBS = $(foreach X,$(POSTRS_SETUP_LIBS),$(BIN_DIR)$(X)) diff --git a/README.md b/README.md index 3f2bfde5a2..2f3fb38e7c 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ the build folder you need to ensure that you have the gpu setup dynamic library binary. The simplest way to do this is just copy the library file to be in the same directory as the go-spacemesh binary. Alternatively you can modify your system's library search paths (e.g. LD_LIBRARY_PATH) to ensure that the -library is found._ +library is found. go-spacemesh is p2p software which is designed to form a decentralized network by connecting to other instances of go-spacemesh running on remote computers. diff --git a/activation/activation.go b/activation/activation.go index ed8cf60afd..d6db514e9c 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -45,6 +45,7 @@ type PoetConfig struct { RequestRetryDelay time.Duration `mapstructure:"retry-delay"` PositioningATXSelectionTimeout time.Duration `mapstructure:"positioning-atx-selection-timeout"` CertifierInfoCacheTTL time.Duration `mapstructure:"certifier-info-cache-ttl"` + PowParamsCacheTTL time.Duration `mapstructure:"pow-params-cache-ttl"` MaxRequestRetries int `mapstructure:"retry-max"` } @@ -53,6 +54,7 @@ func DefaultPoetConfig() PoetConfig { RequestRetryDelay: 400 * time.Millisecond, MaxRequestRetries: 10, CertifierInfoCacheTTL: 5 * time.Minute, + PowParamsCacheTTL: 5 * time.Minute, } } @@ -67,7 +69,7 @@ type Config struct { } // Builder struct is the struct that orchestrates the creation of activation transactions -// it is responsible for initializing post, receiving poet proof and orchestrating nipst. after which it will +// it is responsible for initializing post, receiving poet proof and orchestrating nipost. after which it will // calculate total weight and providing relevant view as proof. type Builder struct { accountLock sync.RWMutex @@ -416,7 +418,13 @@ func (b *Builder) run(ctx context.Context, sig *signing.EdSigner) { for _, poet := range b.poets { eg.Go(func() error { _, err := poet.Certify(ctx, sig.NodeID()) - if err != nil { + switch { + case errors.Is(err, ErrCertificatesNotSupported): + b.logger.Debug("not certifying (not supported in poet)", + log.ZShortStringer("smesherID", sig.NodeID()), + zap.String("poet", poet.Address()), + ) + case err != nil: b.logger.Warn("failed to certify poet", zap.Error(err), log.ZShortStringer("smesherID", sig.NodeID())) } return nil @@ -434,6 +442,7 @@ func (b *Builder) run(ctx context.Context, sig *signing.EdSigner) { b.logger.Warn("failed to publish atx", zap.Error(err)) + poetErr := &PoetSvcUnstableError{} switch { case errors.Is(err, ErrATXChallengeExpired): b.logger.Debug("retrying with new challenge after waiting for a layer") @@ -450,8 +459,11 @@ func (b *Builder) run(ctx context.Context, sig *signing.EdSigner) { return case <-b.layerClock.AwaitLayer(currentLayer.Add(1)): } - case errors.Is(err, ErrPoetServiceUnstable): - b.logger.Warn("retrying after poet retry interval", zap.Duration("interval", b.poetRetryInterval)) + case errors.As(err, &poetErr): + b.logger.Warn("retrying after poet retry interval", + zap.Duration("interval", b.poetRetryInterval), + zap.Error(poetErr.source), + ) select { case <-ctx.Done(): return @@ -585,7 +597,7 @@ func (b *Builder) BuildNIPostChallenge(ctx context.Context, nodeID types.NodeID) PositioningATX: posAtx, } } - logger.Info("persisting the new NiPOST challenge", zap.Object("challenge", challenge)) + logger.Debug("persisting the new NiPOST challenge", zap.Object("challenge", challenge)) if err := nipost.AddChallenge(b.localDB, nodeID, challenge); err != nil { return nil, fmt.Errorf("add nipost challenge: %w", err) } @@ -625,7 +637,7 @@ func (b *Builder) getExistingChallenge( } // challenge is fresh - logger.Info("loaded NiPoST challenge from local state", + logger.Debug("loaded NiPoST challenge from local state", zap.Uint32("current_epoch", currentEpochId.Uint32()), zap.Uint32("publish_epoch", challenge.PublishEpoch.Uint32()), ) @@ -728,7 +740,6 @@ func (b *Builder) PublishActivationTx(ctx context.Context, sig *signing.EdSigner return fmt.Errorf("wait for publication epoch: %w", ctx.Err()) case <-b.layerClock.AwaitLayer(challenge.PublishEpoch.FirstLayer()): } - b.logger.Debug("publication epoch has arrived!", log.ZShortStringer("smesherID", sig.NodeID())) for { b.logger.Info( @@ -958,13 +969,13 @@ func (b *Builder) getPositioningAtx( return types.EmptyATXID, err } - b.logger.Info("found candidate positioning atx", + b.logger.Debug("found candidate positioning atx", log.ZShortStringer("id", id), log.ZShortStringer("smesherID", nodeID), ) if previous == nil { - b.logger.Info("selected atx as positioning atx", + b.logger.Info("selected positioning atx", log.ZShortStringer("id", id), log.ZShortStringer("smesherID", nodeID)) return id, nil @@ -1042,14 +1053,14 @@ func findFullyValidHighTickAtx( // iterate trough epochs, to get first valid, not malicious ATX with the biggest height atxdata.IterateHighTicksInEpoch(publish+1, func(id types.ATXID) (contSearch bool) { - logger.Info("found candidate for high-tick atx", log.ZShortStringer("id", id)) + logger.Debug("found candidate for high-tick atx", log.ZShortStringer("id", id)) if ctx.Err() != nil { return false } // verify ATX-candidate by getting their dependencies (previous Atx, positioning ATX etc.) // and verifying PoST for every dependency if err := validator.VerifyChain(ctx, id, goldenATXID, opts...); err != nil { - logger.Info("rejecting candidate for high-tick atx", zap.Error(err), log.ZShortStringer("id", id)) + logger.Debug("rejecting candidate for high-tick atx", zap.Error(err), log.ZShortStringer("id", id)) return true } found = &id diff --git a/activation/activation_errors.go b/activation/activation_errors.go index 601541e83c..e2e2215e5f 100644 --- a/activation/activation_errors.go +++ b/activation/activation_errors.go @@ -3,13 +3,12 @@ package activation import ( "errors" "fmt" + "strings" ) var ( // ErrATXChallengeExpired is returned when atx missed its publication window and needs to be regenerated. ErrATXChallengeExpired = errors.New("builder: atx expired") - // ErrPoetServiceUnstable is returned when poet quality of service is low. - ErrPoetServiceUnstable = &PoetSvcUnstableError{} // ErrPoetProofNotReceived is returned when no poet proof was received. ErrPoetProofNotReceived = errors.New("builder: didn't receive any poet proof") ) @@ -23,13 +22,31 @@ type PoetSvcUnstableError struct { source error } -func (e *PoetSvcUnstableError) Error() string { +func (e PoetSvcUnstableError) Error() string { return fmt.Sprintf("poet service is unstable: %s (%v)", e.msg, e.source) } func (e *PoetSvcUnstableError) Unwrap() error { return e.source } -func (e *PoetSvcUnstableError) Is(target error) bool { - _, ok := target.(*PoetSvcUnstableError) - return ok +type PoetRegistrationMismatchError struct { + registrations []string + configuredPoets []string +} + +func (e PoetRegistrationMismatchError) Error() string { + var sb strings.Builder + sb.WriteString("builder: none of configured poets matches the existing registrations.\n") + sb.WriteString("registrations:\n") + for _, r := range e.registrations { + sb.WriteString("\t") + sb.WriteString(r) + sb.WriteString("\n") + } + sb.WriteString("\nconfigured poets:\n") + for _, p := range e.configuredPoets { + sb.WriteString("\t") + sb.WriteString(p) + sb.WriteString("\n") + } + return sb.String() } diff --git a/activation/activation_test.go b/activation/activation_test.go index 039218d06c..0305add4c3 100644 --- a/activation/activation_test.go +++ b/activation/activation_test.go @@ -138,6 +138,7 @@ func publishAtxV1( return codec.Decode(got, &watx) }) require.NoError(tb, atxs.Add(tab.db, toAtx(tb, &watx), watx.Blob())) + require.NoError(tb, atxs.SetPost(tab.db, watx.ID(), watx.PrevATXID, 0, watx.SmesherID, watx.NumUnits)) tab.atxsdata.AddFromAtx(toAtx(tb, &watx), false) return &watx } @@ -1077,7 +1078,7 @@ func TestBuilder_RetryPublishActivationTx(t *testing.T) { tries++ t.Logf("try %d: %s", tries, now) if tries < expectedTries { - return nil, ErrPoetServiceUnstable + return nil, &PoetSvcUnstableError{} } close(builderConfirmation) return newNIPostWithPoet(t, []byte("66666")), nil diff --git a/activation/builder_v2_test.go b/activation/builder_v2_test.go index 209570b7c4..0054147f4e 100644 --- a/activation/builder_v2_test.go +++ b/activation/builder_v2_test.go @@ -68,7 +68,7 @@ func TestBuilder_BuildsInitialAtxV2(t *testing.T) { require.Empty(t, atx.Marriages) require.Equal(t, posEpoch+1, atx.PublishEpoch) require.Equal(t, sig.NodeID(), atx.SmesherID) - require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx.SmesherID, atx.SignedBytes(), atx.Signature)) + require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx.SmesherID, atx.ID().Bytes(), atx.Signature)) } func TestBuilder_SwitchesToBuildV2(t *testing.T) { @@ -106,5 +106,5 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { require.Empty(t, atx2.Marriages) require.Equal(t, atx1.PublishEpoch+1, atx2.PublishEpoch) require.Equal(t, sig.NodeID(), atx2.SmesherID) - require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx2.SmesherID, atx2.SignedBytes(), atx2.Signature)) + require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx2.SmesherID, atx2.ID().Bytes(), atx2.Signature)) } diff --git a/activation/certifier.go b/activation/certifier.go index af63f9c49a..636a14ca78 100644 --- a/activation/certifier.go +++ b/activation/certifier.go @@ -116,7 +116,15 @@ func (c *Certifier) Certificate( case !errors.Is(err, sql.ErrNotFound): return nil, fmt.Errorf("getting certificate from DB for: %w", err) } - return c.Recertify(ctx, id, certifier, pubkey) + cert, err = c.client.Certify(ctx, id, certifier, pubkey) + if err != nil { + return nil, fmt.Errorf("certifying POST at %v: %w", certifier, err) + } + + if err := certifierdb.AddCertificate(c.db, id, *cert, pubkey); err != nil { + c.logger.Warn("failed to persist poet cert", zap.Error(err)) + } + return cert, nil }) if err != nil { @@ -125,21 +133,11 @@ func (c *Certifier) Certificate( return cert.(*certifierdb.PoetCert), nil } -func (c *Certifier) Recertify( - ctx context.Context, - id types.NodeID, - certifier *url.URL, - pubkey []byte, -) (*certifierdb.PoetCert, error) { - cert, err := c.client.Certify(ctx, id, certifier, pubkey) - if err != nil { - return nil, fmt.Errorf("certifying POST at %v: %w", certifier, err) - } - - if err := certifierdb.AddCertificate(c.db, id, *cert, pubkey); err != nil { - c.logger.Warn("failed to persist poet cert", zap.Error(err)) +func (c *Certifier) DeleteCertificate(id types.NodeID, pubkey []byte) error { + if err := certifierdb.DeleteCertificate(c.db, id, pubkey); err != nil { + return err } - return cert, nil + return nil } type CertifierClient struct { @@ -208,7 +206,7 @@ func (c *CertifierClient) obtainPostFromLastAtx(ctx context.Context, nodeId type } } - c.logger.Info("found POST in an existing ATX", zap.String("atx_id", atxid.Hash32().ShortString())) + c.logger.Debug("found POST in an existing ATX", zap.String("atx_id", atxid.Hash32().ShortString())) return &nipost.Post{ Nonce: post.Nonce, Indices: post.Indices, @@ -221,11 +219,11 @@ func (c *CertifierClient) obtainPostFromLastAtx(ctx context.Context, nodeId type } func (c *CertifierClient) obtainPost(ctx context.Context, id types.NodeID) (*nipost.Post, error) { - c.logger.Info("looking for POST for poet certification") + c.logger.Debug("looking for POST for poet certification") post, err := nipost.GetPost(c.localDb, id) switch { case err == nil: - c.logger.Info("found POST in local DB") + c.logger.Debug("found POST in local DB") return post, nil case errors.Is(err, sql.ErrNotFound): // no post found @@ -233,9 +231,9 @@ func (c *CertifierClient) obtainPost(ctx context.Context, id types.NodeID) (*nip return nil, fmt.Errorf("loading initial post from db: %w", err) } - c.logger.Info("POST not found in local DB. Trying to obtain POST from an existing ATX") + c.logger.Debug("POST not found in local DB. Trying to obtain POST from an existing ATX") if post, err := c.obtainPostFromLastAtx(ctx, id); err == nil { - c.logger.Info("found POST in an existing ATX") + c.logger.Debug("found POST in an existing ATX") if err := nipost.AddPost(c.localDb, id, *post); err != nil { c.logger.Error("failed to save post", zap.Error(err)) } diff --git a/activation/e2e/activation_test.go b/activation/e2e/activation_test.go index be8aeb249a..1472f4453c 100644 --- a/activation/e2e/activation_test.go +++ b/activation/e2e/activation_test.go @@ -124,7 +124,7 @@ func Test_BuilderWithMultipleClients(t *testing.T) { clock, err := timesync.NewClock( timesync.WithGenesisTime(genesis), timesync.WithLayerDuration(layerDuration), - timesync.WithTickInterval(100*time.Millisecond), + timesync.WithTickInterval(10*time.Millisecond), timesync.WithLogger(zap.NewNop()), ) require.NoError(t, err) diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index e9099dd4cc..8b1fdab98c 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -238,7 +238,7 @@ func Test_MarryAndMerge(t *testing.T) { require.NoError(t, eg.Wait()) // ensure that genesis aligns with layer timings - genesis := time.Now().Round(layerDuration) + genesis := time.Now().Add(layerDuration).Round(layerDuration) epoch := layersPerEpoch * layerDuration poetCfg := activation.PoetConfig{ PhaseShift: epoch, @@ -246,13 +246,13 @@ func Test_MarryAndMerge(t *testing.T) { GracePeriod: epoch / 4, } - client := ae2e.NewTestPoetClient(2) + client := ae2e.NewTestPoetClient(2, poetCfg) poetSvc := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) clock, err := timesync.NewClock( timesync.WithGenesisTime(genesis), timesync.WithLayerDuration(layerDuration), - timesync.WithTickInterval(100*time.Millisecond), + timesync.WithTickInterval(10*time.Millisecond), timesync.WithLogger(zap.NewNop()), ) require.NoError(t, err) @@ -513,6 +513,9 @@ func Test_MarryAndMerge(t *testing.T) { require.Equal(t, units[i], atxFromDb.NumUnits) require.Equal(t, signer.NodeID(), atxFromDb.SmesherID) require.Equal(t, publish, atxFromDb.PublishEpoch) - require.Equal(t, mergedATX2.ID(), atxFromDb.PrevATXID) + prev, err := atxs.Previous(db, atxFromDb.ID()) + require.NoError(t, err) + require.Len(t, prev, 1) + require.Equal(t, mergedATX2.ID(), prev[0]) } } diff --git a/activation/e2e/builds_atx_v2_test.go b/activation/e2e/builds_atx_v2_test.go index 5a7fd4fa0c..af8fad6a55 100644 --- a/activation/e2e/builds_atx_v2_test.go +++ b/activation/e2e/builds_atx_v2_test.go @@ -2,6 +2,7 @@ package activation_test import ( "context" + "sync/atomic" "testing" "time" @@ -74,7 +75,7 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { atxsdata := atxsdata.New() // ensure that genesis aligns with layer timings - genesis := time.Now().Round(layerDuration) + genesis := time.Now().Add(layerDuration).Round(layerDuration) epoch := layersPerEpoch * layerDuration poetCfg := activation.PoetConfig{ PhaseShift: epoch, @@ -85,13 +86,13 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { clock, err := timesync.NewClock( timesync.WithGenesisTime(genesis), timesync.WithLayerDuration(layerDuration), - timesync.WithTickInterval(100*time.Millisecond), + timesync.WithTickInterval(10*time.Millisecond), timesync.WithLogger(zap.NewNop()), ) require.NoError(t, err) t.Cleanup(clock.Close) - client := ae2e.NewTestPoetClient(1) + client := ae2e.NewTestPoetClient(1, poetCfg) poetClient := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) localDB := localsql.InMemory() @@ -135,6 +136,7 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { ) var previous *types.ActivationTx + var publishedAtxs atomic.Uint32 gomock.InOrder( mpub.EXPECT().Publish(gomock.Any(), pubsub.AtxProtocol, gomock.Any()).DoAndReturn( func(ctx context.Context, _ string, msg []byte) error { @@ -157,6 +159,7 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { atx, err := atxs.Get(db, watx.ID()) require.NoError(t, err) previous = atx + publishedAtxs.Add(1) return nil }, ), @@ -189,6 +192,7 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { require.NotZero(t, atx.TickHeight()) require.Equal(t, opts.NumUnits, atx.NumUnits) previous = atx + publishedAtxs.Add(1) return nil }, ).Times(2), @@ -211,6 +215,6 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { tab.Register(sig) require.NoError(t, tab.StartSmeshing(coinbase)) - require.Eventually(t, ctrl.Satisfied, epoch*4, time.Millisecond*100) + require.Eventually(t, func() bool { return publishedAtxs.Load() >= 3 }, epoch*4, time.Millisecond*100) require.NoError(t, tab.StopSmeshing(false)) } diff --git a/activation/e2e/checkpoint_merged_test.go b/activation/e2e/checkpoint_merged_test.go index 91f40dd7b7..970ac803b3 100644 --- a/activation/e2e/checkpoint_merged_test.go +++ b/activation/e2e/checkpoint_merged_test.go @@ -73,7 +73,7 @@ func Test_CheckpointAfterMerge(t *testing.T) { require.NoError(t, eg.Wait()) // ensure that genesis aligns with layer timings - genesis := time.Now().Round(layerDuration) + genesis := time.Now().Add(layerDuration).Round(layerDuration) epoch := layersPerEpoch * layerDuration poetCfg := activation.PoetConfig{ PhaseShift: epoch, @@ -81,13 +81,13 @@ func Test_CheckpointAfterMerge(t *testing.T) { GracePeriod: epoch / 4, } - client := ae2e.NewTestPoetClient(2) + client := ae2e.NewTestPoetClient(2, poetCfg) poetSvc := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) clock, err := timesync.NewClock( timesync.WithGenesisTime(genesis), timesync.WithLayerDuration(layerDuration), - timesync.WithTickInterval(100*time.Millisecond), + timesync.WithTickInterval(10*time.Millisecond), timesync.WithLogger(zap.NewNop()), ) require.NoError(t, err) @@ -273,6 +273,12 @@ func Test_CheckpointAfterMerge(t *testing.T) { require.Equal(t, i, marriage.Index) } + checkpointedMerged, err := atxs.Get(newDB, mergedATX.ID()) + require.NoError(t, err) + require.True(t, checkpointedMerged.Golden()) + require.NotNil(t, checkpointedMerged.MarriageATX) + require.Equal(t, marriageATX.ID(), *checkpointedMerged.MarriageATX) + // 4. Spawn new ATX handler and builder using the new DB poetDb = activation.NewPoetDb(newDB, logger.Named("poetDb")) cdb = datastore.NewCachedDB(newDB, logger) diff --git a/activation/e2e/checkpoint_test.go b/activation/e2e/checkpoint_test.go index c7788733e1..0e28ee72d9 100644 --- a/activation/e2e/checkpoint_test.go +++ b/activation/e2e/checkpoint_test.go @@ -71,7 +71,7 @@ func TestCheckpoint_PublishingSoloATXs(t *testing.T) { CycleGap: 3 * epoch / 4, GracePeriod: epoch / 4, } - client := ae2e.NewTestPoetClient(1) + client := ae2e.NewTestPoetClient(1, poetCfg) poetService := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) // ensure that genesis aligns with layer timings @@ -188,7 +188,7 @@ func TestCheckpoint_PublishingSoloATXs(t *testing.T) { // 3. Spawn new ATX handler and builder using the new DB poetDb = activation.NewPoetDb(newDB, logger.Named("poetDb")) cdb = datastore.NewCachedDB(newDB, logger) - atxdata, err = atxsdata.Warm(newDB, 1) + atxdata, err = atxsdata.Warm(newDB, 1, logger) poetService = activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) validator = activation.NewValidator(newDB, poetDb, cfg, opts.Scrypt, verifier) require.NoError(t, err) diff --git a/activation/e2e/nipost_test.go b/activation/e2e/nipost_test.go index 5ecb004af1..61891ac2aa 100644 --- a/activation/e2e/nipost_test.go +++ b/activation/e2e/nipost_test.go @@ -198,7 +198,7 @@ func TestNIPostBuilderWithClients(t *testing.T) { err = nipost.AddPost(localDb, sig.NodeID(), *fullPost(post, info, shared.ZeroChallenge)) require.NoError(t, err) - client := ae2e.NewTestPoetClient(1) + client := ae2e.NewTestPoetClient(1, poetCfg) poetService := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) localDB := localsql.InMemory() @@ -272,7 +272,7 @@ func Test_NIPostBuilderWithMultipleClients(t *testing.T) { } poetDb := activation.NewPoetDb(db, logger.Named("poetDb")) - client := ae2e.NewTestPoetClient(len(signers)) + client := ae2e.NewTestPoetClient(len(signers), poetCfg) poetService := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) mclock := activation.NewMocklayerClock(ctrl) diff --git a/activation/e2e/poet_client.go b/activation/e2e/poet_client.go index c025ed5302..01fee564ec 100644 --- a/activation/e2e/poet_client.go +++ b/activation/e2e/poet_client.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "net/url" "strconv" "sync" "time" @@ -20,15 +19,17 @@ import ( ) type TestPoet struct { - mu sync.Mutex - round int + mu sync.Mutex + round int + poetCfg activation.PoetConfig expectedMembers int registrations chan []byte } -func NewTestPoetClient(expectedMembers int) *TestPoet { +func NewTestPoetClient(expectedMembers int, poetCfg activation.PoetConfig) *TestPoet { return &TestPoet{ + poetCfg: poetCfg, expectedMembers: expectedMembers, registrations: make(chan []byte, expectedMembers), } @@ -66,8 +67,15 @@ func (p *TestPoet) Submit( return &types.PoetRound{ID: strconv.Itoa(round), End: time.Now()}, nil } -func (p *TestPoet) CertifierInfo(ctx context.Context) (*url.URL, []byte, error) { - return nil, nil, errors.New("not supported") +func (p *TestPoet) CertifierInfo(ctx context.Context) (*types.CertifierInfo, error) { + return nil, errors.New("CertifierInfo: not supported") +} + +func (p *TestPoet) Info(ctx context.Context) (*types.PoetInfo, error) { + return &types.PoetInfo{ + PhaseShift: p.poetCfg.PhaseShift, + CycleGap: p.poetCfg.CycleGap, + }, nil } // Build a proof. diff --git a/activation/e2e/poet_test.go b/activation/e2e/poet_test.go index 3aef153168..27d50f18e0 100644 --- a/activation/e2e/poet_test.go +++ b/activation/e2e/poet_test.go @@ -259,10 +259,10 @@ func TestCertifierInfo(t *testing.T) { ) require.NoError(t, err) - url, pubkey, err := client.CertifierInfo(context.Background()) + certInfo, err := client.CertifierInfo(context.Background()) r.NoError(err) - r.Equal("http://localhost:8080", url.String()) - r.Equal([]byte("pubkey"), pubkey) + r.Equal("http://localhost:8080", certInfo.Url.String()) + r.Equal([]byte("pubkey"), certInfo.Pubkey) } func TestNoCertifierInfo(t *testing.T) { @@ -291,6 +291,6 @@ func TestNoCertifierInfo(t *testing.T) { ) require.NoError(t, err) - _, _, err = client.CertifierInfo(context.Background()) + _, err = client.CertifierInfo(context.Background()) r.ErrorContains(err, "poet doesn't support certificates") } diff --git a/activation/e2e/validation_test.go b/activation/e2e/validation_test.go index 4b63dd2174..0880f2346f 100644 --- a/activation/e2e/validation_test.go +++ b/activation/e2e/validation_test.go @@ -51,7 +51,7 @@ func TestValidator_Validate(t *testing.T) { } poetDb := activation.NewPoetDb(statesql.InMemory(), logger.Named("poetDb")) - client := ae2e.NewTestPoetClient(1) + client := ae2e.NewTestPoetClient(1, poetCfg) poetService := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) mclock := activation.NewMocklayerClock(ctrl) @@ -98,7 +98,7 @@ func TestValidator_Validate(t *testing.T) { newNIPost := *nipost.NIPost newNIPost.Post = &types.Post{} _, err = v.NIPost(context.Background(), sig.NodeID(), goldenATX, &newNIPost, challenge, nipost.NumUnits) - require.ErrorContains(t, err, "invalid Post") + require.ErrorContains(t, err, "validating Post: verifying PoST: proof indices are empty") newPostCfg := cfg newPostCfg.MinNumUnits = nipost.NumUnits + 1 diff --git a/activation/handler.go b/activation/handler.go index 8290d7883c..da99dd999d 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -146,6 +146,7 @@ func NewHandler( fetcher: fetcher, beacon: beacon, tortoise: tortoise, + malPublisher: &MalfeasancePublisher{}, }, } @@ -265,7 +266,7 @@ func (h *Handler) handleAtx( opaqueAtx, err := h.decodeATX(msg) if err != nil { - return nil, fmt.Errorf("decoding ATX: %w", err) + return nil, fmt.Errorf("%w: decoding ATX: %w", pubsub.ErrValidationReject, err) } id := opaqueAtx.ID() @@ -285,7 +286,7 @@ func (h *Handler) handleAtx( case *wire.ActivationTxV1: return h.v1.processATX(ctx, peer, atx, receivedTime) case *wire.ActivationTxV2: - return h.v2.processATX(ctx, peer, atx, receivedTime) + return (*mwire.MalfeasanceProof)(nil), h.v2.processATX(ctx, peer, atx, receivedTime) default: panic("unreachable") } diff --git a/activation/handler_test.go b/activation/handler_test.go index 7b364f3784..29a0f6e3e7 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -120,12 +120,13 @@ func toAtx(t testing.TB, watx *wire.ActivationTxV1) *types.ActivationTx { type handlerMocks struct { goldenATXID types.ATXID - mclock *MocklayerClock - mpub *pubsubmocks.MockPublisher - mockFetch *mocks.MockFetcher - mValidator *MocknipostValidator - mbeacon *MockAtxReceiver - mtortoise *mocks.MockTortoise + mclock *MocklayerClock + mpub *pubsubmocks.MockPublisher + mockFetch *mocks.MockFetcher + mValidator *MocknipostValidator + mbeacon *MockAtxReceiver + mtortoise *mocks.MockTortoise + mMalPublish *MockmalfeasancePublisher } type testHandler struct { @@ -189,6 +190,7 @@ func newTestHandlerMocks(tb testing.TB, golden types.ATXID) handlerMocks { mValidator: NewMocknipostValidator(ctrl), mbeacon: NewMockAtxReceiver(ctrl), mtortoise: mocks.NewMockTortoise(ctrl), + mMalPublish: NewMockmalfeasancePublisher(ctrl), } } @@ -361,7 +363,7 @@ func TestHandler_HandleGossipAtx(t *testing.T) { atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), types.Hash32(second.NIPost.PostMetadata.Challenge)) atxHdlr.mockFetch.EXPECT().GetAtxs(gomock.Any(), []types.ATXID{second.PrevATXID}, gomock.Any()) err = atxHdlr.HandleGossipAtx(context.Background(), "", codec.MustEncode(second)) - require.ErrorContains(t, err, "syntactically invalid based on deps") + require.ErrorIs(t, err, sql.ErrNotFound) // valid first comes in atxHdlr.expectAtxV1(first, sig.NodeID()) @@ -515,6 +517,7 @@ func TestHandler_HandleSyncedAtx(t *testing.T) { err := atxHdlr.HandleSyncedAtx(context.Background(), atx.ID().Hash32(), p2p.NoPeer, buf) require.ErrorIs(t, err, errMalformedData) require.ErrorContains(t, err, "invalid atx signature") + require.ErrorIs(t, err, pubsub.ErrValidationReject) }) t.Run("atx V2", func(t *testing.T) { t.Parallel() @@ -858,12 +861,14 @@ func TestHandler_DecodeATX(t *testing.T) { atxHdlr := newTestHandler(t, types.RandomATXID()) _, err := atxHdlr.decodeATX(nil) require.ErrorIs(t, err, errMalformedData) + require.ErrorIs(t, err, pubsub.ErrValidationReject) }) t.Run("malformed atx", func(t *testing.T) { t.Parallel() atxHdlr := newTestHandler(t, types.RandomATXID()) _, err := atxHdlr.decodeATX([]byte("malformed")) require.ErrorIs(t, err, errMalformedData) + require.ErrorIs(t, err, pubsub.ErrValidationReject) }) t.Run("v1", func(t *testing.T) { t.Parallel() @@ -894,5 +899,6 @@ func TestHandler_DecodeATX(t *testing.T) { atx.PublishEpoch = 9 _, err := atxHdlr.decodeATX(atx.Blob().Blob) require.ErrorIs(t, err, errMalformedData) + require.ErrorIs(t, err, pubsub.ErrValidationReject) }) } diff --git a/activation/handler_v1.go b/activation/handler_v1.go index d1f28ea0f1..a3be09c5b4 100644 --- a/activation/handler_v1.go +++ b/activation/handler_v1.go @@ -22,6 +22,7 @@ import ( "github.com/spacemeshos/go-spacemesh/log" mwire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/signing" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" @@ -144,7 +145,7 @@ func (h *HandlerV1) syntacticallyValidate(ctx context.Context, atx *wire.Activat if err := h.nipostValidator.Post( ctx, atx.SmesherID, *atx.CommitmentATXID, post, &initialPostMetadata, atx.NumUnits, ); err != nil { - return fmt.Errorf("invalid initial post: %w", err) + return fmt.Errorf("validating initial post: %w", err) } default: if atx.NodeID != nil { @@ -203,7 +204,7 @@ func (h *HandlerV1) syntacticallyValidateDeps( } expectedChallengeHash := atx.NIPostChallengeV1.Hash() - h.logger.Info("validating nipost", + h.logger.Debug("validating nipost", log.ZContext(ctx), zap.Stringer("expected_challenge_hash", expectedChallengeHash), zap.Stringer("atx_id", atx.ID()), @@ -220,7 +221,7 @@ func (h *HandlerV1) syntacticallyValidateDeps( ) var invalidIdx *verifying.ErrInvalidIndex if errors.As(err, &invalidIdx) { - h.logger.Info("ATX with invalid post index", + h.logger.Debug("ATX with invalid post index", log.ZContext(ctx), zap.Stringer("atx_id", atx.ID()), zap.Int("index", invalidIdx.Index), @@ -244,7 +245,7 @@ func (h *HandlerV1) syntacticallyValidateDeps( return 0, 0, proof, nil } if err != nil { - return 0, 0, nil, fmt.Errorf("invalid nipost: %w", err) + return 0, 0, nil, fmt.Errorf("validating nipost: %w", err) } return leaves, effectiveNumUnits, nil, err @@ -267,7 +268,7 @@ func (h *HandlerV1) validateNonInitialAtx( } if needRecheck { - h.logger.Info("validating VRF nonce", + h.logger.Debug("validating VRF nonce", log.ZContext(ctx), zap.Stringer("atx_id", atx.ID()), zap.Bool("post increased", atx.NumUnits > previous.NumUnits), @@ -283,52 +284,11 @@ func (h *HandlerV1) validateNonInitialAtx( return nil } -// contextuallyValidateAtx ensures that the previous ATX referenced is the last known ATX for the referenced miner ID. -// If a previous ATX is not referenced, it validates that indeed there's no previous known ATX for that miner ID. -func (h *HandlerV1) contextuallyValidateAtx(atx *wire.ActivationTxV1) error { - lastAtx, err := atxs.GetLastIDByNodeID(h.cdb, atx.SmesherID) - if err == nil && atx.PrevATXID == lastAtx { - // last atx referenced equals last ATX seen from node - return nil - } - - if err == nil && atx.PrevATXID == types.EmptyATXID { - // no previous atx declared, but already seen at least one atx from node - return fmt.Errorf( - "no prev atx reported, but other atx with same node id (%v) found: %v", - atx.SmesherID, - lastAtx.ShortString(), - ) - } - - if err == nil && atx.PrevATXID != lastAtx { - // last atx referenced does not equal last ATX seen from node - return errors.New("last atx is not the one referenced") - } - - if errors.Is(err, sql.ErrNotFound) && atx.PrevATXID == types.EmptyATXID { - // no previous atx found and none referenced - return nil - } - - if err != nil && atx.PrevATXID != types.EmptyATXID { - // no previous atx found but previous atx referenced - h.logger.Error("could not fetch node last atx", - zap.Stringer("atx_id", atx.ID()), - zap.Stringer("smesher", atx.SmesherID), - zap.Error(err), - ) - return fmt.Errorf("could not fetch node last atx: %w", err) - } - - return err -} - // cacheAtx caches the atx in the atxsdata cache. // Returns true if the atx was cached, false otherwise. func (h *HandlerV1) cacheAtx(ctx context.Context, atx *types.ActivationTx) *atxsdata.ATX { if !h.atxsdata.IsEvicted(atx.TargetEpoch()) { - malicious, err := h.cdb.IsMalicious(atx.SmesherID) + malicious, err := identities.IsMalicious(h.cdb, atx.SmesherID) if err != nil { h.logger.Error("failed is malicious read", zap.Error(err), log.ZContext(ctx)) return nil @@ -396,7 +356,7 @@ func (h *HandlerV1) checkDoublePublish( return nil, fmt.Errorf("add malfeasance proof: %w", err) } - h.logger.Warn("smesher produced more than one atx in the same epoch", + h.logger.Debug("smesher produced more than one atx in the same epoch", log.ZContext(ctx), zap.Stringer("smesher", atx.SmesherID), zap.Stringer("previous", prev), @@ -412,11 +372,11 @@ func (h *HandlerV1) checkWrongPrevAtx( tx sql.Executor, atx *wire.ActivationTxV1, ) (*mwire.MalfeasanceProof, error) { - prevID, err := atxs.PrevIDByNodeID(tx, atx.SmesherID, atx.PublishEpoch) + expectedPrevID, err := atxs.PrevIDByNodeID(tx, atx.SmesherID, atx.PublishEpoch) if err != nil && !errors.Is(err, sql.ErrNotFound) { return nil, fmt.Errorf("get last atx by node id: %w", err) } - if prevID == atx.PrevATXID { + if expectedPrevID == atx.PrevATXID { return nil, nil } @@ -427,55 +387,24 @@ func (h *HandlerV1) checkWrongPrevAtx( "registering at PoET", log.ZContext(ctx), zap.Stringer("smesher", atx.SmesherID), - log.ZShortStringer("expected", prevID), + log.ZShortStringer("expected", expectedPrevID), log.ZShortStringer("actual", atx.PrevATXID), ) return nil, fmt.Errorf("%s referenced incorrect previous ATX", atx.SmesherID.ShortString()) } - var atx2ID types.ATXID - if atx.PrevATXID == types.EmptyATXID { - // if the ATX references an empty previous ATX, we can just take the initial ATX and create a proof - // that the node referenced the wrong previous ATX - id, err := atxs.GetFirstIDByNodeID(tx, atx.SmesherID) - if err != nil { - return nil, fmt.Errorf("get initial atx id: %w", err) - } - - atx2ID = id - } else { - prev, err := atxs.Get(tx, atx.PrevATXID) - if err != nil { - return nil, fmt.Errorf("get prev atx: %w", err) - } - - // if atx references a previous ATX that is not the last ATX by the same node, there must be at least one - // atx published between prevATX and the current epoch - pubEpoch := h.clock.CurrentLayer().GetEpoch() - for pubEpoch > prev.PublishEpoch { - id, err := atxs.PrevIDByNodeID(tx, atx.SmesherID, pubEpoch) - if err != nil { - return nil, fmt.Errorf("get prev atx id by node id: %w", err) - } - - atx2, err := atxs.Get(tx, id) - if err != nil { - return nil, fmt.Errorf("get prev atx: %w", err) - } - if atx.ID() != atx2.ID() && atx.PrevATXID == atx2.PrevATXID { - // found an ATX that points to the same previous ATX - atx2ID = id - break - } - pubEpoch = atx2.PublishEpoch - } - } - - if atx2ID == types.EmptyATXID { - // something went wrong, we couldn't find an ATX that points to the same previous ATX - // this should never happen since we are checking in other places that all ATXs from the same node - // form a chain - return nil, errors.New("failed double previous check: could not find an ATX with same previous ATX") + atx2ID, err := atxs.AtxWithPrevious(tx, atx.PrevATXID, atx.SmesherID) + switch { + case errors.Is(err, sql.ErrNotFound): + return nil, nil + case err != nil: + return nil, fmt.Errorf("fetching atx with previous %s: %w", atx.PrevATXID, err) + case atx2ID == atx.ID(): + // We retrieved the same ATX, which means this ATX is already in the DB. + // We don't need to look for a different ATX with the same previous ATX + // because if there are already 2 with the same previous ATX, the + // malfeasance proof was already generated. + return nil, nil } var blob sql.Blob @@ -508,11 +437,11 @@ func (h *HandlerV1) checkWrongPrevAtx( return nil, fmt.Errorf("add malfeasance proof: %w", err) } - h.logger.Warn("smesher referenced the wrong previous in published ATX", + h.logger.Debug("smesher referenced the wrong previous in published ATX", log.ZContext(ctx), zap.Stringer("smesher", atx.SmesherID), - log.ZShortStringer("expected", prevID), log.ZShortStringer("actual", atx.PrevATXID), + log.ZShortStringer("expected", expectedPrevID), ) return proof, nil } @@ -554,7 +483,7 @@ func (h *HandlerV1) storeAtx( if err != nil && !errors.Is(err, sql.ErrObjectExists) { return fmt.Errorf("add atx to db: %w", err) } - err = atxs.SetUnits(tx, atx.ID(), atx.SmesherID, watx.NumUnits) + err = atxs.SetPost(tx, atx.ID(), watx.PrevATXID, 0, atx.SmesherID, watx.NumUnits) if err != nil && !errors.Is(err, sql.ErrObjectExists) { return fmt.Errorf("set atx units: %w", err) } @@ -590,7 +519,7 @@ func (h *HandlerV1) processATX( received time.Time, ) (*mwire.MalfeasanceProof, error) { if !h.edVerifier.Verify(signing.ATX, watx.SmesherID, watx.SignedBytes(), watx.Signature) { - return nil, fmt.Errorf("invalid atx signature: %w", errMalformedData) + return nil, fmt.Errorf("%w: invalid atx signature: %w", pubsub.ErrValidationReject, errMalformedData) } existing, _ := h.cdb.GetAtx(watx.ID()) @@ -606,7 +535,7 @@ func (h *HandlerV1) processATX( ) if err := h.syntacticallyValidate(ctx, watx); err != nil { - return nil, fmt.Errorf("atx %s syntactically invalid: %w", watx.ID(), err) + return nil, fmt.Errorf("%w: validating atx %s: %w", pubsub.ErrValidationReject, watx.ID(), err) } poetRef, atxIDs := collectAtxDeps(h.goldenATXID, watx) @@ -617,23 +546,12 @@ func (h *HandlerV1) processATX( leaves, effectiveNumUnits, proof, err := h.syntacticallyValidateDeps(ctx, watx) if err != nil { - return nil, fmt.Errorf("atx %s syntactically invalid based on deps: %w", watx.ID(), err) + return nil, fmt.Errorf("%w: validating atx %s (deps): %w", pubsub.ErrValidationReject, watx.ID(), err) } if proof != nil { return proof, nil } - if err := h.contextuallyValidateAtx(watx); err != nil { - h.logger.Warn("atx is contextually invalid ", - log.ZContext(ctx), - zap.Stringer("atx_id", watx.ID()), - zap.Stringer("smesherID", watx.SmesherID), - zap.Error(err), - ) - } else { - h.logger.Debug("atx is valid", zap.Stringer("atx_id", watx.ID())) - } - var baseTickHeight uint64 if watx.PositioningATXID != h.goldenATXID { posAtx, err := h.cdb.GetAtx(watx.PositioningATXID) @@ -663,7 +581,7 @@ func (h *HandlerV1) processATX( } events.ReportNewActivation(atx) - h.logger.Info("new atx", + h.logger.Debug("new atx", log.ZContext(ctx), zap.Inline(atx), zap.Bool("malicious", proof != nil), @@ -685,7 +603,7 @@ func (h *HandlerV1) registerHashes(peer p2p.Peer, poetRef types.Hash32, atxIDs [ // fetchReferences makes sure that the referenced poet proof and ATXs are available. func (h *HandlerV1) fetchReferences(ctx context.Context, poetRef types.Hash32, atxIDs []types.ATXID) error { if err := h.fetcher.GetPoetProof(ctx, poetRef); err != nil { - return fmt.Errorf("missing poet proof (%s): %w", poetRef.ShortString(), err) + return fmt.Errorf("fetching poet proof (%s): %w", poetRef.ShortString(), err) } if len(atxIDs) == 0 { @@ -693,7 +611,7 @@ func (h *HandlerV1) fetchReferences(ctx context.Context, poetRef types.Hash32, a } if err := h.fetcher.GetAtxs(ctx, atxIDs, system.WithoutLimiting()); err != nil { - return fmt.Errorf("missing atxs %x: %w", atxIDs, err) + return fmt.Errorf("missing atxs %s: %w", atxIDs, err) } h.logger.Debug("done fetching references", diff --git a/activation/handler_v1_test.go b/activation/handler_v1_test.go index 1cf8ada335..0ebb6f44dd 100644 --- a/activation/handler_v1_test.go +++ b/activation/handler_v1_test.go @@ -15,10 +15,11 @@ import ( "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" + "github.com/spacemeshos/go-spacemesh/fetch" mwire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/signing" - "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" "github.com/spacemeshos/go-spacemesh/sql/identities" "github.com/spacemeshos/go-spacemesh/sql/statesql" @@ -298,7 +299,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { NIPost(gomock.Any(), atx.SmesherID, goldenATXID, gomock.Any(), gomock.Any(), atx.NumUnits, gomock.Any()). Return(0, errors.New("bad nipost")) _, _, _, err := atxHdlr.syntacticallyValidateDeps(context.Background(), atx) - require.EqualError(t, err, "invalid nipost: bad nipost") + require.EqualError(t, err, "validating nipost: bad nipost") }) t.Run("missing NodeID in initial atx", func(t *testing.T) { t.Parallel() @@ -452,85 +453,6 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { }) } -func TestHandler_ContextuallyValidateAtx(t *testing.T) { - goldenATXID := types.ATXID{2, 3, 4} - - sig, err := signing.NewEdSigner() - require.NoError(t, err) - - t.Run("valid initial atx", func(t *testing.T) { - t.Parallel() - - atx := newInitialATXv1(t, goldenATXID) - atx.Sign(sig) - - atxHdlr := newV1TestHandler(t, goldenATXID) - require.NoError(t, atxHdlr.contextuallyValidateAtx(atx)) - }) - - t.Run("missing prevAtx", func(t *testing.T) { - t.Parallel() - - atxHdlr := newV1TestHandler(t, goldenATXID) - - prevAtx := newInitialATXv1(t, goldenATXID) - atx := newChainedActivationTxV1(t, prevAtx, goldenATXID) - - err = atxHdlr.contextuallyValidateAtx(atx) - require.ErrorIs(t, err, sql.ErrNotFound) - }) - - t.Run("wrong previous atx by same node", func(t *testing.T) { - t.Parallel() - - atxHdlr := newV1TestHandler(t, goldenATXID) - - atx0 := newInitialATXv1(t, goldenATXID) - atx0.Sign(sig) - atxHdlr.expectAtxV1(atx0, sig.NodeID()) - _, err := atxHdlr.processATX(context.Background(), "", atx0, time.Now()) - require.NoError(t, err) - - atx1 := newChainedActivationTxV1(t, atx0, goldenATXID) - atx1.Sign(sig) - atxHdlr.expectAtxV1(atx1, sig.NodeID()) - atxHdlr.mockFetch.EXPECT().GetAtxs(gomock.Any(), gomock.Any(), gomock.Any()) - _, err = atxHdlr.processATX(context.Background(), "", atx1, time.Now()) - require.NoError(t, err) - - atxInvalidPrevious := newChainedActivationTxV1(t, atx0, goldenATXID) - atxInvalidPrevious.Sign(sig) - err = atxHdlr.contextuallyValidateAtx(atxInvalidPrevious) - require.EqualError(t, err, "last atx is not the one referenced") - }) - - t.Run("wrong previous atx from different node", func(t *testing.T) { - t.Parallel() - - otherSig, err := signing.NewEdSigner() - require.NoError(t, err) - - atxHdlr := newV1TestHandler(t, goldenATXID) - - atx0 := newInitialATXv1(t, goldenATXID) - atx0.Sign(otherSig) - atxHdlr.expectAtxV1(atx0, otherSig.NodeID()) - _, err = atxHdlr.processATX(context.Background(), "", atx0, time.Now()) - require.NoError(t, err) - - atx1 := newInitialATXv1(t, goldenATXID) - atx1.Sign(sig) - atxHdlr.expectAtxV1(atx1, sig.NodeID()) - _, err = atxHdlr.processATX(context.Background(), "", atx1, time.Now()) - require.NoError(t, err) - - atxInvalidPrevious := newChainedActivationTxV1(t, atx0, goldenATXID) - atxInvalidPrevious.Sign(sig) - err = atxHdlr.contextuallyValidateAtx(atxInvalidPrevious) - require.EqualError(t, err, "last atx is not the one referenced") - }) -} - func TestHandlerV1_StoreAtx(t *testing.T) { goldenATXID := types.RandomATXID() @@ -731,7 +653,6 @@ func TestHandlerV1_StoreAtx(t *testing.T) { })) atxHdlr.mtortoise.EXPECT().OnAtx(watx3.PublishEpoch+1, watx3.ID(), gomock.Any()) atxHdlr.mtortoise.EXPECT().OnMalfeasance(sig.NodeID()) - atxHdlr.mclock.EXPECT().CurrentLayer().Return(watx3.PublishEpoch.FirstLayer()) proof, err = atxHdlr.storeAtx(context.Background(), atx3, watx3) require.NoError(t, err) require.NotNil(t, proof) @@ -855,4 +776,18 @@ func TestHandlerV1_FetchesReferences(t *testing.T) { atxHdlr.mockFetch.EXPECT().GetAtxs(gomock.Any(), atxs, gomock.Any()).Return(errors.New("oh")) require.Error(t, atxHdlr.fetchReferences(context.Background(), poet, atxs)) }) + t.Run("reject ATX when dependency ATX is rejected", func(t *testing.T) { + t.Parallel() + atxHdlr := newV1TestHandler(t, goldenATXID) + + poet := types.RandomHash() + atxs := []types.ATXID{types.RandomATXID(), types.RandomATXID()} + var batchErr fetch.BatchError + batchErr.Add(atxs[0].Hash32(), pubsub.ErrValidationReject) + + atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), poet) + atxHdlr.mockFetch.EXPECT().GetAtxs(gomock.Any(), atxs, gomock.Any()).Return(&batchErr) + + require.ErrorIs(t, atxHdlr.fetchReferences(context.Background(), poet, atxs), pubsub.ErrValidationReject) + }) } diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 1aed281048..d1d8dd4a15 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -23,8 +23,8 @@ import ( "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/events" "github.com/spacemeshos/go-spacemesh/log" - mwire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/signing" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" @@ -66,6 +66,7 @@ type HandlerV2 struct { tortoise system.Tortoise logger *zap.Logger fetcher system.Fetcher + malPublisher malfeasancePublisher } func (h *HandlerV2) processATX( @@ -73,13 +74,13 @@ func (h *HandlerV2) processATX( peer p2p.Peer, watx *wire.ActivationTxV2, received time.Time, -) (*mwire.MalfeasanceProof, error) { +) error { exists, err := atxs.Has(h.cdb, watx.ID()) if err != nil { - return nil, fmt.Errorf("failed to check if atx exists: %w", err) + return fmt.Errorf("failed to check if atx exists: %w", err) } if exists { - return nil, nil + return nil } h.logger.Debug( @@ -91,49 +92,44 @@ func (h *HandlerV2) processATX( ) if err := h.syntacticallyValidate(ctx, watx); err != nil { - return nil, fmt.Errorf("atx %s syntactically invalid: %w", watx.ID(), err) + return fmt.Errorf("%w: validating atx %s: %w", pubsub.ErrValidationReject, watx.ID(), err) } poetRef, atxIDs := h.collectAtxDeps(watx) h.registerHashes(peer, poetRef, atxIDs) if err := h.fetchReferences(ctx, poetRef, atxIDs); err != nil { - return nil, fmt.Errorf("fetching references for atx %s: %w", watx.ID(), err) + return fmt.Errorf("fetching references for atx %s: %w", watx.ID(), err) } baseTickHeight, err := h.validatePositioningAtx(watx.PublishEpoch, h.goldenATXID, watx.PositioningATX) if err != nil { - return nil, fmt.Errorf("validating positioning atx: %w", err) + return fmt.Errorf("%w: validating positioning atx: %w", pubsub.ErrValidationReject, err) } marrying, err := h.validateMarriages(watx) if err != nil { - return nil, fmt.Errorf("validating marriages: %w", err) + return fmt.Errorf("%w: validating marriages: %w", pubsub.ErrValidationReject, err) } - parts, proof, err := h.syntacticallyValidateDeps(ctx, watx) + atxData, err := h.syntacticallyValidateDeps(ctx, watx) if err != nil { - return nil, fmt.Errorf("atx %s syntactically invalid based on deps: %w", watx.ID(), err) - } - - if proof != nil { - return proof, err + return fmt.Errorf("%w: validating atx %s (deps): %w", pubsub.ErrValidationReject, watx.ID(), err) } + atxData.marriages = marrying atx := &types.ActivationTx{ PublishEpoch: watx.PublishEpoch, + MarriageATX: watx.MarriageATX, Coinbase: watx.Coinbase, BaseTickHeight: baseTickHeight, - NumUnits: parts.effectiveUnits, - TickCount: parts.ticks, - Weight: parts.weight, + NumUnits: atxData.effectiveUnits, + TickCount: atxData.ticks, + Weight: atxData.weight, VRFNonce: types.VRFPostIndex(watx.VRFNonce), SmesherID: watx.SmesherID, } - if watx.Initial == nil { - // FIXME: update to keep many previous ATXs to support merged ATXs - atx.PrevATXID = watx.PreviousATXs[0] - } else { + if watx.Initial != nil { atx.CommitmentATX = &watx.Initial.CommitmentATX } @@ -143,19 +139,18 @@ func (h *HandlerV2) processATX( atx.SetID(watx.ID()) atx.SetReceived(received) - proof, err = h.storeAtx(ctx, atx, watx, marrying, parts.units) - if err != nil { - return nil, fmt.Errorf("cannot store atx %s: %w", atx.ShortString(), err) + if err := h.storeAtx(ctx, atx, atxData); err != nil { + return fmt.Errorf("cannot store atx %s: %w", atx.ShortString(), err) } events.ReportNewActivation(atx) - h.logger.Info("new atx", log.ZContext(ctx), zap.Inline(atx), zap.Bool("malicious", proof != nil)) - return proof, err + h.logger.Debug("new atx", log.ZContext(ctx), zap.Inline(atx)) + return err } // Syntactically validate an ATX. func (h *HandlerV2) syntacticallyValidate(ctx context.Context, atx *wire.ActivationTxV2) error { - if !h.edVerifier.Verify(signing.ATX, atx.SmesherID, atx.SignedBytes(), atx.Signature) { + if !h.edVerifier.Verify(signing.ATX, atx.SmesherID, atx.ID().Bytes(), atx.Signature) { return fmt.Errorf("invalid atx signature: %w", errMalformedData) } if atx.PositioningATX == types.EmptyATXID { @@ -210,7 +205,7 @@ func (h *HandlerV2) syntacticallyValidate(ctx context.Context, atx *wire.Activat if err := h.nipostValidator.PostV2( ctx, atx.SmesherID, atx.Initial.CommitmentATX, post, shared.ZeroChallenge, numUnits, ); err != nil { - return fmt.Errorf("invalid initial post: %w", err) + return fmt.Errorf("validating initial post: %w", err) } return nil } @@ -273,7 +268,7 @@ func (h *HandlerV2) fetchReferences(ctx context.Context, poetRefs []types.Hash32 if len(atxIDs) != 0 { eg.Go(func() error { if err := h.fetcher.GetAtxs(ctx, atxIDs, system.WithoutLimiting()); err != nil { - return fmt.Errorf("missing atxs %x: %w", atxIDs, err) + return fmt.Errorf("missing atxs %s: %w", atxIDs, err) } return nil }) @@ -377,7 +372,7 @@ func (h *HandlerV2) validateMarriages(atx *wire.ActivationTxV2) ([]marriage, err return nil, nil } marryingIDsSet := make(map[types.NodeID]struct{}, len(atx.Marriages)) - var marryingIDs []marriage // for deterministic order + var marryingIDs []marriage for i, m := range atx.Marriages { var id types.NodeID if m.ReferenceAtx == types.EmptyATXID { @@ -437,11 +432,19 @@ func (h *HandlerV2) equivocationSet(atx *wire.ActivationTxV2) ([]types.NodeID, e return identities.EquivocationSetByMarriageATX(h.cdb, *atx.MarriageATX) } -type atxParts struct { +type idData struct { + previous types.ATXID + previousIndex int + units uint32 +} + +type activationTx struct { + *wire.ActivationTxV2 ticks uint64 weight uint64 effectiveUnits uint32 - units map[types.NodeID]uint32 + ids map[types.NodeID]idData + marriages []marriage } type nipostSize struct { @@ -499,13 +502,14 @@ func (h *HandlerV2) verifyIncludedIDsUniqueness(atx *wire.ActivationTxV2) error func (h *HandlerV2) syntacticallyValidateDeps( ctx context.Context, atx *wire.ActivationTxV2, -) (*atxParts, *mwire.MalfeasanceProof, error) { - parts := atxParts{ - units: make(map[types.NodeID]uint32), +) (*activationTx, error) { + result := activationTx{ + ActivationTxV2: atx, + ids: make(map[types.NodeID]idData), } if atx.Initial != nil { if err := h.validateCommitmentAtx(h.goldenATXID, atx.Initial.CommitmentATX, atx.PublishEpoch); err != nil { - return nil, nil, fmt.Errorf("verifying commitment ATX: %w", err) + return nil, fmt.Errorf("verifying commitment ATX: %w", err) } } @@ -513,18 +517,18 @@ func (h *HandlerV2) syntacticallyValidateDeps( for i, prev := range atx.PreviousATXs { prevAtx, err := atxs.Get(h.cdb, prev) if err != nil { - return nil, nil, fmt.Errorf("fetching previous atx: %w", err) + return nil, fmt.Errorf("fetching previous atx: %w", err) } if prevAtx.PublishEpoch >= atx.PublishEpoch { err := fmt.Errorf("previous atx is too new (%d >= %d) (%s) ", prevAtx.PublishEpoch, atx.PublishEpoch, prev) - return nil, nil, err + return nil, err } previousAtxs[i] = prevAtx } equivocationSet, err := h.equivocationSet(atx) if err != nil { - return nil, nil, fmt.Errorf("calculating equivocation set: %w", err) + return nil, fmt.Errorf("calculating equivocation set: %w", err) } // validate previous ATXs @@ -534,7 +538,7 @@ func (h *HandlerV2) syntacticallyValidateDeps( for _, post := range niposts.Posts { if post.MarriageIndex >= uint32(len(equivocationSet)) { err := fmt.Errorf("marriage index out of bounds: %d > %d", post.MarriageIndex, len(equivocationSet)-1) - return nil, nil, err + return nil, err } id := equivocationSet[post.MarriageIndex] @@ -543,7 +547,7 @@ func (h *HandlerV2) syntacticallyValidateDeps( var err error effectiveNumUnits, err = h.validatePreviousAtx(id, &post, previousAtxs) if err != nil { - return nil, nil, fmt.Errorf("validating previous atx: %w", err) + return nil, fmt.Errorf("validating previous atx: %w", err) } } nipostSizes[i].addUnits(effectiveNumUnits) @@ -571,27 +575,27 @@ func (h *HandlerV2) syntacticallyValidateDeps( indexedChallenges[post.MembershipLeafIndex] = nipostChallenge.Hash().Bytes() } - leafIndicies := maps.Keys(indexedChallenges) - slices.Sort(leafIndicies) - poetChallenges := make([][]byte, 0, len(leafIndicies)) - for _, i := range leafIndicies { + leafIndices := maps.Keys(indexedChallenges) + slices.Sort(leafIndices) + poetChallenges := make([][]byte, 0, len(leafIndices)) + for _, i := range leafIndices { poetChallenges = append(poetChallenges, indexedChallenges[i]) } membership := types.MultiMerkleProof{ Nodes: niposts.Membership.Nodes, - LeafIndices: leafIndicies, + LeafIndices: leafIndices, } leaves, err := h.nipostValidator.PoetMembership(ctx, &membership, niposts.Challenge, poetChallenges) if err != nil { - return nil, nil, fmt.Errorf("invalid poet membership: %w", err) + return nil, fmt.Errorf("validating poet membership: %w", err) } nipostSizes[i].ticks = leaves / h.tickSize } - parts.effectiveUnits, parts.weight, err = nipostSizes.sumUp() + result.effectiveUnits, result.weight, err = nipostSizes.sumUp() if err != nil { - return nil, nil, err + return nil, err } // validate all niposts @@ -600,17 +604,19 @@ func (h *HandlerV2) syntacticallyValidateDeps( for _, post := range niposts.Posts { id := equivocationSet[post.MarriageIndex] var commitment types.ATXID + var previous types.ATXID if atx.Initial != nil { commitment = atx.Initial.CommitmentATX } else { var err error commitment, err = atxs.CommitmentATX(h.cdb, id) if err != nil { - return nil, nil, fmt.Errorf("commitment atx not found for ID %s: %w", id, err) + return nil, fmt.Errorf("commitment atx not found for ID %s: %w", id, err) } if id == atx.SmesherID { smesherCommitment = &commitment } + previous = previousAtxs[post.PrevATXIndex].ID() } err := h.nipostValidator.PostV2( @@ -622,181 +628,222 @@ func (h *HandlerV2) syntacticallyValidateDeps( post.NumUnits, PostSubset([]byte(h.local)), ) - var invalidIdx *verifying.ErrInvalidIndex - if errors.As(err, &invalidIdx) { - h.logger.Info( + invalidIdx := &verifying.ErrInvalidIndex{} + if errors.As(err, invalidIdx) { + h.logger.Debug( "ATX with invalid post index", zap.Stringer("id", atx.ID()), zap.Int("index", invalidIdx.Index), ) - // TODO generate malfeasance proof + // TODO(mafa): finish proof + var proof wire.Proof + if err := h.malPublisher.Publish(ctx, id, proof); err != nil { + return nil, fmt.Errorf("publishing malfeasance proof for invalid post: %w", err) + } } if err != nil { - return nil, nil, fmt.Errorf("invalid post for ID %s: %w", id, err) + return nil, fmt.Errorf("validating post for ID %s: %w", id.ShortString(), err) + } + result.ids[id] = idData{ + previous: previous, + previousIndex: int(post.PrevATXIndex), + units: post.NumUnits, } - parts.units[id] = post.NumUnits } } if atx.Initial == nil { if smesherCommitment == nil { - return nil, nil, errors.New("ATX signer not present in merged ATX") + return nil, errors.New("ATX signer not present in merged ATX") } err := h.nipostValidator.VRFNonceV2(atx.SmesherID, *smesherCommitment, atx.VRFNonce, atx.TotalNumUnits()) if err != nil { - return nil, nil, fmt.Errorf("validating VRF nonce: %w", err) + return nil, fmt.Errorf("validating VRF nonce: %w", err) } } - parts.ticks = nipostSizes.minTicks() - - return &parts, nil, nil + result.ticks = nipostSizes.minTicks() + return &result, nil } -func (h *HandlerV2) checkMalicious( - tx sql.Transaction, - watx *wire.ActivationTxV2, - marrying []marriage, -) (bool, *mwire.MalfeasanceProof, error) { - malicious, err := identities.IsMalicious(tx, watx.SmesherID) +func (h *HandlerV2) checkMalicious(ctx context.Context, tx sql.Transaction, atx *activationTx) error { + malicious, err := identities.IsMalicious(tx, atx.SmesherID) + if err != nil { + return fmt.Errorf("checking if node is malicious: %w", err) + } + if malicious { + return nil + } + + malicious, err = h.checkDoubleMarry(ctx, tx, atx) + if err != nil { + return fmt.Errorf("checking double marry: %w", err) + } + if malicious { + return nil + } + + malicious, err = h.checkDoublePost(ctx, tx, atx) if err != nil { - return false, nil, fmt.Errorf("checking if node is malicious: %w", err) + return fmt.Errorf("checking double post: %w", err) } if malicious { - return true, nil, nil + return nil } - proof, err := h.checkDoubleMarry(tx, marrying) + malicious, err = h.checkDoubleMerge(ctx, tx, atx) if err != nil { - return false, nil, fmt.Errorf("checking double marry: %w", err) + return fmt.Errorf("checking double merge: %w", err) } - if proof != nil { - return true, proof, nil + if malicious { + return nil } - // TODO: contextual validation: - // 1. check double-publish + // TODO(mafa): contextual validation: + // 1. check double-publish = ID contributed post to two ATXs in the same epoch // 2. check previous ATX // 3 ID already married (same node ID in multiple marriage certificates) // 4. two ATXs referencing the same marriage certificate in the same epoch - // 5. ID participated in two ATXs (merged and solo) in the same epoch - - return false, nil, nil + return nil } -func (h *HandlerV2) checkDoubleMarry(tx sql.Transaction, marrying []marriage) (*mwire.MalfeasanceProof, error) { - for _, m := range marrying { - married, err := identities.Married(tx, m.id) +func (h *HandlerV2) checkDoubleMarry(ctx context.Context, tx sql.Transaction, atx *activationTx) (bool, error) { + for _, m := range atx.marriages { + mATX, err := identities.MarriageATX(tx, m.id) if err != nil { - return nil, fmt.Errorf("checking if ID is married: %w", err) - } - if married { - proof := &mwire.MalfeasanceProof{ - Proof: mwire.Proof{ - Type: mwire.DoubleMarry, - Data: &mwire.DoubleMarryProof{}, - }, + return false, fmt.Errorf("checking if ID is married: %w", err) + } + if mATX != atx.ID() { + var blob sql.Blob + v, err := atxs.LoadBlob(ctx, tx, mATX.Bytes(), &blob) + if err != nil { + return true, fmt.Errorf("creating double marry proof: %w", err) + } + if v != types.AtxV2 { + h.logger.Fatal("Failed to create double marry malfeasance proof: ATX is not v2", + zap.Stringer("atx_id", mATX), + ) } - return proof, nil + var otherAtx wire.ActivationTxV2 + codec.MustDecode(blob.Bytes, &otherAtx) + + proof, err := wire.NewDoubleMarryProof(tx, atx.ActivationTxV2, &otherAtx, m.id) + if err != nil { + return true, fmt.Errorf("creating double marry proof: %w", err) + } + return true, h.malPublisher.Publish(ctx, m.id, proof) } } - return nil, nil + return false, nil } -// Store an ATX in the DB. -// TODO: detect malfeasance and create proofs. -func (h *HandlerV2) storeAtx( - ctx context.Context, - atx *types.ActivationTx, - watx *wire.ActivationTxV2, - marrying []marriage, - units map[types.NodeID]uint32, -) (*mwire.MalfeasanceProof, error) { - var ( - malicious bool - proof *mwire.MalfeasanceProof +func (h *HandlerV2) checkDoublePost(ctx context.Context, tx sql.Transaction, atx *activationTx) (bool, error) { + for id := range atx.ids { + atxids, err := atxs.FindDoublePublish(tx, id, atx.PublishEpoch) + switch { + case errors.Is(err, sql.ErrNotFound): + continue + case err != nil: + return false, fmt.Errorf("searching for double publish: %w", err) + } + otherAtxId := slices.IndexFunc(atxids, func(other types.ATXID) bool { return other != atx.ID() }) + otherAtx := atxids[otherAtxId] + h.logger.Debug( + "found ID that has already contributed its PoST in this epoch", + zap.Stringer("node_id", id), + zap.Stringer("atx_id", atx.ID()), + zap.Stringer("other_atx_id", otherAtx), + zap.Uint32("epoch", atx.PublishEpoch.Uint32()), + ) + // TODO(mafa): finish proof + var proof wire.Proof + return true, h.malPublisher.Publish(ctx, id, proof) + } + return false, nil +} + +func (h *HandlerV2) checkDoubleMerge(ctx context.Context, tx sql.Transaction, watx *activationTx) (bool, error) { + if watx.MarriageATX == nil { + return false, nil + } + ids, err := atxs.MergeConflict(tx, *watx.MarriageATX, watx.PublishEpoch) + switch { + case errors.Is(err, sql.ErrNotFound): + return false, nil + case err != nil: + return false, fmt.Errorf("searching for ATXs with the same marriage ATX: %w", err) + } + otherIndex := slices.IndexFunc(ids, func(id types.ATXID) bool { return id != watx.ID() }) + other := ids[otherIndex] + + h.logger.Debug("second merged ATX for single marriage - creating malfeasance proof", + zap.Stringer("marriage_atx", *watx.MarriageATX), + zap.Stringer("atx", watx.ID()), + zap.Stringer("other_atx", other), + zap.Stringer("smesher_id", watx.SmesherID), ) - if err := h.cdb.WithTx(ctx, func(tx sql.Transaction) error { - var err error - malicious, proof, err = h.checkMalicious(tx, watx, marrying) - if err != nil { - return fmt.Errorf("check malicious: %w", err) - } - if len(marrying) != 0 { + var proof wire.Proof + return true, h.malPublisher.Publish(ctx, watx.SmesherID, proof) +} + +// Store an ATX in the DB. +func (h *HandlerV2) storeAtx(ctx context.Context, atx *types.ActivationTx, watx *activationTx) error { + if err := h.cdb.WithTx(ctx, func(tx sql.Transaction) error { + if len(watx.marriages) != 0 { marriageData := identities.MarriageData{ ATX: atx.ID(), Target: atx.SmesherID, } - for i, m := range marrying { + for i, m := range watx.marriages { marriageData.Signature = m.signature marriageData.Index = i if err := identities.SetMarriage(tx, m.id, &marriageData); err != nil { return err } } - if !malicious && proof == nil { - // We check for malfeasance again because the marriage increased the equivocation set. - malicious, err = identities.IsMalicious(tx, atx.SmesherID) - if err != nil { - return fmt.Errorf("re-checking if smesherID is malicious: %w", err) - } - } } - err = atxs.Add(tx, atx, watx.Blob()) + err := atxs.Add(tx, atx, watx.Blob()) if err != nil && !errors.Is(err, sql.ErrObjectExists) { return fmt.Errorf("add atx to db: %w", err) } - for id, units := range units { - err = atxs.SetUnits(tx, atx.ID(), id, units) + for id, post := range watx.ids { + err = atxs.SetPost(tx, atx.ID(), post.previous, post.previousIndex, id, post.units) if err != nil && !errors.Is(err, sql.ErrObjectExists) { return fmt.Errorf("setting atx units for ID %s: %w", id, err) } } return nil }); err != nil { - return nil, fmt.Errorf("store atx: %w", err) + return fmt.Errorf("store atx: %w", err) } atxs.AtxAdded(h.cdb, atx) - var allMalicious map[types.NodeID]struct{} - if malicious || proof != nil { - // Combine IDs from the present equivocation set for atx.SmesherID and IDs in atx.Marriages. - allMalicious = make(map[types.NodeID]struct{}) - - set, err := identities.EquivocationSet(h.cdb, atx.SmesherID) + var malicious bool + err := h.cdb.WithTx(ctx, func(tx sql.Transaction) error { + // malfeasance check happens after storing the ATX because storing updates the marriage set + // that is needed for the malfeasance proof + // TODO(mafa): don't store own ATX if it would mark the node as malicious + // this probably needs to be done by validating and storing own ATXs eagerly and skipping validation in + // the gossip handler (not sync!) + err := h.checkMalicious(ctx, tx, watx) if err != nil { - return nil, fmt.Errorf("getting equivocation set: %w", err) - } - for _, id := range set { - allMalicious[id] = struct{}{} - } - for _, m := range marrying { - allMalicious[m.id] = struct{}{} + return fmt.Errorf("check malicious: %w", err) } - } - if proof != nil { - encoded, err := codec.Encode(proof) + malicious, err = identities.IsMalicious(tx, watx.SmesherID) if err != nil { - return nil, fmt.Errorf("encoding malfeasance proof: %w", err) + return fmt.Errorf("checking if identity is malicious: %w", err) } - - for id := range allMalicious { - if err := identities.SetMalicious(h.cdb, id, encoded, atx.Received()); err != nil { - return nil, fmt.Errorf("setting malfeasance proof: %w", err) - } - h.cdb.CacheMalfeasanceProof(id, proof) - } - } - - for id := range allMalicious { - h.tortoise.OnMalfeasance(id) + return nil + }) + if err != nil { + return fmt.Errorf("check malicious: %w", err) } h.beacon.OnAtx(atx) - if added := h.atxsdata.AddFromAtx(atx, malicious || proof != nil); added != nil { + if added := h.atxsdata.AddFromAtx(atx, malicious); added != nil { h.tortoise.OnAtx(atx.TargetEpoch(), atx.ID(), added) } @@ -806,5 +853,5 @@ func (h *HandlerV2) storeAtx( zap.Uint32("publish", atx.PublishEpoch.Uint32()), ) - return proof, nil + return nil } diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 4d3c844810..56dbe7303f 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -17,9 +17,11 @@ import ( "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" - mwire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" + "github.com/spacemeshos/go-spacemesh/fetch" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/signing" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" @@ -61,6 +63,7 @@ func newV2TestHandler(tb testing.TB, golden types.ATXID) *v2TestHandler { fetcher: mocks.mockFetch, beacon: mocks.mbeacon, tortoise: mocks.mtortoise, + malPublisher: mocks.mMalPublish, }, handlerMocks: mocks, } @@ -182,19 +185,18 @@ func (h *v2TestHandler) createAndProcessInitial(t testing.TB, sig *signing.EdSig t.Helper() atx := newInitialATXv2(t, h.handlerMocks.goldenATXID) atx.Sign(sig) - p, err := h.processInitial(t, atx) + err := h.processInitial(t, atx) require.NoError(t, err) - require.Nil(t, p) return atx } -func (h *v2TestHandler) processInitial(t testing.TB, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) { +func (h *v2TestHandler) processInitial(t testing.TB, atx *wire.ActivationTxV2) error { t.Helper() h.expectInitialAtxV2(atx) return h.processATX(context.Background(), peer.ID("peer"), atx, time.Now()) } -func (h *v2TestHandler) processSoloAtx(t testing.TB, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) { +func (h *v2TestHandler) processSoloAtx(t testing.TB, atx *wire.ActivationTxV2) error { t.Helper() h.expectAtxV2(atx) return h.processATX(context.Background(), peer.ID("peer"), atx, time.Now()) @@ -466,9 +468,8 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atxHandler.tickSize = tickSize atxHandler.expectInitialAtxV2(atx) - proof, err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) + err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) require.NoError(t, err) - require.Nil(t, proof) atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) require.NoError(t, err) @@ -481,9 +482,8 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { require.EqualValues(t, atx.NiPosts[0].Posts[0].NumUnits*poetLeaves/tickSize, atxFromDb.Weight) // processing ATX for the second time should skip checks - proof, err = atxHandler.processATX(context.Background(), peer, atx, time.Now()) + err = atxHandler.processATX(context.Background(), peer, atx, time.Now()) require.NoError(t, err) - require.Nil(t, proof) }) t.Run("second ATX", func(t *testing.T) { t.Parallel() @@ -495,9 +495,8 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atx.Sign(sig) atxHandler.expectAtxV2(atx) - proof, err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) + err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) require.NoError(t, err) - require.Nil(t, proof) prevAtx, err := atxs.Get(atxHandler.cdb, prev.ID()) require.NoError(t, err) @@ -525,7 +524,7 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atx := newSoloATXv2(t, prev.Epoch+1, prev.ID, golden) atx.Sign(sig) atxHandler.expectAtxV2(atx) - _, err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) + err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) require.NoError(t, err) atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) @@ -543,9 +542,8 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atx.Sign(sig) atxHandler.expectAtxV2(atx) - proof, err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) + err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) require.NoError(t, err) - require.Nil(t, proof) atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) require.NoError(t, err) @@ -571,8 +569,9 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atx.TotalNumUnits(), ).Return(errors.New("vrf nonce is not valid")) - _, err = atxHandler.processATX(context.Background(), peer, atx, time.Now()) + err = atxHandler.processATX(context.Background(), peer, atx, time.Now()) require.ErrorContains(t, err, "vrf nonce is not valid") + require.ErrorIs(t, err, pubsub.ErrValidationReject) _, err = atxs.Get(atxHandler.cdb, atx.ID()) require.ErrorIs(t, err, sql.ErrNotFound) @@ -588,9 +587,8 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atx.Sign(sig) atxHandler.expectAtxV2(atx) - proof, err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) + err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) require.NoError(t, err) - require.Nil(t, proof) // verify that the ATX was added to the DB and it has the lower effective num units atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) @@ -606,8 +604,9 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atxHandler.mclock.EXPECT().CurrentLayer() atxHandler.expectFetchDeps(atx) - _, err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) + err := atxHandler.processATX(context.Background(), peer, atx, time.Now()) require.ErrorContains(t, err, "validating positioning atx") + require.ErrorIs(t, err, pubsub.ErrValidationReject) _, err = atxs.Get(atxHandler.cdb, atx.ID()) require.ErrorIs(t, err, sql.ErrNotFound) @@ -617,18 +616,16 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { func marryIDs( t testing.TB, atxHandler *v2TestHandler, - sig *signing.EdSigner, + signers []*signing.EdSigner, golden types.ATXID, - num int, ) (marriage *wire.ActivationTxV2, other []*wire.ActivationTxV2) { + sig := signers[0] mATX := newInitialATXv2(t, golden) mATX.Marriages = []wire.MarriageCertificate{{ Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), }} - for range num { - signer, err := signing.NewEdSigner() - require.NoError(t, err) + for _, signer := range signers[1:] { atx := atxHandler.createAndProcessInitial(t, signer) other = append(other, atx) mATX.Marriages = append(mATX.Marriages, wire.MarriageCertificate{ @@ -639,29 +636,35 @@ func marryIDs( mATX.Sign(sig) atxHandler.expectInitialAtxV2(mATX) - p, err := atxHandler.processATX(context.Background(), "", mATX, time.Now()) + err := atxHandler.processATX(context.Background(), "", mATX, time.Now()) require.NoError(t, err) - require.Nil(t, p) return mATX, other } func TestHandlerV2_ProcessMergedATX(t *testing.T) { t.Parallel() - golden := types.RandomATXID() - sig, err := signing.NewEdSigner() - require.NoError(t, err) + var ( + golden = types.RandomATXID() + signers []*signing.EdSigner + equivocationSet []types.NodeID + ) + for range 4 { + sig, err := signing.NewEdSigner() + require.NoError(t, err) + signers = append(signers, sig) + equivocationSet = append(equivocationSet, sig.NodeID()) + } + sig := signers[0] t.Run("happy case", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) // Marry IDs - mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 2) + mATX, otherATXs := marryIDs(t, atxHandler, signers, golden) previousATXs := []types.ATXID{mATX.ID()} - equivocationSet := []types.NodeID{sig.NodeID()} for _, atx := range otherATXs { previousATXs = append(previousATXs, atx.ID()) - equivocationSet = append(equivocationSet, atx.SmesherID) } // Process a merged ATX @@ -683,9 +686,8 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { merged.Sign(sig) atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{poetLeaves}) - p, err := atxHandler.processATX(context.Background(), "", merged, time.Now()) + err := atxHandler.processATX(context.Background(), "", merged, time.Now()) require.NoError(t, err) - require.Nil(t, p) atx, err := atxs.Get(atxHandler.cdb, merged.ID()) require.NoError(t, err) @@ -699,12 +701,10 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { atxHandler.tickSize = tickSize // Marry IDs - mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 4) + mATX, otherATXs := marryIDs(t, atxHandler, signers, golden) previousATXs := []types.ATXID{mATX.ID()} - equivocationSet := []types.NodeID{sig.NodeID()} for _, atx := range otherATXs { previousATXs = append(previousATXs, atx.ID()) - equivocationSet = append(equivocationSet, atx.SmesherID) } // Process a merged ATX @@ -747,9 +747,8 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { minPoetLeaves := slices.Min(poetLeaves) atxHandler.expectMergedAtxV2(merged, equivocationSet, poetLeaves) - p, err := atxHandler.processATX(context.Background(), "", merged, time.Now()) + err := atxHandler.processATX(context.Background(), "", merged, time.Now()) require.NoError(t, err) - require.Nil(t, p) marriageATX, err := atxs.Get(atxHandler.cdb, mATX.ID()) require.NoError(t, err) @@ -771,12 +770,10 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { atxHandler := newV2TestHandler(t, golden) // Marry IDs - mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 2) + mATX, otherATXs := marryIDs(t, atxHandler, signers, golden) previousATXs := []types.ATXID{} - equivocationSet := []types.NodeID{sig.NodeID()} for _, atx := range otherATXs { previousATXs = append(previousATXs, atx.ID()) - equivocationSet = append(equivocationSet, atx.SmesherID) } // Process a merged ATX @@ -800,20 +797,18 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { atxHandler.expectFetchDeps(merged) atxHandler.expectVerifyNIPoSTs(merged, equivocationSet, []uint64{200}) - p, err := atxHandler.processATX(context.Background(), "", merged, time.Now()) + err := atxHandler.processATX(context.Background(), "", merged, time.Now()) require.ErrorContains(t, err, "ATX signer not present in merged ATX") - require.Nil(t, p) + require.ErrorIs(t, err, pubsub.ErrValidationReject) }) t.Run("ID must be present max 1 times", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) // Marry IDs - mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 1) + mATX, otherATXs := marryIDs(t, atxHandler, signers[:2], golden) previousATXs := []types.ATXID{mATX.ID()} - equivocationSet := []types.NodeID{sig.NodeID()} for _, atx := range otherATXs { previousATXs = append(previousATXs, atx.ID()) - equivocationSet = append(equivocationSet, atx.SmesherID) } // Process a merged ATX @@ -834,20 +829,18 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { merged.Sign(sig) atxHandler.mclock.EXPECT().CurrentLayer().Return(merged.PublishEpoch.FirstLayer()) - p, err := atxHandler.processATX(context.Background(), "", merged, time.Now()) + err := atxHandler.processATX(context.Background(), "", merged, time.Now()) require.ErrorContains(t, err, "ID present twice (duplicated marriage index)") - require.Nil(t, p) + require.ErrorIs(t, err, pubsub.ErrValidationReject) }) t.Run("ID must use previous ATX containing itself", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) // Marry IDs - mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 1) + mATX, otherATXs := marryIDs(t, atxHandler, signers[:2], golden) previousATXs := []types.ATXID{mATX.ID()} - equivocationSet := []types.NodeID{sig.NodeID()} for _, atx := range otherATXs { previousATXs = append(previousATXs, atx.ID()) - equivocationSet = append(equivocationSet, atx.SmesherID) } // Process a merged ATX @@ -867,19 +860,14 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { atxHandler.mclock.EXPECT().CurrentLayer().Return(merged.PublishEpoch.FirstLayer()) atxHandler.expectFetchDeps(merged) - p, err := atxHandler.processATX(context.Background(), "", merged, time.Now()) - require.Error(t, err) - require.Nil(t, p) + err := atxHandler.processATX(context.Background(), "", merged, time.Now()) + require.ErrorIs(t, err, pubsub.ErrValidationReject) }) t.Run("previous checkpointed ATX must include every ID", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) // Marry IDs - mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 1) - equivocationSet := []types.NodeID{sig.NodeID()} - for _, atx := range otherATXs { - equivocationSet = append(equivocationSet, atx.SmesherID) - } + mATX, _ := marryIDs(t, atxHandler, signers, golden) prev := atxs.CheckpointAtx{ Epoch: mATX.PublishEpoch + 1, @@ -910,11 +898,10 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { merged.Sign(sig) atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100}) - p, err := atxHandler.processATX(context.Background(), "", merged, time.Now()) + err := atxHandler.processATX(context.Background(), "", merged, time.Now()) require.NoError(t, err) - require.Nil(t, p) - // checkpoint again but not inslude one of the IDs + // checkpoint again but not include one of the IDs prev.ID = types.RandomATXID() prev.Epoch = merged.PublishEpoch + 1 clear(prev.Units) @@ -937,9 +924,99 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { atxHandler.mclock.EXPECT().CurrentLayer().Return(merged.PublishEpoch.FirstLayer()) atxHandler.expectFetchDeps(merged) - p, err = atxHandler.processATX(context.Background(), "", merged, time.Now()) - require.Error(t, err) - require.Nil(t, p) + err = atxHandler.processATX(context.Background(), "", merged, time.Now()) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + }) + t.Run("publishing two merged ATXs from one marriage set is malfeasance", func(t *testing.T) { + atxHandler := newV2TestHandler(t, golden) + + // Marry 4 IDs + mATX, otherATXs := marryIDs(t, atxHandler, signers, golden) + previousATXs := []types.ATXID{mATX.ID()} + for _, atx := range otherATXs { + previousATXs = append(previousATXs, atx.ID()) + } + + // Process a merged ATX for 2 IDs + merged := newSoloATXv2(t, mATX.PublishEpoch+2, mATX.ID(), mATX.ID()) + merged.NiPosts[0].Posts = []wire.SubPostV2{} + for i := range equivocationSet[:2] { + post := wire.SubPostV2{ + MarriageIndex: uint32(i), + PrevATXIndex: uint32(i), + NumUnits: 4, + } + merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post) + } + + mATXID := mATX.ID() + + merged.MarriageATX = &mATXID + merged.PreviousATXs = []types.ATXID{mATX.ID(), otherATXs[0].ID()} + merged.Sign(sig) + + atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100}) + err := atxHandler.processATX(context.Background(), "", merged, time.Now()) + require.NoError(t, err) + + // Process a second merged ATX for the same equivocation set, but different IDs + merged = newSoloATXv2(t, mATX.PublishEpoch+2, mATX.ID(), mATX.ID()) + merged.NiPosts[0].Posts = []wire.SubPostV2{} + for i := range equivocationSet[:2] { + post := wire.SubPostV2{ + MarriageIndex: uint32(i + 2), + PrevATXIndex: uint32(i), + NumUnits: 4, + } + merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post) + } + + mATXID = mATX.ID() + merged.MarriageATX = &mATXID + merged.PreviousATXs = []types.ATXID{otherATXs[1].ID(), otherATXs[2].ID()} + merged.Sign(signers[2]) + + atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100}) + atxHandler.mMalPublish.EXPECT().Publish(gomock.Any(), merged.SmesherID, gomock.Any()) + err = atxHandler.processATX(context.Background(), "", merged, time.Now()) + require.NoError(t, err) + }) + t.Run("publishing two merged ATXs (one checkpointed)", func(t *testing.T) { + atxHandler := newV2TestHandler(t, golden) + + mATX, otherATXs := marryIDs(t, atxHandler, signers, golden) + mATXID := mATX.ID() + + // Insert checkpointed merged ATX + checkpointedATX := &atxs.CheckpointAtx{ + Epoch: mATX.PublishEpoch + 2, + ID: types.RandomATXID(), + SmesherID: signers[0].NodeID(), + MarriageATX: &mATXID, + } + require.NoError(t, atxs.AddCheckpointed(atxHandler.cdb, checkpointedATX)) + + // create and process another merged ATX + merged := newSoloATXv2(t, checkpointedATX.Epoch, mATX.ID(), golden) + merged.NiPosts[0].Posts = []wire.SubPostV2{} + for i := range equivocationSet[2:] { + post := wire.SubPostV2{ + MarriageIndex: uint32(i + 2), + PrevATXIndex: uint32(i), + NumUnits: 4, + } + merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post) + } + + merged.MarriageATX = &mATXID + merged.PreviousATXs = []types.ATXID{otherATXs[1].ID(), otherATXs[2].ID()} + merged.Sign(signers[2]) + atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100}) + // TODO: this could be syntactically validated as all nodes in the network + // should already have the checkpointed merged ATX. + atxHandler.mMalPublish.EXPECT().Publish(gomock.Any(), merged.SmesherID, gomock.Any()) + err := atxHandler.processATX(context.Background(), "", merged, time.Now()) + require.NoError(t, err) }) } @@ -1062,6 +1139,20 @@ func TestHandlerV2_FetchesReferences(t *testing.T) { atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), poets[1]).Return(errors.New("pooh")) require.Error(t, atxHdlr.fetchReferences(context.Background(), poets, nil)) }) + t.Run("reject ATX when dependency poet proof is rejected", func(t *testing.T) { + t.Parallel() + atxHdlr := newV2TestHandler(t, golden) + + poets := []types.Hash32{types.RandomHash()} + atxs := []types.ATXID{types.RandomATXID()} + var batchErr fetch.BatchError + batchErr.Add(atxs[0].Hash32(), pubsub.ErrValidationReject) + + atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), poets[0]).Return(&batchErr) + atxHdlr.mockFetch.EXPECT().GetAtxs(gomock.Any(), atxs, gomock.Any()) + + require.ErrorIs(t, atxHdlr.fetchReferences(context.Background(), poets, atxs), pubsub.ErrValidationReject) + }) t.Run("failed to fetch atxs", func(t *testing.T) { t.Parallel() @@ -1075,6 +1166,20 @@ func TestHandlerV2_FetchesReferences(t *testing.T) { atxHdlr.mockFetch.EXPECT().GetAtxs(gomock.Any(), atxs, gomock.Any()).Return(errors.New("oh")) require.Error(t, atxHdlr.fetchReferences(context.Background(), poets, atxs)) }) + t.Run("reject ATX when dependency ATX is rejected", func(t *testing.T) { + t.Parallel() + atxHdlr := newV2TestHandler(t, golden) + + poets := []types.Hash32{types.RandomHash()} + atxs := []types.ATXID{types.RandomATXID(), types.RandomATXID()} + var batchErr fetch.BatchError + batchErr.Add(atxs[0].Hash32(), pubsub.ErrValidationReject) + + atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), poets[0]) + atxHdlr.mockFetch.EXPECT().GetAtxs(gomock.Any(), atxs, gomock.Any()).Return(&batchErr) + + require.ErrorIs(t, atxHdlr.fetchReferences(context.Background(), poets, atxs), pubsub.ErrValidationReject) + }) t.Run("no atxs to fetch", func(t *testing.T) { t.Parallel() atxHdlr := newV2TestHandler(t, golden) @@ -1191,9 +1296,8 @@ func Test_ValidateMarriages(t *testing.T) { marriage.Sign(sig) atxHandler.expectInitialAtxV2(marriage) - p, err := atxHandler.processATX(context.Background(), "", marriage, time.Now()) + err = atxHandler.processATX(context.Background(), "", marriage, time.Now()) require.NoError(t, err) - require.Nil(t, p) atx := newSoloATXv2(t, marriage.PublishEpoch+1, types.RandomATXID(), golden) marriageATXID := marriage.ID() @@ -1225,9 +1329,8 @@ func Test_ValidateMarriages(t *testing.T) { marriage.Sign(sig) atxHandler.expectInitialAtxV2(marriage) - p, err := atxHandler.processATX(context.Background(), "", marriage, time.Now()) + err = atxHandler.processATX(context.Background(), "", marriage, time.Now()) require.NoError(t, err) - require.Nil(t, p) atx := newSoloATXv2(t, marriage.PublishEpoch+1, types.RandomATXID(), golden) marriageATXID := types.RandomATXID() @@ -1265,9 +1368,8 @@ func Test_ValidateMarriages(t *testing.T) { } marriage.Sign(sig) - p, err := atxHandler.processInitial(t, marriage) + err := atxHandler.processInitial(t, marriage) require.NoError(t, err) - require.Nil(t, p) atx := newSoloATXv2(t, 0, marriage.ID(), golden) atx.PublishEpoch = marriage.PublishEpoch + 2 @@ -1334,7 +1436,7 @@ func Test_ValidatePreviousATX(t *testing.T) { t.Parallel() prev := &types.ActivationTx{} prev.SetID(types.RandomATXID()) - require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), types.RandomNodeID(), 13)) + require.NoError(t, atxs.SetPost(atxHandler.cdb, prev.ID(), types.EmptyATXID, 0, types.RandomNodeID(), 13)) _, err := atxHandler.validatePreviousAtx(types.RandomNodeID(), &wire.SubPostV2{}, []*types.ActivationTx{prev}) require.Error(t, err) @@ -1345,8 +1447,8 @@ func Test_ValidatePreviousATX(t *testing.T) { other := types.RandomNodeID() prev := &types.ActivationTx{} prev.SetID(types.RandomATXID()) - require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), id, 7)) - require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), other, 13)) + require.NoError(t, atxs.SetPost(atxHandler.cdb, prev.ID(), types.EmptyATXID, 0, id, 7)) + require.NoError(t, atxs.SetPost(atxHandler.cdb, prev.ID(), types.EmptyATXID, 0, other, 13)) units, err := atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 100}, []*types.ActivationTx{prev}) require.NoError(t, err) @@ -1366,7 +1468,7 @@ func Test_ValidatePreviousATX(t *testing.T) { other := types.RandomNodeID() prev := &types.ActivationTx{} prev.SetID(types.RandomATXID()) - require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), other, 13)) + require.NoError(t, atxs.SetPost(atxHandler.cdb, prev.ID(), types.EmptyATXID, 0, other, 13)) _, err := atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 100}, []*types.ActivationTx{prev}) require.Error(t, err) @@ -1386,9 +1488,8 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { atx.Initial.CommitmentATX = types.RandomATXID() atx.Sign(sig) - _, proof, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) + _, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) require.ErrorContains(t, err, "verifying commitment ATX") - require.Nil(t, proof) }) t.Run("can't find previous ATX", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) @@ -1396,9 +1497,8 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { atx := newSoloATXv2(t, 0, types.RandomATXID(), golden) atx.Sign(sig) - _, proof, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) + _, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) require.ErrorContains(t, err, "fetching previous atx") - require.Nil(t, proof) }) t.Run("previous ATX too new", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) @@ -1408,9 +1508,8 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { atx := newSoloATXv2(t, 0, prev.ID(), golden) atx.Sign(sig) - _, proof, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) + _, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) require.ErrorContains(t, err, "previous atx is too new") - require.Nil(t, proof) }) t.Run("previous ATX by different smesher", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) @@ -1422,9 +1521,8 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { atx := newSoloATXv2(t, 2, prev.ID(), golden) atx.Sign(sig) - _, proof, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) + _, err = atxHandler.syntacticallyValidateDeps(context.Background(), atx) require.Error(t, err) - require.Nil(t, proof) }) t.Run("invalid PoST", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) @@ -1444,17 +1542,16 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { gomock.Any(), ). Return(errors.New("post failure")) - _, proof, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) + _, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) require.ErrorContains(t, err, "post failure") - require.Nil(t, proof) }) t.Run("invalid PoST index - generates a malfeasance proof", func(t *testing.T) { - t.Skip("malfeasance proof is not generated yet") atxHandler := newV2TestHandler(t, golden) atx := newInitialATXv2(t, golden) atx.Sign(sig) + atxHandler.mValidator.EXPECT().PoetMembership(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) atxHandler.mValidator.EXPECT(). PostV2( gomock.Any(), @@ -1466,9 +1563,11 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { gomock.Any(), ). Return(verifying.ErrInvalidIndex{Index: 7}) - _, proof, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) - require.ErrorContains(t, err, "invalid post") - require.NotNil(t, proof) + atxHandler.mMalPublish.EXPECT().Publish(gomock.Any(), sig.NodeID(), gomock.Any()) + _, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) + vErr := &verifying.ErrInvalidIndex{} + require.ErrorAs(t, err, vErr) + require.Equal(t, 7, vErr.Index) }) t.Run("invalid PoET membership proof", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) @@ -1479,9 +1578,8 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { atxHandler.mValidator.EXPECT(). PoetMembership(gomock.Any(), gomock.Any(), atx.NiPosts[0].Challenge, gomock.Any()). Return(0, errors.New("poet failure")) - _, proof, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) + _, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) require.ErrorContains(t, err, "poet failure") - require.Nil(t, proof) }) } @@ -1522,17 +1620,16 @@ func Test_Marriages(t *testing.T) { } atx.Sign(sig) - p, err := atxHandler.processInitial(t, atx) + err = atxHandler.processInitial(t, atx) require.NoError(t, err) - require.Nil(t, p) - married, err := identities.Married(atxHandler.cdb, sig.NodeID()) + mAtx, err := identities.MarriageATX(atxHandler.cdb, sig.NodeID()) require.NoError(t, err) - require.True(t, married) + require.Equal(t, atx.ID(), mAtx) - married, err = identities.Married(atxHandler.cdb, otherSig.NodeID()) + mAtx, err = identities.MarriageATX(atxHandler.cdb, otherSig.NodeID()) require.NoError(t, err) - require.True(t, married) + require.Equal(t, atx.ID(), mAtx) set, err := identities.EquivocationSet(atxHandler.cdb, sig.NodeID()) require.NoError(t, err) @@ -1548,7 +1645,7 @@ func Test_Marriages(t *testing.T) { othersSecondAtx := newSoloATXv2(t, othersAtx.PublishEpoch+1, othersAtx.ID(), othersAtx.ID()) othersSecondAtx.Sign(otherSig) - _, err = atxHandler.processSoloAtx(t, othersSecondAtx) + err = atxHandler.processSoloAtx(t, othersSecondAtx) require.NoError(t, err) atx := newInitialATXv2(t, golden) @@ -1591,7 +1688,7 @@ func Test_Marriages(t *testing.T) { atx.Sign(sig) atxHandler.expectInitialAtxV2(atx) - _, err = atxHandler.processATX(context.Background(), "", atx, time.Now()) + err = atxHandler.processATX(context.Background(), "", atx, time.Now()) require.NoError(t, err) // otherSig2 cannot marry sig, trying to extend its set. @@ -1610,21 +1707,24 @@ func Test_Marriages(t *testing.T) { } atx2.Sign(sig) atxHandler.expectAtxV2(atx2) - ids := []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()} - for _, id := range ids { - atxHandler.mtortoise.EXPECT().OnMalfeasance(id) - } - proof, err := atxHandler.processATX(context.Background(), "", atx2, time.Now()) - require.NoError(t, err) - // TODO: check the proof contents once its implemented - require.NotNil(t, proof) - - // All 3 IDs are marked as malicious - for _, id := range ids { - malicious, err := identities.IsMalicious(atxHandler.cdb, id) + atxHandler.mMalPublish.EXPECT().Publish( + gomock.Any(), + sig.NodeID(), + gomock.Cond(func(data any) bool { + _, ok := data.(*wire.ProofDoubleMarry) + return ok + }), + ).DoAndReturn(func(ctx context.Context, id types.NodeID, proof wire.Proof) error { + malProof := proof.(*wire.ProofDoubleMarry) + nId, err := malProof.Valid(atxHandler.edVerifier) require.NoError(t, err) - require.True(t, malicious) - } + require.Equal(t, sig.NodeID(), nId) + b := codec.MustEncode(malProof) + _ = b + return nil + }) + err = atxHandler.processATX(context.Background(), "", atx2, time.Now()) + require.NoError(t, err) // The equivocation set of sig and otherSig didn't grow equiv, err := identities.EquivocationSet(atxHandler.cdb, sig.NodeID()) @@ -1649,8 +1749,9 @@ func Test_Marriages(t *testing.T) { atx.Sign(sig) atxHandler.mclock.EXPECT().CurrentLayer().AnyTimes() - _, err = atxHandler.processATX(context.Background(), "", atx, time.Now()) + err = atxHandler.processATX(context.Background(), "", atx, time.Now()) require.ErrorContains(t, err, "signer must marry itself") + require.ErrorIs(t, err, pubsub.ErrValidationReject) }) } @@ -1691,14 +1792,10 @@ func Test_MarryingMalicious(t *testing.T) { }, } atx.Sign(sig) - require.NoError(t, identities.SetMalicious(atxHandler.cdb, tc.malicious, []byte("proof"), time.Now())) atxHandler.expectInitialAtxV2(atx) - atxHandler.mtortoise.EXPECT().OnMalfeasance(sig.NodeID()) - atxHandler.mtortoise.EXPECT().OnMalfeasance(otherSig.NodeID()) - - _, err := atxHandler.processATX(context.Background(), "", atx, time.Now()) + err := atxHandler.processATX(context.Background(), "", atx, time.Now()) require.NoError(t, err) equiv, err := identities.EquivocationSet(atxHandler.cdb, sig.NodeID()) @@ -1714,6 +1811,64 @@ func Test_MarryingMalicious(t *testing.T) { } } +func TestContextualValidation_DoublePost(t *testing.T) { + t.Parallel() + golden := types.RandomATXID() + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + atxHandler := newV2TestHandler(t, golden) + + // marry + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + othersAtx := atxHandler.createAndProcessInitial(t, otherSig) + + mATX := newInitialATXv2(t, golden) + mATX.Marriages = []wire.MarriageCertificate{ + { + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + { + ReferenceAtx: othersAtx.ID(), + Signature: otherSig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + } + mATX.Sign(sig) + + atxHandler.expectInitialAtxV2(mATX) + err = atxHandler.processATX(context.Background(), "", mATX, time.Now()) + require.NoError(t, err) + + // publish merged + merged := newSoloATXv2(t, mATX.PublishEpoch+2, mATX.ID(), mATX.ID()) + post := wire.SubPostV2{ + MarriageIndex: 1, + NumUnits: othersAtx.TotalNumUnits(), + PrevATXIndex: 1, + } + merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post) + + mATXID := mATX.ID() + merged.MarriageATX = &mATXID + + merged.PreviousATXs = []types.ATXID{mATX.ID(), othersAtx.ID()} + merged.Sign(sig) + + atxHandler.expectMergedAtxV2(merged, []types.NodeID{sig.NodeID(), otherSig.NodeID()}, []uint64{poetLeaves}) + err = atxHandler.processATX(context.Background(), "", merged, time.Now()) + require.NoError(t, err) + + // The otherSig tries to publish alone in the same epoch. + // This is malfeasance as it tries include his PoST twice. + doubled := newSoloATXv2(t, merged.PublishEpoch, othersAtx.ID(), othersAtx.ID()) + doubled.Sign(otherSig) + atxHandler.expectAtxV2(doubled) + atxHandler.mMalPublish.EXPECT().Publish(gomock.Any(), otherSig.NodeID(), gomock.Any()) + err = atxHandler.processATX(context.Background(), "", doubled, time.Now()) + require.NoError(t, err) +} + func Test_CalculatingUnits(t *testing.T) { t.Parallel() t.Run("units on 1 nipost must not overflow", func(t *testing.T) { diff --git a/activation/interface.go b/activation/interface.go index a4f16782ee..21c414e9c5 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -10,6 +10,7 @@ import ( "github.com/spacemeshos/post/shared" "github.com/spacemeshos/post/verifying" + "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/signing" "github.com/spacemeshos/go-spacemesh/sql/localsql/certifier" @@ -91,6 +92,18 @@ type syncer interface { RegisterForATXSynced() <-chan struct{} } +// malfeasancePublisher is an interface for publishing malfeasance proofs. +// This interface is used to publish proofs in V2. +// +// The provider of that interface ensures that only valid proofs are published (invalid ones return an error). +// Proofs against an identity that is managed by the node will also return an error and will not be gossiped. +// +// Additionally the publisher will only gossip proofs when the node is in sync, otherwise it will only store them. +// and mark the associated identity as malfeasant. +type malfeasancePublisher interface { + Publish(ctx context.Context, id types.NodeID, proof wire.Proof) error +} + type atxProvider interface { GetAtx(id types.ATXID) (*types.ActivationTx, error) } @@ -129,6 +142,9 @@ type PoetService interface { nodeID types.NodeID, ) (*types.PoetRound, error) + // Certify requests a certificate for the given nodeID. + // + // Returns ErrCertificatesNotSupported if the service does not support certificates. Certify(ctx context.Context, id types.NodeID) (*certifier.PoetCert, error) // Proof returns the proof for the given round ID. @@ -160,12 +176,7 @@ type certifierService interface { pubkey []byte, ) (*certifier.PoetCert, error) - Recertify( - ctx context.Context, - id types.NodeID, - certifierAddress *url.URL, - pubkey []byte, - ) (*certifier.PoetCert, error) + DeleteCertificate(id types.NodeID, pubkey []byte) error } type poetDbAPI interface { diff --git a/activation/malfeasance.go b/activation/malfeasance.go index 7505ff84c0..17cae36997 100644 --- a/activation/malfeasance.go +++ b/activation/malfeasance.go @@ -68,7 +68,7 @@ func (mh *MalfeasanceHandler) Validate(ctx context.Context, data wire.ProofData) msg1.InnerMsg.MsgHash != msg2.InnerMsg.MsgHash { return msg1.SmesherID, nil } - mh.logger.Warn("received invalid atx malfeasance proof", + mh.logger.Debug("received invalid atx malfeasance proof", log.ZContext(ctx), zap.Stringer("first_smesher", msg1.SmesherID), zap.Object("first_proof", &msg1.InnerMsg), diff --git a/activation/malfeasance2.go b/activation/malfeasance2.go new file mode 100644 index 0000000000..ff44452b35 --- /dev/null +++ b/activation/malfeasance2.go @@ -0,0 +1,16 @@ +package activation + +import ( + "context" + + "github.com/spacemeshos/go-spacemesh/activation/wire" + "github.com/spacemeshos/go-spacemesh/common/types" +) + +// MalfeasancePublisher is the publisher for ATX proofs. +type MalfeasancePublisher struct{} + +func (p *MalfeasancePublisher) Publish(ctx context.Context, id types.NodeID, proof wire.Proof) error { + // TODO(mafa): implement me + return nil +} diff --git a/activation/mocks.go b/activation/mocks.go index 8f7bd80c76..38a1a47206 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -1085,6 +1085,67 @@ func (c *MocksyncerRegisterForATXSyncedCall) DoAndReturn(f func() <-chan struct{ return c } +// MockmalfeasancePublisher is a mock of malfeasancePublisher interface. +type MockmalfeasancePublisher struct { + ctrl *gomock.Controller + recorder *MockmalfeasancePublisherMockRecorder +} + +// MockmalfeasancePublisherMockRecorder is the mock recorder for MockmalfeasancePublisher. +type MockmalfeasancePublisherMockRecorder struct { + mock *MockmalfeasancePublisher +} + +// NewMockmalfeasancePublisher creates a new mock instance. +func NewMockmalfeasancePublisher(ctrl *gomock.Controller) *MockmalfeasancePublisher { + mock := &MockmalfeasancePublisher{ctrl: ctrl} + mock.recorder = &MockmalfeasancePublisherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockmalfeasancePublisher) EXPECT() *MockmalfeasancePublisherMockRecorder { + return m.recorder +} + +// Publish mocks base method. +func (m *MockmalfeasancePublisher) Publish(ctx context.Context, id types.NodeID, proof wire.Proof) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Publish", ctx, id, proof) + ret0, _ := ret[0].(error) + return ret0 +} + +// Publish indicates an expected call of Publish. +func (mr *MockmalfeasancePublisherMockRecorder) Publish(ctx, id, proof any) *MockmalfeasancePublisherPublishCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockmalfeasancePublisher)(nil).Publish), ctx, id, proof) + return &MockmalfeasancePublisherPublishCall{Call: call} +} + +// MockmalfeasancePublisherPublishCall wrap *gomock.Call +type MockmalfeasancePublisherPublishCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockmalfeasancePublisherPublishCall) Return(arg0 error) *MockmalfeasancePublisherPublishCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockmalfeasancePublisherPublishCall) Do(f func(context.Context, types.NodeID, wire.Proof) error) *MockmalfeasancePublisherPublishCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockmalfeasancePublisherPublishCall) DoAndReturn(f func(context.Context, types.NodeID, wire.Proof) error) *MockmalfeasancePublisherPublishCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // MockatxProvider is a mock of atxProvider interface. type MockatxProvider struct { ctrl *gomock.Controller @@ -1874,41 +1935,40 @@ func (c *MockcertifierServiceCertificateCall) DoAndReturn(f func(context.Context return c } -// Recertify mocks base method. -func (m *MockcertifierService) Recertify(ctx context.Context, id types.NodeID, certifierAddress *url.URL, pubkey []byte) (*certifier.PoetCert, error) { +// DeleteCertificate mocks base method. +func (m *MockcertifierService) DeleteCertificate(id types.NodeID, pubkey []byte) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Recertify", ctx, id, certifierAddress, pubkey) - ret0, _ := ret[0].(*certifier.PoetCert) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "DeleteCertificate", id, pubkey) + ret0, _ := ret[0].(error) + return ret0 } -// Recertify indicates an expected call of Recertify. -func (mr *MockcertifierServiceMockRecorder) Recertify(ctx, id, certifierAddress, pubkey any) *MockcertifierServiceRecertifyCall { +// DeleteCertificate indicates an expected call of DeleteCertificate. +func (mr *MockcertifierServiceMockRecorder) DeleteCertificate(id, pubkey any) *MockcertifierServiceDeleteCertificateCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recertify", reflect.TypeOf((*MockcertifierService)(nil).Recertify), ctx, id, certifierAddress, pubkey) - return &MockcertifierServiceRecertifyCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCertificate", reflect.TypeOf((*MockcertifierService)(nil).DeleteCertificate), id, pubkey) + return &MockcertifierServiceDeleteCertificateCall{Call: call} } -// MockcertifierServiceRecertifyCall wrap *gomock.Call -type MockcertifierServiceRecertifyCall struct { +// MockcertifierServiceDeleteCertificateCall wrap *gomock.Call +type MockcertifierServiceDeleteCertificateCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockcertifierServiceRecertifyCall) Return(arg0 *certifier.PoetCert, arg1 error) *MockcertifierServiceRecertifyCall { - c.Call = c.Call.Return(arg0, arg1) +func (c *MockcertifierServiceDeleteCertificateCall) Return(arg0 error) *MockcertifierServiceDeleteCertificateCall { + c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockcertifierServiceRecertifyCall) Do(f func(context.Context, types.NodeID, *url.URL, []byte) (*certifier.PoetCert, error)) *MockcertifierServiceRecertifyCall { +func (c *MockcertifierServiceDeleteCertificateCall) Do(f func(types.NodeID, []byte) error) *MockcertifierServiceDeleteCertificateCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockcertifierServiceRecertifyCall) DoAndReturn(f func(context.Context, types.NodeID, *url.URL, []byte) (*certifier.PoetCert, error)) *MockcertifierServiceRecertifyCall { +func (c *MockcertifierServiceDeleteCertificateCall) DoAndReturn(f func(types.NodeID, []byte) error) *MockcertifierServiceDeleteCertificateCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/activation/nipost.go b/activation/nipost.go index 1a7b13d8b6..1682861889 100644 --- a/activation/nipost.go +++ b/activation/nipost.go @@ -12,6 +12,7 @@ import ( "github.com/spacemeshos/poet/shared" postshared "github.com/spacemeshos/post/shared" "go.uber.org/zap" + "golang.org/x/exp/maps" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/activation/metrics" @@ -85,8 +86,7 @@ func NewNIPostBuilder( opts ...NIPostBuilderOption, ) (*NIPostBuilder, error) { b := &NIPostBuilder{ - localDB: db, - + localDB: db, postService: postService, logger: lg, poetCfg: poetCfg, @@ -209,7 +209,7 @@ func (nb *NIPostBuilder) BuildNIPost( poetRoundStart := nb.layerClock.LayerToTime((postChallenge.PublishEpoch - 1).FirstLayer()). Add(nb.poetCfg.PhaseShift) - poetRoundEnd := nb.layerClock.LayerToTime(postChallenge.PublishEpoch.FirstLayer()). + curPoetRoundEnd := nb.layerClock.LayerToTime(postChallenge.PublishEpoch.FirstLayer()). Add(nb.poetCfg.PhaseShift). Add(-nb.poetCfg.CycleGap) @@ -223,41 +223,31 @@ func (nb *NIPostBuilder) BuildNIPost( logger.Info("building nipost", zap.Time("poet round start", poetRoundStart), - zap.Time("poet round end", poetRoundEnd), + zap.Time("poet round end", curPoetRoundEnd), zap.Time("publish epoch end", publishEpochEnd), zap.Uint32("publish epoch", postChallenge.PublishEpoch.Uint32()), ) // Phase 0: Submit challenge to PoET services. - count, err := nipost.PoetRegistrationCount(nb.localDB, signer.NodeID()) - if err != nil { - return nil, fmt.Errorf("failed to get poet registration count: %w", err) - } - if count == 0 { - now := time.Now() - // Deadline: start of PoET round for publish epoch. PoET won't accept registrations after that. - if poetRoundStart.Before(now) { - return nil, fmt.Errorf( - "%w: poet round has already started at %s (now: %s)", - ErrATXChallengeExpired, - poetRoundStart, - now, - ) - } - - submitCtx, cancel := context.WithDeadline(ctx, poetRoundStart) - defer cancel() - err := nb.submitPoetChallenges(submitCtx, signer, poetProofDeadline, challenge.Bytes()) - if err != nil { - return nil, fmt.Errorf("submitting to poets: %w", err) - } - count, err := nipost.PoetRegistrationCount(nb.localDB, signer.NodeID()) - if err != nil { - return nil, fmt.Errorf("failed to get poet registration count: %w", err) - } - if count == 0 { - return nil, &PoetSvcUnstableError{msg: "failed to submit challenge to any PoET", source: submitCtx.Err()} - } + // Deadline: start of PoET round: we will not accept registrations after that + submittedRegistrations, err := nb.submitPoetChallenges( + ctx, + signer, + poetProofDeadline, + poetRoundStart, challenge.Bytes(), + ) + regErr := &PoetRegistrationMismatchError{} + switch { + case errors.As(err, ®Err): + logger.Fatal( + "None of the poets listed in the config matches the existing registrations. "+ + "Verify your config and local database state.", + zap.Strings("registrations", regErr.registrations), + zap.Strings("configured_poets", regErr.configuredPoets), + ) + return nil, err + case err != nil: + return nil, fmt.Errorf("submitting to poets: %w", err) } // Phase 1: query PoET services for proofs @@ -279,8 +269,8 @@ func (nb *NIPostBuilder) BuildNIPost( ) } - events.EmitPoetWaitProof(signer.NodeID(), postChallenge.PublishEpoch, poetRoundEnd) - poetProofRef, membership, err = nb.getBestProof(ctx, signer.NodeID(), challenge, postChallenge.PublishEpoch) + events.EmitPoetWaitProof(signer.NodeID(), postChallenge.PublishEpoch, curPoetRoundEnd) + poetProofRef, membership, err = nb.getBestProof(ctx, signer.NodeID(), challenge, submittedRegistrations) if err != nil { return nil, &PoetSvcUnstableError{msg: "getBestProof failed", source: err} } @@ -314,13 +304,17 @@ func (nb *NIPostBuilder) BuildNIPost( defer cancel() nb.logger.Info("starting post execution", zap.Binary("challenge", poetProofRef[:])) + startTime := time.Now() proof, postInfo, err := nb.Proof(postCtx, signer.NodeID(), poetProofRef[:], postChallenge) if err != nil { return nil, fmt.Errorf("failed to generate Post: %w", err) } + postGenDuration := time.Since(startTime) + nb.logger.Info("finished post execution", zap.Duration("duration", postGenDuration)) + metrics.PostDuration.Set(float64(postGenDuration.Nanoseconds())) public.PostSeconds.Set(postGenDuration.Seconds()) @@ -354,7 +348,7 @@ func withConditionalTimeout(ctx context.Context, timeout time.Duration) (context return ctx, func() {} } -// Submit the challenge to a single PoET. +// Submit the challenge (register) to a single PoET. func (nb *NIPostBuilder) submitPoetChallenge( ctx context.Context, nodeID types.NodeID, @@ -362,7 +356,7 @@ func (nb *NIPostBuilder) submitPoetChallenge( client PoetService, prefix, challenge []byte, signature types.EdSignature, -) error { +) (nipost.PoETRegistration, error) { logger := nb.logger.With( log.ZContext(ctx), zap.String("poet", client.Address()), @@ -376,64 +370,143 @@ func (nb *NIPostBuilder) submitPoetChallenge( round, err := client.Submit(submitCtx, deadline, prefix, challenge, signature, nodeID) if err != nil { - return &PoetSvcUnstableError{msg: "failed to submit challenge to poet service", source: err} + return nipost.PoETRegistration{}, + &PoetSvcUnstableError{msg: "failed to submit challenge to poet service", source: err} } logger.Info("challenge submitted to poet proving service", zap.String("round", round.ID)) - return nipost.AddPoetRegistration(nb.localDB, nodeID, nipost.PoETRegistration{ + + registration := nipost.PoETRegistration{ ChallengeHash: types.Hash32(challenge), Address: client.Address(), RoundID: round.ID, RoundEnd: round.End, - }) + } + + if err := nipost.AddPoetRegistration(nb.localDB, nodeID, registration); err != nil { + return nipost.PoETRegistration{}, err + } + + return registration, err } -// Submit the challenge to all registered PoETs. +// submitPoetChallenges submit the challenge to registered PoETs +// if some registrations are missing and PoET round didn't start. func (nb *NIPostBuilder) submitPoetChallenges( ctx context.Context, signer *signing.EdSigner, - deadline time.Time, + poetProofDeadline time.Time, + curPoetRoundStartDeadline time.Time, challenge []byte, -) error { - signature := signer.Sign(signing.POET, challenge) - prefix := bytes.Join([][]byte{signer.Prefix(), {byte(signing.POET)}}, nil) +) ([]nipost.PoETRegistration, error) { + // check if some registrations missing or were removed nodeID := signer.NodeID() - g, ctx := errgroup.WithContext(ctx) - errChan := make(chan error, len(nb.poetProvers)) - for _, client := range nb.poetProvers { - g.Go(func() error { - errChan <- nb.submitPoetChallenge(ctx, nodeID, deadline, client, prefix, challenge, signature) - return nil - }) + registrations, err := nipost.PoetRegistrations(nb.localDB, nodeID) + if err != nil { + return nil, fmt.Errorf("failed to get poet registrations from db: %w", err) } - g.Wait() - close(errChan) - allInvalid := true - for err := range errChan { - if err == nil { - allInvalid = false - continue + registrationsMap := make(map[string]nipost.PoETRegistration) + for _, reg := range registrations { + registrationsMap[reg.Address] = reg + } + + existingRegistrationsMap := make(map[string]nipost.PoETRegistration) + var missingRegistrations []PoetService + for addr, poet := range nb.poetProvers { + if val, ok := registrationsMap[addr]; ok { + existingRegistrationsMap[addr] = val + } else { + missingRegistrations = append(missingRegistrations, poet) } + } - nb.logger.Warn("failed to submit challenge to poet", zap.Error(err), log.ZShortStringer("smesherID", nodeID)) - if !errors.Is(err, ErrInvalidRequest) { - allInvalid = false + misconfiguredRegistrations := make(map[string]struct{}) + for addr := range registrationsMap { + if _, ok := existingRegistrationsMap[addr]; !ok { + misconfiguredRegistrations[addr] = struct{}{} } } - if allInvalid { - nb.logger.Warn("all poet submits were too late. ATX challenge expires", log.ZShortStringer("smesherID", nodeID)) - return ErrATXChallengeExpired + + if len(misconfiguredRegistrations) != 0 { + nb.logger.Warn( + "Found existing registrations for poets not listed in the config. Will not fetch proof from them.", + zap.Strings("registrations_addresses", maps.Keys(misconfiguredRegistrations)), + log.ZShortStringer("smesherID", nodeID), + ) } - return nil -} -func (nb *NIPostBuilder) getPoetService(ctx context.Context, address string) PoetService { - for _, service := range nb.poetProvers { - if address == service.Address() { - return service + existingRegistrations := maps.Values(existingRegistrationsMap) + if len(missingRegistrations) == 0 { + return existingRegistrations, nil + } + + now := time.Now() + + if curPoetRoundStartDeadline.Before(now) { + switch { + case len(existingRegistrations) == 0 && len(registrations) == 0: + // no existing registration at all, drop current registration challenge + return nil, fmt.Errorf( + "%w: poet round has already started at %s (now: %s)", + ErrATXChallengeExpired, + curPoetRoundStartDeadline, + now, + ) + case len(existingRegistrations) == 0: + // no existing registration for given poets set + return nil, &PoetRegistrationMismatchError{ + registrations: maps.Keys(registrationsMap), + configuredPoets: maps.Keys(nb.poetProvers), + } + default: + return existingRegistrations, nil } } - return nil + + // send registrations to missing addresses + signature := signer.Sign(signing.POET, challenge) + prefix := bytes.Join([][]byte{signer.Prefix(), {byte(signing.POET)}}, nil) + + submitCtx, cancel := context.WithDeadline(ctx, curPoetRoundStartDeadline) + defer cancel() + + eg, ctx := errgroup.WithContext(submitCtx) + submittedRegistrationsChan := make(chan nipost.PoETRegistration, len(missingRegistrations)) + + for _, client := range missingRegistrations { + eg.Go(func() error { + registration, err := nb.submitPoetChallenge( + ctx, nodeID, + poetProofDeadline, + client, prefix, challenge, signature, + ) + if err != nil { + nb.logger.Warn("failed to submit challenge to poet", + zap.Error(err), + log.ZShortStringer("smesherID", nodeID), + ) + } else { + submittedRegistrationsChan <- registration + } + return nil + }) + } + + eg.Wait() + close(submittedRegistrationsChan) + + for registration := range submittedRegistrationsChan { + existingRegistrations = append(existingRegistrations, registration) + } + + if len(existingRegistrations) == 0 { + if curPoetRoundStartDeadline.Before(time.Now()) { + return nil, ErrATXChallengeExpired + } + return nil, &PoetSvcUnstableError{msg: "failed to submit challenge to any PoET", source: ctx.Err()} + } + + return existingRegistrations, nil } // membersContainChallenge verifies that the challenge is included in proof's members. @@ -450,16 +523,12 @@ func (nb *NIPostBuilder) getBestProof( ctx context.Context, nodeID types.NodeID, challenge types.Hash32, - publishEpoch types.EpochID, + registrations []nipost.PoETRegistration, ) (types.PoetProofRef, *types.MerkleProof, error) { type poetProof struct { poet *types.PoetProof membership *types.MerkleProof } - registrations, err := nipost.PoetRegistrations(nb.localDB, nodeID) - if err != nil { - return types.PoetProofRef{}, nil, fmt.Errorf("getting poet registrations: %w", err) - } proofs := make(chan *poetProof, len(registrations)) var eg errgroup.Group @@ -470,11 +539,13 @@ func (nb *NIPostBuilder) getBestProof( zap.String("poet_address", r.Address), zap.String("round", r.RoundID), ) - client := nb.getPoetService(ctx, r.Address) - if client == nil { + + client, ok := nb.poetProvers[r.Address] + if !ok { logger.Warn("poet client not found") continue } + round := r.RoundID waitDeadline := proofDeadline(r.RoundEnd, nb.poetCfg.CycleGap) eg.Go(func() error { diff --git a/activation/nipost_test.go b/activation/nipost_test.go index a50a211988..24afc65e8d 100644 --- a/activation/nipost_test.go +++ b/activation/nipost_test.go @@ -214,7 +214,7 @@ func Test_NIPost_PostClientHandling(t *testing.T) { }) t.Run("connect, disconnect, then cancel before reconnect", func(t *testing.T) { - // post client connects, starts post, disconnects in between and proofing is canceled before reconnection + // post client connects, starts post, disconnects in between and proving is canceled before reconnection sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -713,12 +713,17 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { ) require.NoError(t, err) - nipst, err := nb.BuildNIPost(context.Background(), sig, challenge, - &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) - require.ErrorIs(t, err, ErrPoetServiceUnstable) + nipst, err := nb.BuildNIPost( + context.Background(), + sig, + challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}, + ) + poetErr := &PoetSvcUnstableError{} + require.ErrorAs(t, err, &poetErr) require.Nil(t, nipst) }) - t.Run("Submit hangs", func(t *testing.T) { + t.Run("Submit hangs, no registrations submitted", func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) mclock := defaultLayerClockMock(ctrl) @@ -750,9 +755,13 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { ) require.NoError(t, err) - nipst, err := nb.BuildNIPost(context.Background(), sig, challenge, - &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) - require.ErrorIs(t, err, ErrPoetServiceUnstable) + nipst, err := nb.BuildNIPost( + context.Background(), + sig, + challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}, + ) + require.ErrorIs(t, err, ErrATXChallengeExpired) require.Nil(t, nipst) }) t.Run("GetProof fails", func(t *testing.T) { @@ -807,6 +816,302 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { }) } +// TestNIPoSTBuilder_PoETConfigChange checks if +// it properly detects added/deleted PoET services and re-registers if needed. +func TestNIPoSTBuilder_PoETConfigChange(t *testing.T) { + t.Parallel() + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + challenge := types.NIPostChallenge{ + PublishEpoch: postGenesisEpoch + 2, + } + + challengeHash := wire.NIPostChallengeToWireV1(&challenge).Hash() + + const ( + poetProverAddr = "http://localhost:9999" + poetProverAddr2 = "http://localhost:9988" + ) + + t.Run("1 poet deleted BEFORE round started -> continue with submitted registration", func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + poet := NewMockPoetService(ctrl) + poet.EXPECT(). + Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + Return(&types.PoetRound{}, nil) + poet.EXPECT().Address().Return(poetProverAddr).AnyTimes() + + // successfully registered to 2 poets + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr2, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + + nb, err := NewNIPostBuilder( + db, + nil, + zaptest.NewLogger(t), + PoetConfig{}, + nil, + nil, + WithPoetServices(poet), // add only 1 poet prover + ) + require.NoError(t, err) + + existingRegistrations, err := nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(5*time.Second), + challengeHash.Bytes()) + + require.NoError(t, err) + require.Len(t, existingRegistrations, 1) + require.Equal(t, poetProverAddr, existingRegistrations[0].Address) + }) + + t.Run("1 poet added BEFORE round started -> register to missing poet", func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + poetProver := NewMockPoetService(ctrl) + poetProver.EXPECT().Address().Return(poetProverAddr).AnyTimes() + + addedPoetProver := NewMockPoetService(ctrl) + addedPoetProver.EXPECT(). + Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(&types.PoetRound{}, nil) + addedPoetProver.EXPECT().Address().Return(poetProverAddr2).AnyTimes() + + // successfully registered to 1 poet + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + // successful post exec + nb, err := NewNIPostBuilder( + db, + nil, + zaptest.NewLogger(t), + PoetConfig{}, + nil, + nil, + WithPoetServices(poetProver, addedPoetProver), // add both poet provers + ) + require.NoError(t, err) + + existingRegistrations, err := nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(5*time.Second), + challengeHash.Bytes()) + + require.NoError(t, err) + require.Len(t, existingRegistrations, 2) + require.Equal(t, poetProverAddr, existingRegistrations[0].Address) + require.Equal(t, poetProverAddr2, existingRegistrations[1].Address) + }) + + t.Run("completely changed poet service BEFORE round started -> register new poet", func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + addedPoetProver := NewMockPoetService(ctrl) + addedPoetProver.EXPECT(). + Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(&types.PoetRound{}, nil) + addedPoetProver.EXPECT().Address().Return(poetProverAddr2).AnyTimes() + + // successfully registered to removed poet + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + nb, err := NewNIPostBuilder( + db, + nil, + zaptest.NewLogger(t), + PoetConfig{}, + nil, + nil, + WithPoetServices(addedPoetProver), // add new poet + ) + require.NoError(t, err) + + existingRegistrations, err := nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(5*time.Second), + challengeHash.Bytes()) + + require.NoError(t, err) + require.Len(t, existingRegistrations, 1) + require.Equal(t, poetProverAddr2, existingRegistrations[0].Address) + }) + + t.Run("1 poet added AFTER round started -> too late to register to added poet", + func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + poetProver := NewMockPoetService(ctrl) + poetProver.EXPECT().Address().Return(poetProverAddr).AnyTimes() + + addedPoetProver := NewMockPoetService(ctrl) + addedPoetProver.EXPECT().Address().Return(poetProverAddr2).AnyTimes() + + // successfully registered to 1 poet + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + // successful post exec + nb, err := NewNIPostBuilder( + db, + nil, + zaptest.NewLogger(t), + PoetConfig{}, + nil, + nil, + WithPoetServices(poetProver, addedPoetProver), + ) + require.NoError(t, err) + + existingRegistrations, err := nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(-5*time.Second), // poet round started + challengeHash.Bytes()) + + require.NoError(t, err) + require.Len(t, existingRegistrations, 1) + require.Equal(t, poetProverAddr, existingRegistrations[0].Address) + }) + + t.Run("1 poet removed AFTER round started -> too late to register to added poet", + func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + poetProver := NewMockPoetService(ctrl) + poetProver.EXPECT().Address().Return(poetProverAddr).AnyTimes() + + addedPoetProver := NewMockPoetService(ctrl) + addedPoetProver.EXPECT().Address().Return(poetProverAddr2).AnyTimes() + + // successfully registered to 2 poets + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr2, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + + nb, err := NewNIPostBuilder( + db, + nil, + zaptest.NewLogger(t), + PoetConfig{}, + nil, + nil, + WithPoetServices(poetProver), + ) + require.NoError(t, err) + + existingRegistrations, err := nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(-5*time.Second), // poet round started + challengeHash.Bytes()) + + require.NoError(t, err) + require.Len(t, existingRegistrations, 1) + require.Equal(t, poetProverAddr, existingRegistrations[0].Address) + }) + + t.Run("completely changed poet service AFTER round started -> fail, too late to register again", + func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + poetProver := NewMockPoetService(ctrl) + poetProver.EXPECT().Address().Return(poetProverAddr).AnyTimes() + + // successfully registered to removed poet + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr2, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + logger := zaptest.NewLogger(t) + + nb, err := NewNIPostBuilder( + db, + nil, + logger, + PoetConfig{}, + nil, + nil, + WithPoetServices(poetProver), + ) + require.NoError(t, err) + + _, err = nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(-5*time.Second), // poet round started + challengeHash.Bytes(), + ) + poetErr := &PoetRegistrationMismatchError{} + require.ErrorAs(t, err, &poetErr) + require.ElementsMatch(t, poetErr.configuredPoets, []string{poetProverAddr}) + require.ElementsMatch(t, poetErr.registrations, []string{poetProverAddr2}) + }) +} + // TestNIPoSTBuilder_StaleChallenge checks if // it properly detects that the challenge is stale and the poet round has already started. func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { @@ -818,12 +1123,14 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { sig, err := signing.NewEdSigner() require.NoError(t, err) + const poetAddr = "http://localhost:9999" + // Act & Verify t.Run("no requests, poet round started", func(t *testing.T) { ctrl := gomock.NewController(t) mclock := NewMocklayerClock(ctrl) poetProver := NewMockPoetService(ctrl) - poetProver.EXPECT().Address().Return("http://localhost:9999") + poetProver.EXPECT().Address().Return(poetAddr).AnyTimes() mclock.EXPECT().LayerToTime(gomock.Any()).DoAndReturn( func(got types.LayerID) time.Time { return genesis.Add(layerDuration * time.Duration(got)) @@ -852,7 +1159,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { ctrl := gomock.NewController(t) mclock := NewMocklayerClock(ctrl) poetProver := NewMockPoetService(ctrl) - poetProver.EXPECT().Address().Return("http://localhost:9999") + poetProver.EXPECT().Address().Return(poetAddr).AnyTimes() mclock.EXPECT().LayerToTime(gomock.Any()).DoAndReturn( func(got types.LayerID) time.Time { return genesis.Add(layerDuration * time.Duration(got)) @@ -879,7 +1186,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { // successfully registered to at least one poet err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ ChallengeHash: challengeHash, - Address: "http://poet1.com", + Address: poetAddr, RoundID: "1", RoundEnd: time.Now().Add(10 * time.Second), }) @@ -894,7 +1201,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { ctrl := gomock.NewController(t) mclock := NewMocklayerClock(ctrl) poetProver := NewMockPoetService(ctrl) - poetProver.EXPECT().Address().Return("http://localhost:9999") + poetProver.EXPECT().Address().Return(poetAddr).AnyTimes() mclock.EXPECT().LayerToTime(gomock.Any()).DoAndReturn( func(got types.LayerID) time.Time { return genesis.Add(layerDuration * time.Duration(got)) @@ -921,7 +1228,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { // successfully registered to at least one poet err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ ChallengeHash: challengeHash, - Address: "http://poet1.com", + Address: poetAddr, RoundID: "1", RoundEnd: time.Now().Add(10 * time.Second), }) diff --git a/activation/poet.go b/activation/poet.go index 174e3b66c1..af9f1f09a1 100644 --- a/activation/poet.go +++ b/activation/poet.go @@ -21,6 +21,7 @@ import ( "github.com/spacemeshos/go-spacemesh/activation/metrics" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/log" + "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/localsql/certifier" ) @@ -29,7 +30,8 @@ import ( var ( ErrInvalidRequest = errors.New("invalid request") ErrUnauthorized = errors.New("unauthorized") - errCertificatesNotSupported = errors.New("poet doesn't support certificates") + ErrCertificatesNotSupported = errors.New("poet doesn't support certificates") + ErrIncompatiblePhaseShift = errors.New("fetched poet phase_shift is incompatible with configured phase_shift") ) type PoetPowParams struct { @@ -52,7 +54,7 @@ type PoetClient interface { Address() string PowParams(ctx context.Context) (*PoetPowParams, error) - CertifierInfo(ctx context.Context) (*url.URL, []byte, error) + CertifierInfo(ctx context.Context) (*types.CertifierInfo, error) Submit( ctx context.Context, deadline time.Time, @@ -62,6 +64,7 @@ type PoetClient interface { auth PoetAuth, ) (*types.PoetRound, error) Proof(ctx context.Context, roundID string) (*types.PoetProofMessage, []types.Hash32, error) + Info(ctx context.Context) (*types.PoetInfo, error) } // HTTPPoetClient implements PoetProvingServiceClient interface. @@ -114,7 +117,7 @@ func WithLogger(logger *zap.Logger) PoetClientOpts { c.logger = logger c.client.Logger = &retryableHttpLogger{inner: logger} c.client.ResponseLogHook = func(logger retryablehttp.Logger, resp *http.Response) { - c.logger.Info( + c.logger.Debug( "response received", zap.Stringer("url", resp.Request.URL), zap.Int("status", resp.StatusCode), @@ -183,20 +186,15 @@ func (c *HTTPPoetClient) PowParams(ctx context.Context) (*PoetPowParams, error) }, nil } -func (c *HTTPPoetClient) CertifierInfo(ctx context.Context) (*url.URL, []byte, error) { - info, err := c.info(ctx) +func (c *HTTPPoetClient) CertifierInfo(ctx context.Context) (*types.CertifierInfo, error) { + info, err := c.Info(ctx) if err != nil { - return nil, nil, err - } - certifierInfo := info.GetCertifier() - if certifierInfo == nil { - return nil, nil, errCertificatesNotSupported + return nil, err } - url, err := url.Parse(certifierInfo.Url) - if err != nil { - return nil, nil, fmt.Errorf("parsing certifier address: %w", err) + if info.Certifier == nil { + return nil, ErrCertificatesNotSupported } - return url, certifierInfo.Pubkey, nil + return info.Certifier, nil } // Submit registers a challenge in the proving service current open round. @@ -241,12 +239,30 @@ func (c *HTTPPoetClient) Submit( return &types.PoetRound{ID: resBody.RoundId, End: roundEnd}, nil } -func (c *HTTPPoetClient) info(ctx context.Context) (*rpcapi.InfoResponse, error) { +func (c *HTTPPoetClient) Info(ctx context.Context) (*types.PoetInfo, error) { resBody := rpcapi.InfoResponse{} if err := c.req(ctx, http.MethodGet, "/v1/info", nil, &resBody); err != nil { return nil, fmt.Errorf("getting poet info: %w", err) } - return &resBody, nil + + var certifierInfo *types.CertifierInfo + if resBody.GetCertifier() != nil { + url, err := url.Parse(resBody.GetCertifier().Url) + if err != nil { + return nil, fmt.Errorf("parsing certifier address: %w", err) + } + certifierInfo = &types.CertifierInfo{ + Url: url, + Pubkey: resBody.GetCertifier().Pubkey, + } + } + + return &types.PoetInfo{ + ServicePubkey: resBody.ServicePubkey, + PhaseShift: resBody.PhaseShift.AsDuration(), + CycleGap: resBody.CycleGap.AsDuration(), + Certifier: certifierInfo, + }, nil } // Proof implements PoetProvingServiceClient. @@ -308,7 +324,7 @@ func (c *HTTPPoetClient) req(ctx context.Context, method, path string, reqBody, } if res.StatusCode != http.StatusOK { - c.logger.Info("got poet response != 200 OK", zap.String("status", res.Status), zap.String("body", string(data))) + c.logger.Debug("poet request failed", zap.String("status", res.Status), zap.String("body", string(data))) } switch res.StatusCode { @@ -331,10 +347,25 @@ func (c *HTTPPoetClient) req(ctx context.Context, method, path string, reqBody, return nil } -type certifierInfo struct { - obtained time.Time - url *url.URL - pubkey []byte +type cachedData[T any] struct { + mu sync.Mutex + data T + exp time.Time + ttl time.Duration +} + +func (c *cachedData[T]) get(init func() (T, error)) (T, error) { + c.mu.Lock() + defer c.mu.Unlock() + if time.Now().Before(c.exp) { + return c.data, nil + } + d, err := init() + if err == nil { + c.data = d + c.exp = time.Now().Add(c.ttl) + } + return d, err } // poetService is a higher-level interface to communicate with a PoET service. @@ -352,9 +383,11 @@ type poetService struct { certifier certifierService - certifierInfoTTL time.Duration - certifierInfo certifierInfo - certifierInfoMutex sync.Mutex + certifierInfoCache cachedData[*types.CertifierInfo] + mtx sync.Mutex + expectedPhaseShift time.Duration + fetchedPhaseShift time.Duration + powParamsCache cachedData[*PoetPowParams] } type PoetServiceOpt func(*poetService) @@ -374,7 +407,7 @@ func NewPoetService( ) (*poetService, error) { client, err := NewHTTPPoetClient(server, cfg, WithLogger(logger)) if err != nil { - return nil, fmt.Errorf("creating HTTP poet client %s: %w", server.Address, err) + return nil, err } return NewPoetServiceWithClient( db, @@ -392,20 +425,50 @@ func NewPoetServiceWithClient( logger *zap.Logger, opts ...PoetServiceOpt, ) *poetService { - poetClient := &poetService{ - db: db, - logger: logger, - client: client, - requestTimeout: cfg.RequestTimeout, - certifierInfoTTL: cfg.CertifierInfoCacheTTL, - proofMembers: make(map[string][]types.Hash32, 1), + service := &poetService{ + db: db, + logger: logger, + client: client, + requestTimeout: cfg.RequestTimeout, + certifierInfoCache: cachedData[*types.CertifierInfo]{ttl: cfg.CertifierInfoCacheTTL}, + powParamsCache: cachedData[*PoetPowParams]{ttl: cfg.PowParamsCacheTTL}, + proofMembers: make(map[string][]types.Hash32, 1), + expectedPhaseShift: cfg.PhaseShift, } - for _, opt := range opts { - opt(poetClient) + opt(service) + } + + err := service.verifyPhaseShiftConfiguration(context.Background()) + switch { + case errors.Is(err, ErrIncompatiblePhaseShift): + logger.Fatal("failed to create poet service", zap.String("poet", client.Address())) + return nil + case err != nil: + logger.Warn("failed to fetch poet phase shift", + zap.String("poet", client.Address()), + zap.Error(err), + ) + } + return service +} + +func (c *poetService) verifyPhaseShiftConfiguration(ctx context.Context) error { + c.mtx.Lock() + defer c.mtx.Unlock() + + if c.fetchedPhaseShift != 0 { + return nil + } + resp, err := c.client.Info(ctx) + if err != nil { + return err + } else if resp.PhaseShift != c.expectedPhaseShift { + return ErrIncompatiblePhaseShift } - return poetClient + c.fetchedPhaseShift = resp.PhaseShift + return nil } func (c *poetService) Address() string { @@ -423,18 +486,18 @@ func (c *poetService) authorize( switch { case err == nil: return &PoetAuth{PoetCert: cert}, nil - case errors.Is(err, errCertificatesNotSupported): + case errors.Is(err, ErrCertificatesNotSupported): logger.Debug("poet doesn't support certificates") default: logger.Warn("failed to certify", zap.Error(err)) } // Fallback to PoW // TODO: remove this fallback once we migrate to certificates fully. + logger.Info("falling back to PoW authorization") - logger.Debug("querying for poet pow parameters") powCtx, cancel := withConditionalTimeout(ctx, c.requestTimeout) defer cancel() - powParams, err := c.client.PowParams(powCtx) + powParams, err := c.powParams(powCtx) if err != nil { return nil, &PoetSvcUnstableError{msg: "failed to get PoW params", source: err} } @@ -459,6 +522,21 @@ func (c *poetService) authorize( }}, nil } +func (c *poetService) reauthorize( + ctx context.Context, + id types.NodeID, + challenge []byte, +) (*PoetAuth, error) { + if c.certifier != nil { + if info, err := c.getCertifierInfo(ctx); err == nil { + if err := c.certifier.DeleteCertificate(id, info.Pubkey); err != nil { + return nil, fmt.Errorf("deleting cert: %w", err) + } + } + } + return c.authorize(ctx, id, challenge, c.logger) +} + func (c *poetService) Submit( ctx context.Context, deadline time.Time, @@ -472,7 +550,16 @@ func (c *poetService) Submit( log.ZShortStringer("smesherID", nodeID), ) - // Try obtain a certificate + err := c.verifyPhaseShiftConfiguration(ctx) + switch { + case errors.Is(err, ErrIncompatiblePhaseShift): + logger.Fatal("failed to submit challenge", zap.String("poet", c.client.Address())) + return nil, err + case err != nil: + return nil, err + } + + // Try to obtain a certificate auth, err := c.authorize(ctx, nodeID, challenge, logger) if err != nil { return nil, fmt.Errorf("authorizing: %w", err) @@ -487,10 +574,10 @@ func (c *poetService) Submit( case err == nil: return round, nil case errors.Is(err, ErrUnauthorized): - logger.Warn("failed to submit challenge as unathorized - recertifying", zap.Error(err)) - auth.PoetCert, err = c.recertify(ctx, nodeID) + logger.Warn("failed to submit challenge as unauthorized - authorizing again", zap.Error(err)) + auth, err := c.reauthorize(ctx, nodeID, challenge) if err != nil { - return nil, fmt.Errorf("recertifying: %w", err) + return nil, fmt.Errorf("authorizing: %w", err) } return c.client.Submit(submitCtx, deadline, prefix, challenge, signature, nodeID, *auth) } @@ -518,7 +605,7 @@ func (c *poetService) Proof(ctx context.Context, roundID string) (*types.PoetPro return nil, nil, fmt.Errorf("getting proof: %w", err) } - if err := c.db.ValidateAndStore(ctx, proof); err != nil && !errors.Is(err, ErrObjectExists) { + if err := c.db.ValidateAndStore(ctx, proof); err != nil && !errors.Is(err, sql.ErrObjectExists) { c.logger.Warn("failed to validate and store proof", zap.Error(err), zap.Object("proof", proof)) return nil, nil, fmt.Errorf("validating and storing proof: %w", err) } @@ -532,38 +619,29 @@ func (c *poetService) Certify(ctx context.Context, id types.NodeID) (*certifier. if c.certifier == nil { return nil, errors.New("certifier not configured") } - url, pubkey, err := c.getCertifierInfo(ctx) + info, err := c.getCertifierInfo(ctx) if err != nil { return nil, err } - return c.certifier.Certificate(ctx, id, url, pubkey) + return c.certifier.Certificate(ctx, id, info.Url, info.Pubkey) } -func (c *poetService) recertify(ctx context.Context, id types.NodeID) (*certifier.PoetCert, error) { - if c.certifier == nil { - return nil, errors.New("certifier not configured") - } - url, pubkey, err := c.getCertifierInfo(ctx) +func (c *poetService) getCertifierInfo(ctx context.Context) (*types.CertifierInfo, error) { + info, err := c.certifierInfoCache.get(func() (*types.CertifierInfo, error) { + certifierInfo, err := c.client.CertifierInfo(ctx) + if err != nil { + return nil, fmt.Errorf("getting certifier info: %w", err) + } + return certifierInfo, nil + }) if err != nil { return nil, err } - return c.certifier.Recertify(ctx, id, url, pubkey) + return info, nil } -func (c *poetService) getCertifierInfo(ctx context.Context) (*url.URL, []byte, error) { - c.certifierInfoMutex.Lock() - defer c.certifierInfoMutex.Unlock() - if time.Since(c.certifierInfo.obtained) < c.certifierInfoTTL { - return c.certifierInfo.url, c.certifierInfo.pubkey, nil - } - url, pubkey, err := c.client.CertifierInfo(ctx) - if err != nil { - return nil, nil, fmt.Errorf("getting certifier info: %w", err) - } - c.certifierInfo = certifierInfo{ - obtained: time.Now(), - url: url, - pubkey: pubkey, - } - return c.certifierInfo.url, c.certifierInfo.pubkey, nil +func (c *poetService) powParams(ctx context.Context) (*PoetPowParams, error) { + return c.powParamsCache.get(func() (*PoetPowParams, error) { + return c.client.PowParams(ctx) + }) } diff --git a/activation/poet_client_test.go b/activation/poet_client_test.go index 9b9d591053..866348d9b6 100644 --- a/activation/poet_client_test.go +++ b/activation/poet_client_test.go @@ -1,7 +1,9 @@ package activation import ( + "bytes" "context" + "errors" "io" "net/http" "net/http/httptest" @@ -14,6 +16,7 @@ import ( "github.com/spacemeshos/poet/server" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap" "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" "google.golang.org/protobuf/encoding/protojson" @@ -25,7 +28,6 @@ import ( ) func Test_HTTPPoetClient_ParsesURL(t *testing.T) { - t.Parallel() cfg := server.DefaultRoundConfig() t.Run("add http if missing", func(t *testing.T) { @@ -50,7 +52,6 @@ func Test_HTTPPoetClient_ParsesURL(t *testing.T) { } func Test_HTTPPoetClient_Submit(t *testing.T) { - t.Parallel() mux := http.NewServeMux() mux.HandleFunc("POST /v1/submit", func(w http.ResponseWriter, r *http.Request) { resp, err := protojson.Marshal(&rpcapi.SubmitResponse{}) @@ -83,7 +84,6 @@ func Test_HTTPPoetClient_Submit(t *testing.T) { } func Test_HTTPPoetClient_Address(t *testing.T) { - t.Parallel() t.Run("with scheme", func(t *testing.T) { t.Parallel() client, err := NewHTTPPoetClient(types.PoetServer{Address: "https://poet-address"}, PoetConfig{}) @@ -101,7 +101,6 @@ func Test_HTTPPoetClient_Address(t *testing.T) { } func Test_HTTPPoetClient_Address_Mainnet(t *testing.T) { - t.Parallel() poetCfg := server.DefaultRoundConfig() poETServers := []string{ @@ -124,7 +123,6 @@ func Test_HTTPPoetClient_Address_Mainnet(t *testing.T) { } func Test_HTTPPoetClient_Proof(t *testing.T) { - t.Parallel() mux := http.NewServeMux() mux.HandleFunc("GET /v1/proofs/1", func(w http.ResponseWriter, r *http.Request) { resp, err := protojson.Marshal(&rpcapi.ProofResponse{}) @@ -149,8 +147,6 @@ func Test_HTTPPoetClient_Proof(t *testing.T) { } func TestPoetClient_CachesProof(t *testing.T) { - t.Parallel() - var proofsCalled atomic.Uint64 mux := http.NewServeMux() mux.HandleFunc("GET /v1/proofs/", func(w http.ResponseWriter, r *http.Request) { @@ -195,28 +191,27 @@ func TestPoetClient_CachesProof(t *testing.T) { } func TestPoetClient_QueryProofTimeout(t *testing.T) { - t.Parallel() - - block := make(chan struct{}) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - <-block - })) - defer ts.Close() - defer close(block) - - server := types.PoetServer{ - Address: ts.URL, - Pubkey: types.NewBase64Enc([]byte("pubkey")), - } cfg := PoetConfig{ RequestTimeout: time.Millisecond * 100, + PhaseShift: 10 * time.Second, } - client, err := NewHTTPPoetClient(server, cfg, withCustomHttpClient(ts.Client())) - require.NoError(t, err) + client := NewMockPoetClient(gomock.NewController(t)) + // first call on info returns the expected value + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{ + PhaseShift: cfg.PhaseShift, + }, nil) poet := NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) + // any additional call on Info will block + client.EXPECT().Proof(gomock.Any(), "1").DoAndReturn( + func(ctx context.Context, _ string) (*types.PoetProofMessage, []types.Hash32, error) { + <-ctx.Done() + return nil, nil, ctx.Err() + }, + ).AnyTimes() + start := time.Now() - eg := errgroup.Group{} + var eg errgroup.Group for range 50 { eg.Go(func() error { _, _, err := poet.Proof(context.Background(), "1") @@ -229,8 +224,6 @@ func TestPoetClient_QueryProofTimeout(t *testing.T) { } func TestPoetClient_Certify(t *testing.T) { - t.Parallel() - sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -271,8 +264,6 @@ func TestPoetClient_Certify(t *testing.T) { } func TestPoetClient_ObtainsCertOnSubmit(t *testing.T) { - t.Parallel() - sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -316,8 +307,6 @@ func TestPoetClient_ObtainsCertOnSubmit(t *testing.T) { } func TestPoetClient_RecertifiesOnAuthFailure(t *testing.T) { - t.Parallel() - sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -366,8 +355,9 @@ func TestPoetClient_RecertifiesOnAuthFailure(t *testing.T) { mCertifier.EXPECT(). Certificate(gomock.Any(), sig.NodeID(), certifierAddress, certifierPubKey). Return(&certifier.PoetCert{Data: []byte("first")}, nil), + mCertifier.EXPECT().DeleteCertificate(sig.NodeID(), certifierPubKey), mCertifier.EXPECT(). - Recertify(gomock.Any(), sig.NodeID(), certifierAddress, certifierPubKey). + Certificate(gomock.Any(), sig.NodeID(), certifierAddress, certifierPubKey). Return(&certifier.PoetCert{Data: []byte("second")}, nil), ) @@ -382,8 +372,80 @@ func TestPoetClient_RecertifiesOnAuthFailure(t *testing.T) { require.EqualValues(t, "second", <-certs) } +func TestPoetClient_FallbacksToPowWhenCannotRecertify(t *testing.T) { + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + certifierAddress := &url.URL{Scheme: "http", Host: "certifier"} + certifierPubKey := []byte("certifier-pubkey") + + mux := http.NewServeMux() + infoResp, err := protojson.Marshal(&rpcapi.InfoResponse{ + ServicePubkey: []byte("pubkey"), + Certifier: &rpcapi.InfoResponse_Cerifier{ + Url: certifierAddress.String(), + Pubkey: certifierPubKey, + }, + }) + require.NoError(t, err) + mux.HandleFunc("GET /v1/info", func(w http.ResponseWriter, r *http.Request) { w.Write(infoResp) }) + + powChallenge := []byte("challenge") + powResp, err := protojson.Marshal(&rpcapi.PowParamsResponse{PowParams: &rpcapi.PowParams{Challenge: powChallenge}}) + require.NoError(t, err) + mux.HandleFunc("GET /v1/pow_params", func(w http.ResponseWriter, r *http.Request) { w.Write(powResp) }) + + submitResp, err := protojson.Marshal(&rpcapi.SubmitResponse{}) + require.NoError(t, err) + submitCount := 0 + mux.HandleFunc("POST /v1/submit", func(w http.ResponseWriter, r *http.Request) { + req := rpcapi.SubmitRequest{} + body, _ := io.ReadAll(r.Body) + protojson.Unmarshal(body, &req) + + switch { + case submitCount == 0: + w.WriteHeader(http.StatusUnauthorized) + case submitCount == 1 && req.Certificate == nil && bytes.Equal(req.PowParams.Challenge, powChallenge): + w.Write(submitResp) + default: + w.WriteHeader(http.StatusUnauthorized) + } + submitCount++ + }) + + ts := httptest.NewServer(mux) + defer ts.Close() + + server := types.PoetServer{ + Address: ts.URL, + Pubkey: types.NewBase64Enc([]byte("pubkey")), + } + cfg := PoetConfig{CertifierInfoCacheTTL: time.Hour} + + ctrl := gomock.NewController(t) + mCertifier := NewMockcertifierService(ctrl) + gomock.InOrder( + mCertifier.EXPECT(). + Certificate(gomock.Any(), sig.NodeID(), certifierAddress, certifierPubKey). + Return(&certifier.PoetCert{Data: []byte("first")}, nil), + mCertifier.EXPECT().DeleteCertificate(sig.NodeID(), certifierPubKey), + mCertifier.EXPECT(). + Certificate(gomock.Any(), sig.NodeID(), certifierAddress, certifierPubKey). + Return(nil, errors.New("cannot recertify")), + ) + + client, err := NewHTTPPoetClient(server, cfg, withCustomHttpClient(ts.Client())) + require.NoError(t, err) + + poet := NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t), WithCertifier(mCertifier)) + + _, err = poet.Submit(context.Background(), time.Time{}, nil, nil, types.RandomEdSignature(), sig.NodeID()) + require.NoError(t, err) + require.Equal(t, 2, submitCount) +} + func TestPoetService_CachesCertifierInfo(t *testing.T) { - t.Parallel() type test struct { name string ttl time.Duration @@ -398,19 +460,174 @@ func TestPoetService_CachesCertifierInfo(t *testing.T) { cfg.CertifierInfoCacheTTL = tc.ttl client := NewMockPoetClient(gomock.NewController(t)) db := NewPoetDb(statesql.InMemory(), zaptest.NewLogger(t)) + + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{}, nil) + poet := NewPoetServiceWithClient(db, client, cfg, zaptest.NewLogger(t)) + url := &url.URL{Host: "certifier.hello"} pubkey := []byte("pubkey") - exp := client.EXPECT().CertifierInfo(gomock.Any()).Return(url, pubkey, nil) + exp := client.EXPECT().CertifierInfo(gomock.Any()). + Return(&types.CertifierInfo{Url: url, Pubkey: pubkey}, nil) + if tc.ttl == 0 { + exp.Times(5) + } + for range 5 { + info, err := poet.getCertifierInfo(context.Background()) + require.NoError(t, err) + require.Equal(t, url, info.Url) + require.Equal(t, pubkey, info.Pubkey) + } + }) + } +} + +func TestPoetService_CachesPowParams(t *testing.T) { + type test struct { + name string + ttl time.Duration + } + for _, tc := range []test{ + {name: "cache enabled", ttl: time.Hour}, + {name: "cache disabled"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + cfg := DefaultPoetConfig() + cfg.PowParamsCacheTTL = tc.ttl + client := NewMockPoetClient(gomock.NewController(t)) + + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{}, nil) + client.EXPECT().Address().Return("some_address").AnyTimes() + + poet := NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) + + params := PoetPowParams{ + Challenge: types.RandomBytes(10), + Difficulty: 8, + } + exp := client.EXPECT().PowParams(gomock.Any()).Return(¶ms, nil) if tc.ttl == 0 { exp.Times(5) } for range 5 { - gotUrl, gotPubkey, err := poet.getCertifierInfo(context.Background()) + got, err := poet.powParams(context.Background()) require.NoError(t, err) - require.Equal(t, url, gotUrl) - require.Equal(t, pubkey, gotPubkey) + require.Equal(t, params, *got) } }) } } + +func TestPoetService_FetchPoetPhaseShift(t *testing.T) { + t.Parallel() + const phaseShift = time.Second + + t.Run("poet service created: expected and fetched phase shift are matching", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{ + PhaseShift: phaseShift, + }, nil) + + NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) + }) + + t.Run("poet service created: phase shift is not fetched", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(nil, errors.New("some error")) + + NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) + }) + + t.Run("poet service creation failed: expected and fetched phase shift are not matching", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{ + PhaseShift: phaseShift * 2, + }, nil) + + log := zaptest.NewLogger(t).WithOptions(zap.WithFatalHook(calledFatal(t))) + NewPoetServiceWithClient(nil, client, cfg, log) + }) + + t.Run("fetch phase shift before submitting challenge: success", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(nil, errors.New("some error")) + + poet := NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{PhaseShift: phaseShift}, nil) + client.EXPECT().PowParams(gomock.Any()).Return(&PoetPowParams{}, nil) + client.EXPECT(). + Submit( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + gomock.Any(), gomock.Any(), gomock.Any()). + Return(&types.PoetRound{}, nil) + + _, err = poet.Submit(context.Background(), time.Time{}, nil, nil, types.RandomEdSignature(), sig.NodeID()) + require.NoError(t, err) + }) + + t.Run("fetch phase shift before submitting challenge: failed to fetch poet info", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(nil, errors.New("some error")) + + poet := NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + expectedErr := errors.New("some error") + client.EXPECT().Info(gomock.Any()).Return(nil, expectedErr) + + _, err = poet.Submit(context.Background(), time.Time{}, nil, nil, types.RandomEdSignature(), sig.NodeID()) + require.ErrorIs(t, err, expectedErr) + }) + + t.Run("fetch phase shift before submitting challenge: fetched and expected phase shift do not match", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(nil, errors.New("some error")) + + log := zaptest.NewLogger(t).WithOptions(zap.WithFatalHook(calledFatal(t))) + poet := NewPoetServiceWithClient(nil, client, cfg, log) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{ + PhaseShift: phaseShift * 2, + }, nil) + + poet.Submit(context.Background(), time.Time{}, nil, nil, types.RandomEdSignature(), sig.NodeID()) + }) +} diff --git a/activation/poet_mocks.go b/activation/poet_mocks.go index ff746b4de6..885105fef3 100644 --- a/activation/poet_mocks.go +++ b/activation/poet_mocks.go @@ -11,7 +11,6 @@ package activation import ( context "context" - url "net/url" reflect "reflect" time "time" @@ -81,13 +80,12 @@ func (c *MockPoetClientAddressCall) DoAndReturn(f func() string) *MockPoetClient } // CertifierInfo mocks base method. -func (m *MockPoetClient) CertifierInfo(ctx context.Context) (*url.URL, []byte, error) { +func (m *MockPoetClient) CertifierInfo(ctx context.Context) (*types.CertifierInfo, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CertifierInfo", ctx) - ret0, _ := ret[0].(*url.URL) - ret1, _ := ret[1].([]byte) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 + ret0, _ := ret[0].(*types.CertifierInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 } // CertifierInfo indicates an expected call of CertifierInfo. @@ -103,19 +101,19 @@ type MockPoetClientCertifierInfoCall struct { } // Return rewrite *gomock.Call.Return -func (c *MockPoetClientCertifierInfoCall) Return(arg0 *url.URL, arg1 []byte, arg2 error) *MockPoetClientCertifierInfoCall { - c.Call = c.Call.Return(arg0, arg1, arg2) +func (c *MockPoetClientCertifierInfoCall) Return(arg0 *types.CertifierInfo, arg1 error) *MockPoetClientCertifierInfoCall { + c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *MockPoetClientCertifierInfoCall) Do(f func(context.Context) (*url.URL, []byte, error)) *MockPoetClientCertifierInfoCall { +func (c *MockPoetClientCertifierInfoCall) Do(f func(context.Context) (*types.CertifierInfo, error)) *MockPoetClientCertifierInfoCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockPoetClientCertifierInfoCall) DoAndReturn(f func(context.Context) (*url.URL, []byte, error)) *MockPoetClientCertifierInfoCall { +func (c *MockPoetClientCertifierInfoCall) DoAndReturn(f func(context.Context) (*types.CertifierInfo, error)) *MockPoetClientCertifierInfoCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -158,6 +156,45 @@ func (c *MockPoetClientIdCall) DoAndReturn(f func() []byte) *MockPoetClientIdCal return c } +// Info mocks base method. +func (m *MockPoetClient) Info(ctx context.Context) (*types.PoetInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Info", ctx) + ret0, _ := ret[0].(*types.PoetInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Info indicates an expected call of Info. +func (mr *MockPoetClientMockRecorder) Info(ctx any) *MockPoetClientInfoCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockPoetClient)(nil).Info), ctx) + return &MockPoetClientInfoCall{Call: call} +} + +// MockPoetClientInfoCall wrap *gomock.Call +type MockPoetClientInfoCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockPoetClientInfoCall) Return(arg0 *types.PoetInfo, arg1 error) *MockPoetClientInfoCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockPoetClientInfoCall) Do(f func(context.Context) (*types.PoetInfo, error)) *MockPoetClientInfoCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockPoetClientInfoCall) DoAndReturn(f func(context.Context) (*types.PoetInfo, error)) *MockPoetClientInfoCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // PowParams mocks base method. func (m *MockPoetClient) PowParams(ctx context.Context) (*PoetPowParams, error) { m.ctrl.T.Helper() diff --git a/activation/poetdb.go b/activation/poetdb.go index 6ce0f691b5..3905564f89 100644 --- a/activation/poetdb.go +++ b/activation/poetdb.go @@ -19,8 +19,6 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/poets" ) -var ErrObjectExists = sql.ErrObjectExists - // PoetDb is a database for PoET proofs. type PoetDb struct { sqlDB sql.StateDatabase @@ -96,7 +94,6 @@ func (db *PoetDb) Validate( return fmt.Errorf("failed to validate poet proof for poetID %x round %s: %w", shortID, roundID, err) } // TODO(noamnelke): validate signature (or extract public key and use for salting merkle hashes) - return nil } @@ -114,7 +111,7 @@ func (db *PoetDb) StoreProof(ctx context.Context, ref types.PoetProofRef, proofM ) } - db.logger.Info("stored poet proof", + db.logger.Debug("stored poet proof", log.ZContext(ctx), log.ZShortStringer("poet_proof_id", types.Hash32(ref)), zap.String("round_id", proofMessage.RoundID), diff --git a/activation/post_supervisor.go b/activation/post_supervisor.go index 3884e9d6f7..709d008dba 100644 --- a/activation/post_supervisor.go +++ b/activation/post_supervisor.go @@ -277,7 +277,7 @@ func (ps *PostSupervisor) runCmd( ps.logger.Error("start post service", zap.Error(err)) return nil } - ps.logger.Info( + ps.logger.Debug( "post service started", zap.Int("pid", cmd.Process.Pid), zap.String("cmd", cmd.String()), diff --git a/activation/post_supervisor_nix.go b/activation/post_supervisor_nix.go index 637ec40455..bfd2e50af3 100644 --- a/activation/post_supervisor_nix.go +++ b/activation/post_supervisor_nix.go @@ -2,4 +2,4 @@ package activation -const DefaultPostServiceName = "service" +const DefaultPostServiceName = "post-service" diff --git a/activation/post_supervisor_win.go b/activation/post_supervisor_win.go index 99628be55c..a42df38f6f 100644 --- a/activation/post_supervisor_win.go +++ b/activation/post_supervisor_win.go @@ -2,4 +2,4 @@ package activation -const DefaultPostServiceName = "service.exe" +const DefaultPostServiceName = "post-service.exe" diff --git a/activation/post_verifier.go b/activation/post_verifier.go index 4bcf98e152..f72965ef03 100644 --- a/activation/post_verifier.go +++ b/activation/post_verifier.go @@ -246,9 +246,9 @@ func newOffloadingPostVerifier( v.prioritizedIds[id] = struct{}{} } - v.log.Info("starting post verifier") + v.log.Debug("starting post verifier") v.scale(numWorkers) - v.log.Info("started post verifier") + v.log.Debug("started post verifier") return v } @@ -355,18 +355,18 @@ func (v *offloadingPostVerifier) Close() error { return nil default: } - v.log.Info("stopping post verifier") + v.log.Debug("stopping post verifier") close(v.stop) v.eg.Wait() v.verifier.Close() - v.log.Info("stopped post verifier") + v.log.Debug("stopped post verifier") return nil } func (w *postVerifierWorker) start() { - w.log.Info("starting") - defer w.log.Info("stopped") + w.log.Debug("starting") + defer w.log.Debug("stopped") defer close(w.stopped) for { diff --git a/activation/validation.go b/activation/validation.go index 45c1fa3a69..d6d070d895 100644 --- a/activation/validation.go +++ b/activation/validation.go @@ -103,7 +103,7 @@ func (v *Validator) NIPost( err := v.Post(ctx, nodeId, commitmentAtxId, nipost.Post, nipost.PostMetadata, numUnits, opts...) if err != nil { - return 0, fmt.Errorf("invalid Post: %w", err) + return 0, fmt.Errorf("validating Post: %w", err) } var ref types.PoetProofRef @@ -215,7 +215,7 @@ func (v *Validator) Post( start := time.Now() if err := v.postVerifier.Verify(ctx, p, m, callOpts...); err != nil { - return fmt.Errorf("verify PoST: %w", err) + return fmt.Errorf("verifying PoST: %w", err) } metrics.PostVerificationLatency.Observe(time.Since(start).Seconds()) return nil @@ -419,14 +419,14 @@ func (v *Validator) VerifyChain(ctx context.Context, id, goldenATXID types.ATXID for _, opt := range opts { opt(&options) } - options.logger.Info("verifying ATX chain", zap.Stringer("atx_id", id)) + options.logger.Debug("verifying ATX chain", zap.Stringer("atx_id", id)) return v.verifyChainWithOpts(ctx, id, goldenATXID, options) } type atxDeps struct { - nipost types.NIPost + niposts []types.NIPost positioning types.ATXID - previous types.ATXID + previous []types.ATXID commitment types.ATXID } @@ -455,14 +455,16 @@ func (v *Validator) getAtxDeps(ctx context.Context, id types.ATXID) (*atxDeps, e } deps := &atxDeps{ - nipost: *wire.NiPostFromWireV1(atx.NIPost), + niposts: []types.NIPost{*wire.NiPostFromWireV1(atx.NIPost)}, positioning: atx.PositioningATXID, - previous: atx.PrevATXID, commitment: commitment, } + if atx.PrevATXID != types.EmptyATXID { + deps.previous = []types.ATXID{atx.PrevATXID} + } + return deps, nil case types.AtxV2: - // TODO: support merged ATXs var atx wire.ActivationTxV2 if err := codec.Decode(blob.Bytes, &atx); err != nil { return nil, fmt.Errorf("decoding ATX blob: %w", err) @@ -478,23 +480,23 @@ func (v *Validator) getAtxDeps(ctx context.Context, id types.ATXID) (*atxDeps, e } commitment = catx } - var previous types.ATXID - if len(atx.PreviousATXs) != 0 { - previous = atx.PreviousATXs[0] - } deps := &atxDeps{ - nipost: types.NIPost{ - Post: wire.PostFromWireV1(&atx.NiPosts[0].Posts[0].Post), - PostMetadata: &types.PostMetadata{ - Challenge: atx.NiPosts[0].Challenge[:], - LabelsPerUnit: v.cfg.LabelsPerUnit, - }, - }, positioning: atx.PositioningATX, - previous: previous, + previous: atx.PreviousATXs, commitment: commitment, } + for _, nipost := range atx.NiPosts { + for _, post := range nipost.Posts { + deps.niposts = append(deps.niposts, types.NIPost{ + Post: wire.PostFromWireV1(&post.Post), + PostMetadata: &types.PostMetadata{ + Challenge: nipost.Challenge[:], + LabelsPerUnit: v.cfg.LabelsPerUnit, + }, + }) + } + } return deps, nil } @@ -511,12 +513,11 @@ func (v *Validator) verifyChainWithOpts( if err != nil { return fmt.Errorf("get atx: %w", err) } - if atx.Golden() { - log.Debug("not verifying ATX chain", zap.Stringer("atx_id", id), zap.String("reason", "golden")) - return nil - } switch { + case atx.Golden(): + log.Debug("not verifying ATX chain", zap.Stringer("atx_id", id), zap.String("reason", "golden")) + return nil case atx.Validity() == types.Valid: log.Debug("not verifying ATX chain", zap.Stringer("atx_id", id), zap.String("reason", "already verified")) return nil @@ -542,20 +543,21 @@ func (v *Validator) verifyChainWithOpts( if err != nil { return fmt.Errorf("getting ATX dependencies: %w", err) } - - if err := v.Post( - ctx, - atx.SmesherID, - deps.commitment, - deps.nipost.Post, - deps.nipost.PostMetadata, - atx.NumUnits, - []validatorOption{PrioritizeCall()}..., - ); err != nil { - if err := atxs.SetValidity(v.db, id, types.Invalid); err != nil { - log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) + for _, nipost := range deps.niposts { + if err := v.Post( + ctx, + atx.SmesherID, + deps.commitment, + nipost.Post, + nipost.PostMetadata, + atx.NumUnits, + []validatorOption{PrioritizeCall()}..., + ); err != nil { + if err := atxs.SetValidity(v.db, id, types.Invalid); err != nil { + log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) + } + return &InvalidChainError{ID: id, src: err} } - return &InvalidChainError{ID: id, src: err} } err = v.verifyChainDeps(ctx, deps, goldenATXID, opts) @@ -579,9 +581,9 @@ func (v *Validator) verifyChainDeps( goldenATXID types.ATXID, opts verifyChainOpts, ) error { - if deps.previous != types.EmptyATXID { - if err := v.verifyChainWithOpts(ctx, deps.previous, goldenATXID, opts); err != nil { - return fmt.Errorf("validating previous ATX %s chain: %w", deps.previous.ShortString(), err) + for _, prev := range deps.previous { + if err := v.verifyChainWithOpts(ctx, prev, goldenATXID, opts); err != nil { + return fmt.Errorf("validating previous ATX %s chain: %w", prev.ShortString(), err) } } if deps.positioning != goldenATXID { @@ -591,7 +593,7 @@ func (v *Validator) verifyChainDeps( } // verify commitment only if arrived at the first ATX in the chain // to avoid verifying the same commitment ATX multiple times. - if deps.previous == types.EmptyATXID && deps.commitment != goldenATXID { + if len(deps.previous) == 0 && deps.commitment != goldenATXID { if err := v.verifyChainWithOpts(ctx, deps.commitment, goldenATXID, opts); err != nil { return fmt.Errorf("validating commitment ATX %s chain: %w", deps.commitment.ShortString(), err) } diff --git a/activation/validation_test.go b/activation/validation_test.go index 3392594c40..09c5d7c5ed 100644 --- a/activation/validation_test.go +++ b/activation/validation_test.go @@ -616,6 +616,49 @@ func TestVerifyChainDeps(t *testing.T) { err = validator.VerifyChain(ctx, watx.ID(), goldenATXID) require.NoError(t, err) }) + t.Run("merged ATX", func(t *testing.T) { + initialAtx := newInitialATXv1(t, goldenATXID) + initialAtx.Sign(signer) + require.NoError(t, atxs.Add(db, toAtx(t, initialAtx), initialAtx.Blob())) + + // second ID for the merged ATX + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + initialAtx2 := newInitialATXv1(t, goldenATXID) + initialAtx2.Sign(otherSig) + require.NoError(t, atxs.Add(db, toAtx(t, initialAtx2), initialAtx2.Blob())) + + watx := newSoloATXv2(t, initialAtx.PublishEpoch+1, initialAtx.ID(), initialAtx.ID()) + watx.NiPosts[0].Posts = append(watx.NiPosts[0].Posts, wire.SubPostV2{ + MarriageIndex: 1, + PrevATXIndex: 1, + Post: wire.PostV1{ + Nonce: 99, + Pow: 55, + Indices: types.RandomBytes(33), + }, + NumUnits: 77, + }) + watx.PreviousATXs = append(watx.PreviousATXs, initialAtx2.ID()) + watx.Sign(signer) + atx := &types.ActivationTx{ + PublishEpoch: watx.PublishEpoch, + SmesherID: watx.SmesherID, + } + atx.SetID(watx.ID()) + require.NoError(t, atxs.Add(db, atx, watx.Blob())) + + v := NewMockPostVerifier(gomock.NewController(t)) + expectedPost := (*shared.Proof)(wire.PostFromWireV1(&watx.NiPosts[0].Posts[0].Post)) + expectedPost2 := (*shared.Proof)(wire.PostFromWireV1(&watx.NiPosts[0].Posts[1].Post)) + v.EXPECT().Verify(ctx, (*shared.Proof)(initialAtx.NIPost.Post), gomock.Any(), gomock.Any()) + v.EXPECT().Verify(ctx, (*shared.Proof)(initialAtx2.NIPost.Post), gomock.Any(), gomock.Any()) + v.EXPECT().Verify(ctx, expectedPost, gomock.Any(), gomock.Any()) + v.EXPECT().Verify(ctx, expectedPost2, gomock.Any(), gomock.Any()) + validator := NewValidator(db, nil, DefaultPostConfig(), config.ScryptParams{}, v) + err = validator.VerifyChain(ctx, watx.ID(), goldenATXID) + require.NoError(t, err) + }) } func TestVerifyChainDepsAfterCheckpoint(t *testing.T) { diff --git a/activation/verify_state.go b/activation/verify_state.go deleted file mode 100644 index 3300444c84..0000000000 --- a/activation/verify_state.go +++ /dev/null @@ -1,90 +0,0 @@ -package activation - -import ( - "context" - "fmt" - "time" - - "go.uber.org/zap" - - awire "github.com/spacemeshos/go-spacemesh/activation/wire" - "github.com/spacemeshos/go-spacemesh/codec" - "github.com/spacemeshos/go-spacemesh/log" - "github.com/spacemeshos/go-spacemesh/malfeasance/wire" - "github.com/spacemeshos/go-spacemesh/sql" - "github.com/spacemeshos/go-spacemesh/sql/atxs" - "github.com/spacemeshos/go-spacemesh/sql/identities" -) - -func CheckPrevATXs(ctx context.Context, logger *zap.Logger, db sql.Executor) error { - collisions, err := atxs.PrevATXCollisions(db) - if err != nil { - return fmt.Errorf("get prev ATX collisions: %w", err) - } - - logger.Info("found ATX collisions", zap.Int("count", len(collisions))) - count := 0 - for _, collision := range collisions { - select { - case <-ctx.Done(): - // stop on context cancellation - return ctx.Err() - default: - } - - if collision.NodeID1 != collision.NodeID2 { - logger.Panic( - "unexpected collision", - log.ZShortStringer("NodeID1", collision.NodeID1), - log.ZShortStringer("NodeID2", collision.NodeID2), - log.ZShortStringer("ATX1", collision.ATX1), - log.ZShortStringer("ATX2", collision.ATX2), - ) - } - - malicious, err := identities.IsMalicious(db, collision.NodeID1) - if err != nil { - return fmt.Errorf("get malicious status: %w", err) - } - - if malicious { - // already malicious no need to generate proof - continue - } - - // we are ignoring the ATX version, because as of now we only have V1 - // by the time we have V2, this function is not needed anymore and can be deleted - var blob sql.Blob - var atx1 awire.ActivationTxV1 - if _, err := atxs.LoadBlob(ctx, db, collision.ATX1.Bytes(), &blob); err != nil { - return fmt.Errorf("get blob %s: %w", collision.ATX1.ShortString(), err) - } - codec.MustDecode(blob.Bytes, &atx1) - - var atx2 awire.ActivationTxV1 - if _, err := atxs.LoadBlob(ctx, db, collision.ATX2.Bytes(), &blob); err != nil { - return fmt.Errorf("get blob %s: %w", collision.ATX2.ShortString(), err) - } - codec.MustDecode(blob.Bytes, &atx2) - - proof := &wire.MalfeasanceProof{ - Layer: atx1.PublishEpoch.FirstLayer(), - Proof: wire.Proof{ - Type: wire.InvalidPrevATX, - Data: &wire.InvalidPrevATXProof{ - Atx1: atx1, - Atx2: atx2, - }, - }, - } - - encodedProof := codec.MustEncode(proof) - if err := identities.SetMalicious(db, collision.NodeID1, encodedProof, time.Now()); err != nil { - return fmt.Errorf("add malfeasance proof: %w", err) - } - - count++ - } - logger.Info("created malfeasance proofs", zap.Int("count", count)) - return nil -} diff --git a/activation/verify_state_test.go b/activation/verify_state_test.go deleted file mode 100644 index de119a5415..0000000000 --- a/activation/verify_state_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package activation - -import ( - "context" - "math/rand/v2" - "testing" - - "github.com/stretchr/testify/require" - "go.uber.org/zap/zaptest" - - "github.com/spacemeshos/go-spacemesh/activation/wire" - "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/signing" - "github.com/spacemeshos/go-spacemesh/sql/atxs" - "github.com/spacemeshos/go-spacemesh/sql/identities" - "github.com/spacemeshos/go-spacemesh/sql/statesql" -) - -func Test_CheckPrevATXs(t *testing.T) { - db := statesql.InMemory() - logger := zaptest.NewLogger(t) - - // Arrange - sig, err := signing.NewEdSigner() - require.NoError(t, err) - - // create two ATXs with the same PrevATXID - prevATXID := types.RandomATXID() - goldenATXID := types.RandomATXID() - - atx1 := newInitialATXv1(t, goldenATXID, func(atx *wire.ActivationTxV1) { - atx.PrevATXID = prevATXID - atx.PublishEpoch = 2 - }) - atx1.Sign(sig) - vAtx1 := toAtx(t, atx1) - require.NoError(t, atxs.Add(db, vAtx1, atx1.Blob())) - - atx2 := newInitialATXv1(t, goldenATXID, func(atx *wire.ActivationTxV1) { - atx.PrevATXID = prevATXID - atx.PublishEpoch = 3 - }) - atx2.Sign(sig) - vAtx2 := toAtx(t, atx2) - require.NoError(t, atxs.Add(db, vAtx2, atx2.Blob())) - - // create 100 random ATXs that are not malicious - for i := 0; i < 100; i++ { - otherSig, err := signing.NewEdSigner() - require.NoError(t, err) - atx := newInitialATXv1(t, types.RandomATXID(), func(atx *wire.ActivationTxV1) { - atx.PrevATXID = types.RandomATXID() - atx.NumUnits = rand.Uint32() - atx.PublishEpoch = rand.N[types.EpochID](100) - }) - atx.Sign(otherSig) - vAtx := toAtx(t, atx) - require.NoError(t, atxs.Add(db, vAtx, atx.Blob())) - } - - // Act - err = CheckPrevATXs(context.Background(), logger, db) - require.NoError(t, err) - - // Assert - malicious, err := identities.IsMalicious(db, sig.NodeID()) - require.NoError(t, err) - require.True(t, malicious) -} diff --git a/activation/wire/malfeasance.go b/activation/wire/malfeasance.go new file mode 100644 index 0000000000..019a52d6cc --- /dev/null +++ b/activation/wire/malfeasance.go @@ -0,0 +1,60 @@ +package wire + +import ( + "github.com/spacemeshos/go-scale" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" +) + +//go:generate scalegen + +// MerkleTreeIndex is the index of the leaf containing the given field in the merkle tree. +type MerkleTreeIndex uint16 + +const ( + PublishEpochIndex MerkleTreeIndex = iota + PositioningATXIndex + CoinbaseIndex + InitialPostIndex + PreviousATXsRootIndex + NIPostsRootIndex + VRFNonceIndex + MarriagesRootIndex + MarriageATXIndex +) + +// ProofType is an identifier for the type of proof that is encoded in the ATXProof. +type ProofType byte + +const ( + // TODO(mafa): legacy types for future migration to new malfeasance proofs. + LegacyDoublePublish ProofType = 0x00 + LegacyInvalidPost ProofType = 0x01 + LegacyInvalidPrevATX ProofType = 0x02 + + DoublePublish ProofType = 0x10 + DoubleMarry ProofType = 0x11 + DoubleMerge ProofType = 0x12 + InvalidPost ProofType = 0x13 +) + +// ProofVersion is an identifier for the version of the proof that is encoded in the ATXProof. +type ProofVersion byte + +type ATXProof struct { + // Version is the version identifier of the proof. This can be used to extend the ATX proof in the future. + Version ProofVersion + // ProofType is the type of proof that is being provided. + ProofType ProofType + // Proof is the actual proof. Its type depends on the ProofType. + Proof []byte `scale:"max=1048576"` // max size of proof is 1MiB +} + +// Proof is an interface for all types of proofs that can be provided in an ATXProof. +// Generally the proof should be able to validate itself and be scale encoded. +type Proof interface { + scale.Encodable + + Valid(edVerifier *signing.EdVerifier) (types.NodeID, error) +} diff --git a/activation/wire/malfeasance_double_marry.go b/activation/wire/malfeasance_double_marry.go new file mode 100644 index 0000000000..ea946b29ea --- /dev/null +++ b/activation/wire/malfeasance_double_marry.go @@ -0,0 +1,221 @@ +package wire + +import ( + "errors" + "fmt" + "slices" + + "github.com/spacemeshos/merkle-tree" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" +) + +//go:generate scalegen + +// ProofDoubleMarry is a proof that two distinct ATXs contain a marriage certificate signed by the same identity. +// +// We are proving the following: +// 1. The ATXs have different IDs. +// 2. Both ATXs have a valid signature. +// 3. Both ATXs contain a marriage certificate created by the same identity. +// 4. Both marriage certificates have valid signatures. +// +// HINT: this works if the identity that publishes the marriage ATX marries themselves. +type ProofDoubleMarry struct { + // NodeID is the node ID that married twice. + NodeID types.NodeID + + Proofs [2]MarryProof +} + +var _ Proof = &ProofDoubleMarry{} + +func NewDoubleMarryProof(db sql.Executor, atx1, atx2 *ActivationTxV2, nodeID types.NodeID) (*ProofDoubleMarry, error) { + if atx1.ID() == atx2.ID() { + return nil, errors.New("ATXs have the same ID") + } + + proof1, err := createMarryProof(db, atx1, nodeID) + if err != nil { + return nil, fmt.Errorf("proof for atx1: %w", err) + } + + proof2, err := createMarryProof(db, atx2, nodeID) + if err != nil { + return nil, fmt.Errorf("proof for atx2: %w", err) + } + + proof := &ProofDoubleMarry{ + NodeID: nodeID, + Proofs: [2]MarryProof{proof1, proof2}, + } + return proof, nil +} + +func createMarryProof(db sql.Executor, atx *ActivationTxV2, nodeID types.NodeID) (MarryProof, error) { + marriageProof, err := marriageProof(atx) + if err != nil { + return MarryProof{}, fmt.Errorf("failed to create proof for ATX 1: %w", err) + } + + marriageIndex := slices.IndexFunc(atx.Marriages, func(cert MarriageCertificate) bool { + if cert.ReferenceAtx == types.EmptyATXID && atx.SmesherID == nodeID { + // special case of the self signed certificate of the ATX publisher + return true + } + refATX, err := atxs.Get(db, cert.ReferenceAtx) + if err != nil { + return false + } + return refATX.SmesherID == nodeID + }) + if marriageIndex == -1 { + return MarryProof{}, fmt.Errorf("does not contain a marriage certificate signed by %s", nodeID.ShortString()) + } + certProof, err := certificateProof(atx.Marriages, uint64(marriageIndex)) + if err != nil { + return MarryProof{}, fmt.Errorf("failed to create certificate proof for ATX 1: %w", err) + } + + proof := MarryProof{ + ATXID: atx.ID(), + + MarriageRoot: types.Hash32(atx.Marriages.Root()), + MarriageProof: marriageProof, + + CertificateReference: atx.Marriages[marriageIndex].ReferenceAtx, + CertificateSignature: atx.Marriages[marriageIndex].Signature, + CertificateIndex: uint64(marriageIndex), + CertificateProof: certProof, + + SmesherID: atx.SmesherID, + Signature: atx.Signature, + } + return proof, nil +} + +func marriageProof(atx *ActivationTxV2) ([]types.Hash32, error) { + tree, err := merkle.NewTreeBuilder(). + WithLeavesToProve(map[uint64]bool{uint64(MarriagesRootIndex): true}). + WithHashFunc(atxTreeHash). + Build() + if err != nil { + return nil, err + } + atx.merkleTree(tree) + proof := tree.Proof() + + proofHashes := make([]types.Hash32, len(proof)) + for i, p := range proof { + proofHashes[i] = types.Hash32(p) + } + return proofHashes, nil +} + +func certificateProof(certs MarriageCertificates, index uint64) ([]types.Hash32, error) { + tree, err := merkle.NewTreeBuilder(). + WithLeavesToProve(map[uint64]bool{index: true}). + WithHashFunc(atxTreeHash). + Build() + if err != nil { + return nil, err + } + certs.merkleTree(tree) + proof := tree.Proof() + + proofHashes := make([]types.Hash32, len(proof)) + for i, p := range proof { + proofHashes[i] = types.Hash32(p) + } + return proofHashes, nil +} + +func (p ProofDoubleMarry) Valid(edVerifier *signing.EdVerifier) (types.NodeID, error) { + if p.Proofs[0].ATXID == p.Proofs[1].ATXID { + return types.EmptyNodeID, errors.New("proofs have the same ATX ID") + } + + if err := p.Proofs[0].Valid(edVerifier, p.NodeID); err != nil { + return types.EmptyNodeID, fmt.Errorf("proof 1 is invalid: %w", err) + } + if err := p.Proofs[1].Valid(edVerifier, p.NodeID); err != nil { + return types.EmptyNodeID, fmt.Errorf("proof 2 is invalid: %w", err) + } + return p.NodeID, nil +} + +type MarryProof struct { + // ATXID is the ID of the ATX being proven. + ATXID types.ATXID + + // MarriageRoot and its proof that it is contained in the ATX. + MarriageRoot types.Hash32 + MarriageProof []types.Hash32 `scale:"max=32"` + + // The signature of the certificate and the proof that the certificate is contained in the MarriageRoot at + // the given index. + CertificateReference types.ATXID + CertificateSignature types.EdSignature + CertificateIndex uint64 + CertificateProof []types.Hash32 `scale:"max=32"` + + // SmesherID is the ID of the smesher that published the ATX. + SmesherID types.NodeID + // Signature is the signature of the ATXID by the smesher. + Signature types.EdSignature +} + +func (p MarryProof) Valid(edVerifier *signing.EdVerifier, nodeID types.NodeID) error { + if !edVerifier.Verify(signing.ATX, p.SmesherID, p.ATXID.Bytes(), p.Signature) { + return errors.New("invalid ATX signature") + } + + if !edVerifier.Verify(signing.MARRIAGE, nodeID, p.SmesherID.Bytes(), p.CertificateSignature) { + return errors.New("invalid certificate signature") + } + + proof := make([][]byte, len(p.MarriageProof)) + for i, h := range p.MarriageProof { + proof[i] = h.Bytes() + } + ok, err := merkle.ValidatePartialTree( + []uint64{uint64(MarriagesRootIndex)}, + [][]byte{p.MarriageRoot.Bytes()}, + proof, + p.ATXID.Bytes(), + atxTreeHash, + ) + if err != nil { + return fmt.Errorf("validate marriage proof: %w", err) + } + if !ok { + return errors.New("invalid marriage proof") + } + + mc := MarriageCertificate{ + ReferenceAtx: p.CertificateReference, + Signature: p.CertificateSignature, + } + + certProof := make([][]byte, len(p.CertificateProof)) + for i, h := range p.CertificateProof { + certProof[i] = h.Bytes() + } + ok, err = merkle.ValidatePartialTree( + []uint64{p.CertificateIndex}, + [][]byte{mc.Root()}, + certProof, + p.MarriageRoot.Bytes(), + atxTreeHash, + ) + if err != nil { + return fmt.Errorf("validate certificate proof: %w", err) + } + if !ok { + return errors.New("invalid certificate proof") + } + return nil +} diff --git a/activation/wire/malfeasance_double_marry_scale.go b/activation/wire/malfeasance_double_marry_scale.go new file mode 100644 index 0000000000..03f70c95fa --- /dev/null +++ b/activation/wire/malfeasance_double_marry_scale.go @@ -0,0 +1,182 @@ +// Code generated by github.com/spacemeshos/go-scale/scalegen. DO NOT EDIT. + +// nolint +package wire + +import ( + "github.com/spacemeshos/go-scale" + "github.com/spacemeshos/go-spacemesh/common/types" +) + +func (t *ProofDoubleMarry) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeByteArray(enc, t.NodeID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructArray(enc, t.Proofs[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *ProofDoubleMarry) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := scale.DecodeByteArray(dec, t.NodeID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeStructArray(dec, t.Proofs[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *MarryProof) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeByteArray(enc, t.ATXID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.MarriageRoot[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.MarriageProof, 32) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.CertificateReference[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.CertificateSignature[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeCompact64(enc, uint64(t.CertificateIndex)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.CertificateProof, 32) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.SmesherID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *MarryProof) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := scale.DecodeByteArray(dec, t.ATXID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.MarriageRoot[:]) + if err != nil { + return total, err + } + total += n + } + { + field, n, err := scale.DecodeStructSliceWithLimit[types.Hash32](dec, 32) + if err != nil { + return total, err + } + total += n + t.MarriageProof = field + } + { + n, err := scale.DecodeByteArray(dec, t.CertificateReference[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.CertificateSignature[:]) + if err != nil { + return total, err + } + total += n + } + { + field, n, err := scale.DecodeCompact64(dec) + if err != nil { + return total, err + } + total += n + t.CertificateIndex = uint64(field) + } + { + field, n, err := scale.DecodeStructSliceWithLimit[types.Hash32](dec, 32) + if err != nil { + return total, err + } + total += n + t.CertificateProof = field + } + { + n, err := scale.DecodeByteArray(dec, t.SmesherID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} diff --git a/activation/wire/malfeasance_double_marry_test.go b/activation/wire/malfeasance_double_marry_test.go new file mode 100644 index 0000000000..90bfd49de9 --- /dev/null +++ b/activation/wire/malfeasance_double_marry_test.go @@ -0,0 +1,294 @@ +//go:build exclude + +// FIXME: tmp circular dep fix + +package wire + +import ( + "fmt" + "slices" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/sql/statesql" +) + +func Test_DoubleMarryProof(t *testing.T) { + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + + t.Run("valid", func(t *testing.T) { + db := statesql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.NoError(t, err) + require.NotNil(t, proof) + + verifier := signing.NewEdVerifier() + id, err := proof.Valid(verifier) + require.NoError(t, err) + require.Equal(t, otherSig.NodeID(), id) + }) + + t.Run("does not contain same certificate owner", func(t *testing.T) { + db := statesql.InMemory() + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.ErrorContains(t, err, fmt.Sprintf( + "proof for atx1: does not contain a marriage certificate signed by %s", otherSig.NodeID().ShortString(), + )) + require.Nil(t, proof) + + proof, err = NewDoubleMarryProof(db, atx1, atx2, sig.NodeID()) + require.ErrorContains(t, err, fmt.Sprintf( + "proof for atx2: does not contain a marriage certificate signed by %s", sig.NodeID().ShortString(), + )) + require.Nil(t, proof) + }) + + t.Run("same ATX ID", func(t *testing.T) { + atx1 := newActivationTxV2() + atx1.Sign(sig) + + db := statesql.InMemory() + proof, err := NewDoubleMarryProof(db, atx1, atx1, sig.NodeID()) + require.ErrorContains(t, err, "ATXs have the same ID") + require.Nil(t, proof) + + // manually construct an invalid proof + proof = &ProofDoubleMarry{ + Proofs: [2]MarryProof{ + { + ATXID: atx1.ID(), + }, + { + ATXID: atx1.ID(), + }, + }, + } + + verifier := signing.NewEdVerifier() + id, err := proof.Valid(verifier) + require.ErrorContains(t, err, "same ATX ID") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("invalid marriage proof", func(t *testing.T) { + db := statesql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), + ) + atx2.Sign(otherSig) + + // manually construct an invalid proof + proof1, err := createMarryProof(db, atx1, otherSig.NodeID()) + require.NoError(t, err) + proof2, err := createMarryProof(db, atx2, otherSig.NodeID()) + require.NoError(t, err) + + proof := &ProofDoubleMarry{ + NodeID: otherSig.NodeID(), + Proofs: [2]MarryProof{ + proof1, proof2, + }, + } + + verifier := signing.NewEdVerifier() + proof.Proofs[0].MarriageProof = slices.Clone(proof1.MarriageProof) + proof.Proofs[0].MarriageProof[0] = types.RandomHash() + id, err := proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid marriage proof") + require.Equal(t, types.EmptyNodeID, id) + + proof.Proofs[0].MarriageProof[0] = proof1.MarriageProof[0] + proof.Proofs[1].MarriageProof = slices.Clone(proof2.MarriageProof) + proof.Proofs[1].MarriageProof[0] = types.RandomHash() + id, err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid marriage proof") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("invalid certificate proof", func(t *testing.T) { + db := statesql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), + ) + atx2.Sign(otherSig) + + // manually construct an invalid proof + proof1, err := createMarryProof(db, atx1, otherSig.NodeID()) + require.NoError(t, err) + proof2, err := createMarryProof(db, atx2, otherSig.NodeID()) + require.NoError(t, err) + + proof := &ProofDoubleMarry{ + NodeID: otherSig.NodeID(), + Proofs: [2]MarryProof{ + proof1, proof2, + }, + } + + verifier := signing.NewEdVerifier() + proof.Proofs[0].CertificateProof = slices.Clone(proof1.CertificateProof) + proof.Proofs[0].CertificateProof[0] = types.RandomHash() + id, err := proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid certificate proof") + require.Equal(t, types.EmptyNodeID, id) + + proof.Proofs[0].CertificateProof[0] = proof1.CertificateProof[0] + proof.Proofs[1].CertificateProof = slices.Clone(proof2.CertificateProof) + proof.Proofs[1].CertificateProof[0] = types.RandomHash() + id, err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid certificate proof") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("invalid atx signature", func(t *testing.T) { + db := statesql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), sig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.NoError(t, err) + + verifier := signing.NewEdVerifier() + + proof.Proofs[0].Signature = types.RandomEdSignature() + id, err := proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid ATX signature") + require.Equal(t, types.EmptyNodeID, id) + + proof.Proofs[0].Signature = atx1.Signature + proof.Proofs[1].Signature = types.RandomEdSignature() + id, err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid ATX signature") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("invalid certificate signature", func(t *testing.T) { + db := statesql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), sig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.NoError(t, err) + + verifier := signing.NewEdVerifier() + + proof.Proofs[0].CertificateSignature = types.RandomEdSignature() + id, err := proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid certificate signature") + require.Equal(t, types.EmptyNodeID, id) + + proof.Proofs[0].CertificateSignature = atx1.Marriages[1].Signature + proof.Proofs[1].CertificateSignature = types.RandomEdSignature() + id, err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid certificate signature") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("unknown reference ATX", func(t *testing.T) { + db := statesql.InMemory() + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, types.RandomATXID(), sig.NodeID()), // unknown reference ATX + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), sig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.Error(t, err) + require.Nil(t, proof) + }) +} diff --git a/activation/wire/malfeasance_scale.go b/activation/wire/malfeasance_scale.go new file mode 100644 index 0000000000..a2cb9dbed7 --- /dev/null +++ b/activation/wire/malfeasance_scale.go @@ -0,0 +1,61 @@ +// Code generated by github.com/spacemeshos/go-scale/scalegen. DO NOT EDIT. + +// nolint +package wire + +import ( + "github.com/spacemeshos/go-scale" +) + +func (t *ATXProof) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeCompact8(enc, uint8(t.Version)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeCompact8(enc, uint8(t.ProofType)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteSliceWithLimit(enc, t.Proof, 1048576) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *ATXProof) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + field, n, err := scale.DecodeCompact8(dec) + if err != nil { + return total, err + } + total += n + t.Version = ProofVersion(field) + } + { + field, n, err := scale.DecodeCompact8(dec) + if err != nil { + return total, err + } + total += n + t.ProofType = ProofType(field) + } + { + field, n, err := scale.DecodeByteSliceWithLimit(dec, 1048576) + if err != nil { + return total, err + } + total += n + t.Proof = field + } + return total, nil +} diff --git a/activation/wire/wire_v1.go b/activation/wire/wire_v1.go index d76e343ab0..79daf37f66 100644 --- a/activation/wire/wire_v1.go +++ b/activation/wire/wire_v1.go @@ -196,7 +196,6 @@ func ActivationTxFromWireV1(atx *ActivationTxV1) *types.ActivationTx { result := &types.ActivationTx{ PublishEpoch: atx.PublishEpoch, Sequence: atx.Sequence, - PrevATXID: atx.PrevATXID, CommitmentATX: atx.CommitmentATXID, Coinbase: atx.Coinbase, NumUnits: atx.NumUnits, diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index 7cf29c842e..fd5cccbecf 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -32,7 +32,7 @@ type ActivationTxV2 struct { // All new IDs that are married to this ID are added to the equivocation set // that this ID belongs to. // It must contain a self-marriage certificate (needed for malfeasance proofs). - Marriages []MarriageCertificate `scale:"max=256"` + Marriages MarriageCertificates `scale:"max=256"` // The ID of the ATX containing marriage for the included IDs. // Only required when the ATX includes married IDs. @@ -46,10 +46,6 @@ type ActivationTxV2 struct { blob []byte } -func (atx *ActivationTxV2) SignedBytes() []byte { - return atx.ID().Bytes() -} - func (atx *ActivationTxV2) Blob() types.AtxBlob { if len(atx.blob) == 0 { atx.blob = codec.MustEncode(atx) @@ -71,9 +67,9 @@ func DecodeAtxV2(blob []byte) (*ActivationTxV2, error) { } func (atx *ActivationTxV2) merkleTree(tree *merkle.Tree) { - publishEpoch := make([]byte, 4) - binary.LittleEndian.PutUint32(publishEpoch, atx.PublishEpoch.Uint32()) - tree.AddLeaf(publishEpoch) + var publishEpoch types.Hash32 + binary.LittleEndian.PutUint32(publishEpoch[:], atx.PublishEpoch.Uint32()) + tree.AddLeaf(publishEpoch.Bytes()) tree.AddLeaf(atx.PositioningATX.Bytes()) tree.AddLeaf(atx.Coinbase.Bytes()) @@ -111,23 +107,11 @@ func (atx *ActivationTxV2) merkleTree(tree *merkle.Tree) { } tree.AddLeaf(niPostTree.Root()) - vrfNonce := make([]byte, 8) - binary.LittleEndian.PutUint64(vrfNonce, atx.VRFNonce) - tree.AddLeaf(vrfNonce) + var vrfNonce types.Hash32 + binary.LittleEndian.PutUint64(vrfNonce[:], atx.VRFNonce) + tree.AddLeaf(vrfNonce.Bytes()) - marriagesTree, err := merkle.NewTreeBuilder(). - WithHashFunc(atxTreeHash). - Build() - if err != nil { - panic(err) - } - for _, marriage := range atx.Marriages { - marriagesTree.AddLeaf(marriage.Root()) - } - for i := len(atx.Marriages); i < 256; i++ { - marriagesTree.AddLeaf(types.EmptyHash32.Bytes()) - } - tree.AddLeaf(marriagesTree.Root()) + tree.AddLeaf(atx.Marriages.Root()) if atx.MarriageATX != nil { tree.AddLeaf(atx.MarriageATX.Bytes()) @@ -154,7 +138,7 @@ func (atx *ActivationTxV2) ID() types.ATXID { func (atx *ActivationTxV2) Sign(signer *signing.EdSigner) { atx.SmesherID = signer.NodeID() - atx.Signature = signer.Sign(signing.ATX, atx.SignedBytes()) + atx.Signature = signer.Sign(signing.ATX, atx.ID().Bytes()) } func (atx *ActivationTxV2) TotalNumUnits() uint32 { @@ -167,6 +151,28 @@ func (atx *ActivationTxV2) TotalNumUnits() uint32 { return total } +type MarriageCertificates []MarriageCertificate + +func (mcs MarriageCertificates) Root() []byte { + marriagesTree, err := merkle.NewTreeBuilder(). + WithHashFunc(atxTreeHash). + Build() + if err != nil { + panic(err) + } + mcs.merkleTree(marriagesTree) + return marriagesTree.Root() +} + +func (mcs MarriageCertificates) merkleTree(tree *merkle.Tree) { + for _, marriage := range mcs { + tree.AddLeaf(marriage.Root()) + } + for i := len(mcs); i < 256; i++ { + tree.AddLeaf(types.EmptyHash32.Bytes()) + } +} + type InitialAtxPartsV2 struct { CommitmentATX types.ATXID Post PostV1 diff --git a/activation/wire/wire_v2_test.go b/activation/wire/wire_v2_test.go index 596be06091..e0303affb0 100644 --- a/activation/wire/wire_v2_test.go +++ b/activation/wire/wire_v2_test.go @@ -1,17 +1,59 @@ package wire import ( - "encoding/binary" "math/rand/v2" "testing" fuzz "github.com/google/gofuzz" - "github.com/spacemeshos/merkle-tree" "github.com/stretchr/testify/require" "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" ) +type testAtxV2Opt func(*ActivationTxV2) + +func withMarriageCertificate(sig *signing.EdSigner, refAtx types.ATXID, atxPublisher types.NodeID) testAtxV2Opt { + return func(atx *ActivationTxV2) { + certificate := MarriageCertificate{ + ReferenceAtx: refAtx, + Signature: sig.Sign(signing.MARRIAGE, atxPublisher.Bytes()), + } + atx.Marriages = append(atx.Marriages, certificate) + } +} + +func newActivationTxV2(opts ...testAtxV2Opt) *ActivationTxV2 { + atx := &ActivationTxV2{ + PublishEpoch: rand.N(types.EpochID(255)), + PositioningATX: types.RandomATXID(), + PreviousATXs: make([]types.ATXID, 1+rand.IntN(255)), + NiPosts: []NiPostsV2{ + { + Membership: MerkleProofV2{ + Nodes: make([]types.Hash32, 32), + }, + Challenge: types.RandomHash(), + Posts: []SubPostV2{ + { + MarriageIndex: rand.Uint32N(256), + PrevATXIndex: 0, + Post: PostV1{ + Nonce: 0, + Indices: make([]byte, 800), + Pow: 0, + }, + }, + }, + }, + }, + } + for _, opt := range opts { + opt(atx) + } + return atx +} + func Benchmark_ATXv2ID(b *testing.B) { f := fuzz.New() b.ResetTimer() @@ -83,79 +125,3 @@ func Test_NoATXv2IDCollisions(t *testing.T) { atxIDs = append(atxIDs, id) } } - -const PublishEpochIndex = 0 - -func Test_GenerateDoublePublishProof(t *testing.T) { - atx := &ActivationTxV2{ - PublishEpoch: 10, - PositioningATX: types.RandomATXID(), - PreviousATXs: make([]types.ATXID, 1), - NiPosts: []NiPostsV2{ - { - Membership: MerkleProofV2{ - Nodes: make([]types.Hash32, 32), - }, - Challenge: types.RandomHash(), - Posts: []SubPostV2{ - { - MarriageIndex: rand.Uint32N(256), - PrevATXIndex: 0, - Post: PostV1{ - Nonce: 0, - Indices: make([]byte, 800), - Pow: 0, - }, - }, - }, - }, - }, - } - - proof, err := generatePublishEpochProof(atx) - require.NoError(t, err) - require.NotNil(t, proof) - - // a malfeasance proof for double publish will contain - // - the value of the PublishEpoch (here 10) - 4 bytes - // - the two ATX IDs - 32 bytes each - // - the two signatures (atx.Signature + atx.NodeID) - 64 bytes each - // - two merkle proofs - one per ATX - that is 128 bytes each (4 * 32) - // total: 452 bytes instead of two full ATXs (> 20 kB each in the worst case) - - publishEpoch := make([]byte, 4) - binary.LittleEndian.PutUint32(publishEpoch, atx.PublishEpoch.Uint32()) - ok, err := merkle.ValidatePartialTree( - []uint64{PublishEpochIndex}, - [][]byte{publishEpoch}, - proof, - atx.ID().Bytes(), - atxTreeHash, - ) - require.NoError(t, err) - require.True(t, ok) - - // different PublishEpoch doesn't validate - publishEpoch = []byte{0xFF, 0x00, 0x00, 0x00} - ok, err = merkle.ValidatePartialTree( - []uint64{PublishEpochIndex}, - [][]byte{publishEpoch}, - proof, - atx.ID().Bytes(), - atxTreeHash, - ) - require.NoError(t, err) - require.False(t, ok) -} - -func generatePublishEpochProof(atx *ActivationTxV2) ([][]byte, error) { - tree, err := merkle.NewTreeBuilder(). - WithLeavesToProve(map[uint64]bool{PublishEpochIndex: true}). - WithHashFunc(atxTreeHash). - Build() - if err != nil { - return nil, err - } - atx.merkleTree(tree) - return tree.Proof(), nil -} diff --git a/api/grpcserver/activation_service.go b/api/grpcserver/activation_service.go index 5e7bf3c303..4ba005a970 100644 --- a/api/grpcserver/activation_service.go +++ b/api/grpcserver/activation_service.go @@ -63,6 +63,15 @@ func (s *activationService) Get(ctx context.Context, request *pb.GetRequest) (*p ) return nil, status.Error(codes.NotFound, "id was not found") } + prev, err := s.atxProvider.Previous(atxId) + if err != nil { + ctxzap.Error(ctx, "failed to get previous ATX", + zap.Stringer("id", atxId), + zap.Error(err), + ) + return nil, status.Error(codes.Internal, "couldn't get previous ATXs") + } + proof, err := s.atxProvider.GetMalfeasanceProof(atx.SmesherID) if err != nil && !errors.Is(err, sql.ErrNotFound) { ctxzap.Error(ctx, "failed to get malfeasance proof", @@ -74,7 +83,7 @@ func (s *activationService) Get(ctx context.Context, request *pb.GetRequest) (*p return nil, status.Error(codes.NotFound, "id was not found") } resp := &pb.GetResponse{ - Atx: convertActivation(atx), + Atx: convertActivation(atx, prev), } if proof != nil { resp.MalfeasanceProof = events.ToMalfeasancePB(atx.SmesherID, proof, false) @@ -95,7 +104,16 @@ func (s *activationService) Highest(ctx context.Context, req *emptypb.Empty) (*p if err != nil || atx == nil { return nil, status.Error(codes.NotFound, fmt.Sprintf("atx id %v not found: %v", highest, err.Error())) } + prev, err := s.atxProvider.Previous(highest) + if err != nil { + ctxzap.Error(ctx, "failed to get previous ATX", + zap.Stringer("id", highest), + zap.Error(err), + ) + return nil, status.Error(codes.Internal, "couldn't get previous ATXs") + } + return &pb.HighestResponse{ - Atx: convertActivation(atx), + Atx: convertActivation(atx, prev), }, nil } diff --git a/api/grpcserver/activation_service_test.go b/api/grpcserver/activation_service_test.go index f79d12576f..3339cdb4db 100644 --- a/api/grpcserver/activation_service_test.go +++ b/api/grpcserver/activation_service_test.go @@ -33,7 +33,7 @@ func Test_Highest_ReturnsGoldenAtxOnError(t *testing.T) { require.Nil(t, response.Atx.Layer) require.Nil(t, response.Atx.SmesherId) require.Nil(t, response.Atx.Coinbase) - require.Nil(t, response.Atx.PrevAtx) + require.Nil(t, response.Atx.PrevAtx) // nolint:staticcheck // SA1019 (deprecated) require.EqualValues(t, 0, response.Atx.NumUnits) require.EqualValues(t, 0, response.Atx.Sequence) } @@ -44,9 +44,9 @@ func Test_Highest_ReturnsMaxTickHeight(t *testing.T) { goldenAtx := types.ATXID{2, 3, 4} activationService := grpcserver.NewActivationService(atxProvider, goldenAtx) + previous := types.RandomATXID() atx := types.ActivationTx{ Sequence: rand.Uint64(), - PrevATXID: types.RandomATXID(), PublishEpoch: 0, Coinbase: types.GenerateAddress(types.RandomBytes(32)), NumUnits: rand.Uint32(), @@ -55,6 +55,7 @@ func Test_Highest_ReturnsMaxTickHeight(t *testing.T) { atx.SetID(id) atxProvider.EXPECT().MaxHeightAtx().Return(id, nil) atxProvider.EXPECT().GetAtx(id).Return(&atx, nil) + atxProvider.EXPECT().Previous(id).Return([]types.ATXID{previous}, nil) response, err := activationService.Highest(context.Background(), &emptypb.Empty{}) require.NoError(t, err) @@ -62,7 +63,7 @@ func Test_Highest_ReturnsMaxTickHeight(t *testing.T) { require.Equal(t, atx.PublishEpoch.Uint32(), response.Atx.Layer.Number) require.Equal(t, atx.SmesherID.Bytes(), response.Atx.SmesherId.Id) require.Equal(t, atx.Coinbase.String(), response.Atx.Coinbase.Address) - require.Equal(t, atx.PrevATXID.Bytes(), response.Atx.PrevAtx.Id) + require.Equal(t, previous.Bytes(), response.Atx.PrevAtx.Id) // nolint:staticcheck // SA1019 (deprecated) require.Equal(t, atx.NumUnits, response.Atx.NumUnits) require.Equal(t, atx.Sequence, response.Atx.Sequence) } @@ -103,15 +104,29 @@ func TestGet_AtxProviderReturnsFailure(t *testing.T) { require.Equal(t, codes.NotFound, status.Code(err)) } +func TestGet_AtxProviderFailsObtainPreviousAtxs(t *testing.T) { + ctrl := gomock.NewController(t) + atxProvider := grpcserver.NewMockatxProvider(ctrl) + activationService := grpcserver.NewActivationService(atxProvider, types.ATXID{1}) + + id := types.RandomATXID() + atxProvider.EXPECT().GetAtx(id).Return(&types.ActivationTx{}, nil) + atxProvider.EXPECT().Previous(id).Return(nil, errors.New("")) + + _, err := activationService.Get(context.Background(), &pb.GetRequest{Id: id.Bytes()}) + require.Error(t, err) + require.Equal(t, codes.Internal, status.Code(err)) +} + func TestGet_HappyPath(t *testing.T) { ctrl := gomock.NewController(t) atxProvider := grpcserver.NewMockatxProvider(ctrl) activationService := grpcserver.NewActivationService(atxProvider, types.ATXID{1}) + previous := []types.ATXID{types.RandomATXID(), types.RandomATXID()} id := types.RandomATXID() atx := types.ActivationTx{ Sequence: rand.Uint64(), - PrevATXID: types.RandomATXID(), PublishEpoch: 0, Coinbase: types.GenerateAddress(types.RandomBytes(32)), NumUnits: rand.Uint32(), @@ -119,6 +134,7 @@ func TestGet_HappyPath(t *testing.T) { atx.SetID(id) atxProvider.EXPECT().GetAtx(id).Return(&atx, nil) atxProvider.EXPECT().GetMalfeasanceProof(gomock.Any()).Return(nil, sql.ErrNotFound) + atxProvider.EXPECT().Previous(id).Return(previous, nil) response, err := activationService.Get(context.Background(), &pb.GetRequest{Id: id.Bytes()}) require.NoError(t, err) @@ -127,7 +143,10 @@ func TestGet_HappyPath(t *testing.T) { require.Equal(t, atx.PublishEpoch.Uint32(), response.Atx.Layer.Number) require.Equal(t, atx.SmesherID.Bytes(), response.Atx.SmesherId.Id) require.Equal(t, atx.Coinbase.String(), response.Atx.Coinbase.Address) - require.Equal(t, atx.PrevATXID.Bytes(), response.Atx.PrevAtx.Id) + require.Equal(t, previous[0].Bytes(), response.Atx.PrevAtx.Id) // nolint:staticcheck // SA1019 (deprecated) + require.Len(t, response.Atx.PreviousAtxs, 2) + require.Equal(t, previous[0].Bytes(), response.Atx.PreviousAtxs[0].Id) + require.Equal(t, previous[1].Bytes(), response.Atx.PreviousAtxs[1].Id) require.Equal(t, atx.NumUnits, response.Atx.NumUnits) require.Equal(t, atx.Sequence, response.Atx.Sequence) require.Nil(t, response.MalfeasanceProof) @@ -139,10 +158,10 @@ func TestGet_IdentityCanceled(t *testing.T) { activationService := grpcserver.NewActivationService(atxProvider, types.ATXID{1}) smesher, proof := grpcserver.BallotMalfeasance(t, statesql.InMemory()) + previous := types.RandomATXID() id := types.RandomATXID() atx := types.ActivationTx{ Sequence: rand.Uint64(), - PrevATXID: types.RandomATXID(), PublishEpoch: 0, Coinbase: types.GenerateAddress(types.RandomBytes(32)), NumUnits: rand.Uint32(), @@ -151,6 +170,7 @@ func TestGet_IdentityCanceled(t *testing.T) { atx.SetID(id) atxProvider.EXPECT().GetAtx(id).Return(&atx, nil) atxProvider.EXPECT().GetMalfeasanceProof(smesher).Return(proof, nil) + atxProvider.EXPECT().Previous(id).Return([]types.ATXID{previous}, nil) response, err := activationService.Get(context.Background(), &pb.GetRequest{Id: id.Bytes()}) require.NoError(t, err) @@ -159,7 +179,9 @@ func TestGet_IdentityCanceled(t *testing.T) { require.Equal(t, atx.PublishEpoch.Uint32(), response.Atx.Layer.Number) require.Equal(t, atx.SmesherID.Bytes(), response.Atx.SmesherId.Id) require.Equal(t, atx.Coinbase.String(), response.Atx.Coinbase.Address) - require.Equal(t, atx.PrevATXID.Bytes(), response.Atx.PrevAtx.Id) + require.Equal(t, previous.Bytes(), response.Atx.PrevAtx.Id) // nolint:staticcheck // SA1019 (deprecated) + require.Len(t, response.Atx.PreviousAtxs, 1) + require.Equal(t, previous.Bytes(), response.Atx.PreviousAtxs[0].Id) require.Equal(t, atx.NumUnits, response.Atx.NumUnits) require.Equal(t, atx.Sequence, response.Atx.Sequence) require.Equal(t, events.ToMalfeasancePB(smesher, proof, false), response.MalfeasanceProof) diff --git a/api/grpcserver/admin_service.go b/api/grpcserver/admin_service.go index f6e438ae35..75bcbd15ff 100644 --- a/api/grpcserver/admin_service.go +++ b/api/grpcserver/admin_service.go @@ -57,20 +57,20 @@ func NewAdminService(db sql.StateDatabase, dataDir string, p peers) *AdminServic } // RegisterService registers this service with a grpc server instance. -func (a AdminService) RegisterService(server *grpc.Server) { +func (a *AdminService) RegisterService(server *grpc.Server) { pb.RegisterAdminServiceServer(server, a) } -func (s AdminService) RegisterHandlerService(mux *runtime.ServeMux) error { - return pb.RegisterAdminServiceHandlerServer(context.Background(), mux, s) +func (a *AdminService) RegisterHandlerService(mux *runtime.ServeMux) error { + return pb.RegisterAdminServiceHandlerServer(context.Background(), mux, a) } // String returns the name of this service. -func (a AdminService) String() string { +func (a *AdminService) String() string { return "AdminService" } -func (a AdminService) CheckpointStream( +func (a *AdminService) CheckpointStream( req *pb.CheckpointStreamRequest, stream pb.AdminService_CheckpointStreamServer, ) error { @@ -118,13 +118,13 @@ func (a AdminService) CheckpointStream( } } -func (a AdminService) Recover(ctx context.Context, _ *pb.RecoverRequest) (*emptypb.Empty, error) { +func (a *AdminService) Recover(ctx context.Context, _ *pb.RecoverRequest) (*emptypb.Empty, error) { ctxzap.Info(ctx, "going to recover from checkpoint") a.recover() return &emptypb.Empty{}, nil } -func (a AdminService) EventsStream(req *pb.EventStreamRequest, stream pb.AdminService_EventsStreamServer) error { +func (a *AdminService) EventsStream(_ *pb.EventStreamRequest, stream pb.AdminService_EventsStreamServer) error { sub, buf, err := events.SubscribeUserEvents(events.WithBuffer(1000)) if err != nil { return status.Errorf(codes.FailedPrecondition, err.Error()) @@ -156,7 +156,7 @@ func (a AdminService) EventsStream(req *pb.EventStreamRequest, stream pb.AdminSe } } -func (a AdminService) PeerInfoStream(_ *emptypb.Empty, stream pb.AdminService_PeerInfoStreamServer) error { +func (a *AdminService) PeerInfoStream(_ *emptypb.Empty, stream pb.AdminService_PeerInfoStreamServer) error { for _, p := range a.p.GetPeers() { select { case <-stream.Context().Done(): diff --git a/api/grpcserver/admin_service_test.go b/api/grpcserver/admin_service_test.go index be442e5396..02bb7f805c 100644 --- a/api/grpcserver/admin_service_test.go +++ b/api/grpcserver/admin_service_test.go @@ -39,7 +39,7 @@ func newAtx(tb testing.TB, db sql.StateDatabase) { atx.SmesherID = types.BytesToNodeID(types.RandomBytes(20)) atx.SetReceived(time.Now().Local()) require.NoError(tb, atxs.Add(db, atx, types.AtxBlob{})) - require.NoError(tb, atxs.SetUnits(db, atx.ID(), atx.SmesherID, atx.NumUnits)) + require.NoError(tb, atxs.SetPost(db, atx.ID(), types.EmptyATXID, 0, atx.SmesherID, atx.NumUnits)) } func createMesh(tb testing.TB, db sql.StateDatabase) { @@ -66,7 +66,7 @@ func TestAdminService_Checkpoint(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewAdminServiceClient(conn) stream, err := c.CheckpointStream(ctx, &pb.CheckpointStreamRequest{SnapshotLayer: snapshot}) @@ -102,7 +102,7 @@ func TestAdminService_CheckpointError(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewAdminServiceClient(conn) stream, err := c.CheckpointStream(ctx, &pb.CheckpointStreamRequest{SnapshotLayer: snapshot}) @@ -122,7 +122,7 @@ func TestAdminService_Recovery(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewAdminServiceClient(conn) _, err := c.Recover(ctx, &pb.RecoverRequest{}) @@ -142,7 +142,7 @@ func TestAdminService_PeerInfo(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewAdminServiceClient(conn) p1 := p2p.Peer("p1") diff --git a/api/grpcserver/debug_service.go b/api/grpcserver/debug_service.go index 672886bf27..cd107b41eb 100644 --- a/api/grpcserver/debug_service.go +++ b/api/grpcserver/debug_service.go @@ -32,16 +32,16 @@ type DebugService struct { } // RegisterService registers this service with a grpc server instance. -func (d DebugService) RegisterService(server *grpc.Server) { +func (d *DebugService) RegisterService(server *grpc.Server) { pb.RegisterDebugServiceServer(server, d) } -func (s DebugService) RegisterHandlerService(mux *runtime.ServeMux) error { - return pb.RegisterDebugServiceHandlerServer(context.Background(), mux, s) +func (d *DebugService) RegisterHandlerService(mux *runtime.ServeMux) error { + return pb.RegisterDebugServiceHandlerServer(context.Background(), mux, d) } // String returns the name of this service. -func (d DebugService) String() string { +func (d *DebugService) String() string { return "DebugService" } @@ -59,7 +59,7 @@ func NewDebugService(db sql.StateDatabase, conState conservativeState, host netw } // Accounts returns current counter and balance for all accounts. -func (d DebugService) Accounts(ctx context.Context, in *pb.AccountsRequest) (*pb.AccountsResponse, error) { +func (d *DebugService) Accounts(ctx context.Context, in *pb.AccountsRequest) (*pb.AccountsResponse, error) { var ( accts []*types.Account err error @@ -96,7 +96,7 @@ func (d DebugService) Accounts(ctx context.Context, in *pb.AccountsRequest) (*pb } // NetworkInfo query provides NetworkInfoResponse. -func (d DebugService) NetworkInfo(ctx context.Context, _ *emptypb.Empty) (*pb.NetworkInfoResponse, error) { +func (d *DebugService) NetworkInfo(ctx context.Context, _ *emptypb.Empty) (*pb.NetworkInfoResponse, error) { resp := &pb.NetworkInfoResponse{Id: d.netInfo.ID().String()} for _, a := range d.netInfo.ListenAddresses() { resp.ListenAddresses = append(resp.ListenAddresses, a.String()) @@ -139,7 +139,7 @@ func (d DebugService) NetworkInfo(ctx context.Context, _ *emptypb.Empty) (*pb.Ne } // ActiveSet query provides hare active set for the specified epoch. -func (d DebugService) ActiveSet(ctx context.Context, req *pb.ActiveSetRequest) (*pb.ActiveSetResponse, error) { +func (d *DebugService) ActiveSet(ctx context.Context, req *pb.ActiveSetRequest) (*pb.ActiveSetResponse, error) { actives, err := d.oracle.ActiveSet(ctx, types.EpochID(req.Epoch)) if err != nil { return nil, status.Errorf(codes.Internal, fmt.Sprintf("active set for epoch %d: %s", req.Epoch, err.Error())) @@ -152,7 +152,7 @@ func (d DebugService) ActiveSet(ctx context.Context, req *pb.ActiveSetRequest) ( } // ProposalsStream streams all proposals confirmed by hare. -func (d DebugService) ProposalsStream(_ *emptypb.Empty, stream pb.DebugService_ProposalsStreamServer) error { +func (d *DebugService) ProposalsStream(_ *emptypb.Empty, stream pb.DebugService_ProposalsStreamServer) error { sub := events.SubscribeProposals() if sub == nil { return status.Errorf(codes.FailedPrecondition, "event reporting is not enabled") @@ -177,7 +177,7 @@ func (d DebugService) ProposalsStream(_ *emptypb.Empty, stream pb.DebugService_P } } -func (d DebugService) ChangeLogLevel(ctx context.Context, req *pb.ChangeLogLevelRequest) (*emptypb.Empty, error) { +func (d *DebugService) ChangeLogLevel(ctx context.Context, req *pb.ChangeLogLevelRequest) (*emptypb.Empty, error) { level, err := zap.ParseAtomicLevel(req.GetLevel()) if err != nil { return nil, fmt.Errorf("parse level: %w", err) diff --git a/api/grpcserver/globalstate_service.go b/api/grpcserver/globalstate_service.go index f6ed0c260b..b1c1b167bb 100644 --- a/api/grpcserver/globalstate_service.go +++ b/api/grpcserver/globalstate_service.go @@ -24,16 +24,16 @@ type GlobalStateService struct { } // RegisterService registers this service with a grpc server instance. -func (s GlobalStateService) RegisterService(server *grpc.Server) { +func (s *GlobalStateService) RegisterService(server *grpc.Server) { pb.RegisterGlobalStateServiceServer(server, s) } -func (s GlobalStateService) RegisterHandlerService(mux *runtime.ServeMux) error { +func (s *GlobalStateService) RegisterHandlerService(mux *runtime.ServeMux) error { return pb.RegisterGlobalStateServiceHandlerServer(context.Background(), mux, s) } // String returns the name of the service. -func (s GlobalStateService) String() string { +func (s *GlobalStateService) String() string { return "GlobalStateService" } @@ -46,7 +46,7 @@ func NewGlobalStateService(msh meshAPI, conState conservativeState) *GlobalState } // GlobalStateHash returns the latest layer and its computed global state hash. -func (s GlobalStateService) GlobalStateHash( +func (s *GlobalStateService) GlobalStateHash( context.Context, *pb.GlobalStateHashRequest, ) (*pb.GlobalStateHashResponse, error) { @@ -60,7 +60,7 @@ func (s GlobalStateService) GlobalStateHash( }}, nil } -func (s GlobalStateService) getAccount(addr types.Address) (acct *pb.Account, err error) { +func (s *GlobalStateService) getAccount(addr types.Address) (acct *pb.Account, err error) { balanceActual, err := s.conState.GetBalance(addr) if err != nil { return nil, err @@ -84,7 +84,7 @@ func (s GlobalStateService) getAccount(addr types.Address) (acct *pb.Account, er } // Account returns current and projected counter and balance for one account. -func (s GlobalStateService) Account(ctx context.Context, in *pb.AccountRequest) (*pb.AccountResponse, error) { +func (s *GlobalStateService) Account(ctx context.Context, in *pb.AccountRequest) (*pb.AccountResponse, error) { if in.AccountId == nil { return nil, status.Errorf(codes.InvalidArgument, "`AccountId` must be provided") } @@ -112,7 +112,7 @@ func (s GlobalStateService) Account(ctx context.Context, in *pb.AccountRequest) } // AccountDataQuery returns historical account data such as rewards and receipts. -func (s GlobalStateService) AccountDataQuery( +func (s *GlobalStateService) AccountDataQuery( ctx context.Context, in *pb.AccountDataQueryRequest, ) (*pb.AccountDataQueryResponse, error) { @@ -200,7 +200,7 @@ func (s GlobalStateService) AccountDataQuery( } // SmesherDataQuery returns historical info on smesher rewards. -func (s GlobalStateService) SmesherDataQuery( +func (s *GlobalStateService) SmesherDataQuery( _ context.Context, in *pb.SmesherDataQueryRequest, ) (*pb.SmesherDataQueryResponse, error) { @@ -210,7 +210,7 @@ func (s GlobalStateService) SmesherDataQuery( // STREAMS // AccountDataStream exposes a stream of account-related data. -func (s GlobalStateService) AccountDataStream( +func (s *GlobalStateService) AccountDataStream( in *pb.AccountDataStreamRequest, stream pb.GlobalStateService_AccountDataStreamServer, ) error { @@ -328,7 +328,7 @@ func (s GlobalStateService) AccountDataStream( } // SmesherRewardStream exposes a stream of smesher rewards. -func (s GlobalStateService) SmesherRewardStream( +func (s *GlobalStateService) SmesherRewardStream( in *pb.SmesherRewardStreamRequest, stream pb.GlobalStateService_SmesherRewardStreamServer, ) error { @@ -336,7 +336,7 @@ func (s GlobalStateService) SmesherRewardStream( } // AppEventStream exposes a stream of emitted app events. -func (s GlobalStateService) AppEventStream( +func (s *GlobalStateService) AppEventStream( *pb.AppEventStreamRequest, pb.GlobalStateService_AppEventStreamServer, ) error { @@ -347,7 +347,7 @@ func (s GlobalStateService) AppEventStream( } // GlobalStateStream exposes a stream of global data data items: rewards, receipts, account info, global state hash. -func (s GlobalStateService) GlobalStateStream( +func (s *GlobalStateService) GlobalStateStream( in *pb.GlobalStateStreamRequest, stream pb.GlobalStateService_GlobalStateStreamServer, ) error { diff --git a/api/grpcserver/globalstate_service_test.go b/api/grpcserver/globalstate_service_test.go index b2f8566e82..8df8fc6425 100644 --- a/api/grpcserver/globalstate_service_test.go +++ b/api/grpcserver/globalstate_service_test.go @@ -4,7 +4,6 @@ import ( "context" "math" "testing" - "time" pb "github.com/spacemeshos/api/release/go/spacemesh/v1" "github.com/stretchr/testify/require" @@ -30,9 +29,7 @@ func setupGlobalStateService(t *testing.T) (*globalStateServiceConn, context.Con cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - grpcCtx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - conn := dialGrpc(grpcCtx, t, cfg) + conn := dialGrpc(t, cfg) client := pb.NewGlobalStateServiceClient(conn) return &globalStateServiceConn{ diff --git a/api/grpcserver/grpc.go b/api/grpcserver/grpc.go index 25793cf6e8..8737b523ff 100644 --- a/api/grpcserver/grpc.go +++ b/api/grpcserver/grpc.go @@ -44,7 +44,7 @@ type Server struct { } func unaryGrpcLogStart(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - ctxzap.Info(ctx, "started unary call") + ctxzap.Debug(ctx, "started unary call") return handler(ctx, req) } @@ -54,7 +54,7 @@ func streamingGrpcLogStart( _ *grpc.StreamServerInfo, handler grpc.StreamHandler, ) error { - ctxzap.Info(stream.Context(), "started streaming call") + ctxzap.Debug(stream.Context(), "started streaming call") return handler(srv, stream) } diff --git a/api/grpcserver/grpcserver_test.go b/api/grpcserver/grpcserver_test.go index 126ddbf3f4..e4dd0ccad7 100644 --- a/api/grpcserver/grpcserver_test.go +++ b/api/grpcserver/grpcserver_test.go @@ -42,7 +42,6 @@ import ( vm "github.com/spacemeshos/go-spacemesh/genvm" "github.com/spacemeshos/go-spacemesh/genvm/sdk" "github.com/spacemeshos/go-spacemesh/genvm/sdk/wallet" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/peerinfo" peerinfomocks "github.com/spacemeshos/go-spacemesh/p2p/peerinfo/mocks" @@ -124,7 +123,7 @@ func genLayerBlock(layerID types.LayerID, txs []types.TransactionID) *types.Bloc return b } -func dialGrpc(ctx context.Context, tb testing.TB, cfg Config) *grpc.ClientConn { +func dialGrpc(tb testing.TB, cfg Config) *grpc.ClientConn { tb.Helper() conn, err := grpc.NewClient( cfg.PublicListener, @@ -161,7 +160,6 @@ func TestMain(m *testing.M) { globalAtx = &types.ActivationTx{ PublishEpoch: postGenesisEpoch, Sequence: 1, - PrevATXID: types.ATXID{4, 4, 4, 4}, Coinbase: addr1, NumUnits: numUnits, Weight: numUnits, @@ -173,7 +171,6 @@ func TestMain(m *testing.M) { globalAtx2 = &types.ActivationTx{ PublishEpoch: postGenesisEpoch, Sequence: 1, - PrevATXID: types.ATXID{5, 5, 5, 5}, Coinbase: addr2, NumUnits: numUnits, Weight: numUnits, @@ -528,9 +525,7 @@ func setupSmesherService(t *testing.T, sig *signing.EdSigner) (*smesherServiceCo cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := pb.NewSmesherServiceClient(conn) return &smesherServiceConn{ @@ -748,9 +743,7 @@ func TestMeshService(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewMeshServiceClient(conn) // Construct an array of test cases to test each endpoint in turn @@ -1262,7 +1255,7 @@ func TestTransactionServiceSubmitUnsync(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewTransactionServiceClient(conn) serializedTx, err := codec.Encode(globalTx) @@ -1301,7 +1294,7 @@ func TestTransactionServiceSubmitInvalidTx(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewTransactionServiceClient(conn) serializedTx, err := codec.Encode(globalTx) @@ -1334,7 +1327,7 @@ func TestTransactionService_SubmitNoConcurrency(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewTransactionServiceClient(conn) for range numTxs { res, err := c.SubmitTransaction(ctx, &pb.SubmitTransactionRequest{ @@ -1360,9 +1353,7 @@ func TestTransactionService(t *testing.T) { cfg, cleanup := launchServer(t, grpcService) t.Cleanup(cleanup) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewTransactionServiceClient(conn) // Construct an array of test cases to test each endpoint in turn @@ -1679,9 +1670,7 @@ func TestAccountMeshDataStream_comprehensive(t *testing.T) { cfg, cleanup := launchServer(t, grpcService) t.Cleanup(cleanup) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewMeshServiceClient(conn) // set up the grpc listener stream @@ -1694,7 +1683,7 @@ func TestAccountMeshDataStream_comprehensive(t *testing.T) { }, } - ctx, cancel = context.WithTimeout(context.Background(), time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() stream, err := c.AccountMeshDataStream(ctx, req) require.NoError(t, err, "stream request returned unexpected error") @@ -1729,9 +1718,7 @@ func TestAccountDataStream_comprehensive(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewGlobalStateServiceClient(conn) // set up the grpc listener stream @@ -1745,7 +1732,7 @@ func TestAccountDataStream_comprehensive(t *testing.T) { }, } - ctx, cancel = context.WithTimeout(context.Background(), time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() stream, err := c.AccountDataStream(ctx, req) require.NoError(t, err, "stream request returned unexpected error") @@ -1789,9 +1776,7 @@ func TestGlobalStateStream_comprehensive(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewGlobalStateServiceClient(conn) // set up the grpc listener stream @@ -1802,6 +1787,9 @@ func TestGlobalStateStream_comprehensive(t *testing.T) { pb.GlobalStateDataFlag_GLOBAL_STATE_DATA_FLAG_REWARD), } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + stream, err := c.GlobalStateStream(ctx, req) require.NoError(t, err, "stream request returned unexpected error") // Give the server-side time to subscribe to events @@ -1864,12 +1852,13 @@ func TestLayerStream_comprehensive(t *testing.T) { cfg, cleanup := launchServer(t, grpcService) t.Cleanup(cleanup) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) // set up the grpc listener stream c := pb.NewMeshServiceClient(conn) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() stream, err := c.LayerStream(ctx, &pb.LayerStreamRequest{}) require.NoError(t, err, "stream request returned unexpected error") // Give the server-side time to subscribe to events @@ -2007,8 +1996,8 @@ func TestMultiService(t *testing.T) { cfg, shutDown := launchServer(t, svc1, svc2) t.Cleanup(shutDown) - c1 := pb.NewNodeServiceClient(dialGrpc(ctx, t, cfg)) - c2 := pb.NewMeshServiceClient(dialGrpc(ctx, t, cfg)) + c1 := pb.NewNodeServiceClient(dialGrpc(t, cfg)) + c2 := pb.NewMeshServiceClient(dialGrpc(t, cfg)) // call endpoints and validate results const message = "Hello World" @@ -2050,9 +2039,7 @@ func TestDebugService(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := pb.NewDebugServiceClient(conn) t.Run("Accounts", func(t *testing.T) { @@ -2241,8 +2228,8 @@ func TestEventsReceived(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - conn1 := dialGrpc(ctx, t, cfg) - conn2 := dialGrpc(ctx, t, cfg) + conn1 := dialGrpc(t, cfg) + conn2 := dialGrpc(t, cfg) txClient := pb.NewTransactionServiceClient(conn1) accountClient := pb.NewGlobalStateServiceClient(conn2) @@ -2282,9 +2269,9 @@ func TestEventsReceived(t *testing.T) { // Give the server-side time to subscribe to events time.Sleep(time.Millisecond * 50) - lg := logtest.New(t) + lg := zaptest.NewLogger(t) svm := vm.New(statesql.InMemory(), vm.WithLogger(lg)) - conState := txs.NewConservativeState(svm, statesql.InMemory(), txs.WithLogger(lg.Zap().Named("conState"))) + conState := txs.NewConservativeState(svm, statesql.InMemory(), txs.WithLogger(lg.Named("conState"))) conState.AddToCache(context.Background(), globalTx, time.Now()) weight := new(big.Rat).SetFloat64(18.7) @@ -2329,7 +2316,7 @@ func TestTransactionsRewards(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) t.Cleanup(cancel) - client := pb.NewGlobalStateServiceClient(dialGrpc(ctx, t, cfg)) + client := pb.NewGlobalStateServiceClient(dialGrpc(t, cfg)) address := wallet.Address(types.RandomNodeID().Bytes()) weight := new(big.Rat).SetFloat64(18.7) @@ -2347,7 +2334,7 @@ func TestTransactionsRewards(t *testing.T) { req.NoError(err, "stream request returned unexpected error") time.Sleep(50 * time.Millisecond) - svm := vm.New(statesql.InMemory(), vm.WithLogger(logtest.New(t))) + svm := vm.New(statesql.InMemory(), vm.WithLogger(zaptest.NewLogger(t))) _, _, err = svm.Apply(vm.ApplyContext{Layer: types.LayerID(17)}, []types.Transaction{*globalTx}, rewards) req.NoError(err) @@ -2368,7 +2355,7 @@ func TestTransactionsRewards(t *testing.T) { req.NoError(err, "stream request returned unexpected error") time.Sleep(50 * time.Millisecond) - svm := vm.New(statesql.InMemory(), vm.WithLogger(logtest.New(t))) + svm := vm.New(statesql.InMemory(), vm.WithLogger(zaptest.NewLogger(t))) _, _, err = svm.Apply(vm.ApplyContext{Layer: types.LayerID(17)}, []types.Transaction{*globalTx}, rewards) req.NoError(err) @@ -2390,7 +2377,7 @@ func TestVMAccountUpdates(t *testing.T) { db, err := statesql.Open("file:" + filepath.Join(t.TempDir(), "test.sql")) require.NoError(t, err) t.Cleanup(func() { db.Close() }) - svm := vm.New(db, vm.WithLogger(logtest.New(t))) + svm := vm.New(db, vm.WithLogger(zaptest.NewLogger(t))) cfg, cleanup := launchServer(t, NewGlobalStateService(nil, txs.NewConservativeState(svm, db))) t.Cleanup(cleanup) @@ -2419,7 +2406,7 @@ func TestVMAccountUpdates(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) t.Cleanup(cancel) - client := pb.NewGlobalStateServiceClient(dialGrpc(ctx, t, cfg)) + client := pb.NewGlobalStateServiceClient(dialGrpc(t, cfg)) eg, ctx := errgroup.WithContext(ctx) states := make(chan *pb.AccountState, len(accounts)) for _, account := range accounts { @@ -2512,7 +2499,7 @@ func TestMeshService_EpochStream(t *testing.T) { } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := pb.NewMeshServiceClient(conn) stream, err := client.EpochStream(ctx, &pb.EpochStreamRequest{Epoch: epoch.Uint32()}) diff --git a/api/grpcserver/http_server.go b/api/grpcserver/http_server.go index e529f517e3..6c0087c15b 100644 --- a/api/grpcserver/http_server.go +++ b/api/grpcserver/http_server.go @@ -74,7 +74,6 @@ func (s *JSONHTTPServer) Shutdown(ctx context.Context) error { // StartService starts the json api server and listens for status (started, stopped). func (s *JSONHTTPServer) StartService( - ctx context.Context, services ...ServiceAPI, ) error { // At least one service must be enabled diff --git a/api/grpcserver/http_server_test.go b/api/grpcserver/http_server_test.go index 4c5034871b..beb70be6f0 100644 --- a/api/grpcserver/http_server_test.go +++ b/api/grpcserver/http_server_test.go @@ -29,7 +29,7 @@ func launchJsonServer(tb testing.TB, services ...ServiceAPI) (Config, func()) { []string{}, false) // start json server - require.NoError(tb, jsonService.StartService(context.Background(), services...)) + require.NoError(tb, jsonService.StartService(services...)) // update config with bound address cfg.JSONListener = jsonService.BoundAddress diff --git a/api/grpcserver/interface.go b/api/grpcserver/interface.go index 47a009a47c..bfca33b78b 100644 --- a/api/grpcserver/interface.go +++ b/api/grpcserver/interface.go @@ -56,6 +56,7 @@ type txValidator interface { // atxProvider is used by ActivationService to get ATXes. type atxProvider interface { GetAtx(id types.ATXID) (*types.ActivationTx, error) + Previous(id types.ATXID) ([]types.ATXID, error) MaxHeightAtx() (types.ATXID, error) GetMalfeasanceProof(id types.NodeID) (*wire.MalfeasanceProof, error) } diff --git a/api/grpcserver/mesh_service.go b/api/grpcserver/mesh_service.go index 2f18793916..6d8b23ffd2 100644 --- a/api/grpcserver/mesh_service.go +++ b/api/grpcserver/mesh_service.go @@ -38,16 +38,16 @@ type MeshService struct { } // RegisterService registers this service with a grpc server instance. -func (s MeshService) RegisterService(server *grpc.Server) { +func (s *MeshService) RegisterService(server *grpc.Server) { pb.RegisterMeshServiceServer(server, s) } -func (s MeshService) RegisterHandlerService(mux *runtime.ServeMux) error { +func (s *MeshService) RegisterHandlerService(mux *runtime.ServeMux) error { return pb.RegisterMeshServiceHandlerServer(context.Background(), mux, s) } // String returns the name of this service. -func (s MeshService) String() string { +func (s *MeshService) String() string { return "MeshService" } @@ -77,21 +77,21 @@ func NewMeshService( } // GenesisTime returns the network genesis time as UNIX time. -func (s MeshService) GenesisTime(context.Context, *pb.GenesisTimeRequest) (*pb.GenesisTimeResponse, error) { +func (s *MeshService) GenesisTime(context.Context, *pb.GenesisTimeRequest) (*pb.GenesisTimeResponse, error) { return &pb.GenesisTimeResponse{Unixtime: &pb.SimpleInt{ Value: uint64(s.genTime.GenesisTime().Unix()), }}, nil } // CurrentLayer returns the current layer number. -func (s MeshService) CurrentLayer(context.Context, *pb.CurrentLayerRequest) (*pb.CurrentLayerResponse, error) { +func (s *MeshService) CurrentLayer(context.Context, *pb.CurrentLayerRequest) (*pb.CurrentLayerResponse, error) { return &pb.CurrentLayerResponse{Layernum: &pb.LayerNumber{ Number: s.genTime.CurrentLayer().Uint32(), }}, nil } // CurrentEpoch returns the current epoch number. -func (s MeshService) CurrentEpoch(context.Context, *pb.CurrentEpochRequest) (*pb.CurrentEpochResponse, error) { +func (s *MeshService) CurrentEpoch(context.Context, *pb.CurrentEpochRequest) (*pb.CurrentEpochResponse, error) { curLayer := s.genTime.CurrentLayer() return &pb.CurrentEpochResponse{Epochnum: &pb.EpochNumber{ Number: curLayer.GetEpoch().Uint32(), @@ -99,26 +99,26 @@ func (s MeshService) CurrentEpoch(context.Context, *pb.CurrentEpochRequest) (*pb } // GenesisID returns the network ID. -func (s MeshService) GenesisID(context.Context, *pb.GenesisIDRequest) (*pb.GenesisIDResponse, error) { +func (s *MeshService) GenesisID(context.Context, *pb.GenesisIDRequest) (*pb.GenesisIDResponse, error) { return &pb.GenesisIDResponse{GenesisId: s.genesisID.Bytes()}, nil } // EpochNumLayers returns the number of layers per epoch (a network parameter). -func (s MeshService) EpochNumLayers(context.Context, *pb.EpochNumLayersRequest) (*pb.EpochNumLayersResponse, error) { +func (s *MeshService) EpochNumLayers(context.Context, *pb.EpochNumLayersRequest) (*pb.EpochNumLayersResponse, error) { return &pb.EpochNumLayersResponse{Numlayers: &pb.LayerNumber{ Number: s.layersPerEpoch, }}, nil } // LayerDuration returns the layer duration in seconds (a network parameter). -func (s MeshService) LayerDuration(context.Context, *pb.LayerDurationRequest) (*pb.LayerDurationResponse, error) { +func (s *MeshService) LayerDuration(context.Context, *pb.LayerDurationRequest) (*pb.LayerDurationResponse, error) { return &pb.LayerDurationResponse{Duration: &pb.SimpleInt{ Value: uint64(s.layerDuration.Seconds()), }}, nil } // MaxTransactionsPerSecond returns the max number of tx per sec (a network parameter). -func (s MeshService) MaxTransactionsPerSecond( +func (s *MeshService) MaxTransactionsPerSecond( context.Context, *pb.MaxTransactionsPerSecondRequest, ) (*pb.MaxTransactionsPerSecondResponse, error) { @@ -129,7 +129,7 @@ func (s MeshService) MaxTransactionsPerSecond( // QUERIES -func (s MeshService) getFilteredTransactions( +func (s *MeshService) getFilteredTransactions( from types.LayerID, address types.Address, ) ([]*types.MeshTransaction, error) { @@ -142,7 +142,7 @@ func (s MeshService) getFilteredTransactions( } // AccountMeshDataQuery returns account data. -func (s MeshService) AccountMeshDataQuery( +func (s *MeshService) AccountMeshDataQuery( ctx context.Context, in *pb.AccountMeshDataQueryRequest, ) (*pb.AccountMeshDataQueryResponse, error) { @@ -252,19 +252,28 @@ func castTransaction(t *types.Transaction) *pb.Transaction { return tx } -func convertActivation(a *types.ActivationTx) *pb.Activation { - return &pb.Activation{ +func convertActivation(a *types.ActivationTx, previous []types.ATXID) *pb.Activation { + atx := &pb.Activation{ Id: &pb.ActivationId{Id: a.ID().Bytes()}, Layer: &pb.LayerNumber{Number: a.PublishEpoch.Uint32()}, SmesherId: &pb.SmesherId{Id: a.SmesherID.Bytes()}, Coinbase: &pb.AccountId{Address: a.Coinbase.String()}, - PrevAtx: &pb.ActivationId{Id: a.PrevATXID.Bytes()}, NumUnits: uint32(a.NumUnits), Sequence: a.Sequence, } + + if len(previous) == 0 { + previous = []types.ATXID{types.EmptyATXID} + } + // nolint:staticcheck // SA1019 (deprecated) + atx.PrevAtx = &pb.ActivationId{Id: previous[0].Bytes()} + for _, prev := range previous { + atx.PreviousAtxs = append(atx.PreviousAtxs, &pb.ActivationId{Id: prev.Bytes()}) + } + return atx } -func (s MeshService) readLayer( +func (s *MeshService) readLayer( ctx context.Context, layerID types.LayerID, layerStatus pb.Layer_LayerStatus, @@ -332,7 +341,7 @@ func (s MeshService) readLayer( } // LayersQuery returns all mesh data, layer by layer. -func (s MeshService) LayersQuery(ctx context.Context, in *pb.LayersQueryRequest) (*pb.LayersQueryResponse, error) { +func (s *MeshService) LayersQuery(ctx context.Context, in *pb.LayersQueryRequest) (*pb.LayersQueryResponse, error) { var startLayer, endLayer types.LayerID if in.StartLayer != nil { startLayer = types.LayerID(in.StartLayer.Number) @@ -383,7 +392,7 @@ func (s MeshService) LayersQuery(ctx context.Context, in *pb.LayersQueryRequest) // STREAMS // AccountMeshDataStream exposes a stream of transactions and activations for an account. -func (s MeshService) AccountMeshDataStream( +func (s *MeshService) AccountMeshDataStream( in *pb.AccountMeshDataStreamRequest, stream pb.MeshService_AccountMeshDataStreamServer, ) error { @@ -440,10 +449,14 @@ func (s MeshService) AccountMeshDataStream( activation := activationEvent.ActivationTx // Apply address filter if activation.Coinbase == addr { + previous, err := s.cdb.Previous(activation.ID()) + if err != nil { + return status.Error(codes.Internal, "getting previous ATXs failed") + } resp := &pb.AccountMeshDataStreamResponse{ Datum: &pb.AccountMeshData{ Datum: &pb.AccountMeshData_Activation{ - Activation: convertActivation(activation), + Activation: convertActivation(activation, previous), }, }, } @@ -478,7 +491,7 @@ func (s MeshService) AccountMeshDataStream( } // LayerStream exposes a stream of all mesh data per layer. -func (s MeshService) LayerStream(_ *pb.LayerStreamRequest, stream pb.MeshService_LayerStreamServer) error { +func (s *MeshService) LayerStream(_ *pb.LayerStreamRequest, stream pb.MeshService_LayerStreamServer) error { var ( layerCh <-chan events.LayerUpdate layersBufFull <-chan struct{} @@ -528,7 +541,7 @@ func convertLayerStatus(in int) pb.Layer_LayerStatus { } } -func (s MeshService) EpochStream(req *pb.EpochStreamRequest, stream pb.MeshService_EpochStreamServer) error { +func (s *MeshService) EpochStream(req *pb.EpochStreamRequest, stream pb.MeshService_EpochStreamServer) error { epoch := types.EpochID(req.Epoch) var ( sendErr error @@ -565,7 +578,7 @@ func (s MeshService) EpochStream(req *pb.EpochStreamRequest, stream pb.MeshServi return nil } -func (s MeshService) MalfeasanceQuery( +func (s *MeshService) MalfeasanceQuery( ctx context.Context, req *pb.MalfeasanceRequest, ) (*pb.MalfeasanceResponse, error) { @@ -587,7 +600,7 @@ func (s MeshService) MalfeasanceQuery( }, nil } -func (s MeshService) MalfeasanceStream( +func (s *MeshService) MalfeasanceStream( req *pb.MalfeasanceStreamRequest, stream pb.MeshService_MalfeasanceStreamServer, ) error { diff --git a/api/grpcserver/mesh_service_test.go b/api/grpcserver/mesh_service_test.go index 9ce543173f..57c1fc0241 100644 --- a/api/grpcserver/mesh_service_test.go +++ b/api/grpcserver/mesh_service_test.go @@ -158,9 +158,7 @@ func TestMeshService_MalfeasanceQuery(t *testing.T) { cfg, cleanup := launchServer(t, srv) t.Cleanup(cleanup) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := pb.NewMeshServiceClient(conn) nodeID, proof := BallotMalfeasance(t, db) @@ -213,7 +211,7 @@ func TestMeshService_MalfeasanceStream(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := pb.NewMeshServiceClient(conn) for range 10 { diff --git a/api/grpcserver/mocks.go b/api/grpcserver/mocks.go index ab849fa95c..1dab284f2b 100644 --- a/api/grpcserver/mocks.go +++ b/api/grpcserver/mocks.go @@ -990,6 +990,45 @@ func (c *MockatxProviderMaxHeightAtxCall) DoAndReturn(f func() (types.ATXID, err return c } +// Previous mocks base method. +func (m *MockatxProvider) Previous(id types.ATXID) ([]types.ATXID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Previous", id) + ret0, _ := ret[0].([]types.ATXID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Previous indicates an expected call of Previous. +func (mr *MockatxProviderMockRecorder) Previous(id any) *MockatxProviderPreviousCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Previous", reflect.TypeOf((*MockatxProvider)(nil).Previous), id) + return &MockatxProviderPreviousCall{Call: call} +} + +// MockatxProviderPreviousCall wrap *gomock.Call +type MockatxProviderPreviousCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockatxProviderPreviousCall) Return(arg0 []types.ATXID, arg1 error) *MockatxProviderPreviousCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockatxProviderPreviousCall) Do(f func(types.ATXID) ([]types.ATXID, error)) *MockatxProviderPreviousCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockatxProviderPreviousCall) DoAndReturn(f func(types.ATXID) ([]types.ATXID, error)) *MockatxProviderPreviousCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // MockpostState is a mock of postState interface. type MockpostState struct { ctrl *gomock.Controller diff --git a/api/grpcserver/node_service.go b/api/grpcserver/node_service.go index c3fb61e4c2..48261f5921 100644 --- a/api/grpcserver/node_service.go +++ b/api/grpcserver/node_service.go @@ -31,16 +31,16 @@ type NodeService struct { } // RegisterService registers this service with a grpc server instance. -func (s NodeService) RegisterService(server *grpc.Server) { +func (s *NodeService) RegisterService(server *grpc.Server) { pb.RegisterNodeServiceServer(server, s) } -func (s NodeService) RegisterHandlerService(mux *runtime.ServeMux) error { +func (s *NodeService) RegisterHandlerService(mux *runtime.ServeMux) error { return pb.RegisterNodeServiceHandlerServer(context.Background(), mux, s) } // String returns the name of this service. -func (s NodeService) String() string { +func (s *NodeService) String() string { return "NodeService" } @@ -64,7 +64,7 @@ func NewNodeService( } // Echo returns the response for an echo api request. It's used for E2E tests. -func (s NodeService) Echo(_ context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) { +func (s *NodeService) Echo(_ context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) { if in.Msg != nil { return &pb.EchoResponse{Msg: &pb.SimpleString{Value: in.Msg.Value}}, nil } @@ -72,14 +72,14 @@ func (s NodeService) Echo(_ context.Context, in *pb.EchoRequest) (*pb.EchoRespon } // Version returns the version of the node software as a semver string. -func (s NodeService) Version(context.Context, *emptypb.Empty) (*pb.VersionResponse, error) { +func (s *NodeService) Version(context.Context, *emptypb.Empty) (*pb.VersionResponse, error) { return &pb.VersionResponse{ VersionString: &pb.SimpleString{Value: s.appVersion}, }, nil } // Build returns the build of the node software. -func (s NodeService) Build(context.Context, *emptypb.Empty) (*pb.BuildResponse, error) { +func (s *NodeService) Build(context.Context, *emptypb.Empty) (*pb.BuildResponse, error) { return &pb.BuildResponse{ BuildString: &pb.SimpleString{Value: s.appCommit}, }, nil @@ -87,7 +87,7 @@ func (s NodeService) Build(context.Context, *emptypb.Empty) (*pb.BuildResponse, // Status returns a status object providing information about the connected peers, sync status, // current and verified layer. -func (s NodeService) Status(ctx context.Context, _ *pb.StatusRequest) (*pb.StatusResponse, error) { +func (s *NodeService) Status(ctx context.Context, _ *pb.StatusRequest) (*pb.StatusResponse, error) { curLayer, latestLayer, verifiedLayer := s.getLayers() return &pb.StatusResponse{ Status: &pb.NodeStatus{ @@ -100,7 +100,7 @@ func (s NodeService) Status(ctx context.Context, _ *pb.StatusRequest) (*pb.Statu }, nil } -func (s NodeService) NodeInfo(context.Context, *emptypb.Empty) (*pb.NodeInfoResponse, error) { +func (s *NodeService) NodeInfo(context.Context, *emptypb.Empty) (*pb.NodeInfoResponse, error) { return &pb.NodeInfoResponse{ Hrp: types.NetworkHRP(), FirstGenesis: types.FirstEffectiveGenesis().Uint32(), @@ -109,7 +109,7 @@ func (s NodeService) NodeInfo(context.Context, *emptypb.Empty) (*pb.NodeInfoResp }, nil } -func (s NodeService) getLayers() (curLayer, latestLayer, verifiedLayer uint32) { +func (s *NodeService) getLayers() (curLayer, latestLayer, verifiedLayer uint32) { // We cannot get meaningful data from the mesh during the genesis epochs since there are no blocks in these // epochs, so just return the current layer instead curLayerObj := s.genTime.CurrentLayer() @@ -127,7 +127,7 @@ func (s NodeService) getLayers() (curLayer, latestLayer, verifiedLayer uint32) { // STREAMS // StatusStream exposes a stream of node status updates. -func (s NodeService) StatusStream(_ *pb.StatusStreamRequest, stream pb.NodeService_StatusStreamServer) error { +func (s *NodeService) StatusStream(_ *pb.StatusStreamRequest, stream pb.NodeService_StatusStreamServer) error { var ( statusCh <-chan events.Status statusBufFull <-chan struct{} @@ -174,7 +174,7 @@ func (s NodeService) StatusStream(_ *pb.StatusStreamRequest, stream pb.NodeServi } // ErrorStream exposes a stream of node errors. -func (s NodeService) ErrorStream(_ *pb.ErrorStreamRequest, stream pb.NodeService_ErrorStreamServer) error { +func (s *NodeService) ErrorStream(_ *pb.ErrorStreamRequest, stream pb.NodeService_ErrorStreamServer) error { var ( errorsCh <-chan events.NodeError errorsBufFull <-chan struct{} diff --git a/api/grpcserver/node_service_test.go b/api/grpcserver/node_service_test.go index 57d0590bd2..6c0b2e6701 100644 --- a/api/grpcserver/node_service_test.go +++ b/api/grpcserver/node_service_test.go @@ -3,7 +3,6 @@ package grpcserver import ( "context" "testing" - "time" pb "github.com/spacemeshos/api/release/go/spacemesh/v1" "github.com/stretchr/testify/require" @@ -37,9 +36,7 @@ func setupNodeService(t *testing.T) (*nodeServiceConn, context.Context) { cfg, cleanup := launchServer(t, grpcService) t.Cleanup(cleanup) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := pb.NewNodeServiceClient(conn) return &nodeServiceConn{ diff --git a/api/grpcserver/post_info_service_test.go b/api/grpcserver/post_info_service_test.go index fab31cab0c..04ea43d578 100644 --- a/api/grpcserver/post_info_service_test.go +++ b/api/grpcserver/post_info_service_test.go @@ -41,7 +41,7 @@ func TestPostInfoService(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := pb.NewPostInfoServiceClient(conn) existingStates := map[types.IdentityDescriptor]types.PostState{ diff --git a/api/grpcserver/smesher_service.go b/api/grpcserver/smesher_service.go index c2815fb061..3a557f41b4 100644 --- a/api/grpcserver/smesher_service.go +++ b/api/grpcserver/smesher_service.go @@ -36,16 +36,16 @@ type SmesherService struct { } // RegisterService registers this service with a grpc server instance. -func (s SmesherService) RegisterService(server *grpc.Server) { +func (s *SmesherService) RegisterService(server *grpc.Server) { pb.RegisterSmesherServiceServer(server, s) } -func (s SmesherService) RegisterHandlerService(mux *runtime.ServeMux) error { +func (s *SmesherService) RegisterHandlerService(mux *runtime.ServeMux) error { return pb.RegisterSmesherServiceHandlerServer(context.Background(), mux, s) } // String returns the name of this service. -func (s SmesherService) String() string { +func (s *SmesherService) String() string { return "SmesherService" } @@ -74,7 +74,7 @@ func (s *SmesherService) SetPostServiceConfig(cfg activation.PostSupervisorConfi } // IsSmeshing reports whether the node is smeshing. -func (s SmesherService) IsSmeshing(context.Context, *emptypb.Empty) (*pb.IsSmeshingResponse, error) { +func (s *SmesherService) IsSmeshing(context.Context, *emptypb.Empty) (*pb.IsSmeshingResponse, error) { if s.sig == nil { return nil, status.Errorf(codes.FailedPrecondition, "node is not configured for supervised smeshing") } @@ -82,7 +82,7 @@ func (s SmesherService) IsSmeshing(context.Context, *emptypb.Empty) (*pb.IsSmesh } // StartSmeshing requests that the node begin smeshing. -func (s SmesherService) StartSmeshing( +func (s *SmesherService) StartSmeshing( ctx context.Context, in *pb.StartSmeshingRequest, ) (*pb.StartSmeshingResponse, error) { @@ -119,7 +119,7 @@ func (s SmesherService) StartSmeshing( }, nil } -func (s SmesherService) postSetupOpts(in *pb.PostSetupOpts) (activation.PostSetupOpts, error) { +func (s *SmesherService) postSetupOpts(in *pb.PostSetupOpts) (activation.PostSetupOpts, error) { if in == nil { return activation.PostSetupOpts{}, errors.New("`Opts` must be provided") } @@ -146,7 +146,7 @@ func (s SmesherService) postSetupOpts(in *pb.PostSetupOpts) (activation.PostSetu } // StopSmeshing requests that the node stop smeshing. -func (s SmesherService) StopSmeshing( +func (s *SmesherService) StopSmeshing( ctx context.Context, in *pb.StopSmeshingRequest, ) (*pb.StopSmeshingResponse, error) { @@ -164,11 +164,11 @@ func (s SmesherService) StopSmeshing( } // SmesherID returns the smesher ID of this node. -func (s SmesherService) SmesherID(context.Context, *emptypb.Empty) (*pb.SmesherIDResponse, error) { +func (s *SmesherService) SmesherID(context.Context, *emptypb.Empty) (*pb.SmesherIDResponse, error) { return nil, status.Errorf(codes.Unimplemented, "this endpoint has been deprecated, use `SmesherIDs` instead") } -func (s SmesherService) SmesherIDs(context.Context, *emptypb.Empty) (*pb.SmesherIDsResponse, error) { +func (s *SmesherService) SmesherIDs(context.Context, *emptypb.Empty) (*pb.SmesherIDsResponse, error) { ids := s.smeshingProvider.SmesherIDs() res := &pb.SmesherIDsResponse{} for _, id := range ids { @@ -178,12 +178,12 @@ func (s SmesherService) SmesherIDs(context.Context, *emptypb.Empty) (*pb.Smesher } // Coinbase returns the current coinbase setting of this node. -func (s SmesherService) Coinbase(context.Context, *emptypb.Empty) (*pb.CoinbaseResponse, error) { +func (s *SmesherService) Coinbase(context.Context, *emptypb.Empty) (*pb.CoinbaseResponse, error) { return &pb.CoinbaseResponse{AccountId: &pb.AccountId{Address: s.smeshingProvider.Coinbase().String()}}, nil } // SetCoinbase sets the current coinbase setting of this node. -func (s SmesherService) SetCoinbase(_ context.Context, in *pb.SetCoinbaseRequest) (*pb.SetCoinbaseResponse, error) { +func (s *SmesherService) SetCoinbase(_ context.Context, in *pb.SetCoinbaseRequest) (*pb.SetCoinbaseResponse, error) { if in.Id == nil { return nil, status.Errorf(codes.InvalidArgument, "`Id` must be provided") } @@ -200,17 +200,17 @@ func (s SmesherService) SetCoinbase(_ context.Context, in *pb.SetCoinbaseRequest } // MinGas returns the current mingas setting of this node. -func (s SmesherService) MinGas(context.Context, *emptypb.Empty) (*pb.MinGasResponse, error) { +func (s *SmesherService) MinGas(context.Context, *emptypb.Empty) (*pb.MinGasResponse, error) { return nil, status.Errorf(codes.Unimplemented, "this endpoint is not implemented") } // SetMinGas sets the mingas setting of this node. -func (s SmesherService) SetMinGas(context.Context, *pb.SetMinGasRequest) (*pb.SetMinGasResponse, error) { +func (s *SmesherService) SetMinGas(context.Context, *pb.SetMinGasRequest) (*pb.SetMinGasResponse, error) { return nil, status.Errorf(codes.Unimplemented, "this endpoint is not implemented") } // EstimatedRewards returns estimated smeshing rewards over the next epoch. -func (s SmesherService) EstimatedRewards( +func (s *SmesherService) EstimatedRewards( context.Context, *pb.EstimatedRewardsRequest, ) (*pb.EstimatedRewardsResponse, error) { @@ -218,13 +218,13 @@ func (s SmesherService) EstimatedRewards( } // PostSetupStatus returns post data status. -func (s SmesherService) PostSetupStatus(ctx context.Context, _ *emptypb.Empty) (*pb.PostSetupStatusResponse, error) { +func (s *SmesherService) PostSetupStatus(ctx context.Context, _ *emptypb.Empty) (*pb.PostSetupStatusResponse, error) { status := s.postSupervisor.Status() return &pb.PostSetupStatusResponse{Status: statusToPbStatus(status)}, nil } // PostSetupStatusStream exposes a stream of status updates during post setup. -func (s SmesherService) PostSetupStatusStream( +func (s *SmesherService) PostSetupStatusStream( _ *emptypb.Empty, stream pb.SmesherService_PostSetupStatusStreamServer, ) error { @@ -244,8 +244,8 @@ func (s SmesherService) PostSetupStatusStream( } } -// PostSetupComputeProviders returns a list of available Post setup compute providers. -func (s SmesherService) PostSetupProviders( +// PostSetupProviders returns a list of available Post setup compute providers. +func (s *SmesherService) PostSetupProviders( ctx context.Context, in *pb.PostSetupProvidersRequest, ) (*pb.PostSetupProvidersResponse, error) { @@ -279,7 +279,7 @@ func (s SmesherService) PostSetupProviders( } // PostConfig returns the Post protocol config. -func (s SmesherService) PostConfig(context.Context, *emptypb.Empty) (*pb.PostConfigResponse, error) { +func (s *SmesherService) PostConfig(context.Context, *emptypb.Empty) (*pb.PostConfigResponse, error) { cfg := s.postSupervisor.Config() return &pb.PostConfigResponse{ @@ -302,7 +302,7 @@ func statusToPbStatus(status *activation.PostSetupStatus) *pb.PostSetupStatus { var providerID *uint32 if status.LastOpts.ProviderID.Value() != nil { providerID = new(uint32) - *providerID = uint32(*status.LastOpts.ProviderID.Value()) + *providerID = *status.LastOpts.ProviderID.Value() } pbStatus.Opts = &pb.PostSetupOpts{ diff --git a/api/grpcserver/transaction_service.go b/api/grpcserver/transaction_service.go index 80f5155c9a..f5627fd27a 100644 --- a/api/grpcserver/transaction_service.go +++ b/api/grpcserver/transaction_service.go @@ -37,16 +37,16 @@ type TransactionService struct { } // RegisterService registers this service with a grpc server instance. -func (s TransactionService) RegisterService(server *grpc.Server) { +func (s *TransactionService) RegisterService(server *grpc.Server) { pb.RegisterTransactionServiceServer(server, s) } -func (s TransactionService) RegisterHandlerService(mux *runtime.ServeMux) error { +func (s *TransactionService) RegisterHandlerService(mux *runtime.ServeMux) error { return pb.RegisterTransactionServiceHandlerServer(context.Background(), mux, s) } // String returns the name of this service. -func (s TransactionService) String() string { +func (s *TransactionService) String() string { return "TransactionService" } @@ -69,7 +69,7 @@ func NewTransactionService( } } -func (s TransactionService) ParseTransaction( +func (s *TransactionService) ParseTransaction( ctx context.Context, in *pb.ParseTransactionRequest, ) (*pb.ParseTransactionResponse, error) { @@ -94,7 +94,7 @@ func (s TransactionService) ParseTransaction( } // SubmitTransaction allows a new tx to be submitted. -func (s TransactionService) SubmitTransaction( +func (s *TransactionService) SubmitTransaction( ctx context.Context, in *pb.SubmitTransactionRequest, ) (*pb.SubmitTransactionResponse, error) { @@ -129,7 +129,7 @@ func (s TransactionService) SubmitTransaction( // Get transaction and status for a given txid. It's not an error if we cannot find the tx, // we just return all nils. -func (s TransactionService) getTransactionAndStatus( +func (s *TransactionService) getTransactionAndStatus( txID types.TransactionID, ) (*types.Transaction, pb.TransactionState_TransactionState) { var state pb.TransactionState_TransactionState @@ -149,7 +149,7 @@ func (s TransactionService) getTransactionAndStatus( } // TransactionsState returns current tx data for one or more txs. -func (s TransactionService) TransactionsState( +func (s *TransactionService) TransactionsState( _ context.Context, in *pb.TransactionsStateRequest, ) (*pb.TransactionsStateResponse, error) { @@ -186,7 +186,7 @@ func (s TransactionService) TransactionsState( // STREAMS // TransactionsStateStream exposes a stream of tx data. -func (s TransactionService) TransactionsStateStream( +func (s *TransactionService) TransactionsStateStream( in *pb.TransactionsStateStreamRequest, stream pb.TransactionService_TransactionsStateStreamServer, ) error { @@ -338,7 +338,7 @@ func (s TransactionService) TransactionsStateStream( } // StreamResults allows to query historical results and subscribe to live data using the same filter. -func (s TransactionService) StreamResults( +func (s *TransactionService) StreamResults( in *pb.TransactionResultsRequest, stream pb.TransactionService_StreamResultsServer, ) error { diff --git a/api/grpcserver/transaction_service_test.go b/api/grpcserver/transaction_service_test.go index 9e97c0d538..c6b6165ea7 100644 --- a/api/grpcserver/transaction_service_test.go +++ b/api/grpcserver/transaction_service_test.go @@ -52,7 +52,7 @@ func TestTransactionService_StreamResults(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := pb.NewTransactionServiceClient(conn) t.Run("All", func(t *testing.T) { @@ -167,7 +167,7 @@ func BenchmarkStreamResults(b *testing.B) { cfg, cleanup := launchServer(b, svc) b.Cleanup(cleanup) - conn := dialGrpc(ctx, b, cfg) + conn := dialGrpc(b, cfg) client := pb.NewTransactionServiceClient(conn) b.Logf("setup took %s", time.Since(start)) @@ -218,13 +218,12 @@ func parseOk() parseExpectation { func TestParseTransactions(t *testing.T) { db := statesql.InMemory() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - t.Cleanup(cancel) + vminst := vm.New(db) cfg, cleanup := launchServer(t, NewTransactionService(db, nil, nil, txs.NewConservativeState(vminst, db), nil, nil)) t.Cleanup(cleanup) var ( - conn = dialGrpc(ctx, t, cfg) + conn = dialGrpc(t, cfg) client = pb.NewTransactionServiceClient(conn) keys = make([]signing.PrivateKey, 4) accounts = make([]types.Account, len(keys)) @@ -233,7 +232,7 @@ func TestParseTransactions(t *testing.T) { for i := range keys { pub, priv, err := ed25519.GenerateKey(rng) require.NoError(t, err) - keys[i] = signing.PrivateKey(priv) + keys[i] = priv accounts[i] = types.Account{Address: wallet.Address(pub), Balance: 1e12} } require.NoError(t, vminst.ApplyGenesis(accounts)) diff --git a/api/grpcserver/v2alpha1/account.go b/api/grpcserver/v2alpha1/account.go index ee57ce6506..3bc9a30493 100644 --- a/api/grpcserver/v2alpha1/account.go +++ b/api/grpcserver/v2alpha1/account.go @@ -49,7 +49,7 @@ func (s *AccountService) String() string { } func (s *AccountService) List( - ctx context.Context, + _ context.Context, request *spacemeshv2alpha1.AccountRequest, ) (*spacemeshv2alpha1.AccountList, error) { switch { diff --git a/api/grpcserver/v2alpha1/account_test.go b/api/grpcserver/v2alpha1/account_test.go index fb876851e2..3b80a98969 100644 --- a/api/grpcserver/v2alpha1/account_test.go +++ b/api/grpcserver/v2alpha1/account_test.go @@ -66,7 +66,7 @@ func TestAccountService_List(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewAccountServiceClient(conn) t.Run("limit set too high", func(t *testing.T) { diff --git a/api/grpcserver/v2alpha1/activation_test.go b/api/grpcserver/v2alpha1/activation_test.go index a482f05b30..13bd09040f 100644 --- a/api/grpcserver/v2alpha1/activation_test.go +++ b/api/grpcserver/v2alpha1/activation_test.go @@ -37,7 +37,7 @@ func TestActivationService_List(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewActivationServiceClient(conn) t.Run("limit set too high", func(t *testing.T) { @@ -120,7 +120,7 @@ func TestActivationStreamService_Stream(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewActivationStreamServiceClient(conn) t.Run("all", func(t *testing.T) { @@ -240,7 +240,7 @@ func TestActivationService_ActivationsCount(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewActivationServiceClient(conn) t.Run("count without filter", func(t *testing.T) { diff --git a/api/grpcserver/v2alpha1/layer_test.go b/api/grpcserver/v2alpha1/layer_test.go index 2c21967c0d..6a63271d9f 100644 --- a/api/grpcserver/v2alpha1/layer_test.go +++ b/api/grpcserver/v2alpha1/layer_test.go @@ -41,7 +41,7 @@ func TestLayerService_List(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewLayerServiceClient(conn) t.Run("limit set too high", func(t *testing.T) { @@ -117,7 +117,7 @@ func TestLayerStreamService_Stream(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewLayerStreamServiceClient(conn) t.Run("all", func(t *testing.T) { diff --git a/api/grpcserver/v2alpha1/network.go b/api/grpcserver/v2alpha1/network.go index 60d4ad29ce..f9454394cc 100644 --- a/api/grpcserver/v2alpha1/network.go +++ b/api/grpcserver/v2alpha1/network.go @@ -11,17 +11,19 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/config" ) const ( Network = "network_v2alpha1" ) -func NewNetworkService(genesisTime time.Time, genesisID types.Hash20, layerDuration time.Duration) *NetworkService { +func NewNetworkService(genesisTime time.Time, config *config.Config) *NetworkService { return &NetworkService{ genesisTime: genesisTime, - genesisID: genesisID, - layerDuration: layerDuration, + genesisID: config.Genesis.GenesisID(), + layerDuration: config.LayerDuration, + labelsPerUnit: config.POST.LabelsPerUnit, } } @@ -29,6 +31,7 @@ type NetworkService struct { genesisTime time.Time genesisID types.Hash20 layerDuration time.Duration + labelsPerUnit uint64 } func (s *NetworkService) RegisterService(server *grpc.Server) { @@ -54,5 +57,6 @@ func (s *NetworkService) Info(context.Context, Hrp: types.NetworkHRP(), EffectiveGenesisLayer: types.GetEffectiveGenesis().Uint32(), LayersPerEpoch: types.GetLayersPerEpoch(), + LabelsPerUnit: s.labelsPerUnit, }, nil } diff --git a/api/grpcserver/v2alpha1/network_test.go b/api/grpcserver/v2alpha1/network_test.go index c0119ace71..0e87e3a7d1 100644 --- a/api/grpcserver/v2alpha1/network_test.go +++ b/api/grpcserver/v2alpha1/network_test.go @@ -9,17 +9,19 @@ import ( "github.com/stretchr/testify/require" "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/config" ) func TestNetworkService_Info(t *testing.T) { ctx := context.Background() genesis := time.Unix(genTimeUnix, 0) + c := config.DefaultTestConfig() - svc := NewNetworkService(genesis, genesisID, layerDuration) + svc := NewNetworkService(genesis, &c) cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewNetworkServiceClient(conn) t.Run("network info", func(t *testing.T) { @@ -27,10 +29,11 @@ func TestNetworkService_Info(t *testing.T) { require.NoError(t, err) require.Equal(t, genesis.UTC(), info.GenesisTime.AsTime().UTC()) - require.Equal(t, layerDuration, info.LayerDuration.AsDuration()) - require.Equal(t, genesisID.Bytes(), info.GenesisId) + require.Equal(t, c.LayerDuration, info.LayerDuration.AsDuration()) + require.Equal(t, c.Genesis.GenesisID().Bytes(), info.GenesisId) require.Equal(t, types.NetworkHRP(), info.Hrp) require.Equal(t, types.GetEffectiveGenesis().Uint32(), info.EffectiveGenesisLayer) require.Equal(t, types.GetLayersPerEpoch(), info.LayersPerEpoch) + require.Equal(t, c.POST.LabelsPerUnit, info.LabelsPerUnit) }) } diff --git a/api/grpcserver/v2alpha1/node_test.go b/api/grpcserver/v2alpha1/node_test.go index 10b93b0fb0..4ff5a85c27 100644 --- a/api/grpcserver/v2alpha1/node_test.go +++ b/api/grpcserver/v2alpha1/node_test.go @@ -31,7 +31,7 @@ func TestNodeService_Status(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewNodeServiceClient(conn) t.Run("node status", func(t *testing.T) { diff --git a/api/grpcserver/v2alpha1/reward.go b/api/grpcserver/v2alpha1/reward.go index d6f3b88a03..ea54c66cb7 100644 --- a/api/grpcserver/v2alpha1/reward.go +++ b/api/grpcserver/v2alpha1/reward.go @@ -165,7 +165,7 @@ func (s *RewardService) String() string { } func (s *RewardService) List( - ctx context.Context, + _ context.Context, request *spacemeshv2alpha1.RewardRequest, ) (*spacemeshv2alpha1.RewardList, error) { ops, err := toRewardOperations(request) diff --git a/api/grpcserver/v2alpha1/reward_test.go b/api/grpcserver/v2alpha1/reward_test.go index 2e16cab291..624b03e64a 100644 --- a/api/grpcserver/v2alpha1/reward_test.go +++ b/api/grpcserver/v2alpha1/reward_test.go @@ -35,7 +35,7 @@ func TestRewardService_List(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewRewardServiceClient(conn) t.Run("limit set too high", func(t *testing.T) { @@ -118,7 +118,7 @@ func TestRewardStreamService_Stream(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewRewardStreamServiceClient(conn) t.Run("all", func(t *testing.T) { diff --git a/api/grpcserver/v2alpha1/transaction_test.go b/api/grpcserver/v2alpha1/transaction_test.go index 0effec5797..8178e1c7d2 100644 --- a/api/grpcserver/v2alpha1/transaction_test.go +++ b/api/grpcserver/v2alpha1/transaction_test.go @@ -58,7 +58,7 @@ func TestTransactionService_List(t *testing.T) { cfg, cleanup := launchServer(t, svc) t.Cleanup(cleanup) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewTransactionServiceClient(conn) t.Run("limit set too high", func(t *testing.T) { @@ -201,7 +201,7 @@ func TestTransactionService_EstimateGas(t *testing.T) { []types.Transaction{{RawTx: types.NewRawTx(wallet.SelfSpawn(keys[0], 0))}}, nil) require.NoError(t, err) - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewTransactionServiceClient(conn) t.Run("valid tx", func(t *testing.T) { @@ -267,7 +267,7 @@ func TestTransactionService_ParseTransaction(t *testing.T) { mangled := wallet.Spend(keys[0], accounts[3].Address, 100, 0) mangled[len(mangled)-1] -= 1 - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) client := spacemeshv2alpha1.NewTransactionServiceClient(conn) t.Run("valid tx", func(t *testing.T) { @@ -364,7 +364,7 @@ func TestTransactionServiceSubmitUnsync(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := spacemeshv2alpha1.NewTransactionServiceClient(conn) signer, err := signing.NewEdSigner() @@ -407,7 +407,7 @@ func TestTransactionServiceSubmitInvalidTx(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := spacemeshv2alpha1.NewTransactionServiceClient(conn) signer, err := signing.NewEdSigner() @@ -444,7 +444,7 @@ func TestTransactionService_SubmitNoConcurrency(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - conn := dialGrpc(ctx, t, cfg) + conn := dialGrpc(t, cfg) c := spacemeshv2alpha1.NewTransactionServiceClient(conn) signer, err := signing.NewEdSigner() diff --git a/api/grpcserver/v2alpha1/v2alpha1_test.go b/api/grpcserver/v2alpha1/v2alpha1_test.go index 0be2834f50..52ba08f556 100644 --- a/api/grpcserver/v2alpha1/v2alpha1_test.go +++ b/api/grpcserver/v2alpha1/v2alpha1_test.go @@ -1,7 +1,6 @@ package v2alpha1 import ( - "context" "testing" "time" @@ -12,7 +11,6 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/spacemeshos/go-spacemesh/api/grpcserver" - "github.com/spacemeshos/go-spacemesh/common/types" ) const ( @@ -20,8 +18,6 @@ const ( layerDuration = 10 * time.Second ) -var genesisID = types.Hash20{} - func launchServer(tb testing.TB, services ...grpcserver.ServiceAPI) (grpcserver.Config, func()) { cfg := grpcserver.DefaultTestConfig() grpc, err := grpcserver.NewWithServices(cfg.PublicListener, zaptest.NewLogger(tb).Named("grpc"), cfg, services) @@ -36,7 +32,7 @@ func launchServer(tb testing.TB, services ...grpcserver.ServiceAPI) (grpcserver. return cfg, func() { assert.NoError(tb, grpc.Close()) } } -func dialGrpc(ctx context.Context, tb testing.TB, cfg grpcserver.Config) *grpc.ClientConn { +func dialGrpc(tb testing.TB, cfg grpcserver.Config) *grpc.ClientConn { tb.Helper() conn, err := grpc.NewClient( cfg.PublicListener, diff --git a/atxsdata/data.go b/atxsdata/data.go index 94c4cfc89d..6968c1180b 100644 --- a/atxsdata/data.go +++ b/atxsdata/data.go @@ -16,10 +16,6 @@ type ATX struct { Weight uint64 BaseHeight, Height uint64 Nonce types.VRFPostIndex - // unexported to avoid accidental unsynchronized access - // (this field is mutated by the Data under a lock and - // might only be safely read under the same lock) - malicious bool } func New() *Data { @@ -107,9 +103,6 @@ func (d *Data) AddAtx(target types.EpochID, id types.ATXID, atx *ATX) bool { atxsCounter.WithLabelValues(target.String()).Inc() ecache.index[id] = atx - if atx.malicious { - d.malicious[atx.Node] = struct{}{} - } return true } @@ -131,7 +124,9 @@ func (d *Data) Add( BaseHeight: baseHeight, Height: height, Nonce: nonce, - malicious: malicious, + } + if malicious { + d.SetMalicious(node) } if d.AddAtx(epoch, atxid, atx) { return atx @@ -165,8 +160,6 @@ func (d *Data) Get(epoch types.EpochID, atx types.ATXID) *ATX { if !exists { return nil } - _, exists = d.malicious[data.Node] - data.malicious = exists return data } @@ -185,10 +178,11 @@ type lockGuard struct{} // AtxFilter is a function that filters atxs. // The `lockGuard` prevents using the filter functions outside of the allowed context // to prevent data races. -type AtxFilter func(*ATX, lockGuard) bool +type AtxFilter func(*Data, *ATX, lockGuard) bool -func NotMalicious(data *ATX, _ lockGuard) bool { - return !data.malicious +func NotMalicious(d *Data, atx *ATX, _ lockGuard) bool { + _, m := d.malicious[atx.Node] + return !m } // IterateInEpoch calls `fn` for every ATX in epoch. @@ -202,12 +196,9 @@ func (d *Data) IterateInEpoch(epoch types.EpochID, fn func(types.ATXID, *ATX), f return } for id, atx := range ecache.index { - if _, exists := d.malicious[atx.Node]; exists { - atx.malicious = true - } ok := true for _, filter := range filters { - ok = ok && filter(atx, lockGuard{}) + ok = ok && filter(d, atx, lockGuard{}) } if ok { fn(id, atx) diff --git a/atxsdata/data_test.go b/atxsdata/data_test.go index 07111638bc..bb7f4ffc1a 100644 --- a/atxsdata/data_test.go +++ b/atxsdata/data_test.go @@ -42,14 +42,15 @@ func TestData(t *testing.T) { d.BaseHeight, d.Height, d.Nonce, - d.malicious, + false, ) } } for epoch := 0; epoch < epochs; epoch++ { for i := range atxids[epoch] { - byatxid := c.Get(types.EpochID(epoch)+1, atxids[epoch][i]) - require.Equal(t, &data[epoch][i], byatxid) + atx := c.Get(types.EpochID(epoch)+1, atxids[epoch][i]) + require.Equal(t, &data[epoch][i], atx) + require.False(t, c.IsMalicious(atx.Node)) } } } @@ -71,13 +72,9 @@ func TestData(t *testing.T) { ) data := c.Get(types.EpochID(epoch), types.ATXID{byte(epoch)}) require.NotNil(t, data) - require.False(t, data.malicious) + require.False(t, c.IsMalicious(data.Node)) } c.SetMalicious(node) - for epoch := 1; epoch <= 10; epoch++ { - data := c.Get(types.EpochID(epoch), types.ATXID{byte(epoch)}) - require.True(t, data.malicious) - } require.True(t, c.IsMalicious(node)) }) t.Run("eviction", func(t *testing.T) { diff --git a/atxsdata/warmup.go b/atxsdata/warmup.go index 557618dcef..f4ea14eefb 100644 --- a/atxsdata/warmup.go +++ b/atxsdata/warmup.go @@ -3,27 +3,31 @@ package atxsdata import ( "context" "fmt" + "time" + + "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/sql/identities" "github.com/spacemeshos/go-spacemesh/sql/layers" ) -func Warm(db sql.StateDatabase, keep types.EpochID) (*Data, error) { +func Warm(db sql.StateDatabase, keep types.EpochID, logger *zap.Logger) (*Data, error) { cache := New() tx, err := db.Tx(context.Background()) if err != nil { return nil, err } defer tx.Release() - if err := Warmup(tx, cache, keep); err != nil { + if err := Warmup(tx, cache, keep, logger); err != nil { return nil, fmt.Errorf("warmup %w", err) } return cache, nil } -func Warmup(db sql.Executor, cache *Data, keep types.EpochID) error { +func Warmup(db sql.Executor, cache *Data, keep types.EpochID, logger *zap.Logger) error { latest, err := atxs.LatestEpoch(db) if err != nil { return err @@ -38,7 +42,14 @@ func Warmup(db sql.Executor, cache *Data, keep types.EpochID) error { } cache.EvictEpoch(evict) - return atxs.IterateAtxsData(db, cache.Evicted(), latest, + from := cache.Evicted() + logger.Info("Reading ATXs from DB", + zap.Uint32("from epoch", from.Uint32()), + zap.Uint32("to epoch", latest.Uint32()), + ) + start := time.Now() + var processed int + err = atxs.IterateAtxsData(db, cache.Evicted(), latest, func( id types.ATXID, node types.NodeID, @@ -48,7 +59,6 @@ func Warmup(db sql.Executor, cache *Data, keep types.EpochID) error { base, height uint64, nonce types.VRFPostIndex, - malicious bool, ) bool { cache.Add( epoch+1, @@ -59,8 +69,26 @@ func Warmup(db sql.Executor, cache *Data, keep types.EpochID) error { base, height, nonce, - malicious, + false, ) + processed += 1 + if processed%1_000_000 == 0 { + logger.Debug("Processed 1M", zap.Int("total", processed)) + } return true }) + if err != nil { + return fmt.Errorf("warming up atxdata with ATXs: %w", err) + } + logger.Info("Finished reading ATXs. Starting reading malfeasance", zap.Duration("duration", time.Since(start))) + start = time.Now() + err = identities.IterateMalicious(db, func(_ int, id types.NodeID) error { + cache.SetMalicious(id) + return nil + }) + if err != nil { + return fmt.Errorf("warming up atxdata with malfeasance: %w", err) + } + logger.Info("Finished reading malfeasance", zap.Duration("duration", time.Since(start))) + return nil } diff --git a/atxsdata/warmup_test.go b/atxsdata/warmup_test.go index c052e87b96..bf14b257cf 100644 --- a/atxsdata/warmup_test.go +++ b/atxsdata/warmup_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/sql" @@ -54,21 +55,21 @@ func TestWarmup(t *testing.T) { } require.NoError(t, layers.SetApplied(db, applied, types.BlockID{1})) - c, err := Warm(db, 1) + c, err := Warm(db, 1, zaptest.NewLogger(t)) require.NoError(t, err) for _, atx := range data[2:] { require.NotNil(t, c.Get(atx.TargetEpoch(), atx.ID())) } }) t.Run("no data", func(t *testing.T) { - c, err := Warm(statesql.InMemory(), 1) + c, err := Warm(statesql.InMemory(), 1, zaptest.NewLogger(t)) require.NoError(t, err) require.NotNil(t, c) }) t.Run("closed db", func(t *testing.T) { db := statesql.InMemory() require.NoError(t, db.Close()) - c, err := Warm(db, 1) + c, err := Warm(db, 1, zaptest.NewLogger(t)) require.Error(t, err) require.Nil(t, c) }) @@ -95,7 +96,7 @@ func TestWarmup(t *testing.T) { AnyTimes() for range 3 { c := New() - require.Error(t, Warmup(exec, c, 1)) + require.Error(t, Warmup(exec, c, 1, zaptest.NewLogger(t))) fail++ call = 0 } diff --git a/beacon/beacon.go b/beacon/beacon.go index 3a0effae27..7175ede9a7 100644 --- a/beacon/beacon.go +++ b/beacon/beacon.go @@ -245,7 +245,7 @@ func (pd *ProtocolDriver) UpdateBeacon(epoch types.EpochID, beacon types.Beacon) pd.beacons[epoch] = beacon pd.logger.Info("using fallback beacon", zap.Uint32("epoch", epoch.Uint32()), - log.ZShortStringer("beacon", beacon), + zap.Stringer("beacon", beacon), ) pd.onResult(epoch, beacon) return nil @@ -263,13 +263,13 @@ func (pd *ProtocolDriver) Close() { pd.logger.Info("closing beacon protocol") pd.metricsCollector.Stop() close(pd.closed) - pd.logger.Info("waiting for beacon goroutines to finish") + pd.logger.Debug("waiting for beacon goroutines to finish") if err := pd.eg.Wait(); err != nil { - pd.logger.Info("received error waiting for goroutines to finish", zap.Error(err)) + pd.logger.Warn("received error waiting for goroutines to finish", zap.Error(err)) } close(pd.results) pd.results = nil - pd.logger.Info("beacon goroutines finished") + pd.logger.Debug("beacon goroutines finished") } func (pd *ProtocolDriver) onResult(epoch types.EpochID, beacon types.Beacon) { @@ -410,7 +410,7 @@ func (pd *ProtocolDriver) recordBeacon( pd.logger.Debug("added beacon from ballot", zap.Uint32("epoch", epochID.Uint32()), zap.Stringer("ballotID", ballot.ID()), - log.ZShortStringer("beacon", beacon), + zap.Stringer("beacon", beacon), zap.Stringer("weight_per", weightPer), zap.Int("num_eligibility", numEligibility), zap.Stringer("weight", ballotWeight), @@ -433,7 +433,7 @@ func (pd *ProtocolDriver) recordBeacon( pd.logger.Debug("added beacon from ballot", zap.Uint32("epoch", epochID.Uint32()), zap.Stringer("ballotID", ballot.ID()), - log.ZShortStringer("beacon", beacon), + zap.Stringer("beacon", beacon), zap.Stringer("weight_per", weightPer), zap.Int("num_eligibility", numEligibility), zap.Stringer("weight", ballotWeight), @@ -469,7 +469,7 @@ func (pd *ProtocolDriver) findMajorityBeacon(epoch types.EpochID) types.Beacon { for beacon, bw := range epochBeacons { if bw.totalWeight.GreaterThan(majorityWeight) { logger.Info("beacon determined for epoch", - log.ZShortStringer("beacon", beacon), + zap.Stringer("beacon", beacon), zap.Int("total_weight_unit", totalEligibility), zap.Stringer("total_weight", totalWeight), zap.Stringer("beacon_weight", bw.totalWeight), @@ -482,7 +482,7 @@ func (pd *ProtocolDriver) findMajorityBeacon(epoch types.EpochID) types.Beacon { } } logger.Warn("beacon determined for epoch by plurality, not majority", - log.ZShortStringer("beacon", bPlurality), + zap.Stringer("beacon", bPlurality), zap.Int("total_weight_unit", totalEligibility), zap.Stringer("total_weight", totalWeight), zap.Stringer("beacon_weight", maxWeight), @@ -539,7 +539,7 @@ func (pd *ProtocolDriver) setBeacon(targetEpoch types.EpochID, beacon types.Beac if err := beacons.Add(pd.cdb, targetEpoch, beacon); err != nil && !errors.Is(err, sql.ErrObjectExists) { pd.logger.Error("failed to persist beacon", zap.Uint32("target_epoch", targetEpoch.Uint32()), - log.ZShortStringer("beacon", beacon), + zap.Stringer("beacon", beacon), zap.Error(err), ) return fmt.Errorf("persist beacon: %w", err) @@ -651,7 +651,7 @@ func (pd *ProtocolDriver) initEpochStateIfNotPresent(logger *zap.Logger, target } } - logger.Info("selected active signers", + logger.Debug("selected active signers", zap.Int("count", len(active)), zap.Array("signers", zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { for _, p := range active { @@ -726,7 +726,7 @@ func (pd *ProtocolDriver) listenEpochs(ctx context.Context) { case <-pd.clock.AwaitLayer(layer): current := pd.clock.CurrentLayer() if current.Before(layer) { - pd.logger.Info("time sync detected, realigning Beacon") + pd.logger.Debug("time sync detected, realigning Beacon") continue } if !current.FirstInEpoch() { @@ -736,10 +736,7 @@ func (pd *ProtocolDriver) listenEpochs(ctx context.Context) { layer = epoch.Add(1).FirstLayer() pd.setProposalTimeForNextEpoch() - pd.logger.Info("processing epoch", - zap.Uint32("layer", current.Uint32()), - zap.Uint32("epoch", epoch.Uint32()), - ) + pd.logger.Info("processing epoch", zap.Uint32("epoch", epoch.Uint32())) pd.eg.Go(func() error { _ = pd.onNewEpoch(ctx, epoch) return nil @@ -794,7 +791,7 @@ func (pd *ProtocolDriver) onNewEpoch(ctx context.Context, epoch types.EpochID) e logger.Error("failed to set up epoch", zap.Error(err)) return err } - logger.Info("participating in beacon protocol") + logger.Debug("participating in beacon protocol") pd.runProtocol(ctx, epoch, s) return nil } @@ -837,7 +834,7 @@ func (pd *ProtocolDriver) runProtocol(ctx context.Context, epoch types.EpochID, return } - logger.Info("beacon set for epoch", log.ZShortStringer("beacon", beacon)) + logger.Info("beacon set for epoch", zap.Stringer("beacon", beacon)) } func calcBeacon(logger *zap.Logger, set proposalSet) types.Beacon { @@ -847,7 +844,7 @@ func calcBeacon(logger *zap.Logger, set proposalSet) types.Beacon { // to the same size as the proposal beacon := types.BytesToBeacon(allProposals.hash().Bytes()) logger.Info("calculated beacon", - log.ZShortStringer("beacon", beacon), + zap.Stringer("beacon", beacon), zap.Int("num_hashes", len(allProposals)), zap.Array("proposals", allProposals), ) @@ -859,7 +856,7 @@ func (pd *ProtocolDriver) runProposalPhase(ctx context.Context, epoch types.Epoc log.ZContext(ctx), zap.Uint32("epoch", epoch.Uint32()), ) - logger.Info("starting beacon proposal phase") + logger.Debug("starting beacon proposal phase") ctx, cancel := context.WithTimeout(ctx, pd.config.ProposalDuration) defer cancel() @@ -879,7 +876,7 @@ func (pd *ProtocolDriver) runProposalPhase(ctx context.Context, epoch types.Epoc finished := time.Now() pd.markProposalPhaseFinished(st, finished) - logger.Info("proposal phase finished", zap.Time("finished_at", finished)) + logger.Debug("proposal phase finished", zap.Time("finished_at", finished)) return nil } @@ -898,10 +895,7 @@ func (pd *ProtocolDriver) sendProposal( return } - logger := pd.logger.With( - log.ZContext(ctx), - zap.Uint32("epoch", epoch.Uint32()), - ) + logger := pd.logger.With(log.ZContext(ctx), zap.Uint32("epoch", epoch.Uint32())) vrfSig := buildSignedProposal(ctx, pd.logger, s.signer.VRFSigner(), epoch, s.nonce) proposal := ProposalFromVrf(vrfSig) m := ProposalMessage{ @@ -929,7 +923,7 @@ func (pd *ProtocolDriver) sendProposal( log.ZShortStringer("node_id", s.signer.NodeID()), ) } else { - logger.Info("beacon proposal sent", + logger.Debug("beacon proposal sent", zap.Inline(proposal), log.ZShortStringer("node_id", s.signer.NodeID()), ) @@ -939,7 +933,7 @@ func (pd *ProtocolDriver) sendProposal( // runConsensusPhase runs K voting rounds and returns result from last weak coin round. func (pd *ProtocolDriver) runConsensusPhase(ctx context.Context, epoch types.EpochID, st *state) (allVotes, error) { logger := pd.logger.With(log.ZContext(ctx), zap.Uint32("epoch", epoch.Uint32())) - logger.Info("starting consensus phase") + logger.Debug("starting consensus phase") // For K rounds: In each round that lasts δ, wait for votes to come in. // For next rounds, @@ -968,7 +962,7 @@ func (pd *ProtocolDriver) runConsensusPhase(ctx context.Context, epoch types.Epo if err := pd.sendFirstRoundVote(ctx, msg, session.signer); err != nil { logger.Error("failed to send proposal vote", zap.Error(err), - log.ZShortStringer("node_id", session.signer.NodeID()), + log.ZShortStringer("smesherID", session.signer.NodeID()), zap.Uint32("round", uint32(round)), ) } @@ -995,7 +989,7 @@ func (pd *ProtocolDriver) runConsensusPhase(ctx context.Context, epoch types.Epo if err := pd.sendFollowingVote(ctx, epoch, round, votes, session.signer); err != nil { logger.Error("failed to send following vote", zap.Error(err), - log.ZShortStringer("node_id", session.signer.NodeID()), + log.ZShortStringer("smesherID", session.signer.NodeID()), ) } return nil @@ -1044,7 +1038,7 @@ func (pd *ProtocolDriver) runConsensusPhase(ctx context.Context, epoch types.Epo tallyUndecided(&ownVotes, undecided, flip) } - logger.Info("consensus phase finished") + logger.Debug("consensus phase finished") return ownVotes, nil } @@ -1139,7 +1133,7 @@ func createProposalChecker(logger *zap.Logger, conf Config, numEarlyATXs, numATX high := atxThreshold(conf.Kappa, &conf.Q, numEarlyATXs) low := atxThreshold(conf.Kappa, &conf.Q, numATXs) - logger.Info("created proposal checker with ATX threshold", + logger.Debug("created proposal checker with ATX threshold", zap.Int("num_early_atxs", numEarlyATXs), zap.Int("num_atxs", numATXs), zap.Stringer("threshold", high), @@ -1256,7 +1250,7 @@ func (pd *ProtocolDriver) gatherMetricsData() ([]*metrics.BeaconStats, *metrics. for beacon, stats := range epochBeacons { stat := &metrics.BeaconStats{ Epoch: epoch, - Beacon: beacon.ShortString(), + Beacon: beacon.String(), Weight: stats.totalWeight, WeightUnit: stats.numEligibility, } @@ -1268,7 +1262,7 @@ func (pd *ProtocolDriver) gatherMetricsData() ([]*metrics.BeaconStats, *metrics. if beacon, ok := pd.beacons[epoch+1]; ok { calculated = &metrics.BeaconStats{ Epoch: epoch + 1, - Beacon: beacon.ShortString(), + Beacon: beacon.String(), } } diff --git a/beacon/beacon_test.go b/beacon/beacon_test.go index 2854269ffc..1ef3c134d5 100644 --- a/beacon/beacon_test.go +++ b/beacon/beacon_test.go @@ -449,8 +449,8 @@ func TestBeaconWithMetrics(t *testing.T) { spacemesh_beacons_beacon_observed_total{beacon="%s",epoch="%d"} %d spacemesh_beacons_beacon_observed_total{beacon="%s",epoch="%d"} %d `, - beacon1.ShortString(), thisEpoch, count, - beacon2.ShortString(), thisEpoch, count, + beacon1.String(), thisEpoch, count, + beacon2.String(), thisEpoch, count, ) err := testutil.GatherAndCompare( prometheus.DefaultGatherer, @@ -467,8 +467,8 @@ func TestBeaconWithMetrics(t *testing.T) { spacemesh_beacons_beacon_observed_weight{beacon="%s",epoch="%d"} %d spacemesh_beacons_beacon_observed_weight{beacon="%s",epoch="%d"} %d `, - beacon1.ShortString(), thisEpoch, weight, - beacon2.ShortString(), thisEpoch, weight, + beacon1.String(), thisEpoch, weight, + beacon2.String(), thisEpoch, weight, ) err = testutil.GatherAndCompare( prometheus.DefaultGatherer, diff --git a/beacon/weakcoin/weak_coin.go b/beacon/weakcoin/weak_coin.go index 45efe03252..444f9bc664 100644 --- a/beacon/weakcoin/weak_coin.go +++ b/beacon/weakcoin/weak_coin.go @@ -219,7 +219,7 @@ type Participant struct { // StartRound process any buffered messages for this round and broadcast our proposal. func (wc *WeakCoin) StartRound(ctx context.Context, round types.RoundID, participants []Participant) { wc.mu.Lock() - wc.logger.Info("started beacon weak coin round", + wc.logger.Debug("started beacon weak coin round", log.ZContext(ctx), zap.Uint32("epoch", wc.epoch.Uint32()), zap.Uint32("round", uint32(round)), @@ -363,7 +363,7 @@ func (wc *WeakCoin) FinishRound(ctx context.Context) { coinflip := wc.smallest.LSB() == 1 wc.coins[wc.round] = coinflip - wc.logger.Info("completed round with beacon weak coin", + wc.logger.Debug("completed round with beacon weak coin", log.ZContext(ctx), zap.Uint32("epoch", wc.epoch.Uint32()), zap.Uint32("round", uint32(wc.round)), diff --git a/blocks/generator.go b/blocks/generator.go index 6c63db247f..1c7ab6b196 100644 --- a/blocks/generator.go +++ b/blocks/generator.go @@ -13,8 +13,8 @@ import ( "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/hare3" "github.com/spacemeshos/go-spacemesh/hare3/eligibility" + "github.com/spacemeshos/go-spacemesh/hare4" "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/proposals/store" "github.com/spacemeshos/go-spacemesh/sql" @@ -39,7 +39,7 @@ type Generator struct { cert certifier patrol layerPatrol - hareCh <-chan hare3.ConsensusOutput + hareCh <-chan hare4.ConsensusOutput optimisticOutput map[types.LayerID]*proposalMetadata } @@ -76,7 +76,7 @@ func WithGeneratorLogger(logger *zap.Logger) GeneratorOpt { } // WithHareOutputChan sets the chan to listen to hare output. -func WithHareOutputChan(ch <-chan hare3.ConsensusOutput) GeneratorOpt { +func WithHareOutputChan(ch <-chan hare4.ConsensusOutput) GeneratorOpt { return func(g *Generator) { g.hareCh = ch } @@ -154,7 +154,7 @@ func (g *Generator) run(ctx context.Context) error { _, err := g.processHareOutput(ctx, out) if err != nil { if errors.Is(err, errNodeHasBadMeshHash) { - g.logger.Info("node has different mesh hash from majority, will download block instead", + g.logger.Debug("node has different mesh hash from majority, will download block instead", log.ZContext(ctx), zap.Uint32("layer_id", out.Layer.Uint32()), zap.Error(err), @@ -178,7 +178,7 @@ func (g *Generator) run(ctx context.Context) error { } } -func (g *Generator) processHareOutput(ctx context.Context, out hare3.ConsensusOutput) (*types.Block, error) { +func (g *Generator) processHareOutput(ctx context.Context, out hare4.ConsensusOutput) (*types.Block, error) { var md *proposalMetadata if len(out.Proposals) > 0 { getMetadata := func() error { diff --git a/blocks/generator_test.go b/blocks/generator_test.go index 211a985512..6a6bcec958 100644 --- a/blocks/generator_test.go +++ b/blocks/generator_test.go @@ -20,8 +20,8 @@ import ( "github.com/spacemeshos/go-spacemesh/blocks/mocks" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/genvm/sdk/wallet" - "github.com/spacemeshos/go-spacemesh/hare3" "github.com/spacemeshos/go-spacemesh/hare3/eligibility" + "github.com/spacemeshos/go-spacemesh/hare4" "github.com/spacemeshos/go-spacemesh/proposals/store" "github.com/spacemeshos/go-spacemesh/signing" "github.com/spacemeshos/go-spacemesh/sql" @@ -57,13 +57,13 @@ type testGenerator struct { mockFetch *smocks.MockProposalFetcher mockCert *mocks.Mockcertifier mockPatrol *mocks.MocklayerPatrol - hareCh chan hare3.ConsensusOutput + hareCh chan hare4.ConsensusOutput } func createTestGenerator(t *testing.T) *testGenerator { types.SetLayersPerEpoch(3) ctrl := gomock.NewController(t) - ch := make(chan hare3.ConsensusOutput, 100) + ch := make(chan hare4.ConsensusOutput, 100) tg := &testGenerator{ mockMesh: mocks.NewMockmeshProvider(ctrl), mockExec: mocks.NewMockexecutor(ctrl), @@ -272,7 +272,7 @@ func genData( store *store.Store, lid types.LayerID, optimistic bool, -) hare3.ConsensusOutput { +) hare4.ConsensusOutput { numTXs := 1000 numProposals := 10 txIDs := createAndSaveTxs(t, numTXs, db) @@ -284,7 +284,7 @@ func genData( } require.NoError(t, layers.SetMeshHash(db, lid.Sub(1), meshHash)) plist := createProposals(t, db, store, lid, meshHash, signers, activeSet, txIDs) - return hare3.ConsensusOutput{ + return hare4.ConsensusOutput{ Layer: lid, Proposals: types.ToProposalIDs(plist), } @@ -449,7 +449,7 @@ func Test_run(t *testing.T) { }) tg.mockPatrol.EXPECT().CompleteHare(layerID) tg.Start(context.Background()) - tg.hareCh <- hare3.ConsensusOutput{Layer: layerID, Proposals: pids} + tg.hareCh <- hare4.ConsensusOutput{Layer: layerID, Proposals: pids} require.Eventually(t, func() bool { return len(tg.hareCh) == 0 }, time.Second, 100*time.Millisecond) tg.Stop() }) @@ -465,7 +465,7 @@ func Test_processHareOutput_EmptyOutput(t *testing.T) { tg.mockCert.EXPECT().CertifyIfEligible(gomock.Any(), layerID, types.EmptyBlockID) tg.mockMesh.EXPECT().ProcessLayerPerHareOutput(gomock.Any(), layerID, types.EmptyBlockID, false) tg.mockPatrol.EXPECT().CompleteHare(layerID) - tg.hareCh <- hare3.ConsensusOutput{Layer: layerID} + tg.hareCh <- hare4.ConsensusOutput{Layer: layerID} require.Eventually(t, func() bool { return len(tg.hareCh) == 0 }, time.Second, 100*time.Millisecond) tg.Stop() } @@ -481,7 +481,7 @@ func Test_run_FetchFailed(t *testing.T) { return errors.New("unknown") }) tg.mockPatrol.EXPECT().CompleteHare(layerID) - tg.hareCh <- hare3.ConsensusOutput{Layer: layerID, Proposals: pids} + tg.hareCh <- hare4.ConsensusOutput{Layer: layerID, Proposals: pids} require.Eventually(t, func() bool { return len(tg.hareCh) == 0 }, time.Second, 100*time.Millisecond) tg.Stop() } @@ -503,7 +503,7 @@ func Test_run_DiffHasFromConsensus(t *testing.T) { tg.mockFetch.EXPECT().GetProposals(gomock.Any(), pids) tg.mockPatrol.EXPECT().CompleteHare(layerID) - tg.hareCh <- hare3.ConsensusOutput{Layer: layerID, Proposals: pids} + tg.hareCh <- hare4.ConsensusOutput{Layer: layerID, Proposals: pids} require.Eventually(t, func() bool { return len(tg.hareCh) == 0 }, time.Second, 100*time.Millisecond) tg.Stop() } @@ -536,7 +536,7 @@ func Test_run_ExecuteFailed(t *testing.T) { return nil, errors.New("unknown") }) tg.mockPatrol.EXPECT().CompleteHare(layerID) - tg.hareCh <- hare3.ConsensusOutput{Layer: layerID, Proposals: pids} + tg.hareCh <- hare4.ConsensusOutput{Layer: layerID, Proposals: pids} require.Eventually(t, func() bool { return len(tg.hareCh) == 0 }, time.Second, 100*time.Millisecond) tg.Stop() } @@ -561,7 +561,7 @@ func Test_run_AddBlockFailed(t *testing.T) { Return(block, nil) tg.mockMesh.EXPECT().AddBlockWithTXs(gomock.Any(), gomock.Any()).Return(errors.New("unknown")) tg.mockPatrol.EXPECT().CompleteHare(layerID) - tg.hareCh <- hare3.ConsensusOutput{Layer: layerID, Proposals: pids} + tg.hareCh <- hare4.ConsensusOutput{Layer: layerID, Proposals: pids} require.Eventually(t, func() bool { return len(tg.hareCh) == 0 }, time.Second, 100*time.Millisecond) tg.Stop() } @@ -589,7 +589,7 @@ func Test_run_RegisterCertFailureIgnored(t *testing.T) { tg.mockCert.EXPECT().CertifyIfEligible(gomock.Any(), layerID, gomock.Any()) tg.mockMesh.EXPECT().ProcessLayerPerHareOutput(gomock.Any(), layerID, block.ID(), true) tg.mockPatrol.EXPECT().CompleteHare(layerID) - tg.hareCh <- hare3.ConsensusOutput{Layer: layerID, Proposals: pids} + tg.hareCh <- hare4.ConsensusOutput{Layer: layerID, Proposals: pids} require.Eventually(t, func() bool { return len(tg.hareCh) == 0 }, time.Second, 100*time.Millisecond) tg.Stop() } @@ -617,7 +617,7 @@ func Test_run_CertifyFailureIgnored(t *testing.T) { tg.mockCert.EXPECT().CertifyIfEligible(gomock.Any(), layerID, gomock.Any()).Return(errors.New("unknown")) tg.mockMesh.EXPECT().ProcessLayerPerHareOutput(gomock.Any(), layerID, block.ID(), true) tg.mockPatrol.EXPECT().CompleteHare(layerID) - tg.hareCh <- hare3.ConsensusOutput{Layer: layerID, Proposals: pids} + tg.hareCh <- hare4.ConsensusOutput{Layer: layerID, Proposals: pids} require.Eventually(t, func() bool { return len(tg.hareCh) == 0 }, time.Second, 100*time.Millisecond) tg.Stop() } @@ -647,7 +647,7 @@ func Test_run_ProcessLayerFailed(t *testing.T) { ProcessLayerPerHareOutput(gomock.Any(), layerID, block.ID(), true). Return(errors.New("unknown")) tg.mockPatrol.EXPECT().CompleteHare(layerID) - tg.hareCh <- hare3.ConsensusOutput{Layer: layerID, Proposals: pids} + tg.hareCh <- hare4.ConsensusOutput{Layer: layerID, Proposals: pids} require.Eventually(t, func() bool { return len(tg.hareCh) == 0 }, time.Second, 100*time.Millisecond) tg.Stop() } @@ -674,7 +674,7 @@ func Test_processHareOutput_UnequalHeight(t *testing.T) { activeSet := types.ToATXIDs(atxes) pList := createProposals(t, tg.db, tg.proposals, layerID, types.Hash32{}, signers, activeSet, nil) ctx := context.Background() - ho := hare3.ConsensusOutput{ + ho := hare4.ConsensusOutput{ Layer: layerID, Proposals: types.ToProposalIDs(pList), } @@ -731,7 +731,7 @@ func Test_processHareOutput_bad_state(t *testing.T) { []types.TransactionID{types.RandomTransactionID()}, 1, ) - ho := hare3.ConsensusOutput{ + ho := hare4.ConsensusOutput{ Layer: layerID, Proposals: types.ToProposalIDs([]*types.Proposal{p}), } @@ -760,7 +760,7 @@ func Test_processHareOutput_bad_state(t *testing.T) { 1, ) ctx := context.Background() - ho := hare3.ConsensusOutput{ + ho := hare4.ConsensusOutput{ Layer: layerID, Proposals: types.ToProposalIDs([]*types.Proposal{p}), } @@ -784,7 +784,7 @@ func Test_processHareOutput_EmptyProposals(t *testing.T) { plist = append(plist, p) } ctx := context.Background() - ho := hare3.ConsensusOutput{ + ho := hare4.ConsensusOutput{ Layer: lid, Proposals: types.ToProposalIDs(plist), } @@ -833,7 +833,7 @@ func Test_processHareOutput_StableBlockID(t *testing.T) { activeSet := types.ToATXIDs(atxes) plist := createProposals(t, tg.db, tg.proposals, layerID, types.Hash32{}, signers, activeSet, txIDs) ctx := context.Background() - ho1 := hare3.ConsensusOutput{ + ho1 := hare4.ConsensusOutput{ Layer: layerID, Proposals: types.ToProposalIDs(plist), } @@ -853,7 +853,7 @@ func Test_processHareOutput_StableBlockID(t *testing.T) { ordered := plist[numProposals/2 : numProposals] ordered = append(ordered, plist[0:numProposals/2]...) require.NotEqual(t, plist, ordered) - ho2 := hare3.ConsensusOutput{ + ho2 := hare4.ConsensusOutput{ Layer: layerID, Proposals: types.ToProposalIDs(ordered), } @@ -882,7 +882,7 @@ func Test_processHareOutput_SameATX(t *testing.T) { createProposal(t, tg.db, tg.proposals, activeSet, layerID, types.Hash32{}, atxID, signers[0], txIDs[0:500], 1), createProposal(t, tg.db, tg.proposals, activeSet, layerID, types.Hash32{}, atxID, signers[0], txIDs[400:], 1), } - ho := hare3.ConsensusOutput{ + ho := hare4.ConsensusOutput{ Layer: layerID, Proposals: types.ToProposalIDs(plist), } @@ -906,7 +906,7 @@ func Test_processHareOutput_EmptyATXID(t *testing.T) { txIDs, 1, ) plist = append(plist, p) - ho := hare3.ConsensusOutput{ + ho := hare4.ConsensusOutput{ Layer: layerID, Proposals: types.ToProposalIDs(plist), } @@ -929,7 +929,7 @@ func Test_processHareOutput_MultipleEligibilities(t *testing.T) { createProposal(t, tg.db, tg.proposals, activeSet, layerID, types.Hash32{}, atxes[2].ID(), signers[2], ids, 5), } ctx := context.Background() - ho := hare3.ConsensusOutput{ + ho := hare4.ConsensusOutput{ Layer: layerID, Proposals: types.ToProposalIDs(plist), } diff --git a/blocks/handler.go b/blocks/handler.go index 4ff6f9b30e..e892d7f1f3 100644 --- a/blocks/handler.go +++ b/blocks/handler.go @@ -88,7 +88,7 @@ func (h *Handler) HandleSyncedBlock(ctx context.Context, expHash types.Hash32, p return fmt.Errorf("%w: %s", pubsub.ErrValidationReject, err.Error()) } - logger = logger.With(zap.Stringer("block_id", b.ID()), zap.Uint32("layer_id", b.LayerIndex.Uint32())) + logger = logger.With(zap.Stringer("block_id", b.ID()), zap.Uint32("layer", b.LayerIndex.Uint32())) if exists, err := blocks.Has(h.db, b.ID()); err != nil { logger.Error("failed to check block exist", zap.Error(err)) @@ -96,7 +96,7 @@ func (h *Handler) HandleSyncedBlock(ctx context.Context, expHash types.Hash32, p logger.Debug("known block") return nil } - logger.Info("new block") + logger.Debug("new block") if missing := h.tortoise.GetMissingActiveSet(b.LayerIndex.GetEpoch(), toAtxIDs(b.Rewards)); len(missing) > 0 { h.fetcher.RegisterPeerHashes(peer, types.ATXIDsToHashes(missing)) diff --git a/blocks/utils.go b/blocks/utils.go index 4dfb25d1a1..b2d0362287 100644 --- a/blocks/utils.go +++ b/blocks/utils.go @@ -119,7 +119,7 @@ func getProposalMetadata( } } if majorityState == nil { - logger.Info("no consensus on mesh hash. NOT doing optimistic filtering", + logger.Debug("no consensus on mesh hash. NOT doing optimistic filtering", zap.Uint32("layer_id", lid.Uint32()), ) } else { @@ -164,7 +164,7 @@ func getBlockTXs( if err := txCache.BuildFromTXs(mtxs, blockSeed); err != nil { return nil, fmt.Errorf("build txs for block: %w", err) } - byAddrAndNonce := txCache.GetMempool(logger) + byAddrAndNonce := txCache.GetMempool() if len(byAddrAndNonce) == 0 { logger.Warn("no feasible txs for block") return nil, nil @@ -203,7 +203,7 @@ func prune( ) for idx, tid = range tids { if gasRemaining < txs.MinTXGas { - logger.Info("gas exhausted for block", + logger.Debug("gas exhausted for block", zap.Int("num_txs", idx), zap.Uint64("gas_left", gasRemaining), zap.Uint64("gas_limit", gasLimit), diff --git a/bootstrap/updater.go b/bootstrap/updater.go index 04f53cbdd2..9641aefb96 100644 --- a/bootstrap/updater.go +++ b/bootstrap/updater.go @@ -29,6 +29,7 @@ import ( "github.com/santhosh-tekuri/jsonschema/v5" "github.com/spf13/afero" + "go.uber.org/zap" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/common/types" @@ -71,7 +72,7 @@ func DefaultConfig() Config { type Updater struct { cfg Config - logger log.Log + logger *zap.Logger clock layerClock fs afero.Fs client *http.Client @@ -92,7 +93,7 @@ func WithConfig(cfg Config) Opt { } } -func WithLogger(logger log.Log) Opt { +func WithLogger(logger *zap.Logger) Opt { return func(u *Updater) { u.logger = logger } @@ -113,7 +114,7 @@ func WithHttpClient(c *http.Client) Opt { func New(clock layerClock, opts ...Opt) *Updater { u := &Updater{ cfg: DefaultConfig(), - logger: log.NewNop(), + logger: zap.NewNop(), clock: clock, fs: afero.NewOsFs(), client: &http.Client{}, @@ -149,7 +150,7 @@ func (u *Updater) Load(ctx context.Context) error { if err = u.updateAndNotify(ctx, verified); err != nil { return err } - u.logger.With().Info("loaded bootstrap file", log.Inline(verified)) + u.logger.Info("loaded bootstrap file", zap.Inline(verified)) u.addUpdate(verified.Data.Epoch, verified.Persisted[len(verified.Persisted)-suffixLen:]) } return nil @@ -165,14 +166,14 @@ func (u *Updater) Start() error { if err := u.Load(ctx); err != nil { return err } - u.logger.With().Info("start listening to update", - log.String("source", u.cfg.URL), - log.Duration("interval", u.cfg.Interval), + u.logger.Info("start listening to update", + zap.String("source", u.cfg.URL), + zap.Duration("interval", u.cfg.Interval), ) for { if err := u.DoIt(ctx); err != nil { updateFailureCount.Add(1) - u.logger.With().Debug("failed to get bootstrap update", log.Err(err)) + u.logger.Debug("failed to get bootstrap update", zap.Error(err)) } select { case <-u.stop: @@ -233,10 +234,10 @@ func (u *Updater) DoIt(ctx context.Context) error { current := u.clock.CurrentLayer().GetEpoch() defer func() { if err := u.prune(current); err != nil { - u.logger.With().Error("failed to prune", - log.Context(ctx), - log.Uint32("current epoch", current.Uint32()), - log.Err(err), + u.logger.Error("failed to prune", + log.ZContext(ctx), + zap.Uint32("current epoch", current.Uint32()), + zap.Error(err), ) } }() @@ -291,7 +292,7 @@ func (u *Updater) checkEpochUpdate( return nil, false, fmt.Errorf("persist bootstrap %s: %w", filename, err) } verified.Persisted = filename - u.logger.WithContext(ctx).With().Info("new bootstrap file", log.Inline(verified)) + u.logger.Info("new bootstrap file", log.ZContext(ctx), zap.Inline(verified)) if err = u.updateAndNotify(ctx, verified); err != nil { return verified, false, err } diff --git a/bootstrap/updater_test.go b/bootstrap/updater_test.go index caaacde979..4a3b712b36 100644 --- a/bootstrap/updater_test.go +++ b/bootstrap/updater_test.go @@ -15,11 +15,11 @@ import ( "github.com/spf13/afero" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/bootstrap" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/log/logtest" ) const ( @@ -92,7 +92,7 @@ func TestMain(m *testing.M) { func checkUpdate1(t *testing.T, got *bootstrap.VerifiedUpdate) { require.EqualValues(t, 1, got.Data.Epoch) - require.EqualValues(t, "0x6fe7c971", got.Data.Beacon.String()) + require.EqualValues(t, "6fe7c971", got.Data.Beacon.String()) require.Len(t, got.Data.ActiveSet, 2) require.Equal( t, @@ -124,7 +124,7 @@ func checkUpdate2(t *testing.T, got *bootstrap.VerifiedUpdate) { func checkUpdate3(t *testing.T, got *bootstrap.VerifiedUpdate) { require.EqualValues(t, 3, got.Data.Epoch) - require.EqualValues(t, "0xf70cf90b", got.Data.Beacon.String()) + require.EqualValues(t, "f70cf90b", got.Data.Beacon.String()) require.Nil(t, got.Data.ActiveSet) } @@ -196,7 +196,7 @@ func TestLoad(t *testing.T) { updater := bootstrap.New( mc, bootstrap.WithConfig(cfg), - bootstrap.WithLogger(logtest.New(t)), + bootstrap.WithLogger(zaptest.NewLogger(t)), bootstrap.WithFilesystem(fs), ) ch, err := updater.Subscribe() @@ -248,7 +248,7 @@ func TestLoadedNotDownloadedAgain(t *testing.T) { updater := bootstrap.New( mc, bootstrap.WithConfig(cfg), - bootstrap.WithLogger(logtest.New(t)), + bootstrap.WithLogger(zaptest.NewLogger(t)), bootstrap.WithFilesystem(fs), ) ch, err := updater.Subscribe() @@ -276,7 +276,7 @@ func TestStartClose(t *testing.T) { updater := bootstrap.New( mc, bootstrap.WithConfig(cfg), - bootstrap.WithLogger(logtest.New(t)), + bootstrap.WithLogger(zaptest.NewLogger(t)), bootstrap.WithFilesystem(fs), ) ch, err := updater.Subscribe() @@ -322,7 +322,7 @@ func TestPrune(t *testing.T) { updater := bootstrap.New( mc, bootstrap.WithConfig(cfg), - bootstrap.WithLogger(logtest.New(t)), + bootstrap.WithLogger(zaptest.NewLogger(t)), bootstrap.WithFilesystem(fs), bootstrap.WithHttpClient(ts.Client()), ) @@ -384,7 +384,7 @@ func TestDoIt(t *testing.T) { updater := bootstrap.New( mc, bootstrap.WithConfig(cfg), - bootstrap.WithLogger(logtest.New(t)), + bootstrap.WithLogger(zaptest.NewLogger(t)), bootstrap.WithFilesystem(fs), bootstrap.WithHttpClient(ts.Client()), ) @@ -421,7 +421,7 @@ func TestEmptyResponse(t *testing.T) { updater := bootstrap.New( mc, bootstrap.WithConfig(cfg), - bootstrap.WithLogger(logtest.New(t)), + bootstrap.WithLogger(zaptest.NewLogger(t)), bootstrap.WithFilesystem(fs), bootstrap.WithHttpClient(ts.Client()), ) @@ -492,7 +492,7 @@ func TestGetInvalidUpdate(t *testing.T) { updater := bootstrap.New( mc, bootstrap.WithConfig(cfg), - bootstrap.WithLogger(logtest.New(t)), + bootstrap.WithLogger(zaptest.NewLogger(t)), bootstrap.WithFilesystem(fs), bootstrap.WithHttpClient(ts.Client()), ) @@ -526,7 +526,7 @@ func TestNoNewUpdate(t *testing.T) { updater := bootstrap.New( mc, bootstrap.WithConfig(cfg), - bootstrap.WithLogger(logtest.New(t)), + bootstrap.WithLogger(zaptest.NewLogger(t)), bootstrap.WithFilesystem(fs), bootstrap.WithHttpClient(ts.Client()), ) @@ -647,7 +647,7 @@ func TestRequiredEpochs(t *testing.T) { updater := bootstrap.New( mc, bootstrap.WithConfig(cfg), - bootstrap.WithLogger(logtest.New(t)), + bootstrap.WithLogger(zaptest.NewLogger(t)), bootstrap.WithFilesystem(fs), bootstrap.WithHttpClient(ts.Client()), ) @@ -676,7 +676,7 @@ func TestIntegration(t *testing.T) { updater := bootstrap.New( mc, bootstrap.WithConfig(cfg), - bootstrap.WithLogger(logtest.New(t)), + bootstrap.WithLogger(zaptest.NewLogger(t)), bootstrap.WithFilesystem(fs), ) ch, err := updater.Subscribe() @@ -709,7 +709,7 @@ func TestClose(t *testing.T) { updater := bootstrap.New( mc, bootstrap.WithConfig(cfg), - bootstrap.WithLogger(logtest.New(t)), + bootstrap.WithLogger(zaptest.NewLogger(t)), bootstrap.WithFilesystem(fs), ) ch, err := updater.Subscribe() diff --git a/checkpoint/recovery.go b/checkpoint/recovery.go index 8348289c8d..652eda2cd4 100644 --- a/checkpoint/recovery.go +++ b/checkpoint/recovery.go @@ -356,6 +356,10 @@ func checkpointData(fs afero.Fs, file string, newGenesis types.LayerID) (*recove cAtx.ID = types.ATXID(types.BytesToHash(atx.ID)) cAtx.Epoch = types.EpochID(atx.Epoch) cAtx.CommitmentATX = types.ATXID(types.BytesToHash(atx.CommitmentAtx)) + if len(atx.MarriageAtx) == 32 { + marriageATXID := types.ATXID(atx.MarriageAtx) + cAtx.MarriageATX = &marriageATXID + } cAtx.SmesherID = types.BytesToNodeID(atx.PublicKey) cAtx.NumUnits = atx.NumUnits cAtx.VRFNonce = types.VRFPostIndex(atx.VrfNonce) diff --git a/checkpoint/recovery_test.go b/checkpoint/recovery_test.go index 0fa8e530d3..eb0e6d0a58 100644 --- a/checkpoint/recovery_test.go +++ b/checkpoint/recovery_test.go @@ -909,7 +909,8 @@ func TestRecover_OwnAtxInCheckpoint(t *testing.T) { require.NoError(t, err) atxid, err := hex.DecodeString("98e47278c1f58acfd2b670a730f28898f74eb140482a07b91ff81f9ff0b7d9f4") require.NoError(t, err) - atx := newAtx(types.ATXID(atxid), types.EmptyATXID, nil, 3, 1, 0, nid) + atx := &types.ActivationTx{SmesherID: types.NodeID(nid)} + atx.SetID(types.ATXID(atxid)) cfg := &checkpoint.RecoverConfig{ GoldenAtx: goldenAtx, diff --git a/checkpoint/runner.go b/checkpoint/runner.go index 29b32ec156..965dd1ca00 100644 --- a/checkpoint/runner.go +++ b/checkpoint/runner.go @@ -78,10 +78,15 @@ func checkpointDB( if mal, ok := malicious[catx.SmesherID]; ok && mal { continue } + var marriageAtx []byte + if catx.MarriageATX != nil { + marriageAtx = catx.MarriageATX.Bytes() + } checkpoint.Data.Atxs = append(checkpoint.Data.Atxs, types.AtxSnapshot{ ID: catx.ID.Bytes(), Epoch: catx.Epoch.Uint32(), CommitmentAtx: catx.CommitmentATX.Bytes(), + MarriageAtx: marriageAtx, VrfNonce: uint64(catx.VRFNonce), NumUnits: catx.NumUnits, BaseTickHeight: catx.BaseTickHeight, diff --git a/checkpoint/runner_test.go b/checkpoint/runner_test.go index e01d9efe78..727da2843f 100644 --- a/checkpoint/runner_test.go +++ b/checkpoint/runner_test.go @@ -30,52 +30,57 @@ func TestMain(m *testing.M) { os.Exit(res) } +type activationTx struct { + *types.ActivationTx + previous types.ATXID +} + type miner struct { - atxs []*types.ActivationTx + atxs []activationTx malfeasanceProof []byte } var allMiners = []miner{ // smesher 1 has 7 ATXs, one in each epoch from 1 to 7 { - atxs: []*types.ActivationTx{ - newAtx(types.ATXID{17}, types.ATXID{16}, nil, 7, 6, 123, []byte("smesher1")), - newAtx(types.ATXID{16}, types.ATXID{15}, nil, 6, 5, 123, []byte("smesher1")), - newAtx(types.ATXID{15}, types.ATXID{14}, nil, 5, 4, 123, []byte("smesher1")), - newAtx(types.ATXID{14}, types.ATXID{13}, nil, 4, 3, 123, []byte("smesher1")), - newAtx(types.ATXID{13}, types.ATXID{12}, nil, 3, 2, 123, []byte("smesher1")), - newAtx(types.ATXID{12}, types.ATXID{11}, nil, 2, 1, 123, []byte("smesher1")), - newAtx(types.ATXID{11}, types.EmptyATXID, &types.ATXID{1}, 1, 0, 123, []byte("smesher1")), + atxs: []activationTx{ + {newAtx(types.ATXID{17}, nil, 7, 6, 123, []byte("smesher1")), types.ATXID{16}}, + {newAtx(types.ATXID{16}, nil, 6, 5, 123, []byte("smesher1")), types.ATXID{15}}, + {newAtx(types.ATXID{15}, nil, 5, 4, 123, []byte("smesher1")), types.ATXID{14}}, + {newAtx(types.ATXID{14}, nil, 4, 3, 123, []byte("smesher1")), types.ATXID{13}}, + {newAtx(types.ATXID{13}, nil, 3, 2, 123, []byte("smesher1")), types.ATXID{12}}, + {newAtx(types.ATXID{12}, nil, 2, 1, 123, []byte("smesher1")), types.ATXID{11}}, + {newAtx(types.ATXID{11}, &types.ATXID{1}, 1, 0, 123, []byte("smesher1")), types.EmptyATXID}, }, }, // smesher 2 has 1 ATX in epoch 7 { - atxs: []*types.ActivationTx{ - newAtx(types.ATXID{27}, types.EmptyATXID, &types.ATXID{2}, 7, 0, 152, []byte("smesher2")), + atxs: []activationTx{ + {newAtx(types.ATXID{27}, &types.ATXID{2}, 7, 0, 152, []byte("smesher2")), types.EmptyATXID}, }, }, // smesher 3 has 1 ATX in epoch 2 { - atxs: []*types.ActivationTx{ - newAtx(types.ATXID{32}, types.EmptyATXID, &types.ATXID{3}, 2, 0, 211, []byte("smesher3")), + atxs: []activationTx{ + {newAtx(types.ATXID{32}, &types.ATXID{3}, 2, 0, 211, []byte("smesher3")), types.EmptyATXID}, }, }, // smesher 4 has 1 ATX in epoch 3 and one in epoch 7 { - atxs: []*types.ActivationTx{ - newAtx(types.ATXID{47}, types.ATXID{43}, nil, 7, 1, 420, []byte("smesher4")), - newAtx(types.ATXID{43}, types.EmptyATXID, &types.ATXID{4}, 4, 0, 420, []byte("smesher4")), + atxs: []activationTx{ + {newAtx(types.ATXID{47}, nil, 7, 1, 420, []byte("smesher4")), types.ATXID{43}}, + {newAtx(types.ATXID{43}, &types.ATXID{4}, 4, 0, 420, []byte("smesher4")), types.EmptyATXID}, }, }, // smesher 5 is malicious and equivocated in epoch 7 { - atxs: []*types.ActivationTx{ - newAtx(types.ATXID{83}, types.EmptyATXID, &types.ATXID{27}, 7, 0, 113, []byte("smesher5")), - newAtx(types.ATXID{97}, types.EmptyATXID, &types.ATXID{16}, 7, 0, 113, []byte("smesher5")), + atxs: []activationTx{ + {newAtx(types.ATXID{83}, &types.ATXID{27}, 7, 0, 113, []byte("smesher5")), types.EmptyATXID}, + {newAtx(types.ATXID{97}, &types.ATXID{16}, 7, 0, 113, []byte("smesher5")), types.EmptyATXID}, }, malfeasanceProof: []byte("im bad"), }, @@ -182,7 +187,7 @@ func expectedCheckpoint(t testing.TB, snapshot types.LayerID, numAtxs int, miner for i := 0; i < n; i++ { atxData = append( atxData, - asAtxSnapshot(atxs[i], atxs[len(atxs)-1].CommitmentATX), + asAtxSnapshot(atxs[i].ActivationTx, atxs[len(atxs)-1].CommitmentATX), ) } } @@ -216,7 +221,7 @@ func expectedCheckpoint(t testing.TB, snapshot types.LayerID, numAtxs int, miner } func newAtx( - id, prevID types.ATXID, + id types.ATXID, commitAtx *types.ATXID, epoch uint32, seq, vrfnonce uint64, @@ -226,7 +231,6 @@ func newAtx( PublishEpoch: types.EpochID(epoch), Sequence: seq, CommitmentATX: commitAtx, - PrevATXID: prevID, NumUnits: 2, Coinbase: types.Address{1, 2, 3}, TickCount: 1, @@ -239,10 +243,15 @@ func newAtx( } func asAtxSnapshot(v *types.ActivationTx, cmt *types.ATXID) types.AtxSnapshot { + var marriageATX []byte + if v.MarriageATX != nil { + marriageATX = v.MarriageATX.Bytes() + } return types.AtxSnapshot{ ID: v.ID().Bytes(), Epoch: v.PublishEpoch.Uint32(), CommitmentAtx: cmt.Bytes(), + MarriageAtx: marriageATX, VrfNonce: uint64(v.VRFNonce), NumUnits: v.NumUnits, BaseTickHeight: v.BaseTickHeight, @@ -258,8 +267,8 @@ func createMesh(t testing.TB, db sql.StateDatabase, miners []miner, accts []*typ t.Helper() for _, miner := range miners { for _, atx := range miner.atxs { - require.NoError(t, atxs.Add(db, atx, types.AtxBlob{})) - require.NoError(t, atxs.SetUnits(db, atx.ID(), atx.SmesherID, atx.NumUnits)) + require.NoError(t, atxs.Add(db, atx.ActivationTx, types.AtxBlob{})) + require.NoError(t, atxs.SetPost(db, atx.ID(), atx.previous, 0, atx.SmesherID, atx.NumUnits)) } if proof := miner.malfeasanceProof; len(proof) > 0 { require.NoError(t, identities.SetMalicious(db, miner.atxs[0].SmesherID, proof, time.Now())) @@ -340,8 +349,8 @@ func TestRunner_Generate_Error(t *testing.T) { db := statesql.InMemory() snapshot := types.LayerID(5) - atx := newAtx(types.ATXID{13}, types.EmptyATXID, nil, 2, 1, 11, types.RandomNodeID().Bytes()) - createMesh(t, db, []miner{{atxs: []*types.ActivationTx{atx}}}, allAccounts) + atx := newAtx(types.ATXID{13}, nil, 2, 1, 11, types.RandomNodeID().Bytes()) + createMesh(t, db, []miner{{atxs: []activationTx{{atx, types.EmptyATXID}}}}, allAccounts) fs := afero.NewMemMapFs() dir, err := afero.TempDir(fs, "", "Generate") @@ -376,3 +385,35 @@ func TestRunner_Generate_Error(t *testing.T) { require.Error(t, err) }) } + +func TestRunner_Generate_PreservesMarriageATX(t *testing.T) { + t.Parallel() + db := statesql.InMemory() + + require.NoError(t, accounts.Update(db, &types.Account{Address: types.Address{1, 1}})) + + atx := &types.ActivationTx{ + CommitmentATX: &types.ATXID{1, 2, 3, 4, 5}, + MarriageATX: &types.ATXID{6, 7, 8, 9}, + SmesherID: types.RandomNodeID(), + NumUnits: 4, + } + atx.SetID(types.RandomATXID()) + require.NoError(t, atxs.Add(db, atx, types.AtxBlob{})) + require.NoError(t, atxs.SetPost(db, atx.ID(), types.EmptyATXID, 0, atx.SmesherID, atx.NumUnits)) + + fs := afero.NewMemMapFs() + dir, err := afero.TempDir(fs, "", "Generate") + require.NoError(t, err) + + err = checkpoint.Generate(context.Background(), fs, db, dir, 5, 2) + require.NoError(t, err) + + file, err := fs.Open(checkpoint.SelfCheckpointFilename(dir, 5)) + require.NoError(t, err) + defer file.Close() + + var checkpoint types.Checkpoint + require.NoError(t, json.NewDecoder(file).Decode(&checkpoint)) + require.Equal(t, atx.MarriageATX.Bytes(), checkpoint.Data.Atxs[0].MarriageAtx) +} diff --git a/cmd/bootstrapper/bootstrapper.go b/cmd/bootstrapper/bootstrapper.go index 299d72d8ce..af6bf53e17 100644 --- a/cmd/bootstrapper/bootstrapper.go +++ b/cmd/bootstrapper/bootstrapper.go @@ -23,7 +23,6 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/log" ) const ( @@ -89,16 +88,26 @@ var cmd = &cobra.Command{ targetEpochs = append(targetEpochs, types.EpochID(epoch)) } - log.JSONLog(true) - lvl, err := zap.ParseAtomicLevel(strings.ToLower(logLevel)) + lvl, err := zap.ParseAtomicLevel(logLevel) if err != nil { return err } - logger := log.NewWithLevel("", lvl) + + logger, err := zap.Config{ + Level: lvl, + Encoding: "json", + EncoderConfig: zap.NewProductionEncoderConfig(), + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + }.Build() + if err != nil { + return fmt.Errorf("creating logger: %w", err) + } + g := NewGenerator( bitcoinEndpoint, spacemeshEndpoint, - WithLogger(logger.WithName("generator")), + WithLogger(logger.Named("generator")), ) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) @@ -106,7 +115,7 @@ var cmd = &cobra.Command{ if serveUpdate { srv := NewServer(g, genFallback, port, WithSrvFilesystem(afero.NewOsFs()), - WithSrvLogger(logger.WithName("server")), + WithSrvLogger(logger.Named("server")), WithBootstrapEpochs(targetEpochs), ) return runServer(ctx, srv) diff --git a/cmd/bootstrapper/generator.go b/cmd/bootstrapper/generator.go index c6643885b4..0c8ac6456f 100644 --- a/cmd/bootstrapper/generator.go +++ b/cmd/bootstrapper/generator.go @@ -16,12 +16,12 @@ import ( pb "github.com/spacemeshos/api/release/go/spacemesh/v1" "github.com/spf13/afero" + "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/spacemeshos/go-spacemesh/bootstrap" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/log" ) const ( @@ -35,7 +35,7 @@ func PersistedFilename(epoch types.EpochID, suffix string) string { } type Generator struct { - logger log.Log + logger *zap.Logger fs afero.Fs client *http.Client btcEndpoint string @@ -44,7 +44,7 @@ type Generator struct { type Opt func(*Generator) -func WithLogger(logger log.Log) Opt { +func WithLogger(logger *zap.Logger) Opt { return func(g *Generator) { g.logger = logger } @@ -64,7 +64,7 @@ func WithHttpClient(c *http.Client) Opt { func NewGenerator(btcEndpoint, smEndpoint string, opts ...Opt) *Generator { g := &Generator{ - logger: log.NewNop(), + logger: zap.NewNop(), fs: afero.NewOsFs(), client: &http.Client{}, btcEndpoint: btcEndpoint, @@ -125,7 +125,7 @@ type BitcoinResponse struct { Hash string `json:"hash"` } -func (g *Generator) genBeacon(ctx context.Context, logger log.Log) (types.Beacon, error) { +func (g *Generator) genBeacon(ctx context.Context, logger *zap.Logger) (types.Beacon, error) { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() br, err := bitcoinHash(ctx, logger, g.client, g.btcEndpoint) @@ -142,15 +142,17 @@ func (g *Generator) genBeacon(ctx context.Context, logger log.Log) (types.Beacon return beacon, nil } -func bitcoinHash(ctx context.Context, logger log.Log, client *http.Client, targetUrl string) (*BitcoinResponse, error) { +func bitcoinHash( + ctx context.Context, + logger *zap.Logger, + client *http.Client, + targetUrl string, +) (*BitcoinResponse, error) { latest, err := queryBitcoin(ctx, client, targetUrl) if err != nil { return nil, err } - logger.With().Info("latest bitcoin block height", - log.Uint64("height", latest.Height), - log.String("hash", latest.Hash), - ) + logger.Info("latest bitcoin block height", zap.Uint64("height", latest.Height), zap.String("hash", latest.Hash)) height := latest.Height - confirmation blockUrl := fmt.Sprintf("%s/blocks/%d", targetUrl, height) @@ -158,10 +160,7 @@ func bitcoinHash(ctx context.Context, logger log.Log, client *http.Client, targe if err != nil { return nil, err } - logger.With().Info("confirmed bitcoin block", - log.Uint64("height", confirmed.Height), - log.String("hash", confirmed.Hash), - ) + logger.Info("confirmed bitcoin block", zap.Uint64("height", confirmed.Height), zap.String("hash", confirmed.Hash)) return confirmed, nil } @@ -257,8 +256,8 @@ func (g *Generator) GenUpdate( if err != nil { return "", fmt.Errorf("persist epoch update %v: %w", filename, err) } - g.logger.With().Info("generated update", - log.String("filename", filename), + g.logger.Info("generated update", + zap.String("filename", filename), ) return filename, nil } diff --git a/cmd/bootstrapper/generator_test.go b/cmd/bootstrapper/generator_test.go index e76451d50f..e201d9186b 100644 --- a/cmd/bootstrapper/generator_test.go +++ b/cmd/bootstrapper/generator_test.go @@ -22,7 +22,6 @@ import ( "github.com/spacemeshos/go-spacemesh/bootstrap" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" "github.com/spacemeshos/go-spacemesh/sql/statesql" @@ -72,7 +71,7 @@ func launchServer(tb testing.TB, cdb *datastore.CachedDB) (grpcserver.Config, fu // start gRPC and json servers err := grpcService.Start() require.NoError(tb, err) - err = jsonService.StartService(context.Background(), s) + err = jsonService.StartService(s) require.NoError(tb, err) // update config with bound addresses @@ -144,7 +143,7 @@ func TestGenerator_Generate(t *testing.T) { g := NewGenerator( ts.URL, cfg.PublicListener, - WithLogger(logtest.New(t)), + WithLogger(zaptest.NewLogger(t)), WithFilesystem(fs), WithHttpClient(ts.Client()), ) @@ -171,9 +170,9 @@ func TestGenerator_CheckAPI(t *testing.T) { t.Parallel() targetEpoch := types.EpochID(3) db := statesql.InMemory() - lg := logtest.New(t) + lg := zaptest.NewLogger(t) createAtxs(t, db, targetEpoch-1, types.RandomActiveSet(activeSetSize)) - cfg, cleanup := launchServer(t, datastore.NewCachedDB(db, lg.Zap())) + cfg, cleanup := launchServer(t, datastore.NewCachedDB(db, lg)) t.Cleanup(cleanup) fs := afero.NewMemMapFs() diff --git a/cmd/bootstrapper/server.go b/cmd/bootstrapper/server.go index bcc0da5416..a2c737451e 100644 --- a/cmd/bootstrapper/server.go +++ b/cmd/bootstrapper/server.go @@ -13,11 +13,11 @@ import ( "time" "github.com/spf13/afero" + "go.uber.org/zap" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/bootstrap" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/log" ) const fileRegex = "/epoch-(?P[0-9]+)-update-(?P[a-z]+)" @@ -45,7 +45,7 @@ func (np *NetworkParam) updateActiveSetTime(targetEpoch types.EpochID) time.Time type Server struct { *http.Server eg errgroup.Group - logger log.Log + logger *zap.Logger fs afero.Fs gen *Generator genFallback bool @@ -55,7 +55,7 @@ type Server struct { type SrvOpt func(*Server) -func WithSrvLogger(logger log.Log) SrvOpt { +func WithSrvLogger(logger *zap.Logger) SrvOpt { return func(s *Server) { s.logger = logger } @@ -76,7 +76,7 @@ func WithBootstrapEpochs(epochs []types.EpochID) SrvOpt { func NewServer(gen *Generator, fallback bool, port int, opts ...SrvOpt) *Server { s := &Server{ Server: &http.Server{Addr: fmt.Sprintf(":%d", port)}, - logger: log.NewNop(), + logger: zap.NewNop(), fs: afero.NewOsFs(), gen: gen, genFallback: fallback, @@ -94,47 +94,108 @@ func (s *Server) Start(ctx context.Context, errCh chan error, params *NetworkPar errCh <- fmt.Errorf("create persist dir %v: %w", dataDir, err) } s.eg.Go(func() error { - s.startHttp(errCh) + ln, err := net.Listen("tcp", s.Addr) + if err != nil { + errCh <- err + return err + } + http.HandleFunc("/", s.handle) + http.HandleFunc("/checkpoint", s.handleCheckpoint) + http.HandleFunc("/updateCheckpoint", s.handleUpdate) + s.logger.Info("server starts serving", zap.Stringer("addr", ln.Addr())) + if err = s.Serve(ln); err != nil { + errCh <- err + return err + } + return nil }) s.eg.Go(func() error { - s.loop(ctx, errCh, params) + var last types.EpochID + for _, epoch := range s.bootstrapEpochs { + wait := time.Until(params.updateBeaconTime(epoch)) + select { + case <-time.After(wait): + if err := s.GenBootstrap(ctx, epoch); err != nil { + errCh <- err + return err + } + last = epoch + case <-ctx.Done(): + return ctx.Err() + } + } + + if !s.genFallback { + return nil + } + + // start generating fallback data + s.eg.Go(func() error { + for epoch := last; ; epoch++ { + wait := time.Until(params.updateActiveSetTime(epoch)) + select { + case <-time.After(wait): + if err := s.genWithRetry(ctx, epoch, 10); err != nil { + errCh <- err + return nil + } + case <-ctx.Done(): + return nil + } + } + }) + s.eg.Go(func() error { + for epoch := last + 1; ; epoch++ { + wait := time.Until(params.updateBeaconTime(epoch)) + select { + case <-time.After(wait): + if err := s.GenFallbackBeacon(epoch); err != nil { + errCh <- err + return err + } + case <-ctx.Done(): + return nil + } + } + }) + return nil }) } -func (s *Server) loop(ctx context.Context, errCh chan error, params *NetworkParam) { - var last types.EpochID - for _, epoch := range s.bootstrapEpochs { - wait := time.Until(params.updateBeaconTime(epoch)) +func (s *Server) genWithRetry(ctx context.Context, epoch types.EpochID, maxRetries int) error { + err := s.GenFallbackActiveSet(ctx, epoch) + if err == nil { + return nil + } + s.logger.Debug("generate fallback active set retry", zap.Error(err)) + + retries := 0 + backoff := 10 * time.Second + timer := time.NewTimer(backoff) + + for { select { - case <-time.After(wait): - if err := s.GenBootstrap(ctx, epoch); err != nil { - errCh <- err - return + case <-timer.C: + if err := s.GenFallbackActiveSet(ctx, epoch); err != nil { + s.logger.Debug("generate fallback active set retry", zap.Error(err)) + retries++ + if retries >= maxRetries { + return err + } + timer.Reset(backoff) + continue } - last = epoch + return nil case <-ctx.Done(): - return + if !timer.Stop() { + <-timer.C + } + return ctx.Err() } } - - if !s.genFallback { - return - } - - // start generating fallback data - s.eg.Go( - func() error { - s.genDataLoop(ctx, errCh, last, params.updateActiveSetTime, s.GenFallbackActiveSet) - return nil - }) - s.eg.Go( - func() error { - s.genDataLoop(ctx, errCh, last+1, params.updateBeaconTime, s.GenFallbackBeacon) - return nil - }) } // in systests, we want to be sure the nodes use the fallback data unconditionally. @@ -155,7 +216,7 @@ func (s *Server) GenBootstrap(ctx context.Context, epoch types.EpochID) error { return err } -func (s *Server) GenFallbackBeacon(_ context.Context, epoch types.EpochID) error { +func (s *Server) GenFallbackBeacon(epoch types.EpochID) error { suffix := bootstrap.SuffixBeacon _, err := s.gen.GenUpdate(epoch, epochBeacon(epoch), nil, suffix) return err @@ -183,58 +244,22 @@ func getPartialActiveSet(ctx context.Context, smEndpoint string, targetEpoch typ return actives[:cutoff], nil } -func (s *Server) genDataLoop( - ctx context.Context, - errCh chan error, - start types.EpochID, - timeFunc func(types.EpochID) time.Time, - genFunc func(context.Context, types.EpochID) error, -) { - for epoch := start; ; epoch++ { - wait := time.Until(timeFunc(epoch)) - select { - case <-time.After(wait): - if err := genFunc(ctx, epoch); err != nil { - errCh <- err - return - } - case <-ctx.Done(): - return - } - } -} - -func (s *Server) startHttp(ch chan error) { - ln, err := net.Listen("tcp", s.Addr) - if err != nil { - ch <- err - return - } - http.HandleFunc("/", s.handle) - http.HandleFunc("/checkpoint", s.handleCheckpoint) - http.HandleFunc("/updateCheckpoint", s.handleUpdate) - s.logger.With().Info("server starts serving", log.String("addr", ln.Addr().String())) - if err = s.Serve(ln); err != nil { - ch <- err - } -} - func (s *Server) Stop(ctx context.Context) { - s.logger.With().Info("shutting down server") - _ = s.Shutdown(ctx) - _ = s.eg.Wait() + s.logger.Info("shutting down server") + s.Shutdown(ctx) + s.eg.Wait() } func (s *Server) handle(w http.ResponseWriter, r *http.Request) { matches := s.regex.FindStringSubmatch(r.URL.String()) if len(matches) != 3 { - s.logger.With().Error("unrecognized url", log.String("url", r.URL.String())) + s.logger.Error("unrecognized url", zap.Stringer("url", r.URL)) w.WriteHeader(http.StatusNotFound) return } e, err := strconv.Atoi(matches[1]) if err != nil { - s.logger.With().Error("unrecognized url", log.String("url", r.URL.String()), log.Err(err)) + s.logger.Error("unrecognized url", zap.Stringer("url", r.URL), zap.Error(err)) w.WriteHeader(http.StatusNotFound) return } @@ -285,8 +310,5 @@ func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "save checkpoint err: %v", err) return } - s.logger.With().Info("saved checkpoint data", - log.String("data", data), - log.String("filename", filename), - ) + s.logger.Info("saved checkpoint data", zap.String("data", data), zap.String("filename", filename)) } diff --git a/cmd/bootstrapper/server_test.go b/cmd/bootstrapper/server_test.go index 1423048602..df0f20a36f 100644 --- a/cmd/bootstrapper/server_test.go +++ b/cmd/bootstrapper/server_test.go @@ -19,7 +19,6 @@ import ( "github.com/spacemeshos/go-spacemesh/bootstrap" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/sql/statesql" ) @@ -65,14 +64,14 @@ func TestServer(t *testing.T) { g := NewGenerator( "", cfg.PublicListener, - WithLogger(logtest.New(t)), + WithLogger(zaptest.NewLogger(t)), WithFilesystem(fs), ) epochs := []types.EpochID{types.EpochID(4), types.EpochID(5)} srv := NewServer(g, false, port, WithSrvFilesystem(fs), - WithSrvLogger(logtest.New(t)), + WithSrvLogger(zaptest.NewLogger(t)), WithBootstrapEpochs(epochs), ) np := &NetworkParam{ diff --git a/cmd/merge-nodes/internal/merge_action.go b/cmd/merge-nodes/internal/merge_action.go index 1ebc90b00e..426ad82f2c 100644 --- a/cmd/merge-nodes/internal/merge_action.go +++ b/cmd/merge-nodes/internal/merge_action.go @@ -97,10 +97,18 @@ func MergeDBs(ctx context.Context, dbLog *zap.Logger, from, to string) error { if err != nil { return fmt.Errorf("read target key directory: %w", err) } + toKeyDirFiles = slices.DeleteFunc(toKeyDirFiles, func(e fs.DirEntry) bool { + // skip files that are not identity files + return filepath.Ext(e.Name()) != ".key" + }) fromKeyDirFiles, err := os.ReadDir(fromKeyDir) if err != nil { return fmt.Errorf("read source key directory: %w", err) } + fromKeyDirFiles = slices.DeleteFunc(fromKeyDirFiles, func(e fs.DirEntry) bool { + // skip files that are not identity files + return filepath.Ext(e.Name()) != ".key" + }) for _, toFile := range toKeyDirFiles { for _, fromFile := range fromKeyDirFiles { if toFile.Name() == fromFile.Name() { diff --git a/cmd/merge-nodes/internal/merge_action_test.go b/cmd/merge-nodes/internal/merge_action_test.go index a323a16b9e..7fae559a48 100644 --- a/cmd/merge-nodes/internal/merge_action_test.go +++ b/cmd/merge-nodes/internal/merge_action_test.go @@ -219,6 +219,11 @@ func Test_MergeDBs_Successful_Existing_Node(t *testing.T) { err = os.WriteFile(filepath.Join(tmpDst, keyDir, "id1.key"), key, 0o600) require.NoError(t, err) + // this file should be ignored + dstContent := types.RandomBytes(20) + err = os.WriteFile(filepath.Join(tmpDst, keyDir, ".DS_Store"), dstContent, 0o600) + require.NoError(t, err) + dstDB, err := localsql.Open("file:" + filepath.Join(tmpDst, localDbFile)) require.NoError(t, err) @@ -272,6 +277,12 @@ func Test_MergeDBs_Successful_Existing_Node(t *testing.T) { err = os.WriteFile(filepath.Join(tmpSrc, keyDir, "id2.key"), key, 0o600) require.NoError(t, err) + // these files should be ignored + err = os.WriteFile(filepath.Join(tmpSrc, keyDir, ".DS_Store"), types.RandomBytes(20), 0o600) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tmpSrc, keyDir, "desktop.ini"), types.RandomBytes(20), 0o600) + require.NoError(t, err) + srcDB, err := localsql.Open("file:" + filepath.Join(tmpSrc, localDbFile)) require.NoError(t, err) @@ -324,6 +335,11 @@ func Test_MergeDBs_Successful_Existing_Node(t *testing.T) { require.FileExists(t, filepath.Join(tmpDst, keyDir, "id1.key")) require.FileExists(t, filepath.Join(tmpDst, keyDir, "id2.key")) + require.FileExists(t, filepath.Join(tmpDst, keyDir, ".DS_Store")) + content, err := os.ReadFile(filepath.Join(tmpDst, keyDir, ".DS_Store")) + require.NoError(t, err) + require.Equal(t, dstContent, content) + require.NoFileExists(t, filepath.Join(tmpDst, keyDir, "desktop.ini")) dstDB, err = localsql.Open("file:" + filepath.Join(tmpDst, localDbFile)) require.NoError(t, err) diff --git a/cmd/merge-nodes/main.go b/cmd/merge-nodes/main.go index c2623284ee..19bd3f3aa7 100644 --- a/cmd/merge-nodes/main.go +++ b/cmd/merge-nodes/main.go @@ -1,10 +1,10 @@ package main import ( - "fmt" + "log" "os" - "github.com/urfave/cli/v2" + "github.com/spf13/cobra" "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/cmd/merge-nodes/internal" @@ -12,46 +12,44 @@ import ( var version string +func init() { + rootCmd.Flags().StringP("from", "f", "", + "The `data` folder to read identities from and merge into `to`") + rootCmd.MarkFlagRequired("from") + rootCmd.Flags().StringP("to", "t", "", + "The `data` folder to write the merged node to. Can be an existing remote node or empty.") + rootCmd.MarkFlagRequired("to") +} + func main() { - cfg := zap.NewProductionConfig() - cfg.Encoding = "console" - dbLog, err := cfg.Build() + err := rootCmd.Execute() if err != nil { - fmt.Println("create logger:", err) os.Exit(1) } - defer dbLog.Sync() - - app := &cli.App{ - Name: "Spacemesh Node Merger", - Usage: "Merge identities of two Spacemesh nodes into one.\n" + - "The `from` node will be merged into the `to` node, leaving the `from` node untouched.\n" + - "The `to` node can be an existing node or an empty folder.\n" + - "Be sure to backup the `to` node before running this command.\n" + - "NOTE: both `from` and `to` nodes must be upgraded to the latest version before running this command.\n" + - "NOTE: after upgrading and starting the nodes at least once, convert them to remote nodes before merging.", - Version: version, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "from", - Aliases: []string{"f"}, - Usage: "The `data` folder to read identities from and merge into `to`", - Required: true, - }, - &cli.StringFlag{ - Name: "to", - Aliases: []string{"t"}, - Usage: "The `data` folder to write the merged node to. Can be an existing remote node or empty.", - Required: true, - }, - }, - Action: func(ctx *cli.Context) error { - return internal.MergeDBs(ctx.Context, dbLog, ctx.String("from"), ctx.String("to")) - }, - } +} - if err := app.Run(os.Args); err != nil { - dbLog.Sugar().Warnln("app run:", err) - os.Exit(1) - } +var rootCmd = &cobra.Command{ + Use: "merge-nodes -f -t ", + Short: "Spacemesh Node Merger", + Long: `Merge identities of two Spacemesh nodes into one. +The 'from' node will be merged into the 'to' node, leaving the 'from' node untouched. +The 'to' node can be an existing node or an empty folder. +Be sure to backup the 'to' node before running this command. +NOTE: both 'from' and 'to' nodes must be upgraded to the latest version before running this command. +NOTE: after upgrading and starting the nodes at least once, convert them to remote nodes before merging.`, + Version: version, + RunE: func(cmd *cobra.Command, _ []string) error { + cfg := zap.NewProductionConfig() + cfg.Encoding = "console" + dbLog, err := cfg.Build() + if err != nil { + log.Fatalf("create logger: %v", err) + } + defer dbLog.Sync() + + f := cmd.Flag("from").Value.String() + t := cmd.Flag("to").Value.String() + + return internal.MergeDBs(cmd.Context(), dbLog, f, t) + }, } diff --git a/cmd/root.go b/cmd/root.go index 5d8409cfad..1f64748904 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -79,9 +79,6 @@ func AddFlags(flagSet *pflag.FlagSet, cfg *config.Config) (configPath *string) { flagSet.DurationVar(&cfg.DatabasePruneInterval, "db-prune-interval", cfg.DatabasePruneInterval, "configure interval for database pruning") - flagSet.BoolVar(&cfg.ScanMalfeasantATXs, "scan-malfeasant-atxs", cfg.ScanMalfeasantATXs, - "scan for malfeasant ATXs") - flagSet.BoolVar(&cfg.NoMainOverride, "no-main-override", cfg.NoMainOverride, "force 'nomain' builds to run on the mainnet") diff --git a/common/types/activation.go b/common/types/activation.go index 41112efc4c..10a585e80d 100644 --- a/common/types/activation.go +++ b/common/types/activation.go @@ -174,11 +174,11 @@ type ActivationTx struct { // Two ATXs with the same sequence number from the same miner can be used as the proof of malfeasance against // that miner. Sequence uint64 - // the previous ATX's ID (for all but the first in the sequence) - PrevATXID ATXID // CommitmentATX is the ATX used in the commitment for initializing the PoST of the node. - CommitmentATX *ATXID + CommitmentATX *ATXID + // The marriage ATX, used in merged ATXs only. + MarriageATX *ATXID Coinbase Address NumUnits uint32 // the minimum number of space units in this and the previous ATX BaseTickHeight uint64 @@ -226,11 +226,13 @@ func (atx *ActivationTx) MarshalLogObject(encoder log.ObjectEncoder) error { encoder.AddString("atx_id", atx.id.String()) encoder.AddString("smesher", atx.SmesherID.String()) encoder.AddUint32("publish_epoch", atx.PublishEpoch.Uint32()) - encoder.AddString("prev_atx_id", atx.PrevATXID.String()) if atx.CommitmentATX != nil { encoder.AddString("commitment_atx_id", atx.CommitmentATX.String()) } + if atx.MarriageATX != nil { + encoder.AddString("marriage_atx_id", atx.MarriageATX.String()) + } encoder.AddUint64("vrf_nonce", uint64(atx.VRFNonce)) encoder.AddString("coinbase", atx.Coinbase.String()) encoder.AddUint32("epoch", atx.PublishEpoch.Uint32()) diff --git a/common/types/ballot.go b/common/types/ballot.go index 9cc7d18107..c1d41a9f28 100644 --- a/common/types/ballot.go +++ b/common/types/ballot.go @@ -326,7 +326,7 @@ func (b *Ballot) MarshalLogObject(encoder log.ObjectEncoder) error { encoder.AddString("atx_id", b.AtxID.String()) encoder.AddString("ref_ballot", b.RefBallot.String()) encoder.AddString("active set hash", activeHash.ShortString()) - encoder.AddString("beacon", beacon.ShortString()) + encoder.AddString("beacon", beacon.String()) encoder.AddObject("votes", &b.Votes) return nil } @@ -379,11 +379,6 @@ func (id BallotID) AsHash32() Hash32 { return Hash20(id).ToHash32() } -// Field returns a log field. Implements the LoggableField interface. -func (id BallotID) Field() log.Field { - return log.String("ballot_id", id.String()) -} - // Compare returns true if other (the given BallotID) is less than this BallotID, by lexicographic comparison. func (id BallotID) Compare(other BallotID) bool { return bytes.Compare(id.Bytes(), other.Bytes()) < 0 diff --git a/common/types/beacon.go b/common/types/beacon.go index 66444a39f2..eb35b6914d 100644 --- a/common/types/beacon.go +++ b/common/types/beacon.go @@ -1,7 +1,7 @@ package types import ( - "fmt" + "encoding/hex" "github.com/spacemeshos/go-spacemesh/common/util" "github.com/spacemeshos/go-spacemesh/log" @@ -20,19 +20,9 @@ type Beacon [BeaconSize]byte // EmptyBeacon is a canonical empty Beacon. var EmptyBeacon = Beacon{} -// Hex converts a hash to a hex string. -func (b Beacon) Hex() string { return util.Encode(b[:]) } - // String implements the stringer interface and is used also by the logger when // doing full logging into a file. -func (b Beacon) String() string { return b.Hex() } - -// ShortString returns the first 10 characters of the Beacon, usually for logging purposes. -func (b Beacon) ShortString() string { - str := b.Hex() - l := len(str) - return fmt.Sprintf("%.10s", str[min(2, l):]) -} +func (b Beacon) String() string { return hex.EncodeToString(b[:]) } // Bytes gets the byte representation of the underlying hash. func (b Beacon) Bytes() []byte { @@ -41,7 +31,7 @@ func (b Beacon) Bytes() []byte { // Field returns a log field. Implements the LoggableField interface. func (b Beacon) Field() log.Field { - return log.String("beacon", b.ShortString()) + return log.Stringer("beacon", b) } func (b *Beacon) MarshalText() ([]byte, error) { diff --git a/common/types/block.go b/common/types/block.go index ad738cce95..7430467eea 100644 --- a/common/types/block.go +++ b/common/types/block.go @@ -8,10 +8,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/spacemeshos/go-scale" + "go.uber.org/zap/zapcore" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/util" - "github.com/spacemeshos/go-spacemesh/log" ) const ( @@ -133,11 +133,7 @@ func (b *Block) Initialize() { // Bytes returns the serialization of the InnerBlock. func (b *Block) Bytes() []byte { - data, err := codec.Encode(&b.InnerBlock) - if err != nil { - log.Panic("failed to serialize block: %v", err) - } - return data + return codec.MustEncode(&b.InnerBlock) } // ID returns the BlockID. @@ -151,7 +147,7 @@ func (b *Block) ToVote() Vote { } // MarshalLogObject implements logging encoder for Block. -func (b *Block) MarshalLogObject(encoder log.ObjectEncoder) error { +func (b *Block) MarshalLogObject(encoder zapcore.ObjectEncoder) error { encoder.AddString("block_id", b.ID().String()) encoder.AddUint32("layer_id", b.LayerIndex.Uint32()) encoder.AddUint64("tick_height", b.TickHeight) @@ -170,11 +166,6 @@ func (id BlockID) AsHash32() Hash32 { return Hash20(id).ToHash32() } -// Field returns a log field. Implements the LoggableField interface. -func (id BlockID) Field() log.Field { - return log.String("block_id", id.String()) -} - // String implements the Stringer interface. func (id BlockID) String() string { return Hash20(id).ShortString() @@ -196,7 +187,7 @@ func BlockIDsToHashes(ids []BlockID) []Hash32 { type blockIDs []BlockID -func (ids blockIDs) MarshalLogArray(encoder log.ArrayEncoder) error { +func (ids blockIDs) MarshalLogArray(encoder zapcore.ArrayEncoder) error { for i := range ids { encoder.AppendString(ids[i].String()) } @@ -251,9 +242,5 @@ type CertifyContent struct { // Bytes returns the actual data being signed in a CertifyMessage. func (cm *CertifyMessage) Bytes() []byte { - data, err := codec.Encode(&cm.CertifyContent) - if err != nil { - log.Panic("failed to serialize certify msg: %v", err) - } - return data + return codec.MustEncode(&cm.CertifyContent) } diff --git a/common/types/block_test.go b/common/types/block_test.go index 5a7cc02264..eb0ed584ee 100644 --- a/common/types/block_test.go +++ b/common/types/block_test.go @@ -10,7 +10,6 @@ import ( "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/signing" ) @@ -96,18 +95,6 @@ func Test_BlockBytes(t *testing.T) { require.Equal(t, expectedBytes, actualBytes) } -func Test_BlockFieldString(t *testing.T) { - testBlockID := types.BlockID{1, 1} - - expectedField := log.String("block_id", testBlockID.String()) - actualField := testBlockID.Field() - require.Equal(t, expectedField, actualField) - - expectedIDString := testBlockID.AsHash32().ShortString() - actualIDString := testBlockID.String() - require.Equal(t, expectedIDString, actualIDString) -} - func Test_BlockIDCompare(t *testing.T) { testBlockID_1 := types.BlockID{1, 1} testBlockID_2 := types.BlockID{2, 2} diff --git a/common/types/checkpoint.go b/common/types/checkpoint.go index 7f04b35a87..81184e6b30 100644 --- a/common/types/checkpoint.go +++ b/common/types/checkpoint.go @@ -17,6 +17,7 @@ type AtxSnapshot struct { ID []byte `json:"id"` Epoch uint32 `json:"epoch"` CommitmentAtx []byte `json:"commitmentAtx"` + MarriageAtx []byte `json:"marriageAtx"` VrfNonce uint64 `json:"vrfNonce"` BaseTickHeight uint64 `json:"baseTickHeight"` TickCount uint64 `json:"tickCount"` diff --git a/common/types/poet.go b/common/types/poet.go index 0d1d1177ea..f04b613517 100644 --- a/common/types/poet.go +++ b/common/types/poet.go @@ -3,6 +3,7 @@ package types import ( "encoding/hex" "fmt" + "net/url" "time" poetShared "github.com/spacemeshos/poet/shared" @@ -97,3 +98,15 @@ type PoetRound struct { ID string `scale:"max=32"` End time.Time } + +type PoetInfo struct { + ServicePubkey []byte + PhaseShift time.Duration + CycleGap time.Duration + Certifier *CertifierInfo +} + +type CertifierInfo struct { + Url *url.URL + Pubkey []byte +} diff --git a/common/types/proposal.go b/common/types/proposal.go index 37c363e4ff..f29d39d423 100644 --- a/common/types/proposal.go +++ b/common/types/proposal.go @@ -28,6 +28,18 @@ type ProposalID Hash20 // EmptyProposalID is a canonical empty ProposalID. var EmptyProposalID = ProposalID{} +type CompactProposalID [4]byte + +// EncodeScale implements scale codec interface. +func (id *CompactProposalID) EncodeScale(e *scale.Encoder) (int, error) { + return scale.EncodeByteArray(e, id[:]) +} + +// DecodeScale implements scale codec interface. +func (id *CompactProposalID) DecodeScale(d *scale.Decoder) (int, error) { + return scale.DecodeByteArray(d, id[:]) +} + // EncodeScale implements scale codec interface. func (id *ProposalID) EncodeScale(e *scale.Encoder) (int, error) { return scale.EncodeByteArray(e, id[:]) @@ -158,11 +170,6 @@ func (id ProposalID) AsHash32() Hash32 { return Hash20(id).ToHash32() } -// Field returns a log field. Implements the LoggableField interface. -func (id ProposalID) Field() log.Field { - return log.String("proposal_id", id.String()) -} - // Compare returns true if other (the given ProposalID) is less than this ProposalID, by lexicographic comparison. func (id ProposalID) Compare(other ProposalID) bool { return bytes.Compare(id.Bytes(), other.Bytes()) < 0 diff --git a/common/types/testutil.go b/common/types/testutil.go index fae823da49..9018dbabb6 100644 --- a/common/types/testutil.go +++ b/common/types/testutil.go @@ -101,12 +101,16 @@ func RandomTransactionID() TransactionID { // RandomBallot generates a Ballot with random content for testing. func RandomBallot() *Ballot { + var vrf VrfSignature + _, _ = rand.Read(vrf[:]) + return &Ballot{ InnerBallot: InnerBallot{ Layer: LayerID(10), AtxID: RandomATXID(), RefBallot: RandomBallotID(), }, + EligibilityProofs: []VotingEligibility{{Sig: vrf}}, Votes: Votes{ Base: RandomBallotID(), Support: []Vote{{ID: RandomBlockID()}, {ID: RandomBlockID()}}, diff --git a/config/config.go b/config/config.go index 78ef09c66a..0c737b7534 100644 --- a/config/config.go +++ b/config/config.go @@ -22,6 +22,7 @@ import ( vm "github.com/spacemeshos/go-spacemesh/genvm" "github.com/spacemeshos/go-spacemesh/hare3" "github.com/spacemeshos/go-spacemesh/hare3/eligibility" + "github.com/spacemeshos/go-spacemesh/hare4" "github.com/spacemeshos/go-spacemesh/miner" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/syncer" @@ -54,6 +55,7 @@ type Config struct { P2P p2p.Config `mapstructure:"p2p"` API grpcserver.Config `mapstructure:"api"` HARE3 hare3.Config `mapstructure:"hare3"` + HARE4 hare4.Config `mapstructure:"hare4"` HareEligibility eligibility.Config `mapstructure:"hare-eligibility"` Certificate blocks.CertConfig `mapstructure:"certificate"` Beacon beacon.Config `mapstructure:"beacon"` @@ -121,9 +123,6 @@ type BaseConfig struct { PruneActivesetsFrom types.EpochID `mapstructure:"prune-activesets-from"` - // ScanMalfeasantATXs is a flag to enable scanning for malfeasant ATXs. - ScanMalfeasantATXs bool `mapstructure:"scan-malfeasant-atxs"` - NetworkHRP string `mapstructure:"network-hrp"` // MinerGoodAtxsPercent is a threshold to decide if tortoise activeset should be @@ -192,6 +191,7 @@ func DefaultConfig() Config { P2P: p2p.DefaultConfig(), API: grpcserver.DefaultConfig(), HARE3: hare3.DefaultConfig(), + HARE4: hare4.DefaultConfig(), // DEFAULT HARE4 IS DISABLED HareEligibility: eligibility.DefaultConfig(), Beacon: beacon.DefaultConfig(), TIME: timeConfig.DefaultConfig(), diff --git a/config/mainnet.go b/config/mainnet.go index 40b123edba..30d6a71e09 100644 --- a/config/mainnet.go +++ b/config/mainnet.go @@ -21,6 +21,7 @@ import ( "github.com/spacemeshos/go-spacemesh/fetch" "github.com/spacemeshos/go-spacemesh/hare3" "github.com/spacemeshos/go-spacemesh/hare3/eligibility" + "github.com/spacemeshos/go-spacemesh/hare4" "github.com/spacemeshos/go-spacemesh/miner" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/syncer" @@ -59,8 +60,7 @@ func MainnetConfig() Config { } logging := DefaultLoggingConfig() logging.TrtlLoggerLevel = zapcore.WarnLevel.String() - logging.AtxHandlerLevel = zapcore.WarnLevel.String() - logging.ProposalListenerLevel = zapcore.WarnLevel.String() + logging.MeshLoggerLevel = zapcore.WarnLevel.String() hare3conf := hare3.DefaultConfig() hare3conf.Committee = 400 hare3conf.Enable = true @@ -69,6 +69,9 @@ func MainnetConfig() Config { Layer: 105_720, // July 15, 2024, 10:00:00 AM UTC Size: 50, } + + hare4conf := hare4.DefaultConfig() + hare4conf.Enable = false return Config{ BaseConfig: BaseConfig{ DataDirParent: defaultDataDir, @@ -77,8 +80,7 @@ func MainnetConfig() Config { DatabaseConnections: 16, DatabasePruneInterval: 30 * time.Minute, DatabaseVacuumState: 15, - PruneActivesetsFrom: 12, // starting from epoch 13 activesets below 12 will be pruned - ScanMalfeasantATXs: false, // opt-in + PruneActivesetsFrom: 12, // starting from epoch 13 activesets below 12 will be pruned NetworkHRP: "sm", LayerDuration: 5 * time.Minute, @@ -137,6 +139,7 @@ func MainnetConfig() Config { }, }, HARE3: hare3conf, + HARE4: hare4conf, HareEligibility: eligibility.Config{ ConfidenceParam: 200, }, @@ -164,6 +167,8 @@ func MainnetConfig() Config { CycleGap: 12 * time.Hour, GracePeriod: 1 * time.Hour, PositioningATXSelectionTimeout: 50 * time.Minute, + CertifierInfoCacheTTL: 5 * time.Minute, + PowParamsCacheTTL: 5 * time.Minute, // RequestTimeout = RequestRetryDelay * 2 * MaxRequestRetries*(MaxRequestRetries+1)/2 RequestTimeout: 1100 * time.Second, RequestRetryDelay: 10 * time.Second, diff --git a/config/presets/fastnet.go b/config/presets/fastnet.go index de09ac0d19..225a2482d5 100644 --- a/config/presets/fastnet.go +++ b/config/presets/fastnet.go @@ -51,7 +51,9 @@ func fastnet() config.Config { conf.LayerDuration = 15 * time.Second conf.Sync.Interval = 5 * time.Second conf.Sync.GossipDuration = 10 * time.Second - conf.Sync.AtxSync.EpochInfoInterval = 20 * time.Second + conf.Sync.AtxSync.EpochInfoInterval = 1 * time.Second + conf.Sync.AtxSync.EpochInfoPeers = 10 + conf.Sync.AtxSync.RequestsLimit = 100 conf.Sync.MalSync.IDRequestInterval = 20 * time.Second conf.LayersPerEpoch = 4 conf.RegossipAtxInterval = 30 * time.Second @@ -97,5 +99,8 @@ func fastnet() config.Config { conf.POET.RequestTimeout = 12 * time.Second // RequestRetryDelay * 2 * MaxRequestRetries*(MaxRequestRetries+1)/2 conf.POET.RequestRetryDelay = 1 * time.Second conf.POET.MaxRequestRetries = 3 + conf.POET.CertifierInfoCacheTTL = time.Minute + conf.POET.PowParamsCacheTTL = 10 * time.Second + return conf } diff --git a/config/presets/standalone.go b/config/presets/standalone.go index 290b615971..899b1d48ba 100644 --- a/config/presets/standalone.go +++ b/config/presets/standalone.go @@ -83,6 +83,8 @@ func standalone() config.Config { conf.POET.RequestTimeout = 12 * time.Second // RequestRetryDelay * 2 * MaxRequestRetries*(MaxRequestRetries+1)/2 conf.POET.RequestRetryDelay = 1 * time.Second conf.POET.MaxRequestRetries = 3 + conf.POET.CertifierInfoCacheTTL = time.Minute + conf.POET.PowParamsCacheTTL = 10 * time.Second conf.P2P.DisableNatPort = true diff --git a/config/presets/testnet.go b/config/presets/testnet.go index 892d924aaf..d6d80530af 100644 --- a/config/presets/testnet.go +++ b/config/presets/testnet.go @@ -22,6 +22,7 @@ import ( "github.com/spacemeshos/go-spacemesh/fetch" "github.com/spacemeshos/go-spacemesh/hare3" "github.com/spacemeshos/go-spacemesh/hare3/eligibility" + "github.com/spacemeshos/go-spacemesh/hare4" "github.com/spacemeshos/go-spacemesh/miner" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/syncer" @@ -55,6 +56,9 @@ func testnet() config.Config { // NOTE(dshulyak) i forgot to set protocol name for testnet when we configured it manually. // we can't do rolling upgrade if protocol name changes, so lets keep it like that temporarily. hare3conf.ProtocolName = "" + + hare4conf := hare4.DefaultConfig() + hare4conf.Enable = false defaultdir := filepath.Join(home, "spacemesh-testnet", "/") return config.Config{ Preset: "testnet", @@ -95,6 +99,7 @@ func testnet() config.Config { MinimalActiveSetWeight: []types.EpochMinimalActiveWeight{{Weight: 10_000}}, }, HARE3: hare3conf, + HARE4: hare4conf, HareEligibility: eligibility.Config{ ConfidenceParam: 20, }, @@ -118,6 +123,9 @@ func testnet() config.Config { RequestTimeout: 550 * time.Second, // RequestRetryDelay * 2 * MaxRequestRetries*(MaxRequestRetries+1)/2 RequestRetryDelay: 5 * time.Second, MaxRequestRetries: 10, + + CertifierInfoCacheTTL: 5 * time.Minute, + PowParamsCacheTTL: 5 * time.Minute, }, POST: activation.PostConfig{ MinNumUnits: 2, diff --git a/datastore/store.go b/datastore/store.go index 014f23a7e7..073c7bacdc 100644 --- a/datastore/store.go +++ b/datastore/store.go @@ -118,32 +118,6 @@ func (db *CachedDB) MalfeasanceCacheSize() int { return db.malfeasanceCache.Len() } -// IsMalicious returns true if the NodeID is malicious. -func (db *CachedDB) IsMalicious(id types.NodeID) (bool, error) { - if id == types.EmptyNodeID { - panic("invalid argument to IsMalicious") - } - - db.mu.Lock() - defer db.mu.Unlock() - if proof, ok := db.malfeasanceCache.Get(id); ok { - if proof == nil { - return false, nil - } else { - return true, nil - } - } - - bad, err := identities.IsMalicious(db, id) - if err != nil { - return false, err - } - if !bad { - db.malfeasanceCache.Add(id, nil) - } - return bad, nil -} - // GetMalfeasanceProof gets the malfeasance proof associated with the NodeID. func (db *CachedDB) GetMalfeasanceProof(id types.NodeID) (*wire.MalfeasanceProof, error) { if id == types.EmptyNodeID { @@ -216,6 +190,11 @@ func (db *CachedDB) GetAtx(id types.ATXID) (*types.ActivationTx, error) { return atx, nil } +// Previous retrieves the list of previous ATXs for the given ATX ID. +func (db *CachedDB) Previous(id types.ATXID) ([]types.ATXID, error) { + return atxs.Previous(db, id) +} + func (db *CachedDB) IterateMalfeasanceProofs( iter func(types.NodeID, *wire.MalfeasanceProof) error, ) error { diff --git a/datastore/store_test.go b/datastore/store_test.go index 80c73abafb..13372858b1 100644 --- a/datastore/store_test.go +++ b/datastore/store_test.go @@ -11,7 +11,6 @@ import ( "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/activation/wire" - "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/fixture" "github.com/spacemeshos/go-spacemesh/common/types" @@ -54,67 +53,6 @@ func getBytes( return blob.Bytes, nil } -func TestMalfeasanceProof_Honest(t *testing.T) { - db := statesql.InMemory() - cdb := datastore.NewCachedDB(db, zaptest.NewLogger(t)) - require.Equal(t, 0, cdb.MalfeasanceCacheSize()) - - nodeID1 := types.NodeID{1} - got, err := cdb.GetMalfeasanceProof(nodeID1) - require.ErrorIs(t, err, sql.ErrNotFound) - require.Nil(t, got) - require.Equal(t, 1, cdb.MalfeasanceCacheSize()) - - // secretly save the proof to database - require.NoError(t, identities.SetMalicious(db, nodeID1, []byte("bad"), time.Now())) - bad, err := identities.IsMalicious(db, nodeID1) - require.NoError(t, err) - require.True(t, bad) - require.Equal(t, 1, cdb.MalfeasanceCacheSize()) - - // but it will retrieve it from cache - got, err = cdb.GetMalfeasanceProof(nodeID1) - require.ErrorIs(t, err, sql.ErrNotFound) - require.Nil(t, got) - require.Equal(t, 1, cdb.MalfeasanceCacheSize()) - bad, err = cdb.IsMalicious(nodeID1) - require.NoError(t, err) - require.False(t, bad) - - // asking will cause the answer cached for honest nodes - nodeID2 := types.NodeID{2} - bad, err = cdb.IsMalicious(nodeID2) - require.NoError(t, err) - require.False(t, bad) - require.Equal(t, 2, cdb.MalfeasanceCacheSize()) - - // secretly save the proof to database - require.NoError(t, identities.SetMalicious(db, nodeID2, []byte("bad"), time.Now())) - bad, err = identities.IsMalicious(db, nodeID2) - require.NoError(t, err) - require.True(t, bad) - require.Equal(t, 2, cdb.MalfeasanceCacheSize()) - - // but an add will update the cache - proof := &mwire.MalfeasanceProof{ - Layer: types.LayerID(11), - Proof: mwire.Proof{ - Type: mwire.MultipleBallots, - Data: &mwire.BallotProof{ - Messages: [2]mwire.BallotProofMsg{ - {}, - {}, - }, - }, - }, - } - cdb.CacheMalfeasanceProof(nodeID2, proof) - bad, err = cdb.IsMalicious(nodeID2) - require.NoError(t, err) - require.True(t, bad) - require.Equal(t, 2, cdb.MalfeasanceCacheSize()) -} - func TestMalfeasanceProof_Dishonest(t *testing.T) { db := statesql.InMemory() cdb := datastore.NewCachedDB(db, zaptest.NewLogger(t)) @@ -408,16 +346,3 @@ func TestBlobStore_GetActiveSet(t *testing.T) { require.NoError(t, err) require.Equal(t, codec.MustEncode(as), got) } - -func Test_MarkingMalicious(t *testing.T) { - db := statesql.InMemory() - store := atxsdata.New() - id := types.RandomNodeID() - cdb := datastore.NewCachedDB(db, zaptest.NewLogger(t), datastore.WithConsensusCache(store)) - - cdb.CacheMalfeasanceProof(id, &mwire.MalfeasanceProof{}) - m, err := cdb.IsMalicious(id) - require.NoError(t, err) - require.True(t, m) - require.True(t, store.IsMalicious(id)) -} diff --git a/eligibility/fixedoracle.go b/eligibility/fixedoracle.go deleted file mode 100644 index f73cc1b7fc..0000000000 --- a/eligibility/fixedoracle.go +++ /dev/null @@ -1,272 +0,0 @@ -// Package eligibility defines fixed size oracle used for node testing -package eligibility - -import ( - "context" - "encoding/binary" - "sync" - - "github.com/spacemeshos/go-scale" - - "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/hash" - "github.com/spacemeshos/go-spacemesh/log" - "github.com/spacemeshos/go-spacemesh/signing" -) - -// FixedRolacle is an eligibility simulator with pre-determined honest and faulty participants. -type FixedRolacle struct { - logger log.Log - - mutex sync.Mutex - mapRW sync.RWMutex - honest map[types.NodeID]struct{} - faulty map[types.NodeID]struct{} - emaps map[types.Hash32]map[types.NodeID]struct{} -} - -// New initializes the oracle with no participants. -func New(logger log.Log) *FixedRolacle { - return &FixedRolacle{ - logger: logger, - honest: make(map[types.NodeID]struct{}), - faulty: make(map[types.NodeID]struct{}), - emaps: make(map[types.Hash32]map[types.NodeID]struct{}), - } -} - -// IsIdentityActiveOnConsensusView is use to satisfy the API, currently always returns true. -func (fo *FixedRolacle) IsIdentityActiveOnConsensusView( - ctx context.Context, - edID types.NodeID, - layer types.LayerID, -) (bool, error) { - return true, nil -} - -// Export creates a map with the eligible participants for id and committee size. -func (fo *FixedRolacle) Export(id types.Hash32, committeeSize int) map[types.NodeID]struct{} { - fo.mapRW.RLock() - total := len(fo.honest) + len(fo.faulty) - fo.mapRW.RUnlock() - - // normalize committee size - size := committeeSize - if committeeSize > total { - fo.logger.With().Warning("committee size bigger than the number of clients", - log.Int("committee_size", committeeSize), - log.Int("num_clients", total)) - size = total - } - - fo.mapRW.Lock() - // generate if not exist for the requested K - if _, exist := fo.emaps[id]; !exist { - fo.emaps[id] = fo.generateEligibility(context.TODO(), size) - } - m := fo.emaps[id] - fo.mapRW.Unlock() - - return m -} - -func (fo *FixedRolacle) update(m map[types.NodeID]struct{}, client types.NodeID) { - fo.mutex.Lock() - - if _, exist := m[client]; exist { - fo.mutex.Unlock() - return - } - - m[client] = struct{}{} - - fo.mutex.Unlock() -} - -// Register adds a participant to the eligibility map. can be honest or faulty. -func (fo *FixedRolacle) Register(isHonest bool, client types.NodeID) { - if isHonest { - fo.update(fo.honest, client) - } else { - fo.update(fo.faulty, client) - } -} - -// Unregister removes a participant from the eligibility map. can be honest or faulty. -// TODO: just remove from both instead of specifying. -func (fo *FixedRolacle) Unregister(isHonest bool, client types.NodeID) { - fo.mutex.Lock() - if isHonest { - delete(fo.honest, client) - } else { - delete(fo.faulty, client) - } - fo.mutex.Unlock() -} - -func cloneMap(m map[types.NodeID]struct{}) map[types.NodeID]struct{} { - c := make(map[types.NodeID]struct{}, len(m)) - for k, v := range m { - c[k] = v - } - - return c -} - -func pickUnique(pickCount int, orig, dest map[types.NodeID]struct{}) { - i := 0 - for k := range orig { // randomly pass on clients - if i == pickCount { // pick exactly size - break - } - - dest[k] = struct{}{} - delete(orig, k) // unique pick - i++ - } -} - -func (fo *FixedRolacle) generateEligibility(ctx context.Context, expCom int) map[types.NodeID]struct{} { - logger := fo.logger.WithContext(ctx) - emap := make(map[types.NodeID]struct{}, expCom) - - if expCom == 0 { - return emap - } - - expHonest := expCom/2 + 1 - if expHonest > len(fo.honest) { - logger.With().Warning("not enough registered honest participants", - log.Int("expected", expHonest), - log.Int("actual", len(fo.honest))) - expHonest = len(fo.honest) - } - - hon := cloneMap(fo.honest) - pickUnique(expHonest, hon, emap) - - expFaulty := expCom - expHonest - if expFaulty > len(fo.faulty) { - if len(fo.faulty) > 0 { // not enough - logger.With().Debug("not enough registered dishonest participants to pick from, picking all faulty", - log.Int("expected", expFaulty), - log.Int("actual", len(fo.faulty))) - } else { // no faulty at all - acceptable - logger.Debug("no registered dishonest participants to pick from, picking honest instead") - } - expFaulty = len(fo.faulty) - } - - if expFaulty > 0 { // pick faulty if you need - fau := cloneMap(fo.faulty) - pickUnique(expFaulty, fau, emap) - } - - rem := expCom - expHonest - expFaulty - if rem > 0 { // need to pickUnique the remaining from honest - pickUnique(rem, hon, emap) - } - - return emap -} - -func hashLayerAndRound(logger log.Log, instanceID types.LayerID, round uint32) types.Hash32 { - kInBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(kInBytes, round) - h := hash.GetHasher() - defer hash.PutHasher(h) - enc := scale.NewEncoder(h) - _, err := instanceID.EncodeScale(enc) - _, err2 := h.Write(kInBytes) - - if err != nil || err2 != nil { - logger.With().Error("errors trying to create a hash", - log.FieldNamed("err1", log.Err(err)), - log.FieldNamed("err2", log.Err(err2))) - } - return types.BytesToHash(h.Sum([]byte{})) -} - -// Validate is required to conform to the Rolacle interface, but should never be called. -func (fo *FixedRolacle) Validate( - context.Context, - types.LayerID, - uint32, - int, - types.NodeID, - types.VrfSignature, - uint16, -) (bool, error) { - panic("implement me!") -} - -// CalcEligibility returns 1 if the miner is eligible in given layer, and 0 otherwise. -func (fo *FixedRolacle) CalcEligibility( - ctx context.Context, - layer types.LayerID, - round uint32, - committeeSize int, - id types.NodeID, - sig types.VrfSignature, -) (uint16, error) { - eligible, err := fo.eligible(ctx, layer, round, committeeSize, id) - if eligible { - return 1, nil - } - return 0, err -} - -// eligible returns whether the specific NodeID is eligible for layer in round and committee size. -func (fo *FixedRolacle) eligible( - ctx context.Context, - layer types.LayerID, - round uint32, - committeeSize int, - id types.NodeID, -) (bool, error) { - fo.mapRW.RLock() - total := len(fo.honest) + len(fo.faulty) // safe since len >= 0 - fo.mapRW.RUnlock() - - // normalize committee size - size := committeeSize - if committeeSize > total { - fo.logger.WithContext(ctx).With().Warning("committee size bigger than the number of clients", - log.Int("committee_size", committeeSize), - log.Int("num_clients", total), - ) - size = total - } - - instID := hashLayerAndRound(fo.logger, layer, round) - - fo.mapRW.Lock() - // generate if not exist for the requested K - if _, exist := fo.emaps[instID]; !exist { - fo.emaps[instID] = fo.generateEligibility(ctx, size) - } - // get eligibility result - _, exist := fo.emaps[instID][id] - fo.mapRW.Unlock() - - return exist, nil -} - -// Proof generates a proof for the round. used to satisfy interface. -func (fo *FixedRolacle) Proof( - ctx context.Context, - _ *signing.VRFSigner, - layer types.LayerID, - round uint32, -) (types.VrfSignature, error) { - kInBytes := make([]byte, 4) - binary.LittleEndian.PutUint32(kInBytes, round) - h := hash.GetHasher() - defer hash.PutHasher(h) - if _, err := h.Write(kInBytes); err != nil { - fo.logger.WithContext(ctx).With().Error("error writing hash", log.Err(err)) - } - var proof types.VrfSignature - _, err := h.Digest().Read(proof[:]) - return proof, err -} diff --git a/eligibility/fixedoracle_test.go b/eligibility/fixedoracle_test.go deleted file mode 100644 index 8456b6501c..0000000000 --- a/eligibility/fixedoracle_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package eligibility - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/log/logtest" -) - -const ( - numOfClients = 100 -) - -func TestFixedRolacle_Eligible(t *testing.T) { - oracle := New(logtest.New(t)) - for i := 0; i < numOfClients-1; i++ { - oracle.Register(true, types.RandomNodeID()) - } - v := types.RandomNodeID() - oracle.Register(true, v) - - res, err := oracle.eligible(context.Background(), types.LayerID(1), 1, 10, v) - require.NoError(t, err) - res2, err := oracle.eligible(context.Background(), types.LayerID(1), 1, 10, v) - require.NoError(t, err) - require.Equal(t, res, res2) -} - -func TestFixedRolacle_Eligible2(t *testing.T) { - pubs := make([]types.NodeID, 0, numOfClients) - oracle := New(logtest.New(t)) - for i := 0; i < numOfClients; i++ { - s := types.RandomNodeID() - pubs = append(pubs, s) - oracle.Register(true, s) - } - - count := 0 - for _, p := range pubs { - res, _ := oracle.eligible(context.Background(), types.LayerID(1), 1, 10, p) - if res { - count++ - } - } - - assert.Equal(t, 10, count) - - count = 0 - for _, p := range pubs { - res, _ := oracle.eligible(context.Background(), types.LayerID(1), 1, 20, p) - if res { - count++ - } - } - - assert.Equal(t, 10, count) -} - -func TestFixedRolacle_Range(t *testing.T) { - oracle := New(logtest.New(t)) - pubs := make([]types.NodeID, 0, numOfClients) - for i := 0; i < numOfClients; i++ { - s := types.RandomNodeID() - pubs = append(pubs, s) - oracle.Register(true, s) - } - - count := 0 - for _, p := range pubs { - res, _ := oracle.eligible(context.Background(), types.LayerID(1), 1, numOfClients, p) - if res { - count++ - } - } - - // check all eligible - assert.Equal(t, numOfClients, count) - - count = 0 - for _, p := range pubs { - res, _ := oracle.eligible(context.Background(), types.LayerID(2), 1, 0, p) - if res { - count++ - } - } - - // check all not eligible - assert.Equal(t, 0, count) -} - -func TestFixedRolacle_Eligible3(t *testing.T) { - oracle := New(logtest.New(t)) - for i := 0; i < numOfClients/3; i++ { - s := types.RandomNodeID() - oracle.Register(true, s) - } - - for i := 0; i < 2*numOfClients/3; i++ { - s := types.RandomNodeID() - oracle.Register(false, s) - } - - exp := numOfClients / 2 - ok, err := oracle.eligible(context.Background(), types.LayerID(1), 1, exp, types.NodeID{1}) - require.NoError(t, err) - require.False(t, ok) - - hc := 0 - for k := range oracle.honest { - res, _ := oracle.eligible(context.Background(), types.LayerID(1), 1, exp, k) - if res { - hc++ - } - } - - dc := 0 - for k := range oracle.faulty { - res, _ := oracle.eligible(context.Background(), types.LayerID(1), 1, exp, k) - if res { - dc++ - } - } - - assert.Equal(t, exp/2+1, hc) - assert.Equal(t, exp/2-1, dc) -} - -func TestGenerateElibility(t *testing.T) { - oracle := New(logtest.New(t)) - ids := make([]types.NodeID, 0, 30) - for i := 0; i < 30; i++ { - s := types.RandomNodeID() - ids = append(ids, s) - oracle.Register(true, s) - } - - m := oracle.generateEligibility(context.Background(), len(oracle.honest)) - - for _, s := range ids { - _, ok := m[s] - require.True(t, ok) - } -} - -func TestFixedRolacle_Eligible4(t *testing.T) { - oracle := New(logtest.New(t)) - var ids []types.NodeID - for i := 0; i < 33; i++ { - s := types.RandomNodeID() - ids = append(ids, s) - oracle.Register(true, s) - } - - // when requesting a bigger committee size everyone should be eligible - - for _, s := range ids { - res, _ := oracle.eligible(context.Background(), 0, 1, numOfClients, s) - assert.True(t, res) - } -} - -func TestFixedRolacle_Export(t *testing.T) { - oracle := New(logtest.New(t)) - var ids []types.NodeID - for i := 0; i < 35; i++ { - s := types.RandomNodeID() - ids = append(ids, s) - oracle.Register(true, s) - } - - // when requesting a bigger committee size everyone should be eligible - - m := oracle.Export(types.RandomHash(), numOfClients) - - for _, s := range ids { - _, ok := m[s] - assert.True(t, ok) - } -} diff --git a/fetch/fetch.go b/fetch/fetch.go index ac0ea85ba7..94e93caea6 100644 --- a/fetch/fetch.go +++ b/fetch/fetch.go @@ -58,6 +58,7 @@ type request struct { } type promise struct { + once sync.Once completed chan struct{} err error } @@ -435,15 +436,15 @@ func (f *Fetch) Stop() { f.cancel() f.mu.Lock() for _, req := range f.unprocessed { - close(req.promise.completed) + req.promise.once.Do(func() { close(req.promise.completed) }) } for _, req := range f.ongoing { - close(req.promise.completed) + req.promise.once.Do(func() { close(req.promise.completed) }) } f.mu.Unlock() _ = f.eg.Wait() - f.logger.Info("stopped fetch") + f.logger.Debug("stopped fetch") } // stopped returns if we should stop. @@ -459,7 +460,7 @@ func (f *Fetch) stopped() bool { // here we receive all requests for hashes for all DBs and batch them together before we send the request to peer // there can be a priority request that will not be batched. func (f *Fetch) loop() { - f.logger.Info("starting fetch main loop") + f.logger.Debug("starting fetch main loop") for { select { case <-f.batchTimeout.C: @@ -589,7 +590,7 @@ func (f *Fetch) hashValidationDone(hash types.Hash32, err error) { } else { f.logger.Debug("hash request done", log.ZContext(req.ctx), zap.Stringer("hash", hash)) } - close(req.promise.completed) + req.promise.once.Do(func() { close(req.promise.completed) }) delete(f.ongoing, hash) } @@ -605,7 +606,7 @@ func (f *Fetch) failAfterRetry(hash types.Hash32) { // first check if we have it locally from gossips if has, err := f.bs.Has(req.hint, hash.Bytes()); err == nil && has { - close(req.promise.completed) + req.promise.once.Do(func() { close(req.promise.completed) }) delete(f.ongoing, hash) return } @@ -618,7 +619,8 @@ func (f *Fetch) failAfterRetry(hash types.Hash32) { zap.Int("retries", req.retries), ) req.promise.err = ErrExceedMaxRetries - close(req.promise.completed) + req.promise.once.Do(func() { close(req.promise.completed) }) + } else { // put the request back to the unprocessed list f.unprocessed[req.hash] = req @@ -696,14 +698,14 @@ func (f *Fetch) organizeRequests(requests []RequestMessage) map[p2p.Peer][]*batc best := f.peers.SelectBest(RedundantPeers) if len(best) == 0 { - f.logger.Info("cannot send batch: no peers found") + f.logger.Warn("cannot send batch: no peers found") f.mu.Lock() defer f.mu.Unlock() errNoPeer := errors.New("no peers") for _, msg := range requests { if req, ok := f.ongoing[msg.Hash]; ok { req.promise.err = errNoPeer - close(req.promise.completed) + req.promise.once.Do(func() { close(req.promise.completed) }) delete(f.ongoing, req.hash) } else { f.logger.Error("ongoing request missing", @@ -918,7 +920,7 @@ func (f *Fetch) handleHashError(batch *batchInfo, err error) { f.logger.Debug("hash request failed", log.ZContext(req.ctx), zap.Stringer("hash", req.hash), zap.Error(err)) req.promise.err = err peerErrors.WithLabelValues(string(req.hint)).Inc() - close(req.promise.completed) + req.promise.once.Do(func() { close(req.promise.completed) }) delete(f.ongoing, req.hash) } } @@ -956,7 +958,7 @@ func (f *Fetch) getHash( hint: h, validator: receiver, promise: &promise{ - completed: make(chan struct{}, 1), + completed: make(chan struct{}), }, } f.logger.Debug("hash request added to queue", diff --git a/fetch/fetch_test.go b/fetch/fetch_test.go index b93327e7da..d136cb4ac0 100644 --- a/fetch/fetch_test.go +++ b/fetch/fetch_test.go @@ -169,8 +169,6 @@ func TestFetch_RequestHashBatchFromPeers(t *testing.T) { for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - t.Parallel() - f := createFetch(t) f.cfg.MaxRetriesForRequest = 0 peer := p2p.Peer("buddy") diff --git a/fetch/mesh_data.go b/fetch/mesh_data.go index 1c199c2662..f7961cd769 100644 --- a/fetch/mesh_data.go +++ b/fetch/mesh_data.go @@ -5,14 +5,12 @@ import ( "errors" "fmt" "io" - "strings" "sync" "github.com/spacemeshos/go-scale" "go.uber.org/zap" "golang.org/x/sync/errgroup" - "github.com/spacemeshos/go-spacemesh/activation" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" @@ -20,6 +18,7 @@ import ( "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/p2p/server" + "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/system" ) @@ -215,7 +214,7 @@ func (f *Fetch) GetPoetProof(ctx context.Context, id types.Hash32) error { switch { case pm.err == nil: return nil - case errors.Is(pm.err, activation.ErrObjectExists): + case errors.Is(pm.err, sql.ErrObjectExists): // PoET proofs are concurrently stored in DB in two places: // fetcher and nipost builder. Hence, it might happen that // a proof had been inserted into the DB while the fetcher @@ -405,28 +404,37 @@ var ErrIgnore = errors.New("fetch: ignore") type BatchError struct { Errors map[types.Hash32]error + first types.Hash32 } func (b *BatchError) Empty() bool { return len(b.Errors) == 0 } +func (b *BatchError) Is(target error) bool { + for _, err := range b.Errors { + if errors.Is(err, target) { + return true + } + } + return false +} + func (b *BatchError) Add(id types.Hash32, err error) { if b.Errors == nil { b.Errors = map[types.Hash32]error{} } + if b.Empty() { + b.first = id + } b.Errors[id] = err } func (b *BatchError) Error() string { - var builder strings.Builder - builder.WriteString("batch failure: ") - for hash, err := range b.Errors { - builder.WriteString(hash.ShortString()) - builder.WriteString("=") - builder.WriteString(err.Error()) - } - return builder.String() + if len(b.Errors) == 0 { + return "" + } + return fmt.Sprintf("batch failed, first failure: %s: %v", b.first.ShortString(), b.Errors[b.first]) } func (b *BatchError) Ignore() bool { diff --git a/fetch/p2p_test.go b/fetch/p2p_test.go index 25fdad3cda..26dd5524c5 100644 --- a/fetch/p2p_test.go +++ b/fetch/p2p_test.go @@ -91,7 +91,6 @@ func createP2PFetch( ) (*testP2PFetch, context.Context) { lg := zaptest.NewLogger(t) ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - t.Cleanup(cancel) serverHost, err := p2p.AutoStart(ctx, lg, p2pCfg(t), []byte{}, []byte{}) require.NoError(t, err) @@ -101,6 +100,13 @@ func createP2PFetch( require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, clientHost.Stop()) }) + t.Cleanup(func() { + cancel() + time.Sleep(10 * time.Millisecond) + // mafa: p2p internally uses a global logger this should prevent logging after + // the test ends (send PR with fix to libp2p/go-libp2p-pubsub) (go loop in pubsub.go) + }) + var sqlOpts []sql.Opt if sqlCache { sqlOpts = []sql.Opt{sql.WithQueryCache(true)} diff --git a/genvm/rewards.go b/genvm/rewards.go index 2319bfd307..af30bbfd4b 100644 --- a/genvm/rewards.go +++ b/genvm/rewards.go @@ -5,10 +5,10 @@ import ( "math/big" "github.com/spacemeshos/economics/rewards" + "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/genvm/core" - "github.com/spacemeshos/go-spacemesh/log" ) func (v *VM) addRewards( @@ -50,12 +50,12 @@ func (v *VM) addRewards( core.ErrInternal, subsidyReward, blockReward.Coinbase) } - v.logger.With().Debug("rewards for coinbase", - lctx.Layer, - blockReward.Coinbase, - log.Stringer("relative weight", &blockReward.Weight), - log.Uint64("subsidy", subsidyReward.Uint64()), - log.Uint64("total", totalReward.Uint64()), + v.logger.Debug("rewards for coinbase", + zap.Uint32("layer", lctx.Layer.Uint32()), + zap.Stringer("coinbase", blockReward.Coinbase), + zap.Stringer("relative weight", &blockReward.Weight), + zap.Uint64("subsidy", subsidyReward.Uint64()), + zap.Uint64("total", totalReward.Uint64()), ) reward := types.Reward{ @@ -76,14 +76,14 @@ func (v *VM) addRewards( } transferred += totalReward.Uint64() } - v.logger.With().Debug("rewards for layer", - lctx.Layer, - log.Uint32("after genesis", layersAfterEffectiveGenesis), - log.Uint64("subsidy estimated", subsidy), - log.Uint64("fee", fees), - log.Uint64("total estimated", total), - log.Uint64("total transferred", transferred), - log.Uint64("total burnt", total-transferred), + v.logger.Debug("rewards for layer", + zap.Uint32("layer", lctx.Layer.Uint32()), + zap.Uint32("after genesis", layersAfterEffectiveGenesis), + zap.Uint64("subsidy estimated", subsidy), + zap.Uint64("fee", fees), + zap.Uint64("total estimated", total), + zap.Uint64("total transferred", transferred), + zap.Uint64("total burnt", total-transferred), ) feesCount.Add(float64(fees)) subsidyCount.Add(float64(subsidy)) diff --git a/genvm/vm.go b/genvm/vm.go index 61d82c182d..f00c01b721 100644 --- a/genvm/vm.go +++ b/genvm/vm.go @@ -8,6 +8,7 @@ import ( "time" "github.com/spacemeshos/go-scale" + "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/events" @@ -31,7 +32,7 @@ import ( type Opt func(*VM) // WithLogger sets logger for VM. -func WithLogger(logger log.Log) Opt { +func WithLogger(logger *zap.Logger) Opt { return func(vm *VM) { vm.logger = logger } @@ -60,7 +61,7 @@ func WithConfig(cfg Config) Opt { // New returns VM instance. func New(db sql.StateDatabase, opts ...Opt) *VM { vm := &VM{ - logger: log.NewNop(), + logger: zap.NewNop(), db: db, cfg: DefaultConfig(), registry: registry.New(), @@ -77,7 +78,7 @@ func New(db sql.StateDatabase, opts ...Opt) *VM { // VM handles modifications to the account state. type VM struct { - logger log.Log + logger *zap.Logger db sql.StateDatabase cfg Config registry *registry.Registry @@ -143,7 +144,7 @@ func (v *VM) Revert(lid types.LayerID) error { if err := v.revert(lid); err != nil { return err } - v.logger.With().Info("vm reverted to layer", lid) + v.logger.Info("vm reverted to layer", zap.Uint32("layer", lid.Uint32())) return nil } @@ -179,7 +180,7 @@ func (v *VM) ApplyGenesis(genesis []types.Account) error { defer tx.Release() for i := range genesis { account := &genesis[i] - v.logger.With().Info("genesis account", log.Inline(account)) + v.logger.Info("genesis account", zap.Inline(account)) if err := accounts.Update(tx, account); err != nil { return fmt.Errorf("inserting genesis account: %w", err) } @@ -236,7 +237,7 @@ func (v *VM) Apply( ss.IterateChanged(func(account *core.Account) bool { total++ account.Layer = lctx.Layer - v.logger.With().Debug("update account state", log.Inline(account)) + v.logger.Debug("update account state", zap.Inline(account)) err = accounts.Update(tx, account) if err != nil { return false @@ -271,11 +272,11 @@ func (v *VM) Apply( transactionsPerBlock.Observe(float64(len(txs))) appliedLayer.Set(float64(lctx.Layer)) - v.logger.With().Debug("applied layer", - log.Uint32("layer", lctx.Layer.Uint32()), - log.Int("count", len(txs)-len(skipped)), - log.Duration("duration", time.Since(t1)), - log.Stringer("state_hash", hashSum), + v.logger.Debug("applied layer", + zap.Uint32("layer", lctx.Layer.Uint32()), + zap.Int("count", len(txs)-len(skipped)), + zap.Duration("duration", time.Since(t1)), + zap.Stringer("state_hash", hashSum), ) return skipped, results, nil } @@ -294,7 +295,7 @@ func (v *VM) execute( limit = v.cfg.GasLimit ) for i, tx := range txs { - logger := v.logger.WithFields(log.Int("ith", i)) + logger := v.logger.With(zap.Int("ith", i)) txCount.Inc() t1 := time.Now() @@ -310,9 +311,9 @@ func (v *VM) execute( header, err := req.Parse() if err != nil { - logger.With().Warning("ineffective transaction. failed to parse", - tx.GetRaw().ID, - log.Err(err), + logger.Warn("ineffective transaction. failed to parse", + log.ZShortStringer("tx", tx.GetRaw().ID), + zap.Error(err), ) ineffective = append(ineffective, types.Transaction{RawTx: tx.GetRaw()}) invalidTxCount.Inc() @@ -322,30 +323,30 @@ func (v *VM) execute( args := req.args if header.GasPrice == 0 { - logger.With().Warning("ineffective transaction. zero gas price", - log.Object("header", header), - log.Object("account", &ctx.PrincipalAccount), + logger.Warn("ineffective transaction. zero gas price", + zap.Object("header", header), + zap.Object("account", &ctx.PrincipalAccount), ) ineffective = append(ineffective, types.Transaction{RawTx: tx.GetRaw()}) invalidTxCount.Inc() continue } if intrinsic := core.IntrinsicGas(ctx.Gas.BaseGas, tx.GetRaw().Raw); ctx.PrincipalAccount.Balance < intrinsic { - logger.With().Warning("ineffective transaction. intrinsic gas not covered", - log.Object("header", header), - log.Object("account", &ctx.PrincipalAccount), - log.Uint64("intrinsic gas", intrinsic), + logger.Warn("ineffective transaction. intrinsic gas not covered", + zap.Object("header", header), + zap.Object("account", &ctx.PrincipalAccount), + zap.Uint64("intrinsic gas", intrinsic), ) ineffective = append(ineffective, types.Transaction{RawTx: tx.GetRaw()}) invalidTxCount.Inc() continue } if limit < ctx.Header.MaxGas { - logger.With().Warning("ineffective transaction. out of block gas", - log.Uint64("block gas limit", v.cfg.GasLimit), - log.Uint64("current limit", limit), - log.Object("header", header), - log.Object("account", &ctx.PrincipalAccount), + logger.Warn("ineffective transaction. out of block gas", + zap.Uint64("block gas limit", v.cfg.GasLimit), + zap.Uint64("current limit", limit), + zap.Object("header", header), + zap.Object("account", &ctx.PrincipalAccount), ) ineffective = append(ineffective, types.Transaction{RawTx: tx.GetRaw()}) invalidTxCount.Inc() @@ -355,9 +356,9 @@ func (v *VM) execute( // NOTE this part is executed only for transactions that weren't verified // when saved into database by txs module if !tx.Verified() && !req.Verify() { - logger.With().Warning("ineffective transaction. failed verify", - log.Object("header", header), - log.Object("account", &ctx.PrincipalAccount), + logger.Warn("ineffective transaction. failed verify", + zap.Object("header", header), + zap.Object("account", &ctx.PrincipalAccount), ) ineffective = append(ineffective, types.Transaction{RawTx: tx.GetRaw()}) invalidTxCount.Inc() @@ -365,9 +366,9 @@ func (v *VM) execute( } if ctx.PrincipalAccount.NextNonce > ctx.Header.Nonce { - logger.With().Warning("ineffective transaction. nonce too low", - log.Object("header", header), - log.Object("account", &ctx.PrincipalAccount), + logger.Warn("ineffective transaction. nonce too low", + zap.Object("header", header), + zap.Object("account", &ctx.PrincipalAccount), ) ineffective = append(ineffective, types.Transaction{RawTx: tx.GetRaw(), TxHeader: header}) invalidTxCount.Inc() @@ -375,9 +376,9 @@ func (v *VM) execute( } t2 := time.Now() - logger.With().Debug("applying transaction", - log.Object("header", header), - log.Object("account", &ctx.PrincipalAccount), + logger.Debug("applying transaction", + zap.Object("header", header), + zap.Object("account", &ctx.PrincipalAccount), ) rst := types.TransactionWithResult{} @@ -388,10 +389,10 @@ func (v *VM) execute( err = ctx.PrincipalHandler.Exec(ctx, ctx.Header.Method, args) } if err != nil { - logger.With().Debug("transaction failed", - log.Object("header", header), - log.Object("account", &ctx.PrincipalAccount), - log.Err(err), + logger.Debug("transaction failed", + zap.Object("header", header), + zap.Object("account", &ctx.PrincipalAccount), + zap.Error(err), ) if errors.Is(err, core.ErrInternal) { return nil, nil, 0, err @@ -467,7 +468,7 @@ func (r *Request) Verify() bool { } func parse( - logger log.Log, + logger *zap.Logger, lid types.LayerID, reg *registry.Registry, loader core.AccountLoader, @@ -500,7 +501,7 @@ func parse( err, ) } - logger.With().Debug("loaded account state", log.Inline(&account)) + logger.Debug("loaded account state", zap.Inline(&account)) ctx := &core.Context{ GenesisID: cfg.GenesisID, diff --git a/genvm/vm_test.go b/genvm/vm_test.go index 2a81553df7..eb36dcbeab 100644 --- a/genvm/vm_test.go +++ b/genvm/vm_test.go @@ -19,6 +19,7 @@ import ( "github.com/spacemeshos/economics/rewards" "github.com/spacemeshos/go-scale" "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" @@ -32,7 +33,6 @@ import ( "github.com/spacemeshos/go-spacemesh/genvm/templates/vesting" "github.com/spacemeshos/go-spacemesh/genvm/templates/wallet" "github.com/spacemeshos/go-spacemesh/hash" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/signing" "github.com/spacemeshos/go-spacemesh/sql/accounts" "github.com/spacemeshos/go-spacemesh/sql/layers" @@ -49,7 +49,7 @@ func newTester(tb testing.TB) *tester { return &tester{ TB: tb, VM: New(statesql.InMemory(), - WithLogger(logtest.New(tb)), + WithLogger(zaptest.NewLogger(tb)), WithConfig(Config{GasLimit: math.MaxUint64}), ), rng: rand.New(rand.NewSource(time.Now().UnixNano())), @@ -282,7 +282,7 @@ func (t *tester) persistent() *tester { db, err := statesql.Open("file:" + filepath.Join(t.TempDir(), "test.sql")) t.Cleanup(func() { require.NoError(t, db.Close()) }) require.NoError(t, err) - t.VM = New(db, WithLogger(logtest.New(t)), + t.VM = New(db, WithLogger(zaptest.NewLogger(t)), WithConfig(Config{GasLimit: math.MaxUint64})) return t } @@ -2481,6 +2481,7 @@ func FuzzParse(f *testing.F) { } } f.Fuzz(func(t *testing.T, version int, principal []byte, method int, payload, args, sig []byte) { + tt.VM.logger = zaptest.NewLogger(t) var ( buf = bytes.NewBuffer(nil) enc = scale.NewEncoder(buf) @@ -2572,7 +2573,7 @@ func TestVestingData(t *testing.T) { spendAccountNonce := t2.nonces[0] spendAmount := uint64(1_000_000) - vm := New(statesql.InMemory(), WithLogger(logtest.New(t))) + vm := New(statesql.InMemory(), WithLogger(zaptest.NewLogger(t))) require.NoError(t, vm.ApplyGenesis( []core.Account{ { diff --git a/go.mod b/go.mod index 43a58f7bea..d4b4248087 100644 --- a/go.mod +++ b/go.mod @@ -5,22 +5,22 @@ go 1.22.4 require ( cloud.google.com/go/storage v1.43.0 github.com/ALTree/bigfloat v0.2.0 - github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240623075305-82f2d100a2a0 + github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240715013408-888ec3de1c50 github.com/cosmos/btcutil v1.0.5 github.com/go-llsqlite/crawshaw v0.5.3 - github.com/gofrs/flock v0.12.0 + github.com/gofrs/flock v0.12.1 github.com/google/go-cmp v0.6.0 github.com/google/gofuzz v1.2.0 github.com/google/uuid v1.6.0 github.com/grafana/pyroscope-go v1.1.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/ipfs/go-ds-leveldb v0.5.0 github.com/ipfs/go-log/v2 v2.5.1 github.com/jonboulle/clockwork v0.4.0 - github.com/libp2p/go-libp2p v0.35.1 + github.com/libp2p/go-libp2p v0.35.3 github.com/libp2p/go-libp2p-kad-dht v0.25.2 github.com/libp2p/go-libp2p-pubsub v0.11.0 github.com/libp2p/go-libp2p-record v0.2.0 @@ -34,37 +34,36 @@ require ( github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.55.0 - github.com/quic-go/quic-go v0.45.1 + github.com/quic-go/quic-go v0.46.0 github.com/rs/cors v1.11.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/seehuhn/mt19937 v1.0.0 github.com/slok/go-http-metrics v0.12.0 - github.com/spacemeshos/api/release/go v1.50.0 + github.com/spacemeshos/api/release/go v1.52.0 github.com/spacemeshos/economics v0.1.3 github.com/spacemeshos/fixed v0.1.1 github.com/spacemeshos/go-scale v1.2.0 github.com/spacemeshos/merkle-tree v0.2.3 github.com/spacemeshos/poet v0.10.4 - github.com/spacemeshos/post v0.12.7 + github.com/spacemeshos/post v0.12.8 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 - github.com/urfave/cli/v2 v2.27.2 github.com/zeebo/blake3 v0.2.3 go.uber.org/mock v0.4.0 go.uber.org/zap v1.27.0 - golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 - golang.org/x/sync v0.7.0 - golang.org/x/time v0.5.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d + golang.org/x/exp v0.0.0-20240707233637-46b078467d37 + golang.org/x/sync v0.8.0 + golang.org/x/time v0.6.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 - k8s.io/api v0.30.2 - k8s.io/apimachinery v0.30.2 - k8s.io/client-go v0.30.2 + k8s.io/api v0.30.3 + k8s.io/apimachinery v0.30.3 + k8s.io/client-go v0.30.3 sigs.k8s.io/controller-runtime v0.18.4 ) @@ -85,7 +84,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect @@ -116,7 +114,7 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.5 // indirect - github.com/gorilla/websocket v1.5.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.6 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect @@ -202,7 +200,6 @@ require ( github.com/quic-go/webtransport-go v0.8.0 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -211,7 +208,6 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect - github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect @@ -219,21 +215,21 @@ require ( go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/dig v1.17.1 // indirect - go.uber.org/fx v1.21.1 // indirect + go.uber.org/fx v1.22.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/tools v0.23.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect gonum.org/v1/gonum v0.13.0 // indirect google.golang.org/api v0.187.0 // indirect google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240723171418-e6d459c13d2a // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect diff --git a/go.sum b/go.sum index 78a3372a06..394e1c8c26 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,8 @@ github.com/c0mm4nd/go-ripemd v0.0.0-20200326052756-bd1759ad7d10/go.mod h1:mYPR+a github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240623075305-82f2d100a2a0 h1:Zo69MUWFoPKqQmKgSY+F6AwrK5DsAxYv+QEiVcJDrGI= -github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240623075305-82f2d100a2a0/go.mod h1:x11iCbZV6hzzSQWMq610B6Wl5Lg1dhwqcVfeiWQQnQQ= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240715013408-888ec3de1c50 h1:p1+B/vfs0q5p/gUQUSSD/ZfXztP6NjikGVVDilgt8yk= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240715013408-888ec3de1c50/go.mod h1:x11iCbZV6hzzSQWMq610B6Wl5Lg1dhwqcVfeiWQQnQQ= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -76,7 +76,6 @@ github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -155,8 +154,8 @@ github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY= -github.com/gofrs/flock v0.12.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -231,8 +230,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/pyroscope-go v1.1.1 h1:PQoUU9oWtO3ve/fgIiklYuGilvsm8qaGhlY4Vw6MAcQ= github.com/grafana/pyroscope-go v1.1.1/go.mod h1:Mw26jU7jsL/KStNSGGuuVYdUq7Qghem5P8aXYXSXG88= github.com/grafana/pyroscope-go/godeltaprof v0.1.6 h1:nEdZ8louGAplSvIJi1HVp7kWvFvdiiYg3COLlTwJiFo= @@ -245,8 +244,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 h1:CWyXh/jylQWp2dtiV33mY4iSSp6yf4lmn+c7/tN+ObI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0/go.mod h1:nCLIt0w3Ept2NwF8ThLmrppXsfT07oC8k0XNDxd8sVU= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -345,8 +344,8 @@ github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38y github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= -github.com/libp2p/go-libp2p v0.35.1 h1:Hm7Ub2BF+GCb14ojcsEK6WAy5it5smPDK02iXSZLl50= -github.com/libp2p/go-libp2p v0.35.1/go.mod h1:Dnkgba5hsfSv5dvvXC8nfqk44hH0gIKKno+HOMU0fdc= +github.com/libp2p/go-libp2p v0.35.3 h1:7Je71b8YuUD+aYbM6GYU84mNXo9vcc8VDfGuB57RSXs= +github.com/libp2p/go-libp2p v0.35.3/go.mod h1:RKCDNt30IkFipGL0tl8wQW/3zVWEGFUZo8g2gAKxwjU= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-kad-dht v0.25.2 h1:FOIk9gHoe4YRWXTu8SY9Z1d0RILol0TrtApsMDPjAVQ= @@ -540,8 +539,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/quic-go v0.45.1 h1:tPfeYCk+uZHjmDRwHHQmvHRYL2t44ROTujLeFVBmjCA= -github.com/quic-go/quic-go v0.45.1/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= +github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y= +github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv/cD7QFJg= github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM= github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= @@ -555,7 +554,6 @@ github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= @@ -604,8 +602,8 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/spacemeshos/api/release/go v1.50.0 h1:M7Usg/LxymscwqYO7/Doyb+sU4lS1e+JIsSgqTDGk/0= -github.com/spacemeshos/api/release/go v1.50.0/go.mod h1:PvgDpjfwkZLVVNExYG7wDNzgMqT3p+ppfTU2UESSF9U= +github.com/spacemeshos/api/release/go v1.52.0 h1:3cohOoFIk0RLF5fdL0y6pFgZ7Ngg1Yht+aeN3Xm5Qn8= +github.com/spacemeshos/api/release/go v1.52.0/go.mod h1:Qr/pVPMmN5Q5qLHSXqVMDKDCu6LkHWzGPNflylE0u00= github.com/spacemeshos/economics v0.1.3 h1:ACkq3mTebIky4Zwbs9SeSSRZrUCjU/Zk0wq9Z0BTh2A= github.com/spacemeshos/economics v0.1.3/go.mod h1:FH7u0FzTIm6Kpk+X5HOZDvpkgNYBKclmH86rVwYaDAo= github.com/spacemeshos/fixed v0.1.1 h1:N1y4SUpq1EV+IdJrWJwUCt1oBFzeru/VKVcBsvPc2Fk= @@ -616,8 +614,8 @@ github.com/spacemeshos/merkle-tree v0.2.3 h1:zGEgOR9nxAzJr0EWjD39QFngwFEOxfxMloE github.com/spacemeshos/merkle-tree v0.2.3/go.mod h1:VomOcQ5pCBXz7goiWMP5hReyqOfDXGSKbrH2GB9Htww= github.com/spacemeshos/poet v0.10.4 h1:MHGG1dhMVwy5DdlsmwdRLDQTTqlPA21lSQB2PVd8MSk= github.com/spacemeshos/poet v0.10.4/go.mod h1:hz21GMyHb9h29CqNhVeKxCD5dxZdQK27nAqLpT46gjE= -github.com/spacemeshos/post v0.12.7 h1:0pLD19TWM6EktFhnd+7QW8ifvdVH952EKliGUN49gFk= -github.com/spacemeshos/post v0.12.7/go.mod h1:WzfVgaa1wxgrsytC4EVKkG8rwoUxjyoyQL0ZSxs56Y0= +github.com/spacemeshos/post v0.12.8 h1:xcxVPWTvt2zJYJUvdpmX7CwR1CQnMnmd4VndThGbSdY= +github.com/spacemeshos/post v0.12.8/go.mod h1:WzfVgaa1wxgrsytC4EVKkG8rwoUxjyoyQL0ZSxs56Y0= github.com/spacemeshos/sha256-simd v0.1.0 h1:G7Mfu5RYdQiuE+wu4ZyJ7I0TI74uqLhFnKblEnSpjYI= github.com/spacemeshos/sha256-simd v0.1.0/go.mod h1:O8CClVIilId7RtuCMV2+YzMj6qjVn75JsxOxaE8vcfM= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= @@ -660,8 +658,6 @@ github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cb github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= -github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= @@ -669,8 +665,6 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvS github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= -github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -700,8 +694,8 @@ go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.21.1 h1:RqBh3cYdzZS0uqwVeEjOX2p73dddLpym315myy/Bpb0= -go.uber.org/fx v1.21.1/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48= +go.uber.org/fx v1.22.1 h1:nvvln7mwyT5s1q201YE29V/BFrGor6vMiDNpU/78Mys= +go.uber.org/fx v1.22.1/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -736,11 +730,11 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= +golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -754,8 +748,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -784,8 +778,8 @@ golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -803,8 +797,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -842,8 +836,8 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -854,8 +848,8 @@ golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -870,8 +864,8 @@ golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -892,8 +886,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -921,10 +915,10 @@ google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d h1:PksQg4dV6Sem3/HkBX+Ltq8T0ke0PKIRBNBatoDTVls= google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:s7iA721uChleev562UJO2OYB0PPT9CMFjV+Ce7VJH5M= -google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc= -google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240723171418-e6d459c13d2a h1:YIa/rzVqMEokBkPtydCkx1VLmv3An1Uw7w1P1m6EhOY= +google.golang.org/genproto/googleapis/api v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a h1:hqK4+jJZXCU4pW7jsAdGOVFIfLHQeV7LaizZKnZ84HI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240723171418-e6d459c13d2a/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -980,14 +974,14 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= -k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= +k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ= +k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04= k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4= -k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= -k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= -k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= +k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc= +k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k= +k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= diff --git a/hare3/compat/weakcoin.go b/hare3/compat/weakcoin.go index 79404aceb9..6b8758454a 100644 --- a/hare3/compat/weakcoin.go +++ b/hare3/compat/weakcoin.go @@ -6,14 +6,14 @@ import ( "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/hare3" + "github.com/spacemeshos/go-spacemesh/hare4" ) type weakCoin interface { Set(types.LayerID, bool) error } -func ReportWeakcoin(ctx context.Context, logger *zap.Logger, from <-chan hare3.WeakCoinOutput, to weakCoin) { +func ReportWeakcoin(ctx context.Context, logger *zap.Logger, from <-chan hare4.WeakCoinOutput, to weakCoin) { for { select { case <-ctx.Done(): diff --git a/hare3/eligibility/oracle.go b/hare3/eligibility/oracle.go index 823aa71910..c33cf0c50b 100644 --- a/hare3/eligibility/oracle.go +++ b/hare3/eligibility/oracle.go @@ -9,6 +9,8 @@ import ( lru "github.com/hashicorp/golang-lru/v2" "github.com/spacemeshos/fixed" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" "golang.org/x/exp/maps" "github.com/spacemeshos/go-spacemesh/atxsdata" @@ -69,7 +71,7 @@ type Config struct { ConfidenceParam uint32 `mapstructure:"eligibility-confidence-param"` } -func (c *Config) MarshalLogObject(encoder log.ObjectEncoder) error { +func (c *Config) MarshalLogObject(encoder zapcore.ObjectEncoder) error { encoder.AddUint32("confidence param", c.ConfidenceParam) return nil } @@ -96,7 +98,7 @@ type Oracle struct { vrfVerifier vrfVerifier layersPerEpoch uint32 cfg Config - log log.Log + log *zap.Logger } type Opt func(*Oracle) @@ -107,7 +109,7 @@ func WithConfig(config Config) Opt { } } -func WithLogger(logger log.Log) Opt { +func WithLogger(logger *zap.Logger) Opt { return func(o *Oracle) { o.log = logger } @@ -135,12 +137,12 @@ func New( activesCache: activesCache, fallback: map[types.EpochID][]types.ATXID{}, cfg: DefaultConfig(), - log: log.NewNop(), + log: zap.NewNop(), } for _, opt := range opts { opt(oracle) } - oracle.log.With().Info("hare oracle initialized", log.Uint32("epoch size", layersPerEpoch), log.Inline(&oracle.cfg)) + oracle.log.Info("hare oracle initialized", zap.Uint32("epoch size", layersPerEpoch), zap.Inline(&oracle.cfg)) return oracle } @@ -169,7 +171,7 @@ func (o *Oracle) resetCacheOnSynced(ctx context.Context) { if !synced && o.synced { ac, err := lru.New[types.EpochID, *cachedActiveSet](activesCacheSize) if err != nil { - o.log.With().Fatal("failed to create lru cache for active set", log.Err(err)) + o.log.Fatal("failed to create lru cache for active set", zap.Error(err)) } o.activesCache = ac } @@ -217,16 +219,17 @@ func (o *Oracle) prepareEligibilityCheck( id types.NodeID, vrfSig types.VrfSignature, ) (int, fixed.Fixed, fixed.Fixed, bool, error) { - logger := o.log.WithContext(ctx).WithFields( - layer, - layer.GetEpoch(), - log.Stringer("smesher", id), - log.Uint32("round", round), - log.Int("committee_size", committeeSize), + logger := o.log.With( + log.ZContext(ctx), + zap.Uint32("layer", layer.Uint32()), + zap.Uint32("epoch", layer.GetEpoch().Uint32()), + log.ZShortStringer("smesherID", id), + zap.Uint32("round", round), + zap.Int("committee_size", committeeSize), ) if committeeSize < 1 { - logger.With().Error("committee size must be positive", log.Int("committee_size", committeeSize)) + logger.Error("committee size must be positive", zap.Int("committee_size", committeeSize)) return 0, fixed.Fixed{}, fixed.Fixed{}, true, errZeroCommitteeSize } @@ -239,7 +242,7 @@ func (o *Oracle) prepareEligibilityCheck( msg, err := o.buildVRFMessage(ctx, layer, round) if err != nil { - logger.With().Warning("could not build vrf message", log.Err(err)) + logger.Warn("could not build vrf message", zap.Error(err)) return 0, fixed.Fixed{}, fixed.Fixed{}, true, err } @@ -252,28 +255,28 @@ func (o *Oracle) prepareEligibilityCheck( // get active set size totalWeight, err := o.totalWeight(ctx, layer) if err != nil { - logger.With().Error("failed to get total weight", log.Err(err)) + logger.Error("failed to get total weight", zap.Error(err)) return 0, fixed.Fixed{}, fixed.Fixed{}, true, err } // require totalWeight > 0 if totalWeight == 0 { - logger.Warning("eligibility: total weight is zero") + logger.Warn("eligibility: total weight is zero") return 0, fixed.Fixed{}, fixed.Fixed{}, true, errZeroTotalWeight } - logger.With().Debug("preparing eligibility check", - log.Uint64("miner_weight", minerWeight), - log.Uint64("total_weight", totalWeight), + logger.Debug("preparing eligibility check", + zap.Uint64("miner_weight", minerWeight), + zap.Uint64("total_weight", totalWeight), ) n := minerWeight // calc p if uint64(committeeSize) > totalWeight { - logger.With().Warning("committee size is greater than total weight", - log.Int("committee_size", committeeSize), - log.Uint64("total_weight", totalWeight), + logger.Warn("committee size is greater than total weight", + zap.Int("committee_size", committeeSize), + zap.Uint64("total_weight", totalWeight), ) totalWeight *= uint64(committeeSize) n *= uint64(committeeSize) @@ -309,11 +312,12 @@ func (o *Oracle) Validate( defer func() { if msg := recover(); msg != nil { - o.log.WithContext(ctx).With().Fatal("panic in validate", - log.Any("msg", msg), - log.Int("n", n), - log.String("p", p.String()), - log.String("vrf_frac", vrfFrac.String()), + o.log.Fatal("panic in validate", + log.ZContext(ctx), + zap.Any("msg", msg), + zap.Int("n", n), + zap.String("p", p.String()), + zap.String("vrf_frac", vrfFrac.String()), ) } }() @@ -322,16 +326,17 @@ func (o *Oracle) Validate( if !fixed.BinCDF(n, p, x-1).GreaterThan(vrfFrac) && vrfFrac.LessThan(fixed.BinCDF(n, p, x)) { return true, nil } - o.log.WithContext(ctx).With().Warning("eligibility: node did not pass vrf eligibility threshold", - layer, - log.Uint32("round", round), - log.Int("committee_size", committeeSize), - id, - log.Uint16("eligibility_count", eligibilityCount), - log.Int("n", n), - log.Float64("p", p.Float()), - log.Float64("vrf_frac", vrfFrac.Float()), - log.Int("x", x), + o.log.Info("eligibility: node did not pass vrf eligibility threshold", + log.ZContext(ctx), + zap.Uint32("layer", layer.Uint32()), + zap.Uint32("round", round), + zap.Int("committee_size", committeeSize), + log.ZShortStringer("smesherID", id), + zap.Uint16("eligibility_count", eligibilityCount), + zap.Int("n", n), + zap.Float64("p", p.Float()), + zap.Float64("vrf_frac", vrfFrac.Float()), + zap.Int("x", x), ) return false, nil } @@ -351,29 +356,14 @@ func (o *Oracle) CalcEligibility( return 0, err } - defer func() { - if msg := recover(); msg != nil { - o.log.With().Fatal("panic in calc eligibility", - layer, - layer.GetEpoch(), - log.Uint32("round_id", round), - log.Any("msg", msg), - log.Int("committee_size", committeeSize), - log.Int("n", n), - log.Float64("p", p.Float()), - log.Float64("vrf_frac", vrfFrac.Float()), - ) - } - }() - - o.log.With().Debug("params", - layer, - layer.GetEpoch(), - log.Uint32("round_id", round), - log.Int("committee_size", committeeSize), - log.Int("n", n), - log.Float64("p", p.Float()), - log.Float64("vrf_frac", vrfFrac.Float()), + o.log.Debug("params", + zap.Uint32("layer", layer.Uint32()), + zap.Uint32("epoch", layer.GetEpoch().Uint32()), + zap.Uint32("round_id", round), + zap.Int("committee_size", committeeSize), + zap.Int("n", n), + zap.Float64("p", p.Float()), + zap.Float64("vrf_frac", vrfFrac.Float()), ) for x := 0; x < n; x++ { @@ -427,10 +417,11 @@ func (o *Oracle) actives(ctx context.Context, targetLayer types.LayerID) (*cache targetLayer.Difference(targetEpoch.FirstLayer()) < o.cfg.ConfidenceParam { targetEpoch -= 1 } - o.log.WithContext(ctx).With().Debug("hare oracle getting active set", - log.Stringer("target_layer", targetLayer), - log.Stringer("target_layer_epoch", targetLayer.GetEpoch()), - log.Stringer("target_epoch", targetEpoch), + o.log.Debug("hare oracle getting active set", + log.ZContext(ctx), + zap.Uint32("target_layer", targetLayer.Uint32()), + zap.Uint32("target_layer_epoch", targetLayer.GetEpoch().Uint32()), + zap.Uint32("target_epoch", targetEpoch.Uint32()), ) o.mu.Lock() @@ -455,7 +446,7 @@ func (o *Oracle) actives(ctx context.Context, targetLayer types.LayerID) (*cache for _, aweight := range activeWeights { aset.total += aweight.weight } - o.log.WithContext(ctx).With().Info("got hare active set", log.Int("count", len(activeWeights))) + o.log.Debug("got hare active set", log.ZContext(ctx), zap.Int("count", len(activeWeights))) o.activesCache.Add(targetEpoch, aset) return aset, nil } @@ -471,9 +462,10 @@ func (o *Oracle) ActiveSet(ctx context.Context, targetEpoch types.EpochID) ([]ty func (o *Oracle) computeActiveSet(ctx context.Context, targetEpoch types.EpochID) ([]types.ATXID, error) { activeSet, ok := o.fallback[targetEpoch] if ok { - o.log.WithContext(ctx).With().Info("using fallback active set", - targetEpoch, - log.Int("size", len(activeSet)), + o.log.Debug("using fallback active set", + log.ZContext(ctx), + zap.Uint32("target_epoch", targetEpoch.Uint32()), + zap.Int("size", len(activeSet)), ) return activeSet, nil } @@ -515,19 +507,19 @@ func (o *Oracle) activeSetFromRefBallots(epoch types.EpochID) ([]types.ATXID, er activeMap := make(map[types.ATXID]struct{}, len(ballotsrst)) for _, ballot := range ballotsrst { if ballot.EpochData == nil { - o.log.With().Error("invalid data. first ballot doesn't have epoch data", log.Inline(ballot)) + o.log.Error("invalid data. first ballot doesn't have epoch data", zap.Inline(ballot)) continue } if ballot.EpochData.Beacon != beacon { - o.log.With().Debug("beacon mismatch", log.Stringer("local", beacon), log.Object("ballot", ballot)) + o.log.Debug("beacon mismatch", zap.Stringer("local", beacon), zap.Object("ballot", ballot)) continue } actives, err := activesets.Get(o.db, ballot.EpochData.ActiveSetHash) if err != nil { - o.log.With().Error("failed to get active set", - log.String("actives hash", ballot.EpochData.ActiveSetHash.ShortString()), - log.String("ballot ", ballot.ID().String()), - log.Err(err), + o.log.Error("failed to get active set", + zap.String("actives hash", ballot.EpochData.ActiveSetHash.ShortString()), + zap.String("ballot ", ballot.ID().String()), + zap.Error(err), ) continue } @@ -535,10 +527,10 @@ func (o *Oracle) activeSetFromRefBallots(epoch types.EpochID) ([]types.ATXID, er activeMap[id] = struct{}{} } } - o.log.With().Warning("using tortoise active set", - log.Int("actives size", len(activeMap)), - log.Uint32("epoch", epoch.Uint32()), - log.Stringer("beacon", beacon), + o.log.Warn("using tortoise active set", + zap.Int("actives size", len(activeMap)), + zap.Uint32("epoch", epoch.Uint32()), + zap.Stringer("beacon", beacon), ) return maps.Keys(activeMap), nil } @@ -550,9 +542,9 @@ func (o *Oracle) IsIdentityActiveOnConsensusView( edID types.NodeID, layer types.LayerID, ) (bool, error) { - o.log.WithContext(ctx).With().Debug("hare oracle checking for active identity") + o.log.Debug("hare oracle checking for active identity", log.ZContext(ctx)) defer func() { - o.log.WithContext(ctx).With().Debug("hare oracle active identity check complete") + o.log.Debug("hare oracle active identity check complete", log.ZContext(ctx)) }() actives, err := o.actives(ctx, layer) if err != nil { @@ -563,14 +555,14 @@ func (o *Oracle) IsIdentityActiveOnConsensusView( } func (o *Oracle) UpdateActiveSet(epoch types.EpochID, activeSet []types.ATXID) { - o.log.With().Info("received activeset update", - epoch, - log.Int("size", len(activeSet)), + o.log.Debug("received activeset update", + zap.Uint32("epoch", epoch.Uint32()), + zap.Int("size", len(activeSet)), ) o.mu.Lock() defer o.mu.Unlock() if _, ok := o.fallback[epoch]; ok { - o.log.With().Debug("fallback active set already exists", epoch) + o.log.Debug("fallback active set already exists", zap.Uint32("epoch", epoch.Uint32())) return } o.fallback[epoch] = activeSet diff --git a/hare3/eligibility/oracle_test.go b/hare3/eligibility/oracle_test.go index 94addaf509..88c684278a 100644 --- a/hare3/eligibility/oracle_test.go +++ b/hare3/eligibility/oracle_test.go @@ -16,11 +16,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "golang.org/x/exp/maps" "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/signing" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/activesets" @@ -69,7 +69,7 @@ func defaultOracle(tb testing.TB) *testOracle { mVerifier, defLayersPerEpoch, WithConfig(Config{ConfidenceParam: confidenceParam}), - WithLogger(logtest.New(tb)), + WithLogger(zaptest.NewLogger(tb)), ), tb: tb, mBeacon: mBeacon, diff --git a/hare3/hare.go b/hare3/hare.go index 4e8a952da1..2f0076964c 100644 --- a/hare3/hare.go +++ b/hare3/hare.go @@ -17,6 +17,7 @@ import ( "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/hare4" "github.com/spacemeshos/go-spacemesh/layerpatrol" "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/metrics" @@ -154,6 +155,14 @@ func WithTracer(tracer Tracer) Opt { } } +// WithResultsChan overrides the default result channel with a different one. +// This is only needed for the migration period between hare3 and hare4. +func WithResultsChan(c chan hare4.ConsensusOutput) Opt { + return func(hr *Hare) { + hr.results = c + } +} + type nodeclock interface { AwaitLayer(types.LayerID) <-chan struct{} CurrentLayer() types.LayerID @@ -176,8 +185,8 @@ func New( hr := &Hare{ ctx: ctx, cancel: cancel, - results: make(chan ConsensusOutput, 32), - coins: make(chan WeakCoinOutput, 32), + results: make(chan hare4.ConsensusOutput, 32), + coins: make(chan hare4.WeakCoinOutput, 32), signers: map[string]*signing.EdSigner{}, sessions: map[types.LayerID]*protocol{}, @@ -211,8 +220,8 @@ type Hare struct { ctx context.Context cancel context.CancelFunc eg errgroup.Group - results chan ConsensusOutput - coins chan WeakCoinOutput + results chan hare4.ConsensusOutput + coins chan hare4.WeakCoinOutput mu sync.Mutex signers map[string]*signing.EdSigner sessions map[types.LayerID]*protocol @@ -242,11 +251,11 @@ func (h *Hare) Register(sig *signing.EdSigner) { h.signers[string(sig.NodeID().Bytes())] = sig } -func (h *Hare) Results() <-chan ConsensusOutput { +func (h *Hare) Results() <-chan hare4.ConsensusOutput { return h.results } -func (h *Hare) Coins() <-chan WeakCoinOutput { +func (h *Hare) Coins() <-chan hare4.WeakCoinOutput { return h.coins } @@ -260,8 +269,8 @@ func (h *Hare) Start() { } h.log.Info("started", zap.Inline(&h.config), - zap.Uint32("enabled", enabled.Uint32()), - zap.Uint32("disabled", disabled.Uint32()), + zap.Uint32("enabled layer", enabled.Uint32()), + zap.Uint32("disabled layer", disabled.Uint32()), ) h.eg.Go(func() error { for next := enabled; next < disabled; next++ { @@ -466,7 +475,7 @@ func (h *Hare) run(session *session) error { // we are logginng stats 1 network delay after new iteration start // so that we can receive notify messages from previous iteration if session.proto.Round == softlock && h.config.LogStats { - h.log.Info("stats", zap.Uint32("lid", session.lid.Uint32()), zap.Inline(session.proto.Stats())) + h.log.Debug("stats", zap.Uint32("lid", session.lid.Uint32()), zap.Inline(session.proto.Stats())) } if out.terminated { if !result { @@ -475,8 +484,7 @@ func (h *Hare) run(session *session) error { return nil } if current.Iter == h.config.IterationsLimit { - return fmt.Errorf("hare failed to reach consensus in %d iterations", - h.config.IterationsLimit) + return fmt.Errorf("hare failed to reach consensus in %d iterations", h.config.IterationsLimit) } case <-h.ctx.Done(): return nil @@ -508,7 +516,7 @@ func (h *Hare) onOutput(session *session, ir IterRound, out output) error { select { case <-h.ctx.Done(): return h.ctx.Err() - case h.coins <- WeakCoinOutput{Layer: session.lid, Coin: *out.coin}: + case h.coins <- hare4.WeakCoinOutput{Layer: session.lid, Coin: *out.coin}: } sessionCoin.Inc() } @@ -516,7 +524,7 @@ func (h *Hare) onOutput(session *session, ir IterRound, out output) error { select { case <-h.ctx.Done(): return h.ctx.Err() - case h.results <- ConsensusOutput{Layer: session.lid, Proposals: out.result}: + case h.results <- hare4.ConsensusOutput{Layer: session.lid, Proposals: out.result}: } sessionResult.Inc() } @@ -599,8 +607,8 @@ func (h *Hare) selectProposals(session *session) []types.ProposalID { h.log.Warn("proposal has different beacon value", zap.Uint32("lid", session.lid.Uint32()), zap.Stringer("id", p.ID()), - zap.String("proposal_beacon", p.Beacon().ShortString()), - zap.String("epoch_beacon", session.beacon.ShortString()), + zap.Stringer("proposal_beacon", p.Beacon()), + zap.Stringer("epoch_beacon", session.beacon), ) } } @@ -618,7 +626,6 @@ func (h *Hare) OnProposal(p *types.Proposal) error { func (h *Hare) Stop() { h.cancel() h.eg.Wait() - close(h.results) close(h.coins) h.log.Info("stopped") } diff --git a/hare3/hare_test.go b/hare3/hare_test.go index c9ea3634c2..76543ffafe 100644 --- a/hare3/hare_test.go +++ b/hare3/hare_test.go @@ -22,7 +22,6 @@ import ( "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/hare3/eligibility" "github.com/spacemeshos/go-spacemesh/layerpatrol" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" pmocks "github.com/spacemeshos/go-spacemesh/p2p/pubsub/mocks" "github.com/spacemeshos/go-spacemesh/proposals/store" @@ -149,8 +148,8 @@ func (n *node) reuseSigner(signer *signing.EdSigner) *node { return n } -func (n *node) withDb() *node { - n.db = statesql.InMemory() +func (n *node) withDb(tb testing.TB) *node { + n.db = statesql.InMemoryTest(tb) n.atxsdata = atxsdata.New() n.proposals = store.New() return n @@ -211,7 +210,7 @@ func (n *node) withPublisher() *node { } func (n *node) withHare() *node { - logger := logtest.New(n.t).Named(fmt.Sprintf("hare=%d", n.i)) + logger := zaptest.NewLogger(n.t).Named(fmt.Sprintf("hare=%d", n.i)) n.nclock = &testNodeClock{ genesis: n.t.start, @@ -231,7 +230,7 @@ func (n *node) withHare() *node { n.msyncer, n.patrol, WithConfig(n.t.cfg), - WithLogger(logger.Zap()), + WithLogger(logger), WithWallclock(n.clock), WithTracer(tracer), ) @@ -343,7 +342,7 @@ func (cl *lockstepCluster) addActive(n int) *lockstepCluster { for i := last; i < last+n; i++ { cl.addNode((&node{t: cl.t, i: i}). withController().withSyncer().withPublisher(). - withClock().withDb().withSigner().withAtx(cl.units.min, cl.units.max). + withClock().withDb(cl.t).withSigner().withAtx(cl.units.min, cl.units.max). withOracle().withHare()) } return cl @@ -354,7 +353,7 @@ func (cl *lockstepCluster) addInactive(n int) *lockstepCluster { for i := last; i < last+n; i++ { cl.addNode((&node{t: cl.t, i: i}). withController().withSyncer().withPublisher(). - withClock().withDb().withSigner(). + withClock().withDb(cl.t).withSigner(). withOracle().withHare()) } return cl @@ -367,7 +366,7 @@ func (cl *lockstepCluster) addEquivocators(n int) *lockstepCluster { cl.addNode((&node{t: cl.t, i: i}). reuseSigner(cl.nodes[i-last].signer). withController().withSyncer().withPublisher(). - withClock().withDb().withAtx(cl.units.min, cl.units.max). + withClock().withDb(cl.t).withAtx(cl.units.min, cl.units.max). withOracle().withHare()) } return cl diff --git a/hare3/malfeasance.go b/hare3/malfeasance.go index e0037d7ef3..e71a9c6985 100644 --- a/hare3/malfeasance.go +++ b/hare3/malfeasance.go @@ -77,7 +77,7 @@ func (mh *MalfeasanceHandler) Validate(ctx context.Context, data wire.ProofData) msg1.InnerMsg.MsgHash != msg2.InnerMsg.MsgHash { return msg1.SmesherID, nil } - mh.logger.Warn("received invalid hare malfeasance proof", + mh.logger.Debug("received invalid hare malfeasance proof", log.ZContext(ctx), zap.Stringer("first_smesher", hp.Messages[0].SmesherID), zap.Object("first_proof", &hp.Messages[0].InnerMsg), diff --git a/hare3/malfeasance_test.go b/hare3/malfeasance_test.go index c8b1ee37b3..0382ad3741 100644 --- a/hare3/malfeasance_test.go +++ b/hare3/malfeasance_test.go @@ -27,7 +27,7 @@ type testMalfeasanceHandler struct { } func newTestMalfeasanceHandler(tb testing.TB) *testMalfeasanceHandler { - db := statesql.InMemory() + db := statesql.InMemoryTest(tb) observer, observedLogs := observer.New(zapcore.WarnLevel) logger := zaptest.NewLogger(tb, zaptest.WrapOptions(zap.WrapCore( func(core zapcore.Core) zapcore.Core { diff --git a/hare4/compat/weakcoin.go b/hare4/compat/weakcoin.go new file mode 100644 index 0000000000..6b8758454a --- /dev/null +++ b/hare4/compat/weakcoin.go @@ -0,0 +1,34 @@ +package compat + +import ( + "context" + + "go.uber.org/zap" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/hare4" +) + +type weakCoin interface { + Set(types.LayerID, bool) error +} + +func ReportWeakcoin(ctx context.Context, logger *zap.Logger, from <-chan hare4.WeakCoinOutput, to weakCoin) { + for { + select { + case <-ctx.Done(): + logger.Info("weak coin reporter exited") + return + case out, open := <-from: + if !open { + return + } + if err := to.Set(out.Layer, out.Coin); err != nil { + logger.Error("failed to update weakcoin", + zap.Uint32("lid", out.Layer.Uint32()), + zap.Error(err), + ) + } + } + } +} diff --git a/hare4/eligibility/interface.go b/hare4/eligibility/interface.go new file mode 100644 index 0000000000..e87f959dc5 --- /dev/null +++ b/hare4/eligibility/interface.go @@ -0,0 +1,26 @@ +package eligibility + +import ( + "context" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" +) + +//go:generate mockgen -typed -package=eligibility -destination=./mocks.go -source=./interface.go + +type activeSetCache interface { + Add(key types.EpochID, value *cachedActiveSet) (evicted bool) + Get(key types.EpochID) (value *cachedActiveSet, ok bool) +} + +type vrfVerifier interface { + Verify(nodeID types.NodeID, msg []byte, sig types.VrfSignature) bool +} + +// Rolacle is the roles oracle provider. +type Rolacle interface { + Validate(context.Context, types.LayerID, uint32, int, types.NodeID, types.VrfSignature, uint16) (bool, error) + CalcEligibility(context.Context, types.LayerID, uint32, int, types.NodeID, types.VrfSignature) (uint16, error) + Proof(context.Context, *signing.VRFSigner, types.LayerID, uint32) (types.VrfSignature, error) +} diff --git a/hare4/eligibility/mocks.go b/hare4/eligibility/mocks.go new file mode 100644 index 0000000000..e99c29ac08 --- /dev/null +++ b/hare4/eligibility/mocks.go @@ -0,0 +1,320 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./interface.go +// +// Generated by this command: +// +// mockgen -typed -package=eligibility -destination=./mocks.go -source=./interface.go +// + +// Package eligibility is a generated GoMock package. +package eligibility + +import ( + context "context" + reflect "reflect" + + types "github.com/spacemeshos/go-spacemesh/common/types" + signing "github.com/spacemeshos/go-spacemesh/signing" + gomock "go.uber.org/mock/gomock" +) + +// MockactiveSetCache is a mock of activeSetCache interface. +type MockactiveSetCache struct { + ctrl *gomock.Controller + recorder *MockactiveSetCacheMockRecorder +} + +// MockactiveSetCacheMockRecorder is the mock recorder for MockactiveSetCache. +type MockactiveSetCacheMockRecorder struct { + mock *MockactiveSetCache +} + +// NewMockactiveSetCache creates a new mock instance. +func NewMockactiveSetCache(ctrl *gomock.Controller) *MockactiveSetCache { + mock := &MockactiveSetCache{ctrl: ctrl} + mock.recorder = &MockactiveSetCacheMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockactiveSetCache) EXPECT() *MockactiveSetCacheMockRecorder { + return m.recorder +} + +// Add mocks base method. +func (m *MockactiveSetCache) Add(key types.EpochID, value *cachedActiveSet) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Add", key, value) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Add indicates an expected call of Add. +func (mr *MockactiveSetCacheMockRecorder) Add(key, value any) *MockactiveSetCacheAddCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockactiveSetCache)(nil).Add), key, value) + return &MockactiveSetCacheAddCall{Call: call} +} + +// MockactiveSetCacheAddCall wrap *gomock.Call +type MockactiveSetCacheAddCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockactiveSetCacheAddCall) Return(evicted bool) *MockactiveSetCacheAddCall { + c.Call = c.Call.Return(evicted) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockactiveSetCacheAddCall) Do(f func(types.EpochID, *cachedActiveSet) bool) *MockactiveSetCacheAddCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockactiveSetCacheAddCall) DoAndReturn(f func(types.EpochID, *cachedActiveSet) bool) *MockactiveSetCacheAddCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Get mocks base method. +func (m *MockactiveSetCache) Get(key types.EpochID) (*cachedActiveSet, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", key) + ret0, _ := ret[0].(*cachedActiveSet) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockactiveSetCacheMockRecorder) Get(key any) *MockactiveSetCacheGetCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockactiveSetCache)(nil).Get), key) + return &MockactiveSetCacheGetCall{Call: call} +} + +// MockactiveSetCacheGetCall wrap *gomock.Call +type MockactiveSetCacheGetCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockactiveSetCacheGetCall) Return(value *cachedActiveSet, ok bool) *MockactiveSetCacheGetCall { + c.Call = c.Call.Return(value, ok) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockactiveSetCacheGetCall) Do(f func(types.EpochID) (*cachedActiveSet, bool)) *MockactiveSetCacheGetCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockactiveSetCacheGetCall) DoAndReturn(f func(types.EpochID) (*cachedActiveSet, bool)) *MockactiveSetCacheGetCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockvrfVerifier is a mock of vrfVerifier interface. +type MockvrfVerifier struct { + ctrl *gomock.Controller + recorder *MockvrfVerifierMockRecorder +} + +// MockvrfVerifierMockRecorder is the mock recorder for MockvrfVerifier. +type MockvrfVerifierMockRecorder struct { + mock *MockvrfVerifier +} + +// NewMockvrfVerifier creates a new mock instance. +func NewMockvrfVerifier(ctrl *gomock.Controller) *MockvrfVerifier { + mock := &MockvrfVerifier{ctrl: ctrl} + mock.recorder = &MockvrfVerifierMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockvrfVerifier) EXPECT() *MockvrfVerifierMockRecorder { + return m.recorder +} + +// Verify mocks base method. +func (m *MockvrfVerifier) Verify(nodeID types.NodeID, msg []byte, sig types.VrfSignature) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Verify", nodeID, msg, sig) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Verify indicates an expected call of Verify. +func (mr *MockvrfVerifierMockRecorder) Verify(nodeID, msg, sig any) *MockvrfVerifierVerifyCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockvrfVerifier)(nil).Verify), nodeID, msg, sig) + return &MockvrfVerifierVerifyCall{Call: call} +} + +// MockvrfVerifierVerifyCall wrap *gomock.Call +type MockvrfVerifierVerifyCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockvrfVerifierVerifyCall) Return(arg0 bool) *MockvrfVerifierVerifyCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockvrfVerifierVerifyCall) Do(f func(types.NodeID, []byte, types.VrfSignature) bool) *MockvrfVerifierVerifyCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockvrfVerifierVerifyCall) DoAndReturn(f func(types.NodeID, []byte, types.VrfSignature) bool) *MockvrfVerifierVerifyCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockRolacle is a mock of Rolacle interface. +type MockRolacle struct { + ctrl *gomock.Controller + recorder *MockRolacleMockRecorder +} + +// MockRolacleMockRecorder is the mock recorder for MockRolacle. +type MockRolacleMockRecorder struct { + mock *MockRolacle +} + +// NewMockRolacle creates a new mock instance. +func NewMockRolacle(ctrl *gomock.Controller) *MockRolacle { + mock := &MockRolacle{ctrl: ctrl} + mock.recorder = &MockRolacleMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRolacle) EXPECT() *MockRolacleMockRecorder { + return m.recorder +} + +// CalcEligibility mocks base method. +func (m *MockRolacle) CalcEligibility(arg0 context.Context, arg1 types.LayerID, arg2 uint32, arg3 int, arg4 types.NodeID, arg5 types.VrfSignature) (uint16, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CalcEligibility", arg0, arg1, arg2, arg3, arg4, arg5) + ret0, _ := ret[0].(uint16) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CalcEligibility indicates an expected call of CalcEligibility. +func (mr *MockRolacleMockRecorder) CalcEligibility(arg0, arg1, arg2, arg3, arg4, arg5 any) *MockRolacleCalcEligibilityCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CalcEligibility", reflect.TypeOf((*MockRolacle)(nil).CalcEligibility), arg0, arg1, arg2, arg3, arg4, arg5) + return &MockRolacleCalcEligibilityCall{Call: call} +} + +// MockRolacleCalcEligibilityCall wrap *gomock.Call +type MockRolacleCalcEligibilityCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockRolacleCalcEligibilityCall) Return(arg0 uint16, arg1 error) *MockRolacleCalcEligibilityCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockRolacleCalcEligibilityCall) Do(f func(context.Context, types.LayerID, uint32, int, types.NodeID, types.VrfSignature) (uint16, error)) *MockRolacleCalcEligibilityCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockRolacleCalcEligibilityCall) DoAndReturn(f func(context.Context, types.LayerID, uint32, int, types.NodeID, types.VrfSignature) (uint16, error)) *MockRolacleCalcEligibilityCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Proof mocks base method. +func (m *MockRolacle) Proof(arg0 context.Context, arg1 *signing.VRFSigner, arg2 types.LayerID, arg3 uint32) (types.VrfSignature, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Proof", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(types.VrfSignature) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Proof indicates an expected call of Proof. +func (mr *MockRolacleMockRecorder) Proof(arg0, arg1, arg2, arg3 any) *MockRolacleProofCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Proof", reflect.TypeOf((*MockRolacle)(nil).Proof), arg0, arg1, arg2, arg3) + return &MockRolacleProofCall{Call: call} +} + +// MockRolacleProofCall wrap *gomock.Call +type MockRolacleProofCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockRolacleProofCall) Return(arg0 types.VrfSignature, arg1 error) *MockRolacleProofCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockRolacleProofCall) Do(f func(context.Context, *signing.VRFSigner, types.LayerID, uint32) (types.VrfSignature, error)) *MockRolacleProofCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockRolacleProofCall) DoAndReturn(f func(context.Context, *signing.VRFSigner, types.LayerID, uint32) (types.VrfSignature, error)) *MockRolacleProofCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Validate mocks base method. +func (m *MockRolacle) Validate(arg0 context.Context, arg1 types.LayerID, arg2 uint32, arg3 int, arg4 types.NodeID, arg5 types.VrfSignature, arg6 uint16) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validate", arg0, arg1, arg2, arg3, arg4, arg5, arg6) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Validate indicates an expected call of Validate. +func (mr *MockRolacleMockRecorder) Validate(arg0, arg1, arg2, arg3, arg4, arg5, arg6 any) *MockRolacleValidateCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockRolacle)(nil).Validate), arg0, arg1, arg2, arg3, arg4, arg5, arg6) + return &MockRolacleValidateCall{Call: call} +} + +// MockRolacleValidateCall wrap *gomock.Call +type MockRolacleValidateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockRolacleValidateCall) Return(arg0 bool, arg1 error) *MockRolacleValidateCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockRolacleValidateCall) Do(f func(context.Context, types.LayerID, uint32, int, types.NodeID, types.VrfSignature, uint16) (bool, error)) *MockRolacleValidateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockRolacleValidateCall) DoAndReturn(f func(context.Context, types.LayerID, uint32, int, types.NodeID, types.VrfSignature, uint16) (bool, error)) *MockRolacleValidateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/hare4/eligibility/oracle.go b/hare4/eligibility/oracle.go new file mode 100644 index 0000000000..701d8da800 --- /dev/null +++ b/hare4/eligibility/oracle.go @@ -0,0 +1,569 @@ +package eligibility + +import ( + "context" + "errors" + "fmt" + "math" + "sync" + + lru "github.com/hashicorp/golang-lru/v2" + "github.com/spacemeshos/fixed" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "golang.org/x/exp/maps" + + "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/log" + "github.com/spacemeshos/go-spacemesh/miner" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/activesets" + "github.com/spacemeshos/go-spacemesh/sql/ballots" + "github.com/spacemeshos/go-spacemesh/system" +) + +const ( + // CertifyRound is not part of the hare protocol, but it shares the same oracle for eligibility. + CertifyRound uint32 = math.MaxUint32 >> 1 +) + +const ( + activesCacheSize = 5 // we don't expect to handle more than two layers concurrently + maxSupportedN = (math.MaxInt32 / 2) + 1 // higher values result in an overflow when calculating CDF +) + +var ( + errZeroCommitteeSize = errors.New("zero committee size") + errEmptyActiveSet = errors.New("empty active set") + errZeroTotalWeight = errors.New("zero total weight") + ErrNotActive = errors.New("oracle: miner is not active in epoch") +) + +type identityWeight struct { + atx types.ATXID + weight uint64 +} + +type cachedActiveSet struct { + set map[types.NodeID]identityWeight + total uint64 +} + +func (c *cachedActiveSet) atxs() []types.ATXID { + atxs := make([]types.ATXID, 0, len(c.set)) + for _, id := range c.set { + atxs = append(atxs, id.atx) + } + return atxs +} + +// Config is the configuration of the oracle package. +type Config struct { + // ConfidenceParam specifies how many layers into the epoch hare uses active set generated in the previous epoch. + // For example, if epoch size is 100 and confidence is 10 hare will use previous active set for layers 0-9 + // and then generate a new activeset. + // + // This was done like that so that we have higher `confidence` that hare will succeed atleast + // once during this interval. If it doesn't we have to provide centralized fallback. + ConfidenceParam uint32 `mapstructure:"eligibility-confidence-param"` +} + +func (c *Config) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddUint32("confidence param", c.ConfidenceParam) + return nil +} + +// DefaultConfig returns the default configuration for the oracle package. +func DefaultConfig() Config { + return Config{ConfidenceParam: 1} +} + +// Oracle is the hare eligibility oracle. +type Oracle struct { + mu sync.Mutex + activesCache activeSetCache + fallback map[types.EpochID][]types.ATXID + sync system.SyncStateProvider + // NOTE(dshulyak) on switch from synced to not synced reset the cache + // to cope with https://github.com/spacemeshos/go-spacemesh/issues/4552 + // until graded oracle is implemented + synced bool + + beacons system.BeaconGetter + atxsdata *atxsdata.Data + db sql.Executor + vrfVerifier vrfVerifier + layersPerEpoch uint32 + cfg Config + log *zap.Logger +} + +type Opt func(*Oracle) + +func WithConfig(config Config) Opt { + return func(o *Oracle) { + o.cfg = config + } +} + +func WithLogger(logger *zap.Logger) Opt { + return func(o *Oracle) { + o.log = logger + } +} + +// New returns a new eligibility oracle instance. +func New( + beacons system.BeaconGetter, + db sql.Executor, + atxsdata *atxsdata.Data, + vrfVerifier vrfVerifier, + layersPerEpoch uint32, + opts ...Opt, +) *Oracle { + activesCache, err := lru.New[types.EpochID, *cachedActiveSet](activesCacheSize) + if err != nil { + panic("failed to create lru cache for active set" + err.Error()) + } + oracle := &Oracle{ + beacons: beacons, + db: db, + atxsdata: atxsdata, + vrfVerifier: vrfVerifier, + layersPerEpoch: layersPerEpoch, + activesCache: activesCache, + fallback: map[types.EpochID][]types.ATXID{}, + cfg: DefaultConfig(), + log: zap.NewNop(), + } + for _, opt := range opts { + opt(oracle) + } + oracle.log.Info("hare oracle initialized", zap.Uint32("epoch size", layersPerEpoch), zap.Inline(&oracle.cfg)) + return oracle +} + +//go:generate scalegen -types VrfMessage + +// VrfMessage is a verification message. It is also the payload for the signature in `types.HareEligibility`. +type VrfMessage struct { + Type types.EligibilityType // always types.EligibilityHare + Beacon types.Beacon + Round uint32 + Layer types.LayerID +} + +func (o *Oracle) SetSync(sync system.SyncStateProvider) { + o.mu.Lock() + defer o.mu.Unlock() + o.sync = sync +} + +func (o *Oracle) resetCacheOnSynced(ctx context.Context) { + if o.sync == nil { + return + } + synced := o.synced + o.synced = o.sync.IsSynced(ctx) + if !synced && o.synced { + ac, err := lru.New[types.EpochID, *cachedActiveSet](activesCacheSize) + if err != nil { + o.log.Fatal("failed to create lru cache for active set", zap.Error(err)) + } + o.activesCache = ac + } +} + +// buildVRFMessage builds the VRF message used as input for hare eligibility validation. +func (o *Oracle) buildVRFMessage(ctx context.Context, layer types.LayerID, round uint32) ([]byte, error) { + beacon, err := o.beacons.GetBeacon(layer.GetEpoch()) + if err != nil { + return nil, fmt.Errorf("get beacon: %w", err) + } + return codec.MustEncode(&VrfMessage{Type: types.EligibilityHare, Beacon: beacon, Round: round, Layer: layer}), nil +} + +func (o *Oracle) totalWeight(ctx context.Context, layer types.LayerID) (uint64, error) { + actives, err := o.actives(ctx, layer) + if err != nil { + return 0, err + } + return actives.total, nil +} + +func (o *Oracle) minerWeight(ctx context.Context, layer types.LayerID, id types.NodeID) (uint64, error) { + actives, err := o.actives(ctx, layer) + if err != nil { + return 0, err + } + + w, ok := actives.set[id] + if !ok { + return 0, fmt.Errorf("%w: %v", ErrNotActive, id) + } + return w.weight, nil +} + +func calcVrfFrac(vrfSig types.VrfSignature) fixed.Fixed { + return fixed.FracFromBytes(vrfSig[:8]) +} + +func (o *Oracle) prepareEligibilityCheck( + ctx context.Context, + layer types.LayerID, + round uint32, + committeeSize int, + id types.NodeID, + vrfSig types.VrfSignature, +) (int, fixed.Fixed, fixed.Fixed, bool, error) { + logger := o.log.With( + log.ZContext(ctx), + zap.Uint32("layer", layer.Uint32()), + zap.Uint32("epoch", layer.GetEpoch().Uint32()), + log.ZShortStringer("smesherID", id), + zap.Uint32("round", round), + zap.Int("committee_size", committeeSize), + ) + + if committeeSize < 1 { + logger.Error("committee size must be positive", zap.Int("committee_size", committeeSize)) + return 0, fixed.Fixed{}, fixed.Fixed{}, true, errZeroCommitteeSize + } + + // calc hash & check threshold + // this is cheap in case the node is not eligible + minerWeight, err := o.minerWeight(ctx, layer, id) + if err != nil { + return 0, fixed.Fixed{}, fixed.Fixed{}, true, err + } + + msg, err := o.buildVRFMessage(ctx, layer, round) + if err != nil { + logger.Warn("could not build vrf message", zap.Error(err)) + return 0, fixed.Fixed{}, fixed.Fixed{}, true, err + } + + // validate message + if !o.vrfVerifier.Verify(id, msg, vrfSig) { + logger.Debug("eligibility: a node did not pass vrf signature verification") + return 0, fixed.Fixed{}, fixed.Fixed{}, true, nil + } + + // get active set size + totalWeight, err := o.totalWeight(ctx, layer) + if err != nil { + logger.Error("failed to get total weight", zap.Error(err)) + return 0, fixed.Fixed{}, fixed.Fixed{}, true, err + } + + // require totalWeight > 0 + if totalWeight == 0 { + logger.Warn("eligibility: total weight is zero") + return 0, fixed.Fixed{}, fixed.Fixed{}, true, errZeroTotalWeight + } + + logger.Debug("preparing eligibility check", + zap.Uint64("miner_weight", minerWeight), + zap.Uint64("total_weight", totalWeight), + ) + + n := minerWeight + + // calc p + if uint64(committeeSize) > totalWeight { + logger.Warn("committee size is greater than total weight", + zap.Int("committee_size", committeeSize), + zap.Uint64("total_weight", totalWeight), + ) + totalWeight *= uint64(committeeSize) + n *= uint64(committeeSize) + } + if n > maxSupportedN { + return 0, fixed.Fixed{}, fixed.Fixed{}, false, fmt.Errorf( + "miner weight exceeds supported maximum (id: %v, weight: %d, max: %d", + id, + minerWeight, + maxSupportedN, + ) + } + + p := fixed.DivUint64(uint64(committeeSize), totalWeight) + return int(n), p, calcVrfFrac(vrfSig), false, nil +} + +// Validate validates the number of eligibilities of ID on the given Layer where msg is the VRF message, sig is the role +// proof and assuming commSize as the expected committee size. +func (o *Oracle) Validate( + ctx context.Context, + layer types.LayerID, + round uint32, + committeeSize int, + id types.NodeID, + sig types.VrfSignature, + eligibilityCount uint16, +) (bool, error) { + n, p, vrfFrac, done, err := o.prepareEligibilityCheck(ctx, layer, round, committeeSize, id, sig) + if done || err != nil { + return false, err + } + + defer func() { + if msg := recover(); msg != nil { + o.log.Fatal("panic in validate", + log.ZContext(ctx), + zap.Any("msg", msg), + zap.Int("n", n), + zap.Stringer("p", p), + zap.Stringer("vrf_frac", vrfFrac), + ) + } + }() + + x := int(eligibilityCount) + if !fixed.BinCDF(n, p, x-1).GreaterThan(vrfFrac) && vrfFrac.LessThan(fixed.BinCDF(n, p, x)) { + return true, nil + } + o.log.Warn("eligibility: node did not pass vrf eligibility threshold", + log.ZContext(ctx), + zap.Uint32("layer", layer.Uint32()), + zap.Uint32("round", round), + zap.Int("committee_size", committeeSize), + log.ZShortStringer("smesherID", id), + zap.Uint16("eligibility_count", eligibilityCount), + zap.Int("n", n), + zap.Float64("p", p.Float()), + zap.Float64("vrf_frac", vrfFrac.Float()), + zap.Int("x", x), + ) + return false, nil +} + +// CalcEligibility calculates the number of eligibilities of ID on the given Layer where msg is the VRF message, sig is +// the role proof and assuming commSize as the expected committee size. +func (o *Oracle) CalcEligibility( + ctx context.Context, + layer types.LayerID, + round uint32, + committeeSize int, + id types.NodeID, + vrfSig types.VrfSignature, +) (uint16, error) { + n, p, vrfFrac, done, err := o.prepareEligibilityCheck(ctx, layer, round, committeeSize, id, vrfSig) + if done { + return 0, err + } + + o.log.Debug("params", + zap.Uint32("layer", layer.Uint32()), + zap.Uint32("epoch", layer.GetEpoch().Uint32()), + zap.Uint32("round_id", round), + zap.Int("committee_size", committeeSize), + zap.Int("n", n), + zap.Float64("p", p.Float()), + zap.Float64("vrf_frac", vrfFrac.Float()), + ) + + for x := 0; x < n; x++ { + if fixed.BinCDF(n, p, x).GreaterThan(vrfFrac) { + // even with large N and large P, x will be << 2^16, so this cast is safe + return uint16(x), nil + } + } + + // since BinCDF(n, p, n) is 1 for any p, this code can only be reached if n is much smaller + // than 2^16 (so that BinCDF(n, p, n-1) is still lower than vrfFrac) + return uint16(n), nil +} + +// Proof returns the role proof for the current Layer & Round. +func (o *Oracle) Proof( + ctx context.Context, + signer *signing.VRFSigner, + layer types.LayerID, + round uint32, +) (types.VrfSignature, error) { + beacon, err := o.beacons.GetBeacon(layer.GetEpoch()) + if err != nil { + return types.EmptyVrfSignature, fmt.Errorf("get beacon: %w", err) + } + return GenVRF(ctx, signer, beacon, layer, round), nil +} + +// GenVRF generates vrf for hare eligibility. +func GenVRF( + ctx context.Context, + signer *signing.VRFSigner, + beacon types.Beacon, + layer types.LayerID, + round uint32, +) types.VrfSignature { + return signer.Sign( + codec.MustEncode(&VrfMessage{Type: types.EligibilityHare, Beacon: beacon, Round: round, Layer: layer}), + ) +} + +// Returns a map of all active node IDs in the specified layer id. +func (o *Oracle) actives(ctx context.Context, targetLayer types.LayerID) (*cachedActiveSet, error) { + if !targetLayer.After(types.GetEffectiveGenesis()) { + return nil, errEmptyActiveSet + } + targetEpoch := targetLayer.GetEpoch() + // the first bootstrap data targets first epoch after genesis (epoch 2) + // and the epoch where checkpoint recovery happens + if targetEpoch > types.GetEffectiveGenesis().Add(1).GetEpoch() && + targetLayer.Difference(targetEpoch.FirstLayer()) < o.cfg.ConfidenceParam { + targetEpoch -= 1 + } + o.log.Debug("hare oracle getting active set", + log.ZContext(ctx), + zap.Uint32("target_layer", targetLayer.Uint32()), + zap.Uint32("target_layer_epoch", targetLayer.GetEpoch().Uint32()), + zap.Uint32("target_epoch", targetEpoch.Uint32()), + ) + + o.mu.Lock() + defer o.mu.Unlock() + o.resetCacheOnSynced(ctx) + if value, exists := o.activesCache.Get(targetEpoch); exists { + return value, nil + } + activeSet, err := o.computeActiveSet(ctx, targetEpoch) + if err != nil { + return nil, err + } + if len(activeSet) == 0 { + return nil, errEmptyActiveSet + } + activeWeights, err := o.computeActiveWeights(targetEpoch, activeSet) + if err != nil { + return nil, err + } + + aset := &cachedActiveSet{set: activeWeights} + for _, aweight := range activeWeights { + aset.total += aweight.weight + } + o.log.Info("got hare active set", log.ZContext(ctx), zap.Int("count", len(activeWeights))) + o.activesCache.Add(targetEpoch, aset) + return aset, nil +} + +func (o *Oracle) ActiveSet(ctx context.Context, targetEpoch types.EpochID) ([]types.ATXID, error) { + aset, err := o.actives(ctx, targetEpoch.FirstLayer().Add(o.cfg.ConfidenceParam)) + if err != nil { + return nil, err + } + return aset.atxs(), nil +} + +func (o *Oracle) computeActiveSet(ctx context.Context, targetEpoch types.EpochID) ([]types.ATXID, error) { + activeSet, ok := o.fallback[targetEpoch] + if ok { + o.log.Info("using fallback active set", + log.ZContext(ctx), + zap.Uint32("target_epoch", targetEpoch.Uint32()), + zap.Int("size", len(activeSet)), + ) + return activeSet, nil + } + + activeSet, err := miner.ActiveSetFromEpochFirstBlock(o.db, targetEpoch) + if err != nil && !errors.Is(err, sql.ErrNotFound) { + return nil, err + } + if len(activeSet) == 0 { + return o.activeSetFromRefBallots(targetEpoch) + } + return activeSet, nil +} + +func (o *Oracle) computeActiveWeights( + targetEpoch types.EpochID, + activeSet []types.ATXID, +) (map[types.NodeID]identityWeight, error) { + identities := make(map[types.NodeID]identityWeight, len(activeSet)) + for _, id := range activeSet { + atx := o.atxsdata.Get(targetEpoch, id) + if atx == nil { + return nil, fmt.Errorf("oracle: missing atx in atxsdata %s/%s", targetEpoch, id.ShortString()) + } + identities[atx.Node] = identityWeight{atx: id, weight: atx.Weight} + } + return identities, nil +} + +func (o *Oracle) activeSetFromRefBallots(epoch types.EpochID) ([]types.ATXID, error) { + beacon, err := o.beacons.GetBeacon(epoch) + if err != nil { + return nil, fmt.Errorf("get beacon: %w", err) + } + ballotsrst, err := ballots.AllFirstInEpoch(o.db, epoch) + if err != nil { + return nil, fmt.Errorf("first in epoch %d: %w", epoch, err) + } + activeMap := make(map[types.ATXID]struct{}, len(ballotsrst)) + for _, ballot := range ballotsrst { + if ballot.EpochData == nil { + o.log.Error("invalid data. first ballot doesn't have epoch data", zap.Inline(ballot)) + continue + } + if ballot.EpochData.Beacon != beacon { + o.log.Debug("beacon mismatch", zap.Stringer("local", beacon), zap.Object("ballot", ballot)) + continue + } + actives, err := activesets.Get(o.db, ballot.EpochData.ActiveSetHash) + if err != nil { + o.log.Error("failed to get active set", + log.ZShortStringer("actives hash", ballot.EpochData.ActiveSetHash), + zap.Stringer("ballot ", ballot.ID()), + zap.Error(err), + ) + continue + } + for _, id := range actives.Set { + activeMap[id] = struct{}{} + } + } + o.log.Warn("using tortoise active set", + zap.Int("actives size", len(activeMap)), + zap.Uint32("epoch", epoch.Uint32()), + zap.Stringer("beacon", beacon), + ) + return maps.Keys(activeMap), nil +} + +// IsIdentityActiveOnConsensusView returns true if the provided identity is active on the consensus view derived +// from the specified layer, false otherwise. +func (o *Oracle) IsIdentityActiveOnConsensusView( + ctx context.Context, + edID types.NodeID, + layer types.LayerID, +) (bool, error) { + o.log.Debug("hare oracle checking for active identity", log.ZContext(ctx)) + defer func() { + o.log.Debug("hare oracle active identity check complete", log.ZContext(ctx)) + }() + actives, err := o.actives(ctx, layer) + if err != nil { + return false, err + } + _, exist := actives.set[edID] + return exist, nil +} + +func (o *Oracle) UpdateActiveSet(epoch types.EpochID, activeSet []types.ATXID) { + o.log.Info("received activeset update", + zap.Uint32("epoch", epoch.Uint32()), + zap.Int("size", len(activeSet)), + ) + o.mu.Lock() + defer o.mu.Unlock() + if _, ok := o.fallback[epoch]; ok { + o.log.Debug("fallback active set already exists", zap.Uint32("epoch", epoch.Uint32())) + return + } + o.fallback[epoch] = activeSet +} diff --git a/hare4/eligibility/oracle_scale.go b/hare4/eligibility/oracle_scale.go new file mode 100644 index 0000000000..03f43bbfef --- /dev/null +++ b/hare4/eligibility/oracle_scale.go @@ -0,0 +1,76 @@ +// Code generated by github.com/spacemeshos/go-scale/scalegen. DO NOT EDIT. + +// nolint +package eligibility + +import ( + "github.com/spacemeshos/go-scale" + "github.com/spacemeshos/go-spacemesh/common/types" +) + +func (t *VrfMessage) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeCompact16(enc, uint16(t.Type)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.Beacon[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeCompact32(enc, uint32(t.Round)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeCompact32(enc, uint32(t.Layer)) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *VrfMessage) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + field, n, err := scale.DecodeCompact16(dec) + if err != nil { + return total, err + } + total += n + t.Type = types.EligibilityType(field) + } + { + n, err := scale.DecodeByteArray(dec, t.Beacon[:]) + if err != nil { + return total, err + } + total += n + } + { + field, n, err := scale.DecodeCompact32(dec) + if err != nil { + return total, err + } + total += n + t.Round = uint32(field) + } + { + field, n, err := scale.DecodeCompact32(dec) + if err != nil { + return total, err + } + total += n + t.Layer = types.LayerID(field) + } + return total, nil +} diff --git a/hare4/eligibility/oracle_test.go b/hare4/eligibility/oracle_test.go new file mode 100644 index 0000000000..fac51a5b6b --- /dev/null +++ b/hare4/eligibility/oracle_test.go @@ -0,0 +1,966 @@ +package eligibility + +import ( + "context" + "encoding/hex" + "errors" + "math/rand" + "os" + "strconv" + "sync" + "testing" + "time" + + "github.com/spacemeshos/fixed" + "github.com/spacemeshos/go-scale/tester" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" + "golang.org/x/exp/maps" + + "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/activesets" + "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/sql/ballots" + "github.com/spacemeshos/go-spacemesh/sql/blocks" + "github.com/spacemeshos/go-spacemesh/sql/layers" + "github.com/spacemeshos/go-spacemesh/sql/statesql" + "github.com/spacemeshos/go-spacemesh/system/mocks" +) + +const ( + defLayersPerEpoch uint32 = 10 + confidenceParam uint32 = 3 + ballotsPerLayer = 50 +) + +func TestMain(m *testing.M) { + types.SetLayersPerEpoch(defLayersPerEpoch) + res := m.Run() + os.Exit(res) +} + +type testOracle struct { + *Oracle + tb testing.TB + db sql.StateDatabase + atxsdata *atxsdata.Data + mBeacon *mocks.MockBeaconGetter + mVerifier *MockvrfVerifier +} + +func defaultOracle(tb testing.TB) *testOracle { + db := statesql.InMemoryTest(tb) + atxsdata := atxsdata.New() + + ctrl := gomock.NewController(tb) + mBeacon := mocks.NewMockBeaconGetter(ctrl) + mVerifier := NewMockvrfVerifier(ctrl) + + to := &testOracle{ + Oracle: New( + mBeacon, + db, + atxsdata, + mVerifier, + defLayersPerEpoch, + WithConfig(Config{ConfidenceParam: confidenceParam}), + WithLogger(zaptest.NewLogger(tb)), + ), + tb: tb, + mBeacon: mBeacon, + mVerifier: mVerifier, + db: db, + atxsdata: atxsdata, + } + return to +} + +func (t *testOracle) createBallots( + lid types.LayerID, + activeSet types.ATXIDList, + miners []types.NodeID, +) []*types.Ballot { + t.tb.Helper() + numBallots := ballotsPerLayer + if len(activeSet) < numBallots { + numBallots = len(activeSet) + } + var result []*types.Ballot + for i := 0; i < numBallots; i++ { + b := types.RandomBallot() + b.Layer = lid + b.AtxID = activeSet[i] + b.RefBallot = types.EmptyBallotID + b.EpochData = &types.EpochData{ActiveSetHash: activeSet.Hash()} + b.Signature = types.RandomEdSignature() + b.SmesherID = miners[i] + require.NoError(t.tb, b.Initialize()) + require.NoError(t.tb, ballots.Add(t.db, b)) + activesets.Add(t.db, b.EpochData.ActiveSetHash, &types.EpochActiveSet{ + Epoch: lid.GetEpoch(), + Set: activeSet, + }) + result = append(result, b) + } + return result +} + +func (t *testOracle) createBlock(blts []*types.Ballot) { + t.tb.Helper() + block := &types.Block{ + InnerBlock: types.InnerBlock{ + LayerIndex: blts[0].Layer, + }, + } + for _, b := range blts { + block.Rewards = append(block.Rewards, types.AnyReward{AtxID: b.AtxID}) + } + block.Initialize() + require.NoError(t.tb, blocks.Add(t.db, block)) + require.NoError(t.tb, layers.SetApplied(t.db, block.LayerIndex, block.ID())) +} + +func (t *testOracle) createLayerData(lid types.LayerID, numMiners int) []types.NodeID { + t.tb.Helper() + activeSet := types.RandomActiveSet(numMiners) + miners := t.createActiveSet(lid.GetEpoch().FirstLayer().Sub(1), activeSet) + blts := t.createBallots(lid, activeSet, miners) + t.createBlock(blts) + return miners +} + +func (t *testOracle) createActiveSet( + lid types.LayerID, + activeSet []types.ATXID, +) []types.NodeID { + var miners []types.NodeID + for i, id := range activeSet { + nodeID := types.BytesToNodeID([]byte(strconv.Itoa(i))) + miners = append(miners, nodeID) + atx := &types.ActivationTx{ + PublishEpoch: lid.GetEpoch(), + Weight: uint64(i + 1), + SmesherID: nodeID, + } + atx.SetID(id) + atx.SetReceived(time.Now()) + t.addAtx(atx) + } + return miners +} + +func (t *testOracle) addAtx(atx *types.ActivationTx) { + t.tb.Helper() + require.NoError(t.tb, atxs.Add(t.db, atx, types.AtxBlob{})) + t.atxsdata.AddFromAtx(atx, false) +} + +// create n identities with weights and identifiers 1,2,3,...,n. +func createIdentities(n int) map[types.NodeID]identityWeight { + m := map[types.NodeID]identityWeight{} + for i := 0; i < n; i++ { + m[types.BytesToNodeID([]byte(strconv.Itoa(i)))] = identityWeight{ + atx: types.ATXID(types.BytesToHash([]byte(strconv.Itoa(i)))), + weight: uint64(i + 1), + } + } + return m +} + +func TestCalcEligibility(t *testing.T) { + nid := types.NodeID{1, 1} + + t.Run("zero committee", func(t *testing.T) { + o := defaultOracle(t) + res, err := o.CalcEligibility(context.Background(), types.LayerID(50), 1, 0, nid, types.EmptyVrfSignature) + require.ErrorIs(t, err, errZeroCommitteeSize) + require.Equal(t, 0, int(res)) + }) + + t.Run("empty active set", func(t *testing.T) { + o := defaultOracle(t) + o.mBeacon.EXPECT().GetBeacon(gomock.Any()) + lid := types.EpochID(5).FirstLayer() + res, err := o.CalcEligibility(context.Background(), lid, 1, 1, nid, types.EmptyVrfSignature) + require.ErrorIs(t, err, errEmptyActiveSet) + require.Equal(t, 0, int(res)) + }) + + t.Run("miner not active", func(t *testing.T) { + o := defaultOracle(t) + lid := types.EpochID(5).FirstLayer() + o.createLayerData(lid.Sub(defLayersPerEpoch), 11) + res, err := o.CalcEligibility(context.Background(), lid, 1, 1, nid, types.EmptyVrfSignature) + require.ErrorIs(t, err, ErrNotActive) + require.Equal(t, 0, int(res)) + }) + + t.Run("beacon failure", func(t *testing.T) { + o := defaultOracle(t) + layer := types.EpochID(5).FirstLayer() + miners := o.createLayerData(layer.Sub(defLayersPerEpoch), 5) + errUnknown := errors.New("unknown") + o.mBeacon.EXPECT().GetBeacon(layer.GetEpoch()).Return(types.EmptyBeacon, errUnknown).Times(1) + + res, err := o.CalcEligibility(context.Background(), layer, 0, 1, miners[0], types.EmptyVrfSignature) + require.ErrorIs(t, err, errUnknown) + require.Equal(t, 0, int(res)) + }) + + t.Run("verify failure", func(t *testing.T) { + o := defaultOracle(t) + layer := types.EpochID(5).FirstLayer() + miners := o.createLayerData(layer.Sub(defLayersPerEpoch), 5) + o.mBeacon.EXPECT().GetBeacon(layer.GetEpoch()).Return(types.RandomBeacon(), nil).Times(1) + o.mVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any()).Return(false).Times(1) + + res, err := o.CalcEligibility(context.Background(), layer, 0, 1, miners[0], types.EmptyVrfSignature) + require.NoError(t, err) + require.Equal(t, 0, int(res)) + }) + + t.Run("empty active with fallback", func(t *testing.T) { + o := defaultOracle(t) + o.mBeacon.EXPECT().GetBeacon(gomock.Any()) + lid := types.EpochID(5).FirstLayer().Add(o.cfg.ConfidenceParam) + res, err := o.CalcEligibility(context.Background(), lid, 1, 1, nid, types.EmptyVrfSignature) + require.ErrorIs(t, err, errEmptyActiveSet) + require.Equal(t, 0, int(res)) + + activeSet := types.RandomActiveSet(111) + miners := o.createActiveSet(types.EpochID(4).FirstLayer(), activeSet) + o.UpdateActiveSet(5, activeSet) + o.mBeacon.EXPECT().GetBeacon(lid.GetEpoch()).Return(types.RandomBeacon(), nil) + o.mVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any()).Return(true) + _, err = o.CalcEligibility(context.Background(), lid, 1, 1, miners[0], types.EmptyVrfSignature) + require.NoError(t, err) + }) + + t.Run("miner active", func(t *testing.T) { + o := defaultOracle(t) + lid := types.EpochID(5).FirstLayer() + beacon := types.Beacon{1, 0, 0, 0} + miners := o.createLayerData(lid.Sub(defLayersPerEpoch), 5) + sigs := map[string]uint16{ + "0516a574aef37257d6811ea53ef55d4cbb0e14674900a0d5165bd6742513840d" + + "02442d979fdabc7059645d1e8f8a0f44d0db2aa90f23374dd74a3636d4ecdab7": 1, + "73929b4b69090bb6133e2f8cd73989b35228e7e6d8c6745e4100d9c5eb48ca26" + + "24ee2889e55124195a130f74ea56e53a73a1c4dee60baa13ad3b1c0ed4f80d9c": 0, + "e2c27ad65b752b763173b588518764b6c1e42896d57e0eabef9bcac68e07b877" + + "29a4ef9e5f17d8c1cb34ffd0d65ee9a7e63e63b77a7bcab1140a76fc04c271de": 0, + "384460966938c87644987fe00c0f9d4f9a5e2dcd4bdc08392ed94203895ba325" + + "036725a22346e35aa707993babef716aa1b6b3dfc653a44cb23ac8f743cbbc3d": 1, + "15c5f565a75888970059b070bfaed1998a9d423ddac9f6af83da51db02149044" + + "ea6aeb86294341c7a950ac5de2855bbebc11cc28b02c08bc903e4cf41439717d": 1, + } + for vrf, exp := range sigs { + sig, err := hex.DecodeString(vrf) + require.NoError(t, err) + + var vrfSig types.VrfSignature + copy(vrfSig[:], sig) + + o.mBeacon.EXPECT().GetBeacon(lid.GetEpoch()).Return(beacon, nil).Times(1) + o.mVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any()).Return(true).Times(1) + res, err := o.CalcEligibility(context.Background(), lid, 1, 10, miners[0], vrfSig) + require.NoError(t, err, vrf) + require.Equal(t, exp, res, vrf) + } + }) +} + +func TestCalcEligibilityWithSpaceUnit(t *testing.T) { + const committeeSize = 800 + tcs := []struct { + desc string + numMiners int + }{ + { + desc: "small network", + numMiners: 50, + }, + { + desc: "large network", + numMiners: 2000, + }, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + o := defaultOracle(t) + o.mVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any()).Return(true).AnyTimes() + + lid := types.EpochID(5).FirstLayer() + beacon := types.Beacon{1, 0, 0, 0} + miners := o.createLayerData(lid.Sub(defLayersPerEpoch), tc.numMiners) + + var eligibilityCount uint16 + for _, nodeID := range miners { + sig := types.RandomVrfSignature() + + o.mBeacon.EXPECT().GetBeacon(lid.GetEpoch()).Return(beacon, nil).Times(2) + res, err := o.CalcEligibility(context.Background(), lid, 1, committeeSize, nodeID, sig) + require.NoError(t, err) + + valid, err := o.Validate(context.Background(), lid, 1, committeeSize, nodeID, sig, res) + require.NoError(t, err) + require.True(t, valid) + + eligibilityCount += res + } + + require.InDelta(t, committeeSize, eligibilityCount, committeeSize*15/100) // up to 15% difference + // a correct check would be to calculate the expected variance of the binomial distribution + // which depends on the number of miners and the number of units each miner has + // and then assert that the difference is within 3 standard deviations of the expected value + }) + } +} + +func BenchmarkOracle_CalcEligibility(b *testing.B) { + r := require.New(b) + + o := defaultOracle(b) + o.mBeacon.EXPECT().GetBeacon(gomock.Any()).Return(types.RandomBeacon(), nil).AnyTimes() + o.mVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any()).Return(true).AnyTimes() + numOfMiners := 2000 + committeeSize := 800 + + lid := types.EpochID(5).FirstLayer() + o.createLayerData(lid, numOfMiners) + + var nodeIDs []types.NodeID + for pubkey := range createIdentities(b.N) { + nodeIDs = append(nodeIDs, pubkey) + } + b.ResetTimer() + for _, nodeID := range nodeIDs { + res, err := o.CalcEligibility(context.Background(), lid, 1, committeeSize, nodeID, types.EmptyVrfSignature) + + if err == nil { + valid, err := o.Validate(context.Background(), lid, 1, committeeSize, nodeID, types.EmptyVrfSignature, res) + r.NoError(err) + r.True(valid) + } + } +} + +func Test_VrfSignVerify(t *testing.T) { + // eligibility of the proof depends on the identity + rng := rand.New(rand.NewSource(5)) + + signer, err := signing.NewEdSigner(signing.WithKeyFromRand(rng)) + require.NoError(t, err) + + o := defaultOracle(t) + nid := signer.NodeID() + + lid := types.EpochID(5).FirstLayer().Add(confidenceParam) + first := types.EpochID(5).FirstLayer() + prevEpoch := lid.GetEpoch() - 1 + o.mBeacon.EXPECT().GetBeacon(lid.GetEpoch()).Return(types.Beacon{1, 0, 0, 0}, nil).AnyTimes() + + numMiners := 2 + activeSet := types.RandomActiveSet(numMiners) + atx1 := &types.ActivationTx{ + PublishEpoch: prevEpoch, + Weight: 1 * 1024, + SmesherID: signer.NodeID(), + } + atx1.SetID(activeSet[0]) + atx1.SetReceived(time.Now()) + o.addAtx(atx1) + + signer2, err := signing.NewEdSigner(signing.WithKeyFromRand(rng)) + require.NoError(t, err) + + atx2 := &types.ActivationTx{ + PublishEpoch: prevEpoch, + Weight: 9 * 1024, + SmesherID: signer2.NodeID(), + } + atx2.SetID(activeSet[1]) + atx2.SetReceived(time.Now()) + o.addAtx(atx2) + miners := []types.NodeID{atx1.SmesherID, atx2.SmesherID} + o.createBlock(o.createBallots(first, activeSet, miners)) + + o.vrfVerifier = signing.NewVRFVerifier() + + // round is handpicked for vrf signature to pass + const round = 0 + + proof, err := o.Proof(context.Background(), signer.VRFSigner(), lid, round) + require.NoError(t, err) + + res, err := o.CalcEligibility(context.Background(), lid, round, 10, nid, proof) + require.NoError(t, err) + require.Equal(t, 1, int(res)) + + valid, err := o.Validate(context.Background(), lid, round, 10, nid, proof, 1) + require.NoError(t, err) + require.True(t, valid) +} + +func Test_Proof_BeaconError(t *testing.T) { + o := defaultOracle(t) + + signer, err := signing.NewEdSigner() + require.NoError(t, err) + + layer := types.LayerID(2) + errUnknown := errors.New("unknown") + o.mBeacon.EXPECT().GetBeacon(layer.GetEpoch()).Return(types.EmptyBeacon, errUnknown).Times(1) + + _, err = o.Proof(context.Background(), signer.VRFSigner(), layer, 3) + require.ErrorIs(t, err, errUnknown) +} + +func Test_Proof(t *testing.T) { + o := defaultOracle(t) + layer := types.LayerID(2) + o.mBeacon.EXPECT().GetBeacon(layer.GetEpoch()).Return(types.Beacon{1, 0, 0, 0}, nil) + + signer, err := signing.NewEdSigner() + require.NoError(t, err) + + sig, err := o.Proof(context.Background(), signer.VRFSigner(), layer, 3) + require.NoError(t, err) + require.NotNil(t, sig) +} + +func TestOracle_IsIdentityActive(t *testing.T) { + o := defaultOracle(t) + layer := types.LayerID(defLayersPerEpoch * 4) + numMiners := 2 + miners := o.createLayerData(layer.Sub(defLayersPerEpoch), numMiners) + for _, nodeID := range miners { + v, err := o.IsIdentityActiveOnConsensusView(context.Background(), nodeID, layer) + require.NoError(t, err) + require.True(t, v) + } + v, err := o.IsIdentityActiveOnConsensusView(context.Background(), types.NodeID{7, 7, 7}, layer) + require.NoError(t, err) + require.False(t, v) +} + +func TestBuildVRFMessage_BeaconError(t *testing.T) { + o := defaultOracle(t) + errUnknown := errors.New("unknown") + o.mBeacon.EXPECT().GetBeacon(gomock.Any()).Return(types.EmptyBeacon, errUnknown).Times(1) + msg, err := o.buildVRFMessage(context.Background(), types.LayerID(1), 1) + require.ErrorIs(t, err, errUnknown) + require.Nil(t, msg) +} + +func TestBuildVRFMessage(t *testing.T) { + o := defaultOracle(t) + firstLayer := types.LayerID(1) + secondLayer := firstLayer.Add(1) + beacon := types.RandomBeacon() + o.mBeacon.EXPECT().GetBeacon(firstLayer.GetEpoch()).Return(beacon, nil).Times(1) + m1, err := o.buildVRFMessage(context.Background(), firstLayer, 2) + require.NoError(t, err) + + // check not same for different round + o.mBeacon.EXPECT().GetBeacon(firstLayer.GetEpoch()).Return(beacon, nil).Times(1) + m3, err := o.buildVRFMessage(context.Background(), firstLayer, 3) + require.NoError(t, err) + require.NotEqual(t, m1, m3) + + // check not same for different layer + o.mBeacon.EXPECT().GetBeacon(firstLayer.GetEpoch()).Return(beacon, nil).Times(1) + m4, err := o.buildVRFMessage(context.Background(), secondLayer, 2) + require.NoError(t, err) + require.NotEqual(t, m1, m4) + + // check same call returns same result + o.mBeacon.EXPECT().GetBeacon(firstLayer.GetEpoch()).Return(beacon, nil).Times(1) + m5, err := o.buildVRFMessage(context.Background(), firstLayer, 2) + require.NoError(t, err) + require.Equal(t, m1, m5) // check same result +} + +func TestBuildVRFMessage_Concurrency(t *testing.T) { + o := defaultOracle(t) + + total := 1000 + expectAdd := 10 + wg := sync.WaitGroup{} + firstLayer := types.LayerID(1) + o.mBeacon.EXPECT().GetBeacon(firstLayer.GetEpoch()).Return(types.RandomBeacon(), nil).AnyTimes() + for i := 0; i < total; i++ { + wg.Add(1) + go func(x int) { + _, err := o.buildVRFMessage(context.Background(), firstLayer, uint32(x%expectAdd)) + assert.NoError(t, err) + wg.Done() + }(i) + } + + wg.Wait() +} + +func TestActiveSet(t *testing.T) { + numMiners := 5 + o := defaultOracle(t) + targetEpoch := types.EpochID(5) + layer := targetEpoch.FirstLayer().Add(o.cfg.ConfidenceParam) + o.createLayerData(targetEpoch.FirstLayer(), numMiners) + + aset, err := o.actives(context.Background(), layer) + require.NoError(t, err) + require.ElementsMatch( + t, + maps.Keys(createIdentities(numMiners)), + maps.Keys(aset.set), + "assertion relies on the enumeration of identities", + ) + + got, err := o.ActiveSet(context.Background(), targetEpoch) + require.NoError(t, err) + require.Len(t, got, len(aset.set)) + for _, id := range got { + atx, err := atxs.Get(o.db, id) + require.NoError(t, err) + require.Contains(t, aset.set, atx.SmesherID, "id %s atx %s", id.ShortString(), atx.ShortString()) + delete(aset.set, atx.SmesherID) + } +} + +func TestActives(t *testing.T) { + numMiners := 5 + t.Run("genesis bootstrap", func(t *testing.T) { + o := defaultOracle(t) + first := types.GetEffectiveGenesis().Add(1) + bootstrap := types.RandomActiveSet(numMiners) + o.createActiveSet(types.EpochID(1).FirstLayer(), bootstrap) + o.UpdateActiveSet(types.GetEffectiveGenesis().GetEpoch()+1, bootstrap) + + for lid := types.LayerID(0); lid.Before(first); lid = lid.Add(1) { + activeSet, err := o.actives(context.Background(), lid) + require.ErrorIs(t, err, errEmptyActiveSet) + require.Nil(t, activeSet) + } + activeSet, err := o.actives(context.Background(), first) + require.NoError(t, err) + require.ElementsMatch( + t, + maps.Keys(createIdentities(numMiners)), + maps.Keys(activeSet.set), + "assertion relies on the enumeration of identities", + ) + }) + t.Run("steady state", func(t *testing.T) { + numMiners++ + o := defaultOracle(t) + o.mBeacon.EXPECT().GetBeacon(gomock.Any()) + layer := types.EpochID(4).FirstLayer() + o.createLayerData(layer, numMiners) + + start := layer.Add(o.cfg.ConfidenceParam) + activeSet, err := o.actives(context.Background(), start) + require.NoError(t, err) + require.ElementsMatch( + t, + maps.Keys(createIdentities(numMiners)), + maps.Keys(activeSet.set), + "assertion relies on the enumeration of identities", + ) + end := (layer.GetEpoch() + 1).FirstLayer().Add(o.cfg.ConfidenceParam) + + for lid := start.Add(1); lid.Before(end); lid = lid.Add(1) { + got, err := o.actives(context.Background(), lid) + require.NoError(t, err) + // cached + require.Equal(t, &activeSet, &got) + } + got, err := o.actives(context.Background(), end) + require.ErrorIs(t, err, errEmptyActiveSet) + require.Nil(t, got) + }) + t.Run("use fallback despite block", func(t *testing.T) { + numMiners++ + o := defaultOracle(t) + o.mBeacon.EXPECT().GetBeacon(gomock.Any()).AnyTimes() + layer := types.EpochID(4).FirstLayer() + end := layer.Add(o.cfg.ConfidenceParam) + o.createLayerData(layer, numMiners) + fallback := types.RandomActiveSet(numMiners + 1) + o.createActiveSet(types.EpochID(3).FirstLayer(), fallback) + o.UpdateActiveSet(end.GetEpoch(), fallback) + + for lid := layer; lid.Before(end); lid = lid.Add(1) { + got, err := o.actives(context.Background(), lid) + require.ErrorIs(t, err, errEmptyActiveSet) + require.Nil(t, got) + } + activeSet, err := o.actives(context.Background(), end) + require.NoError(t, err) + require.ElementsMatch( + t, + maps.Keys(createIdentities(numMiners+1)), + maps.Keys(activeSet.set), + "assertion relies on the enumeration of identities", + ) + }) + t.Run("recover at epoch start", func(t *testing.T) { + numMiners++ + o := defaultOracle(t) + o.mBeacon.EXPECT().GetBeacon(gomock.Any()).AnyTimes() + layer := types.EpochID(4).FirstLayer() + old := types.GetEffectiveGenesis() + types.SetEffectiveGenesis(layer.Uint32() - 1) + t.Cleanup(func() { + types.SetEffectiveGenesis(old.Uint32()) + }) + o.createLayerData(layer, numMiners) + fallback := types.RandomActiveSet(numMiners + 1) + o.createActiveSet(types.EpochID(3).FirstLayer(), fallback) + o.UpdateActiveSet(layer.GetEpoch(), fallback) + + activeSet, err := o.actives(context.Background(), layer) + require.NoError(t, err) + require.ElementsMatch( + t, + maps.Keys(createIdentities(numMiners+1)), + maps.Keys(activeSet.set), + "assertion relies on the enumeration of identities", + ) + activeSet2, err := o.actives(context.Background(), layer+1) + require.NoError(t, err) + require.Equal(t, activeSet, activeSet2) + }) +} + +func TestActives_ConcurrentCalls(t *testing.T) { + r := require.New(t) + o := defaultOracle(t) + layer := types.LayerID(100) + o.createLayerData(layer.Sub(defLayersPerEpoch), 5) + + mc := NewMockactiveSetCache(gomock.NewController(t)) + firstCall := true + mc.EXPECT().Get(layer.GetEpoch() - 1).DoAndReturn( + func(types.EpochID) (*cachedActiveSet, bool) { + if firstCall { + firstCall = false + return nil, false + } + aset := cachedActiveSet{set: createIdentities(5)} + for _, value := range aset.set { + aset.total += value.weight + } + return &aset, true + }).Times(102) + mc.EXPECT().Add(layer.GetEpoch()-1, gomock.Any()) + o.activesCache = mc + + var wg sync.WaitGroup + wg.Add(102) + runFn := func() { + _, err := o.actives(context.Background(), layer) + r.NoError(err) + wg.Done() + } + + // outstanding probability for concurrent access to calc active set size + for i := 0; i < 100; i++ { + go runFn() + } + + // make sure we wait at least two calls duration + runFn() + runFn() + wg.Wait() +} + +func TestMaxSupportedN(t *testing.T) { + n := maxSupportedN + p := fixed.DivUint64(800, uint64(n*100)) + x := 0 + + require.Panics(t, func() { + fixed.BinCDF(n+1, p, x) + }) + + require.NotPanics(t, func() { + for x = 0; x < 800; x++ { + fixed.BinCDF(n, p, x) + } + }) +} + +func TestActiveSetMatrix(t *testing.T) { + t.Parallel() + + target := types.EpochID(4) + bgen := func( + id types.BallotID, + lid types.LayerID, + node types.NodeID, + beacon types.Beacon, + atxs types.ATXIDList, + option ...func(*types.Ballot), + ) types.Ballot { + ballot := types.Ballot{} + ballot.Layer = lid + ballot.EpochData = &types.EpochData{Beacon: beacon, ActiveSetHash: atxs.Hash()} + ballot.SmesherID = node + ballot.SetID(id) + for _, opt := range option { + opt(&ballot) + } + return ballot + } + agen := func( + id types.ATXID, + node types.NodeID, + option ...func(*types.ActivationTx), + ) *types.ActivationTx { + atx := &types.ActivationTx{ + PublishEpoch: target - 1, + SmesherID: node, + NumUnits: 1, + TickCount: 1, + } + atx.SetID(id) + atx.SetReceived(time.Time{}.Add(1)) + + for _, opt := range option { + opt(atx) + } + return atx + } + + for _, tc := range []struct { + desc string + beacon types.Beacon // local beacon + ballots []types.Ballot + atxs []*types.ActivationTx + actives []types.ATXIDList + expect any + }{ + { + desc: "merged activesets", + beacon: types.Beacon{1}, + ballots: []types.Ballot{ + bgen( + types.BallotID{1}, + target.FirstLayer(), + types.NodeID{1}, + types.Beacon{1}, + []types.ATXID{{1}, {2}}, + ), + bgen( + types.BallotID{2}, + target.FirstLayer(), + types.NodeID{2}, + types.Beacon{1}, + []types.ATXID{{2}, {3}}, + ), + }, + atxs: []*types.ActivationTx{ + agen(types.ATXID{1}, types.NodeID{1}), + agen(types.ATXID{2}, types.NodeID{2}), + agen(types.ATXID{3}, types.NodeID{3}), + }, + actives: []types.ATXIDList{{{1}, {2}}, {{2}, {3}}}, + expect: []types.ATXID{{1}, {2}, {3}}, + }, + { + desc: "filter by beacon", + beacon: types.Beacon{1}, + ballots: []types.Ballot{ + bgen( + types.BallotID{1}, + target.FirstLayer(), + types.NodeID{1}, + types.Beacon{1}, + []types.ATXID{{1}, {2}}, + ), + bgen( + types.BallotID{2}, + target.FirstLayer(), + types.NodeID{2}, + types.Beacon{2, 2, 2, 2}, + []types.ATXID{{2}, {3}}, + ), + }, + atxs: []*types.ActivationTx{ + agen(types.ATXID{1}, types.NodeID{1}), + agen(types.ATXID{2}, types.NodeID{2}), + }, + actives: []types.ATXIDList{{{1}, {2}}, {{2}, {3}}}, + expect: []types.ATXID{{1}, {2}}, + }, + { + desc: "no local beacon", + beacon: types.EmptyBeacon, + ballots: []types.Ballot{ + bgen( + types.BallotID{1}, + target.FirstLayer(), + types.NodeID{1}, + types.Beacon{1}, + []types.ATXID{{1}, {2}}, + ), + bgen( + types.BallotID{2}, + target.FirstLayer(), + types.NodeID{2}, + types.Beacon{2, 2, 2, 2}, + []types.ATXID{{2}, {3}}, + ), + }, + atxs: []*types.ActivationTx{}, + actives: []types.ATXIDList{{{1}, {2}}, {{2}, {3}}}, + expect: "not found", + }, + { + desc: "unknown atxs", + beacon: types.Beacon{1}, + ballots: []types.Ballot{ + bgen( + types.BallotID{1}, + target.FirstLayer(), + types.NodeID{1}, + types.Beacon{1}, + []types.ATXID{{1}, {2}}, + ), + bgen( + types.BallotID{2}, + target.FirstLayer(), + types.NodeID{2}, + types.Beacon{2, 2, 2, 2}, + []types.ATXID{{2}, {3}}, + ), + }, + atxs: []*types.ActivationTx{}, + actives: []types.ATXIDList{{{1}, {2}}, {{2}, {3}}}, + expect: "missing atx in atxsdata", + }, + { + desc: "ballot no epoch data", + beacon: types.Beacon{1}, + ballots: []types.Ballot{ + bgen( + types.BallotID{1}, + target.FirstLayer(), + types.NodeID{1}, + types.Beacon{1}, + []types.ATXID{{1}, {2}}, + func(ballot *types.Ballot) { + ballot.EpochData = nil + }, + ), + bgen( + types.BallotID{2}, + target.FirstLayer(), + types.NodeID{2}, + types.Beacon{1}, + []types.ATXID{{2}, {3}}, + ), + }, + atxs: []*types.ActivationTx{ + agen(types.ATXID{2}, types.NodeID{2}), + agen(types.ATXID{3}, types.NodeID{3}), + }, + actives: []types.ATXIDList{{{2}, {3}}}, + expect: []types.ATXID{{2}, {3}}, + }, + { + desc: "wrong target epoch", + beacon: types.Beacon{1}, + ballots: []types.Ballot{ + bgen( + types.BallotID{1}, + target.FirstLayer(), + types.NodeID{1}, + types.Beacon{1}, + []types.ATXID{{2}}, + ), + }, + atxs: []*types.ActivationTx{ + agen(types.ATXID{2}, types.NodeID{1}, func(verified *types.ActivationTx) { + verified.PublishEpoch = target + }), + }, + actives: []types.ATXIDList{{{2}}}, + expect: "missing atx in atxsdata 4/0200000000", + }, + } { + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + oracle := defaultOracle(t) + for _, actives := range tc.actives { + require.NoError(t, activesets.Add(oracle.db, actives.Hash(), &types.EpochActiveSet{Set: actives})) + } + for _, ballot := range tc.ballots { + require.NoError(t, ballots.Add(oracle.db, &ballot)) + } + for _, atx := range tc.atxs { + require.NoError(t, atxs.Add(oracle.db, atx, types.AtxBlob{})) + oracle.atxsdata.AddFromAtx(atx, false) + } + if tc.beacon != types.EmptyBeacon { + oracle.mBeacon.EXPECT().GetBeacon(target).Return(tc.beacon, nil) + } else { + oracle.mBeacon.EXPECT().GetBeacon(target).Return(types.EmptyBeacon, sql.ErrNotFound) + } + rst, err := oracle.ActiveSet(context.TODO(), target) + + switch typed := tc.expect.(type) { + case []types.ATXID: + require.NoError(t, err) + require.ElementsMatch(t, typed, rst) + case string: + require.Empty(t, rst) + require.ErrorContains(t, err, typed) + default: + require.Failf(t, "unknown assert type", "%v", typed) + } + }) + } +} + +func TestResetCache(t *testing.T) { + oracle := defaultOracle(t) + ctrl := gomock.NewController(t) + + prev := oracle.activesCache + prev.Add(1, nil) + + oracle.resetCacheOnSynced(context.Background()) + require.Equal(t, prev, oracle.activesCache) + + sync := mocks.NewMockSyncStateProvider(ctrl) + oracle.SetSync(sync) + + sync.EXPECT().IsSynced(gomock.Any()).Return(false) + oracle.resetCacheOnSynced(context.Background()) + require.Equal(t, prev, oracle.activesCache) + + sync.EXPECT().IsSynced(gomock.Any()).Return(true) + oracle.resetCacheOnSynced(context.Background()) + require.NotEqual(t, prev, oracle.activesCache) + + prev = oracle.activesCache + prev.Add(1, nil) + + sync.EXPECT().IsSynced(gomock.Any()).Return(true) + oracle.resetCacheOnSynced(context.Background()) + require.Equal(t, prev, oracle.activesCache) +} + +func FuzzVrfMessageConsistency(f *testing.F) { + tester.FuzzConsistency[VrfMessage](f) +} + +func FuzzVrfMessageSafety(f *testing.F) { + tester.FuzzSafety[VrfMessage](f) +} diff --git a/hare4/hare.go b/hare4/hare.go new file mode 100644 index 0000000000..91f8b298be --- /dev/null +++ b/hare4/hare.go @@ -0,0 +1,925 @@ +package hare4 + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "math" + "slices" + "sync" + "time" + + "github.com/jonboulle/clockwork" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "golang.org/x/exp/maps" + "golang.org/x/sync/errgroup" + + "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/layerpatrol" + "github.com/spacemeshos/go-spacemesh/log" + "github.com/spacemeshos/go-spacemesh/metrics" + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub" + "github.com/spacemeshos/go-spacemesh/p2p/server" + "github.com/spacemeshos/go-spacemesh/proposals/store" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/sql/beacons" + "github.com/spacemeshos/go-spacemesh/sql/identities" + "github.com/spacemeshos/go-spacemesh/system" +) + +const ( + PROTOCOL_NAME = "hare4/full_exchange" + MAX_EXCHANGE_SIZE = 1_000_000 // protect against a malicious allocation of too much space. +) + +var ( + errNoLayerProposals = errors.New("no proposals for layer") + errCannotMatchProposals = errors.New("cannot match proposals to compacted form") + errResponseTooBig = errors.New("response too big") + errCannotFindProposal = errors.New("cannot find proposal") + errNoEligibilityProofs = errors.New("no eligibility proofs") + fetchFullTimeout = 5 * time.Second +) + +type CommitteeUpgrade struct { + Layer types.LayerID + Size uint16 +} + +type Config struct { + Enable bool `mapstructure:"enable"` + EnableLayer types.LayerID `mapstructure:"enable-layer"` + DisableLayer types.LayerID `mapstructure:"disable-layer"` + Committee uint16 `mapstructure:"committee"` + CommitteeUpgrade *CommitteeUpgrade + Leaders uint16 `mapstructure:"leaders"` + IterationsLimit uint8 `mapstructure:"iterations-limit"` + PreroundDelay time.Duration `mapstructure:"preround-delay"` + RoundDuration time.Duration `mapstructure:"round-duration"` + // LogStats if true will log iteration statistics with INFO level at the start of the next iteration. + // This requires additional computation and should be used for debugging only. + LogStats bool `mapstructure:"log-stats"` + ProtocolName string `mapstructure:"protocolname"` +} + +func (cfg *Config) CommitteeFor(layer types.LayerID) uint16 { + if cfg.CommitteeUpgrade != nil && layer >= cfg.CommitteeUpgrade.Layer { + return cfg.CommitteeUpgrade.Size + } + return cfg.Committee +} + +func (cfg *Config) Validate(zdist time.Duration) error { + terminates := cfg.roundStart(IterRound{Iter: cfg.IterationsLimit, Round: hardlock}) + if terminates > zdist { + return fmt.Errorf("hare terminates later (%v) than expected (%v)", terminates, zdist) + } + if cfg.Enable && cfg.DisableLayer <= cfg.EnableLayer { + return fmt.Errorf("disabled layer (%d) must be larger than enabled (%d)", + cfg.DisableLayer, cfg.EnableLayer) + } + return nil +} + +func (cfg *Config) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddBool("enabled", cfg.Enable) + encoder.AddUint32("enabled layer", cfg.EnableLayer.Uint32()) + encoder.AddUint32("disabled layer", cfg.DisableLayer.Uint32()) + encoder.AddUint16("committee", cfg.Committee) + if cfg.CommitteeUpgrade != nil { + encoder.AddUint32("committee upgrade layer", cfg.CommitteeUpgrade.Layer.Uint32()) + encoder.AddUint16("committee upgrade size", cfg.CommitteeUpgrade.Size) + } + encoder.AddUint16("leaders", cfg.Leaders) + encoder.AddUint8("iterations limit", cfg.IterationsLimit) + encoder.AddDuration("preround delay", cfg.PreroundDelay) + encoder.AddDuration("round duration", cfg.RoundDuration) + encoder.AddBool("log stats", cfg.LogStats) + encoder.AddString("p2p protocol", cfg.ProtocolName) + return nil +} + +// roundStart returns expected time for iter/round relative to +// layer start. +func (cfg *Config) roundStart(round IterRound) time.Duration { + if round.Round == 0 { + return cfg.PreroundDelay + } + return cfg.PreroundDelay + time.Duration(round.Absolute()-1)*cfg.RoundDuration +} + +func DefaultConfig() Config { + return Config{ + // NOTE(talm) We aim for a 2^{-40} error probability; if the population at large has a 2/3 honest majority, + // we need a committee of size ~800 to guarantee this error rate (at least, + // this is what the Chernoff bound gives you; the actual value is a bit lower, + // so we can probably get away with a smaller committee). For a committee of size 400, + // the Chernoff bound gives 2^{-20} probability of a dishonest majority when 1/3 of the population is dishonest. + Committee: 50, + Leaders: 5, + IterationsLimit: 4, + PreroundDelay: 25 * time.Second, + RoundDuration: 12 * time.Second, + ProtocolName: "/h/4.0", + DisableLayer: math.MaxUint32, + } +} + +type ConsensusOutput struct { + Layer types.LayerID + Proposals []types.ProposalID +} + +type WeakCoinOutput struct { + Layer types.LayerID + Coin bool +} + +type Opt func(*Hare) + +func WithServer(s streamRequester) Opt { + return func(hr *Hare) { + hr.p2p = s + } +} + +func WithWallclock(clock clockwork.Clock) Opt { + return func(hr *Hare) { + hr.wallclock = clock + } +} + +func WithConfig(cfg Config) Opt { + return func(hr *Hare) { + hr.config = cfg + hr.oracle.config = cfg + } +} + +func WithLogger(logger *zap.Logger) Opt { + return func(hr *Hare) { + hr.log = logger + hr.oracle.log = logger + } +} + +func WithTracer(tracer Tracer) Opt { + return func(hr *Hare) { + hr.tracer = tracer + } +} + +// WithResultsChan overrides the default result channel with a different one. +// This is only needed for the migration period between hare3 and hare4. +func WithResultsChan(c chan ConsensusOutput) Opt { + return func(hr *Hare) { + hr.results = c + } +} + +type nodeclock interface { + AwaitLayer(types.LayerID) <-chan struct{} + CurrentLayer() types.LayerID + LayerToTime(types.LayerID) time.Time +} + +func New( + nodeclock nodeclock, + pubsub pubsub.PublishSubsciber, + db sql.StateDatabase, + atxsdata *atxsdata.Data, + proposals *store.Store, + verif verifier, + oracle oracle, + sync system.SyncStateProvider, + patrol *layerpatrol.LayerPatrol, + host server.Host, + opts ...Opt, +) *Hare { + ctx, cancel := context.WithCancel(context.Background()) + hr := &Hare{ + ctx: ctx, + cancel: cancel, + results: make(chan ConsensusOutput, 32), + coins: make(chan WeakCoinOutput, 32), + signers: make(map[string]*signing.EdSigner), + sessions: make(map[types.LayerID]*protocol), + messageCache: make(map[types.Hash32]Message), + + config: DefaultConfig(), + log: zap.NewNop(), + wallclock: clockwork.NewRealClock(), + + nodeclock: nodeclock, + pubsub: pubsub, + db: db, + atxsdata: atxsdata, + proposals: proposals, + verifier: verif, + oracle: &legacyOracle{ + log: zap.NewNop(), + oracle: oracle, + config: DefaultConfig(), + }, + sync: sync, + patrol: patrol, + tracer: noopTracer{}, + } + for _, opt := range opts { + opt(hr) + } + + if host != nil { + hr.p2p = server.New(host, PROTOCOL_NAME, hr.handleProposalsStream) + } + return hr +} + +type Hare struct { + // state + ctx context.Context + cancel context.CancelFunc + eg errgroup.Group + results chan ConsensusOutput + coins chan WeakCoinOutput + mu sync.Mutex + signers map[string]*signing.EdSigner + sessions map[types.LayerID]*protocol + messageCache map[types.Hash32]Message + + // options + config Config + log *zap.Logger + wallclock clockwork.Clock + + // dependencies + nodeclock nodeclock + pubsub pubsub.PublishSubsciber + db sql.StateDatabase + atxsdata *atxsdata.Data + proposals *store.Store + verifier verifier + oracle *legacyOracle + sync system.SyncStateProvider + patrol *layerpatrol.LayerPatrol + p2p streamRequester + tracer Tracer +} + +func (h *Hare) Register(sig *signing.EdSigner) { + h.mu.Lock() + defer h.mu.Unlock() + h.log.Info("registered signing key", log.ZShortStringer("id", sig.NodeID())) + h.signers[string(sig.NodeID().Bytes())] = sig +} + +func (h *Hare) Results() <-chan ConsensusOutput { + return h.results +} + +func (h *Hare) Coins() <-chan WeakCoinOutput { + return h.coins +} + +func (h *Hare) Start() { + h.pubsub.Register(h.config.ProtocolName, h.Handler, pubsub.WithValidatorInline(true)) + current := h.nodeclock.CurrentLayer() + 1 + enabled := max(current, h.config.EnableLayer, types.GetEffectiveGenesis()+1) + disabled := types.LayerID(math.MaxUint32) + if h.config.DisableLayer > 0 { + disabled = h.config.DisableLayer + } + h.log.Info("started", + zap.Inline(&h.config), + zap.Uint32("enabled", enabled.Uint32()), + zap.Uint32("disabled", disabled.Uint32()), + ) + h.eg.Go(func() error { + for next := enabled; next < disabled; next++ { + select { + case <-h.nodeclock.AwaitLayer(next): + h.log.Debug("notified", zap.Uint32("layer", next.Uint32())) + h.onLayer(next) + h.cleanMessageCache(next - 1) + case <-h.ctx.Done(): + return nil + } + } + return nil + }) +} + +func (h *Hare) Running() int { + h.mu.Lock() + defer h.mu.Unlock() + return len(h.sessions) +} + +// fetchFull will fetch the full list of proposal IDs from the provided peer. +func (h *Hare) fetchFull(ctx context.Context, peer p2p.Peer, msgId types.Hash32) ( + []types.ProposalID, error, +) { + ctx, cancel := context.WithTimeout(ctx, fetchFullTimeout) + defer cancel() + + requestCompactCounter.Inc() + req := &CompactIdRequest{MsgId: msgId} + reqBytes := codec.MustEncode(req) + resp := &CompactIdResponse{} + cb := func(ctx context.Context, rw io.ReadWriter) error { + respLen, _, err := codec.DecodeLen(rw) + if err != nil { + return fmt.Errorf("decode length: %w", err) + } + if respLen >= MAX_EXCHANGE_SIZE { + return errResponseTooBig + } + b, err := codec.DecodeFrom(rw, resp) + if err != nil || b != int(respLen) { + return fmt.Errorf("decode response: %w", err) + } + return nil + } + + err := h.p2p.StreamRequest(ctx, peer, reqBytes, cb) + if err != nil { + requestCompactErrorCounter.Inc() + return nil, fmt.Errorf("stream request: %w", err) + } + + h.tracer.OnCompactIdResponse(resp) + + return resp.Ids, nil +} + +func (h *Hare) handleProposalsStream(ctx context.Context, msg []byte, s io.ReadWriter) error { + requestCompactHandlerCounter.Inc() + compactProps := &CompactIdRequest{} + if err := codec.Decode(msg, compactProps); err != nil { + malformedError.Inc() + return fmt.Errorf("%w: decoding error %s", pubsub.ErrValidationReject, err.Error()) + } + h.tracer.OnCompactIdRequest(compactProps) + h.mu.Lock() + m, ok := h.messageCache[compactProps.MsgId] + h.mu.Unlock() + if !ok { + messageCacheMiss.Inc() + return fmt.Errorf("message %s: cache miss", compactProps.MsgId) + } + resp := &CompactIdResponse{Ids: m.Body.Value.Proposals} + respBytes := codec.MustEncode(resp) + if _, err := codec.EncodeLen(s, uint32(len(respBytes))); err != nil { + return fmt.Errorf("encode length: %w", err) + } + + if _, err := s.Write(respBytes); err != nil { + return fmt.Errorf("write response: %w", err) + } + + return nil +} + +// reconstructProposals tries to reconstruct the full list of proposals from a peer based on a delivered +// set of compact IDs. +func (h *Hare) reconstructProposals(ctx context.Context, peer p2p.Peer, msgId types.Hash32, msg *Message) error { + proposals := h.proposals.GetForLayer(msg.Layer) + if len(proposals) == 0 { + return errNoLayerProposals + } + compacted := h.compactProposals(msg.Layer, proposals) + proposalIds := make([]proposalTuple, len(proposals)) + for i := range proposals { + proposalIds[i] = proposalTuple{id: proposals[i].ID(), compact: compacted[i]} + } + slices.SortFunc(proposalIds, func(i, j proposalTuple) int { return bytes.Compare(i.id[:], j.id[:]) }) + taken := make([]bool, len(proposals)) + findProp := func(id types.CompactProposalID) (bool, types.ProposalID) { + for i := 0; i < len(proposalIds); i++ { + if id != proposalIds[i].compact { + continue + } + if taken[i] { + continue + } + // item is both not taken and equals to looked up ID + taken[i] = true + return true, proposalIds[i].id + } + return false, types.EmptyProposalID + } + + msg.Value.Proposals = make([]types.ProposalID, len(msg.Value.CompactProposals)) + ctr := 0 + + for i, compact := range msg.Value.CompactProposals { + // try to see if we can match it to the proposals we have + // if we do, add the proposal ID to the list of hashes in the proposals on the message + found, id := findProp(compact) + if !found { + // if we can't find it, we can already assume that we cannot match the whole + // set and just fail fast + return errCannotMatchProposals + } + msg.Value.Proposals[i] = id + ctr++ + } + + if ctr != len(msg.Value.CompactProposals) { + // this will force the calling context to do a fetchFull + return errCannotMatchProposals + } + + // sort the found proposals and unset the compact proposals + // field before trying to check the signature + // since it would add unnecessary data to the hasher + slices.SortFunc(msg.Value.Proposals, func(i, j types.ProposalID) int { return bytes.Compare(i[:], j[:]) }) + msg.Value.CompactProposals = []types.CompactProposalID{} + return nil +} + +func (h *Hare) Handler(ctx context.Context, peer p2p.Peer, buf []byte) error { + msg := &Message{} + if err := codec.Decode(buf, msg); err != nil { + malformedError.Inc() + return fmt.Errorf("%w: decoding error %s", pubsub.ErrValidationReject, err.Error()) + } + if err := msg.Validate(); err != nil { + malformedError.Inc() + return fmt.Errorf("%w: validation %s", pubsub.ErrValidationReject, err.Error()) + } + h.tracer.OnMessageReceived(msg) + h.mu.Lock() + session, registered := h.sessions[msg.Layer] + h.mu.Unlock() + if !registered { + notRegisteredError.Inc() + return fmt.Errorf("layer %d is not registered", msg.Layer) + } + + var ( + compacts []types.CompactProposalID + msgId = msg.ToHash() + fetched = false + ) + + if msg.IterRound.Round == preround { + // this will mutate the message to conform to the (hopefully) + // original sent message for signature validation to occur + compacts = msg.Value.CompactProposals + messageCompactsCounter.Add(float64(len(compacts))) + err := h.reconstructProposals(ctx, peer, msgId, msg) + switch { + case errors.Is(err, errCannotMatchProposals): + msg.Value.Proposals, err = h.fetchFull(ctx, peer, msgId) + if err != nil { + return fmt.Errorf("fetch full: %w", err) + } + slices.SortFunc(msg.Value.Proposals, func(i, j types.ProposalID) int { return bytes.Compare(i[:], j[:]) }) + msg.Value.CompactProposals = []types.CompactProposalID{} + fetched = true + case err != nil: + return fmt.Errorf("reconstruct proposals: %w", err) + } + } + if !h.verifier.Verify(signing.HARE, msg.Sender, msg.ToMetadata().ToBytes(), msg.Signature) { + if msg.IterRound.Round == preround && !fetched { + preroundSigFailCounter.Inc() + // we might have a bad signature because of a local hash collision + // of a proposal that has the same short hash that the node sent us. + // in this case we try to ask for a full exchange of all full proposal + // ids and try to validate again + var err error + msg.Body.Value.Proposals, err = h.fetchFull(ctx, peer, msgId) + if err != nil { + return fmt.Errorf("signature verify: fetch full: %w", err) + } + if len(msg.Body.Value.Proposals) != len(compacts) { + return fmt.Errorf("signature verify: proposals mismatch: %w", err) + } + if !h.verifier.Verify(signing.HARE, msg.Sender, msg.ToMetadata().ToBytes(), msg.Signature) { + signatureError.Inc() + return fmt.Errorf("%w: signature verify: invalid signature", pubsub.ErrValidationReject) + } + } else { + signatureError.Inc() + return fmt.Errorf("%w: invalid signature", pubsub.ErrValidationReject) + } + } + + if msg.IterRound.Round == preround { + h.mu.Lock() + if _, ok := h.messageCache[msgId]; !ok { + newMsg := *msg + newMsg.Body.Value.CompactProposals = compacts + h.messageCache[msgId] = newMsg + } + h.mu.Unlock() + } + + malicious := h.atxsdata.IsMalicious(msg.Sender) + + start := time.Now() + g := h.oracle.validate(msg) + oracleLatency.Observe(time.Since(start).Seconds()) + if g == grade0 { + oracleError.Inc() + return errors.New("zero grade") + } + start = time.Now() + input := &input{ + Message: msg, + msgHash: msg.ToHash(), + malicious: malicious, + atxgrade: g, + } + h.log.Debug("on message", zap.Inline(input)) + gossip, equivocation := session.OnInput(input) + h.log.Debug("after on message", log.ZShortStringer("hash", input.msgHash), zap.Bool("gossip", gossip)) + submitLatency.Observe(time.Since(start).Seconds()) + if equivocation != nil && !malicious { + h.log.Debug("registered equivocation", + zap.Uint32("lid", msg.Layer.Uint32()), + zap.Stringer("sender", equivocation.Messages[0].SmesherID)) + proof := equivocation.ToMalfeasanceProof() + if err := identities.SetMalicious( + h.db, equivocation.Messages[0].SmesherID, codec.MustEncode(proof), time.Now()); err != nil { + h.log.Error("failed to save malicious identity", zap.Error(err)) + } + h.atxsdata.SetMalicious(equivocation.Messages[0].SmesherID) + } + if !gossip { + droppedMessages.Inc() + return errors.New("dropped by graded gossip") + } + expected := h.nodeclock.LayerToTime(msg.Layer).Add(h.config.roundStart(msg.IterRound)) + metrics.ReportMessageLatency(h.config.ProtocolName, msg.Round.String(), time.Since(expected)) + return nil +} + +func (h *Hare) onLayer(layer types.LayerID) { + h.proposals.OnLayer(layer) + if !h.sync.IsSynced(h.ctx) { + h.log.Debug("not synced", zap.Uint32("lid", layer.Uint32())) + return + } + beacon, err := beacons.Get(h.db, layer.GetEpoch()) + if err != nil || beacon == types.EmptyBeacon { + h.log.Debug("no beacon", + zap.Uint32("epoch", layer.GetEpoch().Uint32()), + zap.Uint32("lid", layer.Uint32()), + zap.Error(err), + ) + return + } + h.patrol.SetHareInCharge(layer) + + h.mu.Lock() + // signer can't join mid session + s := &session{ + lid: layer, + beacon: beacon, + signers: maps.Values(h.signers), + vrfs: make([]*types.HareEligibility, len(h.signers)), + proto: newProtocol(h.config.CommitteeFor(layer)/2 + 1), + } + h.sessions[layer] = s.proto + h.mu.Unlock() + + sessionStart.Inc() + h.tracer.OnStart(layer) + h.log.Debug("registered layer", zap.Uint32("lid", layer.Uint32())) + h.eg.Go(func() error { + if err := h.run(s); err != nil { + h.log.Warn("failed", + zap.Uint32("lid", layer.Uint32()), + zap.Error(err), + ) + exitErrors.Inc() + // if terminated successfully it will notify block generator + // and it will have to CompleteHare + h.patrol.CompleteHare(layer) + } else { + h.log.Debug("terminated", + zap.Uint32("lid", layer.Uint32()), + ) + } + h.mu.Lock() + delete(h.sessions, layer) + h.mu.Unlock() + sessionTerminated.Inc() + h.tracer.OnStop(layer) + return nil + }) +} + +func (h *Hare) run(session *session) error { + // oracle may load non-negligible amount of data from disk + // we do it before preround starts, so that load can have some slack time + // before it needs to be used in validation + var ( + current = IterRound{Round: preround} + start = time.Now() + active bool + ) + for i := range session.signers { + session.vrfs[i] = h.oracle.active(session.signers[i], session.beacon, session.lid, current) + active = active || session.vrfs[i] != nil + } + h.tracer.OnActive(session.vrfs) + activeLatency.Observe(time.Since(start).Seconds()) + + walltime := h.nodeclock.LayerToTime(session.lid).Add(h.config.PreroundDelay) + if active { + h.log.Debug("active in preround. waiting for preround delay", zap.Uint32("lid", session.lid.Uint32())) + // initial set is not needed if node is not active in preround + select { + case <-h.wallclock.After(walltime.Sub(h.wallclock.Now())): + case <-h.ctx.Done(): + return h.ctx.Err() + } + start := time.Now() + session.proto.OnInitial(h.selectProposals(session)) + proposalsLatency.Observe(time.Since(start).Seconds()) + } + if err := h.onOutput(session, current, session.proto.Next()); err != nil { + return err + } + result := false + for { + walltime = walltime.Add(h.config.RoundDuration) + current = session.proto.IterRound + start = time.Now() + + for i := range session.signers { + if current.IsMessageRound() { + session.vrfs[i] = h.oracle.active(session.signers[i], session.beacon, session.lid, current) + } else { + session.vrfs[i] = nil + } + } + h.tracer.OnActive(session.vrfs) + activeLatency.Observe(time.Since(start).Seconds()) + + select { + case <-h.wallclock.After(walltime.Sub(h.wallclock.Now())): + h.log.Debug("execute round", + zap.Uint32("lid", session.lid.Uint32()), + zap.Uint8("iter", session.proto.Iter), zap.Stringer("round", session.proto.Round), + zap.Bool("active", active), + ) + out := session.proto.Next() + if out.result != nil { + result = true + } + if err := h.onOutput(session, current, out); err != nil { + return err + } + // we are logginng stats 1 network delay after new iteration start + // so that we can receive notify messages from previous iteration + if session.proto.Round == softlock && h.config.LogStats { + h.log.Info("stats", zap.Uint32("lid", session.lid.Uint32()), zap.Inline(session.proto.Stats())) + } + if out.terminated { + if !result { + return errors.New("terminated without result") + } + return nil + } + if current.Iter == h.config.IterationsLimit { + return fmt.Errorf("hare failed to reach consensus in %d iterations", + h.config.IterationsLimit) + } + case <-h.ctx.Done(): + return nil + } + } +} + +func (h *Hare) onOutput(session *session, ir IterRound, out output) error { + for i, vrf := range session.vrfs { + if vrf == nil || out.message == nil { + continue + } + msg := *out.message // shallow copy + msg.Layer = session.lid + msg.Eligibility = *vrf + msg.Sender = session.signers[i].NodeID() + msg.Signature = session.signers[i].Sign(signing.HARE, msg.ToMetadata().ToBytes()) + if ir.Round == preround { + var err error + msg.Body.Value.CompactProposals, err = h.compactProposalIds(msg.Layer, + out.message.Body.Value.Proposals) + if err != nil { + h.log.Debug("failed to compact proposals", zap.Error(err)) + continue + } + fullProposals := msg.Body.Value.Proposals + msg.Body.Value.Proposals = []types.ProposalID{} + id := msg.ToHash() + msg.Body.Value.Proposals = fullProposals + h.mu.Lock() + h.messageCache[id] = msg + h.mu.Unlock() + msg.Body.Value.Proposals = []types.ProposalID{} + } + if err := h.pubsub.Publish(h.ctx, h.config.ProtocolName, msg.ToBytes()); err != nil { + h.log.Error("failed to publish", zap.Inline(&msg), zap.Error(err)) + } + } + h.tracer.OnMessageSent(out.message) + h.log.Debug("round output", + zap.Uint32("lid", session.lid.Uint32()), + zap.Uint8("iter", ir.Iter), zap.Stringer("round", ir.Round), + zap.Inline(&out), + ) + if out.coin != nil { + select { + case <-h.ctx.Done(): + return h.ctx.Err() + case h.coins <- WeakCoinOutput{Layer: session.lid, Coin: *out.coin}: + } + sessionCoin.Inc() + } + if out.result != nil { + select { + case <-h.ctx.Done(): + return h.ctx.Err() + case h.results <- ConsensusOutput{Layer: session.lid, Proposals: out.result}: + } + sessionResult.Inc() + } + return nil +} + +func (h *Hare) selectProposals(session *session) []types.ProposalID { + h.log.Debug("requested proposals", + zap.Uint32("lid", session.lid.Uint32()), + zap.Stringer("beacon", session.beacon), + ) + + var ( + result []types.ProposalID + min *atxsdata.ATX + ) + target := session.lid.GetEpoch() + publish := target - 1 + for _, signer := range session.signers { + atxid, err := atxs.GetIDByEpochAndNodeID(h.db, publish, signer.NodeID()) + switch { + case errors.Is(err, sql.ErrNotFound): + // if atx is not registered for identity we will get sql.ErrNotFound + case err != nil: + h.log.Error("failed to get atx id by epoch and node id", zap.Error(err)) + return []types.ProposalID{} + default: + own := h.atxsdata.Get(target, atxid) + if min == nil || (min != nil && own != nil && own.Height < min.Height) { + min = own + } + } + } + if min == nil { + h.log.Debug("no atxs in the requested epoch", zap.Uint32("epoch", session.lid.GetEpoch().Uint32()-1)) + return []types.ProposalID{} + } + + candidates := h.proposals.GetForLayer(session.lid) + atxs := map[types.ATXID]int{} + for _, p := range candidates { + atxs[p.AtxID]++ + } + for _, p := range candidates { + if h.atxsdata.IsMalicious(p.SmesherID) || p.IsMalicious() { + h.log.Warn("not voting on proposal from malicious identity", + zap.Stringer("id", p.ID()), + ) + continue + } + // double check that a single smesher is not included twice + // theoretically it should never happen as it is covered + // by the malicious check above. + if n := atxs[p.AtxID]; n > 1 { + h.log.Error("proposal with same atx added several times in the recorded set", + zap.Int("n", n), + zap.Stringer("id", p.ID()), + zap.Stringer("atxid", p.AtxID), + ) + continue + } + header := h.atxsdata.Get(target, p.AtxID) + if header == nil { + h.log.Error("atx is not loaded", zap.Stringer("atxid", p.AtxID)) + return []types.ProposalID{} + } + if header.BaseHeight >= min.Height { + // does not vote for future proposal + h.log.Warn("proposal base tick height too high. skipping", + zap.Uint32("lid", session.lid.Uint32()), + zap.Uint64("proposal_height", header.BaseHeight), + zap.Uint64("min_height", min.Height), + ) + continue + } + + if p.Beacon() == session.beacon { + result = append(result, p.ID()) + } else { + h.log.Warn("proposal has different beacon value", + zap.Uint32("lid", session.lid.Uint32()), + zap.Stringer("id", p.ID()), + zap.Stringer("proposal_beacon", p.Beacon()), + zap.Stringer("epoch_beacon", session.beacon), + ) + } + } + return result +} + +func (h *Hare) IsKnown(layer types.LayerID, proposal types.ProposalID) bool { + return h.proposals.Get(layer, proposal) != nil +} + +// OnProposal is a hook which gets called when we get a proposal. +func (h *Hare) OnProposal(p *types.Proposal) error { + return h.proposals.Add(p) +} + +// cleanMessageCache cleans old cached preround messages +// once the layers become irrelevant. +func (h *Hare) cleanMessageCache(l types.LayerID) { + var keys []types.Hash32 + h.mu.Lock() + defer h.mu.Unlock() + for k, item := range h.messageCache { + if item.Layer < l { + // mark key for deletion + keys = append(keys, k) + } + } + for _, v := range keys { + delete(h.messageCache, v) + } +} + +func (h *Hare) Stop() { + h.cancel() + h.eg.Wait() + close(h.coins) + h.log.Info("stopped") +} + +type session struct { + proto *protocol + lid types.LayerID + beacon types.Beacon + signers []*signing.EdSigner + vrfs []*types.HareEligibility +} + +func (h *Hare) compactProposals(layer types.LayerID, + proposals []*types.Proposal, +) []types.CompactProposalID { + compactProposals := make([]types.CompactProposalID, len(proposals)) + for i, prop := range proposals { + vrf := prop.EligibilityProofs[0].Sig + var c types.CompactProposalID + copy(c[:], vrf[:4]) + compactProposals[i] = c + } + return compactProposals +} + +func (h *Hare) compactProposalIds(layer types.LayerID, + proposals []types.ProposalID, +) ([]types.CompactProposalID, error) { + compactProposals := make([]types.CompactProposalID, len(proposals)) + for i, prop := range proposals { + fp := h.proposals.Get(layer, prop) + if fp == nil { + return nil, errCannotFindProposal + } + + // we must handle this explicitly or we risk a panic on + // a nil slice access below + if len(fp.EligibilityProofs) == 0 { + return nil, errNoEligibilityProofs + } + + compactProposals[i] = types.CompactProposalID(fp.EligibilityProofs[0].Sig[:]) + } + return compactProposals, nil +} + +type proposalTuple struct { + id types.ProposalID + compact types.CompactProposalID +} diff --git a/hare4/hare_test.go b/hare4/hare_test.go new file mode 100644 index 0000000000..5e2dd16921 --- /dev/null +++ b/hare4/hare_test.go @@ -0,0 +1,1430 @@ +package hare4 + +import ( + "bytes" + "context" + "fmt" + "math/rand" + "os" + "runtime/pprof" + "slices" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest" + + "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/hare4/eligibility" + hmock "github.com/spacemeshos/go-spacemesh/hare4/mocks" + "github.com/spacemeshos/go-spacemesh/layerpatrol" + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub" + pmocks "github.com/spacemeshos/go-spacemesh/p2p/pubsub/mocks" + "github.com/spacemeshos/go-spacemesh/p2p/server" + "github.com/spacemeshos/go-spacemesh/proposals/store" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/sql/ballots" + "github.com/spacemeshos/go-spacemesh/sql/beacons" + "github.com/spacemeshos/go-spacemesh/sql/identities" + "github.com/spacemeshos/go-spacemesh/sql/statesql" + smocks "github.com/spacemeshos/go-spacemesh/system/mocks" +) + +const layersPerEpoch = 4 + +var wait = 10 * time.Second + +func TestMain(m *testing.M) { + types.SetLayersPerEpoch(layersPerEpoch) + res := m.Run() + os.Exit(res) +} + +type tester struct { + testing.TB + + rng *rand.Rand + start time.Time + cfg Config + layerDuration time.Duration + beacon types.Beacon + genesis types.LayerID +} + +type waiter struct { + lid types.LayerID + ch chan struct{} +} + +// timesync.Nodeclock time can't be mocked nicely because of ticks. +type testNodeClock struct { + mu sync.Mutex + started types.LayerID + waiters []waiter + + genesis time.Time + layerDuration time.Duration +} + +func (t *testNodeClock) CurrentLayer() types.LayerID { + t.mu.Lock() + defer t.mu.Unlock() + return t.started +} + +func (t *testNodeClock) LayerToTime(lid types.LayerID) time.Time { + return t.genesis.Add(time.Duration(lid) * t.layerDuration) +} + +func (t *testNodeClock) StartLayer(lid types.LayerID) { + t.mu.Lock() + defer t.mu.Unlock() + t.started = lid + for _, w := range t.waiters { + if w.lid <= lid { + select { + case <-w.ch: + default: + close(w.ch) + } + } + } +} + +func (t *testNodeClock) AwaitLayer(lid types.LayerID) <-chan struct{} { + t.mu.Lock() + defer t.mu.Unlock() + ch := make(chan struct{}) + if lid <= t.started { + close(ch) + return ch + } + t.waiters = append(t.waiters, waiter{lid: lid, ch: ch}) + return ch +} + +type node struct { + t *tester + + i int + clock clockwork.FakeClock + nclock *testNodeClock + signer *signing.EdSigner + registered []*signing.EdSigner + vrfsigner *signing.VRFSigner + atx *types.ActivationTx + oracle *eligibility.Oracle + db sql.StateDatabase + atxsdata *atxsdata.Data + proposals *store.Store + + ctrl *gomock.Controller + mpublisher *pmocks.MockPublishSubsciber + msyncer *smocks.MockSyncStateProvider + mverifier *hmock.Mockverifier + mockStreamRequester *hmock.MockstreamRequester + patrol *layerpatrol.LayerPatrol + tracer *testTracer + hare *Hare +} + +func (n *node) withClock() *node { + n.clock = clockwork.NewFakeClockAt(n.t.start) + return n +} + +func (n *node) withSigner() *node { + signer, err := signing.NewEdSigner(signing.WithKeyFromRand(n.t.rng)) + require.NoError(n.t, err) + n.signer = signer + n.vrfsigner = signer.VRFSigner() + return n +} + +func (n *node) reuseSigner(signer *signing.EdSigner) *node { + n.signer = signer + n.vrfsigner = signer.VRFSigner() + return n +} + +func (n *node) withDb(tb testing.TB) *node { + n.db = statesql.InMemoryTest(tb) + n.atxsdata = atxsdata.New() + n.proposals = store.New() + return n +} + +func (n *node) withAtx(min, max int) *node { + atx := &types.ActivationTx{ + PublishEpoch: n.t.genesis.GetEpoch(), + TickCount: 1, + SmesherID: n.signer.NodeID(), + } + if max-min > 0 { + atx.NumUnits = uint32(n.t.rng.Intn(max-min) + min) + } else { + atx.NumUnits = uint32(min) + } + atx.Weight = uint64(atx.NumUnits) * atx.TickCount + id := types.ATXID{} + n.t.rng.Read(id[:]) + atx.SetID(id) + atx.SetReceived(n.t.start) + atx.VRFNonce = types.VRFPostIndex(n.t.rng.Uint64()) + + n.atx = atx + return n +} + +func (n *node) withController() *node { + n.ctrl = gomock.NewController(n.t) + return n +} + +func (n *node) withSyncer() *node { + n.msyncer = smocks.NewMockSyncStateProvider(n.ctrl) + n.msyncer.EXPECT().IsSynced(gomock.Any()).Return(true).AnyTimes() + return n +} + +func (n *node) withVerifier() *node { + n.mverifier = hmock.NewMockverifier(n.ctrl) + return n +} + +func (n *node) withOracle() *node { + beaconget := smocks.NewMockBeaconGetter(n.ctrl) + beaconget.EXPECT().GetBeacon(gomock.Any()).DoAndReturn(func(epoch types.EpochID) (types.Beacon, error) { + return beacons.Get(n.db, epoch) + }).AnyTimes() + n.oracle = eligibility.New( + beaconget, + n.db, + n.atxsdata, + signing.NewVRFVerifier(), + layersPerEpoch, + ) + return n +} + +func (n *node) withPublisher() *node { + n.mpublisher = pmocks.NewMockPublishSubsciber(n.ctrl) + n.mpublisher.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + return n +} + +func (n *node) withStreamRequester() *node { + n.mockStreamRequester = hmock.NewMockstreamRequester(n.ctrl) + return n +} + +func (n *node) withHare() *node { + logger := zaptest.NewLogger(n.t).Named(fmt.Sprintf("hare=%d", n.i)) + n.nclock = &testNodeClock{ + genesis: n.t.start, + layerDuration: n.t.layerDuration, + } + tracer := newTestTracer(n.t) + n.tracer = tracer + n.patrol = layerpatrol.New() + var verify verifier + if n.mverifier != nil { + verify = n.mverifier + } else { + verify = signing.NewEdVerifier() + } + z, _ := zap.NewDevelopment() + n.hare = New( + n.nclock, + n.mpublisher, + n.db, + n.atxsdata, + n.proposals, + verify, + n.oracle, + n.msyncer, + n.patrol, + nil, + WithConfig(n.t.cfg), + WithLogger(logger), + WithWallclock(n.clock), + WithTracer(tracer), + WithServer(n.mockStreamRequester), + WithLogger(z), + ) + n.register(n.signer) + return n +} + +func (n *node) waitEligibility() { + n.tracer.waitEligibility() +} + +func (n *node) waitSent() { + n.tracer.waitSent() +} + +func (n *node) register(signer *signing.EdSigner) { + n.hare.Register(signer) + n.registered = append(n.registered, signer) +} + +func (n *node) storeAtx(atx *types.ActivationTx) error { + if err := atxs.Add(n.db, atx, types.AtxBlob{}); err != nil { + return err + } + n.atxsdata.AddFromAtx(atx, false) + return nil +} + +func (n *node) peerId() p2p.Peer { + return p2p.Peer(strconv.Itoa(n.i)) +} + +type clusterOpt func(*lockstepCluster) + +func withUnits(min, max int) clusterOpt { + return func(cluster *lockstepCluster) { + cluster.units.min = min + cluster.units.max = max + } +} + +func withMockVerifier() clusterOpt { + return func(cluster *lockstepCluster) { + cluster.mockVerify = true + } +} + +func withCollidingProposals() clusterOpt { + return func(cluster *lockstepCluster) { + cluster.collidingProposals = true + } +} + +func withProposals(fraction float64) clusterOpt { + return func(cluster *lockstepCluster) { + cluster.proposals.fraction = fraction + cluster.proposals.shuffle = true + } +} + +// withSigners creates N signers in addition to regular active nodes. +// this signeres will be partitioned in fair fashion across regular active nodes. +func withSigners(n int) clusterOpt { + return func(cluster *lockstepCluster) { + cluster.signersCount = n + } +} + +func newLockstepCluster(t *tester, opts ...clusterOpt) *lockstepCluster { + t.Helper() + cluster := &lockstepCluster{t: t} + cluster.units.min = 10 + cluster.units.max = 10 + cluster.proposals.fraction = 1 + cluster.proposals.shuffle = false + for _, opt := range opts { + opt(cluster) + } + return cluster +} + +// lockstepCluster allows to run rounds in lockstep +// as no peer will be able to start around until test allows it. +type lockstepCluster struct { + t *tester + nodes []*node + signers []*node // nodes that active on consensus but don't run hare instance + + mockVerify bool + collidingProposals bool + units struct { + min, max int + } + proposals struct { + fraction float64 + shuffle bool + } + signersCount int + + timestamp time.Time +} + +func (cl *lockstepCluster) addNode(n *node) { + n.hare.Start() + cl.t.Cleanup(func() { + n.hare.Stop() + }) + cl.nodes = append(cl.nodes, n) +} + +func (cl *lockstepCluster) partitionSigners() { + for i, signer := range cl.signers { + cl.nodes[i%len(cl.nodes)].register(signer.signer) + } +} + +func (cl *lockstepCluster) addSigner(n int) *lockstepCluster { + last := len(cl.signers) + for i := last; i < last+n; i++ { + n := (&node{t: cl.t, i: i}).withSigner().withAtx(cl.units.min, cl.units.max) + cl.signers = append(cl.signers, n) + } + return cl +} + +func (cl *lockstepCluster) addActive(n int) *lockstepCluster { + last := len(cl.nodes) + for i := last; i < last+n; i++ { + nn := (&node{t: cl.t, i: i}). + withController().withSyncer().withPublisher(). + withClock().withDb(cl.t).withSigner().withAtx(cl.units.min, cl.units.max). + withStreamRequester().withOracle().withHare() + if cl.mockVerify { + nn = nn.withVerifier() + } + cl.addNode(nn) + } + return cl +} + +func (cl *lockstepCluster) addInactive(n int) *lockstepCluster { + last := len(cl.nodes) + for i := last; i < last+n; i++ { + cl.addNode((&node{t: cl.t, i: i}). + withController().withSyncer().withPublisher(). + withClock().withDb(cl.t).withSigner(). + withStreamRequester().withOracle().withHare()) + } + return cl +} + +func (cl *lockstepCluster) addEquivocators(n int) *lockstepCluster { + require.LessOrEqual(cl.t, n, len(cl.nodes)) + last := len(cl.nodes) + for i := last; i < last+n; i++ { + cl.addNode((&node{t: cl.t, i: i}). + reuseSigner(cl.nodes[i-last].signer). + withController().withSyncer().withPublisher(). + withClock().withDb(cl.t).withAtx(cl.units.min, cl.units.max). + withStreamRequester().withOracle().withHare()) + } + return cl +} + +func (cl *lockstepCluster) nogossip() { + for _, n := range cl.nodes { + require.NoError(cl.t, beacons.Add(n.db, cl.t.genesis.GetEpoch()+1, cl.t.beacon)) + n.mpublisher.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + } +} + +func (cl *lockstepCluster) activeSet() types.ATXIDList { + var ids []types.ATXID + unique := map[types.ATXID]struct{}{} + for _, n := range append(cl.nodes, cl.signers...) { + if n.atx == nil { + continue + } + if _, exists := unique[n.atx.ID()]; exists { + continue + } + unique[n.atx.ID()] = struct{}{} + ids = append(ids, n.atx.ID()) + } + return ids +} + +func (cl *lockstepCluster) genProposalNode(lid types.LayerID, node int) { + active := cl.activeSet() + n := cl.nodes[node] + if n.atx == nil { + panic("shouldnt happen") + } + proposal := &types.Proposal{} + proposal.Layer = lid + proposal.EpochData = &types.EpochData{ + Beacon: cl.t.beacon, + ActiveSetHash: active.Hash(), + } + proposal.AtxID = n.atx.ID() + proposal.SmesherID = n.signer.NodeID() + id := types.ProposalID{} + cl.t.rng.Read(id[:]) + bid := types.BallotID{} + cl.t.rng.Read(bid[:]) + proposal.SetID(id) + proposal.Ballot.SetID(bid) + var vrf types.VrfSignature + cl.t.rng.Read(vrf[:]) + proposal.Ballot.EligibilityProofs = append(proposal.Ballot.EligibilityProofs, types.VotingEligibility{Sig: vrf}) + + proposal.SetBeacon(proposal.EpochData.Beacon) + require.NoError(cl.t, ballots.Add(n.db, &proposal.Ballot)) + n.hare.OnProposal(proposal) +} + +func (cl *lockstepCluster) genProposals(lid types.LayerID, skipNodes ...int) { + active := cl.activeSet() + all := []*types.Proposal{} + for i, n := range append(cl.nodes, cl.signers...) { + if n.atx == nil || slices.Contains(skipNodes, i) { + continue + } + proposal := &types.Proposal{} + proposal.Layer = lid + proposal.EpochData = &types.EpochData{ + Beacon: cl.t.beacon, + ActiveSetHash: active.Hash(), + } + proposal.AtxID = n.atx.ID() + proposal.SmesherID = n.signer.NodeID() + id := types.ProposalID{} + cl.t.rng.Read(id[:]) + bid := types.BallotID{} + cl.t.rng.Read(bid[:]) + proposal.SetID(id) + proposal.Ballot.SetID(bid) + var vrf types.VrfSignature + if !cl.collidingProposals { + // if we want non-colliding proposals we copy from the rng + // otherwise it is kept as an array of zeroes + cl.t.rng.Read(vrf[:]) + } + proposal.Ballot.EligibilityProofs = append(proposal.Ballot.EligibilityProofs, types.VotingEligibility{Sig: vrf}) + + proposal.SetBeacon(proposal.EpochData.Beacon) + all = append(all, proposal) + } + for _, other := range cl.nodes { + cp := make([]*types.Proposal, len(all)) + copy(cp, all) + if cl.proposals.shuffle { + cl.t.rng.Shuffle(len(cp), func(i, j int) { + cp[i], cp[j] = cp[j], cp[i] + }) + } + for _, proposal := range cp[:int(float64(len(cp))*cl.proposals.fraction)] { + require.NoError(cl.t, ballots.Add(other.db, &proposal.Ballot)) + other.hare.OnProposal(proposal) + } + } +} + +func (cl *lockstepCluster) setup() { + active := cl.activeSet() + for _, n := range cl.nodes { + require.NoError(cl.t, beacons.Add(n.db, cl.t.genesis.GetEpoch()+1, cl.t.beacon)) + for _, other := range append(cl.nodes, cl.signers...) { + if other.atx == nil { + continue + } + require.NoError(cl.t, n.storeAtx(other.atx)) + } + n.oracle.UpdateActiveSet(cl.t.genesis.GetEpoch()+1, active) + n.mpublisher.EXPECT(). + Publish(gomock.Any(), gomock.Any(), gomock.Any()). + Do(func(ctx context.Context, _ string, msg []byte) error { + for _, other := range cl.nodes { + other.hare.Handler(ctx, n.peerId(), msg) + } + return nil + }). + AnyTimes() + n.mockStreamRequester.EXPECT().StreamRequest(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( + func(ctx context.Context, p p2p.Peer, msg []byte, cb server.StreamRequestCallback, _ ...string) error { + for _, other := range cl.nodes { + if other.peerId() == p { + b := make([]byte, 0, 1024) + buf := bytes.NewBuffer(b) + other.hare.handleProposalsStream(ctx, msg, buf) + cb(ctx, buf) + } + } + return nil + }, + ).AnyTimes() + } +} + +func (cl *lockstepCluster) movePreround(layer types.LayerID) { + cl.timestamp = cl.t.start. + Add(cl.t.layerDuration * time.Duration(layer)). + Add(cl.t.cfg.PreroundDelay) + for _, n := range cl.nodes { + n.nclock.StartLayer(layer) + n.clock.Advance(cl.timestamp.Sub(n.clock.Now())) + } + for _, n := range cl.nodes { + n.waitEligibility() + } + for _, n := range cl.nodes { + n.waitSent() + } +} + +func (cl *lockstepCluster) moveRound() { + cl.timestamp = cl.timestamp.Add(cl.t.cfg.RoundDuration) + for _, n := range cl.nodes { + n.clock.Advance(cl.timestamp.Sub(n.clock.Now())) + } + for _, n := range cl.nodes { + n.waitEligibility() + } + for _, n := range cl.nodes { + n.waitSent() + } +} + +func (cl *lockstepCluster) waitStopped() { + for _, n := range cl.nodes { + n.tracer.waitStopped() + } +} + +// drainInteractiveMessages will make sure that the channels that signal +// that interactive messages came in on the tracer are read from. +func (cl *lockstepCluster) drainInteractiveMessages() { + done := make(chan struct{}) + cl.t.Cleanup(func() { close(done) }) + for _, n := range cl.nodes { + go func() { + for { + select { + case <-n.tracer.compactReq: + case <-n.tracer.compactResp: + case <-done: + } + } + }() + } +} + +func newTestTracer(tb testing.TB) *testTracer { + return &testTracer{ + TB: tb, + stopped: make(chan types.LayerID, 100), + eligibility: make(chan []*types.HareEligibility), + sent: make(chan *Message), + compactReq: make(chan struct{}), + compactResp: make(chan struct{}), + } +} + +type testTracer struct { + testing.TB + stopped chan types.LayerID + eligibility chan []*types.HareEligibility + sent chan *Message + compactReq chan struct{} + compactResp chan struct{} +} + +func waitForChan[T any](t testing.TB, ch <-chan T, timeout time.Duration, failureMsg string) T { + var value T + select { + case <-time.After(timeout): + builder := strings.Builder{} + pprof.Lookup("goroutine").WriteTo(&builder, 2) + t.Fatalf(failureMsg+", waited: %v, stacktraces:\n%s", timeout, builder.String()) + case value = <-ch: + } + return value +} + +func sendWithTimeout[T any](t testing.TB, value T, ch chan<- T, timeout time.Duration, failureMsg string) { + select { + case <-time.After(timeout): + builder := strings.Builder{} + pprof.Lookup("goroutine").WriteTo(&builder, 2) + t.Fatalf(failureMsg+", waited: %v, stacktraces:\n%s", timeout, builder.String()) + case ch <- value: + } +} + +func (t *testTracer) waitStopped() types.LayerID { + return waitForChan(t.TB, t.stopped, wait, "didn't stop") +} + +func (t *testTracer) waitEligibility() []*types.HareEligibility { + return waitForChan(t.TB, t.eligibility, wait, "no eligibility") +} + +func (t *testTracer) waitSent() *Message { + return waitForChan(t.TB, t.sent, wait, "no message") +} + +func (*testTracer) OnStart(types.LayerID) {} + +func (t *testTracer) OnStop(lid types.LayerID) { + select { + case t.stopped <- lid: + default: + } +} + +func (t *testTracer) OnActive(el []*types.HareEligibility) { + sendWithTimeout(t.TB, el, t.eligibility, wait, "eligibility can't be sent") +} + +func (t *testTracer) OnMessageSent(m *Message) { + sendWithTimeout(t.TB, m, t.sent, wait, "message can't be sent") +} + +func (*testTracer) OnMessageReceived(*Message) {} + +func (t *testTracer) OnCompactIdRequest(*CompactIdRequest) { + sendWithTimeout(t.TB, struct{}{}, t.compactReq, wait, "compact req can't be sent") +} + +func (t *testTracer) OnCompactIdResponse(*CompactIdResponse) { + sendWithTimeout(t.TB, struct{}{}, t.compactResp, wait, "compact resp can't be sent") +} + +func testHare(t *testing.T, active, inactive, equivocators int, opts ...clusterOpt) { + t.Helper() + cfg := DefaultConfig() + cfg.LogStats = true + tst := &tester{ + TB: t, + rng: rand.New(rand.NewSource(1001)), + start: time.Now(), + cfg: cfg, + layerDuration: 5 * time.Minute, + beacon: types.Beacon{1, 1, 1, 1}, + genesis: types.GetEffectiveGenesis(), + } + cluster := newLockstepCluster(tst, opts...). + addActive(active). + addInactive(inactive). + addEquivocators(equivocators) + if cluster.signersCount > 0 { + cluster = cluster.addSigner(cluster.signersCount) + cluster.partitionSigners() + } + cluster.drainInteractiveMessages() + + layer := tst.genesis + 1 + cluster.setup() + cluster.genProposals(layer) + cluster.movePreround(layer) + for i := 0; i < 2*int(notify); i++ { + cluster.moveRound() + } + var consistent []types.ProposalID + cluster.waitStopped() + for _, n := range cluster.nodes { + select { + case coin := <-n.hare.Coins(): + require.Equal(t, coin.Layer, layer) + default: + require.FailNow(t, "no coin") + } + select { + case rst := <-n.hare.Results(): + require.Equal(t, rst.Layer, layer) + require.NotEmpty(t, rst.Proposals) + if consistent == nil { + consistent = rst.Proposals + } else { + require.Equal(t, consistent, rst.Proposals) + } + default: + require.FailNow(t, "no result") + } + require.Empty(t, n.hare.Running()) + } +} + +func TestHare(t *testing.T) { + t.Run("one", func(t *testing.T) { testHare(t, 1, 0, 0) }) + t.Run("two", func(t *testing.T) { testHare(t, 2, 0, 0) }) + t.Run("small", func(t *testing.T) { testHare(t, 5, 0, 0) }) + t.Run("with proposals subsets", func(t *testing.T) { testHare(t, 5, 0, 0, withProposals(0.5)) }) + t.Run("with units", func(t *testing.T) { testHare(t, 5, 0, 0, withUnits(10, 50)) }) + t.Run("with inactive", func(t *testing.T) { testHare(t, 3, 2, 0) }) + t.Run("equivocators", func(t *testing.T) { testHare(t, 4, 0, 1, withProposals(0.75)) }) + t.Run("one active multi signers", func(t *testing.T) { testHare(t, 1, 0, 0, withSigners(2)) }) + t.Run("three active multi signers", func(t *testing.T) { testHare(t, 3, 0, 0, withSigners(10)) }) +} + +func TestIterationLimit(t *testing.T) { + t.Parallel() + tst := &tester{ + TB: t, + rng: rand.New(rand.NewSource(1001)), + start: time.Now(), + cfg: DefaultConfig(), + layerDuration: 5 * time.Minute, + beacon: types.Beacon{1, 1, 1, 1}, + genesis: types.GetEffectiveGenesis(), + } + tst.cfg.IterationsLimit = 3 + + layer := tst.genesis + 1 + cluster := newLockstepCluster(tst) + cluster.addActive(1) + cluster.nogossip() + cluster.movePreround(layer) + for i := 0; i < int(tst.cfg.IterationsLimit)*int(notify); i++ { + cluster.moveRound() + } + cluster.waitStopped() + require.Empty(t, cluster.nodes[0].hare.Running()) + require.False(t, cluster.nodes[0].patrol.IsHareInCharge(layer)) +} + +func TestConfigMarshal(t *testing.T) { + enc := zapcore.NewMapObjectEncoder() + cfg := &Config{} + require.NoError(t, cfg.MarshalLogObject(enc)) +} + +func TestHandler(t *testing.T) { + t.Parallel() + tst := &tester{ + TB: t, + rng: rand.New(rand.NewSource(1001)), + start: time.Now(), + cfg: DefaultConfig(), + layerDuration: 5 * time.Minute, + beacon: types.Beacon{1, 1, 1, 1}, + genesis: types.GetEffectiveGenesis(), + } + cluster := newLockstepCluster(tst) + cluster.addActive(1) + n := cluster.nodes[0] + require.NoError(t, beacons.Add(n.db, tst.genesis.GetEpoch()+1, tst.beacon)) + require.NoError(t, n.storeAtx(n.atx)) + n.oracle.UpdateActiveSet(tst.genesis.GetEpoch()+1, []types.ATXID{n.atx.ID()}) + n.mpublisher.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + layer := tst.genesis + 1 + n.nclock.StartLayer(layer) + n.clock.Advance((tst.start. + Add(tst.layerDuration * time.Duration(layer)). + Add(tst.cfg.PreroundDelay)).Sub(n.clock.Now())) + elig := n.tracer.waitEligibility()[0] + + n.tracer.waitSent() + n.tracer.waitEligibility() + + t.Run("malformed", func(t *testing.T) { + require.ErrorIs(t, n.hare.Handler(context.Background(), "", []byte("malformed")), + pubsub.ErrValidationReject) + require.ErrorContains(t, n.hare.Handler(context.Background(), "", []byte("malformed")), + "decoding") + }) + t.Run("invalidated", func(t *testing.T) { + msg := &Message{} + msg.Round = commit + require.ErrorIs(t, n.hare.Handler(context.Background(), "", codec.MustEncode(msg)), + pubsub.ErrValidationReject) + require.ErrorContains(t, n.hare.Handler(context.Background(), "", codec.MustEncode(msg)), + "validation reference") + }) + t.Run("unregistered", func(t *testing.T) { + msg := &Message{} + require.ErrorContains(t, n.hare.Handler(context.Background(), "", codec.MustEncode(msg)), + "is not registered") + }) + t.Run("invalid signature", func(t *testing.T) { + msg := &Message{} + msg.Body.IterRound.Round = propose + msg.Layer = layer + msg.Sender = n.signer.NodeID() + msg.Signature = n.signer.Sign(signing.HARE+1, msg.ToMetadata().ToBytes()) + require.ErrorIs(t, n.hare.Handler(context.Background(), "", codec.MustEncode(msg)), + pubsub.ErrValidationReject) + require.ErrorContains(t, n.hare.Handler(context.Background(), "", codec.MustEncode(msg)), + "invalid signature") + }) + t.Run("zero grade", func(t *testing.T) { + signer, err := signing.NewEdSigner() + require.NoError(t, err) + msg := &Message{} + msg.Body.IterRound.Round = propose + msg.Layer = layer + msg.Sender = signer.NodeID() + msg.Signature = signer.Sign(signing.HARE, msg.ToMetadata().ToBytes()) + require.ErrorContains(t, n.hare.Handler(context.Background(), "", codec.MustEncode(msg)), + "zero grade") + }) + t.Run("equivocation", func(t *testing.T) { + b := types.RandomBallot() + b.InnerBallot.Layer = layer + b.Layer = layer + p1 := &types.Proposal{ + InnerProposal: types.InnerProposal{ + Ballot: *b, + TxIDs: []types.TransactionID{types.RandomTransactionID(), types.RandomTransactionID()}, + }, + } + b2 := types.RandomBallot() + + b2.InnerBallot.Layer = layer + b.Layer = layer + p2 := &types.Proposal{ + InnerProposal: types.InnerProposal{ + Ballot: *b2, + TxIDs: []types.TransactionID{types.RandomTransactionID(), types.RandomTransactionID()}, + }, + } + + p1.Initialize() + p2.Initialize() + + if err := n.hare.OnProposal(p1); err != nil { + panic(err) + } + + if err := n.hare.OnProposal(p2); err != nil { + panic(err) + } + msg1 := &Message{} + msg1.Layer = layer + msg1.Value.Proposals = []types.ProposalID{p1.ID()} + msg1.Eligibility = *elig + msg1.Sender = n.signer.NodeID() + msg1.Signature = n.signer.Sign(signing.HARE, msg1.ToMetadata().ToBytes()) + msg1.Value.Proposals = nil + msg1.Value.CompactProposals = []types.CompactProposalID{ + compactVrf(p1.Ballot.EligibilityProofs[0].Sig), + } + + msg2 := &Message{} + msg2.Layer = layer + msg2.Value.Proposals = []types.ProposalID{p2.ID()} + msg2.Eligibility = *elig + msg2.Sender = n.signer.NodeID() + msg2.Signature = n.signer.Sign(signing.HARE, msg2.ToMetadata().ToBytes()) + msg2.Value.Proposals = nil + msg2.Value.CompactProposals = []types.CompactProposalID{ + compactVrf(p2.Ballot.EligibilityProofs[0].Sig), + } + + require.NoError(t, n.hare.Handler(context.Background(), "", codec.MustEncode(msg1))) + require.NoError(t, n.hare.Handler(context.Background(), "", codec.MustEncode(msg2))) + + malicious, err := identities.IsMalicious(n.db, n.signer.NodeID()) + require.NoError(t, err) + require.True(t, malicious) + + require.ErrorContains(t, + n.hare.Handler(context.Background(), "", codec.MustEncode(msg2)), + "dropped by graded", + ) + }) +} + +func gatx(id types.ATXID, epoch types.EpochID, smesher types.NodeID, base, height uint64) types.ActivationTx { + atx := &types.ActivationTx{ + NumUnits: 10, + PublishEpoch: epoch, + VRFNonce: 1, + BaseTickHeight: base, + TickCount: height - base, + SmesherID: smesher, + } + atx.SetID(id) + atx.SetReceived(time.Time{}.Add(1)) + return *atx +} + +func gproposal( + id types.ProposalID, + atxid types.ATXID, + smesher types.NodeID, + layer types.LayerID, + beacon types.Beacon, +) *types.Proposal { + proposal := types.Proposal{} + proposal.Layer = layer + proposal.EpochData = &types.EpochData{ + Beacon: beacon, + } + proposal.AtxID = atxid + proposal.SmesherID = smesher + proposal.Ballot.SmesherID = smesher + proposal.SetID(id) + proposal.Ballot.SetID(types.BallotID(id)) + proposal.SetBeacon(beacon) + return &proposal +} + +func TestProposals(t *testing.T) { + atxids := [3]types.ATXID{} + pids := [3]types.ProposalID{} + ids := [3]types.NodeID{} + for i := range atxids { + atxids[i][0] = byte(i) + 1 + pids[i][0] = byte(i) + 1 + ids[i][0] = byte(i) + 1 + } + publish := types.EpochID(1) + layer := (publish + 1).FirstLayer() + goodBeacon := types.Beacon{1} + badBeacon := types.Beacon{2} + + signer, err := signing.NewEdSigner() + require.NoError(t, err) + for _, tc := range []struct { + desc string + atxs []types.ActivationTx + proposals []*types.Proposal + malicious []types.NodeID + layer types.LayerID + beacon types.Beacon + expect []types.ProposalID + }{ + { + desc: "sanity", + layer: layer, + beacon: goodBeacon, + atxs: []types.ActivationTx{ + gatx(atxids[0], publish, ids[0], 10, 100), + gatx(atxids[1], publish, ids[1], 10, 100), + gatx(atxids[2], publish, signer.NodeID(), 10, 100), + }, + proposals: []*types.Proposal{ + gproposal(pids[0], atxids[0], ids[0], layer, goodBeacon), + gproposal(pids[1], atxids[1], ids[1], layer, goodBeacon), + }, + expect: []types.ProposalID{pids[0], pids[1]}, + }, + { + desc: "mismatched beacon", + layer: layer, + beacon: goodBeacon, + atxs: []types.ActivationTx{ + gatx(atxids[0], publish, ids[0], 10, 100), + gatx(atxids[1], publish, ids[1], 10, 100), + gatx(atxids[2], publish, signer.NodeID(), 10, 100), + }, + proposals: []*types.Proposal{ + gproposal(pids[0], atxids[0], ids[0], layer, goodBeacon), + gproposal(pids[1], atxids[1], ids[1], layer, badBeacon), + }, + expect: []types.ProposalID{pids[0]}, + }, + { + desc: "multiproposals", + layer: layer, + beacon: goodBeacon, + atxs: []types.ActivationTx{ + gatx(atxids[0], publish, ids[0], 10, 100), + gatx(atxids[1], publish, ids[1], 10, 100), + gatx(atxids[2], publish, signer.NodeID(), 10, 100), + }, + proposals: []*types.Proposal{ + gproposal(pids[0], atxids[0], ids[0], layer, goodBeacon), + gproposal(pids[1], atxids[1], ids[1], layer, goodBeacon), + gproposal(pids[2], atxids[1], ids[1], layer, goodBeacon), + }, + expect: []types.ProposalID{pids[0]}, + }, + { + desc: "future proposal", + layer: layer, + beacon: goodBeacon, + atxs: []types.ActivationTx{ + gatx(atxids[0], publish, ids[0], 101, 1000), + gatx(atxids[1], publish, signer.NodeID(), 10, 100), + }, + proposals: []*types.Proposal{ + gproposal(pids[0], atxids[0], ids[0], layer, goodBeacon), + gproposal(pids[1], atxids[1], ids[1], layer, goodBeacon), + }, + expect: []types.ProposalID{pids[1]}, + }, + { + desc: "malicious", + layer: layer, + beacon: goodBeacon, + atxs: []types.ActivationTx{ + gatx(atxids[0], publish, ids[0], 10, 100), + gatx(atxids[1], publish, ids[1], 10, 100), + gatx(atxids[2], publish, signer.NodeID(), 10, 100), + }, + proposals: []*types.Proposal{ + gproposal(pids[0], atxids[0], ids[0], layer, goodBeacon), + gproposal(pids[1], atxids[1], ids[1], layer, goodBeacon), + }, + malicious: []types.NodeID{ids[0]}, + expect: []types.ProposalID{pids[1]}, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + db := statesql.InMemory() + atxsdata := atxsdata.New() + proposals := store.New() + hare := New( + nil, + nil, + db, + atxsdata, + proposals, + signing.NewEdVerifier(), + nil, + nil, + layerpatrol.New(), + nil, + WithLogger(zaptest.NewLogger(t)), + ) + for _, atx := range tc.atxs { + require.NoError(t, atxs.Add(db, &atx, types.AtxBlob{})) + atxsdata.AddFromAtx(&atx, false) + } + for _, proposal := range tc.proposals { + if err := proposals.Add(proposal); err != nil { + panic(err) + } + } + for _, id := range tc.malicious { + require.NoError(t, identities.SetMalicious(db, id, []byte("non empty"), time.Time{})) + atxsdata.SetMalicious(id) + } + require.ElementsMatch(t, tc.expect, hare.selectProposals(&session{ + lid: tc.layer, + beacon: tc.beacon, + signers: []*signing.EdSigner{signer}, + })) + }) + } +} + +func TestHare_AddProposal(t *testing.T) { + t.Parallel() + proposals := store.New() + hare := New(nil, nil, nil, nil, proposals, nil, nil, nil, nil, nil) + + p := gproposal( + types.RandomProposalID(), + types.RandomATXID(), + types.RandomNodeID(), + types.LayerID(0), + types.RandomBeacon(), + ) + require.False(t, hare.IsKnown(p.Layer, p.ID())) + require.NoError(t, hare.OnProposal(p)) + require.True(t, proposals.Has(p.ID())) + + require.True(t, hare.IsKnown(p.Layer, p.ID())) + require.ErrorIs(t, hare.OnProposal(p), store.ErrProposalExists) +} + +func TestHareConfig_CommitteeUpgrade(t *testing.T) { + t.Parallel() + t.Run("no upgrade", func(t *testing.T) { + cfg := Config{ + Committee: 400, + } + require.Equal(t, cfg.Committee, cfg.CommitteeFor(0)) + require.Equal(t, cfg.Committee, cfg.CommitteeFor(100)) + }) + t.Run("upgrade", func(t *testing.T) { + cfg := Config{ + Committee: 400, + CommitteeUpgrade: &CommitteeUpgrade{ + Layer: 16, + Size: 50, + }, + } + require.EqualValues(t, cfg.Committee, cfg.CommitteeFor(0)) + require.EqualValues(t, cfg.Committee, cfg.CommitteeFor(15)) + require.EqualValues(t, 50, cfg.CommitteeFor(16)) + require.EqualValues(t, 50, cfg.CommitteeFor(100)) + }) +} + +// TestHare_ReconstructForward tests that a message +// could be reconstructed on a downstream peer that +// receives a gossipsub message from a forwarding node +// without needing a direct connection to the original sender. +func TestHare_ReconstructForward(t *testing.T) { + cfg := DefaultConfig() + cfg.LogStats = true + tst := &tester{ + TB: t, + rng: rand.New(rand.NewSource(1001)), + start: time.Now(), + cfg: cfg, + layerDuration: 5 * time.Minute, + beacon: types.Beacon{1, 1, 1, 1}, + genesis: types.GetEffectiveGenesis(), + } + cluster := newLockstepCluster(tst). + addActive(3) + if cluster.signersCount > 0 { + cluster = cluster.addSigner(cluster.signersCount) + cluster.partitionSigners() + } + cluster.drainInteractiveMessages() + layer := tst.genesis + 1 + + // cluster setup + active := cluster.activeSet() + for i, n := range cluster.nodes { + require.NoError(cluster.t, beacons.Add(n.db, cluster.t.genesis.GetEpoch()+1, cluster.t.beacon)) + for _, other := range append(cluster.nodes, cluster.signers...) { + if other.atx == nil { + continue + } + require.NoError(cluster.t, n.storeAtx(other.atx)) + } + n.oracle.UpdateActiveSet(cluster.t.genesis.GetEpoch()+1, active) + n.mpublisher.EXPECT(). + Publish(gomock.Any(), gomock.Any(), gomock.Any()). + Do(func(ctx context.Context, proto string, msg []byte) error { + // here we want to call the handler on the second node + // but then call the handler on the third with the incoming peer id + // of the second node, this way we know the peers could resolve between + // themselves without having the connection to the original sender + // 1st publish call is for the preround, so we will hijack that and + // leave the rest to broadcast + m := &Message{} + codec.MustDecode(msg, m) + if m.Body.IterRound.Round == preround { + other := [2]int{0, 0} + switch i { + case 0: + other[0] = 1 + other[1] = 2 + case 1: + other[0] = 0 + other[1] = 2 + case 2: + other[0] = 1 + other[1] = 0 + default: + panic("bad") + } + if err := cluster.nodes[other[0]].hare. + Handler(ctx, cluster.nodes[i].peerId(), msg); err != nil { + panic(err) + } + if err := cluster.nodes[other[1]].hare. + Handler(ctx, cluster.nodes[other[0]].peerId(), msg); err != nil { + panic(err) + } + return nil + } + + for _, other := range cluster.nodes { + if err := other.hare.Handler(ctx, n.peerId(), msg); err != nil { + panic(err) + } + } + return nil + }). + AnyTimes() + n.mockStreamRequester.EXPECT().StreamRequest(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( + func(ctx context.Context, p p2p.Peer, msg []byte, cb server.StreamRequestCallback, _ ...string) error { + for _, other := range cluster.nodes { + if other.peerId() == p { + b := make([]byte, 0, 1024) + buf := bytes.NewBuffer(b) + if err := other.hare.handleProposalsStream(ctx, msg, buf); err != nil { + return fmt.Errorf("exec handleProposalStream: %w", err) + } + if err := cb(ctx, buf); err != nil { + return fmt.Errorf("exec callback: %w", err) + } + } + } + return nil + }, + ).AnyTimes() + } + + cluster.genProposals(layer, 2) + cluster.genProposalNode(layer, 2) + cluster.movePreround(layer) + for i := 0; i < 2*int(notify); i++ { + cluster.moveRound() + } + var consistent []types.ProposalID + cluster.waitStopped() + for _, n := range cluster.nodes { + select { + case coin := <-n.hare.Coins(): + require.Equal(t, coin.Layer, layer) + default: + require.FailNow(t, "no coin") + } + select { + case rst := <-n.hare.Results(): + require.Equal(t, rst.Layer, layer) + require.NotEmpty(t, rst.Proposals) + if consistent == nil { + consistent = rst.Proposals + } else { + require.Equal(t, consistent, rst.Proposals) + } + default: + require.FailNow(t, "no result") + } + require.Empty(t, n.hare.Running()) + } +} + +// TestHare_ReconstructAll tests that the nodes go into a +// full message exchange in the case that a signature fails +// although all compact hashes and proposals match. +func TestHare_ReconstructAll(t *testing.T) { + cfg := DefaultConfig() + cfg.LogStats = true + tst := &tester{ + TB: t, + rng: rand.New(rand.NewSource(1001)), + start: time.Now(), + cfg: cfg, + layerDuration: 5 * time.Minute, + beacon: types.Beacon{1, 1, 1, 1}, + genesis: types.GetEffectiveGenesis(), + } + cluster := newLockstepCluster(tst, withMockVerifier()). + addActive(3) + if cluster.signersCount > 0 { + cluster = cluster.addSigner(cluster.signersCount) + cluster.partitionSigners() + } + layer := tst.genesis + 1 + + cluster.drainInteractiveMessages() + // cluster setup + for _, n := range cluster.nodes { + gomock.InOrder( + n.mverifier.EXPECT(). + Verify(signing.PROPOSAL, gomock.Any(), gomock.Any(), gomock.Any()). + Return(false). + MaxTimes(1), + n.mverifier.EXPECT(). + Verify(signing.PROPOSAL, gomock.Any(), gomock.Any(), gomock.Any()). + Return(true). + AnyTimes(), + ) + } + cluster.setup() + cluster.genProposals(layer) + cluster.movePreround(layer) + for i := 0; i < 2*int(notify); i++ { + cluster.moveRound() + } + var consistent []types.ProposalID + cluster.waitStopped() + for _, n := range cluster.nodes { + select { + case coin := <-n.hare.Coins(): + require.Equal(t, coin.Layer, layer) + default: + require.FailNow(t, "no coin") + } + select { + case rst := <-n.hare.Results(): + require.Equal(t, rst.Layer, layer) + require.NotEmpty(t, rst.Proposals) + if consistent == nil { + consistent = rst.Proposals + } else { + require.Equal(t, consistent, rst.Proposals) + } + default: + t.Fatal("no result") + } + require.Empty(t, n.hare.Running()) + } +} + +// TestHare_ReconstructCollision tests that the nodes go into a +// full message exchange in the case that there's a compacted id collision. +func TestHare_ReconstructCollision(t *testing.T) { + cfg := DefaultConfig() + cfg.LogStats = true + tst := &tester{ + TB: t, + rng: rand.New(rand.NewSource(1000)), + start: time.Now(), + cfg: cfg, + layerDuration: 5 * time.Minute, + beacon: types.Beacon{1, 1, 1, 1}, + genesis: types.GetEffectiveGenesis(), + } + + cluster := newLockstepCluster(tst, withProposals(1), withCollidingProposals()). + addActive(2) + if cluster.signersCount > 0 { + cluster = cluster.addSigner(cluster.signersCount) + cluster.partitionSigners() + } + layer := tst.genesis + 1 + + // scenario: + // node 1 has generated 1 proposal that hash into 0x00 as prefix - both nodes know the proposal + // node 2 has generated 1 proposal that hash into 0x00 (but node 1 doesn't know about it) + // so the two proposals collide and then we check that the nodes actually go into a round of + // exchanging the missing/colliding hashes and then the signature verification (not mocked) + // should pass and that a full exchange of all hashes is not triggered (disambiguates case of + // failed signature vs. hashes colliding - there's a difference in number of single prefixes + // that are sent, but the response should be the same) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + <-cluster.nodes[1].tracer.compactReq + wg.Done() + }() // node 2 gets the request + go func() { + <-cluster.nodes[0].tracer.compactResp + wg.Done() + }() // node 1 gets the response + + cluster.setup() + + cluster.genProposals(layer, 1) + cluster.genProposalNode(layer, 1) + cluster.movePreround(layer) + for i := 0; i < 2*int(notify); i++ { + cluster.moveRound() + } + var consistent []types.ProposalID + cluster.waitStopped() + for _, n := range cluster.nodes { + select { + case coin := <-n.hare.Coins(): + require.Equal(t, coin.Layer, layer) + default: + require.FailNow(t, "no coin") + } + select { + case rst := <-n.hare.Results(): + require.Equal(t, rst.Layer, layer) + require.NotEmpty(t, rst.Proposals) + if consistent == nil { + consistent = rst.Proposals + } else { + require.Equal(t, consistent, rst.Proposals) + } + default: + t.Fatal("no result") + } + wg.Wait() + require.Empty(t, n.hare.Running()) + } +} + +func compactVrf(v types.VrfSignature) (c types.CompactProposalID) { + return types.CompactProposalID(v[:]) +} diff --git a/hare4/interface.go b/hare4/interface.go new file mode 100644 index 0000000000..1401065dfa --- /dev/null +++ b/hare4/interface.go @@ -0,0 +1,20 @@ +package hare4 + +import ( + "context" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/p2p/server" + "github.com/spacemeshos/go-spacemesh/signing" +) + +//go:generate mockgen -typed -package=mocks -destination=./mocks/mocks.go -source=./interface.go + +type streamRequester interface { + StreamRequest(context.Context, p2p.Peer, []byte, server.StreamRequestCallback, ...string) error +} + +type verifier interface { + Verify(signing.Domain, types.NodeID, []byte, types.EdSignature) bool +} diff --git a/hare4/legacy_oracle.go b/hare4/legacy_oracle.go new file mode 100644 index 0000000000..2c8cc026b8 --- /dev/null +++ b/hare4/legacy_oracle.go @@ -0,0 +1,70 @@ +package hare4 + +import ( + "context" + "errors" + + "go.uber.org/zap" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/hare3/eligibility" + "github.com/spacemeshos/go-spacemesh/signing" +) + +type oracle interface { + Validate(context.Context, types.LayerID, uint32, int, types.NodeID, types.VrfSignature, uint16) (bool, error) + CalcEligibility(context.Context, types.LayerID, uint32, int, types.NodeID, types.VrfSignature) (uint16, error) +} + +type legacyOracle struct { + log *zap.Logger + oracle oracle + config Config +} + +func (lg *legacyOracle) validate(msg *Message) grade { + if msg.Eligibility.Count == 0 { + return grade0 + } + committee := int(lg.config.CommitteeFor(msg.Layer)) + if msg.Round == propose { + committee = int(lg.config.Leaders) + } + valid, err := lg.oracle.Validate(context.Background(), + msg.Layer, msg.Absolute(), committee, msg.Sender, + msg.Eligibility.Proof, msg.Eligibility.Count) + if err != nil { + lg.log.Warn("failed proof validation", zap.Error(err)) + return grade0 + } + if !valid { + return grade0 + } + return grade5 +} + +func (lg *legacyOracle) active( + signer *signing.EdSigner, + beacon types.Beacon, + layer types.LayerID, + ir IterRound, +) *types.HareEligibility { + vrf := eligibility.GenVRF(context.Background(), signer.VRFSigner(), beacon, layer, ir.Absolute()) + committee := int(lg.config.CommitteeFor(layer)) + if ir.Round == propose { + committee = int(lg.config.Leaders) + } + count, err := lg.oracle.CalcEligibility(context.Background(), layer, ir.Absolute(), committee, signer.NodeID(), vrf) + if err != nil { + if !errors.Is(err, eligibility.ErrNotActive) { + lg.log.Error("failed to compute eligibilities", zap.Error(err)) + } else { + lg.log.Debug("identity is not active") + } + return nil + } + if count == 0 { + return nil + } + return &types.HareEligibility{Proof: vrf, Count: count} +} diff --git a/hare4/malfeasance.go b/hare4/malfeasance.go new file mode 100644 index 0000000000..8e2feb9f38 --- /dev/null +++ b/hare4/malfeasance.go @@ -0,0 +1,96 @@ +package hare4 + +import ( + "context" + "errors" + "fmt" + + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/zap" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/log" + "github.com/spacemeshos/go-spacemesh/malfeasance/wire" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" +) + +const ( + hareEquivocate = "hare_eq" +) + +type MalfeasanceHandler struct { + logger *zap.Logger + db sql.Executor + + edVerifier *signing.EdVerifier +} + +type MalfeasanceOpt func(*MalfeasanceHandler) + +func WithMalfeasanceLogger(logger *zap.Logger) MalfeasanceOpt { + return func(mh *MalfeasanceHandler) { + mh.logger = logger + } +} + +func NewMalfeasanceHandler( + db sql.Executor, + edVerifier *signing.EdVerifier, + opt ...MalfeasanceOpt, +) *MalfeasanceHandler { + mh := &MalfeasanceHandler{ + logger: zap.NewNop(), + db: db, + + edVerifier: edVerifier, + } + for _, o := range opt { + o(mh) + } + return mh +} + +func (mh *MalfeasanceHandler) Validate(ctx context.Context, data wire.ProofData) (types.NodeID, error) { + hp, ok := data.(*wire.HareProof) + if !ok { + return types.EmptyNodeID, errors.New("wrong message type for hare equivocation") + } + for _, msg := range hp.Messages { + if !mh.edVerifier.Verify(signing.HARE, msg.SmesherID, msg.SignedBytes(), msg.Signature) { + return types.EmptyNodeID, errors.New("invalid signature") + } + } + msg1, msg2 := hp.Messages[0], hp.Messages[1] + ok, err := atxs.IdentityExists(mh.db, msg1.SmesherID) + if err != nil { + return types.EmptyNodeID, fmt.Errorf("check identity in hare malfeasance %v: %w", msg1.SmesherID, err) + } + if !ok { + return types.EmptyNodeID, fmt.Errorf("identity does not exist: %v", msg1.SmesherID) + } + + if msg1.SmesherID == msg2.SmesherID && + msg1.InnerMsg.Layer == msg2.InnerMsg.Layer && + msg1.InnerMsg.Round == msg2.InnerMsg.Round && + msg1.InnerMsg.MsgHash != msg2.InnerMsg.MsgHash { + return msg1.SmesherID, nil + } + mh.logger.Debug("received invalid hare malfeasance proof", + log.ZContext(ctx), + zap.Stringer("first_smesher", hp.Messages[0].SmesherID), + zap.Object("first_proof", &hp.Messages[0].InnerMsg), + zap.Stringer("second_smesher", hp.Messages[1].SmesherID), + zap.Object("second_proof", &hp.Messages[1].InnerMsg), + ) + return types.EmptyNodeID, errors.New("invalid hare malfeasance proof") +} + +func (mh *MalfeasanceHandler) ReportProof(numProofs *prometheus.CounterVec) { + numProofs.WithLabelValues(hareEquivocate).Inc() +} + +func (mh *MalfeasanceHandler) ReportInvalidProof(numInvalidProofs *prometheus.CounterVec) { + numInvalidProofs.WithLabelValues(hareEquivocate).Inc() +} diff --git a/hare4/malfeasance_test.go b/hare4/malfeasance_test.go new file mode 100644 index 0000000000..6611e36ea5 --- /dev/null +++ b/hare4/malfeasance_test.go @@ -0,0 +1,311 @@ +package hare4 + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest" + "go.uber.org/zap/zaptest/observer" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/malfeasance/wire" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/sql/statesql" +) + +type testMalfeasanceHandler struct { + *MalfeasanceHandler + + observedLogs *observer.ObservedLogs + db sql.StateDatabase +} + +func newTestMalfeasanceHandler(tb testing.TB) *testMalfeasanceHandler { + db := statesql.InMemory() + observer, observedLogs := observer.New(zapcore.WarnLevel) + logger := zaptest.NewLogger(tb, zaptest.WrapOptions(zap.WrapCore( + func(core zapcore.Core) zapcore.Core { + return zapcore.NewTee(core, observer) + }, + ))) + + h := NewMalfeasanceHandler(db, signing.NewEdVerifier(), WithMalfeasanceLogger(logger)) + + return &testMalfeasanceHandler{ + MalfeasanceHandler: h, + + observedLogs: observedLogs, + db: db, + } +} + +func createIdentity(tb testing.TB, db sql.Executor, sig *signing.EdSigner) { + tb.Helper() + atx := &types.ActivationTx{ + PublishEpoch: types.EpochID(1), + NumUnits: 1, + SmesherID: sig.NodeID(), + } + atx.SetReceived(time.Now()) + atx.SetID(types.RandomATXID()) + atx.TickCount = 1 + require.NoError(tb, atxs.Add(db, atx, types.AtxBlob{})) +} + +func TestHandler_Validate(t *testing.T) { + t.Run("unknown identity", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + // identity is unknown to handler + + hp := wire.HareProof{ + Messages: [2]wire.HareProofMsg{ + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(11), + Round: 3, + MsgHash: types.RandomHash(), + }, + }, + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(11), + Round: 3, + MsgHash: types.RandomHash(), + }, + }, + }, + } + hp.Messages[0].Signature = sig.Sign(signing.HARE, hp.Messages[0].SignedBytes()) + hp.Messages[0].SmesherID = sig.NodeID() + hp.Messages[1].Signature = sig.Sign(signing.HARE, hp.Messages[1].SignedBytes()) + hp.Messages[1].SmesherID = sig.NodeID() + + nodeID, err := h.Validate(context.Background(), &hp) + require.ErrorContains(t, err, "identity does not exist") + require.Equal(t, types.EmptyNodeID, nodeID) + }) + + t.Run("invalid signature", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + createIdentity(t, h.db, sig) + + hp := wire.HareProof{ + Messages: [2]wire.HareProofMsg{ + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(11), + Round: 3, + MsgHash: types.RandomHash(), + }, + }, + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(11), + Round: 3, + MsgHash: types.RandomHash(), + }, + }, + }, + } + hp.Messages[0].Signature = sig.Sign(signing.HARE, hp.Messages[0].SignedBytes()) + hp.Messages[0].SmesherID = sig.NodeID() + hp.Messages[1].Signature = types.RandomEdSignature() + hp.Messages[1].SmesherID = sig.NodeID() + + nodeID, err := h.Validate(context.Background(), &hp) + require.ErrorContains(t, err, "invalid signature") + require.Equal(t, types.EmptyNodeID, nodeID) + }) + + t.Run("same msg hash", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + createIdentity(t, h.db, sig) + + msgHash := types.RandomHash() + hp := wire.HareProof{ + Messages: [2]wire.HareProofMsg{ + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(11), + Round: 3, + MsgHash: msgHash, + }, + }, + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(11), + Round: 3, + MsgHash: msgHash, + }, + }, + }, + } + hp.Messages[0].Signature = sig.Sign(signing.HARE, hp.Messages[0].SignedBytes()) + hp.Messages[0].SmesherID = sig.NodeID() + hp.Messages[1].Signature = sig.Sign(signing.HARE, hp.Messages[1].SignedBytes()) + hp.Messages[1].SmesherID = sig.NodeID() + + nodeID, err := h.Validate(context.Background(), &hp) + require.ErrorContains(t, err, "invalid hare malfeasance proof") + require.Equal(t, types.EmptyNodeID, nodeID) + }) + + t.Run("different layer", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + createIdentity(t, h.db, sig) + + hp := wire.HareProof{ + Messages: [2]wire.HareProofMsg{ + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(11), + Round: 3, + MsgHash: types.RandomHash(), + }, + }, + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(10), + Round: 3, + MsgHash: types.RandomHash(), + }, + }, + }, + } + hp.Messages[0].Signature = sig.Sign(signing.HARE, hp.Messages[0].SignedBytes()) + hp.Messages[0].SmesherID = sig.NodeID() + hp.Messages[1].Signature = sig.Sign(signing.HARE, hp.Messages[1].SignedBytes()) + hp.Messages[1].SmesherID = sig.NodeID() + + nodeID, err := h.Validate(context.Background(), &hp) + require.ErrorContains(t, err, "invalid hare malfeasance proof") + require.Equal(t, types.EmptyNodeID, nodeID) + }) + + t.Run("different round", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + createIdentity(t, h.db, sig) + + hp := wire.HareProof{ + Messages: [2]wire.HareProofMsg{ + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(11), + Round: 3, + MsgHash: types.RandomHash(), + }, + }, + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(10), + Round: 4, + MsgHash: types.RandomHash(), + }, + }, + }, + } + hp.Messages[0].Signature = sig.Sign(signing.HARE, hp.Messages[0].SignedBytes()) + hp.Messages[0].SmesherID = sig.NodeID() + hp.Messages[1].Signature = sig.Sign(signing.HARE, hp.Messages[1].SignedBytes()) + hp.Messages[1].SmesherID = sig.NodeID() + + nodeID, err := h.Validate(context.Background(), &hp) + require.ErrorContains(t, err, "invalid hare malfeasance proof") + require.Equal(t, types.EmptyNodeID, nodeID) + }) + + t.Run("different signer", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + createIdentity(t, h.db, sig) + + sig2, err := signing.NewEdSigner() + require.NoError(t, err) + createIdentity(t, h.db, sig2) + + hp := wire.HareProof{ + Messages: [2]wire.HareProofMsg{ + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(11), + Round: 3, + MsgHash: types.RandomHash(), + }, + }, + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(10), + Round: 4, + MsgHash: types.RandomHash(), + }, + }, + }, + } + hp.Messages[0].Signature = sig.Sign(signing.HARE, hp.Messages[0].SignedBytes()) + hp.Messages[0].SmesherID = sig.NodeID() + hp.Messages[1].Signature = sig2.Sign(signing.HARE, hp.Messages[1].SignedBytes()) + hp.Messages[1].SmesherID = sig2.NodeID() + + nodeID, err := h.Validate(context.Background(), &hp) + require.ErrorContains(t, err, "invalid hare malfeasance proof") + require.Equal(t, types.EmptyNodeID, nodeID) + }) + + t.Run("valid", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + createIdentity(t, h.db, sig) + + hp := wire.HareProof{ + Messages: [2]wire.HareProofMsg{ + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(11), + Round: 3, + MsgHash: types.RandomHash(), + }, + }, + { + InnerMsg: wire.HareMetadata{ + Layer: types.LayerID(11), + Round: 3, + MsgHash: types.RandomHash(), + }, + }, + }, + } + hp.Messages[0].Signature = sig.Sign(signing.HARE, hp.Messages[0].SignedBytes()) + hp.Messages[0].SmesherID = sig.NodeID() + hp.Messages[1].Signature = sig.Sign(signing.HARE, hp.Messages[1].SignedBytes()) + hp.Messages[1].SmesherID = sig.NodeID() + + nodeID, err := h.Validate(context.Background(), &hp) + require.NoError(t, err) + require.Equal(t, sig.NodeID(), nodeID) + }) +} diff --git a/hare4/metrics.go b/hare4/metrics.go new file mode 100644 index 0000000000..14f496b938 --- /dev/null +++ b/hare4/metrics.go @@ -0,0 +1,96 @@ +package hare4 + +import ( + "github.com/prometheus/client_golang/prometheus" + + "github.com/spacemeshos/go-spacemesh/metrics" +) + +const namespace = "hare4" // todo change this back to `hare` + +var ( + processCounter = metrics.NewCounter( + "session", + namespace, + "number of hare sessions at different stages", + []string{"stage"}, + ) + sessionStart = processCounter.WithLabelValues("started") + sessionTerminated = processCounter.WithLabelValues("terminated") + sessionCoin = processCounter.WithLabelValues("weakcoin") + sessionResult = processCounter.WithLabelValues("result") + + exitErrors = metrics.NewCounter( + "exit_errors", + namespace, + "number of unexpected exit errors. should remain at zero", + []string{}, + ).WithLabelValues() + validationError = metrics.NewCounter( + "validation_errors", + namespace, + "number of validation errors. not expected to be at zero", + []string{"error"}, + ) + notRegisteredError = validationError.WithLabelValues("not_registered") + malformedError = validationError.WithLabelValues("malformed") + signatureError = validationError.WithLabelValues("signature") + oracleError = validationError.WithLabelValues("oracle") + + droppedMessages = metrics.NewCounter( + "dropped_msgs", + namespace, + "number of messages dropped by gossip", + []string{}, + ).WithLabelValues() + + validationLatency = metrics.NewHistogramWithBuckets( + "validation_seconds", + namespace, + "validation time in seconds", + []string{"step"}, + prometheus.ExponentialBuckets(0.01, 2, 10), + ) + oracleLatency = validationLatency.WithLabelValues("oracle") + submitLatency = validationLatency.WithLabelValues("submit") + + protocolLatency = metrics.NewHistogramWithBuckets( + "protocol_seconds", + namespace, + "protocol time in seconds", + []string{"step"}, + prometheus.ExponentialBuckets(0.01, 2, 10), + ) + proposalsLatency = protocolLatency.WithLabelValues("proposals") + activeLatency = protocolLatency.WithLabelValues("active") + requestCompactCounter = prometheus.NewCounter(metrics.NewCounterOpts( + namespace, + "request_compact_count", + "number of times we needed to go into a clarifying round", + )) + requestCompactErrorCounter = prometheus.NewCounter(metrics.NewCounterOpts( + namespace, + "request_compact_error_count", + "number of errors got when requesting compact proposals from peer", + )) + requestCompactHandlerCounter = prometheus.NewCounter(metrics.NewCounterOpts( + namespace, + "request_compact_handler_count", + "number of requests handled on the compact stream handler", + )) + messageCacheMiss = prometheus.NewCounter(metrics.NewCounterOpts( + namespace, + "message_cache_miss", + "number of message cache misses", + )) + messageCompactsCounter = prometheus.NewCounter(metrics.NewCounterOpts( + namespace, + "message_compacts_count", + "number of compact proposals that arrived to be checked in a message", + )) + preroundSigFailCounter = prometheus.NewCounter(metrics.NewCounterOpts( + namespace, + "preround_signature_fail_count", + "counter for signature fails on preround with compact message", + )) +) diff --git a/hare4/mocks/mocks.go b/hare4/mocks/mocks.go new file mode 100644 index 0000000000..f3e813e77b --- /dev/null +++ b/hare4/mocks/mocks.go @@ -0,0 +1,148 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./interface.go +// +// Generated by this command: +// +// mockgen -typed -package=mocks -destination=./mocks/mocks.go -source=./interface.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + types "github.com/spacemeshos/go-spacemesh/common/types" + p2p "github.com/spacemeshos/go-spacemesh/p2p" + server "github.com/spacemeshos/go-spacemesh/p2p/server" + signing "github.com/spacemeshos/go-spacemesh/signing" + gomock "go.uber.org/mock/gomock" +) + +// MockstreamRequester is a mock of streamRequester interface. +type MockstreamRequester struct { + ctrl *gomock.Controller + recorder *MockstreamRequesterMockRecorder +} + +// MockstreamRequesterMockRecorder is the mock recorder for MockstreamRequester. +type MockstreamRequesterMockRecorder struct { + mock *MockstreamRequester +} + +// NewMockstreamRequester creates a new mock instance. +func NewMockstreamRequester(ctrl *gomock.Controller) *MockstreamRequester { + mock := &MockstreamRequester{ctrl: ctrl} + mock.recorder = &MockstreamRequesterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockstreamRequester) EXPECT() *MockstreamRequesterMockRecorder { + return m.recorder +} + +// StreamRequest mocks base method. +func (m *MockstreamRequester) StreamRequest(arg0 context.Context, arg1 p2p.Peer, arg2 []byte, arg3 server.StreamRequestCallback, arg4 ...string) error { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1, arg2, arg3} + for _, a := range arg4 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "StreamRequest", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// StreamRequest indicates an expected call of StreamRequest. +func (mr *MockstreamRequesterMockRecorder) StreamRequest(arg0, arg1, arg2, arg3 any, arg4 ...any) *MockstreamRequesterStreamRequestCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1, arg2, arg3}, arg4...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StreamRequest", reflect.TypeOf((*MockstreamRequester)(nil).StreamRequest), varargs...) + return &MockstreamRequesterStreamRequestCall{Call: call} +} + +// MockstreamRequesterStreamRequestCall wrap *gomock.Call +type MockstreamRequesterStreamRequestCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockstreamRequesterStreamRequestCall) Return(arg0 error) *MockstreamRequesterStreamRequestCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockstreamRequesterStreamRequestCall) Do(f func(context.Context, p2p.Peer, []byte, server.StreamRequestCallback, ...string) error) *MockstreamRequesterStreamRequestCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockstreamRequesterStreamRequestCall) DoAndReturn(f func(context.Context, p2p.Peer, []byte, server.StreamRequestCallback, ...string) error) *MockstreamRequesterStreamRequestCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Mockverifier is a mock of verifier interface. +type Mockverifier struct { + ctrl *gomock.Controller + recorder *MockverifierMockRecorder +} + +// MockverifierMockRecorder is the mock recorder for Mockverifier. +type MockverifierMockRecorder struct { + mock *Mockverifier +} + +// NewMockverifier creates a new mock instance. +func NewMockverifier(ctrl *gomock.Controller) *Mockverifier { + mock := &Mockverifier{ctrl: ctrl} + mock.recorder = &MockverifierMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Mockverifier) EXPECT() *MockverifierMockRecorder { + return m.recorder +} + +// Verify mocks base method. +func (m *Mockverifier) Verify(arg0 signing.Domain, arg1 types.NodeID, arg2 []byte, arg3 types.EdSignature) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Verify indicates an expected call of Verify. +func (mr *MockverifierMockRecorder) Verify(arg0, arg1, arg2, arg3 any) *MockverifierVerifyCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*Mockverifier)(nil).Verify), arg0, arg1, arg2, arg3) + return &MockverifierVerifyCall{Call: call} +} + +// MockverifierVerifyCall wrap *gomock.Call +type MockverifierVerifyCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockverifierVerifyCall) Return(arg0 bool) *MockverifierVerifyCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockverifierVerifyCall) Do(f func(signing.Domain, types.NodeID, []byte, types.EdSignature) bool) *MockverifierVerifyCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockverifierVerifyCall) DoAndReturn(f func(signing.Domain, types.NodeID, []byte, types.EdSignature) bool) *MockverifierVerifyCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/hare4/protocol.go b/hare4/protocol.go new file mode 100644 index 0000000000..0046a49534 --- /dev/null +++ b/hare4/protocol.go @@ -0,0 +1,631 @@ +package hare4 + +import ( + "bytes" + "fmt" + "slices" + "sync" + + "go.uber.org/zap/zapcore" + "golang.org/x/exp/maps" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/log" + "github.com/spacemeshos/go-spacemesh/malfeasance/wire" +) + +type grade uint8 + +const ( + grade0 grade = iota + grade1 + grade2 + grade3 + grade4 + grade5 +) + +func isSubset(s, v []types.ProposalID) bool { + set := map[types.ProposalID]struct{}{} + for _, id := range v { + set[id] = struct{}{} + } + for _, id := range s { + if _, exist := set[id]; !exist { + return false + } + } + return true +} + +func toHash(proposals []types.ProposalID) types.Hash32 { + return types.CalcProposalHash32Presorted(proposals, nil) +} + +type messageKey struct { + IterRound + Sender types.NodeID +} + +type input struct { + *Message + atxgrade grade + malicious bool + msgHash types.Hash32 +} + +func (i *input) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + if i.Message != nil { + i.Message.MarshalLogObject(encoder) + } + encoder.AddUint8("atxgrade", uint8(i.atxgrade)) + encoder.AddBool("malicious", i.malicious) + encoder.AddString("hash", i.msgHash.ShortString()) + return nil +} + +type output struct { + coin *bool // set based on preround messages right after preround completes in 0 iter + result []types.ProposalID // set based on notify messages at the start of next iter + terminated bool // protocol participates in one more iteration after outputing result + message *Message +} + +func (o *output) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddBool("terminated", o.terminated) + if o.coin != nil { + encoder.AddBool("coin", *o.coin) + } + if o.result != nil { + encoder.AddArray("result", zapcore.ArrayMarshalerFunc(func(encoder log.ArrayEncoder) error { + for _, id := range o.result { + encoder.AppendString(types.Hash20(id).ShortString()) + } + return nil + })) + } + if o.message != nil { + encoder.AddObject("msg", o.message) + } + return nil +} + +func newProtocol(threshold uint16) *protocol { + return &protocol{ + validProposals: map[types.Hash32][]types.ProposalID{}, + gossip: gossip{threshold: threshold, state: map[messageKey]*gossipInput{}}, + } +} + +type protocol struct { + mu sync.Mutex + IterRound + coinout bool + coin *types.VrfSignature // smallest vrf from preround messages. not a part of paper + initial []types.ProposalID // Si + result *types.Hash32 // set after waiting for notify messages. Case 1 + locked *types.Hash32 // Li + hardLocked bool + validProposals map[types.Hash32][]types.ProposalID // Ti + gossip gossip +} + +func (p *protocol) OnInitial(proposals []types.ProposalID) { + slices.SortFunc(proposals, func(i, j types.ProposalID) int { return bytes.Compare(i[:], j[:]) }) + p.mu.Lock() + defer p.mu.Unlock() + p.initial = proposals +} + +func (p *protocol) OnInput(msg *input) (bool, *wire.HareProof) { + p.mu.Lock() + defer p.mu.Unlock() + + gossip, equivocation := p.gossip.receive(p.IterRound, msg) + if !gossip { + return false, equivocation + } + if msg.Round == preround && + (p.coin == nil || (p.coin != nil && msg.Eligibility.Proof.Cmp(p.coin) == -1)) { + p.coin = &msg.Eligibility.Proof + } + return gossip, equivocation +} + +func (p *protocol) thresholdProposals(ir IterRound, grade grade) (*types.Hash32, []types.ProposalID) { + for _, ref := range p.gossip.thresholdGossipRef(ir, grade) { + valid, exist := p.validProposals[ref] + if exist { + return &ref, valid + } + } + return nil, nil +} + +func (p *protocol) commitExists(iter uint8, match types.Hash32, grade grade) bool { + for _, ref := range p.gossip.thresholdGossipRef(IterRound{Iter: iter, Round: commit}, grade) { + if ref == match { + return true + } + } + return false +} + +func (p *protocol) execution(out *output) { + // 4.3 Protocol Execution + switch p.Round { + case preround: + out.message = &Message{Body: Body{ + IterRound: p.IterRound, + Value: Value{Proposals: p.initial}, + }} + case hardlock: + if p.Iter > 0 { + if p.result != nil { + out.terminated = true + } + ref, values := p.thresholdProposals(IterRound{Iter: p.Iter - 1, Round: notify}, grade5) + if ref != nil && p.result == nil { + p.result = ref + out.result = values + if values == nil { + // receiver expects non-nil result + out.result = []types.ProposalID{} + } + } + if ref, _ := p.thresholdProposals(IterRound{Iter: p.Iter - 1, Round: commit}, grade4); ref != nil { + p.locked = ref + p.hardLocked = true + } else { + p.locked = nil + p.hardLocked = false + } + } + case softlock: + if p.Iter > 0 && !p.hardLocked { + if ref, _ := p.thresholdProposals(IterRound{Iter: p.Iter - 1, Round: commit}, grade3); ref != nil { + p.locked = ref + } else { + p.locked = nil + } + } + case propose: + values := p.gossip.thresholdGossip(IterRound{Round: preround}, grade4) + if p.Iter > 0 { + ref, overwrite := p.thresholdProposals(IterRound{Iter: p.Iter - 1, Round: commit}, grade2) + if ref != nil { + values = overwrite + } + } + out.message = &Message{Body: Body{ + IterRound: p.IterRound, + Value: Value{Proposals: values}, + }} + case commit: + // condition (d) is realized by ordering proposals by vrf + proposed := p.gossip.gradecast(IterRound{Iter: p.Iter, Round: propose}) + g2values := p.gossip.thresholdGossip(IterRound{Round: preround}, grade2) + for _, graded := range proposed { + // condition (a) and (b) + // grade0 proposals are not added to the set + if !isSubset(graded.values, g2values) { + continue + } + p.validProposals[toHash(graded.values)] = graded.values + } + if p.hardLocked && p.locked != nil { + out.message = &Message{Body: Body{ + IterRound: p.IterRound, + Value: Value{Reference: p.locked}, + }} + } else { + g3values := p.gossip.thresholdGossip(IterRound{Round: preround}, grade3) + g5values := p.gossip.thresholdGossip(IterRound{Round: preround}, grade5) + for _, graded := range proposed { + id := toHash(graded.values) + // condition (c) + if _, exist := p.validProposals[id]; !exist { + continue + } + // condition (e) + if graded.grade != grade2 { + continue + } + // condition (f) + if !isSubset(graded.values, g3values) { + continue + } + // condition (g) + if !isSubset(g5values, graded.values) && !p.commitExists(p.Iter-1, id, grade1) { + continue + } + // condition (h) + if p.locked != nil && *p.locked != id { + continue + } + out.message = &Message{Body: Body{ + IterRound: p.IterRound, + Value: Value{Reference: &id}, + }} + break + } + } + case notify: + ref := p.result + if ref == nil { + ref, _ = p.thresholdProposals(IterRound{Iter: p.Iter, Round: commit}, grade5) + } + if ref != nil { + out.message = &Message{Body: Body{ + IterRound: p.IterRound, + Value: Value{Reference: ref}, + }} + } + } +} + +func (p *protocol) Next() output { + p.mu.Lock() + defer p.mu.Unlock() + + out := output{} + p.execution(&out) + if p.Round >= softlock && p.coin != nil && !p.coinout { + coin := p.coin.LSB() != 0 + out.coin = &coin + p.coinout = true + } + if p.Round == preround && p.Iter == 0 { + // skips hardlock unlike softlock in the paper. + // this makes no practical difference from correctness. + // but allows to simplify assignment in validValues + p.Round = softlock + } else if p.Round == notify { + p.Round = hardlock + p.Iter++ + } else { + p.Round++ + } + return out +} + +func (p *protocol) Stats() *stats { + p.mu.Lock() + defer p.mu.Unlock() + s := &stats{ + iter: p.Iter - 1, + threshold: p.gossip.threshold, + } + // preround messages that are received after the very first iteration + // has no impact on protocol + if s.iter == 0 { + for grade := grade1; grade <= grade5; grade++ { + s.preround = append(s.preround, preroundStats{ + grade: grade, + tallies: maps.Values( + thresholdTallies(p.gossip.state, IterRound{Round: preround}, grade, tallyProposals), + ), + }) + } + } + proposals := p.gossip.gradecast(IterRound{Iter: p.Iter - 1, Round: propose}) + for _, graded := range proposals { + s.propose = append(s.propose, proposeStats{ + grade: graded.grade, + ref: toHash(graded.values), + proposals: graded.values, + }) + } + // stats are collected at the start of current iteration (p.Iter) + // we expect 2 network delays to pass since commit messages were broadcasted + for grade := grade4; grade <= grade5; grade++ { + s.commit = append(s.commit, commitStats{ + grade: grade, + tallies: maps.Values( + thresholdTallies(p.gossip.state, IterRound{Iter: p.Iter - 1, Round: commit}, grade, tallyRefs), + ), + }) + } + // we are not interested in any other grade for notify message as they have no impact on protocol execution + s.notify = append(s.notify, notifyStats{ + grade: grade5, + tallies: maps.Values( + thresholdTallies(p.gossip.state, IterRound{Iter: p.Iter - 1, Round: notify}, grade5, tallyRefs), + ), + }) + return s +} + +type gossipInput struct { + *input + received IterRound + otherReceived *IterRound +} + +// Protocol 1. graded-gossip. page 10. +type gossip struct { + threshold uint16 + state map[messageKey]*gossipInput +} + +func (g *gossip) receive(current IterRound, input *input) (bool, *wire.HareProof) { + // Case 1: will be discarded earlier + other, exist := g.state[input.key()] + if exist { + if other.msgHash != input.msgHash && !other.malicious { + // Protocol 3. thresh-gossip. keep one with the maximal grade. + if input.atxgrade > other.atxgrade { + input.malicious = true + g.state[input.key()] = &gossipInput{ + input: input, + received: current, + otherReceived: &other.received, + } + } else { + // Case 3 + other.malicious = true + other.otherReceived = ¤t + } + return true, &wire.HareProof{Messages: [2]wire.HareProofMsg{ + other.ToMalfeasanceProof(), input.ToMalfeasanceProof(), + }} + } + // Case 2. but also we filter duplicates from p2p layer here + return false, nil + } + // Case 4 + g.state[input.key()] = &gossipInput{input: input, received: current} + return true, nil +} + +type gset struct { + values []types.ProposalID + grade grade + smallest types.VrfSignature +} + +// Protocol 2. gradecast. page 13. +func (g *gossip) gradecast(target IterRound) []gset { + // unlike paper we use 5-graded gossip for gradecast as well + var rst []gset + for key, value := range g.state { + if key.IterRound == target && (!value.malicious || value.otherReceived != nil) { + if value.atxgrade == grade5 && value.received.Delay(target) <= 1 && + // 2 (a) + (value.otherReceived == nil || value.otherReceived.Delay(target) > 3) { + // 2 (b) + rst = append(rst, gset{ + grade: grade2, + values: value.Value.Proposals, + smallest: value.Eligibility.Proof, + }) + } else if value.atxgrade >= grade4 && value.received.Delay(target) <= 2 && + // 3 (a) + (value.otherReceived == nil || value.otherReceived.Delay(target) > 2) { + // 3 (b) + rst = append(rst, gset{ + grade: grade1, + values: value.Value.Proposals, + smallest: value.Eligibility.Proof, + }) + } + } + } + // hare expects to receive multiple proposals. expected number of leaders is set to 5. + // we need to choose the same one for commit across the cluster. + // we do that by ordering them by vrf value, and picking one that passes other checks (see commit in execution). + // in hare3 paper look for p-Weak leader election property. + slices.SortFunc(rst, func(i, j gset) int { + return i.smallest.Cmp(&j.smallest) + }) + return rst +} + +func tallyProposals(all map[types.ProposalID]proposalTally, inp *gossipInput) { + for _, id := range inp.Value.Proposals { + tally, exist := all[id] + if !exist { + tally = proposalTally{id: id} + } + tally.total += inp.Eligibility.Count + if !inp.malicious { + tally.valid += inp.Eligibility.Count + } + all[id] = tally + } +} + +// Protocol 3. thresh-gossip. Page 15. +// output returns union of sorted proposals received +// in the given round with minimal specified grade. +func (g *gossip) thresholdGossip(filter IterRound, grade grade) []types.ProposalID { + rst := thresholdGossip(thresholdTallies(g.state, filter, grade, tallyProposals), g.threshold) + slices.SortFunc(rst, func(i, j types.ProposalID) int { + return bytes.Compare(i.Bytes(), j.Bytes()) + }) + return rst +} + +func tallyRefs(all map[types.Hash32]refTally, inp *gossipInput) { + tally, exist := all[*inp.Value.Reference] + if !exist { + tally = refTally{id: *inp.Value.Reference} + } + tally.total += inp.Eligibility.Count + if !inp.malicious { + tally.valid += inp.Eligibility.Count + } + all[*inp.Value.Reference] = tally +} + +// thresholdGossipRef returns all references to proposals in the given round with minimal grade. +func (g *gossip) thresholdGossipRef(filter IterRound, grade grade) []types.Hash32 { + return thresholdGossip(thresholdTallies(g.state, filter, grade, tallyRefs), g.threshold) +} + +func thresholdGossip[T interface { + comparable + fmt.Stringer +}]( + tallies map[T]tallyStats[T], threshold uint16, +) []T { + rst := []T{} + for _, item := range tallies { + // valid > 0 and total >= f + // atleast one non-equivocating vote and crossed committee/2 + 1 + if item.total >= threshold && item.valid > 0 { + rst = append(rst, item.id) + } + } + return rst +} + +func thresholdTallies[T interface { + comparable + fmt.Stringer +}]( + state map[messageKey]*gossipInput, + filter IterRound, + msgGrade grade, + tally func(tally map[T]tallyStats[T], inp *gossipInput), +) map[T]tallyStats[T] { + all := map[T]tallyStats[T]{} + min := grade5 + // pick min atx grade from non equivocating identity. + for key, value := range state { + if key.IterRound == filter && value.atxgrade < min && !value.malicious && + value.received.Grade(filter) >= msgGrade { + min = value.atxgrade + } + } + // tally votes for valid and malicious messages + for key, value := range state { + if key.IterRound == filter && value.atxgrade >= min && value.received.Grade(filter) >= msgGrade { + tally(all, value) + } + } + return all +} + +type preroundStats struct { + grade grade + tallies []proposalTally +} + +func (s *preroundStats) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddUint16("grade", uint16(s.grade)) + encoder.AddArray("tallies", zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { + for _, tally := range s.tallies { + enc.AppendObject(&tally) + } + return nil + })) + return nil +} + +type tallyStats[T fmt.Stringer] struct { + id T + total uint16 + valid uint16 +} + +func (s *tallyStats[T]) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddUint16("total", s.total) + encoder.AddUint16("valid", s.valid) + encoder.AddString("id", s.id.String()) + return nil +} + +type ( + proposalTally = tallyStats[types.ProposalID] + refTally = tallyStats[types.Hash32] +) + +type proposeStats struct { + grade grade + ref types.Hash32 + proposals []types.ProposalID +} + +func (s *proposeStats) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddString("ref", s.ref.ShortString()) + encoder.AddUint16("grade", uint16(s.grade)) + encoder.AddArray("proposals", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { + for _, id := range s.proposals { + encoder.AppendString(id.String()) + } + return nil + })) + return nil +} + +type commitStats struct { + grade grade + tallies []refTally +} + +func (s *commitStats) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddUint16("grade", uint16(s.grade)) + encoder.AddArray("tallies", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { + for _, tally := range s.tallies { + encoder.AppendObject(&tally) + } + return nil + })) + return nil +} + +type notifyStats struct { + grade grade + tallies []refTally +} + +func (n *notifyStats) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddUint16("grade", uint16(n.grade)) + encoder.AddArray("tallies", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { + for _, tally := range n.tallies { + encoder.AppendObject(&tally) + } + return nil + })) + return nil +} + +type stats struct { + iter uint8 + threshold uint16 + preround []preroundStats + propose []proposeStats + commit []commitStats + notify []notifyStats +} + +func (s *stats) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddUint8("iter", s.iter) + encoder.AddUint16("threshold", s.threshold) + encoder.AddArray("preround", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { + for _, stat := range s.preround { + encoder.AppendObject(&stat) + } + return nil + })) + encoder.AddArray("propose", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { + for _, stat := range s.propose { + encoder.AppendObject(&stat) + } + return nil + })) + encoder.AddArray("commit", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { + for _, stat := range s.commit { + encoder.AppendObject(&stat) + } + return nil + })) + encoder.AddArray("notify", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { + for _, stat := range s.notify { + encoder.AppendObject(&stat) + } + return nil + })) + return nil +} diff --git a/hare4/protocol_test.go b/hare4/protocol_test.go new file mode 100644 index 0000000000..c04e69fdcf --- /dev/null +++ b/hare4/protocol_test.go @@ -0,0 +1,584 @@ +package hare4 + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/malfeasance/wire" +) + +func castIds(strings ...string) []types.ProposalID { + ids := []types.ProposalID{} + for _, p := range strings { + var id types.ProposalID + copy(id[:], p) + ids = append(ids, id) + } + return ids +} + +type tinput struct { + input + expect *response +} + +type response struct { + gossip bool + equivocation *wire.HareProof +} + +func (t *tinput) ensureMsg() { + if t.Message == nil { + t.Message = &Message{} + } +} + +func (t *tinput) ensureResponse() { + if t.expect == nil { + t.expect = &response{} + } +} + +func (t *tinput) round(r Round) *tinput { + t.ensureMsg() + t.Round = r + return t +} + +func (t *tinput) iter(i uint8) *tinput { + t.ensureMsg() + t.Iter = i + return t +} + +func (t *tinput) proposals(proposals ...string) *tinput { + t.ensureMsg() + t.Value.Proposals = castIds(proposals...) + return t +} + +func (t *tinput) ref(proposals ...string) *tinput { + t.ensureMsg() + hs := types.CalcProposalHash32Presorted(castIds(proposals...), nil) + t.Value.Reference = &hs + return t +} + +func (t *tinput) vrf(vrf ...byte) *tinput { + t.ensureMsg() + copy(t.Eligibility.Proof[:], vrf) + return t +} + +func (t *tinput) vrfcount(c uint16) *tinput { + t.ensureMsg() + t.Eligibility.Count = c + return t +} + +func (t *tinput) sender(name string) *tinput { + t.ensureMsg() + copy(t.Sender[:], name) + return t +} + +func (t *tinput) mshHash(h string) *tinput { + copy(t.input.msgHash[:], h) + return t +} + +func (t *tinput) malicious() *tinput { + t.input.malicious = true + return t +} + +func (t *tinput) gossip() *tinput { + t.ensureResponse() + t.expect.gossip = true + return t +} + +func (t *tinput) equi() *tinput { + // TODO(dshulyak) do i want to test that it constructed correctly here? + t.ensureResponse() + t.expect.equivocation = &wire.HareProof{} + return t +} + +func (t *tinput) nogossip() *tinput { + t.ensureResponse() + t.expect.gossip = false + return t +} + +func (t *tinput) g(g grade) *tinput { + t.atxgrade = g + return t +} + +type toutput struct { + act bool + output +} + +func (t *toutput) ensureMsg() { + if t.message == nil { + t.message = &Message{} + } +} + +func (t *toutput) active() *toutput { + t.act = true + return t +} + +func (t *toutput) round(r Round) *toutput { + t.ensureMsg() + t.message.Round = r + return t +} + +func (t *toutput) iter(i uint8) *toutput { + t.ensureMsg() + t.message.Iter = i + return t +} + +func (t *toutput) proposals(proposals ...string) *toutput { + t.ensureMsg() + t.message.Value.Proposals = castIds(proposals...) + return t +} + +func (t *toutput) ref(proposals ...string) *toutput { + t.ensureMsg() + hs := types.CalcProposalHash32Presorted(castIds(proposals...), nil) + t.message.Value.Reference = &hs + return t +} + +func (t *toutput) terminated() *toutput { + t.output.terminated = true + return t +} + +func (t *toutput) coin(c bool) *toutput { + t.output.coin = &c + return t +} + +func (t *toutput) result(proposals ...string) *toutput { + t.output.result = castIds(proposals...) + return t +} + +type setup struct { + threshold uint16 + proposals []types.ProposalID +} + +func (s *setup) thresh(v uint16) *setup { + s.threshold = v + return s +} + +func (s *setup) initial(proposals ...string) *setup { + s.proposals = castIds(proposals...) + return s +} + +type testCase struct { + desc string + steps []any +} + +func gen(desc string, steps ...any) testCase { + return testCase{desc: desc, steps: steps} +} + +func TestProtocol(t *testing.T) { + for _, tc := range []testCase{ + gen("sanity", // simplest e2e protocol run + new(setup).thresh(10).initial("a", "b"), + new(toutput).active().round(preround).proposals("a", "b"), + new(tinput).sender("1").round(preround).proposals("b", "a").vrfcount(3).g(grade5), + new(tinput).sender("2").round(preround).proposals("a", "c").vrfcount(9).g(grade5), + new(tinput).sender("3").round(preround).proposals("c").vrfcount(6).g(grade5), + new(toutput).coin(false), + new(toutput).active().round(propose).proposals("a", "c"), + new(tinput).sender("1").round(propose).proposals("a", "c").g(grade5).vrf(2), + new(tinput).sender("2").round(propose).proposals("b", "d").g(grade5).vrf(1), + new(toutput), + new(toutput), + new(toutput).active().round(commit).ref("a", "c"), + new(tinput).sender("1").round(commit).ref("a", "c").vrfcount(4).g(grade5), + new(tinput).sender("2").round(commit).ref("a", "c").vrfcount(8).g(grade5), + new(toutput).active().round(notify).ref("a", "c"), + new(tinput).sender("1").round(notify).ref("a", "c").vrfcount(5).g(grade5), + new(tinput).sender("2").round(notify).ref("a", "c").vrfcount(6).g(grade5), + new(toutput).result("a", "c"), // hardlock + new(toutput), // softlock + // propose, commit, notify messages are built based on prervious state + new(toutput).active().round(propose).iter(1).proposals("a", "c"), // propose + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput).active().round(commit).iter(1).ref("a", "c"), // commit + new(toutput).active().round(notify).iter(1).ref("a", "c"), // notify + new(toutput).terminated(), + ), + gen("commit on softlock", + new(setup).thresh(10).initial("a", "b"), + new(toutput), + new(tinput).sender("1").round(preround).proposals("a", "b").vrfcount(11).g(grade5), + new(toutput).coin(false), + new(toutput), + new(tinput).sender("1").round(propose).proposals("a", "b").g(grade5), + new(tinput).sender("2").round(propose).proposals("b").g(grade5), + new(toutput), + new(toutput), + new(toutput), + new(tinput).sender("1").round(commit).ref("b").vrfcount(11).g(grade3), + new(toutput), + new(toutput), // hardlock + new(toutput), // softlock + // propose, commit, notify messages are built based on prervious state + new(toutput), // propose + new(tinput).sender("1").iter(1).round(propose).proposals("b").g(grade5), + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput).active().round(commit).iter(1).ref("b"), // commit + ), + gen("empty 0 iteration", // test that protocol can complete not only in 0st iteration + new(setup).thresh(10).initial("a", "b"), + new(toutput), // preround + new(tinput).sender("1").round(preround).proposals("a", "b").vrfcount(3).g(grade5), + new(tinput).sender("2").round(preround).proposals("a", "b").vrfcount(9).g(grade5), + new(toutput).coin(false), // softlock + new(toutput), // propose + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput), // commit + new(toutput), // notify + new(toutput), // 2nd hardlock + new(toutput), // 2nd softlock + new(toutput).active().iter(1).round(propose).proposals("a", "b"), // 2nd propose + new(tinput).sender("1").iter(1).round(propose).proposals("a", "b").g(grade5), + new(toutput), // 2nd wait1 + new(toutput), // 2nd wait2 + new(toutput).active().iter(1).round(commit).ref("a", "b"), + new(tinput).sender("1").iter(1).round(commit).ref("a", "b").g(grade5).vrfcount(11), + new(toutput).active().iter(1).round(notify).ref("a", "b"), + new(tinput).sender("1").iter(1).round(notify).ref("a", "b").g(grade5).vrfcount(11), + new(toutput).result("a", "b"), // 3rd hardlock + new(toutput), // 3rd softlock + new(toutput), // 3rd propose + new(toutput), // 3rd wait1 + new(toutput), // 3rd wait2 + new(toutput), // 3rd commit + new(toutput), // 3rd notify + new(toutput).terminated(), // 4th softlock + ), + gen("empty proposal", + new(setup).thresh(10), + new(toutput), // preround + new(tinput).sender("1").round(preround).vrfcount(11).g(grade5), + new(toutput).coin(false), // softlock + new(toutput).active().round(propose).proposals(), // propose + new(tinput).sender("1").round(propose).g(grade5).vrf(2), + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput).active().round(commit).ref(), // commit + new(tinput).sender("1").round(commit).ref().vrfcount(11).g(grade5), + new(toutput).active().round(notify).ref(), // notify + new(tinput).sender("1").round(notify).ref().vrfcount(11).g(grade5), + new(toutput).result(), // hardlock + ), + gen("coin true", + new(setup).thresh(10), + new(toutput), + new(tinput).sender("2").round(preround).vrf(2).g(grade5), + new(tinput).sender("1").round(preround).vrf(1).g(grade5), + new(tinput).sender("3").round(preround).vrf(2).g(grade5), + new(toutput).coin(true), + ), + gen("coin false", + new(setup).thresh(10), + new(toutput), + new(tinput).sender("2").round(preround).vrf(1, 2).g(grade5), + new(tinput).sender("1").round(preround).vrf(0, 1).g(grade5), + new(tinput).sender("3").round(preround).vrf(2).g(grade5), + new(toutput).coin(false), + ), + gen("coin delayed", + new(setup).thresh(10), + new(toutput), + new(toutput), + new(tinput).sender("2").round(preround).vrf(1, 2).g(grade5), + new(tinput).sender("1").round(preround).vrf(2, 3).g(grade5), + new(toutput).coin(true), + ), + gen("duplicates don't affect thresholds", + new(setup).thresh(10), + new(toutput), + new(tinput).sender("1").round(preround).proposals("a", "b").vrfcount(5).g(grade5), + new(tinput).sender("3").round(preround).proposals("d").vrfcount(6).g(grade5).gossip(), + new(tinput).sender("2").round(preround).proposals("a", "b").vrfcount(6).g(grade5), + new(tinput).sender("3").round(preround).proposals("d").vrfcount(6).g(grade5), + new(toutput).coin(false), + new(toutput).active().round(propose).proposals("a", "b"), // assert that `d` doesn't cross + new(tinput).sender("1").round(propose).proposals("a").vrf(2).g(grade5), + // this one would be preferred if duplicates were counted + new(tinput).sender("2").round(propose).proposals("b").vrf(1).g(grade5), + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput), // commit + new(tinput).sender("1").round(commit).ref("a").vrfcount(4).g(grade5), + new(tinput).sender("2").round(commit).ref("b").vrfcount(6).g(grade5), + new(tinput).sender("2").round(commit).ref("b").vrfcount(6).g(grade5), + new(tinput).sender("3").round(commit).ref("a").vrfcount(7).g(grade5), + new(toutput).active().round(notify).ref("a"), // duplicates commits were ignored + ), + gen("malicious preround", + new(setup).thresh(10), + new(toutput), + new(tinput).sender("1").round(preround).proposals("a", "b").vrfcount(9).g(grade5), + new(tinput).sender("2").malicious().gossip(). + round(preround).proposals("b", "d").vrfcount(11).g(grade5), + new(toutput).coin(false), + // d would be added if from non-malicious + new(toutput).active().round(propose).proposals("b"), + ), + gen("malicious proposal", + new(setup).thresh(10), + new(toutput), + new(toutput), // softlock + new(tinput).sender("5"). + round(preround).proposals("a", "b", "c").vrfcount(11).g(grade5), + new(toutput).coin(false), // propose + new(tinput).sender("1").round(propose).proposals("a", "c").vrf(2).g(grade5), + // this one would be preferred if from non-malicious + new(tinput).sender("2").malicious().gossip(). + round(propose).proposals("b").vrf(1).g(grade5), + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput).active().round(commit).ref("a", "c"), // commit + ), + gen("malicious commit", + new(setup).thresh(10), + new(toutput), + new(tinput).sender("5"). + round(preround).proposals("a").vrfcount(11).g(grade5), + new(toutput).coin(false), // softlock + new(toutput), // propose + new(tinput).sender("1").round(propose).proposals("a").g(grade5), + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput), // commit + new(tinput).sender("1").malicious().gossip(). + round(commit).ref("a").vrfcount(11).g(grade5), + new(toutput).active(), // notify outputs nothing + ), + gen("malicious notify", + new(setup).thresh(10), + new(toutput), + new(tinput).sender("5"). + round(preround).proposals("a").vrfcount(11).g(grade5), + new(toutput).coin(false), // softlock + new(toutput), // propose + new(tinput).sender("1").round(propose).proposals("a").g(grade5), + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput), // commit + new(tinput).sender("1").round(commit).ref("a").vrfcount(11).g(grade5), + new(toutput), // notify + new(tinput).sender("1").malicious().gossip(). + round(notify).ref("a").vrfcount(11).g(grade5), + new(toutput), // no result as the only notify is malicious + ), + gen("equivocation preround", + new(setup).thresh(10), + new(toutput), + new(tinput).sender("7").mshHash("m0"). + round(preround).proposals("a").vrfcount(11).g(grade4), + new(tinput).sender("7").gossip().mshHash("m1").equi(). + round(preround).proposals("b").vrfcount(11).g(grade5), + new(tinput).sender("7").nogossip().mshHash("m2"). + round(preround).proposals("c").vrfcount(11).g(grade3), + ), + gen("multiple malicious not broadcasted", + new(setup).thresh(10), + new(toutput), + new(tinput).sender("7").malicious().gossip(). + round(preround).proposals("a").vrfcount(11).g(grade5), + new(tinput).sender("7").malicious().nogossip(). + round(preround).proposals("b").vrfcount(11).g(grade5), + new(tinput).sender("7").malicious().nogossip(). + round(preround).proposals("c").vrfcount(11).g(grade5), + ), + gen("no commit for grade1", + new(setup).thresh(10), + new(toutput), + new(tinput).sender("5"). + round(preround).proposals("a").vrfcount(11).g(grade5), + new(toutput).coin(false), // softlock + new(toutput), // propose + new(toutput), // wait1 + new(tinput).sender("1").round(propose).proposals("a").g(grade5), + new(toutput), // wait2 + new(toutput).active(), // commit + ), + gen("other gradecast was received", + new(setup).thresh(10), + new(toutput), + new(tinput).sender("5"). + round(preround).proposals("a").vrfcount(11).g(grade5), + new(toutput).coin(false), // softlock + new(toutput), // propose + new(tinput).sender("1").round(propose).proposals("a").g(grade5), + new(toutput), // wait1 + new(tinput).sender("1").mshHash("a").gossip().round(propose).proposals("b").g(grade3), + new(toutput), // wait2 + new(toutput).active(), // commit + ), + gen("no commit if not subset of grade3", + new(setup).thresh(10), + new(toutput), // preround + new(toutput), // softlock + new(toutput), // propose + new(tinput).sender("1").round(propose).proposals("a").g(grade5), + new(toutput), // wait1 + new(tinput).sender("1"). + round(preround).proposals("a").vrfcount(11).g(grade2), + new(toutput).coin(false), // wait2 + new(toutput).active(), // commit + ), + gen("grade5 proposals are not in propose", + new(setup).thresh(10), + new(toutput), // preround + new(tinput).sender("1"). + round(preround).proposals("a").vrfcount(11).g(grade5), + new(toutput).coin(false), // softlock + new(tinput).sender("2"). + round(preround).proposals("b").vrfcount(11).g(grade5), + new(toutput), // propose + new(tinput).sender("1").round(propose).proposals("b").g(grade5), + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput).active(), // commit + ), + gen("commit locked", + new(setup).thresh(10), + new(toutput), // preround + new(tinput).sender("1"). + round(preround).proposals("a", "b").vrfcount(11).g(grade5), + new(toutput).coin(false), // softlock + new(toutput), // propose + new(tinput).sender("1").round(propose).proposals("a").g(grade5), + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput), // commit + new(toutput), // notify + new(toutput), // hardlock + // commit on a will have grade3, so no hardlock + new(tinput).sender("1").round(commit).ref("a").vrfcount(11).g(grade5), + new(toutput), // softlock + new(toutput), // propose + // commit on b will have grade1, to satisfy condition (g) + new(tinput).sender("2").round(commit).ref("b").vrfcount(11).g(grade5), + new(tinput).sender("1").iter(1).round(propose).proposals("a").g(grade5).vrf(2), + new(tinput).sender("2").iter(1).round(propose).proposals("b").g(grade5).vrf(1), + new(toutput), // wait1 + new(toutput), // wait2 + // condition (h) ensures that we commit on locked value, even though proposal for b + // is first in the order + new(toutput).active().round(commit).iter(1).ref("a"), // commit + ), + gen("early proposal by one", + new(setup).thresh(10), + new(tinput).sender("1").round(preround).proposals("a", "b").vrfcount(11).g(grade5), + new(toutput).coin(false), // preround + new(toutput), // softlock + new(tinput).sender("1").round(propose).proposals("a", "b").g(grade5).vrf(1), + new(toutput), // propose + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput).active().round(commit).ref("a", "b"), + ), + gen("early proposal by two", + new(setup).thresh(10), + new(tinput).sender("1").round(preround).proposals("a", "b").vrfcount(11).g(grade5), + new(toutput).coin(false), // preround + new(tinput).sender("1").round(propose).proposals("a", "b").g(grade5).vrf(1), + new(toutput), // softlock + new(toutput), // propose + new(toutput), // wait1 + new(toutput), // wait2 + new(toutput).active().round(commit).ref("a", "b"), + ), + } { + t.Run(tc.desc, func(t *testing.T) { + var ( + proto *protocol + logger = zaptest.NewLogger(t) + ) + for i, step := range tc.steps { + if i != 0 && proto == nil { + require.FailNow(t, "step with setup should be the first one") + } + switch casted := step.(type) { + case *setup: + proto = newProtocol(casted.threshold) + proto.OnInitial(casted.proposals) + case *tinput: + logger.Debug("input", zap.Int("i", i), zap.Inline(casted)) + gossip, equivocation := proto.OnInput(&casted.input) + if casted.expect != nil { + require.Equal(t, casted.expect.gossip, gossip, "%d", i) + if casted.expect.equivocation != nil { + require.NotEmpty(t, equivocation) + } + } + case *toutput: + before := proto.Round + out := proto.Next() + if casted.act { + require.Equal(t, casted.output, out, "%d", i) + } + logger.Debug("output", + zap.Int("i", i), + zap.Inline(casted), + zap.Stringer("before", before), + zap.Stringer("after", proto.Round), + ) + stats := proto.Stats() + enc := zapcore.NewMapObjectEncoder() + require.NoError(t, stats.MarshalLogObject(enc)) + } + } + }) + } +} + +func TestInputMarshall(t *testing.T) { + enc := zapcore.NewMapObjectEncoder() + inp := &input{ + Message: &Message{}, + } + require.NoError(t, inp.MarshalLogObject(enc)) +} + +func TestOutputMarshall(t *testing.T) { + enc := zapcore.NewMapObjectEncoder() + coin := true + out := &output{ + coin: &coin, + result: []types.ProposalID{{}}, + message: &Message{}, + } + require.NoError(t, out.MarshalLogObject(enc)) +} diff --git a/hare4/tracer.go b/hare4/tracer.go new file mode 100644 index 0000000000..0b4a86f114 --- /dev/null +++ b/hare4/tracer.go @@ -0,0 +1,31 @@ +package hare4 + +import "github.com/spacemeshos/go-spacemesh/common/types" + +type Tracer interface { + OnStart(types.LayerID) + OnStop(types.LayerID) + OnActive([]*types.HareEligibility) + OnMessageSent(*Message) + OnMessageReceived(*Message) + OnCompactIdRequest(*CompactIdRequest) + OnCompactIdResponse(*CompactIdResponse) +} + +var _ Tracer = noopTracer{} + +type noopTracer struct{} + +func (noopTracer) OnStart(types.LayerID) {} + +func (noopTracer) OnStop(types.LayerID) {} + +func (noopTracer) OnActive([]*types.HareEligibility) {} + +func (noopTracer) OnMessageSent(*Message) {} + +func (noopTracer) OnMessageReceived(*Message) {} + +func (noopTracer) OnCompactIdRequest(*CompactIdRequest) {} + +func (noopTracer) OnCompactIdResponse(*CompactIdResponse) {} diff --git a/hare4/types.go b/hare4/types.go new file mode 100644 index 0000000000..3837fcb973 --- /dev/null +++ b/hare4/types.go @@ -0,0 +1,183 @@ +package hare4 + +import ( + "errors" + "fmt" + + "go.uber.org/zap/zapcore" + + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/hash" + "github.com/spacemeshos/go-spacemesh/log" + "github.com/spacemeshos/go-spacemesh/malfeasance/wire" +) + +type Round uint8 + +var roundNames = [...]string{"preround", "hardlock", "softlock", "propose", "wait1", "wait2", "commit", "notify"} + +func (r Round) String() string { + return roundNames[r] +} + +// NOTE(dshulyak) changes in order is a breaking change. +const ( + preround Round = iota + hardlock + softlock + propose + wait1 + wait2 + commit + notify +) + +//go:generate scalegen + +type IterRound struct { + Iter uint8 + Round Round +} + +// Delay returns number of network delays since specified iterround. +func (ir IterRound) Delay(since IterRound) uint32 { + if ir.Absolute() > since.Absolute() { + delay := ir.Absolute() - since.Absolute() + // we skip hardlock round in 0th iteration. + if since.Iter == 0 && since.Round == preround && delay != 0 { + delay-- + } + return delay + } + return 0 +} + +func (ir IterRound) Grade(since IterRound) grade { + return max(grade(6-ir.Delay(since)), grade0) +} + +func (ir IterRound) IsMessageRound() bool { + switch ir.Round { + case preround: + return true + case propose: + return true + case commit: + return true + case notify: + return true + } + return false +} + +func (ir IterRound) Absolute() uint32 { + return uint32(ir.Iter)*uint32(notify) + uint32(ir.Round) +} + +type Value struct { + // Proposals is set in messages for preround and propose rounds. + // + // Worst case scenario is that a single smesher identity has > 99.97% of the total weight of the network. + // In this case they will get all 50 available slots in all 4032 layers of the epoch. + // Additionally every other identity on the network that successfully published an ATX will get 1 slot. + // + // If we expect 7.0 Mio ATXs that would be a total of 7.0 Mio + 50 * 4032 = 8 201 600 slots. + // Since these are randomly distributed across the epoch, we can expect an average of n * p = + // 8 201 600 / 4032 = 2034.1 eligibilities in a layer with a standard deviation of sqrt(n * p * (1 - p)) = + // sqrt(8 201 600 * 1/4032 * 4031/4032) = 45.1 + // + // This means that we can expect a maximum of 2034.1 + 6*45.1 = 2304.7 eligibilities in a layer with + // > 99.9997% probability. + Proposals []types.ProposalID `scale:"max=2350"` + // Reference is set in messages for commit and notify rounds. + Reference *types.Hash32 + // CompactProposals is the array of compacted proposals IDs which are represented as truncated + // eligibility hashes. + CompactProposals []types.CompactProposalID `scale:"max=2350"` +} + +type Body struct { + Layer types.LayerID + IterRound + Value Value + Eligibility types.HareEligibility +} + +type Message struct { + Body + Sender types.NodeID + Signature types.EdSignature +} + +func (m *Message) ToHash() types.Hash32 { + h := hash.GetHasher() + defer hash.PutHasher(h) + codec.MustEncodeTo(h, &m.Body) + var rst types.Hash32 + h.Sum(rst[:0]) + return rst +} + +func (m *Message) ToMetadata() wire.HareMetadata { + return wire.HareMetadata{ + Layer: m.Layer, + Round: m.Absolute(), + MsgHash: m.ToHash(), + } +} + +func (m *Message) ToMalfeasanceProof() wire.HareProofMsg { + return wire.HareProofMsg{ + InnerMsg: m.ToMetadata(), + SmesherID: m.Sender, + Signature: m.Signature, + } +} + +func (m *Message) key() messageKey { + return messageKey{ + Sender: m.Sender, + IterRound: m.IterRound, + } +} + +func (m *Message) ToBytes() []byte { + return codec.MustEncode(m) +} + +func (m *Message) Validate() error { + if (m.Round == commit || m.Round == notify) && m.Value.Reference == nil { + return errors.New("reference can't be nil in commit or notify rounds") + } else if (m.Round == preround || m.Round == propose) && m.Value.Reference != nil { + return fmt.Errorf("reference is set to not nil in round %s", m.Round) + } + return nil +} + +func (m *Message) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddUint32("lid", m.Layer.Uint32()) + encoder.AddUint8("iter", m.Iter) + encoder.AddString("round", m.Round.String()) + encoder.AddString("sender", m.Sender.ShortString()) + if m.Value.Proposals != nil { + encoder.AddArray("full", zapcore.ArrayMarshalerFunc(func(encoder log.ArrayEncoder) error { + for _, id := range m.Value.Proposals { + encoder.AppendString(types.Hash20(id).ShortString()) + } + return nil + })) + } else if m.Value.Reference != nil { + encoder.AddString("ref", m.Value.Reference.ShortString()) + } + encoder.AddUint16("vrf_count", m.Eligibility.Count) + return nil +} + +type CompactIdRequest struct { + MsgId types.Hash32 +} + +type CompactIdResponse struct { + Ids []types.ProposalID `scale:"max=2050"` +} diff --git a/hare4/types_scale.go b/hare4/types_scale.go new file mode 100644 index 0000000000..2f06ce6ff9 --- /dev/null +++ b/hare4/types_scale.go @@ -0,0 +1,260 @@ +// Code generated by github.com/spacemeshos/go-scale/scalegen. DO NOT EDIT. + +// nolint +package hare4 + +import ( + "github.com/spacemeshos/go-scale" + "github.com/spacemeshos/go-spacemesh/common/types" +) + +func (t *IterRound) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeCompact8(enc, uint8(t.Iter)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeCompact8(enc, uint8(t.Round)) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *IterRound) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + field, n, err := scale.DecodeCompact8(dec) + if err != nil { + return total, err + } + total += n + t.Iter = uint8(field) + } + { + field, n, err := scale.DecodeCompact8(dec) + if err != nil { + return total, err + } + total += n + t.Round = Round(field) + } + return total, nil +} + +func (t *Value) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.Proposals, 2350) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeOption(enc, t.Reference) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.CompactProposals, 2350) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *Value) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + field, n, err := scale.DecodeStructSliceWithLimit[types.ProposalID](dec, 2350) + if err != nil { + return total, err + } + total += n + t.Proposals = field + } + { + field, n, err := scale.DecodeOption[types.Hash32](dec) + if err != nil { + return total, err + } + total += n + t.Reference = field + } + { + field, n, err := scale.DecodeStructSliceWithLimit[types.CompactProposalID](dec, 2350) + if err != nil { + return total, err + } + total += n + t.CompactProposals = field + } + return total, nil +} + +func (t *Body) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeCompact32(enc, uint32(t.Layer)) + if err != nil { + return total, err + } + total += n + } + { + n, err := t.IterRound.EncodeScale(enc) + if err != nil { + return total, err + } + total += n + } + { + n, err := t.Value.EncodeScale(enc) + if err != nil { + return total, err + } + total += n + } + { + n, err := t.Eligibility.EncodeScale(enc) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *Body) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + field, n, err := scale.DecodeCompact32(dec) + if err != nil { + return total, err + } + total += n + t.Layer = types.LayerID(field) + } + { + n, err := t.IterRound.DecodeScale(dec) + if err != nil { + return total, err + } + total += n + } + { + n, err := t.Value.DecodeScale(dec) + if err != nil { + return total, err + } + total += n + } + { + n, err := t.Eligibility.DecodeScale(dec) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *Message) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := t.Body.EncodeScale(enc) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.Sender[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *Message) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := t.Body.DecodeScale(dec) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.Sender[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *CompactIdRequest) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeByteArray(enc, t.MsgId[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *CompactIdRequest) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := scale.DecodeByteArray(dec, t.MsgId[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *CompactIdResponse) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.Ids, 2050) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *CompactIdResponse) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + field, n, err := scale.DecodeStructSliceWithLimit[types.ProposalID](dec, 2050) + if err != nil { + return total, err + } + total += n + t.Ids = field + } + return total, nil +} diff --git a/hare4/types_test.go b/hare4/types_test.go new file mode 100644 index 0000000000..91cf9ffe4f --- /dev/null +++ b/hare4/types_test.go @@ -0,0 +1,41 @@ +package hare4 + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" +) + +func TestAbsoluteMaxValue(t *testing.T) { + ir := IterRound{Iter: 40, Round: notify} + require.EqualValues(t, 41*7, ir.Absolute()) +} + +func TestMessageMarshall(t *testing.T) { + enc := zapcore.NewMapObjectEncoder() + msg := &Message{Body: Body{Value: Value{Proposals: []types.ProposalID{{}}}}} + require.NoError(t, msg.MarshalLogObject(enc)) + msg = &Message{Body: Body{Value: Value{Reference: &types.Hash32{}}}} + require.NoError(t, msg.MarshalLogObject(enc)) +} + +func FuzzMessageDecode(f *testing.F) { + for _, buf := range [][]byte{ + {}, + {0}, + {0, 1, 1}, + {0, 1, 1, 0, 10}, + } { + f.Add(buf) + } + f.Fuzz(func(t *testing.T, buf []byte) { + var msg Message + if err := codec.Decode(buf, &msg); err == nil { + _ = msg.Validate() + } + }) +} diff --git a/log/context.go b/log/context.go index d881790263..e2041bea19 100644 --- a/log/context.go +++ b/log/context.go @@ -19,22 +19,14 @@ const ( sessionFieldsKey ) -// WithRequestID returns a context which knows its request ID. +// withRequestID returns a context which knows its request ID. // A request ID tracks the lifecycle of a single request across all execution contexts, including // multiple goroutines, task queues, workers, etc. The canonical example is an incoming message // received over the network. Rule of thumb: requests "traverse the heap" and may be passed from one // session to another via channels. // This requires a requestId string, and optionally, other LoggableFields that are added to // context and printed in contextual logs. -func WithRequestID(ctx context.Context, requestID string, fields ...LoggableField) context.Context { - // Warn if overwriting. This is expected. It happens every time an inbound request triggers a new - // outbound request, e.g., a newly-received block causes us to request the blocks and ATXs it refers to. - // But it's important that we log the old and new reqIDs here so that the thread can be followed. - if curRequestID, ok := ExtractRequestID(ctx); ok && curRequestID != requestID { - GetLogger().WithContext(ctx).With().Info("overwriting requestID in context", - String("old_request_id", curRequestID), - String("new_request_id", requestID)) - } +func withRequestID(ctx context.Context, requestID string, fields ...LoggableField) context.Context { ctx = context.WithValue(ctx, requestIDKey, requestID) if len(fields) > 0 { ctx = context.WithValue(ctx, requestFieldsKey, fields) @@ -46,7 +38,7 @@ func WithRequestID(ctx context.Context, requestID string, fields ...LoggableFiel // It can be used when there isn't a single, clear, unique id associated with a request (e.g., // a block or tx hash). func WithNewRequestID(ctx context.Context, fields ...LoggableField) context.Context { - return WithRequestID(ctx, shortUUID(), fields...) + return withRequestID(ctx, shortUUID(), fields...) } // ExtractSessionID extracts the session id from a context object. diff --git a/log/errcode/fatalerrcode.go b/log/errcode/fatalerrcode.go deleted file mode 100644 index 0042da2c55..0000000000 --- a/log/errcode/fatalerrcode.go +++ /dev/null @@ -1,4 +0,0 @@ -package errcode - -// ErrClockDrift is returned when a clock drift is not acceptable. -var ErrClockDrift = "ERR_CLOCK_DRIFT" diff --git a/log/log_test.go b/log/log_test.go index 3a23b4af76..55260e0d38 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -146,7 +146,7 @@ func TestContextualLogging(t *testing.T) { // test basic context first: try to set and read context, roundtrip ctx := context.Background() - ctx = WithRequestID(ctx, reqID) + ctx = withRequestID(ctx, reqID) if reqID2, ok := ExtractRequestID(ctx); ok { r.Equal(reqID, reqID2) } else { @@ -161,7 +161,7 @@ func TestContextualLogging(t *testing.T) { // try again in reverse order ctx = context.Background() - ctx = WithRequestID(WithSessionID(ctx, sesID), reqID) + ctx = withRequestID(WithSessionID(ctx, sesID), reqID) if reqID2, ok := ExtractRequestID(ctx); ok { r.Equal(reqID, reqID2) } else { @@ -175,7 +175,7 @@ func TestContextualLogging(t *testing.T) { } // try re-setting (in reverse) - ctx = WithRequestID(WithSessionID(ctx, reqID), sesID) + ctx = withRequestID(WithSessionID(ctx, reqID), sesID) if reqID2, ok := ExtractRequestID(ctx); ok { r.Equal(sesID, reqID2) } else { @@ -202,7 +202,7 @@ func TestContextualLogging(t *testing.T) { AppLog = NewDefault(mainLoggerName) // make sure we can set and read context - ctx = WithRequestID(context.Background(), reqID) + ctx = withRequestID(context.Background(), reqID) contextualLogger := AppLog.WithContext(ctx) contextualLogger.Info(teststr) type entry struct { diff --git a/malfeasance/handler.go b/malfeasance/handler.go index a2c8a61c2c..45acdfcd54 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -26,7 +26,7 @@ var ( errMalformedData = fmt.Errorf("%w: malformed data", pubsub.ErrValidationReject) errWrongHash = fmt.Errorf("%w: incorrect hash", pubsub.ErrValidationReject) - errInvalidProof = fmt.Errorf("%w: invalid proof", pubsub.ErrValidationReject) + errUnknownProof = fmt.Errorf("%w: unknown proof type", pubsub.ErrValidationReject) ) type MalfeasanceType byte @@ -44,7 +44,6 @@ const ( InvalidActivation MalfeasanceType = iota + 10 InvalidBallot InvalidHareMsg - DoubleMarry = MalfeasanceType(wire.DoubleMarry) ) // Handler processes MalfeasanceProof from gossip and, if deems it valid, propagates it to peers. @@ -154,6 +153,7 @@ func (h *Handler) HandleMalfeasanceProof(ctx context.Context, peer p2p.Peer, dat func (h *Handler) validateAndSave(ctx context.Context, p *wire.MalfeasanceGossip) (types.NodeID, error) { if p.Eligibility != nil { + numMalformed.Inc() return types.EmptyNodeID, fmt.Errorf( "%w: eligibility field was deprecated with hare3", pubsub.ErrValidationReject, @@ -161,12 +161,12 @@ func (h *Handler) validateAndSave(ctx context.Context, p *wire.MalfeasanceGossip } nodeID, err := h.Validate(ctx, p) switch { - case errors.Is(err, errInvalidProof): + case errors.Is(err, errUnknownProof): numMalformed.Inc() return types.EmptyNodeID, err case err != nil: h.countInvalidProof(&p.MalfeasanceProof) - return types.EmptyNodeID, err + return types.EmptyNodeID, errors.Join(err, pubsub.ErrValidationReject) } if err := h.cdb.WithTx(ctx, func(dbtx sql.Transaction) error { malicious, err := identities.IsMalicious(dbtx, nodeID) @@ -198,7 +198,7 @@ func (h *Handler) validateAndSave(ctx context.Context, p *wire.MalfeasanceGossip h.reportMalfeasance(nodeID, &p.MalfeasanceProof) h.cdb.CacheMalfeasanceProof(nodeID, &p.MalfeasanceProof) h.countProof(&p.MalfeasanceProof) - h.logger.Info("new malfeasance proof", + h.logger.Debug("new malfeasance proof", log.ZContext(ctx), zap.Stringer("smesher", nodeID), zap.Inline(p), @@ -209,14 +209,14 @@ func (h *Handler) validateAndSave(ctx context.Context, p *wire.MalfeasanceGossip func (h *Handler) Validate(ctx context.Context, p *wire.MalfeasanceGossip) (types.NodeID, error) { mh, ok := h.handlersV1[MalfeasanceType(p.Proof.Type)] if !ok { - return types.EmptyNodeID, fmt.Errorf("%w: unknown malfeasance type", errInvalidProof) + return types.EmptyNodeID, fmt.Errorf("%w: unknown malfeasance type", errUnknownProof) } nodeID, err := mh.Validate(ctx, p.Proof.Data) if err == nil { return nodeID, nil } - h.logger.Warn("malfeasance proof failed validation", + h.logger.Debug("malfeasance proof failed validation", log.ZContext(ctx), zap.Inline(p), zap.Error(err), diff --git a/malfeasance/handler_test.go b/malfeasance/handler_test.go index e902712687..86532ce677 100644 --- a/malfeasance/handler_test.go +++ b/malfeasance/handler_test.go @@ -84,7 +84,7 @@ func TestHandler_HandleMalfeasanceProof(t *testing.T) { } err := h.HandleMalfeasanceProof(context.Background(), "peer", codec.MustEncode(gossip)) - require.ErrorIs(t, err, errInvalidProof) + require.ErrorIs(t, err, errUnknownProof) require.ErrorIs(t, err, pubsub.ErrValidationReject) }) @@ -114,6 +114,7 @@ func TestHandler_HandleMalfeasanceProof(t *testing.T) { err := h.HandleMalfeasanceProof(context.Background(), "peer", codec.MustEncode(gossip)) require.ErrorContains(t, err, "invalid proof") + require.ErrorIs(t, err, pubsub.ErrValidationReject) }) t.Run("valid proof", func(t *testing.T) { @@ -224,7 +225,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { "peer", codec.MustEncode(proof), ) - require.ErrorIs(t, err, errInvalidProof) + require.ErrorIs(t, err, errUnknownProof) require.ErrorIs(t, err, pubsub.ErrValidationReject) }) @@ -292,6 +293,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { codec.MustEncode(proof), ) require.ErrorContains(t, err, "invalid proof") + require.ErrorIs(t, err, pubsub.ErrValidationReject) }) t.Run("valid proof", func(t *testing.T) { diff --git a/malfeasance/wire/malfeasance.go b/malfeasance/wire/malfeasance.go index b4132ea568..0ffd8e4228 100644 --- a/malfeasance/wire/malfeasance.go +++ b/malfeasance/wire/malfeasance.go @@ -15,7 +15,7 @@ import ( "github.com/spacemeshos/go-spacemesh/common/types" ) -//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata,InvalidPostIndexProof,InvalidPrevATXProof,DoubleMarryProof +//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata,InvalidPostIndexProof,InvalidPrevATXProof const ( MultipleATXs byte = iota + 1 @@ -23,7 +23,6 @@ const ( HareEquivocation InvalidPostIndex InvalidPrevATX - DoubleMarry ) type MalfeasanceProof struct { @@ -325,12 +324,6 @@ type InvalidPrevATXProof struct { func (p *InvalidPrevATXProof) isProof() {} -type DoubleMarryProof struct { - // TODO: implement -} - -func (p *DoubleMarryProof) isProof() {} - func MalfeasanceInfo(smesher types.NodeID, mp *MalfeasanceProof) string { var b strings.Builder b.WriteString(fmt.Sprintf("generate layer: %v\n", mp.Layer)) diff --git a/malfeasance/wire/malfeasance_scale.go b/malfeasance/wire/malfeasance_scale.go index 6e23fd2175..3ec88a1acc 100644 --- a/malfeasance/wire/malfeasance_scale.go +++ b/malfeasance/wire/malfeasance_scale.go @@ -422,11 +422,3 @@ func (t *InvalidPrevATXProof) DecodeScale(dec *scale.Decoder) (total int, err er } return total, nil } - -func (t *DoubleMarryProof) EncodeScale(enc *scale.Encoder) (total int, err error) { - return total, nil -} - -func (t *DoubleMarryProof) DecodeScale(dec *scale.Decoder) (total int, err error) { - return total, nil -} diff --git a/mesh/executor.go b/mesh/executor.go index edbc4e36e1..ec6a1cb04c 100644 --- a/mesh/executor.go +++ b/mesh/executor.go @@ -9,6 +9,8 @@ import ( "sync" "time" + "go.uber.org/zap" + "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/types" vm "github.com/spacemeshos/go-spacemesh/genvm" @@ -25,7 +27,7 @@ var ( ) type Executor struct { - logger log.Log + logger *zap.Logger db sql.Executor atxsdata *atxsdata.Data vm vmState @@ -34,7 +36,7 @@ type Executor struct { mu sync.Mutex } -func NewExecutor(db sql.Executor, atxsdata *atxsdata.Data, vm vmState, cs conservativeState, lg log.Log) *Executor { +func NewExecutor(db sql.Executor, atxsdata *atxsdata.Data, vm vmState, cs conservativeState, lg *zap.Logger) *Executor { return &Executor{ logger: lg, db: db, @@ -59,10 +61,10 @@ func (e *Executor) Revert(ctx context.Context, revertTo types.LayerID) error { if err != nil { return fmt.Errorf("get state hash: %w", err) } - e.logger.With().Info("reverted state", - log.Context(ctx), - log.Stringer("state_hash", root), - log.Uint32("revert_to", revertTo.Uint32()), + e.logger.Info("reverted state", + log.ZContext(ctx), + zap.Stringer("state_hash", root), + zap.Uint32("revert_to", revertTo.Uint32()), ) return nil } @@ -115,15 +117,15 @@ func (e *Executor) ExecuteOptimistic( if err != nil { return nil, fmt.Errorf("get state hash: %w", err) } - e.logger.With().Info("optimistically executed block", - log.Context(ctx), - log.Uint32("lid", lid.Uint32()), - log.Stringer("block", b.ID()), - log.Stringer("state_hash", state), - log.Duration("duration", time.Since(start)), - log.Int("count", len(executed)), - log.Int("skipped", len(ineffective)), - log.Int("rewards", len(b.Rewards)), + e.logger.Debug("optimistically executed block", + log.ZContext(ctx), + zap.Uint32("lid", lid.Uint32()), + zap.Stringer("block", b.ID()), + zap.Stringer("state_hash", state), + zap.Duration("duration", time.Since(start)), + zap.Int("count", len(executed)), + zap.Int("skipped", len(ineffective)), + zap.Int("rewards", len(b.Rewards)), ) return b, nil } @@ -164,14 +166,14 @@ func (e *Executor) Execute(ctx context.Context, lid types.LayerID, block *types. if err != nil { return fmt.Errorf("get state hash: %w", err) } - e.logger.With().Info("executed block", - log.Context(ctx), - log.Uint32("lid", lid.Uint32()), - log.Stringer("block", block.ID()), - log.Stringer("state_hash", state), - log.Duration("duration", time.Since(start)), - log.Int("count", len(executed)), - log.Int("rewards", len(rewards)), + e.logger.Debug("executed block", + log.ZContext(ctx), + zap.Uint32("lid", lid.Uint32()), + zap.Stringer("block", block.ID()), + zap.Stringer("state_hash", state), + zap.Duration("duration", time.Since(start)), + zap.Int("count", len(executed)), + zap.Int("rewards", len(rewards)), ) return nil } @@ -207,11 +209,11 @@ func (e *Executor) executeEmpty(ctx context.Context, lid types.LayerID) error { if err != nil { return fmt.Errorf("get state hash: %w", err) } - e.logger.With().Info("executed empty layer", - log.Context(ctx), - log.Uint32("lid", lid.Uint32()), - log.Stringer("state_hash", state), - log.Duration("duration", time.Since(start)), + e.logger.Info("executed empty layer", + log.ZContext(ctx), + zap.Uint32("lid", lid.Uint32()), + zap.Stringer("state_hash", state), + zap.Duration("duration", time.Since(start)), ) return nil } diff --git a/mesh/executor_test.go b/mesh/executor_test.go index 2e5732ed08..c15367f715 100644 --- a/mesh/executor_test.go +++ b/mesh/executor_test.go @@ -11,11 +11,11 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/types" vm "github.com/spacemeshos/go-spacemesh/genvm" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/mesh" "github.com/spacemeshos/go-spacemesh/mesh/mocks" "github.com/spacemeshos/go-spacemesh/signing" @@ -49,8 +49,7 @@ func newTestExecutor(t *testing.T) *testExecutor { mvm: mocks.NewMockvmState(ctrl), mcs: mocks.NewMockconservativeState(ctrl), } - lg := logtest.New(t) - te.exec = mesh.NewExecutor(te.db, te.atxsdata, te.mvm, te.mcs, lg) + te.exec = mesh.NewExecutor(te.db, te.atxsdata, te.mvm, te.mcs, zaptest.NewLogger(t)) return te } diff --git a/mesh/malfeasance.go b/mesh/malfeasance.go index 0c97936c52..ad3f090e2a 100644 --- a/mesh/malfeasance.go +++ b/mesh/malfeasance.go @@ -74,7 +74,7 @@ func (mh *MalfeasanceHandler) Validate(ctx context.Context, data wire.ProofData) msg1.InnerMsg.MsgHash != msg2.InnerMsg.MsgHash { return msg1.SmesherID, nil } - mh.logger.Warn("received invalid ballot malfeasance proof", + mh.logger.Debug("received invalid ballot malfeasance proof", log.ZContext(ctx), zap.Stringer("first_smesher", bp.Messages[0].SmesherID), zap.Object("first_proof", &bp.Messages[0].InnerMsg), diff --git a/mesh/mesh.go b/mesh/mesh.go index 94b8b2dc8c..61f05a25d8 100644 --- a/mesh/mesh.go +++ b/mesh/mesh.go @@ -12,6 +12,7 @@ import ( "sync/atomic" "time" + "go.uber.org/zap" "go.uber.org/zap/zapcore" "github.com/spacemeshos/go-spacemesh/atxsdata" @@ -34,7 +35,7 @@ import ( // Mesh is the logic layer above our mesh.DB database. type Mesh struct { - logger log.Log + logger *zap.Logger cdb sql.StateDatabase atxsdata *atxsdata.Data clock layerClock @@ -64,7 +65,7 @@ func NewMesh( trtl system.Tortoise, exec *Executor, state conservativeState, - logger log.Log, + logger *zap.Logger, ) (*Mesh, error) { msh := &Mesh{ logger: logger, @@ -104,7 +105,7 @@ func NewMesh( } return nil }); err != nil { - msh.logger.With().Panic("error initialize genesis data", log.Err(err)) + msh.logger.Panic("error initialize genesis data", zap.Error(err)) } msh.setLatestLayer(genesis) @@ -118,25 +119,27 @@ func (msh *Mesh) recoverFromDB(latest types.LayerID) { lyr, err := layers.GetProcessed(msh.cdb) if err != nil { - msh.logger.With().Fatal("failed to recover processed layer", log.Err(err)) + msh.logger.Fatal("failed to recover processed layer", zap.Error(err)) } msh.processedLayer.Store(lyr) applied, err := layers.GetLastApplied(msh.cdb) if err != nil { - msh.logger.With().Fatal("failed to recover latest applied layer", log.Err(err)) + msh.logger.Fatal("failed to recover latest applied layer", zap.Error(err)) } msh.setLatestLayerInState(applied) if applied.After(types.GetEffectiveGenesis()) { if err = msh.executor.Revert(context.Background(), applied); err != nil { - msh.logger.With(). - Fatal("failed to load state for layer", msh.LatestLayerInState(), log.Err(err)) + msh.logger.Fatal("failed to load state for layer", + zap.Error(err), + zap.Uint32("layer", msh.LatestLayerInState().Uint32()), + ) } } - msh.logger.With().Info("recovered mesh from disk", - log.Stringer("latest", msh.LatestLayer()), - log.Stringer("processed", msh.ProcessedLayer())) + msh.logger.Info("recovered mesh from disk", + zap.Stringer("latest", msh.LatestLayer()), + zap.Stringer("processed", msh.ProcessedLayer())) } // LatestLayerInState returns the latest layer we applied to state. @@ -258,11 +261,11 @@ func (msh *Mesh) ensureStateConsistent(ctx context.Context, results []result.Lay } if bid := layer.FirstValid(); bid != applied { - msh.logger.With().Debug("incorrect block applied", - log.Context(ctx), - log.Uint32("layer_id", layer.Layer.Uint32()), - log.Stringer("expected", bid), - log.Stringer("applied", applied), + msh.logger.Debug("incorrect block applied", + log.ZContext(ctx), + zap.Uint32("layer_id", layer.Layer.Uint32()), + zap.Stringer("expected", bid), + zap.Stringer("applied", applied), ) changed = min(changed, layer.Layer) } @@ -297,10 +300,10 @@ func (msh *Mesh) ProcessLayer(ctx context.Context, lid types.LayerID) error { next := msh.LatestLayerInState() + 1 // TODO(dshulyak) https://github.com/spacemeshos/go-spacemesh/issues/4425 if len(results) > 0 { - msh.logger.With().Debug("consensus results", - log.Context(ctx), - log.Uint32("layer_id", lid.Uint32()), - log.Array("results", log.ArrayMarshalerFunc(func(encoder log.ArrayEncoder) error { + msh.logger.Debug("consensus results", + log.ZContext(ctx), + zap.Uint32("layer_id", lid.Uint32()), + zap.Array("results", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { for i := range results { encoder.AppendObject(&results[i]) } @@ -411,10 +414,10 @@ func (msh *Mesh) applyResults(ctx context.Context, results []result.Layer) error } func (msh *Mesh) saveHareOutput(ctx context.Context, lid types.LayerID, bid types.BlockID) error { - msh.logger.With().Debug("saving hare output", - log.Context(ctx), - log.Uint32("lid", lid.Uint32()), - log.Stringer("block", bid), + msh.logger.Debug("saving hare output", + log.ZContext(ctx), + zap.Uint32("lid", lid.Uint32()), + zap.Stringer("block", bid), ) var ( certs []certificates.CertValidity @@ -456,9 +459,9 @@ func (msh *Mesh) saveHareOutput(ctx context.Context, lid types.LayerID, bid type return err } if len(certs) > 1 { - msh.logger.With().Warning("multiple certificates found in network", - log.Context(ctx), - log.Object("certificates", log.ObjectMarshallerFunc(func(encoder zapcore.ObjectEncoder) error { + msh.logger.Warn("multiple certificates found in network", + log.ZContext(ctx), + zap.Object("certificates", zapcore.ObjectMarshalerFunc(func(encoder zapcore.ObjectEncoder) error { for _, cert := range certs { encoder.AddString("block_id", cert.Block.String()) encoder.AddBool("valid", cert.Valid) @@ -569,16 +572,16 @@ func (msh *Mesh) AddBallot( } encoded, err := codec.Encode(proof) if err != nil { - msh.logger.With().Panic("failed to encode MalfeasanceProof", log.Err(err)) + msh.logger.Panic("failed to encode MalfeasanceProof", zap.Error(err)) } if err := identities.SetMalicious(dbtx, ballot.SmesherID, encoded, time.Now()); err != nil { return fmt.Errorf("add malfeasance proof: %w", err) } ballot.SetMalicious() - msh.logger.With().Warning("smesher produced more than one ballot in the same layer", - log.Stringer("smesher", ballot.SmesherID), - log.Object("prev", prev), - log.Object("curr", ballot), + msh.logger.Warn("smesher produced more than one ballot in the same layer", + zap.Stringer("smesher", ballot.SmesherID), + zap.Object("prev", prev), + zap.Object("curr", ballot), ) } } diff --git a/mesh/mesh_test.go b/mesh/mesh_test.go index 795564a4aa..f32362948c 100644 --- a/mesh/mesh_test.go +++ b/mesh/mesh_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/fixture" @@ -15,7 +16,6 @@ import ( "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/genvm/sdk/wallet" "github.com/spacemeshos/go-spacemesh/hash" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/mesh/mocks" "github.com/spacemeshos/go-spacemesh/signing" "github.com/spacemeshos/go-spacemesh/sql" @@ -50,13 +50,13 @@ type testMesh struct { func createTestMesh(t *testing.T) *testMesh { t.Helper() types.SetLayersPerEpoch(3) - lg := logtest.New(t) + lg := zaptest.NewLogger(t) db := statesql.InMemory() atxsdata := atxsdata.New() ctrl := gomock.NewController(t) tm := &testMesh{ db: db, - cdb: datastore.NewCachedDB(db, lg.Zap()), + cdb: datastore.NewCachedDB(db, lg), atxsdata: atxsdata, mockClock: mocks.NewMocklayerClock(ctrl), mockVM: mocks.NewMockvmState(ctrl), @@ -205,7 +205,7 @@ func TestMesh_WakeUpWhileGenesis(t *testing.T) { tm.mockTortoise, tm.executor, tm.mockState, - logtest.New(t), + zaptest.NewLogger(t), ) require.NoError(t, err) gLid := types.GetEffectiveGenesis() @@ -243,7 +243,7 @@ func TestMesh_WakeUp(t *testing.T) { tm.mockTortoise, tm.executor, tm.mockState, - logtest.New(t), + zaptest.NewLogger(t), ) require.NoError(t, err) gotL := msh.LatestLayer() @@ -390,7 +390,7 @@ func TestMesh_MaliciousBallots(t *testing.T) { require.NotNil(t, malProof) require.True(t, blts[1].IsMalicious()) - mh := NewMalfeasanceHandler(tm.cdb, signing.NewEdVerifier(), WithMalfeasanceLogger(tm.logger.Zap())) + mh := NewMalfeasanceHandler(tm.cdb, signing.NewEdVerifier(), WithMalfeasanceLogger(tm.logger)) nodeID, err := mh.Validate(context.Background(), malProof.Proof.Data) require.NoError(t, err) require.Equal(t, sig.NodeID(), nodeID) diff --git a/metrics/common.go b/metrics/common.go index 23898e7b27..46860e9fb3 100644 --- a/metrics/common.go +++ b/metrics/common.go @@ -75,3 +75,11 @@ func ReportMessageLatency(protocol, msgType string, latency time.Duration) { } receivedMessagesLatency.WithLabelValues(protocol, msgType, sign).Observe(seconds) } + +func NewCounterOpts(ns, name, help string) prometheus.CounterOpts { + return prometheus.CounterOpts{ + Namespace: ns, + Name: name, + Help: help, + } +} diff --git a/miner/proposal_builder.go b/miner/proposal_builder.go index b499903b2d..96c0ee46e4 100644 --- a/miner/proposal_builder.go +++ b/miner/proposal_builder.go @@ -467,7 +467,7 @@ func (pb *ProposalBuilder) initSharedData(ctx context.Context, current types.Lay if err != nil { return err } - pb.logger.Info("loaded prepared active set", + pb.logger.Debug("loaded prepared active set", zap.Uint32("epoch_id", pb.shared.epoch.Uint32()), log.ZShortStringer("id", id), zap.Int("size", len(set)), diff --git a/node/node.go b/node/node.go index ca68c12af4..5643d6a6d2 100644 --- a/node/node.go +++ b/node/node.go @@ -54,6 +54,7 @@ import ( "github.com/spacemeshos/go-spacemesh/hare3" "github.com/spacemeshos/go-spacemesh/hare3/compat" "github.com/spacemeshos/go-spacemesh/hare3/eligibility" + "github.com/spacemeshos/go-spacemesh/hare4" "github.com/spacemeshos/go-spacemesh/hash" "github.com/spacemeshos/go-spacemesh/layerpatrol" "github.com/spacemeshos/go-spacemesh/log" @@ -398,6 +399,8 @@ type App struct { atxsdata *atxsdata.Data clock *timesync.NodeClock hare3 *hare3.Hare + hare4 *hare4.Hare + hareResultsChan chan hare4.ConsensusOutput hOracle *eligibility.Oracle blockGen *blocks.Generator certifier *blocks.Certifier @@ -618,7 +621,7 @@ func (app *App) initServices(ctx context.Context) error { cfg.GenesisID = app.Config.Genesis.GenesisID() state := vm.New(app.db, vm.WithConfig(cfg), - vm.WithLogger(app.addLogger(VMLogger, lg))) + vm.WithLogger(app.addLogger(VMLogger, lg).Zap())) app.conState = txs.NewConservativeState(state, app.db, txs.WithCSConfig(txs.CSConfig{ BlockGasLimit: app.Config.BlockGasLimit, @@ -705,15 +708,15 @@ func (app *App) initServices(ctx context.Context) error { app.atxsdata, state, app.conState, - app.addLogger(ExecutorLogger, lg), + app.addLogger(ExecutorLogger, lg).Zap(), ) - mlog := app.addLogger(MeshLogger, lg) + mlog := app.addLogger(MeshLogger, lg).Zap() msh, err := mesh.NewMesh(app.db, app.atxsdata, app.clock, trtl, executor, app.conState, mlog) if err != nil { return fmt.Errorf("create mesh: %w", err) } - pruner := prune.New(app.db, app.Config.Tortoise.Hdist, app.Config.PruneActivesetsFrom, prune.WithLogger(mlog.Zap())) + pruner := prune.New(app.db, app.Config.Tortoise.Hdist, app.Config.PruneActivesetsFrom, prune.WithLogger(mlog)) if err := pruner.Prune(app.clock.CurrentLayer()); err != nil { return fmt.Errorf("pruner %w", err) } @@ -771,7 +774,7 @@ func (app *App) initServices(ctx context.Context) error { vrfVerifier, app.Config.LayersPerEpoch, eligibility.WithConfig(app.Config.HareEligibility), - eligibility.WithLogger(app.addLogger(HareOracleLogger, lg)), + eligibility.WithLogger(app.addLogger(HareOracleLogger, lg).Zap()), ) // TODO: genesisMinerWeight is set to app.Config.SpaceToCommit, because PoET ticks are currently hardcoded to 1 @@ -781,7 +784,7 @@ func (app *App) initServices(ctx context.Context) error { app.updater = bootstrap.New( app.clock, bootstrap.WithConfig(bscfg), - bootstrap.WithLogger(app.addLogger(BootstrapLogger, lg)), + bootstrap.WithLogger(app.addLogger(BootstrapLogger, lg).Zap()), ) if app.Config.Certificate.CommitteeSize == 0 { app.log.With().Warning("certificate committee size is not set, defaulting to hare committee size", @@ -864,37 +867,80 @@ func (app *App) initServices(ctx context.Context) error { } logger := app.addLogger(HareLogger, lg).Zap() - app.hare3 = hare3.New( - app.clock, - app.host, - app.db, - app.atxsdata, - proposalsStore, - app.edVerifier, - app.hOracle, - newSyncer, - patrol, - hare3.WithLogger(logger), - hare3.WithConfig(app.Config.HARE3), - ) - for _, sig := range app.signers { - app.hare3.Register(sig) + // should be removed after hare4 transition is complete + app.hareResultsChan = make(chan hare4.ConsensusOutput, 32) + if app.Config.HARE3.Enable { + app.hare3 = hare3.New( + app.clock, + app.host, + app.db, + app.atxsdata, + proposalsStore, + app.edVerifier, + app.hOracle, + newSyncer, + patrol, + hare3.WithLogger(logger), + hare3.WithConfig(app.Config.HARE3), + hare3.WithResultsChan(app.hareResultsChan), + ) + for _, sig := range app.signers { + app.hare3.Register(sig) + } + app.hare3.Start() + app.eg.Go(func() error { + compat.ReportWeakcoin( + ctx, + logger, + app.hare3.Coins(), + tortoiseWeakCoin{db: app.cachedDB, tortoise: trtl}, + ) + return nil + }) } - app.hare3.Start() - app.eg.Go(func() error { - compat.ReportWeakcoin( - ctx, - logger, - app.hare3.Coins(), - tortoiseWeakCoin{db: app.cachedDB, tortoise: trtl}, + + if app.Config.HARE4.Enable { + app.hare4 = hare4.New( + app.clock, + app.host, + app.db, + app.atxsdata, + proposalsStore, + app.edVerifier, + app.hOracle, + newSyncer, + patrol, + app.host, + hare4.WithLogger(logger), + hare4.WithConfig(app.Config.HARE4), + hare4.WithResultsChan(app.hareResultsChan), ) - return nil - }) + for _, sig := range app.signers { + app.hare4.Register(sig) + } + app.hare4.Start() + app.eg.Go(func() error { + compat.ReportWeakcoin( + ctx, + logger, + app.hare4.Coins(), + tortoiseWeakCoin{db: app.cachedDB, tortoise: trtl}, + ) + return nil + }) + panic("hare4 still not enabled") + } + + propHare := &proposalConsumerHare{ + hare3: app.hare3, + h3DisableLayer: app.Config.HARE3.DisableLayer, + hare4: app.hare4, + } proposalListener := proposals.NewHandler( app.db, app.atxsdata, - app.hare3, + propHare, app.edVerifier, app.host, fetcherWrapped, @@ -903,7 +949,7 @@ func (app *App) initServices(ctx context.Context) error { trtl, vrfVerifier, app.clock, - proposals.WithLogger(app.addLogger(ProposalListenerLogger, lg)), + proposals.WithLogger(app.addLogger(ProposalListenerLogger, lg).Zap()), proposals.WithConfig(proposals.Config{ LayerSize: layerSize, LayersPerEpoch: layersPerEpoch, @@ -928,7 +974,7 @@ func (app *App) initServices(ctx context.Context) error { OptFilterThreshold: app.Config.OptFilterThreshold, GenBlockInterval: 500 * time.Millisecond, }), - blocks.WithHareOutputChan(app.hare3.Results()), + blocks.WithHareOutputChan(app.hareResultsChan), blocks.WithGeneratorLogger(app.addLogger(BlockGenLogger, lg).Zap()), ) @@ -996,7 +1042,7 @@ func (app *App) initServices(ctx context.Context) error { activation.WithCertifier(certifier), ) if err != nil { - app.log.Panic("failed to create poet client: %v", err) + app.log.Panic("failed to create poet client with address %v: %v", server.Address, err) } poetClients = append(poetClients, client) } @@ -1230,7 +1276,7 @@ func (app *App) initServices(ctx context.Context) error { app.ptimesync = peersync.New( app.host, app.host, - peersync.WithLog(app.addLogger(TimeSyncLogger, lg)), + peersync.WithLog(app.addLogger(TimeSyncLogger, lg).Zap()), peersync.WithConfig(app.Config.TIME.Peersync), ) } @@ -1512,8 +1558,7 @@ func (app *App) grpcService(svc grpcserver.Service, lg log.Log) (grpcserver.Serv case v2alpha1.Network: service := v2alpha1.NewNetworkService( app.clock.GenesisTime(), - app.Config.Genesis.GenesisID(), - app.Config.LayerDuration) + app.Config) app.grpcServices[svc] = service return service, nil case v2alpha1.Node: @@ -1760,7 +1805,7 @@ func (app *App) startAPIServices(ctx context.Context) error { app.Config.CollectMetrics, ) - if err := app.jsonAPIServer.StartService(ctx, maps.Values(publicSvcs)...); err != nil { + if err := app.jsonAPIServer.StartService(maps.Values(publicSvcs)...); err != nil { return fmt.Errorf("start listen server: %w", err) } logger.With().Info("json listener started", @@ -1827,6 +1872,14 @@ func (app *App) stopServices(ctx context.Context) { app.hare3.Stop() } + if app.hare4 != nil { + app.hare4.Stop() + } + + if app.hareResultsChan != nil { + close(app.hareResultsChan) + } + if app.blockGen != nil { app.blockGen.Stop() } @@ -1896,7 +1949,7 @@ func (app *App) setupDBs(ctx context.Context, lg log.Log) error { if err := os.MkdirAll(dbPath, os.ModePerm); err != nil { return fmt.Errorf("failed to create %s: %w", dbPath, err) } - dbLog := app.addLogger(StateDbLogger, lg) + dbLog := app.addLogger(StateDbLogger, lg).Zap() schema, err := statesql.Schema() if err != nil { return fmt.Errorf("error loading db schema: %w", err) @@ -1905,7 +1958,7 @@ func (app *App) setupDBs(ctx context.Context, lg log.Log) error { schema.SkipMigrations(app.Config.DatabaseSkipMigrations...) } dbopts := []sql.Opt{ - sql.WithLogger(dbLog.Zap()), + sql.WithLogger(dbLog), sql.WithDatabaseSchema(schema), sql.WithConnections(app.Config.DatabaseConnections), sql.WithLatencyMetering(app.Config.DatabaseLatencyMetering), @@ -1931,37 +1984,32 @@ func (app *App) setupDBs(ctx context.Context, lg log.Log) error { app.Config.DatabaseSizeMeteringInterval, ) } - app.log.Info("starting cache warmup") - applied, err := layers.GetLastApplied(app.db) - if err != nil { - return err - } - start := time.Now() - data, err := atxsdata.Warm( - app.db, - app.Config.Tortoise.WindowSizeEpochs(applied), - ) - if err != nil { - return err + { + warmupLog := app.log.Zap().Named("warmup") + app.log.Info("starting cache warmup") + applied, err := layers.GetLastApplied(app.db) + if err != nil { + return err + } + start := time.Now() + data, err := atxsdata.Warm( + app.db, + app.Config.Tortoise.WindowSizeEpochs(applied), + warmupLog, + ) + if err != nil { + return err + } + app.atxsdata = data + app.log.With().Info("cache warmup", log.Duration("duration", time.Since(start))) } - app.atxsdata = data - app.log.With().Info("cache warmup", log.Duration("duration", time.Since(start))) app.cachedDB = datastore.NewCachedDB(sqlDB, app.addLogger(CachedDBLogger, lg).Zap(), datastore.WithConfig(app.Config.Cache), - datastore.WithConsensusCache(data), + datastore.WithConsensusCache(app.atxsdata), ) - if app.Config.ScanMalfeasantATXs { - app.log.With().Info("checking DB for malicious ATXs") - start = time.Now() - if err := activation.CheckPrevATXs(ctx, app.log.Zap(), app.db); err != nil { - return fmt.Errorf("malicious ATX check: %w", err) - } - app.log.With().Info("malicious ATX check completed", log.Duration("duration", time.Since(start))) - } - localDB, err := localsql.Open("file:"+filepath.Join(dbPath, localDbFile), - sql.WithLogger(dbLog.Zap()), + sql.WithLogger(dbLog), sql.WithConnections(app.Config.DatabaseConnections), sql.WithAllowSchemaDrift(app.Config.DatabaseSchemaAllowDrift), ) @@ -2236,3 +2284,25 @@ func (w tortoiseWeakCoin) Set(lid types.LayerID, value bool) error { func onMainNet(conf *config.Config) bool { return conf.Genesis.GenesisTime == config.MainnetConfig().Genesis.GenesisTime } + +// proposalConsumerHare is used for the hare3->hare4 migration +// to satisfy the proposals handler dependency on hare. +type proposalConsumerHare struct { + hare3 *hare3.Hare + h3DisableLayer types.LayerID + hare4 *hare4.Hare +} + +func (p *proposalConsumerHare) IsKnown(layer types.LayerID, proposal types.ProposalID) bool { + if layer < p.h3DisableLayer { + return p.hare3.IsKnown(layer, proposal) + } + return p.hare4.IsKnown(layer, proposal) +} + +func (p *proposalConsumerHare) OnProposal(proposal *types.Proposal) error { + if proposal.Layer < p.h3DisableLayer { + return p.hare3.OnProposal(proposal) + } + return p.hare4.OnProposal(proposal) +} diff --git a/node/node_test.go b/node/node_test.go index 513ca40c4d..274ed57ca7 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -997,7 +997,7 @@ func TestAdminEvents(t *testing.T) { select { case <-app.Started(): - case <-time.After(10 * time.Second): + case <-time.After(15 * time.Second): require.Fail(t, "app did not start in time") } @@ -1090,7 +1090,7 @@ func TestAdminEvents_MultiSmesher(t *testing.T) { select { case <-app.Started(): - case <-time.After(10 * time.Second): + case <-time.After(15 * time.Second): require.Fail(t, "app did not start in time") } @@ -1291,6 +1291,9 @@ func getTestDefaultConfig(tb testing.TB) *config.Config { cfg.HARE3.RoundDuration = 2 cfg.HARE3.PreroundDelay = 1 + cfg.HARE4.RoundDuration = 2 + cfg.HARE4.PreroundDelay = 1 + cfg.LayerAvgSize = 5 cfg.LayersPerEpoch = 3 cfg.TxsPerProposal = 100 diff --git a/p2p/ping.go b/p2p/ping.go index c421f96963..8a9171febc 100644 --- a/p2p/ping.go +++ b/p2p/ping.go @@ -109,7 +109,7 @@ func (p *Ping) runForPeer(ctx context.Context, peerID peer.ID, addrs []ma.Multia if r.Error != nil { return r.Error } else { - p.logger.Info("PING succeeded", + p.logger.Debug("PING succeeded", zap.Stringer("peer", peerID), zap.Any("addrs", addrs), zap.Duration("rtt", r.RTT)) diff --git a/p2p/server/server.go b/p2p/server/server.go index 38d52dabbf..a1ead5c8d9 100644 --- a/p2p/server/server.go +++ b/p2p/server/server.go @@ -516,7 +516,7 @@ func ReadResponse(r io.Reader, toCall func(resLen uint32) (int, error)) (int, er n, err := toCall(respLen) nBytes += n if err != nil { - return nBytes, err + return nBytes, fmt.Errorf("callback error: %w", err) } if int(respLen) != n { return nBytes, errors.New("malformed server response") @@ -526,7 +526,7 @@ func ReadResponse(r io.Reader, toCall func(resLen uint32) (int, error)) (int, er nBytes += n switch { case err != nil: - return nBytes, err + return nBytes, fmt.Errorf("decode error: %w", err) case errStr != "": return nBytes, NewServerError(errStr) case respLen == 0: diff --git a/proposals/eligibility_validator.go b/proposals/eligibility_validator.go index a9f5faf000..3f9c62d1d7 100644 --- a/proposals/eligibility_validator.go +++ b/proposals/eligibility_validator.go @@ -5,11 +5,11 @@ import ( "fmt" "github.com/spacemeshos/fixed" + "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/fetch" - "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/miner/minweight" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/system" @@ -25,7 +25,7 @@ type Validator struct { atxsdata *atxsdata.Data clock layerClock beacons system.BeaconCollector - logger log.Log + logger *zap.Logger vrfVerifier vrfVerifier } @@ -40,7 +40,7 @@ func NewEligibilityValidator( tortoise tortoiseProvider, atxsdata *atxsdata.Data, bc system.BeaconCollector, - lg log.Log, + lg *zap.Logger, vrfVerifier vrfVerifier, opts ...ValidatorOpt, ) *Validator { @@ -116,7 +116,7 @@ func (v *Validator) CheckEligibility(ctx context.Context, ballot *types.Ballot, return fmt.Errorf( "%w: proof contains incorrect VRF signature. beacon: %v, epoch: %v, counter: %v, vrfSig: %s", fetch.ErrIgnore, - data.Beacon.ShortString(), + data.Beacon, ballot.Layer.GetEpoch(), proof.J, proof.Sig, @@ -129,11 +129,11 @@ func (v *Validator) CheckEligibility(ctx context.Context, ballot *types.Ballot, } } - v.logger.WithContext(ctx).With().Debug("ballot eligibility verified", - ballot.ID(), - ballot.Layer, - ballot.Layer.GetEpoch(), - data.Beacon, + v.logger.Debug("ballot eligibility verified", + zap.Stringer("id", ballot.ID()), + zap.Uint32("layer", ballot.Layer.Uint32()), + zap.Uint32("layer", ballot.Layer.GetEpoch().Uint32()), + zap.Stringer("beacon", data.Beacon), ) v.beacons.ReportBeaconFromBallot(ballot.Layer.GetEpoch(), ballot, data.Beacon, diff --git a/proposals/eligibility_validator_test.go b/proposals/eligibility_validator_test.go index acdcc9203c..2168c6e5d4 100644 --- a/proposals/eligibility_validator_test.go +++ b/proposals/eligibility_validator_test.go @@ -7,10 +7,10 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/tortoise" ) @@ -553,7 +553,6 @@ func TestEligibilityValidator(t *testing.T) { } }).AnyTimes() - lg := logtest.New(t) c := atxsdata.New() c.EvictEpoch(tc.evicted) tv := NewEligibilityValidator( @@ -564,7 +563,7 @@ func TestEligibilityValidator(t *testing.T) { ms.md, c, ms.mbc, - lg, + zaptest.NewLogger(t), ms.mvrf, ) for _, atx := range tc.atxs { diff --git a/proposals/handler.go b/proposals/handler.go index adcb14f27b..c1bac83869 100644 --- a/proposals/handler.go +++ b/proposals/handler.go @@ -10,6 +10,7 @@ import ( lru "github.com/hashicorp/golang-lru/v2" "github.com/prometheus/client_golang/prometheus" + "go.uber.org/zap" "golang.org/x/exp/slices" "github.com/spacemeshos/go-spacemesh/atxsdata" @@ -46,7 +47,7 @@ var ( // Handler processes Proposal from gossip and, if deems it valid, propagates it to peers. type Handler struct { - logger log.Log + logger *zap.Logger cfg Config db sql.StateDatabase @@ -93,7 +94,7 @@ func withValidator(v eligibilityValidator) Opt { } // WithLogger defines logger for Handler. -func WithLogger(logger log.Log) Opt { +func WithLogger(logger *zap.Logger) Opt { return func(h *Handler) { h.logger = logger } @@ -126,7 +127,7 @@ func NewHandler( panic(err) } b := &Handler{ - logger: log.NewNop(), + logger: zap.NewNop(), cfg: defaultConfig(), db: db, atxsdata: atxsdata, @@ -161,8 +162,6 @@ func NewHandler( // HandleSyncedBallot handles Ballot data from sync. func (h *Handler) HandleSyncedBallot(ctx context.Context, expHash types.Hash32, peer p2p.Peer, data []byte) error { - logger := h.logger.WithContext(ctx) - var b types.Ballot t0 := time.Now() if err := codec.Decode(data, &b); err != nil { @@ -205,8 +204,7 @@ func (h *Handler) HandleSyncedBallot(ctx context.Context, expHash types.Hash32, h.fetcher.RegisterPeerHashes(peer, collectHashes(b)) ballotDuration.WithLabelValues(peerHashes).Observe(float64(time.Since(t1))) - logger = logger.WithFields(b.ID(), b.Layer) - if _, err := h.processBallot(ctx, logger, &b); err != nil { + if _, err := h.processBallot(ctx, &b); err != nil { if errors.Is(err, errKnownBallot) { return nil } @@ -284,15 +282,13 @@ func (h *Handler) HandleSyncedProposal(ctx context.Context, expHash types.Hash32 func (h *Handler) HandleProposal(ctx context.Context, peer p2p.Peer, data []byte) error { err := h.handleProposal(ctx, types.Hash32{}, peer, data) if err != nil { - h.logger.WithContext(ctx).With().Debug("failed to process proposal gossip", log.Err(err)) + h.logger.Debug("failed to process proposal gossip", log.ZContext(ctx), zap.Error(err)) } return err } // HandleProposal is the gossip receiver for Proposal. func (h *Handler) handleProposal(ctx context.Context, expHash types.Hash32, peer p2p.Peer, data []byte) error { - logger := h.logger.WithContext(ctx) - t0 := time.Now() var p types.Proposal if err := codec.Decode(data, &p); err != nil { @@ -339,19 +335,24 @@ func (h *Handler) handleProposal(ctx context.Context, expHash types.Hash32, peer } proposalDuration.WithLabelValues(decodeInit).Observe(float64(time.Since(t0))) - logger = logger.WithFields(p.ID(), p.Ballot.ID(), p.Layer) + logger := h.logger.With( + log.ZContext(ctx), + zap.Stringer("proposal", p.ID()), + zap.Stringer("ballot", p.Ballot.ID()), + zap.Uint32("layer", p.Layer.Uint32()), + ) if h.proposals.IsKnown(p.Layer, p.ID()) { known.Inc() return fmt.Errorf("%w proposal %s", errKnownProposal, p.ID()) } - logger.With().Info("new proposal", log.ShortStringer("exp hash", expHash), log.Int("num_txs", len(p.TxIDs))) + logger.Debug("new proposal", log.ZShortStringer("exp hash", expHash), zap.Int("num_txs", len(p.TxIDs))) t2 := time.Now() h.fetcher.RegisterPeerHashes(peer, collectHashes(p)) proposalDuration.WithLabelValues(peerHashes).Observe(float64(time.Since(t2))) t3 := time.Now() - proof, err := h.processBallot(ctx, logger, &p.Ballot) + proof, err := h.processBallot(ctx, &p.Ballot) if err != nil && !errors.Is(err, errKnownBallot) && !errors.Is(err, errMaliciousBallot) { return err } @@ -367,7 +368,7 @@ func (h *Handler) handleProposal(ctx context.Context, expHash types.Hash32, peer if err := h.setProposalBeacon(&p); err != nil { return fmt.Errorf("setting proposal beacon: %w", err) } - logger.With().Debug("proposal is syntactically valid") + logger.Debug("proposal is syntactically valid") err = h.proposals.OnProposal(&p) switch { @@ -377,11 +378,11 @@ func (h *Handler) handleProposal(ctx context.Context, expHash types.Hash32, peer case err != nil: return fmt.Errorf("saving proposal: %w", err) } - logger.With().Debug("stored proposal") + logger.Debug("stored proposal") t6 := time.Now() if err = h.mesh.AddTXsFromProposal(ctx, p.Layer, p.ID(), p.TxIDs); err != nil { - logger.With().Error("failed to link txs to proposal", log.Err(err)) + logger.Error("failed to link txs to proposal", zap.Error(err)) return fmt.Errorf("proposal add TXs: %w", err) } proposalDuration.WithLabelValues(linkTxs).Observe(float64(time.Since(t6))) @@ -396,11 +397,11 @@ func (h *Handler) handleProposal(ctx context.Context, expHash types.Hash32, peer } encodedProof, err := codec.Encode(&gossip) if err != nil { - h.logger.With().Fatal("failed to encode MalfeasanceGossip", log.Err(err)) + h.logger.Fatal("failed to encode MalfeasanceGossip", zap.Error(err)) } if err = h.publisher.Publish(ctx, pubsub.MalfeasanceProof, encodedProof); err != nil { failedPublish.Inc() - logger.With().Error("failed to broadcast malfeasance proof", log.Err(err)) + logger.Error("failed to broadcast malfeasance proof", zap.Error(err)) return fmt.Errorf("broadcast ballot malfeasance proof: %w", err) } return errMaliciousBallot @@ -432,19 +433,20 @@ func (h *Handler) setProposalBeacon(p *types.Proposal) error { return nil } -func (h *Handler) processBallot(ctx context.Context, logger log.Log, b *types.Ballot) (*wire.MalfeasanceProof, error) { +func (h *Handler) processBallot(ctx context.Context, b *types.Ballot) (*wire.MalfeasanceProof, error) { if data := h.tortoise.GetBallot(b.ID()); data != nil { known.Inc() return nil, fmt.Errorf("%w: ballot %s", errKnownBallot, b.ID()) } - logger.With().Info("new ballot", log.Inline(b)) + h.logger.Debug("new ballot", log.ZContext(ctx), zap.Inline(b)) - decoded, err := h.checkBallotSyntacticValidity(ctx, logger, b) + decoded, err := h.checkBallotSyntacticValidity(ctx, b) if err != nil { return nil, err } b.ActiveSet = nil // the active set is not needed anymore + h.logger.Debug("ballot is syntactically valid", log.ZContext(ctx), zap.Stringer("id", b.ID())) t1 := time.Now() proof, err := h.mesh.AddBallot(ctx, b) @@ -466,11 +468,7 @@ func (h *Handler) processBallot(ctx context.Context, logger log.Log, b *types.Ba return proof, nil } -func (h *Handler) checkBallotSyntacticValidity( - ctx context.Context, - logger log.Log, - b *types.Ballot, -) (*tortoise.DecodedBallot, error) { +func (h *Handler) checkBallotSyntacticValidity(ctx context.Context, b *types.Ballot) (*tortoise.DecodedBallot, error) { t0 := time.Now() ref, err := h.checkBallotDataIntegrity(ctx, b) if err != nil { @@ -519,7 +517,6 @@ func (h *Handler) checkBallotSyntacticValidity( } ballotDuration.WithLabelValues(eligible).Observe(float64(time.Since(t4))) - logger.With().Debug("ballot is syntactically valid") return decoded, nil } @@ -642,13 +639,14 @@ func (h *Handler) checkVotesConsistency(ctx context.Context, b *types.Ballot) er if voted, ok := layers[vote.LayerID]; ok { // already voted for a block in this layer if voted != vote.ID && vote.LayerID.Add(h.cfg.Hdist).After(b.Layer) { - h.logger.WithContext(ctx).With().Warning("ballot doubly voted within hdist, set smesher malicious", - b.ID(), - b.Layer, - log.Stringer("smesher", b.SmesherID), - log.Stringer("voted_bid", voted), - log.Stringer("voted_bid", vote.ID), - log.Uint32("hdist", h.cfg.Hdist), + h.logger.Warn("ballot doubly voted within hdist, set smesher malicious", + log.ZContext(ctx), + zap.Stringer("ballot", b.ID()), + zap.Uint32("layer", b.Layer.Uint32()), + zap.Stringer("smesherID", b.SmesherID), + zap.Stringer("voted_bid", voted), + zap.Stringer("voted_bid", vote.ID), + zap.Uint32("hdist", h.cfg.Hdist), ) return errDoubleVoting } diff --git a/proposals/handler_test.go b/proposals/handler_test.go index 60ed498a5a..8a466fba35 100644 --- a/proposals/handler_test.go +++ b/proposals/handler_test.go @@ -13,13 +13,13 @@ import ( "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/fetch" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/malfeasance/wire" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" @@ -128,7 +128,7 @@ func createTestHandler(t *testing.T) *testHandler { ms.md, ms.mvrf, ms.mclock, - WithLogger(logtest.New(t)), + WithLogger(zaptest.NewLogger(t)), WithConfig(Config{ LayerSize: layerAvgSize, LayersPerEpoch: layersPerEpoch, diff --git a/prune/prune_test.go b/prune/prune_test.go index 0eb4ee5e7e..b914f63a50 100644 --- a/prune/prune_test.go +++ b/prune/prune_test.go @@ -4,9 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/activesets" "github.com/spacemeshos/go-spacemesh/sql/ballots" @@ -49,7 +49,7 @@ func TestPrune(t *testing.T) { } confidenceDist := uint32(3) - pruner := New(db, confidenceDist, current.GetEpoch()-1, WithLogger(logtest.New(t).Zap())) + pruner := New(db, confidenceDist, current.GetEpoch()-1, WithLogger(zaptest.NewLogger(t))) // Act require.NoError(t, pruner.Prune(current)) diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 6cff8db611..248dd99a0d 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -22,7 +22,8 @@ const ( // filters that refer to the id column. const fieldsQuery = `select atxs.id, atxs.nonce, atxs.base_tick_height, atxs.tick_count, atxs.pubkey, atxs.effective_num_units, -atxs.received, atxs.epoch, atxs.sequence, atxs.coinbase, atxs.validity, atxs.prev_id, atxs.commitment_atx, atxs.weight` +atxs.received, atxs.epoch, atxs.sequence, atxs.coinbase, atxs.validity, atxs.commitment_atx, atxs.weight, +atxs.marriage_atx` const fullQuery = fieldsQuery + ` from atxs` @@ -55,13 +56,14 @@ func decoder(fn decoderCallback) sql.Decoder { stmt.ColumnBytes(9, a.Coinbase[:]) a.SetValidity(types.Validity(stmt.ColumnInt(10))) if stmt.ColumnType(11) != sqlite.SQLITE_NULL { - stmt.ColumnBytes(11, a.PrevATXID[:]) - } - if stmt.ColumnType(12) != sqlite.SQLITE_NULL { a.CommitmentATX = new(types.ATXID) - stmt.ColumnBytes(12, a.CommitmentATX[:]) + stmt.ColumnBytes(11, a.CommitmentATX[:]) + } + a.Weight = uint64(stmt.ColumnInt64(12)) + if stmt.ColumnType(13) != sqlite.SQLITE_NULL { + a.MarriageATX = new(types.ATXID) + stmt.ColumnBytes(13, a.MarriageATX[:]) } - a.Weight = uint64(stmt.ColumnInt64(13)) return fn(&a) } @@ -399,6 +401,40 @@ func getBlob(ctx context.Context, db sql.Executor, id []byte, blob *sql.Blob) (t return version, nil } +// Previous gets all previous ATXs for a given ATX ID. +func Previous(db sql.Executor, id types.ATXID) ([]types.ATXID, error) { + var previous []types.ATXID + enc := func(stmt *sql.Statement) { + stmt.BindBytes(1, id.Bytes()) + } + dec := func(stmt *sql.Statement) bool { + var prev types.ATXID + if stmt.ColumnType(0) != sqlite.SQLITE_NULL { + stmt.ColumnBytes(0, prev[:]) + } + // Index is returned in descending order, so the first one defines the length of the slice. + index := stmt.ColumnInt(1) + if previous == nil { + previous = make([]types.ATXID, index+1) + } + previous[index] = prev + return true + } + + rows, err := db.Exec( + "SELECT prev_atxid, prev_atx_index FROM posts WHERE atxid = ?1 ORDER BY prev_atx_index DESC;", + enc, + dec, + ) + switch { + case err != nil: + return nil, fmt.Errorf("previous ATXs for ATX ID %v: %w", id, err) + case rows == 0: + return nil, sql.ErrNotFound + } + return previous, nil +} + // NonceByID retrieves VRFNonce corresponding to the specified ATX ID. func NonceByID(db sql.Executor, id types.ATXID) (nonce types.VRFPostIndex, err error) { enc := func(stmt *sql.Statement) { @@ -425,8 +461,6 @@ func Add(db sql.Executor, atx *types.ActivationTx, blob types.AtxBlob) error { stmt.BindInt64(3, int64(atx.NumUnits)) if atx.CommitmentATX != nil { stmt.BindBytes(4, atx.CommitmentATX.Bytes()) - } else { - stmt.BindNull(4) } stmt.BindInt64(5, int64(atx.VRFNonce)) stmt.BindBytes(6, atx.SmesherID.Bytes()) @@ -436,18 +470,16 @@ func Add(db sql.Executor, atx *types.ActivationTx, blob types.AtxBlob) error { stmt.BindInt64(10, int64(atx.Sequence)) stmt.BindBytes(11, atx.Coinbase.Bytes()) stmt.BindInt64(12, int64(atx.Validity())) - if atx.PrevATXID != types.EmptyATXID { - stmt.BindBytes(13, atx.PrevATXID.Bytes()) - } else { - stmt.BindNull(13) + stmt.BindInt64(13, int64(atx.Weight)) + if atx.MarriageATX != nil { + stmt.BindBytes(14, atx.MarriageATX.Bytes()) } - stmt.BindInt64(14, int64(atx.Weight)) } _, err := db.Exec(` insert into atxs (id, epoch, effective_num_units, commitment_atx, nonce, pubkey, received, base_tick_height, tick_count, sequence, coinbase, - validity, prev_id, weight) + validity, weight, marriage_atx) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)`, enc, nil) if err != nil { return fmt.Errorf("insert ATX ID %v: %w", atx.ID(), err) @@ -462,6 +494,7 @@ func AddBlob(db sql.Executor, id types.ATXID, blob []byte, version types.AtxVers stmt.BindBytes(2, blob) stmt.BindInt64(3, int64(version)) } + _, err := db.Exec("insert into atx_blobs (id, atx, version) values (?1, ?2, ?3)", enc, nil) if err != nil { return fmt.Errorf("insert ATX blob %v: %w", id, err) @@ -539,6 +572,7 @@ type CheckpointAtx struct { ID types.ATXID Epoch types.EpochID CommitmentATX types.ATXID + MarriageATX *types.ATXID VRFNonce types.VRFPostIndex BaseTickHeight uint64 TickCount uint64 @@ -571,16 +605,21 @@ func LatestN(db sql.Executor, n int) ([]CheckpointAtx, error) { catx.Sequence = uint64(stmt.ColumnInt64(6)) stmt.ColumnBytes(7, catx.Coinbase[:]) catx.VRFNonce = types.VRFPostIndex(stmt.ColumnInt64(8)) - catx.Units = make(map[types.NodeID]uint32) + if stmt.ColumnType(9) != sqlite.SQLITE_NULL { + catx.MarriageATX = new(types.ATXID) + stmt.ColumnBytes(9, catx.MarriageATX[:]) + } rst = append(rst, catx) return true } rows, err := db.Exec(` - select id, epoch, effective_num_units, base_tick_height, tick_count, pubkey, sequence, coinbase, nonce + select + id, epoch, effective_num_units, base_tick_height, tick_count, pubkey, sequence, coinbase, nonce, marriage_atx from ( select row_number() over (partition by pubkey order by epoch desc) RowNum, - id, epoch, effective_num_units, base_tick_height, tick_count, pubkey, sequence, coinbase, nonce + id, epoch, effective_num_units, base_tick_height, tick_count, pubkey, sequence, coinbase, nonce, + marriage_atx from atxs ) where RowNum <= ?1 order by pubkey;`, enc, dec) @@ -616,12 +655,15 @@ func AddCheckpointed(db sql.Executor, catx *CheckpointAtx) error { stmt.BindInt64(8, int64(catx.Sequence)) stmt.BindBytes(9, catx.SmesherID.Bytes()) stmt.BindBytes(10, catx.Coinbase.Bytes()) + if catx.MarriageATX != nil { + stmt.BindBytes(11, catx.MarriageATX.Bytes()) + } } _, err := db.Exec(` insert into atxs (id, epoch, effective_num_units, commitment_atx, nonce, - base_tick_height, tick_count, sequence, pubkey, coinbase, received) - values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, 0)`, enc, nil) + base_tick_height, tick_count, sequence, pubkey, coinbase, marriage_atx, received) + values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, 0)`, enc, nil) if err != nil { return fmt.Errorf("insert checkpoint ATX %v: %w", catx.ID, err) } @@ -634,7 +676,8 @@ func AddCheckpointed(db sql.Executor, catx *CheckpointAtx) error { } for id, units := range catx.Units { - if err := SetUnits(db, catx.ID, id, units); err != nil { + // FIXME: should a checkpointed ATX reference its real previous ATX? + if err := SetPost(db, catx.ID, types.EmptyATXID, 0, id, units); err != nil { return fmt.Errorf("insert checkpoint ATX units %v: %w", catx.ID, err) } } @@ -684,28 +727,18 @@ func IterateAtxsData( base uint64, height uint64, nonce types.VRFPostIndex, - isMalicious bool, ) bool, ) error { _, err := db.Exec( - `select - a.id, a.pubkey, a.epoch, a.coinbase, a.effective_num_units, - a.base_tick_height, a.tick_count, a.nonce, - iif(idn.proof is null, 0, 1) as is_malicious - from atxs a left join identities idn on a.pubkey = idn.pubkey`, - // SQLite happens to process the query much faster if we don't - // filter it by epoch - // where a.epoch between ? and ?`, - // func(stmt *sql.Statement) { - // stmt.BindInt64(1, int64(from.Uint32())) - // stmt.BindInt64(2, int64(to.Uint32())) - // }, - nil, + `SELECT id, pubkey, epoch, coinbase, effective_num_units, base_tick_height, tick_count, nonce FROM atxs + WHERE epoch between ?1 and ?2`, + // filtering in CODE is no longer effective on some machines in epoch 29 + func(stmt *sql.Statement) { + stmt.BindInt64(1, int64(from.Uint32())) + stmt.BindInt64(2, int64(to.Uint32())) + }, func(stmt *sql.Statement) bool { epoch := types.EpochID(uint32(stmt.ColumnInt64(2))) - if epoch < from || epoch > to { - return true - } var id types.ATXID stmt.ColumnBytes(0, id[:]) var node types.NodeID @@ -716,9 +749,7 @@ func IterateAtxsData( baseHeight := uint64(stmt.ColumnInt64(5)) ticks := uint64(stmt.ColumnInt64(6)) nonce := types.VRFPostIndex(stmt.ColumnInt64(7)) - isMalicious := stmt.ColumnInt(8) != 0 - return fn(id, node, epoch, coinbase, effectiveUnits*ticks, - baseHeight, baseHeight+ticks, nonce, isMalicious) + return fn(id, node, epoch, coinbase, effectiveUnits*ticks, baseHeight, baseHeight+ticks, nonce) }, ) if err != nil { @@ -862,10 +893,10 @@ func PrevATXCollisions(db sql.Executor) ([]PrevATXCollision, error) { // we are joining the table with itself to find ATXs with the same prevATX // the WHERE clause ensures that we only get the pairs once if _, err := db.Exec(` - SELECT t1.pubkey, t2.pubkey, t1.id, t2.id - FROM atxs t1 - INNER JOIN atxs t2 ON t1.prev_id = t2.prev_id - WHERE t1.id < t2.id;`, nil, dec); err != nil { + SELECT p1.pubkey, p2.pubkey, p1.atxid, p2.atxid + FROM posts p1 + INNER JOIN posts p2 ON p1.prev_atxid = p2.prev_atxid + WHERE p1.atxid < p2.atxid;`, nil, dec); err != nil { return nil, fmt.Errorf("error getting ATXs with same prevATX: %w", err) } @@ -891,6 +922,38 @@ func Units(db sql.Executor, atxID types.ATXID, nodeID types.NodeID) (uint32, err return units, err } +// FindDoublePublish finds 2 distinct ATXIDs that the given identity contributed PoST to in the given epoch. +// +// It is guaranteed to return 2 distinct ATXs when the error is nil. +// It works by finding an ATX in the given epoch that has a PoST contribution from the given identity. +// - `epoch` is looked up in the `atxs` table by matching atxid. +func FindDoublePublish(db sql.Executor, nodeID types.NodeID, epoch types.EpochID) ([]types.ATXID, error) { + var ids []types.ATXID + rows, err := db.Exec(` + SELECT p.atxid + FROM posts p + INNER JOIN atxs a ON p.atxid = a.id + WHERE p.pubkey = ?1 AND a.epoch = ?2;`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, nodeID.Bytes()) + stmt.BindInt64(2, int64(epoch)) + }, + func(stmt *sql.Statement) bool { + var id types.ATXID + stmt.ColumnBytes(0, id[:]) + ids = append(ids, id) + return len(ids) < 2 + }, + ) + if err != nil { + return nil, err + } + if rows != 2 { + return nil, sql.ErrNotFound + } + return ids, nil +} + func AllUnits(db sql.Executor, id types.ATXID) (map[types.NodeID]uint32, error) { units := make(map[types.NodeID]uint32) rows, err := db.Exec( @@ -914,15 +977,81 @@ func AllUnits(db sql.Executor, id types.ATXID) (map[types.NodeID]uint32, error) return units, nil } -func SetUnits(db sql.Executor, atxID types.ATXID, id types.NodeID, units uint32) error { +func SetPost(db sql.Executor, atxID, prev types.ATXID, prevIndex int, id types.NodeID, units uint32) error { _, err := db.Exec( - `INSERT INTO posts (atxid, pubkey, units) VALUES (?1, ?2, ?3);`, + `INSERT INTO posts (atxid, pubkey, prev_atxid, prev_atx_index, units) VALUES (?1, ?2, ?3, ?4, ?5);`, func(stmt *sql.Statement) { stmt.BindBytes(1, atxID.Bytes()) stmt.BindBytes(2, id.Bytes()) - stmt.BindInt64(3, int64(units)) + if prev != types.EmptyATXID { + stmt.BindBytes(3, prev.Bytes()) + } + stmt.BindInt64(4, int64(prevIndex)) + stmt.BindInt64(5, int64(units)) }, nil, ) return err } + +// AtxWithPrevious returns the ATX ID that has the given ATX ID as its previous ATX. +func AtxWithPrevious(db sql.Executor, prev types.ATXID, id types.NodeID) (types.ATXID, error) { + var ( + atxid types.ATXID + rows int + err error + ) + decode := func(s *sql.Statement) bool { + s.ColumnBytes(0, atxid[:]) + return false + } + if prev == types.EmptyATXID { + rows, err = db.Exec("SELECT atxid FROM posts WHERE pubkey = ?1 AND prev_atxid IS NULL;", + func(s *sql.Statement) { + s.BindBytes(1, id.Bytes()) + }, + decode, + ) + } else { + rows, err = db.Exec(` + SELECT atxid FROM posts WHERE pubkey = ?1 AND prev_atxid = ?2;`, + func(s *sql.Statement) { + s.BindBytes(1, id.Bytes()) + s.BindBytes(2, prev.Bytes()) + }, + decode, + ) + } + if err != nil { + return types.EmptyATXID, err + } + if rows == 0 { + return types.EmptyATXID, sql.ErrNotFound + } + return atxid, nil +} + +// Find 2 distinct merged ATXs (having the same marriage ATX) in the same epoch. +func MergeConflict(db sql.Executor, marriage types.ATXID, publish types.EpochID) ([]types.ATXID, error) { + var ids []types.ATXID + rows, err := db.Exec(` + SELECT id FROM atxs WHERE marriage_atx = ?1 and epoch = ?2;`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, marriage.Bytes()) + stmt.BindInt64(2, int64(publish)) + }, + func(stmt *sql.Statement) bool { + var id types.ATXID + stmt.ColumnBytes(0, id[:]) + ids = append(ids, id) + return len(ids) < 2 + }, + ) + if err != nil { + return nil, err + } + if rows != 2 { + return nil, sql.ErrNotFound + } + return ids, nil +} diff --git a/sql/atxs/atxs_test.go b/sql/atxs/atxs_test.go index 99d2c6cc21..b66ef3157e 100644 --- a/sql/atxs/atxs_test.go +++ b/sql/atxs/atxs_test.go @@ -4,10 +4,12 @@ import ( "context" "errors" "os" + "slices" "testing" "time" "github.com/stretchr/testify/require" + "golang.org/x/exp/rand" "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/common/fixture" @@ -160,7 +162,7 @@ func TestLatestN(t *testing.T) { for _, atx := range []*types.ActivationTx{atx1, atx2, atx3, atx4, atx5, atx6} { require.NoError(t, atxs.Add(db, atx, types.AtxBlob{})) - require.NoError(t, atxs.SetUnits(db, atx.ID(), atx.SmesherID, atx.NumUnits)) + require.NoError(t, atxs.SetPost(db, atx.ID(), types.EmptyATXID, 0, atx.SmesherID, atx.NumUnits)) } for _, tc := range []struct { @@ -523,7 +525,7 @@ func TestVRFNonce(t *testing.T) { atx1, blob := newAtx(t, sig, withPublishEpoch(20), withNonce(333)) require.NoError(t, atxs.Add(db, atx1, blob)) - atx2, blob := newAtx(t, sig, withPublishEpoch(50), withNonce(777), withPrevATXID(atx1.ID())) + atx2, blob := newAtx(t, sig, withPublishEpoch(50), withNonce(777)) require.NoError(t, atxs.Add(db, atx2, blob)) // Act & Assert @@ -815,12 +817,6 @@ func withNonce(nonce types.VRFPostIndex) createAtxOpt { } } -func withPrevATXID(id types.ATXID) createAtxOpt { - return func(atx *types.ActivationTx) { - atx.PrevATXID = id - } -} - func withCoinbase(addr types.Address) createAtxOpt { return func(atx *types.ActivationTx) { atx.Coinbase = addr @@ -1036,11 +1032,13 @@ func Test_PrevATXCollisions(t *testing.T) { // create two ATXs with the same PrevATXID prevATXID := types.RandomATXID() - atx1, blob1 := newAtx(t, sig, withPublishEpoch(1), withPrevATXID(prevATXID)) - atx2, blob2 := newAtx(t, sig, withPublishEpoch(2), withPrevATXID(prevATXID)) + atx1, blob1 := newAtx(t, sig, withPublishEpoch(1)) + atx2, blob2 := newAtx(t, sig, withPublishEpoch(2)) require.NoError(t, atxs.Add(db, atx1, blob1)) + require.NoError(t, atxs.SetPost(db, atx1.ID(), prevATXID, 0, sig.NodeID(), 10)) require.NoError(t, atxs.Add(db, atx2, blob2)) + require.NoError(t, atxs.SetPost(db, atx2.ID(), prevATXID, 0, sig.NodeID(), 10)) // verify that the ATXs were added got1, err := atxs.Get(db, atx1.ID()) @@ -1061,9 +1059,9 @@ func Test_PrevATXCollisions(t *testing.T) { atx2, blob2 := newAtx(t, otherSig, withPublishEpoch(types.EpochID(i+1)), - withPrevATXID(atx.ID()), ) require.NoError(t, atxs.Add(db, atx2, blob2)) + require.NoError(t, atxs.SetPost(db, atx2.ID(), atx.ID(), 0, sig.NodeID(), 10)) } // get the collisions @@ -1122,7 +1120,7 @@ func TestUnits(t *testing.T) { t.Parallel() db := statesql.InMemory() atxID := types.RandomATXID() - require.NoError(t, atxs.SetUnits(db, atxID, types.RandomNodeID(), 10)) + require.NoError(t, atxs.SetPost(db, atxID, types.EmptyATXID, 0, types.RandomNodeID(), 10)) _, err := atxs.Units(db, atxID, types.RandomNodeID()) require.ErrorIs(t, err, sql.ErrNotFound) }) @@ -1135,7 +1133,7 @@ func TestUnits(t *testing.T) { {4, 5, 6}: 20, } for id, units := range units { - require.NoError(t, atxs.SetUnits(db, atxID, id, units)) + require.NoError(t, atxs.SetPost(db, atxID, types.EmptyATXID, 0, id, units)) } nodeID := types.NodeID{1, 2, 3} @@ -1149,3 +1147,219 @@ func TestUnits(t *testing.T) { require.Equal(t, units[nodeID], got) }) } + +func Test_AtxWithPrevious(t *testing.T) { + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + prev := types.RandomATXID() + + t.Run("no atxs", func(t *testing.T) { + db := statesql.InMemory() + _, err := atxs.AtxWithPrevious(db, prev, sig.NodeID()) + require.ErrorIs(t, err, sql.ErrNotFound) + }) + t.Run("finds other ATX with same previous", func(t *testing.T) { + db := statesql.InMemory() + + prev := types.RandomATXID() + atx, blob := newAtx(t, sig) + require.NoError(t, atxs.Add(db, atx, blob)) + require.NoError(t, atxs.SetPost(db, atx.ID(), prev, 0, sig.NodeID(), 10)) + + id, err := atxs.AtxWithPrevious(db, prev, sig.NodeID()) + require.NoError(t, err) + require.Equal(t, atx.ID(), id) + }) + t.Run("finds other ATX with same previous (empty)", func(t *testing.T) { + db := statesql.InMemory() + + atx, blob := newAtx(t, sig) + require.NoError(t, atxs.Add(db, atx, blob)) + require.NoError(t, atxs.SetPost(db, atx.ID(), types.EmptyATXID, 0, sig.NodeID(), 10)) + + id, err := atxs.AtxWithPrevious(db, types.EmptyATXID, sig.NodeID()) + require.NoError(t, err) + require.Equal(t, atx.ID(), id) + }) + t.Run("same previous used by 2 IDs in two ATXs", func(t *testing.T) { + db := statesql.InMemory() + + sig2, err := signing.NewEdSigner() + require.NoError(t, err) + prev := types.RandomATXID() + + atx, blob := newAtx(t, sig) + require.NoError(t, atxs.Add(db, atx, blob)) + require.NoError(t, atxs.SetPost(db, atx.ID(), prev, 0, sig.NodeID(), 10)) + + atx2, blob := newAtx(t, sig2) + require.NoError(t, atxs.Add(db, atx2, blob)) + require.NoError(t, atxs.SetPost(db, atx2.ID(), prev, 0, sig2.NodeID(), 10)) + + id, err := atxs.AtxWithPrevious(db, prev, sig.NodeID()) + require.NoError(t, err) + require.Equal(t, atx.ID(), id) + + id, err = atxs.AtxWithPrevious(db, prev, sig2.NodeID()) + require.NoError(t, err) + require.Equal(t, atx2.ID(), id) + }) +} + +func Test_FindDoublePublish(t *testing.T) { + t.Parallel() + sig, err := signing.NewEdSigner() + require.NoError(t, err) + t.Run("no atxs", func(t *testing.T) { + t.Parallel() + db := statesql.InMemory() + _, err := atxs.FindDoublePublish(db, types.RandomNodeID(), 0) + require.ErrorIs(t, err, sql.ErrNotFound) + }) + + t.Run("no double publish", func(t *testing.T) { + t.Parallel() + db := statesql.InMemory() + + // one atx + atx0, blob := newAtx(t, sig, withPublishEpoch(1)) + require.NoError(t, atxs.Add(db, atx0, blob)) + require.NoError(t, atxs.SetPost(db, atx0.ID(), types.EmptyATXID, 0, atx0.SmesherID, 10)) + + _, err = atxs.FindDoublePublish(db, atx0.SmesherID, atx0.PublishEpoch) + require.ErrorIs(t, err, sql.ErrNotFound) + + // two atxs in different epochs + atx1, blob := newAtx(t, sig, withPublishEpoch(atx0.PublishEpoch+1)) + require.NoError(t, atxs.Add(db, atx1, blob)) + require.NoError(t, atxs.SetPost(db, atx1.ID(), types.EmptyATXID, 0, atx0.SmesherID, 10)) + + _, err = atxs.FindDoublePublish(db, atx0.SmesherID, atx0.PublishEpoch) + require.ErrorIs(t, err, sql.ErrNotFound) + }) + t.Run("double publish", func(t *testing.T) { + t.Parallel() + db := statesql.InMemory() + + atx0, blob := newAtx(t, sig) + require.NoError(t, atxs.Add(db, atx0, blob)) + require.NoError(t, atxs.SetPost(db, atx0.ID(), types.EmptyATXID, 0, atx0.SmesherID, 10)) + + atx1, blob := newAtx(t, sig) + require.NoError(t, atxs.Add(db, atx1, blob)) + require.NoError(t, atxs.SetPost(db, atx1.ID(), types.EmptyATXID, 0, atx0.SmesherID, 10)) + + atxids, err := atxs.FindDoublePublish(db, atx0.SmesherID, atx0.PublishEpoch) + require.NoError(t, err) + require.ElementsMatch(t, []types.ATXID{atx0.ID(), atx1.ID()}, atxids) + + // filters by epoch + _, err = atxs.FindDoublePublish(db, atx0.SmesherID, atx0.PublishEpoch+1) + require.ErrorIs(t, err, sql.ErrNotFound) + }) + t.Run("double publish different smesher", func(t *testing.T) { + t.Parallel() + db := statesql.InMemory() + + atx0Signer, err := signing.NewEdSigner() + require.NoError(t, err) + + atx0, blob := newAtx(t, atx0Signer) + require.NoError(t, atxs.Add(db, atx0, blob)) + require.NoError(t, atxs.SetPost(db, atx0.ID(), types.EmptyATXID, 0, atx0.SmesherID, 10)) + require.NoError(t, atxs.SetPost(db, atx0.ID(), types.EmptyATXID, 0, sig.NodeID(), 10)) + + atx1Signer, err := signing.NewEdSigner() + require.NoError(t, err) + + atx1, blob := newAtx(t, atx1Signer) + require.NoError(t, atxs.Add(db, atx1, blob)) + require.NoError(t, atxs.SetPost(db, atx1.ID(), types.EmptyATXID, 0, atx1.SmesherID, 10)) + require.NoError(t, atxs.SetPost(db, atx1.ID(), types.EmptyATXID, 0, sig.NodeID(), 10)) + + atxIDs, err := atxs.FindDoublePublish(db, sig.NodeID(), atx0.PublishEpoch) + require.NoError(t, err) + require.ElementsMatch(t, []types.ATXID{atx0.ID(), atx1.ID()}, atxIDs) + }) +} + +func Test_MergeConflict(t *testing.T) { + t.Parallel() + t.Run("no atxs", func(t *testing.T) { + t.Parallel() + db := statesql.InMemory() + _, err := atxs.MergeConflict(db, types.RandomATXID(), 0) + require.ErrorIs(t, err, sql.ErrNotFound) + }) + t.Run("no conflict", func(t *testing.T) { + t.Parallel() + db := statesql.InMemory() + marriage := types.RandomATXID() + + atx := types.ActivationTx{MarriageATX: &marriage} + atx.SetID(types.RandomATXID()) + require.NoError(t, atxs.Add(db, &atx, types.AtxBlob{})) + + _, err := atxs.MergeConflict(db, types.RandomATXID(), atx.PublishEpoch) + require.ErrorIs(t, err, sql.ErrNotFound) + }) + t.Run("finds conflict", func(t *testing.T) { + t.Parallel() + db := statesql.InMemory() + marriage := types.RandomATXID() + + atx0 := types.ActivationTx{MarriageATX: &marriage} + atx0.SetID(types.RandomATXID()) + require.NoError(t, atxs.Add(db, &atx0, types.AtxBlob{})) + + atx1 := types.ActivationTx{MarriageATX: &marriage} + atx1.SetID(types.RandomATXID()) + require.NoError(t, atxs.Add(db, &atx1, types.AtxBlob{})) + + ids, err := atxs.MergeConflict(db, marriage, atx0.PublishEpoch) + require.NoError(t, err) + require.ElementsMatch(t, []types.ATXID{atx0.ID(), atx1.ID()}, ids) + + // filters by epoch + _, err = atxs.MergeConflict(db, types.RandomATXID(), 8) + require.ErrorIs(t, err, sql.ErrNotFound) + + // returns only 2 ATXs + atx2 := types.ActivationTx{MarriageATX: &marriage} + atx2.SetID(types.RandomATXID()) + require.NoError(t, atxs.Add(db, &atx2, types.AtxBlob{})) + + ids, err = atxs.MergeConflict(db, marriage, atx0.PublishEpoch) + require.NoError(t, err) + require.Len(t, ids, 2) + }) +} + +func Test_Previous(t *testing.T) { + t.Run("not found", func(t *testing.T) { + db := statesql.InMemoryTest(t) + _, err := atxs.Previous(db, types.RandomATXID()) + require.ErrorIs(t, err, sql.ErrNotFound) + }) + t.Run("returns ATXs in order", func(t *testing.T) { + db := statesql.InMemoryTest(t) + + atx := types.RandomATXID() + var previousAtxs []types.ATXID + // 10 previous ATXs + for range 10 { + previousAtxs = append(previousAtxs, types.RandomATXID()) + } + // used by 50 IDs randomly + for range 50 { + prev := previousAtxs[rand.Intn(len(previousAtxs))] + index := slices.Index(previousAtxs, prev) + require.NoError(t, atxs.SetPost(db, atx, prev, index, types.RandomNodeID(), 10)) + } + + got, err := atxs.Previous(db, atx) + require.NoError(t, err) + require.Equal(t, previousAtxs, got) + }) +} diff --git a/sql/ballots/ballots_test.go b/sql/ballots/ballots_test.go index 2f39a01fb3..8c49b67b86 100644 --- a/sql/ballots/ballots_test.go +++ b/sql/ballots/ballots_test.go @@ -148,7 +148,6 @@ func TestLayerBallotBySmesher(t *testing.T) { func newAtx(signer *signing.EdSigner, layerID types.LayerID) *types.ActivationTx { atx := &types.ActivationTx{ PublishEpoch: layerID.GetEpoch(), - PrevATXID: types.RandomATXID(), NumUnits: 2, TickCount: 1, SmesherID: signer.NodeID(), diff --git a/sql/database.go b/sql/database.go index 1a75ab86b8..6e89049ca6 100644 --- a/sql/database.go +++ b/sql/database.go @@ -2,6 +2,7 @@ package sql import ( "context" + "encoding/hex" "errors" "fmt" "maps" @@ -16,7 +17,6 @@ import ( "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/common/util" ) var ( @@ -627,7 +627,7 @@ func LoadBlob(db Executor, cmd string, id []byte, blob *Blob) error { }); err != nil { return fmt.Errorf("get %v: %w", types.BytesToHash(id), err) } else if rows == 0 { - return fmt.Errorf("%w: object %s", ErrNotFound, util.Encode(id)) + return fmt.Errorf("%w: object %s", ErrNotFound, hex.EncodeToString(id)) } return nil } diff --git a/sql/identities/identities.go b/sql/identities/identities.go index 28f7bdabf5..ff4d9d804e 100644 --- a/sql/identities/identities.go +++ b/sql/identities/identities.go @@ -133,19 +133,6 @@ func GetMalicious(db sql.Executor) (nids []types.NodeID, err error) { return nids, nil } -// Married checks if id is married. -// ID is married if it has non-null marriage_atx column. -func Married(db sql.Executor, id types.NodeID) (bool, error) { - rows, err := db.Exec("select 1 from identities where pubkey = ?1 and marriage_atx is not null;", - func(stmt *sql.Statement) { - stmt.BindBytes(1, id.Bytes()) - }, nil) - if err != nil { - return false, fmt.Errorf("married %v: %w", id, err) - } - return rows > 0, nil -} - // MarriageATX obtains the marriage ATX for given ID. func MarriageATX(db sql.Executor, id types.NodeID) (types.ATXID, error) { var atx types.ATXID diff --git a/sql/identities/identities_test.go b/sql/identities/identities_test.go index b1b6b30ee9..0d18ef5b79 100644 --- a/sql/identities/identities_test.go +++ b/sql/identities/identities_test.go @@ -120,43 +120,6 @@ func TestLoadMalfeasanceBlob(t *testing.T) { require.Equal(t, []int{len(blob1.Bytes), -1, len(blob2.Bytes)}, blobSizes) } -func TestMarried(t *testing.T) { - t.Parallel() - t.Run("identity not in DB", func(t *testing.T) { - t.Parallel() - db := statesql.InMemory() - - id := types.RandomNodeID() - married, err := Married(db, id) - require.NoError(t, err) - require.False(t, married) - - require.NoError(t, SetMarriage(db, id, &MarriageData{ATX: types.RandomATXID()})) - - married, err = Married(db, id) - require.NoError(t, err) - require.True(t, married) - }) - t.Run("identity in DB", func(t *testing.T) { - t.Parallel() - db := statesql.InMemory() - - id := types.RandomNodeID() - // add ID in the DB - SetMalicious(db, id, types.RandomBytes(11), time.Now()) - - married, err := Married(db, id) - require.NoError(t, err) - require.False(t, married) - - require.NoError(t, SetMarriage(db, id, &MarriageData{ATX: types.RandomATXID()})) - - married, err = Married(db, id) - require.NoError(t, err) - require.True(t, married) - }) -} - func TestMarriageATX(t *testing.T) { t.Parallel() t.Run("not married", func(t *testing.T) { @@ -224,9 +187,9 @@ func TestEquivocationSet(t *testing.T) { } for _, id := range ids { - married, err := Married(db, id) + mAtx, err := MarriageATX(db, id) require.NoError(t, err) - require.True(t, married) + require.Equal(t, atx, mAtx) set, err := EquivocationSet(db, id) require.NoError(t, err) require.ElementsMatch(t, ids, set) diff --git a/sql/localsql/certifier/db.go b/sql/localsql/certifier/db.go index cf74c50c99..202d93e555 100644 --- a/sql/localsql/certifier/db.go +++ b/sql/localsql/certifier/db.go @@ -28,6 +28,19 @@ func AddCertificate(db sql.Executor, nodeID types.NodeID, cert PoetCert, cerifie return nil } +func DeleteCertificate(db sql.Executor, nodeID types.NodeID, certifierID []byte) error { + enc := func(stmt *sql.Statement) { + stmt.BindBytes(1, nodeID.Bytes()) + stmt.BindBytes(2, certifierID) + } + if _, err := db.Exec(` + DELETE FROM poet_certificates WHERE node_id = ?1 AND certifier_id = ?2;`, enc, nil, + ); err != nil { + return fmt.Errorf("deleting poet certificate for (%s; %x): %w", nodeID.ShortString(), certifierID, err) + } + return nil +} + func Certificate(db sql.Executor, nodeID types.NodeID, certifierID []byte) (*PoetCert, error) { enc := func(stmt *sql.Statement) { stmt.BindBytes(1, nodeID.Bytes()) diff --git a/sql/localsql/localsql.go b/sql/localsql/localsql.go index 0b79bc546f..2dc32f18ad 100644 --- a/sql/localsql/localsql.go +++ b/sql/localsql/localsql.go @@ -3,6 +3,7 @@ package localsql import ( "embed" "strings" + "testing" "github.com/spacemeshos/go-spacemesh/sql" ) @@ -69,3 +70,14 @@ func InMemory(opts ...sql.Opt) *database { db := sql.InMemory(opts...) return &database{Database: db} } + +// InMemoryTest returns an in-mem database for testing and ensures database is closed during `tb.Cleanup`. +func InMemoryTest(tb testing.TB, opts ...sql.Opt) sql.LocalDatabase { + opts = append(opts, sql.WithConnections(1)) + db, err := Open("file::memory:?mode=memory", opts...) + if err != nil { + panic(err) + } + tb.Cleanup(func() { db.Close() }) + return db +} diff --git a/sql/localsql/nipost/poet_registration.go b/sql/localsql/nipost/poet_registration.go index 50aa83ac10..743de7b4cc 100644 --- a/sql/localsql/nipost/poet_registration.go +++ b/sql/localsql/nipost/poet_registration.go @@ -37,23 +37,6 @@ func AddPoetRegistration( return nil } -func PoetRegistrationCount(db sql.Executor, nodeID types.NodeID) (int, error) { - var count int - enc := func(stmt *sql.Statement) { - stmt.BindBytes(1, nodeID.Bytes()) - } - dec := func(stmt *sql.Statement) bool { - count = int(stmt.ColumnInt64(0)) - return true - } - query := `select count(*) from poet_registration where id = ?1;` - _, err := db.Exec(query, enc, dec) - if err != nil { - return 0, fmt.Errorf("get poet registration count for node id %s: %w", nodeID.ShortString(), err) - } - return count, nil -} - func ClearPoetRegistrations(db sql.Executor, nodeID types.NodeID) error { enc := func(stmt *sql.Statement) { stmt.BindBytes(1, nodeID.Bytes()) @@ -66,9 +49,11 @@ func ClearPoetRegistrations(db sql.Executor, nodeID types.NodeID) error { func PoetRegistrations(db sql.Executor, nodeID types.NodeID) ([]PoETRegistration, error) { var registrations []PoETRegistration + enc := func(stmt *sql.Statement) { stmt.BindBytes(1, nodeID.Bytes()) } + dec := func(stmt *sql.Statement) bool { registration := PoETRegistration{ Address: stmt.ColumnText(1), @@ -79,10 +64,13 @@ func PoetRegistrations(db sql.Executor, nodeID types.NodeID) ([]PoETRegistration registrations = append(registrations, registration) return true } - query := `select hash, address, round_id, round_end from poet_registration where id = ?1;` + + query := `SELECT hash, address, round_id, round_end FROM poet_registration WHERE id = ?1;` + _, err := db.Exec(query, enc, dec) if err != nil { return nil, fmt.Errorf("get poet registrations for node id %s: %w", nodeID.ShortString(), err) } + return registrations, nil } diff --git a/sql/localsql/nipost/poet_registration_test.go b/sql/localsql/nipost/poet_registration_test.go index 9d130043ae..a4228a6371 100644 --- a/sql/localsql/nipost/poet_registration_test.go +++ b/sql/localsql/nipost/poet_registration_test.go @@ -31,18 +31,14 @@ func Test_AddPoetRegistration(t *testing.T) { err := AddPoetRegistration(db, nodeID, reg1) require.NoError(t, err) - count, err := PoetRegistrationCount(db, nodeID) + registrations, err := PoetRegistrations(db, nodeID) require.NoError(t, err) - require.Equal(t, 1, count) + require.Len(t, registrations, 1) err = AddPoetRegistration(db, nodeID, reg2) require.NoError(t, err) - count, err = PoetRegistrationCount(db, nodeID) - require.NoError(t, err) - require.Equal(t, 2, count) - - registrations, err := PoetRegistrations(db, nodeID) + registrations, err = PoetRegistrations(db, nodeID) require.NoError(t, err) require.Len(t, registrations, 2) require.Equal(t, reg1, registrations[0]) @@ -51,9 +47,9 @@ func Test_AddPoetRegistration(t *testing.T) { err = ClearPoetRegistrations(db, nodeID) require.NoError(t, err) - count, err = PoetRegistrationCount(db, nodeID) + registrations, err = PoetRegistrations(db, nodeID) require.NoError(t, err) - require.Equal(t, 0, count) + require.Empty(t, registrations) } func Test_AddPoetRegistration_NoDuplicates(t *testing.T) { @@ -70,14 +66,14 @@ func Test_AddPoetRegistration_NoDuplicates(t *testing.T) { err := AddPoetRegistration(db, nodeID, reg) require.NoError(t, err) - count, err := PoetRegistrationCount(db, nodeID) + registrations, err := PoetRegistrations(db, nodeID) require.NoError(t, err) - require.Equal(t, 1, count) + require.Len(t, registrations, 1) err = AddPoetRegistration(db, nodeID, reg) require.ErrorIs(t, err, sql.ErrObjectExists) - count, err = PoetRegistrationCount(db, nodeID) + registrations, err = PoetRegistrations(db, nodeID) require.NoError(t, err) - require.Equal(t, 1, count) + require.Len(t, registrations, 1) } diff --git a/sql/metrics/prometheus.go b/sql/metrics/prometheus.go index dcaa3206ff..fb35490ee0 100644 --- a/sql/metrics/prometheus.go +++ b/sql/metrics/prometheus.go @@ -6,9 +6,9 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "go.uber.org/zap" "golang.org/x/sync/errgroup" - "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/metrics" "github.com/spacemeshos/go-spacemesh/sql" ) @@ -20,7 +20,7 @@ const ( // DBMetricsCollector collects metrics from db. type DBMetricsCollector struct { - logger log.Logger + logger *zap.Logger checkInterval time.Duration db sql.StateDatabase tablesList map[string]struct{} @@ -36,13 +36,13 @@ type DBMetricsCollector struct { func NewDBMetricsCollector( ctx context.Context, db sql.StateDatabase, - logger log.Logger, + logger *zap.Logger, checkInterval time.Duration, ) *DBMetricsCollector { ctx, cancel := context.WithCancel(ctx) collector := &DBMetricsCollector{ checkInterval: checkInterval, - logger: logger.WithName("db_metrics"), + logger: logger.Named("db_metrics"), db: db, cancel: cancel, tableSize: metrics.NewGauge("table_size", subsystem, "Size of table in bytes", []string{"name"}), @@ -51,20 +51,20 @@ func NewDBMetricsCollector( } statEnabled, err := collector.checkCompiledWithDBStat() if err != nil { - collector.logger.With().Error("error check compile options", log.Err(err)) + collector.logger.Error("error check compile options", zap.Error(err)) return nil } if !statEnabled { - collector.logger.With().Info("sqlite compiled without `SQLITE_ENABLE_DBSTAT_VTAB`. Metrics will not collected") + collector.logger.Info("sqlite compiled without `SQLITE_ENABLE_DBSTAT_VTAB`. Metrics will not collected") return nil } collector.tablesList, err = collector.getListOfTables() if err != nil { - collector.logger.With().Error("error get list of tables", log.Err(err)) + collector.logger.Error("error get list of tables", zap.Error(err)) return nil } - collector.logger.With().Info("start collect stat") + collector.logger.Info("start collect stat") collector.eg.Go(func() error { collector.CollectMetrics(ctx) return nil @@ -76,7 +76,7 @@ func NewDBMetricsCollector( func (d *DBMetricsCollector) Close() { d.cancel() if err := d.eg.Wait(); err != nil { - d.logger.With().Error("received error waiting for db metrics collector", log.Err(err)) + d.logger.Error("received error waiting for db metrics collector", zap.Error(err)) } } @@ -89,7 +89,7 @@ func (d *DBMetricsCollector) CollectMetrics(ctx context.Context) { case <-ticker.C: d.logger.Debug("collect stats from db") if err := d.collect(); err != nil { - d.logger.With().Error("error check db metrics", log.Err(err)) + d.logger.Error("error collecting db metrics", zap.Error(err)) } case <-ctx.Done(): return diff --git a/sql/statesql/schema/migrations/0020_atx_merge.sql b/sql/statesql/schema/migrations/0020_atx_merge.sql index 17bcda9c83..8dbff567c0 100644 --- a/sql/statesql/schema/migrations/0020_atx_merge.sql +++ b/sql/statesql/schema/migrations/0020_atx_merge.sql @@ -1,5 +1,6 @@ -- Changes required to handle merged ATXs +ALTER TABLE atxs ADD COLUMN marriage_atx CHAR(32); ALTER TABLE atxs ADD COLUMN weight INTEGER; UPDATE atxs SET weight = effective_num_units * tick_count; diff --git a/sql/statesql/schema/migrations/0021_atx_posts.sql b/sql/statesql/schema/migrations/0021_atx_posts.sql index 25ec2e2ca5..a009bd0655 100644 --- a/sql/statesql/schema/migrations/0021_atx_posts.sql +++ b/sql/statesql/schema/migrations/0021_atx_posts.sql @@ -1,9 +1,14 @@ --- Table showing the exact number of PoST units commited by smesher in given ATX. +-- Table showing the PoST commitment by a smesher in given ATX. +-- It shows the exact number of space units committed and the previous ATX id. CREATE TABLE posts ( - atxid CHAR(32) NOT NULL, - pubkey CHAR(32) NOT NULL, - units INT NOT NULL, + atxid CHAR(32) NOT NULL, + pubkey CHAR(32) NOT NULL, + prev_atxid CHAR(32), + prev_atx_index INT, + units INT NOT NULL, UNIQUE (atxid, pubkey) ); -CREATE INDEX posts_by_atxid_by_pubkey ON posts (atxid, pubkey); +CREATE INDEX posts_by_atxid_by_pubkey ON posts (atxid, pubkey, prev_atxid); + +ALTER TABLE atxs DROP COLUMN prev_id; diff --git a/sql/statesql/state_0021_migration.go b/sql/statesql/state_0021_migration.go index ffa423a012..ec5d5ae9fe 100644 --- a/sql/statesql/state_0021_migration.go +++ b/sql/statesql/state_0021_migration.go @@ -17,6 +17,8 @@ type migration0021 struct { batch int } +var _ sql.Migration = &migration0021{} + func New0021Migration(batch int) *migration0021 { return &migration0021{ batch: batch, @@ -36,7 +38,7 @@ func (*migration0021) Rollback() error { } func (m *migration0021) Apply(db sql.Executor, logger *zap.Logger) error { - if err := m.createTable(db); err != nil { + if err := m.applySql(db); err != nil { return err } var total int @@ -64,10 +66,12 @@ func (m *migration0021) Apply(db sql.Executor, logger *zap.Logger) error { } } -func (m *migration0021) createTable(db sql.Executor) error { +func (m *migration0021) applySql(db sql.Executor) error { query := `CREATE TABLE posts ( atxid CHAR(32) NOT NULL, pubkey CHAR(32) NOT NULL, + prev_atxid CHAR(32), + prev_atx_index INT, units INT NOT NULL, UNIQUE (atxid, pubkey) );` @@ -76,16 +80,23 @@ func (m *migration0021) createTable(db sql.Executor) error { return fmt.Errorf("creating posts table: %w", err) } - query = "CREATE INDEX posts_by_atxid_by_pubkey ON posts (atxid, pubkey);" + query = "CREATE INDEX posts_by_atxid_by_pubkey ON posts (atxid, pubkey, prev_atxid);" _, err = db.Exec(query, nil, nil) if err != nil { return fmt.Errorf("creating index `posts_by_atxid_by_pubkey`: %w", err) } + + query = "ALTER TABLE atxs DROP COLUMN prev_id;" + _, err = db.Exec(query, nil, nil) + if err != nil { + return fmt.Errorf("dropping column `prev_id` from `atxs`: %w", err) + } return nil } type update struct { id types.NodeID + prev types.ATXID units uint32 } @@ -133,7 +144,7 @@ func (m *migration0021) processBatch(db sql.Executor, offset, size int) (int, er func (m *migration0021) applyPendingUpdates(db sql.Executor, updates map[types.ATXID]*update) error { for atxID, upd := range updates { - if err := atxs.SetUnits(db, atxID, upd.id, upd.units); err != nil { + if err := atxs.SetPost(db, atxID, upd.prev, 0, upd.id, upd.units); err != nil { return err } } @@ -151,7 +162,7 @@ func processATX(blob types.AtxBlob) (*update, error) { if err := codec.Decode(blob.Blob, &watx); err != nil { return nil, fmt.Errorf("decoding ATX V1: %w", err) } - return &update{watx.SmesherID, watx.NumUnits}, nil + return &update{watx.SmesherID, watx.PrevATXID, watx.NumUnits}, nil default: return nil, fmt.Errorf("unsupported ATX version: %d", blob.Version) } diff --git a/sql/statesql/statesql.go b/sql/statesql/statesql.go index 252eb5099c..b755237fb6 100644 --- a/sql/statesql/statesql.go +++ b/sql/statesql/statesql.go @@ -3,6 +3,7 @@ package statesql import ( "embed" "strings" + "testing" "github.com/spacemeshos/go-spacemesh/sql" ) @@ -65,3 +66,14 @@ func InMemory(opts ...sql.Opt) sql.StateDatabase { db := sql.InMemory(opts...) return &database{Database: db} } + +// InMemoryTest returns an in-mem database for testing and ensures database is closed during `tb.Cleanup`. +func InMemoryTest(tb testing.TB, opts ...sql.Opt) sql.StateDatabase { + opts = append(opts, sql.WithConnections(1)) + db, err := Open("file::memory:?mode=memory", opts...) + if err != nil { + panic(err) + } + tb.Cleanup(func() { db.Close() }) + return db +} diff --git a/syncer/atxsync/atxsync.go b/syncer/atxsync/atxsync.go index ab93cb62bf..1cee0fa194 100644 --- a/syncer/atxsync/atxsync.go +++ b/syncer/atxsync/atxsync.go @@ -9,6 +9,7 @@ import ( "go.uber.org/zap/zapcore" "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" "github.com/spacemeshos/go-spacemesh/system" @@ -50,12 +51,16 @@ func Download( logger.Info("downloaded atxs", zap.Int("total", total), zap.Int("downloaded", downloaded), - zap.Array("missing", zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { - for _, atx := range missing { - enc.AppendString(atx.ShortString()) - } - return nil - }))) + zap.Int("progress [%]", 100*downloaded/total), + log.DebugField( + logger, + zap.Array("missing", zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { + for _, atx := range missing { + enc.AppendString(atx.ShortString()) + } + return nil + }))), + ) if len(missing) == 0 { return nil } diff --git a/syncer/atxsync/atxsync_test.go b/syncer/atxsync/atxsync_test.go index ffc0556458..126f5ae130 100644 --- a/syncer/atxsync/atxsync_test.go +++ b/syncer/atxsync/atxsync_test.go @@ -8,9 +8,9 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/sql/atxs" "github.com/spacemeshos/go-spacemesh/sql/statesql" "github.com/spacemeshos/go-spacemesh/system" @@ -96,7 +96,7 @@ func TestDownload(t *testing.T) { }, } { t.Run(tc.desc, func(t *testing.T) { - logger := logtest.New(t) + logger := zaptest.NewLogger(t) db := statesql.InMemory() ctrl := gomock.NewController(t) fetcher := mocks.NewMockAtxFetcher(ctrl) @@ -114,7 +114,7 @@ func TestDownload(t *testing.T) { return req.error }) } - require.Equal(t, tc.rst, Download(tc.ctx, tc.retry, logger.Zap(), db, fetcher, tc.set)) + require.Equal(t, tc.rst, Download(tc.ctx, tc.retry, logger, db, fetcher, tc.set)) }) } } diff --git a/syncer/atxsync/syncer.go b/syncer/atxsync/syncer.go index 4d14043a4e..8e276d2e43 100644 --- a/syncer/atxsync/syncer.go +++ b/syncer/atxsync/syncer.go @@ -40,7 +40,7 @@ func DefaultConfig() Config { return Config{ EpochInfoInterval: 4 * time.Hour, AtxsBatch: 1000, - RequestsLimit: 20, + RequestsLimit: 10, EpochInfoPeers: 2, ProgressFraction: 0.1, ProgressInterval: 20 * time.Minute, @@ -313,10 +313,10 @@ func (s *Syncer) downloadAtxs( if _, exists := state[types.ATXID(hash)]; !exists { continue } - if errors.Is(err, fetch.ErrExceedMaxRetries) { - state[types.ATXID(hash)]++ - } else if errors.Is(err, pubsub.ErrValidationReject) { + if errors.Is(err, pubsub.ErrValidationReject) { state[types.ATXID(hash)] = s.cfg.RequestsLimit + } else { + state[types.ATXID(hash)]++ } } } diff --git a/syncer/atxsync/syncer_test.go b/syncer/atxsync/syncer_test.go index caa751eba5..fb75373b1b 100644 --- a/syncer/atxsync/syncer_test.go +++ b/syncer/atxsync/syncer_test.go @@ -9,10 +9,10 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/fetch" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" @@ -46,7 +46,7 @@ func newTester(tb testing.TB, cfg Config) *tester { db := statesql.InMemory() ctrl := gomock.NewController(tb) fetcher := mocks.NewMockfetcher(ctrl) - syncer := New(fetcher, db, localdb, WithConfig(cfg), WithLogger(logtest.New(tb).Zap())) + syncer := New(fetcher, db, localdb, WithConfig(cfg), WithLogger(zaptest.NewLogger(tb))) return &tester{ tb: tb, syncer: syncer, @@ -191,7 +191,7 @@ func TestSyncer(t *testing.T) { } for _, bad := range bad.AtxIDs { if bad == id { - berr.Add(bad.Hash32(), fmt.Errorf("%w: test", fetch.ErrExceedMaxRetries)) + berr.Add(bad.Hash32(), fmt.Errorf("%w: test", errors.New("oh no failed"))) } } } diff --git a/syncer/blockssync/blocks_test.go b/syncer/blockssync/blocks_test.go index a852a31ca4..ae894ee7b6 100644 --- a/syncer/blockssync/blocks_test.go +++ b/syncer/blockssync/blocks_test.go @@ -8,10 +8,10 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/log/logtest" ) func TestCanBeAggregated(t *testing.T) { @@ -28,7 +28,7 @@ func TestCanBeAggregated(t *testing.T) { eg.Wait() }) eg.Go(func() error { - return Sync(ctx, logtest.New(t).Zap(), req, fetch) + return Sync(ctx, zaptest.NewLogger(t), req, fetch) }) fetch.EXPECT(). GetBlocks(gomock.Any(), gomock.Any()). @@ -62,7 +62,7 @@ func TestErrorDoesntExit(t *testing.T) { eg.Wait() }) eg.Go(func() error { - return Sync(ctx, logtest.New(t).Zap(), req, fetch) + return Sync(ctx, zaptest.NewLogger(t), req, fetch) }) fetch.EXPECT(). GetBlocks(gomock.Any(), gomock.Any()). diff --git a/syncer/data_fetch.go b/syncer/data_fetch.go index 7ac485bb0d..bc85a88ce9 100644 --- a/syncer/data_fetch.go +++ b/syncer/data_fetch.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "sync" "go.uber.org/zap" "golang.org/x/exp/maps" @@ -44,17 +43,6 @@ func NewDataFetch( } } -type threadSafeErr struct { - err error - mu sync.Mutex -} - -func (e *threadSafeErr) join(err error) { - e.mu.Lock() - defer e.mu.Unlock() - e.err = errors.Join(e.err, err) -} - // PollLayerData polls all peers for data in the specified layer. func (d *DataFetch) PollLayerData(ctx context.Context, lid types.LayerID, peers ...p2p.Peer) error { if len(peers) == 0 { @@ -67,21 +55,18 @@ func (d *DataFetch) PollLayerData(ctx context.Context, lid types.LayerID, peers logger := d.logger.With(zap.Uint32("layer", lid.Uint32()), log.ZContext(ctx)) layerData := make(chan fetch.LayerData, len(peers)) var eg errgroup.Group - fetchErr := threadSafeErr{} for _, peer := range peers { eg.Go(func() error { data, err := d.fetcher.GetLayerData(ctx, peer, lid) if err != nil { layerPeerError.Inc() logger.With().Debug("failed to get layer data", zap.Error(err), zap.Stringer("peer", peer)) - fetchErr.join(err) - return nil + return err } var ld fetch.LayerData if err := codec.Decode(data, &ld); err != nil { logger.With().Debug("failed to decode", zap.Error(err)) - fetchErr.join(err) - return nil + return err } logger.With().Debug("received layer data from peer", zap.Stringer("peer", peer)) registerLayerHashes(d.fetcher, peer, &ld) @@ -89,19 +74,17 @@ func (d *DataFetch) PollLayerData(ctx context.Context, lid types.LayerID, peers return nil }) } - _ = eg.Wait() + fetchErr := eg.Wait() close(layerData) allBallots := make(map[types.BallotID]struct{}) - success := false for ld := range layerData { - success = true for _, id := range ld.Ballots { allBallots[id] = struct{}{} } } - if !success { - return fetchErr.err + if len(allBallots) == 0 { + return fetchErr } if err := d.fetcher.GetBallots(ctx, maps.Keys(allBallots)); err != nil { @@ -131,7 +114,6 @@ func (d *DataFetch) PollLayerOpinions( logger := d.logger.With(zap.Uint32("layer", lid.Uint32()), log.ZContext(ctx)) opinions := make(chan *fetch.LayerOpinion, len(peers)) var eg errgroup.Group - fetchErr := threadSafeErr{} for _, peer := range peers { eg.Go(func() error { data, err := d.fetcher.GetLayerOpinions(ctx, peer, lid) @@ -139,14 +121,12 @@ func (d *DataFetch) PollLayerOpinions( opnsPeerError.Inc() logger.With(). Debug("received peer error for layer opinions", zap.Error(err), zap.Stringer("peer", peer)) - fetchErr.join(err) - return nil + return err } var lo fetch.LayerOpinion if err := codec.Decode(data, &lo); err != nil { logger.With().Debug("failed to decode layer opinion", zap.Error(err)) - fetchErr.join(err) - return nil + return err } logger.With().Debug("received layer opinion", zap.Stringer("peer", peer)) lo.SetPeer(peer) @@ -154,17 +134,15 @@ func (d *DataFetch) PollLayerOpinions( return nil }) } - _ = eg.Wait() + fetchErr := eg.Wait() close(opinions) var allOpinions []*fetch.LayerOpinion - success := false for op := range opinions { - success = true allOpinions = append(allOpinions, op) } - if !success { - return nil, nil, fetchErr.err + if len(allOpinions) == 0 { + return nil, nil, fetchErr } certs := make([]*types.Certificate, 0, len(allOpinions)) diff --git a/syncer/malsync/syncer.go b/syncer/malsync/syncer.go index 3a14388852..173756002f 100644 --- a/syncer/malsync/syncer.go +++ b/syncer/malsync/syncer.go @@ -434,10 +434,10 @@ func (s *Syncer) downloadMalfeasanceProofs(ctx context.Context, initial bool, up switch { case !sst.has(nodeID): continue - case errors.Is(err, fetch.ErrExceedMaxRetries): - sst.failed(nodeID) case errors.Is(err, pubsub.ErrValidationReject): sst.rejected(nodeID) + default: + sst.failed(nodeID) } } } diff --git a/syncer/malsync/syncer_test.go b/syncer/malsync/syncer_test.go index 3bca66cc17..035b55b025 100644 --- a/syncer/malsync/syncer_test.go +++ b/syncer/malsync/syncer_test.go @@ -10,13 +10,13 @@ import ( "github.com/jonboulle/clockwork" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "golang.org/x/exp/maps" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/fetch" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/malfeasance/wire" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" @@ -160,7 +160,7 @@ func newTester(tb testing.TB, cfg Config) *tester { syncer := New(fetcher, db, localdb, withClock(clock), WithConfig(cfg), - WithLogger(logtest.New(tb).Zap()), + WithLogger(zaptest.NewLogger(tb)), WithPeerErrMetric(peerErrCount), ) return &tester{ @@ -339,7 +339,7 @@ func TestSyncer(t *testing.T) { tester.expectPeers(tester.peers) tester.expectGetMaliciousIDs() tester.expectGetProofs(map[types.NodeID]error{ - nid("2"): fetch.ErrExceedMaxRetries, + nid("2"): errors.New("fail"), }) epochStart := tester.clock.Now().Truncate(time.Second) epochEnd := epochStart.Add(10 * time.Minute) diff --git a/syncer/state_syncer.go b/syncer/state_syncer.go index 6bd7282b14..2b95b00138 100644 --- a/syncer/state_syncer.go +++ b/syncer/state_syncer.go @@ -250,7 +250,7 @@ func (s *Syncer) adopt(ctx context.Context, lid types.LayerID, certs []*types.Ce zap.Error(err), ) } else { - s.logger.Info("adopted cert from peer", + s.logger.Debug("adopted cert from peer", log.ZContext(ctx), zap.Uint32("layer", lid.Uint32()), zap.Stringer("block", cert.BlockID), diff --git a/syncer/syncer.go b/syncer/syncer.go index e03a24129a..7ae227d0fc 100644 --- a/syncer/syncer.go +++ b/syncer/syncer.go @@ -216,9 +216,9 @@ func (s *Syncer) Close() { return // not started yet } s.stop() - s.logger.Info("waiting for syncer goroutines to finish") + s.logger.Debug("waiting for syncer goroutines to finish") err := s.eg.Wait() - s.logger.Info("all syncer goroutines finished", zap.Error(err)) + s.logger.Debug("all syncer goroutines finished", zap.Error(err)) } // RegisterForATXSynced returns a channel for notification when the node enters ATX synced state. @@ -378,7 +378,7 @@ func (s *Syncer) synchronize(ctx context.Context) bool { } // at most one synchronize process can run at any time if !s.setSyncerBusy() { - s.logger.Info("sync is already running, giving up", log.ZContext(ctx)) + s.logger.Debug("sync is already running, giving up", log.ZContext(ctx)) return false } defer s.setSyncerIdle() @@ -426,7 +426,7 @@ func (s *Syncer) synchronize(ctx context.Context) bool { if err := s.syncLayer(ctx, layer); err != nil { batchError := &fetch.BatchError{} if errors.As(err, &batchError) && batchError.Ignore() { - s.logger.Info( + s.logger.Debug( "remaining ballots are rejected in the layer", log.ZContext(ctx), zap.Error(err), diff --git a/syncer/syncer_test.go b/syncer/syncer_test.go index 0327524807..f92c435765 100644 --- a/syncer/syncer_test.go +++ b/syncer/syncer_test.go @@ -10,13 +10,13 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/fixture" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/mesh" mmocks "github.com/spacemeshos/go-spacemesh/mesh/mocks" "github.com/spacemeshos/go-spacemesh/p2p" @@ -109,7 +109,7 @@ func (ts *testSyncer) expectDownloadLoop() chan struct{} { } func newTestSyncer(t *testing.T, interval time.Duration) *testSyncer { - lg := logtest.New(t) + lg := zaptest.NewLogger(t) mt := newMockLayerTicker() ctrl := gomock.NewController(t) @@ -128,7 +128,7 @@ func newTestSyncer(t *testing.T, interval time.Duration) *testSyncer { mForkFinder: mocks.NewMockforkFinder(ctrl), } db := statesql.InMemory() - ts.cdb = datastore.NewCachedDB(db, lg.Zap()) + ts.cdb = datastore.NewCachedDB(db, lg) var err error atxsdata := atxsdata.New() exec := mesh.NewExecutor(ts.cdb, atxsdata, ts.mVm, ts.mConState, lg) @@ -155,7 +155,7 @@ func newTestSyncer(t *testing.T, interval time.Duration) *testSyncer { ts.mAtxSyncer, ts.mMalSyncer, WithConfig(cfg), - WithLogger(lg.Zap()), + WithLogger(lg), withDataFetcher(ts.mDataFetcher), withForkFinder(ts.mForkFinder), ) diff --git a/systest/Dockerfile b/systest/Dockerfile index 4bd9b07388..3bec0ee12d 100644 --- a/systest/Dockerfile +++ b/systest/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22 as build +FROM golang:1.22 AS build RUN set -ex \ && apt-get update --fix-missing \ && apt-get install -qy --no-install-recommends \ @@ -22,14 +22,25 @@ COPY . . RUN --mount=type=cache,id=build,target=/root/.cache/go-build go test -failfast -v -c -o ./build/tests.test ./systest/tests/ -FROM ubuntu:22.04 +ENV CGO_ENABLED=0 +RUN --mount=type=cache,id=build,target=/root/.cache/go-build go build -o ./build/test2json -ldflags="-s -w" cmd/test2json + +ENV GOBIN=/bin +RUN --mount=type=cache,id=build,target=/root/.cache/go-build go install gotest.tools/gotestsum@v1.12.0 + +FROM ubuntu:22.04 AS runtime RUN set -ex \ - && apt-get update --fix-missing \ - && apt-get install -qy --no-install-recommends \ - ocl-icd-libopencl1 clinfo \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && apt-get update --fix-missing \ + && apt-get install -qy --no-install-recommends \ + ocl-icd-libopencl1 clinfo \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* COPY --from=build /src/build/tests.test /bin/tests -COPY --from=build /src/build/libpost.so /bin/libpost.so -COPY --from=build /src/build/service /bin/service +COPY --from=build /src/build/test2json /bin/ +COPY --from=build /bin/gotestsum /bin/ +COPY --from=build /src/build/libpost.so /bin/ +COPY --from=build /src/build/post-service /bin/ ENV LD_LIBRARY_PATH="/bin/" + +ENV GOVERSION=1.22 +ENV GOTESTSUM_FORMAT=standard-quiet diff --git a/systest/Makefile b/systest/Makefile index 489de35f9d..6909ba7f61 100644 --- a/systest/Makefile +++ b/systest/Makefile @@ -5,9 +5,9 @@ tmpfile := $(shell mktemp /tmp/systest-XXX) test_name ?= TestSmeshing org ?= spacemeshos image_name ?= $(org)/systest:$(version_info) -certifier_image ?= spacemeshos/certifier-service:v0.7.8 +certifier_image ?= spacemeshos/certifier-service:v0.7.11 poet_image ?= $(org)/poet:v0.10.3 -post_service_image ?= $(org)/post-service:v0.7.8 +post_service_image ?= $(org)/post-service:v0.7.11 post_init_image ?= $(org)/postcli:v0.12.5 smesher_image ?= $(org)/go-spacemesh-dev:$(version_info) bs_image ?= $(org)/go-spacemesh-dev-bs:$(version_info) @@ -22,8 +22,8 @@ bootstrap ?= 5m storage ?= standard=1Gi node_selector ?= namespace ?= -label ?= count ?= 1 +failfast ?= true configname ?= $(test_job_name) smesher_config ?= parameters/fastnet/smesher.json @@ -36,8 +36,9 @@ ifeq ($(configname),$(test_job_name)) run_deps = config endif -command := tests -test.v -test.count=$(count) -test.timeout=0 -test.run=$(test_name) -clusters=$(clusters) \ --level=$(level) -labels=$(label) -configname=$(configname) +command := gotestsum --raw-command -- test2json -t -p systest \ + /bin/tests -test.v -test.count=$(count) -test.timeout=60m -test.run=$(test_name) -test.parallel=$(clusters) \ + -test.failfast=$(failfast) -clusters=$(clusters) -level=$(level) -configname=$(configname) .PHONY: docker docker: @@ -78,13 +79,13 @@ config: template .PHONY: gomplate gomplate: - @go install github.com/hairyhenderson/gomplate/v4/cmd/gomplate@v4.0.0-pre-1 + @go install github.com/hairyhenderson/gomplate/v4/cmd/gomplate@v4.1.0 # Using bash to invoke ./wait_for_job.sh script to avoid problems on Mac # where /bin/bash is an old bash .PHONY: run run: gomplate $(run_deps) - @echo "launching test job with name=$(test_job_name) and testid=$(test_id)" + @echo "launching test job with name=$(test_job_name) and testid=$(test_id), command=$(command)" @ns=$$(kubectl config view --minify -o jsonpath='{..namespace}' 2>/dev/null); \ export ns="$${ns:-default}"; \ if [ -z "$${norbac}" ]; then \ diff --git a/systest/README.md b/systest/README.md index 27f2377dd0..b9901f551a 100644 --- a/systest/README.md +++ b/systest/README.md @@ -39,6 +39,14 @@ This testing setup can run on top of any k8s installation. The instructions belo helm repo update helm upgrade --install loki grafana/loki-stack --set grafana.enabled=true,prometheus.enabled=true,prometheus.alertmanager.persistentVolume.enabled=false,prometheus.server.persistentVolume.enabled=false,loki.persistence.enabled=true,loki.persistence.storageClassName=standard,loki.persistence.size=20Gi ``` +5. Install `gomplate` + + In linux amd64 use: + ```bash + curl -o /usr/local/bin/gomplate -sSL https://github.com/hairyhenderson/gomplate/releases/download/v4.1.0/gomplate_linux-amd64 + chmod 755 /usr/local/bin/gomplate + ``` + For other OS, please refer to the [releases](https://github.com/hairyhenderson/gomplate/releases) page. ### Using Grafana and Loki @@ -70,14 +78,20 @@ kubectl port-forward service/loki-grafana 9000:80 note the image tag. e.g. `go-spacemesh-dev:develop-dirty` -3. Build test image for `tests` module with `make docker`. +3. Build bootstrapper image. Under the root directory of go-spacemesh: + + ```bash + make dockerbuild-bs + ``` + +4. Build test image for `tests` module with `make docker`. ```bash cd systest make docker ``` -4. Run tests. +5. Run tests. ```bash make run test_name=TestSmeshing smesher_image= e.g. `smesher_image=go-spacemesh-dev:develop-dirty` @@ -160,6 +174,7 @@ This is related to minikube setup though and shouldn't be an issue for Kubernete * If you are switching between remote and local k8s, you have to run `minikube start` before running the tests locally. * If you did `make clean`, you will have to install `loki` again for grafana to be installed. +* If the code changes in the codebase and you'd like to test the changed code, you must rebuild all images again. E.g. `make dockerbuild-go && make dockerbuild-bs && cd systest && make docker` ## Parametrizable tests @@ -190,6 +205,13 @@ make run properties=properties.env test_name=TestStepCreate ## Longevity testing +These tests test the clusters over a long period of time. They are therefore disabled by default so that the CI doesn't run them. + +In order to run them, please define the `LONGEVITY_TEST` environment variable, otherwise they will be skipped: +```bash +export LONGEVITY_TESTS=1 +``` + ### Manual mode Manual mode allows to setup a cluster as a separate step and apply tests on that cluster continuously. diff --git a/systest/cluster/cluster.go b/systest/cluster/cluster.go index 54f54f58c6..36e495d77f 100644 --- a/systest/cluster/cluster.go +++ b/systest/cluster/cluster.go @@ -150,6 +150,9 @@ func ReuseWait(cctx *testcontext.Context, opts ...Opt) (*Cluster, error) { if err := cl.WaitAllTimeout(cctx.BootstrapDuration); err != nil { return nil, err } + if err = cctx.CheckFail(); err != nil { + return nil, err + } return cl, nil } diff --git a/systest/cluster/nodes.go b/systest/cluster/nodes.go index db1afa90df..cba322d326 100644 --- a/systest/cluster/nodes.go +++ b/systest/cluster/nodes.go @@ -62,12 +62,12 @@ var ( "requests and limits for smesher container", &apiv1.ResourceRequirements{ Requests: apiv1.ResourceList{ - apiv1.ResourceCPU: resource.MustParse("0.4"), - apiv1.ResourceMemory: resource.MustParse("400Mi"), + apiv1.ResourceCPU: resource.MustParse("1.3"), + apiv1.ResourceMemory: resource.MustParse("800Mi"), }, Limits: apiv1.ResourceList{ - apiv1.ResourceCPU: resource.MustParse("2"), - apiv1.ResourceMemory: resource.MustParse("1Gi"), + apiv1.ResourceCPU: resource.MustParse("1.3"), + apiv1.ResourceMemory: resource.MustParse("800Mi"), }, }, toResources, @@ -92,11 +92,11 @@ var ( "requests and limits for poet container", &apiv1.ResourceRequirements{ Requests: apiv1.ResourceList{ - apiv1.ResourceCPU: resource.MustParse("0.5"), + apiv1.ResourceCPU: resource.MustParse("0.4"), apiv1.ResourceMemory: resource.MustParse("1Gi"), }, Limits: apiv1.ResourceList{ - apiv1.ResourceCPU: resource.MustParse("0.5"), + apiv1.ResourceCPU: resource.MustParse("0.4"), apiv1.ResourceMemory: resource.MustParse("1Gi"), }, }, @@ -186,7 +186,7 @@ func (n *NodeClient) ensurePubConn(ctx context.Context) (*grpc.ClientConn, error if err != nil { return nil, err } - if err := n.waitForConnectionReady(context.Background(), conn); err != nil { + if err := n.waitForConnectionReady(ctx, conn); err != nil { return nil, err } n.pubConn = conn diff --git a/systest/parameters/bignet/certifier.yaml b/systest/parameters/bignet/certifier.yaml index fb92d02979..0fb3bf3e0d 100644 --- a/systest/parameters/bignet/certifier.yaml +++ b/systest/parameters/bignet/certifier.yaml @@ -13,3 +13,7 @@ init_cfg: n: 2 r: 1 p: 1 +limits: + max_concurrent_requests: 2 + max_pending_requests: 100 + max_body_size: 4096 diff --git a/systest/parameters/fastnet/certifier.yaml b/systest/parameters/fastnet/certifier.yaml index fb92d02979..0fb3bf3e0d 100644 --- a/systest/parameters/fastnet/certifier.yaml +++ b/systest/parameters/fastnet/certifier.yaml @@ -13,3 +13,7 @@ init_cfg: n: 2 r: 1 p: 1 +limits: + max_concurrent_requests: 2 + max_pending_requests: 100 + max_body_size: 4096 diff --git a/systest/parameters/longfast/certifier.yaml b/systest/parameters/longfast/certifier.yaml index fb92d02979..0fb3bf3e0d 100644 --- a/systest/parameters/longfast/certifier.yaml +++ b/systest/parameters/longfast/certifier.yaml @@ -13,3 +13,7 @@ init_cfg: n: 2 r: 1 p: 1 +limits: + max_concurrent_requests: 2 + max_pending_requests: 100 + max_body_size: 4096 diff --git a/systest/systest_rbac.yml.tmpl b/systest/systest_rbac.yml.tmpl index 817bb7ec4a..6060daf979 100644 --- a/systest/systest_rbac.yml.tmpl +++ b/systest/systest_rbac.yml.tmpl @@ -18,6 +18,9 @@ rules: - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get","create","patch","update","list","watch"] +- apiGroups: ["chaos-mesh.org"] + resources: ["networkchaos", "timechaos", "schedule"] + verbs: ["get","create","patch","update","list","watch","delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/systest/testcontext/context.go b/systest/testcontext/context.go index 7a1a88f6f4..34e674aebf 100644 --- a/systest/testcontext/context.go +++ b/systest/testcontext/context.go @@ -2,6 +2,7 @@ package testcontext import ( "context" + "errors" "flag" "fmt" "math/rand/v2" @@ -18,12 +19,13 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest" - "k8s.io/apimachinery/pkg/api/errors" + k8serr "k8s.io/apimachinery/pkg/api/errors" apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" corev1 "k8s.io/client-go/applyconfigurations/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "k8s.io/client-go/util/flowcontrol" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" k8szap "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -49,15 +51,13 @@ var ( ) logLevel = zap.LevelFlag("level", zap.InfoLevel, "verbosity of the logger") testTimeout = flag.Duration("test-timeout", 60*time.Minute, "timeout for a single test") - labels = stringSet{} tokens chan struct{} initTokens sync.Once -) -func init() { - flag.Var(labels, "labels", "test will be executed only if it matches all labels") -} + failed = make(chan struct{}) + failOnce sync.Once +) var ( testid = parameters.String( @@ -231,7 +231,7 @@ func updateContext(ctx *Context) error { ns, err := ctx.Client.CoreV1().Namespaces().Get(ctx, ctx.Namespace, apimetav1.GetOptions{}) if err != nil || ns == nil { - if errors.IsNotFound(err) { + if k8serr.IsNotFound(err) { return nil } return err @@ -260,15 +260,6 @@ func updateContext(ctx *Context) error { return nil } -// Labels sets list of labels for the test. -func Labels(labels ...string) Opt { - return func(c *cfg) { - for _, label := range labels { - c.labels[label] = struct{}{} - } - } -} - // SkipClusterLimits will not block if there are no available tokens. func SkipClusterLimits() Opt { return func(c *cfg) { @@ -280,13 +271,10 @@ func SkipClusterLimits() Opt { type Opt func(*cfg) func newCfg() *cfg { - return &cfg{ - labels: map[string]struct{}{}, - } + return &cfg{} } type cfg struct { - labels map[string]struct{} skipLimits bool } @@ -300,16 +288,24 @@ func New(t *testing.T, opts ...Opt) *Context { for _, opt := range opts { opt(c) } - for label := range labels { - if _, exist := c.labels[label]; !exist { - t.Skipf("not labeled with '%s'", label) - } - } if !c.skipLimits { tokens <- struct{}{} t.Cleanup(func() { <-tokens }) } + + t.Cleanup(func() { + if t.Failed() { + failOnce.Do(func() { close(failed) }) + } + }) config, err := rest.InClusterConfig() + + // The default rate limiter is too slow 5qps and 10 burst, This will prevent the client from being throttled + // Change the limits to the same of kubectl and argo + // That's were those number come from + // https://github.com/kubernetes/kubernetes/pull/105520 + // https://github.com/argoproj/argo-workflows/pull/11603/files + config.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(50, 300) require.NoError(t, err) clientset, err := kubernetes.NewForConfig(config) @@ -383,3 +379,12 @@ func New(t *testing.T, opts ...Opt) *Context { cctx.Log.Infow("using", "namespace", cctx.Namespace) return cctx } + +func (c *Context) CheckFail() error { + select { + case <-failed: + return errors.New("test suite failed. aborting test execution") + default: + } + return nil +} diff --git a/systest/testcontext/string_set.go b/systest/testcontext/string_set.go deleted file mode 100644 index 258fb34f81..0000000000 --- a/systest/testcontext/string_set.go +++ /dev/null @@ -1,29 +0,0 @@ -package testcontext - -import "bytes" - -type stringSet map[string]struct{} - -func (s stringSet) Set(val string) error { - if len(val) == 0 { - return nil - } - s[val] = struct{}{} - return nil -} - -func (s stringSet) Type() string { - return "[]string" -} - -func (s stringSet) String() string { - var buf bytes.Buffer - for k := range s { - buf.WriteString(k) - buf.WriteRune(',') - } - if buf.Len() == 0 { - return "" - } - return buf.String()[:buf.Len()-1] -} diff --git a/systest/tests/checkpoint_test.go b/systest/tests/checkpoint_test.go index ffcabcd020..6d4ccde138 100644 --- a/systest/tests/checkpoint_test.go +++ b/systest/tests/checkpoint_test.go @@ -36,7 +36,7 @@ func TestCheckpoint(t *testing.T) { // TODO(mafa): add new test with multi-smeshing nodes t.Parallel() - tctx := testcontext.New(t, testcontext.Labels("sanity")) + tctx := testcontext.New(t) addedLater := 2 size := min(tctx.ClusterSize, 30) oldSize := size - addedLater @@ -51,8 +51,8 @@ func TestCheckpoint(t *testing.T) { // the start of the next poet round snapshotLayer := uint32(15) restoreLayer := uint32(18) - checkpointEpoch := uint32(4) - lastEpoch := uint32(8) + checkpointEpoch := uint32(8) + lastEpoch := uint32(14) // bootstrap the checkpoint epoch and the next epoch as the beacon protocol was interrupted in the last epoch cl, err := reuseCluster(tctx, restoreLayer) @@ -168,8 +168,8 @@ func TestCheckpoint(t *testing.T) { } } - tctx.Log.Infow("waiting for all miners to be smeshing", "last epoch", checkpointEpoch+2) - ensureSmeshing(t, tctx, cl, checkpointEpoch+2) + tctx.Log.Infow("waiting for all miners to be smeshing", "last epoch", checkpointEpoch) + ensureSmeshing(t, tctx, cl, checkpointEpoch) // increase the cluster size to the original test size tctx.Log.Info("cluster size changed to ", size) diff --git a/systest/tests/common.go b/systest/tests/common.go index d4df0db710..9b31754e47 100644 --- a/systest/tests/common.go +++ b/systest/tests/common.go @@ -28,6 +28,8 @@ const ( attempts = 3 ) +var retryBackoff = 10 * time.Second + func sendTransactions( ctx context.Context, eg *errgroup.Group, @@ -117,24 +119,14 @@ func submitTransaction(ctx context.Context, tx []byte, node *cluster.NodeClient) return response.Txstate.Id.Id, nil } -func watchStateHashes( - ctx context.Context, - eg *errgroup.Group, - node *cluster.NodeClient, - logger *zap.Logger, - collector func(*pb.GlobalStateStreamResponse) (bool, error), -) { - eg.Go(func() error { - return stateHashStream(ctx, node, logger, collector) - }) -} - func stateHashStream( ctx context.Context, node *cluster.NodeClient, logger *zap.Logger, collector func(*pb.GlobalStateStreamResponse) (bool, error), ) error { + retries := 0 +BACKOFF: stateapi := pb.NewGlobalStateServiceClient(node.PubConn()) states, err := stateapi.GlobalStateStream(ctx, &pb.GlobalStateStreamRequest{ @@ -153,7 +145,12 @@ func stateHashStream( zap.Any("status", s), ) if s.Code() == codes.Unavailable { - return nil + if retries == attempts { + return errors.New("state stream unavailable") + } + retries++ + time.Sleep(retryBackoff) + goto BACKOFF } } if err != nil { @@ -185,6 +182,8 @@ func layersStream( logger *zap.Logger, collector layerCollector, ) error { + retries := 0 +BACKOFF: meshapi := pb.NewMeshServiceClient(node.PubConn()) layers, err := meshapi.LayerStream(ctx, &pb.LayerStreamRequest{}) if err != nil { @@ -196,7 +195,12 @@ func layersStream( if ok && s.Code() != codes.OK { logger.Warn("layers stream error", zap.String("client", node.Name), zap.Error(err), zap.Any("status", s)) if s.Code() == codes.Unavailable { - return nil + if retries == attempts { + return errors.New("layer stream unavailable") + } + retries++ + time.Sleep(retryBackoff) + goto BACKOFF } } if err != nil { @@ -214,6 +218,9 @@ func malfeasanceStream( logger *zap.Logger, collector func(*pb.MalfeasanceStreamResponse) (bool, error), ) error { + retries := 0 +BACKOFF: + meshapi := pb.NewMeshServiceClient(node.PubConn()) layers, err := meshapi.MalfeasanceStream(ctx, &pb.MalfeasanceStreamRequest{IncludeProof: true}) if err != nil { @@ -229,7 +236,13 @@ func malfeasanceStream( zap.Any("status", s), ) if s.Code() == codes.Unavailable { - return nil + if retries == attempts { + return errors.New("layer stream unavailable") + } + retries++ + time.Sleep(retryBackoff) + goto BACKOFF + } } if err != nil { @@ -309,6 +322,9 @@ func watchTransactionResults(ctx context.Context, collector func(*pb.TransactionResult) (bool, error), ) { eg.Go(func() error { + retries := 0 + BACKOFF: + api := pb.NewTransactionServiceClient(client.PubConn()) rsts, err := api.StreamResults(ctx, &pb.TransactionResultsRequest{Watch: true}) if err != nil { @@ -324,7 +340,12 @@ func watchTransactionResults(ctx context.Context, zap.Any("status", s), ) if s.Code() == codes.Unavailable { - return nil + if retries == attempts { + return errors.New("transaction results unavailable") + } + retries++ + time.Sleep(retryBackoff) + goto BACKOFF } } if err != nil { @@ -345,6 +366,8 @@ func watchProposals( collector func(*pb.Proposal) (bool, error), ) { eg.Go(func() error { + retries := 0 + BACKOFF: dbg := pb.NewDebugServiceClient(client.PrivConn()) proposals, err := dbg.ProposalsStream(ctx, &emptypb.Empty{}) if err != nil { @@ -360,7 +383,12 @@ func watchProposals( zap.Any("status", s), ) if s.Code() == codes.Unavailable { - return nil + if retries == attempts { + return errors.New("watch proposals unavailable") + } + retries++ + time.Sleep(retryBackoff) + goto BACKOFF } } if err != nil { diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index ef402bbd6f..911b9c9a50 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -42,7 +42,7 @@ func TestPostMalfeasanceProof(t *testing.T) { t.Parallel() testDir := t.TempDir() - ctx := testcontext.New(t, testcontext.Labels("sanity")) + ctx := testcontext.New(t) logger := ctx.Log.Desugar().WithOptions(zap.IncreaseLevel(zap.InfoLevel), zap.WithCaller(false)) // Prepare cluster diff --git a/systest/tests/equivocation_test.go b/systest/tests/equivocation_test.go index 5f9264ca74..262f1ffe0a 100644 --- a/systest/tests/equivocation_test.go +++ b/systest/tests/equivocation_test.go @@ -19,7 +19,7 @@ import ( func TestEquivocation(t *testing.T) { t.Parallel() const bootnodes = 2 - cctx := testcontext.New(t, testcontext.Labels("sanity")) + cctx := testcontext.New(t) keys := make([]ed25519.PrivateKey, cctx.ClusterSize-bootnodes) honest := int(float64(len(keys)) * 0.6) diff --git a/systest/tests/fallback_test.go b/systest/tests/fallback_test.go index 122296bdef..d1c265665c 100644 --- a/systest/tests/fallback_test.go +++ b/systest/tests/fallback_test.go @@ -21,7 +21,7 @@ import ( func TestFallback(t *testing.T) { t.Parallel() - tctx := testcontext.New(t, testcontext.Labels("sanity")) + tctx := testcontext.New(t) cl, err := cluster.ReuseWait(tctx, cluster.WithKeys(tctx.ClusterSize), cluster.WithBootstrapperFlag(cluster.GenerateFallback()), diff --git a/systest/tests/nodes_test.go b/systest/tests/nodes_test.go index e18dd4a855..b68667dd4c 100644 --- a/systest/tests/nodes_test.go +++ b/systest/tests/nodes_test.go @@ -31,7 +31,7 @@ func TestAddNodes(t *testing.T) { addedLater = 2 ) - tctx := testcontext.New(t, testcontext.Labels("sanity")) + tctx := testcontext.New(t) size := min(tctx.ClusterSize, 30) oldSize := size - addedLater if tctx.ClusterSize > oldSize { @@ -127,7 +127,7 @@ func TestAddNodes(t *testing.T) { func TestFailedNodes(t *testing.T) { t.Parallel() - tctx := testcontext.New(t, testcontext.Labels("sanity")) + tctx := testcontext.New(t) cl, err := cluster.ReuseWait(tctx, cluster.WithKeys(tctx.ClusterSize)) require.NoError(t, err) diff --git a/systest/tests/partition_test.go b/systest/tests/partition_test.go index eb671b3eac..ba65b833cc 100644 --- a/systest/tests/partition_test.go +++ b/systest/tests/partition_test.go @@ -80,28 +80,32 @@ func testPartition(t *testing.T, tctx *testcontext.Context, cl *cluster.Cluster, stateCh := make(chan *stateUpdate, uint32(cl.Total())*numLayers*10) tctx.Log.Debug("listening to state hashes...") for i := range cl.Total() { - client := cl.Client(i) - watchStateHashes(ctx, eg, client, tctx.Log.Desugar(), func(state *pb.GlobalStateStreamResponse) (bool, error) { - data := state.Datum.Datum - require.IsType(t, &pb.GlobalStateData_GlobalState{}, data) - - resp := data.(*pb.GlobalStateData_GlobalState) - layer := resp.GlobalState.Layer.Number - if layer > stop { - return false, nil - } - - stateHash := types.BytesToHash(resp.GlobalState.RootHash) - tctx.Log.Debugw("state hash collected", - "client", client.Name, - "layer", layer, - "state", stateHash.ShortString()) - stateCh <- &stateUpdate{ - layer: layer, - hash: stateHash, - client: client.Name, - } - return true, nil + node := cl.Client(i) + + eg.Go(func() error { + return stateHashStream(ctx, node, tctx.Log.Desugar(), + func(state *pb.GlobalStateStreamResponse) (bool, error) { + data := state.Datum.Datum + require.IsType(t, &pb.GlobalStateData_GlobalState{}, data) + + resp := data.(*pb.GlobalStateData_GlobalState) + layer := resp.GlobalState.Layer.Number + if layer > stop { + return false, nil + } + + stateHash := types.BytesToHash(resp.GlobalState.RootHash) + tctx.Log.Debugw("state hash collected", + "client", node.Name, + "layer", layer, + "state", stateHash.ShortString()) + stateCh <- &stateUpdate{ + layer: layer, + hash: stateHash, + client: node.Name, + } + return true, nil + }) }) } @@ -178,7 +182,7 @@ func testPartition(t *testing.T, tctx *testcontext.Context, cl *cluster.Cluster, func TestPartition_30_70(t *testing.T) { t.Parallel() - tctx := testcontext.New(t, testcontext.Labels("sanity")) + tctx := testcontext.New(t) if tctx.ClusterSize > 30 { tctx.Log.Info("cluster size changed to 30") tctx.ClusterSize = 30 @@ -192,7 +196,7 @@ func TestPartition_30_70(t *testing.T) { func TestPartition_50_50(t *testing.T) { t.Parallel() - tctx := testcontext.New(t, testcontext.Labels("sanity")) + tctx := testcontext.New(t) if tctx.ClusterSize > 30 { tctx.Log.Info("cluster size changed to 30") tctx.ClusterSize = 30 diff --git a/systest/tests/poets_test.go b/systest/tests/poets_test.go index b35e64c045..92d10d42c4 100644 --- a/systest/tests/poets_test.go +++ b/systest/tests/poets_test.go @@ -27,7 +27,7 @@ var layersToCheck = parameters.Int( func TestPoetsFailures(t *testing.T) { t.Parallel() - tctx := testcontext.New(t, testcontext.Labels("sanity")) + tctx := testcontext.New(t) tctx.Log.Debug("TestPoetsFailures start") cl, err := cluster.ReuseWait(tctx, cluster.WithKeys(tctx.ClusterSize)) @@ -124,7 +124,7 @@ func testPoetDies(t *testing.T, tctx *testcontext.Context, cl *cluster.Cluster) func TestNodesUsingDifferentPoets(t *testing.T) { t.Parallel() - tctx := testcontext.New(t, testcontext.Labels("sanity")) + tctx := testcontext.New(t) if tctx.PoetSize < 2 { t.Skip("Skipping test for using different poets - test configured with less then 2 poets") } @@ -214,7 +214,7 @@ func TestNodesUsingDifferentPoets(t *testing.T) { // https://github.com/spacemeshos/go-spacemesh/issues/5212 func TestRegisteringInPoetWithPowAndCert(t *testing.T) { t.Parallel() - tctx := testcontext.New(t, testcontext.Labels("sanity")) + tctx := testcontext.New(t) tctx.PoetSize = 2 cl := cluster.New(tctx, cluster.WithKeys(10)) diff --git a/systest/tests/smeshing_test.go b/systest/tests/smeshing_test.go index 4dbfcdfb45..3bc8ad8ede 100644 --- a/systest/tests/smeshing_test.go +++ b/systest/tests/smeshing_test.go @@ -32,7 +32,7 @@ func TestSmeshing(t *testing.T) { // TODO(mafa): add new test with multi-smeshing nodes t.Parallel() - tctx := testcontext.New(t, testcontext.Labels("sanity")) + tctx := testcontext.New(t) tctx.RemoteSize = tctx.ClusterSize / 4 // 25% of nodes are remote vests := vestingAccs{ prepareVesting(t, 3, 8, 20, 1e15, 10e15), diff --git a/systest/tests/steps_test.go b/systest/tests/steps_test.go index 99da4cdfe0..2a49468eab 100644 --- a/systest/tests/steps_test.go +++ b/systest/tests/steps_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "math/rand" + "os" "sync" "testing" "time" @@ -25,13 +26,30 @@ import ( "github.com/spacemeshos/go-spacemesh/systest/validation" ) +const ENV_LONGEVITY_TESTS = "LONGEVITY_TESTS" + +// checkAllowed checks if the tests are allowed to run. +// To prevent the CI from running these test, they require +// an environment variable to be set. If you need to manually +// run the tests locally, just comment out the check or define the +// environment variable. +func checkAllowed(t *testing.T) { + t.Helper() + val := os.Getenv(ENV_LONGEVITY_TESTS) + if val == "" || val == "false" || val == "0" { + t.Skip("test skipped. please define LONGEVITY_TESTS env var to run this test") + } +} + func TestStepCreate(t *testing.T) { + checkAllowed(t) ctx := testcontext.New(t, testcontext.SkipClusterLimits()) _, err := cluster.Reuse(ctx, cluster.WithKeys(ctx.ClusterSize)) require.NoError(t, err) } func TestStepShortDisconnect(t *testing.T) { + checkAllowed(t) tctx := testcontext.New(t, testcontext.SkipClusterLimits()) cl, err := cluster.Reuse(tctx, cluster.WithKeys(tctx.ClusterSize)) require.NoError(t, err) @@ -73,6 +91,7 @@ func TestStepShortDisconnect(t *testing.T) { } func TestStepTransactions(t *testing.T) { + checkAllowed(t) const ( batch = 10 amountLimit = 100_000 @@ -164,6 +183,7 @@ func TestStepTransactions(t *testing.T) { } func TestStepReplaceNodes(t *testing.T) { + checkAllowed(t) cctx := testcontext.New(t, testcontext.SkipClusterLimits()) cl, err := cluster.Reuse(cctx, cluster.WithKeys(cctx.ClusterSize)) require.NoError(t, err) @@ -191,6 +211,7 @@ func TestStepReplaceNodes(t *testing.T) { } func TestStepVerifyConsistency(t *testing.T) { + checkAllowed(t) cctx := testcontext.New(t, testcontext.SkipClusterLimits()) cl, err := cluster.Reuse(cctx, cluster.WithKeys(cctx.ClusterSize)) require.NoError(t, err) @@ -309,6 +330,7 @@ func (r *runner) concurrent(period time.Duration, fn func() bool) { } func TestScheduleBasic(t *testing.T) { + checkAllowed(t) TestStepCreate(t) rn := newRunner() rn.concurrent(30*time.Second, func() bool { @@ -324,6 +346,7 @@ func TestScheduleBasic(t *testing.T) { } func TestScheduleTransactions(t *testing.T) { + checkAllowed(t) TestStepCreate(t) rn := newRunner() rn.concurrent(10*time.Second, func() bool { @@ -333,6 +356,7 @@ func TestScheduleTransactions(t *testing.T) { } func TestStepValidation(t *testing.T) { + checkAllowed(t) tctx := testcontext.New(t, testcontext.SkipClusterLimits()) c, err := cluster.Reuse(tctx, cluster.WithKeys(tctx.ClusterSize)) require.NoError(t, err) diff --git a/systest/tests/timeskew_test.go b/systest/tests/timeskew_test.go index 2a5ab00c8d..5c5c0069f0 100644 --- a/systest/tests/timeskew_test.go +++ b/systest/tests/timeskew_test.go @@ -16,7 +16,7 @@ import ( func TestShortTimeskew(t *testing.T) { t.Parallel() - tctx := testcontext.New(t, testcontext.Labels("sanity")) + tctx := testcontext.New(t) cl, err := cluster.ReuseWait(tctx, cluster.WithKeys(tctx.ClusterSize)) require.NoError(t, err) diff --git a/timesync/clock.go b/timesync/clock.go index a42fd86a23..0cf275bbe6 100644 --- a/timesync/clock.go +++ b/timesync/clock.go @@ -91,7 +91,7 @@ func (t *NodeClock) startClock() error { select { case <-ticker.Chan(): case <-t.stop: - t.log.Info("stopping global clock") + t.log.Debug("stopping global clock") ticker.Stop() return nil } @@ -108,12 +108,12 @@ func (t *NodeClock) GenesisTime() time.Time { // Close closes the clock ticker. func (t *NodeClock) Close() { t.once.Do(func() { - t.log.Info("stopping clock") + t.log.Debug("stopping clock") close(t.stop) if err := t.eg.Wait(); err != nil { t.log.Error("failed to stop clock", zap.Error(err)) } - t.log.Info("clock stopped") + t.log.Debug("clock stopped") }) } @@ -130,7 +130,7 @@ func (t *NodeClock) tick() { layer := t.TimeToLayer(t.clock.Now()) switch { case layer.Before(t.lastTicked): - t.log.Info("clock ticked back in time", + t.log.Warn("clock ticked back in time", zap.Stringer("layer", layer), zap.Stringer("last_ticked_layer", t.lastTicked), ) diff --git a/timesync/peersync/clockerror.go b/timesync/peersync/clockerror.go deleted file mode 100644 index df78b92a22..0000000000 --- a/timesync/peersync/clockerror.go +++ /dev/null @@ -1,37 +0,0 @@ -package peersync - -import ( - "fmt" - "time" - - "github.com/spacemeshos/go-spacemesh/log" - "github.com/spacemeshos/go-spacemesh/log/errcode" -) - -type clockError struct { - err error - details clockErrorDetails -} - -func (c clockError) MarshalLogObject(encoder log.ObjectEncoder) error { - encoder.AddString("code", errcode.ErrClockDrift) - encoder.AddString("errmsg", c.err.Error()) - if err := encoder.AddObject("details", &c.details); err != nil { - return fmt.Errorf("add object: %w", err) - } - - return nil -} - -func (c clockError) Error() string { - return c.err.Error() -} - -type clockErrorDetails struct { - Drift time.Duration -} - -func (c clockErrorDetails) MarshalLogObject(encoder log.ObjectEncoder) error { - encoder.AddDuration("drift", c.Drift) - return nil -} diff --git a/timesync/peersync/sync.go b/timesync/peersync/sync.go index f2633df65a..436c00cf4a 100644 --- a/timesync/peersync/sync.go +++ b/timesync/peersync/sync.go @@ -5,15 +5,14 @@ import ( "errors" "fmt" "sync" - "sync/atomic" "time" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" + "go.uber.org/zap" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/codec" - "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/p2p" ) @@ -22,10 +21,10 @@ const ( ) var ( - // ErrPeersNotSynced returned if system clock is out of sync with peers clock for configured period of time. - ErrPeersNotSynced = errors.New("timesync: peers are not time synced, make sure your system clock is accurate") - // ErrTimesyncFailed returned if we weren't able to collect enough clock samples from peers. - ErrTimesyncFailed = errors.New("timesync: failed request") + // errPeersNotSynced returned if system clock is out of sync with peers clock for configured period of time. + errPeersNotSynced = errors.New("timesync: peers are not time synced") + // errTimesyncFailed returned if we weren't able to collect enough clock samples from peers. + errTimesyncFailed = errors.New("timesync: failed request") ) //go:generate mockgen -typed -package=mocks -destination=./mocks/mocks.go -source=./sync.go @@ -99,7 +98,7 @@ func WithContext(ctx context.Context) Option { } // WithLog modifies Log used in Sync. -func WithLog(lg log.Log) Option { +func WithLog(lg *zap.Logger) Option { return func(s *Sync) { s.log = lg } @@ -115,7 +114,7 @@ func WithConfig(config Config) Option { // New creates Sync instance and returns pointer. func New(h host.Host, peers getPeers, opts ...Option) *Sync { sync := &Sync{ - log: log.NewNop(), + log: zap.NewNop(), ctx: context.Background(), time: systemTime{}, h: h, @@ -132,10 +131,8 @@ func New(h host.Host, peers getPeers, opts ...Option) *Sync { // Sync manages background worker that compares peers time with system time. type Sync struct { - errCnt uint32 - config Config - log log.Log + log *zap.Logger time Time h host.Host peers getPeers @@ -151,7 +148,7 @@ func (s *Sync) streamHandler(stream network.Stream) { defer stream.SetDeadline(time.Time{}) var request Request if _, err := codec.DecodeFrom(stream, &request); err != nil { - s.log.With().Debug("can't decode request", log.Err(err)) + s.log.Debug("can't decode request", zap.Error(err)) return } resp := Response{ @@ -159,7 +156,7 @@ func (s *Sync) streamHandler(stream network.Stream) { Timestamp: uint64(s.time.Now().UnixNano()), } if _, err := codec.EncodeTo(stream, &resp); err != nil { - s.log.With().Debug("can't encode response", log.Err(err)) + s.log.Debug("can't encode response", zap.Error(err)) } } @@ -188,48 +185,48 @@ func (s *Sync) Wait() error { func (s *Sync) run() error { var ( - timer *time.Timer - round uint64 + timer *time.Timer + round uint64 + failures int ) - s.log.With().Debug("started sync background worker") - defer s.log.With().Debug("exiting sync background worker") + s.log.Debug("started sync background worker") + defer s.log.Debug("exiting sync background worker") for { prs := s.peers.GetPeers() timeout := s.config.RoundRetryInterval if len(prs) >= s.config.RequiredResponses { - s.log.With().Debug("starting time sync round with peers", - log.Uint64("round", round), - log.Int("peers_count", len(prs)), - log.Uint32("errors_count", atomic.LoadUint32(&s.errCnt)), + s.log.Debug("starting time sync round with peers", + zap.Uint64("round", round), + zap.Int("peers_count", len(prs)), + zap.Int("errors_count", failures), ) ctx, cancel := context.WithTimeout(s.ctx, s.config.RoundTimeout) offset, err := s.GetOffset(ctx, round, prs) cancel() if err == nil { if offset > s.config.MaxClockOffset || (offset < 0 && -offset > s.config.MaxClockOffset) { - s.log.With().Warning("peers offset is larger than max allowed clock difference", - log.Uint64("round", round), - log.Duration("offset", offset), - log.Duration("max_offset", s.config.MaxClockOffset), + failures += 1 + s.log.Warn("peers offset is larger than max allowed clock difference", + zap.Uint64("round", round), + zap.Duration("offset", offset), + zap.Duration("max_offset", s.config.MaxClockOffset), ) - if atomic.AddUint32(&s.errCnt, 1) == uint32(s.config.MaxOffsetErrors) { - return clockError{ - err: ErrPeersNotSynced, - details: clockErrorDetails{Drift: offset}, - } + if failures == s.config.MaxOffsetErrors { + s.log.Error("peers are not time synced, make sure your system clock is accurate") + return fmt.Errorf("%w: drift = %v", errPeersNotSynced, offset) } } else { - s.log.With().Debug("peers offset is within max allowed clock difference", - log.Uint64("round", round), - log.Duration("offset", offset), - log.Duration("max_offset", s.config.MaxClockOffset), + s.log.Debug("peers offset is within max allowed clock difference", + zap.Uint64("round", round), + zap.Duration("offset", offset), + zap.Duration("max_offset", s.config.MaxClockOffset), ) - atomic.StoreUint32(&s.errCnt, 0) + failures = 0 } offsetGauge.Set(offset.Seconds()) timeout = s.config.RoundInterval } else { - s.log.With().Error("failed to fetch offset from peers", log.Err(err)) + s.log.Error("failed to fetch offset from peers", zap.Error(err)) } round++ } @@ -257,30 +254,27 @@ func (s *Sync) GetOffset(ctx context.Context, id uint64, prs []p2p.Peer) (time.D } wg sync.WaitGroup ) - buf, err := codec.Encode(&Request{ID: id}) - if err != nil { - s.log.With().Panic("can't encode request to bytes", log.Err(err)) - } + buf := codec.MustEncode(&Request{ID: id}) + for _, pid := range prs { wg.Add(1) go func(pid p2p.Peer) { defer wg.Done() - logger := s.log.WithFields(log.Stringer("pid", pid)).With() stream, err := s.h.NewStream(network.WithNoDial(ctx, "existing connection"), pid, protocolName) if err != nil { - logger.Debug("failed to create new stream", log.Err(err)) + s.log.Debug("failed to create new stream", zap.Error(err), zap.Stringer("pid", pid)) return } defer stream.Close() _ = stream.SetDeadline(s.time.Now().Add(s.config.RoundTimeout)) defer stream.SetDeadline(time.Time{}) if _, err := stream.Write(buf); err != nil { - logger.Debug("failed to send a request", log.Err(err)) + s.log.Debug("failed to send a request", zap.Error(err), zap.Stringer("pid", pid)) return } var resp Response if _, err := codec.DecodeFrom(stream, &resp); err != nil { - logger.Debug("failed to read response from peer", log.Err(err)) + s.log.Debug("failed to read response from peer", zap.Error(err), zap.Stringer("pid", pid)) return } select { @@ -299,5 +293,5 @@ func (s *Sync) GetOffset(ctx context.Context, id uint64, prs []p2p.Peer) (time.D if round.Ready() { return round.Offset(), nil } - return 0, fmt.Errorf("%w: failed on timeout", ErrTimesyncFailed) + return 0, fmt.Errorf("%w: failed on timeout", errTimesyncFailed) } diff --git a/timesync/peersync/sync_test.go b/timesync/peersync/sync_test.go index 3880af48f9..5e87ff6384 100644 --- a/timesync/peersync/sync_test.go +++ b/timesync/peersync/sync_test.go @@ -10,8 +10,8 @@ import ( "github.com/spacemeshos/go-scale/tester" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" - "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/timesync/peersync/mocks" ) @@ -70,7 +70,7 @@ func TestSyncGetOffset(t *testing.T) { sync := New(mesh.Hosts()[0], nil, WithTime(tm)) offset, err := sync.GetOffset(context.TODO(), 0, peers) - require.ErrorIs(t, err, ErrTimesyncFailed) + require.ErrorIs(t, err, errTimesyncFailed) require.Empty(t, offset) }) } @@ -118,7 +118,7 @@ func TestSyncTerminateOnError(t *testing.T) { }() select { case err := <-errors: - require.ErrorContains(t, err, ErrPeersNotSynced.Error()) + require.ErrorIs(t, err, errPeersNotSynced) case <-time.After(100 * time.Millisecond): require.FailNow(t, "timed out waiting for sync to fail") } @@ -132,7 +132,7 @@ func TestSyncSimulateMultiple(t *testing.T) { delays := []time.Duration{0, 1200 * time.Millisecond, 1900 * time.Millisecond, 10 * time.Second} instances := []*Sync{} - errors := []error{ErrPeersNotSynced, nil, nil, ErrPeersNotSynced} + errors := []error{errPeersNotSynced, nil, nil, errPeersNotSynced} mesh, err := mocknet.FullMeshLinked(len(delays)) require.NoError(t, err) hosts := []*p2p.Host{} @@ -150,7 +150,7 @@ func TestSyncSimulateMultiple(t *testing.T) { sync := New(hosts[i], hosts[i], WithConfig(config), WithTime(delayedTime(delay)), - WithLog(logtest.New(t).Named(fmt.Sprintf("%d-%s", i, hosts[i].ID()))), + WithLog(zaptest.NewLogger(t).Named(fmt.Sprintf("%d-%s", i, hosts[i].ID()))), ) instances = append(instances, sync) } @@ -168,7 +168,7 @@ func TestSyncSimulateMultiple(t *testing.T) { }() select { case err := <-wait: - require.ErrorContains(t, err, errors[i].Error()) + require.ErrorIs(t, err, errors[i]) case <-time.After(1000 * time.Millisecond): require.FailNowf(t, "timed out waiting for an error", "node %d", i) } diff --git a/tortoise/model/core.go b/tortoise/model/core.go index 0dc78d43da..22a1040ffe 100644 --- a/tortoise/model/core.go +++ b/tortoise/model/core.go @@ -18,6 +18,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/beacons" "github.com/spacemeshos/go-spacemesh/sql/blocks" "github.com/spacemeshos/go-spacemesh/sql/certificates" + "github.com/spacemeshos/go-spacemesh/sql/identities" "github.com/spacemeshos/go-spacemesh/sql/layers" "github.com/spacemeshos/go-spacemesh/sql/statesql" "github.com/spacemeshos/go-spacemesh/tortoise" @@ -176,7 +177,7 @@ func (c *core) OnMessage(m Messenger, event Message) { ev.Atx.BaseTickHeight = 1 ev.Atx.TickCount = 2 atxs.Add(c.cdb, ev.Atx, types.AtxBlob{}) - malicious, err := c.cdb.IsMalicious(ev.Atx.SmesherID) + malicious, err := identities.IsMalicious(c.cdb, ev.Atx.SmesherID) if err != nil { c.logger.Fatal("failed is malicious lookup", zap.Error(err)) } diff --git a/tortoise/replay/replay_test.go b/tortoise/replay/replay_test.go index 9f6dd399fa..0393919fd4 100644 --- a/tortoise/replay/replay_test.go +++ b/tortoise/replay/replay_test.go @@ -57,7 +57,7 @@ func TestReplayMainnet(t *testing.T) { require.NoError(t, err) start := time.Now() - atxsdata, err := atxsdata.Warm(db, cfg.Tortoise.WindowSizeEpochs(applied)) + atxsdata, err := atxsdata.Warm(db, cfg.Tortoise.WindowSizeEpochs(applied), logger) require.NoError(t, err) trtl, err := tortoise.Recover( context.Background(), diff --git a/tortoise/tortoise.go b/tortoise/tortoise.go index 0d6b60983b..b51f052bda 100644 --- a/tortoise/tortoise.go +++ b/tortoise/tortoise.go @@ -869,8 +869,8 @@ func (t *turtle) compareBeacons(bid types.BallotID, lid types.LayerID, beacon ty t.logger.Debug("ballot has different beacon", zap.Uint32("layer_id", lid.Uint32()), zap.Stringer("block", bid), - log.ZShortStringer("ballot_beacon", beacon), - log.ZShortStringer("epoch_beacon", epoch.beacon), + zap.Stringer("ballot_beacon", beacon), + zap.Stringer("epoch_beacon", epoch.beacon), ) return true, nil } diff --git a/txs/cache.go b/txs/cache.go index d3d35a605c..404079c698 100644 --- a/txs/cache.go +++ b/txs/cache.go @@ -97,7 +97,7 @@ func (ac *accountCache) availBalance() uint64 { func (ac *accountCache) precheck(logger *zap.Logger, ntx *NanoTX) (*list.Element, *candidate, error) { if ac.txsByNonce.Len() >= maxTXsPerAcct { ac.moreInDB = true - return nil, nil, errTooManyNonce + return nil, nil, fmt.Errorf("%w: len %d", errTooManyNonce, ac.txsByNonce.Len()) } balance := ac.startBalance var prev *list.Element @@ -115,14 +115,6 @@ func (ac *accountCache) precheck(logger *zap.Logger, ntx *NanoTX) (*list.Element break } if balance < ntx.MaxSpending() { - ac.moreInDB = true - logger.Debug("insufficient balance", - zap.Stringer("tx_id", ntx.ID), - zap.Stringer("address", ntx.Principal), - zap.Uint64("nonce", ntx.Nonce), - zap.Uint64("cons_balance", balance), - zap.Uint64("cons_spending", ntx.MaxSpending()), - ) return nil, nil, errInsufficientBalance } return prev, &candidate{best: ntx, postBalance: balance - ntx.MaxSpending()}, nil @@ -256,30 +248,6 @@ func (ac *accountCache) addBatch(logger *zap.Logger, nonce2TXs map[uint64][]*Nan } ac.moreInDB = len(sortedNonce) > len(added) - if len(added) > 0 { - logger.Debug("added batch to account pool", - zap.Stringer("address", ac.addr), - zap.Array("batch", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { - slices.Sort(added) - for _, nonce := range added { - encoder.AppendUint64(nonce) - } - return nil - })), - ) - } else { - logger.Debug("no feasible txs from batch", - zap.Stringer("address", ac.addr), - zap.Array("batch", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { - nonces := maps.Keys(nonce2TXs) - slices.Sort(nonces) - for _, nonce := range nonces { - encoder.AppendUint64(nonce) - } - return nil - })), - ) - } return nil } @@ -302,11 +270,6 @@ func findBest(ntxs []*NanoTX, balance uint64, blockSeed []byte) *NanoTX { // - nonce not present: add to cache. func (ac *accountCache) add(logger *zap.Logger, tx *types.Transaction, received time.Time) error { if tx.Nonce < ac.startNonce { - logger.Debug("nonce too small", - zap.Stringer("tx_id", tx.ID), - zap.Uint64("next_nonce", ac.startNonce), - zap.Uint64("tx_nonce", tx.Nonce), - ) return errBadNonce } @@ -338,11 +301,7 @@ func (ac *accountCache) addPendingFromNonce( ) error { mtxs, err := transactions.GetAcctPendingFromNonce(db, ac.addr, nonce) if err != nil { - logger.Error("failed to get more pending txs from db", - zap.Stringer("address", ac.addr), - zap.Error(err), - ) - return err + return fmt.Errorf("account pending txs: %w", err) } if len(mtxs) == 0 { @@ -357,7 +316,7 @@ func (ac *accountCache) addPendingFromNonce( } nextLayer, nextBlock, err := getNextIncluded(db, mtx.ID, applied) if err != nil { - return err + return fmt.Errorf("get next included: %w", err) } mtx.LayerID = nextLayer mtx.BlockID = nextBlock @@ -512,7 +471,7 @@ func (c *Cache) buildFromScratch(db sql.StateDatabase) error { } nextLayer, nextBlock, err := getNextIncluded(db, mtx.ID, applied) if err != nil { - return err + return fmt.Errorf("get next included: %w", err) } mtx.LayerID = nextLayer mtx.BlockID = nextBlock @@ -530,7 +489,7 @@ func (c *Cache) BuildFromTXs(rst []*types.MeshTransaction, blockSeed []byte) err for _, tx := range rst { toCleanup[tx.Principal] = struct{}{} } - defer c.cleanupAccounts(toCleanup) + defer c.cleanupAccounts(maps.Keys(toCleanup)...) byPrincipal := groupTXsByPrincipal(c.logger, rst) acctsAdded := 0 @@ -585,23 +544,18 @@ func (c *Cache) MoreInDB(addr types.Address) bool { return acct.moreInDB } -func (c *Cache) cleanupAccounts(accounts map[types.Address]struct{}) { - for addr := range accounts { +func (c *Cache) cleanupAccounts(accounts ...types.Address) { + for _, addr := range accounts { if _, ok := c.pending[addr]; ok && c.pending[addr].shouldEvict() { delete(c.pending, addr) } } } -// - errInsufficientBalance: -// conservative cache is conservative in that it only counts principal's spending for pending transactions. -// a tx rejected due to insufficient balance MAY become feasible after a layer is applied (principal -// received incoming funds). when we receive a errInsufficientBalance tx, we should store it in db and -// re-evaluate it after each layer is applied. // - errTooManyNonce: when a principal has way too many nonces, we don't want to blow up the memory. they should // be stored in db and retrieved after each earlier nonce is applied. func acceptable(err error) bool { - return err == nil || errors.Is(err, errInsufficientBalance) || errors.Is(err, errTooManyNonce) + return err == nil || errors.Is(err, errTooManyNonce) } func (c *Cache) Add( @@ -615,7 +569,7 @@ func (c *Cache) Add( defer c.mu.Unlock() principal := tx.Principal c.createAcctIfNotPresent(principal) - defer c.cleanupAccounts(map[types.Address]struct{}{principal: {}}) + defer c.cleanupAccounts(principal) logger := c.logger.With( log.ZContext(ctx), zap.Stringer("address", principal), @@ -662,10 +616,10 @@ func (c *Cache) LinkTXsWithProposal( return nil } if err := addToProposal(db, lid, pid, tids); err != nil { - c.logger.Error("failed to link txs to proposal in db", zap.Error(err)) - return err + return fmt.Errorf("linking txs to proposal: %w", err) } - return c.updateLayer(lid, types.EmptyBlockID, tids) + c.updateLayer(lid, types.EmptyBlockID, tids) + return nil } // LinkTXsWithBlock associates the transactions to a block. @@ -679,27 +633,27 @@ func (c *Cache) LinkTXsWithBlock( return nil } if err := addToBlock(db, lid, bid, tids); err != nil { - return err + return fmt.Errorf("add to block: %w", err) } - return c.updateLayer(lid, bid, tids) + c.updateLayer(lid, bid, tids) + return nil } // updateLayer associates the transactions to a layer and optionally a block. // A transaction is tagged with a layer when it's included in a proposal/block. // If a transaction is included in multiple proposals/blocks in different layers, // the lowest layer is retained. -func (c *Cache) updateLayer(lid types.LayerID, bid types.BlockID, tids []types.TransactionID) error { +func (c *Cache) updateLayer(lid types.LayerID, bid types.BlockID, tids []types.TransactionID) { c.mu.Lock() defer c.mu.Unlock() for _, ID := range tids { if _, ok := c.cachedTXs[ID]; !ok { // transaction is not considered best in its nonce group - return nil + return } c.cachedTXs[ID].UpdateLayerMaybe(lid, bid) } - return nil } func (c *Cache) applyEmptyLayer(db sql.StateDatabase, lid types.LayerID) error { @@ -794,7 +748,7 @@ func (c *Cache) ApplyLayer( } toReset[tx.Principal] = struct{}{} } - defer c.cleanupAccounts(toCleanup) + defer c.cleanupAccounts(maps.Keys(toCleanup)...) for principal := range byPrincipal { c.createAcctIfNotPresent(principal) @@ -844,8 +798,7 @@ func (c *Cache) RevertToLayer(db sql.StateDatabase, revertTo types.LayerID) erro } if err := c.buildFromScratch(db); err != nil { - c.logger.Error("failed to build from scratch after revert", zap.Error(err)) - return err + return fmt.Errorf("building from scratch after revert: %w", err) } return nil } @@ -863,14 +816,14 @@ func (c *Cache) GetProjection(addr types.Address) (uint64, uint64) { } // GetMempool returns all the transactions that eligible for a proposal/block. -func (c *Cache) GetMempool(logger *zap.Logger) map[types.Address][]*NanoTX { +func (c *Cache) GetMempool() map[types.Address][]*NanoTX { c.mu.Lock() defer c.mu.Unlock() all := make(map[types.Address][]*NanoTX) - logger.Debug("cache has pending accounts", zap.Int("num_acct", len(c.pending))) + c.logger.Debug("cache has pending accounts", zap.Int("num_acct", len(c.pending))) for addr, accCache := range c.pending { - txs := accCache.getMempool(logger.With(zap.Stringer("address", addr))) + txs := accCache.getMempool(c.logger.With(zap.Stringer("address", addr))) if len(txs) > 0 { all[addr] = txs } @@ -882,7 +835,6 @@ func (c *Cache) GetMempool(logger *zap.Logger) map[types.Address][]*NanoTX { func checkApplyOrder(logger *zap.Logger, db sql.StateDatabase, toApply types.LayerID) error { lastApplied, err := layers.GetLastApplied(db) if err != nil { - logger.Error("failed to get last applied layer", zap.Error(err)) return fmt.Errorf("cache get last applied %w", err) } if toApply != lastApplied.Add(1) { diff --git a/txs/cache_test.go b/txs/cache_test.go index 2658cb1dfc..e172e11c5e 100644 --- a/txs/cache_test.go +++ b/txs/cache_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stretchr/testify/require" + "go.uber.org/zap" "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/common/types" @@ -97,6 +98,7 @@ func saveTXs(t *testing.T, db sql.StateDatabase, mtxs []*types.MeshTransaction) } func checkTXStateFromDB(t *testing.T, db sql.StateDatabase, txs []*types.MeshTransaction, state types.TXState) { + t.Helper() for _, mtx := range txs { got, err := transactions.Get(db, mtx.ID) require.NoError(t, err) @@ -105,6 +107,7 @@ func checkTXStateFromDB(t *testing.T, db sql.StateDatabase, txs []*types.MeshTra } func checkTXNotInDB(t *testing.T, db sql.StateDatabase, tid types.TransactionID) { + t.Helper() _, err := transactions.Get(db, tid) require.ErrorIs(t, err, sql.ErrNotFound) } @@ -125,7 +128,7 @@ func checkNoTX(t *testing.T, c *Cache, tid types.TransactionID) { func checkMempool(t *testing.T, c *Cache, expected map[types.Address][]*types.MeshTransaction) { t.Helper() - mempool := c.GetMempool(c.logger) + mempool := c.GetMempool() require.Len(t, mempool, len(expected)) for addr := range mempool { var ( @@ -186,7 +189,7 @@ func createSingleAccountTestCache(tb testing.TB) (*testCache, *testAcct) { states := map[types.Address]*testAcct{principal: ta} db := statesql.InMemory() return &testCache{ - Cache: NewCache(getStateFunc(states), zaptest.NewLogger(tb)), + Cache: NewCache(getStateFunc(states), zap.NewNop()), db: db, }, ta } @@ -705,20 +708,46 @@ func TestCache_Account_Add_RandomOrder(t *testing.T) { checkTXStateFromDB(t, tc.db, mtxs, types.MEMPOOL) } +func TestCache_Account_ReplaceByFee(t *testing.T) { + tc, ta := createSingleAccountTestCache(t) + ta.balance = uint64(1000000) + checkProjection(t, tc.Cache, ta.principal, ta.nonce, ta.balance) + + now := time.Now() + mtx := newMeshTX(t, ta.nonce, ta.signer, defaultAmount, now.Add(time.Second*time.Duration(10))) + mtx.GasPrice = defaultFee + mtx.MaxGas = 1 + require.NoError(t, transactions.Add(tc.db, &mtx.Transaction, mtx.Received)) + require.NoError(t, tc.buildFromScratch(tc.db)) + checkTX(t, tc.Cache, mtx.ID, 0, types.EmptyBlockID) + + rbfTx := newMeshTX(t, ta.nonce, ta.signer, defaultAmount+500, now.Add(time.Second*time.Duration(10))) + rbfTx.GasPrice = defaultFee + 1 + rbfTx.MaxGas = 1 + err := tc.Cache.Add(context.Background(), tc.db, &rbfTx.Transaction, now.Add(time.Second*time.Duration(20)), false) + require.NoError(t, err) + + checkTX(t, tc.Cache, rbfTx.ID, 0, types.EmptyBlockID) + checkNoTX(t, tc.Cache, mtx.ID) + checkProjection(t, tc.Cache, ta.principal, ta.nonce+1, ta.balance-rbfTx.Spending()) + expectedMempool := map[types.Address][]*types.MeshTransaction{ta.principal: {rbfTx}} + checkMempool(t, tc.Cache, expectedMempool) +} + func TestCache_Account_Add_InsufficientBalance_ResetAfterApply(t *testing.T) { tc, ta := createSingleAccountTestCache(t) buildSingleAccountCache(t, tc, ta, nil) - mtx := &types.MeshTransaction{ Transaction: *newTx(t, ta.nonce, ta.balance, defaultFee, ta.signer), Received: time.Now(), } - require.NoError(t, tc.Add(context.Background(), tc.db, &mtx.Transaction, mtx.Received, false)) + require.ErrorIs(t, tc.Add(context.Background(), tc.db, &mtx.Transaction, + mtx.Received, false), errInsufficientBalance) checkNoTX(t, tc.Cache, mtx.ID) checkProjection(t, tc.Cache, ta.principal, ta.nonce, ta.balance) checkMempool(t, tc.Cache, nil) - require.True(t, tc.MoreInDB(ta.principal)) - checkTXStateFromDB(t, tc.db, []*types.MeshTransaction{mtx}, types.MEMPOOL) + require.False(t, tc.MoreInDB(ta.principal)) + checkTXNotInDB(t, tc.db, mtx.ID) lid := types.LayerID(97) require.NoError(t, layers.SetApplied(tc.db, lid.Sub(1), types.RandomBlockID())) @@ -726,11 +755,9 @@ func TestCache_Account_Add_InsufficientBalance_ResetAfterApply(t *testing.T) { ta.balance += ta.balance require.NoError(t, tc.Cache.ApplyLayer(context.Background(), tc.db, lid, types.BlockID{1, 2, 3}, nil, nil)) - checkTX(t, tc.Cache, mtx.ID, 0, types.EmptyBlockID) - expectedMempool := map[types.Address][]*types.MeshTransaction{ta.principal: {mtx}} - checkMempool(t, tc.Cache, expectedMempool) + checkMempool(t, tc.Cache, nil) require.False(t, tc.MoreInDB(ta.principal)) - checkTXStateFromDB(t, tc.db, []*types.MeshTransaction{mtx}, types.MEMPOOL) + checkTXNotInDB(t, tc.db, mtx.ID) } func TestCache_Account_Add_InsufficientBalance_HigherNonceFeasibleFirst(t *testing.T) { @@ -745,27 +772,26 @@ func TestCache_Account_Add_InsufficientBalance_HigherNonceFeasibleFirst(t *testi Transaction: *newTx(t, ta.nonce+10, ta.balance, defaultFee, ta.signer), Received: time.Now(), } - require.NoError(t, tc.Add(context.Background(), tc.db, &mtx0.Transaction, mtx0.Received, false)) - require.NoError(t, tc.Add(context.Background(), tc.db, &mtx1.Transaction, mtx1.Received, false)) + require.ErrorIs(t, tc.Add(context.Background(), tc.db, &mtx0.Transaction, + mtx0.Received, false), errInsufficientBalance) + require.ErrorIs(t, tc.Add(context.Background(), tc.db, &mtx1.Transaction, + mtx1.Received, false), errInsufficientBalance) checkNoTX(t, tc.Cache, mtx0.ID) checkNoTX(t, tc.Cache, mtx1.ID) checkProjection(t, tc.Cache, ta.principal, ta.nonce, ta.balance) checkMempool(t, tc.Cache, nil) - require.True(t, tc.MoreInDB(ta.principal)) - checkTXStateFromDB(t, tc.db, []*types.MeshTransaction{mtx0, mtx1}, types.MEMPOOL) + require.False(t, tc.MoreInDB(ta.principal)) + checkTXNotInDB(t, tc.db, mtx0.ID) + checkTXNotInDB(t, tc.db, mtx1.ID) lid := types.LayerID(97) require.NoError(t, layers.SetApplied(tc.db, lid.Sub(1), types.RandomBlockID())) - // the account receive enough funds in layer 97 (via rewards or incoming transfer) for mtx1 - ta.balance = mtx1.Spending() require.NoError(t, tc.Cache.ApplyLayer(context.Background(), tc.db, lid, types.BlockID{1, 2, 3}, nil, nil)) checkNoTX(t, tc.Cache, mtx0.ID) - checkTX(t, tc.Cache, mtx1.ID, 0, types.EmptyBlockID) - checkProjection(t, tc.Cache, ta.principal, mtx1.Nonce+1, 0) - expectedMempool := map[types.Address][]*types.MeshTransaction{ta.principal: {mtx1}} - checkMempool(t, tc.Cache, expectedMempool) - require.True(t, tc.MoreInDB(ta.principal)) - checkTXStateFromDB(t, tc.db, []*types.MeshTransaction{mtx0, mtx1}, types.MEMPOOL) + checkNoTX(t, tc.Cache, mtx1.ID) + checkProjection(t, tc.Cache, ta.principal, ta.nonce, ta.balance) + checkMempool(t, tc.Cache, nil) + require.False(t, tc.MoreInDB(ta.principal)) lid = lid.Add(1) require.NoError(t, layers.SetApplied(tc.db, lid.Sub(1), types.RandomBlockID())) @@ -773,13 +799,11 @@ func TestCache_Account_Add_InsufficientBalance_HigherNonceFeasibleFirst(t *testi // but the account receive enough funds in layer 98 (via rewards or incoming transfer) for both mtx0 and mtx1 ta.balance = mtx0.Spending() + mtx1.Spending() require.NoError(t, tc.Cache.ApplyLayer(context.Background(), tc.db, lid, types.BlockID{2, 3, 4}, nil, nil)) - checkTX(t, tc.Cache, mtx0.ID, 0, types.EmptyBlockID) - checkTX(t, tc.Cache, mtx1.ID, 0, types.EmptyBlockID) - checkProjection(t, tc.Cache, ta.principal, mtx1.Nonce+1, 0) - expectedMempool = map[types.Address][]*types.MeshTransaction{ta.principal: {mtx0, mtx1}} - checkMempool(t, tc.Cache, expectedMempool) + checkNoTX(t, tc.Cache, mtx0.ID) + checkNoTX(t, tc.Cache, mtx1.ID) + checkProjection(t, tc.Cache, ta.principal, ta.nonce, ta.balance) + checkMempool(t, tc.Cache, nil) require.False(t, tc.MoreInDB(ta.principal)) - checkTXStateFromDB(t, tc.db, []*types.MeshTransaction{mtx0, mtx1}, types.MEMPOOL) } func TestCache_Account_Add_InsufficientBalance_NewNonce(t *testing.T) { @@ -790,12 +814,13 @@ func TestCache_Account_Add_InsufficientBalance_NewNonce(t *testing.T) { Transaction: *newTx(t, ta.nonce, defaultBalance, defaultFee, ta.signer), Received: time.Now(), } - require.NoError(t, tc.Add(context.Background(), tc.db, &mtx.Transaction, mtx.Received, false)) + require.ErrorIs(t, tc.Add(context.Background(), tc.db, &mtx.Transaction, + mtx.Received, false), errInsufficientBalance) checkNoTX(t, tc.Cache, mtx.ID) checkProjection(t, tc.Cache, ta.principal, ta.nonce, ta.balance) checkMempool(t, tc.Cache, nil) - require.True(t, tc.MoreInDB(ta.principal)) - checkTXStateFromDB(t, tc.db, []*types.MeshTransaction{mtx}, types.MEMPOOL) + require.False(t, tc.MoreInDB(ta.principal)) + checkTXNotInDB(t, tc.db, mtx.ID) } func TestCache_Account_Add_InsufficientBalance_ExistingNonce(t *testing.T) { @@ -811,12 +836,13 @@ func TestCache_Account_Add_InsufficientBalance_ExistingNonce(t *testing.T) { Transaction: *newTx(t, ta.nonce, ta.balance, defaultFee, ta.signer), Received: time.Now(), } - require.NoError(t, tc.Add(context.Background(), tc.db, &spender.Transaction, spender.Received, false)) + require.ErrorIs(t, tc.Add(context.Background(), tc.db, &spender.Transaction, + spender.Received, false), errInsufficientBalance) checkNoTX(t, tc.Cache, spender.ID) checkProjection(t, tc.Cache, ta.principal, ta.nonce+1, ta.balance-mtx.Spending()) expectedMempool := map[types.Address][]*types.MeshTransaction{ta.principal: {mtx}} checkMempool(t, tc.Cache, expectedMempool) - checkTXStateFromDB(t, tc.db, []*types.MeshTransaction{mtx, spender}, types.MEMPOOL) + checkTXNotInDB(t, tc.db, spender.ID) } func TestCache_Account_AppliedTXsNotInCache(t *testing.T) { @@ -890,10 +916,11 @@ func TestCache_Account_BalanceRelaxedAfterApply(t *testing.T) { largeAmount := defaultBalance for _, p := range pending { p.MaxSpend = largeAmount - require.NoError(t, tc.Add(context.Background(), tc.db, &p.Transaction, p.Received, false)) + require.ErrorIs(t, tc.Add(context.Background(), tc.db, &p.Transaction, + p.Received, false), errInsufficientBalance) checkNoTX(t, tc.Cache, p.ID) + checkTXNotInDB(t, tc.db, p.ID) } - checkTXStateFromDB(t, tc.db, pending, types.MEMPOOL) checkProjection(t, tc.Cache, ta.principal, newNextNonce, newBalance) expectedMempool := map[types.Address][]*types.MeshTransaction{ta.principal: {mtx}} checkMempool(t, tc.Cache, expectedMempool) @@ -903,28 +930,24 @@ func TestCache_Account_BalanceRelaxedAfterApply(t *testing.T) { // transactions in `pending` feasible now income := defaultBalance * 100 ta.nonce++ - ta.balance = ta.balance - mtx.Spending() + income + ta.balance = ta.balance + income - mtx.Spending() lid := types.LayerID(97) require.NoError(t, layers.SetApplied(tc.db, lid.Sub(1), types.RandomBlockID())) bid := types.BlockID{1, 2, 3} applied := makeResults(lid, bid, mtx.Transaction) require.NoError(t, tc.ApplyLayer(context.Background(), tc.db, lid, bid, applied, []types.Transaction{})) // all pending txs are added to cache now - newNextNonce = ta.nonce + uint64(len(pending)) + newNextNonce = ta.nonce newBalance = ta.balance - for _, p := range pending { - newBalance -= p.Spending() - } checkProjection(t, tc.Cache, ta.principal, newNextNonce, newBalance) - expectedMempool = map[types.Address][]*types.MeshTransaction{ta.principal: pending} - checkMempool(t, tc.Cache, expectedMempool) + checkMempool(t, tc.Cache, nil) checkTXStateFromDB(t, tc.db, []*types.MeshTransaction{mtx}, types.APPLIED) - checkTXStateFromDB(t, tc.db, pending, types.MEMPOOL) } func TestCache_Account_BalanceRelaxedAfterApply_EvictLaterNonce(t *testing.T) { tc, ta := createSingleAccountTestCache(t) mtxs := genAndSaveTXs(t, tc.db, ta.signer, ta.nonce, ta.nonce+4, time.Now()) + newNextNonce, newBalance := buildSingleAccountCache(t, tc, ta, mtxs) higherFee := defaultFee + 1 @@ -934,29 +957,27 @@ func TestCache_Account_BalanceRelaxedAfterApply_EvictLaterNonce(t *testing.T) { Received: time.Now(), } - require.NoError(t, tc.Add(context.Background(), tc.db, &better.Transaction, better.Received, false)) + require.ErrorIs(t, tc.Add(context.Background(), tc.db, &better.Transaction, + better.Received, false), errInsufficientBalance) checkNoTX(t, tc.Cache, better.ID) checkProjection(t, tc.Cache, ta.principal, newNextNonce, newBalance) expectedMempool := map[types.Address][]*types.MeshTransaction{ta.principal: mtxs} checkMempool(t, tc.Cache, expectedMempool) - checkTXStateFromDB(t, tc.db, append(mtxs, better), types.MEMPOOL) + checkTXStateFromDB(t, tc.db, mtxs, types.MEMPOOL) + checkTXNotInDB(t, tc.db, better.ID) // apply lid // there is also an incoming fund of `income` to the principal's account // the income is just enough to allow `better` to be feasible - income := mtxs[0].Spending() - ta.nonce++ - ta.balance = ta.balance - mtxs[0].Spending() + income lid := types.LayerID(97) require.NoError(t, layers.SetApplied(tc.db, lid.Sub(1), types.RandomBlockID())) bid := types.BlockID{1, 2, 3} applied := makeResults(lid, bid, mtxs[0].Transaction) require.NoError(t, tc.ApplyLayer(context.Background(), tc.db, lid, bid, applied, []types.Transaction{})) - checkProjection(t, tc.Cache, ta.principal, ta.nonce+1, 0) - expectedMempool = map[types.Address][]*types.MeshTransaction{ta.principal: {better}} + expectedMempool = map[types.Address][]*types.MeshTransaction{ta.principal: mtxs[1:]} checkMempool(t, tc.Cache, expectedMempool) checkTXStateFromDB(t, tc.db, mtxs[:1], types.APPLIED) - checkTXStateFromDB(t, tc.db, append(mtxs[1:], better), types.MEMPOOL) + checkTXStateFromDB(t, tc.db, mtxs[1:], types.MEMPOOL) } func TestCache_Account_EvictedAfterApply(t *testing.T) { @@ -1079,7 +1100,7 @@ func buildSmallCache( func checkMempoolSize(t *testing.T, c *Cache, expected int) { t.Helper() - mempool := c.GetMempool(c.logger) + mempool := c.GetMempool() numTXs := 0 for _, ntxs := range mempool { numTXs += len(ntxs) diff --git a/txs/conservative_state.go b/txs/conservative_state.go index def31171bf..afe2599a4b 100644 --- a/txs/conservative_state.go +++ b/txs/conservative_state.go @@ -8,6 +8,7 @@ import ( "time" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/events" @@ -221,3 +222,54 @@ func (cs *ConservativeState) GetTransactionsByAddress( ) ([]*types.MeshTransaction, error) { return transactions.GetByAddress(cs.db, from, to, address) } + +// ShuffleWithNonceOrder perform a Fisher-Yates shuffle on the transactions. +// note that after shuffling, the original list of transactions are no longer in nonce order +// within the same principal. we simply check which principal occupies the spot after +// the shuffle and retrieve their transactions in nonce order. +func ShuffleWithNonceOrder( + logger *zap.Logger, + rng *rand.Rand, + numTXs int, + ntxs []*NanoTX, + byAddrAndNonce map[types.Address][]*NanoTX, +) []types.TransactionID { + rng.Shuffle(len(ntxs), func(i, j int) { ntxs[i], ntxs[j] = ntxs[j], ntxs[i] }) + total := min(len(ntxs), numTXs) + result := make([]types.TransactionID, 0, total) + packed := make(map[types.Address][]uint64) + for _, ntx := range ntxs[:total] { + // if a spot is taken by a principal, we add its TX for the next eligible nonce + p := ntx.Principal + if _, ok := byAddrAndNonce[p]; !ok { + logger.Fatal("principal missing", zap.Stringer("address", p)) + } + if len(byAddrAndNonce[p]) == 0 { + logger.Fatal("txs missing", zap.Stringer("address", p)) + } + toAdd := byAddrAndNonce[p][0] + result = append(result, toAdd.ID) + if _, ok := packed[p]; !ok { + packed[p] = []uint64{toAdd.Nonce, toAdd.Nonce} + } else { + packed[p][1] = toAdd.Nonce + } + if len(byAddrAndNonce[p]) == 1 { + delete(byAddrAndNonce, p) + } else { + byAddrAndNonce[p] = byAddrAndNonce[p][1:] + } + } + logger.Debug("packed txs", zap.Array("ranges", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { + for addr, nonces := range packed { + _ = encoder.AppendObject(zapcore.ObjectMarshalerFunc(func(encoder zapcore.ObjectEncoder) error { + encoder.AddString("addr", addr.String()) + encoder.AddUint64("from", nonces[0]) + encoder.AddUint64("to", nonces[1]) + return nil + })) + } + return nil + }))) + return result +} diff --git a/txs/conservative_state_test.go b/txs/conservative_state_test.go index f9dd30478b..2660a28296 100644 --- a/txs/conservative_state_test.go +++ b/txs/conservative_state_test.go @@ -16,7 +16,6 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "go.uber.org/zap" - "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/genvm/sdk" @@ -92,7 +91,7 @@ func createTestState(t *testing.T, gasLimit uint64) *testConState { BlockGasLimit: gasLimit, NumTXsPerProposal: numTXsInProposal, } - logger := zaptest.NewLogger(t) + logger := zap.NewNop() _, pub, err := crypto.GenerateEd25519Key(nil) require.NoError(t, err) id, err := peer.IDFromPublicKey(pub) @@ -110,10 +109,6 @@ func createTestState(t *testing.T, gasLimit uint64) *testConState { } } -func createConservativeState(t *testing.T) *testConState { - return createTestState(t, math.MaxUint64) -} - func addBatch(tb testing.TB, tcs *testConState, numTXs int) ([]types.TransactionID, []*types.Transaction) { tb.Helper() ids := make([]types.TransactionID, 0, numTXs) @@ -133,7 +128,7 @@ func addBatch(tb testing.TB, tcs *testConState, numTXs int) ([]types.Transaction } func TestSelectProposalTXs(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) numTXs := 2 * numTXsInProposal lid := types.LayerID(97) bid := types.BlockID{100} @@ -199,7 +194,7 @@ func TestSelectProposalTXs_ExhaustGas(t *testing.T) { } func TestSelectProposalTXs_ExhaustMemPool(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) numTXs := numTXsInProposal - 1 lid := types.LayerID(97) bid := types.BlockID{100} @@ -250,7 +245,7 @@ func TestSelectProposalTXs_ExhaustMemPool(t *testing.T) { } func TestSelectProposalTXs_SamePrincipal(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) signer, err := signing.NewEdSigner() require.NoError(t, err) addr := types.GenerateAddress(signer.PublicKey().Bytes()) @@ -283,7 +278,7 @@ func TestSelectProposalTXs_TwoPrincipals(t *testing.T) { numTXs = numInProposal * 2 numInDBs = numInProposal ) - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) signer1, err := signing.NewEdSigner() require.NoError(t, err) addr1 := types.GenerateAddress(signer1.PublicKey().Bytes()) @@ -335,7 +330,7 @@ func TestSelectProposalTXs_TwoPrincipals(t *testing.T) { } func TestGetProjection(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) signer, err := signing.NewEdSigner() require.NoError(t, err) addr := types.GenerateAddress(signer.PublicKey().Bytes()) @@ -353,7 +348,7 @@ func TestGetProjection(t *testing.T) { } func TestAddToCache(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) signer, err := signing.NewEdSigner() require.NoError(t, err) addr := types.GenerateAddress(signer.PublicKey().Bytes()) @@ -369,7 +364,7 @@ func TestAddToCache(t *testing.T) { } func TestAddToCache_BadNonceNotPersisted(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) tx := &types.Transaction{ RawTx: types.NewRawTx([]byte{1, 1, 1}), TxHeader: &types.TxHeader{ @@ -384,7 +379,7 @@ func TestAddToCache_BadNonceNotPersisted(t *testing.T) { } func TestAddToCache_NonceGap(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) tx := &types.Transaction{ RawTx: types.NewRawTx([]byte{1, 1, 1}), TxHeader: &types.TxHeader{ @@ -401,21 +396,21 @@ func TestAddToCache_NonceGap(t *testing.T) { } func TestAddToCache_InsufficientBalance(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) signer, err := signing.NewEdSigner() require.NoError(t, err) addr := types.GenerateAddress(signer.PublicKey().Bytes()) tcs.mvm.EXPECT().GetBalance(addr).Return(defaultAmount, nil).Times(1) tcs.mvm.EXPECT().GetNonce(addr).Return(nonce, nil).Times(1) tx := newTx(t, nonce, defaultAmount, defaultFee, signer) - require.NoError(t, tcs.AddToCache(context.Background(), tx, time.Now())) + require.ErrorIs(t, tcs.AddToCache(context.Background(), tx, time.Now()), errInsufficientBalance) checkNoTX(t, tcs.cache, tx.ID) - require.True(t, tcs.cache.MoreInDB(addr)) - checkTXStateFromDB(t, tcs.db, []*types.MeshTransaction{{Transaction: *tx}}, types.MEMPOOL) + require.False(t, tcs.cache.MoreInDB(addr)) + checkTXNotInDB(t, tcs.db, tx.ID) } func TestAddToCache_TooManyForOneAccount(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) signer, err := signing.NewEdSigner() require.NoError(t, err) addr := types.GenerateAddress(signer.PublicKey().Bytes()) @@ -432,7 +427,7 @@ func TestAddToCache_TooManyForOneAccount(t *testing.T) { } func TestGetMeshTransaction(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) signer, err := signing.NewEdSigner() require.NoError(t, err) addr := types.GenerateAddress(signer.PublicKey().Bytes()) @@ -459,7 +454,7 @@ func TestGetMeshTransaction(t *testing.T) { } func TestUpdateCache_UpdateHeader(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) lid := types.LayerID(1) signer, err := signing.NewEdSigner() @@ -501,7 +496,7 @@ func TestUpdateCache_UpdateHeader(t *testing.T) { } func TestUpdateCache(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) lid := types.LayerID(1) ids, txs := addBatch(t, tcs, numTXs) block := types.NewExistingBlock(types.BlockID{1}, @@ -533,7 +528,7 @@ func TestUpdateCache(t *testing.T) { } func TestUpdateCache_EmptyLayer(t *testing.T) { - tcs := createConservativeState(t) + tcs := createTestState(t, math.MaxUint64) lid := types.LayerID(1) ids, _ := addBatch(t, tcs, numTXs) require.NoError(t, tcs.LinkTXsWithBlock(lid, types.BlockID{1, 2, 3}, ids)) @@ -553,8 +548,8 @@ func TestConsistentHandling(t *testing.T) { // conservative cache state instances := []*testConState{ - createConservativeState(t), - createConservativeState(t), + createTestState(t, math.MaxUint64), + createTestState(t, math.MaxUint64), } rng := mrand.New(mrand.NewSource(101)) diff --git a/txs/interface.go b/txs/interface.go index f4414e0276..4c0709b9a0 100644 --- a/txs/interface.go +++ b/txs/interface.go @@ -4,8 +4,6 @@ import ( "context" "time" - "go.uber.org/zap" - "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/system" ) @@ -31,5 +29,5 @@ type vmState interface { } type conStateCache interface { - GetMempool(*zap.Logger) map[types.Address][]*NanoTX + GetMempool() map[types.Address][]*NanoTX } diff --git a/txs/mempool_iterator.go b/txs/mempool_iterator.go index 52f1fb185e..822a3cbcb2 100644 --- a/txs/mempool_iterator.go +++ b/txs/mempool_iterator.go @@ -81,7 +81,7 @@ type mempoolIterator struct { // newMempoolIterator builds and returns a mempoolIterator. func newMempoolIterator(logger *zap.Logger, cs conStateCache, gasLimit uint64) *mempoolIterator { - txs := cs.GetMempool(logger) + txs := cs.GetMempool() mi := &mempoolIterator{ logger: logger, gasRemaining: gasLimit, diff --git a/txs/mempool_iterator_test.go b/txs/mempool_iterator_test.go index d69f55ab4c..738d5d18d2 100644 --- a/txs/mempool_iterator_test.go +++ b/txs/mempool_iterator_test.go @@ -82,7 +82,7 @@ func TestPopAll(t *testing.T) { mempool, expected := makeMempool() ctrl := gomock.NewController(t) mockCache := NewMockconStateCache(ctrl) - mockCache.EXPECT().GetMempool(gomock.Any()).Return(mempool) + mockCache.EXPECT().GetMempool().Return(mempool) gasLimit := uint64(3) mi := newMempoolIterator(zaptest.NewLogger(t), mockCache, gasLimit) testPopAll(t, mi, expected[:gasLimit]) @@ -93,7 +93,7 @@ func TestPopAll_SkipSomeGasTooHigh(t *testing.T) { mempool, orderedByFee := makeMempool() ctrl := gomock.NewController(t) mockCache := NewMockconStateCache(ctrl) - mockCache.EXPECT().GetMempool(gomock.Any()).Return(mempool) + mockCache.EXPECT().GetMempool().Return(mempool) gasLimit := uint64(3) // make the 2nd one too expensive to pick, therefore invalidated all txs from addr0 orderedByFee[1].MaxGas = 10 @@ -107,7 +107,7 @@ func TestPopAll_ExhaustMempool(t *testing.T) { mempool, expected := makeMempool() ctrl := gomock.NewController(t) mockCache := NewMockconStateCache(ctrl) - mockCache.EXPECT().GetMempool(gomock.Any()).Return(mempool) + mockCache.EXPECT().GetMempool().Return(mempool) gasLimit := uint64(100) mi := newMempoolIterator(zaptest.NewLogger(t), mockCache, gasLimit) testPopAll(t, mi, expected) diff --git a/txs/txs_mocks.go b/txs/txs_mocks.go index 1cc13e526f..d53b6654b5 100644 --- a/txs/txs_mocks.go +++ b/txs/txs_mocks.go @@ -17,7 +17,6 @@ import ( types "github.com/spacemeshos/go-spacemesh/common/types" system "github.com/spacemeshos/go-spacemesh/system" gomock "go.uber.org/mock/gomock" - zap "go.uber.org/zap" ) // MockconservativeState is a mock of conservativeState interface. @@ -554,17 +553,17 @@ func (m *MockconStateCache) EXPECT() *MockconStateCacheMockRecorder { } // GetMempool mocks base method. -func (m *MockconStateCache) GetMempool(arg0 *zap.Logger) map[types.Address][]*NanoTX { +func (m *MockconStateCache) GetMempool() map[types.Address][]*NanoTX { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMempool", arg0) + ret := m.ctrl.Call(m, "GetMempool") ret0, _ := ret[0].(map[types.Address][]*NanoTX) return ret0 } // GetMempool indicates an expected call of GetMempool. -func (mr *MockconStateCacheMockRecorder) GetMempool(arg0 any) *MockconStateCacheGetMempoolCall { +func (mr *MockconStateCacheMockRecorder) GetMempool() *MockconStateCacheGetMempoolCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMempool", reflect.TypeOf((*MockconStateCache)(nil).GetMempool), arg0) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMempool", reflect.TypeOf((*MockconStateCache)(nil).GetMempool)) return &MockconStateCacheGetMempoolCall{Call: call} } @@ -580,13 +579,13 @@ func (c *MockconStateCacheGetMempoolCall) Return(arg0 map[types.Address][]*NanoT } // Do rewrite *gomock.Call.Do -func (c *MockconStateCacheGetMempoolCall) Do(f func(*zap.Logger) map[types.Address][]*NanoTX) *MockconStateCacheGetMempoolCall { +func (c *MockconStateCacheGetMempoolCall) Do(f func() map[types.Address][]*NanoTX) *MockconStateCacheGetMempoolCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockconStateCacheGetMempoolCall) DoAndReturn(f func(*zap.Logger) map[types.Address][]*NanoTX) *MockconStateCacheGetMempoolCall { +func (c *MockconStateCacheGetMempoolCall) DoAndReturn(f func() map[types.Address][]*NanoTX) *MockconStateCacheGetMempoolCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/txs/utils.go b/txs/utils.go deleted file mode 100644 index c137339939..0000000000 --- a/txs/utils.go +++ /dev/null @@ -1,61 +0,0 @@ -package txs - -import ( - "math/rand" - - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - - "github.com/spacemeshos/go-spacemesh/common/types" -) - -// ShuffleWithNonceOrder perform a Fisher-Yates shuffle on the transactions. -// note that after shuffling, the original list of transactions are no longer in nonce order -// within the same principal. we simply check which principal occupies the spot after -// the shuffle and retrieve their transactions in nonce order. -func ShuffleWithNonceOrder( - logger *zap.Logger, - rng *rand.Rand, - numTXs int, - ntxs []*NanoTX, - byAddrAndNonce map[types.Address][]*NanoTX, -) []types.TransactionID { - rng.Shuffle(len(ntxs), func(i, j int) { ntxs[i], ntxs[j] = ntxs[j], ntxs[i] }) - total := min(len(ntxs), numTXs) - result := make([]types.TransactionID, 0, total) - packed := make(map[types.Address][]uint64) - for _, ntx := range ntxs[:total] { - // if a spot is taken by a principal, we add its TX for the next eligible nonce - p := ntx.Principal - if _, ok := byAddrAndNonce[p]; !ok { - logger.Fatal("principal missing", zap.Stringer("address", p)) - } - if len(byAddrAndNonce[p]) == 0 { - logger.Fatal("txs missing", zap.Stringer("address", p)) - } - toAdd := byAddrAndNonce[p][0] - result = append(result, toAdd.ID) - if _, ok := packed[p]; !ok { - packed[p] = []uint64{toAdd.Nonce, toAdd.Nonce} - } else { - packed[p][1] = toAdd.Nonce - } - if len(byAddrAndNonce[p]) == 1 { - delete(byAddrAndNonce, p) - } else { - byAddrAndNonce[p] = byAddrAndNonce[p][1:] - } - } - logger.Debug("packed txs", zap.Array("ranges", zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error { - for addr, nonces := range packed { - _ = encoder.AppendObject(zapcore.ObjectMarshalerFunc(func(encoder zapcore.ObjectEncoder) error { - encoder.AddString("addr", addr.String()) - encoder.AddUint64("from", nonces[0]) - encoder.AddUint64("to", nonces[1]) - return nil - })) - } - return nil - }))) - return result -}