CVE-2024-34360 (Go, published 2024-05-14T15:38Z)
Description: value: go-spacemesh is a Go implementation of the Spacemesh protocol full node. Nodes can publish activations transactions (ATXs) which reference the incorrect previous ATX of the Smesher that created the ATX. ATXs are expected to form a single chain from the newest to the first ATX ever published by an identity. Allowing Smeshers to reference an earlier (but not the latest) ATX as previous breaks this protocol rule and can serve as an attack vector where Nodes are rewarded for holding their PoST data for less than one epoch but still being eligible for rewards. This vulnerability is fixed in go-spacemesh 1.5.2-hotfix1 and Spacemesh API 1.37.1.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/spacemeshos/go-spacemesh
Files changed:
.github/workflows/systest.yml
CHANGELOG.md
Dockerfile
Makefile
activation/handler.go
activation/handler_test.go
activation/verify_state.go
activation/verify_state_test.go
activation/wire/wire_v1.go
bootstrap.Dockerfile
cmd/root.go
common/types/hashes.go
common/types/layer.go
config/config.go
config/mainnet.go
events/events.go
go.mod
go.sum
malfeasance/handler.go
malfeasance/handler_test.go
malfeasance/metrics.go
malfeasance/wire/malfeasance.go
malfeasance/wire/malfeasance_scale.go
malfeasance/wire/malfeasance_test.go
node/node.go
sql/atxs/atxs.go
sql/atxs/atxs_test.go
sql/identities/identities.go
sql/migrations/state/0017_atxs_prev_id_nonce_placeholder.sql
sql/migrations/state_0017_migration_test.go
systest/Dockerfile
systest/Makefile
systest/tests/distributed_post_verification_test.go
Commit message: Verify that previous ATX points to correct ATX when handling incoming ATXs (#27)

---------

Co-authored-by: Bartosz Różański <bartek.roza@gmail.com>


Vulnerable code:
RUN go mod download
		-t $(DOCKER_HUB)/$(DOCKER_IMAGE_REPO):$(DOCKER_IMAGE_VERSION) .
	DOCKER_BUILDKIT=1 docker build -t go-spacemesh-bs:$(SHA) -t $(DOCKER_HUB)/$(DOCKER_IMAGE_REPO)-bs:$(DOCKER_IMAGE_VERSION) -f ./bootstrap.Dockerfile .
	var (
		commitmentATX *types.ATXID
		err           error
	)
		commitmentATX = atx.CommitmentATX
// 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)
		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 {
		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) {
			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),
			)
	}); err != nil {
	proof, err := h.handleAtx(ctx, types.Hash32{}, peer, msg)
	if expHash != (types.Hash32{}) && vAtx.ID().Hash32() != expHash {
		log.Bool("malicious", proof != nil))
		types.LayerID(layersPerEpoch).GetEpoch(),
		types.LayerID(layersPerEpoch+1).GetEpoch(),
func TestHandler_ProcessAtx_OwnNotMalicious(t *testing.T) {
		types.LayerID(layersPerEpoch).GetEpoch(),
		types.LayerID(layersPerEpoch+1).GetEpoch(),
		types.LayerID(layersPerEpoch).GetEpoch(),
		types.LayerID(2*layersPerEpoch).GetEpoch(),
RUN go mod download
	EmptyLayerHash = Hash32{}
			PruneActivesetsFrom:   12, // starting from epoch 13 activesets below 12 will be pruned
github.com/spacemeshos/api/release/go v1.37.0 h1:bN6AhSMVSmAShGxUYKwFBfzY3U1XtHezpDjt20dHjBM=
github.com/spacemeshos/api/release/go v1.37.0/go.mod h1:Ed7SdL2YgqNg2SeShEAonW3GTPuuaGzsY5i4bgziCRo=
			"%w: malfesance proof want %s, got %s",
func validateInvalidPostIndex(ctx context.Context,
func TestHandler_HandleMalfeasanceProof_InvalidPostIndex(t *testing.T) {
		trt := malfeasance.NewMocktortoise(gomock.NewController(t))
		postVerifier := malfeasance.NewMockpostVerifier(gomock.NewController(t))
		trt := malfeasance.NewMocktortoise(gomock.NewController(t))
		postVerifier := malfeasance.NewMockpostVerifier(gomock.NewController(t))
		trt := malfeasance.NewMocktortoise(gomock.NewController(t))
		postVerifier := malfeasance.NewMockpostVerifier(gomock.NewController(t))
	numMalformed              = numInvalidProofs.WithLabelValues("mal")
//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata,InvalidPostIndexProof
			atx := wire.ActivationTxFromWireV1(&p.Atx)
			encoder.AddString("atx_id", atx.ID().String())
			atx := wire.ActivationTxFromWireV1(&p.Atx)
					atx.ID().ShortString(),
		switch c.Intn(3) {
	"github.com/spacemeshos/go-spacemesh/activation/wire"
	"github.com/spacemeshos/go-spacemesh/sql/builder"
	// uncomment to verify ATXs signatures
	// app.verifyDB(ctx)

// 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
	})
}

// 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) {
		stmt.BindInt64(1, int64(epoch))
		stmt.BindBytes(2, nodeID.Bytes())
		where epoch = ?1 and pubkey = ?2
		return types.ATXID{}, fmt.Errorf("exec nodeID %v: %w", nodeID, err)
		return types.ATXID{}, fmt.Errorf("exec nodeID %s: %w", nodeID, sql.ErrNotFound)
// 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
		var id types.ATXID
		total := stmt.ColumnInt(0)
		stmt.ColumnBytes(1, id[:])
		if callbackErr = callback(total, id); callbackErr != nil {
			return false
		}
	// 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
				return nil, err
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)
}

