Skip to content

Commit

Permalink
double-merge malfeasance proof (#6339)
Browse files Browse the repository at this point in the history
## Motivation

Closes #6341 



Co-authored-by: Matthias <5011972+fasmat@users.noreply.github.com>
  • Loading branch information
poszu and fasmat committed Nov 6, 2024
1 parent 516fc47 commit aa8d63c
Show file tree
Hide file tree
Showing 7 changed files with 787 additions and 24 deletions.
21 changes: 19 additions & 2 deletions activation/handler_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
47 changes: 26 additions & 21 deletions activation/handler_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down Expand Up @@ -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)
})
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
169 changes: 169 additions & 0 deletions activation/wire/malfeasance_double_merge.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit aa8d63c

Please sign in to comment.