From f9baccf092bfd1e1a7bba2284a7bab47c8aa0b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 2 Feb 2024 14:27:00 +0000 Subject: [PATCH] Distributed POST verification (#5390) Part of #5185, closes #5376 - [x] Local randomness seed to select K3 indices using p2p peer ID - [x] Verifying K3 indices of POST - [x] Verifying ALL indices for initial POST - [x] Publishing POST malfeasance proofs - [x] Verifying only the invalid index in POST malfeasance proofs - [x] Verify the candidate for a positioning ATX and its chain - [x] Verify the candidate for a commitment ATX and its chain - [x] Wait "some time" for malfeasance proofs before creating an active set :point_up: this is covered by ATX grading - [x] configurable duration after which ATXs (and their chains) are considered valid (in terms of POST labels) - [x] system tests --- CHANGELOG.md | 10 +- Makefile-libs.Inc | 2 +- activation/activation.go | 75 ++++- activation/activation_test.go | 130 +++++--- activation/e2e/nipost_test.go | 10 +- activation/e2e/validation_test.go | 11 +- activation/handler.go | 32 +- activation/handler_test.go | 199 ++++++++++-- activation/interface.go | 15 +- activation/mocks.go | 115 ++++++- activation/post.go | 53 +++- activation/post_supervisor.go | 5 +- activation/post_test.go | 13 +- activation/validation.go | 204 +++++++++++- activation/validation_test.go | 155 ++++++++- api/grpcserver/post_service_test.go | 15 +- api/grpcserver/smesher_service.go | 4 +- api/grpcserver/smesher_service_test.go | 6 +- checkpoint/recovery_test.go | 5 +- cmd/root.go | 14 +- common/types/activation.go | 17 + common/types/malfeasance.go | 45 ++- common/types/malfeasance_scale.go | 37 +++ config/config.go | 7 + config/mainnet.go | 1 + datastore/mocks/mocks.go | 117 +++++++ datastore/store.go | 22 +- events/events.go | 2 + fetch/fetch.go | 2 +- fetch/handler_test.go | 2 +- go.mod | 4 +- go.sum | 8 +- 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 | 6 +- sql/atxs/atxs.go | 87 +++-- sql/atxs/atxs_test.go | 106 ++++--- sql/migrations/state/0011_atx_validity.sql | 3 + syncer/syncer.go | 2 +- systest/Dockerfile | 2 + systest/cluster/cluster.go | 29 +- systest/cluster/nodes.go | 41 ++- systest/tests/common.go | 29 ++ .../distributed_post_verification_test.go | 298 ++++++++++++++++++ 48 files changed, 1990 insertions(+), 282 deletions(-) create mode 100644 datastore/mocks/mocks.go create mode 100644 sql/migrations/state/0011_atx_validity.sql create mode 100644 systest/tests/distributed_post_verification_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c97cd1c8c..d8e69b61aa 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 ### Improvements @@ -346,7 +354,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 diff --git a/Makefile-libs.Inc b/Makefile-libs.Inc index 14e45a26f2..09658cc50c 100644 --- a/Makefile-libs.Inc +++ b/Makefile-libs.Inc @@ -50,7 +50,7 @@ else endif endif -POSTRS_SETUP_REV = 0.6.6 +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) diff --git a/activation/activation.go b/activation/activation.go index ff07abc486..3c2315b3b0 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -58,7 +58,6 @@ const ( // Config defines configuration for Builder. type Config struct { GoldenATXID types.ATXID - LayersPerEpoch uint32 RegossipInterval time.Duration } @@ -90,11 +89,18 @@ type Builder struct { stop context.CancelFunc poetCfg PoetConfig poetRetryInterval time.Duration + // delay before PoST in ATX is considered valid (counting from the time it was received) + postValidityDelay time.Duration } -// BuilderOption ... type BuilderOption func(*Builder) +func WithPostValidityDelay(delay time.Duration) BuilderOption { + return func(b *Builder) { + b.postValidityDelay = 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 { @@ -149,6 +155,7 @@ func NewBuilder( syncer: syncer, log: log, poetRetryInterval: defaultPoetRetryInterval, + postValidityDelay: 12 * time.Hour, } for _, opt := range opts { opt(b) @@ -403,7 +410,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) } @@ -584,17 +591,24 @@ 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()) - 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) +// getPositioningAtx returns atx id with the highest tick height. +func (b *Builder) getPositioningAtx(ctx context.Context) (types.ATXID, error) { + id, err := findFullyValidHighTickAtx( + ctx, + b.cdb, + b.signer.NodeID(), + b.goldenATXID, + b.validator, + b.log, + 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 { @@ -630,3 +644,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 := 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 { + return id, nil + } + } +} diff --git a/activation/activation_test.go b/activation/activation_test.go index cad717d16f..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" @@ -71,6 +72,7 @@ func newAtx( atx := types.NewActivationTx(challenge, coinbase, nipost, numUnits, nil) atx.SetEffectiveNumUnits(numUnits) atx.SetReceived(time.Now()) + atx.SetValidity(types.Valid) return atx } @@ -138,8 +140,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() @@ -243,20 +244,13 @@ 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 } -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) @@ -405,10 +399,10 @@ 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.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)) @@ -439,10 +433,10 @@ 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.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)) @@ -467,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 { @@ -485,10 +481,10 @@ 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.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)) @@ -541,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 { @@ -571,7 +567,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)) @@ -649,7 +645,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)) @@ -680,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 @@ -714,7 +711,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)) @@ -740,7 +737,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)) @@ -772,7 +769,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)) @@ -805,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 @@ -851,11 +849,11 @@ 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.EmptyATXID, tab.goldenATXID, postAtxPubEpoch, &tab.goldenATXID) 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)) @@ -864,7 +862,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)) @@ -905,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 { @@ -951,11 +951,11 @@ 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.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)) @@ -996,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 { @@ -1043,7 +1045,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)) @@ -1057,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 @@ -1151,7 +1154,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)) @@ -1199,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() @@ -1266,18 +1270,14 @@ 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.EmptyATXID, tab.goldenATXID, posEpoch, &tab.goldenATXID) 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)) @@ -1377,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) @@ -1388,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())) }) @@ -1427,3 +1427,53 @@ 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(""), []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) + 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(""), []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 d3cdcbe224..df3a344d60 100644 --- a/activation/e2e/nipost_test.go +++ b/activation/e2e/nipost_test.go @@ -118,6 +118,7 @@ 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) syncer := activation.NewMocksyncer(gomock.NewController(t)) syncer.EXPECT().RegisterForATXSynced().AnyTimes().DoAndReturn(func() <-chan struct{} { @@ -126,7 +127,7 @@ func TestNIPostBuilderWithClients(t *testing.T) { return synced }) - mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX, syncer) + mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX, syncer, validator) require.NoError(t, err) opts := activation.DefaultPostSetupOpts() @@ -197,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(), @@ -272,7 +273,8 @@ func TestNewNIPostBuilderNotInitialized(t *testing.T) { return synced }) - mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX, syncer) + validator := activation.NewMocknipostValidator(gomock.NewController(t)) + mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX, syncer, validator) require.NoError(t, err) // ensure that genesis aligns with layer timings @@ -342,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 75fa952e8d..08b2c0467f 100644 --- a/activation/e2e/validation_test.go +++ b/activation/e2e/validation_test.go @@ -34,6 +34,7 @@ func TestValidator_Validate(t *testing.T) { cfg := activation.DefaultPostConfig() cdb := datastore.NewCachedDB(sql.InMemory(), log.NewFromLog(logger)) + validator := activation.NewMocknipostValidator(gomock.NewController(t)) syncer := activation.NewMocksyncer(gomock.NewController(t)) syncer.EXPECT().RegisterForATXSynced().AnyTimes().DoAndReturn(func() <-chan struct{} { synced := make(chan struct{}) @@ -41,7 +42,7 @@ func TestValidator_Validate(t *testing.T) { return synced }) - mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX, syncer) + mgr, err := activation.NewPostSetupManager(sig.NodeID(), cfg, logger, cdb, goldenATX, syncer, validator) require.NoError(t, err) opts := activation.DefaultPostSetupOpts() @@ -114,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) @@ -135,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, @@ -145,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, @@ -155,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/handler.go b/activation/handler.go index 54edf52819..1c3e78fab4 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" "go.uber.org/zap" "golang.org/x/exp/maps" @@ -246,11 +247,40 @@ 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) { + 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, errMaliciousATX + } 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 a9d73d09dd..c673976f75 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" @@ -358,8 +359,9 @@ 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().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) @@ -383,8 +385,9 @@ 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().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(). @@ -415,8 +418,9 @@ 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().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) @@ -446,8 +450,9 @@ 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().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(). @@ -509,7 +514,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()). @@ -517,8 +522,9 @@ 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().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) @@ -597,7 +603,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(). @@ -705,7 +712,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") @@ -990,6 +997,7 @@ func TestHandler_ProcessAtx(t *testing.T) { atxHdlr.log, atxHdlr.cdb, atxHdlr.edVerifier, + NewMockPostVerifier(gomock.NewController(t)), &got, ) require.NoError(t, err) @@ -1065,6 +1073,78 @@ func TestHandler_ProcessAtx_OwnNotMalicious(t *testing.T) { require.Nil(t, proof) } +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.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()) + 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, 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( + 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) + 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 + }) + + 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} @@ -1256,14 +1336,14 @@ 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()) 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)) @@ -1271,8 +1351,10 @@ 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.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)) @@ -1315,24 +1397,29 @@ 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, - ).DoAndReturn( - func(_ context.Context, _ types.NodeID, _ types.ATXID, _ *types.Post, _ *types.PostMetadata, _ uint32) 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.mValidator.EXPECT().IsVerifyingFullPost() atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any()) @@ -1706,10 +1793,12 @@ 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(). - NIPost(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(), 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()) @@ -1752,8 +1841,9 @@ func TestHandler_AtxWeight(t *testing.T) { atxHdlr.mockFetch.EXPECT().GetPoetProof(gomock.Any(), proofRef) atxHdlr.mockFetch.EXPECT().GetAtxs(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().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()) @@ -1806,13 +1896,62 @@ 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(). - NIPost(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(), 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 7d5ae20cc5..e546044221 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -28,26 +28,34 @@ 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 NIPost( ctx context.Context, nodeId types.NodeID, - atxId types.ATXID, + commitmentAtxId types.ATXID, NIPost *types.NIPost, expectedChallenge types.Hash32, numUnits uint32, + opts ...validatorOption, ) (uint64, error) NumUnits(cfg *PostConfig, numUnits uint32) error + + IsVerifyingFullPost() bool + Post( ctx context.Context, nodeId types.NodeID, - atxId types.ATXID, + commitmentAtxId types.ATXID, Post *types.Post, PostMetadata *types.PostMetadata, numUnits uint32, + opts ...validatorOption, ) error PostMetadata(cfg *PostConfig, metadata *types.PostMetadata) error @@ -59,6 +67,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 167c1d7b23..d8cabb115a 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -303,19 +303,62 @@ 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, atxId types.ATXID, NIPost *types.NIPost, expectedChallenge types.Hash32, numUnits uint32) (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() - ret := m.ctrl.Call(m, "NIPost", ctx, nodeId, atxId, NIPost, expectedChallenge, numUnits) + varargs := []any{ctx, nodeId, commitmentAtxId, 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, commitmentAtxId, 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, commitmentAtxId, NIPost, expectedChallenge, numUnits}, opts...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NIPost", reflect.TypeOf((*MocknipostValidator)(nil).NIPost), varargs...) return &nipostValidatorNIPostCall{Call: call} } @@ -331,13 +374,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 } @@ -457,17 +500,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, commitmentAtxId 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, commitmentAtxId, 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, commitmentAtxId, 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, commitmentAtxId, Post, PostMetadata, numUnits}, opts...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MocknipostValidator)(nil).Post), varargs...) return &nipostValidatorPostCall{Call: call} } @@ -483,13 +531,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 } @@ -570,6 +618,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 8351eb6114..38ee4e0fde 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" @@ -24,12 +25,15 @@ 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 uint32 `mapstructure:"post-k1"` - K2 uint32 `mapstructure:"post-k2"` - K3 uint32 `mapstructure:"post-k3"` + 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"` } @@ -40,7 +44,6 @@ func (c PostConfig) ToConfig() config.Config { LabelsPerUnit: c.LabelsPerUnit, K1: c.K1, K2: c.K2, - K3: c.K3, PowDifficulty: [32]byte(c.PowDifficulty), } } @@ -134,7 +137,7 @@ func DefaultPostConfig() PostConfig { LabelsPerUnit: cfg.LabelsPerUnit, K1: cfg.K1, K2: cfg.K2, - K3: cfg.K3, + K3: cfg.K2, // The default is to verify all K2 indices. PowDifficulty: PowDifficulty(cfg.PowDifficulty), } } @@ -180,11 +183,25 @@ 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. 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. @@ -195,6 +212,8 @@ func NewPostSetupManager( db *datastore.CachedDB, goldenATXID types.ATXID, syncer syncer, + validator nipostValidator, + opts ...PostSetupManagerOpt, ) (*PostSetupManager, error) { mgr := &PostSetupManager{ id: id, @@ -204,8 +223,13 @@ func NewPostSetupManager( goldenATXID: goldenATXID, state: PostSetupStateNotStarted, syncer: syncer, - } + validator: validator, + postValidityDelay: 12 * time.Hour, + } + for _, opt := range opts { + opt(mgr) + } return mgr, nil } @@ -376,7 +400,16 @@ func (mgr *PostSetupManager) findCommitmentAtx(ctx context.Context) (types.ATXID mgr.logger.Info("ATXs synced - selecting commitment ATX") } - atx, err := atxs.GetIDWithMaxHeight(mgr.db, types.EmptyNodeID) + atx, err := findFullyValidHighTickAtx( + context.Background(), + mgr.db, + types.EmptyNodeID, + mgr.goldenATXID, + mgr.validator, + mgr.logger, + VerifyChainOpts.AssumeValidBefore(time.Now().Add(-mgr.postValidityDelay)), + VerifyChainOpts.WithLogger(mgr.logger), + ) switch { case errors.Is(err, sql.ErrNotFound): mgr.logger.Info("using golden atx as commitment atx") diff --git a/activation/post_supervisor.go b/activation/post_supervisor.go index ccf1f8b0b1..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, } } @@ -227,7 +227,6 @@ func (ps *PostSupervisor) runCmd( "--labels-per-unit", strconv.FormatUint(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_test.go b/activation/post_test.go index 6577a71118..d13a61e492 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(context.Background()) require.NoError(t, err) require.Equal(t, latestAtx.ID(), atx) @@ -342,13 +346,18 @@ 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() + validator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() syncer := NewMocksyncer(gomock.NewController(tb)) synced := make(chan struct{}) close(synced) syncer.EXPECT().RegisterForATXSynced().AnyTimes().Return(synced) cdb := datastore.NewCachedDB(sql.InMemory(), logtest.New(tb)) - mgr, err := NewPostSetupManager(id, DefaultPostConfig(), zaptest.NewLogger(tb), cdb, goldenATXID, syncer) + mgr, err := NewPostSetupManager(id, DefaultPostConfig(), zaptest.NewLogger(tb), cdb, goldenATXID, syncer, validator) require.NoError(tb, err) return &testPostManager{ diff --git a/activation/validation.go b/activation/validation.go index 3ce6965faf..38b6b8d282 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 { @@ -36,8 +39,21 @@ func (e *ErrAtxNotFound) Is(target error) bool { return false } +type validatorOptions struct { + postSubsetSeed []byte +} + +// 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.postSubsetSeed = seed + } +} + // Validator contains the dependencies required to validate NIPosts. type Validator struct { + db sql.Executor poetDb poetDbAPI cfg PostConfig scrypt config.ScryptParams @@ -45,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. @@ -61,6 +83,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 +93,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) } @@ -118,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( @@ -127,6 +154,7 @@ func (v *Validator) Post( PoST *types.Post, PostMetadata *types.PostMetadata, numUnits uint32, + opts ...validatorOption, ) error { p := (*shared.Proof)(PoST) @@ -138,8 +166,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.postSubsetSeed != nil { + verifyOpts = append(verifyOpts, verifying.Subset(v.cfg.K3, options.postSubsetSeed)) + } + 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()) @@ -264,3 +301,162 @@ func (v *Validator) PositioningAtx( } return nil } + +type verifyChainOpts struct { + assumedValidTime time.Time + trustedNodeID types.NodeID + logger *zap.Logger +} + +type verifyChainOptsNs struct{} + +var VerifyChainOpts verifyChainOptsNs + +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 (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 + } +} + +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{ + logger: zap.NewNop(), + } + for _, opt := range opts { + opt(&options) + } + options.logger.Info("verifying ATX chain", zap.Stringer("atx_id", id)) + return v.verifyChainWithOpts(ctx, id, goldenATXID, options) +} + +func (v *Validator) verifyChainWithOpts( + ctx context.Context, + id, goldenATXID types.ATXID, + opts verifyChainOpts, +) error { + log := opts.logger + atx, err := atxs.Get(v.db, id) + if err != nil { + return fmt.Errorf("get atx: %w", err) + } + + 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 &InvalidChainError{ID: id} + 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 + } + + // validate POST fully + commitmentAtxId := atx.CommitmentATX + if commitmentAtxId == 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 := v.Post( + ctx, + atx.SmesherID, + *commitmentAtxId, + atx.NIPost.Post, + atx.NIPost.PostMetadata, + atx.NumUnits, + ); err != nil { + if err := atxs.SetValidity(v.db, id, types.Invalid); err != nil { + log.Warn("failed to persist atx validity", zap.Error(err), zap.Stringer("atx_id", id)) + } + return &InvalidChainError{ID: id, src: err} + } + + 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.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)) + } + } + return err +} + +func (v *Validator) verifyChainDeps( + ctx context.Context, + atx *types.ActivationTx, + goldenATXID types.ATXID, + opts verifyChainOpts, +) error { + if atx.PrevATXID != types.EmptyATXID { + 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 := 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 := v.verifyChainWithOpts(ctx, *atx.CommitmentATX, goldenATXID, 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..3674685b38 100644 --- a/activation/validation_test.go +++ b/activation/validation_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "testing" + "time" "github.com/spacemeshos/post/config" "github.com/spacemeshos/post/initialization" @@ -13,6 +14,9 @@ import ( "go.uber.org/mock/gomock" "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) { @@ -46,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) { @@ -87,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) { @@ -155,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) { @@ -281,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{} @@ -305,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) { @@ -409,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) { @@ -451,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) { @@ -517,3 +521,140 @@ func TestValidateMerkleProof(t *testing.T) { require.Error(t, err) }) } + +func TestVerifyChainDeps(t *testing.T) { + ctrl := gomock.NewController(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.NIPost, 2, types.Address{}) + require.NoError(t, 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.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)) + + 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, &InvalidChainError{ID: invalidAtx.ID()}) + }) + + 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.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)) + + 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, &InvalidChainError{ID: invalidAtx.ID()}) + }) + + 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.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)) + + 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, &InvalidChainError{ID: invalidAtx.ID()}) + }) + + 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.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)) + + 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) + }) + + 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.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)) + + 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) + }) + + 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) + }) +} + +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/api/grpcserver/post_service_test.go b/api/grpcserver/post_service_test.go index cb0251c753..dae89fda01 100644 --- a/api/grpcserver/post_service_test.go +++ b/api/grpcserver/post_service_test.go @@ -43,15 +43,19 @@ 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() + syncer := activation.NewMocksyncer(gomock.NewController(tb)) syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { ch := make(chan struct{}) close(ch) return ch }) - cdb := datastore.NewCachedDB(sql.InMemory(), logtest.New(tb)) - mgr, err := activation.NewPostSetupManager(id, postCfg, log.Named("post manager"), cdb, goldenATXID, syncer) + mgr, err := activation.NewPostSetupManager(id, postCfg, log.Named("post manager"), cdb, goldenATXID, syncer, validator) require.NoError(tb, err) // start post supervisor @@ -83,15 +87,18 @@ 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() syncer := activation.NewMocksyncer(gomock.NewController(tb)) syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { ch := make(chan struct{}) close(ch) return ch }) - cdb := datastore.NewCachedDB(sql.InMemory(), logtest.New(tb)) - mgr, err := activation.NewPostSetupManager(id, postCfg, log.Named("post manager"), cdb, goldenATXID, syncer) + mgr, err := activation.NewPostSetupManager(id, postCfg, log.Named("post manager"), cdb, goldenATXID, syncer, validator) require.NoError(tb, err) ps, err := activation.NewPostSupervisor(log, serviceCfg, postCfg, provingOpts, mgr) 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 d6a066f423..f2b73dd201 100644 --- a/checkpoint/recovery_test.go +++ b/checkpoint/recovery_test.go @@ -265,7 +265,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 { @@ -273,8 +273,9 @@ 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) + 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)) diff --git a/cmd/root.go b/cmd/root.go index 0fd553868a..89bc05feb8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -228,12 +228,18 @@ func AddFlags(flagSet *pflag.FlagSet, cfg *config.Config) (configPath *string) { cfg.POST.MinNumUnits, "") flagSet.Uint32Var(&cfg.POST.MaxNumUnits, "post-max-numunits", cfg.POST.MaxNumUnits, "") - flagSet.Uint32Var(&cfg.POST.K1, "post-k1", + flagSet.UintVar(&cfg.POST.K1, "post-k1", cfg.POST.K1, "difficulty factor for finding a good label when generating a proof") - flagSet.Uint32Var(&cfg.POST.K2, "post-k2", + flagSet.UintVar(&cfg.POST.K2, "post-k2", cfg.POST.K2, "number of labels to prove") - flagSet.Uint32Var(&cfg.POST.K3, "post-k3", - cfg.POST.K3, "subset of labels to verify in a proof") + flagSet.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", + ) flagSet.AddFlag(&pflag.Flag{ Name: "post-pow-difficulty", Value: &cfg.POST.PowDifficulty, diff --git a/common/types/activation.go b/common/types/activation.go index cf34fa0a72..0dce53dec3 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/common/types/malfeasance.go b/common/types/malfeasance.go index c31b8abe3c..4e6cd6b744 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("unknown malfeasance proof type") } 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/config/config.go b/config/config.go index f3ef07ffd0..61ef6a4f0c 100644 --- a/config/config.go +++ b/config/config.go @@ -131,6 +131,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"` } @@ -218,6 +224,7 @@ func defaultBaseConfig() BaseConfig { DatabasePruneInterval: 30 * time.Minute, NetworkHRP: "sm", ATXGradeDelay: 10 * time.Second, + PostValidDelay: 12 * time.Hour, } } diff --git a/config/mainnet.go b/config/mainnet.go index f5a36c44c6..672661993b 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", 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 8083b0efa9..2250e678bd 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 @@ -76,7 +84,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) @@ -99,7 +107,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, @@ -153,7 +161,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 } @@ -329,7 +337,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. @@ -348,13 +356,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/events/events.go b/events/events.go index fcf02e6d51..cf99f720c0 100644 --- a/events/events.go +++ b/events/events.go @@ -275,6 +275,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 = pb.MalfeasanceProof_MALFEASANCE_POST_INDEX } result := &pb.MalfeasanceProof{ SmesherId: &pb.SmesherId{Id: smesher.Bytes()}, diff --git a/fetch/fetch.go b/fetch/fetch.go index cf06cb8a86..47eebf9113 100644 --- a/fetch/fetch.go +++ b/fetch/fetch.go @@ -228,7 +228,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/go.mod b/go.mod index a8f0f27abd..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.0 + 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.5 + 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 e895422f2c..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.0 h1:wop4OoID/2o8BEwNP/wypCIy//CDTnv14perIZo0WOU= -github.com/spacemeshos/api/release/go v1.26.0/go.mod h1:cQXfRiIRPc8c6bh9+VAK/GwD0zYCu7jKcos/cPaDYcI= +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.5 h1:9g76QQRmj5ySH3llgHkFKk7L9OMalANDftyvcnYR+0Y= -github.com/spacemeshos/post v0.10.5/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= diff --git a/malfeasance/handler.go b/malfeasance/handler.go index 2f3cc0d103..08ac99820f 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -6,6 +6,9 @@ import ( "fmt" "time" + "github.com/spacemeshos/post/shared" + "github.com/spacemeshos/post/verifying" + "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" @@ -27,12 +30,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 +46,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 +101,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 +122,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 +167,7 @@ func Validate( logger log.Log, cdb *datastore.CachedDB, edVerifier SigVerifier, + postVerifier postVerifier, p *types.MalfeasanceGossip, ) (types.NodeID, error) { var ( @@ -174,6 +181,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 +208,8 @@ func updateMetrics(tp types.Proof) { numProofsATX.Inc() case types.MultipleBallots: numProofsBallot.Inc() + case types.InvalidPostIndex: + numProofsPostIndex.Inc() } } @@ -363,3 +375,42 @@ 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, + 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 21ad04030d..e6b786fdd9 100644 --- a/mesh/mesh_test.go +++ b/mesh/mesh_test.go @@ -404,6 +404,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 bdd5ee947d..05bf0a1f1b 100644 --- a/node/node.go +++ b/node/node.go @@ -600,6 +600,7 @@ func (app *App) initServices(ctx context.Context) error { app.postVerifier = verifier validator := activation.NewValidator( + app.db, poetDb, app.Config.POST, app.Config.SMESHING.Opts.Scrypt, @@ -921,6 +922,8 @@ func (app *App) initServices(ctx context.Context) error { app.cachedDB, goldenATXID, newSyncer, + app.validator, + activation.PostValidityDelay(app.Config.PostValidDelay), ) if err != nil { return fmt.Errorf("create post setup manager: %v", err) @@ -957,7 +960,6 @@ func (app *App) initServices(ctx context.Context) error { builderConfig := activation.Config{ GoldenATXID: goldenATXID, - LayersPerEpoch: layersPerEpoch, RegossipInterval: app.Config.RegossipAtxInterval, } atxBuilder := activation.NewBuilder( @@ -975,6 +977,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), + activation.WithPostValidityDelay(app.Config.PostValidDelay), ) malfeasanceHandler := malfeasance.NewHandler( @@ -984,6 +987,7 @@ func (app *App) initServices(ctx context.Context) error { app.edSgn.NodeID(), app.edVerifier, trtl, + app.postVerifier, ) fetcher.SetValidators( fetch.ValidatorFunc( diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 2e4d548989..f19bfc1ead 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) @@ -127,7 +128,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 +151,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 +174,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 { @@ -300,55 +301,76 @@ 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) } 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 + rst types.ATXID + highest uint64 ) 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)) + + switch { + case height < highest: + // Results are ordered by height, so we can stop once we see a lower height. + return false + 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 == highest && 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 { - return types.ATXID{}, fmt.Errorf("select positioning atx: %w", err) - } else if rows == 0 { - return types.ATXID{}, sql.ErrNotFound + _, err := db.Exec(` + 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) + 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 } @@ -386,7 +408,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 @@ -469,3 +491,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/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 { diff --git a/sql/migrations/state/0011_atx_validity.sql b/sql/migrations/state/0011_atx_validity.sql new file mode 100644 index 0000000000..9396d18424 --- /dev/null +++ b/sql/migrations/state/0011_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; diff --git a/syncer/syncer.go b/syncer/syncer.go index f9e19d2b91..7057a1f270 100644 --- a/syncer/syncer.go +++ b/syncer/syncer.go @@ -171,7 +171,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) diff --git a/systest/Dockerfile b/systest/Dockerfile index 83b8816b8e..03887a121f 100644 --- a/systest/Dockerfile +++ b/systest/Dockerfile @@ -16,6 +16,7 @@ COPY go.sum . RUN go mod download COPY . . + RUN --mount=type=cache,id=build,target=/root/.cache/go-build go test -failfast -v -c -o ./build/tests.test ./systest/tests/ FROM ubuntu:22.04 @@ -27,4 +28,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/" diff --git a/systest/cluster/cluster.go b/systest/cluster/cluster.go index e23f558936..3f1fc52877 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" @@ -49,6 +50,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) @@ -62,6 +67,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) @@ -531,7 +540,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) } @@ -554,7 +563,7 @@ func (c *Cluster) AddRemoteSmeshers(tctx *testcontext.Context, n int, opts ...De 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) } @@ -809,7 +818,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) @@ -899,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 345dd4f6e4..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), @@ -850,7 +858,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), @@ -905,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 { @@ -1047,6 +1054,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/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..cb3792e8e9 --- /dev/null +++ b/systest/tests/distributed_post_verification_test.go @@ -0,0 +1,298 @@ +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/datastore" + "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" +) + +var grpclog grpc_logsettable.SettableLoggerV2 + +func init() { + grpclog = grpc_logsettable.ReplaceGrpcLoggerV2() +} + +func TestPostMalfeasanceProof(t *testing.T) { + t.Parallel() + testDir := t.TempDir() + + ctx := testcontext.New(t, testcontext.Labels("sanity")) + 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(), cluster.WithFlags(cluster.PostK3(1)))) + + // Prepare config + cfg, err := cl.NodeConfig(ctx) + require.NoError(t, err) + + types.SetLayersPerEpoch(cfg.LayersPerEpoch) + cfg.DataDirParent = testDir + cfg.SMESHING.Opts.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.POET.RequestTimeout = time.Minute + cfg.POET.MaxRequestRetries = 10 + cfg.PoetServers = []types.PoetServer{ + {Address: cluster.MakePoetGlobalEndpoint(ctx.Namespace, 0)}, + } + + 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 + cfg.P2P.PrivateNetwork = true + cfg.Bootstrap.URL = cluster.BootstrapperGlobalEndpoint(ctx.Namespace, 0) + cfg.P2P.MinPeers = 2 + ctx.Log.Debugw("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(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 }) + + require.NoError(t, host.Start()) + t.Cleanup(func() { assert.NoError(t, host.Stop()) }) + + syncer := activation.NewMocksyncer(gomock.NewController(t)) + syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { + ch := make(chan struct{}) + close(ch) + return ch + }).AnyTimes() + + // 1. Initialize + postSetupMgr, err := activation.NewPostSetupManager( + signer.NodeID(), + cfg.POST, + logger.Named("post"), + datastore.NewCachedDB(sql.InMemory(), log.NewNop()), + cl.GoldenATX(), + syncer, + activation.NewMocknipostValidator(gomock.NewController(t)), + ) + require.NoError(t, err) + + postSupervisor, err := activation.NewPostSupervisor( + logger.Named("post-supervisor"), + cfg.POSTService, + cfg.POST, + cfg.SMESHING.ProvingOpts, + postSetupMgr, + ) + 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(logger.Named("clock"))), + ) + require.NoError(t, err) + + grpcPostService := grpcserver.NewPostService(logger.Named("grpc-post-service")) + grpczap.SetGrpcLoggerV2(grpclog, logger.Named("grpc")) + grpcPrivateServer, err := grpcserver.NewWithServices( + cfg.API.PostListener, + 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.NewNop()), + grpcPostService, + cfg.PoetServers, + logger.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 + } + // Sanity check that the POST is invalid + verifyingOpts := activation.DefaultPostVerifyingOpts() + verifyingOpts.Workers = 1 + verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) + require.NoError(t, err) + err = verifier.Verify(ctx, (*shared.Proof)(nipost.Post), &shared.ProofMetadata{ + NodeId: signer.NodeID().Bytes(), + CommitmentAtxId: challenge.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}, + nipost.NIPost, + nipost.NumUnits, + &nipost.VRFNonce, + ) + nodeID := signer.NodeID() + atx.InnerActivationTx.NodeID = &nodeID + require.NoError(t, activation.SignAndFinalizeAtx(signer, atx)) + + // 3. Wait for publish epoch + 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 + publishCtx, stopPublishing := context.WithCancel(ctx.Context) + 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) + 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 + 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_MALFEASANCE_POST_INDEX, 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) + + 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 = verifier.Verify(ctx, (*shared.Proof)(invalidAtx.NIPost.Post), meta) + var invalidIdxError *verifying.ErrInvalidIndex + require.ErrorAs(t, err, &invalidIdxError) + return false, nil + }) + require.NoError(t, err) +}