// GetMalicious retrives malicious node IDs from the database.
RUN go mod download
	@DOCKER_BUILDKIT=1 docker build ../ -f Dockerfile -t $(image_name)
	logger.Info("waiting for malfeasance proof")
	err = malfeasanceStream(ctx, cl.Client(0), logger, func(malfeasance *pb.MalfeasanceStreamResponse) (bool, error) {
		require.Equal(t, malfeasance.GetProof().GetSmesherId().Id, signer.NodeID().Bytes())
		require.Equal(t, pb.MalfeasanceProof_MALFEASANCE_POST_INDEX, malfeasance.GetProof().GetKind())
		require.NoError(t, codec.Decode(malfeasance.Proof.Proof, &proof))
		err = verifier.Verify(ctx, (*shared.Proof)(invalidAtx.NIPost.Post), meta)

Fixed code:
      - uses: extractions/netrc@v2
        with:
          machine: github.com
          username: ${{ secrets.GH_ACTION_TOKEN_USER }}
          password: ${{ secrets.GH_ACTION_TOKEN }}
        if: vars.GOPRIVATE

      - uses: extractions/netrc@v2
        with:
          machine: github.com
          username: ${{ secrets.GH_ACTION_TOKEN_USER }}
          password: ${{ secrets.GH_ACTION_TOKEN }}
        if: vars.GOPRIVATE

## Release v1.5.2-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.

RUN --mount=type=secret,id=mynetrc,dst=/root/.netrc go mod download
		--secret id=mynetrc,src=$(HOME)/.netrc \
		-t $(DOCKER_HUB)/$(DOCKER_IMAGE_REPO):$(DOCKER_IMAGE_VERSION) \
		.
	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 \
		.
	var commitmentATX *types.ATXID
		commitmentATX = atx.CommitmentATX // checked to be non-nil in syntactic validation
		var err error
// 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)
		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, prevID)
	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(tx, 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(atx2.ActivationTx),
			},
		},
	}

	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 *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)
	}
	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)
}

// 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 err error
		proof, err = h.checkMalicious(ctx, tx, atx)
		if err != nil {
			return fmt.Errorf("check malicious: %w", err)
		}
	})
	if err != nil {
	proof, err := h.handleAtx(ctx, types.EmptyHash32, peer, msg)
	if expHash != types.EmptyHash32 && vAtx.ID().Hash32() != expHash {
		log.Bool("malicious", proof != nil),
	)
		types.EpochID(2),
}

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}
	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)
		types.EpochID(2),
func TestHandler_ProcessAtx_SamePubEpoch_NoSelfIncrimination(t *testing.T) {
		types.EpochID(2),
		types.EpochID(2),
	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)

	// 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
		types.EpochID(2),
		types.EpochID(3),
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
		}

		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.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.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
}
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)
}

	id types.ATXID
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
}

RUN --mount=type=secret,id=mynetrc,dst=/root/.netrc go mod download
	flagSet.BoolVar(&cfg.ScanMalfeasantATXs, "scan-malfeasant-atxs", cfg.ScanMalfeasantATXs,
		"scan for malfeasant ATXs")


	// EmptyHash32 is the zero hash.
	EmptyHash32 = Hash32{}
	EmptyLayerHash = EmptyHash32
	// ScanMalfeasantATXs is a flag to enable scanning for malfeasant ATXs.
	ScanMalfeasantATXs bool `mapstructure:"scan-malfeasant-atxs"`

			PruneActivesetsFrom:   12,    // starting from epoch 13 activesets below 12 will be pruned
			ScanMalfeasantATXs:    false, // opt-in
	case wire.InvalidPrevATX:
		kind = pb.MalfeasanceProof_MALFEASANCE_INCORRECT_PREV_ATX

