From e2bee5597e88370c5d4338b1164f17abe89d87f5 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:39:48 +0000 Subject: [PATCH 01/25] Update API dependency --- go.mod | 2 ++ go.sum | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 9a76abd9..774cc38f 100644 --- a/go.mod +++ b/go.mod @@ -232,3 +232,5 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace github.com/spacemeshos/api/release/go => github.com/spacemeshos/api-cve-fix/release/go v1.36.1-0.20240429173440-42be53a006d3 diff --git a/go.sum b/go.sum index c98cfbe8..83009309 100644 --- a/go.sum +++ b/go.sum @@ -555,8 +555,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.37.0 h1:bN6AhSMVSmAShGxUYKwFBfzY3U1XtHezpDjt20dHjBM= -github.com/spacemeshos/api/release/go v1.37.0/go.mod h1:Ed7SdL2YgqNg2SeShEAonW3GTPuuaGzsY5i4bgziCRo= +github.com/spacemeshos/api-cve-fix/release/go v1.36.1-0.20240429173440-42be53a006d3 h1:AXTfy9764T4zye7fk3V2X4K4xVuyrX0X/n8nXZ4YEMg= +github.com/spacemeshos/api-cve-fix/release/go v1.36.1-0.20240429173440-42be53a006d3/go.mod h1:Ed7SdL2YgqNg2SeShEAonW3GTPuuaGzsY5i4bgziCRo= github.com/spacemeshos/economics v0.1.3 h1:ACkq3mTebIky4Zwbs9SeSSRZrUCjU/Zk0wq9Z0BTh2A= github.com/spacemeshos/economics v0.1.3/go.mod h1:FH7u0FzTIm6Kpk+X5HOZDvpkgNYBKclmH86rVwYaDAo= github.com/spacemeshos/fixed v0.1.1 h1:N1y4SUpq1EV+IdJrWJwUCt1oBFzeru/VKVcBsvPc2Fk= From 364047e8b572ca8ba58e5962045500f42f841f9d Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 29 Apr 2024 20:25:43 +0000 Subject: [PATCH 02/25] Add malfeasance proofs for invalid previous ATX --- activation/handler.go | 27 +-- common/types/hashes.go | 3 + common/types/layer.go | 2 +- events/events.go | 2 + malfeasance/handler.go | 40 ++++- malfeasance/handler_test.go | 229 +++++++++++++++++++++++++- malfeasance/metrics.go | 5 +- malfeasance/wire/malfeasance.go | 44 ++++- malfeasance/wire/malfeasance_scale.go | 36 ++++ malfeasance/wire/malfeasance_test.go | 94 +++++++++++ sql/identities/identities.go | 2 +- 11 files changed, 460 insertions(+), 24 deletions(-) diff --git a/activation/handler.go b/activation/handler.go index aac0b274..b15bb1c4 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -213,16 +213,21 @@ func (h *Handler) SyntacticallyValidateDeps( ctx context.Context, atx *types.ActivationTx, ) (*types.VerifiedActivationTx, *mwire.MalfeasanceProof, error) { - var ( - commitmentATX *types.ATXID - err error - ) + var commitmentATX *types.ATXID if atx.PrevATXID == types.EmptyATXID { if err := h.validateInitialAtx(ctx, atx); err != nil { return nil, nil, err } - commitmentATX = atx.CommitmentATX + commitmentATX = atx.CommitmentATX // checked to be non-nil in syntactic validation } else { + prev, err := atxs.Get(h.cdb, atx.PrevATXID) // TODO(mafa): add tests for this + if err != nil { + return nil, nil, fmt.Errorf("prev atx for %s not found: %w", atx.PrevATXID, err) + } + if prev.SmesherID != atx.SmesherID { + return nil, nil, fmt.Errorf("prev atx smesher id mismatch: %s != %s", prev.SmesherID, atx.SmesherID) + } + commitmentATX, err = h.getCommitmentAtx(atx) if err != nil { return nil, nil, fmt.Errorf("commitment atx for %s not found: %w", atx.SmesherID, err) @@ -411,7 +416,7 @@ func (h *Handler) storeAtx(ctx context.Context, atx *types.VerifiedActivationTx) return nil, fmt.Errorf("checking if node is malicious: %w", err) } var proof *mwire.MalfeasanceProof - if err := h.cdb.WithTx(ctx, func(tx *sql.Tx) error { + err = h.cdb.WithTx(ctx, func(tx *sql.Tx) error { if malicious { if err := atxs.Add(tx, atx); err != nil && !errors.Is(err, sql.ErrObjectExists) { return fmt.Errorf("add atx to db: %w", err) @@ -472,7 +477,8 @@ func (h *Handler) storeAtx(ctx context.Context, atx *types.VerifiedActivationTx) return fmt.Errorf("add atx to db: %w", err) } return nil - }); err != nil { + }) + if err != nil { return nil, fmt.Errorf("store atx: %w", err) } if nonce == nil { @@ -512,7 +518,7 @@ func (h *Handler) HandleSyncedAtx(ctx context.Context, expHash types.Hash32, pee // HandleGossipAtx handles the atx gossip data channel. func (h *Handler) HandleGossipAtx(ctx context.Context, peer p2p.Peer, msg []byte) error { - proof, err := h.handleAtx(ctx, types.Hash32{}, peer, msg) + proof, err := h.handleAtx(ctx, types.EmptyHash32, peer, msg) if err != nil && !errors.Is(err, errMalformedData) && !errors.Is(err, errKnownAtx) { h.log.WithContext(ctx).With().Warning("failed to process atx gossip", log.Stringer("sender", peer), @@ -621,7 +627,7 @@ func (h *Handler) processATX( return proof, err } - if expHash != (types.Hash32{}) && vAtx.ID().Hash32() != expHash { + if expHash != types.EmptyHash32 && vAtx.ID().Hash32() != expHash { return nil, fmt.Errorf( "%w: atx want %s, got %s", errWrongHash, @@ -637,7 +643,8 @@ func (h *Handler) processATX( events.ReportNewActivation(vAtx) h.log.WithContext(ctx).With().Info( "new atx", log.Inline(vAtx), - log.Bool("malicious", proof != nil)) + log.Bool("malicious", proof != nil), + ) return proof, err } diff --git a/common/types/hashes.go b/common/types/hashes.go index c01c748d..5590541d 100644 --- a/common/types/hashes.go +++ b/common/types/hashes.go @@ -20,6 +20,9 @@ const ( var ( hash20T = reflect.TypeOf(Hash20{}) hash32T = reflect.TypeOf(Hash32{}) + + // EmptyHash32 is the zero hash. + EmptyHash32 = Hash32{} ) // Hash32 represents the 32-byte blake3 hash of arbitrary data. diff --git a/common/types/layer.go b/common/types/layer.go index d88dcb59..f760227f 100644 --- a/common/types/layer.go +++ b/common/types/layer.go @@ -18,7 +18,7 @@ var ( effectiveGenesis uint32 // EmptyLayerHash is the layer hash for an empty layer. - EmptyLayerHash = Hash32{} + EmptyLayerHash = EmptyHash32 ) // SetLayersPerEpoch sets global parameter of layers per epoch, all conversions from layer to epoch use this param. diff --git a/events/events.go b/events/events.go index 7501bf73..30107a73 100644 --- a/events/events.go +++ b/events/events.go @@ -298,6 +298,8 @@ func ToMalfeasancePB(nodeID types.NodeID, mp *wire.MalfeasanceProof, includeProo kind = pb.MalfeasanceProof_MALFEASANCE_HARE case wire.InvalidPostIndex: kind = pb.MalfeasanceProof_MALFEASANCE_POST_INDEX + case wire.InvalidPrevATX: + kind = pb.MalfeasanceProof_MALFEASANCE_INCORRECT_PREV_ATX } result := &pb.MalfeasanceProof{ SmesherId: &pb.SmesherId{Id: nodeID.Bytes()}, diff --git a/malfeasance/handler.go b/malfeasance/handler.go index c48ec8e9..724dfe7a 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -10,6 +10,7 @@ import ( "github.com/spacemeshos/post/shared" "github.com/spacemeshos/post/verifying" + awire "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" @@ -86,7 +87,7 @@ func (h *Handler) HandleSyncedMalfeasanceProof( nodeID, err := h.validateAndSave(ctx, &wire.MalfeasanceGossip{MalfeasanceProof: p}) if err == nil && types.Hash32(nodeID) != expHash { return fmt.Errorf( - "%w: malfesance proof want %s, got %s", + "%w: malfeasance proof want %s, got %s", errWrongHash, expHash.ShortString(), nodeID.ShortString(), @@ -187,6 +188,9 @@ func Validate( case wire.InvalidPostIndex: proof := p.MalfeasanceProof.Proof.Data.(*wire.InvalidPostIndexProof) // guaranteed to work by scale func nodeID, err = validateInvalidPostIndex(ctx, logger, cdb, edVerifier, postVerifier, proof) + case wire.InvalidPrevATX: + proof := p.MalfeasanceProof.Proof.Data.(*wire.InvalidPrevATXProof) // guaranteed to work by scale func + nodeID, err = validateInvalidPrevATX(ctx, cdb, edVerifier, proof) default: return nodeID, fmt.Errorf("%w: unknown malfeasance type", errInvalidProof) } @@ -211,6 +215,8 @@ func updateMetrics(tp wire.Proof) { numProofsBallot.Inc() case wire.InvalidPostIndex: numProofsPostIndex.Inc() + case wire.InvalidPrevATX: + numProofsPrevATX.Inc() } } @@ -377,7 +383,8 @@ func validateMultipleBallots( return types.EmptyNodeID, errors.New("invalid ballot malfeasance proof") } -func validateInvalidPostIndex(ctx context.Context, +func validateInvalidPostIndex( + ctx context.Context, logger log.Log, db sql.Executor, edVerifier SigVerifier, @@ -415,3 +422,32 @@ func validateInvalidPostIndex(ctx context.Context, numInvalidProofsPostIndex.Inc() return types.EmptyNodeID, errors.New("invalid post index malfeasance proof - POST is valid") } + +func validateInvalidPrevATX( + ctx context.Context, + db sql.Executor, + edVerifier SigVerifier, + proof *wire.InvalidPrevATXProof, +) (types.NodeID, error) { + atx1 := proof.Atx1 + if !edVerifier.Verify(signing.ATX, atx1.SmesherID, atx1.SignedBytes(), atx1.Signature) { + return types.EmptyNodeID, errors.New("atx1: invalid signature") + } + + atx2 := proof.Atx2 + if !edVerifier.Verify(signing.ATX, atx2.SmesherID, atx2.SignedBytes(), atx2.Signature) { + return types.EmptyNodeID, errors.New("atx2: invalid signature") + } + idATX1 := awire.ActivationTxFromWireV1(&proof.Atx1) + idATX2 := awire.ActivationTxFromWireV1(&proof.Atx2) + + if idATX1.ID() == idATX2.ID() { + numInvalidProofsPrevATX.Inc() + return types.EmptyNodeID, errors.New("invalid old prev ATX malfeasance proof: ATX IDs are the same") + } + if atx1.PrevATXID != atx2.PrevATXID { + numInvalidProofsPrevATX.Inc() + return types.EmptyNodeID, errors.New("invalid old prev ATX malfeasance proof: prev ATX IDs are different") + } + return atx1.SmesherID, nil +} diff --git a/malfeasance/handler_test.go b/malfeasance/handler_test.go index 636a3128..2cb8898d 100644 --- a/malfeasance/handler_test.go +++ b/malfeasance/handler_test.go @@ -1068,7 +1068,7 @@ func TestHandler_HandleSyncedMalfeasanceProof_wrongHash(t *testing.T) { require.True(t, malicious) } -func TestHandler_HandleMalfeasanceProof_InvalidPostIndex(t *testing.T) { +func TestHandler_HandleSyncedMalfeasanceProof_InvalidPostIndex(t *testing.T) { sig, err := signing.NewEdSigner() require.NoError(t, err) nodeIdH32 := types.Hash32(sig.NodeID()) @@ -1090,8 +1090,9 @@ func TestHandler_HandleMalfeasanceProof_InvalidPostIndex(t *testing.T) { t.Run("valid malfeasance proof", func(t *testing.T) { db := sql.InMemory() lg := logtest.New(t) - trt := malfeasance.NewMocktortoise(gomock.NewController(t)) - postVerifier := malfeasance.NewMockpostVerifier(gomock.NewController(t)) + ctrl := gomock.NewController(t) + trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -1128,8 +1129,9 @@ func TestHandler_HandleMalfeasanceProof_InvalidPostIndex(t *testing.T) { t.Run("invalid malfeasance proof (POST valid)", func(t *testing.T) { db := sql.InMemory() lg := logtest.New(t) - trt := malfeasance.NewMocktortoise(gomock.NewController(t)) - postVerifier := malfeasance.NewMockpostVerifier(gomock.NewController(t)) + ctrl := gomock.NewController(t) + trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -1164,8 +1166,9 @@ func TestHandler_HandleMalfeasanceProof_InvalidPostIndex(t *testing.T) { t.Run("invalid malfeasance proof (ATX signature invalid)", func(t *testing.T) { db := sql.InMemory() lg := logtest.New(t) - trt := malfeasance.NewMocktortoise(gomock.NewController(t)) - postVerifier := malfeasance.NewMockpostVerifier(gomock.NewController(t)) + ctrl := gomock.NewController(t) + trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) h := malfeasance.NewHandler( datastore.NewCachedDB(db, lg), @@ -1200,3 +1203,215 @@ func TestHandler_HandleMalfeasanceProof_InvalidPostIndex(t *testing.T) { require.False(t, malicious) }) } + +func TestHandler_HandleSyncedMalfeasanceProof_InvalidPrevATX(t *testing.T) { + sig, err := signing.NewEdSigner() + require.NoError(t, err) + nodeIdH32 := types.Hash32(sig.NodeID()) + + prevATX := *types.NewActivationTx( + types.NIPostChallenge{ + PublishEpoch: types.EpochID(1), + CommitmentATX: &types.ATXID{1, 2, 3}, + }, + types.Address{}, + nil, + 1, + nil, + ) + require.NoError(t, activation.SignAndFinalizeAtx(sig, &prevATX)) + + atx1 := *types.NewActivationTx( + types.NIPostChallenge{ + PublishEpoch: types.EpochID(2), + PrevATXID: prevATX.ID(), + }, + types.Address{}, + nil, + 1, + nil, + ) + require.NoError(t, activation.SignAndFinalizeAtx(sig, &atx1)) + + atx2 := *types.NewActivationTx( + types.NIPostChallenge{ + PublishEpoch: types.EpochID(3), + PrevATXID: prevATX.ID(), + }, + types.Address{}, + nil, + 1, + nil, + ) + require.NoError(t, activation.SignAndFinalizeAtx(sig, &atx2)) + + t.Run("valid malfeasance proof", func(t *testing.T) { + db := sql.InMemory() + lg := logtest.New(t) + ctrl := gomock.NewController(t) + trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) + + h := malfeasance.NewHandler( + datastore.NewCachedDB(db, lg), + lg, + "self", + []types.NodeID{types.RandomNodeID()}, + signing.NewEdVerifier(), + trt, + postVerifier, + ) + + proof := wire.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: *awire.ActivationTxToWireV1(&atx1), + Atx2: *awire.ActivationTxToWireV1(&atx2), + }, + }, + } + + trt.EXPECT().OnMalfeasance(sig.NodeID()) + err := h.HandleSyncedMalfeasanceProof(context.Background(), nodeIdH32, "peer", codec.MustEncode(&proof)) + require.NoError(t, err) + + malicious, err := identities.IsMalicious(db, sig.NodeID()) + require.NoError(t, err) + require.True(t, malicious) + }) + + t.Run("invalid malfeasance proof (same ATX)", func(t *testing.T) { + db := sql.InMemory() + lg := logtest.New(t) + ctrl := gomock.NewController(t) + trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) + + h := malfeasance.NewHandler( + datastore.NewCachedDB(db, lg), + lg, + "self", + []types.NodeID{types.RandomNodeID()}, + signing.NewEdVerifier(), + trt, + postVerifier, + ) + + proof := wire.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: *awire.ActivationTxToWireV1(&atx1), + Atx2: *awire.ActivationTxToWireV1(&atx1), + }, + }, + } + + err := h.HandleSyncedMalfeasanceProof(context.Background(), nodeIdH32, "peer", codec.MustEncode(&proof)) + require.ErrorContains(t, err, "ATX IDs are the same") + + malicious, err := identities.IsMalicious(db, sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) + + t.Run("invalid malfeasance proof (prev ATXs differ)", func(t *testing.T) { + db := sql.InMemory() + lg := logtest.New(t) + ctrl := gomock.NewController(t) + trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) + + h := malfeasance.NewHandler( + datastore.NewCachedDB(db, lg), + lg, + "self", + []types.NodeID{types.RandomNodeID()}, + signing.NewEdVerifier(), + trt, + postVerifier, + ) + + atx3 := *types.NewActivationTx( + types.NIPostChallenge{ + PublishEpoch: types.EpochID(3), + PrevATXID: atx1.ID(), + }, + types.Address{}, + nil, + 1, + nil, + ) + require.NoError(t, activation.SignAndFinalizeAtx(sig, &atx3)) + + proof := wire.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: *awire.ActivationTxToWireV1(&atx1), + Atx2: *awire.ActivationTxToWireV1(&atx3), + }, + }, + } + + err := h.HandleSyncedMalfeasanceProof(context.Background(), nodeIdH32, "peer", codec.MustEncode(&proof)) + require.ErrorContains(t, err, "prev ATX IDs are different") + + malicious, err := identities.IsMalicious(db, sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) + + t.Run("invalid malfeasance proof (ATX signature invalid)", func(t *testing.T) { + db := sql.InMemory() + lg := logtest.New(t) + ctrl := gomock.NewController(t) + trt := malfeasance.NewMocktortoise(ctrl) + postVerifier := malfeasance.NewMockpostVerifier(ctrl) + + h := malfeasance.NewHandler( + datastore.NewCachedDB(db, lg), + lg, + "self", + []types.NodeID{types.RandomNodeID()}, + signing.NewEdVerifier(), + trt, + postVerifier, + ) + + atx3 := *types.NewActivationTx( + types.NIPostChallenge{ + PublishEpoch: types.EpochID(3), + PrevATXID: atx1.ID(), + }, + types.Address{}, + nil, + 1, + nil, + ) + require.NoError(t, activation.SignAndFinalizeAtx(sig, &atx3)) + atx3.PrevATXID = prevATX.ID() // invalidate signature by changing content + + proof := wire.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: *awire.ActivationTxToWireV1(&atx1), + Atx2: *awire.ActivationTxToWireV1(&atx3), + }, + }, + } + + err := h.HandleSyncedMalfeasanceProof(context.Background(), nodeIdH32, "peer", codec.MustEncode(&proof)) + require.ErrorContains(t, err, "invalid signature") + + malicious, err := identities.IsMalicious(db, sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) +} diff --git a/malfeasance/metrics.go b/malfeasance/metrics.go index 8e2eccfa..7592c33c 100644 --- a/malfeasance/metrics.go +++ b/malfeasance/metrics.go @@ -13,6 +13,7 @@ const ( multiBallots = "ballot" hareEquivocate = "hare_eq" invalidPostIndex = "invalid_post_index" + invalidPrevATX = "invalid_prev_atx" ) var ( @@ -29,6 +30,7 @@ var ( numProofsBallot = numProofs.WithLabelValues(multiBallots) numProofsHare = numProofs.WithLabelValues(hareEquivocate) numProofsPostIndex = numProofs.WithLabelValues(invalidPostIndex) + numProofsPrevATX = numProofs.WithLabelValues(invalidPrevATX) numInvalidProofs = metrics.NewCounter( "num_invalid_proofs", @@ -39,9 +41,10 @@ var ( }, ) + numMalformed = numInvalidProofs.WithLabelValues("mal") numInvalidProofsATX = numInvalidProofs.WithLabelValues(multiATXs) numInvalidProofsBallot = numInvalidProofs.WithLabelValues(multiBallots) numInvalidProofsHare = numInvalidProofs.WithLabelValues(hareEquivocate) numInvalidProofsPostIndex = numInvalidProofs.WithLabelValues(invalidPostIndex) - numMalformed = numInvalidProofs.WithLabelValues("mal") + numInvalidProofsPrevATX = numInvalidProofs.WithLabelValues(invalidPrevATX) ) diff --git a/malfeasance/wire/malfeasance.go b/malfeasance/wire/malfeasance.go index c50480cd..75ddb86a 100644 --- a/malfeasance/wire/malfeasance.go +++ b/malfeasance/wire/malfeasance.go @@ -15,13 +15,14 @@ import ( "github.com/spacemeshos/go-spacemesh/log" ) -//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata,InvalidPostIndexProof +//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata,InvalidPostIndexProof,InvalidPrevATXProof const ( MultipleATXs byte = iota + 1 MultipleBallots HareEquivocation InvalidPostIndex + InvalidPrevATX ) type MalfeasanceProof struct { @@ -73,9 +74,20 @@ func (mp *MalfeasanceProof) MarshalLogObject(encoder log.ObjectEncoder) error { if ok { atx := wire.ActivationTxFromWireV1(&p.Atx) encoder.AddString("atx_id", atx.ID().String()) - encoder.AddString("smesher", p.Atx.SmesherID.String()) + encoder.AddString("smesher", atx.SmesherID.String()) encoder.AddUint32("invalid index", p.InvalidIdx) } + case InvalidPrevATX: + encoder.AddString("type", "invalid prev atx") + p, ok := mp.Proof.Data.(*InvalidPrevATXProof) + if ok { + atx1 := wire.ActivationTxFromWireV1(&p.Atx1) + atx2 := wire.ActivationTxFromWireV1(&p.Atx2) + encoder.AddString("atx1_id", atx1.ID().String()) + encoder.AddString("atx2_id", atx2.ID().String()) + encoder.AddString("smesher", atx1.SmesherID.String()) + encoder.AddString("prev_atx", atx1.PrevATXID.String()) + } default: encoder.AddString("type", "unknown") } @@ -153,6 +165,14 @@ func (e *Proof) DecodeScale(dec *scale.Decoder) (int, error) { } e.Data = &proof total += n + case InvalidPrevATX: + var proof InvalidPrevATXProof + n, err := proof.DecodeScale(dec) + if err != nil { + return total, err + } + e.Data = &proof + total += n default: return total, errors.New("unknown malfeasance proof type") } @@ -292,6 +312,13 @@ func (m *HareProofMsg) SignedBytes() []byte { return m.InnerMsg.ToBytes() } +// InvalidPrevAtxProof is a proof that a smesher published an ATX with an old previous ATX ID. +// The proof contains two ATXs that reference the same previous ATX. +type InvalidPrevATXProof struct { + Atx1 wire.ActivationTxV1 + Atx2 wire.ActivationTxV1 +} + func MalfeasanceInfo(smesher types.NodeID, mp *MalfeasanceProof) string { var b strings.Builder b.WriteString(fmt.Sprintf("generate layer: %v\n", mp.Layer)) @@ -368,6 +395,19 @@ func MalfeasanceInfo(smesher types.NodeID, mp *MalfeasanceProof) string { p.Atx.Publish, )) } + case InvalidPrevATX: + p, ok := mp.Proof.Data.(*InvalidPrevATXProof) + if ok { + atx1 := wire.ActivationTxFromWireV1(&p.Atx1) + atx2 := wire.ActivationTxFromWireV1(&p.Atx2) + b.WriteString( + fmt.Sprintf( + "cause: smesher published ATX %s with invalid previous ATX %s in epoch %d\n", + atx1.ID().ShortString(), + atx2.ID().ShortString(), + atx1.PublishEpoch, + )) + } } return b.String() } diff --git a/malfeasance/wire/malfeasance_scale.go b/malfeasance/wire/malfeasance_scale.go index 625292f0..3ec88a1a 100644 --- a/malfeasance/wire/malfeasance_scale.go +++ b/malfeasance/wire/malfeasance_scale.go @@ -386,3 +386,39 @@ func (t *InvalidPostIndexProof) DecodeScale(dec *scale.Decoder) (total int, err } return total, nil } + +func (t *InvalidPrevATXProof) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := t.Atx1.EncodeScale(enc) + if err != nil { + return total, err + } + total += n + } + { + n, err := t.Atx2.EncodeScale(enc) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *InvalidPrevATXProof) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := t.Atx1.DecodeScale(dec) + if err != nil { + return total, err + } + total += n + } + { + n, err := t.Atx2.DecodeScale(dec) + if err != nil { + return total, err + } + total += n + } + return total, nil +} diff --git a/malfeasance/wire/malfeasance_test.go b/malfeasance/wire/malfeasance_test.go index 70cc9426..f5ef8a16 100644 --- a/malfeasance/wire/malfeasance_test.go +++ b/malfeasance/wire/malfeasance_test.go @@ -8,9 +8,12 @@ import ( "github.com/spacemeshos/go-scale/tester" "github.com/stretchr/testify/require" + "github.com/spacemeshos/go-spacemesh/activation" + awire "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/malfeasance/wire" + "github.com/spacemeshos/go-spacemesh/signing" ) func TestMain(m *testing.M) { @@ -182,6 +185,87 @@ func Test_HareMetadata_Equivocation(t *testing.T) { require.False(t, hm1.Equivocation(&hm2)) } +func TestCodec_InvalidPostIndex(t *testing.T) { + lid := types.LayerID(11) + atx := types.NewActivationTx( + types.NIPostChallenge{PublishEpoch: lid.GetEpoch()}, + types.Address{1, 2, 3}, + nil, 10, nil, + ) + + proof := &wire.MalfeasanceProof{ + Layer: lid, + Proof: wire.Proof{ + Type: wire.InvalidPostIndex, + Data: &wire.InvalidPostIndexProof{ + Atx: *awire.ActivationTxToWireV1(atx), + InvalidIdx: 5, + }, + }, + } + encoded, err := codec.Encode(proof) + require.NoError(t, err) + + var decoded wire.MalfeasanceProof + require.NoError(t, codec.Decode(encoded, &decoded)) + require.Equal(t, *proof, decoded) +} + +func TestCodec_OldPrevATX(t *testing.T) { + lid := types.LayerID(45) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + prev := types.NewActivationTx( + types.NIPostChallenge{ + PublishEpoch: lid.GetEpoch() - 2, + }, + types.Address{1, 2, 3}, + nil, 10, nil, + ) + require.NoError(t, activation.SignAndFinalizeAtx(sig, prev)) + + atx1 := types.NewActivationTx( + types.NIPostChallenge{ + PublishEpoch: lid.GetEpoch() - 1, + PrevATXID: prev.ID(), + }, + types.Address{1, 2, 3}, + nil, 10, nil, + ) + require.NoError(t, activation.SignAndFinalizeAtx(sig, atx1)) + + atx2 := types.NewActivationTx( + types.NIPostChallenge{ + PublishEpoch: lid.GetEpoch(), + PrevATXID: prev.ID(), + }, + types.Address{1, 2, 3}, + nil, 10, nil, + ) + require.NoError(t, activation.SignAndFinalizeAtx(sig, atx2)) + + proof := &wire.MalfeasanceProof{ + Layer: lid, + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: *awire.ActivationTxToWireV1(atx1), + Atx2: *awire.ActivationTxToWireV1(atx2), + }, + }, + } + encoded, err := codec.Encode(proof) + require.NoError(t, err) + + var decoded wire.MalfeasanceProof + require.NoError(t, codec.Decode(encoded, &decoded)) + // require.NoError(t, decoded.Proof.Data.(*wire.InvalidPrevATXProof).Atx1.Initialize()) + // require.NoError(t, decoded.Proof.Data.(*wire.InvalidPrevATXProof).Atx2.Initialize()) + require.Equal(t, *proof, decoded) +} + func FuzzProofConsistency(f *testing.F) { tester.FuzzConsistency[wire.Proof](f, func(p *wire.Proof, c fuzz.Continue) { switch c.Intn(3) { @@ -200,6 +284,16 @@ func FuzzProofConsistency(f *testing.F) { data := wire.HareProof{} c.Fuzz(&data) p.Data = &data + case 3: + p.Type = wire.InvalidPostIndex + data := wire.InvalidPostIndexProof{} + c.Fuzz(&data) + p.Data = &data + case 4: + p.Type = wire.InvalidPrevATX + data := wire.InvalidPrevATXProof{} + c.Fuzz(&data) + p.Data = &data } }) } diff --git a/sql/identities/identities.go b/sql/identities/identities.go index f8d523d0..48ef4b36 100644 --- a/sql/identities/identities.go +++ b/sql/identities/identities.go @@ -107,7 +107,7 @@ func IterateMalicious( return callbackErr } -// GetMalicious retrives malicious node IDs from the database. +// GetMalicious retrieves malicious node IDs from the database. func GetMalicious(db sql.Executor) (nids []types.NodeID, err error) { if err = IterateMalicious(db, func(total int, nid types.NodeID) error { if nids == nil { From ba766efa76b50e596381df9223a291ca011c8cc2 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 29 Apr 2024 21:03:26 +0000 Subject: [PATCH 03/25] Update ATX handler --- activation/handler.go | 207 +++++++++++++++++++++++++++++++----------- sql/atxs/atxs.go | 27 +++++- 2 files changed, 180 insertions(+), 54 deletions(-) diff --git a/activation/handler.go b/activation/handler.go index b15bb1c4..dac87361 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -408,6 +408,150 @@ func (h *Handler) cacheAtx(ctx context.Context, atx *types.ActivationTxHeader, n return nil } +// checkDoublePublish verifies if a node has already published an ATX in the same epoch. +func (h *Handler) checkDoublePublish( + ctx context.Context, + tx sql.Executor, + atx *types.VerifiedActivationTx, +) (*mwire.MalfeasanceProof, error) { + prev, err := atxs.GetByEpochAndNodeID(tx, atx.PublishEpoch, atx.SmesherID) + if err != nil && !errors.Is(err, sql.ErrNotFound) { + return nil, err + } + + // do ID check to be absolutely sure. + if prev == nil || prev.ID() == atx.ID() { + return nil, nil + } + if _, ok := h.signers[atx.SmesherID]; ok { + // if we land here we tried to publish 2 ATXs in the same epoch + // don't punish ourselves but fail validation and thereby the handling of the incoming ATX + return nil, fmt.Errorf("%s already published an ATX in epoch %d", atx.SmesherID.ShortString(), atx.PublishEpoch) + } + + var atxProof mwire.AtxProof + for i, a := range []*types.VerifiedActivationTx{prev, atx} { + atxProof.Messages[i] = mwire.AtxProofMsg{ + InnerMsg: types.ATXMetadata{ + PublishEpoch: a.PublishEpoch, + MsgHash: wire.ActivationTxToWireV1(a.ActivationTx).HashInnerBytes(), + }, + SmesherID: a.SmesherID, + Signature: a.Signature, + } + } + proof := &mwire.MalfeasanceProof{ + Layer: atx.PublishEpoch.FirstLayer(), + Proof: mwire.Proof{ + Type: mwire.MultipleATXs, + Data: &atxProof, + }, + } + encoded, err := codec.Encode(proof) + if err != nil { + h.log.With().Panic("failed to encode malfeasance proof", log.Err(err)) + } + if err := identities.SetMalicious(tx, atx.SmesherID, encoded, time.Now()); err != nil { + return nil, fmt.Errorf("add malfeasance proof: %w", err) + } + + h.log.WithContext(ctx).With().Warning("smesher produced more than one atx in the same epoch", + log.Stringer("smesher", atx.SmesherID), + log.Object("prev", prev), + log.Object("curr", atx), + ) + + return proof, nil +} + +// checkWrongPrevAtx verifies if the previous ATX referenced in the ATX is correct. +func (h *Handler) checkWrongPrevAtx( + ctx context.Context, + tx sql.Executor, + atx *types.VerifiedActivationTx, +) (*mwire.MalfeasanceProof, error) { + prevID, err := atxs.PrevIDByNodeID(tx, atx.SmesherID, atx.PublishEpoch) + if err != nil && !errors.Is(err, sql.ErrNotFound) { + return nil, fmt.Errorf("get last atx by node id: %w", err) + } + if prevID == atx.PrevATXID { + return nil, nil + } + + if _, ok := h.signers[atx.SmesherID]; ok { + // if we land here we tried to publish an ATX with a wrong prevATX + h.log.WithContext(ctx).With().Warning( + "Node produced an ATX with a wrong prevATX. This can happened when the node wasn't synced when "+ + "registering at PoET", + log.Stringer("smesher", atx.SmesherID), + log.ShortStringer("expected", prevID), + log.ShortStringer("actual", atx.PrevATXID), + ) + return nil, fmt.Errorf("%s referenced incorrect previous ATX", atx.SmesherID.ShortString()) + } + + // check if atx.PrevATXID is actually the last published ATX by the same node + prev, err := atxs.Get(tx, atx.PrevATXID) + if err != nil { + return nil, fmt.Errorf("get prev atx: %w", err) + } + + // if atx references a previous ATX that is not the last ATX by the same node, there must be at least one + // atx published between prevATX and the current epoch + var atx2 *types.VerifiedActivationTx + pubEpoch := h.clock.CurrentLayer().GetEpoch() + for pubEpoch > prev.PublishEpoch { + id, err := atxs.PrevIDByNodeID(h.cdb, atx.SmesherID, pubEpoch) + if err != nil { + return nil, fmt.Errorf("get prev atx id by node id: %w", err) + } + + atx2, err = atxs.Get(tx, id) + if err != nil { + return nil, fmt.Errorf("get prev atx: %w", err) + } + + if atx.ID() != atx2.ID() && atx.PrevATXID == atx2.PrevATXID { + // found an ATX that points to the same previous ATX + break + } + pubEpoch = atx2.PublishEpoch + } + + if atx2 == nil || atx2.PrevATXID != atx.PrevATXID { + // something went wrong, we couldn't find an ATX that points to the same previous ATX + // this should never happen since we are checking in other places that all ATXs from the same node + // form a chain + return nil, errors.New("failed double previous check: could not find an ATX with same previous ATX") + } + + proof := &mwire.MalfeasanceProof{ + Layer: atx.PublishEpoch.FirstLayer(), + Proof: mwire.Proof{ + Type: mwire.InvalidPrevATX, + Data: &mwire.InvalidPrevATXProof{ + Atx1: *wire.ActivationTxToWireV1(atx.ActivationTx), + Atx2: *wire.ActivationTxToWireV1(prev.ActivationTx), + }, + }, + } + + encoded, err := codec.Encode(proof) + if err != nil { + h.log.With().Panic("failed to encode malfeasance proof", log.Err(err)) + } + if err := identities.SetMalicious(tx, atx.SmesherID, encoded, time.Now()); err != nil { + return nil, fmt.Errorf("add malfeasance proof: %w", err) + } + + h.log.WithContext(ctx).With().Warning("smesher referenced the wrong previous in published ATX", + log.Stringer("smesher", atx.SmesherID), + log.ShortStringer("expected", prevID), + log.ShortStringer("actual", atx.PrevATXID), + ) + return proof, nil +} + // storeAtx stores an ATX and notifies subscribers of the ATXID. func (h *Handler) storeAtx(ctx context.Context, atx *types.VerifiedActivationTx) (*mwire.MalfeasanceProof, error) { var nonce *types.VRFPostIndex @@ -417,66 +561,23 @@ func (h *Handler) storeAtx(ctx context.Context, atx *types.VerifiedActivationTx) } var proof *mwire.MalfeasanceProof err = h.cdb.WithTx(ctx, func(tx *sql.Tx) error { + nonce, err = atxs.AddGettingNonce(tx, atx) + if err != nil && !errors.Is(err, sql.ErrObjectExists) { + return fmt.Errorf("add atx to db: %w", err) + } if malicious { - if err := atxs.Add(tx, atx); err != nil && !errors.Is(err, sql.ErrObjectExists) { - return fmt.Errorf("add atx to db: %w", err) - } return nil } - - prev, err := atxs.GetByEpochAndNodeID(tx, atx.PublishEpoch, atx.SmesherID) - if err != nil && !errors.Is(err, sql.ErrNotFound) { + proof, err = h.checkDoublePublish(ctx, tx, atx) + if err != nil { return err } - - // do ID check to be absolutely sure. - if prev != nil && prev.ID() != atx.ID() { - if _, ok := h.signers[atx.SmesherID]; ok { - // if we land here we tried to publish 2 ATXs in the same epoch - // don't punish ourselves but fail validation and thereby the handling of the incoming ATX - return fmt.Errorf("%s already published an ATX in epoch %d", atx.SmesherID.ShortString(), - atx.PublishEpoch, - ) - } - - var atxProof mwire.AtxProof - for i, a := range []*types.VerifiedActivationTx{prev, atx} { - atxProof.Messages[i] = mwire.AtxProofMsg{ - InnerMsg: types.ATXMetadata{ - PublishEpoch: a.PublishEpoch, - MsgHash: wire.ActivationTxToWireV1(a.ActivationTx).HashInnerBytes(), - }, - SmesherID: a.SmesherID, - Signature: a.Signature, - } - } - proof = &mwire.MalfeasanceProof{ - Layer: atx.PublishEpoch.FirstLayer(), - Proof: mwire.Proof{ - Type: mwire.MultipleATXs, - Data: &atxProof, - }, - } - encoded, err := codec.Encode(proof) - if err != nil { - h.log.With().Panic("failed to encode malfeasance proof", log.Err(err)) - } - if err := identities.SetMalicious(tx, atx.SmesherID, encoded, time.Now()); err != nil { - return fmt.Errorf("add malfeasance proof: %w", err) - } - - h.log.WithContext(ctx).With().Warning("smesher produced more than one atx in the same epoch", - log.Stringer("smesher", atx.SmesherID), - log.Object("prev", prev), - log.Object("curr", atx), - ) + if proof != nil { + return nil } - nonce, err = atxs.AddGettingNonce(tx, atx) - if err != nil && !errors.Is(err, sql.ErrObjectExists) { - return fmt.Errorf("add atx to db: %w", err) - } - return nil + proof, err = h.checkWrongPrevAtx(ctx, tx, atx) + return err }) if err != nil { return nil, fmt.Errorf("store atx: %w", err) diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index d4650193..7e3dd97d 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -205,6 +205,31 @@ func GetLastIDByNodeID(db sql.Executor, nodeID types.NodeID) (id types.ATXID, er return id, err } +// PrevIDByNodeID returns the previous ATX ID for a given node ID and public epoch. +// It returns the newest ATX ID that was published before the given public epoch. +func PrevIDByNodeID(db sql.Executor, nodeID types.NodeID, pubEpoch types.EpochID) (id types.ATXID, err error) { + enc := func(stmt *sql.Statement) { + stmt.BindBytes(1, nodeID.Bytes()) + stmt.BindInt64(2, int64(pubEpoch)) + } + dec := func(stmt *sql.Statement) bool { + stmt.ColumnBytes(0, id[:]) + return true + } + + if rows, err := db.Exec(` + select id from atxs + where pubkey = ?1 and epoch < ?2 + order by epoch desc + limit 1;`, enc, dec); err != nil { + return types.EmptyATXID, fmt.Errorf("exec nodeID %v, epoch %d: %w", nodeID, pubEpoch, err) + } else if rows == 0 { + return types.EmptyATXID, fmt.Errorf("exec nodeID %s, epoch %d: %w", nodeID, pubEpoch, sql.ErrNotFound) + } + + return id, err +} + // GetIDByEpochAndNodeID gets an ATX ID for a given epoch and node ID. func GetIDByEpochAndNodeID(db sql.Executor, epoch types.EpochID, nodeID types.NodeID) (id types.ATXID, err error) { enc := func(stmt *sql.Statement) { @@ -401,7 +426,7 @@ func AddGettingNonce(db sql.Executor, atx *types.VerifiedActivationTx) (*types.V if err == nil { err = add(db, atx, &nonce) if err != nil { - return nil, err + return &nonce, err } else { return &nonce, nil } From b90a95629321be75cbea838755ee589eccd969e0 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 29 Apr 2024 21:10:46 +0000 Subject: [PATCH 04/25] Add new migration --- activation/handler.go | 2 +- node/node.go | 1 + sql/migrations/state_0018_migration.go | 34 +++++++++++++++++++++ sql/migrations/state_0018_migration_test.go | 1 + 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 sql/migrations/state_0018_migration.go create mode 100644 sql/migrations/state_0018_migration_test.go diff --git a/activation/handler.go b/activation/handler.go index dac87361..c96660e0 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -576,7 +576,7 @@ func (h *Handler) storeAtx(ctx context.Context, atx *types.VerifiedActivationTx) return nil } - proof, err = h.checkWrongPrevAtx(ctx, tx, atx) + proof, err = h.checkWrongPrevAtx(ctx, tx, atx) // TODO(mafa): add tests for this return err }) if err != nil { diff --git a/node/node.go b/node/node.go index 356323c2..2efd4441 100644 --- a/node/node.go +++ b/node/node.go @@ -1849,6 +1849,7 @@ func (app *App) setupDBs(ctx context.Context, lg log.Log) error { sql.WithLogger(dbLog.Zap()), sql.WithMigrations(migrations), sql.WithMigration(sqlmigrations.New0017Migration(dbLog.Zap())), + sql.WithMigration(sqlmigrations.New0018Migration(dbLog.Zap())), sql.WithConnections(app.Config.DatabaseConnections), sql.WithLatencyMetering(app.Config.DatabaseLatencyMetering), sql.WithVacuumState(app.Config.DatabaseVacuumState), diff --git a/sql/migrations/state_0018_migration.go b/sql/migrations/state_0018_migration.go new file mode 100644 index 00000000..170aea0d --- /dev/null +++ b/sql/migrations/state_0018_migration.go @@ -0,0 +1,34 @@ +package migrations + +import ( + "go.uber.org/zap" + + "github.com/spacemeshos/go-spacemesh/sql" +) + +type migration0018 struct { + logger *zap.Logger +} + +func New0018Migration(log *zap.Logger) *migration0018 { + return &migration0018{ + logger: log, + } +} + +func (*migration0018) Name() string { + return "add malfeasance proofs for incorrect prevATXID in ATXs" +} + +func (*migration0018) Order() int { + return 18 +} + +func (*migration0018) Rollback() error { + // handled by the DB itself + return nil +} + +func (m *migration0018) Apply(db sql.Executor) error { + return nil +} diff --git a/sql/migrations/state_0018_migration_test.go b/sql/migrations/state_0018_migration_test.go new file mode 100644 index 00000000..a6ea3eef --- /dev/null +++ b/sql/migrations/state_0018_migration_test.go @@ -0,0 +1 @@ +package migrations From d66e1c9b3775e260d6e6e3d776e1ddabc5879e30 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 29 Apr 2024 21:21:55 +0000 Subject: [PATCH 05/25] Cleanup --- node/node.go | 47 ------------------------------------------- sql/atxs/atxs.go | 46 ------------------------------------------ sql/atxs/atxs_test.go | 29 -------------------------- 3 files changed, 122 deletions(-) diff --git a/node/node.go b/node/node.go index 2efd4441..affe2194 100644 --- a/node/node.go +++ b/node/node.go @@ -36,7 +36,6 @@ import ( "google.golang.org/grpc/keepalive" "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/api/grpcserver/v2alpha1" "github.com/spacemeshos/go-spacemesh/atxsdata" @@ -75,7 +74,6 @@ import ( "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/activesets" "github.com/spacemeshos/go-spacemesh/sql/atxs" - "github.com/spacemeshos/go-spacemesh/sql/builder" "github.com/spacemeshos/go-spacemesh/sql/layers" "github.com/spacemeshos/go-spacemesh/sql/localsql" dbmetrics "github.com/spacemeshos/go-spacemesh/sql/metrics" @@ -1934,9 +1932,6 @@ func (app *App) Start(ctx context.Context) error { }) } - // uncomment to verify ATXs signatures - // app.verifyDB(ctx) - // app blocks until it receives a signal to exit // this signal may come from the node or from sig-abort (ctrl-c) select { @@ -1947,48 +1942,6 @@ func (app *App) Start(ctx context.Context) error { } } -// verifyDB performs a verification of ATX signatures in the database. -// -//lint:ignore U1000 This function is currently unused but is left here for future use. -func (app *App) verifyDB(ctx context.Context) { - app.eg.Go(func() error { - app.log.Info("checking ATX signatures") - count := 0 - - // check ATX signatures - atxs.IterateAtxsOps(app.cachedDB, builder.Operations{}, func(atx *types.VerifiedActivationTx) bool { - select { - case <-ctx.Done(): - // stop on context cancellation - return false - default: - } - - // verify atx signature - // TODO: use atx handler to verify signature - if !app.edVerifier.Verify( - signing.ATX, - atx.SmesherID, wire.ActivationTxToWireV1(atx.ActivationTx).SignedBytes(), - atx.Signature, - ) { - app.log.With().Error("ATX signature verification failed", - log.Stringer("atx_id", atx.ID()), - log.Stringer("smesher", atx.SmesherID), - ) - } - - count++ - if count%1000 == 0 { - app.log.With().Info("verifying ATX signatures", log.Int("count", count)) - } - return true - }) - - app.log.With().Info("ATX signatures verified", log.Int("count", count)) - return nil - }) -} - func (app *App) startSynchronous(ctx context.Context) (err error) { // notify anyone who might be listening that the app has finished starting. // this can be used by, e.g., app tests. diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 7e3dd97d..04dbf1a1 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -253,52 +253,6 @@ func GetIDByEpochAndNodeID(db sql.Executor, epoch types.EpochID, nodeID types.No return id, err } -// IterateIDsByEpoch invokes the specified callback for each ATX ID in a given epoch. -// It stops if the callback returns an error. -func IterateIDsByEpoch( - db sql.Executor, - epoch types.EpochID, - callback func(total int, id types.ATXID) error, -) error { - if sql.IsCached(db) { - // If the slices are cached, let's not do more SELECTs - ids, err := GetIDsByEpoch(context.Background(), db, epoch) - if err != nil { - return err - } - for _, id := range ids { - if err := callback(len(ids), id); err != nil { - return err - } - } - return nil - } - - var callbackErr error - enc := func(stmt *sql.Statement) { - stmt.BindInt64(1, int64(epoch)) - } - dec := func(stmt *sql.Statement) bool { - var id types.ATXID - total := stmt.ColumnInt(0) - stmt.ColumnBytes(1, id[:]) - if callbackErr = callback(total, id); callbackErr != nil { - return false - } - return true - } - - // Get total count in the same select statement to avoid the need for transaction - if _, err := db.Exec( - "select (select count(*) from atxs where epoch = ?1) as total, id from atxs where epoch = ?1;", - enc, dec, - ); err != nil { - return fmt.Errorf("exec epoch %v: %w", epoch, err) - } - - return callbackErr -} - // GetIDsByEpoch gets ATX IDs for a given epoch. func GetIDsByEpoch(ctx context.Context, db sql.Executor, epoch types.EpochID) (ids []types.ATXID, err error) { cacheKey := sql.QueryCacheKey(CacheKindEpochATXs, epoch.String()) diff --git a/sql/atxs/atxs_test.go b/sql/atxs/atxs_test.go index a3f35cf9..53c41da4 100644 --- a/sql/atxs/atxs_test.go +++ b/sql/atxs/atxs_test.go @@ -498,35 +498,6 @@ func TestGetIDsByEpochCached(t *testing.T) { require.Equal(t, 16, db.QueryCount()) // not incremented after Add } -func TestForIDsByEpochEarlyStop(t *testing.T) { - db := sql.InMemory() - - e1 := types.EpochID(1) - m := make(map[types.ATXID]struct{}) - for i := 0; i < 4; i++ { - sig, err := signing.NewEdSigner() - require.NoError(t, err) - atx, err := newAtx(sig, withPublishEpoch(e1)) - require.NoError(t, err) - require.NoError(t, atxs.Add(db, atx)) - m[atx.ID()] = struct{}{} - } - - n := 0 - err := atxs.IterateIDsByEpoch(db, e1, func(total int, id types.ATXID) error { - require.Equal(t, 4, total) - delete(m, id) - n++ - if n >= 2 { - return errors.New("test error") - } - return nil - }) - require.ErrorContains(t, err, "test error") - require.Equal(t, 2, n) - require.Len(t, m, 2) -} - func TestVRFNonce(t *testing.T) { // Arrange db := sql.InMemory() From abcf305e20fb910eda6b3b3739fa90f49985d720 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 07:54:34 +0000 Subject: [PATCH 06/25] Add test for prevATX check --- activation/handler.go | 2 +- activation/handler_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/activation/handler.go b/activation/handler.go index c96660e0..a09f0eba 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -220,7 +220,7 @@ func (h *Handler) SyntacticallyValidateDeps( } commitmentATX = atx.CommitmentATX // checked to be non-nil in syntactic validation } else { - prev, err := atxs.Get(h.cdb, atx.PrevATXID) // TODO(mafa): add tests for this + prev, err := atxs.Get(h.cdb, atx.PrevATXID) if err != nil { return nil, nil, fmt.Errorf("prev atx for %s not found: %w", atx.PrevATXID, err) } diff --git a/activation/handler_test.go b/activation/handler_test.go index 70fcc007..17516a95 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -708,6 +708,32 @@ func TestHandler_SyntacticallyValidateAtx(t *testing.T) { err := atxHdlr.SyntacticallyValidate(context.Background(), atx) require.EqualError(t, err, "prev atx declared, but node id is included") }) + + t.Run("prevAtx by different NodeID", func(t *testing.T) { + t.Parallel() + + atxHdlr := newTestHandler(t, goldenATXID) + require.NoError(t, atxs.Add(atxHdlr.cdb, posAtx)) + require.NoError(t, atxs.Add(atxHdlr.cdb, prevAtx)) + + challenge := types.NIPostChallenge{ + Sequence: posAtx.Sequence + 1, + PrevATXID: posAtx.ID(), + PublishEpoch: currentLayer.GetEpoch(), + PositioningATX: posAtx.ID(), + CommitmentATX: nil, + } + nipost := types.NIPost{PostMetadata: &types.PostMetadata{}} + atx := newAtx(challenge, &nipost, 100, types.GenerateAddress([]byte("aaaa"))) + atx.NIPost = newNIPostWithPoet(t, poetRef).NIPost + require.NoError(t, SignAndFinalizeAtx(sig, atx)) + + atxHdlr.mclock.EXPECT().CurrentLayer().Return(currentLayer) + require.NoError(t, atxHdlr.SyntacticallyValidate(context.Background(), atx)) + _, proof, err := atxHdlr.SyntacticallyValidateDeps(context.Background(), atx) + require.ErrorContains(t, err, "prev atx smesher id mismatch") + require.Nil(t, proof) + }) } func TestHandler_ContextuallyValidateAtx(t *testing.T) { From 20baf69766bbb837aefd478b0a84ea93752ab555 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 08:16:33 +0000 Subject: [PATCH 07/25] Add tests --- activation/handler.go | 6 +- activation/handler_test.go | 213 +++++++++++++++++++++++++++++++++++-- 2 files changed, 209 insertions(+), 10 deletions(-) diff --git a/activation/handler.go b/activation/handler.go index a09f0eba..aef6a9c9 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -491,7 +491,7 @@ func (h *Handler) checkWrongPrevAtx( } // check if atx.PrevATXID is actually the last published ATX by the same node - prev, err := atxs.Get(tx, atx.PrevATXID) + prev, err := atxs.Get(tx, prevID) if err != nil { return nil, fmt.Errorf("get prev atx: %w", err) } @@ -501,7 +501,7 @@ func (h *Handler) checkWrongPrevAtx( var atx2 *types.VerifiedActivationTx pubEpoch := h.clock.CurrentLayer().GetEpoch() for pubEpoch > prev.PublishEpoch { - id, err := atxs.PrevIDByNodeID(h.cdb, atx.SmesherID, pubEpoch) + id, err := atxs.PrevIDByNodeID(tx, atx.SmesherID, pubEpoch) if err != nil { return nil, fmt.Errorf("get prev atx id by node id: %w", err) } @@ -576,7 +576,7 @@ func (h *Handler) storeAtx(ctx context.Context, atx *types.VerifiedActivationTx) return nil } - proof, err = h.checkWrongPrevAtx(ctx, tx, atx) // TODO(mafa): add tests for this + proof, err = h.checkWrongPrevAtx(ctx, tx, atx) return err }) if err != nil { diff --git a/activation/handler_test.go b/activation/handler_test.go index 17516a95..7ee949c1 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -957,7 +957,7 @@ func TestHandler_ProcessAtx(t *testing.T) { types.EmptyATXID, types.EmptyATXID, nil, - types.LayerID(layersPerEpoch).GetEpoch(), + types.EpochID(2), 0, 100, coinbase, @@ -975,6 +975,39 @@ func TestHandler_ProcessAtx(t *testing.T) { proof, err = atxHdlr.processVerifiedATX(context.Background(), atx1) require.NoError(t, err) require.Nil(t, proof) +} + +func TestHandler_ProcessAtx_SamePubEpoch(t *testing.T) { + // Arrange + goldenATXID := types.ATXID{2, 3, 4} + atxHdlr := newTestHandler(t, goldenATXID) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + coinbase := types.GenerateAddress([]byte("aaaa")) + + // Act & Assert + atx1 := newActivationTx( + t, + sig, + 0, + types.EmptyATXID, + types.EmptyATXID, + nil, + types.EpochID(2), + 0, + 100, + coinbase, + 100, + &types.NIPost{PostMetadata: &types.PostMetadata{}}, + withVrfNonce(7), + ) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) + atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + proof, err := atxHdlr.processVerifiedATX(context.Background(), atx1) + require.NoError(t, err) + require.Nil(t, proof) // another atx for the same epoch is considered malicious atx2 := newActivationTx( @@ -984,7 +1017,7 @@ func TestHandler_ProcessAtx(t *testing.T) { atx1.ID(), atx1.ID(), nil, - types.LayerID(layersPerEpoch+1).GetEpoch(), + types.EpochID(2), 0, 100, coinbase, @@ -1012,7 +1045,7 @@ func TestHandler_ProcessAtx(t *testing.T) { require.Equal(t, sig.NodeID(), nodeID) } -func TestHandler_ProcessAtx_OwnNotMalicious(t *testing.T) { +func TestHandler_ProcessAtx_SamePubEpoch_NoSelfIncrimination(t *testing.T) { // Arrange goldenATXID := types.ATXID{2, 3, 4} atxHdlr := newTestHandler(t, goldenATXID) @@ -1031,7 +1064,7 @@ func TestHandler_ProcessAtx_OwnNotMalicious(t *testing.T) { types.EmptyATXID, types.EmptyATXID, nil, - types.LayerID(layersPerEpoch).GetEpoch(), + types.EpochID(2), 0, 100, coinbase, @@ -1058,7 +1091,7 @@ func TestHandler_ProcessAtx_OwnNotMalicious(t *testing.T) { atx1.ID(), atx1.ID(), nil, - types.LayerID(layersPerEpoch+1).GetEpoch(), + types.EpochID(2), 0, 100, coinbase, @@ -1070,7 +1103,173 @@ func TestHandler_ProcessAtx_OwnNotMalicious(t *testing.T) { err, fmt.Sprintf("%s already published an ATX", sig.NodeID().ShortString()), ) + require.Nil(t, proof) // no proof against oneself +} + +func TestHandler_ProcessAtx_SamePrevATX(t *testing.T) { + // Arrange + goldenATXID := types.ATXID{2, 3, 4} + atxHdlr := newTestHandler(t, goldenATXID) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + coinbase := types.GenerateAddress([]byte("aaaa")) + + // Act & Assert + prevATX := newActivationTx( + t, + sig, + 0, + types.EmptyATXID, + types.EmptyATXID, + nil, + types.EpochID(2), + 0, + 100, + coinbase, + 100, + &types.NIPost{PostMetadata: &types.PostMetadata{}}, + withVrfNonce(7), + ) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) + atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + proof, err := atxHdlr.processVerifiedATX(context.Background(), prevATX) + require.NoError(t, err) + require.Nil(t, proof) + + // valid first non-initial ATX + atx1 := newActivationTx( + t, + sig, + 1, + prevATX.ID(), + prevATX.ID(), + nil, + types.EpochID(3), + 0, + 100, + coinbase, + 100, + &types.NIPost{PostMetadata: &types.PostMetadata{}}, + ) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) + atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + proof, err = atxHdlr.processVerifiedATX(context.Background(), atx1) + require.NoError(t, err) require.Nil(t, proof) + + // second non-initial ATX references prevATX as prevATX + atx2 := newActivationTx( + t, + sig, + 2, + prevATX.ID(), + atx1.ID(), + nil, + types.EpochID(4), + 0, + 100, + coinbase, + 100, + &types.NIPost{PostMetadata: &types.PostMetadata{}}, + ) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) + atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + atxHdlr.mtortoise.EXPECT().OnMalfeasance(gomock.Any()) + atxHdlr.mclock.EXPECT().CurrentLayer().Return(types.EpochID(4).FirstLayer()) + proof, err = atxHdlr.processVerifiedATX(context.Background(), atx2) + require.NoError(t, err) + proof.SetReceived(time.Time{}) + nodeID, err := malfeasance.Validate( + context.Background(), + atxHdlr.log, + atxHdlr.cdb, + atxHdlr.edVerifier, + nil, + &mwire.MalfeasanceGossip{ + MalfeasanceProof: *proof, + }, + ) + require.NoError(t, err) + require.Equal(t, sig.NodeID(), nodeID) +} + +func TestHandler_ProcessAtx_SamePrevATX_NoSelfIncrimination(t *testing.T) { + // Arrange + goldenATXID := types.ATXID{2, 3, 4} + atxHdlr := newTestHandler(t, goldenATXID) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + atxHdlr.Register(sig) + + coinbase := types.GenerateAddress([]byte("aaaa")) + + // Act & Assert + prevATX := newActivationTx( + t, + sig, + 0, + types.EmptyATXID, + types.EmptyATXID, + nil, + types.EpochID(2), + 0, + 100, + coinbase, + 100, + &types.NIPost{PostMetadata: &types.PostMetadata{}}, + withVrfNonce(7), + ) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) + atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + proof, err := atxHdlr.processVerifiedATX(context.Background(), prevATX) + require.NoError(t, err) + require.Nil(t, proof) + + // valid first non-initial ATX + atx1 := newActivationTx( + t, + sig, + 1, + prevATX.ID(), + prevATX.ID(), + nil, + types.EpochID(3), + 0, + 100, + coinbase, + 100, + &types.NIPost{PostMetadata: &types.PostMetadata{}}, + ) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) + atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + proof, err = atxHdlr.processVerifiedATX(context.Background(), atx1) + require.NoError(t, err) + require.Nil(t, proof) + + // second non-initial ATX references prevATX as prevATX + atx2 := newActivationTx( + t, + sig, + 2, + prevATX.ID(), + atx1.ID(), + nil, + types.EpochID(4), + 0, + 100, + coinbase, + 100, + &types.NIPost{PostMetadata: &types.PostMetadata{}}, + ) + proof, err = atxHdlr.processVerifiedATX(context.Background(), atx2) + require.ErrorContains(t, + err, + fmt.Sprintf("%s referenced incorrect previous ATX", sig.NodeID().ShortString()), + ) + require.Nil(t, proof) // no proof against oneself } func testHandler_PostMalfeasanceProofs(t *testing.T, synced bool) { @@ -1191,7 +1390,7 @@ func TestHandler_ProcessAtxStoresNewVRFNonce(t *testing.T) { types.EmptyATXID, types.EmptyATXID, nil, - types.LayerID(layersPerEpoch).GetEpoch(), + types.EpochID(2), 0, 100, coinbase, @@ -1218,7 +1417,7 @@ func TestHandler_ProcessAtxStoresNewVRFNonce(t *testing.T) { atx1.ID(), atx1.ID(), nil, - types.LayerID(2*layersPerEpoch).GetEpoch(), + types.EpochID(3), 0, 100, coinbase, From e2eb0abc769313e9d1bd976cc6e0f1f1a6612ef3 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 08:44:56 +0000 Subject: [PATCH 08/25] Migration WiP --- sql/migrations/state_0018_migration.go | 102 +++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/sql/migrations/state_0018_migration.go b/sql/migrations/state_0018_migration.go index 170aea0d..2e9b2afb 100644 --- a/sql/migrations/state_0018_migration.go +++ b/sql/migrations/state_0018_migration.go @@ -1,8 +1,11 @@ package migrations import ( + "fmt" + "go.uber.org/zap" + "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/sql" ) @@ -30,5 +33,104 @@ func (*migration0018) Rollback() error { } func (m *migration0018) Apply(db sql.Executor) error { + found, err := m.findMaliciousATXs(db) + if err != nil { + return fmt.Errorf("error finding malfeasant ATXs: %w", err) + } + + for _, collision := range found { + if m.isIdentityMalicious(collision.id) { + continue + } + + // if err := m.createMalfeasanceProof(db, collision); err != nil { + // return fmt.Errorf("error creating malfeasance proof: %w", err) + // } + } + return nil } + +type prevATXCollision struct { + id types.NodeID + atx1 []byte + atx2 []byte +} + +func (m *migration0018) findMaliciousATXs(db sql.Executor) ([]prevATXCollision, error) { + var result []prevATXCollision + + dec := func(stmt *sql.Statement) bool { + var id types.NodeID + stmt.ColumnBytes(0, id[:]) + + var id1, id2 types.ATXID + stmt.ColumnBytes(1, id1[:]) + stmt.ColumnBytes(2, id2[:]) + + m.logger.Debug("found ATXs with same prevATX", + zap.String("node_id", id.String()), + zap.String("atx1", id1.ShortString()), + zap.String("atx2", id2.ShortString()), + ) + + data1 := make([]byte, stmt.ColumnLen(3)) + data2 := make([]byte, stmt.ColumnLen(4)) + stmt.ColumnBytes(3, data1) + stmt.ColumnBytes(4, data2) + + result = append(result, prevATXCollision{ + id: id, + atx1: data1, + atx2: data2, + }) + return true + } + if _, err := db.Exec(` + SELECT t1.pubkey, t1.id, t2.id, t3.atx, t4.atx + FROM atxs t1 + JOIN atxs t2 ON t1.pubkey = t2.pubkey AND t1.prev_id = t2.prev_id + JOIN atxs_blobs t3 ON t1.id = t3.id + JOIN atxs_blobs t4 ON t2.id = t4.id + WHERE t1.id <> t2.id;`, nil, dec); err != nil { + return nil, fmt.Errorf("error getting ATXs with same prevATX: %w", err) + } + + return result, nil +} + +func (m *migration0018) isIdentityMalicious(nodeID types.NodeID) bool { + return false +} + +// func (m *migration0018) createMalfeasanceProof(db sql.Executor, collisions []prevATXCollision) error { +// var wireAtx1 wire.ActivationTxV1 +// if err := codec.Decode(data1, &wireAtx1); err != nil { +// m.logger.Error("failed to decode ATX1", zap.Error(err)) +// return false +// } + +// var wireAtx2 wire.ActivationTxV1 +// if err := codec.Decode(data2, &wireAtx2); err != nil { +// m.logger.Error("failed to decode ATX2", zap.Error(err)) +// return false +// } + +// proof := &mwire.MalfeasanceProof{ +// Layer: m.clock.GetCurrentLayer(), +// Proof: mwire.Proof{ +// Type: mwire.InvalidPrevATX, +// Data: &mwire.InvalidPrevATXProof{ +// Atx1: wireAtx1, +// Atx2: wireAtx2, +// }, +// }, +// } + +// encoded, err := codec.Encode(proof) +// if err != nil { +// m.logger.Error("failed to encode malfeasance proof", zap.Error(err)) +// return false +// } +// } +// } From 3eeaa763b3c4332e01ba3a62a2acaa792cdfb32a Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:17:03 +0000 Subject: [PATCH 09/25] Fix typo in query --- sql/migrations/state_0018_migration.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sql/migrations/state_0018_migration.go b/sql/migrations/state_0018_migration.go index 2e9b2afb..d854eaf0 100644 --- a/sql/migrations/state_0018_migration.go +++ b/sql/migrations/state_0018_migration.go @@ -90,8 +90,8 @@ func (m *migration0018) findMaliciousATXs(db sql.Executor) ([]prevATXCollision, SELECT t1.pubkey, t1.id, t2.id, t3.atx, t4.atx FROM atxs t1 JOIN atxs t2 ON t1.pubkey = t2.pubkey AND t1.prev_id = t2.prev_id - JOIN atxs_blobs t3 ON t1.id = t3.id - JOIN atxs_blobs t4 ON t2.id = t4.id + JOIN atx_blobs t3 ON t1.id = t3.id + JOIN atx_blobs t4 ON t2.id = t4.id WHERE t1.id <> t2.id;`, nil, dec); err != nil { return nil, fmt.Errorf("error getting ATXs with same prevATX: %w", err) } From 34e0db3c9dbb2efd36963d08fcfdca334b0b31e8 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:20:13 +0000 Subject: [PATCH 10/25] Fix typo --- malfeasance/wire/malfeasance_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/malfeasance/wire/malfeasance_test.go b/malfeasance/wire/malfeasance_test.go index f5ef8a16..8a478f94 100644 --- a/malfeasance/wire/malfeasance_test.go +++ b/malfeasance/wire/malfeasance_test.go @@ -211,7 +211,7 @@ func TestCodec_InvalidPostIndex(t *testing.T) { require.Equal(t, *proof, decoded) } -func TestCodec_OldPrevATX(t *testing.T) { +func TestCodec_InvalidPrevATX(t *testing.T) { lid := types.LayerID(45) sig, err := signing.NewEdSigner() From a04aad0e258a9b7d287d2725c2fb1044032d8190 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:20:26 +0000 Subject: [PATCH 11/25] Safe on CI minutes --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5564552c..a23fb4ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,11 +126,11 @@ jobs: fail-fast: true matrix: os: - - ubuntu-latest + # - ubuntu-latest - [self-hosted, linux, arm64] # - macos-12-large - - [self-hosted, macOS, ARM64, go-spacemesh] - - windows-latest + # - [self-hosted, macOS, ARM64, go-spacemesh] + # - windows-latest steps: - name: Add OpenCL support - Ubuntu if: ${{ matrix.os == 'ubuntu-latest' }} @@ -172,11 +172,11 @@ jobs: fail-fast: true matrix: os: - - ubuntu-latest + # - ubuntu-latest - [self-hosted, linux, arm64] # - macos-12-large - - [self-hosted, macOS, ARM64, go-spacemesh] - - windows-latest + # - [self-hosted, macOS, ARM64, go-spacemesh] + # - windows-latest steps: - name: Add OpenCL support - Ubuntu if: ${{ matrix.os == 'ubuntu-latest' }} @@ -216,11 +216,11 @@ jobs: fail-fast: true matrix: os: - - ubuntu-latest + # - ubuntu-latest - [self-hosted, linux, arm64] # - macos-12-large - - [self-hosted, macOS, ARM64, go-spacemesh] - - windows-latest + # - [self-hosted, macOS, ARM64, go-spacemesh] + # - windows-latest steps: # as we use some request to localhost, sometimes it gives us flaky tests. try to disable tcp offloading for fix it # https://github.com/actions/virtual-environments/issues/1187 From c4e697f46bb27e7bf273aa071629d1d2720624dd Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:31:02 +0000 Subject: [PATCH 12/25] Add schema tests for migrations --- .../0017_atxs_prev_id_nonce_placeholder.sql | 1 + ...d_malfeseance_proofs_prevATX_collision.sql | 1 + sql/migrations/state_0017_migration_test.go | 36 +++++++++++++++ sql/migrations/state_0018_migration_test.go | 45 +++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 sql/migrations/state/0018_add_malfeseance_proofs_prevATX_collision.sql diff --git a/sql/migrations/state/0017_atxs_prev_id_nonce_placeholder.sql b/sql/migrations/state/0017_atxs_prev_id_nonce_placeholder.sql index e69de29b..7e4a3435 100644 --- a/sql/migrations/state/0017_atxs_prev_id_nonce_placeholder.sql +++ b/sql/migrations/state/0017_atxs_prev_id_nonce_placeholder.sql @@ -0,0 +1 @@ +-- Migration is done entirely in code diff --git a/sql/migrations/state/0018_add_malfeseance_proofs_prevATX_collision.sql b/sql/migrations/state/0018_add_malfeseance_proofs_prevATX_collision.sql new file mode 100644 index 00000000..7e4a3435 --- /dev/null +++ b/sql/migrations/state/0018_add_malfeseance_proofs_prevATX_collision.sql @@ -0,0 +1 @@ +-- Migration is done entirely in code diff --git a/sql/migrations/state_0017_migration_test.go b/sql/migrations/state_0017_migration_test.go index a5dbcbbe..9c71537c 100644 --- a/sql/migrations/state_0017_migration_test.go +++ b/sql/migrations/state_0017_migration_test.go @@ -2,6 +2,8 @@ package migrations import ( "context" + "path/filepath" + "strings" "testing" "time" @@ -60,6 +62,40 @@ func addAtx( return vAtx.ID() } +func Test_0017Migration_CompatibleSQL(t *testing.T) { + file := filepath.Join(t.TempDir(), "test1.db") + db, err := sql.Open("file:"+file, + sql.WithMigration(New0017Migration(zaptest.NewLogger(t))), + ) + require.NoError(t, err) + + var sqls1 []string + _, err = db.Exec("SELECT sql FROM sqlite_schema;", nil, func(stmt *sql.Statement) bool { + sql := stmt.ColumnText(0) + sql = strings.Join(strings.Fields(sql), " ") // remove whitespace + sqls1 = append(sqls1, sql) + return true + }) + require.NoError(t, err) + require.NoError(t, db.Close()) + + file = filepath.Join(t.TempDir(), "test2.db") + db, err = sql.Open("file:" + file) + require.NoError(t, err) + + var sqls2 []string + _, err = db.Exec("SELECT sql FROM sqlite_schema;", nil, func(stmt *sql.Statement) bool { + sql := stmt.ColumnText(0) + sql = strings.Join(strings.Fields(sql), " ") // remove whitespace + sqls2 = append(sqls2, sql) + return true + }) + require.NoError(t, err) + require.NoError(t, db.Close()) + + require.Equal(t, sqls1, sqls2) +} + func Test0017Migration(t *testing.T) { for i := 0; i < 10; i++ { db := sql.InMemory() diff --git a/sql/migrations/state_0018_migration_test.go b/sql/migrations/state_0018_migration_test.go index a6ea3eef..43c87068 100644 --- a/sql/migrations/state_0018_migration_test.go +++ b/sql/migrations/state_0018_migration_test.go @@ -1 +1,46 @@ package migrations + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + + "github.com/spacemeshos/go-spacemesh/sql" +) + +func Test_0018Migration_CompatibleSQL(t *testing.T) { + file := filepath.Join(t.TempDir(), "test1.db") + db, err := sql.Open("file:"+file, + sql.WithMigration(New0018Migration(zaptest.NewLogger(t))), + ) + require.NoError(t, err) + + var sqls1 []string + _, err = db.Exec("SELECT sql FROM sqlite_schema;", nil, func(stmt *sql.Statement) bool { + sql := stmt.ColumnText(0) + sql = strings.Join(strings.Fields(sql), " ") // remove whitespace + sqls1 = append(sqls1, sql) + return true + }) + require.NoError(t, err) + require.NoError(t, db.Close()) + + file = filepath.Join(t.TempDir(), "test2.db") + db, err = sql.Open("file:" + file) + require.NoError(t, err) + + var sqls2 []string + _, err = db.Exec("SELECT sql FROM sqlite_schema;", nil, func(stmt *sql.Statement) bool { + sql := stmt.ColumnText(0) + sql = strings.Join(strings.Fields(sql), " ") // remove whitespace + sqls2 = append(sqls2, sql) + return true + }) + require.NoError(t, err) + require.NoError(t, db.Close()) + + require.Equal(t, sqls1, sqls2) +} From e244e8068be539cb1baf6014598fc70547809144 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:50:10 +0000 Subject: [PATCH 13/25] Backport of features --- activation/handler.go | 8 ++------ activation/wire/wire_v1.go | 14 ++++++++++++++ malfeasance/wire/malfeasance.go | 26 ++++++++++---------------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/activation/handler.go b/activation/handler.go index aef6a9c9..b59d7f4c 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -531,16 +531,12 @@ func (h *Handler) checkWrongPrevAtx( Type: mwire.InvalidPrevATX, Data: &mwire.InvalidPrevATXProof{ Atx1: *wire.ActivationTxToWireV1(atx.ActivationTx), - Atx2: *wire.ActivationTxToWireV1(prev.ActivationTx), + Atx2: *wire.ActivationTxToWireV1(atx2.ActivationTx), }, }, } - encoded, err := codec.Encode(proof) - if err != nil { - h.log.With().Panic("failed to encode malfeasance proof", log.Err(err)) - } - if err := identities.SetMalicious(tx, atx.SmesherID, encoded, time.Now()); err != nil { + if err := identities.SetMalicious(tx, atx.SmesherID, codec.MustEncode(proof), time.Now()); err != nil { return nil, fmt.Errorf("add malfeasance proof: %w", err) } diff --git a/activation/wire/wire_v1.go b/activation/wire/wire_v1.go index a0947a83..7d57350f 100644 --- a/activation/wire/wire_v1.go +++ b/activation/wire/wire_v1.go @@ -15,6 +15,8 @@ type ActivationTxV1 struct { SmesherID types.NodeID Signature types.EdSignature + + id types.ATXID } // InnerActivationTxV1 is a set of all of an ATX's fields, except the signature. To generate the ATX signature, this @@ -92,6 +94,18 @@ type ATXMetadataV1 struct { MsgHash types.Hash32 } +func (atx *ActivationTxV1) ID() types.ATXID { + if atx.id == types.EmptyATXID { + atx.id = types.ATXID(atx.HashInnerBytes()) + } + return atx.id +} + +// TODO(mafa): this can be inlined. +func (atx *ActivationTxV1) Smesher() types.NodeID { + return atx.SmesherID +} + func (atx *ActivationTxV1) SignedBytes() []byte { data := codec.MustEncode(&ATXMetadataV1{ Publish: atx.Publish, diff --git a/malfeasance/wire/malfeasance.go b/malfeasance/wire/malfeasance.go index 75ddb86a..0f71c207 100644 --- a/malfeasance/wire/malfeasance.go +++ b/malfeasance/wire/malfeasance.go @@ -72,21 +72,18 @@ func (mp *MalfeasanceProof) MarshalLogObject(encoder log.ObjectEncoder) error { encoder.AddString("type", "invalid post index") p, ok := mp.Proof.Data.(*InvalidPostIndexProof) if ok { - atx := wire.ActivationTxFromWireV1(&p.Atx) - encoder.AddString("atx_id", atx.ID().String()) - encoder.AddString("smesher", atx.SmesherID.String()) + encoder.AddString("atx_id", p.Atx.ID().String()) + encoder.AddString("smesher", p.Atx.SmesherID.String()) encoder.AddUint32("invalid index", p.InvalidIdx) } case InvalidPrevATX: encoder.AddString("type", "invalid prev atx") p, ok := mp.Proof.Data.(*InvalidPrevATXProof) if ok { - atx1 := wire.ActivationTxFromWireV1(&p.Atx1) - atx2 := wire.ActivationTxFromWireV1(&p.Atx2) - encoder.AddString("atx1_id", atx1.ID().String()) - encoder.AddString("atx2_id", atx2.ID().String()) - encoder.AddString("smesher", atx1.SmesherID.String()) - encoder.AddString("prev_atx", atx1.PrevATXID.String()) + encoder.AddString("atx1_id", p.Atx2.ID().String()) + encoder.AddString("atx2_id", p.Atx2.ID().String()) + encoder.AddString("smesher", p.Atx1.SmesherID.String()) + encoder.AddString("prev_atx", p.Atx1.PrevATXID.String()) } default: encoder.AddString("type", "unknown") @@ -386,11 +383,10 @@ func MalfeasanceInfo(smesher types.NodeID, mp *MalfeasanceProof) string { case InvalidPostIndex: p, ok := mp.Proof.Data.(*InvalidPostIndexProof) if ok { - atx := wire.ActivationTxFromWireV1(&p.Atx) b.WriteString( fmt.Sprintf( "cause: smesher published ATX %s with invalid post index %d in epoch %d\n", - atx.ID().ShortString(), + p.Atx.ID().ShortString(), p.InvalidIdx, p.Atx.Publish, )) @@ -398,14 +394,12 @@ func MalfeasanceInfo(smesher types.NodeID, mp *MalfeasanceProof) string { case InvalidPrevATX: p, ok := mp.Proof.Data.(*InvalidPrevATXProof) if ok { - atx1 := wire.ActivationTxFromWireV1(&p.Atx1) - atx2 := wire.ActivationTxFromWireV1(&p.Atx2) b.WriteString( fmt.Sprintf( "cause: smesher published ATX %s with invalid previous ATX %s in epoch %d\n", - atx1.ID().ShortString(), - atx2.ID().ShortString(), - atx1.PublishEpoch, + p.Atx1.ID().ShortString(), + p.Atx2.ID().ShortString(), + p.Atx1.Publish, )) } } From a41abafe97653258271f6a906f7a925cc592939e Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:56:18 +0000 Subject: [PATCH 14/25] Avoid conversion --- malfeasance/handler.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/malfeasance/handler.go b/malfeasance/handler.go index 724dfe7a..23421773 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -10,7 +10,6 @@ import ( "github.com/spacemeshos/post/shared" "github.com/spacemeshos/post/verifying" - awire "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" @@ -438,10 +437,8 @@ func validateInvalidPrevATX( if !edVerifier.Verify(signing.ATX, atx2.SmesherID, atx2.SignedBytes(), atx2.Signature) { return types.EmptyNodeID, errors.New("atx2: invalid signature") } - idATX1 := awire.ActivationTxFromWireV1(&proof.Atx1) - idATX2 := awire.ActivationTxFromWireV1(&proof.Atx2) - if idATX1.ID() == idATX2.ID() { + if atx1.ID() == atx2.ID() { numInvalidProofsPrevATX.Inc() return types.EmptyNodeID, errors.New("invalid old prev ATX malfeasance proof: ATX IDs are the same") } From bd97788c6d8a3b207d486be270ff74dce973c8f1 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:09:02 +0000 Subject: [PATCH 15/25] Review feedback --- activation/handler.go | 49 +++++++++++++++++++++----------------- activation/handler_test.go | 34 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/activation/handler.go b/activation/handler.go index b59d7f4c..aa653c79 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -548,32 +548,37 @@ func (h *Handler) checkWrongPrevAtx( return proof, nil } -// storeAtx stores an ATX and notifies subscribers of the ATXID. -func (h *Handler) storeAtx(ctx context.Context, atx *types.VerifiedActivationTx) (*mwire.MalfeasanceProof, error) { - var nonce *types.VRFPostIndex - malicious, err := h.cdb.IsMalicious(atx.SmesherID) +func (h *Handler) checkMalicious( + ctx context.Context, + tx *sql.Tx, + atx *types.VerifiedActivationTx, +) (*mwire.MalfeasanceProof, error) { + malicious, err := identities.IsMalicious(tx, atx.SmesherID) if err != nil { return nil, fmt.Errorf("checking if node is malicious: %w", err) } - var proof *mwire.MalfeasanceProof - err = h.cdb.WithTx(ctx, func(tx *sql.Tx) error { - nonce, err = atxs.AddGettingNonce(tx, atx) - if err != nil && !errors.Is(err, sql.ErrObjectExists) { - return fmt.Errorf("add atx to db: %w", err) - } - if malicious { - return nil - } - proof, err = h.checkDoublePublish(ctx, tx, atx) - if err != nil { - return err - } - if proof != nil { - return nil - } + if malicious { + return nil, nil + } + proof, err := h.checkDoublePublish(ctx, tx, atx) + if proof != nil || err != nil { + return proof, err + } + return h.checkWrongPrevAtx(ctx, tx, atx) +} - proof, err = h.checkWrongPrevAtx(ctx, tx, atx) - return err +// storeAtx stores an ATX and notifies subscribers of the ATXID. +func (h *Handler) storeAtx(ctx context.Context, atx *types.VerifiedActivationTx) (*mwire.MalfeasanceProof, error) { + var nonce *types.VRFPostIndex + var proof *mwire.MalfeasanceProof + err := h.cdb.WithTx(ctx, func(tx *sql.Tx) error { + var err1, err2 error + proof, err1 = h.checkMalicious(ctx, tx, atx) + nonce, err2 = atxs.AddGettingNonce(tx, atx) + if err2 != nil && !errors.Is(err2, sql.ErrObjectExists) { + err2 = fmt.Errorf("add atx to db: %w", err2) + } + return errors.Join(err1, err2) }) if err != nil { return nil, fmt.Errorf("store atx: %w", err) diff --git a/activation/handler_test.go b/activation/handler_test.go index 7ee949c1..02135c82 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -977,6 +977,40 @@ func TestHandler_ProcessAtx(t *testing.T) { require.Nil(t, proof) } +func TestHandler_ProcessAtx_maliciousIdentity(t *testing.T) { + // Arrange + goldenATXID := types.ATXID{2, 3, 4} + atxHdlr := newTestHandler(t, goldenATXID) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + require.NoError(t, identities.SetMalicious(atxHdlr.cdb, sig.NodeID(), types.RandomBytes(10), time.Now())) + + coinbase := types.GenerateAddress([]byte("aaaa")) + + // Act & Assert + atx1 := newActivationTx( + t, + sig, + 0, + types.EmptyATXID, + types.EmptyATXID, + nil, + types.EpochID(2), + 0, + 100, + coinbase, + 100, + &types.NIPost{PostMetadata: &types.PostMetadata{}}, + withVrfNonce(7), + ) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Any()) + atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + proof, err := atxHdlr.processVerifiedATX(context.Background(), atx1) + require.NoError(t, err) + require.Nil(t, proof) +} + func TestHandler_ProcessAtx_SamePubEpoch(t *testing.T) { // Arrange goldenATXID := types.ATXID{2, 3, 4} From 976cd05372e7b4f9f34a27217fe526e0e5b54993 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:22:23 +0000 Subject: [PATCH 16/25] Add code to scan db for malfeasant ATXs --- activation/verify_state.go | 78 +++++++++++ activation/verify_state_test.go | 91 +++++++++++++ node/node.go | 9 +- sql/atxs/atxs.go | 42 ++++++ sql/atxs/atxs_test.go | 57 ++++++++ sql/migrations/state_0018_migration.go | 136 -------------------- sql/migrations/state_0018_migration_test.go | 46 ------- 7 files changed, 276 insertions(+), 183 deletions(-) create mode 100644 activation/verify_state.go create mode 100644 activation/verify_state_test.go delete mode 100644 sql/migrations/state_0018_migration.go delete mode 100644 sql/migrations/state_0018_migration_test.go diff --git a/activation/verify_state.go b/activation/verify_state.go new file mode 100644 index 00000000..f3e9cade --- /dev/null +++ b/activation/verify_state.go @@ -0,0 +1,78 @@ +package activation + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap" + + awire "github.com/spacemeshos/go-spacemesh/activation/wire" + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/malfeasance/wire" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/sql/identities" +) + +func CheckPrevATXs(ctx context.Context, logger *zap.Logger, db sql.Executor) error { + collisions, err := atxs.PrevATXCollisions(db) + if err != nil { + return fmt.Errorf("get prev ATX collisions: %w", err) + } + + logger.Info("found ATX collisions", zap.Int("count", len(collisions))) + count := 0 + for _, collision := range collisions { + select { + case <-ctx.Done(): + // stop on context cancellation + return ctx.Err() + default: + } + + malicious, err := identities.IsMalicious(db, collision.NodeID) + if err != nil { + return fmt.Errorf("get malicious status: %w", err) + } + + if malicious { + // already malicious no need to generate proof + continue + } + + var atx1 awire.ActivationTxV1 + codec.MustDecode(collision.Atx1, &atx1) + + var atx2 awire.ActivationTxV1 + codec.MustDecode(collision.Atx1, &atx2) + + proof := &wire.MalfeasanceProof{ + Layer: atx1.Publish.FirstLayer(), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: atx1, + Atx2: atx2, + }, + }, + } + + encodedProof := codec.MustEncode(proof) + if err := identities.SetMalicious(db, collision.NodeID, encodedProof, time.Now()); err != nil { + return fmt.Errorf("add malfeasance proof: %w", err) + } + + // h.cdb.CacheMalfeasanceProof(atx.SmesherID, proof) + // h.tortoise.OnMalfeasance(atx.SmesherID) + + // if err = h.publisher.Publish(ctx, pubsub.MalfeasanceProof, encodedProof); err != nil { + // h.log.With().Error("failed to broadcast malfeasance proof", log.Err(err)) + // return false + // } + + count++ + } + logger.Info("created malfeasance proofs", zap.Int("count", count)) + return nil +} diff --git a/activation/verify_state_test.go b/activation/verify_state_test.go new file mode 100644 index 00000000..801cd3c6 --- /dev/null +++ b/activation/verify_state_test.go @@ -0,0 +1,91 @@ +package activation + +import ( + "context" + "math/rand/v2" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/sql/identities" +) + +func Test_CheckPrevATXs(t *testing.T) { + db := sql.InMemory() + logger := zaptest.NewLogger(t) + + // Arrange + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + // create two ATXs with the same PrevATXID + prevATXID := types.RandomATXID() + goldenATXID := types.RandomATXID() + + atx1 := newActivationTx( + t, + sig, + 0, + prevATXID, + goldenATXID, + &goldenATXID, + types.EpochID(2), + 0, + 100, + types.GenerateAddress([]byte("aaaa")), + 100, + nil, + ) + require.NoError(t, atxs.Add(db, atx1)) + + atx2 := newActivationTx( + t, + sig, + 1, + prevATXID, + goldenATXID, + &goldenATXID, + types.EpochID(3), + 0, + 100, + types.GenerateAddress([]byte("aaaa")), + 100, + nil, + ) + require.NoError(t, atxs.Add(db, atx2)) + + // create 100 random ATXs that are not malicious + for i := 0; i < 100; i++ { + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + atx := newActivationTx( + t, + otherSig, + rand.Uint64(), + types.RandomATXID(), + types.RandomATXID(), + nil, + rand.N[types.EpochID](100), + 0, + 100, + types.GenerateAddress([]byte("aaaa")), + rand.Uint32(), + nil, + ) + require.NoError(t, atxs.Add(db, atx)) + } + + // Act + err = CheckPrevATXs(context.Background(), logger, db) + require.NoError(t, err) + + // Assert + malicious, err := identities.IsMalicious(db, sig.NodeID()) + require.NoError(t, err) + require.True(t, malicious) +} diff --git a/node/node.go b/node/node.go index affe2194..15ffb8b0 100644 --- a/node/node.go +++ b/node/node.go @@ -1847,7 +1847,6 @@ func (app *App) setupDBs(ctx context.Context, lg log.Log) error { sql.WithLogger(dbLog.Zap()), sql.WithMigrations(migrations), sql.WithMigration(sqlmigrations.New0017Migration(dbLog.Zap())), - sql.WithMigration(sqlmigrations.New0018Migration(dbLog.Zap())), sql.WithConnections(app.Config.DatabaseConnections), sql.WithLatencyMetering(app.Config.DatabaseLatencyMetering), sql.WithVacuumState(app.Config.DatabaseVacuumState), @@ -1893,6 +1892,14 @@ func (app *App) setupDBs(ctx context.Context, lg log.Log) error { datastore.WithConfig(app.Config.Cache), ) + // TODO(mafa): add command line flag to trigger this instead of always running it + app.log.With().Info("checking DB for malicious ATXs") + start = time.Now() + if err := activation.CheckPrevATXs(ctx, app.log.Zap(), app.db); err != nil { + return fmt.Errorf("malicious ATX check: %w", err) + } + app.log.With().Info("malicious ATX check completed", log.Duration("duration", time.Since(start))) + migrations, err = sql.LocalMigrations() if err != nil { return fmt.Errorf("load local migrations: %w", err) diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 04dbf1a1..88a5afbc 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -788,3 +788,45 @@ func PoetProofRef(ctx context.Context, db sql.Executor, id types.ATXID) (types.P return types.PoetProofRef(atx.NIPost.PostMetadata.Challenge), nil } + +type PrevATXCollision struct { + NodeID types.NodeID + Atx1 []byte + Atx2 []byte +} + +func PrevATXCollisions(db sql.Executor) ([]PrevATXCollision, error) { + var result []PrevATXCollision + + dec := func(stmt *sql.Statement) bool { + var id types.NodeID + stmt.ColumnBytes(0, id[:]) + + var id1, id2 types.ATXID + stmt.ColumnBytes(1, id1[:]) + stmt.ColumnBytes(2, id2[:]) + + data1 := make([]byte, stmt.ColumnLen(3)) + data2 := make([]byte, stmt.ColumnLen(4)) + stmt.ColumnBytes(3, data1) + stmt.ColumnBytes(4, data2) + + result = append(result, PrevATXCollision{ + NodeID: id, + Atx1: data1, + Atx2: data2, + }) + return true + } + if _, err := db.Exec(` + SELECT t1.pubkey, t1.id, t2.id, t3.atx, t4.atx + FROM atxs t1 + JOIN atxs t2 ON t1.pubkey = t2.pubkey AND t1.prev_id = t2.prev_id AND t1.id < t2.id + JOIN atx_blobs t3 ON t1.id = t3.id + JOIN atx_blobs t4 ON t2.id = t4.id + WHERE t1.id <> t2.id;`, nil, dec); err != nil { + return nil, fmt.Errorf("error getting ATXs with same prevATX: %w", err) + } + + return result, nil +} diff --git a/sql/atxs/atxs_test.go b/sql/atxs/atxs_test.go index 53c41da4..6dc3b301 100644 --- a/sql/atxs/atxs_test.go +++ b/sql/atxs/atxs_test.go @@ -968,3 +968,60 @@ func TestLatest(t *testing.T) { }) } } + +func Test_PrevATXCollisions(t *testing.T) { + db := sql.InMemory() + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + // create two ATXs with the same PrevATXID + prevATXID := types.RandomATXID() + + atx1, err := newAtx(sig, withPublishEpoch(1), withPrevATXID(prevATXID)) + require.NoError(t, err) + atx2, err := newAtx(sig, withPublishEpoch(2), withPrevATXID(prevATXID)) + require.NoError(t, err) + + require.NoError(t, atxs.Add(db, atx1)) + require.NoError(t, atxs.Add(db, atx2)) + + // verify that the ATXs were added + got1, err := atxs.Get(db, atx1.ID()) + require.NoError(t, err) + require.Equal(t, atx1, got1) + + got2, err := atxs.Get(db, atx2.ID()) + require.NoError(t, err) + require.Equal(t, atx2, got2) + + // add 10 valid ATXs by 10 other smeshers + atxMap := make(map[types.NodeID][]*types.VerifiedActivationTx) + for i := 2; i < 12; i++ { + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + + if len(atxMap[otherSig.NodeID()]) == 0 { + atx, err := newAtx(otherSig, withPublishEpoch(types.EpochID(i))) + require.NoError(t, err) + require.NoError(t, atxs.Add(db, atx)) + } else { + atx, err := newAtx(otherSig, withPublishEpoch(types.EpochID(i)), + withPrevATXID(atxMap[otherSig.NodeID()][len(atxMap[otherSig.NodeID()])-1].ID()), + ) + require.NoError(t, err) + require.NoError(t, atxs.Add(db, atx)) + } + } + + // get the collisions + got, err := atxs.PrevATXCollisions(db) + require.NoError(t, err) + require.Len(t, got, 1) + + require.Equal(t, sig.NodeID(), got[0].NodeID) + var wireAtx1, wireAtx2 wire.ActivationTxV1 + codec.MustDecode(got[0].Atx1, &wireAtx1) + codec.MustDecode(got[0].Atx2, &wireAtx2) + + require.ElementsMatch(t, []types.ATXID{atx1.ID(), atx2.ID()}, []types.ATXID{wireAtx1.ID(), wireAtx2.ID()}) +} diff --git a/sql/migrations/state_0018_migration.go b/sql/migrations/state_0018_migration.go deleted file mode 100644 index d854eaf0..00000000 --- a/sql/migrations/state_0018_migration.go +++ /dev/null @@ -1,136 +0,0 @@ -package migrations - -import ( - "fmt" - - "go.uber.org/zap" - - "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/sql" -) - -type migration0018 struct { - logger *zap.Logger -} - -func New0018Migration(log *zap.Logger) *migration0018 { - return &migration0018{ - logger: log, - } -} - -func (*migration0018) Name() string { - return "add malfeasance proofs for incorrect prevATXID in ATXs" -} - -func (*migration0018) Order() int { - return 18 -} - -func (*migration0018) Rollback() error { - // handled by the DB itself - return nil -} - -func (m *migration0018) Apply(db sql.Executor) error { - found, err := m.findMaliciousATXs(db) - if err != nil { - return fmt.Errorf("error finding malfeasant ATXs: %w", err) - } - - for _, collision := range found { - if m.isIdentityMalicious(collision.id) { - continue - } - - // if err := m.createMalfeasanceProof(db, collision); err != nil { - // return fmt.Errorf("error creating malfeasance proof: %w", err) - // } - } - - return nil -} - -type prevATXCollision struct { - id types.NodeID - atx1 []byte - atx2 []byte -} - -func (m *migration0018) findMaliciousATXs(db sql.Executor) ([]prevATXCollision, error) { - var result []prevATXCollision - - dec := func(stmt *sql.Statement) bool { - var id types.NodeID - stmt.ColumnBytes(0, id[:]) - - var id1, id2 types.ATXID - stmt.ColumnBytes(1, id1[:]) - stmt.ColumnBytes(2, id2[:]) - - m.logger.Debug("found ATXs with same prevATX", - zap.String("node_id", id.String()), - zap.String("atx1", id1.ShortString()), - zap.String("atx2", id2.ShortString()), - ) - - data1 := make([]byte, stmt.ColumnLen(3)) - data2 := make([]byte, stmt.ColumnLen(4)) - stmt.ColumnBytes(3, data1) - stmt.ColumnBytes(4, data2) - - result = append(result, prevATXCollision{ - id: id, - atx1: data1, - atx2: data2, - }) - return true - } - if _, err := db.Exec(` - SELECT t1.pubkey, t1.id, t2.id, t3.atx, t4.atx - FROM atxs t1 - JOIN atxs t2 ON t1.pubkey = t2.pubkey AND t1.prev_id = t2.prev_id - JOIN atx_blobs t3 ON t1.id = t3.id - JOIN atx_blobs t4 ON t2.id = t4.id - WHERE t1.id <> t2.id;`, nil, dec); err != nil { - return nil, fmt.Errorf("error getting ATXs with same prevATX: %w", err) - } - - return result, nil -} - -func (m *migration0018) isIdentityMalicious(nodeID types.NodeID) bool { - return false -} - -// func (m *migration0018) createMalfeasanceProof(db sql.Executor, collisions []prevATXCollision) error { -// var wireAtx1 wire.ActivationTxV1 -// if err := codec.Decode(data1, &wireAtx1); err != nil { -// m.logger.Error("failed to decode ATX1", zap.Error(err)) -// return false -// } - -// var wireAtx2 wire.ActivationTxV1 -// if err := codec.Decode(data2, &wireAtx2); err != nil { -// m.logger.Error("failed to decode ATX2", zap.Error(err)) -// return false -// } - -// proof := &mwire.MalfeasanceProof{ -// Layer: m.clock.GetCurrentLayer(), -// Proof: mwire.Proof{ -// Type: mwire.InvalidPrevATX, -// Data: &mwire.InvalidPrevATXProof{ -// Atx1: wireAtx1, -// Atx2: wireAtx2, -// }, -// }, -// } - -// encoded, err := codec.Encode(proof) -// if err != nil { -// m.logger.Error("failed to encode malfeasance proof", zap.Error(err)) -// return false -// } -// } -// } diff --git a/sql/migrations/state_0018_migration_test.go b/sql/migrations/state_0018_migration_test.go deleted file mode 100644 index 43c87068..00000000 --- a/sql/migrations/state_0018_migration_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package migrations - -import ( - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/require" - "go.uber.org/zap/zaptest" - - "github.com/spacemeshos/go-spacemesh/sql" -) - -func Test_0018Migration_CompatibleSQL(t *testing.T) { - file := filepath.Join(t.TempDir(), "test1.db") - db, err := sql.Open("file:"+file, - sql.WithMigration(New0018Migration(zaptest.NewLogger(t))), - ) - require.NoError(t, err) - - var sqls1 []string - _, err = db.Exec("SELECT sql FROM sqlite_schema;", nil, func(stmt *sql.Statement) bool { - sql := stmt.ColumnText(0) - sql = strings.Join(strings.Fields(sql), " ") // remove whitespace - sqls1 = append(sqls1, sql) - return true - }) - require.NoError(t, err) - require.NoError(t, db.Close()) - - file = filepath.Join(t.TempDir(), "test2.db") - db, err = sql.Open("file:" + file) - require.NoError(t, err) - - var sqls2 []string - _, err = db.Exec("SELECT sql FROM sqlite_schema;", nil, func(stmt *sql.Statement) bool { - sql := stmt.ColumnText(0) - sql = strings.Join(strings.Fields(sql), " ") // remove whitespace - sqls2 = append(sqls2, sql) - return true - }) - require.NoError(t, err) - require.NoError(t, db.Close()) - - require.Equal(t, sqls1, sqls2) -} From a516f4f8ef076d17a8f989909f5585f07bd67f79 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:37:38 +0000 Subject: [PATCH 17/25] Improve Scan process --- activation/verify_state.go | 26 ++++++++++++++++++++++---- sql/atxs/atxs.go | 38 ++++++++++++++++++-------------------- sql/atxs/atxs_test.go | 9 +++------ 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/activation/verify_state.go b/activation/verify_state.go index f3e9cade..d80f1bc9 100644 --- a/activation/verify_state.go +++ b/activation/verify_state.go @@ -9,6 +9,7 @@ import ( awire "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/malfeasance/wire" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" @@ -31,7 +32,17 @@ func CheckPrevATXs(ctx context.Context, logger *zap.Logger, db sql.Executor) err default: } - malicious, err := identities.IsMalicious(db, collision.NodeID) + if collision.NodeID1 != collision.NodeID2 { + logger.Panic( + "unexpected collision", + log.ZShortStringer("NodeID1", collision.NodeID1), + log.ZShortStringer("NodeID2", collision.NodeID2), + log.ZShortStringer("ATX1", collision.ATX1), + log.ZShortStringer("ATX2", collision.ATX2), + ) + } + + malicious, err := identities.IsMalicious(db, collision.NodeID1) if err != nil { return fmt.Errorf("get malicious status: %w", err) } @@ -41,11 +52,18 @@ func CheckPrevATXs(ctx context.Context, logger *zap.Logger, db sql.Executor) err continue } + var blob sql.Blob var atx1 awire.ActivationTxV1 - codec.MustDecode(collision.Atx1, &atx1) + if err := atxs.LoadBlob(ctx, db, collision.ATX1.Bytes(), &blob); err != nil { + return fmt.Errorf("get blob %s: %w", collision.ATX1.ShortString(), err) + } + codec.MustDecode(blob.Bytes, &atx1) var atx2 awire.ActivationTxV1 - codec.MustDecode(collision.Atx1, &atx2) + if err := atxs.LoadBlob(ctx, db, collision.ATX2.Bytes(), &blob); err != nil { + return fmt.Errorf("get blob %s: %w", collision.ATX2.ShortString(), err) + } + codec.MustDecode(blob.Bytes, &atx2) proof := &wire.MalfeasanceProof{ Layer: atx1.Publish.FirstLayer(), @@ -59,7 +77,7 @@ func CheckPrevATXs(ctx context.Context, logger *zap.Logger, db sql.Executor) err } encodedProof := codec.MustEncode(proof) - if err := identities.SetMalicious(db, collision.NodeID, encodedProof, time.Now()); err != nil { + if err := identities.SetMalicious(db, collision.NodeID1, encodedProof, time.Now()); err != nil { return fmt.Errorf("add malfeasance proof: %w", err) } diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 88a5afbc..df6db8b2 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -790,41 +790,39 @@ func PoetProofRef(ctx context.Context, db sql.Executor, id types.ATXID) (types.P } type PrevATXCollision struct { - NodeID types.NodeID - Atx1 []byte - Atx2 []byte + NodeID1 types.NodeID + ATX1 types.ATXID + + NodeID2 types.NodeID + ATX2 types.ATXID } func PrevATXCollisions(db sql.Executor) ([]PrevATXCollision, error) { var result []PrevATXCollision dec := func(stmt *sql.Statement) bool { - var id types.NodeID - stmt.ColumnBytes(0, id[:]) + var nodeID1, nodeID2 types.NodeID + stmt.ColumnBytes(0, nodeID1[:]) + stmt.ColumnBytes(1, nodeID2[:]) var id1, id2 types.ATXID - stmt.ColumnBytes(1, id1[:]) - stmt.ColumnBytes(2, id2[:]) - - data1 := make([]byte, stmt.ColumnLen(3)) - data2 := make([]byte, stmt.ColumnLen(4)) - stmt.ColumnBytes(3, data1) - stmt.ColumnBytes(4, data2) + stmt.ColumnBytes(2, id1[:]) + stmt.ColumnBytes(3, id2[:]) result = append(result, PrevATXCollision{ - NodeID: id, - Atx1: data1, - Atx2: data2, + NodeID1: nodeID1, + ATX1: id1, + + NodeID2: nodeID2, + ATX2: id2, }) return true } if _, err := db.Exec(` - SELECT t1.pubkey, t1.id, t2.id, t3.atx, t4.atx + SELECT t1.pubkey, t2.pubkey, t1.id, t2.id FROM atxs t1 - JOIN atxs t2 ON t1.pubkey = t2.pubkey AND t1.prev_id = t2.prev_id AND t1.id < t2.id - JOIN atx_blobs t3 ON t1.id = t3.id - JOIN atx_blobs t4 ON t2.id = t4.id - WHERE t1.id <> t2.id;`, nil, dec); err != nil { + JOIN atxs t2 ON t1.prev_id = t2.prev_id + WHERE t1.id < t2.id;`, nil, dec); err != nil { return nil, fmt.Errorf("error getting ATXs with same prevATX: %w", err) } diff --git a/sql/atxs/atxs_test.go b/sql/atxs/atxs_test.go index 6dc3b301..302701a9 100644 --- a/sql/atxs/atxs_test.go +++ b/sql/atxs/atxs_test.go @@ -1018,10 +1018,7 @@ func Test_PrevATXCollisions(t *testing.T) { require.NoError(t, err) require.Len(t, got, 1) - require.Equal(t, sig.NodeID(), got[0].NodeID) - var wireAtx1, wireAtx2 wire.ActivationTxV1 - codec.MustDecode(got[0].Atx1, &wireAtx1) - codec.MustDecode(got[0].Atx2, &wireAtx2) - - require.ElementsMatch(t, []types.ATXID{atx1.ID(), atx2.ID()}, []types.ATXID{wireAtx1.ID(), wireAtx2.ID()}) + require.Equal(t, sig.NodeID(), got[0].NodeID1) + require.Equal(t, sig.NodeID(), got[0].NodeID2) + require.ElementsMatch(t, []types.ATXID{atx1.ID(), atx2.ID()}, []types.ATXID{got[0].ATX1, got[0].ATX2}) } From 27c08ed1d873e25c1434f9460e53428dbca08040 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:15:42 +0000 Subject: [PATCH 18/25] Make inner join explicit --- sql/atxs/atxs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index df6db8b2..5f747afa 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -821,7 +821,7 @@ func PrevATXCollisions(db sql.Executor) ([]PrevATXCollision, error) { if _, err := db.Exec(` SELECT t1.pubkey, t2.pubkey, t1.id, t2.id FROM atxs t1 - JOIN atxs t2 ON t1.prev_id = t2.prev_id + INNER JOIN atxs t2 ON t1.prev_id = t2.prev_id WHERE t1.id < t2.id;`, nil, dec); err != nil { return nil, fmt.Errorf("error getting ATXs with same prevATX: %w", err) } From 7a33d28149dbeec23636ce128e2cfa3c33365fee Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:25:57 +0000 Subject: [PATCH 19/25] Remove unneeded migration --- .../state/0018_add_malfeseance_proofs_prevATX_collision.sql | 1 - 1 file changed, 1 deletion(-) delete mode 100644 sql/migrations/state/0018_add_malfeseance_proofs_prevATX_collision.sql diff --git a/sql/migrations/state/0018_add_malfeseance_proofs_prevATX_collision.sql b/sql/migrations/state/0018_add_malfeseance_proofs_prevATX_collision.sql deleted file mode 100644 index 7e4a3435..00000000 --- a/sql/migrations/state/0018_add_malfeseance_proofs_prevATX_collision.sql +++ /dev/null @@ -1 +0,0 @@ --- Migration is done entirely in code From ee6202e05ebf916ae68b6f7ace210a58e7271b8c Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 2 May 2024 08:02:44 +0000 Subject: [PATCH 20/25] Add flag for malfeasant scan --- activation/verify_state.go | 8 -------- cmd/root.go | 3 +++ config/config.go | 3 +++ config/mainnet.go | 3 ++- node/node.go | 13 +++++++------ 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/activation/verify_state.go b/activation/verify_state.go index d80f1bc9..3c9db64b 100644 --- a/activation/verify_state.go +++ b/activation/verify_state.go @@ -81,14 +81,6 @@ func CheckPrevATXs(ctx context.Context, logger *zap.Logger, db sql.Executor) err return fmt.Errorf("add malfeasance proof: %w", err) } - // h.cdb.CacheMalfeasanceProof(atx.SmesherID, proof) - // h.tortoise.OnMalfeasance(atx.SmesherID) - - // if err = h.publisher.Publish(ctx, pubsub.MalfeasanceProof, encodedProof); err != nil { - // h.log.With().Error("failed to broadcast malfeasance proof", log.Err(err)) - // return false - // } - count++ } logger.Info("created malfeasance proofs", zap.Int("count", count)) diff --git a/cmd/root.go b/cmd/root.go index aa0db3e7..4fcd2862 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -79,6 +79,9 @@ func AddFlags(flagSet *pflag.FlagSet, cfg *config.Config) (configPath *string) { flagSet.DurationVar(&cfg.DatabasePruneInterval, "db-prune-interval", cfg.DatabasePruneInterval, "configure interval for database pruning") + flagSet.BoolVar(&cfg.ScanMalfeasantATXs, "scan-malfeasant-atxs", cfg.ScanMalfeasantATXs, + "scan for malfeasant ATXs") + flagSet.BoolVar(&cfg.NoMainOverride, "no-main-override", cfg.NoMainOverride, "force 'nomain' builds to run on the mainnet") diff --git a/config/config.go b/config/config.go index ca176869..27cb0adf 100644 --- a/config/config.go +++ b/config/config.go @@ -124,6 +124,9 @@ type BaseConfig struct { PruneActivesetsFrom types.EpochID `mapstructure:"prune-activesets-from"` + // ScanMalfeasantATXs is a flag to enable scanning for malfeasant ATXs. + ScanMalfeasantATXs bool `mapstructure:"scan-malfeasant-atxs"` + NetworkHRP string `mapstructure:"network-hrp"` // MinerGoodAtxsPercent is a threshold to decide if tortoise activeset should be diff --git a/config/mainnet.go b/config/mainnet.go index b3ebe45f..fa326a8f 100644 --- a/config/mainnet.go +++ b/config/mainnet.go @@ -73,7 +73,8 @@ func MainnetConfig() Config { DatabaseConnections: 16, DatabasePruneInterval: 30 * time.Minute, DatabaseVacuumState: 15, - PruneActivesetsFrom: 12, // starting from epoch 13 activesets below 12 will be pruned + PruneActivesetsFrom: 12, // starting from epoch 13 activesets below 12 will be pruned + ScanMalfeasantATXs: false, // opt-in NetworkHRP: "sm", LayerDuration: 5 * time.Minute, diff --git a/node/node.go b/node/node.go index 15ffb8b0..d143e661 100644 --- a/node/node.go +++ b/node/node.go @@ -1892,13 +1892,14 @@ func (app *App) setupDBs(ctx context.Context, lg log.Log) error { datastore.WithConfig(app.Config.Cache), ) - // TODO(mafa): add command line flag to trigger this instead of always running it - app.log.With().Info("checking DB for malicious ATXs") - start = time.Now() - if err := activation.CheckPrevATXs(ctx, app.log.Zap(), app.db); err != nil { - return fmt.Errorf("malicious ATX check: %w", err) + if app.Config.ScanMalfeasantATXs { + app.log.With().Info("checking DB for malicious ATXs") + start = time.Now() + if err := activation.CheckPrevATXs(ctx, app.log.Zap(), app.db); err != nil { + return fmt.Errorf("malicious ATX check: %w", err) + } + app.log.With().Info("malicious ATX check completed", log.Duration("duration", time.Since(start))) } - app.log.With().Info("malicious ATX check completed", log.Duration("duration", time.Since(start))) migrations, err = sql.LocalMigrations() if err != nil { From 3be4ee21f3329c837c077272655fc064b5ed4643 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 2 May 2024 08:04:24 +0000 Subject: [PATCH 21/25] Revert CI changes --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a23fb4ef..5564552c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,11 +126,11 @@ jobs: fail-fast: true matrix: os: - # - ubuntu-latest + - ubuntu-latest - [self-hosted, linux, arm64] # - macos-12-large - # - [self-hosted, macOS, ARM64, go-spacemesh] - # - windows-latest + - [self-hosted, macOS, ARM64, go-spacemesh] + - windows-latest steps: - name: Add OpenCL support - Ubuntu if: ${{ matrix.os == 'ubuntu-latest' }} @@ -172,11 +172,11 @@ jobs: fail-fast: true matrix: os: - # - ubuntu-latest + - ubuntu-latest - [self-hosted, linux, arm64] # - macos-12-large - # - [self-hosted, macOS, ARM64, go-spacemesh] - # - windows-latest + - [self-hosted, macOS, ARM64, go-spacemesh] + - windows-latest steps: - name: Add OpenCL support - Ubuntu if: ${{ matrix.os == 'ubuntu-latest' }} @@ -216,11 +216,11 @@ jobs: fail-fast: true matrix: os: - # - ubuntu-latest + - ubuntu-latest - [self-hosted, linux, arm64] # - macos-12-large - # - [self-hosted, macOS, ARM64, go-spacemesh] - # - windows-latest + - [self-hosted, macOS, ARM64, go-spacemesh] + - windows-latest steps: # as we use some request to localhost, sometimes it gives us flaky tests. try to disable tcp offloading for fix it # https://github.com/actions/virtual-environments/issues/1187 From ecc4e209637061e57979e80d65171406cb9d4bea Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 2 May 2024 08:32:02 +0000 Subject: [PATCH 22/25] Fix dockerbuild for private repositories --- .github/workflows/systest.yml | 7 +++++++ Dockerfile | 2 +- Makefile | 7 ++++++- bootstrap.Dockerfile | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/systest.yml b/.github/workflows/systest.yml index 12554074..aa81357e 100644 --- a/.github/workflows/systest.yml +++ b/.github/workflows/systest.yml @@ -92,6 +92,13 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} + - uses: extractions/netrc@v2 + with: + machine: github.com + username: ${{ secrets.GH_ACTION_TOKEN_USER }} + password: ${{ secrets.GH_ACTION_TOKEN }} + if: vars.GOPRIVATE + - name: Push go-spacemesh build to docker hub run: make dockerpush diff --git a/Dockerfile b/Dockerfile index f297d93e..bd1223a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,7 +44,7 @@ RUN make get-libs COPY go.mod . COPY go.sum . -RUN go mod download +RUN --mount=type=secret,id=mynetrc,dst=/root/.netrc go mod download # Here we copy the rest of the source code COPY . . diff --git a/Makefile b/Makefile index a82ae6e9..eb6e44a8 100644 --- a/Makefile +++ b/Makefile @@ -154,6 +154,7 @@ list-versions: dockerbuild-go: DOCKER_BUILDKIT=1 docker build \ + --secret id=mynetrc,src=$(HOME)/.netrc \ --build-arg VERSION=${VERSION} \ -t go-spacemesh:$(SHA) \ -t $(DOCKER_HUB)/$(DOCKER_IMAGE_REPO):$(DOCKER_IMAGE_VERSION) . @@ -171,7 +172,11 @@ endif .PHONY: dockerpush-only dockerbuild-bs: - DOCKER_BUILDKIT=1 docker build -t go-spacemesh-bs:$(SHA) -t $(DOCKER_HUB)/$(DOCKER_IMAGE_REPO)-bs:$(DOCKER_IMAGE_VERSION) -f ./bootstrap.Dockerfile . + DOCKER_BUILDKIT=1 docker build \ + --secret id=mynetrc,src=$(HOME)/.netrc \ + -t go-spacemesh-bs:$(SHA) \ + -t $(DOCKER_HUB)/$(DOCKER_IMAGE_REPO)-bs:$(DOCKER_IMAGE_VERSION) \ + -f ./bootstrap.Dockerfile . .PHONY: dockerbuild-bs dockerpush-bs: dockerbuild-bs dockerpush-bs-only diff --git a/bootstrap.Dockerfile b/bootstrap.Dockerfile index d5b4bb8f..25a4524d 100644 --- a/bootstrap.Dockerfile +++ b/bootstrap.Dockerfile @@ -6,7 +6,7 @@ COPY Makefile* . COPY go.mod . COPY go.sum . -RUN go mod download +RUN --mount=type=secret,id=mynetrc,dst=/root/.netrc go mod download # copy the rest of the source code COPY . . From 35073705db2236990babe1e8332e9b115005fc7a Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 2 May 2024 08:48:14 +0000 Subject: [PATCH 23/25] Update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1263984..c166db8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ See [RELEASE](./RELEASE.md) for workflow instructions. +## Release v1.5.1-hotfix1 + +This release includes our first CVE fix. A vulnerability was found in the way a node handles incoming ATXs. We urge all +node operators to update to this version as soon as possible. + +### Improvements + +* Fixed a vulnerability in the way a node handles incoming ATXs. This vulnerability allows an attacker to claim rewards + for a full tick amount although they should not be eligible for them. + ## Release v1.5.1 ### Improvements From e99f1d105022f8e2e8dce67d4e1526a010cfa7e0 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 2 May 2024 09:05:37 +0000 Subject: [PATCH 24/25] Update systest docker image --- .github/workflows/systest.yml | 7 +++++++ Makefile | 6 ++++-- systest/Dockerfile | 5 ++++- systest/Makefile | 6 +++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/systest.yml b/.github/workflows/systest.yml index aa81357e..ef25d026 100644 --- a/.github/workflows/systest.yml +++ b/.github/workflows/systest.yml @@ -110,6 +110,13 @@ jobs: shell: bash run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + - uses: extractions/netrc@v2 + with: + machine: github.com + username: ${{ secrets.GH_ACTION_TOKEN_USER }} + password: ${{ secrets.GH_ACTION_TOKEN }} + if: vars.GOPRIVATE + - name: Build tests docker image run: make -C systest docker diff --git a/Makefile b/Makefile index eb6e44a8..42931938 100644 --- a/Makefile +++ b/Makefile @@ -157,7 +157,8 @@ dockerbuild-go: --secret id=mynetrc,src=$(HOME)/.netrc \ --build-arg VERSION=${VERSION} \ -t go-spacemesh:$(SHA) \ - -t $(DOCKER_HUB)/$(DOCKER_IMAGE_REPO):$(DOCKER_IMAGE_VERSION) . + -t $(DOCKER_HUB)/$(DOCKER_IMAGE_REPO):$(DOCKER_IMAGE_VERSION) \ + . .PHONY: dockerbuild-go dockerpush: dockerbuild-go dockerpush-only @@ -176,7 +177,8 @@ dockerbuild-bs: --secret id=mynetrc,src=$(HOME)/.netrc \ -t go-spacemesh-bs:$(SHA) \ -t $(DOCKER_HUB)/$(DOCKER_IMAGE_REPO)-bs:$(DOCKER_IMAGE_VERSION) \ - -f ./bootstrap.Dockerfile . + -f ./bootstrap.Dockerfile \ + . .PHONY: dockerbuild-bs dockerpush-bs: dockerbuild-bs dockerpush-bs-only diff --git a/systest/Dockerfile b/systest/Dockerfile index 2570a4e8..4bd9b073 100644 --- a/systest/Dockerfile +++ b/systest/Dockerfile @@ -11,10 +11,13 @@ COPY Makefile* . RUN make get-libs RUN make go-env-test +# We want to populate the module cache based on the go.{mod,sum} files. COPY go.mod . COPY go.sum . -RUN go mod download +RUN --mount=type=secret,id=mynetrc,dst=/root/.netrc go mod download + +# Here we copy the rest of the source code COPY . . RUN --mount=type=cache,id=build,target=/root/.cache/go-build go test -failfast -v -c -o ./build/tests.test ./systest/tests/ diff --git a/systest/Makefile b/systest/Makefile index 2ad1a9e6..acc13744 100644 --- a/systest/Makefile +++ b/systest/Makefile @@ -40,7 +40,11 @@ command := tests -test.v -test.count=$(count) -test.timeout=0 -test.run=$(test_n .PHONY: docker docker: - @DOCKER_BUILDKIT=1 docker build ../ -f Dockerfile -t $(image_name) + @DOCKER_BUILDKIT=1 docker build \ + --secret id=mynetrc,src=$(HOME)/.netrc \ + -t $(image_name) \ + -f Dockerfile \ + ../ .PHONY: push push: From ed9692f4fa526d30d384df048196967bc07ade6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 24 Apr 2024 21:29:25 +0000 Subject: [PATCH 25/25] Fix `TestPostMalfeasanceProof` systest (#5878) --- .../distributed_post_verification_test.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 345812c3..1e52808a 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -262,15 +262,19 @@ func TestPostMalfeasanceProof(t *testing.T) { }) // 5. Wait for POST malfeasance proof - logger.Info("waiting for malfeasance proof") - err = malfeasanceStream(ctx, cl.Client(0), logger, func(malfeasance *pb.MalfeasanceStreamResponse) (bool, error) { + 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(t, malfeasance.GetProof().GetSmesherId().Id, signer.NodeID().Bytes()) - require.Equal(t, pb.MalfeasanceProof_MALFEASANCE_POST_INDEX, malfeasance.GetProof().GetKind()) + require.Equal(t, malf.GetProof().GetSmesherId().Id, signer.NodeID().Bytes()) + require.Equal(t, pb.MalfeasanceProof_MALFEASANCE_POST_INDEX, malf.GetProof().GetKind()) var proof mwire.MalfeasanceProof - require.NoError(t, codec.Decode(malfeasance.Proof.Proof, &proof)) + require.NoError(t, codec.Decode(malf.Proof.Proof, &proof)) require.Equal(t, mwire.InvalidPostIndex, proof.Proof.Type) invalidPostProof := proof.Proof.Data.(*mwire.InvalidPostIndexProof) logger.Sugar().Infow("malfeasance post proof", "proof", invalidPostProof) @@ -286,10 +290,12 @@ func TestPostMalfeasanceProof(t *testing.T) { Challenge: invalidAtx.NIPost.PostMetadata.Challenge, LabelsPerUnit: invalidAtx.NIPost.PostMetadata.LabelsPerUnit, } - err = verifier.Verify(ctx, (*shared.Proof)(invalidAtx.NIPost.Post), meta) + err = verifier.Verify(awaitCtx, (*shared.Proof)(invalidAtx.NIPost.Post), meta) var invalidIdxError *verifying.ErrInvalidIndex require.ErrorAs(t, err, &invalidIdxError) + receivedProof = true return false, nil }) require.NoError(t, err) + require.True(t, receivedProof, "malfeasance proof not received") }