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(),