From aa8d63c4c791f052f4133dd32b7c0e6bb8e3714e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 6 Nov 2024 22:32:52 +0000 Subject: [PATCH] double-merge malfeasance proof (#6339) ## Motivation Closes #6341 Co-authored-by: Matthias <5011972+fasmat@users.noreply.github.com> --- activation/handler_v2.go | 21 +- activation/handler_v2_test.go | 47 +-- activation/wire/malfeasance_double_merge.go | 169 +++++++++ .../wire/malfeasance_double_merge_scale.go | 232 +++++++++++++ .../wire/malfeasance_double_merge_test.go | 326 ++++++++++++++++++ activation/wire/wire_v2.go | 10 +- activation/wire/wire_v2_test.go | 6 + 7 files changed, 787 insertions(+), 24 deletions(-) create mode 100644 activation/wire/malfeasance_double_merge.go create mode 100644 activation/wire/malfeasance_double_merge_scale.go create mode 100644 activation/wire/malfeasance_double_merge_test.go diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 3ec7abcca0..6718475065 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -841,8 +841,25 @@ func (h *HandlerV2) checkDoubleMerge(ctx context.Context, tx sql.Transaction, at zap.Stringer("smesher_id", atx.SmesherID), ) - // TODO(mafa): finish proof - var proof wire.Proof + // TODO(mafa): during syntactical validation we should check if a merged ATX is targeting a checkpointed epoch + // merged ATXs need to be checkpointed with their marriage ATXs + // if there is a collision (i.e. the new ATX references the same marriage ATX as a golden ATX) it should be + // considered syntactically invalid + // + // see https://github.com/spacemeshos/go-spacemesh/issues/6434 + otherAtx, err := h.fetchWireAtx(ctx, tx, other) + if err != nil { + return false, fmt.Errorf("fetching other ATX: %w", err) + } + + // TODO(mafa): checkpoints need to include all marriage ATXs in full to be able to create malfeasance proofs + // like this one (but also others) + // + // see https://github.com/spacemeshos/go-spacemesh/issues/6435 + proof, err := wire.NewDoubleMergeProof(tx, atx.ActivationTxV2, otherAtx) + if err != nil { + return true, fmt.Errorf("creating double merge proof: %w", err) + } return true, h.malPublisher.Publish(ctx, atx.SmesherID, proof) } diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index a03058ab3d..f3ab08dabd 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -984,8 +984,24 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { merged.PreviousATXs = []types.ATXID{otherATXs[1].ID(), otherATXs[2].ID()} merged.Sign(signers[2]) + verifier := wire.NewMockMalfeasanceValidator(atxHandler.ctrl) + verifier.EXPECT().Signature(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { + return atxHandler.edVerifier.Verify(d, nodeID, m, sig) + }).AnyTimes() + atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100}) - atxHandler.mMalPublish.EXPECT().Publish(gomock.Any(), merged.SmesherID, gomock.Any()) + atxHandler.mMalPublish.EXPECT().Publish( + gomock.Any(), + merged.SmesherID, + gomock.AssignableToTypeOf(&wire.ProofDoubleMerge{}), + ).DoAndReturn(func(ctx context.Context, id types.NodeID, proof wire.Proof) error { + malProof := proof.(*wire.ProofDoubleMerge) + nId, err := malProof.Valid(context.Background(), verifier) + require.NoError(t, err) + require.Equal(t, merged.SmesherID, nId) + return nil + }) err = atxHandler.processATX(context.Background(), "", merged, time.Now()) require.NoError(t, err) }) @@ -1019,12 +1035,13 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { merged.MarriageATX = &mATXID merged.PreviousATXs = []types.ATXID{otherATXs[1].ID(), otherATXs[2].ID(), otherATXs[3].ID()} merged.Sign(signers[2]) - atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100}) - // TODO: this could be syntactically validated as all nodes in the network + + // This is syntactically invalid as all nodes in the network // should already have the checkpointed merged ATX. - atxHandler.mMalPublish.EXPECT().Publish(gomock.Any(), merged.SmesherID, gomock.Any()) + t.Skip("syntactically validating double merge where one ATX is checkpointed isn't implemented yet") + atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100}) err := atxHandler.processATX(context.Background(), "", merged, time.Now()) - require.NoError(t, err) + require.Error(t, err) }) } @@ -1558,10 +1575,7 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { atxHandler.mMalPublish.EXPECT().Publish( gomock.Any(), sig.NodeID(), - gomock.Cond(func(data wire.Proof) bool { - _, ok := data.(*wire.ProofInvalidPost) - return ok - }), + gomock.AssignableToTypeOf(&wire.ProofInvalidPost{}), ).DoAndReturn(func(ctx context.Context, _ types.NodeID, proof wire.Proof) error { malProof := proof.(*wire.ProofInvalidPost) nId, err := malProof.Valid(ctx, verifier) @@ -1612,10 +1626,7 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { atxHandler.mMalPublish.EXPECT().Publish( gomock.Any(), sig.NodeID(), - gomock.Cond(func(data wire.Proof) bool { - _, ok := data.(*wire.ProofInvalidPost) - return ok - }), + gomock.AssignableToTypeOf(&wire.ProofInvalidPost{}), ).DoAndReturn(func(ctx context.Context, _ types.NodeID, proof wire.Proof) error { malProof := proof.(*wire.ProofInvalidPost) nId, err := malProof.Valid(ctx, verifier) @@ -1697,10 +1708,7 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { atxHandler.mMalPublish.EXPECT().Publish( gomock.Any(), sig.NodeID(), - gomock.Cond(func(data wire.Proof) bool { - _, ok := data.(*wire.ProofInvalidPost) - return ok - }), + gomock.AssignableToTypeOf(&wire.ProofInvalidPost{}), ).DoAndReturn(func(ctx context.Context, _ types.NodeID, proof wire.Proof) error { malProof := proof.(*wire.ProofInvalidPost) nId, err := malProof.Valid(ctx, verifier) @@ -1832,10 +1840,7 @@ func Test_Marriages(t *testing.T) { atxHandler.mMalPublish.EXPECT().Publish( gomock.Any(), sig.NodeID(), - gomock.Cond(func(data wire.Proof) bool { - _, ok := data.(*wire.ProofDoubleMarry) - return ok - }), + gomock.AssignableToTypeOf(&wire.ProofDoubleMarry{}), ).DoAndReturn(func(ctx context.Context, _ types.NodeID, proof wire.Proof) error { malProof := proof.(*wire.ProofDoubleMarry) nId, err := malProof.Valid(ctx, verifier) diff --git a/activation/wire/malfeasance_double_merge.go b/activation/wire/malfeasance_double_merge.go new file mode 100644 index 0000000000..7bbaf51e2d --- /dev/null +++ b/activation/wire/malfeasance_double_merge.go @@ -0,0 +1,169 @@ +package wire + +import ( + "context" + "errors" + "fmt" + + "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" +) + +//go:generate scalegen + +// ProofDoubleMerge is a proof that two distinct ATXs published in the same epoch +// contain the same marriage ATX. +// +// We are proving the following: +// 1. The ATXs have different IDs. +// 2. Both ATXs have a valid signature. +// 3. Both ATXs contain the same marriage ATX. +// 4. Both ATXs were published in the same epoch. +// 5. Signers of both ATXs are married - to prevent banning others by +// publishing an ATX with the same marriage ATX. +type ProofDoubleMerge struct { + // PublishEpoch and its proof that it is contained in the ATX. + PublishEpoch types.EpochID + + // MarriageATXID is the ID of the marriage ATX. + MarriageATX types.ATXID + // MarriageATXSmesherID is the ID of the smesher that published the marriage ATX. + MarriageATXSmesherID types.NodeID + + // ATXID1 is the ID of the ATX being proven. + ATXID1 types.ATXID + // SmesherID1 is the ID of the smesher that published the ATX. + SmesherID1 types.NodeID + // Signature1 is the signature of the ATXID by the smesher. + Signature1 types.EdSignature + // PublishEpochProof1 is the proof that the publish epoch is contained in the ATX. + PublishEpochProof1 PublishEpochProof `scale:"max=32"` + // MarriageATXProof1 is the proof that MarriageATX is contained in the ATX. + MarriageATXProof1 MarriageATXProof `scale:"max=32"` + // SmesherID1MarryProof is the proof that they married in MarriageATX. + SmesherID1MarryProof MarryProof + + // ATXID2 is the ID of the ATX being proven. + ATXID2 types.ATXID + // SmesherID is the ID of the smesher that published the ATX. + SmesherID2 types.NodeID + // Signature2 is the signature of the ATXID by the smesher. + Signature2 types.EdSignature + // PublishEpochProof2 is the proof that the publish epoch is contained in the ATX. + PublishEpochProof2 PublishEpochProof `scale:"max=32"` + // MarriageATXProof1 is the proof that MarriageATX is contained in the ATX. + MarriageATXProof2 MarriageATXProof `scale:"max=32"` + // SmesherID1MarryProof is the proof that they married in MarriageATX. + SmesherID2MarryProof MarryProof +} + +var _ Proof = &ProofDoubleMerge{} + +func NewDoubleMergeProof(db sql.Executor, atx1, atx2 *ActivationTxV2) (*ProofDoubleMerge, error) { + if atx1.ID() == atx2.ID() { + return nil, errors.New("ATXs have the same ID") + } + if atx1.SmesherID == atx2.SmesherID { + return nil, errors.New("ATXs have the same smesher ID") + } + if atx1.PublishEpoch != atx2.PublishEpoch { + return nil, fmt.Errorf("ATXs have different publish epoch (%v != %v)", atx1.PublishEpoch, atx2.PublishEpoch) + } + if atx1.MarriageATX == nil { + return nil, errors.New("ATX 1 have no marriage ATX") + } + if atx2.MarriageATX == nil { + return nil, errors.New("ATX 2 have no marriage ATX") + } + if *atx1.MarriageATX != *atx2.MarriageATX { + return nil, errors.New("ATXs have different marriage ATXs") + } + + var blob sql.Blob + v, err := atxs.LoadBlob(context.Background(), db, atx1.MarriageATX.Bytes(), &blob) + if err != nil { + return nil, fmt.Errorf("get marriage ATX: %w", err) + } + if v != types.AtxV2 { + return nil, errors.New("invalid ATX version for marriage ATX") + } + marriageATX, err := DecodeAtxV2(blob.Bytes) + if err != nil { + return nil, fmt.Errorf("decode marriage ATX: %w", err) + } + + marriageProof1, err := createMarryProof(db, marriageATX, atx1.SmesherID) + if err != nil { + return nil, fmt.Errorf("NodeID marriage proof: %w", err) + } + marriageProof2, err := createMarryProof(db, marriageATX, atx2.SmesherID) + if err != nil { + return nil, fmt.Errorf("SmesherID marriage proof: %w", err) + } + + proof := ProofDoubleMerge{ + PublishEpoch: atx1.PublishEpoch, + MarriageATX: marriageATX.ID(), + MarriageATXSmesherID: marriageATX.SmesherID, + + ATXID1: atx1.ID(), + SmesherID1: atx1.SmesherID, + Signature1: atx1.Signature, + PublishEpochProof1: atx1.PublishEpochProof(), + MarriageATXProof1: atx1.MarriageATXProof(), + SmesherID1MarryProof: marriageProof1, + + ATXID2: atx2.ID(), + SmesherID2: atx2.SmesherID, + Signature2: atx2.Signature, + PublishEpochProof2: atx2.PublishEpochProof(), + MarriageATXProof2: atx2.MarriageATXProof(), + SmesherID2MarryProof: marriageProof2, + } + + return &proof, nil +} + +// Valid implements Proof.Valid. +func (p *ProofDoubleMerge) Valid(_ context.Context, edVerifier MalfeasanceValidator) (types.NodeID, error) { + // 1. The ATXs have different IDs. + if p.ATXID1 == p.ATXID2 { + return types.EmptyNodeID, errors.New("ATXs have the same ID") + } + + // 2. Both ATXs have a valid signature. + if !edVerifier.Signature(signing.ATX, p.SmesherID1, p.ATXID1.Bytes(), p.Signature1) { + return types.EmptyNodeID, errors.New("ATX 1 invalid signature") + } + if !edVerifier.Signature(signing.ATX, p.SmesherID2, p.ATXID2.Bytes(), p.Signature2) { + return types.EmptyNodeID, errors.New("ATX 2 invalid signature") + } + + // 3. and 4. publish epoch is contained in the ATXs + if !p.PublishEpochProof1.Valid(p.ATXID1, p.PublishEpoch) { + return types.EmptyNodeID, errors.New("ATX 1 invalid publish epoch proof") + } + if !p.PublishEpochProof2.Valid(p.ATXID2, p.PublishEpoch) { + return types.EmptyNodeID, errors.New("ATX 2 invalid publish epoch proof") + } + + // 5. signers are married + if !p.MarriageATXProof1.Valid(p.ATXID1, p.MarriageATX) { + return types.EmptyNodeID, errors.New("ATX 1 invalid marriage ATX proof") + } + err := p.SmesherID1MarryProof.Valid(edVerifier, p.MarriageATX, p.MarriageATXSmesherID, p.SmesherID1) + if err != nil { + return types.EmptyNodeID, errors.New("ATX 1 invalid marriage ATX proof") + } + if !p.MarriageATXProof2.Valid(p.ATXID2, p.MarriageATX) { + return types.EmptyNodeID, errors.New("ATX 2 invalid marriage ATX proof") + } + err = p.SmesherID2MarryProof.Valid(edVerifier, p.MarriageATX, p.MarriageATXSmesherID, p.SmesherID2) + if err != nil { + return types.EmptyNodeID, errors.New("ATX 2 invalid marriage ATX proof") + } + + return p.SmesherID1, nil +} diff --git a/activation/wire/malfeasance_double_merge_scale.go b/activation/wire/malfeasance_double_merge_scale.go new file mode 100644 index 0000000000..d4d38b16f3 --- /dev/null +++ b/activation/wire/malfeasance_double_merge_scale.go @@ -0,0 +1,232 @@ +// Code generated by github.com/spacemeshos/go-scale/scalegen. DO NOT EDIT. + +// nolint +package wire + +import ( + "github.com/spacemeshos/go-scale" + "github.com/spacemeshos/go-spacemesh/common/types" +) + +func (t *ProofDoubleMerge) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeCompact32(enc, uint32(t.PublishEpoch)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.MarriageATX[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.MarriageATXSmesherID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.ATXID1[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.SmesherID1[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.Signature1[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.PublishEpochProof1, 32) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.MarriageATXProof1, 32) + if err != nil { + return total, err + } + total += n + } + { + n, err := t.SmesherID1MarryProof.EncodeScale(enc) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.ATXID2[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.SmesherID2[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.Signature2[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.PublishEpochProof2, 32) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.MarriageATXProof2, 32) + if err != nil { + return total, err + } + total += n + } + { + n, err := t.SmesherID2MarryProof.EncodeScale(enc) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *ProofDoubleMerge) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + field, n, err := scale.DecodeCompact32(dec) + if err != nil { + return total, err + } + total += n + t.PublishEpoch = types.EpochID(field) + } + { + n, err := scale.DecodeByteArray(dec, t.MarriageATX[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.MarriageATXSmesherID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.ATXID1[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.SmesherID1[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.Signature1[:]) + if err != nil { + return total, err + } + total += n + } + { + field, n, err := scale.DecodeStructSliceWithLimit[types.Hash32](dec, 32) + if err != nil { + return total, err + } + total += n + t.PublishEpochProof1 = field + } + { + field, n, err := scale.DecodeStructSliceWithLimit[types.Hash32](dec, 32) + if err != nil { + return total, err + } + total += n + t.MarriageATXProof1 = field + } + { + n, err := t.SmesherID1MarryProof.DecodeScale(dec) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.ATXID2[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.SmesherID2[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.Signature2[:]) + if err != nil { + return total, err + } + total += n + } + { + field, n, err := scale.DecodeStructSliceWithLimit[types.Hash32](dec, 32) + if err != nil { + return total, err + } + total += n + t.PublishEpochProof2 = field + } + { + field, n, err := scale.DecodeStructSliceWithLimit[types.Hash32](dec, 32) + if err != nil { + return total, err + } + total += n + t.MarriageATXProof2 = field + } + { + n, err := t.SmesherID2MarryProof.DecodeScale(dec) + if err != nil { + return total, err + } + total += n + } + return total, nil +} diff --git a/activation/wire/malfeasance_double_merge_test.go b/activation/wire/malfeasance_double_merge_test.go new file mode 100644 index 0000000000..707e27ee58 --- /dev/null +++ b/activation/wire/malfeasance_double_merge_test.go @@ -0,0 +1,326 @@ +package wire + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/sql/statesql" +) + +func Test_NewDoubleMergeProof(t *testing.T) { + signer1, err := signing.NewEdSigner() + require.NoError(t, err) + + signer2, err := signing.NewEdSigner() + require.NoError(t, err) + + marrySig, err := signing.NewEdSigner() + require.NoError(t, err) + + edVerifier := signing.NewEdVerifier() + + setupMarriage := func(db sql.Executor) *ActivationTxV2 { + wInitialAtx1 := newActivationTxV2( + withInitial(types.RandomATXID(), PostV1{}), + ) + wInitialAtx1.Sign(signer1) + initialAtx1 := &types.ActivationTx{ + CommitmentATX: &wInitialAtx1.Initial.CommitmentATX, + } + initialAtx1.SetID(wInitialAtx1.ID()) + initialAtx1.SmesherID = signer1.NodeID() + require.NoError(t, atxs.Add(db, initialAtx1, wInitialAtx1.Blob())) + + wInitialAtx2 := newActivationTxV2( + withInitial(types.RandomATXID(), PostV1{}), + ) + wInitialAtx2.Sign(signer2) + initialAtx2 := &types.ActivationTx{} + initialAtx2.SetID(wInitialAtx2.ID()) + initialAtx2.SmesherID = signer2.NodeID() + require.NoError(t, atxs.Add(db, initialAtx2, wInitialAtx2.Blob())) + + wMarriageAtx := newActivationTxV2( + withMarriageCertificate(marrySig, types.EmptyATXID, marrySig.NodeID()), + withMarriageCertificate(signer1, wInitialAtx1.ID(), marrySig.NodeID()), + withMarriageCertificate(signer2, wInitialAtx2.ID(), marrySig.NodeID()), + ) + wMarriageAtx.Sign(marrySig) + + marriageAtx := &types.ActivationTx{} + marriageAtx.SetID(wMarriageAtx.ID()) + marriageAtx.SmesherID = marrySig.NodeID() + require.NoError(t, atxs.Add(db, marriageAtx, wMarriageAtx.Blob())) + return wMarriageAtx + } + + t.Run("ATXs must be different", func(t *testing.T) { + t.Parallel() + db := statesql.InMemoryTest(t) + atx := &ActivationTxV2{} + atx.Sign(signer1) + + proof, err := NewDoubleMergeProof(db, atx, atx) + require.ErrorContains(t, err, "ATXs have the same ID") + require.Nil(t, proof) + }) + + t.Run("ATXs must have different signers", func(t *testing.T) { + // Note: catching this scenario is the responsibility of "invalid previous ATX proof" + t.Parallel() + db := statesql.InMemoryTest(t) + atx1 := &ActivationTxV2{} + atx1.Sign(signer1) + + atx2 := &ActivationTxV2{VRFNonce: 1} + atx2.Sign(signer1) + + proof, err := NewDoubleMergeProof(db, atx1, atx2) + require.ErrorContains(t, err, "ATXs have the same smesher") + require.Nil(t, proof) + }) + + t.Run("ATXs must have marriage ATX", func(t *testing.T) { + t.Parallel() + db := statesql.InMemoryTest(t) + atx := &ActivationTxV2{} + atx.Sign(signer1) + + atx2 := &ActivationTxV2{VRFNonce: 1} + atx2.Sign(signer2) + + // ATX 1 has no marriage + _, err := NewDoubleMergeProof(db, atx, atx2) + require.ErrorContains(t, err, "ATX 1 have no marriage ATX") + + // ATX 2 has no marriage + atx.MarriageATX = new(types.ATXID) + *atx.MarriageATX = types.RandomATXID() + _, err = NewDoubleMergeProof(db, atx, atx2) + require.ErrorContains(t, err, "ATX 2 have no marriage ATX") + + // ATX 1 and 2 must have the same marriage ATX + atx2.MarriageATX = new(types.ATXID) + *atx2.MarriageATX = types.RandomATXID() + _, err = NewDoubleMergeProof(db, atx, atx2) + require.ErrorContains(t, err, "ATXs have different marriage ATXs") + }) + + t.Run("ATXs must be published in the same epoch", func(t *testing.T) { + t.Parallel() + db := statesql.InMemoryTest(t) + marriageID := types.RandomATXID() + atx := &ActivationTxV2{ + MarriageATX: &marriageID, + } + atx.Sign(signer1) + + atx2 := &ActivationTxV2{ + MarriageATX: &marriageID, + PublishEpoch: 1, + } + atx2.Sign(signer2) + proof, err := NewDoubleMergeProof(db, atx, atx2) + require.ErrorContains(t, err, "ATXs have different publish epoch") + require.Nil(t, proof) + }) + + t.Run("valid proof", func(t *testing.T) { + t.Parallel() + db := statesql.InMemoryTest(t) + + ctrl := gomock.NewController(t) + verifier := NewMockMalfeasanceValidator(ctrl) + verifier.EXPECT().Signature(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { + return edVerifier.Verify(d, nodeID, m, sig) + }).AnyTimes() + + marriageAtx := setupMarriage(db) + + atx1 := newActivationTxV2( + withMarriageATX(marriageAtx.ID()), + withPublishEpoch(marriageAtx.PublishEpoch+1), + ) + atx1.Sign(signer1) + + atx2 := newActivationTxV2( + withMarriageATX(marriageAtx.ID()), + withPublishEpoch(marriageAtx.PublishEpoch+1), + ) + atx2.Sign(signer2) + + proof, err := NewDoubleMergeProof(db, atx1, atx2) + require.NoError(t, err) + id, err := proof.Valid(context.Background(), verifier) + require.NoError(t, err) + require.Equal(t, signer1.NodeID(), id) + + require.Equal(t, signer1.NodeID(), proof.SmesherID1) + require.Equal(t, signer2.NodeID(), proof.SmesherID2) + }) + + t.Run("invalid marriage proof", func(t *testing.T) { + t.Parallel() + db := statesql.InMemoryTest(t) + + ctrl := gomock.NewController(t) + verifier := NewMockMalfeasanceValidator(ctrl) + verifier.EXPECT().Signature(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { + return edVerifier.Verify(d, nodeID, m, sig) + }).AnyTimes() + + marriageAtx := setupMarriage(db) + + atx1 := newActivationTxV2( + withMarriageATX(marriageAtx.ID()), + withPublishEpoch(marriageAtx.PublishEpoch+1), + ) + atx1.Sign(signer1) + + atx2 := newActivationTxV2( + withMarriageATX(marriageAtx.ID()), + withPublishEpoch(marriageAtx.PublishEpoch+1), + ) + atx2.Sign(signer2) + + proof, err := NewDoubleMergeProof(db, atx1, atx2) + require.NoError(t, err) + + hash := proof.MarriageATXProof1[0] + proof.MarriageATXProof1[0] = types.RandomHash() + id, err := proof.Valid(context.Background(), verifier) + require.EqualError(t, err, "ATX 1 invalid marriage ATX proof") + require.Equal(t, types.EmptyNodeID, id) + proof.MarriageATXProof1[0] = hash + + hash = proof.MarriageATXProof2[0] + proof.MarriageATXProof2[0] = types.RandomHash() + id, err = proof.Valid(context.Background(), verifier) + require.EqualError(t, err, "ATX 2 invalid marriage ATX proof") + require.Equal(t, types.EmptyNodeID, id) + proof.MarriageATXProof2[0] = hash + }) +} + +func Test_Validate_DoubleMergeProof(t *testing.T) { + edVerifier := signing.NewEdVerifier() + + ctrl := gomock.NewController(t) + verifier := NewMockMalfeasanceValidator(ctrl) + verifier.EXPECT().Signature(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { + return edVerifier.Verify(d, nodeID, m, sig) + }).AnyTimes() + + t.Run("ATXs must have different IDs", func(t *testing.T) { + t.Parallel() + id := types.RandomATXID() + proof := &ProofDoubleMerge{ + ATXID1: id, + ATXID2: id, + } + _, err := proof.Valid(context.Background(), verifier) + require.ErrorContains(t, err, "ATXs have the same ID") + }) + t.Run("ATX 1 must have valid signature", func(t *testing.T) { + t.Parallel() + proof := &ProofDoubleMerge{ + ATXID1: types.RandomATXID(), + ATXID2: types.RandomATXID(), + } + _, err := proof.Valid(context.Background(), verifier) + require.ErrorContains(t, err, "ATX 1 invalid signature") + }) + t.Run("ATX 2 must have valid signature", func(t *testing.T) { + t.Parallel() + + atx1 := &ActivationTxV2{} + signer, err := signing.NewEdSigner() + require.NoError(t, err) + atx1.Sign(signer) + + proof := &ProofDoubleMerge{ + ATXID1: atx1.ID(), + SmesherID1: atx1.SmesherID, + Signature1: atx1.Signature, + + ATXID2: types.RandomATXID(), + } + _, err = proof.Valid(context.Background(), verifier) + require.ErrorContains(t, err, "ATX 2 invalid signature") + }) + + t.Run("epoch proof for ATX1 must be valid", func(t *testing.T) { + t.Parallel() + + atx1 := &ActivationTxV2{} + signer, err := signing.NewEdSigner() + require.NoError(t, err) + atx1.Sign(signer) + + atx2 := &ActivationTxV2{ + NIPosts: []NIPostV2{ + { + Challenge: types.RandomHash(), + }, + }, + } + signer2, err := signing.NewEdSigner() + require.NoError(t, err) + atx2.Sign(signer2) + + proof := &ProofDoubleMerge{ + ATXID1: atx1.ID(), + SmesherID1: atx1.SmesherID, + Signature1: atx1.Signature, + + ATXID2: atx2.ID(), + SmesherID2: atx2.SmesherID, + Signature2: atx2.Signature, + } + _, err = proof.Valid(context.Background(), verifier) + require.ErrorContains(t, err, "ATX 1 invalid publish epoch proof") + }) + + t.Run("epoch proof for ATX2 must be valid", func(t *testing.T) { + t.Parallel() + + atx1 := &ActivationTxV2{} + signer, err := signing.NewEdSigner() + require.NoError(t, err) + atx1.Sign(signer) + + atx2 := &ActivationTxV2{ + NIPosts: []NIPostV2{ + { + Challenge: types.RandomHash(), + }, + }, + } + signer2, err := signing.NewEdSigner() + require.NoError(t, err) + atx2.Sign(signer2) + + proof := &ProofDoubleMerge{ + ATXID1: atx1.ID(), + SmesherID1: atx1.SmesherID, + Signature1: atx1.Signature, + PublishEpochProof1: atx1.PublishEpochProof(), + + ATXID2: atx2.ID(), + SmesherID2: atx2.SmesherID, + Signature2: atx2.Signature, + } + _, err = proof.Valid(context.Background(), verifier) + require.ErrorContains(t, err, "ATX 2 invalid publish epoch proof") + }) +} diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index 08a1390f5d..2c4f281a98 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -158,10 +158,18 @@ func (atx *ActivationTxV2) ID() types.ATXID { return atx.id } -func (atx *ActivationTxV2) PublishEpochProof() []types.Hash32 { +func (atx *ActivationTxV2) PublishEpochProof() PublishEpochProof { return atx.merkleProof(PublishEpochIndex) } +type PublishEpochProof []types.Hash32 + +func (p PublishEpochProof) Valid(atxID types.ATXID, publishEpoch types.EpochID) bool { + var publishEpochBytes types.Hash32 + binary.LittleEndian.PutUint32(publishEpochBytes[:], publishEpoch.Uint32()) + return validateProof(types.Hash32(atxID), publishEpochBytes, p, uint64(PublishEpochIndex)) +} + func (atx *ActivationTxV2) PositioningATXProof() []types.Hash32 { return atx.merkleProof(PositioningATXIndex) } diff --git a/activation/wire/wire_v2_test.go b/activation/wire/wire_v2_test.go index 188e19a130..2478d8f6db 100644 --- a/activation/wire/wire_v2_test.go +++ b/activation/wire/wire_v2_test.go @@ -31,6 +31,12 @@ func withMarriageATX(id types.ATXID) testAtxV2Opt { } } +func withPublishEpoch(epoch types.EpochID) testAtxV2Opt { + return func(atx *ActivationTxV2) { + atx.PublishEpoch = epoch + } +} + func withInitial(commitAtx types.ATXID, post PostV1) testAtxV2Opt { return func(atx *ActivationTxV2) { atx.Initial = &InitialAtxPartsV2{