From 7efcf58ec6c30cda2d3236ce73245dfed716d96c Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Thu, 13 Jun 2024 23:39:39 +0000 Subject: [PATCH] AtxBuilder verification of initial PoST (#6031) ## Motivation In #5921, an edge case is described where a node may start with a certain amount of SUs, generate an initial PoST but for possible reasons, the number of SUs may change before the first ATX is broadcasted. This means that the initial PoST is no longer valid and needs to be regenerated (costing the miner another epoch before being able to submit the initial ATX). --- CHANGELOG.md | 4 + activation/activation.go | 47 ++++-- activation/activation_multi_test.go | 72 ++++++---- activation/activation_test.go | 134 ++++++++++++++---- activation/e2e/activation_test.go | 21 ++- activation/e2e/builds_atx_v2_test.go | 1 + activation/e2e/nipost_test.go | 8 +- activation/e2e/validation_test.go | 5 +- activation/interface.go | 7 +- activation/mocks.go | 24 ++-- activation/nipost.go | 49 +++++-- activation/nipost_test.go | 118 +++++++++++---- activation/post_states_test.go | 3 +- node/node.go | 1 + .../distributed_post_verification_test.go | 35 ++++- 15 files changed, 390 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e5bf89015..3ade858c7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,10 @@ operating your own PoET and want to use certificate authentication please refer ATXs. This vulnerability allows an attacker to claim rewards for a full tick amount although they should not be eligible for them. +* [#6031](https://github.com/spacemeshos/go-spacemesh/pull/6031) Fixed an edge case where the storage units might have + changed after the initial PoST was generated but before the first ATX has been emitted, invalidating the initial PoST. + The node will now try to verify the initial PoST and regenerate it if necessary. + ## Release v1.5.7 ### Improvements diff --git a/activation/activation.go b/activation/activation.go index 8d9838c217..5e85d24aab 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -32,7 +32,10 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/localsql/nipost" ) -var ErrNotFound = errors.New("not found") +var ( + ErrNotFound = errors.New("not found") + errNilVrfNonce = errors.New("nil VRF nonce") +) // PoetConfig is the configuration to interact with the poet server. type PoetConfig struct { @@ -351,10 +354,9 @@ func (b *Builder) buildInitialPost(ctx context.Context, nodeID types.NodeID) err default: return fmt.Errorf("get initial post: %w", err) } - // Create the initial post and save it. startTime := time.Now() - post, postInfo, err := b.nipostBuilder.Proof(ctx, nodeID, shared.ZeroChallenge) + post, postInfo, err := b.nipostBuilder.Proof(ctx, nodeID, shared.ZeroChallenge, nil) if err != nil { return fmt.Errorf("post execution: %w", err) } @@ -362,8 +364,9 @@ func (b *Builder) buildInitialPost(ctx context.Context, nodeID types.NodeID) err b.logger.Error("initial PoST is invalid: missing VRF nonce. Check your PoST data", log.ZShortStringer("smesherID", nodeID), ) - return errors.New("nil VRF nonce") + return errNilVrfNonce } + initialPost := nipost.Post{ Nonce: post.Nonce, Indices: post.Indices, @@ -390,22 +393,28 @@ func (b *Builder) buildInitialPost(ctx context.Context, nodeID types.NodeID) err return nipost.AddPost(b.localDB, nodeID, initialPost) } -func (b *Builder) run(ctx context.Context, sig *signing.EdSigner) { - defer b.logger.Info("atx builder stopped") - +func (b *Builder) buildPost(ctx context.Context, nodeID types.NodeID) error { for { - err := b.buildInitialPost(ctx, sig.NodeID()) + err := b.buildInitialPost(ctx, nodeID) if err == nil { - break + return nil } b.logger.Error("failed to generate initial proof:", zap.Error(err)) currentLayer := b.layerClock.CurrentLayer() select { case <-ctx.Done(): - return + return ctx.Err() case <-b.layerClock.AwaitLayer(currentLayer.Add(1)): } } +} + +func (b *Builder) run(ctx context.Context, sig *signing.EdSigner) { + defer b.logger.Info("atx builder stopped") + if err := b.buildPost(ctx, sig.NodeID()); err != nil { + b.logger.Error("failed to build initial post:", zap.Error(err)) + return + } var eg errgroup.Group for _, poet := range b.poets { eg.Go(func() error { @@ -451,6 +460,22 @@ func (b *Builder) run(ctx context.Context, sig *signing.EdSigner) { return case <-time.After(b.poetRetryInterval): } + case errors.Is(err, ErrInvalidInitialPost): + // delete the existing db post + // call build initial post again + b.logger.Error("initial post is no longer valid. regenerating initial post") + if err := b.nipostBuilder.ResetState(sig.NodeID()); err != nil { + b.logger.Error("failed to reset nipost builder state", zap.Error(err)) + } + if err := nipost.RemoveChallenge(b.localDB, sig.NodeID()); err != nil { + b.logger.Error("failed to discard challenge", zap.Error(err)) + } + if err := nipost.RemovePost(b.localDB, sig.NodeID()); err != nil { + b.logger.Error("failed to remove existing post from db", zap.Error(err)) + } + if err := b.buildPost(ctx, sig.NodeID()); err != nil { + b.logger.Error("failed to regenerate initial post:", zap.Error(err)) + } default: b.logger.Warn("unknown error", zap.Error(err)) // other failures are related to in-process software. we may as well panic here @@ -718,7 +743,7 @@ func (b *Builder) createAtx( return nil, fmt.Errorf("unknown ATX version: %v", version) } b.logger.Info("building ATX", zap.Stringer("smesherID", sig.NodeID()), zap.Stringer("version", version)) - nipostState, err := b.nipostBuilder.BuildNIPost(ctx, sig, challenge.PublishEpoch, challengeHash) + nipostState, err := b.nipostBuilder.BuildNIPost(ctx, sig, challengeHash, challenge) if err != nil { return nil, fmt.Errorf("build NIPost: %w", err) } diff --git a/activation/activation_multi_test.go b/activation/activation_multi_test.go index 70d12891d8..8f676c42c7 100644 --- a/activation/activation_multi_test.go +++ b/activation/activation_multi_test.go @@ -16,6 +16,7 @@ import ( "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "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/localsql/nipost" @@ -26,8 +27,9 @@ func Test_Builder_Multi_StartSmeshingCoinbase(t *testing.T) { coinbase := types.Address{1, 1, 1} for _, sig := range tab.signers { - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn( - func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) { + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn( + func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) { <-ctx.Done() return nil, nil, ctx.Err() }) @@ -51,8 +53,9 @@ func Test_Builder_Multi_RestartSmeshing(t *testing.T) { tab := newTestBuilder(t, 5) for _, sig := range tab.signers { - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).AnyTimes().DoAndReturn( - func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) { + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).AnyTimes().DoAndReturn( + func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) { <-ctx.Done() return nil, nil, ctx.Err() }) @@ -109,8 +112,9 @@ func Test_Builder_Multi_StopSmeshing_Delete(t *testing.T) { tab.mclock.EXPECT().AwaitLayer(gomock.Any()).Return(make(chan struct{})).Times(numIds) for _, sig := range tab.signers { - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn( - func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) { + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn( + func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) { <-ctx.Done() return nil, nil, ctx.Err() }) @@ -132,8 +136,9 @@ func Test_Builder_Multi_StopSmeshing_Delete(t *testing.T) { require.Equal(t, refChallenge, challenge) // challenge still present tab.mnipost.EXPECT().ResetState(sig.NodeID()).Return(nil) - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn( - func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) { + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn( + func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) { <-ctx.Done() return nil, nil, ctx.Err() }) @@ -152,8 +157,9 @@ func Test_Builder_Multi_StopSmeshing_Delete(t *testing.T) { require.Nil(t, challenge) // challenge deleted tab.mnipost.EXPECT().ResetState(sig.NodeID()).Return(nil) - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn( - func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) { + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn( + func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) { <-ctx.Done() return nil, nil, ctx.Err() }) @@ -241,7 +247,7 @@ func Test_Builder_Multi_InitialPost(t *testing.T) { commitmentATX := types.RandomATXID() tab.mValidator.EXPECT(). PostV2(gomock.Any(), sig.NodeID(), commitmentATX, post, shared.ZeroChallenge, numUnits) - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).Return( + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).Return( post, &types.PostInfo{ CommitmentATX: commitmentATX, @@ -252,12 +258,10 @@ func Test_Builder_Multi_InitialPost(t *testing.T) { }, nil, ) - err := tab.buildInitialPost(context.Background(), sig.NodeID()) - require.NoError(t, err) + require.NoError(t, tab.buildInitialPost(context.Background(), sig.NodeID())) // postClient.Proof() should not be called again - err = tab.buildInitialPost(context.Background(), sig.NodeID()) - require.NoError(t, err) + require.NoError(t, tab.buildInitialPost(context.Background(), sig.NodeID())) return nil }) } @@ -277,7 +281,7 @@ func Test_Builder_Multi_HappyPath(t *testing.T) { ch := make(chan struct{}) initialPostStep[sig.NodeID()] = ch - nipost := nipost.Post{ + dbPost := nipost.Post{ Indices: types.RandomBytes(10), Nonce: rand.Uint32(), Pow: rand.Uint64(), @@ -286,28 +290,29 @@ func Test_Builder_Multi_HappyPath(t *testing.T) { CommitmentATX: types.RandomATXID(), VRFNonce: types.VRFPostIndex(rand.Uint64()), } - initialPost[sig.NodeID()] = &nipost + initialPost[sig.NodeID()] = &dbPost post := &types.Post{ - Indices: nipost.Indices, - Nonce: nipost.Nonce, - Pow: nipost.Pow, + Indices: dbPost.Indices, + Nonce: dbPost.Nonce, + Pow: dbPost.Pow, } tab.mValidator.EXPECT(). - PostV2(gomock.Any(), sig.NodeID(), nipost.CommitmentATX, post, shared.ZeroChallenge, nipost.NumUnits) - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn( - func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) { + PostV2(gomock.Any(), sig.NodeID(), dbPost.CommitmentATX, post, shared.ZeroChallenge, dbPost.NumUnits) + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn( + func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) { <-initialPostChan close(ch) post := &types.Post{ - Indices: nipost.Indices, - Nonce: nipost.Nonce, - Pow: nipost.Pow, + Indices: dbPost.Indices, + Nonce: dbPost.Nonce, + Pow: dbPost.Pow, } postInfo := &types.PostInfo{ - NumUnits: nipost.NumUnits, - CommitmentATX: nipost.CommitmentATX, - Nonce: &nipost.VRFNonce, + NumUnits: dbPost.NumUnits, + CommitmentATX: dbPost.CommitmentATX, + Nonce: &dbPost.VRFNonce, LabelsPerUnit: DefaultPostConfig().LabelsPerUnit, } @@ -400,8 +405,13 @@ func Test_Builder_Multi_HappyPath(t *testing.T) { } nipostState[sig.NodeID()] = state tab.mnipost.EXPECT(). - BuildNIPost(gomock.Any(), sig, ref.PublishEpoch, ref.Hash()). - Return(state, nil) + BuildNIPost(gomock.Any(), sig, ref.Hash(), gomock.Any()). + DoAndReturn(func(_ context.Context, _ *signing.EdSigner, _ types.Hash32, + postChallenge *types.NIPostChallenge, + ) (*nipost.NIPostState, error) { + require.Equal(t, postChallenge.PublishEpoch, ref.PublishEpoch, "publish epoch mismatch") + return state, nil + }) // awaiting atx publication epoch log tab.mclock.EXPECT().CurrentLayer().DoAndReturn( diff --git a/activation/activation_test.go b/activation/activation_test.go index 78a81c26b5..1b2372debd 100644 --- a/activation/activation_test.go +++ b/activation/activation_test.go @@ -172,7 +172,8 @@ func publishAtx( LabelsPerUnit: DefaultPostConfig().LabelsPerUnit, }, nil).AnyTimes() tab.mnipost.EXPECT().BuildNIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, _ *signing.EdSigner, _ types.EpochID, _ types.Hash32) (*nipost.NIPostState, error) { + func(_ context.Context, _ *signing.EdSigner, _ types.Hash32, _ *types.NIPostChallenge, + ) (*nipost.NIPostState, error) { *currLayer = currLayer.Add(buildNIPostLayerDuration) return newNIPostWithPoet(tb, types.RandomHash().Bytes()), nil }) @@ -201,8 +202,9 @@ func Test_Builder_StartSmeshingCoinbase(t *testing.T) { sig := maps.Values(tab.signers)[0] coinbase := types.Address{1, 1, 1} - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn( - func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) { + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn( + func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) { <-ctx.Done() return nil, nil, ctx.Err() }) @@ -223,8 +225,9 @@ func TestBuilder_RestartSmeshing(t *testing.T) { tab := newTestBuilder(t, 1) sig := maps.Values(tab.signers)[0] - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).AnyTimes().DoAndReturn( - func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) { + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).AnyTimes().DoAndReturn( + func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) { <-ctx.Done() return nil, nil, ctx.Err() }) @@ -279,8 +282,9 @@ func TestBuilder_StopSmeshing_Delete(t *testing.T) { tab.mclock.EXPECT().CurrentLayer().Return(currLayer) tab.mclock.EXPECT().AwaitLayer(gomock.Any()).Return(make(chan struct{})) - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn( - func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) { + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn( + func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) { <-ctx.Done() return nil, nil, ctx.Err() }) @@ -300,8 +304,9 @@ func TestBuilder_StopSmeshing_Delete(t *testing.T) { require.Equal(t, refChallenge, challenge) // challenge still present tab.mnipost.EXPECT().ResetState(sig.NodeID()).Return(nil) - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn( - func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) { + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn( + func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) { <-ctx.Done() return nil, nil, ctx.Err() }) @@ -318,8 +323,9 @@ func TestBuilder_StopSmeshing_Delete(t *testing.T) { require.Nil(t, challenge) // challenge deleted tab.mnipost.EXPECT().ResetState(sig.NodeID()).Return(nil) - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn( - func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) { + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn( + func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) { <-ctx.Done() return nil, nil, ctx.Err() }) @@ -452,7 +458,8 @@ func TestBuilder_PublishActivationTx_FaultyNet(t *testing.T) { LabelsPerUnit: DefaultPostConfig().LabelsPerUnit, }, nil).AnyTimes() tab.mnipost.EXPECT().BuildNIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, _ *signing.EdSigner, _ types.EpochID, _ types.Hash32) (*nipost.NIPostState, error) { + func(_ context.Context, _ *signing.EdSigner, _ types.Hash32, _ *types.NIPostChallenge, + ) (*nipost.NIPostState, error) { currLayer = currLayer.Add(layersPerEpoch) return newNIPostWithPoet(t, []byte("66666")), nil }) @@ -526,7 +533,8 @@ func TestBuilder_PublishActivationTx_UsesExistingChallengeOnLatePublish(t *testi LabelsPerUnit: DefaultPostConfig().LabelsPerUnit, }, nil).AnyTimes() tab.mnipost.EXPECT().BuildNIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, _ *signing.EdSigner, _ types.EpochID, _ types.Hash32) (*nipost.NIPostState, error) { + func(_ context.Context, _ *signing.EdSigner, _ types.Hash32, _ *types.NIPostChallenge, + ) (*nipost.NIPostState, error) { currLayer = currLayer.Add(1) return newNIPostWithPoet(t, []byte("66666")), nil }) @@ -595,7 +603,8 @@ func TestBuilder_PublishActivationTx_RebuildNIPostWhenTargetEpochPassed(t *testi return genesis.Add(layerDuration * time.Duration(got)) }).AnyTimes() tab.mnipost.EXPECT().BuildNIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, _ *signing.EdSigner, _ types.EpochID, _ types.Hash32) (*nipost.NIPostState, error) { + func(_ context.Context, _ *signing.EdSigner, _ types.Hash32, _ *types.NIPostChallenge, + ) (*nipost.NIPostState, error) { currLayer = currLayer.Add(layersPerEpoch) return newNIPostWithPoet(t, []byte("66666")), nil }) @@ -825,7 +834,8 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { tab.mnipost.EXPECT(). BuildNIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn( - func(_ context.Context, _ *signing.EdSigner, _ types.EpochID, _ types.Hash32) (*nipost.NIPostState, error) { + func(_ context.Context, _ *signing.EdSigner, _ types.Hash32, _ *types.NIPostChallenge, + ) (*nipost.NIPostState, error) { currentLayer = currentLayer.Add(5) return newNIPostWithPoet(t, poetBytes), nil }) @@ -907,7 +917,8 @@ func TestBuilder_PublishActivationTx_TargetsEpochBasedOnPosAtx(t *testing.T) { }, nil).AnyTimes() tab.mnipost.EXPECT().BuildNIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, _ *signing.EdSigner, _ types.EpochID, _ types.Hash32) (*nipost.NIPostState, error) { + func(_ context.Context, _ *signing.EdSigner, _ types.Hash32, _ *types.NIPostChallenge, + ) (*nipost.NIPostState, error) { currentLayer = currentLayer.Add(layersPerEpoch) return newNIPostWithPoet(t, poetBytes), nil }) @@ -1056,7 +1067,7 @@ func TestBuilder_RetryPublishActivationTx(t *testing.T) { Times(expectedTries). DoAndReturn( // nolint:lll - func(_ context.Context, _ *signing.EdSigner, _ types.EpochID, _ types.Hash32) (*nipost.NIPostState, error) { + func(_ context.Context, _ *signing.EdSigner, _ types.Hash32, _ *types.NIPostChallenge) (*nipost.NIPostState, error) { now := time.Now() if now.Sub(last) < retryInterval { require.FailNow(t, "retry interval not respected") @@ -1147,7 +1158,7 @@ func TestBuilder_InitialProofGeneratedOnce(t *testing.T) { Pow: post.Pow, } - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).Return( + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).Return( initialPost, &types.PostInfo{ NodeID: sig.NodeID(), @@ -1161,10 +1172,10 @@ func TestBuilder_InitialProofGeneratedOnce(t *testing.T) { ) tab.mValidator.EXPECT(). PostV2(gomock.Any(), sig.NodeID(), post.CommitmentATX, initialPost, shared.ZeroChallenge, post.NumUnits) + require.NoError(t, tab.buildInitialPost(context.Background(), sig.NodeID())) // postClient.Proof() should not be called again - err := tab.buildInitialPost(context.Background(), sig.NodeID()) - require.NoError(t, err) + require.NoError(t, tab.buildInitialPost(context.Background(), sig.NodeID())) } func TestBuilder_InitialPostIsPersisted(t *testing.T) { @@ -1179,7 +1190,7 @@ func TestBuilder_InitialPostIsPersisted(t *testing.T) { Indices: types.RandomBytes(10), Pow: rand.Uint64(), } - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).Return( + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).Return( initialPost, &types.PostInfo{ NodeID: sig.NodeID(), @@ -1193,12 +1204,11 @@ func TestBuilder_InitialPostIsPersisted(t *testing.T) { ) tab.mValidator.EXPECT(). PostV2(gomock.Any(), sig.NodeID(), commitmentATX, initialPost, shared.ZeroChallenge, numUnits) - err := tab.buildInitialPost(context.Background(), sig.NodeID()) - require.NoError(t, err) + + require.NoError(t, tab.buildInitialPost(context.Background(), sig.NodeID())) // postClient.Proof() should not be called again - err = tab.buildInitialPost(context.Background(), sig.NodeID()) - require.NoError(t, err) + require.NoError(t, tab.buildInitialPost(context.Background(), sig.NodeID())) } func TestBuilder_InitialPostLogErrorMissingVRFNonce(t *testing.T) { @@ -1212,7 +1222,7 @@ func TestBuilder_InitialPostLogErrorMissingVRFNonce(t *testing.T) { Indices: types.RandomBytes(10), Pow: rand.Uint64(), } - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).Return( + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, gomock.Any()).Return( initialPost, &types.PostInfo{ NodeID: sig.NodeID(), @@ -1226,7 +1236,7 @@ func TestBuilder_InitialPostLogErrorMissingVRFNonce(t *testing.T) { tab.mValidator.EXPECT(). PostV2(gomock.Any(), sig.NodeID(), commitmentATX, initialPost, shared.ZeroChallenge, numUnits) err := tab.buildInitialPost(context.Background(), sig.NodeID()) - require.ErrorContains(t, err, "nil VRF nonce") + require.ErrorIs(t, err, errNilVrfNonce) observedLogs := tab.observedLogs.FilterLevelExact(zapcore.ErrorLevel) require.Equal(t, 1, observedLogs.Len(), "expected 1 log message") @@ -1236,7 +1246,7 @@ func TestBuilder_InitialPostLogErrorMissingVRFNonce(t *testing.T) { // postClient.Proof() should be called again and no error if vrf nonce is provided nonce := types.VRFPostIndex(rand.Uint64()) - tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).Return( + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).Return( initialPost, &types.PostInfo{ NodeID: sig.NodeID(), @@ -1248,8 +1258,7 @@ func TestBuilder_InitialPostLogErrorMissingVRFNonce(t *testing.T) { }, nil, ) - err = tab.buildInitialPost(context.Background(), sig.NodeID()) - require.NoError(t, err) + require.NoError(t, tab.buildInitialPost(context.Background(), sig.NodeID())) } func TestWaitPositioningAtx(t *testing.T) { @@ -1487,3 +1496,68 @@ func TestFindFullyValidHighTickAtx(t *testing.T) { require.Equal(t, atxLower.ID(), found) }) } + +// Test_Builder_RegenerateInitialPost tests the coverage for the edge case +// where a node operator may change SUs after creating the initial PoST but before +// submitting the first ATX, which should result in the initial PoST to be deleted +// and for the new PoST to be generated instead (this also loses the eligibility +// for the current epoch). This behavior is mocked by mocking the response of the +// proof validator. +func Test_Builder_RegenerateInitialPost(t *testing.T) { + var ( + tab = newTestBuilder(t, 1) + sig = maps.Values(tab.signers)[0] + genesis = time.Now() + ctx, cancel = context.WithCancel(context.Background()) + commitmentATX = types.RandomATXID() + nonce = types.VRFPostIndex(rand.Uint64()) + numUnits = uint32(12) + initialPost = &types.Post{ + Nonce: rand.Uint32(), + Indices: types.RandomBytes(10), + Pow: rand.Uint64(), + } + ) + + tab.mValidator.EXPECT(). + PostV2(gomock.Any(), sig.NodeID(), commitmentATX, initialPost, shared.ZeroChallenge, numUnits). + Return(nil).Times(4) + + tab.mclock.EXPECT().CurrentLayer().Return(types.LayerID(0)).AnyTimes() + tab.mclock.EXPECT().AwaitLayer(gomock.Any()).DoAndReturn(func(id types.LayerID) <-chan struct{} { + // this is our way to end the test - the code will wait on AwaitLayer in PublishActivationTx while + // waiting for the publication epoch. otherwise the `run` method will keep on looping to broadcast + // future atxs + cancel() + return make(chan struct{}) + }).AnyTimes() + tab.mclock.EXPECT().LayerToTime(gomock.Any()).DoAndReturn(func(lid types.LayerID) time.Time { + // layer duration is 10ms to speed up test + return genesis.Add(time.Duration(lid) * 20 * time.Millisecond) + }).AnyTimes() + tab.mnipost.EXPECT().ResetState(sig.NodeID()).Return(nil).Times(1) + + tab.mnipost.EXPECT(). + BuildNIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, ErrInvalidInitialPost) + tab.mnipost.EXPECT(). + BuildNIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&nipost.NIPostState{}, nil) + + tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).Return(initialPost, + &types.PostInfo{ + NodeID: sig.NodeID(), + CommitmentATX: commitmentATX, + Nonce: &nonce, + + NumUnits: numUnits, + LabelsPerUnit: DefaultPostConfig().LabelsPerUnit, + }, + nil, + ).Times(2) + + var eg errgroup.Group + eg.Go(func() error { + tab.run(ctx, sig) + return nil + }) + t.Cleanup(func() { assert.NoError(t, eg.Wait()) }) +} diff --git a/activation/e2e/activation_test.go b/activation/e2e/activation_test.go index 8913c0feeb..684de8d0fa 100644 --- a/activation/e2e/activation_test.go +++ b/activation/e2e/activation_test.go @@ -140,6 +140,17 @@ func Test_BuilderWithMultipleClients(t *testing.T) { require.NoError(t, err) t.Cleanup(clock.Close) + verifier, err := activation.NewPostVerifier(cfg, logger.Named("verifier")) + require.NoError(t, err) + + validator := activation.NewValidator( + db, + poetDb, + cfg, + opts.Scrypt, + verifier, + ) + postStates := activation.NewMockPostStates(ctrl) nb, err := activation.NewNIPostBuilder( localDB, @@ -147,6 +158,7 @@ func Test_BuilderWithMultipleClients(t *testing.T) { logger.Named("nipostBuilder"), poetCfg, clock, + validator, activation.NipostbuilderWithPostStates(postStates), activation.WithPoetClients(client), ) @@ -187,10 +199,7 @@ func Test_BuilderWithMultipleClients(t *testing.T) { }, ).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(db, poetDb, cfg, opts.Scrypt, verifier) tab := activation.NewBuilder( conf, db, @@ -202,7 +211,7 @@ func Test_BuilderWithMultipleClients(t *testing.T) { syncer, logger, activation.WithPoetConfig(poetCfg), - activation.WithValidator(v), + activation.WithValidator(validator), activation.WithPostStates(postStates), activation.WithPoets(client), ) @@ -241,7 +250,7 @@ func Test_BuilderWithMultipleClients(t *testing.T) { require.Equal(t, sig.NodeID(), *atx.NodeID) require.Equal(t, goldenATX, atx.PositioningATXID) require.NotNil(t, atx.VRFNonce) - err := v.VRFNonce( + err := validator.VRFNonce( sig.NodeID(), commitment, uint64(*atx.VRFNonce), @@ -253,7 +262,7 @@ func Test_BuilderWithMultipleClients(t *testing.T) { require.Nil(t, atx.VRFNonce) require.Equal(t, previous, atx.PositioningATXID) } - _, err = v.NIPost( + _, err = validator.NIPost( context.Background(), sig.NodeID(), commitment, diff --git a/activation/e2e/builds_atx_v2_test.go b/activation/e2e/builds_atx_v2_test.go index 004656d301..057b45beec 100644 --- a/activation/e2e/builds_atx_v2_test.go +++ b/activation/e2e/builds_atx_v2_test.go @@ -131,6 +131,7 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { logger.Named("nipostBuilder"), poetCfg, clock, + validator, activation.NipostbuilderWithPostStates(postStates), activation.WithPoetClients(client), ) diff --git a/activation/e2e/nipost_test.go b/activation/e2e/nipost_test.go index fb195bc308..7b68ec9eff 100644 --- a/activation/e2e/nipost_test.go +++ b/activation/e2e/nipost_test.go @@ -229,12 +229,13 @@ func TestNIPostBuilderWithClients(t *testing.T) { logger.Named("nipostBuilder"), poetCfg, mclock, + nil, activation.WithPoetClients(client), ) require.NoError(t, err) challenge := types.RandomHash() - nipost, err := nb.BuildNIPost(context.Background(), sig, 7, challenge) + nipost, err := nb.BuildNIPost(context.Background(), sig, challenge, &types.NIPostChallenge{PublishEpoch: 7}) require.NoError(t, err) v := activation.NewValidator(nil, poetDb, cfg, opts.Scrypt, verifier) @@ -348,6 +349,7 @@ func Test_NIPostBuilderWithMultipleClients(t *testing.T) { logger.Named("nipostBuilder"), poetCfg, mclock, + validator, activation.WithPoetClients(client), ) require.NoError(t, err) @@ -355,12 +357,12 @@ func Test_NIPostBuilderWithMultipleClients(t *testing.T) { challenge := types.RandomHash() for _, sig := range signers { eg.Go(func() error { - post, info, err := nb.Proof(context.Background(), sig.NodeID(), shared.ZeroChallenge) + post, info, err := nb.Proof(context.Background(), sig.NodeID(), shared.ZeroChallenge, nil) require.NoError(t, err) err = nipost.AddPost(localDB, sig.NodeID(), *fullPost(post, info, shared.ZeroChallenge)) require.NoError(t, err) - nipost, err := nb.BuildNIPost(context.Background(), sig, 7, challenge) + nipost, err := nb.BuildNIPost(context.Background(), sig, challenge, &types.NIPostChallenge{PublishEpoch: 7}) require.NoError(t, err) v := activation.NewValidator(nil, poetDb, cfg, opts.Scrypt, verifier) diff --git a/activation/e2e/validation_test.go b/activation/e2e/validation_test.go index ab17b39a82..7432b5ec2e 100644 --- a/activation/e2e/validation_test.go +++ b/activation/e2e/validation_test.go @@ -86,7 +86,6 @@ func TestValidator_Validate(t *testing.T) { verifier, err := activation.NewPostVerifier(cfg, logger.Named("verifier")) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, verifier.Close()) }) - svc := grpcserver.NewPostService(logger) svc.AllowConnections(true) grpcCfg, cleanup := launchServer(t, svc) @@ -106,11 +105,13 @@ func TestValidator_Validate(t *testing.T) { logger.Named("nipostBuilder"), poetCfg, mclock, + validator, activation.WithPoetClients(client), ) require.NoError(t, err) - nipost, err := nb.BuildNIPost(context.Background(), sig, postGenesisEpoch+2, challenge) + nipost, err := nb.BuildNIPost(context.Background(), sig, challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.NoError(t, err) v := activation.NewValidator(db, poetDb, cfg, opts.Scrypt, verifier) diff --git a/activation/interface.go b/activation/interface.go index 79869e3787..d70c76f927 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -52,10 +52,11 @@ type nipostBuilder interface { BuildNIPost( ctx context.Context, sig *signing.EdSigner, - publish types.EpochID, - challenge types.Hash32, + challengeHash types.Hash32, + postChallenge *types.NIPostChallenge, ) (*nipost.NIPostState, error) - Proof(ctx context.Context, nodeID types.NodeID, challenge []byte) (*types.Post, *types.PostInfo, error) + Proof(ctx context.Context, nodeID types.NodeID, challenge []byte, postChallenge *types.NIPostChallenge, + ) (*types.Post, *types.PostInfo, error) ResetState(types.NodeID) error } diff --git a/activation/mocks.go b/activation/mocks.go index f7f9a5239c..6f81c036bd 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -909,18 +909,18 @@ func (m *MocknipostBuilder) EXPECT() *MocknipostBuilderMockRecorder { } // BuildNIPost mocks base method. -func (m *MocknipostBuilder) BuildNIPost(ctx context.Context, sig *signing.EdSigner, publish types.EpochID, challenge types.Hash32) (*nipost.NIPostState, error) { +func (m *MocknipostBuilder) BuildNIPost(ctx context.Context, sig *signing.EdSigner, challengeHash types.Hash32, postChallenge *types.NIPostChallenge) (*nipost.NIPostState, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BuildNIPost", ctx, sig, publish, challenge) + ret := m.ctrl.Call(m, "BuildNIPost", ctx, sig, challengeHash, postChallenge) ret0, _ := ret[0].(*nipost.NIPostState) ret1, _ := ret[1].(error) return ret0, ret1 } // BuildNIPost indicates an expected call of BuildNIPost. -func (mr *MocknipostBuilderMockRecorder) BuildNIPost(ctx, sig, publish, challenge any) *MocknipostBuilderBuildNIPostCall { +func (mr *MocknipostBuilderMockRecorder) BuildNIPost(ctx, sig, challengeHash, postChallenge any) *MocknipostBuilderBuildNIPostCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildNIPost", reflect.TypeOf((*MocknipostBuilder)(nil).BuildNIPost), ctx, sig, publish, challenge) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildNIPost", reflect.TypeOf((*MocknipostBuilder)(nil).BuildNIPost), ctx, sig, challengeHash, postChallenge) return &MocknipostBuilderBuildNIPostCall{Call: call} } @@ -936,21 +936,21 @@ func (c *MocknipostBuilderBuildNIPostCall) Return(arg0 *nipost.NIPostState, arg1 } // Do rewrite *gomock.Call.Do -func (c *MocknipostBuilderBuildNIPostCall) Do(f func(context.Context, *signing.EdSigner, types.EpochID, types.Hash32) (*nipost.NIPostState, error)) *MocknipostBuilderBuildNIPostCall { +func (c *MocknipostBuilderBuildNIPostCall) Do(f func(context.Context, *signing.EdSigner, types.Hash32, *types.NIPostChallenge) (*nipost.NIPostState, error)) *MocknipostBuilderBuildNIPostCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MocknipostBuilderBuildNIPostCall) DoAndReturn(f func(context.Context, *signing.EdSigner, types.EpochID, types.Hash32) (*nipost.NIPostState, error)) *MocknipostBuilderBuildNIPostCall { +func (c *MocknipostBuilderBuildNIPostCall) DoAndReturn(f func(context.Context, *signing.EdSigner, types.Hash32, *types.NIPostChallenge) (*nipost.NIPostState, error)) *MocknipostBuilderBuildNIPostCall { c.Call = c.Call.DoAndReturn(f) return c } // Proof mocks base method. -func (m *MocknipostBuilder) Proof(ctx context.Context, nodeID types.NodeID, challenge []byte) (*types.Post, *types.PostInfo, error) { +func (m *MocknipostBuilder) Proof(ctx context.Context, nodeID types.NodeID, challenge []byte, postChallenge *types.NIPostChallenge) (*types.Post, *types.PostInfo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Proof", ctx, nodeID, challenge) + ret := m.ctrl.Call(m, "Proof", ctx, nodeID, challenge, postChallenge) ret0, _ := ret[0].(*types.Post) ret1, _ := ret[1].(*types.PostInfo) ret2, _ := ret[2].(error) @@ -958,9 +958,9 @@ func (m *MocknipostBuilder) Proof(ctx context.Context, nodeID types.NodeID, chal } // Proof indicates an expected call of Proof. -func (mr *MocknipostBuilderMockRecorder) Proof(ctx, nodeID, challenge any) *MocknipostBuilderProofCall { +func (mr *MocknipostBuilderMockRecorder) Proof(ctx, nodeID, challenge, postChallenge any) *MocknipostBuilderProofCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Proof", reflect.TypeOf((*MocknipostBuilder)(nil).Proof), ctx, nodeID, challenge) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Proof", reflect.TypeOf((*MocknipostBuilder)(nil).Proof), ctx, nodeID, challenge, postChallenge) return &MocknipostBuilderProofCall{Call: call} } @@ -976,13 +976,13 @@ func (c *MocknipostBuilderProofCall) Return(arg0 *types.Post, arg1 *types.PostIn } // Do rewrite *gomock.Call.Do -func (c *MocknipostBuilderProofCall) Do(f func(context.Context, types.NodeID, []byte) (*types.Post, *types.PostInfo, error)) *MocknipostBuilderProofCall { +func (c *MocknipostBuilderProofCall) Do(f func(context.Context, types.NodeID, []byte, *types.NIPostChallenge) (*types.Post, *types.PostInfo, error)) *MocknipostBuilderProofCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MocknipostBuilderProofCall) DoAndReturn(f func(context.Context, types.NodeID, []byte) (*types.Post, *types.PostInfo, error)) *MocknipostBuilderProofCall { +func (c *MocknipostBuilderProofCall) DoAndReturn(f func(context.Context, types.NodeID, []byte, *types.NIPostChallenge) (*types.Post, *types.PostInfo, error)) *MocknipostBuilderProofCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/activation/nipost.go b/activation/nipost.go index e8ba0e714d..36fe55b108 100644 --- a/activation/nipost.go +++ b/activation/nipost.go @@ -10,6 +10,7 @@ import ( "github.com/spacemeshos/merkle-tree" "github.com/spacemeshos/poet/shared" + postshared "github.com/spacemeshos/post/shared" "go.uber.org/zap" "golang.org/x/sync/errgroup" @@ -42,6 +43,8 @@ const ( maxPoetGetProofJitter = 0.04 ) +var ErrInvalidInitialPost = errors.New("invalid initial post") + // NIPostBuilder holds the required state and dependencies to create Non-Interactive Proofs of Space-Time (NIPost). type NIPostBuilder struct { localDB *localsql.Database @@ -52,6 +55,7 @@ type NIPostBuilder struct { poetCfg PoetConfig layerClock layerClock postStates PostStates + validator nipostValidator } type NIPostBuilderOption func(*NIPostBuilder) @@ -78,6 +82,7 @@ func NewNIPostBuilder( lg *zap.Logger, poetCfg PoetConfig, layerClock layerClock, + validator nipostValidator, opts ...NIPostBuilderOption, ) (*NIPostBuilder, error) { b := &NIPostBuilder{ @@ -88,6 +93,7 @@ func NewNIPostBuilder( poetCfg: poetCfg, layerClock: layerClock, postStates: NewPostStates(lg), + validator: validator, } for _, opt := range opts { @@ -110,6 +116,7 @@ func (nb *NIPostBuilder) Proof( ctx context.Context, nodeID types.NodeID, challenge []byte, + postChallenge *types.NIPostChallenge, ) (*types.Post, *types.PostInfo, error) { nb.postStates.Set(nodeID, types.PostStateProving) started := false @@ -141,6 +148,27 @@ func (nb *NIPostBuilder) Proof( } retries = 0 + // we check whether an initial post is included in the challenge + // if so, we verify it to still be valid before creating the post + // e.g. the PoST size might have changed + if postChallenge != nil && postChallenge.InitialPost != nil { + info, err := client.Info(ctx) + if errors.Is(err, ErrPostClientClosed) { + continue + } else if err != nil { + events.EmitPostFailure(nodeID) + return nil, nil, fmt.Errorf("failed to get post info: %w", err) + } + if err := nb.validator.PostV2(ctx, + nodeID, + info.CommitmentATX, + postChallenge.InitialPost, + postshared.ZeroChallenge, + info.NumUnits, + ); err != nil { + return nil, nil, ErrInvalidInitialPost + } + } post, postInfo, err := client.Proof(ctx, challenge) switch { case errors.Is(err, ErrPostClientClosed): @@ -163,8 +191,8 @@ func (nb *NIPostBuilder) Proof( func (nb *NIPostBuilder) BuildNIPost( ctx context.Context, signer *signing.EdSigner, - publishEpoch types.EpochID, challenge types.Hash32, + postChallenge *types.NIPostChallenge, ) (*nipost.NIPostState, error) { logger := nb.logger.With(log.ZContext(ctx), log.ZShortStringer("smesherID", signer.NodeID())) // Note: to avoid missing next PoET round, we need to publish the ATX before the next PoET round starts. @@ -180,13 +208,14 @@ func (nb *NIPostBuilder) BuildNIPost( // WE ARE HERE PROOF BECOMES ATX PUBLICATION // AVAILABLE DEADLINE - poetRoundStart := nb.layerClock.LayerToTime((publishEpoch - 1).FirstLayer()).Add(nb.poetCfg.PhaseShift) - poetRoundEnd := nb.layerClock.LayerToTime(publishEpoch.FirstLayer()). + poetRoundStart := nb.layerClock.LayerToTime((postChallenge.PublishEpoch - 1).FirstLayer()). + Add(nb.poetCfg.PhaseShift) + poetRoundEnd := nb.layerClock.LayerToTime(postChallenge.PublishEpoch.FirstLayer()). Add(nb.poetCfg.PhaseShift). Add(-nb.poetCfg.CycleGap) // we want to publish before the publish epoch ends or we won't receive rewards - publishEpochEnd := nb.layerClock.LayerToTime((publishEpoch + 1).FirstLayer()) + publishEpochEnd := nb.layerClock.LayerToTime((postChallenge.PublishEpoch + 1).FirstLayer()) // we want to fetch the PoET proof latest 1 CycleGap before the publish epoch ends // so that a node that is setup correctly (i.e. can generate a PoST proof within the cycle gap) @@ -197,7 +226,7 @@ func (nb *NIPostBuilder) BuildNIPost( zap.Time("poet round start", poetRoundStart), zap.Time("poet round end", poetRoundEnd), zap.Time("publish epoch end", publishEpochEnd), - zap.Uint32("publish epoch", publishEpoch.Uint32()), + zap.Uint32("publish epoch", postChallenge.PublishEpoch.Uint32()), ) // Phase 0: Submit challenge to PoET services. @@ -245,14 +274,14 @@ func (nb *NIPostBuilder) BuildNIPost( return nil, fmt.Errorf( "%w: deadline to query poet proof for pub epoch %d exceeded (deadline: %s, now: %s)", ErrATXChallengeExpired, - publishEpoch, + postChallenge.PublishEpoch, poetProofDeadline, now, ) } - events.EmitPoetWaitProof(signer.NodeID(), publishEpoch, poetRoundEnd) - poetProofRef, membership, err = nb.getBestProof(ctx, signer.NodeID(), challenge, publishEpoch) + events.EmitPoetWaitProof(signer.NodeID(), postChallenge.PublishEpoch, poetRoundEnd) + poetProofRef, membership, err = nb.getBestProof(ctx, signer.NodeID(), challenge, postChallenge.PublishEpoch) if err != nil { return nil, &PoetSvcUnstableError{msg: "getBestProof failed", source: err} } @@ -277,7 +306,7 @@ func (nb *NIPostBuilder) BuildNIPost( return nil, fmt.Errorf( "%w: deadline to publish ATX for pub epoch %d exceeded (deadline: %s, now: %s)", ErrATXChallengeExpired, - publishEpoch, + postChallenge.PublishEpoch, publishEpochEnd, now, ) @@ -287,7 +316,7 @@ func (nb *NIPostBuilder) BuildNIPost( nb.logger.Info("starting post execution", zap.Binary("challenge", poetProofRef[:])) startTime := time.Now() - proof, postInfo, err := nb.Proof(postCtx, signer.NodeID(), poetProofRef[:]) + proof, postInfo, err := nb.Proof(postCtx, signer.NodeID(), poetProofRef[:], postChallenge) if err != nil { return nil, fmt.Errorf("failed to generate Post: %w", err) } diff --git a/activation/nipost_test.go b/activation/nipost_test.go index 6856045c81..41043e9543 100644 --- a/activation/nipost_test.go +++ b/activation/nipost_test.go @@ -58,6 +58,7 @@ type testNIPostBuilder struct { mClock *MocklayerClock mPostService *MockpostService mPostClient *MockPostClient + mValidator *MocknipostValidator } func newTestNIPostBuilder(tb testing.TB) *testNIPostBuilder { @@ -88,6 +89,7 @@ func newTestNIPostBuilder(tb testing.TB) *testNIPostBuilder { mPostClient: NewMockPostClient(ctrl), mLogger: logger, mClock: defaultLayerClockMock(ctrl), + mValidator: NewMocknipostValidator(ctrl), } nb, err := NewNIPostBuilder( @@ -96,6 +98,7 @@ func newTestNIPostBuilder(tb testing.TB) *testNIPostBuilder { tnb.mLogger, PoetConfig{}, tnb.mClock, + tnb.mValidator, ) require.NoError(tb, err) tnb.NIPostBuilder = nb @@ -112,7 +115,7 @@ func Test_NIPost_PostClientHandling(t *testing.T) { tnb.mPostService.EXPECT().Client(sig.NodeID()).Return(tnb.mPostClient, nil) tnb.mPostClient.EXPECT().Proof(gomock.Any(), gomock.Any()).Return(&types.Post{}, &types.PostInfo{}, nil) - nipost, nipostInfo, err := tnb.Proof(context.Background(), sig.NodeID(), shared.ZeroChallenge) + nipost, nipostInfo, err := tnb.Proof(context.Background(), sig.NodeID(), shared.ZeroChallenge, nil) require.NoError(t, err) require.NotNil(t, nipost) require.NotNil(t, nipostInfo) @@ -148,7 +151,7 @@ func Test_NIPost_PostClientHandling(t *testing.T) { expectedErr := errors.New("some error") tnb.mPostClient.EXPECT().Proof(gomock.Any(), gomock.Any()).Return(nil, nil, expectedErr) - nipost, nipostInfo, err := tnb.Proof(context.Background(), sig.NodeID(), shared.ZeroChallenge) + nipost, nipostInfo, err := tnb.Proof(context.Background(), sig.NodeID(), shared.ZeroChallenge, nil) require.ErrorIs(t, err, expectedErr) require.Nil(t, nipost) require.Nil(t, nipostInfo) @@ -184,7 +187,7 @@ func Test_NIPost_PostClientHandling(t *testing.T) { tnb.mPostService.EXPECT().Client(sig.NodeID()).Return(tnb.mPostClient, nil) tnb.mPostClient.EXPECT().Proof(gomock.Any(), gomock.Any()).Return(&types.Post{}, &types.PostInfo{}, nil) - nipost, nipostInfo, err := tnb.Proof(context.Background(), sig.NodeID(), shared.ZeroChallenge) + nipost, nipostInfo, err := tnb.Proof(context.Background(), sig.NodeID(), shared.ZeroChallenge, nil) require.NoError(t, err) require.NotNil(t, nipost) require.NotNil(t, nipostInfo) @@ -252,7 +255,7 @@ func Test_NIPost_PostClientHandling(t *testing.T) { return nil, ErrPostClientNotConnected }) - nipost, nipostInfo, err := tnb.Proof(ctx, sig.NodeID(), shared.ZeroChallenge) + nipost, nipostInfo, err := tnb.Proof(ctx, sig.NodeID(), shared.ZeroChallenge, nil) require.ErrorIs(t, err, context.Canceled) require.Nil(t, nipost) require.Nil(t, nipostInfo) @@ -273,7 +276,7 @@ func Test_NIPost_PostClientHandling(t *testing.T) { expectedErr := errors.New("some error") tnb.mPostClient.EXPECT().Proof(gomock.Any(), gomock.Any()).Return(nil, nil, expectedErr) - nipost, nipostInfo, err := tnb.Proof(context.Background(), sig.NodeID(), shared.ZeroChallenge) + nipost, nipostInfo, err := tnb.Proof(context.Background(), sig.NodeID(), shared.ZeroChallenge, nil) require.ErrorIs(t, err, expectedErr) require.Nil(t, nipost) require.Nil(t, nipostInfo) @@ -311,7 +314,7 @@ func Test_NIPost_PostClientHandling(t *testing.T) { return nil, ErrPostClientNotConnected }) - nipost, nipostInfo, err := tnb.Proof(ctx, sig.NodeID(), shared.ZeroChallenge) + nipost, nipostInfo, err := tnb.Proof(ctx, sig.NodeID(), shared.ZeroChallenge, nil) require.ErrorIs(t, err, context.Canceled) require.Nil(t, nipost) require.Nil(t, nipostInfo) @@ -341,6 +344,7 @@ func Test_NIPostBuilder_ResetState(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, mclock, + nil, ) require.NoError(t, err) @@ -399,11 +403,13 @@ func Test_NIPostBuilder_WithMocks(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, mclock, + nil, WithPoetClients(poetProvider), ) require.NoError(t, err) - nipost, err := nb.BuildNIPost(context.Background(), sig, postGenesisEpoch+2, challenge) + nipost, err := nb.BuildNIPost(context.Background(), sig, challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.NoError(t, err) require.NotNil(t, nipost) } @@ -433,11 +439,13 @@ func TestPostSetup(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, mclock, + nil, WithPoetClients(poetProvider), ) require.NoError(t, err) - nipost, err := nb.BuildNIPost(context.Background(), sig, postGenesisEpoch+2, challenge) + nipost, err := nb.BuildNIPost(context.Background(), sig, challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.NoError(t, err) require.NotNil(t, nipost) } @@ -484,11 +492,12 @@ func TestNIPostBuilder_BuildNIPost(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, mclock, + nil, WithPoetClients(poetProver), ) require.NoError(t, err) - nipost, err := nb.BuildNIPost(context.Background(), sig, 7, challengeHash) + nipost, err := nb.BuildNIPost(context.Background(), sig, challengeHash, &types.NIPostChallenge{PublishEpoch: 7}) require.NoError(t, err) require.NotNil(t, nipost) @@ -500,6 +509,7 @@ func TestNIPostBuilder_BuildNIPost(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, mclock, + nil, WithPoetClients(poetProver), ) require.NoError(t, err) @@ -507,7 +517,8 @@ func TestNIPostBuilder_BuildNIPost(t *testing.T) { postClient.EXPECT().Proof(gomock.Any(), gomock.Any()).Return(nil, nil, errors.New("error")) // check that proof ref is not called again - nipost, err = nb.BuildNIPost(context.Background(), sig, 7, challengeHash) + nipost, err = nb.BuildNIPost(context.Background(), sig, challengeHash, &types.NIPostChallenge{PublishEpoch: 7}) + require.Nil(t, nipost) require.Error(t, err) @@ -518,6 +529,7 @@ func TestNIPostBuilder_BuildNIPost(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, mclock, + nil, WithPoetClients(poetProver), ) require.NoError(t, err) @@ -530,7 +542,8 @@ func TestNIPostBuilder_BuildNIPost(t *testing.T) { ) // check that proof ref is not called again - nipost, err = nb.BuildNIPost(context.Background(), sig, 7, challengeHash) + nipost, err = nb.BuildNIPost(context.Background(), sig, challengeHash, &types.NIPostChallenge{PublishEpoch: 7}) + require.NoError(t, err) require.NotNil(t, nipost) } @@ -595,12 +608,14 @@ func TestNIPostBuilder_ManyPoETs_SubmittingChallenge_DeadlineReached(t *testing. zaptest.NewLogger(t), poetCfg, mclock, + nil, WithPoetClients(poets...), ) require.NoError(t, err) // Act - nipost, err := nb.BuildNIPost(context.Background(), sig, postGenesisEpoch+2, challenge) + nipost, err := nb.BuildNIPost(context.Background(), sig, challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.NoError(t, err) // Verify @@ -652,12 +667,14 @@ func TestNIPostBuilder_ManyPoETs_AllFinished(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, mclock, + nil, WithPoetClients(poets...), ) require.NoError(t, err) // Act - nipost, err := nb.BuildNIPost(context.Background(), sig, postGenesisEpoch+2, challenge) + nipost, err := nb.BuildNIPost(context.Background(), sig, challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.NoError(t, err) // Verify @@ -691,11 +708,13 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { zaptest.NewLogger(t), poetCfg, mclock, + nil, WithPoetClients(poetProver), ) require.NoError(t, err) - nipst, err := nb.BuildNIPost(context.Background(), sig, postGenesisEpoch+2, challenge) + nipst, err := nb.BuildNIPost(context.Background(), sig, challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.ErrorIs(t, err, ErrPoetServiceUnstable) require.Nil(t, nipst) }) @@ -726,11 +745,13 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { zaptest.NewLogger(t), poetCfg, mclock, + nil, WithPoetClients(poetProver), ) require.NoError(t, err) - nipst, err := nb.BuildNIPost(context.Background(), sig, postGenesisEpoch+2, challenge) + nipst, err := nb.BuildNIPost(context.Background(), sig, challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.ErrorIs(t, err, ErrPoetServiceUnstable) require.Nil(t, nipst) }) @@ -748,11 +769,13 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { zaptest.NewLogger(t), poetCfg, mclock, + nil, WithPoetClients(poetProver), ) require.NoError(t, err) - nipst, err := nb.BuildNIPost(context.Background(), sig, postGenesisEpoch+2, challenge) + nipst, err := nb.BuildNIPost(context.Background(), sig, challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.ErrorIs(t, err, ErrPoetProofNotReceived) require.Nil(t, nipst) }) @@ -772,11 +795,13 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { zaptest.NewLogger(t), poetCfg, mclock, + nil, WithPoetClients(poetProver), ) require.NoError(t, err) - nipst, err := nb.BuildNIPost(context.Background(), sig, postGenesisEpoch+2, challenge) + nipst, err := nb.BuildNIPost(context.Background(), sig, challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.ErrorIs(t, err, ErrPoetProofNotReceived) require.Nil(t, nipst) }) @@ -812,11 +837,13 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, mclock, + nil, WithPoetClients(poetProver), ) require.NoError(t, err) - nipost, err := nb.BuildNIPost(context.Background(), sig, currLayer.GetEpoch(), types.RandomHash()) + nipost, err := nb.BuildNIPost(context.Background(), sig, types.RandomHash(), + &types.NIPostChallenge{PublishEpoch: currLayer.GetEpoch()}) require.ErrorIs(t, err, ErrATXChallengeExpired) require.ErrorContains(t, err, "poet round has already started") require.Nil(t, nipost) @@ -839,6 +866,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, mclock, + nil, WithPoetClients(poetProver), ) require.NoError(t, err) @@ -857,7 +885,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { }) require.NoError(t, err) - nipost, err := nb.BuildNIPost(context.Background(), sig, challenge.PublishEpoch, challengeHash) + nipost, err := nb.BuildNIPost(context.Background(), sig, challengeHash, challenge) require.ErrorIs(t, err, ErrATXChallengeExpired) require.ErrorContains(t, err, "poet proof for pub epoch") require.Nil(t, nipost) @@ -880,6 +908,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, mclock, + nil, WithPoetClients(poetProver), ) require.NoError(t, err) @@ -902,7 +931,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { err = nipost.UpdatePoetProofRef(db, sig.NodeID(), [32]byte{1, 2, 3}, &types.MerkleProof{}) require.NoError(t, err) - nipost, err := nb.BuildNIPost(context.Background(), sig, challenge.PublishEpoch, challengeHash) + nipost, err := nb.BuildNIPost(context.Background(), sig, challengeHash, challenge) require.ErrorIs(t, err, ErrATXChallengeExpired) require.ErrorContains(t, err, "deadline to publish ATX for pub epoch") require.Nil(t, nipost) @@ -959,12 +988,13 @@ func TestNIPoSTBuilder_Continues_After_Interrupted(t *testing.T) { zaptest.NewLogger(t), poetCfg, mclock, + nil, WithPoetClients(poet), ) require.NoError(t, err) // Act - nipost, err := nb.BuildNIPost(buildCtx, sig, postGenesisEpoch+2, challenge) + nipost, err := nb.BuildNIPost(buildCtx, sig, challenge, &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.ErrorIs(t, err, context.Canceled) require.Nil(t, nipost) @@ -973,7 +1003,8 @@ func TestNIPoSTBuilder_Continues_After_Interrupted(t *testing.T) { Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(&types.PoetRound{}, nil) - nipost, err = nb.BuildNIPost(context.Background(), sig, postGenesisEpoch+2, challenge) + nipost, err = nb.BuildNIPost(context.Background(), sig, challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.NoError(t, err) // Verify @@ -1093,11 +1124,13 @@ func TestNIPostBuilder_Mainnet_Poet_Workaround(t *testing.T) { zaptest.NewLogger(t), poetCfg, mclock, + nil, WithPoetClients(poets...), ) require.NoError(t, err) - nipost, err := nb.BuildNIPost(context.Background(), sig, tc.epoch, challenge) + nipost, err := nb.BuildNIPost(context.Background(), sig, challenge, + &types.NIPostChallenge{PublishEpoch: tc.epoch}) require.NoError(t, err) require.NotNil(t, nipost) }) @@ -1160,6 +1193,7 @@ func TestNIPostBuilder_Close(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, defaultLayerClockMock(ctrl), + nil, WithPoetClients(poet), ) require.NoError(t, err) @@ -1168,6 +1202,42 @@ func TestNIPostBuilder_Close(t *testing.T) { cancel() challenge := types.RandomHash() - _, err = nb.BuildNIPost(ctx, sig, postGenesisEpoch+2, challenge) + _, err = nb.BuildNIPost(ctx, sig, challenge, &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) require.Error(t, err) } + +func TestNIPostBuilderProof_WithBadInitialPost(t *testing.T) { + ctrl := gomock.NewController(t) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + poet := defaultPoetServiceMock(t, ctrl, "http://localhost:9999") + poet.EXPECT().Proof(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn( + func(ctx context.Context, _ string) (*types.PoetProofMessage, []types.Hash32, error) { + return nil, nil, ctx.Err() + }) + validator := NewMocknipostValidator(ctrl) + validator.EXPECT().PostV2(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(errors.New("some error")) + + postClient := NewMockPostClient(ctrl) + postClient.EXPECT().Info(gomock.Any()).Return(&types.PostInfo{}, nil) + postService := NewMockpostService(ctrl) + postService.EXPECT().Client(sig.NodeID()).Return(postClient, nil) + nb, err := NewNIPostBuilder( + localsql.InMemory(), + postService, + zaptest.NewLogger(t), + PoetConfig{}, + defaultLayerClockMock(ctrl), + validator, + WithPoetClients(poet), + ) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + challenge := types.RandomHash() + _, _, err = nb.Proof(ctx, sig.NodeID(), challenge[:], &types.NIPostChallenge{InitialPost: &types.Post{}}) + require.ErrorIs(t, err, ErrInvalidInitialPost) +} diff --git a/activation/post_states_test.go b/activation/post_states_test.go index b737f0fa43..28372afecc 100644 --- a/activation/post_states_test.go +++ b/activation/post_states_test.go @@ -53,6 +53,7 @@ func TestPostState_OnProof(t *testing.T) { zaptest.NewLogger(t), PoetConfig{}, nil, + nil, NipostbuilderWithPostStates(mpostStates), ) require.NoError(t, err) @@ -66,6 +67,6 @@ func TestPostState_OnProof(t *testing.T) { mpostStates.EXPECT().Set(id, types.PostStateIdle), ) - _, _, err = nb.Proof(context.Background(), id, []byte("abc")) + _, _, err = nb.Proof(context.Background(), id, []byte("abc"), nil) require.NoError(t, err) } diff --git a/node/node.go b/node/node.go index 4708f5d3da..96ac1aa7b6 100644 --- a/node/node.go +++ b/node/node.go @@ -1031,6 +1031,7 @@ func (app *App) initServices(ctx context.Context) error { nipostLogger, app.Config.POET, app.clock, + app.validator, activation.NipostbuilderWithPostStates(postStates), activation.WithPoetClients(poetClients...), ) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 718b020e0a..6a0b2bd07d 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -159,8 +159,9 @@ func TestPostMalfeasanceProof(t *testing.T) { localDb := localsql.InMemory() certClient := activation.NewCertifierClient(db, localDb, logger.Named("certifier")) certifier := activation.NewCertifier(localDb, logger, certClient) + poetDb := activation.NewPoetDb(db, zap.NewNop()) poetClient, err := activation.NewPoetClient( - activation.NewPoetDb(db, zap.NewNop()), + poetDb, types.PoetServer{ Address: cluster.MakePoetGlobalEndpoint(ctx.Namespace, 0), }, cfg.POET, @@ -169,12 +170,26 @@ func TestPostMalfeasanceProof(t *testing.T) { ) require.NoError(t, err) + verifyingOpts := activation.DefaultPostVerifyingOpts() + verifyingOpts.Workers = 1 + verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) + require.NoError(t, err) + + validator := activation.NewValidator( + db, + poetDb, + cfg.POST, + cfg.SMESHING.Opts.Scrypt, + verifier, + ) + nipostBuilder, err := activation.NewNIPostBuilder( localDb, grpcPostService, logger.Named("nipostBuilder"), cfg.POET, clock, + validator, activation.WithPoetClients(poetClient), ) require.NoError(t, err) @@ -216,19 +231,27 @@ func TestPostMalfeasanceProof(t *testing.T) { } break } + nipostChallenge := &types.NIPostChallenge{ + PublishEpoch: challenge.PublishEpoch, + PrevATXID: types.EmptyATXID, + PositioningATX: challenge.PositioningATXID, + CommitmentATX: challenge.CommitmentATXID, + InitialPost: &types.Post{ + Nonce: challenge.InitialPost.Nonce, + Indices: challenge.InitialPost.Indices, + Pow: challenge.InitialPost.Pow, + }, + } - nipost, err := nipostBuilder.BuildNIPost(ctx, signer, challenge.PublishEpoch, challenge.Hash()) + nipost, err := nipostBuilder.BuildNIPost(ctx, signer, challenge.Hash(), nipostChallenge) require.NoError(t, err) // 2.2 Create ATX with invalid POST for i := range nipost.Post.Indices { nipost.Post.Indices[i] += 1 } + // Sanity check that the POST is invalid - verifyingOpts := activation.DefaultPostVerifyingOpts() - verifyingOpts.Workers = 1 - verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) - require.NoError(t, err) err = verifier.Verify(ctx, (*shared.Proof)(nipost.Post), &shared.ProofMetadata{ NodeId: signer.NodeID().Bytes(), CommitmentAtxId: challenge.CommitmentATXID.Bytes(),