diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 730e964b5b..6b6ac8af15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ concurrency: jobs: ## stage 0: check which files were changed filter-changes: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 outputs: nondocchanges: ${{ steps.filter.outputs.nondoc }} steps: @@ -55,7 +55,7 @@ jobs: ## these run on all pushes to all pull requests, all branches ## note that secrets may not be accessible in this phase quicktests: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: filter-changes if: ${{ needs.filter-changes.outputs.nondocchanges == 'true' }} # should not take more than 2-3 mins @@ -87,7 +87,7 @@ jobs: make test-generate lint: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: filter-changes if: ${{ needs.filter-changes.outputs.nondocchanges == 'true' }} # should not take more than 15-16 mins @@ -126,17 +126,17 @@ jobs: fail-fast: true matrix: os: - - ubuntu-latest + - ubuntu-22.04 - [self-hosted, linux, arm64] - macos-13 - [self-hosted, macOS, ARM64, go-spacemesh] - - windows-latest + - windows-2022 steps: - name: Add OpenCL support - Ubuntu - if: ${{ matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.os == 'ubuntu-22.04' }} 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-latest' }} + if: ${{ matrix.os == 'windows-2022' }} run: | Set-MpPreference -DisableRealtimeMonitoring $true - name: checkout @@ -172,17 +172,17 @@ jobs: fail-fast: true matrix: os: - - ubuntu-latest + - ubuntu-22.04 - [self-hosted, linux, arm64] - macos-13 - [self-hosted, macOS, ARM64, go-spacemesh] - - windows-latest + - windows-2022 steps: - name: Add OpenCL support - Ubuntu - if: ${{ matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.os == 'ubuntu-22.04' }} 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-latest' }} + if: ${{ matrix.os == 'windows-2022' }} run: | Set-MpPreference -DisableRealtimeMonitoring $true - name: checkout @@ -216,16 +216,16 @@ jobs: fail-fast: true matrix: os: - - ubuntu-latest + - ubuntu-22.04 - [self-hosted, linux, arm64] - macos-13 - [self-hosted, macOS, ARM64, go-spacemesh] - - windows-latest + - windows-2022 steps: # as we use some request to localhost, sometimes it gives us flaky tests. try to disable tcp offloading for fix it # https://github.com/actions/virtual-environments/issues/1187 - name: disable TCP/UDP offload - Ubuntu - if: ${{ matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.os == 'ubuntu-22.04' }} run: | sudo ethtool -K eth0 tx off sudo ethtool -K eth0 rx off @@ -236,11 +236,11 @@ jobs: sudo sysctl -w net.link.generic.system.hwcksum_tx=0 sudo sysctl -w net.link.generic.system.hwcksum_rx=0 - name: disable TCP/UDP offload - Windows - if: ${{ matrix.os == 'windows-latest' }} + if: ${{ matrix.os == 'windows-2022' }} run: | Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6 - name: disable Windows Defender - Windows - if: ${{ matrix.os == 'windows-latest' }} + if: ${{ matrix.os == 'windows-2022' }} run: | Set-MpPreference -DisableRealtimeMonitoring $true - name: checkout @@ -262,10 +262,10 @@ jobs: go-version: ${{ env.go-version }} cache: ${{ runner.arch != 'arm64' }} - name: Add OpenCL support - Ubuntu - if: ${{ matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.os == 'ubuntu-22.04' }} run: sudo apt-get update -q && sudo apt-get install -qy ocl-icd-opencl-dev libpocl2 - name: Add OpenCL support - Windows - if: ${{ matrix.os == 'windows-latest' }} + if: ${{ matrix.os == 'windows-2022' }} run: choco install opencl-intel-cpu-runtime - name: setup env run: make install @@ -295,7 +295,7 @@ jobs: - build-tools - build - unittests - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 env: # short-circuit success if no non-doc files were modified # this is the easiest way to access success/failure state of previous jobs in this workflow diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 9c67d4c42f..25e69fe37a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -10,7 +10,7 @@ on: jobs: filter-changes: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 outputs: nondocchanges: ${{ steps.filter.outputs.nondoc }} steps: @@ -28,7 +28,7 @@ jobs: - '!**/*.md' coverage: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: filter-changes if: ${{ needs.filter-changes.outputs.nondocchanges == 'true' }} steps: diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index 76b4d51efc..f47026fe10 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -20,7 +20,7 @@ on: default: '' jobs: dockerpush: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: checkout uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b8b811039..325b389fd3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: include: - - os: ubuntu-latest + - os: ubuntu-22.04 outname_sufix: "linux-amd64" - os: [self-hosted, linux, arm64] outname_sufix: "linux-arm64" @@ -21,22 +21,22 @@ jobs: outname_sufix: "mac-amd64" - os: [self-hosted, macOS, ARM64, go-spacemesh] outname_sufix: "mac-arm64" - - os: windows-latest + - os: windows-2022 outname_sufix: "win-amd64" steps: - shell: bash run: echo "OUTNAME=go-spacemesh-${{ github.ref_name }}-${{ matrix.outname_sufix }}" >> $GITHUB_ENV - name: Install dependencies in windows - if: ${{ matrix.os == 'windows-latest' }} + if: ${{ matrix.os == 'windows-2022' }} run: choco install make wget zip - name: Add OpenCL support - Ubuntu - if: ${{ matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.os == 'ubuntu-22.04' }} 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-latest' }} + if: ${{ matrix.os == 'windows-2022' }} run: | Set-MpPreference -DisableRealtimeMonitoring $true @@ -99,13 +99,13 @@ jobs: run: brew install coreutils - name: Calculate the hashsum of the zip file - if: ${{ matrix.os != 'windows-latest' }} + if: ${{ matrix.os != 'windows-2022' }} shell: bash run: | sha256sum ${{ env.OUTNAME }}.zip | awk '{ print $1 }' > sha256-${{ matrix.outname_sufix }}.txt - name: Calculate the hashsum of the zip file (Windows) - if: ${{ matrix.os == 'windows-latest' }} + if: ${{ matrix.os == 'windows-2022' }} run: | (Get-FileHash ${{ env.OUTNAME }}.zip -Algorithm SHA256).Hash > sha256-${{ matrix.outname_sufix }}.txt @@ -117,7 +117,7 @@ jobs: retention-days: 5 release: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: build-and-upload steps: - name: Download the artifacts diff --git a/.github/workflows/systest.yml b/.github/workflows/systest.yml index 125540740d..2a3eb10fa8 100644 --- a/.github/workflows/systest.yml +++ b/.github/workflows/systest.yml @@ -40,7 +40,7 @@ concurrency: jobs: filter-changes: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 outputs: nondocchanges: ${{ steps.filter.outputs.nondoc }} steps: @@ -53,7 +53,7 @@ jobs: - '!**/*.md' systest: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: ${{ needs.filter-changes.outputs.nondocchanges == 'true' }} needs: - filter-changes @@ -140,7 +140,7 @@ jobs: needs: - filter-changes - systest - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 env: # short-circuit success if no non-doc files were modified status: ${{ (needs.filter-changes.outputs.nondocchanges == 'false' || needs.systest.result == 'success') && 'success' || 'failure' }} diff --git a/activation/activation.go b/activation/activation.go index 79d075ee14..11aa48898c 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -17,6 +17,7 @@ import ( "github.com/spacemeshos/go-spacemesh/activation/metrics" "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/events" @@ -72,6 +73,7 @@ type Builder struct { coinbaseAccount types.Address conf Config db sql.Executor + atxsdata *atxsdata.Data localDB *localsql.Database publisher pubsub.Publisher nipostBuilder nipostBuilder @@ -152,6 +154,7 @@ func WithPostStates(ps PostStates) BuilderOption { func NewBuilder( conf Config, db sql.Executor, + atxsdata *atxsdata.Data, localDB *localsql.Database, publisher pubsub.Publisher, nipostBuilder nipostBuilder, @@ -165,6 +168,7 @@ func NewBuilder( signers: make(map[types.NodeID]*signing.EdSigner), conf: conf, db: db, + atxsdata: atxsdata, localDB: localDB, publisher: publisher, nipostBuilder: nipostBuilder, @@ -507,12 +511,6 @@ func (b *Builder) BuildNIPostChallenge(ctx context.Context, nodeID types.NodeID) } } - posAtx, err := b.getPositioningAtx(ctx, nodeID, publish) - if err != nil { - return nil, fmt.Errorf("failed to get positioning ATX: %w", err) - } - logger.Info("selected positioning atx", log.ZShortStringer("atx_id", posAtx)) - prevAtx, err = b.GetPrevAtx(nodeID) switch { case errors.Is(err, sql.ErrNotFound): @@ -538,6 +536,10 @@ func (b *Builder) BuildNIPostChallenge(ctx context.Context, nodeID types.NodeID) } return nil, fmt.Errorf("initial POST is invalid: %w", err) } + posAtx, err := b.getPositioningAtx(ctx, nodeID, publish, nil) + if err != nil { + return nil, fmt.Errorf("failed to get positioning ATX: %w", err) + } challenge = &types.NIPostChallenge{ PublishEpoch: publish, Sequence: 0, @@ -554,6 +556,10 @@ func (b *Builder) BuildNIPostChallenge(ctx context.Context, nodeID types.NodeID) return nil, fmt.Errorf("get last ATX: %w", err) default: // regular ATX challenge + posAtx, err := b.getPositioningAtx(ctx, nodeID, publish, prevAtx) + if err != nil { + return nil, fmt.Errorf("failed to get positioning ATX: %w", err) + } challenge = &types.NIPostChallenge{ PublishEpoch: publish, Sequence: prevAtx.Sequence + 1, @@ -692,6 +698,12 @@ func (b *Builder) createAtx( break } if nipostState.VRFNonce != oldNonce { + b.log.Info( + "attaching a new VRF nonce in ATX", + log.ZShortStringer("smesherID", sig.NodeID()), + zap.Uint64("new nonce", uint64(nipostState.VRFNonce)), + zap.Uint64("old nonce", uint64(oldNonce)), + ) nonce = &nipostState.VRFNonce } } @@ -723,9 +735,9 @@ func (b *Builder) broadcast(ctx context.Context, atx scale.Encodable) (int, erro return len(buf), nil } -// getPositioningAtx returns atx id with the highest tick height. +// searchPositioningAtx returns atx id with the highest tick height. // publish epoch is used for caching the positioning atx. -func (b *Builder) getPositioningAtx( +func (b *Builder) searchPositioningAtx( ctx context.Context, nodeID types.NodeID, publish types.EpochID, @@ -738,11 +750,17 @@ func (b *Builder) getPositioningAtx( return found.id, nil } - logger.Info("searching for positioning atx") + latestPublished, err := atxs.LatestEpoch(b.db) + if err != nil { + return types.EmptyATXID, fmt.Errorf("get latest epoch: %w", err) + } + logger.Info("searching for positioning atx", zap.Uint32("latest_epoch", latestPublished.Uint32())) + // positioning ATX publish epoch must be lower than the publish epoch of built ATX + positioningAtxPublished := min(latestPublished, publish-1) id, err := findFullyValidHighTickAtx( ctx, - b.db, - nodeID, + b.atxsdata, + positioningAtxPublished, b.conf.GoldenATXID, b.validator, logger, @@ -750,22 +768,47 @@ func (b *Builder) getPositioningAtx( VerifyChainOpts.WithTrustedID(nodeID), VerifyChainOpts.WithLogger(b.log), ) - switch { - case err == nil: - b.posAtxFinder.found = &struct { - id types.ATXID - forPublish types.EpochID - }{id, publish} - return id, nil - case errors.Is(err, sql.ErrNotFound): - logger.Info("using golden atx as positioning atx") - b.posAtxFinder.found = &struct { - id types.ATXID - forPublish types.EpochID - }{b.conf.GoldenATXID, publish} - return b.conf.GoldenATXID, nil + if err != nil { + logger.Info("search failed - using golden atx as positioning atx", zap.Error(err)) + id = b.conf.GoldenATXID + } + b.posAtxFinder.found = &struct { + id types.ATXID + forPublish types.EpochID + }{id, publish} + + return id, nil +} + +// getPositioningAtx returns the positioning ATX. +// The provided previous ATX is picked if it has a greater or equal +// tick count as the ATX selected in `searchPositioningAtx`. +func (b *Builder) getPositioningAtx( + ctx context.Context, + nodeID types.NodeID, + publish types.EpochID, + previous *types.ActivationTx, +) (types.ATXID, error) { + id, err := b.searchPositioningAtx(ctx, nodeID, publish) + if err != nil { + return types.EmptyATXID, err + } + + if previous != nil { + switch { + case id == b.conf.GoldenATXID: + id = previous.ID() + case id != b.conf.GoldenATXID: + if candidate, err := atxs.Get(b.db, id); err == nil { + if previous.TickHeight() >= candidate.TickHeight() { + id = previous.ID() + } + } + } } - return id, err + + b.log.Info("selected positioning atx", log.ZShortStringer("id", id), log.ZShortStringer("smesherID", nodeID)) + return id, nil } func (b *Builder) Regossip(ctx context.Context, nodeID types.NodeID) error { @@ -797,35 +840,26 @@ func buildNipostChallengeStartDeadline(roundStart time.Time, gracePeriod time.Du func findFullyValidHighTickAtx( ctx context.Context, - db sql.Executor, - prefNodeID types.NodeID, + atxdata *atxsdata.Data, + publish types.EpochID, goldenATXID types.ATXID, validator nipostValidator, logger *zap.Logger, opts ...VerifyChainOption, ) (types.ATXID, error) { - rejectedAtxs := make(map[types.ATXID]struct{}) - filter := func(id types.ATXID) bool { - _, ok := rejectedAtxs[id] - return !ok - } - - for { - select { - case <-ctx.Done(): - return types.ATXID{}, ctx.Err() - default: - } - id, err := atxs.GetIDWithMaxHeight(db, prefNodeID, filter) - if err != nil { - return types.ATXID{}, err - } - logger.Info("found candidate for high-tick atx, verifying its chain", log.ZShortStringer("atx_id", id)) + var found *types.ATXID + atxdata.IterateHighTicksInEpoch(publish+1, func(id types.ATXID) bool { + logger.Info("found candidate for high-tick atx", log.ZShortStringer("id", id)) if err := validator.VerifyChain(ctx, id, goldenATXID, opts...); err != nil { - logger.Info("rejecting candidate for high-tick atx", zap.Error(err), log.ZShortStringer("atx_id", id)) - rejectedAtxs[id] = struct{}{} - } else { - return id, nil + logger.Info("rejecting candidate for high-tick atx", zap.Error(err), log.ZShortStringer("id", id)) + return true } + found = &id + return false + }) + + if found != nil { + return *found, nil } + return types.ATXID{}, ErrNotFound } diff --git a/activation/activation_test.go b/activation/activation_test.go index 6d9d800dfc..e6131e3b52 100644 --- a/activation/activation_test.go +++ b/activation/activation_test.go @@ -22,6 +22,7 @@ import ( "golang.org/x/sync/errgroup" "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/events" @@ -103,6 +104,7 @@ func newTestBuilder(tb testing.TB, numSigners int, opts ...BuilderOption) *testA b := NewBuilder( cfg, tab.db, + atxsdata.New(), tab.localDb, tab.mpub, tab.mnipost, @@ -174,6 +176,7 @@ func publishAtx( built = new(wire.ActivationTxV1) codec.MustDecode(got, built) require.NoError(tb, atxs.Add(tab.db, toAtx(tb, built))) + tab.atxsdata.AddFromAtx(toAtx(tb, built), false) return nil }) @@ -335,6 +338,7 @@ func TestBuilder_PublishActivationTx_HappyFlow(t *testing.T) { prevAtx := newInitialATXv1(t, tab.goldenATXID) prevAtx.Sign(sig) require.NoError(t, atxs.Add(tab.db, toAtx(t, prevAtx))) + tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) // create and publish ATX tab.mclock.EXPECT().CurrentLayer().Return(currLayer).Times(4) @@ -342,16 +346,18 @@ func TestBuilder_PublishActivationTx_HappyFlow(t *testing.T) { atx1, err := publishAtx(t, tab, sig.NodeID(), posEpoch, &currLayer, layersPerEpoch) require.NoError(t, err) require.NotNil(t, atx1) + require.Equal(t, prevAtx.ID(), atx1.PositioningATXID) // create and publish another ATX currLayer = (posEpoch + 1).FirstLayer() tab.mclock.EXPECT().CurrentLayer().Return(currLayer).Times(4) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), prevAtx.ID(), tab.goldenATXID, gomock.Any()) + tab.mValidator.EXPECT().VerifyChain(gomock.Any(), atx1.ID(), tab.goldenATXID, gomock.Any()) atx2, err := publishAtx(t, tab, sig.NodeID(), atx1.PublishEpoch, &currLayer, layersPerEpoch) require.NoError(t, err) require.NotNil(t, atx2) require.NotEqual(t, atx1, atx2) require.Equal(t, atx1.PublishEpoch+1, atx2.PublishEpoch) + require.Equal(t, atx1.ID(), atx2.PositioningATXID) // state is cleaned up _, err = nipost.Challenge(tab.localDB, sig.NodeID()) @@ -370,6 +376,7 @@ func TestBuilder_Loop_WaitsOnStaleChallenge(t *testing.T) { prevAtx := newInitialATXv1(t, tab.goldenATXID) prevAtx.Sign(sig) require.NoError(t, atxs.Add(tab.db, toAtx(t, prevAtx))) + tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) tab.mclock.EXPECT().CurrentLayer().Return(currLayer).AnyTimes() tab.mclock.EXPECT().LayerToTime(gomock.Any()).DoAndReturn( @@ -418,6 +425,7 @@ func TestBuilder_PublishActivationTx_FaultyNet(t *testing.T) { prevAtx := newInitialATXv1(t, tab.goldenATXID) prevAtx.Sign(sig) require.NoError(t, atxs.Add(tab.db, toAtx(t, prevAtx))) + tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) publishEpoch := posEpoch + 1 tab.mclock.EXPECT().CurrentLayer().DoAndReturn(func() types.LayerID { return currLayer }).AnyTimes() @@ -491,6 +499,7 @@ func TestBuilder_PublishActivationTx_UsesExistingChallengeOnLatePublish(t *testi prevAtx.Sign(sig) vPrevAtx := toAtx(t, prevAtx) require.NoError(t, atxs.Add(tab.db, vPrevAtx)) + tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) publishEpoch := currLayer.GetEpoch() tab.mclock.EXPECT().CurrentLayer().DoAndReturn(func() types.LayerID { return currLayer }).AnyTimes() @@ -566,6 +575,7 @@ func TestBuilder_PublishActivationTx_RebuildNIPostWhenTargetEpochPassed(t *testi prevAtx.Sign(sig) vPrevAtx := toAtx(t, prevAtx) require.NoError(t, atxs.Add(tab.db, vPrevAtx)) + tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) publishEpoch := posEpoch + 1 tab.mclock.EXPECT().CurrentLayer().DoAndReturn( @@ -625,6 +635,7 @@ func TestBuilder_PublishActivationTx_RebuildNIPostWhenTargetEpochPassed(t *testi posAtx := newInitialATXv1(t, tab.goldenATXID, func(atx *wire.ActivationTxV1) { atx.PublishEpoch = posEpoch }) posAtx.Sign(sig) require.NoError(t, atxs.Add(tab.db, toAtx(t, posAtx))) + tab.atxsdata.AddFromAtx(toAtx(t, posAtx), false) tab.mclock.EXPECT().CurrentLayer().DoAndReturn(func() types.LayerID { return currLayer }).AnyTimes() tab.mnipost.EXPECT().ResetState(sig.NodeID()).Return(nil) tab.mValidator.EXPECT().VerifyChain(gomock.Any(), posAtx.ID(), tab.goldenATXID, gomock.Any()) @@ -774,6 +785,7 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { vPosAtx := toAtx(t, posAtx) vPosAtx.TickCount = 100 r.NoError(atxs.Add(tab.db, vPosAtx)) + tab.atxsdata.AddFromAtx(vPosAtx, false) nonce := types.VRFPostIndex(123) prevAtx := newInitialATXv1(t, tab.goldenATXID, func(atx *wire.ActivationTxV1) { @@ -782,6 +794,7 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { prevAtx.Sign(sig) vPrevAtx := toAtx(t, prevAtx) r.NoError(atxs.Add(tab.db, vPrevAtx)) + tab.atxsdata.AddFromAtx(vPrevAtx, false) // Act tab.msync.EXPECT().RegisterForATXSynced().DoAndReturn(closedChan).AnyTimes() @@ -862,6 +875,7 @@ func TestBuilder_PublishActivationTx_TargetsEpochBasedOnPosAtx(t *testing.T) { posAtx := newInitialATXv1(t, tab.goldenATXID) posAtx.Sign(otherSigner) r.NoError(atxs.Add(tab.db, toAtx(t, posAtx))) + tab.atxsdata.AddFromAtx(toAtx(t, posAtx), false) // Act & Assert tab.msync.EXPECT().RegisterForATXSynced().DoAndReturn(closedChan).AnyTimes() @@ -955,6 +969,7 @@ func TestBuilder_PublishActivationTx_FailsWhenNIPostBuilderFails(t *testing.T) { prevAtx := newInitialATXv1(t, tab.goldenATXID) prevAtx.Sign(sig) require.NoError(t, atxs.Add(tab.db, toAtx(t, prevAtx))) + tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) tab.mclock.EXPECT().CurrentLayer().Return(posEpoch.FirstLayer()).AnyTimes() tab.mclock.EXPECT().LayerToTime(gomock.Any()).DoAndReturn( @@ -1010,6 +1025,7 @@ func TestBuilder_RetryPublishActivationTx(t *testing.T) { prevAtx := newInitialATXv1(t, tab.goldenATXID) prevAtx.Sign(sig) require.NoError(t, atxs.Add(tab.db, toAtx(t, prevAtx))) + tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) publishEpoch := prevAtx.PublishEpoch + 1 currLayer := prevAtx.PublishEpoch.FirstLayer() @@ -1360,6 +1376,7 @@ func TestGetPositioningAtxPicksAtxWithValidChain(t *testing.T) { vInvalidAtx.TickCount = 100 require.NoError(t, err) require.NoError(t, atxs.Add(tab.db, vInvalidAtx)) + tab.atxsdata.AddFromAtx(vInvalidAtx, false) // Valid chain with lower height sigValid, err := signing.NewEdSigner() @@ -1369,6 +1386,7 @@ func TestGetPositioningAtxPicksAtxWithValidChain(t *testing.T) { validAtx.Sign(sigValid) vValidAtx := toAtx(t, validAtx) require.NoError(t, atxs.Add(tab.db, vValidAtx)) + tab.atxsdata.AddFromAtx(vValidAtx, false) tab.mValidator.EXPECT(). VerifyChain(gomock.Any(), invalidAtx.ID(), tab.goldenATXID, gomock.Any()). @@ -1376,12 +1394,12 @@ func TestGetPositioningAtxPicksAtxWithValidChain(t *testing.T) { tab.mValidator.EXPECT(). VerifyChain(gomock.Any(), validAtx.ID(), tab.goldenATXID, gomock.Any()) - posAtxID, err := tab.getPositioningAtx(context.Background(), sig.NodeID(), 77) + posAtxID, err := tab.getPositioningAtx(context.Background(), sig.NodeID(), 77, nil) require.NoError(t, err) require.Equal(t, posAtxID, vValidAtx.ID()) // should use the cached positioning ATX when asked for the same publish epoch - posAtxID, err = tab.getPositioningAtx(context.Background(), sig.NodeID(), 77) + posAtxID, err = tab.getPositioningAtx(context.Background(), sig.NodeID(), 77, nil) require.NoError(t, err) require.Equal(t, posAtxID, vValidAtx.ID()) @@ -1392,21 +1410,88 @@ func TestGetPositioningAtxPicksAtxWithValidChain(t *testing.T) { tab.mValidator.EXPECT(). VerifyChain(gomock.Any(), validAtx.ID(), tab.goldenATXID, gomock.Any()) - posAtxID, err = tab.getPositioningAtx(context.Background(), sig.NodeID(), 99) + posAtxID, err = tab.getPositioningAtx(context.Background(), sig.NodeID(), 99, nil) require.NoError(t, err) require.Equal(t, posAtxID, vValidAtx.ID()) } -func TestGetPositioningAtxDbFailed(t *testing.T) { - tab := newTestBuilder(t, 1) - sig := maps.Values(tab.signers)[0] +func TestGetPositioningAtx(t *testing.T) { + t.Parallel() + t.Run("db failed", func(t *testing.T) { + t.Parallel() + tab := newTestBuilder(t, 1) - db := sqlmocks.NewMockExecutor(gomock.NewController(t)) - tab.Builder.db = db - expected := errors.New("db error") - db.EXPECT().Exec(gomock.Any(), gomock.Any(), gomock.Any()).Return(0, expected) + db := sqlmocks.NewMockExecutor(gomock.NewController(t)) + tab.Builder.db = db + expected := errors.New("db error") + db.EXPECT().Exec(gomock.Any(), gomock.Any(), gomock.Any()).Return(0, expected) - none, err := tab.getPositioningAtx(context.Background(), sig.NodeID(), 99) - require.ErrorIs(t, err, expected) - require.Equal(t, types.ATXID{}, none) + none, err := tab.getPositioningAtx(context.Background(), types.EmptyNodeID, 99, nil) + require.ErrorIs(t, err, expected) + require.Equal(t, types.ATXID{}, none) + }) + t.Run("picks golden if no ATXs", func(t *testing.T) { + tab := newTestBuilder(t, 1) + atx, err := tab.getPositioningAtx(context.Background(), types.EmptyNodeID, 99, nil) + require.NoError(t, err) + require.Equal(t, tab.goldenATXID, atx) + }) + t.Run("prefers own previous to golden", func(t *testing.T) { + prev := &types.ActivationTx{} + prev.SetID(types.RandomATXID()) + tab := newTestBuilder(t, 1) + atx, err := tab.getPositioningAtx(context.Background(), types.EmptyNodeID, 99, prev) + require.NoError(t, err) + require.Equal(t, prev.ID(), atx) + }) + t.Run("prefers own previous when it has GTE ticks", func(t *testing.T) { + tab := newTestBuilder(t, 1) + + atxInDb := &types.ActivationTx{TickCount: 10} + atxInDb.SetID(types.RandomATXID()) + require.NoError(t, atxs.Add(tab.db, atxInDb)) + tab.atxsdata.AddFromAtx(atxInDb, false) + + prev := &types.ActivationTx{TickCount: 100} + prev.SetID(types.RandomATXID()) + + tab.mValidator.EXPECT().VerifyChain(gomock.Any(), atxInDb.ID(), tab.goldenATXID, gomock.Any()) + found, err := tab.searchPositioningAtx(context.Background(), types.EmptyNodeID, 99) + require.NoError(t, err) + require.Equal(t, atxInDb.ID(), found) + + // prev.Height > found.Height + selected, err := tab.getPositioningAtx(context.Background(), types.EmptyNodeID, 99, prev) + require.NoError(t, err) + require.Equal(t, prev.ID(), selected) + + // prev.Height == found.Height + prev.TickCount = atxInDb.TickCount + selected, err = tab.getPositioningAtx(context.Background(), types.EmptyNodeID, 99, prev) + require.NoError(t, err) + require.Equal(t, prev.ID(), selected) + }) +} + +func TestFindFullyValidHighTickAtx(t *testing.T) { + t.Parallel() + golden := types.RandomATXID() + + t.Run("skips malicious ATXs", func(t *testing.T) { + data := atxsdata.New() + atxMal := &types.ActivationTx{TickCount: 100, SmesherID: types.RandomNodeID()} + atxMal.SetID(types.RandomATXID()) + data.AddFromAtx(atxMal, true) + + atxLower := &types.ActivationTx{TickCount: 10, SmesherID: types.RandomNodeID()} + atxLower.SetID(types.RandomATXID()) + data.AddFromAtx(atxLower, false) + + mValidator := NewMocknipostValidator(gomock.NewController(t)) + mValidator.EXPECT().VerifyChain(gomock.Any(), atxLower.ID(), golden, gomock.Any()) + + found, err := findFullyValidHighTickAtx(context.Background(), data, 0, golden, mValidator, zaptest.NewLogger(t)) + require.NoError(t, err) + require.Equal(t, atxLower.ID(), found) + }) } diff --git a/activation/e2e/activation_test.go b/activation/e2e/activation_test.go index 7e6c4a84a2..352516517f 100644 --- a/activation/e2e/activation_test.go +++ b/activation/e2e/activation_test.go @@ -3,6 +3,7 @@ package activation_test import ( "context" "sync" + "sync/atomic" "testing" "time" @@ -10,20 +11,22 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap" "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/activation" "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/api/grpcserver" + "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" "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/p2p/pubsub/mocks" "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/localsql" "github.com/spacemeshos/go-spacemesh/timesync" ) @@ -31,7 +34,10 @@ import ( func Test_BuilderWithMultipleClients(t *testing.T) { ctrl := gomock.NewController(t) - numSigners := 3 + const numEpochs = 3 + const numSigners = 3 + const totalAtxs = numEpochs * numSigners + signers := make(map[types.NodeID]*signing.EdSigner, numSigners) for range numSigners { sig, err := signing.NewEdSigner() @@ -44,7 +50,6 @@ func Test_BuilderWithMultipleClients(t *testing.T) { goldenATX := types.ATXID{2, 3, 4} cfg := activation.DefaultPostConfig() db := sql.InMemory() - cdb := datastore.NewCachedDB(db, log.NewFromLog(logger)) syncer := activation.NewMocksyncer(ctrl) syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { @@ -71,7 +76,7 @@ func Test_BuilderWithMultipleClients(t *testing.T) { i += 1 eg.Go(func() error { validator := activation.NewMocknipostValidator(ctrl) - mgr, err := activation.NewPostSetupManager(cfg, logger, cdb, goldenATX, syncer, validator) + mgr, err := activation.NewPostSetupManager(cfg, logger, db, atxsdata.New(), goldenATX, syncer, validator) require.NoError(t, err) initPost(t, mgr, opts, sig.NodeID()) @@ -123,7 +128,10 @@ func Test_BuilderWithMultipleClients(t *testing.T) { localDB, poetDb, svc, - []types.PoetServer{{Pubkey: types.NewBase64Enc([]byte("foobar")), Address: poetProver.RestURL().String()}}, + []types.PoetServer{{ + Pubkey: types.NewBase64Enc(poetProver.Service.PublicKey()), + Address: poetProver.RestURL().String(), + }}, logger.Named("nipostBuilder"), poetCfg, clock, @@ -136,8 +144,10 @@ func Test_BuilderWithMultipleClients(t *testing.T) { RegossipInterval: 0, } + data := atxsdata.New() + var atxsPublished atomic.Uint32 var atxMtx sync.Mutex - atxs := make(map[types.NodeID]wire.ActivationTxV1) + gotAtxs := make(map[types.NodeID][]wire.ActivationTxV1) endChan := make(chan struct{}) mpub := mocks.NewMockPublisher(ctrl) mpub.EXPECT().Publish(gomock.Any(), pubsub.AtxProtocol, gomock.Any()).DoAndReturn( @@ -147,21 +157,31 @@ func Test_BuilderWithMultipleClients(t *testing.T) { var gotAtx wire.ActivationTxV1 codec.MustDecode(got, &gotAtx) - atxs[gotAtx.SmesherID] = gotAtx - if len(atxs) == numSigners { + gotAtxs[gotAtx.SmesherID] = append(gotAtxs[gotAtx.SmesherID], gotAtx) + atx := wire.ActivationTxFromWireV1(&gotAtx) + if gotAtx.VRFNonce == nil { + atx.VRFNonce, err = atxs.NonceByID(db, gotAtx.PrevATXID) + require.NoError(t, err) + } + logger.Debug("persisting ATX", zap.Inline(atx)) + require.NoError(t, atxs.Add(db, atx)) + data.AddFromAtx(atx, false) + + if atxsPublished.Add(1) == totalAtxs { close(endChan) } return nil }, - ).Times(numSigners) + ).Times(totalAtxs) verifier, err := activation.NewPostVerifier(cfg, logger.Named("verifier")) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, verifier.Close()) }) - v := activation.NewValidator(nil, poetDb, cfg, opts.Scrypt, verifier) + v := activation.NewValidator(db, poetDb, cfg, opts.Scrypt, verifier) tab := activation.NewBuilder( conf, - cdb, + db, + data, localDB, mpub, nb, @@ -179,7 +199,13 @@ func Test_BuilderWithMultipleClients(t *testing.T) { // initial proof postStates.EXPECT().Set(sig.NodeID(), types.PostStateProving), postStates.EXPECT().Set(sig.NodeID(), types.PostStateIdle), - // post proof + // post proof - 1st epoch + postStates.EXPECT().Set(sig.NodeID(), types.PostStateProving), + postStates.EXPECT().Set(sig.NodeID(), types.PostStateIdle), + // 2nd epoch + postStates.EXPECT().Set(sig.NodeID(), types.PostStateProving), + postStates.EXPECT().Set(sig.NodeID(), types.PostStateIdle), + // 3rd epoch postStates.EXPECT().Set(sig.NodeID(), types.PostStateProving), postStates.EXPECT().Set(sig.NodeID(), types.PostStateIdle), ) @@ -191,34 +217,44 @@ func Test_BuilderWithMultipleClients(t *testing.T) { require.NoError(t, tab.StopSmeshing(false)) for _, sig := range signers { - atx := atxs[sig.NodeID()] - - _, err = v.NIPost( - context.Background(), - sig.NodeID(), - *atx.CommitmentATXID, - wire.NiPostFromWireV1(atx.NIPost), - atx.NIPostChallengeV1.Hash(), - atx.NumUnits, - ) - require.NoError(t, err) + var commitment types.ATXID + var previous types.ATXID - require.NotNil(t, atx.VRFNonce) - err := v.VRFNonce( - sig.NodeID(), - *atx.CommitmentATXID, - uint64(*atx.VRFNonce), - atx.NIPost.PostMetadata.LabelsPerUnit, - atx.NumUnits, - ) - require.NoError(t, err) + for seq, atx := range gotAtxs[sig.NodeID()] { + logger.Debug("checking ATX", zap.Inline(&atx), zap.Uint64("seq", uint64(seq))) + if seq == 0 { + commitment = *atx.CommitmentATXID + require.Equal(t, sig.NodeID(), *atx.NodeID) + require.Equal(t, goldenATX, atx.PositioningATXID) + require.NotNil(t, atx.VRFNonce) + err := v.VRFNonce( + sig.NodeID(), + commitment, + uint64(*atx.VRFNonce), + atx.NIPost.PostMetadata.LabelsPerUnit, + atx.NumUnits, + ) + require.NoError(t, err) + } else { + require.Nil(t, atx.VRFNonce) + require.Equal(t, previous, atx.PositioningATXID) + } + _, err = v.NIPost( + context.Background(), + sig.NodeID(), + commitment, + wire.NiPostFromWireV1(atx.NIPost), + atx.NIPostChallengeV1.Hash(), + atx.NumUnits, + ) + require.NoError(t, err) - require.Equal(t, postGenesisEpoch, atx.PublishEpoch+1) - require.Equal(t, types.EmptyATXID, atx.PrevATXID) - require.Equal(t, goldenATX, atx.PositioningATXID) - require.Equal(t, uint64(0), atx.Sequence) + require.Equal(t, previous, atx.PrevATXID) + require.Equal(t, postGenesisEpoch.Add(uint32(seq)), atx.PublishEpoch+1) + require.Equal(t, uint64(seq), atx.Sequence) + require.Equal(t, types.Address{}, atx.Coinbase) - require.Equal(t, types.Address{}, atx.Coinbase) - require.Equal(t, sig.NodeID(), *atx.NodeID) + previous = atx.ID() + } } } diff --git a/activation/e2e/nipost_test.go b/activation/e2e/nipost_test.go index 872a88fc0e..78f7adf809 100644 --- a/activation/e2e/nipost_test.go +++ b/activation/e2e/nipost_test.go @@ -19,6 +19,7 @@ import ( "github.com/spacemeshos/go-spacemesh/activation" "github.com/spacemeshos/go-spacemesh/api/grpcserver" + "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/log" @@ -129,7 +130,7 @@ func TestNIPostBuilderWithClients(t *testing.T) { }) validator := activation.NewMocknipostValidator(ctrl) - mgr, err := activation.NewPostSetupManager(cfg, logger, cdb, goldenATX, syncer, validator) + mgr, err := activation.NewPostSetupManager(cfg, logger, cdb, atxsdata.New(), goldenATX, syncer, validator) require.NoError(t, err) opts := activation.DefaultPostSetupOpts() @@ -263,7 +264,6 @@ func TestNewNIPostBuilderNotInitialized(t *testing.T) { goldenATX := types.ATXID{2, 3, 4} cfg := activation.DefaultPostConfig() db := sql.InMemory() - cdb := datastore.NewCachedDB(db, log.NewFromLog(logger)) syncer := activation.NewMocksyncer(ctrl) syncer.EXPECT().RegisterForATXSynced().AnyTimes().DoAndReturn(func() <-chan struct{} { @@ -273,7 +273,7 @@ func TestNewNIPostBuilderNotInitialized(t *testing.T) { }) validator := activation.NewMocknipostValidator(ctrl) - mgr, err := activation.NewPostSetupManager(cfg, logger, cdb, goldenATX, syncer, validator) + mgr, err := activation.NewPostSetupManager(cfg, logger, db, atxsdata.New(), goldenATX, syncer, validator) require.NoError(t, err) // ensure that genesis aligns with layer timings @@ -368,7 +368,6 @@ func Test_NIPostBuilderWithMultipleClients(t *testing.T) { goldenATX := types.ATXID{2, 3, 4} cfg := activation.DefaultPostConfig() db := sql.InMemory() - cdb := datastore.NewCachedDB(db, log.NewFromLog(logger)) syncer := activation.NewMocksyncer(ctrl) syncer.EXPECT().RegisterForATXSynced().AnyTimes().DoAndReturn(func() <-chan struct{} { @@ -391,7 +390,7 @@ func Test_NIPostBuilderWithMultipleClients(t *testing.T) { for _, sig := range signers { opts := opts eg.Go(func() error { - mgr, err := activation.NewPostSetupManager(cfg, logger, cdb, goldenATX, syncer, validator) + mgr, err := activation.NewPostSetupManager(cfg, logger, db, atxsdata.New(), goldenATX, syncer, validator) require.NoError(t, err) opts.DataDir = t.TempDir() diff --git a/activation/e2e/validation_test.go b/activation/e2e/validation_test.go index dbfafe3adf..9552489949 100644 --- a/activation/e2e/validation_test.go +++ b/activation/e2e/validation_test.go @@ -14,8 +14,8 @@ import ( "github.com/spacemeshos/go-spacemesh/activation" "github.com/spacemeshos/go-spacemesh/api/grpcserver" + "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/signing" "github.com/spacemeshos/go-spacemesh/sql" @@ -31,7 +31,7 @@ func TestValidator_Validate(t *testing.T) { logger := zaptest.NewLogger(t) goldenATX := types.ATXID{2, 3, 4} cfg := activation.DefaultPostConfig() - cdb := datastore.NewCachedDB(sql.InMemory(), log.NewFromLog(logger)) + db := sql.InMemory() validator := activation.NewMocknipostValidator(gomock.NewController(t)) syncer := activation.NewMocksyncer(gomock.NewController(t)) @@ -41,7 +41,7 @@ func TestValidator_Validate(t *testing.T) { return synced }) - mgr, err := activation.NewPostSetupManager(cfg, logger, cdb, goldenATX, syncer, validator) + mgr, err := activation.NewPostSetupManager(cfg, logger, db, atxsdata.New(), goldenATX, syncer, validator) require.NoError(t, err) opts := activation.DefaultPostSetupOpts() @@ -109,7 +109,7 @@ func TestValidator_Validate(t *testing.T) { nipost, err := nb.BuildNIPost(context.Background(), sig, postGenesisEpoch+2, challenge) require.NoError(t, err) - v := activation.NewValidator(cdb, poetDb, cfg, opts.Scrypt, verifier) + v := activation.NewValidator(db, poetDb, cfg, opts.Scrypt, verifier) _, err = v.NIPost(context.Background(), sig.NodeID(), goldenATX, nipost.NIPost, challenge, nipost.NumUnits) require.NoError(t, err) @@ -130,7 +130,7 @@ func TestValidator_Validate(t *testing.T) { newPostCfg := cfg newPostCfg.MinNumUnits = nipost.NumUnits + 1 - v = activation.NewValidator(cdb, poetDb, newPostCfg, opts.Scrypt, nil) + v = activation.NewValidator(db, poetDb, newPostCfg, opts.Scrypt, nil) _, err = v.NIPost(context.Background(), sig.NodeID(), goldenATX, nipost.NIPost, challenge, nipost.NumUnits) require.EqualError( t, @@ -140,7 +140,7 @@ func TestValidator_Validate(t *testing.T) { newPostCfg = cfg newPostCfg.MaxNumUnits = nipost.NumUnits - 1 - v = activation.NewValidator(cdb, poetDb, newPostCfg, opts.Scrypt, nil) + v = activation.NewValidator(db, poetDb, newPostCfg, opts.Scrypt, nil) _, err = v.NIPost(context.Background(), sig.NodeID(), goldenATX, nipost.NIPost, challenge, nipost.NumUnits) require.EqualError( t, @@ -150,7 +150,7 @@ func TestValidator_Validate(t *testing.T) { newPostCfg = cfg newPostCfg.LabelsPerUnit = nipost.PostMetadata.LabelsPerUnit + 1 - v = activation.NewValidator(cdb, poetDb, newPostCfg, opts.Scrypt, nil) + v = activation.NewValidator(db, poetDb, newPostCfg, opts.Scrypt, nil) _, err = v.NIPost(context.Background(), sig.NodeID(), goldenATX, nipost.NIPost, challenge, nipost.NumUnits) require.EqualError( t, diff --git a/activation/poet.go b/activation/poet.go index dd00bcb7a6..9a938cd3a6 100644 --- a/activation/poet.go +++ b/activation/poet.go @@ -358,10 +358,12 @@ func (c *PoetClient) Proof(ctx context.Context, roundID string) (*types.PoetProo defer c.gettingProof.Unlock() if members, ok := c.proofMembers[roundID]; ok { - if proof, err := c.db.ProofForRound(c.id, roundID); err == nil { + proof, err := c.db.ProofForRound(c.id, roundID) + if err == nil { c.logger.Debug("returning cached proof", zap.String("round_id", roundID)) return proof, members, nil } + c.logger.Warn("cached members found but proof not found in db", zap.String("round_id", roundID), zap.Error(err)) } proof, members, err := c.client.Proof(getProofsCtx, roundID) diff --git a/activation/post.go b/activation/post.go index 9b8cdce06b..08f083592a 100644 --- a/activation/post.go +++ b/activation/post.go @@ -12,8 +12,8 @@ import ( "github.com/spacemeshos/post/initialization" "go.uber.org/zap" + "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/events" "github.com/spacemeshos/go-spacemesh/metrics/public" "github.com/spacemeshos/go-spacemesh/sql" @@ -180,7 +180,8 @@ type PostSetupManager struct { cfg PostConfig logger *zap.Logger - db *datastore.CachedDB + db sql.Executor + atxsdata *atxsdata.Data goldenATXID types.ATXID validator nipostValidator @@ -207,7 +208,8 @@ func PostValidityDelay(delay time.Duration) PostSetupManagerOpt { func NewPostSetupManager( cfg PostConfig, logger *zap.Logger, - db *datastore.CachedDB, + db sql.Executor, + atxsdata *atxsdata.Data, goldenATXID types.ATXID, syncer syncer, validator nipostValidator, @@ -217,6 +219,7 @@ func NewPostSetupManager( cfg: cfg, logger: logger, db: db, + atxsdata: atxsdata, goldenATXID: goldenATXID, state: PostSetupStateNotStarted, syncer: syncer, @@ -397,10 +400,15 @@ func (mgr *PostSetupManager) findCommitmentAtx(ctx context.Context) (types.ATXID mgr.logger.Info("ATXs synced - selecting commitment ATX") } + latest, err := atxs.LatestEpoch(mgr.db) + if err != nil { + return types.EmptyATXID, fmt.Errorf("get latest epoch: %w", err) + } + atx, err := findFullyValidHighTickAtx( context.Background(), - mgr.db, - types.EmptyNodeID, + mgr.atxsdata, + latest, mgr.goldenATXID, mgr.validator, mgr.logger, @@ -408,7 +416,7 @@ func (mgr *PostSetupManager) findCommitmentAtx(ctx context.Context) (types.ATXID VerifyChainOpts.WithLogger(mgr.logger), ) switch { - case errors.Is(err, sql.ErrNotFound): + case errors.Is(err, ErrNotFound): mgr.logger.Info("using golden atx as commitment atx") return mgr.goldenATXID, nil case err != nil: diff --git a/activation/post_supervisor_test.go b/activation/post_supervisor_test.go index deacdb67c4..f26021a364 100644 --- a/activation/post_supervisor_test.go +++ b/activation/post_supervisor_test.go @@ -21,9 +21,8 @@ import ( "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest" + "github.com/spacemeshos/go-spacemesh/atxsdata" "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/signing" "github.com/spacemeshos/go-spacemesh/sql" ) @@ -57,8 +56,9 @@ func newPostManager(t *testing.T, cfg PostConfig, opts PostSetupOpts) *PostSetup close(ch) return ch }) - cdb := datastore.NewCachedDB(sql.InMemory(), logtest.New(t)) - mgr, err := NewPostSetupManager(cfg, zaptest.NewLogger(t), cdb, types.RandomATXID(), syncer, validator) + db := sql.InMemory() + atxsdata := atxsdata.New() + mgr, err := NewPostSetupManager(cfg, zaptest.NewLogger(t), db, atxsdata, types.RandomATXID(), syncer, validator) require.NoError(t, err) return mgr } diff --git a/activation/post_test.go b/activation/post_test.go index fd64ee43bd..b3a8e7c233 100644 --- a/activation/post_test.go +++ b/activation/post_test.go @@ -13,6 +13,7 @@ import ( "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" + "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/log/logtest" @@ -283,6 +284,7 @@ func TestPostSetupManager_findCommitmentAtx_UsesLatestAtx(t *testing.T) { atx.TickCount = 1 require.NoError(t, err) require.NoError(t, atxs.Add(mgr.db, atx)) + mgr.atxsdata.AddFromAtx(atx, false) commitmentAtx, err := mgr.findCommitmentAtx(context.Background()) require.NoError(t, err) @@ -364,7 +366,8 @@ func newTestPostManager(tb testing.TB) *testPostManager { syncer.EXPECT().RegisterForATXSynced().AnyTimes().Return(synced) cdb := datastore.NewCachedDB(sql.InMemory(), logtest.New(tb)) - mgr, err := NewPostSetupManager(DefaultPostConfig(), zaptest.NewLogger(tb), cdb, goldenATXID, syncer, validator) + logger := zaptest.NewLogger(tb) + mgr, err := NewPostSetupManager(DefaultPostConfig(), logger, cdb, atxsdata.New(), goldenATXID, syncer, validator) require.NoError(tb, err) return &testPostManager{ diff --git a/activation/wire/challenge_v2.go b/activation/wire/challenge_v2.go index 79171a3303..0890fad84c 100644 --- a/activation/wire/challenge_v2.go +++ b/activation/wire/challenge_v2.go @@ -16,7 +16,6 @@ type NIPostChallengeV2 struct { PublishEpoch types.EpochID PrevATXID types.ATXID PositioningATXID types.ATXID - CommitmentATXID *types.ATXID InitialPost *PostV1 } @@ -35,9 +34,6 @@ func (c *NIPostChallengeV2) MarshalLogObject(encoder zapcore.ObjectEncoder) erro encoder.AddUint32("PublishEpoch", c.PublishEpoch.Uint32()) encoder.AddString("PrevATXID", c.PrevATXID.String()) encoder.AddString("PositioningATX", c.PositioningATXID.String()) - if c.CommitmentATXID != nil { - encoder.AddString("CommitmentATX", c.CommitmentATXID.String()) - } encoder.AddObject("InitialPost", c.InitialPost) return nil } diff --git a/activation/wire/challenge_v2_scale.go b/activation/wire/challenge_v2_scale.go index 5a999c5be5..be1ec1e811 100644 --- a/activation/wire/challenge_v2_scale.go +++ b/activation/wire/challenge_v2_scale.go @@ -30,13 +30,6 @@ func (t *NIPostChallengeV2) EncodeScale(enc *scale.Encoder) (total int, err erro } total += n } - { - n, err := scale.EncodeOption(enc, t.CommitmentATXID) - if err != nil { - return total, err - } - total += n - } { n, err := scale.EncodeOption(enc, t.InitialPost) if err != nil { @@ -70,14 +63,6 @@ func (t *NIPostChallengeV2) DecodeScale(dec *scale.Decoder) (total int, err erro } total += n } - { - field, n, err := scale.DecodeOption[types.ATXID](dec) - if err != nil { - return total, err - } - total += n - t.CommitmentATXID = field - } { field, n, err := scale.DecodeOption[PostV1](dec) if err != nil { diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index b24e1ce6d9..545873d05b 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -36,14 +36,10 @@ type ActivationTxV2 struct { // Only required when the ATX includes married IDs. MarriageATX *types.ATXID + SmesherID types.NodeID Signature types.EdSignature } -// The first PoST is always for the ATX owner. -func (atx *ActivationTxV2) SmesherID() types.NodeID { - return atx.NiPosts[0].Posts[0].ID -} - type InitialAtxPartsV2 struct { CommitmentATX types.ATXID Post PostV1 @@ -68,10 +64,13 @@ type MerkleProofV2 struct { } type SubPostV2 struct { - ID types.NodeID // The ID that this PoST is for. - PrevATXIndex uint32 // Index of the previous ATX in the `InnerActivationTxV2.PreviousATXs` slice - Post PostV1 - NumUnits uint32 + // Index of marriage certificate for this ID in the 'Marriages' slice. Only valid for merged ATXs. + // Can be used to extract the nodeID and verify if it is married with the smesher of the ATX. + // Must be 0 for non-merged ATXs. + MarriageIndex uint32 + PrevATXIndex uint32 // Index of the previous ATX in the `InnerActivationTxV2.PreviousATXs` slice + Post PostV1 + NumUnits uint32 } type NiPostsV2 struct { diff --git a/activation/wire/wire_v2_scale.go b/activation/wire/wire_v2_scale.go index cb3c3107c1..92c66a0e2b 100644 --- a/activation/wire/wire_v2_scale.go +++ b/activation/wire/wire_v2_scale.go @@ -72,6 +72,13 @@ func (t *ActivationTxV2) EncodeScale(enc *scale.Encoder) (total int, err error) } 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 { @@ -154,6 +161,13 @@ func (t *ActivationTxV2) DecodeScale(dec *scale.Decoder) (total int, err error) total += n t.MarriageATX = 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 { @@ -276,7 +290,7 @@ func (t *MerkleProofV2) DecodeScale(dec *scale.Decoder) (total int, err error) { func (t *SubPostV2) EncodeScale(enc *scale.Encoder) (total int, err error) { { - n, err := scale.EncodeByteArray(enc, t.ID[:]) + n, err := scale.EncodeCompact32(enc, uint32(t.MarriageIndex)) if err != nil { return total, err } @@ -308,11 +322,12 @@ func (t *SubPostV2) EncodeScale(enc *scale.Encoder) (total int, err error) { func (t *SubPostV2) DecodeScale(dec *scale.Decoder) (total int, err error) { { - n, err := scale.DecodeByteArray(dec, t.ID[:]) + field, n, err := scale.DecodeCompact32(dec) if err != nil { return total, err } total += n + t.MarriageIndex = uint32(field) } { field, n, err := scale.DecodeCompact32(dec) diff --git a/api/grpcserver/post_service_test.go b/api/grpcserver/post_service_test.go index 63c73b59dd..f3fddd506b 100644 --- a/api/grpcserver/post_service_test.go +++ b/api/grpcserver/post_service_test.go @@ -20,9 +20,8 @@ import ( "google.golang.org/grpc/status" "github.com/spacemeshos/go-spacemesh/activation" + "github.com/spacemeshos/go-spacemesh/atxsdata" "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/signing" "github.com/spacemeshos/go-spacemesh/sql" ) @@ -59,8 +58,9 @@ func launchPostSupervisor( close(ch) return ch }) - cdb := datastore.NewCachedDB(sql.InMemory(), logtest.New(tb)) - mgr, err := activation.NewPostSetupManager(postCfg, log.Named("post manager"), cdb, goldenATXID, syncer, validator) + db := sql.InMemory() + logger := log.Named("post manager") + mgr, err := activation.NewPostSetupManager(postCfg, logger, db, atxsdata.New(), goldenATXID, syncer, validator) require.NoError(tb, err) // start post supervisor @@ -102,8 +102,9 @@ func launchPostSupervisorTLS( close(ch) return ch }) - cdb := datastore.NewCachedDB(sql.InMemory(), logtest.New(tb)) - mgr, err := activation.NewPostSetupManager(postCfg, log.Named("post manager"), cdb, goldenATXID, syncer, validator) + db := sql.InMemory() + logger := log.Named("post supervisor") + mgr, err := activation.NewPostSetupManager(postCfg, logger, db, atxsdata.New(), goldenATXID, syncer, validator) require.NoError(tb, err) // start post supervisor diff --git a/atxsdata/data.go b/atxsdata/data.go index fb41bd3c2c..f8eae4794c 100644 --- a/atxsdata/data.go +++ b/atxsdata/data.go @@ -1,6 +1,7 @@ package atxsdata import ( + "slices" "sync" "sync/atomic" @@ -169,6 +170,16 @@ func (d *Data) Get(epoch types.EpochID, atx types.ATXID) *ATX { return data } +func (d *Data) Size(target types.EpochID) int { + d.mu.RLock() + defer d.mu.RUnlock() + ecache, exists := d.epochs[target] + if !exists { + return 0 + } + return len(ecache.index) +} + type lockGuard struct{} // AtxFilter is a function that filters atxs. @@ -204,6 +215,33 @@ func (d *Data) IterateInEpoch(epoch types.EpochID, fn func(types.ATXID, *ATX), f } } +func (d *Data) IterateHighTicksInEpoch(target types.EpochID, fn func(types.ATXID) bool) { + type candidate struct { + id types.ATXID + *ATX + } + candidates := make([]candidate, 0, d.Size(target)) + d.IterateInEpoch(target, func(id types.ATXID, atx *ATX) { + candidates = append(candidates, candidate{id: id, ATX: atx}) + }, NotMalicious) + + slices.SortFunc(candidates, func(a, b candidate) int { + switch { + case a.Height < b.Height: + return 1 + case a.Height > b.Height: + return -1 + } + return 0 + }) + + for _, c := range candidates { + if cont := fn(c.id); !cont { + return + } + } +} + func (d *Data) MissingInEpoch(epoch types.EpochID, atxs []types.ATXID) []types.ATXID { d.mu.RLock() defer d.mu.RUnlock() diff --git a/atxsdata/warmup.go b/atxsdata/warmup.go index 2d1178b8ed..84c6850736 100644 --- a/atxsdata/warmup.go +++ b/atxsdata/warmup.go @@ -2,7 +2,6 @@ package atxsdata import ( "context" - "errors" "fmt" "github.com/spacemeshos/go-spacemesh/common/types" @@ -39,8 +38,7 @@ func Warmup(db sql.Executor, cache *Data, keep types.EpochID) error { } cache.EvictEpoch(evict) - var ierr error - if err := atxs.IterateAtxsData(db, cache.Evicted(), latest, + return atxs.IterateAtxsData(db, cache.Evicted(), latest, func( id types.ATXID, node types.NodeID, @@ -49,13 +47,9 @@ func Warmup(db sql.Executor, cache *Data, keep types.EpochID) error { weight, base, height uint64, - nonce *types.VRFPostIndex, + nonce types.VRFPostIndex, malicious bool, ) bool { - if nonce == nil { - ierr = errors.New("missing nonce") - return false - } cache.Add( epoch+1, node, @@ -64,12 +58,9 @@ func Warmup(db sql.Executor, cache *Data, keep types.EpochID) error { weight, base, height, - *nonce, + nonce, malicious, ) return true - }); err != nil { - return err - } - return ierr + }) } diff --git a/datastore/store.go b/datastore/store.go index e18487cdf0..c649cd69d6 100644 --- a/datastore/store.go +++ b/datastore/store.go @@ -49,7 +49,7 @@ type CachedDB struct { atxsdata *atxsdata.Data atxCache *lru.Cache[types.ATXID, *types.ActivationTx] - vrfNonceCache *lru.Cache[VrfNonceKey, *types.VRFPostIndex] + vrfNonceCache *lru.Cache[VrfNonceKey, types.VRFPostIndex] // used to coordinate db update and cache mu sync.Mutex @@ -108,7 +108,7 @@ func NewCachedDB(db Executor, lg log.Log, opts ...Opt) *CachedDB { lg.Fatal("failed to create malfeasance cache", err) } - vrfNonceCache, err := lru.New[VrfNonceKey, *types.VRFPostIndex](o.cfg.ATXSize) + vrfNonceCache, err := lru.New[VrfNonceKey, types.VRFPostIndex](o.cfg.ATXSize) if err != nil { lg.Fatal("failed to create vrf nonce cache", err) } @@ -194,7 +194,7 @@ func (db *CachedDB) CacheMalfeasanceProof(id types.NodeID, proof *wire.Malfeasan func (db *CachedDB) VRFNonce(id types.NodeID, epoch types.EpochID) (types.VRFPostIndex, error) { key := VrfNonceKey{id, epoch} if nonce, ok := db.vrfNonceCache.Get(key); ok { - return *nonce, nil + return nonce, nil } nonce, err := atxs.VRFNonce(db, id, epoch) @@ -202,7 +202,7 @@ func (db *CachedDB) VRFNonce(id types.NodeID, epoch types.EpochID) (types.VRFPos return types.VRFPostIndex(0), err } - db.vrfNonceCache.Add(key, &nonce) + db.vrfNonceCache.Add(key, nonce) return nonce, nil } diff --git a/node/node.go b/node/node.go index 47581f0037..b7ccce4337 100644 --- a/node/node.go +++ b/node/node.go @@ -985,7 +985,8 @@ func (app *App) initServices(ctx context.Context) error { postSetupMgr, err := activation.NewPostSetupManager( app.Config.POST, app.addLogger(PostLogger, lg).Zap(), - app.cachedDB, + app.db, + app.atxsdata, goldenATXID, newSyncer, app.validator, @@ -1020,7 +1021,8 @@ func (app *App) initServices(ctx context.Context) error { } atxBuilder := activation.NewBuilder( builderConfig, - app.cachedDB, + app.db, + app.atxsdata, app.localDB, app.host, nipostBuilder, diff --git a/node/node_test.go b/node/node_test.go index c192bf22b0..67cbf23c4d 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -1102,7 +1102,8 @@ func TestAdminEvents_MultiSmesher(t *testing.T) { mgr, err := activation.NewPostSetupManager( cfg.POST, logger.Zap(), - app.cachedDB, + app.db, + app.atxsdata, types.ATXID(app.Config.Genesis.GoldenATX()), app.syncer, app.validator, diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 4545683f41..404ab0b189 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -2,7 +2,6 @@ package atxs import ( "context" - "errors" "fmt" "time" @@ -381,18 +380,14 @@ func NonceByID(db sql.Executor, id types.ATXID) (nonce types.VRFPostIndex, err e enc := func(stmt *sql.Statement) { stmt.BindBytes(1, id.Bytes()) } - gotNonce := false dec := func(stmt *sql.Statement) bool { - if stmt.ColumnType(0) != sqlite.SQLITE_NULL { - nonce = types.VRFPostIndex(stmt.ColumnInt64(0)) - gotNonce = true - } - return true + nonce = types.VRFPostIndex(stmt.ColumnInt64(0)) + return false } if rows, err := db.Exec("select nonce from atxs where id = ?1", enc, dec); err != nil { return types.VRFPostIndex(0), fmt.Errorf("get nonce for ATX id %v: %w", id, err) - } else if rows == 0 || !gotNonce { + } else if rows == 0 { return types.VRFPostIndex(0), sql.ErrNotFound } @@ -544,12 +539,7 @@ func LatestN(db sql.Executor, n int) ([]CheckpointAtx, error) { stmt.ColumnBytes(5, catx.SmesherID[:]) catx.Sequence = uint64(stmt.ColumnInt64(6)) stmt.ColumnBytes(7, catx.Coinbase[:]) - if sql.IsNull(stmt, 8) { - ierr = errors.New("missing nonce") - return false - } else { - catx.VRFNonce = types.VRFPostIndex(stmt.ColumnInt64(8)) - } + catx.VRFNonce = types.VRFPostIndex(stmt.ColumnInt64(8)) rst = append(rst, catx) return true } @@ -643,7 +633,7 @@ func IterateAtxsData( weight uint64, base uint64, height uint64, - nonce *types.VRFPostIndex, + nonce types.VRFPostIndex, isMalicious bool, ) bool, ) error { @@ -675,14 +665,10 @@ func IterateAtxsData( effectiveUnits := uint64(stmt.ColumnInt64(4)) baseHeight := uint64(stmt.ColumnInt64(5)) ticks := uint64(stmt.ColumnInt64(6)) - var vrfNonce *types.VRFPostIndex - if !sql.IsNull(stmt, 7) { - nonce := types.VRFPostIndex(stmt.ColumnInt64(7)) - vrfNonce = &nonce - } + nonce := types.VRFPostIndex(stmt.ColumnInt64(7)) isMalicious := stmt.ColumnInt(8) != 0 return fn(id, node, epoch, coinbase, effectiveUnits*ticks, - baseHeight, baseHeight+ticks, vrfNonce, isMalicious) + baseHeight, baseHeight+ticks, nonce, isMalicious) }, ) if err != nil { diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 271fbd3103..6bcc04b13f 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -22,6 +22,7 @@ import ( "github.com/spacemeshos/go-spacemesh/activation" "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/api/grpcserver" + "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" @@ -115,6 +116,7 @@ func TestPostMalfeasanceProof(t *testing.T) { cfg.POST, logger.Named("post"), datastore.NewCachedDB(sql.InMemory(), log.NewNop()), + atxsdata.New(), cl.GoldenATX(), syncer, activation.NewMocknipostValidator(ctrl),