From 1cdba4d39f9e5ff38e94814b4dbb02374aa4d449 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 7 Jun 2024 14:38:48 +0000 Subject: [PATCH 01/14] Double Publish malfeasance proof --- activation/builder_v2_test.go | 4 +- activation/handler_v2.go | 2 +- activation/malfeasance_service.go | 22 + activation/malfeasance_service_test.go | 1 + activation/wire/malfeasance.go | 9 + activation/wire/malfeasance_double_publish.go | 82 ++++ .../wire/malfeasance_double_publish_test.go | 376 ++++++++++++++++++ activation/wire/wire_v2.go | 21 +- activation/wire/wire_v2_test.go | 80 ---- malfeasance/wire/malfeasance_v2.go | 12 + 10 files changed, 521 insertions(+), 88 deletions(-) create mode 100644 activation/malfeasance_service.go create mode 100644 activation/malfeasance_service_test.go create mode 100644 activation/wire/malfeasance.go create mode 100644 activation/wire/malfeasance_double_publish.go create mode 100644 activation/wire/malfeasance_double_publish_test.go create mode 100644 malfeasance/wire/malfeasance_v2.go diff --git a/activation/builder_v2_test.go b/activation/builder_v2_test.go index 06e87899bb..34f425ee6e 100644 --- a/activation/builder_v2_test.go +++ b/activation/builder_v2_test.go @@ -68,7 +68,7 @@ func TestBuilder_BuildsInitialAtxV2(t *testing.T) { require.Empty(t, atx.Marriages) require.Equal(t, posEpoch+1, atx.PublishEpoch) require.Equal(t, sig.NodeID(), atx.SmesherID) - require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx.SmesherID, atx.SignedBytes(), atx.Signature)) + require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx.SmesherID, atx.ID().Bytes(), atx.Signature)) } func TestBuilder_SwitchesToBuildV2(t *testing.T) { @@ -106,5 +106,5 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { require.Empty(t, atx2.Marriages) require.Equal(t, atx1.PublishEpoch+1, atx2.PublishEpoch) require.Equal(t, sig.NodeID(), atx2.SmesherID) - require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx2.SmesherID, atx2.SignedBytes(), atx2.Signature)) + require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx2.SmesherID, atx2.ID().Bytes(), atx2.Signature)) } diff --git a/activation/handler_v2.go b/activation/handler_v2.go index c9ec247618..142f6afe12 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -150,7 +150,7 @@ func (h *HandlerV2) processATX( // 1. support marriages // 2. support merged ATXs. func (h *HandlerV2) syntacticallyValidate(ctx context.Context, atx *wire.ActivationTxV2) error { - if !h.edVerifier.Verify(signing.ATX, atx.SmesherID, atx.SignedBytes(), atx.Signature) { + if !h.edVerifier.Verify(signing.ATX, atx.SmesherID, atx.ID().Bytes(), atx.Signature) { return fmt.Errorf("invalid atx signature: %w", errMalformedData) } if atx.PositioningATX == types.EmptyATXID { diff --git a/activation/malfeasance_service.go b/activation/malfeasance_service.go new file mode 100644 index 0000000000..9c56b667a0 --- /dev/null +++ b/activation/malfeasance_service.go @@ -0,0 +1,22 @@ +package activation + +import ( + "context" + "errors" + + "go.uber.org/zap" + + "github.com/spacemeshos/go-spacemesh/common/types" +) + +type MalfeasanceService struct { + logger *zap.Logger +} + +func NewMalfeasanceService(logger *zap.Logger) *MalfeasanceService { + return &MalfeasanceService{logger: logger} +} + +func (ms *MalfeasanceService) Validate(ctx context.Context, data []byte) (types.NodeID, error) { + return types.EmptyNodeID, errors.New("not implemented") +} diff --git a/activation/malfeasance_service_test.go b/activation/malfeasance_service_test.go new file mode 100644 index 0000000000..2d8334572b --- /dev/null +++ b/activation/malfeasance_service_test.go @@ -0,0 +1 @@ +package activation diff --git a/activation/wire/malfeasance.go b/activation/wire/malfeasance.go new file mode 100644 index 0000000000..c97d980292 --- /dev/null +++ b/activation/wire/malfeasance.go @@ -0,0 +1,9 @@ +package wire + +// ATXVersion is an identifier to allow for different versions of ATXs to be part of a proof. +type ATXVersion uint8 + +const ( + ATXVersion1 ATXVersion = 1 + ATXVersion2 ATXVersion = 2 +) diff --git a/activation/wire/malfeasance_double_publish.go b/activation/wire/malfeasance_double_publish.go new file mode 100644 index 0000000000..dbcf7330fe --- /dev/null +++ b/activation/wire/malfeasance_double_publish.go @@ -0,0 +1,82 @@ +package wire + +import ( + "encoding/binary" + "errors" + "fmt" + + "github.com/spacemeshos/merkle-tree" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" +) + +// ProofDoublePublish is a proof that two distinct ATXs with the same publish epoch were signed by the same smesher. +type ProofDoublePublish struct { + Proofs [2]PublishProof +} + +// Valid returns true if the proof is valid. It verifies that the two proofs have the same publish epoch, smesher ID, +// and a valid signature but different ATX IDs as well as that the provided merkle proofs are valid. +func (p ProofDoublePublish) Valid(edVerifier *signing.EdVerifier) (bool, error) { + if p.Proofs[0].PubEpoch != p.Proofs[1].PubEpoch { + return false, errors.New("proofs have different publish epochs") + } + + if p.Proofs[0].SmesherID != p.Proofs[1].SmesherID { + return false, errors.New("proofs have different smesher IDs") + } + + if p.Proofs[0].ATXID == p.Proofs[1].ATXID { + return false, errors.New("proofs have the same ATX ID") + } + + atx1Valid, err := p.Proofs[0].Valid(edVerifier) + if err != nil { + return false, fmt.Errorf("proof 1 is invalid: %w", err) + } + if !atx1Valid { + return false, errors.New("proof 1 is invalid") + } + + atx2Valid, err := p.Proofs[1].Valid(edVerifier) + if err != nil { + return false, fmt.Errorf("proof 2 is invalid: %w", err) + } + return atx2Valid, nil +} + +// PublishProof proofs that an ATX was published with a given publish epoch by a given smesher. +type PublishProof struct { + // ATXID is the ID of the ATX being proven. It is the merkle root from the contents of the ATX. + ATXID types.ATXID + // PubEpoch is the epoch in which the ATX was published. + PubEpoch types.EpochID + // Proof contains the merkle path from the root of the ATX merkle tree (ATXID) to the PublishEpoch field. + Proof []types.Hash32 `scale:"max=32"` + + // SmesherID is the ID of the smesher that published the ATX. + SmesherID types.NodeID + // Signature is the signature of the ATXID by the smesher. + Signature types.EdSignature +} + +// Valid returns true if the proof is valid. It verifies that the signature is valid and that the merkle proof is valid. +func (p PublishProof) Valid(edVerifier *signing.EdVerifier) (bool, error) { + if !edVerifier.Verify(signing.ATX, p.SmesherID, p.ATXID.Bytes(), p.Signature) { + return false, errors.New("invalid signature") + } + proof := make([][]byte, len(p.Proof)) + for i, h := range p.Proof { + proof[i] = h.Bytes() + } + epoch := make([]byte, 4) + binary.LittleEndian.PutUint32(epoch, p.PubEpoch.Uint32()) + return merkle.ValidatePartialTree( + []uint64{uint64(PublishEpochIndex)}, + [][]byte{epoch}, + proof, + p.ATXID.Bytes(), + atxTreeHash, + ) +} diff --git a/activation/wire/malfeasance_double_publish_test.go b/activation/wire/malfeasance_double_publish_test.go new file mode 100644 index 0000000000..08d7bdfe44 --- /dev/null +++ b/activation/wire/malfeasance_double_publish_test.go @@ -0,0 +1,376 @@ +package wire + +import ( + "math/rand/v2" + "testing" + + "github.com/spacemeshos/merkle-tree" + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" +) + +type testAtxV2Opt func(*ActivationTxV2) + +func WithPublishEpoch(epoch types.EpochID) testAtxV2Opt { + return func(atx *ActivationTxV2) { + atx.PublishEpoch = epoch + } +} + +func newActivationTxV2(opts ...testAtxV2Opt) *ActivationTxV2 { + atx := &ActivationTxV2{ + PublishEpoch: rand.N(types.EpochID(255)), + PositioningATX: types.RandomATXID(), + PreviousATXs: make([]types.ATXID, 1+rand.IntN(255)), + NiPosts: []NiPostsV2{ + { + Membership: MerkleProofV2{ + Nodes: make([]types.Hash32, 32), + LeafIndices: make([]uint64, 256), + }, + Challenge: types.RandomHash(), + Posts: []SubPostV2{ + { + MarriageIndex: rand.Uint32N(256), + PrevATXIndex: 0, + Post: PostV1{ + Nonce: 0, + Indices: make([]byte, 800), + Pow: 0, + }, + }, + }, + }, + }, + } + for _, opt := range opts { + opt(atx) + } + return atx +} + +func Test_DoublePublishProof(t *testing.T) { + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + t.Run("valid", func(t *testing.T) { + atx1 := newActivationTxV2(WithPublishEpoch(10)) + atx1.Sign(sig) + + atx2 := newActivationTxV2(WithPublishEpoch(10)) + atx2.Sign(sig) + + proof1, err := publishEpochProof(atx1) + require.NoError(t, err) + + proof2, err := publishEpochProof(atx2) + require.NoError(t, err) + + doublePublishProof := &ProofDoublePublish{ + Proofs: [2]PublishProof{ + { + ATXID: atx1.ID(), + PubEpoch: atx1.PublishEpoch, + Proof: proof1, + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + { + ATXID: atx2.ID(), + PubEpoch: atx2.PublishEpoch, + Proof: proof2, + SmesherID: atx2.SmesherID, + Signature: atx2.Signature, + }, + }, + } + + verifier := signing.NewEdVerifier() + ok, err := doublePublishProof.Valid(verifier) + require.NoError(t, err) + require.True(t, ok) + }) + + t.Run("not same epoch", func(t *testing.T) { + atx1 := newActivationTxV2(WithPublishEpoch(10)) + atx1.Sign(sig) + + atx2 := newActivationTxV2(WithPublishEpoch(11)) + atx2.Sign(sig) + + proof1, err := publishEpochProof(atx1) + require.NoError(t, err) + + proof2, err := publishEpochProof(atx2) + require.NoError(t, err) + + doublePublishProof := &ProofDoublePublish{ + Proofs: [2]PublishProof{ + { + ATXID: atx1.ID(), + PubEpoch: atx1.PublishEpoch, + Proof: proof1, + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + { + ATXID: atx2.ID(), + PubEpoch: atx2.PublishEpoch, + Proof: proof2, + SmesherID: atx2.SmesherID, + Signature: atx2.Signature, + }, + }, + } + + verifier := signing.NewEdVerifier() + ok, err := doublePublishProof.Valid(verifier) + require.ErrorContains(t, err, "different publish epochs") + require.False(t, ok) + }) + + t.Run("not same smesher", func(t *testing.T) { + atx1 := newActivationTxV2(WithPublishEpoch(10)) + atx1.Sign(sig) + + atx2 := newActivationTxV2(WithPublishEpoch(10)) + atx2.Sign(sig) + + proof1, err := publishEpochProof(atx1) + require.NoError(t, err) + + proof2, err := publishEpochProof(atx2) + require.NoError(t, err) + + doublePublishProof := &ProofDoublePublish{ + Proofs: [2]PublishProof{ + { + ATXID: atx1.ID(), + PubEpoch: atx1.PublishEpoch, + Proof: proof1, + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + { + ATXID: atx2.ID(), + PubEpoch: atx2.PublishEpoch, + Proof: proof2, + SmesherID: types.RandomNodeID(), + Signature: atx2.Signature, + }, + }, + } + + verifier := signing.NewEdVerifier() + ok, err := doublePublishProof.Valid(verifier) + require.ErrorContains(t, err, "different smesher IDs") + require.False(t, ok) + }) + + t.Run("same ATX ID", func(t *testing.T) { + atx1 := newActivationTxV2(WithPublishEpoch(10)) + atx1.Sign(sig) + + proof1, err := publishEpochProof(atx1) + require.NoError(t, err) + + doublePublishProof := &ProofDoublePublish{ + Proofs: [2]PublishProof{ + { + ATXID: atx1.ID(), + PubEpoch: atx1.PublishEpoch, + Proof: proof1, + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + { + ATXID: atx1.ID(), + PubEpoch: atx1.PublishEpoch, + Proof: proof1, + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + }, + } + + verifier := signing.NewEdVerifier() + ok, err := doublePublishProof.Valid(verifier) + require.ErrorContains(t, err, "same ATX ID") + require.False(t, ok) + }) + + t.Run("invalid proof 1", func(t *testing.T) { + atx1 := newActivationTxV2(WithPublishEpoch(10)) + atx1.Sign(sig) + + atx2 := newActivationTxV2(WithPublishEpoch(10)) + atx2.Sign(sig) + + proof1, err := publishEpochProof(atx1) + require.NoError(t, err) + proof1[0] = types.RandomHash() + + proof2, err := publishEpochProof(atx2) + require.NoError(t, err) + + doublePublishProof := &ProofDoublePublish{ + Proofs: [2]PublishProof{ + { + ATXID: atx1.ID(), + PubEpoch: atx1.PublishEpoch, + Proof: proof1, + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + { + ATXID: atx2.ID(), + PubEpoch: atx2.PublishEpoch, + Proof: proof2, + SmesherID: atx2.SmesherID, + Signature: atx2.Signature, + }, + }, + } + + verifier := signing.NewEdVerifier() + ok, err := doublePublishProof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid") + require.False(t, ok) + }) + + t.Run("invalid proof 2", func(t *testing.T) { + atx1 := newActivationTxV2(WithPublishEpoch(10)) + atx1.Sign(sig) + + atx2 := newActivationTxV2(WithPublishEpoch(10)) + atx2.Sign(sig) + + proof1, err := publishEpochProof(atx1) + require.NoError(t, err) + + proof2, err := publishEpochProof(atx2) + require.NoError(t, err) + proof2[0] = types.RandomHash() + + doublePublishProof := &ProofDoublePublish{ + Proofs: [2]PublishProof{ + { + ATXID: atx1.ID(), + PubEpoch: atx1.PublishEpoch, + Proof: proof1, + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + { + ATXID: atx2.ID(), + PubEpoch: atx2.PublishEpoch, + Proof: proof2, + SmesherID: atx2.SmesherID, + Signature: atx2.Signature, + }, + }, + } + + verifier := signing.NewEdVerifier() + ok, err := doublePublishProof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid") + require.False(t, ok) + }) + + t.Run("invalid signature 1", func(t *testing.T) { + atx1 := newActivationTxV2(WithPublishEpoch(10)) + atx1.Sign(sig) + + atx2 := newActivationTxV2(WithPublishEpoch(10)) + atx2.Sign(sig) + + proof1, err := publishEpochProof(atx1) + require.NoError(t, err) + + proof2, err := publishEpochProof(atx2) + require.NoError(t, err) + + doublePublishProof := &ProofDoublePublish{ + Proofs: [2]PublishProof{ + { + ATXID: atx1.ID(), + PubEpoch: atx1.PublishEpoch, + Proof: proof1, + SmesherID: atx1.SmesherID, + Signature: types.RandomEdSignature(), + }, + { + ATXID: atx2.ID(), + PubEpoch: atx2.PublishEpoch, + Proof: proof2, + SmesherID: atx2.SmesherID, + Signature: atx2.Signature, + }, + }, + } + + verifier := signing.NewEdVerifier() + ok, err := doublePublishProof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid signature") + require.False(t, ok) + }) + + t.Run("invalid signature 2", func(t *testing.T) { + atx1 := newActivationTxV2(WithPublishEpoch(10)) + atx1.Sign(sig) + + atx2 := newActivationTxV2(WithPublishEpoch(10)) + atx2.Sign(sig) + + proof1, err := publishEpochProof(atx1) + require.NoError(t, err) + + proof2, err := publishEpochProof(atx2) + require.NoError(t, err) + + doublePublishProof := &ProofDoublePublish{ + Proofs: [2]PublishProof{ + { + ATXID: atx1.ID(), + PubEpoch: atx1.PublishEpoch, + Proof: proof1, + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + { + ATXID: atx2.ID(), + PubEpoch: atx2.PublishEpoch, + Proof: proof2, + SmesherID: atx2.SmesherID, + Signature: types.RandomEdSignature(), + }, + }, + } + + verifier := signing.NewEdVerifier() + ok, err := doublePublishProof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid signature") + require.False(t, ok) + }) +} + +func publishEpochProof(atx *ActivationTxV2) ([]types.Hash32, error) { + tree, err := merkle.NewTreeBuilder(). + WithLeavesToProve(map[uint64]bool{uint64(PublishEpochIndex): true}). + WithHashFunc(atxTreeHash). + Build() + if err != nil { + return nil, err + } + atx.merkleTree(tree) + proof := tree.Proof() + + proofHashes := make([]types.Hash32, len(proof)) + for i, h := range proof { + proofHashes[i] = types.Hash32(h) + } + + return proofHashes, nil +} diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index 354ee7c75c..4653eaeb26 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -12,6 +12,21 @@ import ( "github.com/spacemeshos/go-spacemesh/signing" ) +// MerkleTreeIndex is the index of the leaf containing the given field in the merkle tree. +type MerkleTreeIndex uint16 + +const ( + PublishEpochIndex MerkleTreeIndex = iota + PositioningATXIndex + CoinbaseIndex + InitialPostIndex + PreviousATXsRootIndex + NIPostsRootIndex + VRFNonceIndex + MarriagesRootIndex + MarriageATXIndex +) + //go:generate scalegen type ActivationTxV2 struct { @@ -44,10 +59,6 @@ type ActivationTxV2 struct { id types.ATXID } -func (atx *ActivationTxV2) SignedBytes() []byte { - return atx.ID().Bytes() -} - func (atx *ActivationTxV2) merkleTree(tree *merkle.Tree) { publishEpoch := make([]byte, 4) binary.LittleEndian.PutUint32(publishEpoch, atx.PublishEpoch.Uint32()) @@ -132,7 +143,7 @@ func (atx *ActivationTxV2) ID() types.ATXID { func (atx *ActivationTxV2) Sign(signer *signing.EdSigner) { atx.SmesherID = signer.NodeID() - atx.Signature = signer.Sign(signing.ATX, atx.SignedBytes()) + atx.Signature = signer.Sign(signing.ATX, atx.ID().Bytes()) } func (atx *ActivationTxV2) Published() types.EpochID { diff --git a/activation/wire/wire_v2_test.go b/activation/wire/wire_v2_test.go index f56ae7423e..a42527d983 100644 --- a/activation/wire/wire_v2_test.go +++ b/activation/wire/wire_v2_test.go @@ -1,12 +1,9 @@ package wire import ( - "encoding/binary" - "math/rand/v2" "testing" fuzz "github.com/google/gofuzz" - "github.com/spacemeshos/merkle-tree" "github.com/stretchr/testify/require" "github.com/spacemeshos/go-spacemesh/common/types" @@ -85,80 +82,3 @@ func Test_NoATXv2IDCollisions(t *testing.T) { atxIDs = append(atxIDs, id) } } - -const PublishEpochIndex = 0 - -func Test_GenerateDoublePublishProof(t *testing.T) { - atx := &ActivationTxV2{ - PublishEpoch: 10, - PositioningATX: types.RandomATXID(), - PreviousATXs: make([]types.ATXID, 1), - NiPosts: []NiPostsV2{ - { - Membership: MerkleProofV2{ - Nodes: make([]types.Hash32, 32), - LeafIndices: make([]uint64, 256), - }, - Challenge: types.RandomHash(), - Posts: []SubPostV2{ - { - MarriageIndex: rand.Uint32N(256), - PrevATXIndex: 0, - Post: PostV1{ - Nonce: 0, - Indices: make([]byte, 800), - Pow: 0, - }, - }, - }, - }, - }, - } - - proof, err := generatePublishEpochProof(atx) - require.NoError(t, err) - require.NotNil(t, proof) - - // a malfeasance proof for double publish will contain - // - the value of the PublishEpoch (here 10) - 4 bytes - // - the two ATX IDs - 32 bytes each - // - the two signatures (atx.Signature + atx.NodeID) - 64 bytes each - // - two merkle proofs - one per ATX - that is 128 bytes each (4 * 32) - // total: 452 bytes instead of two full ATXs (> 20 kB each in the worst case) - - publishEpoch := make([]byte, 4) - binary.LittleEndian.PutUint32(publishEpoch, atx.PublishEpoch.Uint32()) - ok, err := merkle.ValidatePartialTree( - []uint64{PublishEpochIndex}, - [][]byte{publishEpoch}, - proof, - atx.ID().Bytes(), - atxTreeHash, - ) - require.NoError(t, err) - require.True(t, ok) - - // different PublishEpoch doesn't validate - publishEpoch = []byte{0xFF, 0x00, 0x00, 0x00} - ok, err = merkle.ValidatePartialTree( - []uint64{PublishEpochIndex}, - [][]byte{publishEpoch}, - proof, - atx.ID().Bytes(), - atxTreeHash, - ) - require.NoError(t, err) - require.False(t, ok) -} - -func generatePublishEpochProof(atx *ActivationTxV2) ([][]byte, error) { - tree, err := merkle.NewTreeBuilder(). - WithLeavesToProve(map[uint64]bool{PublishEpochIndex: true}). - WithHashFunc(atxTreeHash). - Build() - if err != nil { - return nil, err - } - atx.merkleTree(tree) - return tree.Proof(), nil -} diff --git a/malfeasance/wire/malfeasance_v2.go b/malfeasance/wire/malfeasance_v2.go new file mode 100644 index 0000000000..f58eaba096 --- /dev/null +++ b/malfeasance/wire/malfeasance_v2.go @@ -0,0 +1,12 @@ +package wire + +import "github.com/spacemeshos/go-spacemesh/common/types" + +//go:generate scalegen + +// TODO(mafa): convert existing malfeasance proofs to new format. +type MalfeasanceProofV2 struct { + // for network upgrade + Layer types.LayerID + Proof []byte +} From df8c57c62152ba95cd585674b442677c0add374a Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:16:14 +0000 Subject: [PATCH 02/14] Double marry malfeasance proof --- activation/wire/malfeasance_double_marry.go | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 activation/wire/malfeasance_double_marry.go diff --git a/activation/wire/malfeasance_double_marry.go b/activation/wire/malfeasance_double_marry.go new file mode 100644 index 0000000000..9913306f30 --- /dev/null +++ b/activation/wire/malfeasance_double_marry.go @@ -0,0 +1,5 @@ +package wire + +type MarryProof struct { + ATXs []PublishProof +} From 1b6910f113a8c0399bf3ad729e22bd22b11d69fe Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:08:06 +0000 Subject: [PATCH 03/14] Add more tests and implement double marry proof --- activation/wire/malfeasance_double_marry.go | 237 +++++++++++++++++- .../wire/malfeasance_double_marry_scale.go | 168 +++++++++++++ .../wire/malfeasance_double_marry_test.go | 1 + activation/wire/malfeasance_double_publish.go | 82 +++++- .../wire/malfeasance_double_publish_scale.go | 111 ++++++++ .../wire/malfeasance_double_publish_test.go | 148 ++++------- activation/wire/wire_v2.go | 38 +-- malfeasance/wire/malfeasance_v2.go | 4 +- malfeasance/wire/malfeasance_v2_scale.go | 47 ++++ 9 files changed, 709 insertions(+), 127 deletions(-) create mode 100644 activation/wire/malfeasance_double_marry_scale.go create mode 100644 activation/wire/malfeasance_double_marry_test.go create mode 100644 activation/wire/malfeasance_double_publish_scale.go create mode 100644 malfeasance/wire/malfeasance_v2_scale.go diff --git a/activation/wire/malfeasance_double_marry.go b/activation/wire/malfeasance_double_marry.go index 9913306f30..b97cd0a2e4 100644 --- a/activation/wire/malfeasance_double_marry.go +++ b/activation/wire/malfeasance_double_marry.go @@ -1,5 +1,240 @@ package wire +import ( + "errors" + "fmt" + "slices" + + "github.com/spacemeshos/merkle-tree" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" +) + +//go:generate scalegen + +// ProofDoubleMarry is a proof that two distinct ATXs contain a marriage certificate signed by the same identity. +// +// We are proofing the following: +// 1. The ATXs have different IDs. +// 2. Both ATXs have a valid signature. +// 3. Both ATXs contain a marriage certificate created by the same identity. +// 4. Both marriage certificates have valid signatures. +// +// HINT: this only works if the identity that publishes the ATX with the certificates marries itself. +type ProofDoubleMarry struct { + ATXs [2]MarryProof +} + +func NewDoubleMarryProof(atx1, atx2 *ActivationTxV2, nodeID types.NodeID) (*ProofDoubleMarry, error) { + if atx1.ID() == atx2.ID() { + return nil, errors.New("ATXs have the same ID") + } + + atx1Index := slices.IndexFunc(atx1.Marriages, func(cert MarriageCertificate) bool { + return cert.ID == nodeID + }) + if atx1Index == -1 { + return nil, errors.New("ATX 1 does not contain a marriage certificate signed by the given node ID") + } + atx2Index := slices.IndexFunc(atx2.Marriages, func(cert MarriageCertificate) bool { + return cert.ID == nodeID + }) + if atx2Index == -1 { + return nil, errors.New("ATX 2 does not contain a marriage certificate signed by the given node ID") + } + + proof1, err := marriageProof(atx1) + if err != nil { + return nil, fmt.Errorf("failed to create proof for ATX 1: %w", err) + } + + proof2, err := marriageProof(atx2) + if err != nil { + return nil, fmt.Errorf("failed to create proof for ATX 2: %w", err) + } + + certProof1, err := certificateProof(atx1.Marriages, uint64(atx1Index)) + if err != nil { + return nil, fmt.Errorf("failed to create certificate proof for ATX 1: %w", err) + } + + certProof2, err := certificateProof(atx2.Marriages, uint64(atx2Index)) + if err != nil { + return nil, fmt.Errorf("failed to create certificate proof for ATX 2: %w", err) + } + + proof := &ProofDoubleMarry{ + ATXs: [2]MarryProof{ + { + ATXID: atx1.ID(), + NodeID: nodeID, + + MarriageRoot: types.Hash32(atx1.Marriages.Root()), + MarriageProof: proof1, + + CertificateSignature: atx1.Marriages[atx1Index].Signature, + CertificateIndex: uint64(atx1Index), + CertificateProof: certProof1, + + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + { + ATXID: atx2.ID(), + NodeID: nodeID, + + MarriageRoot: types.Hash32(atx2.Marriages.Root()), + MarriageProof: proof2, + + CertificateSignature: atx2.Marriages[atx2Index].Signature, + CertificateIndex: uint64(atx2Index), + CertificateProof: certProof2, + + SmesherID: atx2.SmesherID, + Signature: atx2.Signature, + }, + }, + } + return proof, nil +} + +func marriageProof(atx *ActivationTxV2) ([]types.Hash32, error) { + tree, err := merkle.NewTreeBuilder(). + WithLeavesToProve(map[uint64]bool{uint64(MarriagesRootIndex): true}). + WithHashFunc(atxTreeHash). + Build() + if err != nil { + return nil, err + } + atx.merkleTree(tree) + proof := tree.Proof() + + proofHashes := make([]types.Hash32, len(proof)) + for i, p := range proof { + proofHashes[i] = types.Hash32(p) + } + return proofHashes, nil +} + +func certificateProof(certs MarriageCertificates, index uint64) ([]types.Hash32, error) { + tree, err := merkle.NewTreeBuilder(). + WithLeavesToProve(map[uint64]bool{index: true}). + WithHashFunc(atxTreeHash). + Build() + if err != nil { + return nil, err + } + certs.merkleTree(tree) + proof := tree.Proof() + + proofHashes := make([]types.Hash32, len(proof)) + for i, p := range proof { + proofHashes[i] = types.Hash32(p) + } + return proofHashes, nil +} + +func (p ProofDoubleMarry) Valid(edVerifier *signing.EdVerifier) (bool, error) { + if p.ATXs[0].ATXID == p.ATXs[1].ATXID { + return false, errors.New("proofs have the same ATX ID") + } + + if p.ATXs[0].SmesherID != p.ATXs[1].SmesherID { + return false, errors.New("proofs have different smesher IDs") + } + + if p.ATXs[0].NodeID != p.ATXs[1].NodeID { + return false, errors.New("proofs have different node IDs") + } + + atx1Valid, err := p.ATXs[0].Valid(edVerifier) + if err != nil { + return false, fmt.Errorf("proof 1 is invalid: %w", err) + } + if !atx1Valid { + return false, nil + } + + atx2Valid, err := p.ATXs[1].Valid(edVerifier) + if err != nil { + return false, fmt.Errorf("proof 2 is invalid: %w", err) + } + if !atx2Valid { + return false, nil + } + + return true, nil +} + type MarryProof struct { - ATXs []PublishProof + // ATXID is the ID of the ATX being proven. + ATXID types.ATXID + // NodeID is the node ID that married twice. + NodeID types.NodeID + + // MarriageRoot and its proof that it is contained in the ATX. + MarriageRoot types.Hash32 + MarriageProof []types.Hash32 `scale:"max=32"` + + // The signature of the certificate and the proof that the certificate is contained in the MarriageRoot at + // the given index. + CertificateSignature types.EdSignature + CertificateIndex uint64 + CertificateProof []types.Hash32 `scale:"max=32"` + + // SmesherID is the ID of the smesher that published the ATX. + SmesherID types.NodeID + // Signature is the signature of the ATXID by the smesher. + Signature types.EdSignature +} + +func (p MarryProof) Valid(edVerifier *signing.EdVerifier) (bool, error) { + if !edVerifier.Verify(signing.ATX, p.SmesherID, p.ATXID.Bytes(), p.Signature) { + return false, errors.New("invalid ATX signature") + } + + // TODO(mafa): check domain + if !edVerifier.Verify(signing.ATX, p.NodeID, p.SmesherID.Bytes(), p.CertificateSignature) { + return false, errors.New("invalid certificate signature") + } + + proof := make([][]byte, len(p.MarriageProof)) + for i, h := range p.MarriageProof { + proof[i] = h.Bytes() + } + ok, err := merkle.ValidatePartialTree( + []uint64{uint64(MarriagesRootIndex)}, + [][]byte{p.MarriageRoot.Bytes()}, + proof, + p.ATXID.Bytes(), + atxTreeHash, + ) + if err != nil { + return false, fmt.Errorf("validate marriage proof: %w", err) + } + if !ok { + return false, nil + } + + mc := MarriageCertificate{ + ID: p.NodeID, + Signature: p.CertificateSignature, + } + + certProof := make([][]byte, len(p.CertificateProof)) + for i, h := range p.CertificateProof { + certProof[i] = h.Bytes() + } + ok, err = merkle.ValidatePartialTree( + []uint64{p.CertificateIndex}, + [][]byte{mc.Root()}, + certProof, + p.MarriageRoot.Bytes(), + atxTreeHash, + ) + if err != nil { + return false, fmt.Errorf("validate certificate proof: %w", err) + } + return ok, nil } diff --git a/activation/wire/malfeasance_double_marry_scale.go b/activation/wire/malfeasance_double_marry_scale.go new file mode 100644 index 0000000000..dc7aa93233 --- /dev/null +++ b/activation/wire/malfeasance_double_marry_scale.go @@ -0,0 +1,168 @@ +// 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 *ProofDoubleMarry) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeStructArray(enc, t.ATXs[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *ProofDoubleMarry) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := scale.DecodeStructArray(dec, t.ATXs[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *MarryProof) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeByteArray(enc, t.ATXID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.NodeID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.MarriageRoot[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.MarriageProof, 32) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.CertificateSignature[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeCompact64(enc, uint64(t.CertificateIndex)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.CertificateProof, 32) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.SmesherID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *MarryProof) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := scale.DecodeByteArray(dec, t.ATXID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.NodeID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.MarriageRoot[:]) + 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.MarriageProof = field + } + { + n, err := scale.DecodeByteArray(dec, t.CertificateSignature[:]) + if err != nil { + return total, err + } + total += n + } + { + field, n, err := scale.DecodeCompact64(dec) + if err != nil { + return total, err + } + total += n + t.CertificateIndex = uint64(field) + } + { + field, n, err := scale.DecodeStructSliceWithLimit[types.Hash32](dec, 32) + if err != nil { + return total, err + } + total += n + t.CertificateProof = field + } + { + n, err := scale.DecodeByteArray(dec, t.SmesherID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} diff --git a/activation/wire/malfeasance_double_marry_test.go b/activation/wire/malfeasance_double_marry_test.go new file mode 100644 index 0000000000..4048932c22 --- /dev/null +++ b/activation/wire/malfeasance_double_marry_test.go @@ -0,0 +1 @@ +package wire diff --git a/activation/wire/malfeasance_double_publish.go b/activation/wire/malfeasance_double_publish.go index dbcf7330fe..1f6c8182ef 100644 --- a/activation/wire/malfeasance_double_publish.go +++ b/activation/wire/malfeasance_double_publish.go @@ -11,24 +11,95 @@ import ( "github.com/spacemeshos/go-spacemesh/signing" ) +//go:generate scalegen + // ProofDoublePublish is a proof that two distinct ATXs with the same publish epoch were signed by the same smesher. +// +// We are proofing the following: +// 1. The ATXs have different IDs. +// 2. Both ATXs have a valid signature. +// 3. Both ATXs were signed by the same smesher. +// 4. Both ATXs have the same publish epoch. type ProofDoublePublish struct { Proofs [2]PublishProof } +func NewDoublePublishProof(atx1, atx2 *ActivationTxV2) (*ProofDoublePublish, 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 different smesher IDs") + } + + if atx1.PublishEpoch != atx2.PublishEpoch { + return nil, errors.New("ATXs have different publish epochs") + } + + proof1, err := publishEpochProof(atx1) + if err != nil { + return nil, fmt.Errorf("failed to create proof for ATX 1: %w", err) + } + + proof2, err := publishEpochProof(atx2) + if err != nil { + return nil, fmt.Errorf("failed to create proof for ATX 2: %w", err) + } + + proof := &ProofDoublePublish{ + Proofs: [2]PublishProof{ + { + ATXID: atx1.ID(), + PubEpoch: atx1.PublishEpoch, + Proof: proof1, + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + { + ATXID: atx2.ID(), + PubEpoch: atx2.PublishEpoch, + Proof: proof2, + SmesherID: atx2.SmesherID, + Signature: atx2.Signature, + }, + }, + } + + return proof, nil +} + +func publishEpochProof(atx *ActivationTxV2) ([]types.Hash32, error) { + tree, err := merkle.NewTreeBuilder(). + WithLeavesToProve(map[uint64]bool{uint64(PublishEpochIndex): true}). + WithHashFunc(atxTreeHash). + Build() + if err != nil { + return nil, err + } + atx.merkleTree(tree) + proof := tree.Proof() + + proofHashes := make([]types.Hash32, len(proof)) + for i, h := range proof { + proofHashes[i] = types.Hash32(h) + } + return proofHashes, nil +} + // Valid returns true if the proof is valid. It verifies that the two proofs have the same publish epoch, smesher ID, // and a valid signature but different ATX IDs as well as that the provided merkle proofs are valid. func (p ProofDoublePublish) Valid(edVerifier *signing.EdVerifier) (bool, error) { - if p.Proofs[0].PubEpoch != p.Proofs[1].PubEpoch { - return false, errors.New("proofs have different publish epochs") + if p.Proofs[0].ATXID == p.Proofs[1].ATXID { + return false, errors.New("proofs have the same ATX ID") } if p.Proofs[0].SmesherID != p.Proofs[1].SmesherID { return false, errors.New("proofs have different smesher IDs") } - if p.Proofs[0].ATXID == p.Proofs[1].ATXID { - return false, errors.New("proofs have the same ATX ID") + if p.Proofs[0].PubEpoch != p.Proofs[1].PubEpoch { + return false, errors.New("proofs have different publish epochs") } atx1Valid, err := p.Proofs[0].Valid(edVerifier) @@ -36,7 +107,7 @@ func (p ProofDoublePublish) Valid(edVerifier *signing.EdVerifier) (bool, error) return false, fmt.Errorf("proof 1 is invalid: %w", err) } if !atx1Valid { - return false, errors.New("proof 1 is invalid") + return false, nil } atx2Valid, err := p.Proofs[1].Valid(edVerifier) @@ -52,6 +123,7 @@ type PublishProof struct { ATXID types.ATXID // PubEpoch is the epoch in which the ATX was published. PubEpoch types.EpochID + // Proof contains the merkle path from the root of the ATX merkle tree (ATXID) to the PublishEpoch field. Proof []types.Hash32 `scale:"max=32"` diff --git a/activation/wire/malfeasance_double_publish_scale.go b/activation/wire/malfeasance_double_publish_scale.go new file mode 100644 index 0000000000..a9ae2007ee --- /dev/null +++ b/activation/wire/malfeasance_double_publish_scale.go @@ -0,0 +1,111 @@ +// 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 *ProofDoublePublish) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeStructArray(enc, t.Proofs[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *ProofDoublePublish) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := scale.DecodeStructArray(dec, t.Proofs[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *PublishProof) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeByteArray(enc, t.ATXID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeCompact32(enc, uint32(t.PubEpoch)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.Proof, 32) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.SmesherID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *PublishProof) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := scale.DecodeByteArray(dec, t.ATXID[:]) + if err != nil { + return total, err + } + total += n + } + { + field, n, err := scale.DecodeCompact32(dec) + if err != nil { + return total, err + } + total += n + t.PubEpoch = types.EpochID(field) + } + { + field, n, err := scale.DecodeStructSliceWithLimit[types.Hash32](dec, 32) + if err != nil { + return total, err + } + total += n + t.Proof = field + } + { + n, err := scale.DecodeByteArray(dec, t.SmesherID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} diff --git a/activation/wire/malfeasance_double_publish_test.go b/activation/wire/malfeasance_double_publish_test.go index 08d7bdfe44..7df086b20b 100644 --- a/activation/wire/malfeasance_double_publish_test.go +++ b/activation/wire/malfeasance_double_publish_test.go @@ -4,7 +4,6 @@ import ( "math/rand/v2" "testing" - "github.com/spacemeshos/merkle-tree" "github.com/stretchr/testify/require" "github.com/spacemeshos/go-spacemesh/common/types" @@ -62,33 +61,11 @@ func Test_DoublePublishProof(t *testing.T) { atx2 := newActivationTxV2(WithPublishEpoch(10)) atx2.Sign(sig) - proof1, err := publishEpochProof(atx1) + proof, err := NewDoublePublishProof(atx1, atx2) require.NoError(t, err) - proof2, err := publishEpochProof(atx2) - require.NoError(t, err) - - doublePublishProof := &ProofDoublePublish{ - Proofs: [2]PublishProof{ - { - ATXID: atx1.ID(), - PubEpoch: atx1.PublishEpoch, - Proof: proof1, - SmesherID: atx1.SmesherID, - Signature: atx1.Signature, - }, - { - ATXID: atx2.ID(), - PubEpoch: atx2.PublishEpoch, - Proof: proof2, - SmesherID: atx2.SmesherID, - Signature: atx2.Signature, - }, - }, - } - verifier := signing.NewEdVerifier() - ok, err := doublePublishProof.Valid(verifier) + ok, err := proof.Valid(verifier) require.NoError(t, err) require.True(t, ok) }) @@ -100,13 +77,18 @@ func Test_DoublePublishProof(t *testing.T) { atx2 := newActivationTxV2(WithPublishEpoch(11)) atx2.Sign(sig) + proof, err := NewDoublePublishProof(atx1, atx2) + require.ErrorContains(t, err, "ATXs have different publish epochs") + require.Nil(t, proof) + + // manually construct an invalid proof proof1, err := publishEpochProof(atx1) require.NoError(t, err) proof2, err := publishEpochProof(atx2) require.NoError(t, err) - doublePublishProof := &ProofDoublePublish{ + proof = &ProofDoublePublish{ Proofs: [2]PublishProof{ { ATXID: atx1.ID(), @@ -126,25 +108,33 @@ func Test_DoublePublishProof(t *testing.T) { } verifier := signing.NewEdVerifier() - ok, err := doublePublishProof.Valid(verifier) + ok, err := proof.Valid(verifier) require.ErrorContains(t, err, "different publish epochs") require.False(t, ok) }) t.Run("not same smesher", func(t *testing.T) { + sig1 := sig atx1 := newActivationTxV2(WithPublishEpoch(10)) - atx1.Sign(sig) + atx1.Sign(sig1) + sig2, err := signing.NewEdSigner() + require.NoError(t, err) atx2 := newActivationTxV2(WithPublishEpoch(10)) - atx2.Sign(sig) + atx2.Sign(sig2) + + proof, err := NewDoublePublishProof(atx1, atx2) + require.ErrorContains(t, err, "ATXs have different smesher IDs") + require.Nil(t, proof) + // manually construct an invalid proof proof1, err := publishEpochProof(atx1) require.NoError(t, err) proof2, err := publishEpochProof(atx2) require.NoError(t, err) - doublePublishProof := &ProofDoublePublish{ + proof = &ProofDoublePublish{ Proofs: [2]PublishProof{ { ATXID: atx1.ID(), @@ -157,14 +147,14 @@ func Test_DoublePublishProof(t *testing.T) { ATXID: atx2.ID(), PubEpoch: atx2.PublishEpoch, Proof: proof2, - SmesherID: types.RandomNodeID(), + SmesherID: atx2.SmesherID, Signature: atx2.Signature, }, }, } verifier := signing.NewEdVerifier() - ok, err := doublePublishProof.Valid(verifier) + ok, err := proof.Valid(verifier) require.ErrorContains(t, err, "different smesher IDs") require.False(t, ok) }) @@ -173,10 +163,15 @@ func Test_DoublePublishProof(t *testing.T) { atx1 := newActivationTxV2(WithPublishEpoch(10)) atx1.Sign(sig) + proof, err := NewDoublePublishProof(atx1, atx1) + require.ErrorContains(t, err, "ATXs have the same ID") + require.Nil(t, proof) + + // manually construct an invalid proof proof1, err := publishEpochProof(atx1) require.NoError(t, err) - doublePublishProof := &ProofDoublePublish{ + proof = &ProofDoublePublish{ Proofs: [2]PublishProof{ { ATXID: atx1.ID(), @@ -196,7 +191,7 @@ func Test_DoublePublishProof(t *testing.T) { } verifier := signing.NewEdVerifier() - ok, err := doublePublishProof.Valid(verifier) + ok, err := proof.Valid(verifier) require.ErrorContains(t, err, "same ATX ID") require.False(t, ok) }) @@ -208,6 +203,7 @@ func Test_DoublePublishProof(t *testing.T) { atx2 := newActivationTxV2(WithPublishEpoch(10)) atx2.Sign(sig) + // manually construct an invalid proof proof1, err := publishEpochProof(atx1) require.NoError(t, err) proof1[0] = types.RandomHash() @@ -215,7 +211,7 @@ func Test_DoublePublishProof(t *testing.T) { proof2, err := publishEpochProof(atx2) require.NoError(t, err) - doublePublishProof := &ProofDoublePublish{ + proof := &ProofDoublePublish{ Proofs: [2]PublishProof{ { ATXID: atx1.ID(), @@ -235,8 +231,8 @@ func Test_DoublePublishProof(t *testing.T) { } verifier := signing.NewEdVerifier() - ok, err := doublePublishProof.Valid(verifier) - require.ErrorContains(t, err, "proof 1 is invalid") + ok, err := proof.Valid(verifier) + require.NoError(t, err) require.False(t, ok) }) @@ -247,6 +243,7 @@ func Test_DoublePublishProof(t *testing.T) { atx2 := newActivationTxV2(WithPublishEpoch(10)) atx2.Sign(sig) + // manually construct an invalid proof proof1, err := publishEpochProof(atx1) require.NoError(t, err) @@ -254,7 +251,7 @@ func Test_DoublePublishProof(t *testing.T) { require.NoError(t, err) proof2[0] = types.RandomHash() - doublePublishProof := &ProofDoublePublish{ + proof := &ProofDoublePublish{ Proofs: [2]PublishProof{ { ATXID: atx1.ID(), @@ -274,8 +271,8 @@ func Test_DoublePublishProof(t *testing.T) { } verifier := signing.NewEdVerifier() - ok, err := doublePublishProof.Valid(verifier) - require.ErrorContains(t, err, "proof 2 is invalid") + ok, err := proof.Valid(verifier) + require.NoError(t, err) require.False(t, ok) }) @@ -286,33 +283,13 @@ func Test_DoublePublishProof(t *testing.T) { atx2 := newActivationTxV2(WithPublishEpoch(10)) atx2.Sign(sig) - proof1, err := publishEpochProof(atx1) + proof, err := NewDoublePublishProof(atx1, atx2) require.NoError(t, err) - proof2, err := publishEpochProof(atx2) - require.NoError(t, err) - - doublePublishProof := &ProofDoublePublish{ - Proofs: [2]PublishProof{ - { - ATXID: atx1.ID(), - PubEpoch: atx1.PublishEpoch, - Proof: proof1, - SmesherID: atx1.SmesherID, - Signature: types.RandomEdSignature(), - }, - { - ATXID: atx2.ID(), - PubEpoch: atx2.PublishEpoch, - Proof: proof2, - SmesherID: atx2.SmesherID, - Signature: atx2.Signature, - }, - }, - } + proof.Proofs[0].Signature = types.RandomEdSignature() verifier := signing.NewEdVerifier() - ok, err := doublePublishProof.Valid(verifier) + ok, err := proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid signature") require.False(t, ok) }) @@ -324,53 +301,14 @@ func Test_DoublePublishProof(t *testing.T) { atx2 := newActivationTxV2(WithPublishEpoch(10)) atx2.Sign(sig) - proof1, err := publishEpochProof(atx1) + proof, err := NewDoublePublishProof(atx1, atx2) require.NoError(t, err) - proof2, err := publishEpochProof(atx2) - require.NoError(t, err) - - doublePublishProof := &ProofDoublePublish{ - Proofs: [2]PublishProof{ - { - ATXID: atx1.ID(), - PubEpoch: atx1.PublishEpoch, - Proof: proof1, - SmesherID: atx1.SmesherID, - Signature: atx1.Signature, - }, - { - ATXID: atx2.ID(), - PubEpoch: atx2.PublishEpoch, - Proof: proof2, - SmesherID: atx2.SmesherID, - Signature: types.RandomEdSignature(), - }, - }, - } + proof.Proofs[1].Signature = types.RandomEdSignature() verifier := signing.NewEdVerifier() - ok, err := doublePublishProof.Valid(verifier) + ok, err := proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid signature") require.False(t, ok) }) } - -func publishEpochProof(atx *ActivationTxV2) ([]types.Hash32, error) { - tree, err := merkle.NewTreeBuilder(). - WithLeavesToProve(map[uint64]bool{uint64(PublishEpochIndex): true}). - WithHashFunc(atxTreeHash). - Build() - if err != nil { - return nil, err - } - atx.merkleTree(tree) - proof := tree.Proof() - - proofHashes := make([]types.Hash32, len(proof)) - for i, h := range proof { - proofHashes[i] = types.Hash32(h) - } - - return proofHashes, nil -} diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index 4653eaeb26..f7b2c00070 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -46,7 +46,7 @@ type ActivationTxV2 struct { // A marriage is permanent and cannot be revoked or repeated. // All new IDs that are married to this ID are added to the equivocation set // that this ID belongs to. - Marriages []MarriageCertificate `scale:"max=256"` + Marriages MarriageCertificates `scale:"max=256"` // The ID of the ATX containing marriage for the included IDs. // Only required when the ATX includes married IDs. @@ -59,6 +59,28 @@ type ActivationTxV2 struct { id types.ATXID } +type MarriageCertificates []MarriageCertificate + +func (mcs MarriageCertificates) Root() []byte { + marriagesTree, err := merkle.NewTreeBuilder(). + WithHashFunc(atxTreeHash). + Build() + if err != nil { + panic(err) + } + mcs.merkleTree(marriagesTree) + return marriagesTree.Root() +} + +func (mcs MarriageCertificates) merkleTree(tree *merkle.Tree) { + for _, marriage := range mcs { + tree.AddLeaf(marriage.Root()) + } + for i := len(mcs); i < 256; i++ { + tree.AddLeaf(types.EmptyHash32.Bytes()) + } +} + func (atx *ActivationTxV2) merkleTree(tree *merkle.Tree) { publishEpoch := make([]byte, 4) binary.LittleEndian.PutUint32(publishEpoch, atx.PublishEpoch.Uint32()) @@ -104,19 +126,7 @@ func (atx *ActivationTxV2) merkleTree(tree *merkle.Tree) { binary.LittleEndian.PutUint64(vrfNonce, atx.VRFNonce) tree.AddLeaf(vrfNonce) - marriagesTree, err := merkle.NewTreeBuilder(). - WithHashFunc(atxTreeHash). - Build() - if err != nil { - panic(err) - } - for _, marriage := range atx.Marriages { - marriagesTree.AddLeaf(marriage.Root()) - } - for i := len(atx.Marriages); i < 256; i++ { - marriagesTree.AddLeaf(types.EmptyHash32.Bytes()) - } - tree.AddLeaf(marriagesTree.Root()) + tree.AddLeaf(atx.Marriages.Root()) if atx.MarriageATX != nil { tree.AddLeaf(atx.MarriageATX.Bytes()) diff --git a/malfeasance/wire/malfeasance_v2.go b/malfeasance/wire/malfeasance_v2.go index f58eaba096..dd9e388d6a 100644 --- a/malfeasance/wire/malfeasance_v2.go +++ b/malfeasance/wire/malfeasance_v2.go @@ -4,9 +4,9 @@ import "github.com/spacemeshos/go-spacemesh/common/types" //go:generate scalegen -// TODO(mafa): convert existing malfeasance proofs to new format. +// TODO(mafa): create proofs for existing malfeasance proofs in new format. type MalfeasanceProofV2 struct { // for network upgrade Layer types.LayerID - Proof []byte + Proof []byte `scale:"max=1048576"` // max size of proof is 1MiB } diff --git a/malfeasance/wire/malfeasance_v2_scale.go b/malfeasance/wire/malfeasance_v2_scale.go new file mode 100644 index 0000000000..35ed1cb5d2 --- /dev/null +++ b/malfeasance/wire/malfeasance_v2_scale.go @@ -0,0 +1,47 @@ +// 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 *MalfeasanceProofV2) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeCompact32(enc, uint32(t.Layer)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteSliceWithLimit(enc, t.Proof, 1048576) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *MalfeasanceProofV2) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + field, n, err := scale.DecodeCompact32(dec) + if err != nil { + return total, err + } + total += n + t.Layer = types.LayerID(field) + } + { + field, n, err := scale.DecodeByteSliceWithLimit(dec, 1048576) + if err != nil { + return total, err + } + total += n + t.Proof = field + } + return total, nil +} From 28eb9d9073c8f80ecb1ef4fc7c6a2fec583717d9 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 13 Jun 2024 20:28:09 +0000 Subject: [PATCH 04/14] Tests for double marry --- activation/wire/malfeasance_double_marry.go | 16 +- .../wire/malfeasance_double_marry_scale.go | 4 +- .../wire/malfeasance_double_marry_test.go | 290 ++++++++++++++++++ activation/wire/malfeasance_double_publish.go | 2 +- .../wire/malfeasance_double_publish_test.go | 107 +------ activation/wire/malfeasance_test.go | 58 ++++ activation/wire/wire_v2.go | 8 +- 7 files changed, 371 insertions(+), 114 deletions(-) create mode 100644 activation/wire/malfeasance_test.go diff --git a/activation/wire/malfeasance_double_marry.go b/activation/wire/malfeasance_double_marry.go index b97cd0a2e4..56329920e1 100644 --- a/activation/wire/malfeasance_double_marry.go +++ b/activation/wire/malfeasance_double_marry.go @@ -23,7 +23,7 @@ import ( // // HINT: this only works if the identity that publishes the ATX with the certificates marries itself. type ProofDoubleMarry struct { - ATXs [2]MarryProof + Proofs [2]MarryProof } func NewDoubleMarryProof(atx1, atx2 *ActivationTxV2, nodeID types.NodeID) (*ProofDoubleMarry, error) { @@ -65,7 +65,7 @@ func NewDoubleMarryProof(atx1, atx2 *ActivationTxV2, nodeID types.NodeID) (*Proo } proof := &ProofDoubleMarry{ - ATXs: [2]MarryProof{ + Proofs: [2]MarryProof{ { ATXID: atx1.ID(), NodeID: nodeID, @@ -136,19 +136,15 @@ func certificateProof(certs MarriageCertificates, index uint64) ([]types.Hash32, } func (p ProofDoubleMarry) Valid(edVerifier *signing.EdVerifier) (bool, error) { - if p.ATXs[0].ATXID == p.ATXs[1].ATXID { + if p.Proofs[0].ATXID == p.Proofs[1].ATXID { return false, errors.New("proofs have the same ATX ID") } - if p.ATXs[0].SmesherID != p.ATXs[1].SmesherID { - return false, errors.New("proofs have different smesher IDs") - } - - if p.ATXs[0].NodeID != p.ATXs[1].NodeID { + if p.Proofs[0].NodeID != p.Proofs[1].NodeID { return false, errors.New("proofs have different node IDs") } - atx1Valid, err := p.ATXs[0].Valid(edVerifier) + atx1Valid, err := p.Proofs[0].Valid(edVerifier) if err != nil { return false, fmt.Errorf("proof 1 is invalid: %w", err) } @@ -156,7 +152,7 @@ func (p ProofDoubleMarry) Valid(edVerifier *signing.EdVerifier) (bool, error) { return false, nil } - atx2Valid, err := p.ATXs[1].Valid(edVerifier) + atx2Valid, err := p.Proofs[1].Valid(edVerifier) if err != nil { return false, fmt.Errorf("proof 2 is invalid: %w", err) } diff --git a/activation/wire/malfeasance_double_marry_scale.go b/activation/wire/malfeasance_double_marry_scale.go index dc7aa93233..fc2db4e7b4 100644 --- a/activation/wire/malfeasance_double_marry_scale.go +++ b/activation/wire/malfeasance_double_marry_scale.go @@ -10,7 +10,7 @@ import ( func (t *ProofDoubleMarry) EncodeScale(enc *scale.Encoder) (total int, err error) { { - n, err := scale.EncodeStructArray(enc, t.ATXs[:]) + n, err := scale.EncodeStructArray(enc, t.Proofs[:]) if err != nil { return total, err } @@ -21,7 +21,7 @@ func (t *ProofDoubleMarry) EncodeScale(enc *scale.Encoder) (total int, err error func (t *ProofDoubleMarry) DecodeScale(dec *scale.Decoder) (total int, err error) { { - n, err := scale.DecodeStructArray(dec, t.ATXs[:]) + n, err := scale.DecodeStructArray(dec, t.Proofs[:]) if err != nil { return total, err } diff --git a/activation/wire/malfeasance_double_marry_test.go b/activation/wire/malfeasance_double_marry_test.go index 4048932c22..88c74f1488 100644 --- a/activation/wire/malfeasance_double_marry_test.go +++ b/activation/wire/malfeasance_double_marry_test.go @@ -1 +1,291 @@ package wire + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" +) + +func Test_DoubleMarryProof(t *testing.T) { + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + + t.Run("valid", func(t *testing.T) { + atx1 := newActivationTxV2( + WithMarriageCertificate(sig, sig.NodeID()), + WithMarriageCertificate(otherSig, sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + WithMarriageCertificate(otherSig, otherSig.NodeID()), + WithMarriageCertificate(sig, otherSig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(atx1, atx2, otherSig.NodeID()) + require.NoError(t, err) + + verifier := signing.NewEdVerifier() + ok, err := proof.Valid(verifier) + require.NoError(t, err) + require.True(t, ok) + }) + + t.Run("does not contain same certificate owner", func(t *testing.T) { + atx1 := newActivationTxV2( + WithMarriageCertificate(sig, sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + WithMarriageCertificate(otherSig, otherSig.NodeID()), + ) + atx2.Sign(sig) + + proof, err := NewDoubleMarryProof(atx1, atx2, otherSig.NodeID()) + require.ErrorContains(t, err, "ATX 1 does not contain a marriage certificate signed by the given node ID") + require.Nil(t, proof) + + proof, err = NewDoubleMarryProof(atx1, atx2, sig.NodeID()) + require.ErrorContains(t, err, "ATX 2 does not contain a marriage certificate signed by the given node ID") + require.Nil(t, proof) + + // manually construct an invalid proof + proof = &ProofDoubleMarry{ + Proofs: [2]MarryProof{ + { + ATXID: atx1.ID(), + NodeID: sig.NodeID(), + }, + { + ATXID: atx2.ID(), + NodeID: otherSig.NodeID(), + }, + }, + } + + verifier := signing.NewEdVerifier() + ok, err := proof.Valid(verifier) + require.ErrorContains(t, err, "proofs have different node IDs") + require.False(t, ok) + }) + + t.Run("same ATX ID", func(t *testing.T) { + atx1 := newActivationTxV2() + atx1.Sign(sig) + + proof, err := NewDoubleMarryProof(atx1, atx1, sig.NodeID()) + require.ErrorContains(t, err, "ATXs have the same ID") + require.Nil(t, proof) + + // manually construct an invalid proof + proof = &ProofDoubleMarry{ + Proofs: [2]MarryProof{ + { + ATXID: atx1.ID(), + }, + { + ATXID: atx1.ID(), + }, + }, + } + + verifier := signing.NewEdVerifier() + ok, err := proof.Valid(verifier) + require.ErrorContains(t, err, "same ATX ID") + require.False(t, ok) + }) + + t.Run("invalid marriage proof", func(t *testing.T) { + atx1 := newActivationTxV2( + WithMarriageCertificate(sig, sig.NodeID()), + WithMarriageCertificate(otherSig, sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + WithMarriageCertificate(otherSig, sig.NodeID()), + WithMarriageCertificate(sig, sig.NodeID()), + ) + atx2.Sign(otherSig) + + // manually construct an invalid proof + proof1, err := marriageProof(atx1) + require.NoError(t, err) + proof2, err := marriageProof(atx2) + require.NoError(t, err) + certProof1, err := certificateProof(atx1.Marriages, 0) + require.NoError(t, err) + certProof2, err := certificateProof(atx2.Marriages, 1) + require.NoError(t, err) + + proof := &ProofDoubleMarry{ + Proofs: [2]MarryProof{ + { + ATXID: atx1.ID(), + NodeID: sig.NodeID(), + MarriageRoot: types.Hash32(atx1.Marriages.Root()), + MarriageProof: proof1, + CertificateSignature: atx1.Marriages[0].Signature, + CertificateIndex: 0, + CertificateProof: certProof1, + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + { + ATXID: atx2.ID(), + NodeID: sig.NodeID(), + MarriageRoot: types.Hash32(atx2.Marriages.Root()), + MarriageProof: proof2, + CertificateSignature: atx2.Marriages[1].Signature, + CertificateIndex: 1, + CertificateProof: certProof2, + SmesherID: atx2.SmesherID, + Signature: atx2.Signature, + }, + }, + } + + verifier := signing.NewEdVerifier() + proof.Proofs[0].MarriageProof[0] = types.RandomHash() + ok, err := proof.Valid(verifier) + require.NoError(t, err) + require.False(t, ok) + + proof.Proofs[0].MarriageProof[0] = proof1[0] + proof.Proofs[1].MarriageProof[0] = types.RandomHash() + ok, err = proof.Valid(verifier) + require.NoError(t, err) + require.False(t, ok) + }) + + t.Run("invalid certificate proof", func(t *testing.T) { + atx1 := newActivationTxV2( + WithMarriageCertificate(sig, sig.NodeID()), + WithMarriageCertificate(otherSig, sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + WithMarriageCertificate(otherSig, sig.NodeID()), + WithMarriageCertificate(sig, sig.NodeID()), + ) + atx2.Sign(otherSig) + + // manually construct an invalid proof + proof1, err := marriageProof(atx1) + require.NoError(t, err) + proof2, err := marriageProof(atx2) + require.NoError(t, err) + certProof1, err := certificateProof(atx1.Marriages, 0) + require.NoError(t, err) + certProof2, err := certificateProof(atx2.Marriages, 1) + require.NoError(t, err) + + proof := &ProofDoubleMarry{ + Proofs: [2]MarryProof{ + { + ATXID: atx1.ID(), + NodeID: sig.NodeID(), + MarriageRoot: types.Hash32(atx1.Marriages.Root()), + MarriageProof: proof1, + CertificateSignature: atx1.Marriages[0].Signature, + CertificateIndex: 0, + CertificateProof: certProof1, + SmesherID: atx1.SmesherID, + Signature: atx1.Signature, + }, + { + ATXID: atx2.ID(), + NodeID: sig.NodeID(), + MarriageRoot: types.Hash32(atx2.Marriages.Root()), + MarriageProof: proof2, + CertificateSignature: atx2.Marriages[1].Signature, + CertificateIndex: 1, + CertificateProof: certProof2, + SmesherID: atx2.SmesherID, + Signature: atx2.Signature, + }, + }, + } + + verifier := signing.NewEdVerifier() + proof.Proofs[0].CertificateProof[0] = types.RandomHash() + ok, err := proof.Valid(verifier) + require.NoError(t, err) + require.False(t, ok) + + proof.Proofs[0].CertificateProof[0] = certProof1[0] + proof.Proofs[1].CertificateProof[0] = types.RandomHash() + ok, err = proof.Valid(verifier) + require.NoError(t, err) + require.False(t, ok) + }) + + t.Run("invalid atx signature", func(t *testing.T) { + atx1 := newActivationTxV2( + WithMarriageCertificate(sig, sig.NodeID()), + WithMarriageCertificate(otherSig, sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + WithMarriageCertificate(otherSig, sig.NodeID()), + WithMarriageCertificate(sig, sig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(atx1, atx2, otherSig.NodeID()) + require.NoError(t, err) + + verifier := signing.NewEdVerifier() + + proof.Proofs[0].Signature = types.RandomEdSignature() + ok, err := proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid ATX signature") + require.False(t, ok) + + proof.Proofs[0].Signature = atx1.Signature + proof.Proofs[1].Signature = types.RandomEdSignature() + ok, err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid ATX signature") + require.False(t, ok) + }) + + t.Run("invalid certificate signature", func(t *testing.T) { + atx1 := newActivationTxV2( + WithMarriageCertificate(sig, sig.NodeID()), + WithMarriageCertificate(otherSig, sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + WithMarriageCertificate(otherSig, sig.NodeID()), + WithMarriageCertificate(sig, sig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(atx1, atx2, otherSig.NodeID()) + require.NoError(t, err) + + verifier := signing.NewEdVerifier() + + proof.Proofs[0].CertificateSignature = types.RandomEdSignature() + ok, err := proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid certificate signature") + require.False(t, ok) + + proof.Proofs[0].CertificateSignature = atx1.Marriages[1].Signature + proof.Proofs[1].CertificateSignature = types.RandomEdSignature() + ok, err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid certificate signature") + require.False(t, ok) + }) +} diff --git a/activation/wire/malfeasance_double_publish.go b/activation/wire/malfeasance_double_publish.go index 1f6c8182ef..649dcc3ff1 100644 --- a/activation/wire/malfeasance_double_publish.go +++ b/activation/wire/malfeasance_double_publish.go @@ -142,7 +142,7 @@ func (p PublishProof) Valid(edVerifier *signing.EdVerifier) (bool, error) { for i, h := range p.Proof { proof[i] = h.Bytes() } - epoch := make([]byte, 4) + epoch := make([]byte, 32) binary.LittleEndian.PutUint32(epoch, p.PubEpoch.Uint32()) return merkle.ValidatePartialTree( []uint64{uint64(PublishEpochIndex)}, diff --git a/activation/wire/malfeasance_double_publish_test.go b/activation/wire/malfeasance_double_publish_test.go index 7df086b20b..04b64de6bb 100644 --- a/activation/wire/malfeasance_double_publish_test.go +++ b/activation/wire/malfeasance_double_publish_test.go @@ -1,7 +1,6 @@ package wire import ( - "math/rand/v2" "testing" "github.com/stretchr/testify/require" @@ -10,46 +9,6 @@ import ( "github.com/spacemeshos/go-spacemesh/signing" ) -type testAtxV2Opt func(*ActivationTxV2) - -func WithPublishEpoch(epoch types.EpochID) testAtxV2Opt { - return func(atx *ActivationTxV2) { - atx.PublishEpoch = epoch - } -} - -func newActivationTxV2(opts ...testAtxV2Opt) *ActivationTxV2 { - atx := &ActivationTxV2{ - PublishEpoch: rand.N(types.EpochID(255)), - PositioningATX: types.RandomATXID(), - PreviousATXs: make([]types.ATXID, 1+rand.IntN(255)), - NiPosts: []NiPostsV2{ - { - Membership: MerkleProofV2{ - Nodes: make([]types.Hash32, 32), - LeafIndices: make([]uint64, 256), - }, - Challenge: types.RandomHash(), - Posts: []SubPostV2{ - { - MarriageIndex: rand.Uint32N(256), - PrevATXIndex: 0, - Post: PostV1{ - Nonce: 0, - Indices: make([]byte, 800), - Pow: 0, - }, - }, - }, - }, - }, - } - for _, opt := range opts { - opt(atx) - } - return atx -} - func Test_DoublePublishProof(t *testing.T) { sig, err := signing.NewEdSigner() require.NoError(t, err) @@ -196,7 +155,7 @@ func Test_DoublePublishProof(t *testing.T) { require.False(t, ok) }) - t.Run("invalid proof 1", func(t *testing.T) { + t.Run("invalid proof", func(t *testing.T) { atx1 := newActivationTxV2(WithPublishEpoch(10)) atx1.Sign(sig) @@ -206,7 +165,6 @@ func Test_DoublePublishProof(t *testing.T) { // manually construct an invalid proof proof1, err := publishEpochProof(atx1) require.NoError(t, err) - proof1[0] = types.RandomHash() proof2, err := publishEpochProof(atx2) require.NoError(t, err) @@ -231,52 +189,19 @@ func Test_DoublePublishProof(t *testing.T) { } verifier := signing.NewEdVerifier() + proof.Proofs[0].Proof[0] = types.RandomHash() ok, err := proof.Valid(verifier) require.NoError(t, err) require.False(t, ok) - }) - - t.Run("invalid proof 2", func(t *testing.T) { - atx1 := newActivationTxV2(WithPublishEpoch(10)) - atx1.Sign(sig) - - atx2 := newActivationTxV2(WithPublishEpoch(10)) - atx2.Sign(sig) - - // manually construct an invalid proof - proof1, err := publishEpochProof(atx1) - require.NoError(t, err) - - proof2, err := publishEpochProof(atx2) - require.NoError(t, err) - proof2[0] = types.RandomHash() - - proof := &ProofDoublePublish{ - Proofs: [2]PublishProof{ - { - ATXID: atx1.ID(), - PubEpoch: atx1.PublishEpoch, - Proof: proof1, - SmesherID: atx1.SmesherID, - Signature: atx1.Signature, - }, - { - ATXID: atx2.ID(), - PubEpoch: atx2.PublishEpoch, - Proof: proof2, - SmesherID: atx2.SmesherID, - Signature: atx2.Signature, - }, - }, - } - verifier := signing.NewEdVerifier() - ok, err := proof.Valid(verifier) + proof.Proofs[0].Proof[0] = proof1[0] + proof.Proofs[1].Proof[0] = types.RandomHash() + ok, err = proof.Valid(verifier) require.NoError(t, err) require.False(t, ok) }) - t.Run("invalid signature 1", func(t *testing.T) { + t.Run("invalid signature", func(t *testing.T) { atx1 := newActivationTxV2(WithPublishEpoch(10)) atx1.Sign(sig) @@ -286,28 +211,16 @@ func Test_DoublePublishProof(t *testing.T) { proof, err := NewDoublePublishProof(atx1, atx2) require.NoError(t, err) - proof.Proofs[0].Signature = types.RandomEdSignature() - verifier := signing.NewEdVerifier() + + proof.Proofs[0].Signature = types.RandomEdSignature() ok, err := proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid signature") require.False(t, ok) - }) - - t.Run("invalid signature 2", func(t *testing.T) { - atx1 := newActivationTxV2(WithPublishEpoch(10)) - atx1.Sign(sig) - - atx2 := newActivationTxV2(WithPublishEpoch(10)) - atx2.Sign(sig) - - proof, err := NewDoublePublishProof(atx1, atx2) - require.NoError(t, err) + proof.Proofs[0].Signature = atx1.Signature proof.Proofs[1].Signature = types.RandomEdSignature() - - verifier := signing.NewEdVerifier() - ok, err := proof.Valid(verifier) + ok, err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid signature") require.False(t, ok) }) diff --git a/activation/wire/malfeasance_test.go b/activation/wire/malfeasance_test.go new file mode 100644 index 0000000000..9eb5c8faec --- /dev/null +++ b/activation/wire/malfeasance_test.go @@ -0,0 +1,58 @@ +package wire + +import ( + "math/rand/v2" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" +) + +type testAtxV2Opt func(*ActivationTxV2) + +func WithPublishEpoch(epoch types.EpochID) testAtxV2Opt { + return func(atx *ActivationTxV2) { + atx.PublishEpoch = epoch + } +} + +func WithMarriageCertificate(sig *signing.EdSigner, atxPublisher types.NodeID) testAtxV2Opt { + return func(atx *ActivationTxV2) { + certificate := MarriageCertificate{ + ID: sig.NodeID(), + Signature: sig.Sign(signing.ATX, atxPublisher.Bytes()), + } + atx.Marriages = append(atx.Marriages, certificate) + } +} + +func newActivationTxV2(opts ...testAtxV2Opt) *ActivationTxV2 { + atx := &ActivationTxV2{ + PublishEpoch: rand.N(types.EpochID(255)), + PositioningATX: types.RandomATXID(), + PreviousATXs: make([]types.ATXID, 1+rand.IntN(255)), + NiPosts: []NiPostsV2{ + { + Membership: MerkleProofV2{ + Nodes: make([]types.Hash32, 32), + LeafIndices: make([]uint64, 256), + }, + Challenge: types.RandomHash(), + Posts: []SubPostV2{ + { + MarriageIndex: rand.Uint32N(256), + PrevATXIndex: 0, + Post: PostV1{ + Nonce: 0, + Indices: make([]byte, 800), + Pow: 0, + }, + }, + }, + }, + }, + } + for _, opt := range opts { + opt(atx) + } + return atx +} diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index f7b2c00070..084457a18a 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -82,7 +82,7 @@ func (mcs MarriageCertificates) merkleTree(tree *merkle.Tree) { } func (atx *ActivationTxV2) merkleTree(tree *merkle.Tree) { - publishEpoch := make([]byte, 4) + publishEpoch := make([]byte, 32) binary.LittleEndian.PutUint32(publishEpoch, atx.PublishEpoch.Uint32()) tree.AddLeaf(publishEpoch) tree.AddLeaf(atx.PositioningATX.Bytes()) @@ -122,7 +122,7 @@ func (atx *ActivationTxV2) merkleTree(tree *merkle.Tree) { } tree.AddLeaf(niPostTree.Root()) - vrfNonce := make([]byte, 8) + vrfNonce := make([]byte, 32) binary.LittleEndian.PutUint64(vrfNonce, atx.VRFNonce) tree.AddLeaf(vrfNonce) @@ -234,7 +234,7 @@ func (sp *SubPostV2) Root(prevATXs []types.ATXID) []byte { if err != nil { panic(err) } - marriageIndex := make([]byte, 4) + marriageIndex := make([]byte, 32) binary.LittleEndian.PutUint32(marriageIndex, sp.MarriageIndex) tree.AddLeaf(marriageIndex) @@ -244,7 +244,7 @@ func (sp *SubPostV2) Root(prevATXs []types.ATXID) []byte { tree.AddLeaf(prevATXs[sp.PrevATXIndex].Bytes()) tree.AddLeaf(sp.Post.Root()) - numUnits := make([]byte, 4) + numUnits := make([]byte, 32) binary.LittleEndian.PutUint32(numUnits, sp.NumUnits) tree.AddLeaf(numUnits) return tree.Root() From 9086798eed50191b4172a78dad4ae12c259c8e22 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 13 Jun 2024 20:57:42 +0000 Subject: [PATCH 05/14] Cleanup --- activation/wire/malfeasance.go | 1 + activation/wire/malfeasance_double_marry.go | 44 ++++++------- .../wire/malfeasance_double_marry_test.go | 63 ++++++++----------- activation/wire/malfeasance_double_publish.go | 39 ++++++------ .../wire/malfeasance_double_publish_test.go | 34 ++++------ 5 files changed, 77 insertions(+), 104 deletions(-) diff --git a/activation/wire/malfeasance.go b/activation/wire/malfeasance.go index c97d980292..9bbb1d720f 100644 --- a/activation/wire/malfeasance.go +++ b/activation/wire/malfeasance.go @@ -3,6 +3,7 @@ package wire // ATXVersion is an identifier to allow for different versions of ATXs to be part of a proof. type ATXVersion uint8 +// TODO(mafa): this is for proofs that involve ATXs from different versions. This is not yet implemented. const ( ATXVersion1 ATXVersion = 1 ATXVersion2 ATXVersion = 2 diff --git a/activation/wire/malfeasance_double_marry.go b/activation/wire/malfeasance_double_marry.go index 56329920e1..e1edc12ff9 100644 --- a/activation/wire/malfeasance_double_marry.go +++ b/activation/wire/malfeasance_double_marry.go @@ -135,32 +135,21 @@ func certificateProof(certs MarriageCertificates, index uint64) ([]types.Hash32, return proofHashes, nil } -func (p ProofDoubleMarry) Valid(edVerifier *signing.EdVerifier) (bool, error) { +func (p ProofDoubleMarry) Valid(edVerifier *signing.EdVerifier) error { if p.Proofs[0].ATXID == p.Proofs[1].ATXID { - return false, errors.New("proofs have the same ATX ID") + return errors.New("proofs have the same ATX ID") } - if p.Proofs[0].NodeID != p.Proofs[1].NodeID { - return false, errors.New("proofs have different node IDs") + return errors.New("proofs have different node IDs") } - atx1Valid, err := p.Proofs[0].Valid(edVerifier) - if err != nil { - return false, fmt.Errorf("proof 1 is invalid: %w", err) - } - if !atx1Valid { - return false, nil + if err := p.Proofs[0].Valid(edVerifier); err != nil { + return fmt.Errorf("proof 1 is invalid: %w", err) } - - atx2Valid, err := p.Proofs[1].Valid(edVerifier) - if err != nil { - return false, fmt.Errorf("proof 2 is invalid: %w", err) + if err := p.Proofs[1].Valid(edVerifier); err != nil { + return fmt.Errorf("proof 2 is invalid: %w", err) } - if !atx2Valid { - return false, nil - } - - return true, nil + return nil } type MarryProof struct { @@ -185,14 +174,14 @@ type MarryProof struct { Signature types.EdSignature } -func (p MarryProof) Valid(edVerifier *signing.EdVerifier) (bool, error) { +func (p MarryProof) Valid(edVerifier *signing.EdVerifier) error { if !edVerifier.Verify(signing.ATX, p.SmesherID, p.ATXID.Bytes(), p.Signature) { - return false, errors.New("invalid ATX signature") + return errors.New("invalid ATX signature") } // TODO(mafa): check domain if !edVerifier.Verify(signing.ATX, p.NodeID, p.SmesherID.Bytes(), p.CertificateSignature) { - return false, errors.New("invalid certificate signature") + return errors.New("invalid certificate signature") } proof := make([][]byte, len(p.MarriageProof)) @@ -207,10 +196,10 @@ func (p MarryProof) Valid(edVerifier *signing.EdVerifier) (bool, error) { atxTreeHash, ) if err != nil { - return false, fmt.Errorf("validate marriage proof: %w", err) + return fmt.Errorf("validate marriage proof: %w", err) } if !ok { - return false, nil + return errors.New("invalid marriage proof") } mc := MarriageCertificate{ @@ -230,7 +219,10 @@ func (p MarryProof) Valid(edVerifier *signing.EdVerifier) (bool, error) { atxTreeHash, ) if err != nil { - return false, fmt.Errorf("validate certificate proof: %w", err) + return fmt.Errorf("validate certificate proof: %w", err) + } + if !ok { + return errors.New("invalid certificate proof") } - return ok, nil + return nil } diff --git a/activation/wire/malfeasance_double_marry_test.go b/activation/wire/malfeasance_double_marry_test.go index 88c74f1488..a80f57c2f9 100644 --- a/activation/wire/malfeasance_double_marry_test.go +++ b/activation/wire/malfeasance_double_marry_test.go @@ -1,6 +1,7 @@ package wire import ( + "slices" "testing" "github.com/stretchr/testify/require" @@ -33,9 +34,7 @@ func Test_DoubleMarryProof(t *testing.T) { require.NoError(t, err) verifier := signing.NewEdVerifier() - ok, err := proof.Valid(verifier) - require.NoError(t, err) - require.True(t, ok) + require.NoError(t, proof.Valid(verifier)) }) t.Run("does not contain same certificate owner", func(t *testing.T) { @@ -72,9 +71,8 @@ func Test_DoubleMarryProof(t *testing.T) { } verifier := signing.NewEdVerifier() - ok, err := proof.Valid(verifier) + err = proof.Valid(verifier) require.ErrorContains(t, err, "proofs have different node IDs") - require.False(t, ok) }) t.Run("same ATX ID", func(t *testing.T) { @@ -98,9 +96,8 @@ func Test_DoubleMarryProof(t *testing.T) { } verifier := signing.NewEdVerifier() - ok, err := proof.Valid(verifier) + err = proof.Valid(verifier) require.ErrorContains(t, err, "same ATX ID") - require.False(t, ok) }) t.Run("invalid marriage proof", func(t *testing.T) { @@ -111,8 +108,8 @@ func Test_DoubleMarryProof(t *testing.T) { atx1.Sign(sig) atx2 := newActivationTxV2( - WithMarriageCertificate(otherSig, sig.NodeID()), - WithMarriageCertificate(sig, sig.NodeID()), + WithMarriageCertificate(otherSig, otherSig.NodeID()), + WithMarriageCertificate(sig, otherSig.NodeID()), ) atx2.Sign(otherSig) @@ -132,7 +129,7 @@ func Test_DoubleMarryProof(t *testing.T) { ATXID: atx1.ID(), NodeID: sig.NodeID(), MarriageRoot: types.Hash32(atx1.Marriages.Root()), - MarriageProof: proof1, + MarriageProof: slices.Clone(proof1), CertificateSignature: atx1.Marriages[0].Signature, CertificateIndex: 0, CertificateProof: certProof1, @@ -143,7 +140,7 @@ func Test_DoubleMarryProof(t *testing.T) { ATXID: atx2.ID(), NodeID: sig.NodeID(), MarriageRoot: types.Hash32(atx2.Marriages.Root()), - MarriageProof: proof2, + MarriageProof: slices.Clone(proof2), CertificateSignature: atx2.Marriages[1].Signature, CertificateIndex: 1, CertificateProof: certProof2, @@ -155,15 +152,13 @@ func Test_DoubleMarryProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].MarriageProof[0] = types.RandomHash() - ok, err := proof.Valid(verifier) - require.NoError(t, err) - require.False(t, ok) + err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid marriage proof") proof.Proofs[0].MarriageProof[0] = proof1[0] proof.Proofs[1].MarriageProof[0] = types.RandomHash() - ok, err = proof.Valid(verifier) - require.NoError(t, err) - require.False(t, ok) + err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid marriage proof") }) t.Run("invalid certificate proof", func(t *testing.T) { @@ -174,8 +169,8 @@ func Test_DoubleMarryProof(t *testing.T) { atx1.Sign(sig) atx2 := newActivationTxV2( - WithMarriageCertificate(otherSig, sig.NodeID()), - WithMarriageCertificate(sig, sig.NodeID()), + WithMarriageCertificate(otherSig, otherSig.NodeID()), + WithMarriageCertificate(sig, otherSig.NodeID()), ) atx2.Sign(otherSig) @@ -198,7 +193,7 @@ func Test_DoubleMarryProof(t *testing.T) { MarriageProof: proof1, CertificateSignature: atx1.Marriages[0].Signature, CertificateIndex: 0, - CertificateProof: certProof1, + CertificateProof: slices.Clone(certProof1), SmesherID: atx1.SmesherID, Signature: atx1.Signature, }, @@ -209,7 +204,7 @@ func Test_DoubleMarryProof(t *testing.T) { MarriageProof: proof2, CertificateSignature: atx2.Marriages[1].Signature, CertificateIndex: 1, - CertificateProof: certProof2, + CertificateProof: slices.Clone(certProof2), SmesherID: atx2.SmesherID, Signature: atx2.Signature, }, @@ -218,15 +213,13 @@ func Test_DoubleMarryProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].CertificateProof[0] = types.RandomHash() - ok, err := proof.Valid(verifier) - require.NoError(t, err) - require.False(t, ok) + err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid certificate proof") proof.Proofs[0].CertificateProof[0] = certProof1[0] proof.Proofs[1].CertificateProof[0] = types.RandomHash() - ok, err = proof.Valid(verifier) - require.NoError(t, err) - require.False(t, ok) + err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid certificate proof") }) t.Run("invalid atx signature", func(t *testing.T) { @@ -248,15 +241,13 @@ func Test_DoubleMarryProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].Signature = types.RandomEdSignature() - ok, err := proof.Valid(verifier) + err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid ATX signature") - require.False(t, ok) proof.Proofs[0].Signature = atx1.Signature proof.Proofs[1].Signature = types.RandomEdSignature() - ok, err = proof.Valid(verifier) + err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid ATX signature") - require.False(t, ok) }) t.Run("invalid certificate signature", func(t *testing.T) { @@ -267,8 +258,8 @@ func Test_DoubleMarryProof(t *testing.T) { atx1.Sign(sig) atx2 := newActivationTxV2( - WithMarriageCertificate(otherSig, sig.NodeID()), - WithMarriageCertificate(sig, sig.NodeID()), + WithMarriageCertificate(otherSig, otherSig.NodeID()), + WithMarriageCertificate(sig, otherSig.NodeID()), ) atx2.Sign(otherSig) @@ -278,14 +269,12 @@ func Test_DoubleMarryProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].CertificateSignature = types.RandomEdSignature() - ok, err := proof.Valid(verifier) + err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid certificate signature") - require.False(t, ok) proof.Proofs[0].CertificateSignature = atx1.Marriages[1].Signature proof.Proofs[1].CertificateSignature = types.RandomEdSignature() - ok, err = proof.Valid(verifier) + err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid certificate signature") - require.False(t, ok) }) } diff --git a/activation/wire/malfeasance_double_publish.go b/activation/wire/malfeasance_double_publish.go index 649dcc3ff1..724489823f 100644 --- a/activation/wire/malfeasance_double_publish.go +++ b/activation/wire/malfeasance_double_publish.go @@ -89,32 +89,24 @@ func publishEpochProof(atx *ActivationTxV2) ([]types.Hash32, error) { // Valid returns true if the proof is valid. It verifies that the two proofs have the same publish epoch, smesher ID, // and a valid signature but different ATX IDs as well as that the provided merkle proofs are valid. -func (p ProofDoublePublish) Valid(edVerifier *signing.EdVerifier) (bool, error) { +func (p ProofDoublePublish) Valid(edVerifier *signing.EdVerifier) error { if p.Proofs[0].ATXID == p.Proofs[1].ATXID { - return false, errors.New("proofs have the same ATX ID") + return errors.New("proofs have the same ATX ID") } - if p.Proofs[0].SmesherID != p.Proofs[1].SmesherID { - return false, errors.New("proofs have different smesher IDs") + return errors.New("proofs have different smesher IDs") } - if p.Proofs[0].PubEpoch != p.Proofs[1].PubEpoch { - return false, errors.New("proofs have different publish epochs") + return errors.New("proofs have different publish epochs") } - atx1Valid, err := p.Proofs[0].Valid(edVerifier) - if err != nil { - return false, fmt.Errorf("proof 1 is invalid: %w", err) + if err := p.Proofs[0].Valid(edVerifier); err != nil { + return fmt.Errorf("proof 1 is invalid: %w", err) } - if !atx1Valid { - return false, nil + if err := p.Proofs[1].Valid(edVerifier); err != nil { + return fmt.Errorf("proof 2 is invalid: %w", err) } - - atx2Valid, err := p.Proofs[1].Valid(edVerifier) - if err != nil { - return false, fmt.Errorf("proof 2 is invalid: %w", err) - } - return atx2Valid, nil + return nil } // PublishProof proofs that an ATX was published with a given publish epoch by a given smesher. @@ -134,9 +126,9 @@ type PublishProof struct { } // Valid returns true if the proof is valid. It verifies that the signature is valid and that the merkle proof is valid. -func (p PublishProof) Valid(edVerifier *signing.EdVerifier) (bool, error) { +func (p PublishProof) Valid(edVerifier *signing.EdVerifier) error { if !edVerifier.Verify(signing.ATX, p.SmesherID, p.ATXID.Bytes(), p.Signature) { - return false, errors.New("invalid signature") + return errors.New("invalid signature") } proof := make([][]byte, len(p.Proof)) for i, h := range p.Proof { @@ -144,11 +136,18 @@ func (p PublishProof) Valid(edVerifier *signing.EdVerifier) (bool, error) { } epoch := make([]byte, 32) binary.LittleEndian.PutUint32(epoch, p.PubEpoch.Uint32()) - return merkle.ValidatePartialTree( + ok, err := merkle.ValidatePartialTree( []uint64{uint64(PublishEpochIndex)}, [][]byte{epoch}, proof, p.ATXID.Bytes(), atxTreeHash, ) + if err != nil { + return fmt.Errorf("validate publish epoch proof: %w", err) + } + if !ok { + return errors.New("invalid publish epoch proof") + } + return nil } diff --git a/activation/wire/malfeasance_double_publish_test.go b/activation/wire/malfeasance_double_publish_test.go index 04b64de6bb..30c05d3dad 100644 --- a/activation/wire/malfeasance_double_publish_test.go +++ b/activation/wire/malfeasance_double_publish_test.go @@ -1,6 +1,7 @@ package wire import ( + "slices" "testing" "github.com/stretchr/testify/require" @@ -24,9 +25,7 @@ func Test_DoublePublishProof(t *testing.T) { require.NoError(t, err) verifier := signing.NewEdVerifier() - ok, err := proof.Valid(verifier) - require.NoError(t, err) - require.True(t, ok) + require.NoError(t, proof.Valid(verifier)) }) t.Run("not same epoch", func(t *testing.T) { @@ -67,9 +66,8 @@ func Test_DoublePublishProof(t *testing.T) { } verifier := signing.NewEdVerifier() - ok, err := proof.Valid(verifier) + err = proof.Valid(verifier) require.ErrorContains(t, err, "different publish epochs") - require.False(t, ok) }) t.Run("not same smesher", func(t *testing.T) { @@ -113,9 +111,8 @@ func Test_DoublePublishProof(t *testing.T) { } verifier := signing.NewEdVerifier() - ok, err := proof.Valid(verifier) + err = proof.Valid(verifier) require.ErrorContains(t, err, "different smesher IDs") - require.False(t, ok) }) t.Run("same ATX ID", func(t *testing.T) { @@ -150,9 +147,8 @@ func Test_DoublePublishProof(t *testing.T) { } verifier := signing.NewEdVerifier() - ok, err := proof.Valid(verifier) + err = proof.Valid(verifier) require.ErrorContains(t, err, "same ATX ID") - require.False(t, ok) }) t.Run("invalid proof", func(t *testing.T) { @@ -174,14 +170,14 @@ func Test_DoublePublishProof(t *testing.T) { { ATXID: atx1.ID(), PubEpoch: atx1.PublishEpoch, - Proof: proof1, + Proof: slices.Clone(proof1), SmesherID: atx1.SmesherID, Signature: atx1.Signature, }, { ATXID: atx2.ID(), PubEpoch: atx2.PublishEpoch, - Proof: proof2, + Proof: slices.Clone(proof2), SmesherID: atx2.SmesherID, Signature: atx2.Signature, }, @@ -190,15 +186,13 @@ func Test_DoublePublishProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].Proof[0] = types.RandomHash() - ok, err := proof.Valid(verifier) - require.NoError(t, err) - require.False(t, ok) + err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid publish epoch proof") proof.Proofs[0].Proof[0] = proof1[0] proof.Proofs[1].Proof[0] = types.RandomHash() - ok, err = proof.Valid(verifier) - require.NoError(t, err) - require.False(t, ok) + err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid publish epoch proof") }) t.Run("invalid signature", func(t *testing.T) { @@ -214,14 +208,12 @@ func Test_DoublePublishProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].Signature = types.RandomEdSignature() - ok, err := proof.Valid(verifier) + err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid signature") - require.False(t, ok) proof.Proofs[0].Signature = atx1.Signature proof.Proofs[1].Signature = types.RandomEdSignature() - ok, err = proof.Valid(verifier) + err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid signature") - require.False(t, ok) }) } From e5e9dad5b8494ab971c3b7a1c5377e82d1f51902 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 13 Jun 2024 21:25:04 +0000 Subject: [PATCH 06/14] Add more comments --- activation/malfeasance_service.go | 20 ++++++++++++++++++-- activation/wire/malfeasance.go | 15 +++++++++++++++ malfeasance/handler.go | 6 +++--- malfeasance/wire/malfeasance_v2.go | 13 +++++++++++-- malfeasance/wire/malfeasance_v2_scale.go | 15 +++++++++++++++ 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/activation/malfeasance_service.go b/activation/malfeasance_service.go index 9c56b667a0..0b04c435ce 100644 --- a/activation/malfeasance_service.go +++ b/activation/malfeasance_service.go @@ -6,6 +6,7 @@ import ( "go.uber.org/zap" + "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/common/types" ) @@ -17,6 +18,21 @@ func NewMalfeasanceService(logger *zap.Logger) *MalfeasanceService { return &MalfeasanceService{logger: logger} } -func (ms *MalfeasanceService) Validate(ctx context.Context, data []byte) (types.NodeID, error) { - return types.EmptyNodeID, errors.New("not implemented") +func (ms *MalfeasanceService) Validate(ctx context.Context, data []byte) ([]types.NodeID, error) { + // TODO(mafa): this is called by the handler in the malfeasance package if the proof is of type `InvalidActivation` + // + // decode to wire.ATXProof + // decode []byte to proof based on proof type + // call proof.Validate() + // if proof is valid, check certificates + // return node IDs for all valid certificates + return nil, errors.New("not implemented") +} + +func (ms *MalfeasanceService) Publish(ctx context.Context, proof wire.ATXProof) error { + // TODO(mafa): this is called by the ATX handler in the activation package + // + // encode proof to []byte + // bubble up to malfeasance handler to encode as `MalfeasanceProofV2` and publish + return errors.New("not implemented") } diff --git a/activation/wire/malfeasance.go b/activation/wire/malfeasance.go index 9bbb1d720f..23940a4cfa 100644 --- a/activation/wire/malfeasance.go +++ b/activation/wire/malfeasance.go @@ -8,3 +8,18 @@ const ( ATXVersion1 ATXVersion = 1 ATXVersion2 ATXVersion = 2 ) + +type ProofType byte + +const ( + DoublePublish ProofType = iota + 1 + DoubleMarry +) + +type ATXProof struct { + ProofType ProofType + + // TODO(mafa): add field with certificates for IDs that share the marriage set with the ID proven to be malfeasant + + Proof []byte `scale:"max=1048576"` // max size of proof is 1MiB +} diff --git a/malfeasance/handler.go b/malfeasance/handler.go index ff6590ecbb..26f03774fd 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -41,9 +41,9 @@ const ( // V2 types // TODO(mafa): for future use. - InvalidActivation MalfeasanceType = iota + 10 - InvalidBallot - InvalidHareMsg + InvalidActivation MalfeasanceType = MalfeasanceType(wire.InvalidActivation) + InvalidBallot = MalfeasanceType(wire.InvalidBallot) + InvalidHareMsg = MalfeasanceType(wire.InvalidHareMsg) ) // Handler processes MalfeasanceProof from gossip and, if deems it valid, propagates it to peers. diff --git a/malfeasance/wire/malfeasance_v2.go b/malfeasance/wire/malfeasance_v2.go index dd9e388d6a..674d145861 100644 --- a/malfeasance/wire/malfeasance_v2.go +++ b/malfeasance/wire/malfeasance_v2.go @@ -4,9 +4,18 @@ import "github.com/spacemeshos/go-spacemesh/common/types" //go:generate scalegen +type ProofType byte + +const ( + InvalidActivation ProofType = iota + 10 + InvalidBallot + InvalidHareMsg +) + // TODO(mafa): create proofs for existing malfeasance proofs in new format. type MalfeasanceProofV2 struct { // for network upgrade - Layer types.LayerID - Proof []byte `scale:"max=1048576"` // max size of proof is 1MiB + Layer types.LayerID + ProofType ProofType + Proof []byte `scale:"max=1048576"` // max size of proof is 1MiB } diff --git a/malfeasance/wire/malfeasance_v2_scale.go b/malfeasance/wire/malfeasance_v2_scale.go index 35ed1cb5d2..632e548a35 100644 --- a/malfeasance/wire/malfeasance_v2_scale.go +++ b/malfeasance/wire/malfeasance_v2_scale.go @@ -16,6 +16,13 @@ func (t *MalfeasanceProofV2) EncodeScale(enc *scale.Encoder) (total int, err err } total += n } + { + n, err := scale.EncodeCompact8(enc, uint8(t.ProofType)) + if err != nil { + return total, err + } + total += n + } { n, err := scale.EncodeByteSliceWithLimit(enc, t.Proof, 1048576) if err != nil { @@ -35,6 +42,14 @@ func (t *MalfeasanceProofV2) DecodeScale(dec *scale.Decoder) (total int, err err total += n t.Layer = types.LayerID(field) } + { + field, n, err := scale.DecodeCompact8(dec) + if err != nil { + return total, err + } + total += n + t.ProofType = ProofType(field) + } { field, n, err := scale.DecodeByteSliceWithLimit(dec, 1048576) if err != nil { From 06f1f342e51b6feab936aa251f5be2ecfcd537e5 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 13 Jun 2024 21:40:14 +0000 Subject: [PATCH 07/14] Use types.Hash32 for leafs --- activation/wire/wire_v2.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index 084457a18a..5d7e7219bf 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -82,9 +82,9 @@ func (mcs MarriageCertificates) merkleTree(tree *merkle.Tree) { } func (atx *ActivationTxV2) merkleTree(tree *merkle.Tree) { - publishEpoch := make([]byte, 32) - binary.LittleEndian.PutUint32(publishEpoch, atx.PublishEpoch.Uint32()) - tree.AddLeaf(publishEpoch) + var publishEpoch types.Hash32 + binary.LittleEndian.PutUint32(publishEpoch[:], atx.PublishEpoch.Uint32()) + tree.AddLeaf(publishEpoch.Bytes()) tree.AddLeaf(atx.PositioningATX.Bytes()) tree.AddLeaf(atx.Coinbase.Bytes()) @@ -122,9 +122,9 @@ func (atx *ActivationTxV2) merkleTree(tree *merkle.Tree) { } tree.AddLeaf(niPostTree.Root()) - vrfNonce := make([]byte, 32) - binary.LittleEndian.PutUint64(vrfNonce, atx.VRFNonce) - tree.AddLeaf(vrfNonce) + var vrfNonce types.Hash32 + binary.LittleEndian.PutUint64(vrfNonce[:], atx.VRFNonce) + tree.AddLeaf(vrfNonce.Bytes()) tree.AddLeaf(atx.Marriages.Root()) @@ -234,9 +234,9 @@ func (sp *SubPostV2) Root(prevATXs []types.ATXID) []byte { if err != nil { panic(err) } - marriageIndex := make([]byte, 32) - binary.LittleEndian.PutUint32(marriageIndex, sp.MarriageIndex) - tree.AddLeaf(marriageIndex) + var marriageIndex types.Hash32 + binary.LittleEndian.PutUint32(marriageIndex[:], sp.MarriageIndex) + tree.AddLeaf(marriageIndex.Bytes()) if int(sp.PrevATXIndex) >= len(prevATXs) { return nil // invalid index, root cannot be generated @@ -244,9 +244,9 @@ func (sp *SubPostV2) Root(prevATXs []types.ATXID) []byte { tree.AddLeaf(prevATXs[sp.PrevATXIndex].Bytes()) tree.AddLeaf(sp.Post.Root()) - numUnits := make([]byte, 32) - binary.LittleEndian.PutUint32(numUnits, sp.NumUnits) - tree.AddLeaf(numUnits) + var numUnits types.Hash32 + binary.LittleEndian.PutUint32(numUnits[:], sp.NumUnits) + tree.AddLeaf(numUnits.Bytes()) return tree.Root() } From 3103cd7124566e3ce795a0386a8bcd85f15c4fbb Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Fri, 14 Jun 2024 09:41:37 +0000 Subject: [PATCH 08/14] First draft activation malfeasance service --- activation/malfeasance_service.go | 64 +++++++++++++++---- activation/wire/malfeasance.go | 37 ++++++++--- activation/wire/malfeasance_double_marry.go | 17 ++--- .../wire/malfeasance_double_marry_test.go | 34 ++++++---- activation/wire/malfeasance_double_publish.go | 16 +++-- .../wire/malfeasance_double_publish_test.go | 25 +++++--- activation/wire/malfeasance_scale.go | 46 +++++++++++++ activation/wire/malfeasance_test.go | 2 +- signing/signer.go | 1 + 9 files changed, 187 insertions(+), 55 deletions(-) create mode 100644 activation/wire/malfeasance_scale.go diff --git a/activation/malfeasance_service.go b/activation/malfeasance_service.go index 0b04c435ce..6b215cf69c 100644 --- a/activation/malfeasance_service.go +++ b/activation/malfeasance_service.go @@ -3,30 +3,72 @@ package activation import ( "context" "errors" + "fmt" "go.uber.org/zap" "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/signing" ) type MalfeasanceService struct { - logger *zap.Logger + logger *zap.Logger + edVerifier *signing.EdVerifier } -func NewMalfeasanceService(logger *zap.Logger) *MalfeasanceService { - return &MalfeasanceService{logger: logger} +func NewMalfeasanceService(logger *zap.Logger, edVerifier *signing.EdVerifier) *MalfeasanceService { + return &MalfeasanceService{ + logger: logger, + edVerifier: edVerifier, + } } func (ms *MalfeasanceService) Validate(ctx context.Context, data []byte) ([]types.NodeID, error) { - // TODO(mafa): this is called by the handler in the malfeasance package if the proof is of type `InvalidActivation` - // - // decode to wire.ATXProof - // decode []byte to proof based on proof type - // call proof.Validate() - // if proof is valid, check certificates - // return node IDs for all valid certificates - return nil, errors.New("not implemented") + var decoded wire.ATXProof + if err := codec.Decode(data, &decoded); err != nil { + return nil, fmt.Errorf("decoding ATX malfeasance proof: %w", err) + } + + var proof wire.Proof + switch decoded.ProofType { + case wire.DoublePublish: + var p wire.ProofDoublePublish + if err := codec.Decode(decoded.Proof, &p); err != nil { + return nil, fmt.Errorf("decoding ATX double publish proof: %w", err) + } + proof = p + case wire.DoubleMarry: + var p wire.ProofDoubleMarry + if err := codec.Decode(decoded.Proof, &p); err != nil { + return nil, fmt.Errorf("decoding ATX double marry proof: %w", err) + } + proof = p + default: + return nil, fmt.Errorf("unknown ATX malfeasance proof type: %d", decoded.ProofType) + } + + id, err := proof.Valid(ms.edVerifier) + if err != nil { + return nil, fmt.Errorf("validating ATX malfeasance proof: %w", err) + } + + validIDs := make([]types.NodeID, 0, len(decoded.Certificates)+1) + validIDs = append(validIDs, id) // id has already been proven to be malfeasant + + // check certificates provided with the proof + // TODO(mafa): this only works if the main identity becomes malfeasant - try different approach with merkle proofs + for _, cert := range decoded.Certificates { + if id != cert.Target { + continue + } + if !ms.edVerifier.Verify(signing.MARRIAGE, cert.Target, cert.ID.Bytes(), cert.Signature) { + continue + } + validIDs = append(validIDs, cert.ID) + } + return validIDs, nil } func (ms *MalfeasanceService) Publish(ctx context.Context, proof wire.ATXProof) error { diff --git a/activation/wire/malfeasance.go b/activation/wire/malfeasance.go index 23940a4cfa..0296642b6d 100644 --- a/activation/wire/malfeasance.go +++ b/activation/wire/malfeasance.go @@ -1,14 +1,13 @@ package wire -// ATXVersion is an identifier to allow for different versions of ATXs to be part of a proof. -type ATXVersion uint8 - -// TODO(mafa): this is for proofs that involve ATXs from different versions. This is not yet implemented. -const ( - ATXVersion1 ATXVersion = 1 - ATXVersion2 ATXVersion = 2 +import ( + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" ) +//go:generate scalegen + +// ProofType is an identifier for the type of proof that is encoded in the ATXProof. type ProofType byte const ( @@ -17,9 +16,29 @@ const ( ) type ATXProof struct { + // ProofType is the type of proof that is being provided. ProofType ProofType + // Certificates is a slice of marriage certificates showing which identities belong to the same marriage set as + // the one proven to be malfeasant. Up to 1024 can be put into a single proof, since by repeatedly marrying other + // identities there can be much more than 256 in a malfeasant marriage set. Beyond that a second proof could be + // provided to show that additional identities are part of the same malfeasant marriage set. + Certificates []ProofCertificate `scale:"max=1024"` + // Proof is the actual proof. Its type depends on the ProofType. + Proof []byte `scale:"max=1048576"` // max size of proof is 1MiB +} - // TODO(mafa): add field with certificates for IDs that share the marriage set with the ID proven to be malfeasant +// ProofCertificate proofs that two identities are part of the same marriage set. +type ProofCertificate struct { + // Target of the certificate, i.e. the identity that signed the ATX containing the original certificate. + Target types.NodeID + // ID is the identity that signed the certificate. + ID types.NodeID + // Signature is the signature of the certificate, i.e. ID signed with the key of SmesherID. + Signature types.EdSignature +} - Proof []byte `scale:"max=1048576"` // max size of proof is 1MiB +// Proof is an interface for all types of proofs that can be provided in an ATXProof. +// Generally the proof should be able to validate itself. +type Proof interface { + Valid(edVerifier *signing.EdVerifier) (types.NodeID, error) } diff --git a/activation/wire/malfeasance_double_marry.go b/activation/wire/malfeasance_double_marry.go index e1edc12ff9..686f8a4367 100644 --- a/activation/wire/malfeasance_double_marry.go +++ b/activation/wire/malfeasance_double_marry.go @@ -26,6 +26,8 @@ type ProofDoubleMarry struct { Proofs [2]MarryProof } +var _ Proof = &ProofDoubleMarry{} + func NewDoubleMarryProof(atx1, atx2 *ActivationTxV2, nodeID types.NodeID) (*ProofDoubleMarry, error) { if atx1.ID() == atx2.ID() { return nil, errors.New("ATXs have the same ID") @@ -135,21 +137,21 @@ func certificateProof(certs MarriageCertificates, index uint64) ([]types.Hash32, return proofHashes, nil } -func (p ProofDoubleMarry) Valid(edVerifier *signing.EdVerifier) error { +func (p ProofDoubleMarry) Valid(edVerifier *signing.EdVerifier) (types.NodeID, error) { if p.Proofs[0].ATXID == p.Proofs[1].ATXID { - return errors.New("proofs have the same ATX ID") + return types.EmptyNodeID, errors.New("proofs have the same ATX ID") } if p.Proofs[0].NodeID != p.Proofs[1].NodeID { - return errors.New("proofs have different node IDs") + return types.EmptyNodeID, errors.New("proofs have different node IDs") } if err := p.Proofs[0].Valid(edVerifier); err != nil { - return fmt.Errorf("proof 1 is invalid: %w", err) + return types.EmptyNodeID, fmt.Errorf("proof 1 is invalid: %w", err) } if err := p.Proofs[1].Valid(edVerifier); err != nil { - return fmt.Errorf("proof 2 is invalid: %w", err) + return types.EmptyNodeID, fmt.Errorf("proof 2 is invalid: %w", err) } - return nil + return p.Proofs[0].NodeID, nil } type MarryProof struct { @@ -179,8 +181,7 @@ func (p MarryProof) Valid(edVerifier *signing.EdVerifier) error { return errors.New("invalid ATX signature") } - // TODO(mafa): check domain - if !edVerifier.Verify(signing.ATX, p.NodeID, p.SmesherID.Bytes(), p.CertificateSignature) { + if !edVerifier.Verify(signing.MARRIAGE, p.NodeID, p.SmesherID.Bytes(), p.CertificateSignature) { return errors.New("invalid certificate signature") } diff --git a/activation/wire/malfeasance_double_marry_test.go b/activation/wire/malfeasance_double_marry_test.go index a80f57c2f9..0655d44f4e 100644 --- a/activation/wire/malfeasance_double_marry_test.go +++ b/activation/wire/malfeasance_double_marry_test.go @@ -34,7 +34,9 @@ func Test_DoubleMarryProof(t *testing.T) { require.NoError(t, err) verifier := signing.NewEdVerifier() - require.NoError(t, proof.Valid(verifier)) + id, err := proof.Valid(verifier) + require.NoError(t, err) + require.Equal(t, otherSig.NodeID(), id) }) t.Run("does not contain same certificate owner", func(t *testing.T) { @@ -71,8 +73,9 @@ func Test_DoubleMarryProof(t *testing.T) { } verifier := signing.NewEdVerifier() - err = proof.Valid(verifier) + id, err := proof.Valid(verifier) require.ErrorContains(t, err, "proofs have different node IDs") + require.Equal(t, types.EmptyNodeID, id) }) t.Run("same ATX ID", func(t *testing.T) { @@ -96,8 +99,9 @@ func Test_DoubleMarryProof(t *testing.T) { } verifier := signing.NewEdVerifier() - err = proof.Valid(verifier) + id, err := proof.Valid(verifier) require.ErrorContains(t, err, "same ATX ID") + require.Equal(t, types.EmptyNodeID, id) }) t.Run("invalid marriage proof", func(t *testing.T) { @@ -152,13 +156,15 @@ func Test_DoubleMarryProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].MarriageProof[0] = types.RandomHash() - err = proof.Valid(verifier) + id, err := proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid marriage proof") + require.Equal(t, types.EmptyNodeID, id) proof.Proofs[0].MarriageProof[0] = proof1[0] proof.Proofs[1].MarriageProof[0] = types.RandomHash() - err = proof.Valid(verifier) + id, err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid marriage proof") + require.Equal(t, types.EmptyNodeID, id) }) t.Run("invalid certificate proof", func(t *testing.T) { @@ -213,13 +219,15 @@ func Test_DoubleMarryProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].CertificateProof[0] = types.RandomHash() - err = proof.Valid(verifier) + id, err := proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid certificate proof") + require.Equal(t, types.EmptyNodeID, id) proof.Proofs[0].CertificateProof[0] = certProof1[0] proof.Proofs[1].CertificateProof[0] = types.RandomHash() - err = proof.Valid(verifier) + id, err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid certificate proof") + require.Equal(t, types.EmptyNodeID, id) }) t.Run("invalid atx signature", func(t *testing.T) { @@ -241,13 +249,15 @@ func Test_DoubleMarryProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].Signature = types.RandomEdSignature() - err = proof.Valid(verifier) + id, err := proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid ATX signature") + require.Equal(t, types.EmptyNodeID, id) proof.Proofs[0].Signature = atx1.Signature proof.Proofs[1].Signature = types.RandomEdSignature() - err = proof.Valid(verifier) + id, err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid ATX signature") + require.Equal(t, types.EmptyNodeID, id) }) t.Run("invalid certificate signature", func(t *testing.T) { @@ -269,12 +279,14 @@ func Test_DoubleMarryProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].CertificateSignature = types.RandomEdSignature() - err = proof.Valid(verifier) + id, err := proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid certificate signature") + require.Equal(t, types.EmptyNodeID, id) proof.Proofs[0].CertificateSignature = atx1.Marriages[1].Signature proof.Proofs[1].CertificateSignature = types.RandomEdSignature() - err = proof.Valid(verifier) + id, err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid certificate signature") + require.Equal(t, types.EmptyNodeID, id) }) } diff --git a/activation/wire/malfeasance_double_publish.go b/activation/wire/malfeasance_double_publish.go index 724489823f..9072977c3b 100644 --- a/activation/wire/malfeasance_double_publish.go +++ b/activation/wire/malfeasance_double_publish.go @@ -24,6 +24,8 @@ type ProofDoublePublish struct { Proofs [2]PublishProof } +var _ Proof = &ProofDoublePublish{} + func NewDoublePublishProof(atx1, atx2 *ActivationTxV2) (*ProofDoublePublish, error) { if atx1.ID() == atx2.ID() { return nil, errors.New("ATXs have the same ID") @@ -89,24 +91,24 @@ func publishEpochProof(atx *ActivationTxV2) ([]types.Hash32, error) { // Valid returns true if the proof is valid. It verifies that the two proofs have the same publish epoch, smesher ID, // and a valid signature but different ATX IDs as well as that the provided merkle proofs are valid. -func (p ProofDoublePublish) Valid(edVerifier *signing.EdVerifier) error { +func (p ProofDoublePublish) Valid(edVerifier *signing.EdVerifier) (types.NodeID, error) { if p.Proofs[0].ATXID == p.Proofs[1].ATXID { - return errors.New("proofs have the same ATX ID") + return types.EmptyNodeID, errors.New("proofs have the same ATX ID") } if p.Proofs[0].SmesherID != p.Proofs[1].SmesherID { - return errors.New("proofs have different smesher IDs") + return types.EmptyNodeID, errors.New("proofs have different smesher IDs") } if p.Proofs[0].PubEpoch != p.Proofs[1].PubEpoch { - return errors.New("proofs have different publish epochs") + return types.EmptyNodeID, errors.New("proofs have different publish epochs") } if err := p.Proofs[0].Valid(edVerifier); err != nil { - return fmt.Errorf("proof 1 is invalid: %w", err) + return types.EmptyNodeID, fmt.Errorf("proof 1 is invalid: %w", err) } if err := p.Proofs[1].Valid(edVerifier); err != nil { - return fmt.Errorf("proof 2 is invalid: %w", err) + return types.EmptyNodeID, fmt.Errorf("proof 2 is invalid: %w", err) } - return nil + return p.Proofs[0].SmesherID, nil } // PublishProof proofs that an ATX was published with a given publish epoch by a given smesher. diff --git a/activation/wire/malfeasance_double_publish_test.go b/activation/wire/malfeasance_double_publish_test.go index 30c05d3dad..d41cf1afe8 100644 --- a/activation/wire/malfeasance_double_publish_test.go +++ b/activation/wire/malfeasance_double_publish_test.go @@ -25,7 +25,9 @@ func Test_DoublePublishProof(t *testing.T) { require.NoError(t, err) verifier := signing.NewEdVerifier() - require.NoError(t, proof.Valid(verifier)) + id, err := proof.Valid(verifier) + require.NoError(t, err) + require.Equal(t, sig.NodeID(), id) }) t.Run("not same epoch", func(t *testing.T) { @@ -66,8 +68,9 @@ func Test_DoublePublishProof(t *testing.T) { } verifier := signing.NewEdVerifier() - err = proof.Valid(verifier) + id, err := proof.Valid(verifier) require.ErrorContains(t, err, "different publish epochs") + require.Equal(t, types.EmptyNodeID, id) }) t.Run("not same smesher", func(t *testing.T) { @@ -111,8 +114,9 @@ func Test_DoublePublishProof(t *testing.T) { } verifier := signing.NewEdVerifier() - err = proof.Valid(verifier) + id, err := proof.Valid(verifier) require.ErrorContains(t, err, "different smesher IDs") + require.Equal(t, types.EmptyNodeID, id) }) t.Run("same ATX ID", func(t *testing.T) { @@ -147,8 +151,9 @@ func Test_DoublePublishProof(t *testing.T) { } verifier := signing.NewEdVerifier() - err = proof.Valid(verifier) + id, err := proof.Valid(verifier) require.ErrorContains(t, err, "same ATX ID") + require.Equal(t, types.EmptyNodeID, id) }) t.Run("invalid proof", func(t *testing.T) { @@ -186,13 +191,15 @@ func Test_DoublePublishProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].Proof[0] = types.RandomHash() - err = proof.Valid(verifier) + id, err := proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid publish epoch proof") + require.Equal(t, types.EmptyNodeID, id) proof.Proofs[0].Proof[0] = proof1[0] proof.Proofs[1].Proof[0] = types.RandomHash() - err = proof.Valid(verifier) + id, err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid publish epoch proof") + require.Equal(t, types.EmptyNodeID, id) }) t.Run("invalid signature", func(t *testing.T) { @@ -208,12 +215,14 @@ func Test_DoublePublishProof(t *testing.T) { verifier := signing.NewEdVerifier() proof.Proofs[0].Signature = types.RandomEdSignature() - err = proof.Valid(verifier) + id, err := proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid signature") + require.Equal(t, types.EmptyNodeID, id) proof.Proofs[0].Signature = atx1.Signature proof.Proofs[1].Signature = types.RandomEdSignature() - err = proof.Valid(verifier) + id, err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid signature") + require.Equal(t, types.EmptyNodeID, id) }) } diff --git a/activation/wire/malfeasance_scale.go b/activation/wire/malfeasance_scale.go new file mode 100644 index 0000000000..5e4556adba --- /dev/null +++ b/activation/wire/malfeasance_scale.go @@ -0,0 +1,46 @@ +// Code generated by github.com/spacemeshos/go-scale/scalegen. DO NOT EDIT. + +// nolint +package wire + +import ( + "github.com/spacemeshos/go-scale" +) + +func (t *ATXProof) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeCompact8(enc, uint8(t.ProofType)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteSliceWithLimit(enc, t.Proof, 1048576) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *ATXProof) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + field, n, err := scale.DecodeCompact8(dec) + if err != nil { + return total, err + } + total += n + t.ProofType = ProofType(field) + } + { + field, n, err := scale.DecodeByteSliceWithLimit(dec, 1048576) + if err != nil { + return total, err + } + total += n + t.Proof = field + } + return total, nil +} diff --git a/activation/wire/malfeasance_test.go b/activation/wire/malfeasance_test.go index 9eb5c8faec..ece81938f0 100644 --- a/activation/wire/malfeasance_test.go +++ b/activation/wire/malfeasance_test.go @@ -19,7 +19,7 @@ func WithMarriageCertificate(sig *signing.EdSigner, atxPublisher types.NodeID) t return func(atx *ActivationTxV2) { certificate := MarriageCertificate{ ID: sig.NodeID(), - Signature: sig.Sign(signing.ATX, atxPublisher.Bytes()), + Signature: sig.Sign(signing.MARRIAGE, atxPublisher.Bytes()), } atx.Marriages = append(atx.Marriages, certificate) } diff --git a/signing/signer.go b/signing/signer.go index f3b258c1be..c07f8589a1 100644 --- a/signing/signer.go +++ b/signing/signer.go @@ -24,6 +24,7 @@ const ( BALLOT = 2 HARE = 3 POET = 4 + MARRIAGE = 5 BEACON_FIRST_MSG = 10 BEACON_FOLLOWUP_MSG = 11 From 8a547b12c0ddcf9d8069f8dec73e742d90604c6b Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:41:45 +0000 Subject: [PATCH 09/14] make generate --- activation/wire/malfeasance_scale.go | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/activation/wire/malfeasance_scale.go b/activation/wire/malfeasance_scale.go index 5e4556adba..48c7de5ba0 100644 --- a/activation/wire/malfeasance_scale.go +++ b/activation/wire/malfeasance_scale.go @@ -15,6 +15,13 @@ func (t *ATXProof) EncodeScale(enc *scale.Encoder) (total int, err error) { } total += n } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.Certificates, 1024) + if err != nil { + return total, err + } + total += n + } { n, err := scale.EncodeByteSliceWithLimit(enc, t.Proof, 1048576) if err != nil { @@ -34,6 +41,14 @@ func (t *ATXProof) DecodeScale(dec *scale.Decoder) (total int, err error) { total += n t.ProofType = ProofType(field) } + { + field, n, err := scale.DecodeStructSliceWithLimit[ProofCertificate](dec, 1024) + if err != nil { + return total, err + } + total += n + t.Certificates = field + } { field, n, err := scale.DecodeByteSliceWithLimit(dec, 1048576) if err != nil { @@ -44,3 +59,53 @@ func (t *ATXProof) DecodeScale(dec *scale.Decoder) (total int, err error) { } return total, nil } + +func (t *ProofCertificate) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeByteArray(enc, t.Target[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.ID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *ProofCertificate) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := scale.DecodeByteArray(dec, t.Target[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.ID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} From d6053ad84e07c357f9b2a40d87247187877b0321 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Wed, 19 Jun 2024 21:00:30 +0000 Subject: [PATCH 10/14] Change double marry to new atx certificates structure --- activation/wire/malfeasance_double_marry.go | 98 +++++---- .../wire/malfeasance_double_marry_scale.go | 14 ++ .../wire/malfeasance_double_marry_test.go | 193 ++++++++++-------- activation/wire/malfeasance_test.go | 6 +- activation/wire/wire_v2.go | 12 +- activation/wire/wire_v2_scale.go | 4 +- 6 files changed, 178 insertions(+), 149 deletions(-) diff --git a/activation/wire/malfeasance_double_marry.go b/activation/wire/malfeasance_double_marry.go index 686f8a4367..eb02f8d6c9 100644 --- a/activation/wire/malfeasance_double_marry.go +++ b/activation/wire/malfeasance_double_marry.go @@ -9,6 +9,8 @@ import ( "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 @@ -28,75 +30,66 @@ type ProofDoubleMarry struct { var _ Proof = &ProofDoubleMarry{} -func NewDoubleMarryProof(atx1, atx2 *ActivationTxV2, nodeID types.NodeID) (*ProofDoubleMarry, error) { +func NewDoubleMarryProof(db sql.Executor, atx1, atx2 *ActivationTxV2, nodeID types.NodeID) (*ProofDoubleMarry, error) { if atx1.ID() == atx2.ID() { return nil, errors.New("ATXs have the same ID") } - atx1Index := slices.IndexFunc(atx1.Marriages, func(cert MarriageCertificate) bool { - return cert.ID == nodeID - }) - if atx1Index == -1 { - return nil, errors.New("ATX 1 does not contain a marriage certificate signed by the given node ID") - } - atx2Index := slices.IndexFunc(atx2.Marriages, func(cert MarriageCertificate) bool { - return cert.ID == nodeID - }) - if atx2Index == -1 { - return nil, errors.New("ATX 2 does not contain a marriage certificate signed by the given node ID") + proof1, err := createMarryProof(db, atx1, nodeID) + if err != nil { + return nil, fmt.Errorf("proof for atx1: %w", err) } - proof1, err := marriageProof(atx1) + proof2, err := createMarryProof(db, atx2, nodeID) if err != nil { - return nil, fmt.Errorf("failed to create proof for ATX 1: %w", err) + return nil, fmt.Errorf("proof for atx2: %w", err) } - proof2, err := marriageProof(atx2) - if err != nil { - return nil, fmt.Errorf("failed to create proof for ATX 2: %w", err) + proof := &ProofDoubleMarry{ + Proofs: [2]MarryProof{proof1, proof2}, } + return proof, nil +} - certProof1, err := certificateProof(atx1.Marriages, uint64(atx1Index)) +func createMarryProof(db sql.Executor, atx *ActivationTxV2, nodeID types.NodeID) (MarryProof, error) { + marriageProof, err := marriageProof(atx) if err != nil { - return nil, fmt.Errorf("failed to create certificate proof for ATX 1: %w", err) + return MarryProof{}, fmt.Errorf("failed to create proof for ATX 1: %w", err) + } + + marriageIndex := slices.IndexFunc(atx.Marriages, func(cert MarriageCertificate) bool { + if cert.ReferenceAtx == types.EmptyATXID && atx.SmesherID == nodeID { + // special case of the self signed certificate of the ATX publisher + return true + } + refATX, err := atxs.Get(db, cert.ReferenceAtx) + if err != nil { + return false + } + return refATX.SmesherID == nodeID + }) + if marriageIndex == -1 { + return MarryProof{}, fmt.Errorf("does not contain a marriage certificate signed by %s", nodeID.ShortString()) } - - certProof2, err := certificateProof(atx2.Marriages, uint64(atx2Index)) + certProof, err := certificateProof(atx.Marriages, uint64(marriageIndex)) if err != nil { - return nil, fmt.Errorf("failed to create certificate proof for ATX 2: %w", err) + return MarryProof{}, fmt.Errorf("failed to create certificate proof for ATX 1: %w", err) } - proof := &ProofDoubleMarry{ - Proofs: [2]MarryProof{ - { - ATXID: atx1.ID(), - NodeID: nodeID, - - MarriageRoot: types.Hash32(atx1.Marriages.Root()), - MarriageProof: proof1, - - CertificateSignature: atx1.Marriages[atx1Index].Signature, - CertificateIndex: uint64(atx1Index), - CertificateProof: certProof1, - - SmesherID: atx1.SmesherID, - Signature: atx1.Signature, - }, - { - ATXID: atx2.ID(), - NodeID: nodeID, + proof := MarryProof{ + ATXID: atx.ID(), + NodeID: nodeID, - MarriageRoot: types.Hash32(atx2.Marriages.Root()), - MarriageProof: proof2, + MarriageRoot: types.Hash32(atx.Marriages.Root()), + MarriageProof: marriageProof, - CertificateSignature: atx2.Marriages[atx2Index].Signature, - CertificateIndex: uint64(atx2Index), - CertificateProof: certProof2, + CertificateReference: atx.Marriages[marriageIndex].ReferenceAtx, + CertificateSignature: atx.Marriages[marriageIndex].Signature, + CertificateIndex: uint64(marriageIndex), + CertificateProof: certProof, - SmesherID: atx2.SmesherID, - Signature: atx2.Signature, - }, - }, + SmesherID: atx.SmesherID, + Signature: atx.Signature, } return proof, nil } @@ -166,6 +159,7 @@ type MarryProof struct { // The signature of the certificate and the proof that the certificate is contained in the MarriageRoot at // the given index. + CertificateReference types.ATXID CertificateSignature types.EdSignature CertificateIndex uint64 CertificateProof []types.Hash32 `scale:"max=32"` @@ -204,8 +198,8 @@ func (p MarryProof) Valid(edVerifier *signing.EdVerifier) error { } mc := MarriageCertificate{ - ID: p.NodeID, - Signature: p.CertificateSignature, + ReferenceAtx: p.CertificateReference, + Signature: p.CertificateSignature, } certProof := make([][]byte, len(p.CertificateProof)) diff --git a/activation/wire/malfeasance_double_marry_scale.go b/activation/wire/malfeasance_double_marry_scale.go index fc2db4e7b4..7f5caa171d 100644 --- a/activation/wire/malfeasance_double_marry_scale.go +++ b/activation/wire/malfeasance_double_marry_scale.go @@ -59,6 +59,13 @@ func (t *MarryProof) EncodeScale(enc *scale.Encoder) (total int, err error) { } total += n } + { + n, err := scale.EncodeByteArray(enc, t.CertificateReference[:]) + if err != nil { + return total, err + } + total += n + } { n, err := scale.EncodeByteArray(enc, t.CertificateSignature[:]) if err != nil { @@ -127,6 +134,13 @@ func (t *MarryProof) DecodeScale(dec *scale.Decoder) (total int, err error) { total += n t.MarriageProof = field } + { + n, err := scale.DecodeByteArray(dec, t.CertificateReference[:]) + if err != nil { + return total, err + } + total += n + } { n, err := scale.DecodeByteArray(dec, t.CertificateSignature[:]) if err != nil { diff --git a/activation/wire/malfeasance_double_marry_test.go b/activation/wire/malfeasance_double_marry_test.go index 0655d44f4e..cd7358abbb 100644 --- a/activation/wire/malfeasance_double_marry_test.go +++ b/activation/wire/malfeasance_double_marry_test.go @@ -1,6 +1,7 @@ package wire import ( + "fmt" "slices" "testing" @@ -8,6 +9,8 @@ import ( "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" ) func Test_DoubleMarryProof(t *testing.T) { @@ -18,20 +21,27 @@ func Test_DoubleMarryProof(t *testing.T) { require.NoError(t, err) t.Run("valid", func(t *testing.T) { + db := sql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx)) + atx1 := newActivationTxV2( - WithMarriageCertificate(sig, sig.NodeID()), - WithMarriageCertificate(otherSig, sig.NodeID()), + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) atx1.Sign(sig) atx2 := newActivationTxV2( - WithMarriageCertificate(otherSig, otherSig.NodeID()), - WithMarriageCertificate(sig, otherSig.NodeID()), + WithMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + WithMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), ) atx2.Sign(otherSig) - proof, err := NewDoubleMarryProof(atx1, atx2, otherSig.NodeID()) + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) require.NoError(t, err) + require.NotNil(t, proof) verifier := signing.NewEdVerifier() id, err := proof.Valid(verifier) @@ -40,22 +50,28 @@ func Test_DoubleMarryProof(t *testing.T) { }) t.Run("does not contain same certificate owner", func(t *testing.T) { + db := sql.InMemory() + atx1 := newActivationTxV2( - WithMarriageCertificate(sig, sig.NodeID()), + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), ) atx1.Sign(sig) atx2 := newActivationTxV2( - WithMarriageCertificate(otherSig, otherSig.NodeID()), + WithMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), ) - atx2.Sign(sig) + atx2.Sign(otherSig) - proof, err := NewDoubleMarryProof(atx1, atx2, otherSig.NodeID()) - require.ErrorContains(t, err, "ATX 1 does not contain a marriage certificate signed by the given node ID") + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.ErrorContains(t, err, fmt.Sprintf( + "proof for atx1: does not contain a marriage certificate signed by %s", otherSig.NodeID().ShortString(), + )) require.Nil(t, proof) - proof, err = NewDoubleMarryProof(atx1, atx2, sig.NodeID()) - require.ErrorContains(t, err, "ATX 2 does not contain a marriage certificate signed by the given node ID") + proof, err = NewDoubleMarryProof(db, atx1, atx2, sig.NodeID()) + require.ErrorContains(t, err, fmt.Sprintf( + "proof for atx2: does not contain a marriage certificate signed by %s", sig.NodeID().ShortString(), + )) require.Nil(t, proof) // manually construct an invalid proof @@ -82,7 +98,8 @@ func Test_DoubleMarryProof(t *testing.T) { atx1 := newActivationTxV2() atx1.Sign(sig) - proof, err := NewDoubleMarryProof(atx1, atx1, sig.NodeID()) + db := sql.InMemory() + proof, err := NewDoubleMarryProof(db, atx1, atx1, sig.NodeID()) require.ErrorContains(t, err, "ATXs have the same ID") require.Nil(t, proof) @@ -105,62 +122,45 @@ func Test_DoubleMarryProof(t *testing.T) { }) t.Run("invalid marriage proof", func(t *testing.T) { + db := sql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx)) + atx1 := newActivationTxV2( - WithMarriageCertificate(sig, sig.NodeID()), - WithMarriageCertificate(otherSig, sig.NodeID()), + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) atx1.Sign(sig) atx2 := newActivationTxV2( - WithMarriageCertificate(otherSig, otherSig.NodeID()), - WithMarriageCertificate(sig, otherSig.NodeID()), + WithMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + WithMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), ) atx2.Sign(otherSig) // manually construct an invalid proof - proof1, err := marriageProof(atx1) + proof1, err := createMarryProof(db, atx1, otherSig.NodeID()) require.NoError(t, err) - proof2, err := marriageProof(atx2) - require.NoError(t, err) - certProof1, err := certificateProof(atx1.Marriages, 0) - require.NoError(t, err) - certProof2, err := certificateProof(atx2.Marriages, 1) + proof2, err := createMarryProof(db, atx2, otherSig.NodeID()) require.NoError(t, err) proof := &ProofDoubleMarry{ Proofs: [2]MarryProof{ - { - ATXID: atx1.ID(), - NodeID: sig.NodeID(), - MarriageRoot: types.Hash32(atx1.Marriages.Root()), - MarriageProof: slices.Clone(proof1), - CertificateSignature: atx1.Marriages[0].Signature, - CertificateIndex: 0, - CertificateProof: certProof1, - SmesherID: atx1.SmesherID, - Signature: atx1.Signature, - }, - { - ATXID: atx2.ID(), - NodeID: sig.NodeID(), - MarriageRoot: types.Hash32(atx2.Marriages.Root()), - MarriageProof: slices.Clone(proof2), - CertificateSignature: atx2.Marriages[1].Signature, - CertificateIndex: 1, - CertificateProof: certProof2, - SmesherID: atx2.SmesherID, - Signature: atx2.Signature, - }, + proof1, proof2, }, } verifier := signing.NewEdVerifier() + proof.Proofs[0].MarriageProof = slices.Clone(proof1.MarriageProof) proof.Proofs[0].MarriageProof[0] = types.RandomHash() id, err := proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid marriage proof") require.Equal(t, types.EmptyNodeID, id) - proof.Proofs[0].MarriageProof[0] = proof1[0] + proof.Proofs[0].MarriageProof[0] = proof1.MarriageProof[0] + proof.Proofs[1].MarriageProof = slices.Clone(proof2.MarriageProof) proof.Proofs[1].MarriageProof[0] = types.RandomHash() id, err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid marriage proof") @@ -168,62 +168,45 @@ func Test_DoubleMarryProof(t *testing.T) { }) t.Run("invalid certificate proof", func(t *testing.T) { + db := sql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx)) + atx1 := newActivationTxV2( - WithMarriageCertificate(sig, sig.NodeID()), - WithMarriageCertificate(otherSig, sig.NodeID()), + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) atx1.Sign(sig) atx2 := newActivationTxV2( - WithMarriageCertificate(otherSig, otherSig.NodeID()), - WithMarriageCertificate(sig, otherSig.NodeID()), + WithMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + WithMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), ) atx2.Sign(otherSig) // manually construct an invalid proof - proof1, err := marriageProof(atx1) - require.NoError(t, err) - proof2, err := marriageProof(atx2) + proof1, err := createMarryProof(db, atx1, otherSig.NodeID()) require.NoError(t, err) - certProof1, err := certificateProof(atx1.Marriages, 0) - require.NoError(t, err) - certProof2, err := certificateProof(atx2.Marriages, 1) + proof2, err := createMarryProof(db, atx2, otherSig.NodeID()) require.NoError(t, err) proof := &ProofDoubleMarry{ Proofs: [2]MarryProof{ - { - ATXID: atx1.ID(), - NodeID: sig.NodeID(), - MarriageRoot: types.Hash32(atx1.Marriages.Root()), - MarriageProof: proof1, - CertificateSignature: atx1.Marriages[0].Signature, - CertificateIndex: 0, - CertificateProof: slices.Clone(certProof1), - SmesherID: atx1.SmesherID, - Signature: atx1.Signature, - }, - { - ATXID: atx2.ID(), - NodeID: sig.NodeID(), - MarriageRoot: types.Hash32(atx2.Marriages.Root()), - MarriageProof: proof2, - CertificateSignature: atx2.Marriages[1].Signature, - CertificateIndex: 1, - CertificateProof: slices.Clone(certProof2), - SmesherID: atx2.SmesherID, - Signature: atx2.Signature, - }, + proof1, proof2, }, } verifier := signing.NewEdVerifier() + proof.Proofs[0].CertificateProof = slices.Clone(proof1.CertificateProof) proof.Proofs[0].CertificateProof[0] = types.RandomHash() id, err := proof.Valid(verifier) require.ErrorContains(t, err, "proof 1 is invalid: invalid certificate proof") require.Equal(t, types.EmptyNodeID, id) - proof.Proofs[0].CertificateProof[0] = certProof1[0] + proof.Proofs[0].CertificateProof[0] = proof1.CertificateProof[0] + proof.Proofs[1].CertificateProof = slices.Clone(proof2.CertificateProof) proof.Proofs[1].CertificateProof[0] = types.RandomHash() id, err = proof.Valid(verifier) require.ErrorContains(t, err, "proof 2 is invalid: invalid certificate proof") @@ -231,19 +214,25 @@ func Test_DoubleMarryProof(t *testing.T) { }) t.Run("invalid atx signature", func(t *testing.T) { + db := sql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx)) + atx1 := newActivationTxV2( - WithMarriageCertificate(sig, sig.NodeID()), - WithMarriageCertificate(otherSig, sig.NodeID()), + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) atx1.Sign(sig) atx2 := newActivationTxV2( - WithMarriageCertificate(otherSig, sig.NodeID()), - WithMarriageCertificate(sig, sig.NodeID()), + WithMarriageCertificate(otherSig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(sig, atx1.ID(), sig.NodeID()), ) atx2.Sign(otherSig) - proof, err := NewDoubleMarryProof(atx1, atx2, otherSig.NodeID()) + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) require.NoError(t, err) verifier := signing.NewEdVerifier() @@ -261,19 +250,25 @@ func Test_DoubleMarryProof(t *testing.T) { }) t.Run("invalid certificate signature", func(t *testing.T) { + db := sql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx)) + atx1 := newActivationTxV2( - WithMarriageCertificate(sig, sig.NodeID()), - WithMarriageCertificate(otherSig, sig.NodeID()), + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) atx1.Sign(sig) atx2 := newActivationTxV2( - WithMarriageCertificate(otherSig, otherSig.NodeID()), - WithMarriageCertificate(sig, otherSig.NodeID()), + WithMarriageCertificate(otherSig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(sig, atx1.ID(), sig.NodeID()), ) atx2.Sign(otherSig) - proof, err := NewDoubleMarryProof(atx1, atx2, otherSig.NodeID()) + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) require.NoError(t, err) verifier := signing.NewEdVerifier() @@ -289,4 +284,24 @@ func Test_DoubleMarryProof(t *testing.T) { require.ErrorContains(t, err, "proof 2 is invalid: invalid certificate signature") require.Equal(t, types.EmptyNodeID, id) }) + + t.Run("unknown reference ATX", func(t *testing.T) { + db := sql.InMemory() + + atx1 := newActivationTxV2( + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, types.RandomATXID(), sig.NodeID()), // unknown reference ATX + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + WithMarriageCertificate(otherSig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(sig, atx1.ID(), sig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.Error(t, err) + require.Nil(t, proof) + }) } diff --git a/activation/wire/malfeasance_test.go b/activation/wire/malfeasance_test.go index ece81938f0..bec0280a87 100644 --- a/activation/wire/malfeasance_test.go +++ b/activation/wire/malfeasance_test.go @@ -15,11 +15,11 @@ func WithPublishEpoch(epoch types.EpochID) testAtxV2Opt { } } -func WithMarriageCertificate(sig *signing.EdSigner, atxPublisher types.NodeID) testAtxV2Opt { +func WithMarriageCertificate(sig *signing.EdSigner, refAtx types.ATXID, atxPublisher types.NodeID) testAtxV2Opt { return func(atx *ActivationTxV2) { certificate := MarriageCertificate{ - ID: sig.NodeID(), - Signature: sig.Sign(signing.MARRIAGE, atxPublisher.Bytes()), + ReferenceAtx: refAtx, + Signature: sig.Sign(signing.MARRIAGE, atxPublisher.Bytes()), } atx.Marriages = append(atx.Marriages, certificate) } diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index 5d7e7219bf..03dbde8eb2 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -191,7 +191,13 @@ func (i *InitialAtxPartsV2) Root() []byte { // A marriage allows for publishing a merged ATX, which can contain PoST for all married IDs. // Any ID from the marriage can publish a merged ATX on behalf of all married IDs. type MarriageCertificate struct { - ID types.NodeID + // An ATX of the ID that marries. It proves that the ID exists. + // Note: the reference ATX does not need to be from the previous epoch. + // It only needs to prove the existence of the ID. + // + // In the case of a self signed certificate that is included in the Marriage ATX by the Smesher signing the ATX, + // this can be `types.EmptyATXID`. + ReferenceAtx types.ATXID // Signature over the other ID that this ID marries with // If Alice marries Bob, then Alice signs Bob's ID // and Bob includes this certificate in his ATX. @@ -205,7 +211,7 @@ func (mc *MarriageCertificate) Root() []byte { if err != nil { panic(err) } - tree.AddLeaf(mc.ID.Bytes()) + tree.AddLeaf(mc.ReferenceAtx.Bytes()) tree.AddLeaf(mc.Signature.Bytes()) return tree.Root() } @@ -333,7 +339,7 @@ func (marriage *MarriageCertificate) MarshalLogObject(encoder zapcore.ObjectEnco if marriage == nil { return nil } - encoder.AddString("ID", marriage.ID.String()) + encoder.AddString("ReferenceATX", marriage.ReferenceAtx.String()) encoder.AddString("Signature", marriage.Signature.String()) return nil } diff --git a/activation/wire/wire_v2_scale.go b/activation/wire/wire_v2_scale.go index 5024ba3202..286a200428 100644 --- a/activation/wire/wire_v2_scale.go +++ b/activation/wire/wire_v2_scale.go @@ -215,7 +215,7 @@ func (t *InitialAtxPartsV2) DecodeScale(dec *scale.Decoder) (total int, err erro func (t *MarriageCertificate) EncodeScale(enc *scale.Encoder) (total int, err error) { { - n, err := scale.EncodeByteArray(enc, t.ID[:]) + n, err := scale.EncodeByteArray(enc, t.ReferenceAtx[:]) if err != nil { return total, err } @@ -233,7 +233,7 @@ func (t *MarriageCertificate) EncodeScale(enc *scale.Encoder) (total int, err er func (t *MarriageCertificate) DecodeScale(dec *scale.Decoder) (total int, err error) { { - n, err := scale.DecodeByteArray(dec, t.ID[:]) + n, err := scale.DecodeByteArray(dec, t.ReferenceAtx[:]) if err != nil { return total, err } From d2d1612c93dfe06996377fa3fb0f5ff90dfb13ce Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Sun, 23 Jun 2024 21:33:13 +0000 Subject: [PATCH 11/14] Fix a few issues with malfeasance handling --- activation/handler.go | 3 ++- activation/handler_v2.go | 35 +++++++++++++++++---------- activation/handler_v2_test.go | 2 +- malfeasance/wire/malfeasance.go | 9 +------ malfeasance/wire/malfeasance_scale.go | 8 ------ 5 files changed, 26 insertions(+), 31 deletions(-) diff --git a/activation/handler.go b/activation/handler.go index f79daa678f..5a0e5aecb7 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -320,7 +320,8 @@ func (h *Handler) handleAtx( case *wire.ActivationTxV1: proof, err = h.v1.processATX(ctx, peer, atx, msg, receivedTime) case *wire.ActivationTxV2: - proof, err = h.v2.processATX(ctx, peer, atx, msg, receivedTime) + // TODO(mafa): handle proof + _, err = h.v2.processATX(ctx, peer, atx, msg, receivedTime) default: panic("unreachable") } diff --git a/activation/handler_v2.go b/activation/handler_v2.go index c2510936d1..5d561161da 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -72,7 +72,7 @@ func (h *HandlerV2) processATX( watx *wire.ActivationTxV2, blob []byte, received time.Time, -) (*mwire.MalfeasanceProof, error) { +) (*mwire.MalfeasanceProofV2, error) { exists, err := atxs.Has(h.cdb, watx.ID()) if err != nil { return nil, fmt.Errorf("failed to check if atx exists: %w", err) @@ -449,7 +449,7 @@ type atxParts struct { func (h *HandlerV2) syntacticallyValidateDeps( ctx context.Context, atx *wire.ActivationTxV2, -) (*atxParts, *mwire.MalfeasanceProof, error) { +) (*atxParts, *mwire.MalfeasanceProofV2, error) { if atx.Initial != nil { if err := h.validateCommitmentAtx(h.goldenATXID, atx.Initial.CommitmentATX, atx.PublishEpoch); err != nil { return nil, nil, fmt.Errorf("verifying commitment ATX: %w", err) @@ -575,7 +575,7 @@ func (h *HandlerV2) checkMalicious( tx *sql.Tx, watx *wire.ActivationTxV2, marrying []types.NodeID, -) (bool, *mwire.MalfeasanceProof, error) { +) (bool, *mwire.MalfeasanceProofV2, error) { malicious, err := identities.IsMalicious(tx, watx.SmesherID) if err != nil { return false, nil, fmt.Errorf("checking if node is malicious: %w", err) @@ -606,20 +606,28 @@ func (h *HandlerV2) checkDoubleMarry( tx *sql.Tx, watx *wire.ActivationTxV2, marrying []types.NodeID, -) (*mwire.MalfeasanceProof, error) { +) (*mwire.MalfeasanceProofV2, error) { for _, id := range marrying { married, err := identities.Married(tx, id) if err != nil { return nil, fmt.Errorf("checking if ID is married: %w", err) } if married { - proof := &mwire.MalfeasanceProof{ - Proof: mwire.Proof{ - Type: mwire.DoubleMarry, - Data: &mwire.DoubleMarryProof{}, - }, + // TODO(mafa): lookup other ATX with the same marriage certificate + proof, err := wire.NewDoubleMarryProof(tx, watx, watx, id) + if err != nil { + return nil, fmt.Errorf("creating double marry proof: %w", err) + } + atxProof := &wire.ATXProof{ + ProofType: wire.DoubleMarry, + Certificates: []wire.ProofCertificate{}, // TODO(mafa): lookup certificates of other married identities + Proof: codec.MustEncode(proof), } - return proof, nil + return &mwire.MalfeasanceProofV2{ + Layer: h.clock.CurrentLayer(), + ProofType: mwire.InvalidActivation, + Proof: codec.MustEncode(atxProof), + }, nil } } return nil, nil @@ -632,10 +640,10 @@ func (h *HandlerV2) storeAtx( atx *types.ActivationTx, watx *wire.ActivationTxV2, marrying []types.NodeID, -) (*mwire.MalfeasanceProof, error) { +) (*mwire.MalfeasanceProofV2, error) { var ( malicious bool - proof *mwire.MalfeasanceProof + proof *mwire.MalfeasanceProofV2 ) if err := h.cdb.WithTx(ctx, func(tx *sql.Tx) error { var err error @@ -696,7 +704,8 @@ func (h *HandlerV2) storeAtx( if err := identities.SetMalicious(h.cdb, id, encoded, atx.Received()); err != nil { return nil, fmt.Errorf("setting malfeasance proof: %w", err) } - h.cdb.CacheMalfeasanceProof(id, proof) + // TODO(mafa): cache malfeasance proof in v2 + // h.cdb.CacheMalfeasanceProof(id, proof) } } diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 3de4a113eb..86b56a9e80 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -135,7 +135,7 @@ func (h *v2TestHandler) createAndProcessInitial(t *testing.T, sig *signing.EdSig return atx } -func (h *v2TestHandler) processInitial(atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) { +func (h *v2TestHandler) processInitial(atx *wire.ActivationTxV2) (*mwire.MalfeasanceProofV2, error) { h.expectInitialAtxV2(atx) return h.processATX(context.Background(), peer.ID("peer"), atx, codec.MustEncode(atx), time.Now()) } diff --git a/malfeasance/wire/malfeasance.go b/malfeasance/wire/malfeasance.go index b4132ea568..0ffd8e4228 100644 --- a/malfeasance/wire/malfeasance.go +++ b/malfeasance/wire/malfeasance.go @@ -15,7 +15,7 @@ import ( "github.com/spacemeshos/go-spacemesh/common/types" ) -//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata,InvalidPostIndexProof,InvalidPrevATXProof,DoubleMarryProof +//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata,InvalidPostIndexProof,InvalidPrevATXProof const ( MultipleATXs byte = iota + 1 @@ -23,7 +23,6 @@ const ( HareEquivocation InvalidPostIndex InvalidPrevATX - DoubleMarry ) type MalfeasanceProof struct { @@ -325,12 +324,6 @@ type InvalidPrevATXProof struct { func (p *InvalidPrevATXProof) isProof() {} -type DoubleMarryProof struct { - // TODO: implement -} - -func (p *DoubleMarryProof) isProof() {} - func MalfeasanceInfo(smesher types.NodeID, mp *MalfeasanceProof) string { var b strings.Builder b.WriteString(fmt.Sprintf("generate layer: %v\n", mp.Layer)) diff --git a/malfeasance/wire/malfeasance_scale.go b/malfeasance/wire/malfeasance_scale.go index 6e23fd2175..3ec88a1acc 100644 --- a/malfeasance/wire/malfeasance_scale.go +++ b/malfeasance/wire/malfeasance_scale.go @@ -422,11 +422,3 @@ func (t *InvalidPrevATXProof) DecodeScale(dec *scale.Decoder) (total int, err er } return total, nil } - -func (t *DoubleMarryProof) EncodeScale(enc *scale.Encoder) (total int, err error) { - return total, nil -} - -func (t *DoubleMarryProof) DecodeScale(dec *scale.Decoder) (total int, err error) { - return total, nil -} From a8968a336e114fd8377ac5fbc2d0b4c2fbfa383c Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 24 Jun 2024 07:24:52 +0000 Subject: [PATCH 12/14] Add more comments --- activation/handler_v2.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 5d561161da..c7eabfdd86 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -652,14 +652,15 @@ func (h *HandlerV2) storeAtx( return fmt.Errorf("check malicious: %w", err) } - if len(marrying) != 0 { + if len(marrying) > 0 { for _, id := range marrying { if err := identities.SetMarriage(tx, id, atx.ID()); err != nil { return err } } + // TODO(mafa): I don't get this part, why do we need to check for malfeasance again? if !malicious && proof == nil { - // We check for malfeasance again becase the marriage increased the equivocation set. + // We check for malfeasance again because the marriage increased the equivocation set. malicious, err = identities.IsMalicious(tx, atx.SmesherID) if err != nil { return fmt.Errorf("re-checking if smesherID is malicious: %w", err) @@ -704,11 +705,17 @@ func (h *HandlerV2) storeAtx( if err := identities.SetMalicious(h.cdb, id, encoded, atx.Received()); err != nil { return nil, fmt.Errorf("setting malfeasance proof: %w", err) } - // TODO(mafa): cache malfeasance proof in v2 + // TODO(mafa): why do we need to cache the proof here? shouldn't this be done after publishing it? + // if we want to avoid spamming during sync we could add a "no-op" publisher that only validates, + // persists and caches the proof without gossiping it during sync. + // + // better however would be to sync malfeasant identities first and then sync ATXs. + // // h.cdb.CacheMalfeasanceProof(id, proof) } } + // TODO(mafa): same here, why isn't this done in the handler for malfeasance proofs? for id := range allMalicious { h.tortoise.OnMalfeasance(id) } From 937294e77c4207e0bddb6c6f757f4056047f0e7d Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Mon, 24 Jun 2024 07:42:07 +0000 Subject: [PATCH 13/14] Cleanup --- activation/post_verifier_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/activation/post_verifier_test.go b/activation/post_verifier_test.go index d159ca7eb1..6ff9ccaa26 100644 --- a/activation/post_verifier_test.go +++ b/activation/post_verifier_test.go @@ -107,10 +107,7 @@ func TestPostVerifierPrioritization(t *testing.T) { verifier := NewMockPostVerifier(gomock.NewController(t)) v := newOffloadingPostVerifier(verifier, 2, zaptest.NewLogger(t), nodeID) - verifier.EXPECT(). - Verify(gomock.Any(), gomock.Any(), &shared.ProofMetadata{NodeId: nodeID.Bytes()}, gomock.Any()). - Return(nil) - + verifier.EXPECT().Verify(gomock.Any(), gomock.Any(), &shared.ProofMetadata{NodeId: nodeID.Bytes()}, gomock.Any()) err := v.Verify(context.Background(), &shared.Proof{}, &shared.ProofMetadata{NodeId: nodeID.Bytes()}) require.NoError(t, err) From 8ecce308a5064c9e2077dc8c4cc6a3f4d0481b53 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 4 Jul 2024 15:26:50 +0000 Subject: [PATCH 14/14] Fix issue after merge --- activation/wire/malfeasance_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/activation/wire/malfeasance_test.go b/activation/wire/malfeasance_test.go index bec0280a87..920bb86bc4 100644 --- a/activation/wire/malfeasance_test.go +++ b/activation/wire/malfeasance_test.go @@ -33,8 +33,7 @@ func newActivationTxV2(opts ...testAtxV2Opt) *ActivationTxV2 { NiPosts: []NiPostsV2{ { Membership: MerkleProofV2{ - Nodes: make([]types.Hash32, 32), - LeafIndices: make([]uint64, 256), + Nodes: make([]types.Hash32, 32), }, Challenge: types.RandomHash(), Posts: []SubPostV2{