From 61e36d24607d87310279c30795142074b3f8dd79 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:59:22 +0000 Subject: [PATCH 01/56] Optimize MarriagesATX query --- sql/marriage/marriages.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sql/marriage/marriages.go b/sql/marriage/marriages.go index 58410e1a9c7..561a8731941 100644 --- a/sql/marriage/marriages.go +++ b/sql/marriage/marriages.go @@ -3,9 +3,10 @@ package marriage import ( "bytes" "fmt" - "slices" "sort" + "golang.org/x/exp/maps" + "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/builder" @@ -132,7 +133,7 @@ func FindByNodeID(db sql.Executor, nodeID types.NodeID) (Info, error) { } func MarriageATXs(db sql.Executor, id ID) ([]types.ATXID, error) { - var atxs []types.ATXID + atxs := make(map[types.ATXID]struct{}) rows, err := db.Exec(` SELECT marriage_atx FROM marriages @@ -142,7 +143,7 @@ func MarriageATXs(db sql.Executor, id ID) ([]types.ATXID, error) { }, func(s *sql.Statement) bool { var atx types.ATXID s.ColumnBytes(0, atx[:]) - atxs = append(atxs, atx) + atxs[atx] = struct{}{} return true }) if err != nil { @@ -151,8 +152,9 @@ func MarriageATXs(db sql.Executor, id ID) ([]types.ATXID, error) { if rows == 0 { return nil, sql.ErrNotFound } - sort.Slice(atxs, func(i, j int) bool { return bytes.Compare(atxs[i].Bytes(), atxs[j].Bytes()) < 0 }) - return slices.Compact(atxs), nil + atxIDs := maps.Keys(atxs) + sort.Slice(atxIDs, func(i, j int) bool { return bytes.Compare(atxIDs[i].Bytes(), atxIDs[j].Bytes()) < 0 }) + return atxIDs, nil } func NodeIDsByID(db sql.Executor, id ID) ([]types.NodeID, error) { From 9b68d945ef93bc78a2644d70c22dc50188ba6967 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:04:54 +0000 Subject: [PATCH 02/56] Add missing tests --- sql/marriage/marriages_test.go | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/sql/marriage/marriages_test.go b/sql/marriage/marriages_test.go index bb5f940e650..46eed311911 100644 --- a/sql/marriage/marriages_test.go +++ b/sql/marriage/marriages_test.go @@ -48,6 +48,51 @@ func TestFind(t *testing.T) { require.ErrorIs(t, err, sql.ErrNotFound) } +func TestUpdateMarriageID(t *testing.T) { + t.Parallel() + db := statesql.InMemoryTest(t) + + id1, err := marriage.NewID(db) + require.NoError(t, err) + require.NotZero(t, id1) + + nodeID1 := types.RandomNodeID() + nodeID2 := types.RandomNodeID() + info := marriage.Info{ + ID: id1, + NodeID: nodeID1, + ATX: types.RandomATXID(), + MarriageIndex: rand.N(256), + Target: types.RandomNodeID(), + Signature: types.RandomEdSignature(), + } + err = marriage.Add(db, info) + require.NoError(t, err) + + info.NodeID = nodeID2 + info.MarriageIndex = (info.MarriageIndex + 1) % 256 + err = marriage.Add(db, info) + require.NoError(t, err) + + id2, err := marriage.NewID(db) + require.NoError(t, err) + require.NotZero(t, id2) + require.NotEqual(t, id1, id2) + + err = marriage.UpdateMarriageID(db, id1, id2) + require.NoError(t, err) + + for _, nodeID := range []types.NodeID{nodeID1, nodeID2} { + id, err := marriage.FindIDByNodeID(db, nodeID) + require.NoError(t, err) + require.Equal(t, id2, id) + + info, err = marriage.FindByNodeID(db, nodeID) + require.NoError(t, err) + require.Equal(t, id2, info.ID) + } +} + func TestAdd(t *testing.T) { t.Parallel() db := statesql.InMemoryTest(t) From bef01f4189c55f3ff02df5e1da1ac15b26cfab82 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:03:55 +0000 Subject: [PATCH 03/56] Add malfeasance handler to fetcher --- fetch/fetch.go | 39 ++++++++-------- fetch/fetch_test.go | 25 ++++++----- fetch/mesh_data.go | 2 +- fetch/mesh_data_test.go | 2 +- fetch/p2p_test.go | 3 +- malfeasance/handler.go | 8 ++-- malfeasance/handler_test.go | 22 +++++----- node/node.go | 44 ++++--------------- .../distributed_post_verification_test.go | 2 + 9 files changed, 66 insertions(+), 81 deletions(-) diff --git a/fetch/fetch.go b/fetch/fetch.go index a1206baf3f4..ac0e50183ef 100644 --- a/fetch/fetch.go +++ b/fetch/fetch.go @@ -394,15 +394,16 @@ func (f *Fetch) registerServer( } type dataValidators struct { - atx SyncValidator - poet SyncValidator - ballot SyncValidator - activeset SyncValidator - block SyncValidator - proposal SyncValidator - txBlock SyncValidator - txProposal SyncValidator - malfeasance SyncValidator + atx SyncValidator + poet SyncValidator + ballot SyncValidator + activeset SyncValidator + block SyncValidator + proposal SyncValidator + txBlock SyncValidator + txProposal SyncValidator + legacyMalfeasance SyncValidator + malfeasance SyncValidator } // SetValidators sets the handlers to validate various mesh data fetched from peers. @@ -416,17 +417,19 @@ func (f *Fetch) SetValidators( txBlock SyncValidator, txProposal SyncValidator, mal SyncValidator, + mal2 SyncValidator, ) { f.validators = &dataValidators{ - atx: atx, - poet: poet, - ballot: ballot, - activeset: activeset, - block: block, - proposal: prop, - txBlock: txBlock, - txProposal: txProposal, - malfeasance: mal, + atx: atx, + poet: poet, + ballot: ballot, + activeset: activeset, + block: block, + proposal: prop, + txBlock: txBlock, + txProposal: txProposal, + legacyMalfeasance: mal, + malfeasance: mal2, } } diff --git a/fetch/fetch_test.go b/fetch/fetch_test.go index 24748d95fff..e47043e84df 100644 --- a/fetch/fetch_test.go +++ b/fetch/fetch_test.go @@ -36,7 +36,6 @@ type testFetch struct { mMHashS *mocks.Mockrequester mOpn2S *mocks.Mockrequester - mMalH *mocks.MockSyncValidator mAtxH *mocks.MockSyncValidator mBallotH *mocks.MockSyncValidator mActiveSetH *mocks.MockSyncValidator @@ -46,19 +45,21 @@ type testFetch struct { mTxBlocksH *mocks.MockSyncValidator mTxProposalH *mocks.MockSyncValidator mPoetH *mocks.MockSyncValidator + mLegacyMalH *mocks.MockSyncValidator + mMalH *mocks.MockSyncValidator } func createFetch(tb testing.TB) *testFetch { ctrl := gomock.NewController(tb) tf := &testFetch{ - mh: mocks.NewMockhost(ctrl), - mMalS: mocks.NewMockrequester(ctrl), - mAtxS: mocks.NewMockrequester(ctrl), - mLyrS: mocks.NewMockrequester(ctrl), - mHashS: mocks.NewMockrequester(ctrl), - mMHashS: mocks.NewMockrequester(ctrl), - mOpn2S: mocks.NewMockrequester(ctrl), - mMalH: mocks.NewMockSyncValidator(ctrl), + mh: mocks.NewMockhost(ctrl), + mMalS: mocks.NewMockrequester(ctrl), + mAtxS: mocks.NewMockrequester(ctrl), + mLyrS: mocks.NewMockrequester(ctrl), + mHashS: mocks.NewMockrequester(ctrl), + mMHashS: mocks.NewMockrequester(ctrl), + mOpn2S: mocks.NewMockrequester(ctrl), + mAtxH: mocks.NewMockSyncValidator(ctrl), mBallotH: mocks.NewMockSyncValidator(ctrl), mActiveSetH: mocks.NewMockSyncValidator(ctrl), @@ -67,6 +68,8 @@ func createFetch(tb testing.TB) *testFetch { mTxBlocksH: mocks.NewMockSyncValidator(ctrl), mTxProposalH: mocks.NewMockSyncValidator(ctrl), mPoetH: mocks.NewMockSyncValidator(ctrl), + mLegacyMalH: mocks.NewMockSyncValidator(ctrl), + mMalH: mocks.NewMockSyncValidator(ctrl), } for _, srv := range []*mocks.Mockrequester{tf.mMalS, tf.mAtxS, tf.mLyrS, tf.mHashS, tf.mMHashS, tf.mOpn2S} { srv.EXPECT().Run(gomock.Any()).AnyTimes() @@ -114,6 +117,7 @@ func createFetch(tb testing.TB) *testFetch { tf.mProposalH, tf.mTxBlocksH, tf.mTxProposalH, + tf.mLegacyMalH, tf.mMalH, ) return tf @@ -422,7 +426,7 @@ func TestFetch_PeerDroppedWhenMessageResultsInValidationReject(t *testing.T) { vf := ValidatorFunc( func(context.Context, types.Hash32, peer.ID, []byte) error { return pubsub.ErrValidationReject }, ) - fetcher.SetValidators(vf, nil, nil, nil, nil, nil, nil, nil, nil) + fetcher.SetValidators(vf, nil, nil, nil, nil, nil, nil, nil, nil, nil) // Request an atx by hash _, err = fetcher.getHash( @@ -452,6 +456,7 @@ func TestFetch_PeerDroppedWhenMessageResultsInValidationReject(t *testing.T) { nil, nil, nil, + nil, ) // Request an atx by hash diff --git a/fetch/mesh_data.go b/fetch/mesh_data.go index 34a5f62181e..fd1f7b36631 100644 --- a/fetch/mesh_data.go +++ b/fetch/mesh_data.go @@ -167,7 +167,7 @@ func (f *Fetch) GetMalfeasanceProofs(ctx context.Context, ids []types.NodeID) er } f.logger.Debug("requesting malfeasance proofs from peer", log.ZContext(ctx), zap.Int("num_proofs", len(ids))) hashes := types.NodeIDsToHashes(ids) - return f.getHashes(ctx, hashes, datastore.Malfeasance, f.validators.malfeasance.HandleMessage) + return f.getHashes(ctx, hashes, datastore.Malfeasance, f.validators.legacyMalfeasance.HandleMessage) } // GetBallots gets data for the specified BallotIDs and validates them. diff --git a/fetch/mesh_data_test.go b/fetch/mesh_data_test.go index 82c5d422f30..6016a116f02 100644 --- a/fetch/mesh_data_test.go +++ b/fetch/mesh_data_test.go @@ -387,7 +387,7 @@ func TestFetch_getHashesStreaming(t *testing.T) { func TestFetch_GetMalfeasanceProofs(t *testing.T) { nodeIDs := []types.NodeID{{1}, {2}, {3}} f := createFetch(t) - f.mMalH.EXPECT(). + f.mLegacyMalH.EXPECT(). HandleMessage(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(nil). Times(len(nodeIDs)) diff --git a/fetch/p2p_test.go b/fetch/p2p_test.go index 183e5cafa18..26c9bd7620e 100644 --- a/fetch/p2p_test.go +++ b/fetch/p2p_test.go @@ -143,7 +143,7 @@ func createP2PFetch( vf := ValidatorFunc( func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }, ) - tpf.serverFetch.SetValidators(vf, vf, vf, vf, vf, vf, vf, vf, vf) + tpf.serverFetch.SetValidators(vf, vf, vf, vf, vf, vf, vf, vf, vf, vf) require.NoError(tb, tpf.serverFetch.Start()) tb.Cleanup(tpf.serverFetch.Stop) @@ -172,6 +172,7 @@ func createP2PFetch( mkFakeValidator(tpf, "txBlock"), mkFakeValidator(tpf, "txProposal"), mkFakeValidator(tpf, "mal"), + mkFakeValidator(tpf, "mal2"), ) require.NoError(tb, tpf.clientFetch.Start()) tb.Cleanup(tpf.clientFetch.Stop) diff --git a/malfeasance/handler.go b/malfeasance/handler.go index 005a90b86db..88b05b948a7 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -152,8 +152,8 @@ func (h *Handler) Info(ctx context.Context, nodeID types.NodeID) (map[string]str return properties, nil } -// HandleSyncedMalfeasanceProof is the sync validator for MalfeasanceProof. -func (h *Handler) HandleSyncedMalfeasanceProof( +// HandleSynced is the sync validator for MalfeasanceProof. +func (h *Handler) HandleSynced( ctx context.Context, expHash types.Hash32, peer p2p.Peer, @@ -188,8 +188,8 @@ func (h *Handler) HandleSyncedMalfeasanceProof( return err } -// HandleMalfeasanceProof is the gossip receiver for MalfeasanceGossip. -func (h *Handler) HandleMalfeasanceProof(ctx context.Context, peer p2p.Peer, data []byte) error { +// HandleGossip is the gossip receiver for MalfeasanceGossip. +func (h *Handler) HandleGossip(ctx context.Context, peer p2p.Peer, data []byte) error { var p wire.MalfeasanceGossip if err := codec.Decode(data, &p); err != nil { h.numMalformed.Inc() diff --git a/malfeasance/handler_test.go b/malfeasance/handler_test.go index 85dc77304d7..ebf5093e83e 100644 --- a/malfeasance/handler_test.go +++ b/malfeasance/handler_test.go @@ -73,7 +73,7 @@ func TestHandler_HandleMalfeasanceProof(t *testing.T) { t.Run("malformed data", func(t *testing.T) { h := newHandler(t) - err := h.HandleMalfeasanceProof(context.Background(), "peer", []byte{0x01}) + err := h.HandleGossip(context.Background(), "peer", []byte{0x01}) require.ErrorIs(t, err, errMalformedData) require.ErrorIs(t, err, pubsub.ErrValidationReject) @@ -98,7 +98,7 @@ spacemesh_malfeasance_num_invalid_proofs{type="mal"} 1 }, } - err := h.HandleMalfeasanceProof(context.Background(), "peer", codec.MustEncode(gossip)) + err := h.HandleGossip(context.Background(), "peer", codec.MustEncode(gossip)) require.ErrorIs(t, err, errUnknownProof) require.ErrorIs(t, err, pubsub.ErrValidationReject) @@ -134,7 +134,7 @@ spacemesh_malfeasance_num_invalid_proofs{type="mal"} 1 }, } - err := h.HandleMalfeasanceProof(context.Background(), "peer", codec.MustEncode(gossip)) + err := h.HandleGossip(context.Background(), "peer", codec.MustEncode(gossip)) require.ErrorContains(t, err, "invalid proof") require.ErrorIs(t, err, pubsub.ErrValidationReject) @@ -173,7 +173,7 @@ spacemesh_malfeasance_num_invalid_proofs{type="multiATXs"} 1 } h.mockTrt.EXPECT().OnMalfeasance(nodeID) - err := h.HandleMalfeasanceProof(context.Background(), "peer", codec.MustEncode(gossip)) + err := h.HandleGossip(context.Background(), "peer", codec.MustEncode(gossip)) require.NoError(t, err) var blob sql.Blob @@ -221,7 +221,7 @@ spacemesh_malfeasance_num_proofs{type="multiATXs"} 1 }, } - err := h.HandleMalfeasanceProof(context.Background(), "peer", codec.MustEncode(gossip)) + err := h.HandleGossip(context.Background(), "peer", codec.MustEncode(gossip)) require.NoError(t, err) var blob sql.Blob @@ -234,7 +234,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { t.Run("malformed data", func(t *testing.T) { h := newHandler(t) - err := h.HandleSyncedMalfeasanceProof( + err := h.HandleSynced( context.Background(), types.RandomHash(), "peer", @@ -262,7 +262,7 @@ spacemesh_malfeasance_num_invalid_proofs{type="mal"} 1 }, } - err := h.HandleSyncedMalfeasanceProof( + err := h.HandleSynced( context.Background(), types.RandomHash(), "peer", @@ -304,7 +304,7 @@ spacemesh_malfeasance_num_invalid_proofs{type="mal"} 1 expectedHash := types.RandomHash() h.mockTrt.EXPECT().OnMalfeasance(nodeID) - err := h.HandleSyncedMalfeasanceProof( + err := h.HandleSynced( context.Background(), expectedHash, "peer", @@ -351,7 +351,7 @@ spacemesh_malfeasance_num_proofs{type="multiATXs"} 1 }, } - err := h.HandleSyncedMalfeasanceProof( + err := h.HandleSynced( context.Background(), types.Hash32(nodeID), "peer", @@ -394,7 +394,7 @@ spacemesh_malfeasance_num_invalid_proofs{type="multiATXs"} 1 proofBytes := codec.MustEncode(proof) h.mockTrt.EXPECT().OnMalfeasance(nodeID) - err := h.HandleSyncedMalfeasanceProof(context.Background(), types.Hash32(nodeID), "peer", proofBytes) + err := h.HandleSynced(context.Background(), types.Hash32(nodeID), "peer", proofBytes) require.NoError(t, err) var blob sql.Blob @@ -443,7 +443,7 @@ spacemesh_malfeasance_num_proofs{type="multiATXs"} 1 newProofBytes := codec.MustEncode(newProof) require.NotEqual(t, proofBytes, newProofBytes) - err := h.HandleSyncedMalfeasanceProof(context.Background(), types.Hash32(nodeID), "peer", newProofBytes) + err := h.HandleSynced(context.Background(), types.Hash32(nodeID), "peer", newProofBytes) require.NoError(t, err) var blob sql.Blob diff --git a/node/node.go b/node/node.go index 8a98ccd131e..638c8aebdcf 100644 --- a/node/node.go +++ b/node/node.go @@ -1214,11 +1214,7 @@ func (app *App) initServices(ctx context.Context) error { pubsub.DropPeerOnSyncValidationReject(poetDb.ValidateAndStoreMsg, app.host, lg.Zap()), ), fetch.ValidatorFunc( - pubsub.DropPeerOnSyncValidationReject( - proposalListener.HandleSyncedBallot, - app.host, - lg.Zap(), - ), + pubsub.DropPeerOnSyncValidationReject(proposalListener.HandleSyncedBallot, app.host, lg.Zap()), ), fetch.ValidatorFunc( pubsub.DropPeerOnSyncValidationReject(proposalListener.HandleActiveSet, app.host, lg.Zap()), @@ -1227,41 +1223,20 @@ func (app *App) initServices(ctx context.Context) error { pubsub.DropPeerOnSyncValidationReject(blockHandler.HandleSyncedBlock, app.host, lg.Zap()), ), fetch.ValidatorFunc( - pubsub.DropPeerOnSyncValidationReject( - proposalListener.HandleSyncedProposal, - app.host, - lg.Zap(), - ), + pubsub.DropPeerOnSyncValidationReject(proposalListener.HandleSyncedProposal, app.host, lg.Zap()), ), fetch.ValidatorFunc( - pubsub.DropPeerOnSyncValidationReject( - app.txHandler.HandleBlockTransaction, - app.host, - lg.Zap(), - ), + pubsub.DropPeerOnSyncValidationReject(app.txHandler.HandleBlockTransaction, app.host, lg.Zap()), ), fetch.ValidatorFunc( - pubsub.DropPeerOnSyncValidationReject( - app.txHandler.HandleProposalTransaction, - app.host, - lg.Zap(), - ), + pubsub.DropPeerOnSyncValidationReject(app.txHandler.HandleProposalTransaction, app.host, lg.Zap()), ), fetch.ValidatorFunc( - pubsub.DropPeerOnSyncValidationReject( - malHandler.HandleSyncedMalfeasanceProof, - app.host, - lg.Zap(), - ), + pubsub.DropPeerOnSyncValidationReject(malHandler.HandleSynced, app.host, lg.Zap()), + ), + fetch.ValidatorFunc( + pubsub.DropPeerOnSyncValidationReject(malHandler2.HandleSynced, app.host, lg.Zap()), ), - // TODO(mafa): add malfeasance2 handler to fetcher - // fetch.ValidatorFunc( - // pubsub.DropPeerOnSyncValidationReject( - // malHandler2.HandleSyncedMalfeasanceProof, - // app.host, - // lg.Zap(), - // ), - // ), ) checkSynced := func(_ context.Context, _ p2p.Peer, _ []byte) error { @@ -1318,7 +1293,7 @@ func (app *App) initServices(ctx context.Context) error { ) app.host.Register( pubsub.MalfeasanceProof, - pubsub.ChainGossipHandler(checkAtxSynced, malHandler.HandleMalfeasanceProof), + pubsub.ChainGossipHandler(checkAtxSynced, malHandler.HandleGossip), ) app.host.Register( pubsub.MalfeasanceProof2, @@ -2194,7 +2169,6 @@ func (app *App) Start(ctx context.Context) error { Msg: "node is shutting down", Level: zapcore.InfoLevel, }) - // TODO: pass app.eg to components and wait for them collectively if app.ptimesync != nil { app.eg.Go(func() error { app.errCh <- app.ptimesync.Wait() diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 513466bddee..93a060dec34 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -42,6 +42,7 @@ import ( ) // TestPostMalfeasanceProof tests that nodes can detect an invalid PoST and create a malfeasance proof against it. +// TODO(mafa): update test to publish the ATX after v2 ATXs are live and then check for malfeasance. func TestPostMalfeasanceProof(t *testing.T) { t.Parallel() testDir := t.TempDir() @@ -138,6 +139,7 @@ func TestPostMalfeasanceProof(t *testing.T) { fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), + fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), ) require.NoError(t, fetcher.Start()) From b2319d30607a8070c05803cd80c20495d40f8b66 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:22:53 +0000 Subject: [PATCH 04/56] Prepare datastore for malfeasance proofs --- datastore/store.go | 53 +++++++++++++---------- datastore/store_test.go | 95 ++++++++++++++++++----------------------- 2 files changed, 72 insertions(+), 76 deletions(-) diff --git a/datastore/store.go b/datastore/store.go index fbc9a34381d..2c272d8733a 100644 --- a/datastore/store.go +++ b/datastore/store.go @@ -215,15 +215,16 @@ type Hint string // DB hints per DB. const ( - NoHint Hint = "" - BallotDB Hint = "ballotDB" - BlockDB Hint = "blocksDB" - ProposalDB Hint = "proposalDB" - ATXDB Hint = "ATXDB" - TXDB Hint = "TXDB" - POETDB Hint = "POETDB" - Malfeasance Hint = "malfeasance" - ActiveSet Hint = "activeset" + NoHint Hint = "" + BallotDB Hint = "ballotDB" + BlockDB Hint = "blocksDB" + ProposalDB Hint = "proposalDB" + ATXDB Hint = "ATXDB" + TXDB Hint = "TXDB" + POETDB Hint = "POETDB" + LegacyMalfeasance Hint = "malfeasance" + Malfeasance Hint = "malfeasance2" + ActiveSet Hint = "activeset" ) // NewBlobStore returns a BlobStore. @@ -247,22 +248,26 @@ var loadBlobDispatch = map[Hint]loadBlobFunc{ _, err := atxs.LoadBlob(ctx, db, key, blob) return err }, - BallotDB: ballots.LoadBlob, - BlockDB: blocks.LoadBlob, - TXDB: transactions.LoadBlob, - POETDB: poets.LoadBlob, - Malfeasance: identities.LoadMalfeasanceBlob, - ActiveSet: activesets.LoadBlob, + BallotDB: ballots.LoadBlob, + BlockDB: blocks.LoadBlob, + TXDB: transactions.LoadBlob, + POETDB: poets.LoadBlob, + LegacyMalfeasance: identities.LoadMalfeasanceBlob, + // TODO(mafa): implement malfeasance2 + // Malfeasance: malfeasance.LoadBlob, + ActiveSet: activesets.LoadBlob, } var blobSizeDispatch = map[Hint]blobSizeFunc{ - ATXDB: atxs.GetBlobSizes, - BallotDB: ballots.GetBlobSizes, - BlockDB: blocks.GetBlobSizes, - TXDB: transactions.GetBlobSizes, - POETDB: poets.GetBlobSizes, - Malfeasance: identities.GetBlobSizes, - ActiveSet: activesets.GetBlobSizes, + ATXDB: atxs.GetBlobSizes, + BallotDB: ballots.GetBlobSizes, + BlockDB: blocks.GetBlobSizes, + TXDB: transactions.GetBlobSizes, + POETDB: poets.GetBlobSizes, + LegacyMalfeasance: identities.GetBlobSizes, + // TODO(mafa): implement malfeasance2 + // Malfeasance: malfeasance.BlobSizes, + ActiveSet: activesets.GetBlobSizes, } func (bs *BlobStore) loadProposal(key []byte, blob *sql.Blob) error { @@ -349,8 +354,10 @@ func (bs *BlobStore) Has(hint Hint, key []byte) (bool, error) { return transactions.Has(bs.DB, types.TransactionID(types.BytesToHash(key))) case POETDB: return poets.Has(bs.DB, types.ByteToPoetProofRef(key)) - case Malfeasance: + case LegacyMalfeasance: return identities.IsMalicious(bs.DB, types.BytesToNodeID(key)) + case Malfeasance: + // TODO (mafa): implement malfeasance2 case ActiveSet: return activesets.Has(bs.DB, types.BytesToHash(key)) } diff --git a/datastore/store_test.go b/datastore/store_test.go index 8683eb54bb7..061f18c9663 100644 --- a/datastore/store_test.go +++ b/datastore/store_test.go @@ -1,7 +1,6 @@ package datastore_test import ( - "bytes" "context" "errors" "os" @@ -35,18 +34,6 @@ func TestMain(m *testing.M) { os.Exit(res) } -type blobId interface { - Bytes() []byte -} - -func getBytes(ctx context.Context, bs *datastore.BlobStore, hint datastore.Hint, id blobId) ([]byte, error) { - var blob sql.Blob - if err := bs.LoadBlob(ctx, hint, id.Bytes(), &blob); err != nil { - return nil, err - } - return blob.Bytes, nil -} - func TestMalfeasanceProof_Dishonest(t *testing.T) { db := statesql.InMemoryTest(t) cdb := datastore.NewCachedDB(db, zaptest.NewLogger(t)) @@ -110,21 +97,19 @@ func TestBlobStore_GetATXBlob(t *testing.T) { require.NoError(t, err) require.False(t, has) - _, err = getBytes(context.Background(), bs, datastore.ATXDB, atx.ID()) + var blob sql.Blob + err = bs.LoadBlob(context.Background(), datastore.ATXDB, atx.ID().Bytes(), &blob) require.ErrorIs(t, err, datastore.ErrNotFound) - blob := types.AtxBlob{Blob: types.RandomBytes(100)} - require.NoError(t, atxs.Add(db, atx, blob)) + atxBlob := types.AtxBlob{Blob: types.RandomBytes(100)} + require.NoError(t, atxs.Add(db, atx, atxBlob)) has, err = bs.Has(datastore.ATXDB, atx.ID().Bytes()) require.NoError(t, err) require.True(t, has) - got, err := getBytes(context.Background(), bs, datastore.ATXDB, atx.ID()) + err = bs.LoadBlob(context.Background(), datastore.ATXDB, atx.ID().Bytes(), &blob) require.NoError(t, err) - require.Equal(t, blob.Blob, got) - - _, err = getBytes(context.Background(), bs, datastore.BallotDB, atx.ID()) - require.ErrorIs(t, err, datastore.ErrNotFound) + require.Equal(t, atxBlob.Blob, blob.Bytes) } func TestBlobStore_GetBallotBlob(t *testing.T) { @@ -142,23 +127,23 @@ func TestBlobStore_GetBallotBlob(t *testing.T) { has, err := bs.Has(datastore.BallotDB, blt.ID().Bytes()) require.NoError(t, err) require.False(t, has) - _, err = getBytes(context.Background(), bs, datastore.BallotDB, blt.ID()) + + var blob sql.Blob + err = bs.LoadBlob(context.Background(), datastore.BallotDB, blt.ID().Bytes(), &blob) require.ErrorIs(t, err, datastore.ErrNotFound) require.NoError(t, ballots.Add(db, blt)) has, err = bs.Has(datastore.BallotDB, blt.ID().Bytes()) require.NoError(t, err) require.True(t, has) - got, err := getBytes(context.Background(), bs, datastore.BallotDB, blt.ID()) + + err = bs.LoadBlob(context.Background(), datastore.BallotDB, blt.ID().Bytes(), &blob) require.NoError(t, err) var gotB types.Ballot - require.NoError(t, codec.Decode(got, &gotB)) + require.NoError(t, codec.Decode(blob.Bytes, &gotB)) require.NoError(t, gotB.Initialize()) require.Equal(t, *blt, gotB) - - _, err = getBytes(context.Background(), bs, datastore.BlockDB, blt.ID()) - require.ErrorIs(t, err, datastore.ErrNotFound) } func TestBlobStore_GetBlockBlob(t *testing.T) { @@ -177,22 +162,21 @@ func TestBlobStore_GetBlockBlob(t *testing.T) { require.NoError(t, err) require.False(t, has) - _, err = getBytes(context.Background(), bs, datastore.BlockDB, blk.ID()) + var blob sql.Blob + err = bs.LoadBlob(context.Background(), datastore.BlockDB, blk.ID().Bytes(), &blob) require.ErrorIs(t, err, datastore.ErrNotFound) require.NoError(t, blocks.Add(db, &blk)) has, err = bs.Has(datastore.BlockDB, blk.ID().Bytes()) require.NoError(t, err) require.True(t, has) - got, err := getBytes(context.Background(), bs, datastore.BlockDB, blk.ID()) + + err = bs.LoadBlob(context.Background(), datastore.BlockDB, blk.ID().Bytes(), &blob) require.NoError(t, err) var gotB types.Block - require.NoError(t, codec.Decode(got, &gotB)) + require.NoError(t, codec.Decode(blob.Bytes, &gotB)) gotB.Initialize() require.Equal(t, blk, gotB) - - _, err = getBytes(context.Background(), bs, datastore.ProposalDB, blk.ID()) - require.ErrorIs(t, err, datastore.ErrNotFound) } func TestBlobStore_GetPoetBlob(t *testing.T) { @@ -212,15 +196,14 @@ func TestBlobStore_GetPoetBlob(t *testing.T) { var poetRef types.PoetProofRef copy(poetRef[:], ref) require.NoError(t, poets.Add(db, poetRef, poet, sid, rid)) + has, err = bs.Has(datastore.POETDB, ref) require.NoError(t, err) require.True(t, has) var blob sql.Blob require.NoError(t, bs.LoadBlob(context.Background(), datastore.POETDB, poetRef[:], &blob)) - require.True(t, bytes.Equal(poet, blob.Bytes)) - - require.ErrorIs(t, bs.LoadBlob(context.Background(), datastore.BlockDB, ref, &sql.Blob{}), datastore.ErrNotFound) + require.Equal(t, poet, blob.Bytes) } func TestBlobStore_GetProposalBlob(t *testing.T) { @@ -245,17 +228,20 @@ func TestBlobStore_GetProposalBlob(t *testing.T) { has, err := bs.Has(datastore.ProposalDB, p.ID().Bytes()) require.NoError(t, err) require.False(t, has) - _, err = getBytes(context.Background(), bs, datastore.ProposalDB, p.ID()) + + var blob sql.Blob + err = bs.LoadBlob(context.Background(), datastore.ProposalDB, p.ID().Bytes(), &blob) require.ErrorIs(t, err, datastore.ErrNotFound) require.NoError(t, proposals.Add(&p)) has, err = bs.Has(datastore.ProposalDB, p.ID().Bytes()) require.NoError(t, err) require.True(t, has) - got, err := getBytes(context.Background(), bs, datastore.ProposalDB, p.ID()) + + err = bs.LoadBlob(context.Background(), datastore.ProposalDB, p.ID().Bytes(), &blob) require.NoError(t, err) var gotP types.Proposal - require.NoError(t, codec.Decode(got, &gotP)) + require.NoError(t, codec.Decode(blob.Bytes, &gotP)) require.NoError(t, gotP.Initialize()) require.Equal(t, p, gotP) } @@ -272,19 +258,18 @@ func TestBlobStore_GetTXBlob(t *testing.T) { require.NoError(t, err) require.False(t, has) - _, err = getBytes(context.Background(), bs, datastore.TXDB, tx.ID) + var blob sql.Blob + err = bs.LoadBlob(context.Background(), datastore.TXDB, tx.ID.Bytes(), &blob) require.ErrorIs(t, err, datastore.ErrNotFound) require.NoError(t, transactions.Add(db, tx, time.Now())) has, err = bs.Has(datastore.TXDB, tx.ID.Bytes()) require.NoError(t, err) require.True(t, has) - got, err := getBytes(context.Background(), bs, datastore.TXDB, tx.ID) - require.NoError(t, err) - require.Equal(t, tx.Raw, got) - _, err = getBytes(context.Background(), bs, datastore.BlockDB, tx.ID) - require.ErrorIs(t, err, datastore.ErrNotFound) + err = bs.LoadBlob(context.Background(), datastore.TXDB, tx.ID.Bytes(), &blob) + require.NoError(t, err) + require.Equal(t, tx.Raw, blob.Bytes) } func TestBlobStore_GetMalfeasanceBlob(t *testing.T) { @@ -304,20 +289,22 @@ func TestBlobStore_GetMalfeasanceBlob(t *testing.T) { require.NoError(t, err) nodeID := types.NodeID{1, 2, 3} - has, err := bs.Has(datastore.Malfeasance, nodeID.Bytes()) + has, err := bs.Has(datastore.LegacyMalfeasance, nodeID.Bytes()) require.NoError(t, err) require.False(t, has) - _, err = getBytes(context.Background(), bs, datastore.Malfeasance, nodeID) + var blob sql.Blob + err = bs.LoadBlob(context.Background(), datastore.LegacyMalfeasance, nodeID.Bytes(), &blob) require.ErrorIs(t, err, datastore.ErrNotFound) require.NoError(t, identities.SetMalicious(db, nodeID, encoded, time.Now())) - has, err = bs.Has(datastore.Malfeasance, nodeID.Bytes()) + has, err = bs.Has(datastore.LegacyMalfeasance, nodeID.Bytes()) require.NoError(t, err) require.True(t, has) - got, err := getBytes(context.Background(), bs, datastore.Malfeasance, nodeID) + + err = bs.LoadBlob(context.Background(), datastore.LegacyMalfeasance, nodeID.Bytes(), &blob) require.NoError(t, err) - require.Equal(t, encoded, got) + require.Equal(t, encoded, blob.Bytes) } func TestBlobStore_GetActiveSet(t *testing.T) { @@ -331,14 +318,16 @@ func TestBlobStore_GetActiveSet(t *testing.T) { require.NoError(t, err) require.False(t, has) - _, err = getBytes(context.Background(), bs, datastore.ActiveSet, hash) + var blob sql.Blob + err = bs.LoadBlob(context.Background(), datastore.ActiveSet, hash.Bytes(), &blob) require.ErrorIs(t, err, datastore.ErrNotFound) require.NoError(t, activesets.Add(db, hash, as)) has, err = bs.Has(datastore.ActiveSet, hash.Bytes()) require.NoError(t, err) require.True(t, has) - got, err := getBytes(context.Background(), bs, datastore.ActiveSet, hash) + + err = bs.LoadBlob(context.Background(), datastore.ActiveSet, hash.Bytes(), &blob) require.NoError(t, err) - require.Equal(t, codec.MustEncode(as), got) + require.Equal(t, codec.MustEncode(as), blob.Bytes) } From 36a813f01fb70d4aeeae12006ad59b2aabbc14d2 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:25:10 +0000 Subject: [PATCH 05/56] Add malfeasance v2 fetcher WiP --- fetch/fetch.go | 15 ++++-- fetch/fetch_test.go | 58 ++++++++++++--------- fetch/handler.go | 6 +-- fetch/handler_test.go | 2 +- fetch/mesh_data.go | 95 +++++++++++++++++++++++++++-------- fetch/mesh_data_test.go | 34 ++++++------- fetch/p2p_test.go | 6 ++- syncer/interface.go | 2 +- syncer/malsync/mocks/mocks.go | 48 +++++++++--------- syncer/malsync/syncer.go | 8 +-- syncer/malsync/syncer_test.go | 16 +++--- syncer/mocks/mocks.go | 48 +++++++++--------- 12 files changed, 203 insertions(+), 135 deletions(-) diff --git a/fetch/fetch.go b/fetch/fetch.go index ac0e50183ef..87aaddfb112 100644 --- a/fetch/fetch.go +++ b/fetch/fetch.go @@ -33,7 +33,8 @@ const ( hashProtocol = "hs/1" activeSetProtocol = "as/1" meshHashProtocol = "mh/1" - malProtocol = "ml/1" + legacyMalProtocol = "ml/1" + malProtocol = "ml/2" OpnProtocol = "lp/2" cacheSize = 1000 @@ -178,9 +179,11 @@ func DefaultConfig() Config { hashProtocol: {Queue: 2000, Requests: 200, Interval: time.Second}, // active sets (can get quite large) activeSetProtocol: {Queue: 10, Requests: 1, Interval: time.Second}, - // serves at most 100 hashes - 3KB + // serves at most 100 hashes - 3 KB meshHashProtocol: {Queue: 1000, Requests: 100, Interval: time.Second}, - // serves all malicious ids (id - 32 byte) - 10KB + // serves all legacy malicious ids (á 32 byte, ~2255 as of Jan 2025) - <100 KB + legacyMalProtocol: {Queue: 100, Requests: 10, Interval: time.Second}, + // serves all malicious ids (á 32 byte, 0 as of Jan 2025) - <100 KB malProtocol: {Queue: 100, Requests: 10, Interval: time.Second}, // 64 bytes OpnProtocol: {Queue: 10000, Requests: 1000, Interval: time.Second}, @@ -357,7 +360,8 @@ func NewFetch( return h.doHandleHashReqStream(ctx, msg, s, datastore.ActiveSet) }) f.registerServer(host, meshHashProtocol, h.handleMeshHashReqStream) - f.registerServer(host, malProtocol, h.handleMaliciousIDsReqStream) + f.registerServer(host, legacyMalProtocol, h.handleLegacyMaliciousIDsReqStream) + // f.registerServer(host, malProtocol, h.handleMaliciousIDsReqStream) } else { f.registerServer(host, atxProtocol, server.WrapHandler(h.handleEpochInfoReq)) f.registerServer(host, hashProtocol, server.WrapHandler(h.handleHashReq)) @@ -367,7 +371,8 @@ func NewFetch( return h.doHandleHashReq(ctx, data, datastore.ActiveSet) })) f.registerServer(host, meshHashProtocol, server.WrapHandler(h.handleMeshHashReq)) - f.registerServer(host, malProtocol, server.WrapHandler(h.handleMaliciousIDsReq)) + f.registerServer(host, legacyMalProtocol, server.WrapHandler(h.handleLegacyMaliciousIDsReq)) + // f.registerServer(host, malProtocol, server.WrapHandler(h.handleMaliciousIDsReq)) } f.registerServer(host, lyrDataProtocol, server.WrapHandler(h.handleLayerDataReq)) f.registerServer(host, OpnProtocol, server.WrapHandler(h.handleLayerOpinionsReq2)) diff --git a/fetch/fetch_test.go b/fetch/fetch_test.go index e47043e84df..ab06c719b03 100644 --- a/fetch/fetch_test.go +++ b/fetch/fetch_test.go @@ -28,13 +28,14 @@ import ( type testFetch struct { *Fetch - mh *mocks.Mockhost - mMalS *mocks.Mockrequester - mAtxS *mocks.Mockrequester - mLyrS *mocks.Mockrequester - mHashS *mocks.Mockrequester - mMHashS *mocks.Mockrequester - mOpn2S *mocks.Mockrequester + mh *mocks.Mockhost + mAtxS *mocks.Mockrequester + mLyrS *mocks.Mockrequester + mHashS *mocks.Mockrequester + mMHashS *mocks.Mockrequester + mOpn2S *mocks.Mockrequester + mLegacyMalS *mocks.Mockrequester + mMalS *mocks.Mockrequester mAtxH *mocks.MockSyncValidator mBallotH *mocks.MockSyncValidator @@ -52,13 +53,15 @@ type testFetch struct { func createFetch(tb testing.TB) *testFetch { ctrl := gomock.NewController(tb) tf := &testFetch{ - mh: mocks.NewMockhost(ctrl), - mMalS: mocks.NewMockrequester(ctrl), - mAtxS: mocks.NewMockrequester(ctrl), - mLyrS: mocks.NewMockrequester(ctrl), - mHashS: mocks.NewMockrequester(ctrl), - mMHashS: mocks.NewMockrequester(ctrl), - mOpn2S: mocks.NewMockrequester(ctrl), + mh: mocks.NewMockhost(ctrl), + + mAtxS: mocks.NewMockrequester(ctrl), + mLyrS: mocks.NewMockrequester(ctrl), + mHashS: mocks.NewMockrequester(ctrl), + mMHashS: mocks.NewMockrequester(ctrl), + mOpn2S: mocks.NewMockrequester(ctrl), + mLegacyMalS: mocks.NewMockrequester(ctrl), + mMalS: mocks.NewMockrequester(ctrl), mAtxH: mocks.NewMockSyncValidator(ctrl), mBallotH: mocks.NewMockSyncValidator(ctrl), @@ -71,9 +74,15 @@ func createFetch(tb testing.TB) *testFetch { mLegacyMalH: mocks.NewMockSyncValidator(ctrl), mMalH: mocks.NewMockSyncValidator(ctrl), } - for _, srv := range []*mocks.Mockrequester{tf.mMalS, tf.mAtxS, tf.mLyrS, tf.mHashS, tf.mMHashS, tf.mOpn2S} { - srv.EXPECT().Run(gomock.Any()).AnyTimes() - } + + tf.mAtxS.EXPECT().Run(gomock.Any()).AnyTimes() + tf.mLyrS.EXPECT().Run(gomock.Any()).AnyTimes() + tf.mHashS.EXPECT().Run(gomock.Any()).AnyTimes() + tf.mMHashS.EXPECT().Run(gomock.Any()).AnyTimes() + tf.mOpn2S.EXPECT().Run(gomock.Any()).AnyTimes() + tf.mLegacyMalS.EXPECT().Run(gomock.Any()).AnyTimes() + tf.mMalS.EXPECT().Run(gomock.Any()).AnyTimes() + cfg := Config{ BatchTimeout: 2 * time.Second, // make sure we never hit the batch timeout BatchSize: 3, @@ -96,12 +105,13 @@ func createFetch(tb testing.TB) *testFetch { WithConfig(cfg), WithLogger(lg), withServers(map[string]requester{ - malProtocol: tf.mMalS, - atxProtocol: tf.mAtxS, - lyrDataProtocol: tf.mLyrS, - hashProtocol: tf.mHashS, - meshHashProtocol: tf.mMHashS, - OpnProtocol: tf.mOpn2S, + atxProtocol: tf.mAtxS, + lyrDataProtocol: tf.mLyrS, + hashProtocol: tf.mHashS, + meshHashProtocol: tf.mMHashS, + OpnProtocol: tf.mOpn2S, + legacyMalProtocol: tf.mLegacyMalS, + malProtocol: tf.mMalS, }), withHost(tf.mh), ) @@ -144,7 +154,7 @@ func TestFetch_Start(t *testing.T) { WithConfig(DefaultConfig()), WithLogger(lg), withServers(map[string]requester{ - malProtocol: nil, + atxProtocol: nil, }), ) require.NoError(t, err) diff --git a/fetch/handler.go b/fetch/handler.go index 7f10f696af2..7a4838ee207 100644 --- a/fetch/handler.go +++ b/fetch/handler.go @@ -41,8 +41,8 @@ func newHandler( } } -// handleMaliciousIDsReq returns the IDs of all known malicious nodes. -func (h *handler) handleMaliciousIDsReq(ctx context.Context, _ p2p.Peer, _ []byte) ([]byte, error) { +// handleLegacyMaliciousIDsReq returns the IDs of all known malicious nodes. +func (h *handler) handleLegacyMaliciousIDsReq(ctx context.Context, _ p2p.Peer, _ []byte) ([]byte, error) { nodes, err := identities.AllMalicious(h.cdb) if err != nil { return nil, fmt.Errorf("getting malicious IDs: %w", err) @@ -54,7 +54,7 @@ func (h *handler) handleMaliciousIDsReq(ctx context.Context, _ p2p.Peer, _ []byt return codec.MustEncode(malicious), nil } -func (h *handler) handleMaliciousIDsReqStream(ctx context.Context, _ p2p.Peer, msg []byte, s io.ReadWriter) error { +func (h *handler) handleLegacyMaliciousIDsReqStream(ctx context.Context, _ p2p.Peer, msg []byte, s io.ReadWriter) error { if err := h.streamIDs(ctx, s, func(cbk retrieveCallback) error { nodeIDs, err := identities.AllMalicious(h.cdb) if err != nil { diff --git a/fetch/handler_test.go b/fetch/handler_test.go index 2fcaba63ed1..a4a280e02f2 100644 --- a/fetch/handler_test.go +++ b/fetch/handler_test.go @@ -367,7 +367,7 @@ func TestHandleMaliciousIDsReq(t *testing.T) { require.NoError(t, identities.SetMalicious(th.cdb, nid, types.RandomBytes(11), time.Now())) } - out, err := th.handleMaliciousIDsReq(context.Background(), p2p.Peer(""), []byte{}) + out, err := th.handleLegacyMaliciousIDsReq(context.Background(), p2p.Peer(""), []byte{}) require.NoError(t, err) var got MaliciousIDs require.NoError(t, codec.Decode(out, &got)) diff --git a/fetch/mesh_data.go b/fetch/mesh_data.go index fd1f7b36631..57e714d449b 100644 --- a/fetch/mesh_data.go +++ b/fetch/mesh_data.go @@ -160,16 +160,32 @@ func (f *Fetch) GetActiveSet(ctx context.Context, set types.Hash32) error { return f.getHashes(ctx, []types.Hash32{set}, datastore.ActiveSet, f.validators.activeset.HandleMessage) } -// GetMalfeasanceProofs gets malfeasance proofs for the specified NodeIDs and validates them. -func (f *Fetch) GetMalfeasanceProofs(ctx context.Context, ids []types.NodeID) error { +// LegacyMalfeasanceProofs gets legacy malfeasance proofs (v1) for the specified NodeIDs and validates them. +func (f *Fetch) LegacyMalfeasanceProofs(ctx context.Context, ids []types.NodeID) error { if len(ids) == 0 { return nil } - f.logger.Debug("requesting malfeasance proofs from peer", log.ZContext(ctx), zap.Int("num_proofs", len(ids))) + f.logger.Debug("requesting legacy malfeasance proofs from peers", + log.ZContext(ctx), + zap.Int("num_proofs", len(ids)), + ) hashes := types.NodeIDsToHashes(ids) - return f.getHashes(ctx, hashes, datastore.Malfeasance, f.validators.legacyMalfeasance.HandleMessage) + return f.getHashes(ctx, hashes, datastore.LegacyMalfeasance, f.validators.legacyMalfeasance.HandleMessage) } +// MalfeasanceProofs gets malfeasance proofs (v2) for the specified NodeIDs and validates them. +// func (f *Fetch) MalfeasanceProofs(ctx context.Context, ids []types.NodeID) error { +// if len(ids) == 0 { +// return nil +// } +// f.logger.Debug("requesting malfeasance proofs from peers", +// log.ZContext(ctx), +// zap.Int("num_proofs", len(ids)), +// ) +// hashes := types.NodeIDsToHashes(ids) +// return f.getHashes(ctx, hashes, datastore.Malfeasance, f.validators.malfeasance.HandleMessage) +// } + // GetBallots gets data for the specified BallotIDs and validates them. func (f *Fetch) GetBallots(ctx context.Context, ids []types.BallotID) error { if len(ids) == 0 { @@ -269,39 +285,76 @@ func (f *Fetch) GetPoetProof(ctx context.Context, id types.Hash32) error { log.ZContext(ctx), zap.String("hint", string(datastore.POETDB)), zap.Stringer("hash", id), - zap.Error(pm.err)) + zap.Error(pm.err), + ) return pm.err } } -func (f *Fetch) GetMaliciousIDs(ctx context.Context, peer p2p.Peer) ([]types.NodeID, error) { +// LegacyMaliciousIDs gets the malicious IDs from the specified peer. Proofs for those IDs can be fetched via the +// legacy malfeasance proofs protocol (see also LegacyMalfeasanceProofs). +func (f *Fetch) LegacyMaliciousIDs(ctx context.Context, peer p2p.Peer) ([]types.NodeID, error) { var malIDs MaliciousIDs - if f.cfg.Streaming { - if err := f.meteredStreamRequest( - ctx, malProtocol, peer, []byte{}, - func(ctx context.Context, s io.ReadWriter) (int, error) { - total, err := readIDSlice(s, &malIDs.NodeIDs, maxMaliciousIDs) - if ctx.Err() != nil { - return total, ctx.Err() - } - return total, err - }, - ); err != nil { - return nil, err - } - } else { - data, err := f.meteredRequest(ctx, malProtocol, peer, []byte{}) + if !f.cfg.Streaming { + data, err := f.meteredRequest(ctx, legacyMalProtocol, peer, []byte{}) if err != nil { return nil, err } if err := codec.Decode(data, &malIDs); err != nil { return nil, err } + f.RegisterPeerHashes(peer, types.NodeIDsToHashes(malIDs.NodeIDs)) + return malIDs.NodeIDs, nil + } + + err := f.meteredStreamRequest(ctx, legacyMalProtocol, peer, []byte{}, + func(ctx context.Context, s io.ReadWriter) (int, error) { + total, err := readIDSlice(s, &malIDs.NodeIDs, maxMaliciousIDs) + if ctx.Err() != nil { + return total, ctx.Err() + } + return total, err + }, + ) + if err != nil { + return nil, err } f.RegisterPeerHashes(peer, types.NodeIDsToHashes(malIDs.NodeIDs)) return malIDs.NodeIDs, nil } +// MaliciousIDs gets the malicious IDs from the specified peer. Proofs for those IDs can be fetched via the malfeasance +// proof protocol (see also MalfeasanceProofs). +// func (f *Fetch) MaliciousIDs(ctx context.Context, peer p2p.Peer) ([]types.NodeID, error) { +// var malIDs MaliciousIDs +// if !f.cfg.Streaming { +// data, err := f.meteredRequest(ctx, malProtocol, peer, []byte{}) +// if err != nil { +// return nil, err +// } +// if err := codec.Decode(data, &malIDs); err != nil { +// return nil, err +// } +// f.RegisterPeerHashes(peer, types.NodeIDsToHashes(malIDs.NodeIDs)) +// return malIDs.NodeIDs, nil +// } + +// err := f.meteredStreamRequest(ctx, malProtocol, peer, []byte{}, +// func(ctx context.Context, s io.ReadWriter) (int, error) { +// total, err := readIDSlice(s, &malIDs.NodeIDs, maxMaliciousIDs) +// if ctx.Err() != nil { +// return total, ctx.Err() +// } +// return total, err +// }, +// ) +// if err != nil { +// return nil, err +// } +// f.RegisterPeerHashes(peer, types.NodeIDsToHashes(malIDs.NodeIDs)) +// return malIDs.NodeIDs, nil +// } + // GetLayerData get layer data from peers. func (f *Fetch) GetLayerData(ctx context.Context, peer p2p.Peer, lid types.LayerID) ([]byte, error) { lidBytes := codec.MustEncode(&lid) diff --git a/fetch/mesh_data_test.go b/fetch/mesh_data_test.go index 6016a116f02..29a2162a918 100644 --- a/fetch/mesh_data_test.go +++ b/fetch/mesh_data_test.go @@ -100,15 +100,6 @@ func startTestLoop(tb testing.TB, f *Fetch, eg *errgroup.Group, stop chan struct }) } -func generateMaliciousIDs(tb testing.TB) []types.NodeID { - tb.Helper() - malIDs := make([]types.NodeID, numMalicious) - for i := range malIDs { - malIDs[i] = types.RandomNodeID() - } - return malIDs -} - func generateLayerContent(tb testing.TB) []byte { tb.Helper() ballotIDs := make([]types.BallotID, 0, numBallots) @@ -396,7 +387,7 @@ func TestFetch_GetMalfeasanceProofs(t *testing.T) { var eg errgroup.Group startTestLoop(t, f.Fetch, &eg, stop) - require.NoError(t, f.GetMalfeasanceProofs(context.Background(), nodeIDs)) + require.NoError(t, f.LegacyMalfeasanceProofs(context.Background(), nodeIDs)) close(stop) require.NoError(t, eg.Wait()) } @@ -659,29 +650,36 @@ func TestGetPoetProof(t *testing.T) { require.NoError(t, eg.Wait()) } -func TestFetch_GetMaliciousIDs(t *testing.T) { +func TestFetch_LegacyMaliciousIDs(t *testing.T) { t.Run("success", func(t *testing.T) { t.Parallel() f := createFetch(t) - expectedIds := generateMaliciousIDs(t) - resp := codec.MustEncode(&MaliciousIDs{NodeIDs: expectedIds}) + expectedIDs := make([]types.NodeID, numMalicious) + for i := range expectedIDs { + expectedIDs[i] = types.RandomNodeID() + } + resp := codec.MustEncode(&MaliciousIDs{NodeIDs: expectedIDs}) f.mh.EXPECT().ID().Return("self").AnyTimes() - f.mMalS.EXPECT().Request(gomock.Any(), p2p.Peer("p0"), []byte{}).Return(resp, nil) - ids, err := f.GetMaliciousIDs(context.Background(), "p0") + f.mLegacyMalS.EXPECT().Request(gomock.Any(), p2p.Peer("p0"), []byte{}).Return(resp, nil) + ids, err := f.LegacyMaliciousIDs(context.Background(), "p0") require.NoError(t, err) - require.Equal(t, expectedIds, ids) + require.Equal(t, expectedIDs, ids) }) t.Run("failure", func(t *testing.T) { t.Parallel() errUnknown := errors.New("unknown") f := createFetch(t) - f.mMalS.EXPECT().Request(gomock.Any(), p2p.Peer("p0"), []byte{}).Return(nil, errUnknown) - ids, err := f.GetMaliciousIDs(context.Background(), "p0") + f.mLegacyMalS.EXPECT().Request(gomock.Any(), p2p.Peer("p0"), []byte{}).Return(nil, errUnknown) + ids, err := f.LegacyMaliciousIDs(context.Background(), "p0") require.ErrorIs(t, err, errUnknown) require.Nil(t, ids) }) } +func TestFetch_MaliciousIDs(t *testing.T) { + // TODO(mafa): implement for malfeasance2 +} + func TestFetch_GetLayerOpinions(t *testing.T) { t.Run("success", func(t *testing.T) { t.Parallel() diff --git a/fetch/p2p_test.go b/fetch/p2p_test.go index 26c9bd7620e..a7cb214f2b9 100644 --- a/fetch/p2p_test.go +++ b/fetch/p2p_test.go @@ -356,7 +356,7 @@ func TestP2PMaliciousIDs(t *testing.T) { tpf.serverDB.Close() } - malIDs, err := tpf.clientFetch.GetMaliciousIDs(context.Background(), tpf.serverID) + malIDs, err := tpf.clientFetch.LegacyMaliciousIDs(context.Background(), tpf.serverID) if errStr == "" { require.NoError(t, err) require.ElementsMatch(t, bad, malIDs) @@ -531,7 +531,9 @@ func TestP2PGetMalfeasanceProofs(t *testing.T) { proof := types.RandomBytes(11) require.NoError(t, identities.SetMalicious(tpf.serverCDB, nid, proof, time.Now())) tpf.verifyGetHash( - func() error { return tpf.clientFetch.GetMalfeasanceProofs(context.Background(), []types.NodeID{nid}) }, + func() error { + return tpf.clientFetch.LegacyMalfeasanceProofs(context.Background(), []types.NodeID{nid}) + }, errStr, "mal", "hs/1", types.Hash32(nid), nid.Bytes(), proof, ) diff --git a/syncer/interface.go b/syncer/interface.go index 95d8f171258..93bae98e423 100644 --- a/syncer/interface.go +++ b/syncer/interface.go @@ -47,7 +47,7 @@ type fetcher interface { GetCert(context.Context, types.LayerID, types.BlockID, []p2p.Peer) (*types.Certificate, error) GetAtxs(context.Context, []types.ATXID, ...system.GetAtxOpt) error - GetMalfeasanceProofs(context.Context, []types.NodeID) error + MalfeasanceProofs(context.Context, []types.NodeID) error GetBallots(context.Context, []types.BallotID) error GetBlocks(context.Context, []types.BlockID) error RegisterPeerHashes(peer p2p.Peer, hashes []types.Hash32) diff --git a/syncer/malsync/mocks/mocks.go b/syncer/malsync/mocks/mocks.go index e1417c676a3..f3684704331 100644 --- a/syncer/malsync/mocks/mocks.go +++ b/syncer/malsync/mocks/mocks.go @@ -42,79 +42,79 @@ func (m *Mockfetcher) EXPECT() *MockfetcherMockRecorder { return m.recorder } -// GetMalfeasanceProofs mocks base method. -func (m *Mockfetcher) GetMalfeasanceProofs(arg0 context.Context, arg1 []types.NodeID) error { +// LegacyMalfeasanceProofs mocks base method. +func (m *Mockfetcher) LegacyMalfeasanceProofs(arg0 context.Context, arg1 []types.NodeID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMalfeasanceProofs", arg0, arg1) + ret := m.ctrl.Call(m, "LegacyMalfeasanceProofs", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// GetMalfeasanceProofs indicates an expected call of GetMalfeasanceProofs. -func (mr *MockfetcherMockRecorder) GetMalfeasanceProofs(arg0, arg1 any) *MockfetcherGetMalfeasanceProofsCall { +// LegacyMalfeasanceProofs indicates an expected call of LegacyMalfeasanceProofs. +func (mr *MockfetcherMockRecorder) LegacyMalfeasanceProofs(arg0, arg1 any) *MockfetcherLegacyMalfeasanceProofsCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMalfeasanceProofs", reflect.TypeOf((*Mockfetcher)(nil).GetMalfeasanceProofs), arg0, arg1) - return &MockfetcherGetMalfeasanceProofsCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LegacyMalfeasanceProofs", reflect.TypeOf((*Mockfetcher)(nil).LegacyMalfeasanceProofs), arg0, arg1) + return &MockfetcherLegacyMalfeasanceProofsCall{Call: call} } -// MockfetcherGetMalfeasanceProofsCall wrap *gomock.Call -type MockfetcherGetMalfeasanceProofsCall struct { +// MockfetcherLegacyMalfeasanceProofsCall wrap *gomock.Call +type MockfetcherLegacyMalfeasanceProofsCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockfetcherGetMalfeasanceProofsCall) Return(arg0 error) *MockfetcherGetMalfeasanceProofsCall { +func (c *MockfetcherLegacyMalfeasanceProofsCall) Return(arg0 error) *MockfetcherLegacyMalfeasanceProofsCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockfetcherGetMalfeasanceProofsCall) Do(f func(context.Context, []types.NodeID) error) *MockfetcherGetMalfeasanceProofsCall { +func (c *MockfetcherLegacyMalfeasanceProofsCall) Do(f func(context.Context, []types.NodeID) error) *MockfetcherLegacyMalfeasanceProofsCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockfetcherGetMalfeasanceProofsCall) DoAndReturn(f func(context.Context, []types.NodeID) error) *MockfetcherGetMalfeasanceProofsCall { +func (c *MockfetcherLegacyMalfeasanceProofsCall) DoAndReturn(f func(context.Context, []types.NodeID) error) *MockfetcherLegacyMalfeasanceProofsCall { c.Call = c.Call.DoAndReturn(f) return c } -// GetMaliciousIDs mocks base method. -func (m *Mockfetcher) GetMaliciousIDs(arg0 context.Context, arg1 p2p.Peer) ([]types.NodeID, error) { +// LegacyMaliciousIDs mocks base method. +func (m *Mockfetcher) LegacyMaliciousIDs(arg0 context.Context, arg1 p2p.Peer) ([]types.NodeID, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMaliciousIDs", arg0, arg1) + ret := m.ctrl.Call(m, "LegacyMaliciousIDs", arg0, arg1) ret0, _ := ret[0].([]types.NodeID) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetMaliciousIDs indicates an expected call of GetMaliciousIDs. -func (mr *MockfetcherMockRecorder) GetMaliciousIDs(arg0, arg1 any) *MockfetcherGetMaliciousIDsCall { +// LegacyMaliciousIDs indicates an expected call of LegacyMaliciousIDs. +func (mr *MockfetcherMockRecorder) LegacyMaliciousIDs(arg0, arg1 any) *MockfetcherLegacyMaliciousIDsCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaliciousIDs", reflect.TypeOf((*Mockfetcher)(nil).GetMaliciousIDs), arg0, arg1) - return &MockfetcherGetMaliciousIDsCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LegacyMaliciousIDs", reflect.TypeOf((*Mockfetcher)(nil).LegacyMaliciousIDs), arg0, arg1) + return &MockfetcherLegacyMaliciousIDsCall{Call: call} } -// MockfetcherGetMaliciousIDsCall wrap *gomock.Call -type MockfetcherGetMaliciousIDsCall struct { +// MockfetcherLegacyMaliciousIDsCall wrap *gomock.Call +type MockfetcherLegacyMaliciousIDsCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockfetcherGetMaliciousIDsCall) Return(arg0 []types.NodeID, arg1 error) *MockfetcherGetMaliciousIDsCall { +func (c *MockfetcherLegacyMaliciousIDsCall) Return(arg0 []types.NodeID, arg1 error) *MockfetcherLegacyMaliciousIDsCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *MockfetcherGetMaliciousIDsCall) Do(f func(context.Context, p2p.Peer) ([]types.NodeID, error)) *MockfetcherGetMaliciousIDsCall { +func (c *MockfetcherLegacyMaliciousIDsCall) Do(f func(context.Context, p2p.Peer) ([]types.NodeID, error)) *MockfetcherLegacyMaliciousIDsCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockfetcherGetMaliciousIDsCall) DoAndReturn(f func(context.Context, p2p.Peer) ([]types.NodeID, error)) *MockfetcherGetMaliciousIDsCall { +func (c *MockfetcherLegacyMaliciousIDsCall) DoAndReturn(f func(context.Context, p2p.Peer) ([]types.NodeID, error)) *MockfetcherLegacyMaliciousIDsCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/syncer/malsync/syncer.go b/syncer/malsync/syncer.go index a9b2605d367..c6cbf3a579a 100644 --- a/syncer/malsync/syncer.go +++ b/syncer/malsync/syncer.go @@ -25,8 +25,8 @@ import ( type fetcher interface { SelectBestShuffled(int) []p2p.Peer - GetMaliciousIDs(context.Context, p2p.Peer) ([]types.NodeID, error) - GetMalfeasanceProofs(context.Context, []types.NodeID) error + LegacyMaliciousIDs(context.Context, p2p.Peer) ([]types.NodeID, error) + LegacyMalfeasanceProofs(context.Context, []types.NodeID) error } type Opt func(*Syncer) @@ -302,7 +302,7 @@ func (s *Syncer) downloadNodeIDs(ctx context.Context, initial bool, updates chan var eg errgroup.Group for _, peer := range peers { eg.Go(func() error { - malIDs, err := s.fetcher.GetMaliciousIDs(ctx, peer) + malIDs, err := s.fetcher.LegacyMaliciousIDs(ctx, peer) if err != nil { if errors.Is(err, context.Canceled) { return nil @@ -422,7 +422,7 @@ func (s *Syncer) downloadMalfeasanceProofs(ctx context.Context, initial bool, up log.ZContext(ctx), zap.Int("count", len(batch)), ) - err := s.fetcher.GetMalfeasanceProofs(ctx, batch) + err := s.fetcher.LegacyMalfeasanceProofs(ctx, batch) if err != nil { if errors.Is(err, context.Canceled) { return ctx.Err() diff --git a/syncer/malsync/syncer_test.go b/syncer/malsync/syncer_test.go index 498cea440e9..18ab64253cd 100644 --- a/syncer/malsync/syncer_test.go +++ b/syncer/malsync/syncer_test.go @@ -178,18 +178,18 @@ func newTester(tb testing.TB, cfg Config) *tester { func (tester *tester) expectGetMaliciousIDs() { // "2" comes just from a single peer tester.fetcher.EXPECT(). - GetMaliciousIDs(gomock.Any(), tester.peers[0]). + LegacyMaliciousIDs(gomock.Any(), tester.peers[0]). Return(malData("4", "1", "3", "2"), nil) for _, p := range tester.peers[1:] { tester.fetcher.EXPECT(). - GetMaliciousIDs(gomock.Any(), p). + LegacyMaliciousIDs(gomock.Any(), p). Return(malData("4", "1", "3"), nil) } } func (tester *tester) expectGetProofs(errMap map[types.NodeID]error) { tester.fetcher.EXPECT(). - GetMalfeasanceProofs(gomock.Any(), gomock.Any()). + LegacyMalfeasanceProofs(gomock.Any(), gomock.Any()). DoAndReturn(func(_ context.Context, ids []types.NodeID) error { batchErr := &fetch.BatchError{ Errors: make(map[types.Hash32]error), @@ -245,7 +245,7 @@ func TestSyncer(t *testing.T) { tester.expectPeers(tester.peers) for _, p := range tester.peers { tester.fetcher.EXPECT(). - GetMaliciousIDs(gomock.Any(), p). + LegacyMaliciousIDs(gomock.Any(), p). Return(nil, nil) } epochStart := tester.clock.Now().Truncate(time.Second) @@ -260,10 +260,10 @@ func TestSyncer(t *testing.T) { cancel() tester.expectPeers([]p2p.Peer{"a"}) tester.fetcher.EXPECT(). - GetMaliciousIDs(gomock.Any(), gomock.Any()). + LegacyMaliciousIDs(gomock.Any(), gomock.Any()). Return(malData("1"), nil).AnyTimes() tester.fetcher.EXPECT(). - GetMalfeasanceProofs(gomock.Any(), gomock.Any()). + LegacyMalfeasanceProofs(gomock.Any(), gomock.Any()). Return(errors.New("no atxs")).AnyTimes() require.ErrorIs(t, tester.syncer.DownloadLoop(ctx), context.Canceled) }) @@ -299,11 +299,11 @@ func TestSyncer(t *testing.T) { tester := newTester(t, cfg) tester.expectPeers(tester.peers) tester.fetcher.EXPECT(). - GetMaliciousIDs(gomock.Any(), tester.peers[0]). + LegacyMaliciousIDs(gomock.Any(), tester.peers[0]). Return(nil, errors.New("fail")) for _, p := range tester.peers[1:] { tester.fetcher.EXPECT(). - GetMaliciousIDs(gomock.Any(), p). + LegacyMaliciousIDs(gomock.Any(), p). Return(malData("4", "1", "3", "2"), nil) } tester.expectGetProofs(nil) diff --git a/syncer/mocks/mocks.go b/syncer/mocks/mocks.go index 8ad4707d017..f3c7260390a 100644 --- a/syncer/mocks/mocks.go +++ b/syncer/mocks/mocks.go @@ -381,40 +381,40 @@ func (c *MockfetchLogicGetLayerOpinionsCall) DoAndReturn(f func(context.Context, return c } -// GetMalfeasanceProofs mocks base method. -func (m *MockfetchLogic) GetMalfeasanceProofs(arg0 context.Context, arg1 []types.NodeID) error { +// MalfeasanceProofs mocks base method. +func (m *MockfetchLogic) MalfeasanceProofs(arg0 context.Context, arg1 []types.NodeID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMalfeasanceProofs", arg0, arg1) + ret := m.ctrl.Call(m, "MalfeasanceProofs", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// GetMalfeasanceProofs indicates an expected call of GetMalfeasanceProofs. -func (mr *MockfetchLogicMockRecorder) GetMalfeasanceProofs(arg0, arg1 any) *MockfetchLogicGetMalfeasanceProofsCall { +// MalfeasanceProofs indicates an expected call of MalfeasanceProofs. +func (mr *MockfetchLogicMockRecorder) MalfeasanceProofs(arg0, arg1 any) *MockfetchLogicMalfeasanceProofsCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMalfeasanceProofs", reflect.TypeOf((*MockfetchLogic)(nil).GetMalfeasanceProofs), arg0, arg1) - return &MockfetchLogicGetMalfeasanceProofsCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MalfeasanceProofs", reflect.TypeOf((*MockfetchLogic)(nil).MalfeasanceProofs), arg0, arg1) + return &MockfetchLogicMalfeasanceProofsCall{Call: call} } -// MockfetchLogicGetMalfeasanceProofsCall wrap *gomock.Call -type MockfetchLogicGetMalfeasanceProofsCall struct { +// MockfetchLogicMalfeasanceProofsCall wrap *gomock.Call +type MockfetchLogicMalfeasanceProofsCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockfetchLogicGetMalfeasanceProofsCall) Return(arg0 error) *MockfetchLogicGetMalfeasanceProofsCall { +func (c *MockfetchLogicMalfeasanceProofsCall) Return(arg0 error) *MockfetchLogicMalfeasanceProofsCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockfetchLogicGetMalfeasanceProofsCall) Do(f func(context.Context, []types.NodeID) error) *MockfetchLogicGetMalfeasanceProofsCall { +func (c *MockfetchLogicMalfeasanceProofsCall) Do(f func(context.Context, []types.NodeID) error) *MockfetchLogicMalfeasanceProofsCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockfetchLogicGetMalfeasanceProofsCall) DoAndReturn(f func(context.Context, []types.NodeID) error) *MockfetchLogicGetMalfeasanceProofsCall { +func (c *MockfetchLogicMalfeasanceProofsCall) DoAndReturn(f func(context.Context, []types.NodeID) error) *MockfetchLogicMalfeasanceProofsCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -1076,40 +1076,40 @@ func (c *MockfetcherGetLayerOpinionsCall) DoAndReturn(f func(context.Context, p2 return c } -// GetMalfeasanceProofs mocks base method. -func (m *Mockfetcher) GetMalfeasanceProofs(arg0 context.Context, arg1 []types.NodeID) error { +// MalfeasanceProofs mocks base method. +func (m *Mockfetcher) MalfeasanceProofs(arg0 context.Context, arg1 []types.NodeID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMalfeasanceProofs", arg0, arg1) + ret := m.ctrl.Call(m, "MalfeasanceProofs", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// GetMalfeasanceProofs indicates an expected call of GetMalfeasanceProofs. -func (mr *MockfetcherMockRecorder) GetMalfeasanceProofs(arg0, arg1 any) *MockfetcherGetMalfeasanceProofsCall { +// MalfeasanceProofs indicates an expected call of MalfeasanceProofs. +func (mr *MockfetcherMockRecorder) MalfeasanceProofs(arg0, arg1 any) *MockfetcherMalfeasanceProofsCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMalfeasanceProofs", reflect.TypeOf((*Mockfetcher)(nil).GetMalfeasanceProofs), arg0, arg1) - return &MockfetcherGetMalfeasanceProofsCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MalfeasanceProofs", reflect.TypeOf((*Mockfetcher)(nil).MalfeasanceProofs), arg0, arg1) + return &MockfetcherMalfeasanceProofsCall{Call: call} } -// MockfetcherGetMalfeasanceProofsCall wrap *gomock.Call -type MockfetcherGetMalfeasanceProofsCall struct { +// MockfetcherMalfeasanceProofsCall wrap *gomock.Call +type MockfetcherMalfeasanceProofsCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockfetcherGetMalfeasanceProofsCall) Return(arg0 error) *MockfetcherGetMalfeasanceProofsCall { +func (c *MockfetcherMalfeasanceProofsCall) Return(arg0 error) *MockfetcherMalfeasanceProofsCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockfetcherGetMalfeasanceProofsCall) Do(f func(context.Context, []types.NodeID) error) *MockfetcherGetMalfeasanceProofsCall { +func (c *MockfetcherMalfeasanceProofsCall) Do(f func(context.Context, []types.NodeID) error) *MockfetcherMalfeasanceProofsCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockfetcherGetMalfeasanceProofsCall) DoAndReturn(f func(context.Context, []types.NodeID) error) *MockfetcherGetMalfeasanceProofsCall { +func (c *MockfetcherMalfeasanceProofsCall) DoAndReturn(f func(context.Context, []types.NodeID) error) *MockfetcherMalfeasanceProofsCall { c.Call = c.Call.DoAndReturn(f) return c } From c944eac407cf4b91dfd760673d5c9bc2587cfa56 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:03:12 +0000 Subject: [PATCH 06/56] Remove unneeded interface method --- syncer/interface.go | 1 - syncer/mocks/mocks.go | 76 ------------------------------------------- 2 files changed, 77 deletions(-) diff --git a/syncer/interface.go b/syncer/interface.go index 93bae98e423..4011354c3bb 100644 --- a/syncer/interface.go +++ b/syncer/interface.go @@ -47,7 +47,6 @@ type fetcher interface { GetCert(context.Context, types.LayerID, types.BlockID, []p2p.Peer) (*types.Certificate, error) GetAtxs(context.Context, []types.ATXID, ...system.GetAtxOpt) error - MalfeasanceProofs(context.Context, []types.NodeID) error GetBallots(context.Context, []types.BallotID) error GetBlocks(context.Context, []types.BlockID) error RegisterPeerHashes(peer p2p.Peer, hashes []types.Hash32) diff --git a/syncer/mocks/mocks.go b/syncer/mocks/mocks.go index f3c7260390a..fb40131f4a9 100644 --- a/syncer/mocks/mocks.go +++ b/syncer/mocks/mocks.go @@ -381,44 +381,6 @@ func (c *MockfetchLogicGetLayerOpinionsCall) DoAndReturn(f func(context.Context, return c } -// MalfeasanceProofs mocks base method. -func (m *MockfetchLogic) MalfeasanceProofs(arg0 context.Context, arg1 []types.NodeID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MalfeasanceProofs", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// MalfeasanceProofs indicates an expected call of MalfeasanceProofs. -func (mr *MockfetchLogicMockRecorder) MalfeasanceProofs(arg0, arg1 any) *MockfetchLogicMalfeasanceProofsCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MalfeasanceProofs", reflect.TypeOf((*MockfetchLogic)(nil).MalfeasanceProofs), arg0, arg1) - return &MockfetchLogicMalfeasanceProofsCall{Call: call} -} - -// MockfetchLogicMalfeasanceProofsCall wrap *gomock.Call -type MockfetchLogicMalfeasanceProofsCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockfetchLogicMalfeasanceProofsCall) Return(arg0 error) *MockfetchLogicMalfeasanceProofsCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockfetchLogicMalfeasanceProofsCall) Do(f func(context.Context, []types.NodeID) error) *MockfetchLogicMalfeasanceProofsCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockfetchLogicMalfeasanceProofsCall) DoAndReturn(f func(context.Context, []types.NodeID) error) *MockfetchLogicMalfeasanceProofsCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - // PeerEpochInfo mocks base method. func (m *MockfetchLogic) PeerEpochInfo(arg0 context.Context, arg1 p2p.Peer, arg2 types.EpochID) (*fetch.EpochData, error) { m.ctrl.T.Helper() @@ -1076,44 +1038,6 @@ func (c *MockfetcherGetLayerOpinionsCall) DoAndReturn(f func(context.Context, p2 return c } -// MalfeasanceProofs mocks base method. -func (m *Mockfetcher) MalfeasanceProofs(arg0 context.Context, arg1 []types.NodeID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MalfeasanceProofs", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// MalfeasanceProofs indicates an expected call of MalfeasanceProofs. -func (mr *MockfetcherMockRecorder) MalfeasanceProofs(arg0, arg1 any) *MockfetcherMalfeasanceProofsCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MalfeasanceProofs", reflect.TypeOf((*Mockfetcher)(nil).MalfeasanceProofs), arg0, arg1) - return &MockfetcherMalfeasanceProofsCall{Call: call} -} - -// MockfetcherMalfeasanceProofsCall wrap *gomock.Call -type MockfetcherMalfeasanceProofsCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockfetcherMalfeasanceProofsCall) Return(arg0 error) *MockfetcherMalfeasanceProofsCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockfetcherMalfeasanceProofsCall) Do(f func(context.Context, []types.NodeID) error) *MockfetcherMalfeasanceProofsCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockfetcherMalfeasanceProofsCall) DoAndReturn(f func(context.Context, []types.NodeID) error) *MockfetcherMalfeasanceProofsCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - // PeerEpochInfo mocks base method. func (m *Mockfetcher) PeerEpochInfo(arg0 context.Context, arg1 p2p.Peer, arg2 types.EpochID) (*fetch.EpochData, error) { m.ctrl.T.Helper() From 7610f311a7479417f76365c3ea6e5cd60f5365a6 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 11:37:04 +0000 Subject: [PATCH 07/56] Deprecate function in CachedDB --- datastore/store.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/datastore/store.go b/datastore/store.go index 2c272d8733a..0ce6eb746d9 100644 --- a/datastore/store.go +++ b/datastore/store.go @@ -20,6 +20,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/blocks" "github.com/spacemeshos/go-spacemesh/sql/builder" "github.com/spacemeshos/go-spacemesh/sql/identities" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" "github.com/spacemeshos/go-spacemesh/sql/poets" "github.com/spacemeshos/go-spacemesh/sql/transactions" ) @@ -113,7 +114,9 @@ func NewCachedDB(db sql.StateDatabase, lg *zap.Logger, opts ...Opt) *CachedDB { } } -// TODO(mafa): this needs to be removed, since it only works with v1 malfeasance proofs. +// MalfeasanceProof returns the malfeasance proof for the given node ID. This function is thread safe and will return +// an error if the proof is not found in the ATX DB. +// Deprecated: use functions in the `sql/identities` and `sql/malfeasance` packages. func (db *CachedDB) MalfeasanceProof(id types.NodeID) ([]byte, error) { if id == types.EmptyNodeID { panic("invalid argument to GetMalfeasanceProof") @@ -137,7 +140,8 @@ func (db *CachedDB) MalfeasanceProof(id types.NodeID) ([]byte, error) { return blob.Bytes, err } -// TODO(mafa): this needs to be removed, since it only works with v1 malfeasance proofs. +// CacheMalfeasanceProof caches the malfeasance proof for the given node ID. This function is thread safe. +// Deprecated: caching is done by the sql database automatically. func (db *CachedDB) CacheMalfeasanceProof(id types.NodeID, proof []byte) { if id == types.EmptyNodeID { panic("invalid argument to CacheMalfeasanceProof") @@ -188,10 +192,14 @@ func (db *CachedDB) GetAtx(id types.ATXID) (*types.ActivationTx, error) { } // Previous retrieves the list of previous ATXs for the given ATX ID. +// Deprecated: replaced by atxs.Previous. func (db *CachedDB) Previous(id types.ATXID) ([]types.ATXID, error) { return atxs.Previous(db, id) } +// IterateMalfeasanceProofs iterates over all malfeasance proofs in the database and calls the provided callback on +// each. +// Deprecated: replaced by identities.IterateOps and malfeasance.IterateOps. func (db *CachedDB) IterateMalfeasanceProofs( iter func(types.NodeID, []byte) error, ) error { @@ -206,6 +214,8 @@ func (db *CachedDB) IterateMalfeasanceProofs( return callbackErr } +// MaxHeightAtx returns the ATX ID with the maximum height. +// Deprecated: replaced by atxs.GetIDWithMaxHeight. func (db *CachedDB) MaxHeightAtx() (types.ATXID, error) { return atxs.GetIDWithMaxHeight(db, types.EmptyNodeID, atxs.FilterAll) } @@ -357,7 +367,7 @@ func (bs *BlobStore) Has(hint Hint, key []byte) (bool, error) { case LegacyMalfeasance: return identities.IsMalicious(bs.DB, types.BytesToNodeID(key)) case Malfeasance: - // TODO (mafa): implement malfeasance2 + return malfeasance.IsMalicious(bs.DB, types.BytesToNodeID(key)) case ActiveSet: return activesets.Has(bs.DB, types.BytesToHash(key)) } From f3426fef0eb9b1d3ec62ddd1dbef354f9bd395b2 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 11:45:26 +0000 Subject: [PATCH 08/56] Use statesql instead of cached DB for datastore --- datastore/store.go | 4 ++-- fetch/fetch.go | 3 ++- fetch/fetch_test.go | 15 ++++++--------- fetch/handler.go | 4 ++-- fetch/handler_test.go | 14 +++++--------- fetch/mesh_data_test.go | 6 ++---- fetch/p2p_test.go | 4 ++-- node/node.go | 2 +- .../tests/distributed_post_verification_test.go | 2 +- 9 files changed, 23 insertions(+), 31 deletions(-) diff --git a/datastore/store.go b/datastore/store.go index 0ce6eb746d9..f8ac6dc343d 100644 --- a/datastore/store.go +++ b/datastore/store.go @@ -238,13 +238,13 @@ const ( ) // NewBlobStore returns a BlobStore. -func NewBlobStore(db sql.Executor, proposals *store.Store) *BlobStore { +func NewBlobStore(db sql.StateDatabase, proposals *store.Store) *BlobStore { return &BlobStore{DB: db, proposals: proposals} } // BlobStore gets data as a blob to serve direct fetch requests. type BlobStore struct { - DB sql.Executor + DB sql.StateDatabase proposals *store.Store } diff --git a/fetch/fetch.go b/fetch/fetch.go index 87aaddfb112..ecce4bed645 100644 --- a/fetch/fetch.go +++ b/fetch/fetch.go @@ -25,6 +25,7 @@ import ( "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/server" "github.com/spacemeshos/go-spacemesh/proposals/store" + "github.com/spacemeshos/go-spacemesh/sql" ) const ( @@ -271,7 +272,7 @@ type Fetch struct { // NewFetch creates a new Fetch struct. func NewFetch( - cdb *datastore.CachedDB, + cdb sql.StateDatabase, proposals *store.Store, host *p2p.Host, peerCache *peers.Peers, diff --git a/fetch/fetch_test.go b/fetch/fetch_test.go index ab06c719b03..5b4ee38ccbe 100644 --- a/fetch/fetch_test.go +++ b/fetch/fetch_test.go @@ -94,10 +94,9 @@ func createFetch(tb testing.TB) *testFetch { } lg := zaptest.NewLogger(tb) - cdb := datastore.NewCachedDB(statesql.InMemoryTest(tb), lg) - tb.Cleanup(func() { require.NoError(tb, cdb.Close()) }) + db := statesql.InMemoryTest(tb) fetch, err := NewFetch( - cdb, + db, store.New(), nil, peers.New(), @@ -143,10 +142,9 @@ func badReceiver(context.Context, types.Hash32, p2p.Peer, []byte) error { func TestFetch_Start(t *testing.T) { lg := zaptest.NewLogger(t) - cdb := datastore.NewCachedDB(statesql.InMemoryTest(t), lg) - t.Cleanup(func() { require.NoError(t, cdb.Close()) }) + db := statesql.InMemoryTest(t) f, err := NewFetch( - cdb, + db, store.New(), nil, peers.New(), @@ -418,10 +416,9 @@ func TestFetch_PeerDroppedWhenMessageResultsInValidationReject(t *testing.T) { }) defer eg.Wait() - cdb := datastore.NewCachedDB(statesql.InMemoryTest(t), lg) - t.Cleanup(func() { require.NoError(t, cdb.Close()) }) + db := statesql.InMemoryTest(t) fetcher, err := NewFetch( - cdb, + db, store.New(), h, peers.New(), diff --git a/fetch/handler.go b/fetch/handler.go index 7a4838ee207..881b70c495b 100644 --- a/fetch/handler.go +++ b/fetch/handler.go @@ -25,12 +25,12 @@ import ( type handler struct { logger *zap.Logger - cdb *datastore.CachedDB + cdb sql.StateDatabase bs *datastore.BlobStore } func newHandler( - cdb *datastore.CachedDB, + cdb sql.StateDatabase, bs *datastore.BlobStore, lg *zap.Logger, ) *handler { diff --git a/fetch/handler_test.go b/fetch/handler_test.go index a4a280e02f2..a30ef1b5f1a 100644 --- a/fetch/handler_test.go +++ b/fetch/handler_test.go @@ -28,23 +28,19 @@ import ( type testHandler struct { *handler - db sql.StateDatabase - cdb *datastore.CachedDB + db sql.StateDatabase } func createTestHandler(tb testing.TB, opts ...sql.Opt) *testHandler { lg := zaptest.NewLogger(tb) db := statesql.InMemoryTest(tb, opts...) - cdb := datastore.NewCachedDB(db, lg) - tb.Cleanup(func() { require.NoError(tb, cdb.Close()) }) return &testHandler{ - handler: newHandler(cdb, datastore.NewBlobStore(cdb, store.New()), lg), + handler: newHandler(db, datastore.NewBlobStore(db, store.New()), lg), db: db, - cdb: cdb, } } -func createLayer(tb testing.TB, db *datastore.CachedDB, lid types.LayerID) ([]types.BallotID, []types.BlockID) { +func createLayer(tb testing.TB, db sql.StateDatabase, lid types.LayerID) ([]types.BallotID, []types.BlockID) { num := 5 blts := make([]types.BallotID, 0, num) blks := make([]types.BlockID, 0, num) @@ -69,7 +65,7 @@ func createLayer(tb testing.TB, db *datastore.CachedDB, lid types.LayerID) ([]ty func createOpinions( tb testing.TB, - db *datastore.CachedDB, + db sql.StateDatabase, lid types.LayerID, genCert bool, ) (types.BlockID, types.Hash32) { @@ -104,7 +100,7 @@ func TestHandleLayerDataReq(t *testing.T) { lid := types.LayerID(111) th := createTestHandler(t) - blts, _ := createLayer(t, th.cdb, lid) + blts, _ := createLayer(t, th.db, lid) lidBytes, err := codec.Encode(&lid) require.NoError(t, err) diff --git a/fetch/mesh_data_test.go b/fetch/mesh_data_test.go index 29a2162a918..5b8ab6bb629 100644 --- a/fetch/mesh_data_test.go +++ b/fetch/mesh_data_test.go @@ -15,7 +15,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/codec" @@ -1017,13 +1016,12 @@ func Test_GetAtxsLimiting(t *testing.T) { cfg.QueueSize = 1000 cfg.GetAtxsConcurrency = getAtxConcurrency - cdb := datastore.NewCachedDB(statesql.InMemoryTest(t), zaptest.NewLogger(t)) - t.Cleanup(func() { require.NoError(t, cdb.Close()) }) + db := statesql.InMemoryTest(t) client := server.New(wrapHost(mesh.Hosts()[0]), hashProtocol, nil) host, err := p2p.Upgrade(mesh.Hosts()[0]) require.NoError(t, err) ps := peers.New() - f, err := NewFetch(cdb, store.New(), host, + f, err := NewFetch(db, store.New(), host, ps, WithContext(context.Background()), withServers(map[string]requester{hashProtocol: client}), diff --git a/fetch/p2p_test.go b/fetch/p2p_test.go index a7cb214f2b9..920f4a71765 100644 --- a/fetch/p2p_test.go +++ b/fetch/p2p_test.go @@ -130,7 +130,7 @@ func createP2PFetch( } fetcher, err := NewFetch( - tpf.serverCDB, + tpf.serverDB, tpf.serverPDB, serverHost, peers.New(), @@ -152,7 +152,7 @@ func createP2PFetch( }, 10*time.Second, 10*time.Millisecond) fetcher, err = NewFetch( - tpf.clientCDB, + clientDB, tpf.clientPDB, clientHost, peers.New(), diff --git a/node/node.go b/node/node.go index 638c8aebdcf..1af039a4197 100644 --- a/node/node.go +++ b/node/node.go @@ -749,7 +749,7 @@ func (app *App) initServices(ctx context.Context) error { peerCache := peers.New() flog := app.addLogger(Fetcher, lg).Zap() fetcher, err := fetch.NewFetch( - app.cachedDB, + app.db, proposalsStore, app.host, peerCache, diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 93a060dec34..cf4b9b7f8df 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -121,7 +121,7 @@ func TestPostMalfeasanceProof(t *testing.T) { store.WithCapacity(cfg.Tortoise.Zdist+1), ) - fetcher, err := fetch.NewFetch(cdb, proposalsStore, host, + fetcher, err := fetch.NewFetch(db, proposalsStore, host, peers.New(), fetch.WithContext(ctx), fetch.WithConfig(cfg.FETCH), From 597dd08fa02a78a2222f2c78009917647e5850ad Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:50:42 +0000 Subject: [PATCH 09/56] Add proof function to publisher --- malfeasance2/publisher.go | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/malfeasance2/publisher.go b/malfeasance2/publisher.go index 4bd1dfaf6c3..fb9c7a623b3 100644 --- a/malfeasance2/publisher.go +++ b/malfeasance2/publisher.go @@ -194,3 +194,59 @@ func (p *Publisher) publish( return nil } + +func (p *Publisher) ProofByID(ctx context.Context, nodeID types.NodeID) ([]byte, error) { + tx, err := p.db.TxImmediate(ctx) + if err != nil { + return nil, fmt.Errorf("starting transaction: %w", err) + } + defer tx.Release() + info, err := marriage.FindByNodeID(tx, nodeID) + switch { + case errors.Is(err, sql.ErrNotFound): // smesher is not married + proof, domain, err := malfeasance.NodeIDProof(tx, nodeID) + if err != nil { + return nil, fmt.Errorf("getting malfeasance proof: %w", err) + } + atxID, err := atxs.GetFirstIDByNodeID(tx, nodeID) + if err != nil { + return nil, fmt.Errorf("getting first atx of identity %s: %w", nodeID.ShortString(), err) + } + malfeasanceProof := &MalfeasanceProof{ + Version: 0, + RefATXs: []types.ATXID{atxID}, + Domain: ProofDomain(domain), + Proof: proof, + } + return codec.MustEncode(malfeasanceProof), nil + case err != nil: + return nil, fmt.Errorf("getting equivocation set: %w", err) + default: // smesher is married + } + + set, err := marriage.NodeIDsByID(tx, info.ID) + if err != nil { + return nil, fmt.Errorf("getting equivocation set: %w", err) + } + + refATXs := make(map[types.ATXID]struct{}) + for _, id := range set { + info, err := marriage.FindByNodeID(tx, id) + if err != nil { + return nil, fmt.Errorf("getting marriage info: %w", err) + } + refATXs[info.ATX] = struct{}{} + } + + proof, domain, err := malfeasance.MarriageProof(tx, info.ID) + if err != nil { + return nil, fmt.Errorf("getting malfeasance proof: %w", err) + } + malfeasanceProof := &MalfeasanceProof{ + Version: 0, + RefATXs: maps.Keys(refATXs), + Domain: ProofDomain(domain), + Proof: proof, + } + return codec.MustEncode(malfeasanceProof), nil +} From 2ba6c7118e0057670457d17599c1c652a816ff5d Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:50:50 +0000 Subject: [PATCH 10/56] Make proof handling transactional --- malfeasance2/publisher.go | 154 +++++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 70 deletions(-) diff --git a/malfeasance2/publisher.go b/malfeasance2/publisher.go index fb9c7a623b3..3835005a7af 100644 --- a/malfeasance2/publisher.go +++ b/malfeasance2/publisher.go @@ -21,7 +21,7 @@ import ( type Publisher struct { logger *zap.Logger - db sql.Executor + db sql.StateDatabase sync syncer tortoise tortoise publisher pubsub.Publisher @@ -29,7 +29,7 @@ type Publisher struct { func NewPublisher( logger *zap.Logger, - db sql.Executor, + db sql.StateDatabase, sync syncer, tortoise tortoise, publisher pubsub.Publisher, @@ -44,77 +44,87 @@ func NewPublisher( } func (p *Publisher) PublishATXProof(ctx context.Context, nodeID types.NodeID, proof []byte) error { - marriageID, err := marriage.FindIDByNodeID(p.db, nodeID) - switch { - case errors.Is(err, sql.ErrNotFound): // smesher is not married - malicious, err := malfeasance.IsMalicious(p.db, nodeID) - if err != nil { - return fmt.Errorf("check if smesher is malicious: %w", err) - } - if malicious { - p.logger.Debug("smesher is already marked as malicious", zap.String("smesher_id", nodeID.ShortString())) + publish := false // whether to publish the proof + var set []types.NodeID + var refATXs []types.ATXID + err := p.db.WithTxImmediate(ctx, func(tx sql.Transaction) error { + marriageID, err := marriage.FindIDByNodeID(tx, nodeID) + switch { + case errors.Is(err, sql.ErrNotFound): // smesher is not married + malicious, err := malfeasance.IsMalicious(tx, nodeID) + if err != nil { + return fmt.Errorf("check if smesher is malicious: %w", err) + } + if malicious { + p.logger.Debug("smesher is already marked as malicious", zap.String("smesher_id", nodeID.ShortString())) + return nil + } + if err := malfeasance.AddProof(tx, nodeID, nil, proof, int(InvalidActivation), time.Now()); err != nil { + return fmt.Errorf("setting malfeasance proof: %w", err) + } + atxID, err := atxs.GetFirstIDByNodeID(tx, nodeID) + if err != nil { + return fmt.Errorf("getting atx id: %w", err) + } + publish = true + set = []types.NodeID{nodeID} + refATXs = []types.ATXID{atxID} return nil + case err != nil: + return fmt.Errorf("getting equivocation set: %w", err) + default: // smesher is married } - if err := malfeasance.AddProof(p.db, nodeID, nil, proof, int(InvalidActivation), time.Now()); err != nil { - return fmt.Errorf("setting malfeasance proof: %w", err) - } - atxID, err := atxs.GetFirstIDByNodeID(p.db, nodeID) - if err != nil { - return fmt.Errorf("getting atx id: %w", err) - } - p.tortoise.OnMalfeasance(nodeID) - return p.publish(ctx, []types.NodeID{nodeID}, []types.ATXID{atxID}, proof, InvalidActivation) - case err != nil: - return fmt.Errorf("getting equivocation set: %w", err) - default: // smesher is married - } - - // Combine IDs from the present equivocation set for atx.SmesherID and IDs in atx.Marriages. - set, err := marriage.NodeIDsByID(p.db, marriageID) - if err != nil { - return fmt.Errorf("getting equivocation set: %w", err) - } - publish := false // whether to publish the proof - malicious, err := malfeasance.IsMalicious(p.db, nodeID) - if err != nil { - return fmt.Errorf("check if smesher is malicious: %w", err) - } - if !malicious { - err := malfeasance.AddProof(p.db, nodeID, &marriageID, proof, int(InvalidActivation), time.Now()) + // Combine IDs from the present equivocation set for atx.SmesherID and IDs in atx.Marriages. + set, err = marriage.NodeIDsByID(tx, marriageID) if err != nil { - return fmt.Errorf("setting malfeasance proof: %w", err) + return fmt.Errorf("getting equivocation set: %w", err) } - publish = true - } else { - p.logger.Debug("smesher is already marked as malicious", zap.String("smesher_id", nodeID.ShortString())) - } - mATXs := make(map[types.ATXID]struct{}) - for _, id := range set { - info, err := marriage.FindByNodeID(p.db, id) - if err != nil { - return fmt.Errorf("getting marriage info: %w", err) - } - mATXs[info.ATX] = struct{}{} - if id == nodeID { - // already handled - continue - } - malicious, err := malfeasance.IsMalicious(p.db, id) + malicious, err := malfeasance.IsMalicious(tx, nodeID) if err != nil { return fmt.Errorf("check if smesher is malicious: %w", err) } - if malicious { - p.logger.Debug("smesher is already marked as malicious", zap.String("smesher_id", id.ShortString())) - continue + if !malicious { + err := malfeasance.AddProof(tx, nodeID, &marriageID, proof, int(InvalidActivation), time.Now()) + if err != nil { + return fmt.Errorf("setting malfeasance proof: %w", err) + } + publish = true + } else { + p.logger.Debug("smesher is already marked as malicious", zap.String("smesher_id", nodeID.ShortString())) } - publish = true - if err := malfeasance.SetMalicious(p.db, id, marriageID, time.Now()); err != nil { - return fmt.Errorf("setting malicious: %w", err) + + mATXs := make(map[types.ATXID]struct{}) + for _, id := range set { + info, err := marriage.FindByNodeID(tx, id) + if err != nil { + return fmt.Errorf("getting marriage info: %w", err) + } + mATXs[info.ATX] = struct{}{} + if id == nodeID { + // already handled + continue + } + malicious, err := malfeasance.IsMalicious(tx, id) + if err != nil { + return fmt.Errorf("check if smesher is malicious: %w", err) + } + if malicious { + p.logger.Debug("smesher is already marked as malicious", zap.String("smesher_id", id.ShortString())) + continue + } + publish = true + if err := malfeasance.SetMalicious(tx, id, marriageID, time.Now()); err != nil { + return fmt.Errorf("setting malicious: %w", err) + } } + refATXs = maps.Keys(mATXs) + return nil + }) + if err != nil { + return err } - if !publish { // all smeshers were already marked as malicious - no gossip to void spamming the network return nil @@ -122,20 +132,25 @@ func (p *Publisher) PublishATXProof(ctx context.Context, nodeID types.NodeID, pr for _, nodeID := range set { p.tortoise.OnMalfeasance(nodeID) } - return p.publish(ctx, set, maps.Keys(mATXs), proof, ProofDomain(InvalidActivation)) + return p.publish(ctx, set, refATXs, proof, ProofDomain(InvalidActivation)) } func (p *Publisher) Regossip(ctx context.Context, nodeID types.NodeID) error { - marriageID, err := marriage.FindIDByNodeID(p.db, nodeID) + tx, err := p.db.TxImmediate(ctx) + if err != nil { + return fmt.Errorf("starting transaction: %w", err) + } + defer tx.Release() + marriageID, err := marriage.FindIDByNodeID(tx, nodeID) switch { case errors.Is(err, sql.ErrNotFound): // smesher is not married - proof, domain, err := malfeasance.NodeIDProof(p.db, nodeID) + proof, domain, err := malfeasance.NodeIDProof(tx, nodeID) if err != nil { return fmt.Errorf("getting malfeasance proof: %w", err) } - atxID, err := atxs.GetFirstIDByNodeID(p.db, nodeID) + atxID, err := atxs.GetFirstIDByNodeID(tx, nodeID) if err != nil { - return fmt.Errorf("getting atx id: %w", err) + return fmt.Errorf("getting first atx of identity %s: %w", nodeID.ShortString(), err) } return p.publish(ctx, []types.NodeID{nodeID}, []types.ATXID{atxID}, proof, ProofDomain(domain)) case err != nil: @@ -143,17 +158,17 @@ func (p *Publisher) Regossip(ctx context.Context, nodeID types.NodeID) error { default: // smesher is married } - proof, domain, err := malfeasance.MarriageProof(p.db, marriageID) + proof, domain, err := malfeasance.MarriageProof(tx, marriageID) if err != nil { return fmt.Errorf("getting malfeasance proof: %w", err) } - nodeIDs, err := marriage.NodeIDsByID(p.db, marriageID) + nodeIDs, err := marriage.NodeIDsByID(tx, marriageID) if err != nil { return fmt.Errorf("getting equivocation set: %w", err) } - atxs, err := marriage.MarriageATXs(p.db, marriageID) + atxs, err := marriage.MarriageATXs(tx, marriageID) if err != nil { return fmt.Errorf("getting equivocation info: %w", err) } @@ -191,7 +206,6 @@ func (p *Publisher) publish( p.logger.Error("failed to broadcast malfeasance proof", zap.Error(err)) return fmt.Errorf("broadcast atx malfeasance proof: %w", err) } - return nil } From baea754b35ef8182e5466fd20c3cfdfe195e0797 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 13:11:10 +0000 Subject: [PATCH 11/56] Add Malfeasance2 to datastore package --- datastore/mocks.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++ datastore/store.go | 80 +++++++++++++++++++++++++++++++++++++-------- fetch/fetch.go | 9 ++++++ node/node.go | 3 +- 4 files changed, 159 insertions(+), 14 deletions(-) create mode 100644 datastore/mocks.go diff --git a/datastore/mocks.go b/datastore/mocks.go new file mode 100644 index 00000000000..9d85e67ddfa --- /dev/null +++ b/datastore/mocks.go @@ -0,0 +1,81 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./store.go +// +// Generated by this command: +// +// mockgen -typed -package=datastore -destination=./mocks.go -source=./store.go +// + +// Package datastore is a generated GoMock package. +package datastore + +import ( + context "context" + reflect "reflect" + + types "github.com/spacemeshos/go-spacemesh/common/types" + gomock "go.uber.org/mock/gomock" +) + +// MockmalfeasanceProvider is a mock of malfeasanceProvider interface. +type MockmalfeasanceProvider struct { + ctrl *gomock.Controller + recorder *MockmalfeasanceProviderMockRecorder + isgomock struct{} +} + +// MockmalfeasanceProviderMockRecorder is the mock recorder for MockmalfeasanceProvider. +type MockmalfeasanceProviderMockRecorder struct { + mock *MockmalfeasanceProvider +} + +// NewMockmalfeasanceProvider creates a new mock instance. +func NewMockmalfeasanceProvider(ctrl *gomock.Controller) *MockmalfeasanceProvider { + mock := &MockmalfeasanceProvider{ctrl: ctrl} + mock.recorder = &MockmalfeasanceProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockmalfeasanceProvider) EXPECT() *MockmalfeasanceProviderMockRecorder { + return m.recorder +} + +// ProofByID mocks base method. +func (m *MockmalfeasanceProvider) ProofByID(ctx context.Context, nodeID types.NodeID) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProofByID", ctx, nodeID) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProofByID indicates an expected call of ProofByID. +func (mr *MockmalfeasanceProviderMockRecorder) ProofByID(ctx, nodeID any) *MockmalfeasanceProviderProofByIDCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProofByID", reflect.TypeOf((*MockmalfeasanceProvider)(nil).ProofByID), ctx, nodeID) + return &MockmalfeasanceProviderProofByIDCall{Call: call} +} + +// MockmalfeasanceProviderProofByIDCall wrap *gomock.Call +type MockmalfeasanceProviderProofByIDCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockmalfeasanceProviderProofByIDCall) Return(arg0 []byte, arg1 error) *MockmalfeasanceProviderProofByIDCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockmalfeasanceProviderProofByIDCall) Do(f func(context.Context, types.NodeID) ([]byte, error)) *MockmalfeasanceProviderProofByIDCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockmalfeasanceProviderProofByIDCall) DoAndReturn(f func(context.Context, types.NodeID) ([]byte, error)) *MockmalfeasanceProviderProofByIDCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/datastore/store.go b/datastore/store.go index f8ac6dc343d..1620c188e6d 100644 --- a/datastore/store.go +++ b/datastore/store.go @@ -239,13 +239,32 @@ const ( // NewBlobStore returns a BlobStore. func NewBlobStore(db sql.StateDatabase, proposals *store.Store) *BlobStore { - return &BlobStore{DB: db, proposals: proposals} + return &BlobStore{ + DB: db, + proposals: proposals, + } +} + +// SetMalfeasanceProvider sets the malfeasance provider dependency. +// +// TODO(mafa): this is a hack because of a cyclic dependency between the packages +// +// malfeasance2 -> fetcher -> datastore -> malfeasance2 +func (bs *BlobStore) SetMalfeasanceProvider(p MalfeasanceProvider) { + bs.malfeasance = p +} + +//go:generate mockgen -typed -package=datastore -destination=./mocks.go -source=./store.go + +type MalfeasanceProvider interface { + ProofByID(ctx context.Context, nodeID types.NodeID) ([]byte, error) } // BlobStore gets data as a blob to serve direct fetch requests. type BlobStore struct { - DB sql.StateDatabase - proposals *store.Store + DB sql.StateDatabase + proposals *store.Store + malfeasance MalfeasanceProvider } type ( @@ -263,9 +282,7 @@ var loadBlobDispatch = map[Hint]loadBlobFunc{ TXDB: transactions.LoadBlob, POETDB: poets.LoadBlob, LegacyMalfeasance: identities.LoadMalfeasanceBlob, - // TODO(mafa): implement malfeasance2 - // Malfeasance: malfeasance.LoadBlob, - ActiveSet: activesets.LoadBlob, + ActiveSet: activesets.LoadBlob, } var blobSizeDispatch = map[Hint]blobSizeFunc{ @@ -275,9 +292,7 @@ var blobSizeDispatch = map[Hint]blobSizeFunc{ TXDB: transactions.GetBlobSizes, POETDB: poets.GetBlobSizes, LegacyMalfeasance: identities.GetBlobSizes, - // TODO(mafa): implement malfeasance2 - // Malfeasance: malfeasance.BlobSizes, - ActiveSet: activesets.GetBlobSizes, + ActiveSet: activesets.GetBlobSizes, } func (bs *BlobStore) loadProposal(key []byte, blob *sql.Blob) error { @@ -294,7 +309,7 @@ func (bs *BlobStore) loadProposal(key []byte, blob *sql.Blob) error { } } -func (bs *BlobStore) getProposalSizes(keys [][]byte) (sizes []int, err error) { +func (bs *BlobStore) proposalSizes(keys [][]byte) (sizes []int, err error) { sizes = make([]int, len(keys)) for n, k := range keys { id := types.ProposalID(types.BytesToHash(k).ToHash20()) @@ -311,10 +326,45 @@ func (bs *BlobStore) getProposalSizes(keys [][]byte) (sizes []int, err error) { return sizes, err } +func (bs *BlobStore) loadMalfeasance(key []byte, blob *sql.Blob) error { + id := types.BytesToNodeID(key) + b, err := bs.malfeasance.ProofByID(context.Background(), id) + switch { + case err == nil: + blob.Bytes = b + return nil + case errors.Is(err, sql.ErrNotFound): + return ErrNotFound + default: + return err + } +} + +func (bs *BlobStore) malfeasanceSizes(keys [][]byte) (sizes []int, err error) { + sizes = make([]int, len(keys)) + for n, k := range keys { + id := types.NodeID(k) + b, err := bs.malfeasance.ProofByID(context.Background(), id) + switch { + case err == nil: + sizes[n] = len(b) + case errors.Is(err, store.ErrNotFound): + sizes[n] = -1 + default: + return nil, err + } + } + return sizes, err +} + // LoadBlob gets an blob as bytes by an object ID as bytes. func (bs *BlobStore) LoadBlob(ctx context.Context, hint Hint, key []byte, blob *sql.Blob) error { - if hint == ProposalDB { + switch hint { + case ProposalDB: return bs.loadProposal(key, blob) + case Malfeasance: + return bs.loadMalfeasance(key, blob) + default: } loader, found := loadBlobDispatch[hint] if !found { @@ -334,8 +384,12 @@ func (bs *BlobStore) LoadBlob(ctx context.Context, hint Hint, key []byte, blob * // GetBlobSizes returns the sizes of the blobs corresponding to the specified ids. For // non-existent objects, the corresponding items are set to -1. func (bs *BlobStore) GetBlobSizes(hint Hint, ids [][]byte) (sizes []int, err error) { - if hint == ProposalDB { - return bs.getProposalSizes(ids) + switch hint { + case ProposalDB: + return bs.proposalSizes(ids) + case Malfeasance: + return bs.malfeasanceSizes(ids) + default: } getSizes, found := blobSizeDispatch[hint] if !found { diff --git a/fetch/fetch.go b/fetch/fetch.go index ecce4bed645..c7eff895c98 100644 --- a/fetch/fetch.go +++ b/fetch/fetch.go @@ -412,6 +412,15 @@ type dataValidators struct { malfeasance SyncValidator } +// SetMalfeasanceProvider sets the malfeasance provider dependency. +// +// TODO(mafa): this is a hack because of a cyclic dependency between the packages +// +// malfeasance2 -> fetcher -> datastore -> malfeasance2 +func (f *Fetch) SetMalfeasanceProvider(p datastore.MalfeasanceProvider) { + f.bs.SetMalfeasanceProvider(p) +} + // SetValidators sets the handlers to validate various mesh data fetched from peers. func (f *Fetch) SetValidators( atx SyncValidator, diff --git a/node/node.go b/node/node.go index 1af039a4197..171fb4eb2f8 100644 --- a/node/node.go +++ b/node/node.go @@ -852,7 +852,7 @@ func (app *App) initServices(ctx context.Context) error { malfeasanceLogger := app.addLogger(Malfeasance2Logger, lg).Zap() malfeasancePublisher := malfeasance2.NewPublisher( malfeasanceLogger, - app.cachedDB, + app.db, syncer, trtl, app.host, @@ -1206,6 +1206,7 @@ func (app *App) initServices(ctx context.Context) error { ) malHandler2.RegisterHandler(malfeasance2.InvalidActivation, atxMalHandler) + fetcher.SetMalfeasanceProvider(malfeasancePublisher) fetcher.SetValidators( fetch.ValidatorFunc( pubsub.DropPeerOnSyncValidationReject(atxHandler.HandleSyncedAtx, app.host, lg.Zap()), From a936e10a69af59cda4091ac84050a564328b3104 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 15:31:01 +0000 Subject: [PATCH 12/56] Add more tests --- malfeasance2/publisher.go | 19 ++--- malfeasance2/publisher_test.go | 141 +++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 14 deletions(-) diff --git a/malfeasance2/publisher.go b/malfeasance2/publisher.go index 3835005a7af..86a61a52ab6 100644 --- a/malfeasance2/publisher.go +++ b/malfeasance2/publisher.go @@ -215,7 +215,7 @@ func (p *Publisher) ProofByID(ctx context.Context, nodeID types.NodeID) ([]byte, return nil, fmt.Errorf("starting transaction: %w", err) } defer tx.Release() - info, err := marriage.FindByNodeID(tx, nodeID) + mID, err := marriage.FindIDByNodeID(tx, nodeID) switch { case errors.Is(err, sql.ErrNotFound): // smesher is not married proof, domain, err := malfeasance.NodeIDProof(tx, nodeID) @@ -238,27 +238,18 @@ func (p *Publisher) ProofByID(ctx context.Context, nodeID types.NodeID) ([]byte, default: // smesher is married } - set, err := marriage.NodeIDsByID(tx, info.ID) + atxs, err := marriage.MarriageATXs(tx, mID) if err != nil { - return nil, fmt.Errorf("getting equivocation set: %w", err) - } - - refATXs := make(map[types.ATXID]struct{}) - for _, id := range set { - info, err := marriage.FindByNodeID(tx, id) - if err != nil { - return nil, fmt.Errorf("getting marriage info: %w", err) - } - refATXs[info.ATX] = struct{}{} + return nil, fmt.Errorf("getting equivocation info: %w", err) } - proof, domain, err := malfeasance.MarriageProof(tx, info.ID) + proof, domain, err := malfeasance.MarriageProof(tx, mID) if err != nil { return nil, fmt.Errorf("getting malfeasance proof: %w", err) } malfeasanceProof := &MalfeasanceProof{ Version: 0, - RefATXs: maps.Keys(refATXs), + RefATXs: atxs, Domain: ProofDomain(domain), Proof: proof, } diff --git a/malfeasance2/publisher_test.go b/malfeasance2/publisher_test.go index 0353250a5cb..770b011be32 100644 --- a/malfeasance2/publisher_test.go +++ b/malfeasance2/publisher_test.go @@ -627,3 +627,144 @@ func TestRegossip(t *testing.T) { require.NoError(t, err) }) } + +func TestProofByID(t *testing.T) { + t.Run("not married no proof", func(t *testing.T) { + t.Parallel() + tp := newTestPublisher(t) + nodeID := types.RandomNodeID() + + proofBytes, err := tp.ProofByID(context.Background(), nodeID) + require.ErrorIs(t, err, sql.ErrNotFound) + require.Nil(t, proofBytes) + }) + + t.Run("not married with proof", func(t *testing.T) { + t.Parallel() + tp := newTestPublisher(t) + nodeID := types.RandomNodeID() + atx := types.ActivationTx{ + SmesherID: nodeID, + } + atx.SetID(types.RandomATXID()) + atxs.Add(tp.db, &atx, types.AtxBlob{}) + proof := types.RandomBytes(10) + err := malfeasance.AddProof(tp.db, nodeID, nil, proof, int(malfeasance2.InvalidActivation), time.Now()) + require.NoError(t, err) + + proofBytes, err := tp.ProofByID(context.Background(), nodeID) + require.NoError(t, err) + + var malProof malfeasance2.MalfeasanceProof + require.NoError(t, codec.Decode(proofBytes, &malProof)) + require.Equal(t, proof, malProof.Proof) + require.Equal(t, malfeasance2.InvalidActivation, malProof.Domain) + require.Len(t, malProof.RefATXs, 1) + require.Equal(t, atx.ID(), malProof.RefATXs[0]) + }) + + t.Run("married no proof", func(t *testing.T) { + t.Parallel() + tp := newTestPublisher(t) + nodeIDs := make([]types.NodeID, 20) + for i := range nodeIDs { + nodeIDs[i] = types.RandomNodeID() + } + mATXID := types.RandomATXID() + atx := &types.ActivationTx{ + SmesherID: nodeIDs[0], + } + atx.SetID(mATXID) + require.NoError(t, atxs.Add(tp.db, atx, types.AtxBlob{})) + mATX2ID := types.RandomATXID() + atx2 := &types.ActivationTx{ + SmesherID: nodeIDs[0], + } + atx2.SetID(mATX2ID) + require.NoError(t, atxs.Add(tp.db, atx2, types.AtxBlob{})) + + mID, err := marriage.NewID(tp.db) + require.NoError(t, err) + + for i := range nodeIDs { + require.NoError(t, marriage.Add(tp.db, marriage.Info{ + ID: mID, + NodeID: nodeIDs[i], + ATX: mATXID, + MarriageIndex: i, + Target: nodeIDs[0], + Signature: types.RandomEdSignature(), + })) + } + + proofBytes, err := tp.ProofByID(context.Background(), nodeIDs[4]) + require.ErrorIs(t, err, sql.ErrNotFound) + require.Nil(t, proofBytes) + }) + + t.Run("married with proof", func(t *testing.T) { + t.Parallel() + tp := newTestPublisher(t) + proof := types.RandomBytes(10) + nodeIDs := make([]types.NodeID, 20) + for i := range nodeIDs { + nodeIDs[i] = types.RandomNodeID() + } + atx := &types.ActivationTx{ + SmesherID: nodeIDs[0], + } + atx.SetID(types.RandomATXID()) + require.NoError(t, atxs.Add(tp.db, atx, types.AtxBlob{})) + atx2 := &types.ActivationTx{ + SmesherID: nodeIDs[0], + } + atx2.SetID(types.RandomATXID()) + require.NoError(t, atxs.Add(tp.db, atx2, types.AtxBlob{})) + + mID, err := marriage.NewID(tp.db) + require.NoError(t, err) + + for i := range nodeIDs[:10] { + require.NoError(t, marriage.Add(tp.db, marriage.Info{ + ID: mID, + NodeID: nodeIDs[i], + ATX: atx.ID(), + MarriageIndex: i, + Target: nodeIDs[0], + Signature: types.RandomEdSignature(), + })) + if i == 0 { + require.NoError(t, malfeasance.AddProof( + tp.db, + nodeIDs[i], + &mID, + proof, + int(malfeasance2.InvalidActivation), + time.Now()), + ) + continue + } + require.NoError(t, malfeasance.SetMalicious(tp.db, nodeIDs[i], mID, time.Now())) + } + for i := range nodeIDs[10:] { // smesher has married twice + require.NoError(t, marriage.Add(tp.db, marriage.Info{ + ID: mID, + NodeID: nodeIDs[i+10], + ATX: atx2.ID(), + MarriageIndex: i, + Target: nodeIDs[0], + Signature: types.RandomEdSignature(), + })) + require.NoError(t, malfeasance.SetMalicious(tp.db, nodeIDs[i], mID, time.Now())) + } + + proofBytes, err := tp.ProofByID(context.Background(), nodeIDs[4]) + require.NoError(t, err) + + var malProof malfeasance2.MalfeasanceProof + require.NoError(t, codec.Decode(proofBytes, &malProof)) + require.Equal(t, proof, malProof.Proof) + require.Equal(t, malfeasance2.InvalidActivation, malProof.Domain) + require.ElementsMatch(t, []types.ATXID{atx.ID(), atx2.ID()}, malProof.RefATXs) + }) +} From fdb4571d3e895b2afc69f21e69b291c694c32d94 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:50:14 +0000 Subject: [PATCH 13/56] Fix linter message --- fetch/p2p_test.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/fetch/p2p_test.go b/fetch/p2p_test.go index 920f4a71765..96b58923668 100644 --- a/fetch/p2p_test.go +++ b/fetch/p2p_test.go @@ -42,7 +42,7 @@ type testP2PFetch struct { tb testing.TB // client proposals clientPDB *store.Store - clientCDB *datastore.CachedDB + clientDB sql.StateDatabase clientFetch *Fetch serverID peer.ID serverDB sql.StateDatabase @@ -113,15 +113,13 @@ func createP2PFetch( sqlOpts = []sql.Opt{sql.WithQueryCache(true)} } clientDB := statesql.InMemoryTest(tb, sqlOpts...) - clientCDB := datastore.NewCachedDB(clientDB, lg) - tb.Cleanup(func() { assert.NoError(tb, clientDB.Close()) }) serverDB := statesql.InMemoryTest(tb, sqlOpts...) serverCDB := datastore.NewCachedDB(serverDB, lg) tb.Cleanup(func() { assert.NoError(tb, serverDB.Close()) }) tpf := &testP2PFetch{ tb: tb, clientPDB: store.New(store.WithLogger(lg)), - clientCDB: clientCDB, + clientDB: clientDB, serverID: serverHost.ID(), serverDB: serverDB, serverPDB: store.New(store.WithLogger(lg)), @@ -152,7 +150,7 @@ func createP2PFetch( }, 10*time.Second, 10*time.Millisecond) fetcher, err = NewFetch( - clientDB, + tpf.clientDB, tpf.clientPDB, clientHost, peers.New(), From 21f9a2f78a0a7efc18e210ae262a12fae9dd5e3b Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:30:08 +0000 Subject: [PATCH 14/56] Update and integrate new fetcher --- fetch/fetch.go | 4 +- fetch/handler.go | 66 ++++++++++++++++++++++++++--- fetch/mesh_data.go | 22 +++++----- fetch/mesh_data_test.go | 19 ++++++++- fetch/p2p_test.go | 50 ++++++++++++++++++---- sql/malfeasance/malfeasance.go | 15 +++++++ sql/malfeasance/malfeasance_test.go | 47 ++++++++++++++++++++ syncer/malsync/syncer.go | 58 ++++++++++++------------- 8 files changed, 223 insertions(+), 58 deletions(-) diff --git a/fetch/fetch.go b/fetch/fetch.go index c7eff895c98..51f95890f48 100644 --- a/fetch/fetch.go +++ b/fetch/fetch.go @@ -362,7 +362,7 @@ func NewFetch( }) f.registerServer(host, meshHashProtocol, h.handleMeshHashReqStream) f.registerServer(host, legacyMalProtocol, h.handleLegacyMaliciousIDsReqStream) - // f.registerServer(host, malProtocol, h.handleMaliciousIDsReqStream) + f.registerServer(host, malProtocol, h.handleMaliciousIDsReqStream) } else { f.registerServer(host, atxProtocol, server.WrapHandler(h.handleEpochInfoReq)) f.registerServer(host, hashProtocol, server.WrapHandler(h.handleHashReq)) @@ -373,7 +373,7 @@ func NewFetch( })) f.registerServer(host, meshHashProtocol, server.WrapHandler(h.handleMeshHashReq)) f.registerServer(host, legacyMalProtocol, server.WrapHandler(h.handleLegacyMaliciousIDsReq)) - // f.registerServer(host, malProtocol, server.WrapHandler(h.handleMaliciousIDsReq)) + f.registerServer(host, malProtocol, server.WrapHandler(h.handleMaliciousIDsReq)) } f.registerServer(host, lyrDataProtocol, server.WrapHandler(h.handleLayerDataReq)) f.registerServer(host, OpnProtocol, server.WrapHandler(h.handleLayerOpinionsReq2)) diff --git a/fetch/handler.go b/fetch/handler.go index 881b70c495b..89a6de4c9f1 100644 --- a/fetch/handler.go +++ b/fetch/handler.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "time" "github.com/spacemeshos/go-scale" "go.uber.org/zap" @@ -18,9 +19,11 @@ import ( "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" "github.com/spacemeshos/go-spacemesh/sql/ballots" + "github.com/spacemeshos/go-spacemesh/sql/builder" "github.com/spacemeshos/go-spacemesh/sql/certificates" "github.com/spacemeshos/go-spacemesh/sql/identities" "github.com/spacemeshos/go-spacemesh/sql/layers" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" ) type handler struct { @@ -43,31 +46,82 @@ func newHandler( // handleLegacyMaliciousIDsReq returns the IDs of all known malicious nodes. func (h *handler) handleLegacyMaliciousIDsReq(ctx context.Context, _ p2p.Peer, _ []byte) ([]byte, error) { - nodes, err := identities.AllMalicious(h.cdb) + nodeIDs, err := identities.AllMalicious(h.cdb) if err != nil { return nil, fmt.Errorf("getting malicious IDs: %w", err) } - h.logger.Debug("responded to malicious IDs request", log.ZContext(ctx), zap.Int("num_malicious", len(nodes))) + h.logger.Debug("responded to malicious IDs request", log.ZContext(ctx), zap.Int("num_malicious", len(nodeIDs))) malicious := &MaliciousIDs{ - NodeIDs: nodes, + NodeIDs: nodeIDs, } return codec.MustEncode(malicious), nil } -func (h *handler) handleLegacyMaliciousIDsReqStream(ctx context.Context, _ p2p.Peer, msg []byte, s io.ReadWriter) error { +// handleMaliciousIDsReq returns the IDs of all known malicious nodes. +func (h *handler) handleMaliciousIDsReq(ctx context.Context, _ p2p.Peer, _ []byte) ([]byte, error) { + tx, err := h.cdb.TxImmediate(ctx) + if err != nil { + return nil, fmt.Errorf("starting transaction: %w", err) + } + defer tx.Release() + total, err := malfeasance.Count(tx) + if err != nil { + return nil, fmt.Errorf("counting malicious nodes: %w", err) + } + nodeIDs := make([]types.NodeID, 0, total) + err = malfeasance.IterateOps(h.cdb, builder.Operations{}, + func(nodeID types.NodeID, _ []byte, _ int, _ time.Time) bool { + nodeIDs = append(nodeIDs, nodeID) + return true + }) + if err != nil { + return nil, fmt.Errorf("getting malicious IDs: %w", err) + } + h.logger.Debug("responded to malicious IDs request", log.ZContext(ctx), zap.Int("num_malicious", len(nodeIDs))) + malicious := &MaliciousIDs{ + NodeIDs: nodeIDs, + } + return codec.MustEncode(malicious), nil +} + +func (h *handler) handleLegacyMaliciousIDsReqStream(ctx context.Context, _ p2p.Peer, _ []byte, s io.ReadWriter) error { if err := h.streamIDs(ctx, s, func(cbk retrieveCallback) error { nodeIDs, err := identities.AllMalicious(h.cdb) if err != nil { return fmt.Errorf("getting malicious IDs: %w", err) } for _, nodeID := range nodeIDs { - cbk(len(nodeIDs), nodeID[:]) + cbk(len(nodeIDs), nodeID.Bytes()) } return nil }); err != nil { h.logger.Debug("failed to stream malicious node IDs", log.ZContext(ctx), zap.Error(err)) } + return nil +} +func (h *handler) handleMaliciousIDsReqStream(ctx context.Context, _ p2p.Peer, _ []byte, s io.ReadWriter) error { + tx, err := h.cdb.TxImmediate(ctx) + if err != nil { + return fmt.Errorf("starting transaction: %w", err) + } + defer tx.Release() + total, err := malfeasance.Count(tx) + if err != nil { + return fmt.Errorf("counting malicious nodes: %w", err) + } + if err := h.streamIDs(ctx, s, func(cbk retrieveCallback) error { + return malfeasance.IterateOps(tx, builder.Operations{}, + func(nodeID types.NodeID, _ []byte, _ int, _ time.Time) bool { + if err := cbk(total, nodeID.Bytes()); err != nil { + h.logger.Debug("failed to stream malicious node IDs", log.ZContext(ctx), zap.Error(err)) + return false + } + return true + }) + }); err != nil { + h.logger.Debug("failed to stream malicious node IDs", log.ZContext(ctx), zap.Error(err)) + } return nil } @@ -140,7 +194,7 @@ func (h *handler) streamIDs(ctx context.Context, s io.ReadWriter, retrieve retri return err } } - if _, err := s.Write(id[:]); err != nil { + if _, err := s.Write(id); err != nil { return err } return nil diff --git a/fetch/mesh_data.go b/fetch/mesh_data.go index 57e714d449b..e8fe5e10a25 100644 --- a/fetch/mesh_data.go +++ b/fetch/mesh_data.go @@ -174,17 +174,17 @@ func (f *Fetch) LegacyMalfeasanceProofs(ctx context.Context, ids []types.NodeID) } // MalfeasanceProofs gets malfeasance proofs (v2) for the specified NodeIDs and validates them. -// func (f *Fetch) MalfeasanceProofs(ctx context.Context, ids []types.NodeID) error { -// if len(ids) == 0 { -// return nil -// } -// f.logger.Debug("requesting malfeasance proofs from peers", -// log.ZContext(ctx), -// zap.Int("num_proofs", len(ids)), -// ) -// hashes := types.NodeIDsToHashes(ids) -// return f.getHashes(ctx, hashes, datastore.Malfeasance, f.validators.malfeasance.HandleMessage) -// } +func (f *Fetch) MalfeasanceProofs(ctx context.Context, ids []types.NodeID) error { + if len(ids) == 0 { + return nil + } + f.logger.Debug("requesting malfeasance proofs from peers", + log.ZContext(ctx), + zap.Int("num_proofs", len(ids)), + ) + hashes := types.NodeIDsToHashes(ids) + return f.getHashes(ctx, hashes, datastore.Malfeasance, f.validators.malfeasance.HandleMessage) +} // GetBallots gets data for the specified BallotIDs and validates them. func (f *Fetch) GetBallots(ctx context.Context, ids []types.BallotID) error { diff --git a/fetch/mesh_data_test.go b/fetch/mesh_data_test.go index 5b8ab6bb629..124cce5c8ca 100644 --- a/fetch/mesh_data_test.go +++ b/fetch/mesh_data_test.go @@ -374,7 +374,7 @@ func TestFetch_getHashesStreaming(t *testing.T) { }) } -func TestFetch_GetMalfeasanceProofs(t *testing.T) { +func TestFetch_LegacyMalfeasanceProofs(t *testing.T) { nodeIDs := []types.NodeID{{1}, {2}, {3}} f := createFetch(t) f.mLegacyMalH.EXPECT(). @@ -391,6 +391,23 @@ func TestFetch_GetMalfeasanceProofs(t *testing.T) { require.NoError(t, eg.Wait()) } +func TestFetch_MalfeasanceProofs(t *testing.T) { + nodeIDs := []types.NodeID{{1}, {2}, {3}} + f := createFetch(t) + f.mMalH.EXPECT(). + HandleMessage(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil). + Times(len(nodeIDs)) + + stop := make(chan struct{}, 1) + var eg errgroup.Group + startTestLoop(t, f.Fetch, &eg, stop) + + require.NoError(t, f.MalfeasanceProofs(context.Background(), nodeIDs)) + close(stop) + require.NoError(t, eg.Wait()) +} + func TestFetch_GetBlocks(t *testing.T) { blks := []*types.Block{ genLayerBlock(types.LayerID(10), types.RandomTXSet(10)), diff --git a/fetch/p2p_test.go b/fetch/p2p_test.go index 96b58923668..29caf5de57d 100644 --- a/fetch/p2p_test.go +++ b/fetch/p2p_test.go @@ -27,6 +27,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/blocks" "github.com/spacemeshos/go-spacemesh/sql/identities" "github.com/spacemeshos/go-spacemesh/sql/layers" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" "github.com/spacemeshos/go-spacemesh/sql/poets" "github.com/spacemeshos/go-spacemesh/sql/statesql" "github.com/spacemeshos/go-spacemesh/sql/transactions" @@ -47,9 +48,12 @@ type testP2PFetch struct { serverID peer.ID serverDB sql.StateDatabase // server proposals - serverPDB *store.Store - serverCDB *datastore.CachedDB - serverFetch *Fetch + serverPDB *store.Store + serverCDB *datastore.CachedDB + serverFetch *Fetch + + malProvider *malProvider + recvMtx sync.Mutex receivedData map[blobKey][]byte } @@ -83,6 +87,17 @@ func p2pCfg(tb testing.TB) p2p.Config { return p2pconf } +type malProvider struct { + db sql.StateDatabase +} + +func (m *malProvider) ProofByID(ctx context.Context, nodeID types.NodeID) ([]byte, error) { + // this is an incomplete implementation, the proof returned here normally needs to be wrapped into a + // malfeasance2.MalfeasanceProof struct before being returned to the fetcher, but for the test it is sufficient + proof, _, err := malfeasance.NodeIDProof(m.db, nodeID) + return proof, err +} + func createP2PFetch( tb testing.TB, clientStreaming, @@ -124,6 +139,7 @@ func createP2PFetch( serverDB: serverDB, serverPDB: store.New(store.WithLogger(lg)), serverCDB: serverCDB, + malProvider: &malProvider{db: serverDB}, receivedData: make(map[blobKey][]byte), } @@ -141,6 +157,7 @@ func createP2PFetch( vf := ValidatorFunc( func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }, ) + tpf.serverFetch.SetMalfeasanceProvider(tpf.malProvider) tpf.serverFetch.SetValidators(vf, vf, vf, vf, vf, vf, vf, vf, vf, vf) require.NoError(tb, tpf.serverFetch.Start()) tb.Cleanup(tpf.serverFetch.Stop) @@ -521,18 +538,35 @@ func TestP2PGetProposalTransactions(t *testing.T) { }) } -func TestP2PGetMalfeasanceProofs(t *testing.T) { +func TestP2PLegacyMalfeasanceProofs(t *testing.T) { + forStreaming( + t, "database closed", false, + func(t *testing.T, ctx context.Context, tpf *testP2PFetch, errStr string) { + nodeID := types.RandomNodeID() + proof := types.RandomBytes(11) + require.NoError(t, identities.SetMalicious(tpf.serverCDB, nodeID, proof, time.Now())) + tpf.verifyGetHash( + func() error { + return tpf.clientFetch.LegacyMalfeasanceProofs(context.Background(), []types.NodeID{nodeID}) + }, + errStr, "mal", "hs/1", types.Hash32(nodeID), nodeID.Bytes(), + proof, + ) + }) +} + +func TestP2PMalfeasanceProofs(t *testing.T) { forStreaming( t, "database closed", false, func(t *testing.T, ctx context.Context, tpf *testP2PFetch, errStr string) { - nid := types.RandomNodeID() + nodeID := types.RandomNodeID() proof := types.RandomBytes(11) - require.NoError(t, identities.SetMalicious(tpf.serverCDB, nid, proof, time.Now())) + require.NoError(t, malfeasance.AddProof(tpf.serverCDB, nodeID, nil, proof, 1, time.Now())) tpf.verifyGetHash( func() error { - return tpf.clientFetch.LegacyMalfeasanceProofs(context.Background(), []types.NodeID{nid}) + return tpf.clientFetch.MalfeasanceProofs(context.Background(), []types.NodeID{nodeID}) }, - errStr, "mal", "hs/1", types.Hash32(nid), nid.Bytes(), + errStr, "mal2", "hs/1", types.Hash32(nodeID), nodeID.Bytes(), proof, ) }) diff --git a/sql/malfeasance/malfeasance.go b/sql/malfeasance/malfeasance.go index ce313721ca8..5524758c0a4 100644 --- a/sql/malfeasance/malfeasance.go +++ b/sql/malfeasance/malfeasance.go @@ -70,6 +70,21 @@ func IsMalicious(db sql.Executor, nodeID types.NodeID) (bool, error) { return rows > 0, nil } +func Count(db sql.Executor) (int, error) { + var count int + _, err := db.Exec(` + SELECT COUNT(*) + FROM malfeasance + `, nil, func(stmt *sql.Statement) bool { + count = stmt.ColumnInt(0) + return true + }) + if err != nil { + return 0, fmt.Errorf("count identities: %w", err) + } + return count, nil +} + func IterateOps( db sql.Executor, operations builder.Operations, diff --git a/sql/malfeasance/malfeasance_test.go b/sql/malfeasance/malfeasance_test.go index 078f021f70f..f2538de4211 100644 --- a/sql/malfeasance/malfeasance_test.go +++ b/sql/malfeasance/malfeasance_test.go @@ -202,6 +202,53 @@ func TestIsMalicious(t *testing.T) { }) } +func Test_Count(t *testing.T) { + db := statesql.InMemoryTest(t) + + count, err := malfeasance.Count(db) + require.NoError(t, err) + require.Zero(t, count) + + nodeID := types.RandomNodeID() + err = malfeasance.AddProof(db, nodeID, nil, types.RandomBytes(100), 1, time.Now()) + require.NoError(t, err) + + count, err = malfeasance.Count(db) + require.NoError(t, err) + require.Equal(t, 1, count) + + nodeID = types.RandomNodeID() + marriageATX := types.RandomATXID() + id, err := marriage.NewID(db) + require.NoError(t, err) + + err = marriage.Add(db, marriage.Info{ + ID: id, + NodeID: nodeID, + ATX: marriageATX, + MarriageIndex: 0, + Target: nodeID, + Signature: types.RandomEdSignature(), + }) + require.NoError(t, err) + + ids := make([]types.NodeID, 5) + ids[0] = nodeID + proof := types.RandomBytes(11) + err = malfeasance.AddProof(db, ids[0], &id, proof, 1, time.Now()) + require.NoError(t, err) + + for i := 1; i < len(ids); i++ { + ids[i] = types.RandomNodeID() + err := malfeasance.SetMalicious(db, ids[i], id, time.Now()) + require.NoError(t, err) + } + + count, err = malfeasance.Count(db) + require.NoError(t, err) + require.Equal(t, 6, count) +} + func Test_IterateMaliciousOps(t *testing.T) { db := statesql.InMemoryTest(t) tt := []struct { diff --git a/syncer/malsync/syncer.go b/syncer/malsync/syncer.go index c6cbf3a579a..54bdb515d2f 100644 --- a/syncer/malsync/syncer.go +++ b/syncer/malsync/syncer.go @@ -260,7 +260,7 @@ func (s *Syncer) download(parent context.Context, initial bool) error { }) eg.Go(func() error { defer cancel() - return s.downloadMalfeasanceProofs(ctx, initial, updates) + return s.downloadLegacyMalfeasanceProofs(ctx, initial, updates) }) if err := eg.Wait(); err != nil { return err @@ -355,7 +355,7 @@ func (s *Syncer) updateState(ctx context.Context) error { return nil } -func (s *Syncer) downloadMalfeasanceProofs(ctx context.Context, initial bool, updates <-chan malUpdate) error { +func (s *Syncer) downloadLegacyMalfeasanceProofs(ctx context.Context, initial bool, updates <-chan malUpdate) error { var ( update malUpdate sst = newSyncState(s.cfg.RequestsLimit, initial) @@ -416,38 +416,36 @@ func (s *Syncer) downloadMalfeasanceProofs(ctx context.Context, initial bool, up } nothingToDownload = len(batch) == 0 - - if len(batch) != 0 { - s.logger.Debug("retrieving malfeasant identities", + if len(batch) == 0 { + s.logger.Debug("no new malfeasant identities", log.ZContext(ctx)) + continue + } + s.logger.Debug("retrieving malfeasant identities", + log.ZContext(ctx), + zap.Int("count", len(batch)), + ) + if err := s.fetcher.LegacyMalfeasanceProofs(ctx, batch); err != nil { + if errors.Is(err, context.Canceled) { + return ctx.Err() + } + s.logger.Debug("failed to download malfeasance proofs", log.ZContext(ctx), - zap.Int("count", len(batch)), + log.NiceZapError(err), ) - err := s.fetcher.LegacyMalfeasanceProofs(ctx, batch) - if err != nil { - if errors.Is(err, context.Canceled) { - return ctx.Err() - } - s.logger.Debug("failed to download malfeasance proofs", - log.ZContext(ctx), - log.NiceZapError(err), - ) - } - batchError := &fetch.BatchError{} - if errors.As(err, &batchError) { - for hash, err := range batchError.Errors { - nodeID := types.NodeID(hash) - switch { - case !sst.has(nodeID): - continue - case errors.Is(err, pubsub.ErrValidationReject): - sst.rejected(nodeID) - default: - sst.failed(nodeID) - } + } + batchError := &fetch.BatchError{} + if errors.As(err, &batchError) { + for hash, err := range batchError.Errors { + nodeID := types.NodeID(hash) + switch { + case !sst.has(nodeID): + continue + case errors.Is(err, pubsub.ErrValidationReject): + sst.rejected(nodeID) + default: + sst.failed(nodeID) } } - } else { - s.logger.Debug("no new malfeasant identities", log.ZContext(ctx)) } } } From baf54478edb20fa4a33f4f57294555c6c0213c4c Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:42:04 +0000 Subject: [PATCH 15/56] Ensure an already handled proof isn't stored again --- malfeasance2/handler.go | 76 +++++++++++++++++++++++++++-------------- node/node.go | 2 +- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/malfeasance2/handler.go b/malfeasance2/handler.go index c2d04a73742..dc912224e51 100644 --- a/malfeasance2/handler.go +++ b/malfeasance2/handler.go @@ -36,7 +36,7 @@ var ( type Handler struct { logger *zap.Logger - db sql.Executor + db sql.StateDatabase self p2p.Peer nodeIDs []types.NodeID fetcher system.Fetcher @@ -51,7 +51,7 @@ type Handler struct { } func NewHandler( - db sql.Executor, + db sql.StateDatabase, lg *zap.Logger, self p2p.Peer, nodeIDs []types.NodeID, @@ -315,33 +315,57 @@ func (h *Handler) fetchReferences(ctx context.Context, peer p2p.Peer, atxIDs []t } func (h *Handler) storeProof(ctx context.Context, nodeIDs []types.NodeID, proof []byte, domain ProofDomain) error { - if len(nodeIDs) == 1 { - // smesher is not married - malicious, err := malfeasance.IsMalicious(h.db, nodeIDs[0]) + return h.db.WithTxImmediate(ctx, func(tx sql.Transaction) error { + if len(nodeIDs) == 1 { + // smesher is not married + malicious, err := malfeasance.IsMalicious(tx, nodeIDs[0]) + if err != nil { + return fmt.Errorf("check if smesher is malicious: %w", err) + } + if malicious { + h.logger.Debug("smesher is already marked as malicious", + zap.String("smesher_id", nodeIDs[0].ShortString()), + ) + return nil + } + if err := malfeasance.AddProof(tx, nodeIDs[0], nil, proof, int(domain), time.Now()); err != nil { + return fmt.Errorf("store malfeasance proof for %s: %w", nodeIDs[0], err) + } + return nil + } + + mID, err := marriage.FindIDByNodeID(tx, nodeIDs[0]) if err != nil { - return fmt.Errorf("check if smesher is malicious: %w", err) + return fmt.Errorf("get marriage ID for %s: %w", nodeIDs[0].ShortString(), err) } - if malicious { - h.logger.Debug("smesher is already marked as malicious", zap.String("smesher_id", nodeIDs[0].ShortString())) - return nil + malicious, err := malfeasance.IsMalicious(tx, nodeIDs[0]) + if err != nil { + return fmt.Errorf("check if smesher %s is malicious: %w", nodeIDs[0].ShortString(), err) } - if err := malfeasance.AddProof(h.db, nodeIDs[0], nil, proof, int(domain), time.Now()); err != nil { - return fmt.Errorf("store malfeasance proof for %s: %w", nodeIDs[0], err) + if !malicious { + if err := malfeasance.AddProof(tx, nodeIDs[0], &mID, proof, int(domain), time.Now()); err != nil { + return fmt.Errorf("store malfeasance proof for %s: %w", nodeIDs[0].ShortString(), err) + } + } else { + h.logger.Debug("smesher is already marked as malicious", + zap.String("smesher_id", nodeIDs[0].ShortString()), + ) } - return nil - } - - mID, err := marriage.FindIDByNodeID(h.db, nodeIDs[0]) - if err != nil { - return fmt.Errorf("get marriage ID for %s: %w", nodeIDs[0].ShortString(), err) - } - if err := malfeasance.AddProof(h.db, nodeIDs[0], &mID, proof, int(domain), time.Now()); err != nil { - return fmt.Errorf("store malfeasance proof for %s: %w", nodeIDs[0].ShortString(), err) - } - for _, nodeID := range nodeIDs[1:] { - if err := malfeasance.SetMalicious(h.db, nodeID, mID, time.Now()); err != nil { - return fmt.Errorf("update malfeasance state for %s: %w", nodeID.ShortString(), err) + for _, nodeID := range nodeIDs[1:] { + malicious, err := malfeasance.IsMalicious(tx, nodeID) + if err != nil { + return fmt.Errorf("check if smesher %s is malicious: %w", nodeID.ShortString(), err) + } + if malicious { + h.logger.Debug("smesher is already marked as malicious", + zap.String("smesher_id", nodeID.ShortString()), + ) + continue + } + if err := malfeasance.SetMalicious(tx, nodeID, mID, time.Now()); err != nil { + return fmt.Errorf("update malfeasance state for %s: %w", nodeID.ShortString(), err) + } } - } - return nil + return nil + }) } diff --git a/node/node.go b/node/node.go index 171fb4eb2f8..361c278d65a 100644 --- a/node/node.go +++ b/node/node.go @@ -1197,7 +1197,7 @@ func (app *App) initServices(ctx context.Context) error { malHandler.RegisterHandler(malfeasance.InvalidPrevATX, invalidPrevMH) malHandler2 := malfeasance2.NewHandler( - app.cachedDB, + app.db, malfeasanceLogger, app.host.ID(), nodeIDs, From 6e253ed4634d1bb1b90f95e5f84a42724c1d6e2d Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:48:07 +0000 Subject: [PATCH 16/56] More tests for handler --- fetch/handler_test.go | 44 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/fetch/handler_test.go b/fetch/handler_test.go index a30ef1b5f1a..5b54dd44672 100644 --- a/fetch/handler_test.go +++ b/fetch/handler_test.go @@ -23,6 +23,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/certificates" "github.com/spacemeshos/go-spacemesh/sql/identities" "github.com/spacemeshos/go-spacemesh/sql/layers" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" "github.com/spacemeshos/go-spacemesh/sql/statesql" ) @@ -337,7 +338,7 @@ func TestHandleEpochInfoReq(t *testing.T) { } } -func TestHandleMaliciousIDsReq(t *testing.T) { +func TestHandleLegacyMaliciousIDsReq(t *testing.T) { tt := []struct { name string numBad int @@ -358,9 +359,9 @@ func TestHandleMaliciousIDsReq(t *testing.T) { th := createTestHandler(t) var bad []types.NodeID for i := 0; i < tc.numBad; i++ { - nid := types.NodeID{byte(i + 1)} - bad = append(bad, nid) - require.NoError(t, identities.SetMalicious(th.cdb, nid, types.RandomBytes(11), time.Now())) + nodeID := types.NodeID{byte(i + 1)} + bad = append(bad, nodeID) + require.NoError(t, identities.SetMalicious(th.cdb, nodeID, types.RandomBytes(11), time.Now())) } out, err := th.handleLegacyMaliciousIDsReq(context.Background(), p2p.Peer(""), []byte{}) @@ -371,3 +372,38 @@ func TestHandleMaliciousIDsReq(t *testing.T) { }) } } + +func TestHandleMaliciousIDsReq(t *testing.T) { + tt := []struct { + name string + numBad int + }{ + { + name: "some bad guys", + numBad: 11, + }, + { + name: "no bad guys", + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + th := createTestHandler(t) + var bad []types.NodeID + for i := 0; i < tc.numBad; i++ { + nodeID := types.NodeID{byte(i + 1)} + bad = append(bad, nodeID) + require.NoError(t, malfeasance.AddProof(th.cdb, nodeID, nil, types.RandomBytes(11), 1, time.Now())) + } + + out, err := th.handleMaliciousIDsReq(context.Background(), p2p.Peer(""), []byte{}) + require.NoError(t, err) + var got MaliciousIDs + require.NoError(t, codec.Decode(out, &got)) + require.ElementsMatch(t, bad, got.NodeIDs) + }) + } +} From c5a67b0298730a5528c623674b60d4c356dbe9c1 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 21:34:01 +0000 Subject: [PATCH 17/56] Add fetcher for malicious IDs and more tests --- fetch/handler.go | 33 +++++++++++------------ fetch/handler_test.go | 18 ++++++------- fetch/mesh_data.go | 58 ++++++++++++++++++++--------------------- fetch/mesh_data_test.go | 24 ++++++++++++++++- fetch/p2p_test.go | 29 ++++++++++++++++++--- p2p/server/server.go | 6 ++--- 6 files changed, 105 insertions(+), 63 deletions(-) diff --git a/fetch/handler.go b/fetch/handler.go index 89a6de4c9f1..8b8bb176ad1 100644 --- a/fetch/handler.go +++ b/fetch/handler.go @@ -28,7 +28,7 @@ import ( type handler struct { logger *zap.Logger - cdb sql.StateDatabase + db sql.StateDatabase bs *datastore.BlobStore } @@ -39,14 +39,14 @@ func newHandler( ) *handler { return &handler{ logger: lg, - cdb: cdb, + db: cdb, bs: bs, } } // handleLegacyMaliciousIDsReq returns the IDs of all known malicious nodes. func (h *handler) handleLegacyMaliciousIDsReq(ctx context.Context, _ p2p.Peer, _ []byte) ([]byte, error) { - nodeIDs, err := identities.AllMalicious(h.cdb) + nodeIDs, err := identities.AllMalicious(h.db) if err != nil { return nil, fmt.Errorf("getting malicious IDs: %w", err) } @@ -59,7 +59,7 @@ func (h *handler) handleLegacyMaliciousIDsReq(ctx context.Context, _ p2p.Peer, _ // handleMaliciousIDsReq returns the IDs of all known malicious nodes. func (h *handler) handleMaliciousIDsReq(ctx context.Context, _ p2p.Peer, _ []byte) ([]byte, error) { - tx, err := h.cdb.TxImmediate(ctx) + tx, err := h.db.TxImmediate(ctx) if err != nil { return nil, fmt.Errorf("starting transaction: %w", err) } @@ -69,7 +69,7 @@ func (h *handler) handleMaliciousIDsReq(ctx context.Context, _ p2p.Peer, _ []byt return nil, fmt.Errorf("counting malicious nodes: %w", err) } nodeIDs := make([]types.NodeID, 0, total) - err = malfeasance.IterateOps(h.cdb, builder.Operations{}, + err = malfeasance.IterateOps(h.db, builder.Operations{}, func(nodeID types.NodeID, _ []byte, _ int, _ time.Time) bool { nodeIDs = append(nodeIDs, nodeID) return true @@ -86,7 +86,7 @@ func (h *handler) handleMaliciousIDsReq(ctx context.Context, _ p2p.Peer, _ []byt func (h *handler) handleLegacyMaliciousIDsReqStream(ctx context.Context, _ p2p.Peer, _ []byte, s io.ReadWriter) error { if err := h.streamIDs(ctx, s, func(cbk retrieveCallback) error { - nodeIDs, err := identities.AllMalicious(h.cdb) + nodeIDs, err := identities.AllMalicious(h.db) if err != nil { return fmt.Errorf("getting malicious IDs: %w", err) } @@ -101,9 +101,10 @@ func (h *handler) handleLegacyMaliciousIDsReqStream(ctx context.Context, _ p2p.P } func (h *handler) handleMaliciousIDsReqStream(ctx context.Context, _ p2p.Peer, _ []byte, s io.ReadWriter) error { - tx, err := h.cdb.TxImmediate(ctx) + tx, err := h.db.TxImmediate(ctx) if err != nil { - return fmt.Errorf("starting transaction: %w", err) + h.logger.Debug("failed to stream malicious node IDs", log.ZContext(ctx), zap.Error(err)) + return nil } defer tx.Release() total, err := malfeasance.Count(tx) @@ -132,7 +133,7 @@ func (h *handler) handleEpochInfoReq(ctx context.Context, _ p2p.Peer, msg []byte return nil, err } - atxids, err := atxs.GetIDsByEpoch(ctx, h.cdb, epoch) + atxids, err := atxs.GetIDsByEpoch(ctx, h.db, epoch) if err != nil { return nil, fmt.Errorf("getting ATX IDs: %w", err) } @@ -157,7 +158,7 @@ func (h *handler) handleEpochInfoReqStream(ctx context.Context, _ p2p.Peer, msg return err } if err := h.streamIDs(ctx, s, func(cbk retrieveCallback) error { - atxids, err := atxs.GetIDsByEpoch(ctx, h.cdb, epoch) + atxids, err := atxs.GetIDsByEpoch(ctx, h.db, epoch) if err != nil { return fmt.Errorf("getting ATX IDs: %w", err) } @@ -238,7 +239,7 @@ func (h *handler) handleLayerDataReq(ctx context.Context, _ p2p.Peer, req []byte if err := codec.Decode(req, &lid); err != nil { return nil, err } - ld.Ballots, err = ballots.IDsInLayer(h.cdb, lid) + ld.Ballots, err = ballots.IDsInLayer(h.db, lid) if err != nil && !errors.Is(err, sql.ErrNotFound) { return nil, fmt.Errorf("getting ballots for layer %d: %w", lid, err) } @@ -267,11 +268,11 @@ func (h *handler) handleLayerOpinionsReq2(ctx context.Context, _ p2p.Peer, data ) opnReqV2.Inc() - lo.PrevAggHash, err = layers.GetAggregatedHash(h.cdb, lid.Sub(1)) + lo.PrevAggHash, err = layers.GetAggregatedHash(h.db, lid.Sub(1)) if err != nil && !errors.Is(err, sql.ErrNotFound) { return nil, fmt.Errorf("getting aggregated hash for layer %d: %w", lid.Sub(1), err) } - bid, err := certificates.CertifiedBlock(h.cdb, lid) + bid, err := certificates.CertifiedBlock(h.db, lid) if err != nil && !errors.Is(err, sql.ErrNotFound) { return nil, fmt.Errorf("getting certified block for layer %d: %w", lid, err) } @@ -287,7 +288,7 @@ func (h *handler) handleLayerOpinionsReq2(ctx context.Context, _ p2p.Peer, data func (h *handler) handleCertReq(ctx context.Context, lid types.LayerID, bid types.BlockID) ([]byte, error) { certReq.Inc() - certs, err := certificates.Get(h.cdb, lid) + certs, err := certificates.Get(h.db, lid) if err != nil && !errors.Is(err, sql.ErrNotFound) { return nil, fmt.Errorf("getting certificates for layer %d: %w", lid, err) } @@ -479,7 +480,7 @@ func (h *handler) handleMeshHashReq(ctx context.Context, _ p2p.Peer, reqData []b if err := req.Validate(); err != nil { return nil, fmt.Errorf("validating request: %w", err) } - hashes, err = layers.GetAggHashes(h.cdb, req.From, req.To, req.Step) + hashes, err = layers.GetAggHashes(h.db, req.From, req.To, req.Step) if err != nil { return nil, err } @@ -508,7 +509,7 @@ func (h *handler) handleMeshHashReqStream(ctx context.Context, _ p2p.Peer, reqDa return fmt.Errorf("validating request: %w", err) } - hashes, err := layers.GetAggHashes(h.cdb, req.From, req.To, req.Step) + hashes, err := layers.GetAggHashes(h.db, req.From, req.To, req.Step) if err != nil { return err } diff --git a/fetch/handler_test.go b/fetch/handler_test.go index 5b54dd44672..0b0b1217034 100644 --- a/fetch/handler_test.go +++ b/fetch/handler_test.go @@ -29,7 +29,6 @@ import ( type testHandler struct { *handler - db sql.StateDatabase } func createTestHandler(tb testing.TB, opts ...sql.Opt) *testHandler { @@ -37,7 +36,6 @@ func createTestHandler(tb testing.TB, opts ...sql.Opt) *testHandler { db := statesql.InMemoryTest(tb, opts...) return &testHandler{ handler: newHandler(db, datastore.NewBlobStore(db, store.New()), lg), - db: db, } } @@ -140,13 +138,13 @@ func TestHandleLayerOpinionsReq(t *testing.T) { th := createTestHandler(t) lid := types.LayerID(111) - _, aggHash := createOpinions(t, th.cdb, lid, !tc.missingCert) + _, aggHash := createOpinions(t, th.db, lid, !tc.missingCert) if tc.multipleCerts { bid := types.RandomBlockID() - require.NoError(t, certificates.Add(th.cdb, lid, &types.Certificate{ + require.NoError(t, certificates.Add(th.db, lid, &types.Certificate{ BlockID: bid, })) - require.NoError(t, certificates.SetInvalid(th.cdb, lid, bid)) + require.NoError(t, certificates.SetInvalid(th.db, lid, bid)) } req := OpinionRequest{Layer: lid} @@ -185,7 +183,7 @@ func TestHandleCertReq(t *testing.T) { require.Nil(t, resp) cert := &types.Certificate{BlockID: bid} - require.NoError(t, certificates.Add(th.cdb, lid, cert)) + require.NoError(t, certificates.Add(th.db, lid, cert)) resp, err = th.handleLayerOpinionsReq2(context.Background(), p2p.Peer(""), reqData) require.NoError(t, err) @@ -241,7 +239,7 @@ func TestHandleMeshHashReq(t *testing.T) { } if !tc.hashMissing { for lid := req.From; !lid.After(req.To); lid = lid.Add(1) { - require.NoError(t, layers.SetMeshHash(th.cdb, lid, types.RandomHash())) + require.NoError(t, layers.SetMeshHash(th.db, lid, types.RandomHash())) } } reqData, err := codec.Encode(req) @@ -297,7 +295,7 @@ func TestHandleEpochInfoReq(t *testing.T) { if !tc.missingData { for i := 0; i < 10; i++ { vatx := newAtx(t, epoch) - require.NoError(t, atxs.Add(th.cdb, vatx, types.AtxBlob{})) + require.NoError(t, atxs.Add(th.db, vatx, types.AtxBlob{})) expected.AtxIDs = append(expected.AtxIDs, vatx.ID()) } } @@ -361,7 +359,7 @@ func TestHandleLegacyMaliciousIDsReq(t *testing.T) { for i := 0; i < tc.numBad; i++ { nodeID := types.NodeID{byte(i + 1)} bad = append(bad, nodeID) - require.NoError(t, identities.SetMalicious(th.cdb, nodeID, types.RandomBytes(11), time.Now())) + require.NoError(t, identities.SetMalicious(th.db, nodeID, types.RandomBytes(11), time.Now())) } out, err := th.handleLegacyMaliciousIDsReq(context.Background(), p2p.Peer(""), []byte{}) @@ -396,7 +394,7 @@ func TestHandleMaliciousIDsReq(t *testing.T) { for i := 0; i < tc.numBad; i++ { nodeID := types.NodeID{byte(i + 1)} bad = append(bad, nodeID) - require.NoError(t, malfeasance.AddProof(th.cdb, nodeID, nil, types.RandomBytes(11), 1, time.Now())) + require.NoError(t, malfeasance.AddProof(th.db, nodeID, nil, types.RandomBytes(11), 1, time.Now())) } out, err := th.handleMaliciousIDsReq(context.Background(), p2p.Peer(""), []byte{}) diff --git a/fetch/mesh_data.go b/fetch/mesh_data.go index e8fe5e10a25..c060659adcc 100644 --- a/fetch/mesh_data.go +++ b/fetch/mesh_data.go @@ -325,35 +325,35 @@ func (f *Fetch) LegacyMaliciousIDs(ctx context.Context, peer p2p.Peer) ([]types. // MaliciousIDs gets the malicious IDs from the specified peer. Proofs for those IDs can be fetched via the malfeasance // proof protocol (see also MalfeasanceProofs). -// func (f *Fetch) MaliciousIDs(ctx context.Context, peer p2p.Peer) ([]types.NodeID, error) { -// var malIDs MaliciousIDs -// if !f.cfg.Streaming { -// data, err := f.meteredRequest(ctx, malProtocol, peer, []byte{}) -// if err != nil { -// return nil, err -// } -// if err := codec.Decode(data, &malIDs); err != nil { -// return nil, err -// } -// f.RegisterPeerHashes(peer, types.NodeIDsToHashes(malIDs.NodeIDs)) -// return malIDs.NodeIDs, nil -// } - -// err := f.meteredStreamRequest(ctx, malProtocol, peer, []byte{}, -// func(ctx context.Context, s io.ReadWriter) (int, error) { -// total, err := readIDSlice(s, &malIDs.NodeIDs, maxMaliciousIDs) -// if ctx.Err() != nil { -// return total, ctx.Err() -// } -// return total, err -// }, -// ) -// if err != nil { -// return nil, err -// } -// f.RegisterPeerHashes(peer, types.NodeIDsToHashes(malIDs.NodeIDs)) -// return malIDs.NodeIDs, nil -// } +func (f *Fetch) MaliciousIDs(ctx context.Context, peer p2p.Peer) ([]types.NodeID, error) { + var malIDs MaliciousIDs + if !f.cfg.Streaming { + data, err := f.meteredRequest(ctx, malProtocol, peer, []byte{}) + if err != nil { + return nil, err + } + if err := codec.Decode(data, &malIDs); err != nil { + return nil, err + } + f.RegisterPeerHashes(peer, types.NodeIDsToHashes(malIDs.NodeIDs)) + return malIDs.NodeIDs, nil + } + + err := f.meteredStreamRequest(ctx, malProtocol, peer, []byte{}, + func(ctx context.Context, s io.ReadWriter) (int, error) { + total, err := readIDSlice(s, &malIDs.NodeIDs, maxMaliciousIDs) + if ctx.Err() != nil { + return total, ctx.Err() + } + return total, err + }, + ) + if err != nil { + return nil, err + } + f.RegisterPeerHashes(peer, types.NodeIDsToHashes(malIDs.NodeIDs)) + return malIDs.NodeIDs, nil +} // GetLayerData get layer data from peers. func (f *Fetch) GetLayerData(ctx context.Context, peer p2p.Peer, lid types.LayerID) ([]byte, error) { diff --git a/fetch/mesh_data_test.go b/fetch/mesh_data_test.go index 124cce5c8ca..74a003f7d4a 100644 --- a/fetch/mesh_data_test.go +++ b/fetch/mesh_data_test.go @@ -693,7 +693,29 @@ func TestFetch_LegacyMaliciousIDs(t *testing.T) { } func TestFetch_MaliciousIDs(t *testing.T) { - // TODO(mafa): implement for malfeasance2 + t.Run("success", func(t *testing.T) { + t.Parallel() + f := createFetch(t) + expectedIDs := make([]types.NodeID, numMalicious) + for i := range expectedIDs { + expectedIDs[i] = types.RandomNodeID() + } + resp := codec.MustEncode(&MaliciousIDs{NodeIDs: expectedIDs}) + f.mh.EXPECT().ID().Return("self").AnyTimes() + f.mMalS.EXPECT().Request(gomock.Any(), p2p.Peer("p0"), []byte{}).Return(resp, nil) + ids, err := f.MaliciousIDs(context.Background(), "p0") + require.NoError(t, err) + require.Equal(t, expectedIDs, ids) + }) + t.Run("failure", func(t *testing.T) { + t.Parallel() + errUnknown := errors.New("unknown") + f := createFetch(t) + f.mMalS.EXPECT().Request(gomock.Any(), p2p.Peer("p0"), []byte{}).Return(nil, errUnknown) + ids, err := f.MaliciousIDs(context.Background(), "p0") + require.ErrorIs(t, err, errUnknown) + require.Nil(t, ids) + }) } func TestFetch_GetLayerOpinions(t *testing.T) { diff --git a/fetch/p2p_test.go b/fetch/p2p_test.go index 29caf5de57d..52ba8bf5e2e 100644 --- a/fetch/p2p_test.go +++ b/fetch/p2p_test.go @@ -356,7 +356,7 @@ func TestP2PPeerMeshHashes(t *testing.T) { }) } -func TestP2PMaliciousIDs(t *testing.T) { +func TestP2PLegacyMaliciousIDs(t *testing.T) { forStreaming( t, "database closed", false, func(t *testing.T, ctx context.Context, tpf *testP2PFetch, errStr string) { @@ -364,8 +364,7 @@ func TestP2PMaliciousIDs(t *testing.T) { for i := 0; i < 11; i++ { nid := types.NodeID{byte(i + 1)} bad = append(bad, nid) - require.NoError(t, identities.SetMalicious( - tpf.serverCDB, nid, types.RandomBytes(11), time.Now())) + require.NoError(t, identities.SetMalicious(tpf.serverCDB, nid, types.RandomBytes(11), time.Now())) } if errStr != "" { tpf.serverDB.Close() @@ -381,6 +380,30 @@ func TestP2PMaliciousIDs(t *testing.T) { }) } +func TestP2PMaliciousIDs(t *testing.T) { + forStreaming( + t, "database closed", false, + func(t *testing.T, ctx context.Context, tpf *testP2PFetch, errStr string) { + var bad []types.NodeID + for i := 0; i < 11; i++ { + nid := types.NodeID{byte(i + 1)} + bad = append(bad, nid) + require.NoError(t, malfeasance.AddProof(tpf.serverCDB, nid, nil, types.RandomBytes(11), 1, time.Now())) + } + if errStr != "" { + tpf.serverDB.Close() + } + + malIDs, err := tpf.clientFetch.MaliciousIDs(context.Background(), tpf.serverID) + if errStr == "" { + require.NoError(t, err) + require.ElementsMatch(t, bad, malIDs) + } else { + require.ErrorContains(t, err, errStr) + } + }) +} + func TestP2PGetATXs(t *testing.T) { forStreamingCachedUncached( t, "database closed", diff --git a/p2p/server/server.go b/p2p/server/server.go index ebc424cc249..45d2222607a 100644 --- a/p2p/server/server.go +++ b/p2p/server/server.go @@ -496,12 +496,10 @@ func (s *Server) NumAcceptedRequests() int { func writeResponse(w io.Writer, resp *Response) error { wr := bufio.NewWriter(w) if _, err := codec.EncodeTo(wr, resp); err != nil { - return fmt.Errorf("failed to write response (len %d err len %d): %w", - len(resp.Data), len(resp.Error), err) + return fmt.Errorf("failed to write response (len %d err len %d): %w", len(resp.Data), len(resp.Error), err) } if err := wr.Flush(); err != nil { - return fmt.Errorf("failed to write response (len %d err len %d): %w", - len(resp.Data), len(resp.Error), err) + return fmt.Errorf("failed to write response (len %d err len %d): %w", len(resp.Data), len(resp.Error), err) } return nil } From 41483bbc124e74fc2e9cf5b1979d97fe3453524b Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 22:55:53 +0000 Subject: [PATCH 18/56] Add syncer code --- datastore/mocks.go | 40 ++--- sql/malsync/malsync.go | 43 ++++-- sql/malsync/malsync_test.go | 25 +++- syncer/interface.go | 1 + syncer/malsync/mocks/mocks.go | 77 ++++++++++ syncer/malsync/syncer.go | 268 +++++++++++++++++++++++++++++++--- syncer/malsync/syncer_test.go | 146 +++++++++++++----- syncer/mocks/mocks.go | 38 +++++ syncer/syncer.go | 11 +- syncer/syncer_test.go | 6 + 10 files changed, 567 insertions(+), 88 deletions(-) diff --git a/datastore/mocks.go b/datastore/mocks.go index 9d85e67ddfa..f2c8f836a81 100644 --- a/datastore/mocks.go +++ b/datastore/mocks.go @@ -17,32 +17,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockmalfeasanceProvider is a mock of malfeasanceProvider interface. -type MockmalfeasanceProvider struct { +// MockMalfeasanceProvider is a mock of MalfeasanceProvider interface. +type MockMalfeasanceProvider struct { ctrl *gomock.Controller - recorder *MockmalfeasanceProviderMockRecorder + recorder *MockMalfeasanceProviderMockRecorder isgomock struct{} } -// MockmalfeasanceProviderMockRecorder is the mock recorder for MockmalfeasanceProvider. -type MockmalfeasanceProviderMockRecorder struct { - mock *MockmalfeasanceProvider +// MockMalfeasanceProviderMockRecorder is the mock recorder for MockMalfeasanceProvider. +type MockMalfeasanceProviderMockRecorder struct { + mock *MockMalfeasanceProvider } -// NewMockmalfeasanceProvider creates a new mock instance. -func NewMockmalfeasanceProvider(ctrl *gomock.Controller) *MockmalfeasanceProvider { - mock := &MockmalfeasanceProvider{ctrl: ctrl} - mock.recorder = &MockmalfeasanceProviderMockRecorder{mock} +// NewMockMalfeasanceProvider creates a new mock instance. +func NewMockMalfeasanceProvider(ctrl *gomock.Controller) *MockMalfeasanceProvider { + mock := &MockMalfeasanceProvider{ctrl: ctrl} + mock.recorder = &MockMalfeasanceProviderMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockmalfeasanceProvider) EXPECT() *MockmalfeasanceProviderMockRecorder { +func (m *MockMalfeasanceProvider) EXPECT() *MockMalfeasanceProviderMockRecorder { return m.recorder } // ProofByID mocks base method. -func (m *MockmalfeasanceProvider) ProofByID(ctx context.Context, nodeID types.NodeID) ([]byte, error) { +func (m *MockMalfeasanceProvider) ProofByID(ctx context.Context, nodeID types.NodeID) ([]byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ProofByID", ctx, nodeID) ret0, _ := ret[0].([]byte) @@ -51,31 +51,31 @@ func (m *MockmalfeasanceProvider) ProofByID(ctx context.Context, nodeID types.No } // ProofByID indicates an expected call of ProofByID. -func (mr *MockmalfeasanceProviderMockRecorder) ProofByID(ctx, nodeID any) *MockmalfeasanceProviderProofByIDCall { +func (mr *MockMalfeasanceProviderMockRecorder) ProofByID(ctx, nodeID any) *MockMalfeasanceProviderProofByIDCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProofByID", reflect.TypeOf((*MockmalfeasanceProvider)(nil).ProofByID), ctx, nodeID) - return &MockmalfeasanceProviderProofByIDCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProofByID", reflect.TypeOf((*MockMalfeasanceProvider)(nil).ProofByID), ctx, nodeID) + return &MockMalfeasanceProviderProofByIDCall{Call: call} } -// MockmalfeasanceProviderProofByIDCall wrap *gomock.Call -type MockmalfeasanceProviderProofByIDCall struct { +// MockMalfeasanceProviderProofByIDCall wrap *gomock.Call +type MockMalfeasanceProviderProofByIDCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockmalfeasanceProviderProofByIDCall) Return(arg0 []byte, arg1 error) *MockmalfeasanceProviderProofByIDCall { +func (c *MockMalfeasanceProviderProofByIDCall) Return(arg0 []byte, arg1 error) *MockMalfeasanceProviderProofByIDCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *MockmalfeasanceProviderProofByIDCall) Do(f func(context.Context, types.NodeID) ([]byte, error)) *MockmalfeasanceProviderProofByIDCall { +func (c *MockMalfeasanceProviderProofByIDCall) Do(f func(context.Context, types.NodeID) ([]byte, error)) *MockMalfeasanceProviderProofByIDCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockmalfeasanceProviderProofByIDCall) DoAndReturn(f func(context.Context, types.NodeID) ([]byte, error)) *MockmalfeasanceProviderProofByIDCall { +func (c *MockMalfeasanceProviderProofByIDCall) DoAndReturn(f func(context.Context, types.NodeID) ([]byte, error)) *MockMalfeasanceProviderProofByIDCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/sql/malsync/malsync.go b/sql/malsync/malsync.go index fac8b50fbaa..18e64396458 100644 --- a/sql/malsync/malsync.go +++ b/sql/malsync/malsync.go @@ -7,10 +7,21 @@ import ( "github.com/spacemeshos/go-spacemesh/sql" ) -func GetSyncState(db sql.Executor) (time.Time, error) { +func LegacySyncState(db sql.Executor) (time.Time, error) { + return getSyncState(db, 1) +} + +func SyncState(db sql.Executor) (time.Time, error) { + return getSyncState(db, 2) +} + +func getSyncState(db sql.Executor, version int64) (time.Time, error) { var timestamp time.Time - rows, err := db.Exec("select timestamp from malfeasance_sync_state where id = 1", - nil, func(stmt *sql.Statement) bool { + rows, err := db.Exec("select timestamp from malfeasance_sync_state where id = ?1", + func(s *sql.Statement) { + s.BindInt64(1, version) + }, + func(stmt *sql.Statement) bool { v := stmt.ColumnInt64(0) if v > 0 { timestamp = time.Unix(v, 0) @@ -27,23 +38,31 @@ func GetSyncState(db sql.Executor) (time.Time, error) { } } -func updateSyncState(db sql.Executor, ts int64) error { - if _, err := db.Exec( - `insert into malfeasance_sync_state (id, timestamp) values(1, ?1) - on conflict (id) do update set timestamp = ?1`, - func(stmt *sql.Statement) { - stmt.BindInt64(1, ts) - }, nil, +func updateSyncState(db sql.Executor, version, ts int64) error { + if _, err := db.Exec(` + insert into malfeasance_sync_state (id, timestamp) values(?1, ?2) + on conflict (id) do update set timestamp = ?2 + `, func(stmt *sql.Statement) { + stmt.BindInt64(1, version) + stmt.BindInt64(2, ts) + }, nil, ); err != nil { return fmt.Errorf("error initializing malfeasance sync state: %w", err) } return nil } +func UpdateLegacySyncState(db sql.Executor, timestamp time.Time) error { + return updateSyncState(db, 1, timestamp.Unix()) +} + func UpdateSyncState(db sql.Executor, timestamp time.Time) error { - return updateSyncState(db, timestamp.Unix()) + return updateSyncState(db, 2, timestamp.Unix()) } func Clear(db sql.Executor) error { - return updateSyncState(db, 0) + if err := updateSyncState(db, 1, 0); err != nil { + return err + } + return updateSyncState(db, 2, 0) } diff --git a/sql/malsync/malsync_test.go b/sql/malsync/malsync_test.go index 38624ca27e1..d994e94fe89 100644 --- a/sql/malsync/malsync_test.go +++ b/sql/malsync/malsync_test.go @@ -9,21 +9,40 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/localsql" ) +func TestLegacyMalfeasanceSyncState(t *testing.T) { + db := localsql.InMemoryTest(t) + timestamp, err := LegacySyncState(db) + require.NoError(t, err) + require.Equal(t, time.Time{}, timestamp) + ts := time.Now() + for i := 0; i < 3; i++ { + require.NoError(t, UpdateLegacySyncState(db, ts)) + timestamp, err = LegacySyncState(db) + require.NoError(t, err) + require.Equal(t, ts.Truncate(time.Second), timestamp) + ts = ts.Add(3 * time.Minute) + } + require.NoError(t, Clear(db)) + timestamp, err = LegacySyncState(db) + require.NoError(t, err) + require.Equal(t, time.Time{}, timestamp) +} + func TestMalfeasanceSyncState(t *testing.T) { db := localsql.InMemoryTest(t) - timestamp, err := GetSyncState(db) + timestamp, err := SyncState(db) require.NoError(t, err) require.Equal(t, time.Time{}, timestamp) ts := time.Now() for i := 0; i < 3; i++ { require.NoError(t, UpdateSyncState(db, ts)) - timestamp, err = GetSyncState(db) + timestamp, err = SyncState(db) require.NoError(t, err) require.Equal(t, ts.Truncate(time.Second), timestamp) ts = ts.Add(3 * time.Minute) } require.NoError(t, Clear(db)) - timestamp, err = GetSyncState(db) + timestamp, err = SyncState(db) require.NoError(t, err) require.Equal(t, time.Time{}, timestamp) } diff --git a/syncer/interface.go b/syncer/interface.go index 4011354c3bb..13367152ed9 100644 --- a/syncer/interface.go +++ b/syncer/interface.go @@ -36,6 +36,7 @@ type atxSyncer interface { } type malSyncer interface { + EnsureLegacyInSync(parent context.Context, epochStart, epochEnd time.Time) error EnsureInSync(parent context.Context, epochStart, epochEnd time.Time) error DownloadLoop(parent context.Context) error } diff --git a/syncer/malsync/mocks/mocks.go b/syncer/malsync/mocks/mocks.go index f3684704331..3be31c849d5 100644 --- a/syncer/malsync/mocks/mocks.go +++ b/syncer/malsync/mocks/mocks.go @@ -119,6 +119,83 @@ func (c *MockfetcherLegacyMaliciousIDsCall) DoAndReturn(f func(context.Context, return c } +// MalfeasanceProofs mocks base method. +func (m *Mockfetcher) MalfeasanceProofs(arg0 context.Context, arg1 []types.NodeID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MalfeasanceProofs", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// MalfeasanceProofs indicates an expected call of MalfeasanceProofs. +func (mr *MockfetcherMockRecorder) MalfeasanceProofs(arg0, arg1 any) *MockfetcherMalfeasanceProofsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MalfeasanceProofs", reflect.TypeOf((*Mockfetcher)(nil).MalfeasanceProofs), arg0, arg1) + return &MockfetcherMalfeasanceProofsCall{Call: call} +} + +// MockfetcherMalfeasanceProofsCall wrap *gomock.Call +type MockfetcherMalfeasanceProofsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockfetcherMalfeasanceProofsCall) Return(arg0 error) *MockfetcherMalfeasanceProofsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockfetcherMalfeasanceProofsCall) Do(f func(context.Context, []types.NodeID) error) *MockfetcherMalfeasanceProofsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockfetcherMalfeasanceProofsCall) DoAndReturn(f func(context.Context, []types.NodeID) error) *MockfetcherMalfeasanceProofsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MaliciousIDs mocks base method. +func (m *Mockfetcher) MaliciousIDs(arg0 context.Context, arg1 p2p.Peer) ([]types.NodeID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MaliciousIDs", arg0, arg1) + ret0, _ := ret[0].([]types.NodeID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MaliciousIDs indicates an expected call of MaliciousIDs. +func (mr *MockfetcherMockRecorder) MaliciousIDs(arg0, arg1 any) *MockfetcherMaliciousIDsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaliciousIDs", reflect.TypeOf((*Mockfetcher)(nil).MaliciousIDs), arg0, arg1) + return &MockfetcherMaliciousIDsCall{Call: call} +} + +// MockfetcherMaliciousIDsCall wrap *gomock.Call +type MockfetcherMaliciousIDsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockfetcherMaliciousIDsCall) Return(arg0 []types.NodeID, arg1 error) *MockfetcherMaliciousIDsCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockfetcherMaliciousIDsCall) Do(f func(context.Context, p2p.Peer) ([]types.NodeID, error)) *MockfetcherMaliciousIDsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockfetcherMaliciousIDsCall) DoAndReturn(f func(context.Context, p2p.Peer) ([]types.NodeID, error)) *MockfetcherMaliciousIDsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SelectBestShuffled mocks base method. func (m *Mockfetcher) SelectBestShuffled(arg0 int) []p2p.Peer { m.ctrl.T.Helper() diff --git a/syncer/malsync/syncer.go b/syncer/malsync/syncer.go index 54bdb515d2f..78b2fd1950e 100644 --- a/syncer/malsync/syncer.go +++ b/syncer/malsync/syncer.go @@ -18,6 +18,7 @@ import ( "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/identities" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" "github.com/spacemeshos/go-spacemesh/sql/malsync" ) @@ -26,7 +27,9 @@ import ( type fetcher interface { SelectBestShuffled(int) []p2p.Peer LegacyMaliciousIDs(context.Context, p2p.Peer) ([]types.NodeID, error) + MaliciousIDs(context.Context, p2p.Peer) ([]types.NodeID, error) LegacyMalfeasanceProofs(context.Context, []types.NodeID) error + MalfeasanceProofs(context.Context, []types.NodeID) error } type Opt func(*Syncer) @@ -237,8 +240,20 @@ func New(fetcher fetcher, db sql.Executor, localdb sql.LocalDatabase, opts ...Op return s } +func (s *Syncer) shouldSyncLegacy(epochStart, epochEnd time.Time) (bool, error) { + timestamp, err := malsync.LegacySyncState(s.localdb) + if err != nil { + return false, fmt.Errorf("error getting malfeasance sync state: %w", err) + } + if timestamp.Before(epochStart) { + return true, nil + } + cutoff := epochEnd.Sub(epochStart).Seconds() * s.cfg.MaxEpochFraction + return s.clock.Now().Sub(timestamp).Seconds() > cutoff, nil +} + func (s *Syncer) shouldSync(epochStart, epochEnd time.Time) (bool, error) { - timestamp, err := malsync.GetSyncState(s.localdb) + timestamp, err := malsync.SyncState(s.localdb) if err != nil { return false, fmt.Errorf("error getting malfeasance sync state: %w", err) } @@ -249,6 +264,25 @@ func (s *Syncer) shouldSync(epochStart, epochEnd time.Time) (bool, error) { return s.clock.Now().Sub(timestamp).Seconds() > cutoff, nil } +func (s *Syncer) downloadLegacy(parent context.Context, initial bool) error { + s.logger.Info("starting malfeasance proof sync", log.ZContext(parent)) + defer s.logger.Debug("malfeasance proof sync terminated", log.ZContext(parent)) + ctx, cancel := context.WithCancel(parent) + eg, ctx := errgroup.WithContext(ctx) + updates := make(chan malUpdate, s.cfg.MalfeasanceIDPeers) + eg.Go(func() error { + return s.downloadLegacyNodeIDs(ctx, initial, updates) + }) + eg.Go(func() error { + defer cancel() + return s.downloadLegacyMalfeasanceProofs(ctx, initial, updates) + }) + if err := eg.Wait(); err != nil { + return err + } + return parent.Err() +} + func (s *Syncer) download(parent context.Context, initial bool) error { s.logger.Info("starting malfeasance proof sync", log.ZContext(parent)) defer s.logger.Debug("malfeasance proof sync terminated", log.ZContext(parent)) @@ -260,7 +294,7 @@ func (s *Syncer) download(parent context.Context, initial bool) error { }) eg.Go(func() error { defer cancel() - return s.downloadLegacyMalfeasanceProofs(ctx, initial, updates) + return s.downloadMalfeasanceProofs(ctx, initial, updates) }) if err := eg.Wait(); err != nil { return err @@ -268,6 +302,78 @@ func (s *Syncer) download(parent context.Context, initial bool) error { return parent.Err() } +func (s *Syncer) downloadLegacyNodeIDs(ctx context.Context, initial bool, updates chan<- malUpdate) error { + interval := s.cfg.IDRequestInterval + if initial { + interval = 0 + } + for { + if interval != 0 { + s.logger.Debug( + "pausing between legacy malicious node ID requests", + zap.Duration("duration", interval), + ) + select { + case <-ctx.Done(): + return nil + // TODO(ivan4th) this has to be randomized in a followup + // when sync will be scheduled in advance, in order to smooth out request rate across the network + case <-s.clock.After(interval): + } + } + + peers := s.fetcher.SelectBestShuffled(s.cfg.MalfeasanceIDPeers) + if len(peers) == 0 { + s.logger.Debug( + "don't have enough peers for legacy malfeasance sync", + zap.Int("nPeers", s.cfg.MalfeasanceIDPeers), + ) + if interval == 0 { + interval = s.cfg.RetryInterval + } + continue + } + + var eg errgroup.Group + for _, peer := range peers { + eg.Go(func() error { + malIDs, err := s.fetcher.LegacyMaliciousIDs(ctx, peer) + if err != nil { + if errors.Is(err, context.Canceled) { + return nil + } + s.peerErrMetric.Inc() + s.logger.Warn("failed to download legacy malicious node IDs", + log.ZContext(ctx), + zap.String("peer", peer.String()), + zap.Error(err), + ) + return nil + } + s.logger.Debug("downloaded legacy malicious node IDs", + log.ZContext(ctx), + zap.String("peer", peer.String()), + zap.Int("ids", len(malIDs)), + ) + select { + case <-ctx.Done(): + return ctx.Err() + case updates <- malUpdate{peer: peer, nodeIDs: malIDs}: + } + return nil + }) + } + + if err := eg.Wait(); err != nil { + return err + } + + if interval == 0 { + interval = s.cfg.RetryInterval + } + } +} + func (s *Syncer) downloadNodeIDs(ctx context.Context, initial bool, updates chan<- malUpdate) error { interval := s.cfg.IDRequestInterval if initial { @@ -276,8 +382,9 @@ func (s *Syncer) downloadNodeIDs(ctx context.Context, initial bool, updates chan for { if interval != 0 { s.logger.Debug( - "pausing between malfeasant node ID requests", - zap.Duration("duration", interval)) + "pausing between malicious node ID requests", + zap.Duration("duration", interval), + ) select { case <-ctx.Done(): return nil @@ -302,20 +409,20 @@ func (s *Syncer) downloadNodeIDs(ctx context.Context, initial bool, updates chan var eg errgroup.Group for _, peer := range peers { eg.Go(func() error { - malIDs, err := s.fetcher.LegacyMaliciousIDs(ctx, peer) + malIDs, err := s.fetcher.MaliciousIDs(ctx, peer) if err != nil { if errors.Is(err, context.Canceled) { return nil } s.peerErrMetric.Inc() - s.logger.Warn("failed to download malfeasant node IDs", + s.logger.Warn("failed to download malicious node IDs", log.ZContext(ctx), zap.String("peer", peer.String()), zap.Error(err), ) return nil } - s.logger.Debug("downloaded malfeasant node IDs", + s.logger.Debug("downloaded malicious node IDs", log.ZContext(ctx), zap.String("peer", peer.String()), zap.Int("ids", len(malIDs)), @@ -339,6 +446,22 @@ func (s *Syncer) downloadNodeIDs(ctx context.Context, initial bool, updates chan } } +func (s *Syncer) updateLegacyState(ctx context.Context) error { + if err := s.localdb.WithTxImmediate(ctx, func(tx sql.Transaction) error { + return malsync.UpdateLegacySyncState(tx, s.clock.Now()) + }); err != nil { + if ctx.Err() != nil { + // FIXME: with crawshaw, canceling the context which has been used to get + // a connection from the pool may cause "database: no free connection" errors. + // Related: #6273 + err = ctx.Err() + } + return fmt.Errorf("error updating legacy malsync state: %w", err) + } + + return nil +} + func (s *Syncer) updateState(ctx context.Context) error { if err := s.localdb.WithTxImmediate(ctx, func(tx sql.Transaction) error { return malsync.UpdateSyncState(tx, s.clock.Now()) @@ -356,6 +479,101 @@ func (s *Syncer) updateState(ctx context.Context) error { } func (s *Syncer) downloadLegacyMalfeasanceProofs(ctx context.Context, initial bool, updates <-chan malUpdate) error { + var ( + update malUpdate + sst = newSyncState(s.cfg.RequestsLimit, initial) + nothingToDownload = true + gotUpdate = false + ) + for { + if nothingToDownload { + sst.done() + if initial && sst.numSyncedPeers() >= s.cfg.MinSyncPeers { + if err := s.updateLegacyState(ctx); err != nil { + return err + } + s.logger.Info("initial sync of legacy malfeasance proofs completed", log.ZContext(ctx)) + return nil + } else if !initial && gotUpdate { + if err := s.updateLegacyState(ctx); err != nil { + return err + } + } + select { + case <-ctx.Done(): + return ctx.Err() + case update = <-updates: + s.logger.Debug("legacy malfeasance sync update", + log.ZContext(ctx), + zap.Int("count", len(update.nodeIDs)), + ) + sst.update(update) + gotUpdate = true + } + } else { + select { + case <-ctx.Done(): + return ctx.Err() + case update = <-updates: + s.logger.Debug("legacy malfeasance sync update", + log.ZContext(ctx), + zap.Int("count", len(update.nodeIDs)), + ) + sst.update(update) + gotUpdate = true + default: + // If we have some hashes to fetch already, don't wait for + // another update + } + } + batch, err := sst.missing(s.cfg.MaxBatchSize, func(nodeID types.NodeID) (bool, error) { + // TODO(ivan4th): check multiple node IDs at once in a single SQL query + isMalicious, err := identities.IsMalicious(s.db, nodeID) + if errors.Is(err, sql.ErrNotFound) { + return false, nil + } + return isMalicious, err + }) + if err != nil { + return fmt.Errorf("error checking legacy malicious node IDs: %w", err) + } + + nothingToDownload = len(batch) == 0 + if len(batch) == 0 { + s.logger.Debug("no new legacy malicious identities", log.ZContext(ctx)) + continue + } + s.logger.Debug("retrieving legacy malicious identities", + log.ZContext(ctx), + zap.Int("count", len(batch)), + ) + if err := s.fetcher.LegacyMalfeasanceProofs(ctx, batch); err != nil { + if errors.Is(err, context.Canceled) { + return ctx.Err() + } + s.logger.Debug("failed to download malfeasance proofs", + log.ZContext(ctx), + log.NiceZapError(err), + ) + } + batchError := &fetch.BatchError{} + if errors.As(err, &batchError) { + for hash, err := range batchError.Errors { + nodeID := types.NodeID(hash) + switch { + case !sst.has(nodeID): + continue + case errors.Is(err, pubsub.ErrValidationReject): + sst.rejected(nodeID) + default: + sst.failed(nodeID) + } + } + } + } +} + +func (s *Syncer) downloadMalfeasanceProofs(ctx context.Context, initial bool, updates <-chan malUpdate) error { var ( update malUpdate sst = newSyncState(s.cfg.RequestsLimit, initial) @@ -404,27 +622,27 @@ func (s *Syncer) downloadLegacyMalfeasanceProofs(ctx context.Context, initial bo } } batch, err := sst.missing(s.cfg.MaxBatchSize, func(nodeID types.NodeID) (bool, error) { - // TODO(ivan4th): check multiple node IDs at once in a single SQL query - isMalicious, err := identities.IsMalicious(s.db, nodeID) - if err != nil && errors.Is(err, sql.ErrNotFound) { + // TODO(mafa): check multiple node IDs at once in a single SQL query + isMalicious, err := malfeasance.IsMalicious(s.db, nodeID) + if errors.Is(err, sql.ErrNotFound) { return false, nil } return isMalicious, err }) if err != nil { - return fmt.Errorf("error checking malfeasant node IDs: %w", err) + return fmt.Errorf("error checking malicious node IDs: %w", err) } nothingToDownload = len(batch) == 0 if len(batch) == 0 { - s.logger.Debug("no new malfeasant identities", log.ZContext(ctx)) + s.logger.Debug("no new malicious identities", log.ZContext(ctx)) continue } - s.logger.Debug("retrieving malfeasant identities", + s.logger.Debug("retrieving malicious identities", log.ZContext(ctx), zap.Int("count", len(batch)), ) - if err := s.fetcher.LegacyMalfeasanceProofs(ctx, batch); err != nil { + if err := s.fetcher.MalfeasanceProofs(ctx, batch); err != nil { if errors.Is(err, context.Canceled) { return ctx.Err() } @@ -450,17 +668,33 @@ func (s *Syncer) downloadLegacyMalfeasanceProofs(ctx context.Context, initial bo } } -func (s *Syncer) EnsureInSync(parent context.Context, epochStart, epochEnd time.Time) error { +func (s *Syncer) EnsureLegacyInSync(ctx context.Context, epochStart, epochEnd time.Time) error { + if shouldSync, err := s.shouldSyncLegacy(epochStart, epochEnd); err != nil { + return err + } else if !shouldSync { + return nil + } + return s.downloadLegacy(ctx, true) +} + +func (s *Syncer) EnsureInSync(ctx context.Context, epochStart, epochEnd time.Time) error { if shouldSync, err := s.shouldSync(epochStart, epochEnd); err != nil { return err } else if !shouldSync { return nil } - return s.download(parent, true) + return s.download(ctx, true) } func (s *Syncer) DownloadLoop(parent context.Context) error { - return s.download(parent, false) + eg, ctx := errgroup.WithContext(parent) + eg.Go(func() error { + return s.downloadLegacy(ctx, false) + }) + eg.Go(func() error { + return s.download(ctx, false) + }) + return eg.Wait() } type malUpdate struct { diff --git a/syncer/malsync/syncer_test.go b/syncer/malsync/syncer_test.go index 18ab64253cd..fa4b40e6afa 100644 --- a/syncer/malsync/syncer_test.go +++ b/syncer/malsync/syncer_test.go @@ -23,6 +23,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/identities" "github.com/spacemeshos/go-spacemesh/sql/localsql" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" "github.com/spacemeshos/go-spacemesh/sql/statesql" "github.com/spacemeshos/go-spacemesh/syncer/malsync/mocks" ) @@ -175,8 +176,8 @@ func newTester(tb testing.TB, cfg Config) *tester { } } -func (tester *tester) expectGetMaliciousIDs() { - // "2" comes just from a single peer +func (tester *tester) expectLegacyMaliciousIDs() { + // "2" comes just from a single peer via legacy protocol tester.fetcher.EXPECT(). LegacyMaliciousIDs(gomock.Any(), tester.peers[0]). Return(malData("4", "1", "3", "2"), nil) @@ -187,24 +188,60 @@ func (tester *tester) expectGetMaliciousIDs() { } } -func (tester *tester) expectGetProofs(errMap map[types.NodeID]error) { +func (tester *tester) expectMaliciousIDs() { + // "102" comes just from a single peer tester.fetcher.EXPECT(). + MaliciousIDs(gomock.Any(), tester.peers[0]). + Return(malData("104", "101", "103", "102"), nil) + for _, p := range tester.peers[1:] { + tester.fetcher.EXPECT(). + MaliciousIDs(gomock.Any(), p). + Return(malData("104", "101", "103"), nil) + } +} + +func (t *tester) expectLegacyProofs(errMap map[types.NodeID]error) { + t.fetcher.EXPECT(). LegacyMalfeasanceProofs(gomock.Any(), gomock.Any()). DoAndReturn(func(_ context.Context, ids []types.NodeID) error { batchErr := &fetch.BatchError{ Errors: make(map[types.Hash32]error), } for _, id := range ids { - tester.attempts[id]++ - require.NotContains(tester.tb, tester.received, id) + t.attempts[id]++ + require.NotContains(t.tb, t.received, id) if err := errMap[id]; err != nil { batchErr.Errors[types.Hash32(id)] = err continue } - tester.received[id] = true + t.received[id] = true proofData := codec.MustEncode(mproof(id)) - require.NoError(tester.tb, identities.SetMalicious( - tester.db, id, proofData, tester.syncer.clock.Now())) + require.NoError(t.tb, identities.SetMalicious(t.db, id, proofData, t.syncer.clock.Now())) + } + if len(batchErr.Errors) != 0 { + return batchErr + } + return nil + }).AnyTimes() +} + +func (t *tester) expectProofs(errMap map[types.NodeID]error) { + t.fetcher.EXPECT(). + MalfeasanceProofs(gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, ids []types.NodeID) error { + batchErr := &fetch.BatchError{ + Errors: make(map[types.Hash32]error), + } + for _, id := range ids { + t.attempts[id]++ + require.NotContains(t.tb, t.received, id) + if err := errMap[id]; err != nil { + batchErr.Errors[types.Hash32(id)] = err + continue + } + t.received[id] = true + proof := codec.MustEncode(mproof(id)) + require.NoError(t.tb, malfeasance.AddProof(t.db, id, nil, proof, 1, t.syncer.clock.Now())) } if len(batchErr.Errors) != 0 { return batchErr @@ -218,14 +255,14 @@ func (tester *tester) expectPeers(peers []p2p.Peer) { } func TestSyncer(t *testing.T) { - t.Run("EnsureInSync", func(t *testing.T) { + t.Run("EnsureLegacyInSync", func(t *testing.T) { tester := newTester(t, DefaultConfig()) tester.expectPeers(tester.peers) - tester.expectGetMaliciousIDs() - tester.expectGetProofs(nil) + tester.expectLegacyMaliciousIDs() + tester.expectLegacyProofs(nil) epochStart := tester.clock.Now().Truncate(time.Second) epochEnd := epochStart.Add(10 * time.Minute) - require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.ElementsMatch(t, []types.NodeID{ nid("1"), nid("2"), nid("3"), nid("4"), }, maps.Keys(tester.received)) @@ -237,7 +274,41 @@ func TestSyncer(t *testing.T) { }, tester.attempts) tester.clock.Advance(1 * time.Minute) // second call does nothing after recent sync + require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) + require.Zero(t, tester.peerErrCount.n) + }) + t.Run("EnsureInSync", func(t *testing.T) { + tester := newTester(t, DefaultConfig()) + tester.expectPeers(tester.peers) + tester.expectMaliciousIDs() + tester.expectProofs(nil) + epochStart := tester.clock.Now().Truncate(time.Second) + epochEnd := epochStart.Add(10 * time.Minute) require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.ElementsMatch(t, []types.NodeID{ + nid("101"), nid("102"), nid("103"), nid("104"), + }, maps.Keys(tester.received)) + require.Equal(t, map[types.NodeID]int{ + nid("101"): 1, + nid("102"): 1, + nid("103"): 1, + nid("104"): 1, + }, tester.attempts) + tester.clock.Advance(1 * time.Minute) + // second call does nothing after recent sync + require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + }) + t.Run("EnsureLegacyInSync with no malfeasant identities", func(t *testing.T) { + tester := newTester(t, DefaultConfig()) + tester.expectPeers(tester.peers) + for _, p := range tester.peers { + tester.fetcher.EXPECT(). + LegacyMaliciousIDs(gomock.Any(), p). + Return(nil, nil) + } + epochStart := tester.clock.Now().Truncate(time.Second) + epochEnd := epochStart.Add(10 * time.Minute) + require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.Zero(t, tester.peerErrCount.n) }) t.Run("EnsureInSync with no malfeasant identities", func(t *testing.T) { @@ -245,13 +316,12 @@ func TestSyncer(t *testing.T) { tester.expectPeers(tester.peers) for _, p := range tester.peers { tester.fetcher.EXPECT(). - LegacyMaliciousIDs(gomock.Any(), p). + MaliciousIDs(gomock.Any(), p). Return(nil, nil) } epochStart := tester.clock.Now().Truncate(time.Second) epochEnd := epochStart.Add(10 * time.Minute) - require.NoError(t, - tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) require.Zero(t, tester.peerErrCount.n) }) t.Run("interruptible", func(t *testing.T) { @@ -265,6 +335,12 @@ func TestSyncer(t *testing.T) { tester.fetcher.EXPECT(). LegacyMalfeasanceProofs(gomock.Any(), gomock.Any()). Return(errors.New("no atxs")).AnyTimes() + tester.fetcher.EXPECT(). + MaliciousIDs(gomock.Any(), gomock.Any()). + Return(malData("101"), nil).AnyTimes() + tester.fetcher.EXPECT(). + MalfeasanceProofs(gomock.Any(), gomock.Any()). + Return(errors.New("no atxs")).AnyTimes() require.ErrorIs(t, tester.syncer.DownloadLoop(ctx), context.Canceled) }) t.Run("retries on no peers", func(t *testing.T) { @@ -280,16 +356,20 @@ func TestSyncer(t *testing.T) { require.ErrorIs(t, tester.syncer.DownloadLoop(ctx), context.Canceled) return nil }) - tester.clock.BlockUntilContext(context.Background(), 1) + tester.clock.BlockUntilContext(context.Background(), 2) tester.clock.Advance(tester.cfg.IDRequestInterval) ch <- nil - tester.clock.BlockUntilContext(context.Background(), 1) + ch <- nil + tester.clock.BlockUntilContext(context.Background(), 2) tester.clock.Advance(tester.cfg.IDRequestInterval) - tester.expectGetMaliciousIDs() - tester.expectGetProofs(nil) + tester.expectLegacyMaliciousIDs() + tester.expectLegacyProofs(nil) + tester.expectMaliciousIDs() + tester.expectProofs(nil) + ch <- tester.peers ch <- tester.peers - tester.clock.BlockUntilContext(context.Background(), 1) + tester.clock.BlockUntilContext(context.Background(), 2) cancel() eg.Wait() }) @@ -306,11 +386,11 @@ func TestSyncer(t *testing.T) { LegacyMaliciousIDs(gomock.Any(), p). Return(malData("4", "1", "3", "2"), nil) } - tester.expectGetProofs(nil) + tester.expectLegacyProofs(nil) epochStart := tester.clock.Now().Truncate(time.Second) epochEnd := epochStart.Add(10 * time.Minute) require.NoError(t, - tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.ElementsMatch(t, []types.NodeID{ nid("1"), nid("2"), nid("3"), nid("4"), }, maps.Keys(tester.received)) @@ -322,7 +402,7 @@ func TestSyncer(t *testing.T) { }, tester.attempts) tester.clock.Advance(1 * time.Minute) // second call does nothing after recent sync - require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.Equal(t, 1, tester.peerErrCount.n) }) t.Run("skip hashes after max retries", func(t *testing.T) { @@ -330,14 +410,13 @@ func TestSyncer(t *testing.T) { cfg.RequestsLimit = 3 tester := newTester(t, cfg) tester.expectPeers(tester.peers) - tester.expectGetMaliciousIDs() - tester.expectGetProofs(map[types.NodeID]error{ + tester.expectLegacyMaliciousIDs() + tester.expectLegacyProofs(map[types.NodeID]error{ nid("2"): errors.New("fail"), }) epochStart := tester.clock.Now().Truncate(time.Second) epochEnd := epochStart.Add(10 * time.Minute) - require.NoError(t, - tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.ElementsMatch(t, []types.NodeID{ nid("1"), nid("3"), nid("4"), }, maps.Keys(tester.received)) @@ -349,21 +428,20 @@ func TestSyncer(t *testing.T) { }, tester.attempts) tester.clock.Advance(1 * time.Minute) // second call does nothing after recent sync - require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) }) t.Run("skip hashes after validation reject", func(t *testing.T) { tester := newTester(t, DefaultConfig()) tester.expectPeers(tester.peers) - tester.expectGetMaliciousIDs() - tester.expectGetProofs(map[types.NodeID]error{ + tester.expectLegacyMaliciousIDs() + tester.expectLegacyProofs(map[types.NodeID]error{ // note that "2" comes just from a single peer - // (see expectGetMaliciousIDs) + // (see expectMaliciousIDs) nid("2"): pubsub.ErrValidationReject, }) epochStart := tester.clock.Now().Truncate(time.Second) epochEnd := epochStart.Add(10 * time.Minute) - require.NoError(t, - tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.ElementsMatch(t, []types.NodeID{ nid("1"), nid("3"), nid("4"), }, maps.Keys(tester.received)) @@ -375,6 +453,6 @@ func TestSyncer(t *testing.T) { }, tester.attempts) tester.clock.Advance(1 * time.Minute) // second call does nothing after recent sync - require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) }) } diff --git a/syncer/mocks/mocks.go b/syncer/mocks/mocks.go index fb40131f4a9..8c567af5a3c 100644 --- a/syncer/mocks/mocks.go +++ b/syncer/mocks/mocks.go @@ -778,6 +778,44 @@ func (c *MockmalSyncerEnsureInSyncCall) DoAndReturn(f func(context.Context, time return c } +// EnsureLegacyInSync mocks base method. +func (m *MockmalSyncer) EnsureLegacyInSync(parent context.Context, epochStart, epochEnd time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureLegacyInSync", parent, epochStart, epochEnd) + ret0, _ := ret[0].(error) + return ret0 +} + +// EnsureLegacyInSync indicates an expected call of EnsureLegacyInSync. +func (mr *MockmalSyncerMockRecorder) EnsureLegacyInSync(parent, epochStart, epochEnd any) *MockmalSyncerEnsureLegacyInSyncCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureLegacyInSync", reflect.TypeOf((*MockmalSyncer)(nil).EnsureLegacyInSync), parent, epochStart, epochEnd) + return &MockmalSyncerEnsureLegacyInSyncCall{Call: call} +} + +// MockmalSyncerEnsureLegacyInSyncCall wrap *gomock.Call +type MockmalSyncerEnsureLegacyInSyncCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockmalSyncerEnsureLegacyInSyncCall) Return(arg0 error) *MockmalSyncerEnsureLegacyInSyncCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockmalSyncerEnsureLegacyInSyncCall) Do(f func(context.Context, time.Time, time.Time) error) *MockmalSyncerEnsureLegacyInSyncCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockmalSyncerEnsureLegacyInSyncCall) DoAndReturn(f func(context.Context, time.Time, time.Time) error) *MockmalSyncerEnsureLegacyInSyncCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // Mockfetcher is a mock of fetcher interface. type Mockfetcher struct { ctrl *gomock.Controller diff --git a/syncer/syncer.go b/syncer/syncer.go index af05f1463e4..d227401b7d8 100644 --- a/syncer/syncer.go +++ b/syncer/syncer.go @@ -788,10 +788,17 @@ func (s *Syncer) setStateAfterSync(ctx context.Context, success bool) { } } -func (s *Syncer) syncMalfeasance(ctx context.Context, epoch types.EpochID) error { +func (s *Syncer) syncMalfeasance(parent context.Context, epoch types.EpochID) error { epochStart := s.ticker.LayerToTime(epoch.FirstLayer()) epochEnd := s.ticker.LayerToTime(epoch.Add(1).FirstLayer()) - if err := s.malsyncer.EnsureInSync(ctx, epochStart, epochEnd); err != nil { + eg, ctx := errgroup.WithContext(parent) + eg.Go(func() error { + return s.malsyncer.EnsureLegacyInSync(ctx, epochStart, epochEnd) + }) + eg.Go(func() error { + return s.malsyncer.EnsureInSync(ctx, epochStart, epochEnd) + }) + if err := eg.Wait(); err != nil { return fmt.Errorf("syncing malfeasance proof: %w", err) } return nil diff --git a/syncer/syncer_test.go b/syncer/syncer_test.go index febf439f161..cb25fc905c5 100644 --- a/syncer/syncer_test.go +++ b/syncer/syncer_test.go @@ -85,6 +85,11 @@ type testSyncer struct { } func (ts *testSyncer) expectMalEnsureInSync(current types.LayerID) { + ts.mMalSyncer.EXPECT().EnsureLegacyInSync( + gomock.Any(), + ts.mTicker.LayerToTime(current.GetEpoch().FirstLayer()), + ts.mTicker.LayerToTime(current.GetEpoch().Add(1).FirstLayer()), + ) ts.mMalSyncer.EXPECT().EnsureInSync( gomock.Any(), ts.mTicker.LayerToTime(current.GetEpoch().FirstLayer()), @@ -392,6 +397,7 @@ func TestSynchronize_FetchMalfeasanceFailed(t *testing.T) { ts.mTicker.advanceToLayer(current) lyr := current.Sub(1) ts.mAtxSyncer.EXPECT().Download(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + ts.mMalSyncer.EXPECT().EnsureLegacyInSync(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("meh")) ts.mMalSyncer.EXPECT().EnsureInSync(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("meh")) require.False(t, ts.syncer.synchronize(context.Background())) From 387736078f2a08ef92f37605d286968dc746054b Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 23:10:50 +0000 Subject: [PATCH 19/56] Fix import errors --- go.mod | 21 ++++++++++----------- go.sum | 46 ++++++++++++++++++++++++---------------------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/go.mod b/go.mod index 19cd6e16344..cc568a64d22 100644 --- a/go.mod +++ b/go.mod @@ -61,8 +61,8 @@ require ( golang.org/x/sync v0.10.0 golang.org/x/time v0.9.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f - google.golang.org/grpc v1.69.4 - google.golang.org/protobuf v1.36.3 + google.golang.org/grpc v1.70.0 + google.golang.org/protobuf v1.36.4 k8s.io/api v0.32.1 k8s.io/apimachinery v0.32.1 k8s.io/client-go v0.32.1 @@ -70,7 +70,7 @@ require ( ) require ( - cel.dev/expr v0.16.2 // indirect + cel.dev/expr v0.19.0 // indirect cloud.google.com/go v0.116.0 // indirect cloud.google.com/go/auth v0.13.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect @@ -88,7 +88,6 @@ require ( github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/c0mm4nd/go-ripemd v0.0.0-20200326052756-bd1759ad7d10 // indirect - github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/containerd/cgroups v1.1.0 // indirect @@ -99,7 +98,7 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/elastic/gosigar v0.14.3 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect - github.com/envoyproxy/go-control-plane v0.13.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.3 // indirect github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect @@ -223,14 +222,14 @@ require ( github.com/wlynxg/anet v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.31.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.32.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect - go.opentelemetry.io/otel/metric v1.31.0 // indirect - go.opentelemetry.io/otel/sdk v1.31.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/sdk v1.32.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect go.uber.org/dig v1.18.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.32.0 // indirect diff --git a/go.sum b/go.sum index 703c9296e6e..b1e1dcb2f07 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.16.2 h1:RwRhoH17VhAu9U5CMvMhH1PDVgf0tuz9FT+24AfMLfU= -cel.dev/expr v0.16.2/go.mod h1:gXngZQMkWJoSbE8mOzehJlXQyubn/Vg0vR9/F3W7iw8= +cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0= +cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -74,8 +74,6 @@ github.com/bxcodec/faker v2.0.1+incompatible/go.mod h1:BNzfpVdTwnFJ6GtfYTcQu6l6r github.com/c0mm4nd/go-ripemd v0.0.0-20200326052756-bd1759ad7d10 h1:wJ2csnFApV9G1jgh5KmYdxVOQMi+fihIggVTjcbM7ts= github.com/c0mm4nd/go-ripemd v0.0.0-20200326052756-bd1759ad7d10/go.mod h1:mYPR+a1fzjnHY3VFH5KL3PkEjMlVfGXP7c8rbWlkLJg= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chaos-mesh/chaos-mesh/api v0.0.0-20250108051104-b3d81ecc62fa h1:0OwWndUnfgo4ZC1jKF08aRZmPFxHGEBpa5AQPYZOu5E= @@ -122,8 +120,12 @@ github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRr github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= -github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.3 h1:hVEaommgvzTjTd4xCaFd+kEQ2iYBtGxP6luyLrx6uOk= +github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= @@ -709,24 +711,24 @@ github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/detectors/gcp v1.31.0 h1:G1JQOreVrfhRkner+l4mrGxmfqYCAuy76asTDAo0xsA= -go.opentelemetry.io/contrib/detectors/gcp v1.31.0/go.mod h1:tzQL6E1l+iV44YFTkcAeNQqzXUiekSYP9jjJjXwEd00= +go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8= +go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= @@ -950,8 +952,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= -google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -961,8 +963,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 7393c448a51370810ac2295bb1201e4d78908b55 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 24 Jan 2025 23:22:40 +0000 Subject: [PATCH 20/56] Add more tests --- datastore/store_test.go | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/datastore/store_test.go b/datastore/store_test.go index 061f18c9663..c5bdce96480 100644 --- a/datastore/store_test.go +++ b/datastore/store_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/codec" @@ -22,6 +23,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/ballots" "github.com/spacemeshos/go-spacemesh/sql/blocks" "github.com/spacemeshos/go-spacemesh/sql/identities" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" "github.com/spacemeshos/go-spacemesh/sql/poets" "github.com/spacemeshos/go-spacemesh/sql/statesql" "github.com/spacemeshos/go-spacemesh/sql/transactions" @@ -272,7 +274,7 @@ func TestBlobStore_GetTXBlob(t *testing.T) { require.Equal(t, tx.Raw, blob.Bytes) } -func TestBlobStore_GetMalfeasanceBlob(t *testing.T) { +func TestBlobStore_GetLegacyMalfeasanceBlob(t *testing.T) { db := statesql.InMemoryTest(t) bs := datastore.NewBlobStore(db, store.New()) @@ -307,6 +309,37 @@ func TestBlobStore_GetMalfeasanceBlob(t *testing.T) { require.Equal(t, encoded, blob.Bytes) } +func TestBlobStore_GetMalfeasanceBlob(t *testing.T) { + db := statesql.InMemoryTest(t) + bs := datastore.NewBlobStore(db, store.New()) + + ctrl := gomock.NewController(t) + mMal := datastore.NewMockMalfeasanceProvider(ctrl) + bs.SetMalfeasanceProvider(mMal) + + proofBytes := types.RandomBytes(100) + nodeID := types.NodeID{1, 2, 3} + + has, err := bs.Has(datastore.Malfeasance, nodeID.Bytes()) + require.NoError(t, err) + require.False(t, has) + + mMal.EXPECT().ProofByID(gomock.Any(), nodeID).Return(nil, sql.ErrNotFound) + var blob sql.Blob + err = bs.LoadBlob(context.Background(), datastore.Malfeasance, nodeID.Bytes(), &blob) + require.ErrorIs(t, err, datastore.ErrNotFound) + + require.NoError(t, malfeasance.AddProof(db, nodeID, nil, proofBytes, 1, time.Now())) + has, err = bs.Has(datastore.Malfeasance, nodeID.Bytes()) + require.NoError(t, err) + require.True(t, has) + + mMal.EXPECT().ProofByID(gomock.Any(), nodeID).Return(proofBytes, nil) + err = bs.LoadBlob(context.Background(), datastore.Malfeasance, nodeID.Bytes(), &blob) + require.NoError(t, err) + require.Equal(t, proofBytes, blob.Bytes) +} + func TestBlobStore_GetActiveSet(t *testing.T) { db := statesql.InMemoryTest(t) bs := datastore.NewBlobStore(db, store.New()) From b560110ae3c587302c740ac792c3132dc301f774 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Sat, 25 Jan 2025 00:38:17 +0000 Subject: [PATCH 21/56] Revert some changes --- syncer/malsync/syncer.go | 108 +++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/syncer/malsync/syncer.go b/syncer/malsync/syncer.go index 78b2fd1950e..aeadd18eb04 100644 --- a/syncer/malsync/syncer.go +++ b/syncer/malsync/syncer.go @@ -529,7 +529,7 @@ func (s *Syncer) downloadLegacyMalfeasanceProofs(ctx context.Context, initial bo batch, err := sst.missing(s.cfg.MaxBatchSize, func(nodeID types.NodeID) (bool, error) { // TODO(ivan4th): check multiple node IDs at once in a single SQL query isMalicious, err := identities.IsMalicious(s.db, nodeID) - if errors.Is(err, sql.ErrNotFound) { + if err != nil && errors.Is(err, sql.ErrNotFound) { return false, nil } return isMalicious, err @@ -539,36 +539,36 @@ func (s *Syncer) downloadLegacyMalfeasanceProofs(ctx context.Context, initial bo } nothingToDownload = len(batch) == 0 - if len(batch) == 0 { - s.logger.Debug("no new legacy malicious identities", log.ZContext(ctx)) - continue - } - s.logger.Debug("retrieving legacy malicious identities", - log.ZContext(ctx), - zap.Int("count", len(batch)), - ) - if err := s.fetcher.LegacyMalfeasanceProofs(ctx, batch); err != nil { - if errors.Is(err, context.Canceled) { - return ctx.Err() - } - s.logger.Debug("failed to download malfeasance proofs", + if len(batch) != 0 { + s.logger.Debug("retrieving legacy malicious identities", log.ZContext(ctx), - log.NiceZapError(err), + zap.Int("count", len(batch)), ) - } - batchError := &fetch.BatchError{} - if errors.As(err, &batchError) { - for hash, err := range batchError.Errors { - nodeID := types.NodeID(hash) - switch { - case !sst.has(nodeID): - continue - case errors.Is(err, pubsub.ErrValidationReject): - sst.rejected(nodeID) - default: - sst.failed(nodeID) + if err := s.fetcher.LegacyMalfeasanceProofs(ctx, batch); err != nil { + if errors.Is(err, context.Canceled) { + return ctx.Err() } + s.logger.Debug("failed to download malfeasance proofs", + log.ZContext(ctx), + log.NiceZapError(err), + ) } + batchError := &fetch.BatchError{} + if errors.As(err, &batchError) { + for hash, err := range batchError.Errors { + nodeID := types.NodeID(hash) + switch { + case !sst.has(nodeID): + continue + case errors.Is(err, pubsub.ErrValidationReject): + sst.rejected(nodeID) + default: + sst.failed(nodeID) + } + } + } + } else { + s.logger.Debug("no new legacy malicious identities", log.ZContext(ctx)) } } } @@ -624,7 +624,7 @@ func (s *Syncer) downloadMalfeasanceProofs(ctx context.Context, initial bool, up batch, err := sst.missing(s.cfg.MaxBatchSize, func(nodeID types.NodeID) (bool, error) { // TODO(mafa): check multiple node IDs at once in a single SQL query isMalicious, err := malfeasance.IsMalicious(s.db, nodeID) - if errors.Is(err, sql.ErrNotFound) { + if err != nil && errors.Is(err, sql.ErrNotFound) { return false, nil } return isMalicious, err @@ -634,36 +634,36 @@ func (s *Syncer) downloadMalfeasanceProofs(ctx context.Context, initial bool, up } nothingToDownload = len(batch) == 0 - if len(batch) == 0 { - s.logger.Debug("no new malicious identities", log.ZContext(ctx)) - continue - } - s.logger.Debug("retrieving malicious identities", - log.ZContext(ctx), - zap.Int("count", len(batch)), - ) - if err := s.fetcher.MalfeasanceProofs(ctx, batch); err != nil { - if errors.Is(err, context.Canceled) { - return ctx.Err() - } - s.logger.Debug("failed to download malfeasance proofs", + if len(batch) != 0 { + s.logger.Debug("retrieving malicious identities", log.ZContext(ctx), - log.NiceZapError(err), + zap.Int("count", len(batch)), ) - } - batchError := &fetch.BatchError{} - if errors.As(err, &batchError) { - for hash, err := range batchError.Errors { - nodeID := types.NodeID(hash) - switch { - case !sst.has(nodeID): - continue - case errors.Is(err, pubsub.ErrValidationReject): - sst.rejected(nodeID) - default: - sst.failed(nodeID) + if err := s.fetcher.MalfeasanceProofs(ctx, batch); err != nil { + if errors.Is(err, context.Canceled) { + return ctx.Err() } + s.logger.Debug("failed to download malfeasance proofs", + log.ZContext(ctx), + log.NiceZapError(err), + ) } + batchError := &fetch.BatchError{} + if errors.As(err, &batchError) { + for hash, err := range batchError.Errors { + nodeID := types.NodeID(hash) + switch { + case !sst.has(nodeID): + continue + case errors.Is(err, pubsub.ErrValidationReject): + sst.rejected(nodeID) + default: + sst.failed(nodeID) + } + } + } + } else { + s.logger.Debug("no new malicious identities", log.ZContext(ctx)) } } } From 06d9865e6a523161ef772db63dda90a718229cb2 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Sat, 25 Jan 2025 00:45:52 +0000 Subject: [PATCH 22/56] fix failing tests --- fetch/handler.go | 35 +++++------ syncer/malsync/syncer.go | 114 +++++++++++++++++----------------- syncer/malsync/syncer_test.go | 54 +++++++++++++++- 3 files changed, 126 insertions(+), 77 deletions(-) diff --git a/fetch/handler.go b/fetch/handler.go index 8b8bb176ad1..d36bf327215 100644 --- a/fetch/handler.go +++ b/fetch/handler.go @@ -101,26 +101,23 @@ func (h *handler) handleLegacyMaliciousIDsReqStream(ctx context.Context, _ p2p.P } func (h *handler) handleMaliciousIDsReqStream(ctx context.Context, _ p2p.Peer, _ []byte, s io.ReadWriter) error { - tx, err := h.db.TxImmediate(ctx) - if err != nil { - h.logger.Debug("failed to stream malicious node IDs", log.ZContext(ctx), zap.Error(err)) - return nil - } - defer tx.Release() - total, err := malfeasance.Count(tx) + err := h.streamIDs(ctx, s, func(cbk retrieveCallback) error { + return h.db.WithTxImmediate(ctx, func(tx sql.Transaction) error { + total, err := malfeasance.Count(tx) + if err != nil { + return fmt.Errorf("counting malicious nodes: %w", err) + } + return malfeasance.IterateOps(tx, builder.Operations{}, + func(nodeID types.NodeID, _ []byte, _ int, _ time.Time) bool { + if err := cbk(total, nodeID.Bytes()); err != nil { + h.logger.Debug("failed to stream malicious node IDs", log.ZContext(ctx), zap.Error(err)) + return false + } + return true + }) + }) + }) if err != nil { - return fmt.Errorf("counting malicious nodes: %w", err) - } - if err := h.streamIDs(ctx, s, func(cbk retrieveCallback) error { - return malfeasance.IterateOps(tx, builder.Operations{}, - func(nodeID types.NodeID, _ []byte, _ int, _ time.Time) bool { - if err := cbk(total, nodeID.Bytes()); err != nil { - h.logger.Debug("failed to stream malicious node IDs", log.ZContext(ctx), zap.Error(err)) - return false - } - return true - }) - }); err != nil { h.logger.Debug("failed to stream malicious node IDs", log.ZContext(ctx), zap.Error(err)) } return nil diff --git a/syncer/malsync/syncer.go b/syncer/malsync/syncer.go index aeadd18eb04..afd6344aae4 100644 --- a/syncer/malsync/syncer.go +++ b/syncer/malsync/syncer.go @@ -539,36 +539,37 @@ func (s *Syncer) downloadLegacyMalfeasanceProofs(ctx context.Context, initial bo } nothingToDownload = len(batch) == 0 - if len(batch) != 0 { - s.logger.Debug("retrieving legacy malicious identities", - log.ZContext(ctx), - zap.Int("count", len(batch)), - ) - if err := s.fetcher.LegacyMalfeasanceProofs(ctx, batch); err != nil { - if errors.Is(err, context.Canceled) { - return ctx.Err() - } - s.logger.Debug("failed to download malfeasance proofs", - log.ZContext(ctx), - log.NiceZapError(err), - ) - } - batchError := &fetch.BatchError{} - if errors.As(err, &batchError) { - for hash, err := range batchError.Errors { - nodeID := types.NodeID(hash) - switch { - case !sst.has(nodeID): - continue - case errors.Is(err, pubsub.ErrValidationReject): - sst.rejected(nodeID) - default: - sst.failed(nodeID) - } + if len(batch) == 0 { + s.logger.Debug("no new legacy malicious identities", log.ZContext(ctx)) + continue + } + + s.logger.Debug("retrieving legacy malicious identities", + log.ZContext(ctx), + zap.Int("count", len(batch)), + ) + batchError := &fetch.BatchError{} + err = s.fetcher.LegacyMalfeasanceProofs(ctx, batch) + switch { + case errors.Is(err, context.Canceled): + return ctx.Err() + case errors.As(err, &batchError): + for hash, err := range batchError.Errors { + nodeID := types.NodeID(hash) + switch { + case !sst.has(nodeID): + continue + case errors.Is(err, pubsub.ErrValidationReject): + sst.rejected(nodeID) + default: + sst.failed(nodeID) } } - } else { - s.logger.Debug("no new legacy malicious identities", log.ZContext(ctx)) + case err != nil: + s.logger.Debug("failed to download malfeasance proofs", + log.ZContext(ctx), + log.NiceZapError(err), + ) } } } @@ -634,36 +635,37 @@ func (s *Syncer) downloadMalfeasanceProofs(ctx context.Context, initial bool, up } nothingToDownload = len(batch) == 0 - if len(batch) != 0 { - s.logger.Debug("retrieving malicious identities", - log.ZContext(ctx), - zap.Int("count", len(batch)), - ) - if err := s.fetcher.MalfeasanceProofs(ctx, batch); err != nil { - if errors.Is(err, context.Canceled) { - return ctx.Err() - } - s.logger.Debug("failed to download malfeasance proofs", - log.ZContext(ctx), - log.NiceZapError(err), - ) - } - batchError := &fetch.BatchError{} - if errors.As(err, &batchError) { - for hash, err := range batchError.Errors { - nodeID := types.NodeID(hash) - switch { - case !sst.has(nodeID): - continue - case errors.Is(err, pubsub.ErrValidationReject): - sst.rejected(nodeID) - default: - sst.failed(nodeID) - } + if len(batch) == 0 { + s.logger.Debug("no new malicious identities", log.ZContext(ctx)) + continue + } + + s.logger.Debug("retrieving malicious identities", + log.ZContext(ctx), + zap.Int("count", len(batch)), + ) + batchError := &fetch.BatchError{} + err = s.fetcher.MalfeasanceProofs(ctx, batch) + switch { + case errors.Is(err, context.Canceled): + return ctx.Err() + case errors.As(err, &batchError): + for hash, err := range batchError.Errors { + nodeID := types.NodeID(hash) + switch { + case !sst.has(nodeID): + continue + case errors.Is(err, pubsub.ErrValidationReject): + sst.rejected(nodeID) + default: + sst.failed(nodeID) } } - } else { - s.logger.Debug("no new malicious identities", log.ZContext(ctx)) + case err != nil: + s.logger.Debug("failed to download malfeasance proofs", + log.ZContext(ctx), + log.NiceZapError(err), + ) } } } diff --git a/syncer/malsync/syncer_test.go b/syncer/malsync/syncer_test.go index fa4b40e6afa..f5ed95c072b 100644 --- a/syncer/malsync/syncer_test.go +++ b/syncer/malsync/syncer_test.go @@ -405,7 +405,7 @@ func TestSyncer(t *testing.T) { require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.Equal(t, 1, tester.peerErrCount.n) }) - t.Run("skip hashes after max retries", func(t *testing.T) { + t.Run("skip hashes after max retries - legacy", func(t *testing.T) { cfg := DefaultConfig() cfg.RequestsLimit = 3 tester := newTester(t, cfg) @@ -430,7 +430,32 @@ func TestSyncer(t *testing.T) { // second call does nothing after recent sync require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) }) - t.Run("skip hashes after validation reject", func(t *testing.T) { + t.Run("skip hashes after max retries", func(t *testing.T) { + cfg := DefaultConfig() + cfg.RequestsLimit = 3 + tester := newTester(t, cfg) + tester.expectPeers(tester.peers) + tester.expectMaliciousIDs() + tester.expectProofs(map[types.NodeID]error{ + nid("102"): errors.New("fail"), + }) + epochStart := tester.clock.Now().Truncate(time.Second) + epochEnd := epochStart.Add(10 * time.Minute) + require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.ElementsMatch(t, []types.NodeID{ + nid("101"), nid("103"), nid("104"), + }, maps.Keys(tester.received)) + require.Equal(t, map[types.NodeID]int{ + nid("101"): 1, + nid("102"): tester.cfg.RequestsLimit, + nid("103"): 1, + nid("104"): 1, + }, tester.attempts) + tester.clock.Advance(1 * time.Minute) + // second call does nothing after recent sync + require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + }) + t.Run("skip hashes after validation reject - legacy", func(t *testing.T) { tester := newTester(t, DefaultConfig()) tester.expectPeers(tester.peers) tester.expectLegacyMaliciousIDs() @@ -455,4 +480,29 @@ func TestSyncer(t *testing.T) { // second call does nothing after recent sync require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) }) + t.Run("skip hashes after validation reject", func(t *testing.T) { + tester := newTester(t, DefaultConfig()) + tester.expectPeers(tester.peers) + tester.expectMaliciousIDs() + tester.expectProofs(map[types.NodeID]error{ + // note that "102" comes just from a single peer + // (see expectMaliciousIDs) + nid("102"): pubsub.ErrValidationReject, + }) + epochStart := tester.clock.Now().Truncate(time.Second) + epochEnd := epochStart.Add(10 * time.Minute) + require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.ElementsMatch(t, []types.NodeID{ + nid("101"), nid("103"), nid("104"), + }, maps.Keys(tester.received)) + require.Equal(t, map[types.NodeID]int{ + nid("101"): 1, + nid("102"): 1, + nid("103"): 1, + nid("104"): 1, + }, tester.attempts) + tester.clock.Advance(1 * time.Minute) + // second call does nothing after recent sync + require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + }) } From 58f3e02654426fe8c64b9ac2021e4007ce30bf2e Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Sat, 25 Jan 2025 02:18:26 +0000 Subject: [PATCH 23/56] Check for malfeasance old and new --- activation/handler_v1.go | 13 +++++++++++++ checkpoint/runner.go | 7 ++++++- mesh/mesh_test.go | 3 --- tortoise/model/core.go | 7 ++++++- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/activation/handler_v1.go b/activation/handler_v1.go index 653727bc1d1..2eeb4f3af28 100644 --- a/activation/handler_v1.go +++ b/activation/handler_v1.go @@ -27,6 +27,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" "github.com/spacemeshos/go-spacemesh/sql/identities" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" "github.com/spacemeshos/go-spacemesh/system" ) @@ -236,6 +237,13 @@ func (h *HandlerV1) syntacticallyValidateDeps( if malicious { return nil, fmt.Errorf("smesher %s is known malfeasant", watx.SmesherID.ShortString()) } + malicious, err = malfeasance.IsMalicious(h.cdb, watx.SmesherID) + if err != nil { + return nil, fmt.Errorf("check if smesher is malicious: %w", err) + } + if malicious { + return nil, fmt.Errorf("smesher %s is known malfeasant", watx.SmesherID.ShortString()) + } proof := &mwire.MalfeasanceProof{ Layer: watx.PublishEpoch.FirstLayer(), Proof: mwire.Proof{ @@ -489,6 +497,11 @@ func (h *HandlerV1) storeAtx(ctx context.Context, atx *types.ActivationTx, watx if err != nil { return fmt.Errorf("check if node is malicious: %w", err) } + malicious2, err := malfeasance.IsMalicious(tx, atx.SmesherID) + if err != nil { + return fmt.Errorf("check if node is malicious: %w", err) + } + malicious = malicious || malicious2 if !malicious { malicious, err = h.checkMalicious(ctx, tx, watx) if err != nil { diff --git a/checkpoint/runner.go b/checkpoint/runner.go index 614a7d7ffeb..a1413ca9c24 100644 --- a/checkpoint/runner.go +++ b/checkpoint/runner.go @@ -15,6 +15,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/atxs" "github.com/spacemeshos/go-spacemesh/sql/builder" "github.com/spacemeshos/go-spacemesh/sql/identities" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" "github.com/spacemeshos/go-spacemesh/sql/marriage" ) @@ -68,7 +69,11 @@ func checkpointDB( if err != nil { return nil, fmt.Errorf("atxs snapshot check identity: %w", err) } - malicious[catx.SmesherID] = mal + mal2, err := malfeasance.IsMalicious(tx, catx.SmesherID) + if err != nil { + return nil, fmt.Errorf("atxs snapshot check malfeasance: %w", err) + } + malicious[catx.SmesherID] = mal || mal2 } commitmentAtx, err := atxs.CommitmentATX(tx, catx.SmesherID) if err != nil { diff --git a/mesh/mesh_test.go b/mesh/mesh_test.go index f76c1a77df4..8a4d7115c5f 100644 --- a/mesh/mesh_test.go +++ b/mesh/mesh_test.go @@ -383,9 +383,6 @@ func TestMesh_MaliciousBallots(t *testing.T) { require.NoError(t, err) require.Nil(t, malProof) require.False(t, blts[0].IsMalicious()) - mal, err := identities.IsMalicious(tm.cdb, sig.NodeID()) - require.NoError(t, err) - require.False(t, mal) malicious, err := identities.IsMalicious(tm.cdb, sig.NodeID()) require.NoError(t, err) diff --git a/tortoise/model/core.go b/tortoise/model/core.go index cd762fb1025..38750ab3e7a 100644 --- a/tortoise/model/core.go +++ b/tortoise/model/core.go @@ -22,6 +22,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/certificates" "github.com/spacemeshos/go-spacemesh/sql/identities" "github.com/spacemeshos/go-spacemesh/sql/layers" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" "github.com/spacemeshos/go-spacemesh/sql/statesql" "github.com/spacemeshos/go-spacemesh/tortoise" ) @@ -184,7 +185,11 @@ func (c *core) OnMessage(m Messenger, event Message) { if err != nil { c.logger.Fatal("failed is malicious lookup", zap.Error(err)) } - c.atxdata.AddFromAtx(ev.Atx, malicious) + malicious2, err := malfeasance.IsMalicious(c.cdb, ev.Atx.SmesherID) + if err != nil { + c.logger.Fatal("failed is malicious lookup", zap.Error(err)) + } + c.atxdata.AddFromAtx(ev.Atx, malicious || malicious2) case MessageBeacon: beacons.Add(c.cdb, ev.EpochID+1, ev.Beacon) case MessageCoinflip: From 2f38268eefe7b03852a7ea5d7fb239a2b8f4c60f Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Sat, 25 Jan 2025 02:18:32 +0000 Subject: [PATCH 24/56] Fix data race in test --- syncer/malsync/syncer_test.go | 101 +++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 32 deletions(-) diff --git a/syncer/malsync/syncer_test.go b/syncer/malsync/syncer_test.go index f5ed95c072b..28c7e4c042e 100644 --- a/syncer/malsync/syncer_test.go +++ b/syncer/malsync/syncer_test.go @@ -137,16 +137,19 @@ func malData(ids ...string) []types.NodeID { } type tester struct { - tb testing.TB - syncer *Syncer - db sql.StateDatabase - cfg Config - fetcher *mocks.Mockfetcher - clock *clockwork.FakeClock - received map[types.NodeID]bool - attempts map[types.NodeID]int - peers []p2p.Peer - peerErrCount *fakeCounter + tb testing.TB + syncer *Syncer + db sql.StateDatabase + cfg Config + fetcher *mocks.Mockfetcher + clock *clockwork.FakeClock + + peers []p2p.Peer + peerErrCount *fakeCounter + receivedLegacy map[types.NodeID]bool + attemptsLegacy map[types.NodeID]int + received map[types.NodeID]bool + attempts map[types.NodeID]int } func newTester(tb testing.TB, cfg Config) *tester { @@ -163,16 +166,18 @@ func newTester(tb testing.TB, cfg Config) *tester { WithPeerErrMetric(peerErrCount), ) return &tester{ - tb: tb, - syncer: syncer, - db: db, - cfg: cfg, - fetcher: fetcher, - clock: clock, - received: make(map[types.NodeID]bool), - attempts: make(map[types.NodeID]int), - peers: []p2p.Peer{"a", "b", "c"}, - peerErrCount: peerErrCount, + tb: tb, + syncer: syncer, + db: db, + cfg: cfg, + fetcher: fetcher, + clock: clock, + receivedLegacy: make(map[types.NodeID]bool), + attemptsLegacy: make(map[types.NodeID]int), + received: make(map[types.NodeID]bool), + attempts: make(map[types.NodeID]int), + peers: []p2p.Peer{"a", "b", "c"}, + peerErrCount: peerErrCount, } } @@ -208,13 +213,13 @@ func (t *tester) expectLegacyProofs(errMap map[types.NodeID]error) { Errors: make(map[types.Hash32]error), } for _, id := range ids { - t.attempts[id]++ - require.NotContains(t.tb, t.received, id) + t.attemptsLegacy[id]++ + require.NotContains(t.tb, t.receivedLegacy, id) if err := errMap[id]; err != nil { batchErr.Errors[types.Hash32(id)] = err continue } - t.received[id] = true + t.receivedLegacy[id] = true proofData := codec.MustEncode(mproof(id)) require.NoError(t.tb, identities.SetMalicious(t.db, id, proofData, t.syncer.clock.Now())) } @@ -265,13 +270,13 @@ func TestSyncer(t *testing.T) { require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.ElementsMatch(t, []types.NodeID{ nid("1"), nid("2"), nid("3"), nid("4"), - }, maps.Keys(tester.received)) + }, maps.Keys(tester.receivedLegacy)) require.Equal(t, map[types.NodeID]int{ nid("1"): 1, nid("2"): 1, nid("3"): 1, nid("4"): 1, - }, tester.attempts) + }, tester.attemptsLegacy) tester.clock.Advance(1 * time.Minute) // second call does nothing after recent sync require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) @@ -373,7 +378,7 @@ func TestSyncer(t *testing.T) { cancel() eg.Wait() }) - t.Run("getting ids from MinSyncPeers peers is enough", func(t *testing.T) { + t.Run("getting ids from MinSyncPeers peers is enough - legacy", func(t *testing.T) { cfg := DefaultConfig() cfg.MinSyncPeers = 2 tester := newTester(t, cfg) @@ -393,18 +398,50 @@ func TestSyncer(t *testing.T) { tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.ElementsMatch(t, []types.NodeID{ nid("1"), nid("2"), nid("3"), nid("4"), - }, maps.Keys(tester.received)) + }, maps.Keys(tester.receivedLegacy)) require.Equal(t, map[types.NodeID]int{ nid("1"): 1, nid("2"): 1, nid("3"): 1, nid("4"): 1, - }, tester.attempts) + }, tester.attemptsLegacy) tester.clock.Advance(1 * time.Minute) // second call does nothing after recent sync require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.Equal(t, 1, tester.peerErrCount.n) }) + t.Run("getting ids from MinSyncPeers peers is enough", func(t *testing.T) { + cfg := DefaultConfig() + cfg.MinSyncPeers = 2 + tester := newTester(t, cfg) + tester.expectPeers(tester.peers) + tester.fetcher.EXPECT(). + MaliciousIDs(gomock.Any(), tester.peers[0]). + Return(nil, errors.New("fail")) + for _, p := range tester.peers[1:] { + tester.fetcher.EXPECT(). + MaliciousIDs(gomock.Any(), p). + Return(malData("104", "101", "103", "102"), nil) + } + tester.expectProofs(nil) + epochStart := tester.clock.Now().Truncate(time.Second) + epochEnd := epochStart.Add(10 * time.Minute) + require.NoError(t, + tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.ElementsMatch(t, []types.NodeID{ + nid("101"), nid("102"), nid("103"), nid("104"), + }, maps.Keys(tester.received)) + require.Equal(t, map[types.NodeID]int{ + nid("101"): 1, + nid("102"): 1, + nid("103"): 1, + nid("104"): 1, + }, tester.attempts) + tester.clock.Advance(1 * time.Minute) + // second call does nothing after recent sync + require.NoError(t, tester.syncer.EnsureInSync(context.Background(), epochStart, epochEnd)) + require.Equal(t, 1, tester.peerErrCount.n) + }) t.Run("skip hashes after max retries - legacy", func(t *testing.T) { cfg := DefaultConfig() cfg.RequestsLimit = 3 @@ -419,13 +456,13 @@ func TestSyncer(t *testing.T) { require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.ElementsMatch(t, []types.NodeID{ nid("1"), nid("3"), nid("4"), - }, maps.Keys(tester.received)) + }, maps.Keys(tester.receivedLegacy)) require.Equal(t, map[types.NodeID]int{ nid("1"): 1, nid("2"): tester.cfg.RequestsLimit, nid("3"): 1, nid("4"): 1, - }, tester.attempts) + }, tester.attemptsLegacy) tester.clock.Advance(1 * time.Minute) // second call does nothing after recent sync require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) @@ -469,13 +506,13 @@ func TestSyncer(t *testing.T) { require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) require.ElementsMatch(t, []types.NodeID{ nid("1"), nid("3"), nid("4"), - }, maps.Keys(tester.received)) + }, maps.Keys(tester.receivedLegacy)) require.Equal(t, map[types.NodeID]int{ nid("1"): 1, nid("2"): 1, nid("3"): 1, nid("4"): 1, - }, tester.attempts) + }, tester.attemptsLegacy) tester.clock.Advance(1 * time.Minute) // second call does nothing after recent sync require.NoError(t, tester.syncer.EnsureLegacyInSync(context.Background(), epochStart, epochEnd)) From aae80aca21554ad04633a140a7679f280722b489 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:02:41 +0000 Subject: [PATCH 25/56] Split up test into sections --- ...tributed_post_verification_helpers_test.go | 170 ++++++++++ .../distributed_post_verification_test.go | 314 +++++++----------- 2 files changed, 295 insertions(+), 189 deletions(-) create mode 100644 systest/tests/distributed_post_verification_helpers_test.go diff --git a/systest/tests/distributed_post_verification_helpers_test.go b/systest/tests/distributed_post_verification_helpers_test.go new file mode 100644 index 00000000000..15df1cbb4ac --- /dev/null +++ b/systest/tests/distributed_post_verification_helpers_test.go @@ -0,0 +1,170 @@ +package tests + +import ( + "testing" + "time" + + "github.com/spacemeshos/post/shared" + "github.com/spacemeshos/post/verifying" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "github.com/spacemeshos/go-spacemesh/activation" + "github.com/spacemeshos/go-spacemesh/activation/wire" + "github.com/spacemeshos/go-spacemesh/api/grpcserver" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/config" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/localsql/nipost" + "github.com/spacemeshos/go-spacemesh/systest/cluster" + "github.com/spacemeshos/go-spacemesh/systest/testcontext" + "github.com/spacemeshos/go-spacemesh/timesync" +) + +func createInitialAtxV1(t testing.TB, + ctx *testcontext.Context, + logger *zap.Logger, + cl *cluster.Cluster, + cfg *config.Config, + signer *signing.EdSigner, + db sql.StateDatabase, + localDb sql.LocalDatabase, + clock *timesync.NodeClock, + verifier activation.PostVerifier, +) wire.ActivationTxV1 { + // 2. create ATX with invalid POST labels + grpcPostService := grpcserver.NewPostService( + logger.Named("grpc-post-service"), + grpcserver.PostServiceQueryInterval(500*time.Millisecond), + ) + grpcPostService.AllowConnections(true) + + 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()) }) + + // 2.1. Create initial POST + certClient := activation.NewCertifierClient(db, localDb, logger.Named("certifier")) + certifier := activation.NewCertifier(localDb, logger, certClient) + poetDb, err := activation.NewPoetDb(db, zap.NewNop()) + require.NoError(t, err) + poetService, err := activation.NewPoetService( + poetDb, + types.PoetServer{Address: cluster.MakePoetGlobalEndpoint(ctx.Namespace, 0)}, + cfg.POET, + logger, + 1, + activation.WithCertifier(certifier), + ) + require.NoError(t, err) + + validator := activation.NewValidator( + db, + poetDb, + cfg.POST, + cfg.SMESHING.Opts.Scrypt, + verifier, + ) + + nipostBuilder, err := activation.NewNIPostBuilder( + localDb, + grpcPostService, + logger.Named("nipostBuilder"), + cfg.POET, + clock, + validator, + activation.WithPoetServices(poetService), + ) + require.NoError(t, err) + + var challenge *wire.NIPostChallengeV1 + 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) + + err = nipost.AddPost(localDb, signer.NodeID(), nipost.Post{ + Nonce: post.Nonce, + Indices: post.Indices, + Pow: post.Pow, + Challenge: shared.ZeroChallenge, + NumUnits: postInfo.NumUnits, + CommitmentATX: postInfo.CommitmentATX, + VRFNonce: *postInfo.Nonce, + }) + require.NoError(t, err) + + challenge = &wire.NIPostChallengeV1{ + PrevATXID: types.EmptyATXID, + PublishEpoch: 1, + PositioningATXID: cl.GoldenATX(), + CommitmentATXID: &postInfo.CommitmentATX, + InitialPost: &wire.PostV1{ + Nonce: post.Nonce, + Indices: post.Indices, + Pow: post.Pow, + }, + } + break + } + nipostChallenge := &types.NIPostChallenge{ + PublishEpoch: challenge.PublishEpoch, + PrevATXID: types.EmptyATXID, + PositioningATX: challenge.PositioningATXID, + CommitmentATX: challenge.CommitmentATXID, + InitialPost: &types.Post{ + Nonce: challenge.InitialPost.Nonce, + Indices: challenge.InitialPost.Indices, + Pow: challenge.InitialPost.Pow, + }, + } + err = nipost.AddChallenge(localDb, signer.NodeID(), nipostChallenge) + require.NoError(t, err) + + nipost, err := nipostBuilder.BuildNIPost(ctx, signer, challenge.Hash(), nipostChallenge) + require.NoError(t, err) + + // 2.2 Create ATX with invalid POST + for i := range nipost.Post.Indices { + nipost.Post.Indices[i] += 1 + } + + // Sanity check that the POST is invalid + err = verifier.Verify(ctx, (*shared.Proof)(nipost.Post), &shared.ProofMetadata{ + NodeId: signer.NodeID().Bytes(), + CommitmentAtxId: challenge.CommitmentATXID.Bytes(), + NumUnits: nipost.NumUnits, + Challenge: nipost.PostMetadata.Challenge, + LabelsPerUnit: nipost.PostMetadata.LabelsPerUnit, + }) + var invalidIdxError *verifying.ErrInvalidIndex + require.ErrorAs(t, err, &invalidIdxError) + + nodeID := signer.NodeID() + atx := wire.ActivationTxV1{ + InnerActivationTxV1: wire.InnerActivationTxV1{ + NIPostChallengeV1: *challenge, + Coinbase: types.Address{1, 2, 3, 4}, + NumUnits: nipost.NumUnits, + NIPost: wire.NiPostToWireV1(nipost.NIPost), + NodeID: &nodeID, + VRFNonce: (*uint64)(&nipost.VRFNonce), + }, + } + atx.Sign(signer) + return atx +} diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index cf4b9b7f8df..3f05e9e16df 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -20,10 +20,10 @@ import ( "github.com/spacemeshos/go-spacemesh/activation" "github.com/spacemeshos/go-spacemesh/activation/wire" - "github.com/spacemeshos/go-spacemesh/api/grpcserver" "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/config" "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/fetch" "github.com/spacemeshos/go-spacemesh/fetch/peers" @@ -33,8 +33,8 @@ import ( "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/proposals/store" "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/localsql" - "github.com/spacemeshos/go-spacemesh/sql/localsql/nipost" "github.com/spacemeshos/go-spacemesh/sql/statesql" "github.com/spacemeshos/go-spacemesh/systest/cluster" "github.com/spacemeshos/go-spacemesh/systest/testcontext" @@ -52,22 +52,64 @@ func TestPostMalfeasanceProof(t *testing.T) { // Prepare cluster ctx.PoetSize = 1 // one poet guarantees everybody gets the same proof - ctx.ClusterSize = 3 + ctx.ClusterSize = 5 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 + t.Run("distributed post v1", func(t *testing.T) { + // Prepare config + cfg := getConfig(t, cl, ctx) + + 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)) + + signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + db := statesql.InMemoryTest(t) + cdb := datastore.NewCachedDB(db, zap.NewNop()) + t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) + + host := setupHost(t, logger, cl, cfg) + clock := setupClock(t, logger, cl, cfg) + setupFetcher(t, cl, ctx, logger, cfg, db, clock, host) + + syncer := activation.NewMocksyncer(ctrl) + syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { + ch := make(chan struct{}) + close(ch) + return ch + }).AnyTimes() + + initPost(t, cl, ctx, logger, cfg, signer, cdb, syncer) + + verifyingOpts := activation.DefaultPostVerifyingOpts() + verifyingOpts.Workers = 1 + verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) + require.NoError(t, err) + + localDb := localsql.InMemoryTest(t) + atx := createInitialAtxV1(t, ctx, logger, cl, cfg, signer, db, localDb, clock, verifier) + + publishCtx, stopPublishing := context.WithCancel(ctx.Context) + defer stopPublishing() + publishATX(t, cl, ctx, logger, host, publishCtx, atx) + + verifyMalfeasanceProof(t, cl, ctx, logger, stopPublishing, signer, atx, verifier) + }) +} + +func getConfig(t testing.TB, cl *cluster.Cluster, ctx *testcontext.Context) *config.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 @@ -81,14 +123,14 @@ func TestPostMalfeasanceProof(t *testing.T) { 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) + return cfg +} - goldenATXID := cl.GoldenATX() - signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) - require.NoError(t, err) - +func setupHost(t testing.TB, logger *zap.Logger, cl *cluster.Cluster, cfg *config.Config) *p2p.Host { prologue := fmt.Sprintf("%x-%v", cl.GenesisID(), cfg.LayersPerEpoch*2-1) host, err := p2p.New( logger.Named("p2p"), @@ -101,11 +143,10 @@ func TestPostMalfeasanceProof(t *testing.T) { 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()) }) + return host +} - db := statesql.InMemoryTest(t) - cdb := datastore.NewCachedDB(db, zap.NewNop()) - t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) - +func setupClock(t testing.TB, logger *zap.Logger, cl *cluster.Cluster, cfg *config.Config) *timesync.NodeClock { clock, err := timesync.NewClock( timesync.WithLayerDuration(cfg.LayerDuration), timesync.WithTickInterval(1*time.Second), @@ -114,7 +155,19 @@ func TestPostMalfeasanceProof(t *testing.T) { ) require.NoError(t, err) t.Cleanup(clock.Close) + return clock +} +func setupFetcher( + tb testing.TB, + cl *cluster.Cluster, + ctx *testcontext.Context, + logger *zap.Logger, + cfg *config.Config, + db sql.StateDatabase, + clock *timesync.NodeClock, + host *p2p.Host, +) *fetch.Fetch { proposalsStore := store.New( store.WithEvictedLayer(clock.CurrentLayer()), store.WithLogger(logger.Named("proposals-store")), @@ -127,7 +180,7 @@ func TestPostMalfeasanceProof(t *testing.T) { fetch.WithConfig(cfg.FETCH), fetch.WithLogger(logger.Named("fetcher")), ) - require.NoError(t, err) + require.NoError(tb, err) fetcher.SetValidators( fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), @@ -142,18 +195,22 @@ func TestPostMalfeasanceProof(t *testing.T) { fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), ) - require.NoError(t, fetcher.Start()) - t.Cleanup(fetcher.Stop) - - ctrl := gomock.NewController(t) - syncer := activation.NewMocksyncer(ctrl) - syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { - ch := make(chan struct{}) - close(ch) - return ch - }).AnyTimes() + require.NoError(tb, fetcher.Start()) + tb.Cleanup(fetcher.Stop) + return fetcher +} - // 1. Initialize +func initPost( + tb testing.TB, + cl *cluster.Cluster, + ctx *testcontext.Context, + logger *zap.Logger, + cfg *config.Config, + signer *signing.EdSigner, + cdb *datastore.CachedDB, + syncer *activation.Mocksyncer, +) { + ctrl := gomock.NewController(tb) postSetupMgr, err := activation.NewPostSetupManager( cfg.POST, logger.Named("post"), @@ -163,7 +220,7 @@ func TestPostMalfeasanceProof(t *testing.T) { syncer, activation.NewMocknipostValidator(ctrl), ) - require.NoError(t, err) + require.NoError(tb, err) builder := activation.NewMockatxBuilder(ctrl) builder.EXPECT().Register(signer) @@ -174,169 +231,38 @@ func TestPostMalfeasanceProof(t *testing.T) { postSetupMgr, builder, ) - require.NoError(t, postSupervisor.Start(cfg.POSTService, cfg.SMESHING.Opts, signer)) - t.Cleanup(func() { assert.NoError(t, postSupervisor.Stop(false)) }) - - // 2. create ATX with invalid POST labels - grpcPostService := grpcserver.NewPostService( - logger.Named("grpc-post-service"), - grpcserver.PostServiceQueryInterval(500*time.Millisecond), - ) - grpcPostService.AllowConnections(true) - - 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()) }) - - localDb := localsql.InMemoryTest(t) - certClient := activation.NewCertifierClient(db, localDb, logger.Named("certifier")) - certifier := activation.NewCertifier(localDb, logger, certClient) - poetDb, err := activation.NewPoetDb(db, zap.NewNop()) - require.NoError(t, err) - poetService, err := activation.NewPoetService( - poetDb, - types.PoetServer{Address: cluster.MakePoetGlobalEndpoint(ctx.Namespace, 0)}, - cfg.POET, - logger, - 1, - activation.WithCertifier(certifier), - ) - require.NoError(t, err) - - verifyingOpts := activation.DefaultPostVerifyingOpts() - verifyingOpts.Workers = 1 - verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) - require.NoError(t, err) - - validator := activation.NewValidator( - db, - poetDb, - cfg.POST, - cfg.SMESHING.Opts.Scrypt, - verifier, - ) - - nipostBuilder, err := activation.NewNIPostBuilder( - localDb, - grpcPostService, - logger.Named("nipostBuilder"), - cfg.POET, - clock, - validator, - activation.WithPoetServices(poetService), - ) - require.NoError(t, err) - - // 2.1. Create initial POST - var challenge *wire.NIPostChallengeV1 - 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) - - err = nipost.AddPost(localDb, signer.NodeID(), nipost.Post{ - Nonce: post.Nonce, - Indices: post.Indices, - Pow: post.Pow, - Challenge: shared.ZeroChallenge, - NumUnits: postInfo.NumUnits, - CommitmentATX: postInfo.CommitmentATX, - VRFNonce: *postInfo.Nonce, - }) - require.NoError(t, err) - - challenge = &wire.NIPostChallengeV1{ - PrevATXID: types.EmptyATXID, - PublishEpoch: 1, - PositioningATXID: goldenATXID, - CommitmentATXID: &postInfo.CommitmentATX, - InitialPost: &wire.PostV1{ - Nonce: post.Nonce, - Indices: post.Indices, - Pow: post.Pow, - }, - } - break - } - nipostChallenge := &types.NIPostChallenge{ - PublishEpoch: challenge.PublishEpoch, - PrevATXID: types.EmptyATXID, - PositioningATX: challenge.PositioningATXID, - CommitmentATX: challenge.CommitmentATXID, - InitialPost: &types.Post{ - Nonce: challenge.InitialPost.Nonce, - Indices: challenge.InitialPost.Indices, - Pow: challenge.InitialPost.Pow, - }, - } - err = nipost.AddChallenge(localDb, signer.NodeID(), nipostChallenge) - require.NoError(t, err) - - nipost, err := nipostBuilder.BuildNIPost(ctx, signer, challenge.Hash(), nipostChallenge) - require.NoError(t, err) - - // 2.2 Create ATX with invalid POST - for i := range nipost.Post.Indices { - nipost.Post.Indices[i] += 1 - } - - // Sanity check that the POST is invalid - err = verifier.Verify(ctx, (*shared.Proof)(nipost.Post), &shared.ProofMetadata{ - NodeId: signer.NodeID().Bytes(), - CommitmentAtxId: challenge.CommitmentATXID.Bytes(), - NumUnits: nipost.NumUnits, - Challenge: nipost.PostMetadata.Challenge, - LabelsPerUnit: nipost.PostMetadata.LabelsPerUnit, - }) - var invalidIdxError *verifying.ErrInvalidIndex - require.ErrorAs(t, err, &invalidIdxError) - - nodeID := signer.NodeID() - atx := wire.ActivationTxV1{ - InnerActivationTxV1: wire.InnerActivationTxV1{ - NIPostChallengeV1: *challenge, - Coinbase: types.Address{1, 2, 3, 4}, - NumUnits: nipost.NumUnits, - NIPost: wire.NiPostToWireV1(nipost.NIPost), - NodeID: &nodeID, - VRFNonce: (*uint64)(&nipost.VRFNonce), - }, - } - atx.Sign(signer) + require.NoError(tb, postSupervisor.Start(cfg.POSTService, cfg.SMESHING.Opts, signer)) + tb.Cleanup(func() { assert.NoError(tb, postSupervisor.Stop(false)) }) +} +func publishATX( + tb testing.TB, + cl *cluster.Cluster, + ctx *testcontext.Context, + logger *zap.Logger, + host *p2p.Host, + publishCtx context.Context, + atx wire.ActivationTxV1, +) { // 3. Wait for publish epoch - require.NoError(t, cl.WaitAll(ctx)) + require.NoError(tb, cl.WaitAll(ctx)) 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) { + 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) + require.NoError(tb, err) // 4. Publish ATX - publishCtx, stopPublishing := context.WithCancel(ctx.Context) - defer stopPublishing() var eg errgroup.Group - t.Cleanup(func() { assert.NoError(t, eg.Wait()) }) + tb.Cleanup(func() { assert.NoError(tb, eg.Wait()) }) eg.Go(func() error { for { logger.Info("publishing ATX", zap.Object("atx", &atx)) buf := codec.MustEncode(&atx) err = host.Publish(ctx, pubsub.AtxProtocol, buf) - require.NoError(t, err) + require.NoError(tb, err) select { case <-publishCtx.Done(): @@ -345,28 +271,38 @@ func TestPostMalfeasanceProof(t *testing.T) { } } }) +} - // 5. Wait for POST malfeasance proof +func verifyMalfeasanceProof( + tb testing.TB, + cl *cluster.Cluster, + ctx *testcontext.Context, + logger *zap.Logger, + stopPublishing context.CancelFunc, + signer *signing.EdSigner, + atx wire.ActivationTxV1, + verifier activation.PostVerifier, +) { receivedProof := false timeout := time.Minute * 2 logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) awaitCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - err = malfeasanceStream(awaitCtx, cl.Client(0), logger, func(malf *pb.MalfeasanceStreamResponse) (bool, error) { + err := malfeasanceStream(awaitCtx, cl.Client(0), logger, func(malf *pb.MalfeasanceStreamResponse) (bool, error) { stopPublishing() logger.Info("malfeasance proof received") - require.Equal(t, malf.GetProof().GetSmesherId().Id, signer.NodeID().Bytes()) - require.Equal(t, pb.MalfeasanceProof_MALFEASANCE_POST_INDEX, malf.GetProof().GetKind()) + require.Equal(tb, malf.GetProof().GetSmesherId().Id, signer.NodeID().Bytes()) + require.Equal(tb, pb.MalfeasanceProof_MALFEASANCE_POST_INDEX, malf.GetProof().GetKind()) var proof mwire.MalfeasanceProof - require.NoError(t, codec.Decode(malf.Proof.Proof, &proof)) - require.Equal(t, mwire.InvalidPostIndex, proof.Proof.Type) + require.NoError(tb, codec.Decode(malf.Proof.Proof, &proof)) + require.Equal(tb, mwire.InvalidPostIndex, proof.Proof.Type) invalidPostProof := proof.Proof.Data.(*mwire.InvalidPostIndexProof) logger.Info("malfeasance post proof", zap.Object("proof", invalidPostProof)) invalidAtx := invalidPostProof.Atx - require.Equal(t, atx.PublishEpoch, invalidAtx.PublishEpoch) - require.Equal(t, atx.SmesherID, invalidAtx.SmesherID) - require.Equal(t, atx.ID(), invalidAtx.ID()) + require.Equal(tb, atx.PublishEpoch, invalidAtx.PublishEpoch) + require.Equal(tb, atx.SmesherID, invalidAtx.SmesherID) + require.Equal(tb, atx.ID(), invalidAtx.ID()) meta := &shared.ProofMetadata{ NodeId: invalidAtx.NodeID.Bytes(), @@ -377,10 +313,10 @@ func TestPostMalfeasanceProof(t *testing.T) { } err := verifier.Verify(awaitCtx, (*shared.Proof)(invalidAtx.NIPost.Post), meta) var invalidIdxError *verifying.ErrInvalidIndex - require.ErrorAs(t, err, &invalidIdxError) + require.ErrorAs(tb, err, &invalidIdxError) receivedProof = true return false, nil }) - require.NoError(t, err) - require.True(t, receivedProof, "malfeasance proof not received") + require.NoError(tb, err) + require.True(tb, receivedProof, "malfeasance proof not received") } From d83dd3c45dcd65f76ff6861cf0d1b6aa71d13481 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:48:54 +0000 Subject: [PATCH 26/56] Fix missing logger config for malfeasance2 --- config/logging.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/logging.go b/config/logging.go index 15c5f647bf7..fe1573589ac 100644 --- a/config/logging.go +++ b/config/logging.go @@ -49,6 +49,7 @@ type LoggerConfig struct { ConStateLoggerLevel string `mapstructure:"conState"` ExecutorLoggerLevel string `mapstructure:"executor"` MalfeasanceLoggerLevel string `mapstructure:"malfeasance"` + Malfeasance2LoggerLevel string `mapstructure:"malfeasance2"` BootstrapLoggerLevel string `mapstructure:"bootstrap"` } @@ -86,6 +87,7 @@ func DefaultLoggingConfig() LoggerConfig { PostServiceLoggerLevel: defaultLoggingLevel.String(), ConStateLoggerLevel: defaultLoggingLevel.String(), MalfeasanceLoggerLevel: defaultLoggingLevel.String(), + Malfeasance2LoggerLevel: defaultLoggingLevel.String(), BootstrapLoggerLevel: defaultLoggingLevel.String(), } } From c938443fc2245e93173c6c0b406c71dbbb337f64 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:24:01 +0000 Subject: [PATCH 27/56] Update systest for malfeasance --- activation/nipost.go | 3 +- ...tributed_post_verification_helpers_test.go | 177 ++++++++++++------ .../distributed_post_verification_test.go | 105 +++++++++-- 3 files changed, 215 insertions(+), 70 deletions(-) diff --git a/activation/nipost.go b/activation/nipost.go index 22d8d62f1bd..7c008fe5423 100644 --- a/activation/nipost.go +++ b/activation/nipost.go @@ -234,7 +234,8 @@ func (nb *NIPostBuilder) BuildNIPost( ctx, signer, poetProofDeadline, - poetRoundStart, challenge.Bytes(), + poetRoundStart, + challenge.Bytes(), ) regErr := &PoetRegistrationMismatchError{} switch { diff --git a/systest/tests/distributed_post_verification_helpers_test.go b/systest/tests/distributed_post_verification_helpers_test.go index 15df1cbb4ac..a528a6d32c8 100644 --- a/systest/tests/distributed_post_verification_helpers_test.go +++ b/systest/tests/distributed_post_verification_helpers_test.go @@ -1,14 +1,18 @@ package tests import ( + "fmt" "testing" "time" + "github.com/spacemeshos/go-scale" "github.com/spacemeshos/post/shared" "github.com/spacemeshos/post/verifying" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "golang.org/x/exp/maps" "github.com/spacemeshos/go-spacemesh/activation" "github.com/spacemeshos/go-spacemesh/activation/wire" @@ -23,7 +27,14 @@ import ( "github.com/spacemeshos/go-spacemesh/timesync" ) -func createInitialAtxV1(t testing.TB, +type builtAtx interface { + ID() types.ATXID + + scale.Encodable + zapcore.ObjectMarshaler +} + +func createInitialAtx(t testing.TB, ctx *testcontext.Context, logger *zap.Logger, cl *cluster.Cluster, @@ -33,7 +44,8 @@ func createInitialAtxV1(t testing.TB, localDb sql.LocalDatabase, clock *timesync.NodeClock, verifier activation.PostVerifier, -) wire.ActivationTxV1 { + publishEpoch types.EpochID, +) (builtAtx, types.EpochID) { // 2. create ATX with invalid POST labels grpcPostService := grpcserver.NewPostService( logger.Named("grpc-post-service"), @@ -85,57 +97,69 @@ func createInitialAtxV1(t testing.TB, ) require.NoError(t, err) - var challenge *wire.NIPostChallengeV1 + var client activation.PostClient for { - client, err := grpcPostService.Client(signer.NodeID()) - if err != nil { - ctx.Log.Info("waiting for poet service to connect") - time.Sleep(time.Second) - continue + client, err = grpcPostService.Client(signer.NodeID()) + if err == nil { + break } - ctx.Log.Info("poet service to connected") - post, postInfo, err := client.Proof(ctx, shared.ZeroChallenge) - require.NoError(t, err) - - err = nipost.AddPost(localDb, signer.NodeID(), nipost.Post{ - Nonce: post.Nonce, - Indices: post.Indices, - Pow: post.Pow, - Challenge: shared.ZeroChallenge, - NumUnits: postInfo.NumUnits, - CommitmentATX: postInfo.CommitmentATX, - VRFNonce: *postInfo.Nonce, - }) - require.NoError(t, err) - - challenge = &wire.NIPostChallengeV1{ - PrevATXID: types.EmptyATXID, - PublishEpoch: 1, - PositioningATXID: cl.GoldenATX(), - CommitmentATXID: &postInfo.CommitmentATX, - InitialPost: &wire.PostV1{ - Nonce: post.Nonce, - Indices: post.Indices, - Pow: post.Pow, - }, - } - break + ctx.Log.Info("waiting for poet service to connect") + time.Sleep(time.Second) + } + ctx.Log.Info("poet service to connected") + initialPost, initialPostInfo, err := client.Proof(ctx, shared.ZeroChallenge) + require.NoError(t, err) + + err = nipost.AddPost(localDb, signer.NodeID(), nipost.Post{ + Nonce: initialPost.Nonce, + Indices: initialPost.Indices, + Pow: initialPost.Pow, + Challenge: shared.ZeroChallenge, + NumUnits: initialPostInfo.NumUnits, + CommitmentATX: initialPostInfo.CommitmentATX, + VRFNonce: *initialPostInfo.Nonce, + }) + require.NoError(t, err) + + registerEpoch := publishEpoch - 1 + ctx.Log.Info("waiting for epoch to register at poet", + zap.Uint32("register_epoch", uint32(registerEpoch)), + zap.Uint32("publish_epoch", uint32(publishEpoch)), + ) + select { + case <-ctx.Done(): + ctx.Log.Info("context canceled") + return nil, 0 + case <-clock.AwaitLayer(registerEpoch.FirstLayer()): } + + registerEpoch = clock.CurrentLayer().GetEpoch() + publishEpoch = registerEpoch + 1 nipostChallenge := &types.NIPostChallenge{ - PublishEpoch: challenge.PublishEpoch, + PublishEpoch: publishEpoch, PrevATXID: types.EmptyATXID, - PositioningATX: challenge.PositioningATXID, - CommitmentATX: challenge.CommitmentATXID, + PositioningATX: cl.GoldenATX(), + CommitmentATX: &initialPostInfo.CommitmentATX, InitialPost: &types.Post{ - Nonce: challenge.InitialPost.Nonce, - Indices: challenge.InitialPost.Indices, - Pow: challenge.InitialPost.Pow, + Nonce: initialPost.Nonce, + Indices: initialPost.Indices, + Pow: initialPost.Pow, }, } err = nipost.AddChallenge(localDb, signer.NodeID(), nipostChallenge) require.NoError(t, err) - nipost, err := nipostBuilder.BuildNIPost(ctx, signer, challenge.Hash(), nipostChallenge) + version := version(cfg, nipostChallenge.PublishEpoch) + var challengeHash types.Hash32 + switch version { + case types.AtxV1: + challengeHash = wire.NIPostChallengeToWireV1(nipostChallenge).Hash() + case types.AtxV2: + challengeHash = wire.NIPostChallengeToWireV2(nipostChallenge).Hash() + default: + require.Fail(t, fmt.Sprintf("unsupported ATX version: %v", version)) + } + nipost, err := nipostBuilder.BuildNIPost(ctx, signer, challengeHash, nipostChallenge) require.NoError(t, err) // 2.2 Create ATX with invalid POST @@ -146,7 +170,7 @@ func createInitialAtxV1(t testing.TB, // Sanity check that the POST is invalid err = verifier.Verify(ctx, (*shared.Proof)(nipost.Post), &shared.ProofMetadata{ NodeId: signer.NodeID().Bytes(), - CommitmentAtxId: challenge.CommitmentATXID.Bytes(), + CommitmentAtxId: nipostChallenge.CommitmentATX.Bytes(), NumUnits: nipost.NumUnits, Challenge: nipost.PostMetadata.Challenge, LabelsPerUnit: nipost.PostMetadata.LabelsPerUnit, @@ -154,17 +178,60 @@ func createInitialAtxV1(t testing.TB, var invalidIdxError *verifying.ErrInvalidIndex require.ErrorAs(t, err, &invalidIdxError) - nodeID := signer.NodeID() - atx := wire.ActivationTxV1{ - InnerActivationTxV1: wire.InnerActivationTxV1{ - NIPostChallengeV1: *challenge, - Coinbase: types.Address{1, 2, 3, 4}, - NumUnits: nipost.NumUnits, - NIPost: wire.NiPostToWireV1(nipost.NIPost), - NodeID: &nodeID, - VRFNonce: (*uint64)(&nipost.VRFNonce), - }, + switch version { + case types.AtxV1: + atx := &wire.ActivationTxV1{ + InnerActivationTxV1: wire.InnerActivationTxV1{ + NIPostChallengeV1: *wire.NIPostChallengeToWireV1(nipostChallenge), + Coinbase: types.Address{1, 2, 3, 4}, + NumUnits: nipost.NumUnits, + NIPost: wire.NiPostToWireV1(nipost.NIPost), + VRFNonce: (*uint64)(&nipost.VRFNonce), + }, + } + atx.Sign(signer) + return atx, atx.PublishEpoch + case types.AtxV2: + atx := &wire.ActivationTxV2{ + PublishEpoch: nipostChallenge.PublishEpoch, + PositioningATX: nipostChallenge.PositioningATX, + Coinbase: types.Address{1, 2, 3, 4}, + VRFNonce: (uint64)(nipost.VRFNonce), + NIPosts: []wire.NIPostV2{ + { + Membership: wire.MerkleProofV2{ + Nodes: nipost.NIPost.Membership.Nodes, + }, + Challenge: types.Hash32(nipost.PostMetadata.Challenge), + Posts: []wire.SubPostV2{ + { + Post: *wire.PostToWireV1(nipost.Post), + NumUnits: nipost.NumUnits, + MembershipLeafIndex: nipost.NIPost.Membership.LeafIndex, + }, + }, + }, + }, + Initial: &wire.InitialAtxPartsV2{ + Post: *wire.PostToWireV1(nipostChallenge.InitialPost), + CommitmentATX: *nipostChallenge.CommitmentATX, + }, + } + atx.Sign(signer) + return atx, atx.PublishEpoch + default: + require.Fail(t, fmt.Sprintf("unsupported ATX version: %v", version)) + return nil, 0 + } +} + +func version(cfg *config.Config, publish types.EpochID) types.AtxVersion { + epochs := append([]types.EpochID{0}, maps.Keys(cfg.AtxVersions)...) + version := types.AtxV1 + for _, epoch := range epochs { + if publish >= epoch { + version = cfg.AtxVersions[epoch] + } } - atx.Sign(signer) - return atx + return version } diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 3f05e9e16df..c6bbc56f804 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -19,7 +19,6 @@ import ( "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/activation" - "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" @@ -42,7 +41,6 @@ import ( ) // TestPostMalfeasanceProof tests that nodes can detect an invalid PoST and create a malfeasance proof against it. -// TODO(mafa): update test to publish the ATX after v2 ATXs are live and then check for malfeasance. func TestPostMalfeasanceProof(t *testing.T) { t.Parallel() testDir := t.TempDir() @@ -60,6 +58,7 @@ func TestPostMalfeasanceProof(t *testing.T) { require.NoError(t, cl.AddSmeshers(ctx, ctx.ClusterSize-cl.Total(), cluster.WithFlags(cluster.PostK3(1)))) t.Run("distributed post v1", func(t *testing.T) { + t.Parallel() // Prepare config cfg := getConfig(t, cl, ctx) @@ -95,13 +94,90 @@ func TestPostMalfeasanceProof(t *testing.T) { require.NoError(t, err) localDb := localsql.InMemoryTest(t) - atx := createInitialAtxV1(t, ctx, logger, cl, cfg, signer, db, localDb, clock, verifier) + atx, publishEpoch := createInitialAtx( + t, + ctx, + logger, + cl, + cfg, + signer, + db, + localDb, + clock, + verifier, + types.EpochID(1), + ) publishCtx, stopPublishing := context.WithCancel(ctx.Context) defer stopPublishing() - publishATX(t, cl, ctx, logger, host, publishCtx, atx) + publishATX(t, cl, ctx, logger, host, publishCtx, publishEpoch, atx) - verifyMalfeasanceProof(t, cl, ctx, logger, stopPublishing, signer, atx, verifier) + verifyMalfeasanceProof(t, cl, ctx, logger, stopPublishing, signer, publishEpoch, atx, verifier) + }) + + t.Run("distributed post v2", func(t *testing.T) { + t.Parallel() + t.Skip("malfeasance stream needs to be updated first") + // Prepare config + cfg := getConfig(t, cl, ctx) + + 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)) + + signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + db := statesql.InMemoryTest(t) + cdb := datastore.NewCachedDB(db, zap.NewNop()) + t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) + + host := setupHost(t, logger, cl, cfg) + clock := setupClock(t, logger, cl, cfg) + setupFetcher(t, cl, ctx, logger, cfg, db, clock, host) + + syncer := activation.NewMocksyncer(ctrl) + syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { + ch := make(chan struct{}) + close(ch) + return ch + }).AnyTimes() + + initPost(t, cl, ctx, logger, cfg, signer, cdb, syncer) + + verifyingOpts := activation.DefaultPostVerifyingOpts() + verifyingOpts.Workers = 1 + verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) + require.NoError(t, err) + + localDb := localsql.InMemoryTest(t) + var publishEpoch types.EpochID + for k, v := range cfg.AtxVersions { + if v == 2 { + publishEpoch = types.EpochID(k) + } + } + atx, publishEpoch := createInitialAtx( + t, + ctx, + logger, + cl, + cfg, + signer, + db, + localDb, + clock, + verifier, + publishEpoch, + ) + + publishCtx, stopPublishing := context.WithCancel(ctx.Context) + defer stopPublishing() + publishATX(t, cl, ctx, logger, host, publishCtx, publishEpoch, atx) + + verifyMalfeasanceProof(t, cl, ctx, logger, stopPublishing, signer, publishEpoch, atx, verifier) }) } @@ -242,15 +318,15 @@ func publishATX( logger *zap.Logger, host *p2p.Host, publishCtx context.Context, - atx wire.ActivationTxV1, + publishEpoch types.EpochID, + atx builtAtx, ) { // 3. Wait for publish epoch require.NoError(tb, cl.WaitAll(ctx)) - epoch := atx.PublishEpoch - logger.Sugar().Infow("waiting for publish epoch", "epoch", epoch, "layer", epoch.FirstLayer()) + logger.Sugar().Infow("waiting for publish epoch", "epoch", publishEpoch, "layer", publishEpoch.FirstLayer()) err := layersStream(ctx, cl.Client(0), logger, func(resp *pb.LayerStreamResponse) (bool, error) { logger.Info("new layer", zap.Uint32("layer", resp.Layer.Number.Number)) - return resp.Layer.Number.Number < epoch.FirstLayer().Uint32(), nil + return resp.Layer.Number.Number < publishEpoch.FirstLayer().Uint32(), nil }) require.NoError(tb, err) @@ -259,8 +335,8 @@ func publishATX( tb.Cleanup(func() { assert.NoError(tb, eg.Wait()) }) eg.Go(func() error { for { - logger.Info("publishing ATX", zap.Object("atx", &atx)) - buf := codec.MustEncode(&atx) + logger.Info("publishing ATX", zap.Object("atx", atx)) + buf := codec.MustEncode(atx) err = host.Publish(ctx, pubsub.AtxProtocol, buf) require.NoError(tb, err) @@ -280,7 +356,8 @@ func verifyMalfeasanceProof( logger *zap.Logger, stopPublishing context.CancelFunc, signer *signing.EdSigner, - atx wire.ActivationTxV1, + publishEpoch types.EpochID, + atx builtAtx, verifier activation.PostVerifier, ) { receivedProof := false @@ -300,8 +377,8 @@ func verifyMalfeasanceProof( invalidPostProof := proof.Proof.Data.(*mwire.InvalidPostIndexProof) logger.Info("malfeasance post proof", zap.Object("proof", invalidPostProof)) invalidAtx := invalidPostProof.Atx - require.Equal(tb, atx.PublishEpoch, invalidAtx.PublishEpoch) - require.Equal(tb, atx.SmesherID, invalidAtx.SmesherID) + require.Equal(tb, publishEpoch, invalidAtx.PublishEpoch) + require.Equal(tb, signer.NodeID(), invalidAtx.SmesherID) require.Equal(tb, atx.ID(), invalidAtx.ID()) meta := &shared.ProofMetadata{ From 4ee8331bc943a0267e9b806b93870d7a4ff6a402 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:56:28 +0000 Subject: [PATCH 28/56] Update malfeasance stream and add more logging --- systest/tests/common.go | 7 +- ...tributed_post_verification_helpers_test.go | 3 +- .../distributed_post_verification_test.go | 102 +++++++++--------- systest/tests/equivocation_test.go | 7 +- 4 files changed, 59 insertions(+), 60 deletions(-) diff --git a/systest/tests/common.go b/systest/tests/common.go index 8b488630bd4..aa210f3ea97 100644 --- a/systest/tests/common.go +++ b/systest/tests/common.go @@ -9,6 +9,7 @@ import ( "time" pb "github.com/spacemeshos/api/release/go/spacemesh/v1" + pb2 "github.com/spacemeshos/api/release/go/spacemesh/v2beta1" "github.com/stretchr/testify/require" "go.uber.org/zap" "golang.org/x/sync/errgroup" @@ -228,12 +229,12 @@ func malfeasanceStream( ctx context.Context, node *cluster.NodeClient, logger *zap.Logger, - collector func(*pb.MalfeasanceStreamResponse) (bool, error), + collector func(*pb2.MalfeasanceProof) (bool, error), ) error { retries := 0 BACKOFF: - meshapi := pb.NewMeshServiceClient(node.PubConn()) - proofs, err := meshapi.MalfeasanceStream(ctx, &pb.MalfeasanceStreamRequest{IncludeProof: true}) + malapi := pb2.NewMalfeasanceStreamServiceClient(node.PubConn()) + proofs, err := malapi.Stream(ctx, &pb2.MalfeasanceStreamRequest{Watch: true}) if err != nil { return err } diff --git a/systest/tests/distributed_post_verification_helpers_test.go b/systest/tests/distributed_post_verification_helpers_test.go index a528a6d32c8..07b601f1e51 100644 --- a/systest/tests/distributed_post_verification_helpers_test.go +++ b/systest/tests/distributed_post_verification_helpers_test.go @@ -122,7 +122,7 @@ func createInitialAtx(t testing.TB, require.NoError(t, err) registerEpoch := publishEpoch - 1 - ctx.Log.Info("waiting for epoch to register at poet", + ctx.Log.With().Info("waiting for epoch to register at poet", zap.Uint32("register_epoch", uint32(registerEpoch)), zap.Uint32("publish_epoch", uint32(publishEpoch)), ) @@ -132,6 +132,7 @@ func createInitialAtx(t testing.TB, return nil, 0 case <-clock.AwaitLayer(registerEpoch.FirstLayer()): } + ctx.Log.With().Info("reached register epoch", zap.Uint32("register_epoch", uint32(registerEpoch))) registerEpoch = clock.CurrentLayer().GetEpoch() publishEpoch = registerEpoch + 1 diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index c6bbc56f804..9c07521c474 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -1,6 +1,7 @@ package tests import ( + "bytes" "context" "fmt" "os" @@ -10,8 +11,7 @@ import ( "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" + pb2 "github.com/spacemeshos/api/release/go/spacemesh/v2beta1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -27,6 +27,7 @@ import ( "github.com/spacemeshos/go-spacemesh/fetch" "github.com/spacemeshos/go-spacemesh/fetch/peers" mwire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" + "github.com/spacemeshos/go-spacemesh/malfeasance2" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/handshake" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" @@ -107,12 +108,38 @@ func TestPostMalfeasanceProof(t *testing.T) { verifier, types.EpochID(1), ) + ctx.Log.With().Info("created initial ATX", + zap.Object("atx", atx), + zap.Uint32("pub_epoch", publishEpoch.Uint32()), + ) publishCtx, stopPublishing := context.WithCancel(ctx.Context) defer stopPublishing() publishATX(t, cl, ctx, logger, host, publishCtx, publishEpoch, atx) + ctx.Log.With().Info("published initial ATX", + zap.Object("atx", atx), + zap.Uint32("pub_epoch", publishEpoch.Uint32()), + ) - verifyMalfeasanceProof(t, cl, ctx, logger, stopPublishing, signer, publishEpoch, atx, verifier) + receivedProof := false + timeout := time.Minute * 2 + logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) + awaitCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + err = malfeasanceStream(awaitCtx, cl.Client(0), logger, func(proof *pb2.MalfeasanceProof) (bool, error) { + if !bytes.Equal(proof.GetSmesher(), signer.NodeID().Bytes()) { + return true, nil + } + stopPublishing() + logger.Info("malfeasance proof received") + require.Equal(t, 0, proof.Domain) + require.Equal(t, mwire.InvalidPostIndex, proof.Type) + require.Equal(t, atx.ID(), proof.Properties["atx"]) + receivedProof = true + return false, nil + }) + require.NoError(t, err) + require.True(t, receivedProof, "malfeasance proof not received") }) t.Run("distributed post v2", func(t *testing.T) { @@ -177,7 +204,25 @@ func TestPostMalfeasanceProof(t *testing.T) { defer stopPublishing() publishATX(t, cl, ctx, logger, host, publishCtx, publishEpoch, atx) - verifyMalfeasanceProof(t, cl, ctx, logger, stopPublishing, signer, publishEpoch, atx, verifier) + receivedProof := false + timeout := time.Minute * 2 + logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) + awaitCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + err = malfeasanceStream(awaitCtx, cl.Client(0), logger, func(proof *pb2.MalfeasanceProof) (bool, error) { + if !bytes.Equal(proof.GetSmesher(), signer.NodeID().Bytes()) { + return true, nil + } + stopPublishing() + logger.Info("malfeasance proof received") + require.Equal(t, malfeasance2.InvalidActivation, proof.Domain) + require.Equal(t, "InvalidPoSTProof", proof.Type) + require.Equal(t, atx.ID(), proof.Properties["atx"]) + receivedProof = true + return false, nil + }) + require.NoError(t, err) + require.True(t, receivedProof, "malfeasance proof not received") }) } @@ -348,52 +393,3 @@ func publishATX( } }) } - -func verifyMalfeasanceProof( - tb testing.TB, - cl *cluster.Cluster, - ctx *testcontext.Context, - logger *zap.Logger, - stopPublishing context.CancelFunc, - signer *signing.EdSigner, - publishEpoch types.EpochID, - atx builtAtx, - verifier activation.PostVerifier, -) { - receivedProof := false - timeout := time.Minute * 2 - logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) - awaitCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - err := malfeasanceStream(awaitCtx, cl.Client(0), logger, func(malf *pb.MalfeasanceStreamResponse) (bool, error) { - stopPublishing() - logger.Info("malfeasance proof received") - require.Equal(tb, malf.GetProof().GetSmesherId().Id, signer.NodeID().Bytes()) - require.Equal(tb, pb.MalfeasanceProof_MALFEASANCE_POST_INDEX, malf.GetProof().GetKind()) - - var proof mwire.MalfeasanceProof - require.NoError(tb, codec.Decode(malf.Proof.Proof, &proof)) - require.Equal(tb, mwire.InvalidPostIndex, proof.Proof.Type) - invalidPostProof := proof.Proof.Data.(*mwire.InvalidPostIndexProof) - logger.Info("malfeasance post proof", zap.Object("proof", invalidPostProof)) - invalidAtx := invalidPostProof.Atx - require.Equal(tb, publishEpoch, invalidAtx.PublishEpoch) - require.Equal(tb, signer.NodeID(), invalidAtx.SmesherID) - require.Equal(tb, atx.ID(), invalidAtx.ID()) - - meta := &shared.ProofMetadata{ - NodeId: invalidAtx.NodeID.Bytes(), - CommitmentAtxId: invalidAtx.CommitmentATXID.Bytes(), - NumUnits: invalidAtx.NumUnits, - Challenge: invalidAtx.NIPost.PostMetadata.Challenge, - LabelsPerUnit: invalidAtx.NIPost.PostMetadata.LabelsPerUnit, - } - err := verifier.Verify(awaitCtx, (*shared.Proof)(invalidAtx.NIPost.Post), meta) - var invalidIdxError *verifying.ErrInvalidIndex - require.ErrorAs(tb, err, &invalidIdxError) - receivedProof = true - return false, nil - }) - require.NoError(tb, err) - require.True(tb, receivedProof, "malfeasance proof not received") -} diff --git a/systest/tests/equivocation_test.go b/systest/tests/equivocation_test.go index bb7104b6952..7a21628f6a7 100644 --- a/systest/tests/equivocation_test.go +++ b/systest/tests/equivocation_test.go @@ -8,6 +8,7 @@ import ( "github.com/oasisprotocol/curve25519-voi/primitives/ed25519" pb "github.com/spacemeshos/api/release/go/spacemesh/v1" + pb2 "github.com/spacemeshos/api/release/go/spacemesh/v2beta1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" @@ -125,9 +126,9 @@ func TestEquivocation(t *testing.T) { proofs := make([]types.NodeID, 0, len(malfeasants)) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() - malfeasanceStream(ctx, client, cctx.Log.Desugar(), func(malf *pb.MalfeasanceStreamResponse) (bool, error) { - malfeasant := malf.GetProof().GetSmesherId().Id - proofs = append(proofs, types.NodeID(malfeasant)) + malfeasanceStream(ctx, client, cctx.Log.Desugar(), func(proof *pb2.MalfeasanceProof) (bool, error) { + malfeasant := proof.GetSmesher() + proofs = append(proofs, types.BytesToNodeID(malfeasant)) return len(proofs) < len(malfeasants), nil }) assert.ElementsMatchf(t, expected, proofs, "client: %s", cl.Client(i).Name) From 6c0cdff94d7be4b0deaf95611cfb611c99389c41 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:45:28 +0000 Subject: [PATCH 29/56] Use priv connection for malfeasance stream --- systest/tests/common.go | 2 +- ..._post_verification_helpers_test.go => malfeasance_common.go} | 0 ...istributed_post_verification_test.go => malfeasance_test.go} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename systest/tests/{distributed_post_verification_helpers_test.go => malfeasance_common.go} (100%) rename systest/tests/{distributed_post_verification_test.go => malfeasance_test.go} (100%) diff --git a/systest/tests/common.go b/systest/tests/common.go index aa210f3ea97..99950fb61bc 100644 --- a/systest/tests/common.go +++ b/systest/tests/common.go @@ -233,7 +233,7 @@ func malfeasanceStream( ) error { retries := 0 BACKOFF: - malapi := pb2.NewMalfeasanceStreamServiceClient(node.PubConn()) + malapi := pb2.NewMalfeasanceStreamServiceClient(node.PrivConn()) proofs, err := malapi.Stream(ctx, &pb2.MalfeasanceStreamRequest{Watch: true}) if err != nil { return err diff --git a/systest/tests/distributed_post_verification_helpers_test.go b/systest/tests/malfeasance_common.go similarity index 100% rename from systest/tests/distributed_post_verification_helpers_test.go rename to systest/tests/malfeasance_common.go diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/malfeasance_test.go similarity index 100% rename from systest/tests/distributed_post_verification_test.go rename to systest/tests/malfeasance_test.go From 15cf90d591226fd84d5f89532bae650aeba93d72 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:20:02 +0000 Subject: [PATCH 30/56] Use waitgroup instead of t.Run --- systest/tests/malfeasance_common.go | 4 +- systest/tests/malfeasance_test.go | 190 ++++++++++++++-------------- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/systest/tests/malfeasance_common.go b/systest/tests/malfeasance_common.go index 07b601f1e51..3b8457fe9f2 100644 --- a/systest/tests/malfeasance_common.go +++ b/systest/tests/malfeasance_common.go @@ -122,7 +122,7 @@ func createInitialAtx(t testing.TB, require.NoError(t, err) registerEpoch := publishEpoch - 1 - ctx.Log.With().Info("waiting for epoch to register at poet", + ctx.Log.Desugar().Info("waiting for epoch to register at poet", zap.Uint32("register_epoch", uint32(registerEpoch)), zap.Uint32("publish_epoch", uint32(publishEpoch)), ) @@ -132,7 +132,7 @@ func createInitialAtx(t testing.TB, return nil, 0 case <-clock.AwaitLayer(registerEpoch.FirstLayer()): } - ctx.Log.With().Info("reached register epoch", zap.Uint32("register_epoch", uint32(registerEpoch))) + ctx.Log.Desugar().Info("reached register epoch", zap.Uint32("register_epoch", uint32(registerEpoch))) registerEpoch = clock.CurrentLayer().GetEpoch() publishEpoch = registerEpoch + 1 diff --git a/systest/tests/malfeasance_test.go b/systest/tests/malfeasance_test.go index 9c07521c474..c022a59ba9e 100644 --- a/systest/tests/malfeasance_test.go +++ b/systest/tests/malfeasance_test.go @@ -27,7 +27,6 @@ import ( "github.com/spacemeshos/go-spacemesh/fetch" "github.com/spacemeshos/go-spacemesh/fetch/peers" mwire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" - "github.com/spacemeshos/go-spacemesh/malfeasance2" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/handshake" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" @@ -58,8 +57,8 @@ func TestPostMalfeasanceProof(t *testing.T) { require.NoError(t, cl.AddPoets(ctx)) require.NoError(t, cl.AddSmeshers(ctx, ctx.ClusterSize-cl.Total(), cluster.WithFlags(cluster.PostK3(1)))) - t.Run("distributed post v1", func(t *testing.T) { - t.Parallel() + var eg errgroup.Group + eg.Go(func() error { // Prepare config cfg := getConfig(t, cl, ctx) @@ -108,7 +107,7 @@ func TestPostMalfeasanceProof(t *testing.T) { verifier, types.EpochID(1), ) - ctx.Log.With().Info("created initial ATX", + ctx.Log.Desugar().Info("created initial ATX", zap.Object("atx", atx), zap.Uint32("pub_epoch", publishEpoch.Uint32()), ) @@ -116,7 +115,7 @@ func TestPostMalfeasanceProof(t *testing.T) { publishCtx, stopPublishing := context.WithCancel(ctx.Context) defer stopPublishing() publishATX(t, cl, ctx, logger, host, publishCtx, publishEpoch, atx) - ctx.Log.With().Info("published initial ATX", + ctx.Log.Desugar().Info("published initial ATX", zap.Object("atx", atx), zap.Uint32("pub_epoch", publishEpoch.Uint32()), ) @@ -140,90 +139,91 @@ func TestPostMalfeasanceProof(t *testing.T) { }) require.NoError(t, err) require.True(t, receivedProof, "malfeasance proof not received") + return nil }) - t.Run("distributed post v2", func(t *testing.T) { - t.Parallel() - t.Skip("malfeasance stream needs to be updated first") - // Prepare config - cfg := getConfig(t, cl, ctx) - - 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)) - - signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) - require.NoError(t, err) - - ctrl := gomock.NewController(t) - db := statesql.InMemoryTest(t) - cdb := datastore.NewCachedDB(db, zap.NewNop()) - t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) - - host := setupHost(t, logger, cl, cfg) - clock := setupClock(t, logger, cl, cfg) - setupFetcher(t, cl, ctx, logger, cfg, db, clock, host) - - syncer := activation.NewMocksyncer(ctrl) - syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { - ch := make(chan struct{}) - close(ch) - return ch - }).AnyTimes() - - initPost(t, cl, ctx, logger, cfg, signer, cdb, syncer) - - verifyingOpts := activation.DefaultPostVerifyingOpts() - verifyingOpts.Workers = 1 - verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) - require.NoError(t, err) - - localDb := localsql.InMemoryTest(t) - var publishEpoch types.EpochID - for k, v := range cfg.AtxVersions { - if v == 2 { - publishEpoch = types.EpochID(k) - } - } - atx, publishEpoch := createInitialAtx( - t, - ctx, - logger, - cl, - cfg, - signer, - db, - localDb, - clock, - verifier, - publishEpoch, - ) - - publishCtx, stopPublishing := context.WithCancel(ctx.Context) - defer stopPublishing() - publishATX(t, cl, ctx, logger, host, publishCtx, publishEpoch, atx) - - receivedProof := false - timeout := time.Minute * 2 - logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) - awaitCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - err = malfeasanceStream(awaitCtx, cl.Client(0), logger, func(proof *pb2.MalfeasanceProof) (bool, error) { - if !bytes.Equal(proof.GetSmesher(), signer.NodeID().Bytes()) { - return true, nil - } - stopPublishing() - logger.Info("malfeasance proof received") - require.Equal(t, malfeasance2.InvalidActivation, proof.Domain) - require.Equal(t, "InvalidPoSTProof", proof.Type) - require.Equal(t, atx.ID(), proof.Properties["atx"]) - receivedProof = true - return false, nil - }) - require.NoError(t, err) - require.True(t, receivedProof, "malfeasance proof not received") - }) + // eg.Go(func() error { + // // Prepare config + // cfg := getConfig(t, cl, ctx) + + // 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)) + + // signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) + // require.NoError(t, err) + + // ctrl := gomock.NewController(t) + // db := statesql.InMemoryTest(t) + // cdb := datastore.NewCachedDB(db, zap.NewNop()) + // t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) + + // host := setupHost(t, logger, cl, cfg) + // clock := setupClock(t, logger, cl, cfg) + // setupFetcher(t, cl, ctx, logger, cfg, db, clock, host) + + // syncer := activation.NewMocksyncer(ctrl) + // syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { + // ch := make(chan struct{}) + // close(ch) + // return ch + // }).AnyTimes() + + // initPost(t, cl, ctx, logger, cfg, signer, cdb, syncer) + + // verifyingOpts := activation.DefaultPostVerifyingOpts() + // verifyingOpts.Workers = 1 + // verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) + // require.NoError(t, err) + + // localDb := localsql.InMemoryTest(t) + // var publishEpoch types.EpochID + // for k, v := range cfg.AtxVersions { + // if v == 2 { + // publishEpoch = types.EpochID(k) + // } + // } + // atx, publishEpoch := createInitialAtx( + // t, + // ctx, + // logger, + // cl, + // cfg, + // signer, + // db, + // localDb, + // clock, + // verifier, + // publishEpoch, + // ) + + // publishCtx, stopPublishing := context.WithCancel(ctx.Context) + // defer stopPublishing() + // publishATX(t, cl, ctx, logger, host, publishCtx, publishEpoch, atx) + + // receivedProof := false + // timeout := time.Minute * 2 + // logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) + // awaitCtx, cancel := context.WithTimeout(ctx, timeout) + // defer cancel() + // err = malfeasanceStream(awaitCtx, cl.Client(0), logger, func(proof *pb2.MalfeasanceProof) (bool, error) { + // if !bytes.Equal(proof.GetSmesher(), signer.NodeID().Bytes()) { + // return true, nil + // } + // stopPublishing() + // logger.Info("malfeasance proof received") + // require.Equal(t, malfeasance2.InvalidActivation, proof.Domain) + // require.Equal(t, "InvalidPoSTProof", proof.Type) + // require.Equal(t, atx.ID(), proof.Properties["atx"]) + // receivedProof = true + // return false, nil + // }) + // require.NoError(t, err) + // require.True(t, receivedProof, "malfeasance proof not received") + // return nil + // }) + eg.Wait() } func getConfig(t testing.TB, cl *cluster.Cluster, ctx *testcontext.Context) *config.Config { @@ -247,11 +247,11 @@ func getConfig(t testing.TB, cl *cluster.Cluster, ctx *testcontext.Context) *con cfg.Bootstrap.URL = cluster.BootstrapperGlobalEndpoint(ctx.Namespace, 0) cfg.P2P.MinPeers = 2 - ctx.Log.Debugw("Prepared config", "cfg", cfg) + ctx.Log.Desugar().Debug("Prepared config", zap.Any("cfg", cfg)) return cfg } -func setupHost(t testing.TB, logger *zap.Logger, cl *cluster.Cluster, cfg *config.Config) *p2p.Host { +func setupHost(tb testing.TB, logger *zap.Logger, cl *cluster.Cluster, cfg *config.Config) *p2p.Host { prologue := fmt.Sprintf("%x-%v", cl.GenesisID(), cfg.LayersPerEpoch*2-1) host, err := p2p.New( logger.Named("p2p"), @@ -259,23 +259,23 @@ func setupHost(t testing.TB, logger *zap.Logger, cl *cluster.Cluster, cfg *confi []byte(prologue), handshake.NetworkCookie(prologue), ) - require.NoError(t, err) + require.NoError(tb, 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()) }) + require.NoError(tb, host.Start()) + tb.Cleanup(func() { assert.NoError(tb, host.Stop()) }) return host } -func setupClock(t testing.TB, logger *zap.Logger, cl *cluster.Cluster, cfg *config.Config) *timesync.NodeClock { +func setupClock(tb testing.TB, logger *zap.Logger, cl *cluster.Cluster, cfg *config.Config) *timesync.NodeClock { clock, err := timesync.NewClock( timesync.WithLayerDuration(cfg.LayerDuration), timesync.WithTickInterval(1*time.Second), timesync.WithGenesisTime(cl.Genesis()), timesync.WithLogger(logger.Named("clock")), ) - require.NoError(t, err) - t.Cleanup(clock.Close) + require.NoError(tb, err) + tb.Cleanup(clock.Close) return clock } From 90b72a443216c094b981d186f059acb1e6bd51e2 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:04:07 +0000 Subject: [PATCH 31/56] Activate malfeasance for v2 --- systest/tests/malfeasance_test.go | 163 +++++++++++++++--------------- 1 file changed, 82 insertions(+), 81 deletions(-) diff --git a/systest/tests/malfeasance_test.go b/systest/tests/malfeasance_test.go index c022a59ba9e..5226e62d612 100644 --- a/systest/tests/malfeasance_test.go +++ b/systest/tests/malfeasance_test.go @@ -27,6 +27,7 @@ import ( "github.com/spacemeshos/go-spacemesh/fetch" "github.com/spacemeshos/go-spacemesh/fetch/peers" mwire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" + "github.com/spacemeshos/go-spacemesh/malfeasance2" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/handshake" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" @@ -142,87 +143,87 @@ func TestPostMalfeasanceProof(t *testing.T) { return nil }) - // eg.Go(func() error { - // // Prepare config - // cfg := getConfig(t, cl, ctx) - - // 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)) - - // signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) - // require.NoError(t, err) - - // ctrl := gomock.NewController(t) - // db := statesql.InMemoryTest(t) - // cdb := datastore.NewCachedDB(db, zap.NewNop()) - // t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) - - // host := setupHost(t, logger, cl, cfg) - // clock := setupClock(t, logger, cl, cfg) - // setupFetcher(t, cl, ctx, logger, cfg, db, clock, host) - - // syncer := activation.NewMocksyncer(ctrl) - // syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { - // ch := make(chan struct{}) - // close(ch) - // return ch - // }).AnyTimes() - - // initPost(t, cl, ctx, logger, cfg, signer, cdb, syncer) - - // verifyingOpts := activation.DefaultPostVerifyingOpts() - // verifyingOpts.Workers = 1 - // verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) - // require.NoError(t, err) - - // localDb := localsql.InMemoryTest(t) - // var publishEpoch types.EpochID - // for k, v := range cfg.AtxVersions { - // if v == 2 { - // publishEpoch = types.EpochID(k) - // } - // } - // atx, publishEpoch := createInitialAtx( - // t, - // ctx, - // logger, - // cl, - // cfg, - // signer, - // db, - // localDb, - // clock, - // verifier, - // publishEpoch, - // ) - - // publishCtx, stopPublishing := context.WithCancel(ctx.Context) - // defer stopPublishing() - // publishATX(t, cl, ctx, logger, host, publishCtx, publishEpoch, atx) - - // receivedProof := false - // timeout := time.Minute * 2 - // logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) - // awaitCtx, cancel := context.WithTimeout(ctx, timeout) - // defer cancel() - // err = malfeasanceStream(awaitCtx, cl.Client(0), logger, func(proof *pb2.MalfeasanceProof) (bool, error) { - // if !bytes.Equal(proof.GetSmesher(), signer.NodeID().Bytes()) { - // return true, nil - // } - // stopPublishing() - // logger.Info("malfeasance proof received") - // require.Equal(t, malfeasance2.InvalidActivation, proof.Domain) - // require.Equal(t, "InvalidPoSTProof", proof.Type) - // require.Equal(t, atx.ID(), proof.Properties["atx"]) - // receivedProof = true - // return false, nil - // }) - // require.NoError(t, err) - // require.True(t, receivedProof, "malfeasance proof not received") - // return nil - // }) + eg.Go(func() error { + // Prepare config + cfg := getConfig(t, cl, ctx) + + 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)) + + signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + db := statesql.InMemoryTest(t) + cdb := datastore.NewCachedDB(db, zap.NewNop()) + t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) + + host := setupHost(t, logger, cl, cfg) + clock := setupClock(t, logger, cl, cfg) + setupFetcher(t, cl, ctx, logger, cfg, db, clock, host) + + syncer := activation.NewMocksyncer(ctrl) + syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { + ch := make(chan struct{}) + close(ch) + return ch + }).AnyTimes() + + initPost(t, cl, ctx, logger, cfg, signer, cdb, syncer) + + verifyingOpts := activation.DefaultPostVerifyingOpts() + verifyingOpts.Workers = 1 + verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) + require.NoError(t, err) + + localDb := localsql.InMemoryTest(t) + var publishEpoch types.EpochID + for k, v := range cfg.AtxVersions { + if v == 2 { + publishEpoch = types.EpochID(k) + } + } + atx, publishEpoch := createInitialAtx( + t, + ctx, + logger, + cl, + cfg, + signer, + db, + localDb, + clock, + verifier, + publishEpoch, + ) + + publishCtx, stopPublishing := context.WithCancel(ctx.Context) + defer stopPublishing() + publishATX(t, cl, ctx, logger, host, publishCtx, publishEpoch, atx) + + receivedProof := false + timeout := time.Minute * 2 + logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) + awaitCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + err = malfeasanceStream(awaitCtx, cl.Client(0), logger, func(proof *pb2.MalfeasanceProof) (bool, error) { + if !bytes.Equal(proof.GetSmesher(), signer.NodeID().Bytes()) { + return true, nil + } + stopPublishing() + logger.Info("malfeasance proof received") + require.Equal(t, malfeasance2.InvalidActivation, proof.Domain) + require.Equal(t, "InvalidPoSTProof", proof.Type) + require.Equal(t, atx.ID(), proof.Properties["atx"]) + receivedProof = true + return false, nil + }) + require.NoError(t, err) + require.True(t, receivedProof, "malfeasance proof not received") + return nil + }) eg.Wait() } From 733e9399dc7de092e26dfa22d21fb80134f25bed Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:12:05 +0000 Subject: [PATCH 32/56] use different dirs for services --- systest/tests/malfeasance_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/systest/tests/malfeasance_test.go b/systest/tests/malfeasance_test.go index 5226e62d612..66dcc5d8a34 100644 --- a/systest/tests/malfeasance_test.go +++ b/systest/tests/malfeasance_test.go @@ -44,7 +44,6 @@ import ( // TestPostMalfeasanceProof tests that nodes can detect an invalid PoST and create a malfeasance proof against it. func TestPostMalfeasanceProof(t *testing.T) { t.Parallel() - testDir := t.TempDir() ctx := testcontext.New(t) logger := ctx.Log.Desugar().WithOptions(zap.IncreaseLevel(zap.InfoLevel), zap.WithCaller(false)) @@ -62,6 +61,7 @@ func TestPostMalfeasanceProof(t *testing.T) { eg.Go(func() error { // Prepare config cfg := getConfig(t, cl, ctx) + testDir := t.TempDir() cfg.DataDirParent = testDir cfg.SMESHING.Opts.DataDir = filepath.Join(testDir, "post-data") @@ -146,6 +146,7 @@ func TestPostMalfeasanceProof(t *testing.T) { eg.Go(func() error { // Prepare config cfg := getConfig(t, cl, ctx) + testDir := t.TempDir() cfg.DataDirParent = testDir cfg.SMESHING.Opts.DataDir = filepath.Join(testDir, "post-data") From c6eb4e83141f2d3ff1faab4e08e7cdf00c4c58b0 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:32:38 +0000 Subject: [PATCH 33/56] Add test for checkpoint --- checkpoint/runner_test.go | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/checkpoint/runner_test.go b/checkpoint/runner_test.go index 6b3a870bbb7..0528ea8f4c6 100644 --- a/checkpoint/runner_test.go +++ b/checkpoint/runner_test.go @@ -21,6 +21,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/accounts" "github.com/spacemeshos/go-spacemesh/sql/atxs" "github.com/spacemeshos/go-spacemesh/sql/identities" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" "github.com/spacemeshos/go-spacemesh/sql/statesql" ) @@ -35,9 +36,15 @@ type activationTx struct { previous types.ATXID } +type malProof struct { + proof []byte + domain int +} + type miner struct { - atxs []activationTx - malfeasanceProof []byte + atxs []activationTx + malfeasanceProof []byte + malfeasanceProof2 *malProof } var allMiners = []miner{ @@ -76,14 +83,25 @@ var allMiners = []miner{ }, }, - // smesher 5 is malicious and equivocated in epoch 7 + // smesher 5 is malicious and equivocated in epoch 6 { atxs: []activationTx{ - {newAtx(types.ATXID{83}, &types.ATXID{27}, 7, 0, 113, []byte("smesher5")), types.EmptyATXID}, - {newAtx(types.ATXID{97}, &types.ATXID{16}, 7, 0, 113, []byte("smesher5")), types.EmptyATXID}, + {newAtx(types.ATXID{53}, &types.ATXID{27}, 6, 0, 113, []byte("smesher5")), types.EmptyATXID}, + {newAtx(types.ATXID{57}, &types.ATXID{16}, 6, 0, 113, []byte("smesher5")), types.EmptyATXID}, }, malfeasanceProof: []byte("im bad"), }, + // smesher 6 is malicious and equivocated in epoch 7 + { + atxs: []activationTx{ + {newAtx(types.ATXID{63}, &types.ATXID{27}, 7, 0, 113, []byte("smesher6")), types.EmptyATXID}, + {newAtx(types.ATXID{67}, &types.ATXID{16}, 7, 0, 113, []byte("smesher6")), types.EmptyATXID}, + }, + malfeasanceProof2: &malProof{ + proof: []byte("im bad"), + domain: 1, + }, + }, } var allAccounts = []*types.Account{ @@ -179,6 +197,9 @@ func expectedCheckpoint(tb testing.TB, snapshot types.LayerID, numAtxs int, mine if len(miner.malfeasanceProof) > 0 { continue } + if miner.malfeasanceProof2 != nil { + continue + } atxs := miner.atxs n := len(atxs) if n > numAtxs { @@ -193,7 +214,6 @@ func expectedCheckpoint(tb testing.TB, snapshot types.LayerID, numAtxs int, mine } result.Data.Atxs = atxData - accounts := make(map[types.Address]*types.Account) for _, account := range allAccounts { if account.Layer <= snapshot { @@ -276,6 +296,10 @@ func createMesh(tb testing.TB, db sql.StateDatabase, miners []miner, accts []*ty if proof := miner.malfeasanceProof; len(proof) > 0 { require.NoError(tb, identities.SetMalicious(db, miner.atxs[0].SmesherID, proof, time.Now())) } + if proof := miner.malfeasanceProof2; proof != nil { + err := malfeasance.AddProof(db, miner.atxs[1].SmesherID, nil, proof.proof, proof.domain, time.Now()) + require.NoError(tb, err) + } } for _, it := range accts { From 2b75bd178da31e1ce18cf266ccb0cb0a3a78e019 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:55:31 +0000 Subject: [PATCH 34/56] Avoid port conflict --- systest/tests/malfeasance_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/systest/tests/malfeasance_test.go b/systest/tests/malfeasance_test.go index 66dcc5d8a34..b1fd9561b08 100644 --- a/systest/tests/malfeasance_test.go +++ b/systest/tests/malfeasance_test.go @@ -46,7 +46,6 @@ func TestPostMalfeasanceProof(t *testing.T) { t.Parallel() ctx := testcontext.New(t) - logger := ctx.Log.Desugar().WithOptions(zap.IncreaseLevel(zap.InfoLevel), zap.WithCaller(false)) // Prepare cluster ctx.PoetSize = 1 // one poet guarantees everybody gets the same proof @@ -59,6 +58,9 @@ func TestPostMalfeasanceProof(t *testing.T) { var eg errgroup.Group eg.Go(func() error { + logger := ctx.Log.Desugar().Named("malfeasance1-test"). + WithOptions(zap.IncreaseLevel(zap.InfoLevel), zap.WithCaller(false)) + // Prepare config cfg := getConfig(t, cl, ctx) testDir := t.TempDir() @@ -144,6 +146,9 @@ func TestPostMalfeasanceProof(t *testing.T) { }) eg.Go(func() error { + logger := ctx.Log.Desugar().Named("malfeasance2-test"). + WithOptions(zap.IncreaseLevel(zap.InfoLevel), zap.WithCaller(false)) + // Prepare config cfg := getConfig(t, cl, ctx) testDir := t.TempDir() @@ -172,6 +177,7 @@ func TestPostMalfeasanceProof(t *testing.T) { return ch }).AnyTimes() + cfg.API.PostListener = "0.0.0.0:10094" // avoid port conflict with v1 identity initPost(t, cl, ctx, logger, cfg, signer, cdb, syncer) verifyingOpts := activation.DefaultPostVerifyingOpts() From eb230fa633618488c252c59bf771a89e6a161407 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:55:44 +0000 Subject: [PATCH 35/56] Ensure at least some labels are valid --- systest/tests/malfeasance_common.go | 38 ++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/systest/tests/malfeasance_common.go b/systest/tests/malfeasance_common.go index 3b8457fe9f2..0e44e09ecc6 100644 --- a/systest/tests/malfeasance_common.go +++ b/systest/tests/malfeasance_common.go @@ -1,6 +1,7 @@ package tests import ( + "errors" "fmt" "testing" "time" @@ -164,20 +165,33 @@ func createInitialAtx(t testing.TB, require.NoError(t, err) // 2.2 Create ATX with invalid POST - for i := range nipost.Post.Indices { - nipost.Post.Indices[i] += 1 + invalidPost := false + for i := range nipost.Post.Indices { // we only want some indices to be invalid but not all of them + for range 256 { // we manipulate each byte of the index to find one that causes validation to fail + nipost.Post.Indices[i] += 1 + + // Sanity check that the POST is invalid + err = verifier.Verify(ctx, (*shared.Proof)(nipost.Post), &shared.ProofMetadata{ + NodeId: signer.NodeID().Bytes(), + CommitmentAtxId: nipostChallenge.CommitmentATX.Bytes(), + NumUnits: nipost.NumUnits, + Challenge: nipost.PostMetadata.Challenge, + LabelsPerUnit: nipost.PostMetadata.LabelsPerUnit, + }) + var invalidIdxError *verifying.ErrInvalidIndex + if errors.As(err, &invalidIdxError) { + invalidPost = true + break + } + // post still valid try again + } + if invalidPost { + break + } + // try manipulating another index } - // Sanity check that the POST is invalid - err = verifier.Verify(ctx, (*shared.Proof)(nipost.Post), &shared.ProofMetadata{ - NodeId: signer.NodeID().Bytes(), - CommitmentAtxId: nipostChallenge.CommitmentATX.Bytes(), - NumUnits: nipost.NumUnits, - Challenge: nipost.PostMetadata.Challenge, - LabelsPerUnit: nipost.PostMetadata.LabelsPerUnit, - }) - var invalidIdxError *verifying.ErrInvalidIndex - require.ErrorAs(t, err, &invalidIdxError) + require.True(t, invalidPost, "expected invalid POST") switch version { case types.AtxV1: From 2b89775d77304a636d102fed84fef555908ae253 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:01:30 +0000 Subject: [PATCH 36/56] Separate tests --- .../distributed_post_verification_test.go.go | 475 ++++++++++++++++++ systest/tests/malfeasance_common.go | 252 ---------- systest/tests/malfeasance_test.go | 403 --------------- 3 files changed, 475 insertions(+), 655 deletions(-) create mode 100644 systest/tests/distributed_post_verification_test.go.go delete mode 100644 systest/tests/malfeasance_common.go delete mode 100644 systest/tests/malfeasance_test.go diff --git a/systest/tests/distributed_post_verification_test.go.go b/systest/tests/distributed_post_verification_test.go.go new file mode 100644 index 00000000000..f9971cc59ad --- /dev/null +++ b/systest/tests/distributed_post_verification_test.go.go @@ -0,0 +1,475 @@ +package tests + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/peer" + pb "github.com/spacemeshos/api/release/go/spacemesh/v1" + pb2 "github.com/spacemeshos/api/release/go/spacemesh/v2beta1" + "github.com/spacemeshos/go-scale" + "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/exp/maps" + "golang.org/x/sync/errgroup" + + "github.com/spacemeshos/go-spacemesh/activation" + "github.com/spacemeshos/go-spacemesh/activation/wire" + "github.com/spacemeshos/go-spacemesh/api/grpcserver" + "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/config" + "github.com/spacemeshos/go-spacemesh/datastore" + "github.com/spacemeshos/go-spacemesh/fetch" + "github.com/spacemeshos/go-spacemesh/fetch/peers" + "github.com/spacemeshos/go-spacemesh/malfeasance2" + "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/proposals/store" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql/localsql" + "github.com/spacemeshos/go-spacemesh/sql/localsql/nipost" + "github.com/spacemeshos/go-spacemesh/sql/statesql" + "github.com/spacemeshos/go-spacemesh/systest/cluster" + "github.com/spacemeshos/go-spacemesh/systest/testcontext" + "github.com/spacemeshos/go-spacemesh/timesync" +) + +type builtAtx interface { + ID() types.ATXID + + scale.Encodable + zapcore.ObjectMarshaler +} + +func version(cfg *config.Config, publish types.EpochID) types.AtxVersion { + epochs := append([]types.EpochID{0}, maps.Keys(cfg.AtxVersions)...) + version := types.AtxV1 + for _, epoch := range epochs { + if publish >= epoch { + version = cfg.AtxVersions[epoch] + } + } + return version +} + +// TestPostMalfeasanceV1Proof tests that nodes can detect an invalid PoST and create a malfeasance proof against it. +func TestPostMalfeasanceV1Proof(t *testing.T) { + t.Parallel() + + ctx := testcontext.New(t) + + // Prepare cluster + ctx.PoetSize = 1 // one poet guarantees everybody gets the same proof + ctx.ClusterSize = 5 + 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)))) + + logger := ctx.Log.Desugar().WithOptions(zap.IncreaseLevel(zap.InfoLevel), zap.WithCaller(false)) + cfg := getConfig(t, logger, cl, ctx) + publishEpoch := types.EpochID(1) + + testPostMalfeasance(t, cfg, cl, logger, ctx, publishEpoch) +} + +// TestPostMalfeasanceV2Proof tests that nodes can detect an invalid PoST and create a malfeasance proof against it. +func TestPostMalfeasanceV2Proof(t *testing.T) { + t.Parallel() + + ctx := testcontext.New(t) + + // Prepare cluster + ctx.PoetSize = 1 // one poet guarantees everybody gets the same proof + ctx.ClusterSize = 5 + 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)))) + + logger := ctx.Log.Desugar().WithOptions(zap.IncreaseLevel(zap.InfoLevel), zap.WithCaller(false)) + cfg := getConfig(t, logger, cl, ctx) + + var publishEpoch types.EpochID + for k, v := range cfg.AtxVersions { + if v == 2 { + publishEpoch = types.EpochID(k) + } + } + + testPostMalfeasance(t, cfg, cl, logger, ctx, publishEpoch) +} + +func testPostMalfeasance( + t *testing.T, + cfg *config.Config, + cl *cluster.Cluster, + logger *zap.Logger, + ctx *testcontext.Context, + publishEpoch types.EpochID, +) { + // Prepare config + testDir := t.TempDir() + + 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)) + + signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + db := statesql.InMemoryTest(t) + cdb := datastore.NewCachedDB(db, zap.NewNop()) + t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) + + prologue := fmt.Sprintf("%x-%v", cl.GenesisID(), cfg.LayersPerEpoch*2-1) + host, err := p2p.New(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()) }) + + clock, err := timesync.NewClock( + timesync.WithLayerDuration(cfg.LayerDuration), + timesync.WithTickInterval(1*time.Second), + timesync.WithGenesisTime(cl.Genesis()), + timesync.WithLogger(logger.Named("clock")), + ) + require.NoError(t, err) + t.Cleanup(clock.Close) + + proposalsStore := store.New( + store.WithEvictedLayer(clock.CurrentLayer()), + store.WithLogger(logger.Named("proposals-store")), + store.WithCapacity(cfg.Tortoise.Zdist+1), + ) + fetcher, err := fetch.NewFetch( + db, + proposalsStore, + host, + peers.New(), + fetch.WithContext(ctx), + fetch.WithConfig(cfg.FETCH), + fetch.WithLogger(logger.Named("fetcher")), + ) + require.NoError(t, err) + fetcher.SetValidators( + fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), + fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), + fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), + fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), + fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), + fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), + fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), + fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), + fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), + fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), + ) + require.NoError(t, fetcher.Start()) + t.Cleanup(fetcher.Stop) + + syncer := activation.NewMocksyncer(ctrl) + syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { + ch := make(chan struct{}) + close(ch) + return ch + }).AnyTimes() + + mocknipostValidator := activation.NewMocknipostValidator(ctrl) + postSetupMgr, err := activation.NewPostSetupManager( + cfg.POST, + logger.Named("post"), + cdb, + atxsdata.New(), + cl.GoldenATX(), + syncer, + mocknipostValidator, + ) + require.NoError(t, err) + builder := activation.NewMockatxBuilder(ctrl) + builder.EXPECT().Register(signer) + postSupervisor := activation.NewPostSupervisor( + logger.Named("post-supervisor"), + cfg.POST, + cfg.SMESHING.ProvingOpts, + postSetupMgr, + builder, + ) + require.NoError(t, postSupervisor.Start(cfg.POSTService, cfg.SMESHING.Opts, signer)) + t.Cleanup(func() { assert.NoError(t, postSupervisor.Stop(false)) }) + + verifyingOpts := activation.DefaultPostVerifyingOpts() + verifyingOpts.Workers = 1 + verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) + require.NoError(t, err) + + localDb := localsql.InMemoryTest(t) + + grpcPostService := grpcserver.NewPostService( + logger.Named("grpc-post-service"), + grpcserver.PostServiceQueryInterval(500*time.Millisecond), + ) + grpcPostService.AllowConnections(true) + 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()) }) + + certClient := activation.NewCertifierClient(db, localDb, logger.Named("certifier")) + certifier := activation.NewCertifier(localDb, logger, certClient) + poetDb, err := activation.NewPoetDb(db, zap.NewNop()) + require.NoError(t, err) + + poetService, err := activation.NewPoetService( + poetDb, + types.PoetServer{Address: cluster.MakePoetGlobalEndpoint(ctx.Namespace, 0)}, + cfg.POET, + logger, + 1, + activation.WithCertifier(certifier), + ) + require.NoError(t, err) + validator := activation.NewValidator(db, poetDb, cfg.POST, cfg.SMESHING.Opts.Scrypt, verifier) + nipostBuilder, err := activation.NewNIPostBuilder( + localDb, + grpcPostService, + logger.Named("nipostBuilder"), + cfg.POET, + clock, + validator, + activation.WithPoetServices(poetService), + ) + require.NoError(t, err) + var client activation.PostClient + for { + client, err = grpcPostService.Client(signer.NodeID()) + if err == nil { + break + } + logger.Info("waiting for poet service to connect") + time.Sleep(time.Second) + } + logger.Info("poet service to connected") + initialPost, initialPostInfo, err := client.Proof(ctx, shared.ZeroChallenge) + require.NoError(t, err) + + err = nipost.AddPost(localDb, signer.NodeID(), nipost.Post{ + Nonce: initialPost.Nonce, + Indices: initialPost.Indices, + Pow: initialPost.Pow, + Challenge: shared.ZeroChallenge, + NumUnits: initialPostInfo.NumUnits, + CommitmentATX: initialPostInfo.CommitmentATX, + VRFNonce: *initialPostInfo.Nonce, + }) + require.NoError(t, err) + + registerEpoch := publishEpoch - 1 + logger.Info("waiting for epoch to register at poet", + zap.Uint32("register_epoch", uint32(registerEpoch)), + zap.Uint32("publish_epoch", uint32(publishEpoch)), + ) + select { + case <-ctx.Done(): + require.Fail(t, "context canceled") + return + case <-clock.AwaitLayer(registerEpoch.FirstLayer()): + } + logger.Info("reached register epoch", zap.Uint32("register_epoch", uint32(registerEpoch))) + + registerEpoch = clock.CurrentLayer().GetEpoch() + publishEpoch = registerEpoch + 1 + nipostChallenge := &types.NIPostChallenge{ + PublishEpoch: publishEpoch, + PrevATXID: types.EmptyATXID, + PositioningATX: cl.GoldenATX(), + CommitmentATX: &initialPostInfo.CommitmentATX, + InitialPost: &types.Post{ + Nonce: initialPost.Nonce, + Indices: initialPost.Indices, Pow: initialPost.Pow, + }, + } + err = nipost.AddChallenge(localDb, signer.NodeID(), nipostChallenge) + require.NoError(t, err) + + version := version(cfg, nipostChallenge.PublishEpoch) + var challengeHash types.Hash32 + switch version { + case types.AtxV1: + challengeHash = wire.NIPostChallengeToWireV1(nipostChallenge).Hash() + case types.AtxV2: + challengeHash = wire.NIPostChallengeToWireV2(nipostChallenge).Hash() + default: + require.Fail(t, fmt.Sprintf("unsupported ATX version: %v", version)) + } + nipost, err := nipostBuilder.BuildNIPost(ctx, signer, challengeHash, nipostChallenge) + require.NoError(t, err) + invalidPost := false + for i := range nipost.Post.Indices { + for range 256 { + nipost.Post.Indices[i] += 1 + err = verifier.Verify(ctx, (*shared.Proof)(nipost.Post), &shared.ProofMetadata{ + NodeId: signer.NodeID().Bytes(), + CommitmentAtxId: nipostChallenge.CommitmentATX.Bytes(), + NumUnits: nipost.NumUnits, + Challenge: nipost.PostMetadata.Challenge, + LabelsPerUnit: nipost.PostMetadata.LabelsPerUnit, + }) + var invalidIdxError *verifying.ErrInvalidIndex + if errors.As(err, &invalidIdxError) { + invalidPost = true + break + } + } + if invalidPost { + break + } + } + require.True(t, invalidPost, "expected invalid POST") + + var atx builtAtx + switch version { + case types.AtxV1: + watx := &wire.ActivationTxV1{ + InnerActivationTxV1: wire.InnerActivationTxV1{ + NIPostChallengeV1: *wire.NIPostChallengeToWireV1(nipostChallenge), + Coinbase: types.Address{1, 2, 3, 4}, + NumUnits: nipost.NumUnits, + NIPost: wire.NiPostToWireV1(nipost.NIPost), + VRFNonce: (*uint64)(&nipost.VRFNonce), + }, + } + watx.Sign(signer) + atx = watx + case types.AtxV2: + watx := &wire.ActivationTxV2{ + PublishEpoch: nipostChallenge.PublishEpoch, + PositioningATX: nipostChallenge.PositioningATX, + Coinbase: types.Address{1, 2, 3, 4}, + VRFNonce: (uint64)(nipost.VRFNonce), + NIPosts: []wire.NIPostV2{ + { + Membership: wire.MerkleProofV2{ + Nodes: nipost.NIPost.Membership.Nodes, + }, + Challenge: types.Hash32(nipost.PostMetadata.Challenge), + Posts: []wire.SubPostV2{ + { + Post: *wire.PostToWireV1(nipost.Post), + NumUnits: nipost.NumUnits, + MembershipLeafIndex: nipost.NIPost.Membership.LeafIndex, + }, + }, + }, + }, + Initial: &wire.InitialAtxPartsV2{ + Post: *wire.PostToWireV1(nipostChallenge.InitialPost), + CommitmentATX: *nipostChallenge.CommitmentATX, + }, + } + watx.Sign(signer) + atx = watx + default: + require.Fail(t, fmt.Sprintf("unsupported ATX version: %v", version)) + return + } + + publishCtx, stopPublishing := context.WithCancel(ctx.Context) + defer stopPublishing() + require.NoError(t, cl.WaitAll(ctx)) + logger.Info("waiting for publish epoch", + zap.Uint32("epoch", publishEpoch.Uint32()), + zap.Uint32("layer", publishEpoch.FirstLayer().Uint32()), + ) + 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 < publishEpoch.FirstLayer().Uint32(), nil + }) + require.NoError(t, err) + + var eg errgroup.Group // 4. Publish ATX + t.Cleanup(func() { assert.NoError(t, eg.Wait()) }) + eg.Go(func() error { + for { + logger.Info("publishing ATX", zap.Object("atx", atx)) + buf := codec.MustEncode(atx) + err = host.Publish(ctx, pubsub.AtxProtocol, buf) + require.NoError(t, err) + select { + case <-publishCtx.Done(): + return nil + case <-time.After(10 * time.Second): + } + } + }) + + receivedProof := false + timeout := time.Minute * 2 + logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) + awaitCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + err = malfeasanceStream(awaitCtx, cl.Client(0), logger, func(proof *pb2.MalfeasanceProof) (bool, error) { + if !bytes.Equal(proof.GetSmesher(), signer.NodeID().Bytes()) { + return true, nil + } + stopPublishing() + logger.Info("malfeasance proof received") + require.Equal(t, malfeasance2.InvalidActivation, proof.Domain) + require.Equal(t, "InvalidPoSTProof", proof.Type) + require.Equal(t, atx.ID(), proof.Properties["atx"]) + receivedProof = true + return false, nil + }) + require.NoError(t, err) + require.True(t, receivedProof, "malfeasance proof not received") +} + +func getConfig(t testing.TB, logger *zap.Logger, cl *cluster.Cluster, ctx *testcontext.Context) *config.Config { + cfg, err := cl.NodeConfig(ctx) + require.NoError(t, err) + + types.SetLayersPerEpoch(cfg.LayersPerEpoch) + + cfg.POET.RequestTimeout = time.Minute + cfg.POET.MaxRequestRetries = 10 + + 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 + logger.Debug("Prepared config", zap.Any("cfg", cfg)) + return cfg +} diff --git a/systest/tests/malfeasance_common.go b/systest/tests/malfeasance_common.go deleted file mode 100644 index 0e44e09ecc6..00000000000 --- a/systest/tests/malfeasance_common.go +++ /dev/null @@ -1,252 +0,0 @@ -package tests - -import ( - "errors" - "fmt" - "testing" - "time" - - "github.com/spacemeshos/go-scale" - "github.com/spacemeshos/post/shared" - "github.com/spacemeshos/post/verifying" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "golang.org/x/exp/maps" - - "github.com/spacemeshos/go-spacemesh/activation" - "github.com/spacemeshos/go-spacemesh/activation/wire" - "github.com/spacemeshos/go-spacemesh/api/grpcserver" - "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/config" - "github.com/spacemeshos/go-spacemesh/signing" - "github.com/spacemeshos/go-spacemesh/sql" - "github.com/spacemeshos/go-spacemesh/sql/localsql/nipost" - "github.com/spacemeshos/go-spacemesh/systest/cluster" - "github.com/spacemeshos/go-spacemesh/systest/testcontext" - "github.com/spacemeshos/go-spacemesh/timesync" -) - -type builtAtx interface { - ID() types.ATXID - - scale.Encodable - zapcore.ObjectMarshaler -} - -func createInitialAtx(t testing.TB, - ctx *testcontext.Context, - logger *zap.Logger, - cl *cluster.Cluster, - cfg *config.Config, - signer *signing.EdSigner, - db sql.StateDatabase, - localDb sql.LocalDatabase, - clock *timesync.NodeClock, - verifier activation.PostVerifier, - publishEpoch types.EpochID, -) (builtAtx, types.EpochID) { - // 2. create ATX with invalid POST labels - grpcPostService := grpcserver.NewPostService( - logger.Named("grpc-post-service"), - grpcserver.PostServiceQueryInterval(500*time.Millisecond), - ) - grpcPostService.AllowConnections(true) - - 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()) }) - - // 2.1. Create initial POST - certClient := activation.NewCertifierClient(db, localDb, logger.Named("certifier")) - certifier := activation.NewCertifier(localDb, logger, certClient) - poetDb, err := activation.NewPoetDb(db, zap.NewNop()) - require.NoError(t, err) - poetService, err := activation.NewPoetService( - poetDb, - types.PoetServer{Address: cluster.MakePoetGlobalEndpoint(ctx.Namespace, 0)}, - cfg.POET, - logger, - 1, - activation.WithCertifier(certifier), - ) - require.NoError(t, err) - - validator := activation.NewValidator( - db, - poetDb, - cfg.POST, - cfg.SMESHING.Opts.Scrypt, - verifier, - ) - - nipostBuilder, err := activation.NewNIPostBuilder( - localDb, - grpcPostService, - logger.Named("nipostBuilder"), - cfg.POET, - clock, - validator, - activation.WithPoetServices(poetService), - ) - require.NoError(t, err) - - var client activation.PostClient - for { - client, err = grpcPostService.Client(signer.NodeID()) - if err == nil { - break - } - ctx.Log.Info("waiting for poet service to connect") - time.Sleep(time.Second) - } - ctx.Log.Info("poet service to connected") - initialPost, initialPostInfo, err := client.Proof(ctx, shared.ZeroChallenge) - require.NoError(t, err) - - err = nipost.AddPost(localDb, signer.NodeID(), nipost.Post{ - Nonce: initialPost.Nonce, - Indices: initialPost.Indices, - Pow: initialPost.Pow, - Challenge: shared.ZeroChallenge, - NumUnits: initialPostInfo.NumUnits, - CommitmentATX: initialPostInfo.CommitmentATX, - VRFNonce: *initialPostInfo.Nonce, - }) - require.NoError(t, err) - - registerEpoch := publishEpoch - 1 - ctx.Log.Desugar().Info("waiting for epoch to register at poet", - zap.Uint32("register_epoch", uint32(registerEpoch)), - zap.Uint32("publish_epoch", uint32(publishEpoch)), - ) - select { - case <-ctx.Done(): - ctx.Log.Info("context canceled") - return nil, 0 - case <-clock.AwaitLayer(registerEpoch.FirstLayer()): - } - ctx.Log.Desugar().Info("reached register epoch", zap.Uint32("register_epoch", uint32(registerEpoch))) - - registerEpoch = clock.CurrentLayer().GetEpoch() - publishEpoch = registerEpoch + 1 - nipostChallenge := &types.NIPostChallenge{ - PublishEpoch: publishEpoch, - PrevATXID: types.EmptyATXID, - PositioningATX: cl.GoldenATX(), - CommitmentATX: &initialPostInfo.CommitmentATX, - InitialPost: &types.Post{ - Nonce: initialPost.Nonce, - Indices: initialPost.Indices, - Pow: initialPost.Pow, - }, - } - err = nipost.AddChallenge(localDb, signer.NodeID(), nipostChallenge) - require.NoError(t, err) - - version := version(cfg, nipostChallenge.PublishEpoch) - var challengeHash types.Hash32 - switch version { - case types.AtxV1: - challengeHash = wire.NIPostChallengeToWireV1(nipostChallenge).Hash() - case types.AtxV2: - challengeHash = wire.NIPostChallengeToWireV2(nipostChallenge).Hash() - default: - require.Fail(t, fmt.Sprintf("unsupported ATX version: %v", version)) - } - nipost, err := nipostBuilder.BuildNIPost(ctx, signer, challengeHash, nipostChallenge) - require.NoError(t, err) - - // 2.2 Create ATX with invalid POST - invalidPost := false - for i := range nipost.Post.Indices { // we only want some indices to be invalid but not all of them - for range 256 { // we manipulate each byte of the index to find one that causes validation to fail - nipost.Post.Indices[i] += 1 - - // Sanity check that the POST is invalid - err = verifier.Verify(ctx, (*shared.Proof)(nipost.Post), &shared.ProofMetadata{ - NodeId: signer.NodeID().Bytes(), - CommitmentAtxId: nipostChallenge.CommitmentATX.Bytes(), - NumUnits: nipost.NumUnits, - Challenge: nipost.PostMetadata.Challenge, - LabelsPerUnit: nipost.PostMetadata.LabelsPerUnit, - }) - var invalidIdxError *verifying.ErrInvalidIndex - if errors.As(err, &invalidIdxError) { - invalidPost = true - break - } - // post still valid try again - } - if invalidPost { - break - } - // try manipulating another index - } - - require.True(t, invalidPost, "expected invalid POST") - - switch version { - case types.AtxV1: - atx := &wire.ActivationTxV1{ - InnerActivationTxV1: wire.InnerActivationTxV1{ - NIPostChallengeV1: *wire.NIPostChallengeToWireV1(nipostChallenge), - Coinbase: types.Address{1, 2, 3, 4}, - NumUnits: nipost.NumUnits, - NIPost: wire.NiPostToWireV1(nipost.NIPost), - VRFNonce: (*uint64)(&nipost.VRFNonce), - }, - } - atx.Sign(signer) - return atx, atx.PublishEpoch - case types.AtxV2: - atx := &wire.ActivationTxV2{ - PublishEpoch: nipostChallenge.PublishEpoch, - PositioningATX: nipostChallenge.PositioningATX, - Coinbase: types.Address{1, 2, 3, 4}, - VRFNonce: (uint64)(nipost.VRFNonce), - NIPosts: []wire.NIPostV2{ - { - Membership: wire.MerkleProofV2{ - Nodes: nipost.NIPost.Membership.Nodes, - }, - Challenge: types.Hash32(nipost.PostMetadata.Challenge), - Posts: []wire.SubPostV2{ - { - Post: *wire.PostToWireV1(nipost.Post), - NumUnits: nipost.NumUnits, - MembershipLeafIndex: nipost.NIPost.Membership.LeafIndex, - }, - }, - }, - }, - Initial: &wire.InitialAtxPartsV2{ - Post: *wire.PostToWireV1(nipostChallenge.InitialPost), - CommitmentATX: *nipostChallenge.CommitmentATX, - }, - } - atx.Sign(signer) - return atx, atx.PublishEpoch - default: - require.Fail(t, fmt.Sprintf("unsupported ATX version: %v", version)) - return nil, 0 - } -} - -func version(cfg *config.Config, publish types.EpochID) types.AtxVersion { - epochs := append([]types.EpochID{0}, maps.Keys(cfg.AtxVersions)...) - version := types.AtxV1 - for _, epoch := range epochs { - if publish >= epoch { - version = cfg.AtxVersions[epoch] - } - } - return version -} diff --git a/systest/tests/malfeasance_test.go b/systest/tests/malfeasance_test.go deleted file mode 100644 index b1fd9561b08..00000000000 --- a/systest/tests/malfeasance_test.go +++ /dev/null @@ -1,403 +0,0 @@ -package tests - -import ( - "bytes" - "context" - "fmt" - "os" - "path/filepath" - "testing" - "time" - - "github.com/libp2p/go-libp2p/core/peer" - pb "github.com/spacemeshos/api/release/go/spacemesh/v1" - pb2 "github.com/spacemeshos/api/release/go/spacemesh/v2beta1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" - "go.uber.org/zap" - "golang.org/x/sync/errgroup" - - "github.com/spacemeshos/go-spacemesh/activation" - "github.com/spacemeshos/go-spacemesh/atxsdata" - "github.com/spacemeshos/go-spacemesh/codec" - "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/config" - "github.com/spacemeshos/go-spacemesh/datastore" - "github.com/spacemeshos/go-spacemesh/fetch" - "github.com/spacemeshos/go-spacemesh/fetch/peers" - mwire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" - "github.com/spacemeshos/go-spacemesh/malfeasance2" - "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/proposals/store" - "github.com/spacemeshos/go-spacemesh/signing" - "github.com/spacemeshos/go-spacemesh/sql" - "github.com/spacemeshos/go-spacemesh/sql/localsql" - "github.com/spacemeshos/go-spacemesh/sql/statesql" - "github.com/spacemeshos/go-spacemesh/systest/cluster" - "github.com/spacemeshos/go-spacemesh/systest/testcontext" - "github.com/spacemeshos/go-spacemesh/timesync" -) - -// TestPostMalfeasanceProof tests that nodes can detect an invalid PoST and create a malfeasance proof against it. -func TestPostMalfeasanceProof(t *testing.T) { - t.Parallel() - - ctx := testcontext.New(t) - - // Prepare cluster - ctx.PoetSize = 1 // one poet guarantees everybody gets the same proof - ctx.ClusterSize = 5 - 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)))) - - var eg errgroup.Group - eg.Go(func() error { - logger := ctx.Log.Desugar().Named("malfeasance1-test"). - WithOptions(zap.IncreaseLevel(zap.InfoLevel), zap.WithCaller(false)) - - // Prepare config - cfg := getConfig(t, cl, ctx) - testDir := t.TempDir() - - 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)) - - signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) - require.NoError(t, err) - - ctrl := gomock.NewController(t) - db := statesql.InMemoryTest(t) - cdb := datastore.NewCachedDB(db, zap.NewNop()) - t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) - - host := setupHost(t, logger, cl, cfg) - clock := setupClock(t, logger, cl, cfg) - setupFetcher(t, cl, ctx, logger, cfg, db, clock, host) - - syncer := activation.NewMocksyncer(ctrl) - syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { - ch := make(chan struct{}) - close(ch) - return ch - }).AnyTimes() - - initPost(t, cl, ctx, logger, cfg, signer, cdb, syncer) - - verifyingOpts := activation.DefaultPostVerifyingOpts() - verifyingOpts.Workers = 1 - verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) - require.NoError(t, err) - - localDb := localsql.InMemoryTest(t) - atx, publishEpoch := createInitialAtx( - t, - ctx, - logger, - cl, - cfg, - signer, - db, - localDb, - clock, - verifier, - types.EpochID(1), - ) - ctx.Log.Desugar().Info("created initial ATX", - zap.Object("atx", atx), - zap.Uint32("pub_epoch", publishEpoch.Uint32()), - ) - - publishCtx, stopPublishing := context.WithCancel(ctx.Context) - defer stopPublishing() - publishATX(t, cl, ctx, logger, host, publishCtx, publishEpoch, atx) - ctx.Log.Desugar().Info("published initial ATX", - zap.Object("atx", atx), - zap.Uint32("pub_epoch", publishEpoch.Uint32()), - ) - - receivedProof := false - timeout := time.Minute * 2 - logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) - awaitCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - err = malfeasanceStream(awaitCtx, cl.Client(0), logger, func(proof *pb2.MalfeasanceProof) (bool, error) { - if !bytes.Equal(proof.GetSmesher(), signer.NodeID().Bytes()) { - return true, nil - } - stopPublishing() - logger.Info("malfeasance proof received") - require.Equal(t, 0, proof.Domain) - require.Equal(t, mwire.InvalidPostIndex, proof.Type) - require.Equal(t, atx.ID(), proof.Properties["atx"]) - receivedProof = true - return false, nil - }) - require.NoError(t, err) - require.True(t, receivedProof, "malfeasance proof not received") - return nil - }) - - eg.Go(func() error { - logger := ctx.Log.Desugar().Named("malfeasance2-test"). - WithOptions(zap.IncreaseLevel(zap.InfoLevel), zap.WithCaller(false)) - - // Prepare config - cfg := getConfig(t, cl, ctx) - testDir := t.TempDir() - - 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)) - - signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) - require.NoError(t, err) - - ctrl := gomock.NewController(t) - db := statesql.InMemoryTest(t) - cdb := datastore.NewCachedDB(db, zap.NewNop()) - t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) - - host := setupHost(t, logger, cl, cfg) - clock := setupClock(t, logger, cl, cfg) - setupFetcher(t, cl, ctx, logger, cfg, db, clock, host) - - syncer := activation.NewMocksyncer(ctrl) - syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { - ch := make(chan struct{}) - close(ch) - return ch - }).AnyTimes() - - cfg.API.PostListener = "0.0.0.0:10094" // avoid port conflict with v1 identity - initPost(t, cl, ctx, logger, cfg, signer, cdb, syncer) - - verifyingOpts := activation.DefaultPostVerifyingOpts() - verifyingOpts.Workers = 1 - verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) - require.NoError(t, err) - - localDb := localsql.InMemoryTest(t) - var publishEpoch types.EpochID - for k, v := range cfg.AtxVersions { - if v == 2 { - publishEpoch = types.EpochID(k) - } - } - atx, publishEpoch := createInitialAtx( - t, - ctx, - logger, - cl, - cfg, - signer, - db, - localDb, - clock, - verifier, - publishEpoch, - ) - - publishCtx, stopPublishing := context.WithCancel(ctx.Context) - defer stopPublishing() - publishATX(t, cl, ctx, logger, host, publishCtx, publishEpoch, atx) - - receivedProof := false - timeout := time.Minute * 2 - logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) - awaitCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - err = malfeasanceStream(awaitCtx, cl.Client(0), logger, func(proof *pb2.MalfeasanceProof) (bool, error) { - if !bytes.Equal(proof.GetSmesher(), signer.NodeID().Bytes()) { - return true, nil - } - stopPublishing() - logger.Info("malfeasance proof received") - require.Equal(t, malfeasance2.InvalidActivation, proof.Domain) - require.Equal(t, "InvalidPoSTProof", proof.Type) - require.Equal(t, atx.ID(), proof.Properties["atx"]) - receivedProof = true - return false, nil - }) - require.NoError(t, err) - require.True(t, receivedProof, "malfeasance proof not received") - return nil - }) - eg.Wait() -} - -func getConfig(t testing.TB, cl *cluster.Cluster, ctx *testcontext.Context) *config.Config { - cfg, err := cl.NodeConfig(ctx) - require.NoError(t, err) - - types.SetLayersPerEpoch(cfg.LayersPerEpoch) - - cfg.POET.RequestTimeout = time.Minute - cfg.POET.MaxRequestRetries = 10 - - 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.Desugar().Debug("Prepared config", zap.Any("cfg", cfg)) - return cfg -} - -func setupHost(tb testing.TB, logger *zap.Logger, cl *cluster.Cluster, cfg *config.Config) *p2p.Host { - prologue := fmt.Sprintf("%x-%v", cl.GenesisID(), cfg.LayersPerEpoch*2-1) - host, err := p2p.New( - logger.Named("p2p"), - cfg.P2P, - []byte(prologue), - handshake.NetworkCookie(prologue), - ) - require.NoError(tb, 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(tb, host.Start()) - tb.Cleanup(func() { assert.NoError(tb, host.Stop()) }) - return host -} - -func setupClock(tb testing.TB, logger *zap.Logger, cl *cluster.Cluster, cfg *config.Config) *timesync.NodeClock { - clock, err := timesync.NewClock( - timesync.WithLayerDuration(cfg.LayerDuration), - timesync.WithTickInterval(1*time.Second), - timesync.WithGenesisTime(cl.Genesis()), - timesync.WithLogger(logger.Named("clock")), - ) - require.NoError(tb, err) - tb.Cleanup(clock.Close) - return clock -} - -func setupFetcher( - tb testing.TB, - cl *cluster.Cluster, - ctx *testcontext.Context, - logger *zap.Logger, - cfg *config.Config, - db sql.StateDatabase, - clock *timesync.NodeClock, - host *p2p.Host, -) *fetch.Fetch { - proposalsStore := store.New( - store.WithEvictedLayer(clock.CurrentLayer()), - store.WithLogger(logger.Named("proposals-store")), - store.WithCapacity(cfg.Tortoise.Zdist+1), - ) - - fetcher, err := fetch.NewFetch(db, proposalsStore, host, - peers.New(), - fetch.WithContext(ctx), - fetch.WithConfig(cfg.FETCH), - fetch.WithLogger(logger.Named("fetcher")), - ) - require.NoError(tb, err) - - fetcher.SetValidators( - fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), - fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), - fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), - fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), - fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), - fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), - fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), - fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), - fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), - fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), - ) - - require.NoError(tb, fetcher.Start()) - tb.Cleanup(fetcher.Stop) - return fetcher -} - -func initPost( - tb testing.TB, - cl *cluster.Cluster, - ctx *testcontext.Context, - logger *zap.Logger, - cfg *config.Config, - signer *signing.EdSigner, - cdb *datastore.CachedDB, - syncer *activation.Mocksyncer, -) { - ctrl := gomock.NewController(tb) - postSetupMgr, err := activation.NewPostSetupManager( - cfg.POST, - logger.Named("post"), - cdb, - atxsdata.New(), - cl.GoldenATX(), - syncer, - activation.NewMocknipostValidator(ctrl), - ) - require.NoError(tb, err) - - builder := activation.NewMockatxBuilder(ctrl) - builder.EXPECT().Register(signer) - postSupervisor := activation.NewPostSupervisor( - logger.Named("post-supervisor"), - cfg.POST, - cfg.SMESHING.ProvingOpts, - postSetupMgr, - builder, - ) - require.NoError(tb, postSupervisor.Start(cfg.POSTService, cfg.SMESHING.Opts, signer)) - tb.Cleanup(func() { assert.NoError(tb, postSupervisor.Stop(false)) }) -} - -func publishATX( - tb testing.TB, - cl *cluster.Cluster, - ctx *testcontext.Context, - logger *zap.Logger, - host *p2p.Host, - publishCtx context.Context, - publishEpoch types.EpochID, - atx builtAtx, -) { - // 3. Wait for publish epoch - require.NoError(tb, cl.WaitAll(ctx)) - logger.Sugar().Infow("waiting for publish epoch", "epoch", publishEpoch, "layer", publishEpoch.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 < publishEpoch.FirstLayer().Uint32(), nil - }) - require.NoError(tb, err) - - // 4. Publish ATX - var eg errgroup.Group - tb.Cleanup(func() { assert.NoError(tb, eg.Wait()) }) - eg.Go(func() error { - for { - logger.Info("publishing ATX", zap.Object("atx", atx)) - buf := codec.MustEncode(atx) - err = host.Publish(ctx, pubsub.AtxProtocol, buf) - require.NoError(tb, err) - - select { - case <-publishCtx.Done(): - return nil - case <-time.After(10 * time.Second): - } - } - }) -} From e227831cef8c4d6a4eec3ca13b9605e8652e3bd9 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:11:28 +0000 Subject: [PATCH 37/56] Cleanup --- ... => distributed_post_verification_test.go} | 89 ++++++++++++------- 1 file changed, 56 insertions(+), 33 deletions(-) rename systest/tests/{distributed_post_verification_test.go.go => distributed_post_verification_test.go} (94%) diff --git a/systest/tests/distributed_post_verification_test.go.go b/systest/tests/distributed_post_verification_test.go similarity index 94% rename from systest/tests/distributed_post_verification_test.go.go rename to systest/tests/distributed_post_verification_test.go index f9971cc59ad..64fad7471a0 100644 --- a/systest/tests/distributed_post_verification_test.go.go +++ b/systest/tests/distributed_post_verification_test.go @@ -135,21 +135,23 @@ func testPostMalfeasance( signer, err := signing.NewEdSigner(signing.WithPrefix(cl.GenesisID().Bytes())) require.NoError(t, err) - ctrl := gomock.NewController(t) - db := statesql.InMemoryTest(t) - cdb := datastore.NewCachedDB(db, zap.NewNop()) - t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) - prologue := fmt.Sprintf("%x-%v", cl.GenesisID(), cfg.LayersPerEpoch*2-1) - host, err := p2p.New(logger.Named("p2p"), cfg.P2P, []byte(prologue), handshake.NetworkCookie(prologue)) + host, err := p2p.New( + 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 - }) + 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()) }) + db := statesql.InMemoryTest(t) + cdb := datastore.NewCachedDB(db, zap.NewNop()) + t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) + clock, err := timesync.NewClock( timesync.WithLayerDuration(cfg.LayerDuration), timesync.WithTickInterval(1*time.Second), @@ -164,16 +166,14 @@ func testPostMalfeasance( store.WithLogger(logger.Named("proposals-store")), store.WithCapacity(cfg.Tortoise.Zdist+1), ) - fetcher, err := fetch.NewFetch( - db, - proposalsStore, - host, + fetcher, err := fetch.NewFetch(db, proposalsStore, host, peers.New(), fetch.WithContext(ctx), fetch.WithConfig(cfg.FETCH), fetch.WithLogger(logger.Named("fetcher")), ) require.NoError(t, err) + fetcher.SetValidators( fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), @@ -186,9 +186,11 @@ func testPostMalfeasance( fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), fetch.ValidatorFunc(func(context.Context, types.Hash32, peer.ID, []byte) error { return nil }), ) + require.NoError(t, fetcher.Start()) t.Cleanup(fetcher.Stop) + ctrl := gomock.NewController(t) syncer := activation.NewMocksyncer(ctrl) syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { ch := make(chan struct{}) @@ -196,7 +198,7 @@ func testPostMalfeasance( return ch }).AnyTimes() - mocknipostValidator := activation.NewMocknipostValidator(ctrl) + // 1. Initialize postSetupMgr, err := activation.NewPostSetupManager( cfg.POST, logger.Named("post"), @@ -204,9 +206,10 @@ func testPostMalfeasance( atxsdata.New(), cl.GoldenATX(), syncer, - mocknipostValidator, + activation.NewMocknipostValidator(ctrl), ) require.NoError(t, err) + builder := activation.NewMockatxBuilder(ctrl) builder.EXPECT().Register(signer) postSupervisor := activation.NewPostSupervisor( @@ -219,32 +222,28 @@ func testPostMalfeasance( require.NoError(t, postSupervisor.Start(cfg.POSTService, cfg.SMESHING.Opts, signer)) t.Cleanup(func() { assert.NoError(t, postSupervisor.Stop(false)) }) - verifyingOpts := activation.DefaultPostVerifyingOpts() - verifyingOpts.Workers = 1 - verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) - require.NoError(t, err) - - localDb := localsql.InMemoryTest(t) - + // 2. create ATX with invalid POST labels grpcPostService := grpcserver.NewPostService( logger.Named("grpc-post-service"), grpcserver.PostServiceQueryInterval(500*time.Millisecond), ) grpcPostService.AllowConnections(true) + grpcPrivateServer, err := grpcserver.NewWithServices( cfg.API.PostListener, logger.Named("grpc-server"), - cfg.API, []grpcserver.ServiceAPI{grpcPostService}, + cfg.API, + []grpcserver.ServiceAPI{grpcPostService}, ) require.NoError(t, err) require.NoError(t, grpcPrivateServer.Start()) t.Cleanup(func() { assert.NoError(t, grpcPrivateServer.Close()) }) + localDb := localsql.InMemoryTest(t) certClient := activation.NewCertifierClient(db, localDb, logger.Named("certifier")) certifier := activation.NewCertifier(localDb, logger, certClient) poetDb, err := activation.NewPoetDb(db, zap.NewNop()) require.NoError(t, err) - poetService, err := activation.NewPoetService( poetDb, types.PoetServer{Address: cluster.MakePoetGlobalEndpoint(ctx.Namespace, 0)}, @@ -254,7 +253,20 @@ func testPostMalfeasance( activation.WithCertifier(certifier), ) require.NoError(t, err) - validator := activation.NewValidator(db, poetDb, cfg.POST, cfg.SMESHING.Opts.Scrypt, verifier) + + verifyingOpts := activation.DefaultPostVerifyingOpts() + verifyingOpts.Workers = 1 + verifier, err := activation.NewPostVerifier(cfg.POST, logger, activation.WithVerifyingOpts(verifyingOpts)) + require.NoError(t, err) + + validator := activation.NewValidator( + db, + poetDb, + cfg.POST, + cfg.SMESHING.Opts.Scrypt, + verifier, + ) + nipostBuilder, err := activation.NewNIPostBuilder( localDb, grpcPostService, @@ -265,16 +277,19 @@ func testPostMalfeasance( activation.WithPoetServices(poetService), ) require.NoError(t, err) + + // 2.1. Create initial POST var client activation.PostClient for { client, err = grpcPostService.Client(signer.NodeID()) - if err == nil { - break + if err != nil { + logger.Info("waiting for post service to connect") + time.Sleep(time.Second) + continue } - logger.Info("waiting for poet service to connect") - time.Sleep(time.Second) + break } - logger.Info("poet service to connected") + logger.Info("post service connected") initialPost, initialPostInfo, err := client.Proof(ctx, shared.ZeroChallenge) require.NoError(t, err) @@ -329,6 +344,9 @@ func testPostMalfeasance( } nipost, err := nipostBuilder.BuildNIPost(ctx, signer, challengeHash, nipostChallenge) require.NoError(t, err) + + // 2.2 Create ATX with invalid POST + logger.Info("invalidating PoST") invalidPost := false for i := range nipost.Post.Indices { for range 256 { @@ -351,6 +369,7 @@ func testPostMalfeasance( } } require.True(t, invalidPost, "expected invalid POST") + logger.Info("PoST invalidated") var atx builtAtx switch version { @@ -399,8 +418,7 @@ func testPostMalfeasance( return } - publishCtx, stopPublishing := context.WithCancel(ctx.Context) - defer stopPublishing() + // 3. Wait for publish epoch require.NoError(t, cl.WaitAll(ctx)) logger.Info("waiting for publish epoch", zap.Uint32("epoch", publishEpoch.Uint32()), @@ -412,7 +430,10 @@ func testPostMalfeasance( }) require.NoError(t, err) - var eg errgroup.Group // 4. Publish ATX + // 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 { @@ -420,6 +441,7 @@ func testPostMalfeasance( buf := codec.MustEncode(atx) err = host.Publish(ctx, pubsub.AtxProtocol, buf) require.NoError(t, err) + select { case <-publishCtx.Done(): return nil @@ -428,6 +450,7 @@ func testPostMalfeasance( } }) + // 5. Wait for POST malfeasance proof receivedProof := false timeout := time.Minute * 2 logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) From 3d16a41d5d435cb52dafc7790378de4b378beabb Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:14:01 +0000 Subject: [PATCH 38/56] More cleanup --- systest/tests/distributed_post_verification_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 64fad7471a0..45d96e2c64e 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -166,6 +166,7 @@ func testPostMalfeasance( store.WithLogger(logger.Named("proposals-store")), store.WithCapacity(cfg.Tortoise.Zdist+1), ) + fetcher, err := fetch.NewFetch(db, proposalsStore, host, peers.New(), fetch.WithContext(ctx), From 8c99ec681a661e38a1c25e19b53395dca5114d76 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:43:31 +0000 Subject: [PATCH 39/56] Run sequentially instead of parallel --- .../distributed_post_verification_test.go | 43 ++++++------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 45d96e2c64e..73a5fd1634e 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -16,7 +16,6 @@ import ( "github.com/spacemeshos/go-scale" "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" @@ -66,8 +65,8 @@ func version(cfg *config.Config, publish types.EpochID) types.AtxVersion { return version } -// TestPostMalfeasanceV1Proof tests that nodes can detect an invalid PoST and create a malfeasance proof against it. -func TestPostMalfeasanceV1Proof(t *testing.T) { +// TestPostMalfeasanceProof tests that nodes can detect an invalid PoST and create a malfeasance proof against it. +func TestPostMalfeasanceProof(t *testing.T) { t.Parallel() ctx := testcontext.New(t) @@ -83,36 +82,18 @@ func TestPostMalfeasanceV1Proof(t *testing.T) { logger := ctx.Log.Desugar().WithOptions(zap.IncreaseLevel(zap.InfoLevel), zap.WithCaller(false)) cfg := getConfig(t, logger, cl, ctx) - publishEpoch := types.EpochID(1) + // Test malfeasance for each ATX version, malfeasance1 in first epoch + publishEpoch := types.EpochID(1) testPostMalfeasance(t, cfg, cl, logger, ctx, publishEpoch) -} - -// TestPostMalfeasanceV2Proof tests that nodes can detect an invalid PoST and create a malfeasance proof against it. -func TestPostMalfeasanceV2Proof(t *testing.T) { - t.Parallel() - - ctx := testcontext.New(t) - - // Prepare cluster - ctx.PoetSize = 1 // one poet guarantees everybody gets the same proof - ctx.ClusterSize = 5 - 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)))) - - logger := ctx.Log.Desugar().WithOptions(zap.IncreaseLevel(zap.InfoLevel), zap.WithCaller(false)) - cfg := getConfig(t, logger, cl, ctx) - var publishEpoch types.EpochID for k, v := range cfg.AtxVersions { if v == 2 { publishEpoch = types.EpochID(k) } } + // malfeasance2 in first epoch with ATXv2 testPostMalfeasance(t, cfg, cl, logger, ctx, publishEpoch) } @@ -146,11 +127,11 @@ func testPostMalfeasance( 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()) }) + defer host.Stop() db := statesql.InMemoryTest(t) cdb := datastore.NewCachedDB(db, zap.NewNop()) - t.Cleanup(func() { assert.NoError(t, cdb.Close()) }) + defer cdb.Close() clock, err := timesync.NewClock( timesync.WithLayerDuration(cfg.LayerDuration), @@ -159,7 +140,7 @@ func testPostMalfeasance( timesync.WithLogger(logger.Named("clock")), ) require.NoError(t, err) - t.Cleanup(clock.Close) + defer clock.Close() proposalsStore := store.New( store.WithEvictedLayer(clock.CurrentLayer()), @@ -189,7 +170,7 @@ func testPostMalfeasance( ) require.NoError(t, fetcher.Start()) - t.Cleanup(fetcher.Stop) + defer fetcher.Stop() ctrl := gomock.NewController(t) syncer := activation.NewMocksyncer(ctrl) @@ -221,7 +202,7 @@ func testPostMalfeasance( builder, ) require.NoError(t, postSupervisor.Start(cfg.POSTService, cfg.SMESHING.Opts, signer)) - t.Cleanup(func() { assert.NoError(t, postSupervisor.Stop(false)) }) + defer postSupervisor.Stop(false) // 2. create ATX with invalid POST labels grpcPostService := grpcserver.NewPostService( @@ -238,7 +219,7 @@ func testPostMalfeasance( ) require.NoError(t, err) require.NoError(t, grpcPrivateServer.Start()) - t.Cleanup(func() { assert.NoError(t, grpcPrivateServer.Close()) }) + defer grpcPrivateServer.Close() localDb := localsql.InMemoryTest(t) certClient := activation.NewCertifierClient(db, localDb, logger.Named("certifier")) @@ -435,7 +416,7 @@ func testPostMalfeasance( publishCtx, stopPublishing := context.WithCancel(ctx.Context) defer stopPublishing() var eg errgroup.Group - t.Cleanup(func() { assert.NoError(t, eg.Wait()) }) + defer eg.Wait() eg.Go(func() error { for { logger.Info("publishing ATX", zap.Object("atx", atx)) From 80583724343ce33f4b45138e57e46dfb09176039 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Wed, 29 Jan 2025 00:08:56 +0000 Subject: [PATCH 40/56] Fix wrong ATX version selection --- systest/tests/distributed_post_verification_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 73a5fd1634e..089f6d3b8cb 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "testing" "time" @@ -55,7 +56,9 @@ type builtAtx interface { } func version(cfg *config.Config, publish types.EpochID) types.AtxVersion { - epochs := append([]types.EpochID{0}, maps.Keys(cfg.AtxVersions)...) + cfg.AtxVersions[0] = types.AtxV1 + epochs := maps.Keys(cfg.AtxVersions) + slices.Sort(epochs) version := types.AtxV1 for _, epoch := range epochs { if publish >= epoch { From cadc317e24548e3b5809fead6eb8725267ead2b1 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:19:29 +0000 Subject: [PATCH 41/56] Fix assertions --- systest/tests/distributed_post_verification_test.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 089f6d3b8cb..ae1cb6ce867 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "slices" + "strconv" "testing" "time" @@ -34,7 +35,7 @@ import ( "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/fetch" "github.com/spacemeshos/go-spacemesh/fetch/peers" - "github.com/spacemeshos/go-spacemesh/malfeasance2" + "github.com/spacemeshos/go-spacemesh/malfeasance" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/handshake" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" @@ -357,6 +358,8 @@ func testPostMalfeasance( logger.Info("PoST invalidated") var atx builtAtx + var expectedDomain pb2.MalfeasanceProof_MalfeasanceDomain + var expectedType string switch version { case types.AtxV1: watx := &wire.ActivationTxV1{ @@ -370,6 +373,8 @@ func testPostMalfeasance( } watx.Sign(signer) atx = watx + expectedDomain = pb2.MalfeasanceProof_DOMAIN_UNSPECIFIED + expectedType = strconv.FormatUint(uint64(malfeasance.InvalidPostIndex), 10) case types.AtxV2: watx := &wire.ActivationTxV2{ PublishEpoch: nipostChallenge.PublishEpoch, @@ -398,6 +403,8 @@ func testPostMalfeasance( } watx.Sign(signer) atx = watx + expectedDomain = pb2.MalfeasanceProof_DOMAIN_ACTIVATION + expectedType = "InvalidPoSTProof" default: require.Fail(t, fmt.Sprintf("unsupported ATX version: %v", version)) return @@ -447,8 +454,8 @@ func testPostMalfeasance( } stopPublishing() logger.Info("malfeasance proof received") - require.Equal(t, malfeasance2.InvalidActivation, proof.Domain) - require.Equal(t, "InvalidPoSTProof", proof.Type) + require.Equal(t, expectedDomain, proof.Domain) + require.Equal(t, expectedType, proof.Type) require.Equal(t, atx.ID(), proof.Properties["atx"]) receivedProof = true return false, nil From 5856c6a2048020ddb36018cf4940ead91eda4583 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:36:27 +0000 Subject: [PATCH 42/56] Add more info to logs --- p2p/server/deadline_adjuster.go | 3 ++- syncer/malsync/syncer.go | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/p2p/server/deadline_adjuster.go b/p2p/server/deadline_adjuster.go index 634e6382721..e03a33f5464 100644 --- a/p2p/server/deadline_adjuster.go +++ b/p2p/server/deadline_adjuster.go @@ -39,7 +39,8 @@ func (err *deadlineAdjusterError) Error() string { err.totalWritten, err.timeout, err.hardTimeout, - err.innerErr) + err.innerErr, + ) } type deadlineAdjuster struct { diff --git a/syncer/malsync/syncer.go b/syncer/malsync/syncer.go index afd6344aae4..7ccf4672d66 100644 --- a/syncer/malsync/syncer.go +++ b/syncer/malsync/syncer.go @@ -265,8 +265,8 @@ func (s *Syncer) shouldSync(epochStart, epochEnd time.Time) (bool, error) { } func (s *Syncer) downloadLegacy(parent context.Context, initial bool) error { - s.logger.Info("starting malfeasance proof sync", log.ZContext(parent)) - defer s.logger.Debug("malfeasance proof sync terminated", log.ZContext(parent)) + s.logger.Info("starting legacy malfeasance proof sync", log.ZContext(parent)) + defer s.logger.Debug("legacy malfeasance proof sync terminated", log.ZContext(parent)) ctx, cancel := context.WithCancel(parent) eg, ctx := errgroup.WithContext(ctx) updates := make(chan malUpdate, s.cfg.MalfeasanceIDPeers) From a85a1544730a0cefe98b2a1cc50106d8d3048e83 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:53:06 +0000 Subject: [PATCH 43/56] Allow type to be a string in malfeasance 2 --- api/grpcserver/v2alpha1/malfeasance.go | 6 +++--- api/grpcserver/v2alpha1/malfeasance_test.go | 12 ++++++------ api/grpcserver/v2beta1/malfeasance.go | 6 +++--- api/grpcserver/v2beta1/malfeasance_test.go | 12 ++++++------ .../distributed_post_verification_test.go | 19 ++++++++++++------- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/api/grpcserver/v2alpha1/malfeasance.go b/api/grpcserver/v2alpha1/malfeasance.go index 10e91527aca..42502056cf1 100644 --- a/api/grpcserver/v2alpha1/malfeasance.go +++ b/api/grpcserver/v2alpha1/malfeasance.go @@ -280,12 +280,12 @@ func fetchMetaData( zap.String("type", properties["type"]), zap.Error(err), ) - return nil + } else { + delete(properties, "type") } - delete(properties, "type") return &spacemeshv2alpha1.MalfeasanceProof{ Smesher: id.Bytes(), - Domain: spacemeshv2alpha1.MalfeasanceProof_MalfeasanceDomain(domain), // TODO(mafa): add new domains + Domain: spacemeshv2alpha1.MalfeasanceProof_MalfeasanceDomain(domain), Type: uint32(proofType), Properties: properties, } diff --git a/api/grpcserver/v2alpha1/malfeasance_test.go b/api/grpcserver/v2alpha1/malfeasance_test.go index 4ed7616e3c2..9589d9b0aff 100644 --- a/api/grpcserver/v2alpha1/malfeasance_test.go +++ b/api/grpcserver/v2alpha1/malfeasance_test.go @@ -62,7 +62,7 @@ func TestMalfeasanceService_List(t *testing.T) { proofs[i] = malInfo{ID: types.RandomNodeID(), Proof: types.RandomBytes(100)} proofs[i].Properties = map[string]string{ "domain": strconv.FormatUint(uint64(i%4+1), 10), - "type": strconv.FormatUint(uint64(i%4+1), 10), + "type": fmt.Sprintf("Type %d", i%4+1), fmt.Sprintf("key%d", i): fmt.Sprintf("value%d", i), } info.EXPECT().Info(gomock.Any(), proofs[i].ID).DoAndReturn( @@ -92,7 +92,7 @@ func TestMalfeasanceService_List(t *testing.T) { proofs[70].Proof = types.RandomBytes(100) proofs[70].Properties = map[string]string{ "domain": "1", - "type": "1", + "type": "Type Marry", "key": "value", } info.EXPECT().Info(gomock.Any(), proofs[70].ID).DoAndReturn( @@ -198,7 +198,7 @@ func TestMalfeasanceStreamService_Stream(t *testing.T) { proofs[i] = malInfo{ID: types.RandomNodeID(), Proof: types.RandomBytes(100)} proofs[i].Properties = map[string]string{ "domain": strconv.FormatUint(uint64(i%4+1), 10), - "type": strconv.FormatUint(uint64(i%4+1), 10), + "type": fmt.Sprintf("Type %d", i%4+1), fmt.Sprintf("key%d", i): fmt.Sprintf("value%d", i), } info.EXPECT().Info(gomock.Any(), proofs[i].ID).DoAndReturn( @@ -228,7 +228,7 @@ func TestMalfeasanceStreamService_Stream(t *testing.T) { proofs[70].Proof = types.RandomBytes(100) proofs[70].Properties = map[string]string{ "domain": "1", - "type": "1", + "type": "Type Marry", "key": "value", } info.EXPECT().Info(gomock.Any(), proofs[70].ID).DoAndReturn( @@ -324,7 +324,7 @@ func TestMalfeasanceStreamService_Stream(t *testing.T) { }) properties := map[string]string{ "domain": strconv.FormatUint(uint64(i%4+1), 10), - "type": strconv.FormatUint(uint64(i%4+1), 10), + "type": fmt.Sprintf("Type %d", i%4+1), fmt.Sprintf("key%d", i): fmt.Sprintf("value%d", i), } info.EXPECT().Info(gomock.Any(), streamed[i].Smesher).DoAndReturn( @@ -344,7 +344,7 @@ func TestMalfeasanceStreamService_Stream(t *testing.T) { }) properties := map[string]string{ "domain": "1", - "type": "1", + "type": "Type Marry", "key": "value", } info.EXPECT().Info(gomock.Any(), streamed[i].Smesher).DoAndReturn( diff --git a/api/grpcserver/v2beta1/malfeasance.go b/api/grpcserver/v2beta1/malfeasance.go index 1ce85fe254d..3a1150dd166 100644 --- a/api/grpcserver/v2beta1/malfeasance.go +++ b/api/grpcserver/v2beta1/malfeasance.go @@ -280,12 +280,12 @@ func fetchMetaData( zap.String("type", properties["type"]), zap.Error(err), ) - return nil + } else { + delete(properties, "type") } - delete(properties, "type") return &spacemeshv2beta1.MalfeasanceProof{ Smesher: id.Bytes(), - Domain: spacemeshv2beta1.MalfeasanceProof_MalfeasanceDomain(domain), // TODO(mafa): add new domains + Domain: spacemeshv2beta1.MalfeasanceProof_MalfeasanceDomain(domain), Type: uint32(proofType), Properties: properties, } diff --git a/api/grpcserver/v2beta1/malfeasance_test.go b/api/grpcserver/v2beta1/malfeasance_test.go index 2572f5aac1e..50945796c49 100644 --- a/api/grpcserver/v2beta1/malfeasance_test.go +++ b/api/grpcserver/v2beta1/malfeasance_test.go @@ -62,7 +62,7 @@ func TestMalfeasanceService_List(t *testing.T) { proofs[i] = malInfo{ID: types.RandomNodeID(), Proof: types.RandomBytes(100)} proofs[i].Properties = map[string]string{ "domain": strconv.FormatUint(uint64(i%4+1), 10), - "type": strconv.FormatUint(uint64(i%4+1), 10), + "type": fmt.Sprintf("Type %d", i%4+1), fmt.Sprintf("key%d", i): fmt.Sprintf("value%d", i), } info.EXPECT().Info(gomock.Any(), proofs[i].ID).DoAndReturn( @@ -92,7 +92,7 @@ func TestMalfeasanceService_List(t *testing.T) { proofs[70].Proof = types.RandomBytes(100) proofs[70].Properties = map[string]string{ "domain": "1", - "type": "1", + "type": "Type Marry", "key": "value", } info.EXPECT().Info(gomock.Any(), proofs[70].ID).DoAndReturn( @@ -198,7 +198,7 @@ func TestMalfeasanceStreamService_Stream(t *testing.T) { proofs[i] = malInfo{ID: types.RandomNodeID(), Proof: types.RandomBytes(100)} proofs[i].Properties = map[string]string{ "domain": strconv.FormatUint(uint64(i%4+1), 10), - "type": strconv.FormatUint(uint64(i%4+1), 10), + "type": fmt.Sprintf("Type %d", i%4+1), fmt.Sprintf("key%d", i): fmt.Sprintf("value%d", i), } info.EXPECT().Info(gomock.Any(), proofs[i].ID).DoAndReturn( @@ -228,7 +228,7 @@ func TestMalfeasanceStreamService_Stream(t *testing.T) { proofs[70].Proof = types.RandomBytes(100) proofs[70].Properties = map[string]string{ "domain": "1", - "type": "1", + "type": "Type Marry", "key": "value", } info.EXPECT().Info(gomock.Any(), proofs[70].ID).DoAndReturn( @@ -324,7 +324,7 @@ func TestMalfeasanceStreamService_Stream(t *testing.T) { }) properties := map[string]string{ "domain": strconv.FormatUint(uint64(i%4+1), 10), - "type": strconv.FormatUint(uint64(i%4+1), 10), + "type": fmt.Sprintf("Type %d", i%4+1), fmt.Sprintf("key%d", i): fmt.Sprintf("value%d", i), } info.EXPECT().Info(gomock.Any(), streamed[i].Smesher).DoAndReturn( @@ -344,7 +344,7 @@ func TestMalfeasanceStreamService_Stream(t *testing.T) { }) properties := map[string]string{ "domain": "1", - "type": "1", + "type": "Type Marry", "key": "value", } info.EXPECT().Info(gomock.Any(), streamed[i].Smesher).DoAndReturn( diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index ae1cb6ce867..a7136cfd803 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -8,7 +8,6 @@ import ( "os" "path/filepath" "slices" - "strconv" "testing" "time" @@ -35,7 +34,6 @@ import ( "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/fetch" "github.com/spacemeshos/go-spacemesh/fetch/peers" - "github.com/spacemeshos/go-spacemesh/malfeasance" "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/handshake" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" @@ -357,9 +355,12 @@ func testPostMalfeasance( require.True(t, invalidPost, "expected invalid POST") logger.Info("PoST invalidated") - var atx builtAtx - var expectedDomain pb2.MalfeasanceProof_MalfeasanceDomain - var expectedType string + var ( + atx builtAtx + expectedDomain pb2.MalfeasanceProof_MalfeasanceDomain + expectedType uint32 + ) + expectedProperties := make(map[string]string) switch version { case types.AtxV1: watx := &wire.ActivationTxV1{ @@ -374,7 +375,8 @@ func testPostMalfeasance( watx.Sign(signer) atx = watx expectedDomain = pb2.MalfeasanceProof_DOMAIN_UNSPECIFIED - expectedType = strconv.FormatUint(uint64(malfeasance.InvalidPostIndex), 10) + expectedType = 4 + expectedProperties["atx"] = atx.ID().String() case types.AtxV2: watx := &wire.ActivationTxV2{ PublishEpoch: nipostChallenge.PublishEpoch, @@ -404,7 +406,9 @@ func testPostMalfeasance( watx.Sign(signer) atx = watx expectedDomain = pb2.MalfeasanceProof_DOMAIN_ACTIVATION - expectedType = "InvalidPoSTProof" + expectedType = 0 + expectedProperties["type"] = "InvalidPoSTProof" + expectedProperties["atx"] = atx.ID().String() default: require.Fail(t, fmt.Sprintf("unsupported ATX version: %v", version)) return @@ -456,6 +460,7 @@ func testPostMalfeasance( logger.Info("malfeasance proof received") require.Equal(t, expectedDomain, proof.Domain) require.Equal(t, expectedType, proof.Type) + require.Subset(t, proof.Properties, expectedProperties) require.Equal(t, atx.ID(), proof.Properties["atx"]) receivedProof = true return false, nil From 4f30b2f81800a9ffd44efb891cb07125c5fb6193 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:24:50 +0000 Subject: [PATCH 44/56] ATXID is short string --- systest/tests/distributed_post_verification_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index a7136cfd803..df884935f2c 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -461,7 +461,7 @@ func testPostMalfeasance( require.Equal(t, expectedDomain, proof.Domain) require.Equal(t, expectedType, proof.Type) require.Subset(t, proof.Properties, expectedProperties) - require.Equal(t, atx.ID(), proof.Properties["atx"]) + require.Equal(t, atx.ID().ShortString(), proof.Properties["atx"]) receivedProof = true return false, nil }) From 0ffed26094fd36f47998b3d2bf737c8a6150ccf0 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:29:08 +0000 Subject: [PATCH 45/56] Publishing should be on the same timeout as streaming for the malfeasance proof --- systest/tests/distributed_post_verification_test.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index df884935f2c..1431aad881c 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -427,7 +427,8 @@ func testPostMalfeasance( require.NoError(t, err) // 4. Publish ATX - publishCtx, stopPublishing := context.WithCancel(ctx.Context) + timeout := time.Minute * 2 + publishCtx, stopPublishing := context.WithTimeout(ctx.Context, timeout) defer stopPublishing() var eg errgroup.Group defer eg.Wait() @@ -441,18 +442,15 @@ func testPostMalfeasance( select { case <-publishCtx.Done(): return nil - case <-time.After(10 * time.Second): + case <-time.After(10 * time.Second): // retry every 10 seconds until context is done } } }) // 5. Wait for POST malfeasance proof receivedProof := false - timeout := time.Minute * 2 logger.Info("waiting for malfeasance proof", zap.Duration("timeout", timeout)) - awaitCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - err = malfeasanceStream(awaitCtx, cl.Client(0), logger, func(proof *pb2.MalfeasanceProof) (bool, error) { + err = malfeasanceStream(publishCtx, cl.Client(0), logger, func(proof *pb2.MalfeasanceProof) (bool, error) { if !bytes.Equal(proof.GetSmesher(), signer.NodeID().Bytes()) { return true, nil } From 7c263da007b53bd806f13cf79386437c6b0aa8bf Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:31:58 +0000 Subject: [PATCH 46/56] Increase logging of systests --- systest/parameters/fastnet/smesher.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/systest/parameters/fastnet/smesher.json b/systest/parameters/fastnet/smesher.json index fca3cf6fc71..05d066b4548 100644 --- a/systest/parameters/fastnet/smesher.json +++ b/systest/parameters/fastnet/smesher.json @@ -44,10 +44,15 @@ } }, "logging": { - "log-encoder": "json", - "txHandler": "debug", + "atxHandler": "debug", + "fetcher": "debug", "grpc": "debug", + "log-encoder": "json", + "malfeasance": "debug", + "malfeasance2": "debug", + "nipostBuilder": "debug", + "nipostValidator": "debug", "sync": "debug", - "fetcher": "debug" + "txHandler": "debug" } } From 3181538694aa783569e3ed5963ec38eb9c64f34f Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Wed, 29 Jan 2025 13:28:20 +0000 Subject: [PATCH 47/56] Increase probability to detect invalid post labels --- activation/handler_v1.go | 8 ++++---- activation/handler_v2.go | 13 +++++++++---- config/presets/fastnet.go | 2 +- systest/tests/distributed_post_verification_test.go | 2 +- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/activation/handler_v1.go b/activation/handler_v1.go index 2eeb4f3af28..f59b4071d15 100644 --- a/activation/handler_v1.go +++ b/activation/handler_v1.go @@ -223,12 +223,12 @@ func (h *HandlerV1) syntacticallyValidateDeps( watx.NumUnits, PostSubset([]byte(h.local)), // use the local peer ID as seed for random subset ) - var invalidIdx *verifying.ErrInvalidIndex - if errors.As(err, &invalidIdx) { + var errInvalidIdx *verifying.ErrInvalidIndex + if errors.As(err, &errInvalidIdx) { h.logger.Debug("ATX with invalid post index", log.ZContext(ctx), zap.Stringer("atx_id", watx.ID()), - zap.Int("index", invalidIdx.Index), + zap.Int("index", errInvalidIdx.Index), ) malicious, err := identities.IsMalicious(h.cdb, watx.SmesherID) if err != nil { @@ -250,7 +250,7 @@ func (h *HandlerV1) syntacticallyValidateDeps( Type: mwire.InvalidPostIndex, Data: &mwire.InvalidPostIndexProof{ Atx: *watx, - InvalidIdx: uint32(invalidIdx.Index), + InvalidIdx: uint32(errInvalidIdx.Index), }, }, } diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 74574fae4d0..1e967c36492 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -679,10 +679,15 @@ func (h *HandlerV2) validatePost( if err == nil { return nil } - errInvalid := &verifying.ErrInvalidIndex{} - if !errors.As(err, &errInvalid) { + errInvalidIdx := &verifying.ErrInvalidIndex{} + if !errors.As(err, &errInvalidIdx) { return fmt.Errorf("validating post for ID %s: %w", nodeID.ShortString(), err) } + h.logger.Debug("ATX with invalid post index", + log.ZContext(ctx), + zap.Stringer("atx_id", atx.ID()), + zap.Int("index", errInvalidIdx.Index), + ) // check if post contains at least one valid label validIdx := 0 @@ -715,7 +720,7 @@ func (h *HandlerV2) validatePost( commitment, nodeID, nipostIndex, - uint32(errInvalid.Index), + uint32(errInvalidIdx.Index), uint32(validIdx), ) if err != nil { @@ -724,7 +729,7 @@ func (h *HandlerV2) validatePost( if err := h.malPublisher.Publish(ctx, nodeID, proof); err != nil { return fmt.Errorf("publishing malfeasance proof for invalid post: %w", err) } - return fmt.Errorf("invalid post for ID %s: %w", nodeID.ShortString(), errInvalid) + return fmt.Errorf("invalid post for ID %s: %w", nodeID.ShortString(), errInvalidIdx) } func (h *HandlerV2) checkMalicious(ctx context.Context, tx sql.Transaction, atx *activationTx) (bool, error) { diff --git a/config/presets/fastnet.go b/config/presets/fastnet.go index ba9838d26d3..19292ad671c 100644 --- a/config/presets/fastnet.go +++ b/config/presets/fastnet.go @@ -68,7 +68,7 @@ func fastnet() config.Config { conf.POST.K1 = 12 conf.POST.K2 = 4 - conf.POST.K3 = 1 + conf.POST.K3 = 2 conf.POST.LabelsPerUnit = 128 conf.POST.MaxNumUnits = 4 conf.POST.MinNumUnits = 2 diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 1431aad881c..4b50846fdab 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -75,7 +75,7 @@ func TestPostMalfeasanceProof(t *testing.T) { // Prepare cluster ctx.PoetSize = 1 // one poet guarantees everybody gets the same proof - ctx.ClusterSize = 5 + ctx.ClusterSize = 8 cl := cluster.New(ctx, cluster.WithKeys(10)) require.NoError(t, cl.AddBootnodes(ctx, 1)) require.NoError(t, cl.AddBootstrappers(ctx)) From abacfb294bf51a805f2761cc6f1021c19a8b2876 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:11:42 +0000 Subject: [PATCH 48/56] Add more logging --- activation/malfeasance2.go | 1 + malfeasance2/publisher.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/activation/malfeasance2.go b/activation/malfeasance2.go index 9c5c6135afc..d76beacbcdc 100644 --- a/activation/malfeasance2.go +++ b/activation/malfeasance2.go @@ -80,6 +80,7 @@ func (p *MalfeasanceHandlerV2) Publish(ctx context.Context, nodeID types.NodeID, Proof: codec.MustEncode(proof), } + p.logger.Debug("publishing ATX malfeasance proof", log.ZShortStringer("node_id", nodeID)) return p.malPublisher.PublishATXProof(ctx, nodeID, codec.MustEncode(atxProof)) } diff --git a/malfeasance2/publisher.go b/malfeasance2/publisher.go index 86a61a52ab6..4a6fecee10b 100644 --- a/malfeasance2/publisher.go +++ b/malfeasance2/publisher.go @@ -12,6 +12,7 @@ import ( "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" @@ -123,8 +124,13 @@ func (p *Publisher) PublishATXProof(ctx context.Context, nodeID types.NodeID, pr return nil }) if err != nil { + p.logger.Error("failed to persist malfeasance proof", + zap.Error(err), + log.ZShortStringer("node_id", nodeID), + ) return err } + p.logger.Debug("persisted malfeasance proof", log.ZShortStringer("node_id", nodeID)) if !publish { // all smeshers were already marked as malicious - no gossip to void spamming the network return nil @@ -206,6 +212,14 @@ func (p *Publisher) publish( p.logger.Error("failed to broadcast malfeasance proof", zap.Error(err)) return fmt.Errorf("broadcast atx malfeasance proof: %w", err) } + p.logger.Debug("broadcasted malfeasance proof", + zap.Array("smesher_ids", zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { + for _, nodeID := range nodeID { + enc.AppendString(nodeID.ShortString()) + } + return nil + })), + ) return nil } From 21f08dffa95abce75d1a58a455056fefa6e3b98e Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Wed, 29 Jan 2025 20:17:15 +0000 Subject: [PATCH 49/56] Avoid nested tx when publishing malfeasance proofs --- activation/handler_v2.go | 159 ++++++++++++++++++++++++--------------- 1 file changed, 97 insertions(+), 62 deletions(-) diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 1e967c36492..c6a191f88e0 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -732,37 +732,76 @@ func (h *HandlerV2) validatePost( return fmt.Errorf("invalid post for ID %s: %w", nodeID.ShortString(), errInvalidIdx) } -func (h *HandlerV2) checkMalicious(ctx context.Context, tx sql.Transaction, atx *activationTx) (bool, error) { - malicious, err := malfeasance.IsMalicious(tx, atx.SmesherID) - if err != nil { - return malicious, fmt.Errorf("checking if node is malicious: %w", err) - } - if malicious { +func (h *HandlerV2) checkMalicious(ctx context.Context, watx *activationTx, republishProof bool) (bool, error) { + if republishProof { + if err := h.malPublisher.Regossip(ctx, watx.SmesherID); err != nil { + h.logger.Error("failed to regossip malfeasance proof", + zap.Stringer("atx_id", watx.ID()), + zap.Stringer("smesher_id", watx.SmesherID), + zap.Error(err), + ) + return true, err + } return true, nil } - malicious, err = h.checkDoubleMarry(ctx, tx, atx) - if err != nil { - return malicious, fmt.Errorf("checking double marry: %w", err) - } - if malicious { - return true, nil - } + var malicious bool + var proof wire.Proof + var nodeID types.NodeID + err := h.cdb.WithTxImmediate(ctx, func(tx sql.Transaction) error { + // malfeasance check happens after storing the ATX because storing updates the marriage set + // that is needed for the malfeasance proof + // + // TODO(mafa): don't store own ATX if it would mark the node as malicious + // this probably needs to be done by validating and storing own ATXs eagerly and skipping validation in + // the gossip handler (not sync!) + var err error + malicious, err = malfeasance.IsMalicious(tx, watx.SmesherID) + if err != nil { + return fmt.Errorf("checking if node is malicious: %w", err) + } + if malicious { + return nil + } + + proof, nodeID, err = h.checkDoubleMarry(ctx, tx, watx) + if err != nil { + return fmt.Errorf("checking double marry: %w", err) + } + if proof != nil { + return nil + } + + proof, nodeID, err = h.checkDoubleMerge(ctx, tx, watx) + if err != nil { + return fmt.Errorf("checking double merge: %w", err) + } + if proof != nil { + return nil + } - malicious, err = h.checkDoubleMerge(ctx, tx, atx) + proof, nodeID, err = h.checkPrevAtx(ctx, tx, watx) + if err != nil { + return fmt.Errorf("checking previous ATX: %w", err) + } + return nil + }) if err != nil { - return malicious, fmt.Errorf("checking double merge: %w", err) + return malicious, fmt.Errorf("check malicious: %w", err) } - if malicious { - return true, nil + if proof == nil { + return malicious, nil } - malicious, err = h.checkPrevAtx(ctx, tx, atx) - if err != nil { - return malicious, fmt.Errorf("checking previous ATX: %w", err) + if err := h.malPublisher.Publish(ctx, nodeID, proof); err != nil { + h.logger.Error("failed to publish malfeasance proof", + zap.Stringer("atx_id", watx.ID()), + zap.Stringer("smesher_id", watx.SmesherID), + zap.Error(err), + ) + return true, err } - - return malicious, err + return true, nil } func (h *HandlerV2) fetchWireAtx( @@ -783,11 +822,15 @@ func (h *HandlerV2) fetchWireAtx( return atx, nil } -func (h *HandlerV2) checkDoubleMarry(ctx context.Context, tx sql.Transaction, atx *activationTx) (bool, error) { +func (h *HandlerV2) checkDoubleMarry( + ctx context.Context, + tx sql.Transaction, + atx *activationTx, +) (wire.Proof, types.NodeID, error) { for _, m := range atx.marriages { info, err := marriage.FindByNodeID(tx, m.id) if err != nil { - return false, fmt.Errorf("checking if ID is married: %w", err) + return nil, types.EmptyNodeID, fmt.Errorf("checking if ID is married: %w", err) } if info.ATX == atx.ID() { continue @@ -800,28 +843,32 @@ func (h *HandlerV2) checkDoubleMarry(ctx context.Context, tx sql.Transaction, at zap.Stringer("atx_id", info.ATX), ) case err != nil: - return false, fmt.Errorf("fetching other ATX: %w", err) + return nil, types.EmptyNodeID, fmt.Errorf("fetching other ATX: %w", err) } proof, err := wire.NewDoubleMarryProof(tx, atx.ActivationTxV2, otherAtx, m.id) if err != nil { - return true, fmt.Errorf("creating double marry proof: %w", err) + return nil, types.EmptyNodeID, fmt.Errorf("creating double marry proof: %w", err) } - return true, h.malPublisher.Publish(ctx, m.id, proof) + return proof, m.id, nil } - return false, nil + return nil, types.EmptyNodeID, nil } -func (h *HandlerV2) checkDoubleMerge(ctx context.Context, tx sql.Transaction, atx *activationTx) (bool, error) { +func (h *HandlerV2) checkDoubleMerge( + ctx context.Context, + tx sql.Transaction, + atx *activationTx, +) (wire.Proof, types.NodeID, error) { if atx.MarriageATX == nil { - return false, nil + return nil, types.EmptyNodeID, nil } ids, err := atxs.MergeConflict(tx, *atx.MarriageATX, atx.PublishEpoch) switch { case errors.Is(err, sql.ErrNotFound): - return false, nil + return nil, types.EmptyNodeID, nil case err != nil: - return false, fmt.Errorf("searching for ATXs with the same marriage ATX: %w", err) + return nil, types.EmptyNodeID, fmt.Errorf("searching for ATXs with the same marriage ATX: %w", err) } otherIndex := slices.IndexFunc(ids, func(id types.ATXID) bool { return id != atx.ID() }) other := ids[otherIndex] @@ -841,7 +888,7 @@ func (h *HandlerV2) checkDoubleMerge(ctx context.Context, tx sql.Transaction, at // see https://github.com/spacemeshos/go-spacemesh/issues/6434 otherAtx, err := h.fetchWireAtx(ctx, tx, other) if err != nil { - return false, fmt.Errorf("fetching other ATX: %w", err) + return nil, types.EmptyNodeID, fmt.Errorf("fetching other ATX: %w", err) } // TODO(mafa): checkpoints need to include all marriage ATXs in full to be able to create malfeasance proofs @@ -850,16 +897,20 @@ func (h *HandlerV2) checkDoubleMerge(ctx context.Context, tx sql.Transaction, at // see https://github.com/spacemeshos/go-spacemesh/issues/6435 proof, err := wire.NewDoubleMergeProof(tx, atx.ActivationTxV2, otherAtx) if err != nil { - return true, fmt.Errorf("creating double merge proof: %w", err) + return nil, types.EmptyNodeID, fmt.Errorf("creating double merge proof: %w", err) } - return true, h.malPublisher.Publish(ctx, atx.ActivationTxV2.SmesherID, proof) + return proof, atx.ActivationTxV2.SmesherID, nil } -func (h *HandlerV2) checkPrevAtx(ctx context.Context, tx sql.Transaction, atx *activationTx) (bool, error) { +func (h *HandlerV2) checkPrevAtx( + ctx context.Context, + tx sql.Transaction, + atx *activationTx, +) (wire.Proof, types.NodeID, error) { for id, data := range atx.ids { expectedPrevID, err := atxs.PrevIDByNodeID(tx, id, atx.PublishEpoch) if err != nil && !errors.Is(err, sql.ErrNotFound) { - return false, fmt.Errorf("get last atx by node id: %w", err) + return nil, types.EmptyNodeID, fmt.Errorf("get last atx by node id: %w", err) } if expectedPrevID == data.previous { continue @@ -876,7 +927,7 @@ func (h *HandlerV2) checkPrevAtx(ctx context.Context, tx sql.Transaction, atx *a case errors.Is(err, sql.ErrNotFound): continue case err != nil: - return true, fmt.Errorf("checking for previous ATX collision: %w", err) + return nil, types.EmptyNodeID, fmt.Errorf("checking for previous ATX collision: %w", err) } var wireAtxV1 *wire.ActivationTxV1 @@ -887,7 +938,7 @@ func (h *HandlerV2) checkPrevAtx(ctx context.Context, tx sql.Transaction, atx *a var blob sql.Blob v, err := atxs.LoadBlob(ctx, tx, collision.Bytes(), &blob) if err != nil { - return true, fmt.Errorf("get atx blob %s: %w", id.ShortString(), err) + return nil, types.EmptyNodeID, fmt.Errorf("get atx blob %s: %w", id.ShortString(), err) } switch v { case types.AtxV1: @@ -908,9 +959,9 @@ func (h *HandlerV2) checkPrevAtx(ctx context.Context, tx sql.Transaction, atx *a ) proof, err := wire.NewInvalidPrevAtxProofV2(tx, atx.ActivationTxV2, wireAtx, id) if err != nil { - return true, fmt.Errorf("creating invalid previous ATX proof: %w", err) + return nil, types.EmptyNodeID, fmt.Errorf("creating invalid previous ATX proof: %w", err) } - return true, h.malPublisher.Publish(ctx, id, proof) + return proof, id, nil default: h.logger.Fatal("Failed to create invalid previous ATX proof: unknown ATX version", zap.Stringer("atx_id", collision), @@ -926,11 +977,11 @@ func (h *HandlerV2) checkPrevAtx(ctx context.Context, tx sql.Transaction, atx *a ) proof, err := wire.NewInvalidPrevAtxProofV1(tx, atx.ActivationTxV2, wireAtxV1, id) if err != nil { - return true, fmt.Errorf("creating invalid previous ATX proof: %w", err) + return nil, types.EmptyNodeID, fmt.Errorf("creating invalid previous ATX proof: %w", err) } - return true, h.malPublisher.Publish(ctx, id, proof) + return proof, id, nil } - return false, nil + return nil, types.EmptyNodeID, nil } // Store an ATX in the DB. @@ -1019,25 +1070,9 @@ func (h *HandlerV2) storeAtx(ctx context.Context, atx *types.ActivationTx, watx return fmt.Errorf("store atx: %w", err) } - malicious := false - err := h.cdb.WithTxImmediate(ctx, func(tx sql.Transaction) error { - // malfeasance check happens after storing the ATX because storing updates the marriage set - // that is needed for the malfeasance proof - // - // TODO(mafa): don't store own ATX if it would mark the node as malicious - // this probably needs to be done by validating and storing own ATXs eagerly and skipping validation in - // the gossip handler (not sync!) - if republishProof { - malicious = true - return h.malPublisher.Regossip(ctx, atx.SmesherID) - } - - var err error - malicious, err = h.checkMalicious(ctx, tx, watx) - return err - }) + malicious, err := h.checkMalicious(ctx, watx, republishProof) if err != nil { - return fmt.Errorf("check malicious: %w", err) + return err } h.beacon.OnAtx(atx) From a7c003f88b188e87556cf3d5d4f7440cee23964c Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 30 Jan 2025 14:03:41 +0000 Subject: [PATCH 50/56] Move check for identities existence into proof validation, allow invalid post without proof for identities existence --- activation/interface.go | 2 +- activation/malfeasance2.go | 11 ++- activation/malfeasance2_test.go | 56 +++++++++++++- activation/mocks.go | 12 +-- activation/wire/interface.go | 7 ++ activation/wire/malfeasance_double_marry.go | 9 +++ .../wire/malfeasance_double_marry_test.go | 41 ++++++++++ activation/wire/malfeasance_double_merge.go | 27 +++++-- .../wire/malfeasance_double_merge_test.go | 74 ++++++++++++++++++ activation/wire/malfeasance_invalid_post.go | 4 + .../wire/malfeasance_invalid_prev_atx.go | 13 ++++ .../wire/malfeasance_invalid_prev_atx_test.go | 37 +++++++++ activation/wire/mocks.go | 77 +++++++++++++++++++ malfeasance2/handler.go | 15 ++-- malfeasance2/handler_test.go | 52 ------------- malfeasance2/publisher.go | 12 ++- malfeasance2/publisher_test.go | 72 ++++++++++++++--- node/node.go | 1 + 18 files changed, 431 insertions(+), 91 deletions(-) diff --git a/activation/interface.go b/activation/interface.go index a49f05496e0..91fcc9f1bb8 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -116,7 +116,7 @@ type atxMalfeasancePublisher interface { // and mark the associated identity as malfeasant. We do this to prevent spamming the network with proofs for identities // where most likely the network already knows they are malicious. type malfeasancePublisher interface { - PublishATXProof(ctx context.Context, nodeID types.NodeID, proof []byte) error + PublishATXProof(ctx context.Context, nodeID types.NodeID, proof []byte, allowNoRefATXs bool) error Regossip(ctx context.Context, nodeID types.NodeID) error } diff --git a/activation/malfeasance2.go b/activation/malfeasance2.go index d76beacbcdc..6390f63768d 100644 --- a/activation/malfeasance2.go +++ b/activation/malfeasance2.go @@ -12,10 +12,13 @@ import ( "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" ) type MalfeasanceHandlerV2 struct { logger *zap.Logger + db sql.StateDatabase malPublisher malfeasancePublisher edVerifier *signing.EdVerifier @@ -27,12 +30,14 @@ type MalfeasanceHandlerV2 struct { func NewMalfeasanceHandlerV2( logger *zap.Logger, + db sql.StateDatabase, malPublisher malfeasancePublisher, edVerifier *signing.EdVerifier, validator nipostValidatorV2, ) *MalfeasanceHandlerV2 { return &MalfeasanceHandlerV2{ logger: logger, + db: db, malPublisher: malPublisher, edVerifier: edVerifier, validator: validator, @@ -81,7 +86,7 @@ func (p *MalfeasanceHandlerV2) Publish(ctx context.Context, nodeID types.NodeID, Proof: codec.MustEncode(proof), } p.logger.Debug("publishing ATX malfeasance proof", log.ZShortStringer("node_id", nodeID)) - return p.malPublisher.PublishATXProof(ctx, nodeID, codec.MustEncode(atxProof)) + return p.malPublisher.PublishATXProof(ctx, nodeID, codec.MustEncode(atxProof), proof.AllowNoRefATXs()) } func (p *MalfeasanceHandlerV2) Regossip(ctx context.Context, nodeID types.NodeID) error { @@ -159,3 +164,7 @@ func (mh *MalfeasanceHandlerV2) PostIndex( func (mh *MalfeasanceHandlerV2) Signature(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { return mh.edVerifier.Verify(d, nodeID, m, sig) } + +func (mh *MalfeasanceHandlerV2) IdentityExists(nodeID types.NodeID) (bool, error) { + return atxs.IdentityExists(mh.db, nodeID) +} diff --git a/activation/malfeasance2_test.go b/activation/malfeasance2_test.go index 3e84aa5ac8d..97eb0081417 100644 --- a/activation/malfeasance2_test.go +++ b/activation/malfeasance2_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/signing" + "github.com/spacemeshos/go-spacemesh/sql/atxs" "github.com/spacemeshos/go-spacemesh/sql/statesql" ) @@ -41,11 +42,13 @@ func newTestMalHandler(tb testing.TB) *testMalHandler { ))) ctrl := gomock.NewController(tb) + db := statesql.InMemoryTest(tb) mPublish := NewMockmalfeasancePublisher(ctrl) mValidator := NewMocknipostValidator(ctrl) handler := NewMalfeasanceHandlerV2( logger, + db, mPublish, edVerifier, mValidator, @@ -237,8 +240,9 @@ func TestPublish(t *testing.T) { nodeID := types.RandomNodeID() proof := wire.NewMockProof(th.ctrl) + proof.EXPECT().AllowNoRefATXs().Return(false) proof.EXPECT().Valid(context.Background(), th.MalfeasanceHandlerV2).Return(nodeID, nil) - proof.EXPECT().Type().Return(wire.DoubleMarry) + proof.EXPECT().Type().Return(wire.DoubleMarry).AnyTimes() proof.EXPECT().EncodeScale(gomock.Any()) atxProof := &wire.ATXProof{ @@ -247,7 +251,32 @@ func TestPublish(t *testing.T) { Proof: []byte{}, } - th.mPublish.EXPECT().PublishATXProof(context.Background(), nodeID, codec.MustEncode(atxProof)).Return(nil) + th.mPublish.EXPECT().PublishATXProof(context.Background(), nodeID, codec.MustEncode(atxProof), false) + + err := th.Publish(context.Background(), nodeID, proof) + require.NoError(t, err) + }) + + t.Run("valid invalid post proof", func(t *testing.T) { + t.Parallel() + + th := newTestMalHandler(t) + + nodeID := types.RandomNodeID() + proof := wire.NewMockProof(th.ctrl) + + proof.EXPECT().AllowNoRefATXs().Return(true) + proof.EXPECT().Valid(context.Background(), th.MalfeasanceHandlerV2).Return(nodeID, nil) + proof.EXPECT().Type().Return(wire.InvalidPost).AnyTimes() + proof.EXPECT().EncodeScale(gomock.Any()) + + atxProof := &wire.ATXProof{ + Version: 0x01, // for now we only have one version + ProofType: wire.InvalidPost, + + Proof: []byte{}, + } + th.mPublish.EXPECT().PublishATXProof(context.Background(), nodeID, codec.MustEncode(atxProof), true) err := th.Publish(context.Background(), nodeID, proof) require.NoError(t, err) @@ -482,3 +511,26 @@ func TestValidate(t *testing.T) { require.Equal(t, types.EmptyNodeID, id) }) } + +func TestIdentityExists(t *testing.T) { + t.Parallel() + + th := newTestMalHandler(t) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + yes, err := th.IdentityExists(sig.NodeID()) + require.NoError(t, err) + require.False(t, yes) + + atx := &types.ActivationTx{ + SmesherID: sig.NodeID(), + } + atx.SetID(types.RandomATXID()) + require.NoError(t, atxs.Add(th.db, atx, types.AtxBlob{})) + + yes, err = th.IdentityExists(sig.NodeID()) + require.NoError(t, err) + require.True(t, yes) +} diff --git a/activation/mocks.go b/activation/mocks.go index 9e46e4224be..157ce3cf4a9 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -1280,17 +1280,17 @@ func (m *MockmalfeasancePublisher) EXPECT() *MockmalfeasancePublisherMockRecorde } // PublishATXProof mocks base method. -func (m *MockmalfeasancePublisher) PublishATXProof(ctx context.Context, nodeID types.NodeID, proof []byte) error { +func (m *MockmalfeasancePublisher) PublishATXProof(ctx context.Context, nodeID types.NodeID, proof []byte, allowNoRefATXs bool) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PublishATXProof", ctx, nodeID, proof) + ret := m.ctrl.Call(m, "PublishATXProof", ctx, nodeID, proof, allowNoRefATXs) ret0, _ := ret[0].(error) return ret0 } // PublishATXProof indicates an expected call of PublishATXProof. -func (mr *MockmalfeasancePublisherMockRecorder) PublishATXProof(ctx, nodeID, proof any) *MockmalfeasancePublisherPublishATXProofCall { +func (mr *MockmalfeasancePublisherMockRecorder) PublishATXProof(ctx, nodeID, proof, allowNoRefATXs any) *MockmalfeasancePublisherPublishATXProofCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishATXProof", reflect.TypeOf((*MockmalfeasancePublisher)(nil).PublishATXProof), ctx, nodeID, proof) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishATXProof", reflect.TypeOf((*MockmalfeasancePublisher)(nil).PublishATXProof), ctx, nodeID, proof, allowNoRefATXs) return &MockmalfeasancePublisherPublishATXProofCall{Call: call} } @@ -1306,13 +1306,13 @@ func (c *MockmalfeasancePublisherPublishATXProofCall) Return(arg0 error) *Mockma } // Do rewrite *gomock.Call.Do -func (c *MockmalfeasancePublisherPublishATXProofCall) Do(f func(context.Context, types.NodeID, []byte) error) *MockmalfeasancePublisherPublishATXProofCall { +func (c *MockmalfeasancePublisherPublishATXProofCall) Do(f func(context.Context, types.NodeID, []byte, bool) error) *MockmalfeasancePublisherPublishATXProofCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockmalfeasancePublisherPublishATXProofCall) DoAndReturn(f func(context.Context, types.NodeID, []byte) error) *MockmalfeasancePublisherPublishATXProofCall { +func (c *MockmalfeasancePublisherPublishATXProofCall) DoAndReturn(f func(context.Context, types.NodeID, []byte, bool) error) *MockmalfeasancePublisherPublishATXProofCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/activation/wire/interface.go b/activation/wire/interface.go index 46db9f6f69c..c275516b4e9 100644 --- a/activation/wire/interface.go +++ b/activation/wire/interface.go @@ -2,6 +2,7 @@ package wire import ( "context" + "errors" "github.com/spacemeshos/go-scale" @@ -26,14 +27,20 @@ type MalfeasanceValidator interface { // Signature validates the given signature against the given message and public key. Signature(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool + + // IdentityExists returns true if the given identity has published a valid ATX before. + IdentityExists(nodeID types.NodeID) (bool, error) } +var ErrUnknownIdentity = errors.New("unknown identity") + // Proof is an interface for all types of proofs that can be provided in an ATXProof. // Generally the proof should be able to validate itself and be scale encoded. type Proof interface { scale.Encodable scale.Decodable + AllowNoRefATXs() bool Type() ProofType TypeName() string Info() map[string]string diff --git a/activation/wire/malfeasance_double_marry.go b/activation/wire/malfeasance_double_marry.go index c81cb8b5c57..0f53c6e74ba 100644 --- a/activation/wire/malfeasance_double_marry.go +++ b/activation/wire/malfeasance_double_marry.go @@ -42,6 +42,10 @@ type ProofDoubleMarry struct { Proof2 MarryProof } +func (p ProofDoubleMarry) AllowNoRefATXs() bool { + return false +} + func (p ProofDoubleMarry) TypeName() string { return "DoubleMarryProof" } @@ -107,5 +111,10 @@ func (p ProofDoubleMarry) Valid(_ context.Context, malValidator MalfeasanceValid if err := p.Proof2.Valid(malValidator, p.ATXID2, p.SmesherID2, p.NodeID); err != nil { return types.EmptyNodeID, fmt.Errorf("proof 2 is invalid: %w", err) } + if ok, err := malValidator.IdentityExists(p.NodeID); err != nil { + return types.EmptyNodeID, fmt.Errorf("checking identity: %w", err) + } else if !ok { + return types.EmptyNodeID, ErrUnknownIdentity + } return p.NodeID, nil } diff --git a/activation/wire/malfeasance_double_marry_test.go b/activation/wire/malfeasance_double_marry_test.go index 66f030da3aa..550d85e516c 100644 --- a/activation/wire/malfeasance_double_marry_test.go +++ b/activation/wire/malfeasance_double_marry_test.go @@ -58,12 +58,53 @@ func Test_DoubleMarryProof(t *testing.T) { DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { return edVerifier.Verify(d, nodeID, m, sig) }).AnyTimes() + verifier.EXPECT().IdentityExists(otherSig.NodeID()).Return(true, nil).AnyTimes() id, err := proof.Valid(context.Background(), verifier) require.NoError(t, err) require.Equal(t, otherSig.NodeID(), id) }) + t.Run("identity unknown", func(t *testing.T) { + t.Parallel() + db := statesql.InMemoryTest(t) + + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) + + atx1 := NewTestActivationTxV2( + t, + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := NewTestActivationTxV2( + t, + WithMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + WithMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.NoError(t, err) + require.NotNil(t, proof) + + ctrl := gomock.NewController(t) + verifier := NewMockMalfeasanceValidator(ctrl) + verifier.EXPECT().Signature(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { + return edVerifier.Verify(d, nodeID, m, sig) + }).AnyTimes() + verifier.EXPECT().IdentityExists(otherSig.NodeID()).Return(false, nil).AnyTimes() + + id, err := proof.Valid(context.Background(), verifier) + require.ErrorIs(t, err, ErrUnknownIdentity) + require.Equal(t, types.EmptyNodeID, id) + }) + t.Run("identity is not included in both ATXs", func(t *testing.T) { t.Parallel() db := statesql.InMemoryTest(t) diff --git a/activation/wire/malfeasance_double_merge.go b/activation/wire/malfeasance_double_merge.go index 3047623c941..53a59de8007 100644 --- a/activation/wire/malfeasance_double_merge.go +++ b/activation/wire/malfeasance_double_merge.go @@ -59,6 +59,10 @@ type ProofDoubleMerge struct { SmesherID2MarryProof MarryProof } +func (p ProofDoubleMerge) AllowNoRefATXs() bool { + return false +} + func (p ProofDoubleMerge) TypeName() string { return "DoubleMergeProof" } @@ -145,17 +149,17 @@ func NewDoubleMergeProof(db sql.Executor, atx1, atx2 *ActivationTxV2) (*ProofDou return &proof, nil } -func (p *ProofDoubleMerge) Valid(_ context.Context, edVerifier MalfeasanceValidator) (types.NodeID, error) { +func (p *ProofDoubleMerge) Valid(_ context.Context, malValidator MalfeasanceValidator) (types.NodeID, error) { // 1. The ATXs have different IDs. if p.ATXID1 == p.ATXID2 { return types.EmptyNodeID, errors.New("ATXs have the same ID") } // 2. Both ATXs have a valid signature. - if !edVerifier.Signature(signing.ATX, p.SmesherID1, p.ATXID1.Bytes(), p.Signature1) { + if !malValidator.Signature(signing.ATX, p.SmesherID1, p.ATXID1.Bytes(), p.Signature1) { return types.EmptyNodeID, errors.New("ATX 1 invalid signature") } - if !edVerifier.Signature(signing.ATX, p.SmesherID2, p.ATXID2.Bytes(), p.Signature2) { + if !malValidator.Signature(signing.ATX, p.SmesherID2, p.ATXID2.Bytes(), p.Signature2) { return types.EmptyNodeID, errors.New("ATX 2 invalid signature") } @@ -171,17 +175,30 @@ func (p *ProofDoubleMerge) Valid(_ context.Context, edVerifier MalfeasanceValida if !p.MarriageATXProof1.Valid(p.ATXID1, p.MarriageATX) { return types.EmptyNodeID, errors.New("ATX 1 invalid marriage ATX proof") } - err := p.SmesherID1MarryProof.Valid(edVerifier, p.MarriageATX, p.MarriageATXSmesherID, p.SmesherID1) + err := p.SmesherID1MarryProof.Valid(malValidator, p.MarriageATX, p.MarriageATXSmesherID, p.SmesherID1) if err != nil { return types.EmptyNodeID, errors.New("ATX 1 invalid marriage ATX proof") } if !p.MarriageATXProof2.Valid(p.ATXID2, p.MarriageATX) { return types.EmptyNodeID, errors.New("ATX 2 invalid marriage ATX proof") } - err = p.SmesherID2MarryProof.Valid(edVerifier, p.MarriageATX, p.MarriageATXSmesherID, p.SmesherID2) + err = p.SmesherID2MarryProof.Valid(malValidator, p.MarriageATX, p.MarriageATXSmesherID, p.SmesherID2) if err != nil { return types.EmptyNodeID, errors.New("ATX 2 invalid marriage ATX proof") } + // 6. smeshers have published valid ATXs before + if ok, err := malValidator.IdentityExists(p.SmesherID1); err != nil { + return types.EmptyNodeID, fmt.Errorf("checking identity: %w", err) + } else if !ok { + return types.EmptyNodeID, ErrUnknownIdentity + } + + if ok, err := malValidator.IdentityExists(p.SmesherID2); err != nil { + return types.EmptyNodeID, fmt.Errorf("checking identity: %w", err) + } else if !ok { + return types.EmptyNodeID, ErrUnknownIdentity + } + return p.SmesherID1, nil } diff --git a/activation/wire/malfeasance_double_merge_test.go b/activation/wire/malfeasance_double_merge_test.go index bf0370f79ca..c4d546cfe36 100644 --- a/activation/wire/malfeasance_double_merge_test.go +++ b/activation/wire/malfeasance_double_merge_test.go @@ -76,6 +76,8 @@ func Test_DoubleMergeProof(t *testing.T) { DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { return edVerifier.Verify(d, nodeID, m, sig) }).AnyTimes() + verifier.EXPECT().IdentityExists(sig.NodeID()).Return(true, nil).AnyTimes() + verifier.EXPECT().IdentityExists(otherSig.NodeID()).Return(true, nil).AnyTimes() marriageAtx := setupMarriage(db) @@ -100,6 +102,78 @@ func Test_DoubleMergeProof(t *testing.T) { require.Equal(t, sig.NodeID(), id) }) + t.Run("identity1 unknown", func(t *testing.T) { + t.Parallel() + db := statesql.InMemoryTest(t) + + ctrl := gomock.NewController(t) + verifier := NewMockMalfeasanceValidator(ctrl) + verifier.EXPECT().Signature(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { + return edVerifier.Verify(d, nodeID, m, sig) + }).AnyTimes() + verifier.EXPECT().IdentityExists(sig.NodeID()).Return(false, nil).AnyTimes() + verifier.EXPECT().IdentityExists(otherSig.NodeID()).Return(true, nil).AnyTimes() + + marriageAtx := setupMarriage(db) + + atx1 := NewTestActivationTxV2( + t, + WithMarriageATX(marriageAtx.ID()), + WithPublishEpoch(marriageAtx.PublishEpoch+1), + ) + atx1.Sign(sig) + + atx2 := NewTestActivationTxV2( + t, + WithMarriageATX(marriageAtx.ID()), + WithPublishEpoch(marriageAtx.PublishEpoch+1), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMergeProof(db, atx1, atx2) + require.NoError(t, err) + id, err := proof.Valid(context.Background(), verifier) + require.ErrorIs(t, err, ErrUnknownIdentity) + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("identity2 unknown", func(t *testing.T) { + t.Parallel() + db := statesql.InMemoryTest(t) + + ctrl := gomock.NewController(t) + verifier := NewMockMalfeasanceValidator(ctrl) + verifier.EXPECT().Signature(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { + return edVerifier.Verify(d, nodeID, m, sig) + }).AnyTimes() + verifier.EXPECT().IdentityExists(sig.NodeID()).Return(true, nil).AnyTimes() + verifier.EXPECT().IdentityExists(otherSig.NodeID()).Return(false, nil).AnyTimes() + + marriageAtx := setupMarriage(db) + + atx1 := NewTestActivationTxV2( + t, + WithMarriageATX(marriageAtx.ID()), + WithPublishEpoch(marriageAtx.PublishEpoch+1), + ) + atx1.Sign(sig) + + atx2 := NewTestActivationTxV2( + t, + WithMarriageATX(marriageAtx.ID()), + WithPublishEpoch(marriageAtx.PublishEpoch+1), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMergeProof(db, atx1, atx2) + require.NoError(t, err) + id, err := proof.Valid(context.Background(), verifier) + require.ErrorIs(t, err, ErrUnknownIdentity) + require.Equal(t, types.EmptyNodeID, id) + }) + t.Run("same ATX ID", func(t *testing.T) { t.Parallel() db := statesql.InMemoryTest(t) diff --git a/activation/wire/malfeasance_invalid_post.go b/activation/wire/malfeasance_invalid_post.go index e674d7f6828..cb062e044ad 100644 --- a/activation/wire/malfeasance_invalid_post.go +++ b/activation/wire/malfeasance_invalid_post.go @@ -39,6 +39,10 @@ type ProofInvalidPost struct { InvalidPostProof InvalidPostProof } +func (p ProofInvalidPost) AllowNoRefATXs() bool { + return true +} + func (p ProofInvalidPost) TypeName() string { return "InvalidPoSTProof" } diff --git a/activation/wire/malfeasance_invalid_prev_atx.go b/activation/wire/malfeasance_invalid_prev_atx.go index b1a93768327..ea9c32c3e99 100644 --- a/activation/wire/malfeasance_invalid_prev_atx.go +++ b/activation/wire/malfeasance_invalid_prev_atx.go @@ -32,6 +32,10 @@ type ProofInvalidPrevAtxV2 struct { Proofs [2]InvalidPrevAtxProof } +func (p ProofInvalidPrevAtxV2) AllowNoRefATXs() bool { + return false +} + func (p ProofInvalidPrevAtxV2) TypeName() string { return "InvalidPreviousATXProofV2" } @@ -186,6 +190,11 @@ func (p ProofInvalidPrevAtxV2) Valid(_ context.Context, malValidator Malfeasance if err := p.Proofs[1].Valid(p.PrevATXID, p.NodeID, malValidator); err != nil { return types.EmptyNodeID, fmt.Errorf("proof 2 is invalid: %w", err) } + if ok, err := malValidator.IdentityExists(p.NodeID); err != nil { + return types.EmptyNodeID, fmt.Errorf("checking identity: %w", err) + } else if !ok { + return types.EmptyNodeID, ErrUnknownIdentity + } return p.NodeID, nil } @@ -209,6 +218,10 @@ type ProofInvalidPrevAtxV1 struct { ATXv1 ActivationTxV1 } +func (p ProofInvalidPrevAtxV1) AllowNoRefATXs() bool { + return false +} + func (p ProofInvalidPrevAtxV1) TypeName() string { return "InvalidPreviousATXProofV1" } diff --git a/activation/wire/malfeasance_invalid_prev_atx_test.go b/activation/wire/malfeasance_invalid_prev_atx_test.go index 0a4356b2d4f..eedb3345b5a 100644 --- a/activation/wire/malfeasance_invalid_prev_atx_test.go +++ b/activation/wire/malfeasance_invalid_prev_atx_test.go @@ -125,6 +125,7 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { return edVerifier.Verify(d, nodeID, m, sig) }).AnyTimes() + verifier.EXPECT().IdentityExists(sig.NodeID()).Return(true, nil).AnyTimes() // verify the proof id, err := proof.Valid(context.Background(), verifier) @@ -132,6 +133,41 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { require.Equal(t, sig.NodeID(), id) }) + t.Run("identity unknown", func(t *testing.T) { + t.Parallel() + db := statesql.InMemoryTest(t) + + prevATXID := types.RandomATXID() + atx1 := NewTestActivationTxV2( + t, + WithPreviousATXs(prevATXID), + WithPublishEpoch(5), + ) + atx1.Sign(sig) + atx2 := NewTestActivationTxV2( + t, + WithPreviousATXs(prevATXID), + WithPublishEpoch(7), + ) + atx2.Sign(sig) + + proof, err := NewInvalidPrevAtxProofV2(db, atx1, atx2, sig.NodeID()) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + verifier := NewMockMalfeasanceValidator(ctrl) + verifier.EXPECT().Signature(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { + return edVerifier.Verify(d, nodeID, m, sig) + }).AnyTimes() + verifier.EXPECT().IdentityExists(sig.NodeID()).Return(false, nil).AnyTimes() + + // verify the proof + id, err := proof.Valid(context.Background(), verifier) + require.ErrorIs(t, err, ErrUnknownIdentity) + require.Equal(t, types.EmptyNodeID, id) + }) + t.Run("valid merged & solo atx", func(t *testing.T) { t.Parallel() db := statesql.InMemoryTest(t) @@ -158,6 +194,7 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { return edVerifier.Verify(d, nodeID, m, sig) }).AnyTimes() + verifier.EXPECT().IdentityExists(sig.NodeID()).Return(true, nil).AnyTimes() // verify the proof id, err := proof.Valid(context.Background(), verifier) diff --git a/activation/wire/mocks.go b/activation/wire/mocks.go index be5ddb89501..0994ebd820b 100644 --- a/activation/wire/mocks.go +++ b/activation/wire/mocks.go @@ -43,6 +43,45 @@ func (m *MockMalfeasanceValidator) EXPECT() *MockMalfeasanceValidatorMockRecorde return m.recorder } +// IdentityExists mocks base method. +func (m *MockMalfeasanceValidator) IdentityExists(nodeID types.NodeID) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IdentityExists", nodeID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IdentityExists indicates an expected call of IdentityExists. +func (mr *MockMalfeasanceValidatorMockRecorder) IdentityExists(nodeID any) *MockMalfeasanceValidatorIdentityExistsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IdentityExists", reflect.TypeOf((*MockMalfeasanceValidator)(nil).IdentityExists), nodeID) + return &MockMalfeasanceValidatorIdentityExistsCall{Call: call} +} + +// MockMalfeasanceValidatorIdentityExistsCall wrap *gomock.Call +type MockMalfeasanceValidatorIdentityExistsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockMalfeasanceValidatorIdentityExistsCall) Return(arg0 bool, arg1 error) *MockMalfeasanceValidatorIdentityExistsCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockMalfeasanceValidatorIdentityExistsCall) Do(f func(types.NodeID) (bool, error)) *MockMalfeasanceValidatorIdentityExistsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockMalfeasanceValidatorIdentityExistsCall) DoAndReturn(f func(types.NodeID) (bool, error)) *MockMalfeasanceValidatorIdentityExistsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // PostIndex mocks base method. func (m *MockMalfeasanceValidator) PostIndex(ctx context.Context, smesherID types.NodeID, commitment types.ATXID, post *types.Post, challenge []byte, numUnits uint32, idx int) error { m.ctrl.T.Helper() @@ -143,6 +182,44 @@ func (m *MockProof) EXPECT() *MockProofMockRecorder { return m.recorder } +// AllowNoRefATXs mocks base method. +func (m *MockProof) AllowNoRefATXs() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AllowNoRefATXs") + ret0, _ := ret[0].(bool) + return ret0 +} + +// AllowNoRefATXs indicates an expected call of AllowNoRefATXs. +func (mr *MockProofMockRecorder) AllowNoRefATXs() *MockProofAllowNoRefATXsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllowNoRefATXs", reflect.TypeOf((*MockProof)(nil).AllowNoRefATXs)) + return &MockProofAllowNoRefATXsCall{Call: call} +} + +// MockProofAllowNoRefATXsCall wrap *gomock.Call +type MockProofAllowNoRefATXsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockProofAllowNoRefATXsCall) Return(arg0 bool) *MockProofAllowNoRefATXsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockProofAllowNoRefATXsCall) Do(f func() bool) *MockProofAllowNoRefATXsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockProofAllowNoRefATXsCall) DoAndReturn(f func() bool) *MockProofAllowNoRefATXsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // DecodeScale mocks base method. func (m *MockProof) DecodeScale(dec *scale.Decoder) (int, error) { m.ctrl.T.Helper() diff --git a/malfeasance2/handler.go b/malfeasance2/handler.go index dc912224e51..e9af69d1615 100644 --- a/malfeasance2/handler.go +++ b/malfeasance2/handler.go @@ -21,7 +21,6 @@ import ( "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/sql" - "github.com/spacemeshos/go-spacemesh/sql/atxs" "github.com/spacemeshos/go-spacemesh/sql/malfeasance" "github.com/spacemeshos/go-spacemesh/sql/marriage" "github.com/spacemeshos/go-spacemesh/system" @@ -262,24 +261,20 @@ func (h *Handler) handleProof(ctx context.Context, peer p2p.Peer, proof Malfeasa return nil, fmt.Errorf("%w: %d", ErrUnknownDomain, proof.Domain) } + if err := h.fetchReferences(ctx, peer, proof.RefATXs); err != nil { + return nil, fmt.Errorf("fetch references: %w", err) + } + nodeID, err := handler.Validate(ctx, proof.Proof) if err != nil { h.countInvalidProof(proof) return nil, err } - if err := h.fetchReferences(ctx, peer, proof.RefATXs); err != nil { - return nil, fmt.Errorf("fetch references: %w", err) - } - mID, err := marriage.FindIDByNodeID(h.db, nodeID) switch { case errors.Is(err, sql.ErrNotFound): - // smesher is not married, check if identity exists in the DB - _, err := atxs.GetFirstIDByNodeID(h.db, nodeID) - if err != nil { - return nil, fmt.Errorf("%w: missing proof for identities existence", ErrMalformedData) - } + // smesher is not married return []types.NodeID{nodeID}, nil case err != nil: return nil, fmt.Errorf("get marriage ID for %s: %w", nodeID.ShortString(), err) diff --git a/malfeasance2/handler_test.go b/malfeasance2/handler_test.go index 81bf411fd21..f2d065cb2e5 100644 --- a/malfeasance2/handler_test.go +++ b/malfeasance2/handler_test.go @@ -308,7 +308,6 @@ spacemesh_malfeasance2_num_proofs{domain="ATX",type="invalidPost"} 1 nodeID := types.RandomNodeID() atxID := types.RandomATXID() mockHandler := malfeasance2.NewMockMalfeasanceHandler(th.ctrl) - mockHandler.EXPECT().Validate(gomock.Any(), validProof).Return(nodeID, nil) th.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) th.mockFetch.EXPECT().RegisterPeerHashes(p2p.Peer("peer"), []types.Hash32{atxID.Hash32()}) errFetchFailed := errors.New("fetch failed") @@ -370,31 +369,6 @@ spacemesh_malfeasance2_num_proofs{domain="ATX",type="invalidPost"} 1 require.True(t, malicious) }) - t.Run("valid proof, no reference ATX and identity is unknown", func(t *testing.T) { - t.Parallel() - th := newTestHandler(t) - validProof := []byte("valid") - nodeID := types.RandomNodeID() - mockHandler := malfeasance2.NewMockMalfeasanceHandler(th.ctrl) - mockHandler.EXPECT().Validate(gomock.Any(), validProof).Return(nodeID, nil) - th.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) - - proof := &malfeasance2.MalfeasanceProof{ - Version: 0, - // no reference ATX - Domain: malfeasance2.InvalidActivation, - Proof: validProof, - } - - err := th.HandleSynced(context.Background(), types.Hash32(nodeID), "peer", codec.MustEncode(proof)) - require.ErrorIs(t, err, pubsub.ErrValidationReject) - - // not marked malicious since no proof of existence - malicious, err := malfeasance.IsMalicious(th.db, nodeID) - require.NoError(t, err) - require.False(t, malicious) - }) - t.Run("valid proof, wrong hash", func(t *testing.T) { t.Parallel() th := newTestHandler(t) @@ -642,7 +616,6 @@ spacemesh_malfeasance2_num_proofs{domain="ATX",type="invalidPost"} 1 nodeID := types.RandomNodeID() atxID := types.RandomATXID() mockHandler := malfeasance2.NewMockMalfeasanceHandler(th.ctrl) - mockHandler.EXPECT().Validate(gomock.Any(), validProof).Return(nodeID, nil) th.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) th.mockFetch.EXPECT().RegisterPeerHashes(p2p.Peer("peer"), []types.Hash32{atxID.Hash32()}) errFetchFailed := errors.New("fetch failed") @@ -704,31 +677,6 @@ spacemesh_malfeasance2_num_proofs{domain="ATX",type="invalidPost"} 1 require.True(t, malicious) }) - t.Run("valid proof, no reference ATX and identity is unknown", func(t *testing.T) { - t.Parallel() - th := newTestHandler(t) - validProof := []byte("valid") - nodeID := types.RandomNodeID() - mockHandler := malfeasance2.NewMockMalfeasanceHandler(th.ctrl) - mockHandler.EXPECT().Validate(gomock.Any(), validProof).Return(nodeID, nil) - th.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) - - proof := &malfeasance2.MalfeasanceProof{ - Version: 0, - // no reference ATX - Domain: malfeasance2.InvalidActivation, - Proof: validProof, - } - - err := th.HandleGossip(context.Background(), "peer", codec.MustEncode(proof)) - require.ErrorIs(t, err, pubsub.ErrValidationReject) - - // not marked malicious since no proof of existence - malicious, err := malfeasance.IsMalicious(th.db, nodeID) - require.NoError(t, err) - require.False(t, malicious) - }) - t.Run("valid proof for known malicious identity", func(t *testing.T) { t.Parallel() th := newTestHandler(t) diff --git a/malfeasance2/publisher.go b/malfeasance2/publisher.go index 4a6fecee10b..a3be2609680 100644 --- a/malfeasance2/publisher.go +++ b/malfeasance2/publisher.go @@ -44,7 +44,7 @@ func NewPublisher( } } -func (p *Publisher) PublishATXProof(ctx context.Context, nodeID types.NodeID, proof []byte) error { +func (p *Publisher) PublishATXProof(ctx context.Context, nodeID types.NodeID, proof []byte, allowNoRefATXs bool) error { publish := false // whether to publish the proof var set []types.NodeID var refATXs []types.ATXID @@ -64,12 +64,16 @@ func (p *Publisher) PublishATXProof(ctx context.Context, nodeID types.NodeID, pr return fmt.Errorf("setting malfeasance proof: %w", err) } atxID, err := atxs.GetFirstIDByNodeID(tx, nodeID) - if err != nil { + switch { + case errors.Is(err, sql.ErrNotFound) && allowNoRefATXs: + // no ATXs found for this node, but we allow it + case err != nil: return fmt.Errorf("getting atx id: %w", err) + default: // ATX found + refATXs = []types.ATXID{atxID} } publish = true set = []types.NodeID{nodeID} - refATXs = []types.ATXID{atxID} return nil case err != nil: return fmt.Errorf("getting equivocation set: %w", err) @@ -212,7 +216,7 @@ func (p *Publisher) publish( p.logger.Error("failed to broadcast malfeasance proof", zap.Error(err)) return fmt.Errorf("broadcast atx malfeasance proof: %w", err) } - p.logger.Debug("broadcasted malfeasance proof", + p.logger.Debug("broadcast malfeasance proof", zap.Array("smesher_ids", zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { for _, nodeID := range nodeID { enc.AppendString(nodeID.ShortString()) diff --git a/malfeasance2/publisher_test.go b/malfeasance2/publisher_test.go index 770b011be32..5fb92109517 100644 --- a/malfeasance2/publisher_test.go +++ b/malfeasance2/publisher_test.go @@ -95,7 +95,33 @@ func TestPublishATXProof(t *testing.T) { tp.mockSync.EXPECT().ListenToATXGossip().Return(true) tp.mockPub.EXPECT().Publish(gomock.Any(), pubsub.MalfeasanceProof2, codec.MustEncode(malfeasanceProof)) - err := tp.PublishATXProof(context.Background(), nodeID, proof) + err := tp.PublishATXProof(context.Background(), nodeID, proof, false) + require.NoError(t, err) + + dbProof, domain, err := malfeasance.NodeIDProof(tp.db, nodeID) + require.NoError(t, err) + require.Equal(t, malfeasance2.InvalidActivation, malfeasance2.ProofDomain(domain)) + require.Equal(t, proof, dbProof) + }) + + t.Run("not married and in sync, allow without refATXs", func(t *testing.T) { + t.Parallel() + tp := newTestPublisher(t) + proof := types.RandomBytes(10) + nodeID := types.RandomNodeID() + + malfeasanceProof := &malfeasance2.MalfeasanceProof{ + Version: 0, + RefATXs: []types.ATXID{}, + Domain: malfeasance2.InvalidActivation, + Proof: proof, + } + + tp.mockTrt.EXPECT().OnMalfeasance(nodeID) + tp.mockSync.EXPECT().ListenToATXGossip().Return(true) + tp.mockPub.EXPECT().Publish(gomock.Any(), pubsub.MalfeasanceProof2, codec.MustEncode(malfeasanceProof)) + + err := tp.PublishATXProof(context.Background(), nodeID, proof, true) require.NoError(t, err) dbProof, domain, err := malfeasance.NodeIDProof(tp.db, nodeID) @@ -128,7 +154,7 @@ func TestPublishATXProof(t *testing.T) { tp.mockPub.EXPECT().Publish(gomock.Any(), pubsub.MalfeasanceProof2, codec.MustEncode(malfeasanceProof)). Return(errPublish) - err := tp.PublishATXProof(context.Background(), nodeID, proof) + err := tp.PublishATXProof(context.Background(), nodeID, proof, false) require.ErrorIs(t, err, errPublish) logs := tp.observedLogs.FilterLevelExact(zap.ErrorLevel) @@ -158,7 +184,25 @@ func TestPublishATXProof(t *testing.T) { tp.mockTrt.EXPECT().OnMalfeasance(nodeID) tp.mockSync.EXPECT().ListenToATXGossip().Return(false) // results in no gossip but only storing the proof - err := tp.PublishATXProof(context.Background(), nodeID, proof) + err := tp.PublishATXProof(context.Background(), nodeID, proof, false) + require.NoError(t, err) + + dbProof, domain, err := malfeasance.NodeIDProof(tp.db, nodeID) + require.NoError(t, err) + require.Equal(t, malfeasance2.InvalidActivation, malfeasance2.ProofDomain(domain)) + require.Equal(t, proof, dbProof) + }) + + t.Run("not married, not in sync, allow no ref ATXs", func(t *testing.T) { + t.Parallel() + tp := newTestPublisher(t) + proof := types.RandomBytes(10) + nodeID := types.RandomNodeID() + + tp.mockTrt.EXPECT().OnMalfeasance(nodeID) + tp.mockSync.EXPECT().ListenToATXGossip().Return(false) // results in no gossip but only storing the proof + + err := tp.PublishATXProof(context.Background(), nodeID, proof, true) require.NoError(t, err) dbProof, domain, err := malfeasance.NodeIDProof(tp.db, nodeID) @@ -240,7 +284,7 @@ func TestPublishATXProof(t *testing.T) { }, ) - err = tp.PublishATXProof(context.Background(), nodeIDs[2], proof) + err = tp.PublishATXProof(context.Background(), nodeIDs[2], proof, false) require.NoError(t, err) for i := range nodeIDs { @@ -268,7 +312,7 @@ func TestPublishATXProof(t *testing.T) { err := malfeasance.AddProof(tp.db, nodeID, nil, proof, int(malfeasance2.InvalidActivation), time.Now()) require.NoError(t, err) - err = tp.PublishATXProof(context.Background(), nodeID, proof) + err = tp.PublishATXProof(context.Background(), nodeID, proof, false) require.NoError(t, err) dbProof, domain, err := malfeasance.NodeIDProof(tp.db, nodeID) @@ -278,9 +322,11 @@ func TestPublishATXProof(t *testing.T) { logs := tp.observedLogs.FilterLevelExact(zap.DebugLevel) - require.Equal(t, 1, logs.Len()) + require.Equal(t, 2, logs.Len()) require.Equal(t, zap.DebugLevel, logs.All()[0].Level) require.Contains(t, logs.All()[0].Message, "smesher is already marked as malicious") + require.Equal(t, zap.DebugLevel, logs.All()[1].Level) + require.Contains(t, logs.All()[1].Message, "persisted malfeasance proof") }) t.Run("married and all already malicious", func(t *testing.T) { @@ -343,7 +389,7 @@ func TestPublishATXProof(t *testing.T) { require.NoError(t, malfeasance.SetMalicious(tp.db, nodeID, mID, time.Now())) } - err = tp.PublishATXProof(context.Background(), nodeIDs[2], proof) + err = tp.PublishATXProof(context.Background(), nodeIDs[2], proof, false) require.NoError(t, err) for i := range nodeIDs { @@ -358,11 +404,13 @@ func TestPublishATXProof(t *testing.T) { logs := tp.observedLogs.FilterLevelExact(zap.DebugLevel) - require.Equal(t, 30, logs.Len()) + require.Equal(t, 31, logs.Len()) require.Equal(t, zap.DebugLevel, logs.All()[0].Level) for i := range nodeIDs { require.Contains(t, logs.All()[i].Message, "smesher is already marked as malicious") } + require.Equal(t, zap.DebugLevel, logs.All()[30].Level) + require.Contains(t, logs.All()[30].Message, "persisted malfeasance proof") }) t.Run("married and some already malicious", func(t *testing.T) { @@ -450,7 +498,7 @@ func TestPublishATXProof(t *testing.T) { }, ) - err = tp.PublishATXProof(context.Background(), nodeIDs[2], proof) + err = tp.PublishATXProof(context.Background(), nodeIDs[2], proof, false) require.NoError(t, err) for i := range nodeIDs { @@ -465,12 +513,16 @@ func TestPublishATXProof(t *testing.T) { logs := tp.observedLogs.FilterLevelExact(zap.DebugLevel) - require.Equal(t, 20, logs.Len()) + require.Equal(t, 22, logs.Len()) require.Equal(t, zap.DebugLevel, logs.All()[0].Level) for i := range nodeIDs[:20] { // first 20 were already malicious require.Contains(t, logs.All()[i].Message, "smesher is already marked as malicious") } + require.Equal(t, zap.DebugLevel, logs.All()[20].Level) + require.Contains(t, logs.All()[20].Message, "persisted malfeasance proof") + require.Equal(t, zap.DebugLevel, logs.All()[21].Level) + require.Contains(t, logs.All()[21].Message, "broadcast malfeasance proof") }) } diff --git a/node/node.go b/node/node.go index 361c278d65a..ff7ccda2e4d 100644 --- a/node/node.go +++ b/node/node.go @@ -859,6 +859,7 @@ func (app *App) initServices(ctx context.Context) error { ) atxMalHandler := activation.NewMalfeasanceHandlerV2( malfeasanceLogger, + app.db, malfeasancePublisher, app.edVerifier, validator, From cbe73fd29e5a5014e8cea793f934c589bd18aa52 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 30 Jan 2025 14:21:49 +0000 Subject: [PATCH 51/56] Fix failing tests --- activation/handler_v2_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index f373b4ef779..0dda1991b59 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -991,6 +991,8 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { return atxHandler.edVerifier.Verify(d, nodeID, m, sig) }).AnyTimes() + verifier.EXPECT().IdentityExists(sig.NodeID()).Return(true, nil).AnyTimes() + verifier.EXPECT().IdentityExists(signers[2].NodeID()).Return(true, nil).AnyTimes() atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100}) atxHandler.mMalPublish.EXPECT().Publish( @@ -1966,6 +1968,7 @@ func Test_Marriages(t *testing.T) { DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { return atxHandler.edVerifier.Verify(d, nodeID, m, sig) }).AnyTimes() + verifier.EXPECT().IdentityExists(sig.NodeID()).Return(true, nil).AnyTimes() atxHandler.mMalPublish.EXPECT().Publish( gomock.Any(), @@ -2281,6 +2284,7 @@ func TestContextual_PreviousATX(t *testing.T) { DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { return atxHdlr.edVerifier.Verify(d, nodeID, m, sig) }).AnyTimes() + verifier.EXPECT().IdentityExists(signers[1].NodeID()).Return(true, nil).AnyTimes() atxHdlr.mMalPublish.EXPECT().Publish( gomock.Any(), @@ -2436,6 +2440,7 @@ func TestContextual_PreviousATX(t *testing.T) { DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { return atxHdlr.edVerifier.Verify(d, nodeID, m, sig) }).AnyTimes() + verifier.EXPECT().IdentityExists(otherSig.NodeID()).Return(true, nil).AnyTimes() atxHdlr.mMalPublish.EXPECT().Publish( gomock.Any(), From f3758d7a66d99e437ac8371d72354f2a3c7edbc5 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 31 Jan 2025 08:39:48 +0000 Subject: [PATCH 52/56] Update api dependency --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 15cf4a79797..3b83b3ef57a 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/seehuhn/mt19937 v1.0.0 github.com/slok/go-http-metrics v0.13.0 - github.com/spacemeshos/api/release/go v1.60.0 + github.com/spacemeshos/api/release/go v1.61.0 github.com/spacemeshos/economics v0.1.4 github.com/spacemeshos/fixed v0.1.2 github.com/spacemeshos/go-scale v1.2.1 diff --git a/go.sum b/go.sum index a793a2c4db1..f64d4e7441f 100644 --- a/go.sum +++ b/go.sum @@ -630,8 +630,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.60.0 h1:mCwWjjYBbKW/Zyp2Fdb5mV097u84fh1HdIIhgnlIyM0= -github.com/spacemeshos/api/release/go v1.60.0/go.mod h1:QnZOhlUupLbfogjCmo9uf/PRYRNCFNFCKqgD8tRL7dM= +github.com/spacemeshos/api/release/go v1.61.0 h1:Jdni0kODbpDL/F7MkbNN1R4ioVC3lHk3TA8E04p/ENc= +github.com/spacemeshos/api/release/go v1.61.0/go.mod h1:uBm4XPydRb5emOW6TckYzXcSQDyAv4q6Z4ud7HaLzxI= github.com/spacemeshos/economics v0.1.4 h1:twlawrcQhYNqPgyDv08+24EL/OgUKz3d7q+PvJIAND0= github.com/spacemeshos/economics v0.1.4/go.mod h1:6HKWKiKdxjVQcGa2z/wA0LR4M/DzKib856bP16yqNmQ= github.com/spacemeshos/fixed v0.1.2 h1:pENQ8pXFAqin3f15ZLoOVVeSgcmcFJ0IFdFm4+9u4SM= From 8c357f862eda10b9ee2008dde84a4e081e618419 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:39:26 +0000 Subject: [PATCH 53/56] Review feedback --- activation/wire/interface.go | 13 +++++++++++++ fetch/fetch.go | 6 +++--- fetch/handler.go | 4 ++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/activation/wire/interface.go b/activation/wire/interface.go index c275516b4e9..d7030bfb09a 100644 --- a/activation/wire/interface.go +++ b/activation/wire/interface.go @@ -40,7 +40,20 @@ type Proof interface { scale.Encodable scale.Decodable + // AllowNoRefATXs returns true if the proof type is valid without reference ATXs proofing the existence of the + // malicious identity. + // + // To avoid spamming of malfeasance proofs for identities that do not exist, by default all proofs require reference + // ATXs (syntactically valid ATXs published by the malicious identity) to be provided. This way any identity that + // the network considers malicious must have been in good standing at some point before the malicious behavior. + // + // For some malfeasance proofs this requirement is not necessary, for example invalid post proofs. Since those + // require the creator of the proof to show that some labels in the post are valid and some invalid. If all are + // invalid, the ATX would be considered syntactically invalid by the network anyway and a proof is not needed. + // In contrast if we would require a reference ATX we couldn't proof an invalid post in an initial ATX of any new + // identity. AllowNoRefATXs() bool + Type() ProofType TypeName() string Info() map[string]string diff --git a/fetch/fetch.go b/fetch/fetch.go index 51f95890f48..807704c608a 100644 --- a/fetch/fetch.go +++ b/fetch/fetch.go @@ -272,13 +272,13 @@ type Fetch struct { // NewFetch creates a new Fetch struct. func NewFetch( - cdb sql.StateDatabase, + db sql.StateDatabase, proposals *store.Store, host *p2p.Host, peerCache *peers.Peers, opts ...Option, ) (*Fetch, error) { - bs := datastore.NewBlobStore(cdb, proposals) + bs := datastore.NewBlobStore(db, proposals) hashPeerCache, err := NewHashPeersCache(cacheSize) if err != nil { @@ -351,7 +351,7 @@ func NewFetch( f.batchTimeout = time.NewTicker(f.cfg.BatchTimeout) if len(f.servers) == 0 { - h := newHandler(cdb, bs, f.logger.Named("handler")) + h := newHandler(db, bs, f.logger.Named("handler")) if f.cfg.Streaming { f.registerServer(host, atxProtocol, h.handleEpochInfoReqStream) f.registerServer(host, hashProtocol, h.handleHashReqStream) diff --git a/fetch/handler.go b/fetch/handler.go index d36bf327215..21e9181da45 100644 --- a/fetch/handler.go +++ b/fetch/handler.go @@ -33,13 +33,13 @@ type handler struct { } func newHandler( - cdb sql.StateDatabase, + db sql.StateDatabase, bs *datastore.BlobStore, lg *zap.Logger, ) *handler { return &handler{ logger: lg, - db: cdb, + db: db, bs: bs, } } From a39c41c86a7a921abbe1199c4d775378e3dfd86a Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:01:01 +0000 Subject: [PATCH 54/56] Review feedback --- malfeasance2/handler.go | 37 +++++-------------------------------- malfeasance2/publisher.go | 4 ++++ 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/malfeasance2/handler.go b/malfeasance2/handler.go index e9af69d1615..c4d75c58c38 100644 --- a/malfeasance2/handler.go +++ b/malfeasance2/handler.go @@ -310,19 +310,12 @@ func (h *Handler) fetchReferences(ctx context.Context, peer p2p.Peer, atxIDs []t } func (h *Handler) storeProof(ctx context.Context, nodeIDs []types.NodeID, proof []byte, domain ProofDomain) error { + // Persisting the proof in the DB has to be done within a transaction to ensure consistency. The ATX handler could + // update the (or merge multiple) marriage set in parallel, so we need to make sure data is consistent while we + // update the malfeasance table. return h.db.WithTxImmediate(ctx, func(tx sql.Transaction) error { if len(nodeIDs) == 1 { // smesher is not married - malicious, err := malfeasance.IsMalicious(tx, nodeIDs[0]) - if err != nil { - return fmt.Errorf("check if smesher is malicious: %w", err) - } - if malicious { - h.logger.Debug("smesher is already marked as malicious", - zap.String("smesher_id", nodeIDs[0].ShortString()), - ) - return nil - } if err := malfeasance.AddProof(tx, nodeIDs[0], nil, proof, int(domain), time.Now()); err != nil { return fmt.Errorf("store malfeasance proof for %s: %w", nodeIDs[0], err) } @@ -333,30 +326,10 @@ func (h *Handler) storeProof(ctx context.Context, nodeIDs []types.NodeID, proof if err != nil { return fmt.Errorf("get marriage ID for %s: %w", nodeIDs[0].ShortString(), err) } - malicious, err := malfeasance.IsMalicious(tx, nodeIDs[0]) - if err != nil { - return fmt.Errorf("check if smesher %s is malicious: %w", nodeIDs[0].ShortString(), err) - } - if !malicious { - if err := malfeasance.AddProof(tx, nodeIDs[0], &mID, proof, int(domain), time.Now()); err != nil { - return fmt.Errorf("store malfeasance proof for %s: %w", nodeIDs[0].ShortString(), err) - } - } else { - h.logger.Debug("smesher is already marked as malicious", - zap.String("smesher_id", nodeIDs[0].ShortString()), - ) + if err := malfeasance.AddProof(tx, nodeIDs[0], &mID, proof, int(domain), time.Now()); err != nil { + return fmt.Errorf("store malfeasance proof for %s: %w", nodeIDs[0].ShortString(), err) } for _, nodeID := range nodeIDs[1:] { - malicious, err := malfeasance.IsMalicious(tx, nodeID) - if err != nil { - return fmt.Errorf("check if smesher %s is malicious: %w", nodeID.ShortString(), err) - } - if malicious { - h.logger.Debug("smesher is already marked as malicious", - zap.String("smesher_id", nodeID.ShortString()), - ) - continue - } if err := malfeasance.SetMalicious(tx, nodeID, mID, time.Now()); err != nil { return fmt.Errorf("update malfeasance state for %s: %w", nodeID.ShortString(), err) } diff --git a/malfeasance2/publisher.go b/malfeasance2/publisher.go index a3be2609680..6f545b0a67a 100644 --- a/malfeasance2/publisher.go +++ b/malfeasance2/publisher.go @@ -48,6 +48,10 @@ func (p *Publisher) PublishATXProof(ctx context.Context, nodeID types.NodeID, pr publish := false // whether to publish the proof var set []types.NodeID var refATXs []types.ATXID + + // Persisting the proof in the DB has to be done within a transaction to ensure consistency. The ATX handler could + // update the (or merge multiple) marriage set in parallel, so we need to make sure data is consistent while we + // update the malfeasance table. err := p.db.WithTxImmediate(ctx, func(tx sql.Transaction) error { marriageID, err := marriage.FindIDByNodeID(tx, nodeID) switch { From 9514603736472a22e8d41ebe6e8ca62c6bcdd310 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:16:32 +0000 Subject: [PATCH 55/56] Add comment --- activation/handler_v2.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/activation/handler_v2.go b/activation/handler_v2.go index c6a191f88e0..597541116ec 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -748,6 +748,9 @@ func (h *HandlerV2) checkMalicious(ctx context.Context, watx *activationTx, repu var malicious bool var proof wire.Proof var nodeID types.NodeID + // we have to do malfeasance checks in a transaction to ensure we have the latest state on the marriage set of the + // identity we are processing. Otherwise a new ATX could come in that updates the (or merges multiple existing) + // marriage sets and this would lead to inconsistent behavior in the malfeasance checks. err := h.cdb.WithTxImmediate(ctx, func(tx sql.Transaction) error { // malfeasance check happens after storing the ATX because storing updates the marriage set // that is needed for the malfeasance proof From b98d79192d7ac100692c700f03e463d12722bbef Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:39:34 +0000 Subject: [PATCH 56/56] Check for malfeasance in single TX --- activation/handler_v2.go | 125 +++++----- activation/handler_v2_test.go | 430 ++++++++++++++++++++++++---------- 2 files changed, 362 insertions(+), 193 deletions(-) diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 597541116ec..a16969379f7 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -732,79 +732,32 @@ func (h *HandlerV2) validatePost( return fmt.Errorf("invalid post for ID %s: %w", nodeID.ShortString(), errInvalidIdx) } -func (h *HandlerV2) checkMalicious(ctx context.Context, watx *activationTx, republishProof bool) (bool, error) { - if republishProof { - if err := h.malPublisher.Regossip(ctx, watx.SmesherID); err != nil { - h.logger.Error("failed to regossip malfeasance proof", - zap.Stringer("atx_id", watx.ID()), - zap.Stringer("smesher_id", watx.SmesherID), - zap.Error(err), - ) - return true, err - } - return true, nil +func (h *HandlerV2) checkMalicious( + ctx context.Context, + tx sql.Transaction, + watx *activationTx, +) (wire.Proof, types.NodeID, error) { + proof, nodeID, err := h.checkDoubleMarry(ctx, tx, watx) + if err != nil { + return nil, types.EmptyNodeID, fmt.Errorf("checking double marry: %w", err) + } + if proof != nil { + return proof, nodeID, nil } - var malicious bool - var proof wire.Proof - var nodeID types.NodeID - // we have to do malfeasance checks in a transaction to ensure we have the latest state on the marriage set of the - // identity we are processing. Otherwise a new ATX could come in that updates the (or merges multiple existing) - // marriage sets and this would lead to inconsistent behavior in the malfeasance checks. - err := h.cdb.WithTxImmediate(ctx, func(tx sql.Transaction) error { - // malfeasance check happens after storing the ATX because storing updates the marriage set - // that is needed for the malfeasance proof - // - // TODO(mafa): don't store own ATX if it would mark the node as malicious - // this probably needs to be done by validating and storing own ATXs eagerly and skipping validation in - // the gossip handler (not sync!) - var err error - malicious, err = malfeasance.IsMalicious(tx, watx.SmesherID) - if err != nil { - return fmt.Errorf("checking if node is malicious: %w", err) - } - if malicious { - return nil - } - - proof, nodeID, err = h.checkDoubleMarry(ctx, tx, watx) - if err != nil { - return fmt.Errorf("checking double marry: %w", err) - } - if proof != nil { - return nil - } - - proof, nodeID, err = h.checkDoubleMerge(ctx, tx, watx) - if err != nil { - return fmt.Errorf("checking double merge: %w", err) - } - if proof != nil { - return nil - } - - proof, nodeID, err = h.checkPrevAtx(ctx, tx, watx) - if err != nil { - return fmt.Errorf("checking previous ATX: %w", err) - } - return nil - }) + proof, nodeID, err = h.checkDoubleMerge(ctx, tx, watx) if err != nil { - return malicious, fmt.Errorf("check malicious: %w", err) + return nil, types.EmptyNodeID, fmt.Errorf("checking double merge: %w", err) } - if proof == nil { - return malicious, nil + if proof != nil { + return proof, nodeID, nil } - if err := h.malPublisher.Publish(ctx, nodeID, proof); err != nil { - h.logger.Error("failed to publish malfeasance proof", - zap.Stringer("atx_id", watx.ID()), - zap.Stringer("smesher_id", watx.SmesherID), - zap.Error(err), - ) - return true, err + proof, nodeID, err = h.checkPrevAtx(ctx, tx, watx) + if err != nil { + return nil, types.EmptyNodeID, fmt.Errorf("checking previous ATX: %w", err) } - return true, nil + return proof, nodeID, nil } func (h *HandlerV2) fetchWireAtx( @@ -990,6 +943,9 @@ func (h *HandlerV2) checkPrevAtx( // Store an ATX in the DB. func (h *HandlerV2) storeAtx(ctx context.Context, atx *types.ActivationTx, watx *activationTx) error { republishProof := false + malicious := false + var proof wire.Proof + var nodeID types.NodeID if err := h.cdb.WithTxImmediate(ctx, func(tx sql.Transaction) error { if len(watx.marriages) != 0 { newMarriageID, err := marriage.NewID(tx) @@ -1001,7 +957,6 @@ func (h *HandlerV2) storeAtx(ctx context.Context, atx *types.ActivationTx, watx ATX: atx.ID(), Target: atx.SmesherID, } - malicious := false marriageIDs := make(map[marriage.ID]struct{}, 1) marriageIDs[newMarriageID] = struct{}{} for i, m := range watx.marriages { @@ -1068,18 +1023,44 @@ func (h *HandlerV2) storeAtx(ctx context.Context, atx *types.ActivationTx, watx return fmt.Errorf("setting atx units for ID %s: %w", id, err) } } - return nil + + if malicious || republishProof { + return nil + } + + // malfeasance check happens after storing the ATX because storing updates the marriage set + // that is needed for the malfeasance proof + // + // TODO(mafa): don't store own ATX if it would mark the node as malicious + // this probably needs to be done by validating and storing own ATXs eagerly and skipping validation in + // the gossip handler (not sync!) + proof, nodeID, err = h.checkMalicious(ctx, tx, watx) + return err }); err != nil { return fmt.Errorf("store atx: %w", err) } - malicious, err := h.checkMalicious(ctx, watx, republishProof) - if err != nil { - return err + switch { + case republishProof: // marriage set of known malicious smesher has changed, force re-gossip of proof + if err := h.malPublisher.Regossip(ctx, watx.SmesherID); err != nil { + h.logger.Error("failed to regossip malfeasance proof", + zap.Stringer("atx_id", watx.ID()), + zap.Stringer("smesher_id", watx.SmesherID), + zap.Error(err), + ) + } + case proof != nil: // new malfeasance proof for identity created, publish proof (gossip is decided by publisher) + if err := h.malPublisher.Publish(ctx, nodeID, proof); err != nil { + h.logger.Error("failed to publish malfeasance proof", + zap.Stringer("atx_id", watx.ID()), + zap.Stringer("smesher_id", watx.SmesherID), + zap.Error(err), + ) + } } h.beacon.OnAtx(atx) - if added := h.atxsdata.AddFromAtx(atx, malicious); added != nil { + if added := h.atxsdata.AddFromAtx(atx, malicious || proof != nil); added != nil { h.tortoise.OnAtx(atx.TargetEpoch(), atx.ID(), added) } diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 0dda1991b59..7cbe9681d0b 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -15,7 +15,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest" + "go.uber.org/zap/zaptest/observer" "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/atxsdata" @@ -35,7 +38,8 @@ import ( type v2TestHandler struct { *HandlerV2 - tb testing.TB + tb testing.TB + observedLogs *observer.ObservedLogs handlerMocks } @@ -50,8 +54,13 @@ const ( ) func newV2TestHandler(tb testing.TB, golden types.ATXID) *v2TestHandler { - lg := zaptest.NewLogger(tb) - cdb := datastore.NewCachedDB(statesql.InMemoryTest(tb), lg) + observer, observedLogs := observer.New(zapcore.WarnLevel) + logger := zaptest.NewLogger(tb, zaptest.WrapOptions(zap.WrapCore( + func(core zapcore.Core) zapcore.Core { + return zapcore.NewTee(core, observer) + }, + ))) + cdb := datastore.NewCachedDB(statesql.InMemoryTest(tb), logger) tb.Cleanup(func() { assert.NoError(tb, cdb.Close()) }) mocks := newTestHandlerMocks(tb, golden) return &v2TestHandler{ @@ -64,13 +73,14 @@ func newV2TestHandler(tb testing.TB, golden types.ATXID) *v2TestHandler { tickSize: tickSize, goldenATXID: golden, nipostValidator: mocks.mValidator, - logger: lg, + logger: logger, fetcher: mocks.mockFetch, beacon: mocks.mBeacon, tortoise: mocks.mTortoise, malPublisher: mocks.mMalPublish, }, tb: tb, + observedLogs: observedLogs, handlerMocks: mocks, } } @@ -1940,151 +1950,329 @@ func Test_Marriages(t *testing.T) { }) t.Run("can't marry twice (separate marriages)", func(t *testing.T) { t.Parallel() - atxHandler := newV2TestHandler(t, golden) + t.Run("publish succeeds", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) - otherSig, err := signing.NewEdSigner() - require.NoError(t, err) - atx, _ := marryIDs(t, atxHandler, []*signing.EdSigner{sig, otherSig}, golden) + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + atx, _ := marryIDs(t, atxHandler, []*signing.EdSigner{sig, otherSig}, golden) - // otherSig2 cannot marry sig, trying to extend its set. - otherSig2, err := signing.NewEdSigner() - require.NoError(t, err) - others2Atx := atxHandler.createAndProcessInitial(otherSig2) - atx2 := newSoloATXv2(t, atx.PublishEpoch+1, atx.ID(), atx.ID()) - atx2.Marriages = []wire.MarriageCertificate{ - { - Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), - }, - { - ReferenceAtx: others2Atx.ID(), - Signature: otherSig2.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), - }, - } - atx2.Sign(sig) - atxHandler.expectAtxV2(atx2) + // otherSig2 cannot marry sig, trying to extend its set. + otherSig2, err := signing.NewEdSigner() + require.NoError(t, err) + others2Atx := atxHandler.createAndProcessInitial(otherSig2) + atx2 := newSoloATXv2(t, atx.PublishEpoch+1, atx.ID(), atx.ID()) + atx2.Marriages = []wire.MarriageCertificate{ + { + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + { + ReferenceAtx: others2Atx.ID(), + Signature: otherSig2.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + } + atx2.Sign(sig) + atxHandler.expectAtxV2(atx2) - verifier := wire.NewMockMalfeasanceValidator(atxHandler.ctrl) - verifier.EXPECT().Signature(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { - return atxHandler.edVerifier.Verify(d, nodeID, m, sig) - }).AnyTimes() - verifier.EXPECT().IdentityExists(sig.NodeID()).Return(true, nil).AnyTimes() + verifier := wire.NewMockMalfeasanceValidator(atxHandler.ctrl) + verifier.EXPECT().Signature(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { + return atxHandler.edVerifier.Verify(d, nodeID, m, sig) + }).AnyTimes() + verifier.EXPECT().IdentityExists(sig.NodeID()).Return(true, nil).AnyTimes() - atxHandler.mMalPublish.EXPECT().Publish( - gomock.Any(), - sig.NodeID(), - gomock.AssignableToTypeOf(&wire.ProofDoubleMarry{}), - ).DoAndReturn(func(ctx context.Context, _ types.NodeID, proof wire.Proof) error { - malProof := proof.(*wire.ProofDoubleMarry) - nId, err := malProof.Valid(ctx, verifier) + atxHandler.mMalPublish.EXPECT().Publish( + gomock.Any(), + sig.NodeID(), + gomock.AssignableToTypeOf(&wire.ProofDoubleMarry{}), + ).DoAndReturn(func(ctx context.Context, _ types.NodeID, proof wire.Proof) error { + malProof := proof.(*wire.ProofDoubleMarry) + nId, err := malProof.Valid(ctx, verifier) + require.NoError(t, err) + require.Equal(t, sig.NodeID(), nId) + return nil + }) + err = atxHandler.processATX(context.Background(), "", atx2, time.Now()) require.NoError(t, err) - require.Equal(t, sig.NodeID(), nId) - return nil + + // The equivocation set of sig and otherSig were merged + id, err := marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id) + require.NoError(t, err) + require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv) }) - err = atxHandler.processATX(context.Background(), "", atx2, time.Now()) - require.NoError(t, err) - // The equivocation set of sig and otherSig were merged - id, err := marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) - require.NoError(t, err) - equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id) - require.NoError(t, err) - require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv) + t.Run("publish fails", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) + + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + atx, _ := marryIDs(t, atxHandler, []*signing.EdSigner{sig, otherSig}, golden) + + // otherSig2 cannot marry sig, trying to extend its set. + otherSig2, err := signing.NewEdSigner() + require.NoError(t, err) + others2Atx := atxHandler.createAndProcessInitial(otherSig2) + atx2 := newSoloATXv2(t, atx.PublishEpoch+1, atx.ID(), atx.ID()) + atx2.Marriages = []wire.MarriageCertificate{ + { + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + { + ReferenceAtx: others2Atx.ID(), + Signature: otherSig2.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + } + atx2.Sign(sig) + atxHandler.expectAtxV2(atx2) + + verifier := wire.NewMockMalfeasanceValidator(atxHandler.ctrl) + verifier.EXPECT().Signature(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { + return atxHandler.edVerifier.Verify(d, nodeID, m, sig) + }).AnyTimes() + verifier.EXPECT().IdentityExists(sig.NodeID()).Return(true, nil).AnyTimes() + + atxHandler.mMalPublish.EXPECT().Publish( + gomock.Any(), + sig.NodeID(), + gomock.AssignableToTypeOf(&wire.ProofDoubleMarry{}), + ).Return(errors.New("publish failed")) + err = atxHandler.processATX(context.Background(), "", atx2, time.Now()) + require.NoError(t, err) + + // The equivocation set of sig and otherSig were merged + id, err := marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id) + require.NoError(t, err) + require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv) + + observedLogs := atxHandler.observedLogs.FilterLevelExact(zapcore.ErrorLevel) + require.Equal(t, 1, observedLogs.Len(), "expected 1 log message") + require.Equal(t, zapcore.ErrorLevel, observedLogs.All()[0].Level) + require.Equal(t, "failed to publish malfeasance proof", observedLogs.All()[0].Message) + require.Equal(t, sig.NodeID().String(), observedLogs.All()[0].ContextMap()["smesher_id"]) + require.Equal(t, atx2.ID().ShortString(), observedLogs.All()[0].ContextMap()["atx_id"]) + require.Equal(t, "publish failed", observedLogs.All()[0].ContextMap()["error"]) + }) }) t.Run("marring existing malicious equivocation set: mark all malicious and regossip proof", func(t *testing.T) { t.Parallel() - atxHandler := newV2TestHandler(t, golden) + t.Run("regossip succeeds", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) - otherSig, err := signing.NewEdSigner() - require.NoError(t, err) - atx, _ := marryIDs(t, atxHandler, []*signing.EdSigner{sig, otherSig}, golden) + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + atx, _ := marryIDs(t, atxHandler, []*signing.EdSigner{sig, otherSig}, golden) - // sig becomes malicious in some way and with it otherSig - id, err := marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) - require.NoError(t, err) - require.NoError(t, malfeasance.AddProof(atxHandler.cdb, sig.NodeID(), &id, []byte("proof"), 0, time.Now())) - require.NoError(t, malfeasance.SetMalicious(atxHandler.cdb, otherSig.NodeID(), id, time.Now())) + // sig becomes malicious in some way and with it otherSig + id, err := marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + require.NoError(t, malfeasance.AddProof(atxHandler.cdb, sig.NodeID(), &id, []byte("proof"), 0, time.Now())) + require.NoError(t, malfeasance.SetMalicious(atxHandler.cdb, otherSig.NodeID(), id, time.Now())) - // otherSig2 cannot marry sig, trying to extend its set. - otherSig2, err := signing.NewEdSigner() - require.NoError(t, err) - others2Atx := atxHandler.createAndProcessInitial(otherSig2) - atx2 := newSoloATXv2(t, atx.PublishEpoch+1, atx.ID(), atx.ID()) - atx2.Marriages = []wire.MarriageCertificate{ - { - Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), - }, - { - ReferenceAtx: others2Atx.ID(), - Signature: otherSig2.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), - }, - } - atx2.Sign(sig) - atxHandler.expectAtxV2(atx2) + // otherSig2 cannot marry sig, trying to extend its set. + otherSig2, err := signing.NewEdSigner() + require.NoError(t, err) + others2Atx := atxHandler.createAndProcessInitial(otherSig2) + atx2 := newSoloATXv2(t, atx.PublishEpoch+1, atx.ID(), atx.ID()) + atx2.Marriages = []wire.MarriageCertificate{ + { + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + { + ReferenceAtx: others2Atx.ID(), + Signature: otherSig2.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + } + atx2.Sign(sig) + atxHandler.expectAtxV2(atx2) - atxHandler.mMalPublish.EXPECT().Regossip(gomock.Any(), sig.NodeID()) - err = atxHandler.processATX(context.Background(), "", atx2, time.Now()) - require.NoError(t, err) + atxHandler.mMalPublish.EXPECT().Regossip(gomock.Any(), sig.NodeID()) + err = atxHandler.processATX(context.Background(), "", atx2, time.Now()) + require.NoError(t, err) - // The equivocation set of sig and otherSig were merged - id, err = marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) - require.NoError(t, err) - equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id) - require.NoError(t, err) - require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv) + // The equivocation set of sig and otherSig were merged + id, err = marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id) + require.NoError(t, err) + require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv) - for _, sig := range []*signing.EdSigner{sig, otherSig, otherSig2} { - m, err := malfeasance.IsMalicious(atxHandler.cdb, sig.NodeID()) + for _, sig := range []*signing.EdSigner{sig, otherSig, otherSig2} { + m, err := malfeasance.IsMalicious(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + require.True(t, m, "expected %s to be malicious", sig) + } + }) + + t.Run("regossip fails", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) + + otherSig, err := signing.NewEdSigner() require.NoError(t, err) - require.True(t, m, "expected %s to be malicious", sig) - } + atx, _ := marryIDs(t, atxHandler, []*signing.EdSigner{sig, otherSig}, golden) + + // sig becomes malicious in some way and with it otherSig + id, err := marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + require.NoError(t, malfeasance.AddProof(atxHandler.cdb, sig.NodeID(), &id, []byte("proof"), 0, time.Now())) + require.NoError(t, malfeasance.SetMalicious(atxHandler.cdb, otherSig.NodeID(), id, time.Now())) + + // otherSig2 cannot marry sig, trying to extend its set. + otherSig2, err := signing.NewEdSigner() + require.NoError(t, err) + others2Atx := atxHandler.createAndProcessInitial(otherSig2) + atx2 := newSoloATXv2(t, atx.PublishEpoch+1, atx.ID(), atx.ID()) + atx2.Marriages = []wire.MarriageCertificate{ + { + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + { + ReferenceAtx: others2Atx.ID(), + Signature: otherSig2.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + } + atx2.Sign(sig) + atxHandler.expectAtxV2(atx2) + + atxHandler.mMalPublish.EXPECT().Regossip(gomock.Any(), sig.NodeID()).Return(errors.New("regossip failed")) + err = atxHandler.processATX(context.Background(), "", atx2, time.Now()) + require.NoError(t, err) + + // The equivocation set of sig and otherSig were merged + id, err = marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id) + require.NoError(t, err) + require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv) + + for _, sig := range []*signing.EdSigner{sig, otherSig, otherSig2} { + m, err := malfeasance.IsMalicious(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + require.True(t, m, "expected %s to be malicious", sig) + } + + observedLogs := atxHandler.observedLogs.FilterLevelExact(zapcore.ErrorLevel) + require.Equal(t, 1, observedLogs.Len(), "expected 1 log message") + require.Equal(t, zapcore.ErrorLevel, observedLogs.All()[0].Level) + require.Equal(t, "failed to regossip malfeasance proof", observedLogs.All()[0].Message) + require.Equal(t, sig.NodeID().String(), observedLogs.All()[0].ContextMap()["smesher_id"]) + require.Equal(t, atx2.ID().ShortString(), observedLogs.All()[0].ContextMap()["atx_id"]) + require.Equal(t, "regossip failed", observedLogs.All()[0].ContextMap()["error"]) + }) }) t.Run("malicious marring existing equivocation set: mark all malicious and regossip proof", func(t *testing.T) { t.Parallel() - atxHandler := newV2TestHandler(t, golden) + t.Run("regossip succeeds", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) - otherSig, err := signing.NewEdSigner() - require.NoError(t, err) - atx, _ := marryIDs(t, atxHandler, []*signing.EdSigner{sig, otherSig}, golden) + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + atx, _ := marryIDs(t, atxHandler, []*signing.EdSigner{sig, otherSig}, golden) - // otherSig2 cannot marry sig, trying to extend its set. - otherSig2, err := signing.NewEdSigner() - require.NoError(t, err) - others2Atx := atxHandler.createAndProcessInitial(otherSig2) + // otherSig2 cannot marry sig, trying to extend its set. + otherSig2, err := signing.NewEdSigner() + require.NoError(t, err) + others2Atx := atxHandler.createAndProcessInitial(otherSig2) - // otherSig2 becomes malicious in some way - err = malfeasance.AddProof(atxHandler.cdb, otherSig2.NodeID(), nil, []byte("proof"), 0, time.Now()) - require.NoError(t, err) + // otherSig2 becomes malicious in some way + err = malfeasance.AddProof(atxHandler.cdb, otherSig2.NodeID(), nil, []byte("proof"), 0, time.Now()) + require.NoError(t, err) - atx2 := newSoloATXv2(t, atx.PublishEpoch+1, atx.ID(), atx.ID()) - atx2.Marriages = []wire.MarriageCertificate{ - { - Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), - }, - { - ReferenceAtx: others2Atx.ID(), - Signature: otherSig2.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), - }, - } - atx2.Sign(sig) - atxHandler.expectAtxV2(atx2) + atx2 := newSoloATXv2(t, atx.PublishEpoch+1, atx.ID(), atx.ID()) + atx2.Marriages = []wire.MarriageCertificate{ + { + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + { + ReferenceAtx: others2Atx.ID(), + Signature: otherSig2.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + } + atx2.Sign(sig) + atxHandler.expectAtxV2(atx2) - atxHandler.mMalPublish.EXPECT().Regossip(gomock.Any(), sig.NodeID()) - err = atxHandler.processATX(context.Background(), "", atx2, time.Now()) - require.NoError(t, err) + atxHandler.mMalPublish.EXPECT().Regossip(gomock.Any(), sig.NodeID()) + err = atxHandler.processATX(context.Background(), "", atx2, time.Now()) + require.NoError(t, err) - // The equivocation set of sig and otherSig were merged - id, err := marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) - require.NoError(t, err) - equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id) - require.NoError(t, err) - require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv) + // The equivocation set of sig and otherSig were merged + id, err := marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id) + require.NoError(t, err) + require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv) - for _, sig := range []*signing.EdSigner{sig, otherSig, otherSig2} { - m, err := malfeasance.IsMalicious(atxHandler.cdb, sig.NodeID()) + for _, sig := range []*signing.EdSigner{sig, otherSig, otherSig2} { + m, err := malfeasance.IsMalicious(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + require.True(t, m, "expected %s to be malicious", sig) + } + }) + + t.Run("regossip fails", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) + + otherSig, err := signing.NewEdSigner() require.NoError(t, err) - require.True(t, m, "expected %s to be malicious", sig) - } + atx, _ := marryIDs(t, atxHandler, []*signing.EdSigner{sig, otherSig}, golden) + + // otherSig2 cannot marry sig, trying to extend its set. + otherSig2, err := signing.NewEdSigner() + require.NoError(t, err) + others2Atx := atxHandler.createAndProcessInitial(otherSig2) + + // otherSig2 becomes malicious in some way + err = malfeasance.AddProof(atxHandler.cdb, otherSig2.NodeID(), nil, []byte("proof"), 0, time.Now()) + require.NoError(t, err) + + atx2 := newSoloATXv2(t, atx.PublishEpoch+1, atx.ID(), atx.ID()) + atx2.Marriages = []wire.MarriageCertificate{ + { + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + { + ReferenceAtx: others2Atx.ID(), + Signature: otherSig2.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + } + atx2.Sign(sig) + atxHandler.expectAtxV2(atx2) + + atxHandler.mMalPublish.EXPECT().Regossip(gomock.Any(), sig.NodeID()).Return(errors.New("regossip failed")) + err = atxHandler.processATX(context.Background(), "", atx2, time.Now()) + require.NoError(t, err) + + // The equivocation set of sig and otherSig were merged + id, err := marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id) + require.NoError(t, err) + require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv) + + for _, sig := range []*signing.EdSigner{sig, otherSig, otherSig2} { + m, err := malfeasance.IsMalicious(atxHandler.cdb, sig.NodeID()) + require.NoError(t, err) + require.True(t, m, "expected %s to be malicious", sig) + } + + observedLogs := atxHandler.observedLogs.FilterLevelExact(zapcore.ErrorLevel) + require.Equal(t, 1, observedLogs.Len(), "expected 1 log message") + require.Equal(t, zapcore.ErrorLevel, observedLogs.All()[0].Level) + require.Equal(t, "failed to regossip malfeasance proof", observedLogs.All()[0].Message) + require.Equal(t, sig.NodeID().String(), observedLogs.All()[0].ContextMap()["smesher_id"]) + require.Equal(t, atx2.ID().ShortString(), observedLogs.All()[0].ContextMap()["atx_id"]) + require.Equal(t, "regossip failed", observedLogs.All()[0].ContextMap()["error"]) + }) }) t.Run("malicious marring malicious equivocation set: no proof published", func(t *testing.T) { t.Parallel()