replace github.com/spacemeshos/api/release/go => github.com/spacemeshos/api-cve-fix/release/go v1.37.1
github.com/spacemeshos/api-cve-fix/release/go v1.37.1 h1:AOhiRthCKb65ugnwYvA+dVqPNqKAEkPcFx4L8nJWtdI=
github.com/spacemeshos/api-cve-fix/release/go v1.37.1/go.mod h1:Ed7SdL2YgqNg2SeShEAonW3GTPuuaGzsY5i4bgziCRo=
			"%w: malfeasance proof want %s, got %s",
	case wire.InvalidPrevATX:
		proof := p.MalfeasanceProof.Proof.Data.(*wire.InvalidPrevATXProof) // guaranteed to work by scale func
		nodeID, err = validateInvalidPrevATX(ctx, cdb, edVerifier, proof)
	case wire.InvalidPrevATX:
		numProofsPrevATX.Inc()
func validateInvalidPostIndex(
	ctx context.Context,

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")
	}

	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
}
func TestHandler_HandleSyncedMalfeasanceProof_InvalidPostIndex(t *testing.T) {
		ctrl := gomock.NewController(t)
		trt := malfeasance.NewMocktortoise(ctrl)
		postVerifier := malfeasance.NewMockpostVerifier(ctrl)
		ctrl := gomock.NewController(t)
		trt := malfeasance.NewMocktortoise(ctrl)
		postVerifier := malfeasance.NewMockpostVerifier(ctrl)
		ctrl := gomock.NewController(t)
		trt := malfeasance.NewMocktortoise(ctrl)
		postVerifier := malfeasance.NewMockpostVerifier(ctrl)

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)
	})
}
	invalidPrevATX   = "invalid_prev_atx"
	numProofsPrevATX   = numProofs.WithLabelValues(invalidPrevATX)
	numMalformed              = numInvalidProofs.WithLabelValues("mal")
	numInvalidProofsPrevATX   = numInvalidProofs.WithLabelValues(invalidPrevATX)
//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata,InvalidPostIndexProof,InvalidPrevATXProof
	InvalidPrevATX
			encoder.AddString("atx_id", p.Atx.ID().String())
	case InvalidPrevATX:
		encoder.AddString("type", "invalid prev atx")
		p, ok := mp.Proof.Data.(*InvalidPrevATXProof)
		if ok {
			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())
		}
	case InvalidPrevATX:
		var proof InvalidPrevATXProof
		n, err := proof.DecodeScale(dec)
		if err != nil {
			return total, err
		}
		e.Data = &proof
		total += n
// 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
}

					p.Atx.ID().ShortString(),
	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.Publish,
				))
		}

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
}
	"github.com/spacemeshos/go-spacemesh/activation"
	awire "github.com/spacemeshos/go-spacemesh/activation/wire"
	"github.com/spacemeshos/go-spacemesh/signing"
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_InvalidPrevATX(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.Equal(t, *proof, decoded)
}

		switch c.Intn(5) {
		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
	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)))
	}

// 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) {
		stmt.BindBytes(1, nodeID.Bytes())
		stmt.BindInt64(2, int64(pubEpoch))
		where pubkey = ?1 and epoch < ?2
		order by epoch desc
		return types.EmptyATXID, fmt.Errorf("exec nodeID %v, epoch %d: %w", nodeID, pubEpoch, err)
		return types.EmptyATXID, fmt.Errorf("exec nodeID %s, epoch %d: %w", nodeID, pubEpoch, sql.ErrNotFound)
// 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) {
		stmt.BindBytes(2, nodeID.Bytes())
		stmt.ColumnBytes(0, id[:])
	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 id, err
				return &nonce, 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
	}
	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
}

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].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})
}
// GetMalicious retrieves malicious node IDs from the database.
-- Migration is done entirely in code
	"path/filepath"
	"strings"
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)
}

# We want to populate the module cache based on the go.{mod,sum} files.
RUN --mount=type=secret,id=mynetrc,dst=/root/.netrc go mod download

# Here we copy the rest of the source code
	@DOCKER_BUILDKIT=1 docker build \
		--secret id=mynetrc,src=$(HOME)/.netrc \
		-t $(image_name) \
		-f Dockerfile \
		../
	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) {
		require.Equal(t, malf.GetProof().GetSmesherId().Id, signer.NodeID().Bytes())
		require.Equal(t, pb.MalfeasanceProof_MALFEASANCE_POST_INDEX, malf.GetProof().GetKind())
		require.NoError(t, codec.Decode(malf.Proof.Proof, &proof))
		err = verifier.Verify(awaitCtx, (*shared.Proof)(invalidAtx.NIPost.Post), meta)
		receivedProof = true
	require.True(t, receivedProof, "malfeasance proof not received")