Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WiP: Double Publish and double Marry Malfeasance Proofs for ATX v2 #6043

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1cdba4d
Double Publish malfeasance proof
fasmat Jun 7, 2024
df8c57c
Double marry malfeasance proof
fasmat Jun 11, 2024
1b6910f
Add more tests and implement double marry proof
fasmat Jun 13, 2024
28eb9d9
Tests for double marry
fasmat Jun 13, 2024
9086798
Cleanup
fasmat Jun 13, 2024
e5e9dad
Add more comments
fasmat Jun 13, 2024
06f1f34
Use types.Hash32 for leafs
fasmat Jun 13, 2024
3103cd7
First draft activation malfeasance service
fasmat Jun 14, 2024
8a547b1
make generate
fasmat Jun 18, 2024
d6053ad
Change double marry to new atx certificates structure
fasmat Jun 19, 2024
1acf8b4
Merge remote-tracking branch 'origin/develop' into malfeasance-v2
fasmat Jun 23, 2024
d2d1612
Fix a few issues with malfeasance handling
fasmat Jun 23, 2024
a8968a3
Add more comments
fasmat Jun 24, 2024
937294e
Cleanup
fasmat Jun 24, 2024
da9db77
Merge remote-tracking branch 'origin/develop' into malfeasance-v2
fasmat Jun 24, 2024
bc47352
Merge remote-tracking branch 'origin/develop' into malfeasance-v2
fasmat Jun 24, 2024
188d372
Merge remote-tracking branch 'origin/develop' into malfeasance-v2
fasmat Jun 26, 2024
02447ab
Merge remote-tracking branch 'origin/develop' into malfeasance-v2
fasmat Jul 1, 2024
7a64494
Merge remote-tracking branch 'origin/develop' into malfeasance-v2
fasmat Jul 3, 2024
4c93d60
Merge remote-tracking branch 'origin/develop' into malfeasance-v2
fasmat Jul 4, 2024
8ecce30
Fix issue after merge
fasmat Jul 4, 2024
ff552a5
Merge remote-tracking branch 'origin/develop' into malfeasance-v2
fasmat Jul 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions activation/builder_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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))
}
3 changes: 2 additions & 1 deletion activation/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,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")
}
Expand Down
50 changes: 35 additions & 15 deletions activation/handler_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,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)
Expand Down Expand Up @@ -157,7 +157,7 @@ func (h *HandlerV2) processATX(

// Syntactically validate an ATX.
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 {
Expand Down Expand Up @@ -493,7 +493,7 @@ func (h *HandlerV2) verifyIncludedIDsUniqueness(atx *wire.ActivationTxV2) error
func (h *HandlerV2) syntacticallyValidateDeps(
ctx context.Context,
atx *wire.ActivationTxV2,
) (*atxParts, *mwire.MalfeasanceProof, error) {
) (*atxParts, *mwire.MalfeasanceProofV2, error) {
parts := atxParts{
units: make(map[types.NodeID]uint32),
}
Expand Down Expand Up @@ -651,7 +651,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)
Expand All @@ -660,7 +660,7 @@ func (h *HandlerV2) checkMalicious(
return true, nil, nil
}

proof, err := h.checkDoubleMarry(tx, marrying)
proof, err := h.checkDoubleMarry(tx, watx, marrying)
if err != nil {
return false, nil, fmt.Errorf("checking double marry: %w", err)
}
Expand All @@ -678,20 +678,32 @@ func (h *HandlerV2) checkMalicious(
return false, nil, nil
}

func (h *HandlerV2) checkDoubleMarry(tx *sql.Tx, marrying []types.NodeID) (*mwire.MalfeasanceProof, error) {
func (h *HandlerV2) checkDoubleMarry(
tx *sql.Tx,
watx *wire.ActivationTxV2,
marrying []types.NodeID,
) (*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
Expand All @@ -705,10 +717,10 @@ func (h *HandlerV2) storeAtx(
watx *wire.ActivationTxV2,
marrying []types.NodeID,
units map[types.NodeID]uint32,
) (*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
Expand All @@ -723,6 +735,7 @@ func (h *HandlerV2) storeAtx(
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 because the marriage increased the equivocation set.
malicious, err = identities.IsMalicious(tx, atx.SmesherID)
Expand Down Expand Up @@ -775,10 +788,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)
}
h.cdb.CacheMalfeasanceProof(id, proof)
// 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)
}
Expand Down
4 changes: 2 additions & 2 deletions activation/handler_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,13 @@ func (h *v2TestHandler) createAndProcessInitial(t testing.TB, sig *signing.EdSig
return atx
}

func (h *v2TestHandler) processInitial(t testing.TB, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) {
func (h *v2TestHandler) processInitial(t testing.TB, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProofV2, error) {
t.Helper()
h.expectInitialAtxV2(atx)
return h.processATX(context.Background(), peer.ID("peer"), atx, codec.MustEncode(atx), time.Now())
}

func (h *v2TestHandler) processSoloAtx(t testing.TB, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) {
func (h *v2TestHandler) processSoloAtx(t testing.TB, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProofV2, error) {
t.Helper()
h.expectAtxV2(atx)
return h.processATX(context.Background(), peer.ID("peer"), atx, codec.MustEncode(atx), time.Now())
Expand Down
80 changes: 80 additions & 0 deletions activation/malfeasance_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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
edVerifier *signing.EdVerifier
}

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) {
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 {
// 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")
}
1 change: 1 addition & 0 deletions activation/malfeasance_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package activation
5 changes: 1 addition & 4 deletions activation/post_verifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
44 changes: 44 additions & 0 deletions activation/wire/malfeasance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package wire

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 (
DoublePublish ProofType = iota + 1
DoubleMarry
)

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"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe name it

Suggested change
Certificates []ProofCertificate `scale:"max=1024"`
Certificates []MarriageCertificate `scale:"max=1024"`

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MarriageCertificate already exists as a type in the wire package (for v2 ATXs) but this is a different type it not only contains the signature and the payload but also the signer (which isn't needed for ATXs).

// Proof is the actual proof. Its type depends on the ProofType.
Proof []byte `scale:"max=1048576"` // max size of proof is 1MiB
}

// 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
}
Comment on lines +31 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having Target in each certificate is quite redundant as in most cases, all certs will share it. How about structure like this:

type MarriageCertificates {
	Target types.NodeID
	Certificates []MarriageCertificate `scale:"max=256"`
}

type ATXProof struct {
	ProofType ProofType
	Certificates []MarriageCertificates `scale:"max=2"`
	Proof []byte `scale:"max=1048576"`
}


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