From 5e6551a0cca606d494c3d92bba38c772cf3b2f09 Mon Sep 17 00:00:00 2001 From: Matthias Fasching <5011972+fasmat@users.noreply.github.com> Date: Tue, 21 May 2024 13:10:14 +0000 Subject: [PATCH] Verify that previous ATX points to correct ATX when handling incoming ATXs (#5927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Motivation This integrates the changes from the CVE fix into the main development branch. Closes https://github.com/spacemeshos/go-spacemesh/issues/5692 Co-authored-by: Bartosz Różański --- .github/workflows/systest.yml | 14 + CHANGELOG.md | 4 + Dockerfile | 2 +- Makefile | 11 +- activation/handler.go | 2 +- activation/handler_test.go | 12 +- activation/handler_v1.go | 262 ++++++--- activation/handler_v1_test.go | 249 ++++++-- activation/validation_test.go | 4 +- activation/verify_state.go | 90 +++ activation/verify_state_test.go | 69 +++ activation/wire/wire_v1.go | 4 - bootstrap.Dockerfile | 2 +- cmd/root.go | 3 + common/types/hashes.go | 3 + common/types/layer.go | 2 +- config/config.go | 3 + config/mainnet.go | 3 +- events/events.go | 2 + go.mod | 2 +- go.sum | 4 +- malfeasance/handler.go | 55 +- malfeasance/handler_test.go | 534 +++++++++++++++--- malfeasance/metrics.go | 5 +- malfeasance/wire/malfeasance.go | 38 +- malfeasance/wire/malfeasance_scale.go | 36 ++ malfeasance/wire/malfeasance_test.go | 94 ++- node/node.go | 12 +- sql/atxs/atxs.go | 105 ++-- sql/atxs/atxs_test.go | 50 ++ sql/blocks/blocks_test.go | 8 +- sql/identities/identities.go | 2 +- .../0017_atxs_prev_id_nonce_placeholder.sql | 1 + systest/Dockerfile | 5 +- systest/Makefile | 6 +- .../distributed_post_verification_test.go | 2 +- 36 files changed, 1428 insertions(+), 272 deletions(-) create mode 100644 activation/verify_state.go create mode 100644 activation/verify_state_test.go diff --git a/.github/workflows/systest.yml b/.github/workflows/systest.yml index 125540740da..ef25d026498 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 @@ -103,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/CHANGELOG.md b/CHANGELOG.md index a1f95a26b27..72f2411d994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,10 @@ Upgrading to this version requires going through v1.5.x first. Removed migration Ensure that your key file in `data/identities` is named `local.key` if you run a supervised node or with the change the node will not start. +* [#5927](https://github.com/spacemeshos/go-spacemesh/pull/5927) Fixed 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.3 ### Improvements diff --git a/Dockerfile b/Dockerfile index f297d93ed08..bd1223a3425 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 a82ae6e9acb..4293193880a 100644 --- a/Makefile +++ b/Makefile @@ -154,9 +154,11 @@ 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) . + -t $(DOCKER_HUB)/$(DOCKER_IMAGE_REPO):$(DOCKER_IMAGE_VERSION) \ + . .PHONY: dockerbuild-go dockerpush: dockerbuild-go dockerpush-only @@ -171,7 +173,12 @@ 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/activation/handler.go b/activation/handler.go index 6354810ec3f..8a3bf8ce24b 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -171,7 +171,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), diff --git a/activation/handler_test.go b/activation/handler_test.go index d0aea3513f8..df8e7b7ceb2 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -227,9 +227,8 @@ func testHandler_PostMalfeasanceProofs(t *testing.T, synced bool) { sig, err := signing.NewEdSigner() require.NoError(t, err) - nodeID := sig.NodeID() - _, err = identities.GetMalfeasanceProof(atxHdlr.cdb, nodeID) + _, err = identities.GetMalfeasanceProof(atxHdlr.cdb, sig.NodeID()) require.ErrorIs(t, err, sql.ErrNotFound) atx := newInitialATXv1(t, goldenATXID) @@ -314,7 +313,7 @@ func TestHandler_ProcessAtxStoresNewVRFNonce(t *testing.T) { require.NoError(t, err) require.Equal(t, types.VRFPostIndex(*atx1.VRFNonce), got) - atx2 := newChainedActivationTxV1(t, goldenATXID, atx1, atx1.ID()) + atx2 := newChainedActivationTxV1(t, atx1, atx1.ID()) nonce2 := types.VRFPostIndex(456) atx2.VRFNonce = (*uint64)(&nonce2) atx2.Sign(sig) @@ -336,7 +335,7 @@ func TestHandler_HandleGossipAtx(t *testing.T) { first := newInitialATXv1(t, goldenATXID) first.Sign(sig) - second := newChainedActivationTxV1(t, goldenATXID, first, first.ID()) + second := newChainedActivationTxV1(t, first, first.ID()) second.Sign(sig) // the poet is missing @@ -639,7 +638,7 @@ func TestHandler_AtxWeight(t *testing.T) { require.Equal(t, leaves/tickSize, stored1.TickHeight()) require.Equal(t, (leaves/tickSize)*units, stored1.GetWeight()) - atx2 := newChainedActivationTxV1(t, goldenATXID, atx1, atx1.ID()) + atx2 := newChainedActivationTxV1(t, atx1, atx1.ID()) atx2.Sign(sig) buf = codec.MustEncode(atx2) @@ -740,7 +739,6 @@ func newInitialATXv1( func newChainedActivationTxV1( t testing.TB, - goldenATXID types.ATXID, prev *wire.ActivationTxV1, pos types.ATXID, ) *wire.ActivationTxV1 { @@ -751,7 +749,7 @@ func newChainedActivationTxV1( NIPostChallengeV1: wire.NIPostChallengeV1{ PrevATXID: prev.ID(), PublishEpoch: prev.PublishEpoch + 1, - PositioningATXID: prev.ID(), + PositioningATXID: pos, }, NIPost: newNIPosV1tWithPoet(t, poetRef.Bytes()), Coinbase: prev.Coinbase, diff --git a/activation/handler_v1.go b/activation/handler_v1.go index 61e3c899b37..a7726005cdc 100644 --- a/activation/handler_v1.go +++ b/activation/handler_v1.go @@ -123,7 +123,7 @@ func (h *HandlerV1) syntacticallyValidate(ctx context.Context, atx *wire.Activat } // Obtain the commitment ATX ID for the given ATX. -func (h *HandlerV1) commitment(ctx context.Context, atx *wire.ActivationTxV1) (types.ATXID, error) { +func (h *HandlerV1) commitment(atx *wire.ActivationTxV1) (types.ATXID, error) { if atx.PrevATXID == types.EmptyATXID { return *atx.CommitmentATXID, nil } @@ -175,7 +175,7 @@ func (h *HandlerV1) syntacticallyValidateDeps( ctx context.Context, atx *wire.ActivationTxV1, ) (leaves uint64, effectiveNumUnits uint32, proof *mwire.MalfeasanceProof, err error) { - commitmentATX, err := h.commitment(ctx, atx) + commitmentATX, err := h.commitment(atx) if err != nil { return 0, 0, nil, fmt.Errorf("commitment atx for %s not found: %w", atx.SmesherID, err) } @@ -330,81 +330,213 @@ func (h *HandlerV1) cacheAtx(ctx context.Context, atx *types.ActivationTx) *atxs return nil } -// storeAtx stores an ATX and notifies subscribers of the ATXID. -func (h *HandlerV1) storeAtx( +// checkDoublePublish verifies if a node has already published an ATX in the same epoch. +func (h *HandlerV1) checkDoublePublish( ctx context.Context, - atx *types.ActivationTx, - signature types.EdSignature, + tx sql.Executor, + atx *wire.ActivationTxV1, ) (*mwire.MalfeasanceProof, error) { - malicious, err := h.cdb.IsMalicious(atx.SmesherID) + prev, err := atxs.GetByEpochAndNodeID(tx, atx.PublishEpoch, atx.SmesherID) + if err != nil && !errors.Is(err, sql.ErrNotFound) { + return nil, err + } + if prev == types.EmptyATXID || prev == atx.ID() { + // no ATX previously published for this epoch, or we are handling the same ATX again + 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) + } + + prevSignature, err := atxSignature(ctx, tx, prev) if err != nil { - return nil, fmt.Errorf("checking if node is malicious: %w", err) + return nil, fmt.Errorf("extracting signature for malfeasance proof: %w", err) } - var proof *mwire.MalfeasanceProof - if 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) - } - return nil + + atxProof := mwire.AtxProof{ + Messages: [2]mwire.AtxProofMsg{{ + InnerMsg: types.ATXMetadata{ + PublishEpoch: atx.PublishEpoch, + MsgHash: prev.Hash32(), + }, + SmesherID: atx.SmesherID, + Signature: prevSignature, + }, { + InnerMsg: types.ATXMetadata{ + PublishEpoch: atx.PublishEpoch, + MsgHash: atx.ID().Hash32(), + }, + SmesherID: atx.SmesherID, + Signature: atx.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.Stringer("previous", prev), + log.Stringer("current", atx.ID()), + ) + + return proof, nil +} + +// checkWrongPrevAtx verifies if the previous ATX referenced in the ATX is correct. +func (h *HandlerV1) checkWrongPrevAtx( + ctx context.Context, + tx sql.Executor, + atx *wire.ActivationTxV1, +) (*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()) + } + + var atx2ID types.ATXID + if atx.PrevATXID == types.EmptyATXID { + // if the ATX references an empty previous ATX, we can just take the initial ATX and create a proof + // that the node referenced the wrong previous ATX + id, err := atxs.GetFirstIDByNodeID(tx, atx.SmesherID) + if err != nil { + return nil, fmt.Errorf("get initial atx id: %w", err) } - prev, err := atxs.GetByEpochAndNodeID(tx, atx.PublishEpoch, atx.SmesherID) - if err != nil && !errors.Is(err, sql.ErrNotFound) { - return err + atx2ID = id + } else { + prev, err := atxs.Get(tx, atx.PrevATXID) + if err != nil { + return nil, fmt.Errorf("get prev atx: %w", err) } - // do ID check to be absolutely sure. - if err == nil && prev != 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, - ) - } - prevSignature, err := atxSignature(ctx, tx, prev) + // 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 + pubEpoch := h.clock.CurrentLayer().GetEpoch() + for pubEpoch > prev.PublishEpoch { + id, err := atxs.PrevIDByNodeID(tx, atx.SmesherID, pubEpoch) if err != nil { - return fmt.Errorf("extracting signature for malfeasance proof: %w", err) + return nil, fmt.Errorf("get prev atx id by node id: %w", err) } - atxProof := mwire.AtxProof{ - Messages: [2]mwire.AtxProofMsg{{ - InnerMsg: types.ATXMetadata{ - PublishEpoch: atx.PublishEpoch, - MsgHash: prev.Hash32(), - }, - SmesherID: atx.SmesherID, - Signature: prevSignature, - }, { - InnerMsg: types.ATXMetadata{ - PublishEpoch: atx.PublishEpoch, - MsgHash: atx.ID().Hash32(), - }, - SmesherID: atx.SmesherID, - Signature: signature, - }}, - } - proof = &mwire.MalfeasanceProof{ - Layer: atx.PublishEpoch.FirstLayer(), - Proof: mwire.Proof{ - Type: mwire.MultipleATXs, - Data: &atxProof, - }, - } - encoded, err := codec.Encode(proof) + atx2, err := atxs.Get(tx, id) if err != nil { - h.log.With().Panic("failed to encode malfeasance proof", log.Err(err)) + return nil, fmt.Errorf("get prev atx: %w", err) } - if err := identities.SetMalicious(tx, atx.SmesherID, encoded, time.Now()); err != nil { - return fmt.Errorf("add malfeasance proof: %w", err) + if atx.ID() != atx2.ID() && atx.PrevATXID == atx2.PrevATXID { + // found an ATX that points to the same previous ATX + atx2ID = id + break } + pubEpoch = atx2.PublishEpoch + } + } + + if atx2ID == types.EmptyATXID { + // 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") + } + + var blob sql.Blob + v, err := atxs.LoadBlob(ctx, tx, atx2ID.Bytes(), &blob) + if err != nil { + return nil, err + } + if v != types.AtxV1 { + // TODO(mafa): update when V2 is introduced + return nil, fmt.Errorf("ATX %s with same prev ATX as %s is not version 1", atx2ID, atx.PrevATXID) + } - h.log.WithContext(ctx).With().Warning("smesher produced more than one atx in the same epoch", - log.Stringer("smesher", atx.SmesherID), - log.Stringer("previous", prev), - log.Object("current", atx), - ) + var watx2 wire.ActivationTxV1 + if err := codec.Decode(blob.Bytes, &watx2); err != nil { + return nil, fmt.Errorf("decoding previous atx: %w", err) + } + + proof := &mwire.MalfeasanceProof{ + Layer: atx.PublishEpoch.FirstLayer(), + Proof: mwire.Proof{ + Type: mwire.InvalidPrevATX, + Data: &mwire.InvalidPrevATXProof{ + Atx1: *atx, + Atx2: watx2, + }, + }, + } + + if err := identities.SetMalicious(tx, atx.SmesherID, codec.MustEncode(proof), 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 +} + +func (h *HandlerV1) checkMalicious( + ctx context.Context, + tx *sql.Tx, + watx *wire.ActivationTxV1, +) (*mwire.MalfeasanceProof, error) { + malicious, err := identities.IsMalicious(tx, watx.SmesherID) + if err != nil { + return nil, fmt.Errorf("checking if node is malicious: %w", err) + } + if malicious { + return nil, nil + } + proof, err := h.checkDoublePublish(ctx, tx, watx) + if proof != nil || err != nil { + return proof, err + } + return h.checkWrongPrevAtx(ctx, tx, watx) +} + +// storeAtx stores an ATX and notifies subscribers of the ATXID. +func (h *HandlerV1) storeAtx( + ctx context.Context, + atx *types.ActivationTx, + watx *wire.ActivationTxV1, +) (*mwire.MalfeasanceProof, error) { + var proof *mwire.MalfeasanceProof + if err := h.cdb.WithTx(ctx, func(tx *sql.Tx) error { + var err error + proof, err = h.checkMalicious(ctx, tx, watx) + if err != nil { + return fmt.Errorf("check malicious: %w", err) } err = atxs.Add(tx, atx) @@ -429,7 +561,6 @@ func (h *HandlerV1) storeAtx( } h.log.WithContext(ctx).With().Debug("finished storing atx in epoch", atx.ID(), atx.PublishEpoch) - return proof, nil } @@ -466,9 +597,8 @@ func (h *HandlerV1) processATX( if err != nil { return nil, fmt.Errorf("atx %s syntactically invalid based on deps: %w", watx.ID(), err) } - if proof != nil { - return proof, err + return proof, nil } if err := h.contextuallyValidateAtx(watx); err != nil { @@ -496,7 +626,7 @@ func (h *HandlerV1) processATX( atx.BaseTickHeight = baseTickHeight atx.TickCount = leaves / h.tickSize - proof, err = h.storeAtx(ctx, atx, watx.Signature) + proof, err = h.storeAtx(ctx, atx, watx) if err != nil { return nil, fmt.Errorf("cannot store atx %s: %w", atx.ShortString(), err) } diff --git a/activation/handler_v1_test.go b/activation/handler_v1_test.go index 7fd48a6b9cb..38658e2ca37 100644 --- a/activation/handler_v1_test.go +++ b/activation/handler_v1_test.go @@ -85,7 +85,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { t.Parallel() atxHdlr, prevAtx, posAtx := setup(t) - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, posAtx.ID()) + atx := newChainedActivationTxV1(t, prevAtx, posAtx.ID()) atx.PositioningATXID = posAtx.ID() atx.Sign(sig) @@ -108,7 +108,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { atxHdlr, prevAtx, posAtx := setup(t) newNonce := *prevAtx.VRFNonce + 100 - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, posAtx.ID()) + atx := newChainedActivationTxV1(t, prevAtx, posAtx.ID()) atx.VRFNonce = &newNonce atx.Sign(sig) @@ -132,7 +132,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { t.Parallel() atxHdlr, prevAtx, posAtx := setup(t) - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, posAtx.ID()) + atx := newChainedActivationTxV1(t, prevAtx, posAtx.ID()) atx.NumUnits = prevAtx.NumUnits - 10 atx.Sign(sig) @@ -153,7 +153,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { t.Parallel() atxHdlr, prevAtx, posAtx := setup(t) - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, posAtx.ID()) + atx := newChainedActivationTxV1(t, prevAtx, posAtx.ID()) atx.NumUnits = prevAtx.NumUnits + 10 atx.Sign(sig) @@ -175,7 +175,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { t.Parallel() atxHdlr, prevAtx, posAtx := setup(t) - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, posAtx.ID()) + atx := newChainedActivationTxV1(t, prevAtx, posAtx.ID()) atx.NumUnits = prevAtx.NumUnits + 10 atx.Sign(sig) @@ -220,7 +220,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { t.Parallel() atxHdlr, prevAtx, posAtx := setup(t) - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, posAtx.ID()) + atx := newChainedActivationTxV1(t, prevAtx, posAtx.ID()) atx.Sign(sig) atxHdlr.mclock.EXPECT().CurrentLayer().Return((atx.PublishEpoch - 2).FirstLayer()) @@ -231,7 +231,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { t.Parallel() atxHdlr, prevAtx, posAtx := setup(t) - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, posAtx.ID()) + atx := newChainedActivationTxV1(t, prevAtx, posAtx.ID()) atx.Sign(sig) atxHdlr.mclock.EXPECT().CurrentLayer().Return(atx.PublishEpoch.FirstLayer()) @@ -248,7 +248,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { t.Parallel() atxHdlr, prevAtx, posAtx := setup(t) - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, posAtx.ID()) + atx := newChainedActivationTxV1(t, prevAtx, posAtx.ID()) atx.Sign(sig) atxHdlr.mclock.EXPECT().CurrentLayer().Return(atx.PublishEpoch.FirstLayer()) @@ -287,7 +287,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { t.Parallel() atxHdlr, prevATX, postAtx := setup(t) - atx := newChainedActivationTxV1(t, goldenATXID, prevATX, postAtx.ID()) + atx := newChainedActivationTxV1(t, prevATX, postAtx.ID()) atx.Sign(sig) atxHdlr.mclock.EXPECT().CurrentLayer().Return(atx.PublishEpoch.FirstLayer()) @@ -343,7 +343,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { t.Parallel() atxHdlr, prevAtx, posAtx := setup(t) - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, posAtx.ID()) + atx := newChainedActivationTxV1(t, prevAtx, posAtx.ID()) atx.PrevATXID = types.EmptyATXID atx.Sign(sig) @@ -431,7 +431,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { t.Parallel() atxHdlr, prevAtx, posAtx := setup(t) - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, posAtx.ID()) + atx := newChainedActivationTxV1(t, prevAtx, posAtx.ID()) atx.NodeID = &types.NodeID{1, 2, 3} atx.Sign(sig) @@ -443,7 +443,7 @@ func TestHandlerV1_SyntacticallyValidateAtx(t *testing.T) { t.Parallel() atxHdlr, prevAtx, posAtx := setup(t) - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, posAtx.ID()) + atx := newChainedActivationTxV1(t, prevAtx, posAtx.ID()) atx.CommitmentATXID = &types.EmptyATXID atx.Sign(sig) @@ -475,7 +475,7 @@ func TestHandler_ContextuallyValidateAtx(t *testing.T) { atxHdlr := newV1TestHandler(t, goldenATXID) prevAtx := newInitialATXv1(t, goldenATXID) - atx := newChainedActivationTxV1(t, goldenATXID, prevAtx, goldenATXID) + atx := newChainedActivationTxV1(t, prevAtx, goldenATXID) err = atxHdlr.contextuallyValidateAtx(atx) require.ErrorIs(t, err, sql.ErrNotFound) @@ -492,14 +492,14 @@ func TestHandler_ContextuallyValidateAtx(t *testing.T) { _, err := atxHdlr.processATX(context.Background(), "", atx0, codec.MustEncode(atx0), time.Now()) require.NoError(t, err) - atx1 := newChainedActivationTxV1(t, goldenATXID, atx0, goldenATXID) + atx1 := newChainedActivationTxV1(t, atx0, goldenATXID) atx1.Sign(sig) atxHdlr.expectAtxV1(atx1, sig.NodeID()) atxHdlr.mockFetch.EXPECT().GetAtxs(gomock.Any(), gomock.Any(), gomock.Any()) _, err = atxHdlr.processATX(context.Background(), "", atx1, codec.MustEncode(atx1), time.Now()) require.NoError(t, err) - atxInvalidPrevious := newChainedActivationTxV1(t, goldenATXID, atx0, goldenATXID) + atxInvalidPrevious := newChainedActivationTxV1(t, atx0, goldenATXID) atxInvalidPrevious.Sign(sig) err = atxHdlr.contextuallyValidateAtx(atxInvalidPrevious) require.EqualError(t, err, "last atx is not the one referenced") @@ -525,7 +525,7 @@ func TestHandler_ContextuallyValidateAtx(t *testing.T) { _, err = atxHdlr.processATX(context.Background(), "", atx1, codec.MustEncode(atx1), time.Now()) require.NoError(t, err) - atxInvalidPrevious := newChainedActivationTxV1(t, goldenATXID, atx0, goldenATXID) + atxInvalidPrevious := newChainedActivationTxV1(t, atx0, goldenATXID) atxInvalidPrevious.Sign(sig) err = atxHdlr.contextuallyValidateAtx(atxInvalidPrevious) require.EqualError(t, err, "last atx is not the one referenced") @@ -543,20 +543,21 @@ func TestHandlerV1_StoreAtx(t *testing.T) { watx := newInitialATXv1(t, goldenATXID) watx.Sign(sig) - vAtx := toAtx(t, watx) - require.NoError(t, err) + atx := toAtx(t, watx) - atxHdlr.mbeacon.EXPECT().OnAtx(vAtx) - atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), vAtx.ID(), gomock.Any()) - proof, err := atxHdlr.storeAtx(context.Background(), vAtx, watx.Signature) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == watx.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(watx.PublishEpoch+1, watx.ID(), gomock.Any()) + proof, err := atxHdlr.storeAtx(context.Background(), atx, watx) require.NoError(t, err) require.Nil(t, proof) - atxFromDb, err := atxs.Get(atxHdlr.cdb, vAtx.ID()) + atxFromDb, err := atxs.Get(atxHdlr.cdb, atx.ID()) require.NoError(t, err) - vAtx.SetReceived(time.Unix(0, vAtx.Received().UnixNano())) - vAtx.AtxBlob = types.AtxBlob{} - require.Equal(t, vAtx, atxFromDb) + atx.SetReceived(time.Unix(0, atx.Received().UnixNano())) + atx.AtxBlob = types.AtxBlob{} + require.Equal(t, atx, atxFromDb) }) t.Run("storing an already known ATX returns no error", func(t *testing.T) { @@ -564,19 +565,49 @@ func TestHandlerV1_StoreAtx(t *testing.T) { watx := newInitialATXv1(t, goldenATXID) watx.Sign(sig) - vAtx := toAtx(t, watx) + atx := toAtx(t, watx) - atxHdlr.mbeacon.EXPECT().OnAtx(vAtx) - atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), vAtx.ID(), gomock.Any()) - proof, err := atxHdlr.storeAtx(context.Background(), vAtx, watx.Signature) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == watx.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(watx.PublishEpoch+1, watx.ID(), gomock.Any()) + proof, err := atxHdlr.storeAtx(context.Background(), atx, watx) require.NoError(t, err) require.Nil(t, proof) - atxHdlr.mbeacon.EXPECT().OnAtx(vAtx) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == watx.ID() + })) // Note: tortoise is not informed about the same ATX again - proof, err = atxHdlr.storeAtx(context.Background(), vAtx, watx.Signature) + proof, err = atxHdlr.storeAtx(context.Background(), atx, watx) + require.NoError(t, err) + require.Nil(t, proof) + }) + + t.Run("stores ATX of malicious identity", func(t *testing.T) { + atxHdlr := newV1TestHandler(t, goldenATXID) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + require.NoError(t, identities.SetMalicious(atxHdlr.cdb, sig.NodeID(), types.RandomBytes(10), time.Now())) + + watx := newInitialATXv1(t, goldenATXID) + watx.Sign(sig) + atx := toAtx(t, watx) + + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == watx.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(watx.PublishEpoch+1, watx.ID(), gomock.Any()) + proof, err := atxHdlr.storeAtx(context.Background(), atx, watx) require.NoError(t, err) require.Nil(t, proof) + + atxFromDb, err := atxs.Get(atxHdlr.cdb, atx.ID()) + require.NoError(t, err) + atx.SetReceived(time.Unix(0, atx.Received().UnixNano())) + atx.AtxBlob = types.AtxBlob{} + require.Equal(t, atx, atxFromDb) }) t.Run("another atx for the same epoch is considered malicious", func(t *testing.T) { @@ -584,25 +615,27 @@ func TestHandlerV1_StoreAtx(t *testing.T) { watx0 := newInitialATXv1(t, goldenATXID) watx0.Sign(sig) - vAtx0 := toAtx(t, watx0) + atx0 := toAtx(t, watx0) - atxHdlr.mbeacon.EXPECT().OnAtx(vAtx0) - atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), vAtx0.ID(), gomock.Any()) - - proof, err := atxHdlr.storeAtx(context.Background(), vAtx0, watx0.Signature) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == watx0.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(watx0.PublishEpoch+1, watx0.ID(), gomock.Any()) + proof, err := atxHdlr.storeAtx(context.Background(), atx0, watx0) require.NoError(t, err) require.Nil(t, proof) watx1 := newInitialATXv1(t, goldenATXID) watx1.Coinbase = types.GenerateAddress([]byte("aaaa")) watx1.Sign(sig) - vAtx1 := toAtx(t, watx1) + atx1 := toAtx(t, watx1) - atxHdlr.mbeacon.EXPECT().OnAtx(vAtx1) - atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), vAtx1.ID(), gomock.Any()) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == watx1.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(watx1.PublishEpoch+1, watx1.ID(), gomock.Any()) atxHdlr.mtortoise.EXPECT().OnMalfeasance(sig.NodeID()) - - proof, err = atxHdlr.storeAtx(context.Background(), vAtx1, watx1.Signature) + proof, err = atxHdlr.storeAtx(context.Background(), atx1, watx1) require.NoError(t, err) require.NotNil(t, proof) require.Equal(t, mwire.MultipleATXs, proof.Proof.Type) @@ -630,21 +663,22 @@ func TestHandlerV1_StoreAtx(t *testing.T) { watx0 := newInitialATXv1(t, goldenATXID) watx0.Sign(sig) - vAtx0 := toAtx(t, watx0) + atx0 := toAtx(t, watx0) - atxHdlr.mbeacon.EXPECT().OnAtx(vAtx0) - atxHdlr.mtortoise.EXPECT().OnAtx(gomock.Any(), vAtx0.ID(), gomock.Any()) - - proof, err := atxHdlr.storeAtx(context.Background(), vAtx0, watx0.Signature) + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == watx0.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(watx0.PublishEpoch+1, watx0.ID(), gomock.Any()) + proof, err := atxHdlr.storeAtx(context.Background(), atx0, watx0) require.NoError(t, err) require.Nil(t, proof) watx1 := newInitialATXv1(t, goldenATXID) watx1.Coinbase = types.GenerateAddress([]byte("aaaa")) watx1.Sign(sig) - vAtx1 := toAtx(t, watx1) + atx1 := toAtx(t, watx1) - proof, err = atxHdlr.storeAtx(context.Background(), vAtx1, watx1.Signature) + proof, err = atxHdlr.storeAtx(context.Background(), atx1, watx1) require.ErrorContains(t, err, fmt.Sprintf("%s already published an ATX", sig.NodeID().ShortString()), @@ -655,6 +689,125 @@ func TestHandlerV1_StoreAtx(t *testing.T) { require.NoError(t, err) require.False(t, malicious) }) + + t.Run("another atx with the same prevatx is considered malicious", func(t *testing.T) { + atxHdlr := newV1TestHandler(t, goldenATXID) + + initialATX := newInitialATXv1(t, goldenATXID) + initialATX.Sign(sig) + wInitialATX := toAtx(t, initialATX) + + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == initialATX.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(initialATX.PublishEpoch+1, initialATX.ID(), gomock.Any()) + proof, err := atxHdlr.storeAtx(context.Background(), wInitialATX, initialATX) + require.NoError(t, err) + require.Nil(t, proof) + + // valid first non-initial ATX + watx1 := newChainedActivationTxV1(t, initialATX, goldenATXID) + watx1.Sign(sig) + atx1 := toAtx(t, watx1) + + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == watx1.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(watx1.PublishEpoch+1, watx1.ID(), gomock.Any()) + proof, err = atxHdlr.storeAtx(context.Background(), atx1, watx1) + require.NoError(t, err) + require.Nil(t, proof) + + watx2 := newChainedActivationTxV1(t, watx1, goldenATXID) + watx2.Sign(sig) + atx2 := toAtx(t, watx2) + + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == watx2.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(watx2.PublishEpoch+1, watx2.ID(), gomock.Any()) + proof, err = atxHdlr.storeAtx(context.Background(), atx2, watx2) + require.NoError(t, err) + require.Nil(t, proof) + + // third non-initial ATX references initial ATX as prevATX + watx3 := newChainedActivationTxV1(t, initialATX, goldenATXID) + watx3.PublishEpoch = watx2.PublishEpoch + 1 + watx3.Sign(sig) + atx3 := toAtx(t, watx3) + + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == watx3.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(watx3.PublishEpoch+1, watx3.ID(), gomock.Any()) + atxHdlr.mtortoise.EXPECT().OnMalfeasance(sig.NodeID()) + atxHdlr.mclock.EXPECT().CurrentLayer().Return(watx3.PublishEpoch.FirstLayer()) + proof, err = atxHdlr.storeAtx(context.Background(), atx3, watx3) + require.NoError(t, err) + require.NotNil(t, proof) + require.Equal(t, mwire.InvalidPrevATX, proof.Proof.Type) + 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) + }) + + t.Run("another atx with the same prevatx for registered ID doesn't create a malfeasance proof", func(t *testing.T) { + atxHdlr := newV1TestHandler(t, goldenATXID) + atxHdlr.Register(sig) + + // Act & Assert + wInitialATX := newInitialATXv1(t, goldenATXID) + wInitialATX.Sign(sig) + initialAtx := toAtx(t, wInitialATX) + + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == wInitialATX.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(wInitialATX.PublishEpoch+1, wInitialATX.ID(), gomock.Any()) + proof, err := atxHdlr.storeAtx(context.Background(), initialAtx, wInitialATX) + require.NoError(t, err) + require.Nil(t, proof) + + // valid first non-initial ATX + watx1 := newChainedActivationTxV1(t, wInitialATX, goldenATXID) + watx1.Sign(sig) + atx1 := toAtx(t, watx1) + + atxHdlr.mbeacon.EXPECT().OnAtx(gomock.Cond(func(atx any) bool { + return atx.(*types.ActivationTx).ID() == watx1.ID() + })) + atxHdlr.mtortoise.EXPECT().OnAtx(watx1.PublishEpoch+1, watx1.ID(), gomock.Any()) + proof, err = atxHdlr.storeAtx(context.Background(), atx1, watx1) + require.NoError(t, err) + require.Nil(t, proof) + + // second non-initial ATX references empty as prevATX + watx2 := newInitialATXv1(t, goldenATXID) + watx2.PublishEpoch = watx1.PublishEpoch + 1 + watx2.Sign(sig) + atx2 := toAtx(t, watx2) + + proof, err = atxHdlr.storeAtx(context.Background(), atx2, watx2) + require.ErrorContains(t, + err, + fmt.Sprintf("%s referenced incorrect previous ATX", sig.NodeID().ShortString()), + ) + require.Nil(t, proof) + + malicious, err := identities.IsMalicious(atxHdlr.cdb, sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) } func TestHandlerV1_RegistersHashesInPeer(t *testing.T) { diff --git a/activation/validation_test.go b/activation/validation_test.go index f5b648d9a2b..13af35c306d 100644 --- a/activation/validation_test.go +++ b/activation/validation_test.go @@ -489,7 +489,7 @@ func TestVerifyChainDeps(t *testing.T) { require.NoError(t, atxs.Add(db, vInvalidAtx)) t.Run("invalid prev ATX", func(t *testing.T) { - atx := newChainedActivationTxV1(t, goldenATXID, invalidAtx, goldenATXID) + atx := newChainedActivationTxV1(t, invalidAtx, goldenATXID) atx.Sign(signer) vAtx := toAtx(t, atx) require.NoError(t, atxs.Add(db, vAtx)) @@ -602,7 +602,7 @@ func TestVerifyChainDepsAfterCheckpoint(t *testing.T) { Coinbase: vCheckpointedAtx.Coinbase, })) - atx := newChainedActivationTxV1(t, goldenATXID, checkpointedAtx, checkpointedAtx.ID()) + atx := newChainedActivationTxV1(t, checkpointedAtx, checkpointedAtx.ID()) atx.Sign(signer) vAtx := toAtx(t, atx) require.NoError(t, atxs.Add(db, vAtx)) diff --git a/activation/verify_state.go b/activation/verify_state.go new file mode 100644 index 00000000000..3300444c847 --- /dev/null +++ b/activation/verify_state.go @@ -0,0 +1,90 @@ +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/log" + "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: + } + + 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) + } + + if malicious { + // already malicious no need to generate proof + continue + } + + // we are ignoring the ATX version, because as of now we only have V1 + // by the time we have V2, this function is not needed anymore and can be deleted + var blob sql.Blob + var atx1 awire.ActivationTxV1 + 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 + 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.PublishEpoch.FirstLayer(), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: atx1, + Atx2: atx2, + }, + }, + } + + encodedProof := codec.MustEncode(proof) + if err := identities.SetMalicious(db, collision.NodeID1, encodedProof, time.Now()); err != nil { + return fmt.Errorf("add malfeasance proof: %w", err) + } + + 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 00000000000..b0e46a78c84 --- /dev/null +++ b/activation/verify_state_test.go @@ -0,0 +1,69 @@ +package activation + +import ( + "context" + "math/rand/v2" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + + "github.com/spacemeshos/go-spacemesh/activation/wire" + "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 := newInitialATXv1(t, goldenATXID, func(atx *wire.ActivationTxV1) { + atx.PrevATXID = prevATXID + atx.PublishEpoch = 2 + }) + atx1.Sign(sig) + vAtx1 := toAtx(t, atx1) + require.NoError(t, atxs.Add(db, vAtx1)) + + atx2 := newInitialATXv1(t, goldenATXID, func(atx *wire.ActivationTxV1) { + atx.PrevATXID = prevATXID + atx.PublishEpoch = 3 + }) + atx2.Sign(sig) + vAtx2 := toAtx(t, atx2) + require.NoError(t, atxs.Add(db, vAtx2)) + + // create 100 random ATXs that are not malicious + for i := 0; i < 100; i++ { + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + atx := newInitialATXv1(t, types.RandomATXID(), func(atx *wire.ActivationTxV1) { + atx.PrevATXID = types.RandomATXID() + atx.NumUnits = rand.Uint32() + atx.PublishEpoch = rand.N[types.EpochID](100) + }) + atx.Sign(otherSig) + vAtx := toAtx(t, atx) + require.NoError(t, atxs.Add(db, vAtx)) + } + + // 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/activation/wire/wire_v1.go b/activation/wire/wire_v1.go index 941ad13ee38..35bcebdc7fb 100644 --- a/activation/wire/wire_v1.go +++ b/activation/wire/wire_v1.go @@ -84,10 +84,6 @@ func (atx *ActivationTxV1) SetID(id types.ATXID) { atx.id = id } -func (atx *ActivationTxV1) Smesher() types.NodeID { - return atx.SmesherID -} - func (atx *ActivationTxV1) Sign(signer *signing.EdSigner) { if atx.PrevATXID == types.EmptyATXID { nodeID := signer.NodeID() diff --git a/bootstrap.Dockerfile b/bootstrap.Dockerfile index d5b4bb8f75e..25a4524d46c 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 . . diff --git a/cmd/root.go b/cmd/root.go index e5ba174e795..2b73faa34ae 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/common/types/hashes.go b/common/types/hashes.go index c01c748dbd1..5590541d9c4 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 d88dcb597df..f760227fcb4 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/config/config.go b/config/config.go index 0a408945139..673d497bb73 100644 --- a/config/config.go +++ b/config/config.go @@ -119,6 +119,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 fdd17cec03f..5c98f0248c9 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/events/events.go b/events/events.go index 7501bf733b0..30107a733b8 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/go.mod b/go.mod index f88ee60b188..e08817fd528 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/rs/cors v1.11.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/seehuhn/mt19937 v1.0.0 - github.com/spacemeshos/api/release/go v1.39.0 + github.com/spacemeshos/api/release/go v1.40.0 github.com/spacemeshos/economics v0.1.3 github.com/spacemeshos/fixed v0.1.1 github.com/spacemeshos/go-scale v1.2.0 diff --git a/go.sum b/go.sum index 4499ddf88e5..0e3f3df769d 100644 --- a/go.sum +++ b/go.sum @@ -601,8 +601,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.39.0 h1:LfvCDvyyakm4MMk8J8kWC+XlF3PMEj/bJlqoHVszrbQ= -github.com/spacemeshos/api/release/go v1.39.0/go.mod h1:khlc+6+zDFlVaoAVLnBQoD/UqsPQ5Rrsrnb++NFR3gw= +github.com/spacemeshos/api/release/go v1.40.0 h1:pMbvppSzfoN7TlcLn2y4ervm8pCBhhVIT4flX3v6wI4= +github.com/spacemeshos/api/release/go v1.40.0/go.mod h1:i7TMrpz5l0Swa9OclV2FBaqbn7l1/gEiYhlSl3AWA5c= 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= diff --git a/malfeasance/handler.go b/malfeasance/handler.go index aca29824a17..017749568a3 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -86,7 +86,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 +187,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,10 +214,12 @@ func updateMetrics(tp wire.Proof) { numProofsBallot.Inc() case wire.InvalidPostIndex: numProofsPostIndex.Inc() + case wire.InvalidPrevATX: + numProofsPrevATX.Inc() } } -func checkIdentityExists(db sql.Executor, nodeID types.NodeID) error { +func hasPublishedAtxs(db sql.Executor, nodeID types.NodeID) error { _, err := atxs.GetLastIDByNodeID(db, nodeID) if err != nil { if errors.Is(err, sql.ErrNotFound) { @@ -252,7 +257,7 @@ func validateHareEquivocation( return types.EmptyNodeID, errors.New("invalid signature") } if firstNid == types.EmptyNodeID { - if err := checkIdentityExists(db, msg.SmesherID); err != nil { + if err := hasPublishedAtxs(db, msg.SmesherID); err != nil { return types.EmptyNodeID, fmt.Errorf("check identity in hare malfeasance %v: %w", msg.SmesherID, err) } firstNid = msg.SmesherID @@ -303,7 +308,7 @@ func validateMultipleATXs( return types.EmptyNodeID, errors.New("invalid signature") } if firstNid == types.EmptyNodeID { - if err := checkIdentityExists(db, msg.SmesherID); err != nil { + if err := hasPublishedAtxs(db, msg.SmesherID); err != nil { return types.EmptyNodeID, fmt.Errorf("check identity in atx malfeasance %v: %w", msg.SmesherID, err) } firstNid = msg.SmesherID @@ -354,7 +359,7 @@ func validateMultipleBallots( return types.EmptyNodeID, errors.New("invalid signature") } if firstNid == types.EmptyNodeID { - if err = checkIdentityExists(db, msg.SmesherID); err != nil { + if err = hasPublishedAtxs(db, msg.SmesherID); err != nil { return types.EmptyNodeID, fmt.Errorf("check identity in ballot malfeasance %v: %w", msg.SmesherID, err) } firstNid = msg.SmesherID @@ -377,7 +382,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, @@ -385,6 +391,7 @@ func validateInvalidPostIndex(ctx context.Context, proof *wire.InvalidPostIndexProof, ) (types.NodeID, error) { atx := &proof.Atx + if !edVerifier.Verify(signing.ATX, atx.SmesherID, atx.SignedBytes(), atx.Signature) { return types.EmptyNodeID, errors.New("invalid signature") } @@ -415,3 +422,39 @@ 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 err := hasPublishedAtxs(db, atx1.SmesherID); err != nil { + return types.EmptyNodeID, fmt.Errorf("check identity %v in invalid previous ATX: %w", atx1.SmesherID, err) + } + + if !edVerifier.Verify(signing.ATX, atx1.SmesherID, atx1.SignedBytes(), atx1.Signature) { + return types.EmptyNodeID, errors.New("atx1: invalid signature") + } + + atx2 := proof.Atx2 + if atx1.SmesherID != atx2.SmesherID { + numInvalidProofsPrevATX.Inc() + return types.EmptyNodeID, errors.New("invalid old prev ATX malfeasance proof: smesher IDs are different") + } + + if !edVerifier.Verify(signing.ATX, atx2.SmesherID, atx2.SignedBytes(), atx2.Signature) { + return types.EmptyNodeID, errors.New("atx2: invalid signature") + } + + if atx1.ID() == atx2.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 ee5aba93054..f5dd2174cf6 100644 --- a/malfeasance/handler_test.go +++ b/malfeasance/handler_test.go @@ -33,15 +33,18 @@ func TestMain(m *testing.M) { os.Exit(res) } -func createIdentity(t *testing.T, db *sql.Database, sig *signing.EdSigner) { - challenge := types.NIPostChallenge{ +func createIdentity(tb testing.TB, db sql.Executor, sig *signing.EdSigner) { + tb.Helper() + atx := &types.ActivationTx{ PublishEpoch: types.EpochID(1), + Coinbase: types.Address{}, + NumUnits: 1, + SmesherID: sig.NodeID(), } - atx := types.NewActivationTx(challenge, types.Address{}, 1) - atx.SmesherID = sig.NodeID() atx.SetReceived(time.Now()) + atx.SetID(types.RandomATXID()) atx.TickCount = 1 - require.NoError(t, atxs.Add(db, atx)) + require.NoError(tb, atxs.Add(db, atx)) } func TestHandler_HandleMalfeasanceProof_multipleATXs(t *testing.T) { @@ -1073,41 +1076,70 @@ func TestHandler_HandleSyncedMalfeasanceProof_wrongHash(t *testing.T) { require.True(t, malicious) } -func TestHandler_HandleMalfeasanceProof_InvalidPostIndex(t *testing.T) { +type testMalfeasanceHandler struct { + *malfeasance.Handler + + db *sql.Database + sig *signing.EdSigner + + mPostVerifier *malfeasance.MockpostVerifier + mTortoise *malfeasance.Mocktortoise +} + +func newTestMalfeasanceHandler(t testing.TB) *testMalfeasanceHandler { + db := sql.InMemory() + sig, err := signing.NewEdSigner() require.NoError(t, err) - nodeIdH32 := types.Hash32(sig.NodeID()) - id := sig.NodeID() - atx := awire.ActivationTxV1{ - InnerActivationTxV1: awire.InnerActivationTxV1{ - NIPostChallengeV1: awire.NIPostChallengeV1{ - CommitmentATXID: &types.ATXID{1, 2, 3}, - }, - NIPost: &awire.NIPostV1{ - Post: &awire.PostV1{}, - PostMetadata: &awire.PostMetadataV1{}, - }, - }, - SmesherID: id, + + createIdentity(t, db, sig) + + 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, + ) + + return &testMalfeasanceHandler{ + Handler: h, + + db: db, + sig: sig, + + mPostVerifier: postVerifier, + mTortoise: trt, } - atx.Signature = sig.Sign(signing.ATX, atx.SignedBytes()) +} +func TestHandler_HandleSyncedMalfeasanceProof_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)) - - h := malfeasance.NewHandler( - datastore.NewCachedDB(db, lg), - lg, - "self", - []types.NodeID{types.RandomNodeID()}, - signing.NewEdVerifier(), - trt, - postVerifier, - ) + h := newTestMalfeasanceHandler(t) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + atx := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + CommitmentATXID: &types.ATXID{1, 2, 3}, + }, + NIPost: &awire.NIPostV1{ + Post: &awire.PostV1{}, + PostMetadata: &awire.PostMetadataV1{}, + }, + }, + SmesherID: sig.NodeID(), + } + atx.Signature = sig.Sign(signing.ATX, atx.SignedBytes()) proof := wire.MalfeasanceProof{ Layer: types.LayerID(11), Proof: wire.Proof{ @@ -1119,33 +1151,38 @@ func TestHandler_HandleMalfeasanceProof_InvalidPostIndex(t *testing.T) { }, } - postVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + h.mPostVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(errors.New("invalid")) - trt.EXPECT().OnMalfeasance(sig.NodeID()) - err := h.HandleSyncedMalfeasanceProof(context.Background(), nodeIdH32, "peer", codec.MustEncode(&proof)) - require.NoError(t, err) + h.mTortoise.EXPECT().OnMalfeasance(sig.NodeID()) + err = h.HandleSyncedMalfeasanceProof( + context.Background(), + types.Hash32(h.sig.NodeID()), + "peer", + codec.MustEncode(&proof), + ) + require.ErrorIs(t, err, pubsub.ErrValidationReject) - malicious, err := identities.IsMalicious(db, sig.NodeID()) + malicious, err := identities.IsMalicious(h.db, sig.NodeID()) require.NoError(t, err) require.True(t, malicious) }) t.Run("invalid malfeasance proof (POST valid)", func(t *testing.T) { - db := sql.InMemory() - lg := logtest.New(t) - trt := malfeasance.NewMocktortoise(gomock.NewController(t)) - postVerifier := malfeasance.NewMockpostVerifier(gomock.NewController(t)) - - h := malfeasance.NewHandler( - datastore.NewCachedDB(db, lg), - lg, - "self", - []types.NodeID{types.RandomNodeID()}, - signing.NewEdVerifier(), - trt, - postVerifier, - ) + h := newTestMalfeasanceHandler(t) + atx := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + CommitmentATXID: &types.ATXID{1, 2, 3}, + }, + NIPost: &awire.NIPostV1{ + Post: &awire.PostV1{}, + PostMetadata: &awire.PostMetadataV1{}, + }, + }, + SmesherID: h.sig.NodeID(), + } + atx.Signature = h.sig.Sign(signing.ATX, atx.SignedBytes()) proof := wire.MalfeasanceProof{ Layer: types.LayerID(11), Proof: wire.Proof{ @@ -1157,32 +1194,36 @@ func TestHandler_HandleMalfeasanceProof_InvalidPostIndex(t *testing.T) { }, } - postVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - err := h.HandleSyncedMalfeasanceProof(context.Background(), nodeIdH32, "peer", codec.MustEncode(&proof)) + h.mPostVerifier.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + err := h.HandleSyncedMalfeasanceProof( + context.Background(), + types.Hash32(h.sig.NodeID()), + "peer", + codec.MustEncode(&proof), + ) require.ErrorIs(t, err, pubsub.ErrValidationReject) - malicious, err := identities.IsMalicious(db, sig.NodeID()) + malicious, err := identities.IsMalicious(h.db, h.sig.NodeID()) require.NoError(t, err) require.False(t, malicious) }) t.Run("invalid malfeasance proof (ATX signature invalid)", func(t *testing.T) { - db := sql.InMemory() - lg := logtest.New(t) - trt := malfeasance.NewMocktortoise(gomock.NewController(t)) - postVerifier := malfeasance.NewMockpostVerifier(gomock.NewController(t)) - - h := malfeasance.NewHandler( - datastore.NewCachedDB(db, lg), - lg, - "self", - []types.NodeID{types.RandomNodeID()}, - signing.NewEdVerifier(), - trt, - postVerifier, - ) + h := newTestMalfeasanceHandler(t) - atx := atx + atx := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + CommitmentATXID: &types.ATXID{1, 2, 3}, + }, + NIPost: &awire.NIPostV1{ + Post: &awire.PostV1{}, + PostMetadata: &awire.PostMetadataV1{}, + }, + }, + SmesherID: h.sig.NodeID(), + } + atx.Signature = h.sig.Sign(signing.ATX, atx.SignedBytes()) atx.NIPost.Post.Pow += 1 // invalidate signature by changing content proof := wire.MalfeasanceProof{ @@ -1196,11 +1237,358 @@ func TestHandler_HandleMalfeasanceProof_InvalidPostIndex(t *testing.T) { }, } - err := h.HandleSyncedMalfeasanceProof(context.Background(), nodeIdH32, "peer", codec.MustEncode(&proof)) + err := h.HandleSyncedMalfeasanceProof( + context.Background(), + types.Hash32(h.sig.NodeID()), + "peer", + codec.MustEncode(&proof), + ) require.ErrorIs(t, err, pubsub.ErrValidationReject) require.ErrorContains(t, err, "invalid signature") - malicious, err := identities.IsMalicious(db, sig.NodeID()) + malicious, err := identities.IsMalicious(h.db, h.sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) +} + +func TestHandler_HandleSyncedMalfeasanceProof_InvalidPrevATX(t *testing.T) { + t.Run("valid malfeasance proof", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + prevATXID := types.RandomATXID() + + atx1 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: prevATXID, + PublishEpoch: types.EpochID(2), + }, + }, + } + atx1.Sign(h.sig) + + atx2 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: prevATXID, + PublishEpoch: types.EpochID(3), + }, + }, + } + atx2.Sign(h.sig) + + proof := wire.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: atx1, + Atx2: atx2, + }, + }, + } + + h.mTortoise.EXPECT().OnMalfeasance(h.sig.NodeID()) + err := h.HandleSyncedMalfeasanceProof( + context.Background(), + types.Hash32(h.sig.NodeID()), + "peer", + codec.MustEncode(&proof), + ) + require.NoError(t, err) + + malicious, err := identities.IsMalicious(h.db, h.sig.NodeID()) + require.NoError(t, err) + require.True(t, malicious) + }) + + t.Run("unknown identity", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + prevATXID := types.RandomATXID() + + atx1 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: prevATXID, + PublishEpoch: types.EpochID(2), + }, + }, + } + atx1.Sign(sig) + + atx2 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: prevATXID, + PublishEpoch: types.EpochID(3), + }, + }, + } + atx2.Sign(sig) + + proof := wire.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: atx1, + Atx2: atx2, + }, + }, + } + + err = h.HandleSyncedMalfeasanceProof( + context.Background(), + types.Hash32(h.sig.NodeID()), + "peer", + codec.MustEncode(&proof), + ) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + + malicious, err := identities.IsMalicious(h.db, h.sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) + + t.Run("invalid malfeasance proof (invalid signature for first)", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + prevATXID := types.RandomATXID() + + atx1 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: prevATXID, + PublishEpoch: types.EpochID(2), + }, + }, + } + atx1.Signature = types.EdSignature(types.RandomBytes(64)) + atx1.SmesherID = h.sig.NodeID() + + atx2 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: prevATXID, + PublishEpoch: types.EpochID(3), + }, + }, + } + atx2.Sign(h.sig) + + proof := wire.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: atx1, + Atx2: atx2, + }, + }, + } + + err := h.HandleSyncedMalfeasanceProof( + context.Background(), + types.Hash32(h.sig.NodeID()), + "peer", + codec.MustEncode(&proof), + ) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + require.ErrorContains(t, err, "invalid signature") + + malicious, err := identities.IsMalicious(h.db, h.sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) + + t.Run("invalid malfeasance proof (invalid signature for second)", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + prevATXID := types.RandomATXID() + + atx1 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: prevATXID, + PublishEpoch: types.EpochID(2), + }, + }, + } + atx1.Sign(h.sig) + + atx2 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: prevATXID, + PublishEpoch: types.EpochID(3), + }, + }, + } + atx2.Signature = types.EdSignature(types.RandomBytes(64)) + atx2.SmesherID = h.sig.NodeID() + + proof := wire.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: atx1, + Atx2: atx2, + }, + }, + } + + err := h.HandleSyncedMalfeasanceProof( + context.Background(), + types.Hash32(h.sig.NodeID()), + "peer", + codec.MustEncode(&proof), + ) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + require.ErrorContains(t, err, "invalid signature") + + malicious, err := identities.IsMalicious(h.db, h.sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) + + t.Run("invalid malfeasance proof (same ATX)", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + atx := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: types.RandomATXID(), + PublishEpoch: types.EpochID(2), + }, + }, + } + atx.Sign(h.sig) + + proof := wire.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: atx, + Atx2: atx, + }, + }, + } + + err := h.HandleSyncedMalfeasanceProof( + context.Background(), + types.Hash32(h.sig.NodeID()), + "peer", + codec.MustEncode(&proof), + ) + require.ErrorContains(t, err, "ATX IDs are the same") + + malicious, err := identities.IsMalicious(h.db, h.sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) + + t.Run("invalid malfeasance proof (prev ATXs differ)", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + atx1 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: types.RandomATXID(), + PublishEpoch: types.EpochID(2), + }, + }, + } + atx1.Sign(h.sig) + + atx2 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: atx1.ID(), + PublishEpoch: types.EpochID(3), + }, + }, + } + atx2.Sign(h.sig) + + proof := wire.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: atx1, + Atx2: atx2, + }, + }, + } + + err := h.HandleSyncedMalfeasanceProof( + context.Background(), + types.Hash32(h.sig.NodeID()), + "peer", + codec.MustEncode(&proof), + ) + require.ErrorContains(t, err, "prev ATX IDs are different") + + malicious, err := identities.IsMalicious(h.db, h.sig.NodeID()) + require.NoError(t, err) + require.False(t, malicious) + }) + + t.Run("invalid malfeasance proof (ATXs by different identities)", func(t *testing.T) { + h := newTestMalfeasanceHandler(t) + + sig2, err := signing.NewEdSigner() + require.NoError(t, err) + createIdentity(t, h.db, sig2) + + prevATXID := types.RandomATXID() + + atx1 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: prevATXID, + PublishEpoch: types.EpochID(2), + }, + }, + } + atx1.Sign(h.sig) + + atx2 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PrevATXID: prevATXID, + PublishEpoch: types.EpochID(3), + }, + }, + } + atx2.Sign(sig2) + + proof := wire.MalfeasanceProof{ + Layer: types.LayerID(11), + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: atx1, + Atx2: atx2, + }, + }, + } + + err = h.HandleSyncedMalfeasanceProof( + context.Background(), + types.Hash32(h.sig.NodeID()), + "peer", + codec.MustEncode(&proof), + ) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + + malicious, err := identities.IsMalicious(h.db, h.sig.NodeID()) require.NoError(t, err) require.False(t, malicious) }) diff --git a/malfeasance/metrics.go b/malfeasance/metrics.go index 8e2eccfae3b..7592c33c0f6 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 51e5e979808..b787e65f728 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 { @@ -75,6 +76,15 @@ func (mp *MalfeasanceProof) MarshalLogObject(encoder log.ObjectEncoder) error { 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 { + encoder.AddString("atx1_id", p.Atx1.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") } @@ -152,6 +162,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") } @@ -291,6 +309,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)) @@ -366,6 +391,17 @@ func MalfeasanceInfo(smesher types.NodeID, mp *MalfeasanceProof) string { p.Atx.PublishEpoch, )) } + case InvalidPrevATX: + p, ok := mp.Proof.Data.(*InvalidPrevATXProof) + if ok { + b.WriteString( + fmt.Sprintf( + "cause: smesher published ATX %s with invalid previous ATX %s in epoch %d\n", + p.Atx1.ID().ShortString(), + p.Atx2.ID().ShortString(), + p.Atx1.PublishEpoch, + )) + } } return b.String() } diff --git a/malfeasance/wire/malfeasance_scale.go b/malfeasance/wire/malfeasance_scale.go index 625292f0195..3ec88a1acca 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 3972bc4cc6a..b367d24ee0b 100644 --- a/malfeasance/wire/malfeasance_test.go +++ b/malfeasance/wire/malfeasance_test.go @@ -8,9 +8,11 @@ import ( "github.com/spacemeshos/go-scale/tester" "github.com/stretchr/testify/require" + 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) { @@ -180,9 +182,89 @@ func Test_HareMetadata_Equivocation(t *testing.T) { require.False(t, hm1.Equivocation(&hm2)) } +func TestCodec_InvalidPostIndex(t *testing.T) { + lid := types.LayerID(11) + + atx := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PublishEpoch: lid.GetEpoch(), + }, + Coinbase: types.Address{1, 2, 3}, + NumUnits: 10, + }, + } + + proof := &wire.MalfeasanceProof{ + Layer: lid, + Proof: wire.Proof{ + Type: wire.InvalidPostIndex, + Data: &wire.InvalidPostIndexProof{ + Atx: atx, + InvalidIdx: 5, + }, + }, + } + encoded := codec.MustEncode(proof) + + var decoded wire.MalfeasanceProof + codec.MustDecode(encoded, &decoded) + require.Equal(t, *proof, decoded) +} + +func TestCodec_InvalidPrevATX(t *testing.T) { + lid := types.LayerID(45) + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + prevATXID := types.RandomATXID() + + atx1 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PublishEpoch: lid.GetEpoch() - 1, + PrevATXID: prevATXID, + }, + Coinbase: types.Address{1, 2, 3}, + NumUnits: 10, + }, + } + atx1.Sign(sig) + + atx2 := awire.ActivationTxV1{ + InnerActivationTxV1: awire.InnerActivationTxV1{ + NIPostChallengeV1: awire.NIPostChallengeV1{ + PublishEpoch: lid.GetEpoch(), + PrevATXID: prevATXID, + }, + Coinbase: types.Address{1, 2, 3}, + NumUnits: 10, + }, + } + atx2.Sign(sig) + + proof := &wire.MalfeasanceProof{ + Layer: lid, + Proof: wire.Proof{ + Type: wire.InvalidPrevATX, + Data: &wire.InvalidPrevATXProof{ + Atx1: atx1, + Atx2: atx2, + }, + }, + } + 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 FuzzProofConsistency(f *testing.F) { tester.FuzzConsistency[wire.Proof](f, func(p *wire.Proof, c fuzz.Continue) { - switch c.Intn(3) { + switch c.Intn(5) { case 0: p.Type = wire.MultipleATXs data := wire.AtxProof{} @@ -198,6 +280,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/node/node.go b/node/node.go index 47581f00374..507b7c35d86 100644 --- a/node/node.go +++ b/node/node.go @@ -1901,6 +1901,15 @@ func (app *App) setupDBs(ctx context.Context, lg log.Log) error { datastore.WithConsensusCache(data), ) + 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))) + } + migrations, err = sql.LocalMigrations() if err != nil { return fmt.Errorf("load local migrations: %w", err) @@ -1940,9 +1949,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 { diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 4545683f413..24b921f04ba 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -199,11 +199,12 @@ func GetLastIDByNodeID(db sql.Executor, nodeID types.NodeID) (id types.ATXID, er 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) { +// 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.BindInt64(1, int64(epoch)) - stmt.BindBytes(2, nodeID.Bytes()) + stmt.BindBytes(1, nodeID.Bytes()) + stmt.BindInt64(2, int64(pubEpoch)) } dec := func(stmt *sql.Statement) bool { stmt.ColumnBytes(0, id[:]) @@ -212,60 +213,38 @@ func GetIDByEpochAndNodeID(db sql.Executor, epoch types.EpochID, nodeID types.No if rows, err := db.Exec(` select id from atxs - where epoch = ?1 and pubkey = ?2 + where pubkey = ?1 and epoch < ?2 + order by epoch desc limit 1;`, enc, dec); err != nil { - return types.ATXID{}, fmt.Errorf("exec nodeID %v: %w", nodeID, err) + return types.EmptyATXID, fmt.Errorf("exec nodeID %v, epoch %d: %w", nodeID, pubEpoch, err) } else if rows == 0 { - return types.ATXID{}, fmt.Errorf("exec nodeID %s: %w", nodeID, sql.ErrNotFound) + return types.EmptyATXID, fmt.Errorf("exec nodeID %s, epoch %d: %w", nodeID, pubEpoch, sql.ErrNotFound) } 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 +// 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) { stmt.BindInt64(1, int64(epoch)) + stmt.BindBytes(2, nodeID.Bytes()) } 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 - } + stmt.ColumnBytes(0, id[:]) 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) + if rows, err := db.Exec(` + select id from atxs + where epoch = ?1 and pubkey = ?2 + limit 1;`, enc, dec); err != nil { + return types.ATXID{}, fmt.Errorf("exec nodeID %v: %w", nodeID, err) + } else if rows == 0 { + return types.ATXID{}, fmt.Errorf("exec nodeID %s: %w", nodeID, sql.ErrNotFound) } - return callbackErr + return id, err } // GetIDsByEpoch gets ATX IDs for a given epoch. @@ -793,3 +772,45 @@ func IterateAtxIdsWithMalfeasance( ) return err } + +type PrevATXCollision struct { + 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 nodeID1, nodeID2 types.NodeID + stmt.ColumnBytes(0, nodeID1[:]) + stmt.ColumnBytes(1, nodeID2[:]) + + var id1, id2 types.ATXID + stmt.ColumnBytes(2, id1[:]) + stmt.ColumnBytes(3, id2[:]) + + result = append(result, PrevATXCollision{ + NodeID1: nodeID1, + ATX1: id1, + + NodeID2: nodeID2, + ATX2: id2, + }) + return true + } + // we are joining the table with itself to find ATXs with the same prevATX + // the WHERE clause ensures that we only get the pairs once + if _, err := db.Exec(` + SELECT t1.pubkey, t2.pubkey, t1.id, t2.id + FROM atxs t1 + 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) + } + + return result, nil +} diff --git a/sql/atxs/atxs_test.go b/sql/atxs/atxs_test.go index 9a2352829fb..cc13ba4d9e3 100644 --- a/sql/atxs/atxs_test.go +++ b/sql/atxs/atxs_test.go @@ -981,3 +981,53 @@ 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 := newAtx(t, sig, withPublishEpoch(1), withPrevATXID(prevATXID)) + atx2 := newAtx(t, sig, withPublishEpoch(2), withPrevATXID(prevATXID)) + + 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) + atx1.AtxBlob = types.AtxBlob{} + require.Equal(t, atx1, got1) + + got2, err := atxs.Get(db, atx2.ID()) + require.NoError(t, err) + atx2.AtxBlob = types.AtxBlob{} + require.Equal(t, atx2, got2) + + // add 10 valid ATXs by 10 other smeshers + for i := 2; i < 6; i++ { + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + + atx := newAtx(t, otherSig, withPublishEpoch(types.EpochID(i))) + require.NoError(t, atxs.Add(db, atx)) + + atx2 := newAtx(t, otherSig, + withPublishEpoch(types.EpochID(i+1)), + withPrevATXID(atx.ID()), + ) + require.NoError(t, atxs.Add(db, atx2)) + } + + // get the collisions + got, err := atxs.PrevATXCollisions(db) + require.NoError(t, err) + require.Len(t, got, 1) + + 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}) +} diff --git a/sql/blocks/blocks_test.go b/sql/blocks/blocks_test.go index eb4a8e74344..8b64d2ab65c 100644 --- a/sql/blocks/blocks_test.go +++ b/sql/blocks/blocks_test.go @@ -316,14 +316,14 @@ func TestLoadBlob(t *testing.T) { func TestLayerForMangledBlock(t *testing.T) { db := sql.InMemory() - if _, err := db.Exec("insert into blocks (id, layer, block) values (?1, ?2, ?3);", + _, err := db.Exec("insert into blocks (id, layer, block) values (?1, ?2, ?3);", func(stmt *sql.Statement) { stmt.BindBytes(1, []byte(`mangled-block-id`)) stmt.BindInt64(2, 1010101) stmt.BindBytes(3, []byte(`mangled-block`)) // this is actually should encode block - }, nil); err != nil { - require.NoError(t, err) - } + }, nil) + require.NoError(t, err) + rst, err := Layer(db, types.LayerID(1010101)) require.Empty(t, rst, 0) require.Error(t, err) diff --git a/sql/identities/identities.go b/sql/identities/identities.go index f8d523d0c3e..48ef4b36d81 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 { 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 e69de29bb2d..7e4a34351e9 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/systest/Dockerfile b/systest/Dockerfile index 2570a4e85be..4bd9b07388e 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 d0cfa8041d7..c3a96138d92 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: diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 271fbd3103c..5588609e5a0 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -293,5 +293,5 @@ func TestPostMalfeasanceProof(t *testing.T) { return false, nil }) require.NoError(t, err) - require.True(t, receivedProof) + require.True(t, receivedProof, "malfeasance proof not received") }