From 74d1639f7201cd1e927d2ef1f0114dcfc53f6d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 22 Dec 2023 16:15:35 +0100 Subject: [PATCH 01/30] Distributed verification - WIP - publishing malf proofs --- Makefile-libs.Inc | 2 +- activation/e2e/nipost_test.go | 4 +- activation/e2e/validation_test.go | 2 +- activation/handler.go | 29 +++++- activation/handler_test.go | 102 ++++++++++++++++++-- activation/interface.go | 5 + activation/mocks.go | 34 ++++--- activation/post_verifier.go | 9 +- activation/post_verifier_test.go | 2 +- activation/validation.go | 26 ++++- checkpoint/recovery_test.go | 2 +- common/types/malfeasance.go | 45 ++++++++- common/types/malfeasance_scale.go | 37 ++++++++ go.mod | 2 +- go.sum | 4 +- malfeasance/handler.go | 79 +++++++++++++--- malfeasance/handler_test.go | 151 ++++++++++++++++++++++++++++++ malfeasance/interface.go | 9 ++ malfeasance/metrics.go | 23 +++-- malfeasance/mocks.go | 69 ++++++++++++++ mesh/mesh_test.go | 1 + node/node.go | 2 + 22 files changed, 578 insertions(+), 61 deletions(-) diff --git a/Makefile-libs.Inc b/Makefile-libs.Inc index 62902dd876..215754d1a5 100644 --- a/Makefile-libs.Inc +++ b/Makefile-libs.Inc @@ -50,7 +50,7 @@ else endif endif -POSTRS_SETUP_REV = 0.6.1 +POSTRS_SETUP_REV = 0.7.0-rc2-distributed-verification POSTRS_SETUP_ZIP = libpost-$(platform)-v$(POSTRS_SETUP_REV).zip POSTRS_SETUP_URL_ZIP ?= https://github.com/spacemeshos/post-rs/releases/download/v$(POSTRS_SETUP_REV)/$(POSTRS_SETUP_ZIP) POSTRS_PROFILER_ZIP = profiler-$(platform)-v$(POSTRS_SETUP_REV).zip diff --git a/activation/e2e/nipost_test.go b/activation/e2e/nipost_test.go index 645f9cea2f..5c2f0fd47f 100644 --- a/activation/e2e/nipost_test.go +++ b/activation/e2e/nipost_test.go @@ -160,7 +160,7 @@ func TestNIPostBuilderWithClients(t *testing.T) { }, ) - verifier, err := activation.NewPostVerifier(cfg, logger.Named("verifier")) + verifier, err := activation.NewPostVerifier(nil, cfg, logger.Named("verifier")) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, verifier.Close()) }) @@ -329,7 +329,7 @@ func TestNewNIPostBuilderNotInitialized(t *testing.T) { require.NoError(t, err) require.NotNil(t, nipost) - verifier, err := activation.NewPostVerifier(cfg, logger.Named("verifier")) + verifier, err := activation.NewPostVerifier(nil, cfg, logger.Named("verifier")) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, verifier.Close()) }) diff --git a/activation/e2e/validation_test.go b/activation/e2e/validation_test.go index 4ba2ed8bf7..f7c5b55c80 100644 --- a/activation/e2e/validation_test.go +++ b/activation/e2e/validation_test.go @@ -67,7 +67,7 @@ func TestValidator_Validate(t *testing.T) { }, ) - verifier, err := activation.NewPostVerifier(cfg, logger.Named("verifier")) + verifier, err := activation.NewPostVerifier(nil, cfg, logger.Named("verifier")) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, verifier.Close()) }) diff --git a/activation/handler.go b/activation/handler.go index 9fd4222778..1b536b65c9 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -8,6 +8,7 @@ import ( "time" "github.com/spacemeshos/post/shared" + "github.com/spacemeshos/post/verifying" "golang.org/x/exp/maps" "github.com/spacemeshos/go-spacemesh/atxsdata" @@ -169,7 +170,7 @@ func (h *Handler) SyntacticallyValidate(ctx context.Context, atx *types.Activati return fmt.Errorf("invalid vrf nonce: %w", err) } if err := h.nipostValidator.Post( - ctx, atx.SmesherID, *atx.CommitmentATX, atx.InitialPost, &initialPostMetadata, atx.NumUnits, + ctx, atx.SmesherID, *atx.CommitmentATX, atx.InitialPost, &initialPostMetadata, atx.NumUnits, FullPost(), ); err != nil { return fmt.Errorf("invalid initial post: %w", err) } @@ -233,6 +234,32 @@ func (h *Handler) SyntacticallyValidateDeps( expectedChallengeHash, atx.NumUnits, ) + var invalidIdx *verifying.ErrInvalidIndex + if errors.As(err, &invalidIdx) { + h.log.WithContext(ctx).With().Info("ATX with invalid post index", atx.ID(), log.Int("index", invalidIdx.Index)) + gossip := types.MalfeasanceGossip{ + MalfeasanceProof: types.MalfeasanceProof{ + Layer: atx.PublishEpoch.FirstLayer(), + Proof: types.Proof{ + Type: types.InvalidPostIndex, + Data: &types.InvalidPostIndexProof{ + Atx: *atx, + InvalidIdx: uint32(invalidIdx.Index), + }, + }, + }, + } + encodedProof := codec.MustEncode(&gossip.MalfeasanceProof) + if err := identities.SetMalicious(h.cdb, atx.SmesherID, encodedProof, time.Now()); err != nil { + return nil, fmt.Errorf("adding malfeasance proof: %w", err) + } + if err := h.publisher.Publish(ctx, pubsub.MalfeasanceProof, codec.MustEncode(&gossip)); err != nil { + h.log.With().Error("failed to broadcast malfeasance proof", log.Err(err)) + } + h.cdb.CacheMalfeasanceProof(atx.SmesherID, &gossip.MalfeasanceProof) + h.tortoise.OnMalfeasance(atx.SmesherID) + return nil, errors.Join(errMaliciousATX, err) + } if err != nil { return nil, fmt.Errorf("invalid nipost: %w", err) } diff --git a/activation/handler_test.go b/activation/handler_test.go index 2fbe20cd56..367394e8bb 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -12,6 +12,7 @@ import ( "github.com/spacemeshos/merkle-tree" poetShared "github.com/spacemeshos/poet/shared" + "github.com/spacemeshos/post/verifying" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -503,7 +504,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { atxHdlr.mclock.EXPECT().CurrentLayer().Return(currentLayer) atxHdlr.mValidator.EXPECT(). - Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(nil) atxHdlr.mValidator.EXPECT(). VRFNonce(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). @@ -591,7 +592,8 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { atx.SmesherID = sig.NodeID() atxHdlr.mclock.EXPECT().CurrentLayer().Return(currentLayer) - atxHdlr.mValidator.EXPECT().Post(gomock.Any(), sig.NodeID(), cATX, atx.InitialPost, gomock.Any(), atx.NumUnits) + atxHdlr.mValidator.EXPECT(). + Post(gomock.Any(), sig.NodeID(), cATX, atx.InitialPost, gomock.Any(), atx.NumUnits, gomock.Any()) atxHdlr.mValidator.EXPECT().VRFNonce(sig.NodeID(), cATX, &vrfNonce, gomock.Any(), atx.NumUnits) require.NoError(t, atxHdlr.SyntacticallyValidate(context.Background(), atx)) atxHdlr.mValidator.EXPECT(). @@ -699,7 +701,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { atxHdlr.mclock.EXPECT().CurrentLayer().Return(currentLayer) atxHdlr.mValidator.EXPECT().VRFNonce(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) atxHdlr.mValidator.EXPECT(). - Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(errors.New("failed post validation")) err := atxHdlr.SyntacticallyValidate(context.Background(), atx) require.ErrorContains(t, err, "failed post validation") @@ -984,6 +986,7 @@ func TestHandler_ProcessAtx(t *testing.T) { atxHdlr.log, atxHdlr.cdb, atxHdlr.edVerifier, + NewMockPostVerifier(gomock.NewController(t)), &got, ) require.NoError(t, err) @@ -999,6 +1002,80 @@ func TestHandler_ProcessAtx(t *testing.T) { require.Equal(t, atx2.PublishEpoch.FirstLayer(), got.MalfeasanceProof.Layer) } +func TestHandler_PublishesPostMalfeasanceProofs(t *testing.T) { + goldenATXID := types.ATXID{2, 3, 4} + atxHdlr := newTestHandler(t, goldenATXID) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + nodeID := sig.NodeID() + + proof, err := identities.GetMalfeasanceProof(atxHdlr.cdb, nodeID) + require.ErrorIs(t, err, sql.ErrNotFound) + require.Nil(t, proof) + + ch := newChallenge(0, types.EmptyATXID, goldenATXID, 1, &goldenATXID) + ch.InitialPost = &types.Post{} + nipost := newNIPostWithChallenge(t, types.HexToHash32("0x3333"), []byte{0x76, 0x45}) + + atx := newAtx(t, sig, ch, nipost, 100, types.GenerateAddress([]byte("aaaa"))) + atx.NodeID = &nodeID + vrfNonce := types.VRFPostIndex(0) + atx.VRFNonce = &vrfNonce + atx.SetEffectiveNumUnits(100) + atx.SetReceived(time.Now()) + require.NoError(t, SignAndFinalizeAtx(sig, atx)) + _, err = atx.Verify(0, 100) + require.NoError(t, err) + + var got types.MalfeasanceGossip + atxHdlr.mclock.EXPECT().CurrentLayer().Return(atx.PublishEpoch.FirstLayer()) + atxHdlr.mValidator.EXPECT().VRFNonce(gomock.Any(), goldenATXID, gomock.Any(), gomock.Any(), gomock.Any()) + atxHdlr.mValidator.EXPECT().Post( + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ) + atxHdlr.mockFetch.EXPECT().RegisterPeerHashes(gomock.Any(), gomock.Any()) + atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), atx.GetPoetProofRef()) + atxHdlr.mValidator.EXPECT().InitialNIPostChallenge(&atx.NIPostChallenge, gomock.Any(), goldenATXID) + atxHdlr.mValidator.EXPECT().PositioningAtx(goldenATXID, gomock.Any(), goldenATXID, atx.PublishEpoch) + atxHdlr.mValidator.EXPECT().NIPost(gomock.Any(), gomock.Any(), goldenATXID, atx.NIPost, gomock.Any(), atx.NumUnits). + Return(0, &verifying.ErrInvalidIndex{Index: 2}) + atxHdlr.mtortoise.EXPECT().OnMalfeasance(gomock.Any()) + atxHdlr.mpub.EXPECT().Publish(gomock.Any(), pubsub.MalfeasanceProof, gomock.Any()).DoAndReturn( + func(_ context.Context, _ string, data []byte) error { + require.NoError(t, codec.Decode(data, &got)) + postVerifier := NewMockPostVerifier(gomock.NewController(t)) + postVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("invalid")) + nodeID, err := malfeasance.Validate( + context.Background(), + atxHdlr.log, + atxHdlr.cdb, + atxHdlr.edVerifier, + postVerifier, + &got, + ) + require.NoError(t, err) + require.Equal(t, sig.NodeID(), nodeID) + return nil + }) + + err = atxHdlr.handleAtx(context.Background(), types.Hash32{}, p2p.NoPeer, codec.MustEncode(atx)) + require.ErrorIs(t, err, errMaliciousATX) + + proof, err = identities.GetMalfeasanceProof(atxHdlr.cdb, atx.SmesherID) + require.NoError(t, err) + require.NotNil(t, proof.Received()) + proof.SetReceived(time.Time{}) + require.Equal(t, got.MalfeasanceProof, *proof) + require.Equal(t, atx.PublishEpoch.FirstLayer(), got.MalfeasanceProof.Layer) +} + func TestHandler_ProcessAtxStoresNewVRFNonce(t *testing.T) { // Arrange goldenATXID := types.ATXID{2, 3, 4} @@ -1190,7 +1267,7 @@ func TestHandler_HandleGossipAtx(t *testing.T) { require.NoError(t, err) atxHdlr.mclock.EXPECT().CurrentLayer().Return(first.PublishEpoch.FirstLayer()) atxHdlr.mValidator.EXPECT(). - Post(gomock.Any(), nodeID1, goldenATXID, first.InitialPost, gomock.Any(), first.NumUnits) + Post(gomock.Any(), nodeID1, goldenATXID, first.InitialPost, gomock.Any(), first.NumUnits, gomock.Any()) atxHdlr.mValidator.EXPECT().VRFNonce(nodeID1, goldenATXID, &vrfNonce, gomock.Any(), first.NumUnits) atxHdlr.mockFetch.EXPECT().RegisterPeerHashes(gomock.Any(), gomock.Any()) atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), first.GetPoetProofRef()) @@ -1256,8 +1333,17 @@ func TestHandler_HandleParallelGossipAtx(t *testing.T) { atx.InitialPost, gomock.Any(), atx.NumUnits, + gomock.Any(), ).DoAndReturn( - func(_ context.Context, _ types.NodeID, _ types.ATXID, _ *types.Post, _ *types.PostMetadata, _ uint32) error { + func( + _ context.Context, + _ types.NodeID, + _ types.ATXID, + _ *types.Post, + _ *types.PostMetadata, + _ uint32, + _ ...validatorOption, + ) error { time.Sleep(100 * time.Millisecond) return nil }, @@ -1641,7 +1727,8 @@ func TestHandler_AtxWeight(t *testing.T) { atxHdlr.mockFetch.EXPECT().RegisterPeerHashes(peer, []types.Hash32{proofRef}) atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), proofRef) atxHdlr.mValidator.EXPECT().VRFNonce(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - atxHdlr.mValidator.EXPECT().Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + atxHdlr.mValidator.EXPECT(). + Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(leaves, nil) @@ -1741,7 +1828,8 @@ func TestHandler_WrongHash(t *testing.T) { atxHdlr.mockFetch.EXPECT().RegisterPeerHashes(peer, []types.Hash32{proofRef}) atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), proofRef) atxHdlr.mValidator.EXPECT().VRFNonce(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - atxHdlr.mValidator.EXPECT().Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + atxHdlr.mValidator.EXPECT(). + Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(111), nil) diff --git a/activation/interface.go b/activation/interface.go index 31be6c84df..f0ac8e5c75 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -26,6 +26,9 @@ type scaler interface { scale(int) } +// validatorOption is a functional option type for the validator. +type validatorOption func(*validatorOptions) + type nipostValidator interface { InitialNIPostChallenge(challenge *types.NIPostChallenge, atxs atxProvider, goldenATXID types.ATXID) error NIPostChallenge(challenge *types.NIPostChallenge, atxs atxProvider, nodeID types.NodeID) error @@ -36,6 +39,7 @@ type nipostValidator interface { NIPost *types.NIPost, expectedChallenge types.Hash32, numUnits uint32, + opts ...validatorOption, ) (uint64, error) NumUnits(cfg *PostConfig, numUnits uint32) error @@ -46,6 +50,7 @@ type nipostValidator interface { Post *types.Post, PostMetadata *types.PostMetadata, numUnits uint32, + opts ...validatorOption, ) error PostMetadata(cfg *PostConfig, metadata *types.PostMetadata) error diff --git a/activation/mocks.go b/activation/mocks.go index dd0987aaf8..4cb2e9c0dd 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -303,18 +303,23 @@ func (c *nipostValidatorInitialNIPostChallengeCall) DoAndReturn(f func(*types.NI } // NIPost mocks base method. -func (m *MocknipostValidator) NIPost(ctx context.Context, nodeId types.NodeID, atxId types.ATXID, NIPost *types.NIPost, expectedChallenge types.Hash32, numUnits uint32) (uint64, error) { +func (m *MocknipostValidator) NIPost(ctx context.Context, nodeId types.NodeID, atxId types.ATXID, NIPost *types.NIPost, expectedChallenge types.Hash32, numUnits uint32, opts ...validatorOption) (uint64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NIPost", ctx, nodeId, atxId, NIPost, expectedChallenge, numUnits) + varargs := []any{ctx, nodeId, atxId, NIPost, expectedChallenge, numUnits} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "NIPost", varargs...) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(error) return ret0, ret1 } // NIPost indicates an expected call of NIPost. -func (mr *MocknipostValidatorMockRecorder) NIPost(ctx, nodeId, atxId, NIPost, expectedChallenge, numUnits any) *nipostValidatorNIPostCall { +func (mr *MocknipostValidatorMockRecorder) NIPost(ctx, nodeId, atxId, NIPost, expectedChallenge, numUnits any, opts ...any) *nipostValidatorNIPostCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NIPost", reflect.TypeOf((*MocknipostValidator)(nil).NIPost), ctx, nodeId, atxId, NIPost, expectedChallenge, numUnits) + varargs := append([]any{ctx, nodeId, atxId, NIPost, expectedChallenge, numUnits}, opts...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NIPost", reflect.TypeOf((*MocknipostValidator)(nil).NIPost), varargs...) return &nipostValidatorNIPostCall{Call: call} } @@ -330,13 +335,13 @@ func (c *nipostValidatorNIPostCall) Return(arg0 uint64, arg1 error) *nipostValid } // Do rewrite *gomock.Call.Do -func (c *nipostValidatorNIPostCall) Do(f func(context.Context, types.NodeID, types.ATXID, *types.NIPost, types.Hash32, uint32) (uint64, error)) *nipostValidatorNIPostCall { +func (c *nipostValidatorNIPostCall) Do(f func(context.Context, types.NodeID, types.ATXID, *types.NIPost, types.Hash32, uint32, ...validatorOption) (uint64, error)) *nipostValidatorNIPostCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *nipostValidatorNIPostCall) DoAndReturn(f func(context.Context, types.NodeID, types.ATXID, *types.NIPost, types.Hash32, uint32) (uint64, error)) *nipostValidatorNIPostCall { +func (c *nipostValidatorNIPostCall) DoAndReturn(f func(context.Context, types.NodeID, types.ATXID, *types.NIPost, types.Hash32, uint32, ...validatorOption) (uint64, error)) *nipostValidatorNIPostCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -456,17 +461,22 @@ func (c *nipostValidatorPositioningAtxCall) DoAndReturn(f func(types.ATXID, atxP } // Post mocks base method. -func (m *MocknipostValidator) Post(ctx context.Context, nodeId types.NodeID, atxId types.ATXID, Post *types.Post, PostMetadata *types.PostMetadata, numUnits uint32) error { +func (m *MocknipostValidator) Post(ctx context.Context, nodeId types.NodeID, atxId types.ATXID, Post *types.Post, PostMetadata *types.PostMetadata, numUnits uint32, opts ...validatorOption) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Post", ctx, nodeId, atxId, Post, PostMetadata, numUnits) + varargs := []any{ctx, nodeId, atxId, Post, PostMetadata, numUnits} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Post", varargs...) ret0, _ := ret[0].(error) return ret0 } // Post indicates an expected call of Post. -func (mr *MocknipostValidatorMockRecorder) Post(ctx, nodeId, atxId, Post, PostMetadata, numUnits any) *nipostValidatorPostCall { +func (mr *MocknipostValidatorMockRecorder) Post(ctx, nodeId, atxId, Post, PostMetadata, numUnits any, opts ...any) *nipostValidatorPostCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MocknipostValidator)(nil).Post), ctx, nodeId, atxId, Post, PostMetadata, numUnits) + varargs := append([]any{ctx, nodeId, atxId, Post, PostMetadata, numUnits}, opts...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MocknipostValidator)(nil).Post), varargs...) return &nipostValidatorPostCall{Call: call} } @@ -482,13 +492,13 @@ func (c *nipostValidatorPostCall) Return(arg0 error) *nipostValidatorPostCall { } // Do rewrite *gomock.Call.Do -func (c *nipostValidatorPostCall) Do(f func(context.Context, types.NodeID, types.ATXID, *types.Post, *types.PostMetadata, uint32) error) *nipostValidatorPostCall { +func (c *nipostValidatorPostCall) Do(f func(context.Context, types.NodeID, types.ATXID, *types.Post, *types.PostMetadata, uint32, ...validatorOption) error) *nipostValidatorPostCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *nipostValidatorPostCall) DoAndReturn(f func(context.Context, types.NodeID, types.ATXID, *types.Post, *types.PostMetadata, uint32) error) *nipostValidatorPostCall { +func (c *nipostValidatorPostCall) DoAndReturn(f func(context.Context, types.NodeID, types.ATXID, *types.Post, *types.PostMetadata, uint32, ...validatorOption) error) *nipostValidatorPostCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/activation/post_verifier.go b/activation/post_verifier.go index 25120b9b97..8ddd1326b1 100644 --- a/activation/post_verifier.go +++ b/activation/post_verifier.go @@ -94,8 +94,13 @@ func (v *postVerifier) Verify( } // NewPostVerifier creates a new post verifier. -func NewPostVerifier(cfg PostConfig, logger *zap.Logger, opts ...verifying.OptionFunc) (PostVerifier, error) { - verifier, err := verifying.NewProofVerifier(opts...) +func NewPostVerifier( + id []byte, + cfg PostConfig, + logger *zap.Logger, + opts ...verifying.OptionFunc, +) (PostVerifier, error) { + verifier, err := verifying.NewProofVerifier(id, opts...) if err != nil { return nil, err } diff --git a/activation/post_verifier_test.go b/activation/post_verifier_test.go index d1d3cde9de..9f30fa000e 100644 --- a/activation/post_verifier_test.go +++ b/activation/post_verifier_test.go @@ -34,7 +34,7 @@ func TestOffloadingPostVerifier(t *testing.T) { } func TestPostVerifierDetectsInvalidProof(t *testing.T) { - verifier, err := activation.NewPostVerifier(activation.PostConfig{}, zaptest.NewLogger(t)) + verifier, err := activation.NewPostVerifier(nil, activation.PostConfig{}, zaptest.NewLogger(t)) require.NoError(t, err) defer verifier.Close() require.Error(t, verifier.Verify(context.Background(), &shared.Proof{}, &shared.ProofMetadata{})) diff --git a/activation/validation.go b/activation/validation.go index 3ce6965faf..b8738cd367 100644 --- a/activation/validation.go +++ b/activation/validation.go @@ -36,6 +36,17 @@ func (e *ErrAtxNotFound) Is(target error) bool { return false } +type validatorOptions struct { + fullPost bool +} + +// FullPost is a validator option that configures the validator to validate all indices in POST. +func FullPost() validatorOption { + return func(o *validatorOptions) { + o.fullPost = true + } +} + // Validator contains the dependencies required to validate NIPosts. type Validator struct { poetDb poetDbAPI @@ -61,6 +72,7 @@ func (v *Validator) NIPost( nipost *types.NIPost, expectedChallenge types.Hash32, numUnits uint32, + opts ...validatorOption, ) (uint64, error) { if err := v.NumUnits(&v.cfg, numUnits); err != nil { return 0, err @@ -70,7 +82,7 @@ func (v *Validator) NIPost( return 0, err } - if err := v.Post(ctx, nodeId, commitmentAtxId, nipost.Post, nipost.PostMetadata, numUnits); err != nil { + if err := v.Post(ctx, nodeId, commitmentAtxId, nipost.Post, nipost.PostMetadata, numUnits, opts...); err != nil { return 0, fmt.Errorf("invalid Post: %w", err) } @@ -127,6 +139,7 @@ func (v *Validator) Post( PoST *types.Post, PostMetadata *types.PostMetadata, numUnits uint32, + opts ...validatorOption, ) error { p := (*shared.Proof)(PoST) @@ -138,8 +151,17 @@ func (v *Validator) Post( LabelsPerUnit: PostMetadata.LabelsPerUnit, } + options := &validatorOptions{} + for _, opt := range opts { + opt(options) + } + verifyOpts := []verifying.OptionFunc{verifying.WithLabelScryptParams(v.scrypt)} + if options.fullPost { + verifyOpts = append(verifyOpts, verifying.AllIndices()) + } + start := time.Now() - if err := v.postVerifier.Verify(ctx, p, m, verifying.WithLabelScryptParams(v.scrypt)); err != nil { + if err := v.postVerifier.Verify(ctx, p, m, verifyOpts...); err != nil { return fmt.Errorf("verify PoST: %w", err) } metrics.PostVerificationLatency.Observe(time.Since(start).Seconds()) diff --git a/checkpoint/recovery_test.go b/checkpoint/recovery_test.go index dff028f019..d7d1d35663 100644 --- a/checkpoint/recovery_test.go +++ b/checkpoint/recovery_test.go @@ -266,7 +266,7 @@ func validateAndPreserveData( InitialNIPostChallenge(&vatx.ActivationTx.NIPostChallenge, gomock.Any(), goldenAtx). AnyTimes() mvalidator.EXPECT(). - Post(gomock.Any(), vatx.SmesherID, *vatx.CommitmentATX, vatx.InitialPost, gomock.Any(), vatx.NumUnits) + Post(gomock.Any(), vatx.SmesherID, *vatx.CommitmentATX, vatx.InitialPost, gomock.Any(), vatx.NumUnits, gomock.Any()) mvalidator.EXPECT(). VRFNonce(vatx.SmesherID, *vatx.CommitmentATX, vatx.VRFNonce, gomock.Any(), vatx.NumUnits) } else { diff --git a/common/types/malfeasance.go b/common/types/malfeasance.go index c31b8abe3c..33bdc843bb 100644 --- a/common/types/malfeasance.go +++ b/common/types/malfeasance.go @@ -13,12 +13,13 @@ import ( "github.com/spacemeshos/go-spacemesh/log" ) -//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata +//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata,InvalidPostIndexProof const ( MultipleATXs byte = iota + 1 MultipleBallots HareEquivocation + InvalidPostIndex ) type MalfeasanceProof struct { @@ -64,6 +65,15 @@ func (mp *MalfeasanceProof) MarshalLogObject(encoder log.ObjectEncoder) error { } else { encoder.AddObject("msgs", p) } + case InvalidPostIndex: + encoder.AddString("type", "invalid post index") + p, ok := mp.Proof.Data.(*InvalidPostIndexProof) + if ok { + p.Atx.Initialize() + encoder.AddString("atx_id", p.Atx.ID().String()) + encoder.AddString("smesher", p.Atx.SmesherID.String()) + encoder.AddUint32("invalid index", p.InvalidIdx) + } default: encoder.AddString("type", "unknown") } @@ -72,9 +82,9 @@ func (mp *MalfeasanceProof) MarshalLogObject(encoder log.ObjectEncoder) error { } type Proof struct { - // MultipleATXs | MultipleBallots | HareEquivocation + // MultipleATXs | MultipleBallots | HareEquivocation | InvalidPostIndex Type uint8 - // AtxProof | BallotProof | HareProof + // AtxProof | BallotProof | HareProof | InvalidPostIndexProof Data scale.Type } @@ -133,8 +143,16 @@ func (e *Proof) DecodeScale(dec *scale.Decoder) (int, error) { } e.Data = &proof total += n + case InvalidPostIndex: + var proof InvalidPostIndexProof + n, err := proof.DecodeScale(dec) + if err != nil { + return total, err + } + e.Data = &proof + total += n default: - return total, errors.New("unknown malfeasance type") + return total, errors.New("invalid ballot malfeasance proof") } return total, nil } @@ -208,6 +226,13 @@ func (m *AtxProofMsg) SignedBytes() []byte { return data } +type InvalidPostIndexProof struct { + Atx ActivationTx + + // Which index in POST is invalid + InvalidIdx uint32 +} + type BallotProofMsg struct { InnerMsg BallotMetadata @@ -329,6 +354,18 @@ func MalfeasanceInfo(smesher NodeID, mp *MalfeasanceProof) string { fmt.Sprintf("2nd message signature: %s\n", hex.EncodeToString(p.Messages[1].Signature.Bytes())), ) } + case InvalidPostIndex: + p, ok := mp.Proof.Data.(*InvalidPostIndexProof) + if ok { + p.Atx.Initialize() + b.WriteString( + fmt.Sprintf( + "cause: smesher published ATX %s with invalid post index %d in epoch %d\n", + p.Atx.ID().ShortString(), + p.InvalidIdx, + p.Atx.PublishEpoch, + )) + } } return b.String() } diff --git a/common/types/malfeasance_scale.go b/common/types/malfeasance_scale.go index 1df278acf5..6f337dbb82 100644 --- a/common/types/malfeasance_scale.go +++ b/common/types/malfeasance_scale.go @@ -348,3 +348,40 @@ func (t *HareMetadata) DecodeScale(dec *scale.Decoder) (total int, err error) { } return total, nil } + +func (t *InvalidPostIndexProof) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := t.Atx.EncodeScale(enc) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeCompact32(enc, uint32(t.InvalidIdx)) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *InvalidPostIndexProof) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := t.Atx.DecodeScale(dec) + if err != nil { + return total, err + } + total += n + } + { + field, n, err := scale.DecodeCompact32(dec) + if err != nil { + return total, err + } + total += n + t.InvalidIdx = uint32(field) + } + return total, nil +} diff --git a/go.mod b/go.mod index cc5fd5e4c3..1152bc9259 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/spacemeshos/go-scale v1.1.12 github.com/spacemeshos/merkle-tree v0.2.3 github.com/spacemeshos/poet v0.9.7 - github.com/spacemeshos/post v0.10.2 + github.com/spacemeshos/post v0.10.3-0.20231222143012-df4c17a08074 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index bd0f441873..450e1ec5ea 100644 --- a/go.sum +++ b/go.sum @@ -633,8 +633,8 @@ github.com/spacemeshos/merkle-tree v0.2.3 h1:zGEgOR9nxAzJr0EWjD39QFngwFEOxfxMloE github.com/spacemeshos/merkle-tree v0.2.3/go.mod h1:VomOcQ5pCBXz7goiWMP5hReyqOfDXGSKbrH2GB9Htww= github.com/spacemeshos/poet v0.9.7 h1:FmKhgUKj//8Tzn8czWSIrn6+FVUFZbvLh8zqLfB8dfE= github.com/spacemeshos/poet v0.9.7/go.mod h1:wGCdhs2jnfQ52Amcmygv9uEEwYpdHAPjbiPg0Uf6cNQ= -github.com/spacemeshos/post v0.10.2 h1:FjRbrceno8xjBZi+53a+Xjoy81XormxDBvN0mN+0wxg= -github.com/spacemeshos/post v0.10.2/go.mod h1:lSZOkvCna1UuXgdGrvc+2SPMM+f+IUi9whZpBH7wUbM= +github.com/spacemeshos/post v0.10.3-0.20231222143012-df4c17a08074 h1:xE0mPB1S3VfDRvaslt61QsBiWMeGOS1W6mN9gnvAYvo= +github.com/spacemeshos/post v0.10.3-0.20231222143012-df4c17a08074/go.mod h1:lSZOkvCna1UuXgdGrvc+2SPMM+f+IUi9whZpBH7wUbM= github.com/spacemeshos/sha256-simd v0.1.0 h1:G7Mfu5RYdQiuE+wu4ZyJ7I0TI74uqLhFnKblEnSpjYI= github.com/spacemeshos/sha256-simd v0.1.0/go.mod h1:O8CClVIilId7RtuCMV2+YzMj6qjVn75JsxOxaE8vcfM= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= diff --git a/malfeasance/handler.go b/malfeasance/handler.go index 2f3cc0d103..71b3d63126 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -6,6 +6,8 @@ import ( "fmt" "time" + "github.com/spacemeshos/post/shared" + "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" @@ -27,12 +29,13 @@ var ( // Handler processes MalfeasanceProof from gossip and, if deems it valid, propagates it to peers. type Handler struct { - logger log.Log - cdb *datastore.CachedDB - self p2p.Peer - nodeID types.NodeID - edVerifier SigVerifier - tortoise tortoise + logger log.Log + cdb *datastore.CachedDB + self p2p.Peer + nodeID types.NodeID + edVerifier SigVerifier + tortoise tortoise + postVerifier postVerifier } func NewHandler( @@ -42,14 +45,16 @@ func NewHandler( nodeID types.NodeID, edVerifier SigVerifier, tortoise tortoise, + postVerifier postVerifier, ) *Handler { return &Handler{ - logger: lg, - cdb: cdb, - self: self, - nodeID: nodeID, - edVerifier: edVerifier, - tortoise: tortoise, + logger: lg, + cdb: cdb, + self: self, + nodeID: nodeID, + edVerifier: edVerifier, + tortoise: tortoise, + postVerifier: postVerifier, } } @@ -95,7 +100,7 @@ func (h *Handler) HandleMalfeasanceProof(ctx context.Context, peer p2p.Peer, dat return errMalformedData } if peer == h.self { - id, err := Validate(ctx, h.logger, h.cdb, h.edVerifier, &p) + id, err := Validate(ctx, h.logger, h.cdb, h.edVerifier, h.postVerifier, &p) if err != nil { return err } @@ -116,7 +121,7 @@ func (h *Handler) validateAndSave(ctx context.Context, p *types.MalfeasanceGossi pubsub.ErrValidationReject, ) } - nodeID, err := Validate(ctx, h.logger, h.cdb, h.edVerifier, p) + nodeID, err := Validate(ctx, h.logger, h.cdb, h.edVerifier, h.postVerifier, p) if err != nil { return types.EmptyNodeID, err } @@ -161,6 +166,7 @@ func Validate( logger log.Log, cdb *datastore.CachedDB, edVerifier SigVerifier, + postVerifier postVerifier, p *types.MalfeasanceGossip, ) (types.NodeID, error) { var ( @@ -174,6 +180,9 @@ func Validate( nodeID, err = validateMultipleATXs(ctx, logger, cdb, edVerifier, &p.MalfeasanceProof) case types.MultipleBallots: nodeID, err = validateMultipleBallots(ctx, logger, cdb, edVerifier, &p.MalfeasanceProof) + case types.InvalidPostIndex: + proof := p.MalfeasanceProof.Proof.Data.(*types.InvalidPostIndexProof) // guaranteed to work by scale func + nodeID, err = validateInvalidPostIndex(ctx, logger, cdb, edVerifier, postVerifier, proof) default: return nodeID, errors.New("unknown malfeasance type") } @@ -198,6 +207,8 @@ func updateMetrics(tp types.Proof) { numProofsATX.Inc() case types.MultipleBallots: numProofsBallot.Inc() + case types.InvalidPostIndex: + numProofsPostIndex.Inc() } } @@ -363,3 +374,43 @@ func validateMultipleBallots( numInvalidProofsBallot.Inc() return types.EmptyNodeID, errors.New("invalid ballot malfeasance proof") } + +func validateInvalidPostIndex(ctx context.Context, + logger log.Log, + db sql.Executor, + edVerifier SigVerifier, + postVerifier postVerifier, + proof *types.InvalidPostIndexProof, +) (types.NodeID, error) { + atx := &proof.Atx + if !edVerifier.Verify(signing.ATX, atx.SmesherID, atx.SignedBytes(), atx.Signature) { + return types.EmptyNodeID, errors.New("invalid signature") + } + commitmentAtx := atx.CommitmentATX + if commitmentAtx == nil { + atx, err := atxs.CommitmentATX(db, atx.SmesherID) + if err != nil { + return types.EmptyNodeID, fmt.Errorf("getting commitment ATX: %w", err) + } + commitmentAtx = &atx + } + post := (*shared.Proof)(atx.NIPost.Post) + meta := &shared.ProofMetadata{ + NodeId: atx.SmesherID.Bytes(), + CommitmentAtxId: commitmentAtx.Bytes(), + NumUnits: atx.NumUnits, + Challenge: atx.NIPost.PostMetadata.Challenge, + LabelsPerUnit: atx.NIPost.PostMetadata.LabelsPerUnit, + } + if err := postVerifier.Verify( + ctx, + post, + meta, + // TODO coming soon + // verifying.SelectedIndex(int(proof.InvalidIdx)), + ); err != nil { + return atx.SmesherID, nil + } + numInvalidProofsPostIndex.Inc() + return types.EmptyNodeID, errors.New("invalid post index malfeasance proof - POST is valid") +} diff --git a/malfeasance/handler_test.go b/malfeasance/handler_test.go index 2182eff52a..392306fd4e 100644 --- a/malfeasance/handler_test.go +++ b/malfeasance/handler_test.go @@ -2,6 +2,7 @@ package malfeasance_test import ( "context" + "errors" "os" "testing" "time" @@ -49,6 +50,7 @@ func TestHandler_HandleMalfeasanceProof_multipleATXs(t *testing.T) { ctrl := gomock.NewController(t) trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -57,6 +59,7 @@ func TestHandler_HandleMalfeasanceProof_multipleATXs(t *testing.T) { types.EmptyNodeID, signing.NewEdVerifier(), trt, + postVerifier, ) sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -261,6 +264,7 @@ func TestHandler_HandleMalfeasanceProof_multipleBallots(t *testing.T) { lg := logtest.New(t) ctrl := gomock.NewController(t) trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -269,6 +273,7 @@ func TestHandler_HandleMalfeasanceProof_multipleBallots(t *testing.T) { types.EmptyNodeID, signing.NewEdVerifier(), trt, + postVerifier, ) sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -480,6 +485,7 @@ func TestHandler_HandleMalfeasanceProof_hareEquivocation(t *testing.T) { lg := logtest.New(t) ctrl := gomock.NewController(t) trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -488,6 +494,7 @@ func TestHandler_HandleMalfeasanceProof_hareEquivocation(t *testing.T) { types.EmptyNodeID, signing.NewEdVerifier(), trt, + postVerifier, ) sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -714,6 +721,7 @@ func TestHandler_HandleMalfeasanceProof_validateHare(t *testing.T) { lg := logtest.New(t) ctrl := gomock.NewController(t) trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -722,6 +730,7 @@ func TestHandler_HandleMalfeasanceProof_validateHare(t *testing.T) { types.EmptyNodeID, signing.NewEdVerifier(), trt, + postVerifier, ) sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -774,6 +783,7 @@ func TestHandler_CrossDomain(t *testing.T) { lg := logtest.New(t) ctrl := gomock.NewController(t) trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -782,6 +792,7 @@ func TestHandler_CrossDomain(t *testing.T) { types.EmptyNodeID, signing.NewEdVerifier(), trt, + postVerifier, ) sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -836,6 +847,7 @@ func TestHandler_HandleSyncedMalfeasanceProof_multipleATXs(t *testing.T) { lg := logtest.New(t) ctrl := gomock.NewController(t) trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -844,6 +856,7 @@ func TestHandler_HandleSyncedMalfeasanceProof_multipleATXs(t *testing.T) { types.EmptyNodeID, signing.NewEdVerifier(), trt, + postVerifier, ) sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -897,6 +910,7 @@ func TestHandler_HandleSyncedMalfeasanceProof_multipleBallots(t *testing.T) { lg := logtest.New(t) ctrl := gomock.NewController(t) trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -905,6 +919,7 @@ func TestHandler_HandleSyncedMalfeasanceProof_multipleBallots(t *testing.T) { types.EmptyNodeID, signing.NewEdVerifier(), trt, + postVerifier, ) sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -957,6 +972,7 @@ func TestHandler_HandleSyncedMalfeasanceProof_hareEquivocation(t *testing.T) { lg := logtest.New(t) ctrl := gomock.NewController(t) trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -965,6 +981,7 @@ func TestHandler_HandleSyncedMalfeasanceProof_hareEquivocation(t *testing.T) { types.EmptyNodeID, signing.NewEdVerifier(), trt, + postVerifier, ) sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -1020,6 +1037,7 @@ func TestHandler_HandleSyncedMalfeasanceProof_wrongHash(t *testing.T) { lg := logtest.New(t) ctrl := gomock.NewController(t) trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -1028,6 +1046,7 @@ func TestHandler_HandleSyncedMalfeasanceProof_wrongHash(t *testing.T) { types.EmptyNodeID, signing.NewEdVerifier(), trt, + postVerifier, ) sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -1075,3 +1094,135 @@ func TestHandler_HandleSyncedMalfeasanceProof_wrongHash(t *testing.T) { require.NoError(t, err) require.True(t, malicious) } + +func TestHandler_HandleMalfeasanceProof_InvalidPostIndex(t *testing.T) { + sig, err := signing.NewEdSigner() + require.NoError(t, err) + nodeIdH32 := types.Hash32(sig.NodeID()) + + atx := *types.NewActivationTx( + types.NIPostChallenge{ + PublishEpoch: types.EpochID(1), + CommitmentATX: &types.ATXID{1, 2, 3}, + }, + types.Address{}, + &types.NIPost{ + Post: &types.Post{}, + PostMetadata: &types.PostMetadata{}, + }, + 1, + nil, + ) + require.NoError(t, activation.SignAndFinalizeAtx(sig, &atx)) + + t.Run("valid malfeasance proof", func(t *testing.T) { + db := sql.InMemory() + lg := logtest.New(t) + trt := malfeasance.NewMocktortoise(gomock.NewController(t)) + postVerifier := malfeasance.NewMockpostVerifier(gomock.NewController(t)) + + h := malfeasance.NewHandler( + datastore.NewCachedDB(db, lg), + lg, + "self", + types.EmptyNodeID, + signing.NewEdVerifier(), + trt, + postVerifier, + ) + + proof := types.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: types.Proof{ + Type: types.InvalidPostIndex, + Data: &types.InvalidPostIndexProof{ + Atx: atx, + InvalidIdx: 7, + }, + }, + } + + postVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("invalid")) + trt.EXPECT().OnMalfeasance(sig.NodeID()) + err := h.HandleSyncedMalfeasanceProof(context.Background(), nodeIdH32, "peer", codec.MustEncode(&proof)) + require.NoError(t, err) + + malicious, err := identities.IsMalicious(db, sig.NodeID()) + require.NoError(t, err) + require.True(t, malicious) + }) + + t.Run("invalid malfeasance proof (POST valid)", func(t *testing.T) { + db := sql.InMemory() + lg := logtest.New(t) + trt := malfeasance.NewMocktortoise(gomock.NewController(t)) + postVerifier := malfeasance.NewMockpostVerifier(gomock.NewController(t)) + + h := malfeasance.NewHandler( + datastore.NewCachedDB(db, lg), + lg, + "self", + types.EmptyNodeID, + signing.NewEdVerifier(), + trt, + postVerifier, + ) + + proof := types.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: types.Proof{ + Type: types.InvalidPostIndex, + Data: &types.InvalidPostIndexProof{ + Atx: atx, + InvalidIdx: 7, + }, + }, + } + + postVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + err := h.HandleSyncedMalfeasanceProof(context.Background(), nodeIdH32, "peer", codec.MustEncode(&proof)) + require.Error(t, err) + + malicious, err := identities.IsMalicious(db, sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) + + t.Run("invalid malfeasance proof (ATX signature invalid)", func(t *testing.T) { + db := sql.InMemory() + lg := logtest.New(t) + trt := malfeasance.NewMocktortoise(gomock.NewController(t)) + postVerifier := malfeasance.NewMockpostVerifier(gomock.NewController(t)) + + h := malfeasance.NewHandler( + datastore.NewCachedDB(db, lg), + lg, + "self", + types.EmptyNodeID, + signing.NewEdVerifier(), + trt, + postVerifier, + ) + + atx := atx + atx.NIPost.Post.Pow += 1 // invalidate signature by changing content + + proof := types.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: types.Proof{ + Type: types.InvalidPostIndex, + Data: &types.InvalidPostIndexProof{ + Atx: atx, + InvalidIdx: 7, + }, + }, + } + + err := h.HandleSyncedMalfeasanceProof(context.Background(), nodeIdH32, "peer", codec.MustEncode(&proof)) + require.ErrorContains(t, err, "invalid signature") + + malicious, err := identities.IsMalicious(db, sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) +} diff --git a/malfeasance/interface.go b/malfeasance/interface.go index 61968a22ed..4b1a310c41 100644 --- a/malfeasance/interface.go +++ b/malfeasance/interface.go @@ -1,6 +1,11 @@ package malfeasance import ( + "context" + + "github.com/spacemeshos/post/shared" + "github.com/spacemeshos/post/verifying" + "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/signing" ) @@ -14,3 +19,7 @@ type SigVerifier interface { type tortoise interface { OnMalfeasance(types.NodeID) } + +type postVerifier interface { + Verify(ctx context.Context, p *shared.Proof, m *shared.ProofMetadata, opts ...verifying.OptionFunc) error +} diff --git a/malfeasance/metrics.go b/malfeasance/metrics.go index 30a6632444..8e2eccfae3 100644 --- a/malfeasance/metrics.go +++ b/malfeasance/metrics.go @@ -9,9 +9,10 @@ const ( typeLabel = "type" - multiATXs = "atx" - multiBallots = "ballot" - hareEquivocate = "hare_eq" + multiATXs = "atx" + multiBallots = "ballot" + hareEquivocate = "hare_eq" + invalidPostIndex = "invalid_post_index" ) var ( @@ -24,9 +25,10 @@ var ( }, ) - numProofsATX = numProofs.WithLabelValues(multiATXs) - numProofsBallot = numProofs.WithLabelValues(multiBallots) - numProofsHare = numProofs.WithLabelValues(hareEquivocate) + numProofsATX = numProofs.WithLabelValues(multiATXs) + numProofsBallot = numProofs.WithLabelValues(multiBallots) + numProofsHare = numProofs.WithLabelValues(hareEquivocate) + numProofsPostIndex = numProofs.WithLabelValues(invalidPostIndex) numInvalidProofs = metrics.NewCounter( "num_invalid_proofs", @@ -37,8 +39,9 @@ var ( }, ) - numInvalidProofsATX = numInvalidProofs.WithLabelValues(multiATXs) - numInvalidProofsBallot = numInvalidProofs.WithLabelValues(multiBallots) - numInvalidProofsHare = numInvalidProofs.WithLabelValues(hareEquivocate) - numMalformed = numInvalidProofs.WithLabelValues("mal") + numInvalidProofsATX = numInvalidProofs.WithLabelValues(multiATXs) + numInvalidProofsBallot = numInvalidProofs.WithLabelValues(multiBallots) + numInvalidProofsHare = numInvalidProofs.WithLabelValues(hareEquivocate) + numInvalidProofsPostIndex = numInvalidProofs.WithLabelValues(invalidPostIndex) + numMalformed = numInvalidProofs.WithLabelValues("mal") ) diff --git a/malfeasance/mocks.go b/malfeasance/mocks.go index e3d7395e73..aa8e02d037 100644 --- a/malfeasance/mocks.go +++ b/malfeasance/mocks.go @@ -9,10 +9,13 @@ package malfeasance import ( + context "context" reflect "reflect" types "github.com/spacemeshos/go-spacemesh/common/types" signing "github.com/spacemeshos/go-spacemesh/signing" + shared "github.com/spacemeshos/post/shared" + verifying "github.com/spacemeshos/post/verifying" gomock "go.uber.org/mock/gomock" ) @@ -135,3 +138,69 @@ func (c *tortoiseOnMalfeasanceCall) DoAndReturn(f func(types.NodeID)) *tortoiseO c.Call = c.Call.DoAndReturn(f) return c } + +// MockpostVerifier is a mock of postVerifier interface. +type MockpostVerifier struct { + ctrl *gomock.Controller + recorder *MockpostVerifierMockRecorder +} + +// MockpostVerifierMockRecorder is the mock recorder for MockpostVerifier. +type MockpostVerifierMockRecorder struct { + mock *MockpostVerifier +} + +// NewMockpostVerifier creates a new mock instance. +func NewMockpostVerifier(ctrl *gomock.Controller) *MockpostVerifier { + mock := &MockpostVerifier{ctrl: ctrl} + mock.recorder = &MockpostVerifierMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockpostVerifier) EXPECT() *MockpostVerifierMockRecorder { + return m.recorder +} + +// Verify mocks base method. +func (m_2 *MockpostVerifier) Verify(ctx context.Context, p *shared.Proof, m *shared.ProofMetadata, opts ...verifying.OptionFunc) error { + m_2.ctrl.T.Helper() + varargs := []any{ctx, p, m} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m_2.ctrl.Call(m_2, "Verify", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Verify indicates an expected call of Verify. +func (mr *MockpostVerifierMockRecorder) Verify(ctx, p, m any, opts ...any) *postVerifierVerifyCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, p, m}, opts...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockpostVerifier)(nil).Verify), varargs...) + return &postVerifierVerifyCall{Call: call} +} + +// postVerifierVerifyCall wrap *gomock.Call +type postVerifierVerifyCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *postVerifierVerifyCall) Return(arg0 error) *postVerifierVerifyCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *postVerifierVerifyCall) Do(f func(context.Context, *shared.Proof, *shared.ProofMetadata, ...verifying.OptionFunc) error) *postVerifierVerifyCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *postVerifierVerifyCall) DoAndReturn(f func(context.Context, *shared.Proof, *shared.ProofMetadata, ...verifying.OptionFunc) error) *postVerifierVerifyCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/mesh/mesh_test.go b/mesh/mesh_test.go index 2470799fa6..6bebabd188 100644 --- a/mesh/mesh_test.go +++ b/mesh/mesh_test.go @@ -378,6 +378,7 @@ func TestMesh_MaliciousBallots(t *testing.T) { tm.logger, tm.cdb, signing.NewEdVerifier(), + malfeasance.NewMockpostVerifier(gomock.NewController(t)), &types.MalfeasanceGossip{MalfeasanceProof: *malProof}, ) require.NoError(t, err) diff --git a/node/node.go b/node/node.go index 59ffe1b23c..fde47edadf 100644 --- a/node/node.go +++ b/node/node.go @@ -562,6 +562,7 @@ func (app *App) initServices(ctx context.Context) error { lg.Debug("creating post verifier") verifier, err := activation.NewPostVerifier( + app.edSgn.NodeID().Bytes(), app.Config.POST, nipostValidatorLogger.Zap(), verifying.WithPowFlags(app.Config.SMESHING.VerifyingOpts.Flags.Value()), @@ -949,6 +950,7 @@ func (app *App) initServices(ctx context.Context) error { app.edSgn.NodeID(), app.edVerifier, trtl, + app.postVerifier, ) fetcher.SetValidators( fetch.ValidatorFunc( From ff5dce8c935f004c84490cfb631395784e94df02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 28 Dec 2023 14:17:39 +0100 Subject: [PATCH 02/30] Verify only selected index in post malf proof --- Makefile-libs.Inc | 2 +- activation/e2e/nipost_test.go | 4 +- activation/e2e/validation_test.go | 2 +- activation/handler.go | 3 +- activation/handler_test.go | 84 ++++++++++++-------------- activation/post.go | 9 ++- activation/post_supervisor.go | 1 - activation/post_verifier.go | 3 +- activation/post_verifier_test.go | 2 +- activation/validation.go | 13 ++-- api/grpcserver/smesher_service.go | 4 +- api/grpcserver/smesher_service_test.go | 6 +- checkpoint/recovery_test.go | 2 +- cmd/root.go | 6 +- go.mod | 2 +- go.sum | 4 +- malfeasance/handler.go | 4 +- node/node.go | 1 - 18 files changed, 72 insertions(+), 80 deletions(-) diff --git a/Makefile-libs.Inc b/Makefile-libs.Inc index 215754d1a5..6a2b8a62b4 100644 --- a/Makefile-libs.Inc +++ b/Makefile-libs.Inc @@ -50,7 +50,7 @@ else endif endif -POSTRS_SETUP_REV = 0.7.0-rc2-distributed-verification +POSTRS_SETUP_REV = 0.7.0-rc4-distributed-verification POSTRS_SETUP_ZIP = libpost-$(platform)-v$(POSTRS_SETUP_REV).zip POSTRS_SETUP_URL_ZIP ?= https://github.com/spacemeshos/post-rs/releases/download/v$(POSTRS_SETUP_REV)/$(POSTRS_SETUP_ZIP) POSTRS_PROFILER_ZIP = profiler-$(platform)-v$(POSTRS_SETUP_REV).zip diff --git a/activation/e2e/nipost_test.go b/activation/e2e/nipost_test.go index 5c2f0fd47f..645f9cea2f 100644 --- a/activation/e2e/nipost_test.go +++ b/activation/e2e/nipost_test.go @@ -160,7 +160,7 @@ func TestNIPostBuilderWithClients(t *testing.T) { }, ) - verifier, err := activation.NewPostVerifier(nil, cfg, logger.Named("verifier")) + verifier, err := activation.NewPostVerifier(cfg, logger.Named("verifier")) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, verifier.Close()) }) @@ -329,7 +329,7 @@ func TestNewNIPostBuilderNotInitialized(t *testing.T) { require.NoError(t, err) require.NotNil(t, nipost) - verifier, err := activation.NewPostVerifier(nil, cfg, logger.Named("verifier")) + verifier, err := activation.NewPostVerifier(cfg, logger.Named("verifier")) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, verifier.Close()) }) diff --git a/activation/e2e/validation_test.go b/activation/e2e/validation_test.go index f7c5b55c80..4ba2ed8bf7 100644 --- a/activation/e2e/validation_test.go +++ b/activation/e2e/validation_test.go @@ -67,7 +67,7 @@ func TestValidator_Validate(t *testing.T) { }, ) - verifier, err := activation.NewPostVerifier(nil, cfg, logger.Named("verifier")) + verifier, err := activation.NewPostVerifier(cfg, logger.Named("verifier")) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, verifier.Close()) }) diff --git a/activation/handler.go b/activation/handler.go index 1b536b65c9..24a3e9ec8d 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -170,7 +170,7 @@ func (h *Handler) SyntacticallyValidate(ctx context.Context, atx *types.Activati return fmt.Errorf("invalid vrf nonce: %w", err) } if err := h.nipostValidator.Post( - ctx, atx.SmesherID, *atx.CommitmentATX, atx.InitialPost, &initialPostMetadata, atx.NumUnits, FullPost(), + ctx, atx.SmesherID, *atx.CommitmentATX, atx.InitialPost, &initialPostMetadata, atx.NumUnits, ); err != nil { return fmt.Errorf("invalid initial post: %w", err) } @@ -233,6 +233,7 @@ func (h *Handler) SyntacticallyValidateDeps( atx.NIPost, expectedChallengeHash, atx.NumUnits, + PostSubset([]byte(h.local)), // use the local peer ID as seed for random subset ) var invalidIdx *verifying.ErrInvalidIndex if errors.As(err, &invalidIdx) { diff --git a/activation/handler_test.go b/activation/handler_test.go index 367394e8bb..346900d288 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -353,7 +353,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { atxHdlr.mclock.EXPECT().CurrentLayer().Return(currentLayer) require.NoError(t, atxHdlr.SyntacticallyValidate(context.Background(), atx)) atxHdlr.mValidator.EXPECT(). - NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(1), nil) atxHdlr.mValidator.EXPECT().NIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) @@ -378,7 +378,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { require.NoError(t, atxHdlr.SyntacticallyValidate(context.Background(), atx)) atxHdlr.mValidator.EXPECT(). - NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(1), nil) atxHdlr.mValidator.EXPECT().NIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) @@ -410,7 +410,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { atxHdlr.mclock.EXPECT().CurrentLayer().Return(currentLayer) require.NoError(t, atxHdlr.SyntacticallyValidate(context.Background(), atx)) atxHdlr.mValidator.EXPECT(). - NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(1), nil) atxHdlr.mValidator.EXPECT().NIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) @@ -441,7 +441,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { atxHdlr.mclock.EXPECT().CurrentLayer().Return(currentLayer) require.NoError(t, atxHdlr.SyntacticallyValidate(context.Background(), atx)) atxHdlr.mValidator.EXPECT(). - NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(1), nil) atxHdlr.mValidator.EXPECT().NIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) @@ -512,7 +512,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { require.NoError(t, atxHdlr.SyntacticallyValidate(context.Background(), atx)) atxHdlr.mValidator.EXPECT().InitialNIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT(). - NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(1), nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) _, err := atxHdlr.SyntacticallyValidateDeps(context.Background(), atx) @@ -1031,20 +1031,14 @@ func TestHandler_PublishesPostMalfeasanceProofs(t *testing.T) { var got types.MalfeasanceGossip atxHdlr.mclock.EXPECT().CurrentLayer().Return(atx.PublishEpoch.FirstLayer()) atxHdlr.mValidator.EXPECT().VRFNonce(gomock.Any(), goldenATXID, gomock.Any(), gomock.Any(), gomock.Any()) - atxHdlr.mValidator.EXPECT().Post( - gomock.Any(), - gomock.Any(), - gomock.Any(), - gomock.Any(), - gomock.Any(), - gomock.Any(), - gomock.Any(), - ) + atxHdlr.mValidator.EXPECT(). + Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) atxHdlr.mockFetch.EXPECT().RegisterPeerHashes(gomock.Any(), gomock.Any()) atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), atx.GetPoetProofRef()) atxHdlr.mValidator.EXPECT().InitialNIPostChallenge(&atx.NIPostChallenge, gomock.Any(), goldenATXID) atxHdlr.mValidator.EXPECT().PositioningAtx(goldenATXID, gomock.Any(), goldenATXID, atx.PublishEpoch) - atxHdlr.mValidator.EXPECT().NIPost(gomock.Any(), gomock.Any(), goldenATXID, atx.NIPost, gomock.Any(), atx.NumUnits). + atxHdlr.mValidator.EXPECT(). + NIPost(gomock.Any(), gomock.Any(), goldenATXID, atx.NIPost, gomock.Any(), atx.NumUnits, gomock.Any()). Return(0, &verifying.ErrInvalidIndex{Index: 2}) atxHdlr.mtortoise.EXPECT().OnMalfeasance(gomock.Any()) atxHdlr.mpub.EXPECT().Publish(gomock.Any(), pubsub.MalfeasanceProof, gomock.Any()).DoAndReturn( @@ -1062,6 +1056,10 @@ func TestHandler_PublishesPostMalfeasanceProofs(t *testing.T) { ) require.NoError(t, err) require.Equal(t, sig.NodeID(), nodeID) + require.Equal(t, types.InvalidPostIndex, got.Proof.Type) + p, ok := got.Proof.Data.(*types.InvalidPostIndexProof) + require.True(t, ok) + require.EqualValues(t, 2, p.InvalidIdx) return nil }) @@ -1274,7 +1272,7 @@ func TestHandler_HandleGossipAtx(t *testing.T) { atxHdlr.mValidator.EXPECT().InitialNIPostChallenge(&first.NIPostChallenge, gomock.Any(), goldenATXID) atxHdlr.mValidator.EXPECT().PositioningAtx(goldenATXID, gomock.Any(), goldenATXID, first.PublishEpoch) atxHdlr.mValidator.EXPECT(). - NIPost(gomock.Any(), nodeID1, goldenATXID, second.NIPost, gomock.Any(), second.NumUnits) + NIPost(gomock.Any(), nodeID1, goldenATXID, second.NIPost, gomock.Any(), second.NumUnits, gomock.Any()) atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any()) require.NoError(t, atxHdlr.HandleGossipAtx(context.Background(), "", data)) @@ -1282,7 +1280,8 @@ func TestHandler_HandleGossipAtx(t *testing.T) { }, ) atxHdlr.mValidator.EXPECT().NIPostChallenge(&second.NIPostChallenge, gomock.Any(), nodeID1) - atxHdlr.mValidator.EXPECT().NIPost(gomock.Any(), nodeID1, goldenATXID, second.NIPost, gomock.Any(), second.NumUnits) + atxHdlr.mValidator.EXPECT(). + NIPost(gomock.Any(), nodeID1, goldenATXID, second.NIPost, gomock.Any(), second.NumUnits, gomock.Any()) atxHdlr.mValidator.EXPECT().PositioningAtx(second.PositioningATX, gomock.Any(), goldenATXID, second.PublishEpoch) atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any()) @@ -1326,33 +1325,28 @@ func TestHandler_HandleParallelGossipAtx(t *testing.T) { atxHdlr.mclock.EXPECT().CurrentLayer().Return(atx.PublishEpoch.FirstLayer()) atxHdlr.mValidator.EXPECT().VRFNonce(nodeID, goldenATXID, &vrfNonce, gomock.Any(), atx.NumUnits) - atxHdlr.mValidator.EXPECT().Post( - gomock.Any(), - atx.SmesherID, - goldenATXID, - atx.InitialPost, - gomock.Any(), - atx.NumUnits, - gomock.Any(), - ).DoAndReturn( - func( - _ context.Context, - _ types.NodeID, - _ types.ATXID, - _ *types.Post, - _ *types.PostMetadata, - _ uint32, - _ ...validatorOption, - ) error { - time.Sleep(100 * time.Millisecond) - return nil - }, - ) + atxHdlr.mValidator.EXPECT(). + Post(gomock.Any(), atx.SmesherID, goldenATXID, atx.InitialPost, gomock.Any(), atx.NumUnits). + DoAndReturn( + func( + _ context.Context, + _ types.NodeID, + _ types.ATXID, + _ *types.Post, + _ *types.PostMetadata, + _ uint32, + _ ...validatorOption, + ) error { + time.Sleep(100 * time.Millisecond) + return nil + }, + ) atxHdlr.mockFetch.EXPECT().RegisterPeerHashes(gomock.Any(), gomock.Any()) atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), atx.GetPoetProofRef()) atxHdlr.mValidator.EXPECT().InitialNIPostChallenge(&atx.NIPostChallenge, gomock.Any(), goldenATXID) atxHdlr.mValidator.EXPECT().PositioningAtx(goldenATXID, gomock.Any(), goldenATXID, atx.PublishEpoch) - atxHdlr.mValidator.EXPECT().NIPost(gomock.Any(), nodeID, goldenATXID, atx.NIPost, gomock.Any(), atx.NumUnits) + atxHdlr.mValidator.EXPECT(). + NIPost(gomock.Any(), nodeID, goldenATXID, atx.NIPost, gomock.Any(), atx.NumUnits, gomock.Any()) atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any()) @@ -1728,9 +1722,9 @@ func TestHandler_AtxWeight(t *testing.T) { atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), proofRef) atxHdlr.mValidator.EXPECT().VRFNonce(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) atxHdlr.mValidator.EXPECT(). - Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) atxHdlr.mValidator.EXPECT(). - NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(leaves, nil) atxHdlr.mValidator.EXPECT().InitialNIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) @@ -1774,7 +1768,7 @@ func TestHandler_AtxWeight(t *testing.T) { atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), proofRef) atxHdlr.mockFetch.EXPECT().GetAtxs(gomock.Any(), gomock.Any()) atxHdlr.mValidator.EXPECT(). - NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(leaves, nil) atxHdlr.mValidator.EXPECT().NIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) @@ -1829,9 +1823,9 @@ func TestHandler_WrongHash(t *testing.T) { atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), proofRef) atxHdlr.mValidator.EXPECT().VRFNonce(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) atxHdlr.mValidator.EXPECT(). - Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) atxHdlr.mValidator.EXPECT(). - NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(111), nil) atxHdlr.mValidator.EXPECT().InitialNIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) diff --git a/activation/post.go b/activation/post.go index cdf4346aff..5b16356d98 100644 --- a/activation/post.go +++ b/activation/post.go @@ -27,9 +27,9 @@ type PostConfig struct { MinNumUnits uint32 `mapstructure:"post-min-numunits"` MaxNumUnits uint32 `mapstructure:"post-max-numunits"` LabelsPerUnit uint64 `mapstructure:"post-labels-per-unit"` - K1 uint32 `mapstructure:"post-k1"` - K2 uint32 `mapstructure:"post-k2"` - K3 uint32 `mapstructure:"post-k3"` + K1 uint `mapstructure:"post-k1"` + K2 uint `mapstructure:"post-k2"` + K3 uint `mapstructure:"post-k3"` PowDifficulty PowDifficulty `mapstructure:"post-pow-difficulty"` } @@ -40,7 +40,6 @@ func (c PostConfig) ToConfig() config.Config { LabelsPerUnit: c.LabelsPerUnit, K1: c.K1, K2: c.K2, - K3: c.K3, PowDifficulty: [32]byte(c.PowDifficulty), } } @@ -127,7 +126,7 @@ func DefaultPostConfig() PostConfig { LabelsPerUnit: cfg.LabelsPerUnit, K1: cfg.K1, K2: cfg.K2, - K3: cfg.K3, + K3: cfg.K2, PowDifficulty: PowDifficulty(cfg.PowDifficulty), } } diff --git a/activation/post_supervisor.go b/activation/post_supervisor.go index 8c596a90be..e9a05fcbe8 100644 --- a/activation/post_supervisor.go +++ b/activation/post_supervisor.go @@ -233,7 +233,6 @@ func (ps *PostSupervisor) runCmd( "--labels-per-unit", strconv.FormatUint(uint64(postCfg.LabelsPerUnit), 10), "--k1", strconv.FormatUint(uint64(postCfg.K1), 10), "--k2", strconv.FormatUint(uint64(postCfg.K2), 10), - "--k3", strconv.FormatUint(uint64(postCfg.K3), 10), "--pow-difficulty", postCfg.PowDifficulty.String(), "--dir", postOpts.DataDir, diff --git a/activation/post_verifier.go b/activation/post_verifier.go index 8ddd1326b1..5e8f844b4f 100644 --- a/activation/post_verifier.go +++ b/activation/post_verifier.go @@ -95,12 +95,11 @@ func (v *postVerifier) Verify( // NewPostVerifier creates a new post verifier. func NewPostVerifier( - id []byte, cfg PostConfig, logger *zap.Logger, opts ...verifying.OptionFunc, ) (PostVerifier, error) { - verifier, err := verifying.NewProofVerifier(id, opts...) + verifier, err := verifying.NewProofVerifier(opts...) if err != nil { return nil, err } diff --git a/activation/post_verifier_test.go b/activation/post_verifier_test.go index 9f30fa000e..d1d3cde9de 100644 --- a/activation/post_verifier_test.go +++ b/activation/post_verifier_test.go @@ -34,7 +34,7 @@ func TestOffloadingPostVerifier(t *testing.T) { } func TestPostVerifierDetectsInvalidProof(t *testing.T) { - verifier, err := activation.NewPostVerifier(nil, activation.PostConfig{}, zaptest.NewLogger(t)) + verifier, err := activation.NewPostVerifier(activation.PostConfig{}, zaptest.NewLogger(t)) require.NoError(t, err) defer verifier.Close() require.Error(t, verifier.Verify(context.Background(), &shared.Proof{}, &shared.ProofMetadata{})) diff --git a/activation/validation.go b/activation/validation.go index b8738cd367..3b90163536 100644 --- a/activation/validation.go +++ b/activation/validation.go @@ -37,13 +37,14 @@ func (e *ErrAtxNotFound) Is(target error) bool { } type validatorOptions struct { - fullPost bool + postSubsetSeed []byte } -// FullPost is a validator option that configures the validator to validate all indices in POST. -func FullPost() validatorOption { +// PostSubset configures the validator to validate only a subset of the POST indices. +// The `seed` is used to randomize the selection of indices. +func PostSubset(seed []byte) validatorOption { return func(o *validatorOptions) { - o.fullPost = true + o.postSubsetSeed = seed } } @@ -156,8 +157,8 @@ func (v *Validator) Post( opt(options) } verifyOpts := []verifying.OptionFunc{verifying.WithLabelScryptParams(v.scrypt)} - if options.fullPost { - verifyOpts = append(verifyOpts, verifying.AllIndices()) + if options.postSubsetSeed != nil { + verifyOpts = append(verifyOpts, verifying.Subset(v.cfg.K3, options.postSubsetSeed)) } start := time.Now() diff --git a/api/grpcserver/smesher_service.go b/api/grpcserver/smesher_service.go index 4979bd1f47..df438bb9ba 100644 --- a/api/grpcserver/smesher_service.go +++ b/api/grpcserver/smesher_service.go @@ -253,8 +253,8 @@ func (s SmesherService) PostConfig(context.Context, *emptypb.Empty) (*pb.PostCon LabelsPerUnit: cfg.LabelsPerUnit, MinNumUnits: cfg.MinNumUnits, MaxNumUnits: cfg.MaxNumUnits, - K1: cfg.K1, - K2: cfg.K2, + K1: uint32(cfg.K1), + K2: uint32(cfg.K2), }, nil } diff --git a/api/grpcserver/smesher_service_test.go b/api/grpcserver/smesher_service_test.go index c9d4afc1c7..221d65bdcc 100644 --- a/api/grpcserver/smesher_service_test.go +++ b/api/grpcserver/smesher_service_test.go @@ -32,8 +32,8 @@ func TestPostConfig(t *testing.T) { MinNumUnits: rand.Uint32(), MaxNumUnits: rand.Uint32(), LabelsPerUnit: rand.Uint64(), - K1: rand.Uint32(), - K2: rand.Uint32(), + K1: uint(rand.Uint32()), + K2: uint(rand.Uint32()), } postSupervisor.EXPECT().Config().Return(postConfig) @@ -43,7 +43,7 @@ func TestPostConfig(t *testing.T) { require.Equal(t, postConfig.MinNumUnits, response.MinNumUnits) require.Equal(t, postConfig.MaxNumUnits, response.MaxNumUnits) require.Equal(t, postConfig.LabelsPerUnit, response.LabelsPerUnit) - require.Equal(t, postConfig.K1, response.K1) + require.EqualValues(t, postConfig.K1, response.K1) require.EqualValues(t, postConfig.K2, response.K2) } diff --git a/checkpoint/recovery_test.go b/checkpoint/recovery_test.go index d7d1d35663..95ead7434b 100644 --- a/checkpoint/recovery_test.go +++ b/checkpoint/recovery_test.go @@ -274,7 +274,7 @@ func validateAndPreserveData( } mvalidator.EXPECT().PositioningAtx(vatx.PositioningATX, cdb, goldenAtx, vatx.PublishEpoch) mvalidator.EXPECT(). - NIPost(gomock.Any(), vatx.SmesherID, gomock.Any(), vatx.NIPost, gomock.Any(), vatx.NumUnits). + NIPost(gomock.Any(), vatx.SmesherID, gomock.Any(), vatx.NIPost, gomock.Any(), vatx.NumUnits, gomock.Any()). Return(uint64(1111111), nil) mreceiver.EXPECT().OnAtx(gomock.Any()) mtrtl.EXPECT().OnAtx(gomock.Any()) diff --git a/cmd/root.go b/cmd/root.go index 7e0c8c8313..d8839b76cb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -229,11 +229,11 @@ func AddCommands(cmd *cobra.Command) { cfg.POST.MinNumUnits, "") cmd.PersistentFlags().Uint32Var(&cfg.POST.MaxNumUnits, "post-max-numunits", cfg.POST.MaxNumUnits, "") - cmd.PersistentFlags().Uint32Var(&cfg.POST.K1, "post-k1", + cmd.PersistentFlags().UintVar(&cfg.POST.K1, "post-k1", cfg.POST.K1, "difficulty factor for finding a good label when generating a proof") - cmd.PersistentFlags().Uint32Var(&cfg.POST.K2, "post-k2", + cmd.PersistentFlags().UintVar(&cfg.POST.K2, "post-k2", cfg.POST.K2, "number of labels to prove") - cmd.PersistentFlags().Uint32Var(&cfg.POST.K3, "post-k3", + cmd.PersistentFlags().UintVar(&cfg.POST.K3, "post-k3", cfg.POST.K3, "subset of labels to verify in a proof") cmd.PersistentFlags().AddFlag(&pflag.Flag{ Name: "post-pow-difficulty", diff --git a/go.mod b/go.mod index 1152bc9259..26928eb797 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/spacemeshos/go-scale v1.1.12 github.com/spacemeshos/merkle-tree v0.2.3 github.com/spacemeshos/poet v0.9.7 - github.com/spacemeshos/post v0.10.3-0.20231222143012-df4c17a08074 + github.com/spacemeshos/post v0.10.3-0.20231228141353-523b1c994625 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index 450e1ec5ea..5d1cd24399 100644 --- a/go.sum +++ b/go.sum @@ -633,8 +633,8 @@ github.com/spacemeshos/merkle-tree v0.2.3 h1:zGEgOR9nxAzJr0EWjD39QFngwFEOxfxMloE github.com/spacemeshos/merkle-tree v0.2.3/go.mod h1:VomOcQ5pCBXz7goiWMP5hReyqOfDXGSKbrH2GB9Htww= github.com/spacemeshos/poet v0.9.7 h1:FmKhgUKj//8Tzn8czWSIrn6+FVUFZbvLh8zqLfB8dfE= github.com/spacemeshos/poet v0.9.7/go.mod h1:wGCdhs2jnfQ52Amcmygv9uEEwYpdHAPjbiPg0Uf6cNQ= -github.com/spacemeshos/post v0.10.3-0.20231222143012-df4c17a08074 h1:xE0mPB1S3VfDRvaslt61QsBiWMeGOS1W6mN9gnvAYvo= -github.com/spacemeshos/post v0.10.3-0.20231222143012-df4c17a08074/go.mod h1:lSZOkvCna1UuXgdGrvc+2SPMM+f+IUi9whZpBH7wUbM= +github.com/spacemeshos/post v0.10.3-0.20231228141353-523b1c994625 h1:83iNwxBzgGpq2RjBKWqDgQyTaIqmK3SF5DRbdzpj8K0= +github.com/spacemeshos/post v0.10.3-0.20231228141353-523b1c994625/go.mod h1:lSZOkvCna1UuXgdGrvc+2SPMM+f+IUi9whZpBH7wUbM= github.com/spacemeshos/sha256-simd v0.1.0 h1:G7Mfu5RYdQiuE+wu4ZyJ7I0TI74uqLhFnKblEnSpjYI= github.com/spacemeshos/sha256-simd v0.1.0/go.mod h1:O8CClVIilId7RtuCMV2+YzMj6qjVn75JsxOxaE8vcfM= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= diff --git a/malfeasance/handler.go b/malfeasance/handler.go index 71b3d63126..08ac99820f 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -7,6 +7,7 @@ import ( "time" "github.com/spacemeshos/post/shared" + "github.com/spacemeshos/post/verifying" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" @@ -406,8 +407,7 @@ func validateInvalidPostIndex(ctx context.Context, ctx, post, meta, - // TODO coming soon - // verifying.SelectedIndex(int(proof.InvalidIdx)), + verifying.SelectedIndex(int(proof.InvalidIdx)), ); err != nil { return atx.SmesherID, nil } diff --git a/node/node.go b/node/node.go index fde47edadf..21d0448d13 100644 --- a/node/node.go +++ b/node/node.go @@ -562,7 +562,6 @@ func (app *App) initServices(ctx context.Context) error { lg.Debug("creating post verifier") verifier, err := activation.NewPostVerifier( - app.edSgn.NodeID().Bytes(), app.Config.POST, nipostValidatorLogger.Zap(), verifying.WithPowFlags(app.Config.SMESHING.VerifyingOpts.Flags.Value()), From 9525a1f21298405e8c9f6075916b6cf86286f2fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 29 Dec 2023 13:30:26 +0100 Subject: [PATCH 03/30] Allow filtering out ATXs when quering for ATX with max height --- activation/activation.go | 5 +- activation/post.go | 5 +- datastore/store.go | 2 +- sql/atxs/atxs.go | 53 +++++++++++++------- sql/atxs/atxs_test.go | 106 +++++++++++++++++++++++---------------- 5 files changed, 108 insertions(+), 63 deletions(-) diff --git a/activation/activation.go b/activation/activation.go index 55e87f4cb9..bc92a118b4 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -602,7 +602,10 @@ func (b *Builder) broadcast(ctx context.Context, atx *types.ActivationTx) (int, // GetPositioningAtx returns atx id with the highest tick height. func (b *Builder) GetPositioningAtx() (types.ATXID, error) { - id, err := atxs.GetIDWithMaxHeight(b.cdb, b.signer.NodeID()) + id, err := atxs.GetIDWithMaxHeight(b.cdb, b.signer.NodeID(), func(a types.ATXID) bool { + // TODO(poszu): verify POST fully + return true + }) if err != nil { if errors.Is(err, sql.ErrNotFound) { b.log.Info("using golden atx as positioning atx") diff --git a/activation/post.go b/activation/post.go index 5b16356d98..82c24b74de 100644 --- a/activation/post.go +++ b/activation/post.go @@ -356,7 +356,10 @@ func (mgr *PostSetupManager) commitmentAtx(dataDir string) (types.ATXID, error) // It will use the ATX with the highest height seen by the node and defaults to the goldenATX, // when no ATXs have yet been published. func (mgr *PostSetupManager) findCommitmentAtx() (types.ATXID, error) { - atx, err := atxs.GetIDWithMaxHeight(mgr.db, types.EmptyNodeID) + atx, err := atxs.GetIDWithMaxHeight(mgr.db, types.EmptyNodeID, func(a types.ATXID) bool { + // TODO(poszu): verify POST fully + return true + }) switch { case errors.Is(err, sql.ErrNotFound): mgr.logger.Info("using golden atx as commitment atx") diff --git a/datastore/store.go b/datastore/store.go index ad1bea5ee6..09c9cce91b 100644 --- a/datastore/store.go +++ b/datastore/store.go @@ -330,7 +330,7 @@ func (db *CachedDB) IdentityExists(nodeID types.NodeID) (bool, error) { } func (db *CachedDB) MaxHeightAtx() (types.ATXID, error) { - return atxs.GetIDWithMaxHeight(db, types.EmptyNodeID) + return atxs.GetIDWithMaxHeight(db, types.EmptyNodeID, atxs.FilterAll) } // Hint marks which DB should be queried for a certain provided hash. diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 2e4d548989..81afb53be0 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -127,7 +127,7 @@ func CommitmentATX(db sql.Executor, nodeID types.NodeID) (id types.ATXID, err er } if rows, err := db.Exec(` - select commitment_atx from atxs + select commitment_atx from atxs where pubkey = ?1 and commitment_atx is not null order by epoch desc limit 1;`, enc, dec); err != nil { @@ -150,7 +150,7 @@ func GetFirstIDByNodeID(db sql.Executor, nodeID types.NodeID) (id types.ATXID, e } if rows, err := db.Exec(` - select id from atxs + select id from atxs where pubkey = ?1 order by epoch asc limit 1;`, enc, dec); err != nil { @@ -173,7 +173,7 @@ func GetLastIDByNodeID(db sql.Executor, nodeID types.NodeID) (id types.ATXID, er } if rows, err := db.Exec(` - select id from atxs + select id from atxs where pubkey = ?1 order by epoch desc, received desc limit 1;`, enc, dec); err != nil { @@ -312,12 +312,19 @@ func Add(db sql.Executor, atx *types.VerifiedActivationTx) error { return nil } +type Filter func(types.ATXID) bool + +func FilterAll(types.ATXID) bool { return true } + // GetIDWithMaxHeight returns the ID of the atx from the last 2 epoch with the highest (or tied for the highest) // tick height. It is possible that some poet servers are faster than others and the network ends up having its // highest ticked atx still in previous epoch and the atxs building on top of it have not been published yet. // Selecting from the last two epochs to strike a balance between being fair to honest miners while not giving // unfair advantage for malicious actors who retroactively publish a high tick atx many epochs back. -func GetIDWithMaxHeight(db sql.Executor, pref types.NodeID) (types.ATXID, error) { +func GetIDWithMaxHeight(db sql.Executor, pref types.NodeID, filter Filter) (types.ATXID, error) { + if filter == nil { + filter = FilterAll + } var ( rst types.ATXID max uint64 @@ -325,26 +332,38 @@ func GetIDWithMaxHeight(db sql.Executor, pref types.NodeID) (types.ATXID, error) dec := func(stmt *sql.Statement) bool { var id types.ATXID stmt.ColumnBytes(0, id[:]) - height := uint64(stmt.ColumnInt64(1)) + uint64(stmt.ColumnInt64(2)) - if height >= max { + height := uint64(stmt.ColumnInt64(1)) + epoch := uint64(stmt.ColumnInt64(3)) + _ = epoch + + switch { + case height < max: + // Results are ordered by height, so we can stop once we see a lower height. + return false + case height > max && filter(id): + max = height + rst = id + // We can stop on the first ATX if `pref` is empty. + return pref != types.EmptyNodeID + case height == max && filter(id): + // prefer atxs from `pref` var smesher types.NodeID - stmt.ColumnBytes(3, smesher[:]) - if height > max { - max = height - rst = id - } else if pref != types.EmptyNodeID && smesher == pref { - // height is equal. prefer atxs from `pref` + stmt.ColumnBytes(2, smesher[:]) + if smesher == pref { rst = id + return false } + return true } + return true } if rows, err := db.Exec(` - select id, base_tick_height, tick_count, pubkey - from atxs left join identities using(pubkey) - where identities.pubkey is null and epoch >= (select max(epoch) from atxs)-1 - order by epoch desc;`, nil, dec); err != nil { + SELECT id, base_tick_height + tick_count AS height, pubkey, epoch + FROM atxs LEFT JOIN identities using(pubkey) + WHERE identities.pubkey is null and epoch >= (select max(epoch) from atxs)-1 + ORDER BY height DESC, epoch DESC;`, nil, dec); err != nil { return types.ATXID{}, fmt.Errorf("select positioning atx: %w", err) } else if rows == 0 { return types.ATXID{}, sql.ErrNotFound @@ -386,7 +405,7 @@ func LatestN(db sql.Executor, n int) ([]CheckpointAtx, error) { } if rows, err := db.Exec(` - select id, epoch, effective_num_units, base_tick_height, tick_count, pubkey, sequence, coinbase + select id, epoch, effective_num_units, base_tick_height, tick_count, pubkey, sequence, coinbase from ( select row_number() over (partition by pubkey order by epoch desc) RowNum, id, epoch, effective_num_units, base_tick_height, tick_count, pubkey, sequence, coinbase from atxs diff --git a/sql/atxs/atxs_test.go b/sql/atxs/atxs_test.go index d4f08bc25d..914b473841 100644 --- a/sql/atxs/atxs_test.go +++ b/sql/atxs/atxs_test.go @@ -578,30 +578,47 @@ func newAtx(signer *signing.EdSigner, opts ...createAtxOpt) (*types.VerifiedActi return atx.Verify(0, 1) } -func createIdentities(tb testing.TB, db sql.Executor, n int, midxs ...int) []*signing.EdSigner { - var sigs []*signing.EdSigner - for i := 0; i < n; i++ { - sig, err := signing.NewEdSigner() - require.NoError(tb, err) - sigs = append(sigs, sig) +type header struct { + coinbase types.Address + base, count uint64 + epoch types.EpochID + malicious bool + filteredOut bool +} + +func createAtx(tb testing.TB, db *sql.Database, hdr header) (types.ATXID, *signing.EdSigner) { + full := &types.ActivationTx{ + InnerActivationTx: types.InnerActivationTx{ + NIPostChallenge: types.NIPostChallenge{ + PublishEpoch: hdr.epoch, + }, + Coinbase: hdr.coinbase, + NumUnits: 2, + }, } - for _, idx := range midxs { - require.NoError(tb, identities.SetMalicious(db, sigs[idx].NodeID(), []byte("bad"), time.Now())) + sig, err := signing.NewEdSigner() + require.NoError(tb, err) + + require.NoError(tb, activation.SignAndFinalizeAtx(sig, full)) + + full.SetEffectiveNumUnits(full.NumUnits) + full.SetReceived(time.Now()) + vAtx, err := full.Verify(hdr.base, hdr.count) + require.NoError(tb, err) + + require.NoError(tb, atxs.Add(db, vAtx)) + if hdr.malicious { + require.NoError(tb, identities.SetMalicious(db, sig.NodeID(), []byte("bad"), time.Now())) } - return sigs + + return full.ID(), sig } func TestGetIDWithMaxHeight(t *testing.T) { - type header struct { - coinbase types.Address - base, count uint64 - epoch types.EpochID - } for _, tc := range []struct { desc string atxs []header pref int - midxs []int expect int }{ { @@ -648,23 +665,21 @@ func TestGetIDWithMaxHeight(t *testing.T) { { desc: "skip malicious id", atxs: []header{ - {coinbase: types.Address{1}, base: 1, count: 2, epoch: 1}, - {coinbase: types.Address{2}, base: 1, count: 2, epoch: 1}, + {coinbase: types.Address{1}, base: 1, count: 2, epoch: 1, malicious: true}, + {coinbase: types.Address{2}, base: 1, count: 2, epoch: 1, malicious: true}, {coinbase: types.Address{3}, base: 1, count: 1, epoch: 2}, }, pref: 1, - midxs: []int{0, 1}, expect: 2, }, { desc: "skip malicious id not found", atxs: []header{ - {coinbase: types.Address{1}, base: 1, count: 2, epoch: 1}, - {coinbase: types.Address{2}, base: 1, count: 2, epoch: 1}, - {coinbase: types.Address{3}, base: 1, count: 2, epoch: 2}, + {coinbase: types.Address{1}, base: 1, count: 2, epoch: 1, malicious: true}, + {coinbase: types.Address{2}, base: 1, count: 2, epoch: 1, malicious: true}, + {coinbase: types.Address{3}, base: 1, count: 2, epoch: 2, malicious: true}, }, pref: 1, - midxs: []int{0, 1, 2}, expect: -1, }, { @@ -676,36 +691,41 @@ func TestGetIDWithMaxHeight(t *testing.T) { pref: -1, expect: 0, }, + { + desc: "by filter", + atxs: []header{ + {coinbase: types.Address{1}, base: 1, count: 30, epoch: 3, filteredOut: true}, + {coinbase: types.Address{2}, base: 1, count: 20, epoch: 3, filteredOut: true}, + {coinbase: types.Address{3}, base: 1, count: 10, epoch: 2, filteredOut: true}, + {coinbase: types.Address{4}, base: 1, count: 1, epoch: 2}, + {coinbase: types.Address{5}, base: 1, count: 100, epoch: 1}, + }, + pref: -1, + expect: 3, + }, } { t.Run(tc.desc, func(t *testing.T) { db := sql.InMemory() - sigs := createIdentities(t, db, len(tc.atxs), tc.midxs...) - ids := []types.ATXID{} - for i, atx := range tc.atxs { - full := &types.ActivationTx{ - InnerActivationTx: types.InnerActivationTx{ - NIPostChallenge: types.NIPostChallenge{ - PublishEpoch: atx.epoch, - }, - Coinbase: atx.coinbase, - NumUnits: 2, - }, + var sigs []*signing.EdSigner + var ids []types.ATXID + filtered := make(map[types.ATXID]struct{}) + + for _, atx := range tc.atxs { + id, sig := createAtx(t, db, atx) + ids = append(ids, id) + sigs = append(sigs, sig) + if atx.filteredOut { + filtered[id] = struct{}{} } - require.NoError(t, activation.SignAndFinalizeAtx(sigs[i], full)) - - full.SetEffectiveNumUnits(full.NumUnits) - full.SetReceived(time.Now()) - vAtx, err := full.Verify(atx.base, atx.count) - require.NoError(t, err) - - require.NoError(t, atxs.Add(db, vAtx)) - ids = append(ids, full.ID()) } var pref types.NodeID if tc.pref > 0 { pref = sigs[tc.pref].NodeID() } - rst, err := atxs.GetIDWithMaxHeight(db, pref) + rst, err := atxs.GetIDWithMaxHeight(db, pref, func(id types.ATXID) bool { + _, ok := filtered[id] + return !ok + }) if len(tc.atxs) == 0 || tc.expect < 0 { require.ErrorIs(t, err, sql.ErrNotFound) } else { From 33b678106ce6d6bcac60b03017f404991e1ef297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 29 Dec 2023 16:35:55 +0100 Subject: [PATCH 04/30] Verify POST of a candidate for a positioning ATX --- activation/activation.go | 24 ++++++++++++++++--- activation/activation_test.go | 44 ++++++++++++++++++++++++++++------- activation/interface.go | 4 ++-- activation/mocks.go | 16 ++++++------- sql/database.go | 12 ++++++++-- 5 files changed, 77 insertions(+), 23 deletions(-) diff --git a/activation/activation.go b/activation/activation.go index bc92a118b4..6c91affb7d 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -602,9 +602,27 @@ func (b *Builder) broadcast(ctx context.Context, atx *types.ActivationTx) (int, // GetPositioningAtx returns atx id with the highest tick height. func (b *Builder) GetPositioningAtx() (types.ATXID, error) { - id, err := atxs.GetIDWithMaxHeight(b.cdb, b.signer.NodeID(), func(a types.ATXID) bool { - // TODO(poszu): verify POST fully - return true + id, err := atxs.GetIDWithMaxHeight(b.cdb, b.signer.NodeID(), func(id types.ATXID) bool { + atx, err := atxs.Get(b.cdb, id) + if err != nil { + return false + } + commitmentAtxId := atx.CommitmentATX + if commitmentAtxId == nil { + if atxId, err := atxs.CommitmentATX(b.cdb, atx.SmesherID); err != nil { + return false + } else { + commitmentAtxId = &atxId + } + } + return b.validator.Post( + context.Background(), + atx.SmesherID, + *commitmentAtxId, + atx.NIPost.Post, + atx.NIPost.PostMetadata, + atx.NumUnits, + ) == nil }) if err != nil { if errors.Is(err, sql.ErrNotFound) { diff --git a/activation/activation_test.go b/activation/activation_test.go index f5940cf47d..77db23c3fa 100644 --- a/activation/activation_test.go +++ b/activation/activation_test.go @@ -895,7 +895,7 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { prevAtxPostEpoch := postGenesisEpoch postAtxPubEpoch := postGenesisEpoch - challenge := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, postAtxPubEpoch, nil) + challenge := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, postAtxPubEpoch, &types.ATXID{4, 5, 6}) poetBytes := []byte("66666") nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), poetBytes) posAtx := newAtx(t, otherSigner, challenge, nipostData, 2, types.Address{}) @@ -976,6 +976,16 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { return nil }) + // The candidate for a positioning ATX should be verified + tab.mValidator.EXPECT().Post( + gomock.Any(), + otherSigner.NodeID(), + *posAtx.CommitmentATX, + posAtx.NIPost.Post, + posAtx.NIPost.PostMetadata, + posAtx.NumUnits, + ) + r.NoError(tab.PublishActivationTx(context.Background())) // state is cleaned up @@ -993,7 +1003,7 @@ func TestBuilder_PublishActivationTx_TargetsEpochBasedOnPosAtx(t *testing.T) { currentLayer := postGenesisEpoch.FirstLayer().Add(3) posEpoch := postGenesisEpoch - challenge := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, posEpoch, nil) + challenge := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, posEpoch, &types.ATXID{4, 5, 6}) poetBytes := []byte("66666") nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), poetBytes) posAtx := newAtx(t, otherSigner, challenge, nipostData, 2, types.Address{}) @@ -1069,6 +1079,16 @@ func TestBuilder_PublishActivationTx_TargetsEpochBasedOnPosAtx(t *testing.T) { nipost.Post{Indices: make([]byte, 10)}, )) + // The candidate for a positioning ATX should be verified + tab.mValidator.EXPECT().Post( + gomock.Any(), + otherSigner.NodeID(), + *posAtx.CommitmentATX, + posAtx.NIPost.Post, + posAtx.NIPost.PostMetadata, + posAtx.NumUnits, + ) + r.NoError(tab.PublishActivationTx(context.Background())) // state is cleaned up @@ -1293,14 +1313,10 @@ func TestBuilder_InitialProofGeneratedOnce(t *testing.T) { }, nil, ) - tab.mValidator.EXPECT(). - Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - AnyTimes(). - Return(nil) require.NoError(t, tab.buildInitialPost(context.Background())) posEpoch := postGenesisEpoch + 1 - challenge := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, posEpoch, nil) + challenge := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, posEpoch, &types.ATXID{4, 5, 6}) poetByte := []byte("66666") nipost := newNIPostWithChallenge(t, types.HexToHash32("55555"), poetByte) prevAtx := newAtx(t, tab.sig, challenge, nipost, 2, types.Address{}) @@ -1309,12 +1325,24 @@ func TestBuilder_InitialProofGeneratedOnce(t *testing.T) { require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPrevAtx)) + posAtx := vPrevAtx + tab.mValidator.EXPECT(). + Post( + gomock.Any(), + posAtx.SmesherID, + *posAtx.CommitmentATX, + posAtx.NIPost.Post, + posAtx.NIPost.PostMetadata, + posAtx.NumUnits, + ). + Return(nil) + currLayer := posEpoch.FirstLayer().Add(1) tab.mclock.EXPECT().CurrentLayer().Return(currLayer).AnyTimes() atx, err := publishAtx(t, tab, posEpoch, &currLayer, layersPerEpoch) require.NoError(t, err) require.NotNil(t, atx) - assertLastAtx(require.New(t), tab.sig.NodeID(), types.BytesToHash(poetByte), atx, vPrevAtx, vPrevAtx, layersPerEpoch) + assertLastAtx(require.New(t), tab.sig.NodeID(), types.BytesToHash(poetByte), atx, posAtx, vPrevAtx, layersPerEpoch) // postClient.Proof() should not be called again require.NoError(t, tab.buildInitialPost(context.Background())) diff --git a/activation/interface.go b/activation/interface.go index f0ac8e5c75..d437a43c96 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -35,7 +35,7 @@ type nipostValidator interface { NIPost( ctx context.Context, nodeId types.NodeID, - atxId types.ATXID, + commitmentAtxId types.ATXID, NIPost *types.NIPost, expectedChallenge types.Hash32, numUnits uint32, @@ -46,7 +46,7 @@ type nipostValidator interface { Post( ctx context.Context, nodeId types.NodeID, - atxId types.ATXID, + commitmentAtxId types.ATXID, Post *types.Post, PostMetadata *types.PostMetadata, numUnits uint32, diff --git a/activation/mocks.go b/activation/mocks.go index 4cb2e9c0dd..4959ec2206 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -303,9 +303,9 @@ func (c *nipostValidatorInitialNIPostChallengeCall) DoAndReturn(f func(*types.NI } // NIPost mocks base method. -func (m *MocknipostValidator) NIPost(ctx context.Context, nodeId types.NodeID, atxId types.ATXID, NIPost *types.NIPost, expectedChallenge types.Hash32, numUnits uint32, opts ...validatorOption) (uint64, error) { +func (m *MocknipostValidator) NIPost(ctx context.Context, nodeId types.NodeID, commitmentAtxId types.ATXID, NIPost *types.NIPost, expectedChallenge types.Hash32, numUnits uint32, opts ...validatorOption) (uint64, error) { m.ctrl.T.Helper() - varargs := []any{ctx, nodeId, atxId, NIPost, expectedChallenge, numUnits} + varargs := []any{ctx, nodeId, commitmentAtxId, NIPost, expectedChallenge, numUnits} for _, a := range opts { varargs = append(varargs, a) } @@ -316,9 +316,9 @@ func (m *MocknipostValidator) NIPost(ctx context.Context, nodeId types.NodeID, a } // NIPost indicates an expected call of NIPost. -func (mr *MocknipostValidatorMockRecorder) NIPost(ctx, nodeId, atxId, NIPost, expectedChallenge, numUnits any, opts ...any) *nipostValidatorNIPostCall { +func (mr *MocknipostValidatorMockRecorder) NIPost(ctx, nodeId, commitmentAtxId, NIPost, expectedChallenge, numUnits any, opts ...any) *nipostValidatorNIPostCall { mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, nodeId, atxId, NIPost, expectedChallenge, numUnits}, opts...) + varargs := append([]any{ctx, nodeId, commitmentAtxId, NIPost, expectedChallenge, numUnits}, opts...) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NIPost", reflect.TypeOf((*MocknipostValidator)(nil).NIPost), varargs...) return &nipostValidatorNIPostCall{Call: call} } @@ -461,9 +461,9 @@ func (c *nipostValidatorPositioningAtxCall) DoAndReturn(f func(types.ATXID, atxP } // Post mocks base method. -func (m *MocknipostValidator) Post(ctx context.Context, nodeId types.NodeID, atxId types.ATXID, Post *types.Post, PostMetadata *types.PostMetadata, numUnits uint32, opts ...validatorOption) error { +func (m *MocknipostValidator) Post(ctx context.Context, nodeId types.NodeID, commitmentAtxId types.ATXID, Post *types.Post, PostMetadata *types.PostMetadata, numUnits uint32, opts ...validatorOption) error { m.ctrl.T.Helper() - varargs := []any{ctx, nodeId, atxId, Post, PostMetadata, numUnits} + varargs := []any{ctx, nodeId, commitmentAtxId, Post, PostMetadata, numUnits} for _, a := range opts { varargs = append(varargs, a) } @@ -473,9 +473,9 @@ func (m *MocknipostValidator) Post(ctx context.Context, nodeId types.NodeID, atx } // Post indicates an expected call of Post. -func (mr *MocknipostValidatorMockRecorder) Post(ctx, nodeId, atxId, Post, PostMetadata, numUnits any, opts ...any) *nipostValidatorPostCall { +func (mr *MocknipostValidatorMockRecorder) Post(ctx, nodeId, commitmentAtxId, Post, PostMetadata, numUnits any, opts ...any) *nipostValidatorPostCall { mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, nodeId, atxId, Post, PostMetadata, numUnits}, opts...) + varargs := append([]any{ctx, nodeId, commitmentAtxId, Post, PostMetadata, numUnits}, opts...) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MocknipostValidator)(nil).Post), varargs...) return &nipostValidatorPostCall{Call: call} } diff --git a/sql/database.go b/sql/database.go index a477bfbdc6..f0287b0275 100644 --- a/sql/database.go +++ b/sql/database.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math/rand" "slices" "sort" "sync" @@ -129,8 +130,15 @@ type Opt func(c *conf) // InMemory database for testing. func InMemory(opts ...Opt) *Database { - opts = append(opts, WithConnections(1)) - db, err := Open("file::memory:?mode=memory", opts...) + opts = append(opts, WithConnections(2)) + + var name []rune + runes := []rune("abcdefghijklmnopqrstuvwxyz") + for i := 0; i < 5; i++ { + name = append(name, (runes[rand.Intn(len(runes))])) + } + + db, err := Open(fmt.Sprintf("file:db-%s?mode=memory&cache=shared", string(name)), opts...) if err != nil { panic(err) } From f3486a67a1518643306104de54e1e8a9da9d0b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 3 Jan 2024 13:40:26 +0100 Subject: [PATCH 05/30] Verify positioning ATX candidate chain --- activation/activation.go | 63 +++++++------- activation/activation_test.go | 81 +++++++++--------- activation/validation.go | 129 +++++++++++++++++++++++++++++ activation/validation_test.go | 112 +++++++++++++++++++++++++ common/types/activation.go | 17 ++++ node/node.go | 2 +- sql/atxs/atxs.go | 21 ++++- sql/database.go | 12 +-- sql/migrations/state/0008_next.sql | 4 + 9 files changed, 357 insertions(+), 84 deletions(-) diff --git a/activation/activation.go b/activation/activation.go index 6c91affb7d..a84556b805 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -59,7 +59,6 @@ const ( // Config defines configuration for Builder. type Config struct { GoldenATXID types.ATXID - LayersPerEpoch uint32 RegossipInterval time.Duration } @@ -92,11 +91,17 @@ type Builder struct { stop context.CancelFunc poetCfg PoetConfig poetRetryInterval time.Duration + atxValidityDelay time.Duration // delay before an atx is considered valid (counting from the time it was received) } -// BuilderOption ... type BuilderOption func(*Builder) +func WithAtxValidityDelay(delay time.Duration) BuilderOption { + return func(b *Builder) { + b.atxValidityDelay = delay + } +} + // WithPoetRetryInterval modifies time that builder will have to wait before retrying ATX build process // if it failed due to issues with PoET server. func WithPoetRetryInterval(interval time.Duration) BuilderOption { @@ -422,7 +427,7 @@ func (b *Builder) buildNIPostChallenge(ctx context.Context) (*types.NIPostChalle } } - posAtx, err := b.GetPositioningAtx() + posAtx, err := b.getPositioningAtx(ctx) if err != nil { return nil, fmt.Errorf("failed to get positioning ATX: %w", err) } @@ -600,38 +605,36 @@ func (b *Builder) broadcast(ctx context.Context, atx *types.ActivationTx) (int, return len(buf), nil } -// GetPositioningAtx returns atx id with the highest tick height. -func (b *Builder) GetPositioningAtx() (types.ATXID, error) { - id, err := atxs.GetIDWithMaxHeight(b.cdb, b.signer.NodeID(), func(id types.ATXID) bool { - atx, err := atxs.Get(b.cdb, id) - if err != nil { - return false +// getPositioningAtx returns atx id with the highest tick height. +func (b *Builder) getPositioningAtx(ctx context.Context) (types.ATXID, error) { + rejectedAtxs := make(map[types.ATXID]struct{}) + filter := func(id types.ATXID) bool { + _, ok := rejectedAtxs[id] + return !ok + } + + for { + select { + case <-ctx.Done(): + return types.ATXID{}, ctx.Err() + default: } - commitmentAtxId := atx.CommitmentATX - if commitmentAtxId == nil { - if atxId, err := atxs.CommitmentATX(b.cdb, atx.SmesherID); err != nil { - return false - } else { - commitmentAtxId = &atxId + id, err := atxs.GetIDWithMaxHeight(b.cdb, b.signer.NodeID(), filter) + if err != nil { + if errors.Is(err, sql.ErrNotFound) { + b.log.Info("using golden atx as positioning atx") + return b.goldenATXID, nil } + return types.ATXID{}, fmt.Errorf("cannot find pos atx: %w", err) } - return b.validator.Post( - context.Background(), - atx.SmesherID, - *commitmentAtxId, - atx.NIPost.Post, - atx.NIPost.PostMetadata, - atx.NumUnits, - ) == nil - }) - if err != nil { - if errors.Is(err, sql.ErrNotFound) { - b.log.Info("using golden atx as positioning atx") - return b.goldenATXID, nil + validBefore := time.Now().Add(-b.atxValidityDelay) + if err := VerifyChain(ctx, b.cdb, id, b.goldenATXID, b.validator, b.log, AssumeValidBefore(validBefore)); err != nil { + b.log.Info("rejecting candidate for positioning atx", zap.Error(err), zap.Stringer("atx_id", id)) + rejectedAtxs[id] = struct{}{} + } else { + return id, nil } - return types.ATXID{}, fmt.Errorf("cannot find pos atx: %w", err) } - return id, nil } func (b *Builder) Regossip(ctx context.Context) error { diff --git a/activation/activation_test.go b/activation/activation_test.go index 77db23c3fa..c4bbcea79b 100644 --- a/activation/activation_test.go +++ b/activation/activation_test.go @@ -71,6 +71,7 @@ func newAtx( atx := types.NewActivationTx(challenge, coinbase, nipost, numUnits, nil) atx.SetEffectiveNumUnits(numUnits) atx.SetReceived(time.Now()) + atx.SetValidity(types.Valid) return atx } @@ -140,8 +141,7 @@ func newTestBuilder(tb testing.TB, opts ...BuilderOption) *testAtxBuilder { opts = append(opts, WithValidator(tab.mValidator)) cfg := Config{ - GoldenATXID: tab.goldenATXID, - LayersPerEpoch: layersPerEpoch, + GoldenATXID: tab.goldenATXID, } tab.msync.EXPECT().RegisterForATXSynced().DoAndReturn(closedChan).AnyTimes() @@ -450,7 +450,7 @@ func TestBuilder_PublishActivationTx_HappyFlow(t *testing.T) { tab := newTestBuilder(t, WithPoetConfig(PoetConfig{PhaseShift: layerDuration})) posEpoch := postGenesisEpoch currLayer := posEpoch.FirstLayer() - ch := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, posEpoch, nil) + ch := newChallenge(1, types.EmptyATXID, tab.goldenATXID, posEpoch, &tab.goldenATXID) nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), []byte("66666")) prevAtx := newAtx(t, tab.sig, ch, nipostData, 2, types.Address{}) SignAndFinalizeAtx(tab.sig, prevAtx) @@ -484,7 +484,7 @@ func TestBuilder_Loop_WaitsOnStaleChallenge(t *testing.T) { tab := newTestBuilder(t, WithPoetConfig(PoetConfig{PhaseShift: layerDuration * 4})) // current layer is too late to be able to build a nipost on time currLayer := (postGenesisEpoch + 1).FirstLayer() - ch := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, postGenesisEpoch, nil) + ch := newChallenge(1, types.EmptyATXID, tab.goldenATXID, postGenesisEpoch, &tab.goldenATXID) nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), []byte("66666")) prevAtx := newAtx(t, tab.sig, ch, nipostData, 2, types.Address{}) SignAndFinalizeAtx(tab.sig, prevAtx) @@ -528,7 +528,7 @@ func TestBuilder_PublishActivationTx_FaultyNet(t *testing.T) { tab := newTestBuilder(t, WithPoetConfig(PoetConfig{PhaseShift: layerDuration * 4})) posEpoch := postGenesisEpoch currLayer := postGenesisEpoch.FirstLayer() - ch := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, postGenesisEpoch, nil) + ch := newChallenge(1, types.EmptyATXID, tab.goldenATXID, postGenesisEpoch, &tab.goldenATXID) nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), []byte("66666")) prevAtx := newAtx(t, tab.sig, ch, nipostData, 2, types.Address{}) SignAndFinalizeAtx(tab.sig, prevAtx) @@ -895,7 +895,7 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { prevAtxPostEpoch := postGenesisEpoch postAtxPubEpoch := postGenesisEpoch - challenge := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, postAtxPubEpoch, &types.ATXID{4, 5, 6}) + challenge := newChallenge(1, types.EmptyATXID, tab.goldenATXID, postAtxPubEpoch, &tab.goldenATXID) poetBytes := []byte("66666") nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), poetBytes) posAtx := newAtx(t, otherSigner, challenge, nipostData, 2, types.Address{}) @@ -976,16 +976,6 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { return nil }) - // The candidate for a positioning ATX should be verified - tab.mValidator.EXPECT().Post( - gomock.Any(), - otherSigner.NodeID(), - *posAtx.CommitmentATX, - posAtx.NIPost.Post, - posAtx.NIPost.PostMetadata, - posAtx.NumUnits, - ) - r.NoError(tab.PublishActivationTx(context.Background())) // state is cleaned up @@ -1079,16 +1069,6 @@ func TestBuilder_PublishActivationTx_TargetsEpochBasedOnPosAtx(t *testing.T) { nipost.Post{Indices: make([]byte, 10)}, )) - // The candidate for a positioning ATX should be verified - tab.mValidator.EXPECT().Post( - gomock.Any(), - otherSigner.NodeID(), - *posAtx.CommitmentATX, - posAtx.NIPost.Post, - posAtx.NIPost.PostMetadata, - posAtx.NumUnits, - ) - r.NoError(tab.PublishActivationTx(context.Background())) // state is cleaned up @@ -1316,7 +1296,7 @@ func TestBuilder_InitialProofGeneratedOnce(t *testing.T) { require.NoError(t, tab.buildInitialPost(context.Background())) posEpoch := postGenesisEpoch + 1 - challenge := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, posEpoch, &types.ATXID{4, 5, 6}) + challenge := newChallenge(1, types.EmptyATXID, tab.goldenATXID, posEpoch, &tab.goldenATXID) poetByte := []byte("66666") nipost := newNIPostWithChallenge(t, types.HexToHash32("55555"), poetByte) prevAtx := newAtx(t, tab.sig, challenge, nipost, 2, types.Address{}) @@ -1325,24 +1305,12 @@ func TestBuilder_InitialProofGeneratedOnce(t *testing.T) { require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPrevAtx)) - posAtx := vPrevAtx - tab.mValidator.EXPECT(). - Post( - gomock.Any(), - posAtx.SmesherID, - *posAtx.CommitmentATX, - posAtx.NIPost.Post, - posAtx.NIPost.PostMetadata, - posAtx.NumUnits, - ). - Return(nil) - currLayer := posEpoch.FirstLayer().Add(1) tab.mclock.EXPECT().CurrentLayer().Return(currLayer).AnyTimes() atx, err := publishAtx(t, tab, posEpoch, &currLayer, layersPerEpoch) require.NoError(t, err) require.NotNil(t, atx) - assertLastAtx(require.New(t), tab.sig.NodeID(), types.BytesToHash(poetByte), atx, posAtx, vPrevAtx, layersPerEpoch) + assertLastAtx(require.New(t), tab.sig.NodeID(), types.BytesToHash(poetByte), atx, vPrevAtx, vPrevAtx, layersPerEpoch) // postClient.Proof() should not be called again require.NoError(t, tab.buildInitialPost(context.Background())) @@ -1482,3 +1450,36 @@ func TestWaitingToBuildNipostChallengeWithJitter(t *testing.T) { require.Less(t, deadline, time.Now()) }) } + +// Test if GetPositioningAtx disregards ATXs with invalid POST in their chain. +// It should pick an ATX with valid POST even though it's a lower height. +func TestGetPositioningAtxPicksAtxWithValidChain(t *testing.T) { + tab := newTestBuilder(t) + + // Invalid chain with high height + sigInvalid, err := signing.NewEdSigner() + require.NoError(t, err) + ch := newChallenge(1, types.EmptyATXID, tab.goldenATXID, postGenesisEpoch, &tab.goldenATXID) + nipostData := newNIPostWithChallenge(t, types.HexToHash32("for invalid"), []byte("66")) + invalidAtx := newAtx(t, sigInvalid, ch, nipostData, 2, types.Address{}) + SignAndFinalizeAtx(sigInvalid, invalidAtx) + vInvalidAtx, err := invalidAtx.Verify(0, 100) + require.NoError(t, err) + vInvalidAtx.SetValidity(types.Invalid) + require.NoError(t, atxs.Add(tab.cdb, vInvalidAtx)) + + // Valid chain with lower height + sigValid, err := signing.NewEdSigner() + require.NoError(t, err) + ch = newChallenge(1, types.EmptyATXID, tab.goldenATXID, postGenesisEpoch, &tab.goldenATXID) + nipostData = newNIPostWithChallenge(t, types.HexToHash32("for valid"), []byte("77")) + validAtx := newAtx(t, sigValid, ch, nipostData, 2, types.Address{}) + SignAndFinalizeAtx(sigValid, validAtx) + vValidAtx, err := validAtx.Verify(0, 1) + require.NoError(t, err) + require.NoError(t, atxs.Add(tab.cdb, vValidAtx)) + + posAtxID, err := tab.getPositioningAtx(context.Background()) + require.NoError(t, err) + require.Equal(t, posAtxID, vValidAtx.ID()) +} diff --git a/activation/validation.go b/activation/validation.go index 3b90163536..ac4c77df28 100644 --- a/activation/validation.go +++ b/activation/validation.go @@ -11,10 +11,13 @@ import ( "github.com/spacemeshos/post/config" "github.com/spacemeshos/post/shared" "github.com/spacemeshos/post/verifying" + "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/activation/metrics" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/common/util" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" ) type ErrAtxNotFound struct { @@ -287,3 +290,129 @@ func (v *Validator) PositioningAtx( } return nil } + +type verifyChainOpts struct { + assumedValidTime time.Time + trustedNodeID types.NodeID +} + +type verifyChainOption func(*verifyChainOpts) + +// AssumeValidBefore configures the validator to assume that ATXs received before the given time are valid. +func AssumeValidBefore(val time.Time) verifyChainOption { + return func(o *verifyChainOpts) { + o.assumedValidTime = val + } +} + +// WithTrustedID configures the validator to assume that ATXs created by the given node ID are valid. +func WithTrustedID(val types.NodeID) verifyChainOption { + return func(o *verifyChainOpts) { + o.trustedNodeID = val + } +} + +var ErrInvalidChain = errors.New("invalid ATX chain") + +func VerifyChain( + ctx context.Context, + db sql.Executor, + id, goldenATXID types.ATXID, + validator nipostValidator, + log *zap.Logger, + opts ...verifyChainOption, +) error { + options := verifyChainOpts{} + for _, opt := range opts { + opt(&options) + } + return verifyChainWithOpts(ctx, db, id, goldenATXID, validator, log, options) +} + +func verifyChainWithOpts( + ctx context.Context, + db sql.Executor, + id, goldenATXID types.ATXID, + validator nipostValidator, + log *zap.Logger, + opts verifyChainOpts, +) error { + atx, err := atxs.Get(db, id) + if err != nil { + return fmt.Errorf("get atx: %w", err) + } + + switch { + case atx.Validity() == types.Valid: + return nil + case atx.Validity() == types.Invalid: + return errors.Join(ErrInvalidChain, errors.New("atx is marked as invalid")) + case atx.Received().Before(opts.assumedValidTime): + return nil + case atx.SmesherID == opts.trustedNodeID: + return nil + } + + // validate POST fully + commitmentAtxId := atx.CommitmentATX + if commitmentAtxId == nil { + if atxId, err := atxs.CommitmentATX(db, atx.SmesherID); err != nil { + return fmt.Errorf("getting commitment atx: %w", err) + } else { + commitmentAtxId = &atxId + } + } + if err := validator.Post( + ctx, + atx.SmesherID, + *commitmentAtxId, + atx.NIPost.Post, + atx.NIPost.PostMetadata, + atx.NumUnits, + ); err != nil { + if err := atxs.SetValidity(db, id, types.Invalid); err != nil { + log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) + } + return errors.Join(ErrInvalidChain, fmt.Errorf("invalid post in ATX %s: %w", id.ShortString(), err)) + } + + err = verifyChainDeps(ctx, db, atx.ActivationTx, goldenATXID, validator, log, opts) + switch { + case err == nil: + if err := atxs.SetValidity(db, id, types.Valid); err != nil { + log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) + } + case errors.Is(err, ErrInvalidChain): + if err := atxs.SetValidity(db, id, types.Invalid); err != nil { + log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) + } + } + return err +} + +func verifyChainDeps( + ctx context.Context, + db sql.Executor, + atx *types.ActivationTx, + goldenATXID types.ATXID, + v nipostValidator, + log *zap.Logger, + opts verifyChainOpts, +) error { + if atx.PrevATXID != types.EmptyATXID { + if err := verifyChainWithOpts(ctx, db, atx.PrevATXID, goldenATXID, v, log, opts); err != nil { + return fmt.Errorf("validating previous ATX %s chain: %w", atx.PrevATXID.ShortString(), err) + } + } + if atx.PositioningATX != goldenATXID { + if err := verifyChainWithOpts(ctx, db, atx.PositioningATX, goldenATXID, v, log, opts); err != nil { + return fmt.Errorf("validating positioning ATX %s chain: %w", atx.PositioningATX.ShortString(), err) + } + } + if atx.CommitmentATX != nil && *atx.CommitmentATX != goldenATXID { + if err := verifyChainWithOpts(ctx, db, *atx.CommitmentATX, goldenATXID, v, log, opts); err != nil { + return fmt.Errorf("validating commitment ATX %s chain: %w", atx.CommitmentATX.ShortString(), err) + } + } + return nil +} diff --git a/activation/validation_test.go b/activation/validation_test.go index af42d52c78..5f097531d1 100644 --- a/activation/validation_test.go +++ b/activation/validation_test.go @@ -5,14 +5,19 @@ import ( "errors" "fmt" "testing" + "time" "github.com/spacemeshos/post/config" "github.com/spacemeshos/post/initialization" "github.com/spacemeshos/post/shared" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" ) func Test_Validation_VRFNonce(t *testing.T) { @@ -517,3 +522,110 @@ func TestValidateMerkleProof(t *testing.T) { require.Error(t, err) }) } + +func TestVerifyChainDeps(t *testing.T) { + ctrl := gomock.NewController(t) + logger := zaptest.NewLogger(t) + db := sql.InMemory() + ctx := context.Background() + goldenATXID := types.ATXID{2, 3, 4} + signer, err := signing.NewEdSigner() + require.NoError(t, err) + + ch := newChallenge(1, types.EmptyATXID, goldenATXID, postGenesisEpoch, &goldenATXID) + nipostData := newNIPostWithChallenge(t, types.HexToHash32(""), []byte("00")) + invalidAtx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) + SignAndFinalizeAtx(signer, invalidAtx) + vInvalidAtx, err := invalidAtx.Verify(0, 1) + require.NoError(t, err) + vInvalidAtx.SetValidity(types.Invalid) + require.NoError(t, atxs.Add(db, vInvalidAtx)) + + t.Run("invalid prev ATX", func(t *testing.T) { + ch = newChallenge(1, vInvalidAtx.ID(), goldenATXID, postGenesisEpoch, nil) + nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("01")) + atx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) + SignAndFinalizeAtx(signer, atx) + vAtx, err := atx.Verify(0, 1) + require.NoError(t, err) + vAtx.SetValidity(types.Unknown) + require.NoError(t, atxs.Add(db, vAtx)) + + v := NewMocknipostValidator(ctrl) + v.EXPECT(). + Post(ctx, signer.NodeID(), goldenATXID, atx.NIPost.Post, atx.NIPost.PostMetadata, atx.NumUnits). + Return(nil) + + err = VerifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) + require.ErrorIs(t, err, ErrInvalidChain) + }) + + t.Run("invalid pos ATX", func(t *testing.T) { + ch = newChallenge(1, types.EmptyATXID, vInvalidAtx.ID(), postGenesisEpoch, nil) + nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("02")) + atx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) + SignAndFinalizeAtx(signer, atx) + vAtx, err := atx.Verify(0, 1) + require.NoError(t, err) + vAtx.SetValidity(types.Unknown) + require.NoError(t, atxs.Add(db, vAtx)) + + v := NewMocknipostValidator(ctrl) + v.EXPECT(). + Post(ctx, signer.NodeID(), goldenATXID, atx.NIPost.Post, atx.NIPost.PostMetadata, atx.NumUnits). + Return(nil) + + err = VerifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) + require.ErrorIs(t, err, ErrInvalidChain) + }) + + t.Run("invalid commitment ATX", func(t *testing.T) { + commitmentAtxID := vInvalidAtx.ID() + ch = newChallenge(1, types.EmptyATXID, goldenATXID, postGenesisEpoch, &commitmentAtxID) + nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("03")) + atx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) + SignAndFinalizeAtx(signer, atx) + vAtx, err := atx.Verify(0, 1) + require.NoError(t, err) + vAtx.SetValidity(types.Unknown) + require.NoError(t, atxs.Add(db, vAtx)) + + v := NewMocknipostValidator(ctrl) + v.EXPECT(). + Post(ctx, signer.NodeID(), commitmentAtxID, atx.NIPost.Post, atx.NIPost.PostMetadata, atx.NumUnits). + Return(nil) + + err = VerifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) + require.ErrorIs(t, err, ErrInvalidChain) + }) + + t.Run("with trusted node ID", func(t *testing.T) { + ch = newChallenge(1, types.EmptyATXID, vInvalidAtx.ID(), postGenesisEpoch, nil) + nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("04")) + atx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) + SignAndFinalizeAtx(signer, atx) + vAtx, err := atx.Verify(0, 1) + require.NoError(t, err) + vAtx.SetValidity(types.Unknown) + require.NoError(t, atxs.Add(db, vAtx)) + + v := NewMocknipostValidator(ctrl) + err = VerifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, WithTrustedID(signer.NodeID())) + require.NoError(t, err) + }) + + t.Run("assume valid if older than X", func(t *testing.T) { + ch = newChallenge(1, types.EmptyATXID, vInvalidAtx.ID(), postGenesisEpoch, nil) + nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("05")) + atx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) + SignAndFinalizeAtx(signer, atx) + vAtx, err := atx.Verify(0, 1) + require.NoError(t, err) + vAtx.SetValidity(types.Unknown) + require.NoError(t, atxs.Add(db, vAtx)) + + v := NewMocknipostValidator(ctrl) + err = VerifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, AssumeValidBefore(time.Now())) + require.NoError(t, err) + }) +} diff --git a/common/types/activation.go b/common/types/activation.go index b308800a86..a8dabe6154 100644 --- a/common/types/activation.go +++ b/common/types/activation.go @@ -22,6 +22,14 @@ func BytesToATXID(buf []byte) (id ATXID) { return id } +type Validity int + +const ( + Unknown Validity = iota + Valid + Invalid +) + // ATXID is a 32-bit hash used to identify an activation transaction. type ATXID Hash32 @@ -161,6 +169,7 @@ type InnerActivationTx struct { id ATXID // non-exported cache of the ATXID effectiveNumUnits uint32 // the number of effective units in the ATX (minimum of this ATX and the previous ATX) received time.Time // time received by node, gossiped or synced + validity Validity // whether the chain is fully verified and OK } // ATXMetadata is the data of ActivationTx that is signed. @@ -313,6 +322,14 @@ func (atx *ActivationTx) Received() time.Time { return atx.received } +func (atx *ActivationTx) Validity() Validity { + return atx.validity +} + +func (atx *ActivationTx) SetValidity(validity Validity) { + atx.validity = validity +} + // Verify an ATX for a given base TickHeight and TickCount. func (atx *ActivationTx) Verify(baseTickHeight, tickCount uint64) (*VerifiedActivationTx, error) { if atx.id == EmptyATXID { diff --git a/node/node.go b/node/node.go index 21d0448d13..3707cf6325 100644 --- a/node/node.go +++ b/node/node.go @@ -921,7 +921,6 @@ func (app *App) initServices(ctx context.Context) error { builderConfig := activation.Config{ GoldenATXID: goldenATXID, - LayersPerEpoch: layersPerEpoch, RegossipInterval: app.Config.RegossipAtxInterval, } atxBuilder := activation.NewBuilder( @@ -940,6 +939,7 @@ func (app *App) initServices(ctx context.Context) error { // TODO(dshulyak) makes no sense. how we ended using it? activation.WithPoetRetryInterval(app.Config.HARE3.PreroundDelay), activation.WithValidator(app.validator), + // TODO(poszu): configure activation.WithAtxValidityDelay() from the config ) malfeasanceHandler := malfeasance.NewHandler( diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 81afb53be0..851c18f6bd 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -10,7 +10,7 @@ import ( ) const fullQuery = `select id, atx, base_tick_height, tick_count, pubkey, - effective_num_units, received, epoch, sequence, coinbase from atxs` + effective_num_units, received, epoch, sequence, coinbase, validity from atxs` type decoderCallback func(*types.VerifiedActivationTx, error) bool @@ -43,6 +43,7 @@ func decoder(fn decoderCallback) sql.Decoder { a.PublishEpoch = types.EpochID(uint32(stmt.ColumnInt(7))) a.Sequence = uint64(stmt.ColumnInt64(8)) stmt.ColumnBytes(9, a.Coinbase[:]) + a.SetValidity(types.Validity(stmt.ColumnInt(10))) v, err := a.Verify(baseTickHeight, tickCount) if err != nil { return fn(nil, err) @@ -300,12 +301,13 @@ func Add(db sql.Executor, atx *types.VerifiedActivationTx) error { stmt.BindInt64(10, int64(atx.TickCount())) stmt.BindInt64(11, int64(atx.Sequence)) stmt.BindBytes(12, atx.Coinbase.Bytes()) + stmt.BindInt64(13, int64(atx.Validity())) } _, err = db.Exec(` insert into atxs (id, epoch, effective_num_units, commitment_atx, nonce, - pubkey, atx, received, base_tick_height, tick_count, sequence, coinbase) - values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12);`, enc, nil) + pubkey, atx, received, base_tick_height, tick_count, sequence, coinbase, validity) + values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13);`, enc, nil) if err != nil { return fmt.Errorf("insert ATX ID %v: %w", atx.ID(), err) } @@ -488,3 +490,16 @@ func IterateAtxs(db sql.Executor, from, to types.EpochID, fn func(*types.Verifie } return derr } + +func SetValidity(db sql.Executor, id types.ATXID, validity types.Validity) error { + _, err := db.Exec("UPDATE atxs SET validity = ?1 where id = ?2;", + func(stmt *sql.Statement) { + stmt.BindInt64(1, int64(validity)) + stmt.BindBytes(2, id.Bytes()) + }, nil, + ) + if err != nil { + return fmt.Errorf("setting validity %v: %w", id, err) + } + return nil +} diff --git a/sql/database.go b/sql/database.go index f0287b0275..a477bfbdc6 100644 --- a/sql/database.go +++ b/sql/database.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "math/rand" "slices" "sort" "sync" @@ -130,15 +129,8 @@ type Opt func(c *conf) // InMemory database for testing. func InMemory(opts ...Opt) *Database { - opts = append(opts, WithConnections(2)) - - var name []rune - runes := []rune("abcdefghijklmnopqrstuvwxyz") - for i := 0; i < 5; i++ { - name = append(name, (runes[rand.Intn(len(runes))])) - } - - db, err := Open(fmt.Sprintf("file:db-%s?mode=memory&cache=shared", string(name)), opts...) + opts = append(opts, WithConnections(1)) + db, err := Open("file::memory:?mode=memory", opts...) if err != nil { panic(err) } diff --git a/sql/migrations/state/0008_next.sql b/sql/migrations/state/0008_next.sql index 77c781b54e..3df04b2b8a 100644 --- a/sql/migrations/state/0008_next.sql +++ b/sql/migrations/state/0008_next.sql @@ -13,3 +13,7 @@ CREATE INDEX rewards_by_coinbase ON rewards (coinbase, layer); CREATE INDEX rewards_by_layer ON rewards (layer asc); INSERT INTO rewards (coinbase, layer, total_reward, layer_reward) SELECT coinbase, layer, total_reward, layer_reward FROM rewards_old; DROP TABLE rewards_old; + +-- For distributed POST verification +ALTER TABLE atxs ADD COLUMN validity INTEGER DEFAULT false; +UPDATE atxs SET validity = 1; \ No newline at end of file From f640fd4db0e5d54efdd05ad30c46eb181cc8f375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 5 Jan 2024 13:41:11 +0100 Subject: [PATCH 06/30] Verify commitment ATX candidate chain --- activation/activation.go | 76 +++++++++++++++++++---------- activation/e2e/nipost_test.go | 6 ++- activation/e2e/validation_test.go | 3 +- activation/post.go | 18 +++++-- activation/post_test.go | 7 ++- activation/validation.go | 2 +- activation/validation_test.go | 10 ++-- api/grpcserver/post_service_test.go | 12 ++++- node/node.go | 1 + 9 files changed, 92 insertions(+), 43 deletions(-) diff --git a/activation/activation.go b/activation/activation.go index a84556b805..f38a7f6b40 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -607,34 +607,21 @@ func (b *Builder) broadcast(ctx context.Context, atx *types.ActivationTx) (int, // getPositioningAtx returns atx id with the highest tick height. func (b *Builder) getPositioningAtx(ctx context.Context) (types.ATXID, error) { - rejectedAtxs := make(map[types.ATXID]struct{}) - filter := func(id types.ATXID) bool { - _, ok := rejectedAtxs[id] - return !ok - } - - for { - select { - case <-ctx.Done(): - return types.ATXID{}, ctx.Err() - default: - } - id, err := atxs.GetIDWithMaxHeight(b.cdb, b.signer.NodeID(), filter) - if err != nil { - if errors.Is(err, sql.ErrNotFound) { - b.log.Info("using golden atx as positioning atx") - return b.goldenATXID, nil - } - return types.ATXID{}, fmt.Errorf("cannot find pos atx: %w", err) - } - validBefore := time.Now().Add(-b.atxValidityDelay) - if err := VerifyChain(ctx, b.cdb, id, b.goldenATXID, b.validator, b.log, AssumeValidBefore(validBefore)); err != nil { - b.log.Info("rejecting candidate for positioning atx", zap.Error(err), zap.Stringer("atx_id", id)) - rejectedAtxs[id] = struct{}{} - } else { - return id, nil - } + id, err := findFullyValidHighTickAtx( + ctx, + b.cdb, + b.signer.NodeID(), + b.goldenATXID, + b.validator, + b.log, + AssumeValidBefore(time.Now().Add(-b.atxValidityDelay)), + WithTrustedID(b.signer.NodeID()), + ) + if errors.Is(err, sql.ErrNotFound) { + b.log.Info("using golden atx as positioning atx") + return b.goldenATXID, nil } + return id, nil } func (b *Builder) Regossip(ctx context.Context) error { @@ -670,3 +657,38 @@ func buildNipostChallengeStartDeadline(roundStart time.Time, gracePeriod time.Du jitter := randomDurationInRange(time.Duration(0), gracePeriod*maxNipostChallengeBuildJitter/100.0) return roundStart.Add(jitter).Add(-gracePeriod) } + +func findFullyValidHighTickAtx( + ctx context.Context, + db sql.Executor, + prefNodeID types.NodeID, + goldenATXID types.ATXID, + validator nipostValidator, + log *zap.Logger, + opts ...verifyChainOption, +) (types.ATXID, error) { + rejectedAtxs := make(map[types.ATXID]struct{}) + filter := func(id types.ATXID) bool { + _, ok := rejectedAtxs[id] + return !ok + } + + for { + select { + case <-ctx.Done(): + return types.ATXID{}, ctx.Err() + default: + } + id, err := atxs.GetIDWithMaxHeight(db, prefNodeID, filter) + if err != nil { + return types.ATXID{}, err + } + + if err := verifyChain(ctx, db, id, goldenATXID, validator, log, opts...); err != nil { + log.Info("rejecting candidate for high-tick atx", zap.Error(err), zap.Stringer("atx_id", id)) + rejectedAtxs[id] = struct{}{} + } else { + return id, nil + } + } +} diff --git a/activation/e2e/nipost_test.go b/activation/e2e/nipost_test.go index 645f9cea2f..d0ddd08019 100644 --- a/activation/e2e/nipost_test.go +++ b/activation/e2e/nipost_test.go @@ -124,8 +124,9 @@ func TestNIPostBuilderWithClients(t *testing.T) { goldenATX := types.ATXID{2, 3, 4} cfg := activation.DefaultPostConfig() cdb := datastore.NewCachedDB(sql.InMemory(), log.NewFromLog(logger)) + validator := activation.NewMocknipostValidator(ctrl) - mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX) + mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX, validator) require.NoError(t, err) opts := activation.DefaultPostSetupOpts() @@ -263,7 +264,8 @@ func TestNewNIPostBuilderNotInitialized(t *testing.T) { cfg := activation.DefaultPostConfig() cdb := datastore.NewCachedDB(sql.InMemory(), log.NewFromLog(logger)) - mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX) + validator := activation.NewMocknipostValidator(gomock.NewController(t)) + mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX, validator) require.NoError(t, err) // ensure that genesis aligns with layer timings diff --git a/activation/e2e/validation_test.go b/activation/e2e/validation_test.go index 4ba2ed8bf7..c498db4bc1 100644 --- a/activation/e2e/validation_test.go +++ b/activation/e2e/validation_test.go @@ -32,7 +32,8 @@ func TestValidator_Validate(t *testing.T) { cfg := activation.DefaultPostConfig() cdb := datastore.NewCachedDB(sql.InMemory(), log.NewFromLog(logger)) - mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX) + validator := activation.NewMocknipostValidator(gomock.NewController(t)) + mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX, validator) require.NoError(t, err) opts := activation.DefaultPostSetupOpts() diff --git a/activation/post.go b/activation/post.go index 82c24b74de..50b90dc07b 100644 --- a/activation/post.go +++ b/activation/post.go @@ -6,6 +6,7 @@ import ( "fmt" "runtime" "sync" + "time" "github.com/spacemeshos/post/config" "github.com/spacemeshos/post/initialization" @@ -171,6 +172,7 @@ type PostSetupManager struct { logger *zap.Logger db *datastore.CachedDB goldenATXID types.ATXID + validator nipostValidator mu sync.Mutex // mu protects setting the values below. lastOpts *PostSetupOpts // the last options used to initiate a Post setup session. @@ -185,6 +187,7 @@ func NewPostSetupManager( logger *zap.Logger, db *datastore.CachedDB, goldenATXID types.ATXID, + validator nipostValidator, ) (*PostSetupManager, error) { mgr := &PostSetupManager{ id: id, @@ -193,6 +196,7 @@ func NewPostSetupManager( db: db, goldenATXID: goldenATXID, state: PostSetupStateNotStarted, + validator: validator, } return mgr, nil @@ -356,10 +360,16 @@ func (mgr *PostSetupManager) commitmentAtx(dataDir string) (types.ATXID, error) // It will use the ATX with the highest height seen by the node and defaults to the goldenATX, // when no ATXs have yet been published. func (mgr *PostSetupManager) findCommitmentAtx() (types.ATXID, error) { - atx, err := atxs.GetIDWithMaxHeight(mgr.db, types.EmptyNodeID, func(a types.ATXID) bool { - // TODO(poszu): verify POST fully - return true - }) + atx, err := findFullyValidHighTickAtx( + context.Background(), + mgr.db, + types.EmptyNodeID, + mgr.goldenATXID, + mgr.validator, + mgr.logger, + // TODO(poszu): the duration could be configurable + AssumeValidBefore(time.Now().Add(-time.Hour*12)), + ) switch { case errors.Is(err, sql.ErrNotFound): mgr.logger.Info("using golden atx as commitment atx") diff --git a/activation/post_test.go b/activation/post_test.go index 107bd94a01..754f266a5a 100644 --- a/activation/post_test.go +++ b/activation/post_test.go @@ -9,6 +9,7 @@ import ( "github.com/spacemeshos/post/initialization" "github.com/spacemeshos/post/shared" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" @@ -340,8 +341,12 @@ func newTestPostManager(tb testing.TB) *testPostManager { goldenATXID := types.ATXID{2, 3, 4} + validator := NewMocknipostValidator(gomock.NewController(tb)) + validator.EXPECT(). + Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes() cdb := datastore.NewCachedDB(sql.InMemory(), logtest.New(tb)) - mgr, err := NewPostSetupManager(id, DefaultPostConfig(), zaptest.NewLogger(tb), cdb, goldenATXID) + mgr, err := NewPostSetupManager(id, DefaultPostConfig(), zaptest.NewLogger(tb), cdb, goldenATXID, validator) require.NoError(tb, err) return &testPostManager{ diff --git a/activation/validation.go b/activation/validation.go index ac4c77df28..9d4e12c3cb 100644 --- a/activation/validation.go +++ b/activation/validation.go @@ -314,7 +314,7 @@ func WithTrustedID(val types.NodeID) verifyChainOption { var ErrInvalidChain = errors.New("invalid ATX chain") -func VerifyChain( +func verifyChain( ctx context.Context, db sql.Executor, id, goldenATXID types.ATXID, diff --git a/activation/validation_test.go b/activation/validation_test.go index 5f097531d1..6a58b93024 100644 --- a/activation/validation_test.go +++ b/activation/validation_test.go @@ -556,7 +556,7 @@ func TestVerifyChainDeps(t *testing.T) { Post(ctx, signer.NodeID(), goldenATXID, atx.NIPost.Post, atx.NIPost.PostMetadata, atx.NumUnits). Return(nil) - err = VerifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) + err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) require.ErrorIs(t, err, ErrInvalidChain) }) @@ -575,7 +575,7 @@ func TestVerifyChainDeps(t *testing.T) { Post(ctx, signer.NodeID(), goldenATXID, atx.NIPost.Post, atx.NIPost.PostMetadata, atx.NumUnits). Return(nil) - err = VerifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) + err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) require.ErrorIs(t, err, ErrInvalidChain) }) @@ -595,7 +595,7 @@ func TestVerifyChainDeps(t *testing.T) { Post(ctx, signer.NodeID(), commitmentAtxID, atx.NIPost.Post, atx.NIPost.PostMetadata, atx.NumUnits). Return(nil) - err = VerifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) + err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) require.ErrorIs(t, err, ErrInvalidChain) }) @@ -610,7 +610,7 @@ func TestVerifyChainDeps(t *testing.T) { require.NoError(t, atxs.Add(db, vAtx)) v := NewMocknipostValidator(ctrl) - err = VerifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, WithTrustedID(signer.NodeID())) + err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, WithTrustedID(signer.NodeID())) require.NoError(t, err) }) @@ -625,7 +625,7 @@ func TestVerifyChainDeps(t *testing.T) { require.NoError(t, atxs.Add(db, vAtx)) v := NewMocknipostValidator(ctrl) - err = VerifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, AssumeValidBefore(time.Now())) + err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, AssumeValidBefore(time.Now())) require.NoError(t, err) }) } diff --git a/api/grpcserver/post_service_test.go b/api/grpcserver/post_service_test.go index d8803e7bf4..f9dd25afcd 100644 --- a/api/grpcserver/post_service_test.go +++ b/api/grpcserver/post_service_test.go @@ -43,8 +43,12 @@ func launchPostSupervisor( id := sig.NodeID() goldenATXID := types.RandomATXID() + validator := activation.NewMocknipostValidator(gomock.NewController(tb)) + validator.EXPECT(). + Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes() cdb := datastore.NewCachedDB(sql.InMemory(), logtest.New(tb)) - mgr, err := activation.NewPostSetupManager(id, postCfg, log.Named("post manager"), cdb, goldenATXID) + mgr, err := activation.NewPostSetupManager(id, postCfg, log.Named("post manager"), cdb, goldenATXID, validator) require.NoError(tb, err) syncer := activation.NewMocksyncer(gomock.NewController(tb)) @@ -83,8 +87,12 @@ func launchPostSupervisorTLS( id := sig.NodeID() goldenATXID := types.RandomATXID() + validator := activation.NewMocknipostValidator(gomock.NewController(tb)) + validator.EXPECT(). + Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes() cdb := datastore.NewCachedDB(sql.InMemory(), logtest.New(tb)) - mgr, err := activation.NewPostSetupManager(id, postCfg, log.Named("post manager"), cdb, goldenATXID) + mgr, err := activation.NewPostSetupManager(id, postCfg, log.Named("post manager"), cdb, goldenATXID, validator) require.NoError(tb, err) syncer := activation.NewMocksyncer(gomock.NewController(tb)) diff --git a/node/node.go b/node/node.go index 3707cf6325..88ac399a98 100644 --- a/node/node.go +++ b/node/node.go @@ -887,6 +887,7 @@ func (app *App) initServices(ctx context.Context) error { app.Config.POST, app.addLogger(PostLogger, lg).Zap(), app.cachedDB, goldenATXID, + app.validator, ) if err != nil { return fmt.Errorf("create post setup manager: %v", err) From 8e1c9e98298cfb2d0c89040aea7ff0f4c664f1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 5 Jan 2024 13:51:02 +0100 Subject: [PATCH 07/30] Move SQL migration to 0010 --- sql/migrations/state/0008_rewards.sql | 4 ---- sql/migrations/state/0010_atx_validity.sql | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 sql/migrations/state/0010_atx_validity.sql diff --git a/sql/migrations/state/0008_rewards.sql b/sql/migrations/state/0008_rewards.sql index 3df04b2b8a..77c781b54e 100644 --- a/sql/migrations/state/0008_rewards.sql +++ b/sql/migrations/state/0008_rewards.sql @@ -13,7 +13,3 @@ CREATE INDEX rewards_by_coinbase ON rewards (coinbase, layer); CREATE INDEX rewards_by_layer ON rewards (layer asc); INSERT INTO rewards (coinbase, layer, total_reward, layer_reward) SELECT coinbase, layer, total_reward, layer_reward FROM rewards_old; DROP TABLE rewards_old; - --- For distributed POST verification -ALTER TABLE atxs ADD COLUMN validity INTEGER DEFAULT false; -UPDATE atxs SET validity = 1; \ No newline at end of file diff --git a/sql/migrations/state/0010_atx_validity.sql b/sql/migrations/state/0010_atx_validity.sql new file mode 100644 index 0000000000..9396d18424 --- /dev/null +++ b/sql/migrations/state/0010_atx_validity.sql @@ -0,0 +1,3 @@ +-- For distributed POST verification +ALTER TABLE atxs ADD COLUMN validity INTEGER DEFAULT false; +UPDATE atxs SET validity = 1; From 6e80ec13224ab8e6fe317d395fc12b26c84b64b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 11 Jan 2024 09:28:01 +0100 Subject: [PATCH 08/30] Fix tests --- activation/activation_test.go | 45 ++++++++++++++--------------------- activation/handler_test.go | 2 +- activation/post_test.go | 6 ++++- activation/validation_test.go | 24 +++++++++---------- sql/atxs/atxs.go | 25 ++++++++++--------- sql/database_test.go | 2 +- sql/migrations_test.go | 2 +- 7 files changed, 52 insertions(+), 54 deletions(-) diff --git a/activation/activation_test.go b/activation/activation_test.go index f484afa179..0c15be313c 100644 --- a/activation/activation_test.go +++ b/activation/activation_test.go @@ -252,15 +252,6 @@ func publishAtx( return built, err } -func addPrevAtx(t *testing.T, db sql.Executor, epoch types.EpochID, sig *signing.EdSigner) *types.VerifiedActivationTx { - challenge := types.NIPostChallenge{ - PublishEpoch: epoch, - } - atx := types.NewActivationTx(challenge, types.Address{}, nil, 2, nil) - atx.SetEffectiveNumUnits(2) - return addAtx(t, db, sig, atx) -} - func addAtx(t *testing.T, db sql.Executor, sig *signing.EdSigner, atx *types.ActivationTx) *types.VerifiedActivationTx { require.NoError(t, SignAndFinalizeAtx(sig, atx)) atx.SetEffectiveNumUnits(atx.NumUnits) @@ -417,7 +408,7 @@ func TestBuilder_PublishActivationTx_HappyFlow(t *testing.T) { ch := newChallenge(1, types.EmptyATXID, tab.goldenATXID, posEpoch, &tab.goldenATXID) nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), []byte("66666")) prevAtx := newAtx(t, tab.sig, ch, nipostData.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(tab.sig, prevAtx) + require.NoError(t, SignAndFinalizeAtx(tab.sig, prevAtx)) vPrevAtx, err := prevAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPrevAtx)) @@ -451,7 +442,7 @@ func TestBuilder_Loop_WaitsOnStaleChallenge(t *testing.T) { ch := newChallenge(1, types.EmptyATXID, tab.goldenATXID, postGenesisEpoch, &tab.goldenATXID) nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), []byte("66666")) prevAtx := newAtx(t, tab.sig, ch, nipostData.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(tab.sig, prevAtx) + require.NoError(t, SignAndFinalizeAtx(tab.sig, prevAtx)) vPrevAtx, err := prevAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPrevAtx)) @@ -497,7 +488,7 @@ func TestBuilder_PublishActivationTx_FaultyNet(t *testing.T) { ch := newChallenge(1, types.EmptyATXID, tab.goldenATXID, postGenesisEpoch, &tab.goldenATXID) nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), []byte("66666")) prevAtx := newAtx(t, tab.sig, ch, nipostData.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(tab.sig, prevAtx) + require.NoError(t, SignAndFinalizeAtx(tab.sig, prevAtx)) vPrevAtx, err := prevAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPrevAtx)) @@ -580,7 +571,7 @@ func TestBuilder_PublishActivationTx_UsesExistingChallengeOnLatePublish(t *testi challenge := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, postGenesisEpoch, nil) nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), []byte("66666")) prevAtx := newAtx(t, tab.sig, challenge, nipostData.NIPost, posEpoch.Uint32(), types.Address{}) - SignAndFinalizeAtx(tab.sig, prevAtx) + require.NoError(t, SignAndFinalizeAtx(tab.sig, prevAtx)) vPrevAtx, err := prevAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPrevAtx)) @@ -658,7 +649,7 @@ func TestBuilder_PublishActivationTx_RebuildNIPostWhenTargetEpochPassed(t *testi ch := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, posEpoch, nil) nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), []byte("66666")) prevAtx := newAtx(t, tab.sig, ch, nipostData.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(tab.sig, prevAtx) + require.NoError(t, SignAndFinalizeAtx(tab.sig, prevAtx)) vPrevAtx, err := prevAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPrevAtx)) @@ -723,7 +714,7 @@ func TestBuilder_PublishActivationTx_RebuildNIPostWhenTargetEpochPassed(t *testi currLayer = posEpoch.FirstLayer() ch = newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, posEpoch, nil) posAtx := newAtx(t, tab.sig, ch, nipostData.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(tab.sig, posAtx) + require.NoError(t, SignAndFinalizeAtx(tab.sig, posAtx)) vPosAtx, err := posAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPosAtx)) @@ -749,7 +740,7 @@ func TestBuilder_PublishActivationTx_NoPrevATX(t *testing.T) { otherSigner, err := signing.NewEdSigner() require.NoError(t, err) posAtx := newAtx(t, otherSigner, challenge, nipostData.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(otherSigner, posAtx) + require.NoError(t, SignAndFinalizeAtx(otherSigner, posAtx)) vPosAtx, err := posAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPosAtx)) @@ -781,7 +772,7 @@ func TestBuilder_PublishActivationTx_NoPrevATX_PublishFails_InitialPost_preserve otherSigner, err := signing.NewEdSigner() require.NoError(t, err) posAtx := newAtx(t, otherSigner, challenge, nipostData.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(otherSigner, posAtx) + require.NoError(t, SignAndFinalizeAtx(otherSigner, posAtx)) vPosAtx, err := posAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPosAtx)) @@ -864,7 +855,7 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { poetBytes := []byte("66666") nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), poetBytes) posAtx := newAtx(t, otherSigner, challenge, nipostData.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(otherSigner, posAtx) + require.NoError(t, SignAndFinalizeAtx(otherSigner, posAtx)) vPosAtx, err := posAtx.Verify(0, 2) r.NoError(err) r.NoError(atxs.Add(tab.cdb, vPosAtx)) @@ -873,7 +864,7 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { challenge.InitialPost = initialPost prevAtx := newAtx(t, tab.sig, challenge, nipostData.NIPost, 2, types.Address{}) prevAtx.InitialPost = initialPost - SignAndFinalizeAtx(tab.sig, prevAtx) + require.NoError(t, SignAndFinalizeAtx(tab.sig, prevAtx)) vPrevAtx, err := prevAtx.Verify(0, 1) r.NoError(err) r.NoError(atxs.Add(tab.cdb, vPrevAtx)) @@ -964,7 +955,7 @@ func TestBuilder_PublishActivationTx_TargetsEpochBasedOnPosAtx(t *testing.T) { poetBytes := []byte("66666") nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), poetBytes) posAtx := newAtx(t, otherSigner, challenge, nipostData.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(otherSigner, posAtx) + require.NoError(t, SignAndFinalizeAtx(otherSigner, posAtx)) vPosAtx, err := posAtx.Verify(0, 1) r.NoError(err) r.NoError(atxs.Add(tab.cdb, vPosAtx)) @@ -1052,7 +1043,7 @@ func TestBuilder_PublishActivationTx_FailsWhenNIPostBuilderFails(t *testing.T) { ch := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, posEpoch, nil) nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), []byte("66666")) posAtx := newAtx(t, tab.sig, ch, nipostData.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(tab.sig, posAtx) + require.NoError(t, SignAndFinalizeAtx(tab.sig, posAtx)) vPosAtx, err := posAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPosAtx)) @@ -1160,7 +1151,7 @@ func TestBuilder_RetryPublishActivationTx(t *testing.T) { poetBytes := []byte("66666") nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), poetBytes) prevAtx := newAtx(t, tab.sig, challenge, nipostData.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(tab.sig, prevAtx) + require.NoError(t, SignAndFinalizeAtx(tab.sig, prevAtx)) vPrevAtx, err := prevAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPrevAtx)) @@ -1283,7 +1274,7 @@ func TestBuilder_InitialProofGeneratedOnce(t *testing.T) { poetByte := []byte("66666") nipost := newNIPostWithChallenge(t, types.HexToHash32("55555"), poetByte) prevAtx := newAtx(t, tab.sig, challenge, nipost.NIPost, 2, types.Address{}) - SignAndFinalizeAtx(tab.sig, prevAtx) + require.NoError(t, SignAndFinalizeAtx(tab.sig, prevAtx)) vPrevAtx, err := prevAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vPrevAtx)) @@ -1445,8 +1436,8 @@ func TestGetPositioningAtxPicksAtxWithValidChain(t *testing.T) { require.NoError(t, err) ch := newChallenge(1, types.EmptyATXID, tab.goldenATXID, postGenesisEpoch, &tab.goldenATXID) nipostData := newNIPostWithChallenge(t, types.HexToHash32("for invalid"), []byte("66")) - invalidAtx := newAtx(t, sigInvalid, ch, nipostData, 2, types.Address{}) - SignAndFinalizeAtx(sigInvalid, invalidAtx) + invalidAtx := newAtx(t, sigInvalid, ch, nipostData.NIPost, 2, types.Address{}) + require.NoError(t, SignAndFinalizeAtx(sigInvalid, invalidAtx)) vInvalidAtx, err := invalidAtx.Verify(0, 100) require.NoError(t, err) vInvalidAtx.SetValidity(types.Invalid) @@ -1457,8 +1448,8 @@ func TestGetPositioningAtxPicksAtxWithValidChain(t *testing.T) { require.NoError(t, err) ch = newChallenge(1, types.EmptyATXID, tab.goldenATXID, postGenesisEpoch, &tab.goldenATXID) nipostData = newNIPostWithChallenge(t, types.HexToHash32("for valid"), []byte("77")) - validAtx := newAtx(t, sigValid, ch, nipostData, 2, types.Address{}) - SignAndFinalizeAtx(sigValid, validAtx) + validAtx := newAtx(t, sigValid, ch, nipostData.NIPost, 2, types.Address{}) + require.NoError(t, SignAndFinalizeAtx(sigValid, validAtx)) vValidAtx, err := validAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vValidAtx)) diff --git a/activation/handler_test.go b/activation/handler_test.go index 11f43b8885..8b7b30ffe3 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -1024,7 +1024,7 @@ func TestHandler_PublishesPostMalfeasanceProofs(t *testing.T) { ch.InitialPost = &types.Post{} nipost := newNIPostWithChallenge(t, types.HexToHash32("0x3333"), []byte{0x76, 0x45}) - atx := newAtx(t, sig, ch, nipost, 100, types.GenerateAddress([]byte("aaaa"))) + atx := newAtx(t, sig, ch, nipost.NIPost, 100, types.GenerateAddress([]byte("aaaa"))) atx.NodeID = &nodeID vrfNonce := types.VRFPostIndex(0) atx.VRFNonce = &vrfNonce diff --git a/activation/post_test.go b/activation/post_test.go index 754f266a5a..2c77a689dd 100644 --- a/activation/post_test.go +++ b/activation/post_test.go @@ -274,7 +274,11 @@ func TestPostSetupManager_Stop_WhileInProgress(t *testing.T) { func TestPostSetupManager_findCommitmentAtx_UsesLatestAtx(t *testing.T) { mgr := newTestPostManager(t) - latestAtx := addPrevAtx(t, mgr.db, 1, mgr.signer) + ch := newChallenge(0, types.EmptyATXID, mgr.goldenATXID, 2, &mgr.goldenATXID) + nipostData := newNIPostWithChallenge(t, types.HexToHash32("55555"), []byte("66666")) + latestAtx := newAtx(t, mgr.signer, ch, nipostData.NIPost, 2, types.Address{}) + addAtx(t, mgr.db, mgr.signer, latestAtx) + atx, err := mgr.findCommitmentAtx() require.NoError(t, err) require.Equal(t, latestAtx.ID(), atx) diff --git a/activation/validation_test.go b/activation/validation_test.go index 6a58b93024..1f244bc907 100644 --- a/activation/validation_test.go +++ b/activation/validation_test.go @@ -534,8 +534,8 @@ func TestVerifyChainDeps(t *testing.T) { ch := newChallenge(1, types.EmptyATXID, goldenATXID, postGenesisEpoch, &goldenATXID) nipostData := newNIPostWithChallenge(t, types.HexToHash32(""), []byte("00")) - invalidAtx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) - SignAndFinalizeAtx(signer, invalidAtx) + invalidAtx := newAtx(t, signer, ch, nipostData.NIPost, 2, types.Address{}) + require.NoError(t, SignAndFinalizeAtx(signer, invalidAtx)) vInvalidAtx, err := invalidAtx.Verify(0, 1) require.NoError(t, err) vInvalidAtx.SetValidity(types.Invalid) @@ -544,8 +544,8 @@ func TestVerifyChainDeps(t *testing.T) { t.Run("invalid prev ATX", func(t *testing.T) { ch = newChallenge(1, vInvalidAtx.ID(), goldenATXID, postGenesisEpoch, nil) nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("01")) - atx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) - SignAndFinalizeAtx(signer, atx) + atx := newAtx(t, signer, ch, nipostData.NIPost, 2, types.Address{}) + require.NoError(t, SignAndFinalizeAtx(signer, atx)) vAtx, err := atx.Verify(0, 1) require.NoError(t, err) vAtx.SetValidity(types.Unknown) @@ -563,8 +563,8 @@ func TestVerifyChainDeps(t *testing.T) { t.Run("invalid pos ATX", func(t *testing.T) { ch = newChallenge(1, types.EmptyATXID, vInvalidAtx.ID(), postGenesisEpoch, nil) nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("02")) - atx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) - SignAndFinalizeAtx(signer, atx) + atx := newAtx(t, signer, ch, nipostData.NIPost, 2, types.Address{}) + require.NoError(t, SignAndFinalizeAtx(signer, atx)) vAtx, err := atx.Verify(0, 1) require.NoError(t, err) vAtx.SetValidity(types.Unknown) @@ -583,8 +583,8 @@ func TestVerifyChainDeps(t *testing.T) { commitmentAtxID := vInvalidAtx.ID() ch = newChallenge(1, types.EmptyATXID, goldenATXID, postGenesisEpoch, &commitmentAtxID) nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("03")) - atx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) - SignAndFinalizeAtx(signer, atx) + atx := newAtx(t, signer, ch, nipostData.NIPost, 2, types.Address{}) + require.NoError(t, SignAndFinalizeAtx(signer, atx)) vAtx, err := atx.Verify(0, 1) require.NoError(t, err) vAtx.SetValidity(types.Unknown) @@ -602,8 +602,8 @@ func TestVerifyChainDeps(t *testing.T) { t.Run("with trusted node ID", func(t *testing.T) { ch = newChallenge(1, types.EmptyATXID, vInvalidAtx.ID(), postGenesisEpoch, nil) nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("04")) - atx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) - SignAndFinalizeAtx(signer, atx) + atx := newAtx(t, signer, ch, nipostData.NIPost, 2, types.Address{}) + require.NoError(t, SignAndFinalizeAtx(signer, atx)) vAtx, err := atx.Verify(0, 1) require.NoError(t, err) vAtx.SetValidity(types.Unknown) @@ -617,8 +617,8 @@ func TestVerifyChainDeps(t *testing.T) { t.Run("assume valid if older than X", func(t *testing.T) { ch = newChallenge(1, types.EmptyATXID, vInvalidAtx.ID(), postGenesisEpoch, nil) nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("05")) - atx := newAtx(t, signer, ch, nipostData, 2, types.Address{}) - SignAndFinalizeAtx(signer, atx) + atx := newAtx(t, signer, ch, nipostData.NIPost, 2, types.Address{}) + require.NoError(t, SignAndFinalizeAtx(signer, atx)) vAtx, err := atx.Verify(0, 1) require.NoError(t, err) vAtx.SetValidity(types.Unknown) diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 851c18f6bd..ff35492af9 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -328,8 +328,8 @@ func GetIDWithMaxHeight(db sql.Executor, pref types.NodeID, filter Filter) (type filter = FilterAll } var ( - rst types.ATXID - max uint64 + rst types.ATXID + highest uint64 ) dec := func(stmt *sql.Statement) bool { var id types.ATXID @@ -339,15 +339,15 @@ func GetIDWithMaxHeight(db sql.Executor, pref types.NodeID, filter Filter) (type _ = epoch switch { - case height < max: + case height < highest: // Results are ordered by height, so we can stop once we see a lower height. return false - case height > max && filter(id): - max = height + case height > highest && filter(id): + highest = height rst = id // We can stop on the first ATX if `pref` is empty. return pref != types.EmptyNodeID - case height == max && filter(id): + case height == highest && filter(id): // prefer atxs from `pref` var smesher types.NodeID stmt.ColumnBytes(2, smesher[:]) @@ -361,15 +361,18 @@ func GetIDWithMaxHeight(db sql.Executor, pref types.NodeID, filter Filter) (type return true } - if rows, err := db.Exec(` + _, err := db.Exec(` SELECT id, base_tick_height + tick_count AS height, pubkey, epoch FROM atxs LEFT JOIN identities using(pubkey) WHERE identities.pubkey is null and epoch >= (select max(epoch) from atxs)-1 - ORDER BY height DESC, epoch DESC;`, nil, dec); err != nil { - return types.ATXID{}, fmt.Errorf("select positioning atx: %w", err) - } else if rows == 0 { - return types.ATXID{}, sql.ErrNotFound + ORDER BY height DESC, epoch DESC;`, nil, dec) + switch { + case err != nil: + return types.ATXID{}, fmt.Errorf("selecting high-tick atx: %w", err) + case rst == types.EmptyATXID: + return types.ATXID{}, fmt.Errorf("selecting high-tick atx: %w", sql.ErrNotFound) } + return rst, nil } diff --git a/sql/database_test.go b/sql/database_test.go index 67a7984e88..47fd90dfe0 100644 --- a/sql/database_test.go +++ b/sql/database_test.go @@ -135,7 +135,7 @@ func TestDatabaseVacuumState(t *testing.T) { db, err = Open("file:"+dbFile, WithMigrations([]Migration{}), - WithVacuumState(9), + WithVacuumState(1000), ) require.NoError(t, err) require.NoError(t, db.Close()) diff --git a/sql/migrations_test.go b/sql/migrations_test.go index edc8951a67..9804cfabd9 100644 --- a/sql/migrations_test.go +++ b/sql/migrations_test.go @@ -15,5 +15,5 @@ func TestMigrationsAppliedOnce(t *testing.T) { return true }) require.NoError(t, err) - require.Equal(t, version, 9) + require.Equal(t, version, 10) } From ce86414f58bd33fefd2d34d8fc9a7be6bc8eb18d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 11 Jan 2024 12:38:43 +0100 Subject: [PATCH 09/30] Configurable delay after which POST is assumed valid --- activation/activation.go | 12 +++++++----- activation/post.go | 23 ++++++++++++++++++++--- activation/validation.go | 19 +++++++++++++++---- activation/validation_test.go | 4 ++-- config/config.go | 7 +++++++ node/node.go | 3 ++- 6 files changed, 53 insertions(+), 15 deletions(-) diff --git a/activation/activation.go b/activation/activation.go index 966863e2c2..5c7d619a5c 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -90,14 +90,15 @@ type Builder struct { stop context.CancelFunc poetCfg PoetConfig poetRetryInterval time.Duration - atxValidityDelay time.Duration // delay before an atx is considered valid (counting from the time it was received) + // delay before PoST in ATX is considered valid (counting from the time it was received) + postValidityDelay time.Duration } type BuilderOption func(*Builder) -func WithAtxValidityDelay(delay time.Duration) BuilderOption { +func WithPostValidityDelay(delay time.Duration) BuilderOption { return func(b *Builder) { - b.atxValidityDelay = delay + b.postValidityDelay = delay } } @@ -157,6 +158,7 @@ func NewBuilder( syncer: syncer, log: log, poetRetryInterval: defaultPoetRetryInterval, + postValidityDelay: 12 * time.Hour, } for _, opt := range opts { opt(b) @@ -622,8 +624,8 @@ func (b *Builder) getPositioningAtx(ctx context.Context) (types.ATXID, error) { b.goldenATXID, b.validator, b.log, - AssumeValidBefore(time.Now().Add(-b.atxValidityDelay)), - WithTrustedID(b.signer.NodeID()), + assumeValidBefore(time.Now().Add(-b.postValidityDelay)), + withTrustedID(b.signer.NodeID()), ) if errors.Is(err, sql.ErrNotFound) { b.log.Info("using golden atx as positioning atx") diff --git a/activation/post.go b/activation/post.go index c771ada3f9..6e083f5126 100644 --- a/activation/post.go +++ b/activation/post.go @@ -180,6 +180,19 @@ type PostSetupManager struct { lastOpts *PostSetupOpts // the last options used to initiate a Post setup session. state PostSetupState // state is the current state of the Post setup. init *initialization.Initializer // init is the current initializer instance. + + // delay before PoST in ATX is considered valid (counting from the time it was received) + // used to decide whether to fully verify a candidate for commitment ATX + postValidityDelay time.Duration +} + +type PostSetupManagerOpt func(*PostSetupManager) + +// PostValidityDelay sets the delay before PoST in ATX is considered valid. +func PostValidityDelay(delay time.Duration) PostSetupManagerOpt { + return func(mgr *PostSetupManager) { + mgr.postValidityDelay = delay + } } // NewPostSetupManager creates a new instance of PostSetupManager. @@ -190,6 +203,7 @@ func NewPostSetupManager( db *datastore.CachedDB, goldenATXID types.ATXID, validator nipostValidator, + opts ...PostSetupManagerOpt, ) (*PostSetupManager, error) { mgr := &PostSetupManager{ id: id, @@ -199,8 +213,12 @@ func NewPostSetupManager( goldenATXID: goldenATXID, state: PostSetupStateNotStarted, validator: validator, - } + postValidityDelay: 12 * time.Hour, + } + for _, opt := range opts { + opt(mgr) + } return mgr, nil } @@ -369,8 +387,7 @@ func (mgr *PostSetupManager) findCommitmentAtx() (types.ATXID, error) { mgr.goldenATXID, mgr.validator, mgr.logger, - // TODO(poszu): the duration could be configurable - AssumeValidBefore(time.Now().Add(-time.Hour*12)), + assumeValidBefore(time.Now().Add(-mgr.postValidityDelay)), ) switch { case errors.Is(err, sql.ErrNotFound): diff --git a/activation/validation.go b/activation/validation.go index 9d4e12c3cb..97f88b614a 100644 --- a/activation/validation.go +++ b/activation/validation.go @@ -298,15 +298,15 @@ type verifyChainOpts struct { type verifyChainOption func(*verifyChainOpts) -// AssumeValidBefore configures the validator to assume that ATXs received before the given time are valid. -func AssumeValidBefore(val time.Time) verifyChainOption { +// assumeValidBefore configures the validator to assume that ATXs received before the given time are valid. +func assumeValidBefore(val time.Time) verifyChainOption { return func(o *verifyChainOpts) { o.assumedValidTime = val } } -// WithTrustedID configures the validator to assume that ATXs created by the given node ID are valid. -func WithTrustedID(val types.NodeID) verifyChainOption { +// withTrustedID configures the validator to assume that ATXs created by the given node ID are valid. +func withTrustedID(val types.NodeID) verifyChainOption { return func(o *verifyChainOpts) { o.trustedNodeID = val } @@ -322,6 +322,7 @@ func verifyChain( log *zap.Logger, opts ...verifyChainOption, ) error { + log.Info("verifying ATX chain", zap.Stringer("atx_id", id)) options := verifyChainOpts{} for _, opt := range opts { opt(&options) @@ -344,12 +345,22 @@ func verifyChainWithOpts( switch { case atx.Validity() == types.Valid: + log.Debug("not verifying ATX chain", zap.Stringer("atx_id", id), zap.String("reason", "already verified")) return nil case atx.Validity() == types.Invalid: + log.Debug("not verifying ATX chain", zap.Stringer("atx_id", id), zap.String("reason", "invalid")) return errors.Join(ErrInvalidChain, errors.New("atx is marked as invalid")) case atx.Received().Before(opts.assumedValidTime): + log.Debug( + "not verifying ATX chain", + zap.Stringer("atx_id", id), + zap.String("reason", "assumed valid"), + zap.Time("received", atx.Received()), + zap.Time("valid_before", opts.assumedValidTime), + ) return nil case atx.SmesherID == opts.trustedNodeID: + log.Debug("not verifying ATX chain", zap.Stringer("atx_id", id), zap.String("reason", "trusted")) return nil } diff --git a/activation/validation_test.go b/activation/validation_test.go index 1f244bc907..1244a4d016 100644 --- a/activation/validation_test.go +++ b/activation/validation_test.go @@ -610,7 +610,7 @@ func TestVerifyChainDeps(t *testing.T) { require.NoError(t, atxs.Add(db, vAtx)) v := NewMocknipostValidator(ctrl) - err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, WithTrustedID(signer.NodeID())) + err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, withTrustedID(signer.NodeID())) require.NoError(t, err) }) @@ -625,7 +625,7 @@ func TestVerifyChainDeps(t *testing.T) { require.NoError(t, atxs.Add(db, vAtx)) v := NewMocknipostValidator(ctrl) - err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, AssumeValidBefore(time.Now())) + err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, assumeValidBefore(time.Now())) require.NoError(t, err) }) } diff --git a/config/config.go b/config/config.go index 42da28bb1e..83558cf822 100644 --- a/config/config.go +++ b/config/config.go @@ -132,6 +132,12 @@ type BaseConfig struct { // See grading fuction in miner/proposals_builder.go ATXGradeDelay time.Duration `mapstructure:"atx-grade-delay"` + // PostValidDelay is the time after which a PoST is considered valid + // counting from the time an ATX was received. + // Before that time, the PoST must be fully verified. + // After that time, we depend on PoST malfeasance proofs. + PostValidDelay time.Duration `mapstructure:"post-valid-delay"` + // NoMainOverride forces the "nomain" builds to run on the mainnet NoMainOverride bool `mapstructure:"no-main-override"` } @@ -219,6 +225,7 @@ func defaultBaseConfig() BaseConfig { DatabasePruneInterval: 30 * time.Minute, NetworkHRP: "sm", ATXGradeDelay: 10 * time.Second, + PostValidDelay: 12 * time.Hour, } } diff --git a/node/node.go b/node/node.go index bbfd283763..1f24334a62 100644 --- a/node/node.go +++ b/node/node.go @@ -894,6 +894,7 @@ func (app *App) initServices(ctx context.Context) error { app.addLogger(PostLogger, lg).Zap(), app.cachedDB, goldenATXID, app.validator, + activation.PostValidityDelay(app.Config.PostValidDelay), ) if err != nil { return fmt.Errorf("create post setup manager: %v", err) @@ -946,7 +947,7 @@ func (app *App) initServices(ctx context.Context) error { // TODO(dshulyak) makes no sense. how we ended using it? activation.WithPoetRetryInterval(app.Config.HARE3.PreroundDelay), activation.WithValidator(app.validator), - // TODO(poszu): configure activation.WithAtxValidityDelay() from the config + activation.WithPostValidityDelay(app.Config.PostValidDelay), ) malfeasanceHandler := malfeasance.NewHandler( From 4552066445fa36e1e3ff6a256cd8f1b3d3062646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 11 Jan 2024 20:00:52 +0100 Subject: [PATCH 10/30] [wip] systest for distributed post verification --- activation/post.go | 5 +- node/node.go | 3 +- systest/Dockerfile | 19 +- systest/cluster/cluster.go | 28 +- systest/tests/common.go | 29 +++ .../distributed_post_verification_test.go | 239 ++++++++++++++++++ 6 files changed, 311 insertions(+), 12 deletions(-) create mode 100644 systest/tests/distributed_post_verification_test.go diff --git a/activation/post.go b/activation/post.go index 6e083f5126..e8caa9f751 100644 --- a/activation/post.go +++ b/activation/post.go @@ -13,7 +13,6 @@ import ( "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/events" "github.com/spacemeshos/go-spacemesh/metrics/public" "github.com/spacemeshos/go-spacemesh/sql" @@ -172,7 +171,7 @@ type PostSetupManager struct { cfg PostConfig logger *zap.Logger - db *datastore.CachedDB + db *sql.Database goldenATXID types.ATXID validator nipostValidator @@ -200,7 +199,7 @@ func NewPostSetupManager( id types.NodeID, cfg PostConfig, logger *zap.Logger, - db *datastore.CachedDB, + db *sql.Database, goldenATXID types.ATXID, validator nipostValidator, opts ...PostSetupManagerOpt, diff --git a/node/node.go b/node/node.go index 1f24334a62..9d3f999976 100644 --- a/node/node.go +++ b/node/node.go @@ -892,7 +892,8 @@ func (app *App) initServices(ctx context.Context) error { app.edSgn.NodeID(), app.Config.POST, app.addLogger(PostLogger, lg).Zap(), - app.cachedDB, goldenATXID, + app.cachedDB.Database, + goldenATXID, app.validator, activation.PostValidityDelay(app.Config.PostValidDelay), ) diff --git a/systest/Dockerfile b/systest/Dockerfile index bd12f679c9..12c84a3ab0 100644 --- a/systest/Dockerfile +++ b/systest/Dockerfile @@ -1,5 +1,7 @@ -FROM golang:1.21-alpine as build -RUN apk add libc6-compat gcc musl-dev +FROM golang:1.21 as build +RUN apt-get update -q +RUN apt-get install -qy ocl-icd-opencl-dev libpocl2 unzip + WORKDIR /build/ COPY go.mod . @@ -7,7 +9,18 @@ COPY go.sum . RUN go mod download COPY . . +RUN make get-postrs-service +RUN make get-postrs-lib +RUN make go-env-test + RUN --mount=type=cache,target=/root/.cache/go-build go test -failfast -v -c -o /build/tests.test ./systest/tests/ -FROM alpine +FROM debian:bookworm +RUN apt-get update -q +RUN apt-get install -qy libpocl2 + COPY --from=build /build/tests.test /bin/tests +COPY --from=build /build/build/libpost.so /bin/libpost.so +COPY --from=build /build/build/service /bin/service +ENV LD_LIBRARY_PATH="/bin/" +ENV RUST_LOG=trace diff --git a/systest/cluster/cluster.go b/systest/cluster/cluster.go index 41407b57cb..68f885cf3e 100644 --- a/systest/cluster/cluster.go +++ b/systest/cluster/cluster.go @@ -55,6 +55,10 @@ func MakePoetEndpoint(ith int) string { return fmt.Sprintf("http://%s:%d", createPoetIdentifier(ith), poetPort) } +func MakePoetGlobalEndpoint(testNamespace string, ith int) string { + return fmt.Sprintf("http://%s.%s:%d", createPoetIdentifier(ith), testNamespace, poetPort) +} + // Deterministically generate poet keys for given instance. func MakePoetKey(ith int) (ed25519.PublicKey, ed25519.PrivateKey) { seed := make([]byte, ed25519.SeedSize) @@ -170,8 +174,9 @@ func New(cctx *testcontext.Context, opts ...Opt) *Cluster { bootstrapperFlags: map[string]DeploymentFlag{}, bootstrapEpochs: []int{2}, genesisBalances: map[string]uint64{}, + genesis: time.Now().Add(cctx.BootstrapDuration), } - genesis := GenesisTime(time.Now().Add(cctx.BootstrapDuration)) + genesis := GenesisTime(cluster.genesis) cluster.addFlag(genesis) cluster.addFlag(GenesisExtraData(defaultExtraData)) cluster.addFlag(MinPeers(minPeers(cctx.ClusterSize))) @@ -195,6 +200,7 @@ type Cluster struct { accounts genesisBalances map[string]uint64 + genesis time.Time bootnodes int smeshers int @@ -205,16 +211,28 @@ type Cluster struct { bootstrapEpochs []int } +func (c *Cluster) Genesis() time.Time { + return c.genesis +} + +func (c *Cluster) GenesisExtraData() string { + return defaultExtraData +} + // GenesisID computes id from the configuration. func (c *Cluster) GenesisID() types.Hash20 { + return types.Hash32(c.GoldenATX()).ToHash20() +} + +func (c *Cluster) GoldenATX() types.ATXID { parsed, err := time.Parse(time.RFC3339, c.smesherFlags[genesisTimeFlag].Value) if err != nil { panic("invalid genesis time") } - return types.Hash32(hash.Sum( + return types.ATXID(hash.Sum( []byte(strconv.FormatInt(parsed.Unix(), 10)), []byte(c.smesherFlags[genesisExtraData].Value), - )).ToHash20() + )) } func (c *Cluster) nextSmesher() int { @@ -497,7 +515,7 @@ func (c *Cluster) AddSmeshers(tctx *testcontext.Context, n int, opts ...Deployme return err } flags := maps.Values(c.smesherFlags) - endpoints, err := extractP2PEndpoints(tctx, c.clients[:c.bootnodes]) + endpoints, err := ExtractP2PEndpoints(tctx, c.clients[:c.bootnodes]) if err != nil { return fmt.Errorf("extracting p2p endpoints %w", err) } @@ -757,7 +775,7 @@ func genSigner() *signer { return &signer{Pub: pub, PK: pk} } -func extractP2PEndpoints(tctx *testcontext.Context, nodes []*NodeClient) ([]string, error) { +func ExtractP2PEndpoints(tctx *testcontext.Context, nodes []*NodeClient) ([]string, error) { var ( rst = make([]string, len(nodes)) rctx, cancel = context.WithTimeout(tctx, 5*time.Minute) diff --git a/systest/tests/common.go b/systest/tests/common.go index 2ddd97b8bc..e9b6664169 100644 --- a/systest/tests/common.go +++ b/systest/tests/common.go @@ -205,6 +205,35 @@ func layersStream( } } +func malfeasanceStream( + ctx context.Context, + node *cluster.NodeClient, + logger *zap.Logger, + collector func(*pb.MalfeasanceStreamResponse) (bool, error), +) error { + meshapi := pb.NewMeshServiceClient(node.PubConn()) + layers, err := meshapi.MalfeasanceStream(ctx, &pb.MalfeasanceStreamRequest{IncludeProof: true}) + if err != nil { + return err + } + for { + proof, err := layers.Recv() + s, ok := status.FromError(err) + if ok && s.Code() != codes.OK { + logger.Warn("malfeasance stream error", zap.String("client", node.Name), zap.Error(err), zap.Any("status", s)) + if s.Code() == codes.Unavailable { + return nil + } + } + if err != nil { + return err + } + if cont, err := collector(proof); !cont { + return err + } + } +} + func waitGenesis(ctx *testcontext.Context, node *cluster.NodeClient) error { svc := pb.NewMeshServiceClient(node.PubConn()) resp, err := svc.GenesisTime(ctx, &pb.GenesisTimeRequest{}) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go new file mode 100644 index 0000000000..13affb5784 --- /dev/null +++ b/systest/tests/distributed_post_verification_test.go @@ -0,0 +1,239 @@ +package tests + +import ( + "context" + "fmt" + grpc_logsettable "github.com/grpc-ecosystem/go-grpc-middleware/logging/settable" + grpczap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" + "github.com/libp2p/go-libp2p/core/peer" + pb "github.com/spacemeshos/api/release/go/spacemesh/v1" + "github.com/spacemeshos/go-spacemesh/activation" + "github.com/spacemeshos/go-spacemesh/api/grpcserver" + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/config" + "github.com/spacemeshos/go-spacemesh/config/presets" + "github.com/spacemeshos/go-spacemesh/log" + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/p2p/handshake" + "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/localsql" + "github.com/spacemeshos/go-spacemesh/systest/cluster" + "github.com/spacemeshos/go-spacemesh/systest/testcontext" + "github.com/spacemeshos/go-spacemesh/timesync" + "github.com/spacemeshos/go-spacemesh/timesync/peersync" + "github.com/spacemeshos/post/shared" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "go.uber.org/zap" + "os" + "path/filepath" + "testing" + "time" +) + +var ( + grpclog grpc_logsettable.SettableLoggerV2 +) + +func init() { + grpclog = grpc_logsettable.ReplaceGrpcLoggerV2() +} + +func TestCreatingPostMalfeasanceProof(t *testing.T) { + t.Parallel() + + testDir := t.TempDir() + + ctx := testcontext.New(t, testcontext.Labels("sanity")) + cl, err := cluster.Reuse(ctx, cluster.WithKeys(10)) + require.NoError(t, err) + + cfg, err := presets.Get("fastnet") + require.NoError(t, err) + cfg.Genesis = &config.GenesisConfig{ + GenesisTime: cl.Genesis().Format(time.RFC3339), + ExtraData: cl.GenesisExtraData(), + } + cfg.LayersPerEpoch = uint32(testcontext.LayersPerEpoch.Get(ctx.Parameters)) + types.SetLayersPerEpoch(cfg.LayersPerEpoch) + cfg.LayerDuration = testcontext.LayerDuration.Get(ctx.Parameters) + + cfg.DataDirParent = testDir + cfg.SMESHING.Opts.DataDir = filepath.Join(testDir, "post-data") + cfg.P2P.DataDir = filepath.Join(testDir, "post-data") + require.NoError(t, os.Mkdir(cfg.P2P.DataDir, os.ModePerm)) + + cfg.PoetServers = []types.PoetServer{ + {Address: cluster.MakePoetGlobalEndpoint(ctx.Namespace, 0)}, + } + cfg.POET.MaxRequestRetries = 10 + cfg.POET.RequestTimeout = time.Minute + cfg.POET.RequestRetryDelay = 5 * time.Second + + cfg.API.PrivateListener = "0.0.0.0:9093" + + ctx.Log.Desugar().Info("Prepared config", zap.Any("cfg", cfg)) + goldenATXID := cl.GoldenATX() + + signer, err := signing.NewEdSigner() + require.NoError(t, err) + + var bootnodes []*cluster.NodeClient + for i := 0; i < cl.Bootnodes(); i++ { + bootnodes = append(bootnodes, cl.Client(i)) + } + + endpoints, err := cluster.ExtractP2PEndpoints(ctx, bootnodes) + require.NoError(t, err) + cfg.P2P.Bootnodes = endpoints + prologue := fmt.Sprintf("%x-%v", cl.GenesisID(), cfg.LayersPerEpoch*2-1) + + host, err := p2p.New( + ctx, + log.NewFromLog(ctx.Log.Desugar().Named("p2p")), + cfg.P2P, + []byte(prologue), + handshake.NetworkCookie(prologue), + ) + require.NoError(t, err) + host.Register(pubsub.AtxProtocol, func(context.Context, peer.ID, []byte) error { return nil }) + ptimesync := peersync.New( + host, + host, + peersync.WithLog(log.NewFromLog(ctx.Log.Named("peersync").Desugar())), + peersync.WithConfig(cfg.TIME.Peersync), + ) + ptimesync.Start() + t.Cleanup(ptimesync.Stop) + + require.NoError(t, host.Start()) + t.Cleanup(func() { host.Stop() }) + + mValidator := activation.NewMocknipostValidator(gomock.NewController(t)) + // 1. Initialize + postSetupMgr, err := activation.NewPostSetupManager( + signer.NodeID(), + cfg.POST, + ctx.Log.Named("post").Desugar(), + sql.InMemory(), + cl.GoldenATX(), + mValidator, + ) + require.NoError(t, err) + + syncer := activation.NewMocksyncer(gomock.NewController(t)) + syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { + ch := make(chan struct{}) + close(ch) + return ch + }).AnyTimes() + + postSupervisor, err := activation.NewPostSupervisor( + ctx.Log.Named("post-supervisor").Desugar(), + cfg.POSTService, + cfg.POST, + cfg.SMESHING.ProvingOpts, + postSetupMgr, + syncer, + ) + require.NoError(t, err) + require.NoError(t, postSupervisor.Start(cfg.SMESHING.Opts)) + + // 2. create ATX with invalid POST labels + clock, err := timesync.NewClock( + timesync.WithLayerDuration(cfg.LayerDuration), + timesync.WithTickInterval(1*time.Second), + timesync.WithGenesisTime(cl.Genesis()), + timesync.WithLogger(log.NewFromLog(ctx.Log.Desugar().Named("clock"))), + ) + require.NoError(t, err) + + grpcPostService := grpcserver.NewPostService(ctx.Log.Desugar().Named("grpc-post-service")) + grpczap.SetGrpcLoggerV2(grpclog, ctx.Log.Desugar().Named("grpc")) + grpcPrivateServer, err := grpcserver.NewPrivate( + ctx.Log.Desugar().Named("grpc-server"), + cfg.API, + []grpcserver.ServiceAPI{grpcPostService}, + ) + require.NoError(t, err) + require.NoError(t, grpcPrivateServer.Start()) + + nipostBuilder, err := activation.NewNIPostBuilder( + localsql.InMemory(), + activation.NewPoetDb(sql.InMemory(), log.NewFromLog(ctx.Log.Desugar().Named("poet-db"))), + grpcPostService, + cfg.PoetServers, + ctx.Log.Desugar().Named("nipostBuilder"), + signer, + cfg.POET, + clock, + ) + require.NoError(t, err) + + // 2.1. Create initial POST + var challenge *types.NIPostChallenge + for { + client, err := grpcPostService.Client(signer.NodeID()) + if err != nil { + ctx.Log.Info("waiting for poet service to connect") + time.Sleep(time.Second) + continue + } + ctx.Log.Info("poet service to connected") + post, postInfo, err := client.Proof(ctx, shared.ZeroChallenge) + require.NoError(t, err) + + challenge = &types.NIPostChallenge{ + PrevATXID: types.EmptyATXID, + PublishEpoch: 2, + PositioningATX: goldenATXID, + CommitmentATX: &postInfo.CommitmentATX, + InitialPost: post, + } + break + } + + nipost, err := nipostBuilder.BuildNIPost(ctx, challenge) + require.NoError(t, err) + + // 2.2 Create ATX with invalid POST + for i := range nipost.Post.Indices { + nipost.Post.Indices[i] += 1 + } + atx := types.NewActivationTx( + *challenge, + types.Address{1, 2, 3, 4}, + nipost.NIPost, + nipost.NumUnits, + &nipost.VRFNonce, + ) + nodeID := signer.NodeID() + atx.InnerActivationTx.NodeID = &nodeID + err = activation.SignAndFinalizeAtx(signer, atx) + require.NoError(t, err) + + require.NoError(t, cl.WaitAll(ctx)) + + // 3. Wait for publish epoch + err = layersStream(ctx, cl.Client(0), ctx.Log.Desugar(), func(layer *pb.LayerStreamResponse) (bool, error) { + return layer.Layer.Number.Number == cfg.LayersPerEpoch*2, nil + }) + require.NoError(t, err) + + // 4. Publish ATX + buf, err := codec.Encode(atx) + require.NoError(t, err) + err = host.Publish(ctx, pubsub.AtxProtocol, buf) + require.NoError(t, err) + + // 5. Wait for POST malfeasance proof + err = malfeasanceStream(ctx, cl.Client(0), ctx.Log.Desugar(), func(malfeasance *pb.MalfeasanceStreamResponse) (bool, error) { + ctx.Log.Desugar().Info("malfeasance proof received", zap.Any("malfeasance", malfeasance)) + require.Equal(t, malfeasance.Proof.SmesherId, signer.NodeID().String()) + return false, nil + }) + require.NoError(t, err) +} From 9f14f0c5c1fcb1d0c30e02cf5f19821672255e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 12 Jan 2024 13:13:17 +0100 Subject: [PATCH 11/30] Working systest for post malf proofs --- events/events.go | 2 + systest/cluster/cluster.go | 4 + .../distributed_post_verification_test.go | 134 +++++++++++++----- 3 files changed, 104 insertions(+), 36 deletions(-) diff --git a/events/events.go b/events/events.go index be353e4a55..66a603077a 100644 --- a/events/events.go +++ b/events/events.go @@ -272,6 +272,8 @@ func ToMalfeasancePB(smesher types.NodeID, mp *types.MalfeasanceProof, includePr kind = pb.MalfeasanceProof_MALFEASANCE_BALLOT case types.HareEquivocation: kind = pb.MalfeasanceProof_MALFEASANCE_HARE + case types.InvalidPostIndex: + kind = 4 } result := &pb.MalfeasanceProof{ SmesherId: &pb.SmesherId{Id: smesher.Bytes()}, diff --git a/systest/cluster/cluster.go b/systest/cluster/cluster.go index 68f885cf3e..2bcfc61e9c 100644 --- a/systest/cluster/cluster.go +++ b/systest/cluster/cluster.go @@ -72,6 +72,10 @@ func BootstrapperEndpoint(ith int) string { return fmt.Sprintf("http://%s:%d", createBootstrapperIdentifier(ith), bootstrapperPort) } +func BootstrapperGlobalEndpoint(namespace string, ith int) string { + return fmt.Sprintf("http://%s.%s:%d", createBootstrapperIdentifier(ith), namespace, bootstrapperPort) +} + // Opt is for configuring cluster. type Opt func(c *Cluster) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 13affb5784..e736f83f58 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -25,9 +25,13 @@ import ( "github.com/spacemeshos/go-spacemesh/timesync" "github.com/spacemeshos/go-spacemesh/timesync/peersync" "github.com/spacemeshos/post/shared" + "github.com/spacemeshos/post/verifying" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "golang.org/x/sync/errgroup" "os" "path/filepath" "testing" @@ -44,13 +48,21 @@ func init() { func TestCreatingPostMalfeasanceProof(t *testing.T) { t.Parallel() - testDir := t.TempDir() ctx := testcontext.New(t, testcontext.Labels("sanity")) - cl, err := cluster.Reuse(ctx, cluster.WithKeys(10)) - require.NoError(t, err) + logger := ctx.Log.Desugar().WithOptions(zap.IncreaseLevel(zapcore.InfoLevel), zap.WithCaller(false)) + + // Prepare cluster + ctx.PoetSize = 1 // one poet guarantees everybody gets the same proof + ctx.ClusterSize = 3 + cl := cluster.New(ctx, cluster.WithKeys(10)) + require.NoError(t, cl.AddBootnodes(ctx, 1)) + require.NoError(t, cl.AddBootstrappers(ctx)) + require.NoError(t, cl.AddPoets(ctx)) + require.NoError(t, cl.AddSmeshers(ctx, ctx.ClusterSize-cl.Total())) + // Prepare config cfg, err := presets.Get("fastnet") require.NoError(t, err) cfg.Genesis = &config.GenesisConfig{ @@ -69,18 +81,8 @@ func TestCreatingPostMalfeasanceProof(t *testing.T) { cfg.PoetServers = []types.PoetServer{ {Address: cluster.MakePoetGlobalEndpoint(ctx.Namespace, 0)}, } - cfg.POET.MaxRequestRetries = 10 - cfg.POET.RequestTimeout = time.Minute cfg.POET.RequestRetryDelay = 5 * time.Second - cfg.API.PrivateListener = "0.0.0.0:9093" - - ctx.Log.Desugar().Info("Prepared config", zap.Any("cfg", cfg)) - goldenATXID := cl.GoldenATX() - - signer, err := signing.NewEdSigner() - require.NoError(t, err) - var bootnodes []*cluster.NodeClient for i := 0; i < cl.Bootnodes(); i++ { bootnodes = append(bootnodes, cl.Client(i)) @@ -89,38 +91,46 @@ func TestCreatingPostMalfeasanceProof(t *testing.T) { endpoints, err := cluster.ExtractP2PEndpoints(ctx, bootnodes) require.NoError(t, err) cfg.P2P.Bootnodes = endpoints - prologue := fmt.Sprintf("%x-%v", cl.GenesisID(), cfg.LayersPerEpoch*2-1) + cfg.P2P.PrivateNetwork = true + cfg.Bootstrap.URL = cluster.BootstrapperGlobalEndpoint(ctx.Namespace, 0) + cfg.P2P.MinPeers = 2 + ctx.Log.Infow("Prepared config", "cfg", cfg) + + goldenATXID := cl.GoldenATX() + signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) + require.NoError(t, err) + prologue := fmt.Sprintf("%x-%v", cl.GenesisID(), cfg.LayersPerEpoch*2-1) host, err := p2p.New( ctx, - log.NewFromLog(ctx.Log.Desugar().Named("p2p")), + log.NewFromLog(logger.Named("p2p")), cfg.P2P, []byte(prologue), handshake.NetworkCookie(prologue), ) require.NoError(t, err) + logger.Info("p2p host created", zap.Stringer("id", host.ID())) host.Register(pubsub.AtxProtocol, func(context.Context, peer.ID, []byte) error { return nil }) ptimesync := peersync.New( host, host, - peersync.WithLog(log.NewFromLog(ctx.Log.Named("peersync").Desugar())), + peersync.WithLog(log.NewFromLog(logger.Named("peersync"))), peersync.WithConfig(cfg.TIME.Peersync), ) ptimesync.Start() t.Cleanup(ptimesync.Stop) require.NoError(t, host.Start()) - t.Cleanup(func() { host.Stop() }) + t.Cleanup(func() { assert.NoError(t, host.Stop()) }) - mValidator := activation.NewMocknipostValidator(gomock.NewController(t)) // 1. Initialize postSetupMgr, err := activation.NewPostSetupManager( signer.NodeID(), cfg.POST, - ctx.Log.Named("post").Desugar(), + logger.Named("post"), sql.InMemory(), cl.GoldenATX(), - mValidator, + activation.NewMocknipostValidator(gomock.NewController(t)), ) require.NoError(t, err) @@ -132,7 +142,7 @@ func TestCreatingPostMalfeasanceProof(t *testing.T) { }).AnyTimes() postSupervisor, err := activation.NewPostSupervisor( - ctx.Log.Named("post-supervisor").Desugar(), + logger.Named("post-supervisor"), cfg.POSTService, cfg.POST, cfg.SMESHING.ProvingOpts, @@ -141,32 +151,34 @@ func TestCreatingPostMalfeasanceProof(t *testing.T) { ) require.NoError(t, err) require.NoError(t, postSupervisor.Start(cfg.SMESHING.Opts)) + t.Cleanup(func() { assert.NoError(t, postSupervisor.Stop(false)) }) // 2. create ATX with invalid POST labels clock, err := timesync.NewClock( timesync.WithLayerDuration(cfg.LayerDuration), timesync.WithTickInterval(1*time.Second), timesync.WithGenesisTime(cl.Genesis()), - timesync.WithLogger(log.NewFromLog(ctx.Log.Desugar().Named("clock"))), + timesync.WithLogger(log.NewFromLog(logger.Named("clock"))), ) require.NoError(t, err) - grpcPostService := grpcserver.NewPostService(ctx.Log.Desugar().Named("grpc-post-service")) - grpczap.SetGrpcLoggerV2(grpclog, ctx.Log.Desugar().Named("grpc")) + grpcPostService := grpcserver.NewPostService(logger.Named("grpc-post-service")) + grpczap.SetGrpcLoggerV2(grpclog, logger.Named("grpc")) grpcPrivateServer, err := grpcserver.NewPrivate( - ctx.Log.Desugar().Named("grpc-server"), + logger.Named("grpc-server"), cfg.API, []grpcserver.ServiceAPI{grpcPostService}, ) require.NoError(t, err) require.NoError(t, grpcPrivateServer.Start()) + t.Cleanup(func() { assert.NoError(t, grpcPrivateServer.Close()) }) nipostBuilder, err := activation.NewNIPostBuilder( localsql.InMemory(), - activation.NewPoetDb(sql.InMemory(), log.NewFromLog(ctx.Log.Desugar().Named("poet-db"))), + activation.NewPoetDb(sql.InMemory(), log.NewFromLog(logger.Named("poet-db"))), grpcPostService, cfg.PoetServers, - ctx.Log.Desugar().Named("nipostBuilder"), + logger.Named("nipostBuilder"), signer, cfg.POET, clock, @@ -218,21 +230,71 @@ func TestCreatingPostMalfeasanceProof(t *testing.T) { require.NoError(t, cl.WaitAll(ctx)) // 3. Wait for publish epoch - err = layersStream(ctx, cl.Client(0), ctx.Log.Desugar(), func(layer *pb.LayerStreamResponse) (bool, error) { - return layer.Layer.Number.Number == cfg.LayersPerEpoch*2, nil + epoch := atx.PublishEpoch + logger.Sugar().Infow("waiting for publish epoch", "epoch", epoch, "layer", epoch.FirstLayer()) + err = layersStream(ctx, cl.Client(0), logger, func(resp *pb.LayerStreamResponse) (bool, error) { + logger.Info("new layer", zap.Uint32("layer", resp.Layer.Number.Number)) + return resp.Layer.Number.Number != epoch.FirstLayer().Uint32(), nil }) require.NoError(t, err) // 4. Publish ATX - buf, err := codec.Encode(atx) - require.NoError(t, err) - err = host.Publish(ctx, pubsub.AtxProtocol, buf) - require.NoError(t, err) + publishCtx, stopPublishing := context.WithCancel(ctx.Context) + t.Cleanup(stopPublishing) + var eg errgroup.Group + eg.Go(func() error { + for { + logger.Sugar().Infow("publishing ATX", "atx", atx) + buf, err := codec.Encode(atx) + require.NoError(t, err) + err = host.Publish(ctx, pubsub.AtxProtocol, buf) + require.NoError(t, err) + + select { + case <-publishCtx.Done(): + return nil + case <-time.After(10 * time.Second): + } + } + }) // 5. Wait for POST malfeasance proof - err = malfeasanceStream(ctx, cl.Client(0), ctx.Log.Desugar(), func(malfeasance *pb.MalfeasanceStreamResponse) (bool, error) { - ctx.Log.Desugar().Info("malfeasance proof received", zap.Any("malfeasance", malfeasance)) - require.Equal(t, malfeasance.Proof.SmesherId, signer.NodeID().String()) + logger.Info("waiting for malfeasance proof") + err = malfeasanceStream(ctx, cl.Client(0), logger, func(malfeasance *pb.MalfeasanceStreamResponse) (bool, error) { + stopPublishing() + logger.Info("malfeasance proof received") + require.Equal(t, malfeasance.GetProof().GetSmesherId().Id, signer.NodeID().Bytes()) + require.Equal(t, pb.MalfeasanceProof_MalfeasanceType(4), malfeasance.GetProof().GetKind()) + + var proof types.MalfeasanceProof + require.NoError(t, codec.Decode(malfeasance.Proof.Proof, &proof)) + require.Equal(t, types.InvalidPostIndex, proof.Proof.Type) + invalidPostProof := proof.Proof.Data.(*types.InvalidPostIndexProof) + logger.Sugar().Infow("malfeasance post proof", "proof", invalidPostProof) + invalidAtx := invalidPostProof.Atx + require.Equal(t, atx.PublishEpoch, invalidAtx.PublishEpoch) + require.Equal(t, atx.SmesherID, invalidAtx.SmesherID) + require.Equal(t, atx.NodeID, invalidAtx.NodeID) + require.Equal(t, atx.PositioningATX, invalidAtx.PositioningATX) + require.Equal(t, atx.PrevATXID, invalidAtx.PrevATXID) + require.Equal(t, atx.Signature, invalidAtx.Signature) + require.Equal(t, atx.Coinbase, invalidAtx.Coinbase) + require.Equal(t, *atx.CommitmentATX, *invalidAtx.CommitmentATX) + require.Equal(t, atx.NIPostChallenge, invalidAtx.NIPostChallenge) + require.Equal(t, atx.NIPost.Post.Indices, invalidAtx.NIPost.Post.Indices) + + postVerifier, err := activation.NewPostVerifier(cfg.POST, logger.Named("post-verifier")) + require.NoError(t, err) + meta := &shared.ProofMetadata{ + NodeId: invalidAtx.NodeID.Bytes(), + CommitmentAtxId: invalidAtx.CommitmentATX.Bytes(), + NumUnits: invalidAtx.NumUnits, + Challenge: invalidAtx.NIPost.PostMetadata.Challenge, + LabelsPerUnit: invalidAtx.NIPost.PostMetadata.LabelsPerUnit, + } + err = postVerifier.Verify(ctx, (*shared.Proof)(invalidAtx.NIPost.Post), meta) + var invalidIdxError *verifying.ErrInvalidIndex + require.ErrorAs(t, err, &invalidIdxError) return false, nil }) require.NoError(t, err) From 3a0b2496dc4888694f719c17a9c34aadd1b0f42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 12 Jan 2024 13:23:15 +0100 Subject: [PATCH 12/30] Bump api --- events/events.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- systest/tests/distributed_post_verification_test.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/events/events.go b/events/events.go index 66a603077a..86a3ff3278 100644 --- a/events/events.go +++ b/events/events.go @@ -273,7 +273,7 @@ func ToMalfeasancePB(smesher types.NodeID, mp *types.MalfeasanceProof, includePr case types.HareEquivocation: kind = pb.MalfeasanceProof_MALFEASANCE_HARE case types.InvalidPostIndex: - kind = 4 + kind = pb.MalfeasanceProof_MALFEASANCE_POST_INDEX } result := &pb.MalfeasanceProof{ SmesherId: &pb.SmesherId{Id: smesher.Bytes()}, diff --git a/go.mod b/go.mod index fe4cba72a6..10188b4537 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/quic-go/quic-go v0.40.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/seehuhn/mt19937 v1.0.0 - github.com/spacemeshos/api/release/go v1.25.0 + github.com/spacemeshos/api/release/go v1.25.1-0.20240112122020-f3d0b62e039f github.com/spacemeshos/economics v0.1.1 github.com/spacemeshos/fixed v0.1.1 github.com/spacemeshos/go-scale v1.1.12 diff --git a/go.sum b/go.sum index 4739a51a29..0fe83fa394 100644 --- a/go.sum +++ b/go.sum @@ -621,8 +621,8 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/spacemeshos/api/release/go v1.25.0 h1:HqIykavooxUxZySiN7yiILRX+Kx6WxM1NF6w72PNmMc= -github.com/spacemeshos/api/release/go v1.25.0/go.mod h1:Ffvt8Zl5K6k0In0OF7PUJ2JoxrW8zBRXKF1kG0abigg= +github.com/spacemeshos/api/release/go v1.25.1-0.20240112122020-f3d0b62e039f h1:F8j/xyzXoNbsrOLykiEggKisoy+fbyUcEjpSsKlvVbY= +github.com/spacemeshos/api/release/go v1.25.1-0.20240112122020-f3d0b62e039f/go.mod h1:cQXfRiIRPc8c6bh9+VAK/GwD0zYCu7jKcos/cPaDYcI= github.com/spacemeshos/economics v0.1.1 h1:BPgMoTaeQ05ME6wEA1+MvXMp+wvXr51bIuN23thrCAk= github.com/spacemeshos/economics v0.1.1/go.mod h1:76nTjugYRiQ5/eD/DQs2dXPPilp28URMswUKncfdanY= github.com/spacemeshos/fixed v0.1.1 h1:N1y4SUpq1EV+IdJrWJwUCt1oBFzeru/VKVcBsvPc2Fk= diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index e736f83f58..a731d33ad5 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -264,7 +264,7 @@ func TestCreatingPostMalfeasanceProof(t *testing.T) { stopPublishing() logger.Info("malfeasance proof received") require.Equal(t, malfeasance.GetProof().GetSmesherId().Id, signer.NodeID().Bytes()) - require.Equal(t, pb.MalfeasanceProof_MalfeasanceType(4), malfeasance.GetProof().GetKind()) + require.Equal(t, pb.MalfeasanceProof_MALFEASANCE_POST_INDEX, malfeasance.GetProof().GetKind()) var proof types.MalfeasanceProof require.NoError(t, codec.Decode(malfeasance.Proof.Proof, &proof)) From e8b1878d680125f8e7d287c7d538c3de145abc02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 12 Jan 2024 13:31:48 +0100 Subject: [PATCH 13/30] Revert sql DB changes in post setup mgr and lint --- activation/post.go | 5 ++- node/node.go | 2 +- .../distributed_post_verification_test.go | 42 +++++++++---------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/activation/post.go b/activation/post.go index e8caa9f751..6e083f5126 100644 --- a/activation/post.go +++ b/activation/post.go @@ -13,6 +13,7 @@ import ( "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/events" "github.com/spacemeshos/go-spacemesh/metrics/public" "github.com/spacemeshos/go-spacemesh/sql" @@ -171,7 +172,7 @@ type PostSetupManager struct { cfg PostConfig logger *zap.Logger - db *sql.Database + db *datastore.CachedDB goldenATXID types.ATXID validator nipostValidator @@ -199,7 +200,7 @@ func NewPostSetupManager( id types.NodeID, cfg PostConfig, logger *zap.Logger, - db *sql.Database, + db *datastore.CachedDB, goldenATXID types.ATXID, validator nipostValidator, opts ...PostSetupManagerOpt, diff --git a/node/node.go b/node/node.go index 9d3f999976..5fdd628bb3 100644 --- a/node/node.go +++ b/node/node.go @@ -892,7 +892,7 @@ func (app *App) initServices(ctx context.Context) error { app.edSgn.NodeID(), app.Config.POST, app.addLogger(PostLogger, lg).Zap(), - app.cachedDB.Database, + app.cachedDB, goldenATXID, app.validator, activation.PostValidityDelay(app.Config.PostValidDelay), diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index a731d33ad5..32a8dfa30d 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -3,16 +3,31 @@ package tests import ( "context" "fmt" + "os" + "path/filepath" + "testing" + "time" + grpc_logsettable "github.com/grpc-ecosystem/go-grpc-middleware/logging/settable" grpczap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" "github.com/libp2p/go-libp2p/core/peer" pb "github.com/spacemeshos/api/release/go/spacemesh/v1" + "github.com/spacemeshos/post/shared" + "github.com/spacemeshos/post/verifying" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "golang.org/x/sync/errgroup" + "github.com/spacemeshos/go-spacemesh/activation" "github.com/spacemeshos/go-spacemesh/api/grpcserver" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/config" "github.com/spacemeshos/go-spacemesh/config/presets" + "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/handshake" @@ -24,29 +39,15 @@ import ( "github.com/spacemeshos/go-spacemesh/systest/testcontext" "github.com/spacemeshos/go-spacemesh/timesync" "github.com/spacemeshos/go-spacemesh/timesync/peersync" - "github.com/spacemeshos/post/shared" - "github.com/spacemeshos/post/verifying" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "golang.org/x/sync/errgroup" - "os" - "path/filepath" - "testing" - "time" ) -var ( - grpclog grpc_logsettable.SettableLoggerV2 -) +var grpclog grpc_logsettable.SettableLoggerV2 func init() { grpclog = grpc_logsettable.ReplaceGrpcLoggerV2() } -func TestCreatingPostMalfeasanceProof(t *testing.T) { +func TestPostMalfeasanceProof(t *testing.T) { t.Parallel() testDir := t.TempDir() @@ -128,7 +129,7 @@ func TestCreatingPostMalfeasanceProof(t *testing.T) { signer.NodeID(), cfg.POST, logger.Named("post"), - sql.InMemory(), + datastore.NewCachedDB(sql.InMemory(), log.NewNop()), cl.GoldenATX(), activation.NewMocknipostValidator(gomock.NewController(t)), ) @@ -175,7 +176,7 @@ func TestCreatingPostMalfeasanceProof(t *testing.T) { nipostBuilder, err := activation.NewNIPostBuilder( localsql.InMemory(), - activation.NewPoetDb(sql.InMemory(), log.NewFromLog(logger.Named("poet-db"))), + activation.NewPoetDb(sql.InMemory(), log.NewNop()), grpcPostService, cfg.PoetServers, logger.Named("nipostBuilder"), @@ -224,10 +225,7 @@ func TestCreatingPostMalfeasanceProof(t *testing.T) { ) nodeID := signer.NodeID() atx.InnerActivationTx.NodeID = &nodeID - err = activation.SignAndFinalizeAtx(signer, atx) - require.NoError(t, err) - - require.NoError(t, cl.WaitAll(ctx)) + require.NoError(t, activation.SignAndFinalizeAtx(signer, atx)) // 3. Wait for publish epoch epoch := atx.PublishEpoch From 04bdc36dda8ccda82c191bee477240519e86daed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 12 Jan 2024 13:49:17 +0100 Subject: [PATCH 14/30] Use K3=1 in distributed post systest --- systest/cluster/nodes.go | 4 ++++ systest/tests/distributed_post_verification_test.go | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/systest/cluster/nodes.go b/systest/cluster/nodes.go index 7ecfcd2b8f..1fc0d1ef58 100644 --- a/systest/cluster/nodes.go +++ b/systest/cluster/nodes.go @@ -797,6 +797,10 @@ func PoetEndpoints(ids ...int) DeploymentFlag { return DeploymentFlag{Name: "--poet-servers", Value: string(value)} } +func PostK3(k3 int) DeploymentFlag { + return DeploymentFlag{Name: "--post-k3", Value: strconv.Itoa(k3)} +} + // MinPeers flag. func MinPeers(target int) DeploymentFlag { return DeploymentFlag{Name: "--min-peers", Value: strconv.Itoa(target)} diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 32a8dfa30d..69f577e964 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -61,7 +61,7 @@ func TestPostMalfeasanceProof(t *testing.T) { require.NoError(t, cl.AddBootnodes(ctx, 1)) require.NoError(t, cl.AddBootstrappers(ctx)) require.NoError(t, cl.AddPoets(ctx)) - require.NoError(t, cl.AddSmeshers(ctx, ctx.ClusterSize-cl.Total())) + require.NoError(t, cl.AddSmeshers(ctx, ctx.ClusterSize-cl.Total(), cluster.WithFlags(cluster.PostK3(1)))) // Prepare config cfg, err := presets.Get("fastnet") @@ -82,7 +82,6 @@ func TestPostMalfeasanceProof(t *testing.T) { cfg.PoetServers = []types.PoetServer{ {Address: cluster.MakePoetGlobalEndpoint(ctx.Namespace, 0)}, } - cfg.POET.RequestRetryDelay = 5 * time.Second var bootnodes []*cluster.NodeClient for i := 0; i < cl.Bootnodes(); i++ { From f12f0ae7b14933ccc43928cb30c4772e6d587b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 12 Jan 2024 14:19:37 +0100 Subject: [PATCH 15/30] Fix waiting for layer end condition --- systest/tests/distributed_post_verification_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 7701a0fd62..32e4c11c89 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -231,7 +231,7 @@ func TestPostMalfeasanceProof(t *testing.T) { logger.Sugar().Infow("waiting for publish epoch", "epoch", epoch, "layer", epoch.FirstLayer()) err = layersStream(ctx, cl.Client(0), logger, func(resp *pb.LayerStreamResponse) (bool, error) { logger.Info("new layer", zap.Uint32("layer", resp.Layer.Number.Number)) - return resp.Layer.Number.Number != epoch.FirstLayer().Uint32(), nil + return resp.Layer.Number.Number < epoch.FirstLayer().Uint32(), nil }) require.NoError(t, err) From 2e63575e14f1603808c849d465fa450d77bc105f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 26 Jan 2024 11:43:57 +0100 Subject: [PATCH 16/30] Bump post and api deps --- Makefile-libs.Inc | 2 +- go.mod | 4 ++-- go.sum | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile-libs.Inc b/Makefile-libs.Inc index 6a2b8a62b4..dd314b30e0 100644 --- a/Makefile-libs.Inc +++ b/Makefile-libs.Inc @@ -50,7 +50,7 @@ else endif endif -POSTRS_SETUP_REV = 0.7.0-rc4-distributed-verification +POSTRS_SETUP_REV = 0.7.0-rc6-distributed-verification POSTRS_SETUP_ZIP = libpost-$(platform)-v$(POSTRS_SETUP_REV).zip POSTRS_SETUP_URL_ZIP ?= https://github.com/spacemeshos/post-rs/releases/download/v$(POSTRS_SETUP_REV)/$(POSTRS_SETUP_ZIP) POSTRS_PROFILER_ZIP = profiler-$(platform)-v$(POSTRS_SETUP_REV).zip diff --git a/go.mod b/go.mod index 3561236493..20037a0cd1 100644 --- a/go.mod +++ b/go.mod @@ -36,13 +36,13 @@ require ( github.com/quic-go/quic-go v0.41.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/seehuhn/mt19937 v1.0.0 - github.com/spacemeshos/api/release/go v1.26.1-0.20240112122020-f3d0b62e039f + github.com/spacemeshos/api/release/go v1.26.1-0.20240126094723-69af34e8ef63 github.com/spacemeshos/economics v0.1.2 github.com/spacemeshos/fixed v0.1.1 github.com/spacemeshos/go-scale v1.1.12 github.com/spacemeshos/merkle-tree v0.2.3 github.com/spacemeshos/poet v0.10.2 - github.com/spacemeshos/post v0.10.3-0.20231228141353-523b1c994625 + github.com/spacemeshos/post v0.10.6-0.20240125144730-2c926a601046 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index b88f1cf350..68f19faf9a 100644 --- a/go.sum +++ b/go.sum @@ -622,8 +622,8 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/spacemeshos/api/release/go v1.25.1-0.20240112122020-f3d0b62e039f h1:F8j/xyzXoNbsrOLykiEggKisoy+fbyUcEjpSsKlvVbY= -github.com/spacemeshos/api/release/go v1.25.1-0.20240112122020-f3d0b62e039f/go.mod h1:cQXfRiIRPc8c6bh9+VAK/GwD0zYCu7jKcos/cPaDYcI= +github.com/spacemeshos/api/release/go v1.26.1-0.20240126094723-69af34e8ef63 h1:a8td/tqVAvpONDjKmP1R7nSo10X0nmxh9Cm0oJzCmig= +github.com/spacemeshos/api/release/go v1.26.1-0.20240126094723-69af34e8ef63/go.mod h1:lQ9UtxTzMAyC2q3DkE349L2FXeLkc9cPoEs4osQ20xM= github.com/spacemeshos/economics v0.1.2 h1:kw8cE5SMa/7svHOGorCd2w8ef1y8iP0p47/2VDOK8Ns= github.com/spacemeshos/economics v0.1.2/go.mod h1:ngeWn5E/jy9dJP1MHyuk3ehF8NBMTYhchqVDhAHUUNk= github.com/spacemeshos/fixed v0.1.1 h1:N1y4SUpq1EV+IdJrWJwUCt1oBFzeru/VKVcBsvPc2Fk= @@ -634,8 +634,8 @@ github.com/spacemeshos/merkle-tree v0.2.3 h1:zGEgOR9nxAzJr0EWjD39QFngwFEOxfxMloE github.com/spacemeshos/merkle-tree v0.2.3/go.mod h1:VomOcQ5pCBXz7goiWMP5hReyqOfDXGSKbrH2GB9Htww= github.com/spacemeshos/poet v0.10.2 h1:FVb0xgCFcjZyIGBQ92SlOZVx4KCmlCRRL4JSHL6LMGU= github.com/spacemeshos/poet v0.10.2/go.mod h1:73ROEXGladw3RbvhAk0sIGi/ttfpo+ASUBRvnBK55N8= -github.com/spacemeshos/post v0.10.3-0.20231228141353-523b1c994625 h1:83iNwxBzgGpq2RjBKWqDgQyTaIqmK3SF5DRbdzpj8K0= -github.com/spacemeshos/post v0.10.3-0.20231228141353-523b1c994625/go.mod h1:lSZOkvCna1UuXgdGrvc+2SPMM+f+IUi9whZpBH7wUbM= +github.com/spacemeshos/post v0.10.6-0.20240125144730-2c926a601046 h1:ycGUfegcGYJOixpsS/KLiCtcKCwtl0s+0L96WQNWyc4= +github.com/spacemeshos/post v0.10.6-0.20240125144730-2c926a601046/go.mod h1:qKoQBbbvGptdf2CZxI1u7jnpJuXei1uEzgRQHbLiko4= github.com/spacemeshos/sha256-simd v0.1.0 h1:G7Mfu5RYdQiuE+wu4ZyJ7I0TI74uqLhFnKblEnSpjYI= github.com/spacemeshos/sha256-simd v0.1.0/go.mod h1:O8CClVIilId7RtuCMV2+YzMj6qjVn75JsxOxaE8vcfM= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= From 252ee4419065d8b0561af120efcd6064d919c436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 26 Jan 2024 15:12:31 +0100 Subject: [PATCH 17/30] Review feedback --- activation/activation.go | 11 +-- activation/activation_test.go | 46 ++++++++++-- activation/e2e/nipost_test.go | 4 +- activation/e2e/validation_test.go | 8 +- activation/interface.go | 3 + activation/mocks.go | 43 +++++++++++ activation/post.go | 5 +- activation/post_test.go | 1 + activation/validation.go | 80 +++++++++++--------- activation/validation_test.go | 54 +++++++------- common/types/malfeasance.go | 2 +- datastore/mocks/mocks.go | 117 ++++++++++++++++++++++++++++++ datastore/store.go | 20 +++-- fetch/fetch.go | 2 +- fetch/handler_test.go | 2 +- node/node.go | 1 + sql/atxs/atxs.go | 4 +- syncer/syncer.go | 2 +- 18 files changed, 306 insertions(+), 99 deletions(-) create mode 100644 datastore/mocks/mocks.go diff --git a/activation/activation.go b/activation/activation.go index ce97cb0293..3c2315b3b0 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -600,14 +600,15 @@ func (b *Builder) getPositioningAtx(ctx context.Context) (types.ATXID, error) { b.goldenATXID, b.validator, b.log, - assumeValidBefore(time.Now().Add(-b.postValidityDelay)), - withTrustedID(b.signer.NodeID()), + VerifyChainOpts.AssumeValidBefore(time.Now().Add(-b.postValidityDelay)), + VerifyChainOpts.WithTrustedID(b.signer.NodeID()), + VerifyChainOpts.WithLogger(b.log), ) if errors.Is(err, sql.ErrNotFound) { b.log.Info("using golden atx as positioning atx") return b.goldenATXID, nil } - return id, nil + return id, err } func (b *Builder) Regossip(ctx context.Context) error { @@ -651,7 +652,7 @@ func findFullyValidHighTickAtx( goldenATXID types.ATXID, validator nipostValidator, log *zap.Logger, - opts ...verifyChainOption, + opts ...VerifyChainOption, ) (types.ATXID, error) { rejectedAtxs := make(map[types.ATXID]struct{}) filter := func(id types.ATXID) bool { @@ -670,7 +671,7 @@ func findFullyValidHighTickAtx( return types.ATXID{}, err } - if err := verifyChain(ctx, db, id, goldenATXID, validator, log, opts...); err != nil { + if err := validator.VerifyChain(ctx, id, goldenATXID, opts...); err != nil { log.Info("rejecting candidate for high-tick atx", zap.Error(err), zap.Stringer("atx_id", id)) rejectedAtxs[id] = struct{}{} } else { diff --git a/activation/activation_test.go b/activation/activation_test.go index 41a739765c..c329a6cbca 100644 --- a/activation/activation_test.go +++ b/activation/activation_test.go @@ -18,6 +18,7 @@ import ( "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" + datastoremocks "github.com/spacemeshos/go-spacemesh/datastore/mocks" "github.com/spacemeshos/go-spacemesh/events" "github.com/spacemeshos/go-spacemesh/log/logtest" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" @@ -243,6 +244,8 @@ func publishAtx( }) tab.mnipost.EXPECT().ResetState().Return(nil) + // Expect verification of positioning ATX candidate chain. + tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) // create and publish ATX err := tab.PublishActivationTx(context.Background()) return built, err @@ -458,6 +461,8 @@ func TestBuilder_Loop_WaitsOnStaleChallenge(t *testing.T) { return ch }) + tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).MinTimes(1) + // Act & Verify var eg errgroup.Group eg.Go(func() error { @@ -532,7 +537,7 @@ func TestBuilder_PublishActivationTx_FaultyNet(t *testing.T) { // after successful publish, state is cleaned up tab.mnipost.EXPECT().ResetState().Return(nil) - + tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) tab.mpub.EXPECT().Publish(gomock.Any(), pubsub.AtxProtocol, gomock.Any()).DoAndReturn( // second publish succeeds func(_ context.Context, _ string, got []byte) error { @@ -671,6 +676,7 @@ func TestBuilder_PublishActivationTx_RebuildNIPostWhenTargetEpochPassed(t *testi } return done }) + tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) ctx, cancel := context.WithCancel(context.Background()) defer cancel() var built *types.ActivationTx @@ -796,6 +802,7 @@ func TestBuilder_PublishActivationTx_NoPrevATX_PublishFails_InitialPost_preserve close(ch) return ch }) + tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) ctx, cancel := context.WithCancel(context.Background()) var eg errgroup.Group @@ -896,6 +903,8 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { return newNIPostWithChallenge(t, challenge.Hash(), poetBytes), nil }) + tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + tab.mpub.EXPECT(). Publish(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, _ string, msg []byte) error { @@ -987,6 +996,8 @@ func TestBuilder_PublishActivationTx_TargetsEpochBasedOnPosAtx(t *testing.T) { return newNIPostWithChallenge(t, challenge.Hash(), poetBytes), nil }) + tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + tab.mpub.EXPECT(). Publish(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, _ string, msg []byte) error { @@ -1048,6 +1059,7 @@ func TestBuilder_PublishActivationTx_FailsWhenNIPostBuilderFails(t *testing.T) { }).AnyTimes() nipostErr := fmt.Errorf("NIPost builder error") tab.mnipost.EXPECT().BuildNIPost(gomock.Any(), gomock.Any()).Return(nil, nipostErr) + tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) require.ErrorIs(t, tab.PublishActivationTx(context.Background()), nipostErr) // state is preserved @@ -1190,6 +1202,7 @@ func TestBuilder_RetryPublishActivationTx(t *testing.T) { ) tab.mnipost.EXPECT().ResetState().Return(nil) + tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) nonce := types.VRFPostIndex(123) commitmentATX := types.RandomATXID() @@ -1364,8 +1377,8 @@ func TestRegossip(t *testing.T) { atx := newActivationTx(t, h.signer, 0, types.EmptyATXID, types.EmptyATXID, nil, layer.GetEpoch(), 0, 1, types.Address{}, 1, &types.NIPost{}) - require.NoError(t, atxs.Add(h.cdb.Database, atx)) - blob, err := atxs.GetBlob(h.cdb.Database, atx.ID().Bytes()) + require.NoError(t, atxs.Add(h.cdb, atx)) + blob, err := atxs.GetBlob(h.cdb, atx.ID().Bytes()) require.NoError(t, err) h.mclock.EXPECT().CurrentLayer().Return(layer) @@ -1375,8 +1388,8 @@ func TestRegossip(t *testing.T) { }) t.Run("checkpointed", func(t *testing.T) { h := newTestBuilder(t) - require.NoError(t, atxs.AddCheckpointed(h.cdb.Database, - &atxs.CheckpointAtx{ID: types.ATXID{1}, Epoch: layer.GetEpoch(), SmesherID: h.sig.NodeID()})) + atx := atxs.CheckpointAtx{ID: types.ATXID{1}, Epoch: layer.GetEpoch(), SmesherID: h.sig.NodeID()} + require.NoError(t, atxs.AddCheckpointed(h.cdb, &atx)) h.mclock.EXPECT().CurrentLayer().Return(layer) require.NoError(t, h.Regossip(context.Background())) }) @@ -1424,26 +1437,43 @@ func TestGetPositioningAtxPicksAtxWithValidChain(t *testing.T) { sigInvalid, err := signing.NewEdSigner() require.NoError(t, err) ch := newChallenge(1, types.EmptyATXID, tab.goldenATXID, postGenesisEpoch, &tab.goldenATXID) - nipostData := newNIPostWithChallenge(t, types.HexToHash32("for invalid"), []byte("66")) + nipostData := newNIPostWithChallenge(t, types.HexToHash32(""), []byte("0")) invalidAtx := newAtx(t, sigInvalid, ch, nipostData.NIPost, 2, types.Address{}) require.NoError(t, SignAndFinalizeAtx(sigInvalid, invalidAtx)) vInvalidAtx, err := invalidAtx.Verify(0, 100) require.NoError(t, err) - vInvalidAtx.SetValidity(types.Invalid) require.NoError(t, atxs.Add(tab.cdb, vInvalidAtx)) // Valid chain with lower height sigValid, err := signing.NewEdSigner() require.NoError(t, err) ch = newChallenge(1, types.EmptyATXID, tab.goldenATXID, postGenesisEpoch, &tab.goldenATXID) - nipostData = newNIPostWithChallenge(t, types.HexToHash32("for valid"), []byte("77")) + nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("1")) validAtx := newAtx(t, sigValid, ch, nipostData.NIPost, 2, types.Address{}) require.NoError(t, SignAndFinalizeAtx(sigValid, validAtx)) vValidAtx, err := validAtx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(tab.cdb, vValidAtx)) + tab.mValidator.EXPECT(). + VerifyChain(gomock.Any(), invalidAtx.ID(), tab.goldenATXID, gomock.Any()). + Return(errors.New("")) + tab.mValidator.EXPECT(). + VerifyChain(gomock.Any(), validAtx.ID(), tab.goldenATXID, gomock.Any()) + posAtxID, err := tab.getPositioningAtx(context.Background()) require.NoError(t, err) require.Equal(t, posAtxID, vValidAtx.ID()) } + +func TestGetPositioningAtxDbFailed(t *testing.T) { + tab := newTestBuilder(t) + db := datastoremocks.NewMockExecutor(gomock.NewController(t)) + tab.Builder.cdb = datastore.NewCachedDB(db, logtest.New(t)) + expected := errors.New("db error") + db.EXPECT().Exec(gomock.Any(), gomock.Any(), gomock.Any()).Return(0, expected) + + none, err := tab.getPositioningAtx(context.Background()) + require.ErrorIs(t, err, expected) + require.Equal(t, types.ATXID{}, none) +} diff --git a/activation/e2e/nipost_test.go b/activation/e2e/nipost_test.go index 02304b8113..df3a344d60 100644 --- a/activation/e2e/nipost_test.go +++ b/activation/e2e/nipost_test.go @@ -198,7 +198,7 @@ func TestNIPostBuilderWithClients(t *testing.T) { nipost, err := nb.BuildNIPost(context.Background(), &challenge) require.NoError(t, err) - v := activation.NewValidator(poetDb, cfg, opts.Scrypt, verifier) + v := activation.NewValidator(nil, poetDb, cfg, opts.Scrypt, verifier) _, err = v.NIPost( context.Background(), sig.NodeID(), @@ -344,7 +344,7 @@ func TestNewNIPostBuilderNotInitialized(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, verifier.Close()) }) - v := activation.NewValidator(poetDb, cfg, opts.Scrypt, verifier) + v := activation.NewValidator(nil, poetDb, cfg, opts.Scrypt, verifier) _, err = v.NIPost( context.Background(), sig.NodeID(), diff --git a/activation/e2e/validation_test.go b/activation/e2e/validation_test.go index 284d8420ed..08b2c0467f 100644 --- a/activation/e2e/validation_test.go +++ b/activation/e2e/validation_test.go @@ -115,7 +115,7 @@ func TestValidator_Validate(t *testing.T) { nipost, err := nb.BuildNIPost(context.Background(), &challenge) require.NoError(t, err) - v := activation.NewValidator(poetDb, cfg, opts.Scrypt, verifier) + v := activation.NewValidator(cdb, poetDb, cfg, opts.Scrypt, verifier) _, err = v.NIPost(context.Background(), sig.NodeID(), goldenATX, nipost.NIPost, challenge.Hash(), nipost.NumUnits) require.NoError(t, err) @@ -136,7 +136,7 @@ func TestValidator_Validate(t *testing.T) { newPostCfg := cfg newPostCfg.MinNumUnits = nipost.NumUnits + 1 - v = activation.NewValidator(poetDb, newPostCfg, opts.Scrypt, nil) + v = activation.NewValidator(cdb, poetDb, newPostCfg, opts.Scrypt, nil) _, err = v.NIPost(context.Background(), sig.NodeID(), goldenATX, nipost.NIPost, challenge.Hash(), nipost.NumUnits) require.EqualError( t, @@ -146,7 +146,7 @@ func TestValidator_Validate(t *testing.T) { newPostCfg = cfg newPostCfg.MaxNumUnits = nipost.NumUnits - 1 - v = activation.NewValidator(poetDb, newPostCfg, opts.Scrypt, nil) + v = activation.NewValidator(cdb, poetDb, newPostCfg, opts.Scrypt, nil) _, err = v.NIPost(context.Background(), sig.NodeID(), goldenATX, nipost.NIPost, challenge.Hash(), nipost.NumUnits) require.EqualError( t, @@ -156,7 +156,7 @@ func TestValidator_Validate(t *testing.T) { newPostCfg = cfg newPostCfg.LabelsPerUnit = nipost.PostMetadata.LabelsPerUnit + 1 - v = activation.NewValidator(poetDb, newPostCfg, opts.Scrypt, nil) + v = activation.NewValidator(cdb, poetDb, newPostCfg, opts.Scrypt, nil) _, err = v.NIPost(context.Background(), sig.NodeID(), goldenATX, nipost.NIPost, challenge.Hash(), nipost.NumUnits) require.EqualError( t, diff --git a/activation/interface.go b/activation/interface.go index 50ac088e87..3af8fb618d 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -64,6 +64,9 @@ type nipostValidator interface { numUnits uint32, ) error PositioningAtx(id types.ATXID, atxs atxProvider, goldenATXID types.ATXID, pubepoch types.EpochID) error + + // VerifyChain fully verifies all dependencies of the given ATX and the ATX itself. + VerifyChain(ctx context.Context, id, goldenATXID types.ATXID, opts ...VerifyChainOption) error } type layerClock interface { diff --git a/activation/mocks.go b/activation/mocks.go index 7ce925b741..1b4d08a64a 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -580,6 +580,49 @@ func (c *nipostValidatorVRFNonceCall) DoAndReturn(f func(types.NodeID, types.ATX return c } +// VerifyChain mocks base method. +func (m *MocknipostValidator) VerifyChain(ctx context.Context, id, goldenATXID types.ATXID, opts ...VerifyChainOption) error { + m.ctrl.T.Helper() + varargs := []any{ctx, id, goldenATXID} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "VerifyChain", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// VerifyChain indicates an expected call of VerifyChain. +func (mr *MocknipostValidatorMockRecorder) VerifyChain(ctx, id, goldenATXID any, opts ...any) *nipostValidatorVerifyChainCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, id, goldenATXID}, opts...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyChain", reflect.TypeOf((*MocknipostValidator)(nil).VerifyChain), varargs...) + return &nipostValidatorVerifyChainCall{Call: call} +} + +// nipostValidatorVerifyChainCall wrap *gomock.Call +type nipostValidatorVerifyChainCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *nipostValidatorVerifyChainCall) Return(arg0 error) *nipostValidatorVerifyChainCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *nipostValidatorVerifyChainCall) Do(f func(context.Context, types.ATXID, types.ATXID, ...VerifyChainOption) error) *nipostValidatorVerifyChainCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *nipostValidatorVerifyChainCall) DoAndReturn(f func(context.Context, types.ATXID, types.ATXID, ...VerifyChainOption) error) *nipostValidatorVerifyChainCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // MocklayerClock is a mock of layerClock interface. type MocklayerClock struct { ctrl *gomock.Controller diff --git a/activation/post.go b/activation/post.go index c5976071a5..120d1bf79d 100644 --- a/activation/post.go +++ b/activation/post.go @@ -129,7 +129,7 @@ func DefaultPostConfig() PostConfig { LabelsPerUnit: cfg.LabelsPerUnit, K1: cfg.K1, K2: cfg.K2, - K3: cfg.K2, + K3: cfg.K2, // The default is to verify all K2 indices. PowDifficulty: PowDifficulty(cfg.PowDifficulty), } } @@ -399,7 +399,8 @@ func (mgr *PostSetupManager) findCommitmentAtx(ctx context.Context) (types.ATXID mgr.goldenATXID, mgr.validator, mgr.logger, - assumeValidBefore(time.Now().Add(-mgr.postValidityDelay)), + VerifyChainOpts.AssumeValidBefore(time.Now().Add(-mgr.postValidityDelay)), + VerifyChainOpts.WithLogger(mgr.logger), ) switch { case errors.Is(err, sql.ErrNotFound): diff --git a/activation/post_test.go b/activation/post_test.go index 752dc6e13c..d13a61e492 100644 --- a/activation/post_test.go +++ b/activation/post_test.go @@ -350,6 +350,7 @@ func newTestPostManager(tb testing.TB) *testPostManager { validator.EXPECT(). Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). AnyTimes() + validator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() syncer := NewMocksyncer(gomock.NewController(tb)) synced := make(chan struct{}) close(synced) diff --git a/activation/validation.go b/activation/validation.go index 97f88b614a..dd2c347470 100644 --- a/activation/validation.go +++ b/activation/validation.go @@ -53,6 +53,7 @@ func PostSubset(seed []byte) validatorOption { // Validator contains the dependencies required to validate NIPosts. type Validator struct { + db sql.Executor poetDb poetDbAPI cfg PostConfig scrypt config.ScryptParams @@ -60,8 +61,14 @@ type Validator struct { } // NewValidator returns a new NIPost validator. -func NewValidator(poetDb poetDbAPI, cfg PostConfig, scrypt config.ScryptParams, postVerifier PostVerifier) *Validator { - return &Validator{poetDb, cfg, scrypt, postVerifier} +func NewValidator( + db sql.Executor, + poetDb poetDbAPI, + cfg PostConfig, + scrypt config.ScryptParams, + postVerifier PostVerifier, +) *Validator { + return &Validator{db, poetDb, cfg, scrypt, postVerifier} } // NIPost validates a NIPost, given a node id and expected challenge. It returns an error if the NIPost is invalid. @@ -294,51 +301,55 @@ func (v *Validator) PositioningAtx( type verifyChainOpts struct { assumedValidTime time.Time trustedNodeID types.NodeID + logger *zap.Logger } -type verifyChainOption func(*verifyChainOpts) +type verifyChainOptsNs struct{} + +var VerifyChainOpts verifyChainOptsNs -// assumeValidBefore configures the validator to assume that ATXs received before the given time are valid. -func assumeValidBefore(val time.Time) verifyChainOption { +type VerifyChainOption func(*verifyChainOpts) + +// AssumeValidBefore configures the validator to assume that ATXs received before the given time are valid. +func (verifyChainOptsNs) AssumeValidBefore(val time.Time) VerifyChainOption { return func(o *verifyChainOpts) { o.assumedValidTime = val } } -// withTrustedID configures the validator to assume that ATXs created by the given node ID are valid. -func withTrustedID(val types.NodeID) verifyChainOption { +// WithTrustedID configures the validator to assume that ATXs created by the given node ID are valid. +func (verifyChainOptsNs) WithTrustedID(val types.NodeID) VerifyChainOption { return func(o *verifyChainOpts) { o.trustedNodeID = val } } +func (verifyChainOptsNs) WithLogger(log *zap.Logger) VerifyChainOption { + return func(o *verifyChainOpts) { + o.logger = log + } +} + var ErrInvalidChain = errors.New("invalid ATX chain") -func verifyChain( - ctx context.Context, - db sql.Executor, - id, goldenATXID types.ATXID, - validator nipostValidator, - log *zap.Logger, - opts ...verifyChainOption, -) error { - log.Info("verifying ATX chain", zap.Stringer("atx_id", id)) - options := verifyChainOpts{} +func (v *Validator) VerifyChain(ctx context.Context, id, goldenATXID types.ATXID, opts ...VerifyChainOption) error { + options := verifyChainOpts{ + logger: zap.NewNop(), + } for _, opt := range opts { opt(&options) } - return verifyChainWithOpts(ctx, db, id, goldenATXID, validator, log, options) + options.logger.Info("verifying ATX chain", zap.Stringer("atx_id", id)) + return v.verifyChainWithOpts(ctx, id, goldenATXID, options) } -func verifyChainWithOpts( +func (v *Validator) verifyChainWithOpts( ctx context.Context, - db sql.Executor, id, goldenATXID types.ATXID, - validator nipostValidator, - log *zap.Logger, opts verifyChainOpts, ) error { - atx, err := atxs.Get(db, id) + log := opts.logger + atx, err := atxs.Get(v.db, id) if err != nil { return fmt.Errorf("get atx: %w", err) } @@ -367,13 +378,13 @@ func verifyChainWithOpts( // validate POST fully commitmentAtxId := atx.CommitmentATX if commitmentAtxId == nil { - if atxId, err := atxs.CommitmentATX(db, atx.SmesherID); err != nil { + if atxId, err := atxs.CommitmentATX(v.db, atx.SmesherID); err != nil { return fmt.Errorf("getting commitment atx: %w", err) } else { commitmentAtxId = &atxId } } - if err := validator.Post( + if err := v.Post( ctx, atx.SmesherID, *commitmentAtxId, @@ -381,47 +392,44 @@ func verifyChainWithOpts( atx.NIPost.PostMetadata, atx.NumUnits, ); err != nil { - if err := atxs.SetValidity(db, id, types.Invalid); err != nil { + if err := atxs.SetValidity(v.db, id, types.Invalid); err != nil { log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) } return errors.Join(ErrInvalidChain, fmt.Errorf("invalid post in ATX %s: %w", id.ShortString(), err)) } - err = verifyChainDeps(ctx, db, atx.ActivationTx, goldenATXID, validator, log, opts) + err = v.verifyChainDeps(ctx, atx.ActivationTx, goldenATXID, opts) switch { case err == nil: - if err := atxs.SetValidity(db, id, types.Valid); err != nil { + if err := atxs.SetValidity(v.db, id, types.Valid); err != nil { log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) } case errors.Is(err, ErrInvalidChain): - if err := atxs.SetValidity(db, id, types.Invalid); err != nil { + if err := atxs.SetValidity(v.db, id, types.Invalid); err != nil { log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) } } return err } -func verifyChainDeps( +func (v *Validator) verifyChainDeps( ctx context.Context, - db sql.Executor, atx *types.ActivationTx, goldenATXID types.ATXID, - v nipostValidator, - log *zap.Logger, opts verifyChainOpts, ) error { if atx.PrevATXID != types.EmptyATXID { - if err := verifyChainWithOpts(ctx, db, atx.PrevATXID, goldenATXID, v, log, opts); err != nil { + if err := v.verifyChainWithOpts(ctx, atx.PrevATXID, goldenATXID, opts); err != nil { return fmt.Errorf("validating previous ATX %s chain: %w", atx.PrevATXID.ShortString(), err) } } if atx.PositioningATX != goldenATXID { - if err := verifyChainWithOpts(ctx, db, atx.PositioningATX, goldenATXID, v, log, opts); err != nil { + if err := v.verifyChainWithOpts(ctx, atx.PositioningATX, goldenATXID, opts); err != nil { return fmt.Errorf("validating positioning ATX %s chain: %w", atx.PositioningATX.ShortString(), err) } } if atx.CommitmentATX != nil && *atx.CommitmentATX != goldenATXID { - if err := verifyChainWithOpts(ctx, db, *atx.CommitmentATX, goldenATXID, v, log, opts); err != nil { + if err := v.verifyChainWithOpts(ctx, *atx.CommitmentATX, goldenATXID, opts); err != nil { return fmt.Errorf("validating commitment ATX %s chain: %w", atx.CommitmentATX.ShortString(), err) } } diff --git a/activation/validation_test.go b/activation/validation_test.go index 1244a4d016..7a3cb19bb9 100644 --- a/activation/validation_test.go +++ b/activation/validation_test.go @@ -12,7 +12,6 @@ import ( "github.com/spacemeshos/post/shared" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/signing" @@ -51,7 +50,7 @@ func Test_Validation_VRFNonce(t *testing.T) { nonce := (*types.VRFPostIndex)(init.Nonce()) - v := NewValidator(poetDbAPI, postCfg, initOpts.Scrypt, nil) + v := NewValidator(nil, poetDbAPI, postCfg, initOpts.Scrypt, nil) // Act & Assert t.Run("valid vrf nonce", func(t *testing.T) { @@ -92,7 +91,7 @@ func Test_Validation_InitialNIPostChallenge(t *testing.T) { postCfg := DefaultPostConfig() goldenATXID := types.ATXID{2, 3, 4} - v := NewValidator(poetDbAPI, postCfg, config.ScryptParams{}, nil) + v := NewValidator(nil, poetDbAPI, postCfg, config.ScryptParams{}, nil) // Act & Assert t.Run("valid initial nipost challenge passes", func(t *testing.T) { @@ -160,7 +159,7 @@ func Test_Validation_NIPostChallenge(t *testing.T) { poetDbAPI := NewMockpoetDbAPI(ctrl) postCfg := DefaultPostConfig() - v := NewValidator(poetDbAPI, postCfg, config.ScryptParams{}, nil) + v := NewValidator(nil, poetDbAPI, postCfg, config.ScryptParams{}, nil) // Act & Assert t.Run("valid nipost challenge passes", func(t *testing.T) { @@ -286,7 +285,7 @@ func Test_Validation_Post(t *testing.T) { postCfg := DefaultPostConfig() postVerifier := NewMockPostVerifier(ctrl) - v := NewValidator(poetDbAPI, postCfg, config.ScryptParams{}, postVerifier) + v := NewValidator(nil, poetDbAPI, postCfg, config.ScryptParams{}, postVerifier) post := types.Post{} meta := types.PostMetadata{} @@ -310,7 +309,7 @@ func Test_Validation_PositioningAtx(t *testing.T) { poetDbAPI := NewMockpoetDbAPI(ctrl) postCfg := DefaultPostConfig() - v := NewValidator(poetDbAPI, postCfg, config.ScryptParams{}, nil) + v := NewValidator(nil, poetDbAPI, postCfg, config.ScryptParams{}, nil) // Act & Assert t.Run("valid nipost challenge passes", func(t *testing.T) { @@ -414,7 +413,7 @@ func Test_Validate_NumUnits(t *testing.T) { poetDbAPI := NewMockpoetDbAPI(ctrl) postCfg := DefaultPostConfig() - v := NewValidator(poetDbAPI, postCfg, config.ScryptParams{}, nil) + v := NewValidator(nil, poetDbAPI, postCfg, config.ScryptParams{}, nil) // Act & Assert t.Run("valid number of num units passes", func(t *testing.T) { @@ -456,7 +455,7 @@ func Test_Validate_PostMetadata(t *testing.T) { poetDbAPI := NewMockpoetDbAPI(ctrl) postCfg := DefaultPostConfig() - v := NewValidator(poetDbAPI, postCfg, config.ScryptParams{}, nil) + v := NewValidator(nil, poetDbAPI, postCfg, config.ScryptParams{}, nil) // Act & Assert t.Run("valid post metadata", func(t *testing.T) { @@ -525,7 +524,6 @@ func TestValidateMerkleProof(t *testing.T) { func TestVerifyChainDeps(t *testing.T) { ctrl := gomock.NewController(t) - logger := zaptest.NewLogger(t) db := sql.InMemory() ctx := context.Background() goldenATXID := types.ATXID{2, 3, 4} @@ -551,12 +549,11 @@ func TestVerifyChainDeps(t *testing.T) { vAtx.SetValidity(types.Unknown) require.NoError(t, atxs.Add(db, vAtx)) - v := NewMocknipostValidator(ctrl) - v.EXPECT(). - Post(ctx, signer.NodeID(), goldenATXID, atx.NIPost.Post, atx.NIPost.PostMetadata, atx.NumUnits). - Return(nil) + v := NewMockPostVerifier(ctrl) + v.EXPECT().Verify(ctx, (*shared.Proof)(atx.NIPost.Post), gomock.Any(), gomock.Any()) - err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) + validator := NewValidator(db, nil, DefaultPostConfig(), config.ScryptParams{}, v) + err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID) require.ErrorIs(t, err, ErrInvalidChain) }) @@ -570,12 +567,11 @@ func TestVerifyChainDeps(t *testing.T) { vAtx.SetValidity(types.Unknown) require.NoError(t, atxs.Add(db, vAtx)) - v := NewMocknipostValidator(ctrl) - v.EXPECT(). - Post(ctx, signer.NodeID(), goldenATXID, atx.NIPost.Post, atx.NIPost.PostMetadata, atx.NumUnits). - Return(nil) + v := NewMockPostVerifier(ctrl) + v.EXPECT().Verify(ctx, (*shared.Proof)(atx.NIPost.Post), gomock.Any(), gomock.Any()) - err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) + validator := NewValidator(db, nil, DefaultPostConfig(), config.ScryptParams{}, v) + err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID) require.ErrorIs(t, err, ErrInvalidChain) }) @@ -590,12 +586,10 @@ func TestVerifyChainDeps(t *testing.T) { vAtx.SetValidity(types.Unknown) require.NoError(t, atxs.Add(db, vAtx)) - v := NewMocknipostValidator(ctrl) - v.EXPECT(). - Post(ctx, signer.NodeID(), commitmentAtxID, atx.NIPost.Post, atx.NIPost.PostMetadata, atx.NumUnits). - Return(nil) - - err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger) + v := NewMockPostVerifier(ctrl) + v.EXPECT().Verify(ctx, (*shared.Proof)(atx.NIPost.Post), gomock.Any(), gomock.Any()) + validator := NewValidator(db, nil, DefaultPostConfig(), config.ScryptParams{}, v) + err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID) require.ErrorIs(t, err, ErrInvalidChain) }) @@ -609,8 +603,9 @@ func TestVerifyChainDeps(t *testing.T) { vAtx.SetValidity(types.Unknown) require.NoError(t, atxs.Add(db, vAtx)) - v := NewMocknipostValidator(ctrl) - err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, withTrustedID(signer.NodeID())) + v := NewMockPostVerifier(ctrl) + validator := NewValidator(db, nil, DefaultPostConfig(), config.ScryptParams{}, v) + err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID, VerifyChainOpts.WithTrustedID(signer.NodeID())) require.NoError(t, err) }) @@ -624,8 +619,9 @@ func TestVerifyChainDeps(t *testing.T) { vAtx.SetValidity(types.Unknown) require.NoError(t, atxs.Add(db, vAtx)) - v := NewMocknipostValidator(ctrl) - err = verifyChain(ctx, db, vAtx.ID(), goldenATXID, v, logger, assumeValidBefore(time.Now())) + v := NewMockPostVerifier(ctrl) + validator := NewValidator(db, nil, DefaultPostConfig(), config.ScryptParams{}, v) + err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID, VerifyChainOpts.AssumeValidBefore(time.Now())) require.NoError(t, err) }) } diff --git a/common/types/malfeasance.go b/common/types/malfeasance.go index 33bdc843bb..4e6cd6b744 100644 --- a/common/types/malfeasance.go +++ b/common/types/malfeasance.go @@ -152,7 +152,7 @@ func (e *Proof) DecodeScale(dec *scale.Decoder) (int, error) { e.Data = &proof total += n default: - return total, errors.New("invalid ballot malfeasance proof") + return total, errors.New("unknown malfeasance proof type") } return total, nil } diff --git a/datastore/mocks/mocks.go b/datastore/mocks/mocks.go new file mode 100644 index 0000000000..4046a79488 --- /dev/null +++ b/datastore/mocks/mocks.go @@ -0,0 +1,117 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./store.go +// +// Generated by this command: +// +// mockgen -typed -package=mocks -destination=./mocks/mocks.go -source=./store.go +// +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + sql "github.com/spacemeshos/go-spacemesh/sql" + gomock "go.uber.org/mock/gomock" +) + +// MockExecutor is a mock of Executor interface. +type MockExecutor struct { + ctrl *gomock.Controller + recorder *MockExecutorMockRecorder +} + +// MockExecutorMockRecorder is the mock recorder for MockExecutor. +type MockExecutorMockRecorder struct { + mock *MockExecutor +} + +// NewMockExecutor creates a new mock instance. +func NewMockExecutor(ctrl *gomock.Controller) *MockExecutor { + mock := &MockExecutor{ctrl: ctrl} + mock.recorder = &MockExecutorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExecutor) EXPECT() *MockExecutorMockRecorder { + return m.recorder +} + +// Exec mocks base method. +func (m *MockExecutor) Exec(arg0 string, arg1 sql.Encoder, arg2 sql.Decoder) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Exec", arg0, arg1, arg2) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Exec indicates an expected call of Exec. +func (mr *MockExecutorMockRecorder) Exec(arg0, arg1, arg2 any) *ExecutorExecCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockExecutor)(nil).Exec), arg0, arg1, arg2) + return &ExecutorExecCall{Call: call} +} + +// ExecutorExecCall wrap *gomock.Call +type ExecutorExecCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *ExecutorExecCall) Return(arg0 int, arg1 error) *ExecutorExecCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *ExecutorExecCall) Do(f func(string, sql.Encoder, sql.Decoder) (int, error)) *ExecutorExecCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *ExecutorExecCall) DoAndReturn(f func(string, sql.Encoder, sql.Decoder) (int, error)) *ExecutorExecCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// WithTx mocks base method. +func (m *MockExecutor) WithTx(arg0 context.Context, arg1 func(*sql.Tx) error) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithTx", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// WithTx indicates an expected call of WithTx. +func (mr *MockExecutorMockRecorder) WithTx(arg0, arg1 any) *ExecutorWithTxCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithTx", reflect.TypeOf((*MockExecutor)(nil).WithTx), arg0, arg1) + return &ExecutorWithTxCall{Call: call} +} + +// ExecutorWithTxCall wrap *gomock.Call +type ExecutorWithTxCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *ExecutorWithTxCall) Return(arg0 error) *ExecutorWithTxCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *ExecutorWithTxCall) Do(f func(context.Context, func(*sql.Tx) error) error) *ExecutorWithTxCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *ExecutorWithTxCall) DoAndReturn(f func(context.Context, func(*sql.Tx) error) error) *ExecutorWithTxCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/datastore/store.go b/datastore/store.go index 5be0837359..1501ae2128 100644 --- a/datastore/store.go +++ b/datastore/store.go @@ -1,6 +1,7 @@ package datastore import ( + "context" "errors" "fmt" "sync" @@ -27,9 +28,16 @@ type VrfNonceKey struct { Epoch types.EpochID } +//go:generate mockgen -typed -package=mocks -destination=./mocks/mocks.go -source=./store.go + +type Executor interface { + sql.Executor + WithTx(context.Context, func(*sql.Tx) error) error +} + // CachedDB is simply a database injected with cache. type CachedDB struct { - *sql.Database + Executor logger log.Log // cache is optional @@ -77,7 +85,7 @@ func WithConsensusCache(c *atxsdata.Data) Opt { } // NewCachedDB create an instance of a CachedDB. -func NewCachedDB(db *sql.Database, lg log.Log, opts ...Opt) *CachedDB { +func NewCachedDB(db Executor, lg log.Log, opts ...Opt) *CachedDB { o := cacheOpts{cfg: DefaultConfig()} for _, opt := range opts { opt(&o) @@ -100,7 +108,7 @@ func NewCachedDB(db *sql.Database, lg log.Log, opts ...Opt) *CachedDB { } return &CachedDB{ - Database: db, + Executor: db, atxsdata: o.atxsdata, logger: lg, atxHdrCache: atxHdrCache, @@ -154,7 +162,7 @@ func (db *CachedDB) GetMalfeasanceProof(id types.NodeID) (*types.MalfeasanceProo return proof, nil } - proof, err := identities.GetMalfeasanceProof(db.Database, id) + proof, err := identities.GetMalfeasanceProof(db.Executor, id) if err != nil && err != sql.ErrNotFound { return nil, err } @@ -349,13 +357,13 @@ const ( ) // NewBlobStore returns a BlobStore. -func NewBlobStore(db *sql.Database) *BlobStore { +func NewBlobStore(db sql.Executor) *BlobStore { return &BlobStore{DB: db} } // BlobStore gets data as a blob to serve direct fetch requests. type BlobStore struct { - DB *sql.Database + DB sql.Executor } // Get gets an ATX as bytes by an ATX ID as bytes. diff --git a/fetch/fetch.go b/fetch/fetch.go index 3a411f7032..25644df458 100644 --- a/fetch/fetch.go +++ b/fetch/fetch.go @@ -230,7 +230,7 @@ func NewFetch( host *p2p.Host, opts ...Option, ) *Fetch { - bs := datastore.NewBlobStore(cdb.Database) + bs := datastore.NewBlobStore(cdb) f := &Fetch{ cfg: DefaultConfig(), diff --git a/fetch/handler_test.go b/fetch/handler_test.go index 229e2d462d..7c767df23c 100644 --- a/fetch/handler_test.go +++ b/fetch/handler_test.go @@ -31,7 +31,7 @@ func createTestHandler(t testing.TB) *testHandler { lg := logtest.New(t) cdb := datastore.NewCachedDB(sql.InMemory(), lg) return &testHandler{ - handler: newHandler(cdb, datastore.NewBlobStore(cdb.Database), lg), + handler: newHandler(cdb, datastore.NewBlobStore(cdb), lg), cdb: cdb, } } diff --git a/node/node.go b/node/node.go index 6a9b2be97e..1d1dc9db6a 100644 --- a/node/node.go +++ b/node/node.go @@ -602,6 +602,7 @@ func (app *App) initServices(ctx context.Context) error { app.postVerifier.Autoscale(minWorkers, workers) validator := activation.NewValidator( + app.db, poetDb, app.Config.POST, app.Config.SMESHING.Opts.Scrypt, diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index ff35492af9..f19bfc1ead 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -335,8 +335,6 @@ func GetIDWithMaxHeight(db sql.Executor, pref types.NodeID, filter Filter) (type var id types.ATXID stmt.ColumnBytes(0, id[:]) height := uint64(stmt.ColumnInt64(1)) - epoch := uint64(stmt.ColumnInt64(3)) - _ = epoch switch { case height < highest: @@ -362,7 +360,7 @@ func GetIDWithMaxHeight(db sql.Executor, pref types.NodeID, filter Filter) (type } _, err := db.Exec(` - SELECT id, base_tick_height + tick_count AS height, pubkey, epoch + SELECT id, base_tick_height + tick_count AS height, pubkey FROM atxs LEFT JOIN identities using(pubkey) WHERE identities.pubkey is null and epoch >= (select max(epoch) from atxs)-1 ORDER BY height DESC, epoch DESC;`, nil, dec) diff --git a/syncer/syncer.go b/syncer/syncer.go index bfe9cd6949..09ac5e5f62 100644 --- a/syncer/syncer.go +++ b/syncer/syncer.go @@ -170,7 +170,7 @@ func NewSyncer( s.dataFetcher = NewDataFetch(mesh, fetcher, cdb, cache, s.logger) } if s.forkFinder == nil { - s.forkFinder = NewForkFinder(s.logger, cdb.Database, fetcher, s.cfg.MaxStaleDuration) + s.forkFinder = NewForkFinder(s.logger, cdb, fetcher, s.cfg.MaxStaleDuration) } s.syncState.Store(notSynced) s.atxSyncState.Store(notSynced) From a5ddcc5afab16d81b4e28d890beb32925f143b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 26 Jan 2024 15:24:34 +0100 Subject: [PATCH 18/30] Fix TestMigrationsAppliedOnce --- sql/migrations_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sql/migrations_test.go b/sql/migrations_test.go index ebd5c10115..bb0f8a95d1 100644 --- a/sql/migrations_test.go +++ b/sql/migrations_test.go @@ -1,6 +1,7 @@ package sql import ( + "slices" "testing" "github.com/stretchr/testify/require" @@ -15,5 +16,11 @@ func TestMigrationsAppliedOnce(t *testing.T) { return true }) require.NoError(t, err) - require.Equal(t, 11, version) + + migrations, err := StateMigrations() + require.NoError(t, err) + lastMigration := slices.MaxFunc(migrations, func(a, b Migration) int { + return a.Order() - b.Order() + }) + require.Equal(t, lastMigration.Order(), version) } From 1386d47bb5a1d72e062ef363639fedc3e73c28e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Mon, 29 Jan 2024 12:35:01 +0100 Subject: [PATCH 19/30] Refactor ErrInvalidChain as per review --- activation/handler.go | 2 +- activation/validation.go | 29 +++++++++++++++++++++++++---- activation/validation_test.go | 25 ++++++++++++++++++++++--- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/activation/handler.go b/activation/handler.go index 41e39dd270..af66f42030 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -259,7 +259,7 @@ func (h *Handler) SyntacticallyValidateDeps( } h.cdb.CacheMalfeasanceProof(atx.SmesherID, &gossip.MalfeasanceProof) h.tortoise.OnMalfeasance(atx.SmesherID) - return nil, errors.Join(errMaliciousATX, err) + return nil, errMaliciousATX } if err != nil { return nil, fmt.Errorf("invalid nipost: %w", err) diff --git a/activation/validation.go b/activation/validation.go index dd2c347470..dd347adc2b 100644 --- a/activation/validation.go +++ b/activation/validation.go @@ -330,7 +330,27 @@ func (verifyChainOptsNs) WithLogger(log *zap.Logger) VerifyChainOption { } } -var ErrInvalidChain = errors.New("invalid ATX chain") +type InvalidChainError struct { + ID types.ATXID + src error +} + +func (e *InvalidChainError) Error() string { + msg := fmt.Sprintf("invalid POST found in ATX chain for ID %v", e.ID.String()) + if e.src != nil { + msg = fmt.Sprintf("%s: %v", msg, e.src) + } + return msg +} + +func (e *InvalidChainError) Unwrap() error { return e.src } + +func (e *InvalidChainError) Is(target error) bool { + if err, ok := target.(*InvalidChainError); ok { + return err.ID == e.ID + } + return false +} func (v *Validator) VerifyChain(ctx context.Context, id, goldenATXID types.ATXID, opts ...VerifyChainOption) error { options := verifyChainOpts{ @@ -360,7 +380,7 @@ func (v *Validator) verifyChainWithOpts( return nil case atx.Validity() == types.Invalid: log.Debug("not verifying ATX chain", zap.Stringer("atx_id", id), zap.String("reason", "invalid")) - return errors.Join(ErrInvalidChain, errors.New("atx is marked as invalid")) + return &InvalidChainError{ID: id} case atx.Received().Before(opts.assumedValidTime): log.Debug( "not verifying ATX chain", @@ -395,16 +415,17 @@ func (v *Validator) verifyChainWithOpts( if err := atxs.SetValidity(v.db, id, types.Invalid); err != nil { log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) } - return errors.Join(ErrInvalidChain, fmt.Errorf("invalid post in ATX %s: %w", id.ShortString(), err)) + return &InvalidChainError{ID: id, src: err} } err = v.verifyChainDeps(ctx, atx.ActivationTx, goldenATXID, opts) + invalidChain := &InvalidChainError{} switch { case err == nil: if err := atxs.SetValidity(v.db, id, types.Valid); err != nil { log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) } - case errors.Is(err, ErrInvalidChain): + case errors.As(err, &invalidChain): if err := atxs.SetValidity(v.db, id, types.Invalid); err != nil { log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) } diff --git a/activation/validation_test.go b/activation/validation_test.go index 7a3cb19bb9..ff7b8dce5a 100644 --- a/activation/validation_test.go +++ b/activation/validation_test.go @@ -554,7 +554,7 @@ func TestVerifyChainDeps(t *testing.T) { validator := NewValidator(db, nil, DefaultPostConfig(), config.ScryptParams{}, v) err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID) - require.ErrorIs(t, err, ErrInvalidChain) + require.ErrorIs(t, err, &InvalidChainError{ID: invalidAtx.ID()}) }) t.Run("invalid pos ATX", func(t *testing.T) { @@ -572,7 +572,7 @@ func TestVerifyChainDeps(t *testing.T) { validator := NewValidator(db, nil, DefaultPostConfig(), config.ScryptParams{}, v) err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID) - require.ErrorIs(t, err, ErrInvalidChain) + require.ErrorIs(t, err, &InvalidChainError{ID: invalidAtx.ID()}) }) t.Run("invalid commitment ATX", func(t *testing.T) { @@ -590,7 +590,7 @@ func TestVerifyChainDeps(t *testing.T) { v.EXPECT().Verify(ctx, (*shared.Proof)(atx.NIPost.Post), gomock.Any(), gomock.Any()) validator := NewValidator(db, nil, DefaultPostConfig(), config.ScryptParams{}, v) err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID) - require.ErrorIs(t, err, ErrInvalidChain) + require.ErrorIs(t, err, &InvalidChainError{ID: invalidAtx.ID()}) }) t.Run("with trusted node ID", func(t *testing.T) { @@ -624,4 +624,23 @@ func TestVerifyChainDeps(t *testing.T) { err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID, VerifyChainOpts.AssumeValidBefore(time.Now())) require.NoError(t, err) }) + + t.Run("invalid top-level", func(t *testing.T) { + ch = newChallenge(1, types.EmptyATXID, vInvalidAtx.ID(), postGenesisEpoch, nil) + nipostData = newNIPostWithChallenge(t, types.HexToHash32(""), []byte("06")) + atx := newAtx(t, signer, ch, nipostData.NIPost, 2, types.Address{}) + require.NoError(t, SignAndFinalizeAtx(signer, atx)) + vAtx, err := atx.Verify(0, 1) + require.NoError(t, err) + vAtx.SetValidity(types.Unknown) + require.NoError(t, atxs.Add(db, vAtx)) + + expected := errors.New("post is invalid") + v := NewMockPostVerifier(ctrl) + v.EXPECT().Verify(ctx, (*shared.Proof)(atx.NIPost.Post), gomock.Any(), gomock.Any()).Return(expected) + validator := NewValidator(db, nil, DefaultPostConfig(), config.ScryptParams{}, v) + err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID) + require.ErrorIs(t, err, &InvalidChainError{ID: vAtx.ID()}) + require.ErrorIs(t, err, expected) + }) } From b164fb021a4a75cbaec7a2cf361420ad987fa4b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 31 Jan 2024 09:55:38 +0100 Subject: [PATCH 20/30] Mark stored ATXs 'valid' if k3=k2 --- activation/handler.go | 4 ++- activation/handler_test.go | 57 +++++++++++++++++++++++++++++++++++ activation/interface.go | 3 ++ activation/mocks.go | 38 +++++++++++++++++++++++ activation/validation.go | 4 +++ activation/validation_test.go | 14 +++++++++ checkpoint/recovery_test.go | 1 + 7 files changed, 120 insertions(+), 1 deletion(-) diff --git a/activation/handler.go b/activation/handler.go index af66f42030..c05cf33654 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -264,7 +264,9 @@ func (h *Handler) SyntacticallyValidateDeps( if err != nil { return nil, fmt.Errorf("invalid nipost: %w", err) } - + if h.nipostValidator.IsVerifyingFullPost() { + atx.SetValidity(types.Valid) + } return atx.Verify(baseTickHeight, leaves/h.tickSize) } diff --git a/activation/handler_test.go b/activation/handler_test.go index 96699dce8b..07360df5db 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -362,6 +362,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(1), nil) + atxHdlr.mValidator.EXPECT().IsVerifyingFullPost() atxHdlr.mValidator.EXPECT().NIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) _, err := atxHdlr.SyntacticallyValidateDeps(context.Background(), atx) @@ -387,6 +388,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(1), nil) + atxHdlr.mValidator.EXPECT().IsVerifyingFullPost() atxHdlr.mValidator.EXPECT().NIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT(). @@ -419,6 +421,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(1), nil) + atxHdlr.mValidator.EXPECT().IsVerifyingFullPost() atxHdlr.mValidator.EXPECT().NIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) vAtx, err := atxHdlr.SyntacticallyValidateDeps(context.Background(), atx) @@ -450,6 +453,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(1), nil) + atxHdlr.mValidator.EXPECT().IsVerifyingFullPost() atxHdlr.mValidator.EXPECT().NIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT(). @@ -521,6 +525,7 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(1), nil) + atxHdlr.mValidator.EXPECT().IsVerifyingFullPost() atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) _, err := atxHdlr.SyntacticallyValidateDeps(context.Background(), atx) require.NoError(t, err) @@ -1290,6 +1295,7 @@ func TestHandler_HandleGossipAtx(t *testing.T) { atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), nodeID1, goldenATXID, second.NIPost, gomock.Any(), second.NumUnits, gomock.Any()) atxHdlr.mValidator.EXPECT().PositioningAtx(second.PositioningATX, gomock.Any(), goldenATXID, second.PublishEpoch) + atxHdlr.mValidator.EXPECT().IsVerifyingFullPost().AnyTimes().Return(true) atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any()) require.NoError(t, atxHdlr.HandleGossipAtx(context.Background(), "", secondData)) @@ -1354,6 +1360,7 @@ func TestHandler_HandleParallelGossipAtx(t *testing.T) { atxHdlr.mValidator.EXPECT().PositioningAtx(goldenATXID, gomock.Any(), goldenATXID, atx.PublishEpoch) atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), nodeID, goldenATXID, atx.NIPost, gomock.Any(), atx.NumUnits, gomock.Any()) + atxHdlr.mValidator.EXPECT().IsVerifyingFullPost() atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any()) @@ -1732,6 +1739,7 @@ func TestHandler_AtxWeight(t *testing.T) { atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(leaves, nil) + atxHdlr.mValidator.EXPECT().IsVerifyingFullPost() atxHdlr.mValidator.EXPECT().InitialNIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) @@ -1776,6 +1784,7 @@ func TestHandler_AtxWeight(t *testing.T) { atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(leaves, nil) + atxHdlr.mValidator.EXPECT().IsVerifyingFullPost() atxHdlr.mValidator.EXPECT().NIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) @@ -1833,9 +1842,57 @@ func TestHandler_WrongHash(t *testing.T) { atxHdlr.mValidator.EXPECT(). NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(uint64(111), nil) + atxHdlr.mValidator.EXPECT().IsVerifyingFullPost() atxHdlr.mValidator.EXPECT().InitialNIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) atxHdlr.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) err = atxHdlr.HandleSyncedAtx(context.Background(), types.RandomHash(), peer, buf) require.ErrorIs(t, err, errWrongHash) require.ErrorIs(t, err, pubsub.ErrValidationReject) } + +func TestHandler_MarksAtxValid(t *testing.T) { + t.Parallel() + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + goldenATXID := types.ATXID{2, 3, 4} + challenge := newChallenge(0, types.EmptyATXID, goldenATXID, 2, &goldenATXID) + nipost := newNIPostWithChallenge(t, challenge.Hash(), []byte("poet")).NIPost + + t.Run("post verified fully", func(t *testing.T) { + t.Parallel() + handler := newTestHandler(t, goldenATXID) + handler.mValidator.EXPECT().InitialNIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()) + handler.mValidator.EXPECT(). + NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(uint64(1), nil) + handler.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + handler.mValidator.EXPECT().IsVerifyingFullPost().Return(true) + + atx := newAtx(t, sig, challenge, nipost, 2, types.Address{1, 2, 3, 4}) + atx.SetValidity(types.Unknown) + require.NoError(t, SignAndFinalizeAtx(sig, atx)) + _, err := handler.SyntacticallyValidateDeps(context.Background(), atx) + require.NoError(t, err) + require.Equal(t, types.Valid, atx.Validity()) + }) + t.Run("post verified fully", func(t *testing.T) { + t.Parallel() + handler := newTestHandler(t, goldenATXID) + handler.mValidator.EXPECT().InitialNIPostChallenge(gomock.Any(), gomock.Any(), gomock.Any()) + handler.mValidator.EXPECT(). + NIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(uint64(1), nil) + handler.mValidator.EXPECT().PositioningAtx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + handler.mValidator.EXPECT().IsVerifyingFullPost().Return(false) + + atx := newAtx(t, sig, challenge, nipost, 2, types.Address{1, 2, 3, 4}) + atx.SetValidity(types.Unknown) + require.NoError(t, SignAndFinalizeAtx(sig, atx)) + _, err := handler.SyntacticallyValidateDeps(context.Background(), atx) + require.NoError(t, err) + require.Equal(t, types.Unknown, atx.Validity()) + }) + require.NoError(t, err) +} diff --git a/activation/interface.go b/activation/interface.go index 3af8fb618d..e546044221 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -45,6 +45,9 @@ type nipostValidator interface { ) (uint64, error) NumUnits(cfg *PostConfig, numUnits uint32) error + + IsVerifyingFullPost() bool + Post( ctx context.Context, nodeId types.NodeID, diff --git a/activation/mocks.go b/activation/mocks.go index 1b4d08a64a..d8cabb115a 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -303,6 +303,44 @@ func (c *nipostValidatorInitialNIPostChallengeCall) DoAndReturn(f func(*types.NI return c } +// IsVerifyingFullPost mocks base method. +func (m *MocknipostValidator) IsVerifyingFullPost() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsVerifyingFullPost") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsVerifyingFullPost indicates an expected call of IsVerifyingFullPost. +func (mr *MocknipostValidatorMockRecorder) IsVerifyingFullPost() *nipostValidatorIsVerifyingFullPostCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsVerifyingFullPost", reflect.TypeOf((*MocknipostValidator)(nil).IsVerifyingFullPost)) + return &nipostValidatorIsVerifyingFullPostCall{Call: call} +} + +// nipostValidatorIsVerifyingFullPostCall wrap *gomock.Call +type nipostValidatorIsVerifyingFullPostCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *nipostValidatorIsVerifyingFullPostCall) Return(arg0 bool) *nipostValidatorIsVerifyingFullPostCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *nipostValidatorIsVerifyingFullPostCall) Do(f func() bool) *nipostValidatorIsVerifyingFullPostCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *nipostValidatorIsVerifyingFullPostCall) DoAndReturn(f func() bool) *nipostValidatorIsVerifyingFullPostCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // NIPost mocks base method. func (m *MocknipostValidator) NIPost(ctx context.Context, nodeId types.NodeID, commitmentAtxId types.ATXID, NIPost *types.NIPost, expectedChallenge types.Hash32, numUnits uint32, opts ...validatorOption) (uint64, error) { m.ctrl.T.Helper() diff --git a/activation/validation.go b/activation/validation.go index dd347adc2b..38b6b8d282 100644 --- a/activation/validation.go +++ b/activation/validation.go @@ -141,6 +141,10 @@ func validateMerkleProof(leaf []byte, proof *types.MerkleProof, expectedRoot []b return nil } +func (v *Validator) IsVerifyingFullPost() bool { + return v.cfg.K3 >= v.cfg.K2 +} + // Post validates a Proof of Space-Time (PoST). It returns nil if validation passed or an error indicating why // validation failed. func (v *Validator) Post( diff --git a/activation/validation_test.go b/activation/validation_test.go index ff7b8dce5a..3674685b38 100644 --- a/activation/validation_test.go +++ b/activation/validation_test.go @@ -644,3 +644,17 @@ func TestVerifyChainDeps(t *testing.T) { require.ErrorIs(t, err, expected) }) } + +func TestIsVerifyingFullPost(t *testing.T) { + t.Run("full", func(t *testing.T) { + t.Parallel() + validator := NewValidator(nil, nil, PostConfig{K2: 7, K3: 7}, config.ScryptParams{}, nil) + require.True(t, validator.IsVerifyingFullPost()) + }) + + t.Run("partial", func(t *testing.T) { + t.Parallel() + validator := NewValidator(nil, nil, PostConfig{K2: 7, K3: 2}, config.ScryptParams{}, nil) + require.False(t, validator.IsVerifyingFullPost()) + }) +} diff --git a/checkpoint/recovery_test.go b/checkpoint/recovery_test.go index 73085e3d98..fa0f389b65 100644 --- a/checkpoint/recovery_test.go +++ b/checkpoint/recovery_test.go @@ -276,6 +276,7 @@ func validateAndPreserveData( mvalidator.EXPECT(). NIPost(gomock.Any(), vatx.SmesherID, gomock.Any(), vatx.NIPost, gomock.Any(), vatx.NumUnits, gomock.Any()). Return(uint64(1111111), nil) + mvalidator.EXPECT().IsVerifyingFullPost().AnyTimes().Return(true) mreceiver.EXPECT().OnAtx(gomock.Any()) mtrtl.EXPECT().OnAtx(gomock.Any()) require.NoError(tb, atxHandler.HandleSyncedAtx(context.Background(), vatx.ID().Hash32(), "self", encoded)) From 2e8ee523a9b4a8fd9c1cb9d957426b6f5afd5fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 31 Jan 2024 11:22:15 +0100 Subject: [PATCH 21/30] Improve documentation & help of post-k3 config parameter --- activation/post.go | 13 ++++++++----- cmd/root.go | 10 ++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/activation/post.go b/activation/post.go index 120d1bf79d..48486c8a6d 100644 --- a/activation/post.go +++ b/activation/post.go @@ -25,11 +25,14 @@ type PostSetupProvider initialization.Provider // PostConfig is the configuration of the Post protocol, used for data creation, proofs generation and validation. type PostConfig struct { - MinNumUnits uint32 `mapstructure:"post-min-numunits"` - MaxNumUnits uint32 `mapstructure:"post-max-numunits"` - LabelsPerUnit uint64 `mapstructure:"post-labels-per-unit"` - K1 uint `mapstructure:"post-k1"` - K2 uint `mapstructure:"post-k2"` + MinNumUnits uint32 `mapstructure:"post-min-numunits"` + MaxNumUnits uint32 `mapstructure:"post-max-numunits"` + LabelsPerUnit uint64 `mapstructure:"post-labels-per-unit"` + K1 uint `mapstructure:"post-k1"` + K2 uint `mapstructure:"post-k2"` + // size of the subset of labels to verify in POST proofs + // lower values will result in faster ATX verification but increase the risk + // as the node must depend on malfeasance proofs to detect invalid ATXs K3 uint `mapstructure:"post-k3"` PowDifficulty PowDifficulty `mapstructure:"post-pow-difficulty"` } diff --git a/cmd/root.go b/cmd/root.go index 72c4893bab..c4a3a1ab59 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -240,8 +240,14 @@ func AddCommands(cmd *cobra.Command) { cfg.POST.K1, "difficulty factor for finding a good label when generating a proof") cmd.PersistentFlags().UintVar(&cfg.POST.K2, "post-k2", cfg.POST.K2, "number of labels to prove") - cmd.PersistentFlags().UintVar(&cfg.POST.K3, "post-k3", - cfg.POST.K3, "subset of labels to verify in a proof") + cmd.PersistentFlags().UintVar( + &cfg.POST.K3, + "post-k3", + cfg.POST.K3, + "size of the subset of labels to verify in POST proofs\n"+ + "lower values will result in faster ATX verification but increase the risk\n"+ + "as the node must depend on malfeasance proofs to detect invalid ATXs", + ) cmd.PersistentFlags().AddFlag(&pflag.Flag{ Name: "post-pow-difficulty", Value: &cfg.POST.PowDifficulty, From 0c1edc863e2a0ab7dbcd7634d42d873643dc791e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 31 Jan 2024 13:55:55 +0100 Subject: [PATCH 22/30] Verify if corrupted post is invalid --- systest/tests/distributed_post_verification_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index d9312f89bc..b15150632b 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -216,6 +216,19 @@ func TestPostMalfeasanceProof(t *testing.T) { for i := range nipost.Post.Indices { nipost.Post.Indices[i] += 1 } + // Sanity check that the POST is invalid + verifier, err := activation.NewPostVerifier(cfg.POST, logger.Named("post-verifier")) + require.NoError(t, err) + err = verifier.Verify(ctx, (*shared.Proof)(nipost.Post), &shared.ProofMetadata{ + NodeId: signer.NodeID().Bytes(), + CommitmentAtxId: challenge.CommitmentATX.Bytes(), + NumUnits: nipost.NumUnits, + Challenge: nipost.PostMetadata.Challenge, + LabelsPerUnit: nipost.PostMetadata.LabelsPerUnit, + }) + var invalidIdxError *verifying.ErrInvalidIndex + require.ErrorAs(t, err, &invalidIdxError) + atx := types.NewActivationTx( *challenge, types.Address{1, 2, 3, 4}, From b1a61fdeee75289331399960be243895df39b5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 31 Jan 2024 14:21:23 +0100 Subject: [PATCH 23/30] Fixes in distributed post systest --- activation/post_supervisor.go | 4 ++-- systest/Dockerfile | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/activation/post_supervisor.go b/activation/post_supervisor.go index 8553b20e16..c59f04ec81 100644 --- a/activation/post_supervisor.go +++ b/activation/post_supervisor.go @@ -30,7 +30,7 @@ func DefaultPostServiceConfig() PostSupervisorConfig { return PostSupervisorConfig{ PostServiceCmd: filepath.Join(filepath.Dir(path), DefaultPostServiceName), - NodeAddress: "http://127.0.0.1:9093", + NodeAddress: "http://127.0.0.1:9094", MaxRetries: 10, } } @@ -44,7 +44,7 @@ func DefaultTestPostServiceConfig() PostSupervisorConfig { return PostSupervisorConfig{ PostServiceCmd: filepath.Join(filepath.Dir(string(path)), "build", DefaultPostServiceName), - NodeAddress: "http://127.0.0.1:9093", + NodeAddress: "http://127.0.0.1:9094", MaxRetries: 10, } } diff --git a/systest/Dockerfile b/systest/Dockerfile index 0c2306f2c3..9d7d1b61c4 100644 --- a/systest/Dockerfile +++ b/systest/Dockerfile @@ -31,4 +31,5 @@ RUN set -ex \ && rm -rf /var/lib/apt/lists/* COPY --from=build /src/build/tests.test /bin/tests COPY --from=build /src/build/libpost.so /bin/libpost.so +COPY --from=build /src/build/service /bin/service ENV LD_LIBRARY_PATH="/bin/" From afa1cd8a5b9c4c54ea5025d008c7e9ea07493a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 31 Jan 2024 15:38:41 +0100 Subject: [PATCH 24/30] Bump post to v0.11.0 and api to v1.27.0 --- Makefile-libs.Inc | 2 +- go.mod | 4 ++-- go.sum | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile-libs.Inc b/Makefile-libs.Inc index dd314b30e0..df078254fd 100644 --- a/Makefile-libs.Inc +++ b/Makefile-libs.Inc @@ -50,7 +50,7 @@ else endif endif -POSTRS_SETUP_REV = 0.7.0-rc6-distributed-verification +POSTRS_SETUP_REV = 0.7.0 POSTRS_SETUP_ZIP = libpost-$(platform)-v$(POSTRS_SETUP_REV).zip POSTRS_SETUP_URL_ZIP ?= https://github.com/spacemeshos/post-rs/releases/download/v$(POSTRS_SETUP_REV)/$(POSTRS_SETUP_ZIP) POSTRS_PROFILER_ZIP = profiler-$(platform)-v$(POSTRS_SETUP_REV).zip diff --git a/go.mod b/go.mod index e5ae12a426..dbc541112b 100644 --- a/go.mod +++ b/go.mod @@ -36,13 +36,13 @@ require ( github.com/quic-go/quic-go v0.41.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/seehuhn/mt19937 v1.0.0 - github.com/spacemeshos/api/release/go v1.26.1-0.20240126094723-69af34e8ef63 + github.com/spacemeshos/api/release/go v1.27.0 github.com/spacemeshos/economics v0.1.2 github.com/spacemeshos/fixed v0.1.1 github.com/spacemeshos/go-scale v1.1.12 github.com/spacemeshos/merkle-tree v0.2.3 github.com/spacemeshos/poet v0.10.2 - github.com/spacemeshos/post v0.10.6-0.20240125144730-2c926a601046 + github.com/spacemeshos/post v0.11.0 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index b3283bc4f8..ea6ec48fab 100644 --- a/go.sum +++ b/go.sum @@ -622,8 +622,8 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/spacemeshos/api/release/go v1.26.1-0.20240126094723-69af34e8ef63 h1:a8td/tqVAvpONDjKmP1R7nSo10X0nmxh9Cm0oJzCmig= -github.com/spacemeshos/api/release/go v1.26.1-0.20240126094723-69af34e8ef63/go.mod h1:lQ9UtxTzMAyC2q3DkE349L2FXeLkc9cPoEs4osQ20xM= +github.com/spacemeshos/api/release/go v1.27.0 h1:LPWgr70NC1aNd4MLqv2TD/bq4qqyH2O8RyCrsR+NLwI= +github.com/spacemeshos/api/release/go v1.27.0/go.mod h1:fK9RBD8eTVXHrqkkal2bwQB4N8M9sOhPs4rnVmWqEc0= github.com/spacemeshos/economics v0.1.2 h1:kw8cE5SMa/7svHOGorCd2w8ef1y8iP0p47/2VDOK8Ns= github.com/spacemeshos/economics v0.1.2/go.mod h1:ngeWn5E/jy9dJP1MHyuk3ehF8NBMTYhchqVDhAHUUNk= github.com/spacemeshos/fixed v0.1.1 h1:N1y4SUpq1EV+IdJrWJwUCt1oBFzeru/VKVcBsvPc2Fk= @@ -634,8 +634,8 @@ github.com/spacemeshos/merkle-tree v0.2.3 h1:zGEgOR9nxAzJr0EWjD39QFngwFEOxfxMloE github.com/spacemeshos/merkle-tree v0.2.3/go.mod h1:VomOcQ5pCBXz7goiWMP5hReyqOfDXGSKbrH2GB9Htww= github.com/spacemeshos/poet v0.10.2 h1:FVb0xgCFcjZyIGBQ92SlOZVx4KCmlCRRL4JSHL6LMGU= github.com/spacemeshos/poet v0.10.2/go.mod h1:73ROEXGladw3RbvhAk0sIGi/ttfpo+ASUBRvnBK55N8= -github.com/spacemeshos/post v0.10.6-0.20240125144730-2c926a601046 h1:ycGUfegcGYJOixpsS/KLiCtcKCwtl0s+0L96WQNWyc4= -github.com/spacemeshos/post v0.10.6-0.20240125144730-2c926a601046/go.mod h1:qKoQBbbvGptdf2CZxI1u7jnpJuXei1uEzgRQHbLiko4= +github.com/spacemeshos/post v0.11.0 h1:pXklHaBLgErq/HY9F7PNdRJaNb4Dk2xDFA9a5oA6Es0= +github.com/spacemeshos/post v0.11.0/go.mod h1:qKoQBbbvGptdf2CZxI1u7jnpJuXei1uEzgRQHbLiko4= github.com/spacemeshos/sha256-simd v0.1.0 h1:G7Mfu5RYdQiuE+wu4ZyJ7I0TI74uqLhFnKblEnSpjYI= github.com/spacemeshos/sha256-simd v0.1.0/go.mod h1:O8CClVIilId7RtuCMV2+YzMj6qjVn75JsxOxaE8vcfM= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= From 303620ed6baa65cc4ad360cb077d0d96fe191d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 31 Jan 2024 16:14:50 +0100 Subject: [PATCH 25/30] Don't pass --k3 to post-service --- systest/cluster/nodes.go | 1 - 1 file changed, 1 deletion(-) diff --git a/systest/cluster/nodes.go b/systest/cluster/nodes.go index e2f439a80c..65f68e3a71 100644 --- a/systest/cluster/nodes.go +++ b/systest/cluster/nodes.go @@ -850,7 +850,6 @@ func deployPostService( "--labels-per-unit", strconv.FormatUint(uint64(conf.POST.LabelsPerUnit), 10), "--k1", strconv.FormatUint(uint64(conf.POST.K1), 10), "--k2", strconv.FormatUint(uint64(conf.POST.K2), 10), - "--k3", strconv.FormatUint(uint64(conf.POST.K3), 10), "--pow-difficulty", conf.POST.PowDifficulty.String(), "-n", strconv.FormatUint(uint64(conf.SMESHING.Opts.Scrypt.N), 10), "-r", strconv.FormatUint(uint64(conf.SMESHING.Opts.Scrypt.R), 10), From b46f240aa5c99101fbf683a777d11721774a2b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 1 Feb 2024 11:15:18 +0100 Subject: [PATCH 26/30] Reuse code to load node config in distributed post systest --- systest/cluster/cluster.go | 15 ++++++++ systest/cluster/nodes.go | 36 +++++++++++-------- .../distributed_post_verification_test.go | 14 ++------ 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/systest/cluster/cluster.go b/systest/cluster/cluster.go index 37d2310b5e..9d6a80b6b8 100644 --- a/systest/cluster/cluster.go +++ b/systest/cluster/cluster.go @@ -20,6 +20,7 @@ import ( corev1 "k8s.io/client-go/applyconfigurations/core/v1" "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/config" "github.com/spacemeshos/go-spacemesh/genvm/sdk/wallet" "github.com/spacemeshos/go-spacemesh/hash" "github.com/spacemeshos/go-spacemesh/systest/parameters" @@ -907,3 +908,17 @@ func fillNetworkConfig(ctx *testcontext.Context, node *NodeClient) error { ctx.Log.Debugw("updated param layer duration", "duration", testcontext.LayerDuration.Get(ctx.Parameters)) return nil } + +func (c *Cluster) NodeConfig(ctx *testcontext.Context) (*config.Config, error) { + cfg, err := loadSmesherConfig(ctx) + if err != nil { + return nil, err + } + cfg.Genesis = &config.GenesisConfig{ + GenesisTime: c.Genesis().Format(time.RFC3339), + ExtraData: c.GenesisExtraData(), + } + cfg.LayersPerEpoch = uint32(testcontext.LayersPerEpoch.Get(ctx.Parameters)) + cfg.LayerDuration = testcontext.LayerDuration.Get(ctx.Parameters) + return cfg, nil +} diff --git a/systest/cluster/nodes.go b/systest/cluster/nodes.go index 65f68e3a71..3977724298 100644 --- a/systest/cluster/nodes.go +++ b/systest/cluster/nodes.go @@ -797,26 +797,19 @@ func deployNode(ctx *testcontext.Context, id string, labels map[string]string, f return nil } -func deployPostService( - ctx *testcontext.Context, - id string, - labels map[string]string, - nodeId string, - pubKey string, - goldenAtxId string, -) error { - ctx.Log.Debugw("deploying post service", "id", id) - +func loadSmesherConfig(ctx *testcontext.Context) (*config.Config, error) { + // TODO(poszu): this is mostly a copy of the code in cmd/node.go + // refactor the code below to reuse it after https://github.com/spacemeshos/go-spacemesh/pull/5485 lands. vip := viper.New() vip.SetConfigType("json") if err := vip.ReadConfig(strings.NewReader(smesherConfig.Get(ctx.Parameters))); err != nil { - return fmt.Errorf("load config: %w", err) + return nil, fmt.Errorf("reading config: %w", err) } conf := config.MainnetConfig() if name := vip.GetString("preset"); len(name) > 0 { preset, err := presets.Get(name) if err != nil { - return err + return nil, err } conf = preset } @@ -836,9 +829,24 @@ func deployPostService( node.WithIgnoreUntagged(), } if err := vip.Unmarshal(&conf, opts...); err != nil { - return fmt.Errorf("unmarshal config: %w", err) + return nil, fmt.Errorf("unmarshaling config: %w", err) } + return &conf, nil +} +func deployPostService( + ctx *testcontext.Context, + id string, + labels map[string]string, + nodeId string, + pubKey string, + goldenAtxId string, +) error { + ctx.Log.Debugw("deploying post service", "id", id) + conf, err := loadSmesherConfig(ctx) + if err != nil { + return fmt.Errorf("loading smesher config: %w", err) + } args := []string{ "--dir", "/data", "--address", fmt.Sprintf("http://%s:%d", nodeId, 9094), @@ -904,7 +912,7 @@ func deployPostService( ), ), ) - _, err := ctx.Client.AppsV1(). + _, err = ctx.Client.AppsV1(). Deployments(ctx.Namespace). Apply(ctx, deployment, apimetav1.ApplyOptions{FieldManager: "test"}) if err != nil { diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index b15150632b..44f59682fc 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -25,8 +25,6 @@ import ( "github.com/spacemeshos/go-spacemesh/api/grpcserver" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/config" - "github.com/spacemeshos/go-spacemesh/config/presets" "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/p2p" @@ -64,19 +62,13 @@ func TestPostMalfeasanceProof(t *testing.T) { require.NoError(t, cl.AddSmeshers(ctx, ctx.ClusterSize-cl.Total(), cluster.WithFlags(cluster.PostK3(1)))) // Prepare config - cfg, err := presets.Get("fastnet") + cfg, err := cl.NodeConfig(ctx) require.NoError(t, err) - cfg.Genesis = &config.GenesisConfig{ - GenesisTime: cl.Genesis().Format(time.RFC3339), - ExtraData: cl.GenesisExtraData(), - } - cfg.LayersPerEpoch = uint32(testcontext.LayersPerEpoch.Get(ctx.Parameters)) - types.SetLayersPerEpoch(cfg.LayersPerEpoch) - cfg.LayerDuration = testcontext.LayerDuration.Get(ctx.Parameters) + types.SetLayersPerEpoch(cfg.LayersPerEpoch) cfg.DataDirParent = testDir cfg.SMESHING.Opts.DataDir = filepath.Join(testDir, "post-data") - cfg.P2P.DataDir = filepath.Join(testDir, "post-data") + cfg.P2P.DataDir = filepath.Join(testDir, "p2p-dir") require.NoError(t, os.Mkdir(cfg.P2P.DataDir, os.ModePerm)) cfg.PoetServers = []types.PoetServer{ From de7eba460c5f62348397ada8837f4066a6ce5975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 1 Feb 2024 11:39:58 +0100 Subject: [PATCH 27/30] Review feedback --- systest/Dockerfile | 3 --- .../distributed_post_verification_test.go | 22 ++++++------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/systest/Dockerfile b/systest/Dockerfile index 9d7d1b61c4..03887a121f 100644 --- a/systest/Dockerfile +++ b/systest/Dockerfile @@ -16,9 +16,6 @@ COPY go.sum . RUN go mod download COPY . . -RUN make get-postrs-service -RUN make get-postrs-lib -RUN make go-env-test RUN --mount=type=cache,id=build,target=/root/.cache/go-build go test -failfast -v -c -o ./build/tests.test ./systest/tests/ diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 44f59682fc..7fecb8db87 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -36,7 +36,6 @@ import ( "github.com/spacemeshos/go-spacemesh/systest/cluster" "github.com/spacemeshos/go-spacemesh/systest/testcontext" "github.com/spacemeshos/go-spacemesh/timesync" - "github.com/spacemeshos/go-spacemesh/timesync/peersync" ) var grpclog grpc_logsettable.SettableLoggerV2 @@ -86,7 +85,7 @@ func TestPostMalfeasanceProof(t *testing.T) { cfg.P2P.PrivateNetwork = true cfg.Bootstrap.URL = cluster.BootstrapperGlobalEndpoint(ctx.Namespace, 0) cfg.P2P.MinPeers = 2 - ctx.Log.Infow("Prepared config", "cfg", cfg) + ctx.Log.Debugw("Prepared config", "cfg", cfg) goldenATXID := cl.GoldenATX() signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) @@ -103,14 +102,6 @@ func TestPostMalfeasanceProof(t *testing.T) { require.NoError(t, err) logger.Info("p2p host created", zap.Stringer("id", host.ID())) host.Register(pubsub.AtxProtocol, func(context.Context, peer.ID, []byte) error { return nil }) - ptimesync := peersync.New( - host, - host, - peersync.WithLog(log.NewFromLog(logger.Named("peersync"))), - peersync.WithConfig(cfg.TIME.Peersync), - ) - ptimesync.Start() - t.Cleanup(ptimesync.Stop) require.NoError(t, host.Start()) t.Cleanup(func() { assert.NoError(t, host.Stop()) }) @@ -209,7 +200,9 @@ func TestPostMalfeasanceProof(t *testing.T) { nipost.Post.Indices[i] += 1 } // Sanity check that the POST is invalid - verifier, err := activation.NewPostVerifier(cfg.POST, logger.Named("post-verifier")) + 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(), @@ -243,8 +236,9 @@ func TestPostMalfeasanceProof(t *testing.T) { // 4. Publish ATX publishCtx, stopPublishing := context.WithCancel(ctx.Context) - t.Cleanup(stopPublishing) + defer stopPublishing() var eg errgroup.Group + t.Cleanup(func() { assert.NoError(t, eg.Wait()) }) eg.Go(func() error { for { logger.Sugar().Infow("publishing ATX", "atx", atx) @@ -286,8 +280,6 @@ func TestPostMalfeasanceProof(t *testing.T) { require.Equal(t, atx.NIPostChallenge, invalidAtx.NIPostChallenge) require.Equal(t, atx.NIPost.Post.Indices, invalidAtx.NIPost.Post.Indices) - postVerifier, err := activation.NewPostVerifier(cfg.POST, logger.Named("post-verifier")) - require.NoError(t, err) meta := &shared.ProofMetadata{ NodeId: invalidAtx.NodeID.Bytes(), CommitmentAtxId: invalidAtx.CommitmentATX.Bytes(), @@ -295,7 +287,7 @@ func TestPostMalfeasanceProof(t *testing.T) { Challenge: invalidAtx.NIPost.PostMetadata.Challenge, LabelsPerUnit: invalidAtx.NIPost.PostMetadata.LabelsPerUnit, } - err = postVerifier.Verify(ctx, (*shared.Proof)(invalidAtx.NIPost.Post), meta) + err = verifier.Verify(ctx, (*shared.Proof)(invalidAtx.NIPost.Post), meta) var invalidIdxError *verifying.ErrInvalidIndex require.ErrorAs(t, err, &invalidIdxError) return false, nil From 9f55e78afc6320d8ae98ddb7bb705a943b4df525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 1 Feb 2024 14:03:04 +0100 Subject: [PATCH 28/30] Update CHANGELOG.md --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6a5a8ae0c..259d1d85ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,14 @@ configuration is as follows: The node will automatically migrate the data from disk and store it in the database. The migration will take place at the first startup after the upgrade. +* [#5390](https://github.com/spacemeshos/go-spacemesh/pull/5390) + Distributed PoST verification. + + The nodes on the network can now choose to verify + only a subset of labels in PoST proofs by choosing a K3 value lower than K2. + If a node finds a proof invalid, it will report it to the network by + creating a malfeasance proof. The malicious node will then be blacklisted by the network. + ### Features * [#5517](https://github.com/spacemeshos/go-spacemesh/pull/5517) @@ -334,7 +342,7 @@ for more information on how to configure the node to work with the PoST service. ### Improvements -* further increased cache sizes and and p2p timeouts to compensate for the increased number of nodes on the network. +* further increased cache sizes and p2p timeouts to compensate for the increased number of nodes on the network. * [#5329](https://github.com/spacemeshos/go-spacemesh/pull/5329) P2P decentralization improvements. Added support for QUIC transport and DHT routing discovery for finding peers and relays. Also, added the `ping-peers` feature which is useful From dae203996f565c9c0ec4742e7291272854a35123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 1 Feb 2024 14:03:46 +0100 Subject: [PATCH 29/30] Set the duration after which POST in ATXs is considered valid on mainnet to max --- config/mainnet.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config/mainnet.go b/config/mainnet.go index 88a536b495..7a148f2e88 100644 --- a/config/mainnet.go +++ b/config/mainnet.go @@ -111,6 +111,7 @@ func MainnetConfig() Config { }, RegossipAtxInterval: 2 * time.Hour, ATXGradeDelay: 30 * time.Minute, + PostValidDelay: time.Duration(math.MaxInt64), }, Genesis: &GenesisConfig{ GenesisTime: "2023-07-14T08:00:00Z", From 73fec9e737b666fa8dd0395ee2e9671b4ff10743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 2 Feb 2024 13:39:53 +0100 Subject: [PATCH 30/30] Improve poet request timeouts in post malfeasance systest" --- systest/tests/distributed_post_verification_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 7fecb8db87..cb3792e8e9 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -70,6 +70,8 @@ func TestPostMalfeasanceProof(t *testing.T) { cfg.P2P.DataDir = filepath.Join(testDir, "p2p-dir") require.NoError(t, os.Mkdir(cfg.P2P.DataDir, os.ModePerm)) + cfg.POET.RequestTimeout = time.Minute + cfg.POET.MaxRequestRetries = 10 cfg.PoetServers = []types.PoetServer{ {Address: cluster.MakePoetGlobalEndpoint(ctx.Namespace, 0)}, }