Skip to content

Commit

Permalink
Update malfeasance for double marry (#6600)
Browse files Browse the repository at this point in the history
## Motivation

This updates the handler for ATXv2 to handle double marry correctly when at least one but not all of the involved marriage sets is already known to be malfeasant.
  • Loading branch information
fasmat committed Jan 10, 2025
1 parent 723753d commit bad13bc
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 39 deletions.
64 changes: 40 additions & 24 deletions activation/handler_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,8 +495,8 @@ func (n nipostSizes) sumUp() (units uint32, weight uint64, err error) {

func (h *HandlerV2) verifyIncludedIDsUniqueness(atx *wire.ActivationTxV2) error {
seen := make(map[uint32]struct{})
for _, niposts := range atx.NIPosts {
for _, post := range niposts.Posts {
for _, niPosts := range atx.NIPosts {
for _, post := range niPosts.Posts {
if _, ok := seen[post.MarriageIndex]; ok {
return fmt.Errorf("ID present twice (duplicated marriage index): %d", post.MarriageIndex)
}
Expand Down Expand Up @@ -528,7 +528,7 @@ func (h *HandlerV2) syntacticallyValidateDeps(
return nil, fmt.Errorf("fetching previous atx: %w", err)
}
if prevAtx.PublishEpoch >= atx.PublishEpoch {
err := fmt.Errorf("previous atx is too new (%d >= %d) (%s) ", prevAtx.PublishEpoch, atx.PublishEpoch, prev)
err := fmt.Errorf("previous atx (%s) is too new (%d >= %d)", prev, prevAtx.PublishEpoch, atx.PublishEpoch)
return nil, err
}
previousAtxs[i] = prevAtx
Expand All @@ -541,9 +541,9 @@ func (h *HandlerV2) syntacticallyValidateDeps(

// validate previous ATXs
nipostSizes := make(nipostSizes, len(atx.NIPosts))
for i, niposts := range atx.NIPosts {
for i, niPosts := range atx.NIPosts {
nipostSizes[i] = new(nipostSize)
for _, post := range niposts.Posts {
for _, post := range niPosts.Posts {
if post.MarriageIndex >= uint32(len(equivocationSet)) {
err := fmt.Errorf("marriage index out of bounds: %d > %d", post.MarriageIndex, len(equivocationSet)-1)
return nil, err
Expand All @@ -563,11 +563,11 @@ func (h *HandlerV2) syntacticallyValidateDeps(
}

// validate poet membership proofs
for i, niposts := range atx.NIPosts {
for i, niPosts := range atx.NIPosts {
// verify PoET memberships in a single go
indexedChallenges := make(map[uint64][]byte)

for _, post := range niposts.Posts {
for _, post := range niPosts.Posts {
if _, ok := indexedChallenges[post.MembershipLeafIndex]; ok {
continue
}
Expand All @@ -591,10 +591,10 @@ func (h *HandlerV2) syntacticallyValidateDeps(
}

membership := types.MultiMerkleProof{
Nodes: niposts.Membership.Nodes,
Nodes: niPosts.Membership.Nodes,
LeafIndices: leafIndices,
}
leaves, err := h.nipostValidator.PoetMembership(ctx, &membership, niposts.Challenge, poetChallenges)
leaves, err := h.nipostValidator.PoetMembership(ctx, &membership, niPosts.Challenge, poetChallenges)
if err != nil {
return nil, fmt.Errorf("validating poet membership: %w", err)
}
Expand All @@ -606,7 +606,7 @@ func (h *HandlerV2) syntacticallyValidateDeps(
return nil, err
}

// validate all niposts
// validate all NIPoSTs
if atx.Initial != nil {
commitment := atx.Initial.CommitmentATX
nipostIdx := 0
Expand All @@ -625,8 +625,8 @@ func (h *HandlerV2) syntacticallyValidateDeps(
}

var smesherCommitment *types.ATXID
for idx, niposts := range atx.NIPosts {
for _, post := range niposts.Posts {
for idx, niPosts := range atx.NIPosts {
for _, post := range niPosts.Posts {
id := equivocationSet[post.MarriageIndex]
commitment, err := atxs.CommitmentATX(h.cdb, id)
if err != nil {
Expand All @@ -635,7 +635,7 @@ func (h *HandlerV2) syntacticallyValidateDeps(
if id == atx.SmesherID {
smesherCommitment = &commitment
}
if err := h.validatePost(ctx, id, atx, commitment, niposts.Challenge, post, idx); err != nil {
if err := h.validatePost(ctx, id, atx, commitment, niPosts.Challenge, post, idx); err != nil {
return nil, err
}
result.ids[id] = idData{
Expand Down Expand Up @@ -847,7 +847,7 @@ func (h *HandlerV2) checkDoubleMerge(ctx context.Context, tx sql.Transaction, at
if err != nil {
return true, fmt.Errorf("creating double merge proof: %w", err)
}
return true, h.malPublisher.Publish(ctx, atx.SmesherID, proof)
return true, h.malPublisher.Publish(ctx, atx.ActivationTxV2.SmesherID, proof)
}

func (h *HandlerV2) checkPrevAtx(ctx context.Context, tx sql.Transaction, atx *activationTx) (bool, error) {
Expand Down Expand Up @@ -930,6 +930,7 @@ func (h *HandlerV2) checkPrevAtx(ctx context.Context, tx sql.Transaction, atx *a

// Store an ATX in the DB.
func (h *HandlerV2) storeAtx(ctx context.Context, atx *types.ActivationTx, watx *activationTx) error {
republishProof := false
if err := h.cdb.WithTxImmediate(ctx, func(tx sql.Transaction) error {
if len(watx.marriages) != 0 {
newMarriageID, err := marriage.NewID(tx)
Expand All @@ -942,7 +943,8 @@ func (h *HandlerV2) storeAtx(ctx context.Context, atx *types.ActivationTx, watx
Target: atx.SmesherID,
}
malicious := false
marriageIDs := make([]marriage.ID, 0)
marriageIDs := make([]marriage.ID, 1)
marriageIDs[0] = newMarriageID
for i, m := range watx.marriages {
info.NodeID = m.id
info.MarriageIndex = i
Expand All @@ -955,7 +957,6 @@ func (h *HandlerV2) storeAtx(ctx context.Context, atx *types.ActivationTx, watx
return fmt.Errorf("find marriage ID for node ID %s: %w", m.id.ShortString(), err)
}
marriageIDs = append(marriageIDs, id)
continue
case err != nil:
return fmt.Errorf("adding marriage: %w", err)
}
Expand All @@ -967,21 +968,30 @@ func (h *HandlerV2) storeAtx(ctx context.Context, atx *types.ActivationTx, watx
return fmt.Errorf("checking if node is malicious: %w", err)
}
}
if len(marriageIDs) != 0 {
marriageIDs := append(marriageIDs, newMarriageID)
combinedID := slices.Min(marriageIDs)
if len(marriageIDs) > 1 {
newMarriageID = slices.Min(marriageIDs)
for _, id := range marriageIDs {
if id != combinedID {
if err := marriage.UpdateMarriageID(tx, id, combinedID); err != nil {
if id != newMarriageID {
if err := marriage.UpdateMarriageID(tx, id, newMarriageID); err != nil {
return fmt.Errorf("updating marriage ID for %d: %w", id, err)
}
}
}
newMarriageID = combinedID
}
if malicious {
for _, m := range watx.marriages {
if err := malfeasance.SetMalicious(tx, m.id, newMarriageID, time.Now()); err != nil {
nodeIDs, err := marriage.NodeIDsByID(tx, newMarriageID)
if err != nil {
return fmt.Errorf("fetching node IDs by marriage ID: %w", err)
}
for _, id := range nodeIDs {
malicious, err := malfeasance.IsMalicious(tx, id)
if err != nil {
return fmt.Errorf("checking if node ID is malicious: %w", err)
}
if !malicious {
republishProof = true
}
if err := malfeasance.SetMalicious(tx, id, newMarriageID, time.Now()); err != nil {
return fmt.Errorf("marking node as malicious: %w", err)
}
}
Expand All @@ -1007,9 +1017,15 @@ func (h *HandlerV2) storeAtx(ctx context.Context, atx *types.ActivationTx, watx
err := h.cdb.WithTxImmediate(ctx, func(tx sql.Transaction) error {
// malfeasance check happens after storing the ATX because storing updates the marriage set
// that is needed for the malfeasance proof
//
// TODO(mafa): don't store own ATX if it would mark the node as malicious
// this probably needs to be done by validating and storing own ATXs eagerly and skipping validation in
// the gossip handler (not sync!)
if republishProof {
malicious = true
return h.malPublisher.Republish(ctx, atx.SmesherID)
}

var err error
malicious, err = h.checkMalicious(ctx, tx, watx)
return err
Expand Down
115 changes: 112 additions & 3 deletions activation/handler_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package activation
import (
"context"
"errors"
"fmt"
"math"
"slices"
"testing"
Expand Down Expand Up @@ -996,7 +997,7 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) {
gomock.Any(),
merged.SmesherID,
gomock.AssignableToTypeOf(&wire.ProofDoubleMerge{}),
).DoAndReturn(func(ctx context.Context, id types.NodeID, proof wire.Proof) error {
).DoAndReturn(func(ctx context.Context, _ types.NodeID, proof wire.Proof) error {
malProof := proof.(*wire.ProofDoubleMerge)
nId, err := malProof.Valid(context.Background(), verifier)
require.NoError(t, err)
Expand Down Expand Up @@ -1504,7 +1505,7 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) {
atx.Sign(sig)

_, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx)
require.ErrorContains(t, err, "previous atx is too new")
require.ErrorContains(t, err, fmt.Sprintf("previous atx (%s) is too new", prev.ID()))
})
t.Run("previous ATX by different smesher", func(t *testing.T) {
atxHandler := newV2TestHandler(t, golden)
Expand Down Expand Up @@ -1987,7 +1988,7 @@ func Test_Marriages(t *testing.T) {
require.NoError(t, err)
require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv)
})
t.Run("marring into existing malicious equivocation set sets identity as malicious", func(t *testing.T) {
t.Run("marring existing malicious equivocation set: marks all malicious and republishes proof", func(t *testing.T) {
t.Parallel()
atxHandler := newV2TestHandler(t, golden)

Expand Down Expand Up @@ -2017,6 +2018,8 @@ func Test_Marriages(t *testing.T) {
}
atx2.Sign(sig)
atxHandler.expectAtxV2(atx2)

atxHandler.mMalPublish.EXPECT().Republish(gomock.Any(), sig.NodeID())
err = atxHandler.processATX(context.Background(), "", atx2, time.Now())
require.NoError(t, err)

Expand All @@ -2026,6 +2029,110 @@ func Test_Marriages(t *testing.T) {
equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id)
require.NoError(t, err)
require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv)

for _, sig := range []*signing.EdSigner{sig, otherSig, otherSig2} {
m, err := malfeasance.IsMalicious(atxHandler.cdb, sig.NodeID())
require.NoError(t, err)
require.True(t, m, "expected %s to be malicious", sig)
}
})
t.Run("malicious marring existing equivocation set: marks all malicious and republishes proof", func(t *testing.T) {
t.Parallel()
atxHandler := newV2TestHandler(t, golden)

otherSig, err := signing.NewEdSigner()
require.NoError(t, err)
atx, _ := marryIDs(t, atxHandler, []*signing.EdSigner{sig, otherSig}, golden)

// otherSig2 cannot marry sig, trying to extend its set.
otherSig2, err := signing.NewEdSigner()
require.NoError(t, err)
others2Atx := atxHandler.createAndProcessInitial(otherSig2)

// otherSig2 becomes malicious in some way
err = malfeasance.AddProof(atxHandler.cdb, otherSig2.NodeID(), nil, []byte("proof"), 0, time.Now())
require.NoError(t, err)

atx2 := newSoloATXv2(t, atx.PublishEpoch+1, atx.ID(), atx.ID())
atx2.Marriages = []wire.MarriageCertificate{
{
Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()),
},
{
ReferenceAtx: others2Atx.ID(),
Signature: otherSig2.Sign(signing.MARRIAGE, sig.NodeID().Bytes()),
},
}
atx2.Sign(sig)
atxHandler.expectAtxV2(atx2)

atxHandler.mMalPublish.EXPECT().Republish(gomock.Any(), sig.NodeID())
err = atxHandler.processATX(context.Background(), "", atx2, time.Now())
require.NoError(t, err)

// The equivocation set of sig and otherSig were merged
id, err := marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID())
require.NoError(t, err)
equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id)
require.NoError(t, err)
require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv)

for _, sig := range []*signing.EdSigner{sig, otherSig, otherSig2} {
m, err := malfeasance.IsMalicious(atxHandler.cdb, sig.NodeID())
require.NoError(t, err)
require.True(t, m, "expected %s to be malicious", sig)
}
})
t.Run("malicious marring malicious equivocation set: no proof published", func(t *testing.T) {
t.Parallel()
atxHandler := newV2TestHandler(t, golden)

otherSig, err := signing.NewEdSigner()
require.NoError(t, err)
atx, _ := marryIDs(t, atxHandler, []*signing.EdSigner{sig, otherSig}, golden)

// sig becomes malicious in some way and with it otherSig
id, err := marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID())
require.NoError(t, err)
require.NoError(t, malfeasance.AddProof(atxHandler.cdb, sig.NodeID(), &id, []byte("proof"), 0, time.Now()))
require.NoError(t, malfeasance.SetMalicious(atxHandler.cdb, otherSig.NodeID(), id, time.Now()))

// otherSig2 cannot marry sig, trying to extend its set.
otherSig2, err := signing.NewEdSigner()
require.NoError(t, err)
others2Atx := atxHandler.createAndProcessInitial(otherSig2)

// otherSig2 becomes malicious in some way
err = malfeasance.AddProof(atxHandler.cdb, otherSig2.NodeID(), nil, []byte("proof"), 0, time.Now())
require.NoError(t, err)

atx2 := newSoloATXv2(t, atx.PublishEpoch+1, atx.ID(), atx.ID())
atx2.Marriages = []wire.MarriageCertificate{
{
Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()),
},
{
ReferenceAtx: others2Atx.ID(),
Signature: otherSig2.Sign(signing.MARRIAGE, sig.NodeID().Bytes()),
},
}
atx2.Sign(sig)
atxHandler.expectAtxV2(atx2)
err = atxHandler.processATX(context.Background(), "", atx2, time.Now())
require.NoError(t, err)

// The equivocation set of sig and otherSig were merged
id, err = marriage.FindIDByNodeID(atxHandler.cdb, sig.NodeID())
require.NoError(t, err)
equiv, err := marriage.NodeIDsByID(atxHandler.cdb, id)
require.NoError(t, err)
require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID(), otherSig2.NodeID()}, equiv)

for _, sig := range []*signing.EdSigner{sig, otherSig, otherSig2} {
m, err := malfeasance.IsMalicious(atxHandler.cdb, sig.NodeID())
require.NoError(t, err)
require.True(t, m, "expected %s to be malicious", sig)
}
})
t.Run("signer must marry self", func(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -2078,6 +2185,8 @@ func Test_MarryingMalicious(t *testing.T) {
require.NoError(t, malfeasance.AddProof(atxHandler.cdb, malicious, nil, []byte("proof"), 0, time.Now()))

atxHandler.expectInitialAtxV2(atx)
atxHandler.mMalPublish.EXPECT().Republish(gomock.Any(), sig.NodeID())

err := atxHandler.processATX(context.Background(), "", atx, time.Now())
require.NoError(t, err)

Expand Down
3 changes: 2 additions & 1 deletion activation/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,10 @@ type legacyMalfeasancePublisher interface {
// atxMalfeasancePublisher is an interface for publishing atx malfeasance proofs.
//
// It encapsulates a specific malfeasance proof into a generic ATX malfeasance proof and publishes it by calling
// the underlying malfeasancePublisher.
// the underlying malfeasancePublisher. It also allows republishing of existing proofs.
type atxMalfeasancePublisher interface {
Publish(ctx context.Context, nodeID types.NodeID, proof wire.Proof) error
Republish(ctx context.Context, nodeID types.NodeID) error
}

type atxProvider interface {
Expand Down
5 changes: 5 additions & 0 deletions activation/malfeasance2.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,8 @@ func (p *MalfeasanceHandlerV2) Publish(ctx context.Context, id types.NodeID, pro
// TODO(mafa): implement me
return nil
}

func (p *MalfeasanceHandlerV2) Republish(ctx context.Context, id types.NodeID) error {
// TODO(mafa): implement me
return nil
}
Loading

0 comments on commit bad13bc

Please sign in to comment.