diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index 3c52357aaa..2cbc7885d5 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -276,6 +276,7 @@ func Test_MarryAndMerge(t *testing.T) { require.NoError(t, err) mFetch := smocks.NewMockFetcher(ctrl) + mMalPublish := activation.NewMockatxMalfeasancePublisher(ctrl) mLegacyPublish := activation.NewMocklegacyMalfeasancePublisher(ctrl) mBeacon := activation.NewMockatxReceiver(ctrl) mTortoise := smocks.NewMockTortoise(ctrl) @@ -290,6 +291,7 @@ func Test_MarryAndMerge(t *testing.T) { mFetch, goldenATX, validator, + mMalPublish, mLegacyPublish, mBeacon, mTortoise, diff --git a/activation/e2e/builds_atx_v2_test.go b/activation/e2e/builds_atx_v2_test.go index d7075ed982..f3992b6b49 100644 --- a/activation/e2e/builds_atx_v2_test.go +++ b/activation/e2e/builds_atx_v2_test.go @@ -118,6 +118,7 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { edVerifier := signing.NewEdVerifier() mpub := mocks.NewMockPublisher(ctrl) mFetch := smocks.NewMockFetcher(ctrl) + mMalPublish := activation.NewMockatxMalfeasancePublisher(ctrl) mLegacyPublish := activation.NewMocklegacyMalfeasancePublisher(ctrl) mBeacon := activation.NewMockatxReceiver(ctrl) mTortoise := smocks.NewMockTortoise(ctrl) @@ -131,6 +132,7 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { mFetch, goldenATX, validator, + mMalPublish, mLegacyPublish, mBeacon, mTortoise, diff --git a/activation/e2e/checkpoint_merged_test.go b/activation/e2e/checkpoint_merged_test.go index dcf2e49353..88c2a87238 100644 --- a/activation/e2e/checkpoint_merged_test.go +++ b/activation/e2e/checkpoint_merged_test.go @@ -107,6 +107,7 @@ func Test_CheckpointAfterMerge(t *testing.T) { require.NoError(t, err) mFetch := smocks.NewMockFetcher(ctrl) + mMalPublish := activation.NewMockatxMalfeasancePublisher(ctrl) mLegacyPublish := activation.NewMocklegacyMalfeasancePublisher(ctrl) mBeacon := activation.NewMockatxReceiver(ctrl) mTortoise := smocks.NewMockTortoise(ctrl) @@ -120,6 +121,7 @@ func Test_CheckpointAfterMerge(t *testing.T) { mFetch, goldenATX, validator, + mMalPublish, mLegacyPublish, mBeacon, mTortoise, @@ -299,6 +301,7 @@ func Test_CheckpointAfterMerge(t *testing.T) { mFetch, goldenATX, validator, + mMalPublish, mLegacyPublish, mBeacon, mTortoise, diff --git a/activation/e2e/checkpoint_test.go b/activation/e2e/checkpoint_test.go index f280718ba7..cf481facb7 100644 --- a/activation/e2e/checkpoint_test.go +++ b/activation/e2e/checkpoint_test.go @@ -103,6 +103,7 @@ func TestCheckpoint_PublishingSoloATXs(t *testing.T) { atxVersions := activation.AtxVersions{0: types.AtxV2} edVerifier := signing.NewEdVerifier() mFetch := smocks.NewMockFetcher(ctrl) + mMalPublish := activation.NewMockatxMalfeasancePublisher(ctrl) mLegacyPublish := activation.NewMocklegacyMalfeasancePublisher(ctrl) mBeacon := activation.NewMockatxReceiver(ctrl) mTortoise := smocks.NewMockTortoise(ctrl) @@ -116,6 +117,7 @@ func TestCheckpoint_PublishingSoloATXs(t *testing.T) { mFetch, goldenATX, validator, + mMalPublish, mLegacyPublish, mBeacon, mTortoise, @@ -206,6 +208,7 @@ func TestCheckpoint_PublishingSoloATXs(t *testing.T) { mFetch, goldenATX, validator, + mMalPublish, mLegacyPublish, mBeacon, mTortoise, diff --git a/activation/e2e/nipost_test.go b/activation/e2e/nipost_test.go index 927a89851d..a9a13970e9 100644 --- a/activation/e2e/nipost_test.go +++ b/activation/e2e/nipost_test.go @@ -97,7 +97,7 @@ func launchPostSupervisor( provingOpts := activation.DefaultPostProvingOpts() provingOpts.RandomXMode = activation.PostRandomXModeLight - builder := activation.NewMockAtxBuilder(gomock.NewController(tb)) + builder := activation.NewMockatxBuilder(gomock.NewController(tb)) builder.EXPECT().Register(gomock.Any()) ps := activation.NewPostSupervisor(log, postCfg, provingOpts, mgr, builder) require.NoError(tb, ps.Start(cmdCfg, postOpts, sig)) diff --git a/activation/handler.go b/activation/handler.go index e2bd9acd39..32cfeed5bf 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -20,8 +20,6 @@ import ( "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/signing" - "github.com/spacemeshos/go-spacemesh/sql" - "github.com/spacemeshos/go-spacemesh/sql/atxs" "github.com/spacemeshos/go-spacemesh/system" ) @@ -103,6 +101,7 @@ func NewHandler( fetcher system.Fetcher, goldenATXID types.ATXID, nipostValidator nipostValidator, + malPublisher atxMalfeasancePublisher, legacyMalPublisher legacyMalfeasancePublisher, beacon atxReceiver, tortoise system.Tortoise, @@ -144,7 +143,7 @@ func NewHandler( fetcher: fetcher, beacon: beacon, tortoise: tortoise, - malPublisher: &MalfeasancePublisher{}, // TODO(mafa): pass real publisher when available + malPublisher: malPublisher, }, } @@ -291,28 +290,3 @@ func (h *Handler) handleAtx(ctx context.Context, expHash types.Hash32, peer p2p. h.inProgress.Forget(key) return err } - -// Obtain the atxSignature of the given ATX. -func atxSignature(ctx context.Context, db sql.Executor, id types.ATXID) (types.EdSignature, error) { - var blob sql.Blob - v, err := atxs.LoadBlob(ctx, db, id.Bytes(), &blob) - if err != nil { - return types.EmptyEdSignature, err - } - - if len(blob.Bytes) == 0 { - // An empty blob indicates a golden ATX (after a checkpoint-recovery). - return types.EmptyEdSignature, fmt.Errorf("can't get signature for a golden (checkpointed) ATX: %s", id) - } - - // TODO: implement for ATX V2 - switch v { - case types.AtxV1: - var atx wire.ActivationTxV1 - if err := codec.Decode(blob.Bytes, &atx); err != nil { - return types.EmptyEdSignature, fmt.Errorf("decoding atx v1: %w", err) - } - return atx.Signature, nil - } - return types.EmptyEdSignature, fmt.Errorf("unsupported ATX version: %v", v) -} diff --git a/activation/handler_test.go b/activation/handler_test.go index 63d7cdc123..1ca409cdc2 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -215,8 +215,6 @@ func newTestHandler(tb testing.TB, goldenATXID types.ATXID, opts ...HandlerOptio edVerifier := signing.NewEdVerifier() mocks := newTestHandlerMocks(tb, goldenATXID) - // TODO(mafa): make mandatory parameter when real publisher is available - opts = append(opts, func(h *Handler) { h.v2.malPublisher = mocks.mMalPublish }) atxHdlr := NewHandler( "localID", cdb, @@ -226,6 +224,7 @@ func newTestHandler(tb testing.TB, goldenATXID types.ATXID, opts ...HandlerOptio mocks.mockFetch, goldenATXID, mocks.mValidator, + mocks.mMalPublish, mocks.mLegacyMalPublish, mocks.mBeacon, mocks.mTortoise, diff --git a/activation/handler_v1.go b/activation/handler_v1.go index 216e09bdec..42f911ad3e 100644 --- a/activation/handler_v1.go +++ b/activation/handler_v1.go @@ -438,7 +438,6 @@ func (h *HandlerV1) checkWrongPrevAtx( return nil, err } if v != types.AtxV1 { - // TODO(mafa): update when V2 is introduced return nil, fmt.Errorf("ATX %s with same prev ATX as %s is not version 1", atx2ID, atx.PrevATXID) } @@ -505,22 +504,19 @@ func (h *HandlerV1) storeAtx(ctx context.Context, atx *types.ActivationTx, watx } atxs.AtxAdded(h.cdb, atx) - if proof != nil { - if err := h.malPublisher.PublishProof(ctx, atx.SmesherID, proof); err != nil { - return fmt.Errorf("publishing malfeasance proof: %w", err) - } - } - - added := h.cacheAtx(ctx, atx, malicious || proof != nil) h.beacon.OnAtx(atx) - if added != nil { + if added := h.cacheAtx(ctx, atx, malicious || proof != nil); added != nil { h.tortoise.OnAtx(atx.TargetEpoch(), atx.ID(), added) } - h.logger.Debug("finished storing atx in epoch", zap.Stringer("atx_id", atx.ID()), zap.Uint32("epoch_id", atx.PublishEpoch.Uint32()), ) + if proof != nil { + if err := h.malPublisher.PublishProof(ctx, atx.SmesherID, proof); err != nil { + return fmt.Errorf("publishing malfeasance proof: %w", err) + } + } return nil } @@ -632,3 +628,28 @@ func collectAtxDeps(goldenAtxId types.ATXID, atx *wire.ActivationTxV1) (types.Ha return types.BytesToHash(atx.NIPost.PostMetadata.Challenge), maps.Keys(filtered) } + +// Obtain the atxSignature of the given ATXv1. +func atxSignature(ctx context.Context, db sql.Executor, id types.ATXID) (types.EdSignature, error) { + var blob sql.Blob + v, err := atxs.LoadBlob(ctx, db, id.Bytes(), &blob) + if err != nil { + return types.EmptyEdSignature, err + } + + if len(blob.Bytes) == 0 { + // An empty blob indicates a golden ATX (after a checkpoint-recovery). + return types.EmptyEdSignature, fmt.Errorf("can't get signature for a golden (checkpointed) ATX: %s", id) + } + + switch v { + case types.AtxV1: + var atx wire.ActivationTxV1 + if err := codec.Decode(blob.Bytes, &atx); err != nil { + return types.EmptyEdSignature, fmt.Errorf("decoding atx v1: %w", err) + } + return atx.Signature, nil + default: // only needed for V1 ATXs + return types.EmptyEdSignature, fmt.Errorf("unsupported ATX version: %v", v) + } +} diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 858b9430b0..4c396a0e7a 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -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) } @@ -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 @@ -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 @@ -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 } @@ -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) } @@ -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 @@ -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 { @@ -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{ @@ -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) { diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 662b9ed33d..365dc0d96b 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -3,6 +3,7 @@ package activation import ( "context" "errors" + "fmt" "math" "slices" "testing" @@ -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) @@ -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) diff --git a/activation/interface.go b/activation/interface.go index ae410e6291..ab9289ec3d 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -109,6 +109,14 @@ type atxMalfeasancePublisher interface { Publish(ctx context.Context, nodeID types.NodeID, proof wire.Proof) error } +// malfeasancePublisher is an interface for publishing malfeasance proofs. +// +// Additionally the publisher will only gossip proofs when the node is in sync, otherwise it will only store them +// and mark the associated identity as malfeasant. +type malfeasancePublisher interface { + PublishATXProof(ctx context.Context, nodeID types.NodeID, proof []byte) error +} + type atxProvider interface { GetAtx(id types.ATXID) (*types.ActivationTx, error) } @@ -198,7 +206,7 @@ var ( ErrPostClientNotConnected = errors.New("post service not registered") ) -type AtxBuilder interface { +type atxBuilder interface { Register(sig *signing.EdSigner) } diff --git a/activation/malfeasance.go b/activation/malfeasance.go index 042da18e03..8b249b436b 100644 --- a/activation/malfeasance.go +++ b/activation/malfeasance.go @@ -6,7 +6,6 @@ import ( "fmt" "strconv" - "github.com/prometheus/client_golang/prometheus" "github.com/spacemeshos/post/shared" "github.com/spacemeshos/post/verifying" "go.uber.org/zap" @@ -92,12 +91,8 @@ func (mh *MalfeasanceHandler) Validate(ctx context.Context, data wire.ProofData) return types.EmptyNodeID, errors.New("invalid atx malfeasance proof") } -func (mh *MalfeasanceHandler) ReportProof(numProofs *prometheus.CounterVec) { - numProofs.WithLabelValues(multiATXs).Inc() -} - -func (mh *MalfeasanceHandler) ReportInvalidProof(numInvalidProofs *prometheus.CounterVec) { - numInvalidProofs.WithLabelValues(multiATXs).Inc() +func (mh *MalfeasanceHandler) ReportLabel() string { + return multiATXs } type InvalidPostIndexHandler struct { @@ -169,12 +164,8 @@ func (mh *InvalidPostIndexHandler) Validate(ctx context.Context, data wire.Proof return types.EmptyNodeID, errors.New("invalid post index malfeasance proof - POST is valid") } -func (mh *InvalidPostIndexHandler) ReportProof(numProofs *prometheus.CounterVec) { - numProofs.WithLabelValues(invalidPostIndex).Inc() -} - -func (mh *InvalidPostIndexHandler) ReportInvalidProof(numInvalidProofs *prometheus.CounterVec) { - numInvalidProofs.WithLabelValues(invalidPostIndex).Inc() +func (mh *InvalidPostIndexHandler) ReportLabel() string { + return invalidPostIndex } type InvalidPrevATXHandler struct { @@ -241,10 +232,6 @@ func (mh *InvalidPrevATXHandler) Validate(ctx context.Context, data wire.ProofDa return atx1.SmesherID, nil } -func (mh *InvalidPrevATXHandler) ReportProof(numProofs *prometheus.CounterVec) { - numProofs.WithLabelValues(invalidPrevATX).Inc() -} - -func (mh *InvalidPrevATXHandler) ReportInvalidProof(numInvalidProofs *prometheus.CounterVec) { - numInvalidProofs.WithLabelValues(invalidPrevATX).Inc() +func (mh *InvalidPrevATXHandler) ReportLabel() string { + return invalidPrevATX } diff --git a/activation/malfeasance2.go b/activation/malfeasance2.go index ff44452b35..7394074e45 100644 --- a/activation/malfeasance2.go +++ b/activation/malfeasance2.go @@ -2,15 +2,141 @@ package activation import ( "context" + "fmt" + "sync" + + "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/log" + "github.com/spacemeshos/go-spacemesh/signing" ) -// MalfeasancePublisher is the publisher for ATX proofs. -type MalfeasancePublisher struct{} +type MalfeasanceHandlerV2 struct { + logger *zap.Logger + + malPublisher malfeasancePublisher + edVerifier *signing.EdVerifier + validator nipostValidatorV2 + + smeshingMutex sync.Mutex + signers map[types.NodeID]*signing.EdSigner +} + +func NewMalfeasanceHandlerV2( + logger *zap.Logger, + malPublisher malfeasancePublisher, + edVerifier *signing.EdVerifier, + validator nipostValidatorV2, +) *MalfeasanceHandlerV2 { + return &MalfeasanceHandlerV2{ + logger: logger, + malPublisher: malPublisher, + edVerifier: edVerifier, + validator: validator, + + signers: make(map[types.NodeID]*signing.EdSigner), + } +} + +func (p *MalfeasanceHandlerV2) Register(sig *signing.EdSigner) { + p.smeshingMutex.Lock() + defer p.smeshingMutex.Unlock() + if _, exists := p.signers[sig.NodeID()]; exists { + p.logger.Error("signing key already registered", log.ZShortStringer("id", sig.NodeID())) + return + } + + p.logger.Debug("registered signing key", log.ZShortStringer("id", sig.NodeID())) + p.signers[sig.NodeID()] = sig +} + +// Publish publishes an ATX proof by encoding it and sending it to the malfeasance publisher. +func (p *MalfeasanceHandlerV2) Publish(ctx context.Context, nodeID types.NodeID, proof wire.Proof) error { + p.smeshingMutex.Lock() + _, exists := p.signers[nodeID] + p.smeshingMutex.Unlock() + + if exists { + // do not publish proofs against one self + return fmt.Errorf("publish ATX malfeasance proof: identity %s is managed by node", nodeID) + } + + proofNodeID, err := proof.Valid(ctx, p) + if err != nil { + return fmt.Errorf("publish ATX malfeasance proof: proof not valid: %w", err) + } + if proofNodeID != nodeID { + return fmt.Errorf("publish ATX malfeasance proof: proof for %s does not match node ID %s", + proofNodeID.ShortString(), nodeID.ShortString(), + ) + } + + atxProof := &wire.ATXProof{ + Version: 0x01, // for now we only have one version + ProofType: proof.Type(), + + Proof: codec.MustEncode(proof), + } + return p.malPublisher.PublishATXProof(ctx, nodeID, codec.MustEncode(atxProof)) +} + +func (mh *MalfeasanceHandlerV2) decodeProof(data []byte) (wire.Proof, error) { + var atxProof wire.ATXProof + if err := codec.Decode(data, &atxProof); err != nil { + return nil, err + } + + proof, err := atxProof.Decode() + if err != nil { + return nil, err + } + return proof, nil +} + +func (mh *MalfeasanceHandlerV2) Validate(ctx context.Context, data []byte) (types.NodeID, error) { + proof, err := mh.decodeProof(data) + if err != nil { + return types.EmptyNodeID, fmt.Errorf("decoding ATX malfeasance proof: %w", err) + } + + id, err := proof.Valid(ctx, mh) + if err != nil { + return types.EmptyNodeID, fmt.Errorf("validating ATX malfeasance proof: %w", err) + } + return id, nil +} + +func (mh *MalfeasanceHandlerV2) Info(data []byte) (map[string]string, error) { + proof, err := mh.decodeProof(data) + if err != nil { + return nil, fmt.Errorf("decoding ATX malfeasance proof: %w", err) + } + return proof.Info(), nil +} + +func (mh *MalfeasanceHandlerV2) ReportLabels(data []byte) []string { + proof, err := mh.decodeProof(data) + if err != nil { + return []string{"ATX", "unknown"} + } + return []string{"ATX", proof.String()} +} + +func (mh *MalfeasanceHandlerV2) PostIndex( + ctx context.Context, + smesherID types.NodeID, + commitment types.ATXID, + post *types.Post, + challenge []byte, + numUnits uint32, + idx int, +) error { + return mh.validator.PostV2(ctx, smesherID, commitment, post, challenge, numUnits, PostIndex(idx)) +} -func (p *MalfeasancePublisher) Publish(ctx context.Context, id types.NodeID, proof wire.Proof) error { - // TODO(mafa): implement me - return nil +func (mh *MalfeasanceHandlerV2) Signature(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool { + return mh.edVerifier.Verify(d, nodeID, m, sig) } diff --git a/activation/malfeasance2_test.go b/activation/malfeasance2_test.go new file mode 100644 index 0000000000..692c728a2e --- /dev/null +++ b/activation/malfeasance2_test.go @@ -0,0 +1,309 @@ +package activation + +import ( + "context" + "errors" + "fmt" + "math/rand/v2" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest" + "go.uber.org/zap/zaptest/observer" + + "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" + "github.com/spacemeshos/go-spacemesh/sql/statesql" +) + +type testMalHandler struct { + *MalfeasanceHandlerV2 + + observedLogs *observer.ObservedLogs + ctrl *gomock.Controller + mPublish *MockmalfeasancePublisher + mValidator *MocknipostValidator +} + +func newTestMalHandler(tb testing.TB) *testMalHandler { + edVerifier := signing.NewEdVerifier() + + observer, observedLogs := observer.New(zap.DebugLevel) + logger := zaptest.NewLogger(tb, zaptest.WrapOptions(zap.WrapCore( + func(core zapcore.Core) zapcore.Core { + return zapcore.NewTee(core, observer) + }, + ))) + + ctrl := gomock.NewController(tb) + mPublish := NewMockmalfeasancePublisher(ctrl) + mValidator := NewMocknipostValidator(ctrl) + + handler := NewMalfeasanceHandlerV2( + logger, + mPublish, + edVerifier, + mValidator, + ) + + return &testMalHandler{ + MalfeasanceHandlerV2: handler, + + observedLogs: observedLogs, + ctrl: ctrl, + mPublish: mPublish, + mValidator: mValidator, + } +} + +func TestRegister(t *testing.T) { + t.Parallel() + + t.Run("register", func(t *testing.T) { + t.Parallel() + th := newTestMalHandler(t) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + th.Register(sig) + + require.Equal(t, 1, th.observedLogs.Len()) + require.Equal(t, zap.DebugLevel, th.observedLogs.All()[0].Level) + require.Contains(t, th.observedLogs.All()[0].Message, "registered signing key") + }) + + t.Run("already registered", func(t *testing.T) { + t.Parallel() + th := newTestMalHandler(t) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + th.Register(sig) + th.Register(sig) + + logs := th.observedLogs.FilterLevelExact(zap.ErrorLevel) + + require.Equal(t, 1, logs.Len()) + require.Equal(t, zap.ErrorLevel, logs.All()[0].Level) + require.Contains(t, logs.All()[0].Message, "signing key already registered") + }) +} + +func TestPublish(t *testing.T) { + t.Parallel() + + t.Run("valid proof", func(t *testing.T) { + t.Parallel() + + th := newTestMalHandler(t) + + nodeID := types.RandomNodeID() + proof := wire.NewMockProof(th.ctrl) + + proof.EXPECT().Valid(context.Background(), th.MalfeasanceHandlerV2).Return(nodeID, nil) + proof.EXPECT().Type().Return(wire.DoubleMarry) + proof.EXPECT().EncodeScale(gomock.Any()) + + atxProof := &wire.ATXProof{ + Version: 0x01, // for now we only have one version + ProofType: wire.DoubleMarry, + + Proof: []byte{}, + } + th.mPublish.EXPECT().PublishATXProof(context.Background(), nodeID, codec.MustEncode(atxProof)).Return(nil) + + err := th.Publish(context.Background(), nodeID, proof) + require.NoError(t, err) + }) + + t.Run("invalid proof", func(t *testing.T) { + t.Parallel() + + th := newTestMalHandler(t) + + proof := wire.NewMockProof(th.ctrl) + nodeID := types.RandomNodeID() + errInvalidProof := errors.New("invalid proof") + proof.EXPECT().Valid(context.Background(), th.MalfeasanceHandlerV2).Return(types.EmptyNodeID, errInvalidProof) + + err := th.Publish(context.Background(), nodeID, proof) + require.ErrorIs(t, err, errInvalidProof) + require.ErrorContains(t, err, "proof not valid") + }) + + t.Run("proof for self", func(t *testing.T) { + t.Parallel() + + th := newTestMalHandler(t) + + sig1, err := signing.NewEdSigner() + require.NoError(t, err) + th.Register(sig1) + + proof := wire.NewMockProof(th.ctrl) + + err = th.Publish(context.Background(), sig1.NodeID(), proof) + require.ErrorContains(t, err, fmt.Sprintf("identity %s is managed by node", sig1.NodeID())) + }) + + t.Run("proof for different nodeID", func(t *testing.T) { + t.Parallel() + + th := newTestMalHandler(t) + + sig1 := types.RandomNodeID() + sig2 := types.RandomNodeID() + + proof := wire.NewMockProof(th.ctrl) + proof.EXPECT().Valid(context.Background(), th.MalfeasanceHandlerV2).Return(sig2, nil) + + err := th.Publish(context.Background(), sig1, proof) + require.ErrorContains(t, err, + fmt.Sprintf("proof for %s does not match node ID %s", sig2.ShortString(), sig1.ShortString()), + ) + }) +} + +func TestValidate(t *testing.T) { + t.Parallel() + + t.Run("proof fails decoding", func(t *testing.T) { + t.Parallel() + + th := newTestMalHandler(t) + + id, err := th.Validate(context.Background(), []byte{}) + require.ErrorContains(t, err, "decoding ATX malfeasance proof") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("unknown proof type", func(t *testing.T) { + t.Parallel() + + th := newTestMalHandler(t) + + atxProof := &wire.ATXProof{ + Version: 0x01, + ProofType: 0x42, // unknown proof type + } + + id, err := th.Validate(context.Background(), codec.MustEncode(atxProof)) + require.ErrorContains(t, err, "unknown ATX malfeasance proof type") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("atx proof fails decoding", func(t *testing.T) { + t.Parallel() + + th := newTestMalHandler(t) + + atxProof := &wire.ATXProof{ + Version: 0x01, + ProofType: wire.DoubleMarry, + Proof: []byte{}, // invalid proof + } + + id, err := th.Validate(context.Background(), codec.MustEncode(atxProof)) + require.ErrorContains(t, err, "decoding ATX malfeasance proof of type 0x11") + require.Equal(t, types.EmptyNodeID, id) + }) + + genProof := func(t *testing.T, sig *signing.EdSigner) *wire.ProofInvalidPost { + db := statesql.InMemoryTest(t) + + nipostChallenge := types.RandomHash() + const numUnits = uint32(11) + post := wire.PostV1{ + Nonce: rand.Uint32(), + Indices: types.RandomBytes(11), + Pow: rand.Uint64(), + } + atx := wire.NewTestActivationTxV2( + wire.WithNIPost( + wire.WithNIPostChallenge(nipostChallenge), + wire.WithNIPostSubPost(wire.SubPostV2{ + Post: post, + NumUnits: numUnits, + }), + ), + ) + atx.Sign(sig) + commitmentATX := types.RandomATXID() + + const invalidPostIdx = 7 + const validPostIdx = 15 + proof, err := wire.NewInvalidPostProof(db, atx, commitmentATX, sig.NodeID(), 0, invalidPostIdx, validPostIdx) + require.NoError(t, err) + return proof + } + + t.Run("valid proof", func(t *testing.T) { + t.Parallel() + + th := newTestMalHandler(t) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + proof := genProof(t, sig) + + atxProof := &wire.ATXProof{ + Version: 0x01, // for now we only have one version + ProofType: proof.Type(), + Proof: codec.MustEncode(proof), + } + + th.mValidator.EXPECT().PostV2( + context.Background(), + proof.NodeID, + proof.InvalidPostProof.CommitmentATX, + wire.PostFromWireV1(&proof.InvalidPostProof.Post), + proof.InvalidPostProof.Challenge.Bytes(), + proof.InvalidPostProof.NumUnits, + gomock.Cond(func(opt validatorOption) bool { + opts := &validatorOptions{} + opt(opts) + return *opts.postIdx == int(proof.InvalidPostProof.InvalidPostIndex) + }), + ).Return(errors.New("invalid post")) + th.mValidator.EXPECT().PostV2( + context.Background(), + proof.NodeID, + proof.InvalidPostProof.CommitmentATX, + wire.PostFromWireV1(&proof.InvalidPostProof.Post), + proof.InvalidPostProof.Challenge.Bytes(), + proof.InvalidPostProof.NumUnits, + gomock.Cond(func(opt validatorOption) bool { + opts := &validatorOptions{} + opt(opts) + return *opts.postIdx == int(proof.InvalidPostProof.ValidPostIndex) + }), + ).Return(nil) + id, err := th.Validate(context.Background(), codec.MustEncode(atxProof)) + require.NoError(t, err) + require.Equal(t, sig.NodeID(), id) + }) + + t.Run("invalid proof", func(t *testing.T) { + t.Parallel() + + th := newTestMalHandler(t) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + proof := genProof(t, sig) + proof.NodeID = types.RandomNodeID() + + atxProof := &wire.ATXProof{ + Version: 0x01, // for now we only have one version + ProofType: proof.Type(), + Proof: codec.MustEncode(proof), + } + + id, err := th.Validate(context.Background(), codec.MustEncode(atxProof)) + require.ErrorContains(t, err, "validating ATX malfeasance proof:") + require.Equal(t, types.EmptyNodeID, id) + }) +} diff --git a/activation/mocks.go b/activation/mocks.go index 00ba677320..71b5c01e0a 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -1217,6 +1217,68 @@ func (c *MockatxMalfeasancePublisherPublishCall) DoAndReturn(f func(context.Cont return c } +// MockmalfeasancePublisher is a mock of malfeasancePublisher interface. +type MockmalfeasancePublisher struct { + ctrl *gomock.Controller + recorder *MockmalfeasancePublisherMockRecorder + isgomock struct{} +} + +// MockmalfeasancePublisherMockRecorder is the mock recorder for MockmalfeasancePublisher. +type MockmalfeasancePublisherMockRecorder struct { + mock *MockmalfeasancePublisher +} + +// NewMockmalfeasancePublisher creates a new mock instance. +func NewMockmalfeasancePublisher(ctrl *gomock.Controller) *MockmalfeasancePublisher { + mock := &MockmalfeasancePublisher{ctrl: ctrl} + mock.recorder = &MockmalfeasancePublisherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockmalfeasancePublisher) EXPECT() *MockmalfeasancePublisherMockRecorder { + return m.recorder +} + +// PublishATXProof mocks base method. +func (m *MockmalfeasancePublisher) PublishATXProof(ctx context.Context, nodeID types.NodeID, proof []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PublishATXProof", ctx, nodeID, proof) + ret0, _ := ret[0].(error) + return ret0 +} + +// PublishATXProof indicates an expected call of PublishATXProof. +func (mr *MockmalfeasancePublisherMockRecorder) PublishATXProof(ctx, nodeID, proof any) *MockmalfeasancePublisherPublishATXProofCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishATXProof", reflect.TypeOf((*MockmalfeasancePublisher)(nil).PublishATXProof), ctx, nodeID, proof) + return &MockmalfeasancePublisherPublishATXProofCall{Call: call} +} + +// MockmalfeasancePublisherPublishATXProofCall wrap *gomock.Call +type MockmalfeasancePublisherPublishATXProofCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockmalfeasancePublisherPublishATXProofCall) Return(arg0 error) *MockmalfeasancePublisherPublishATXProofCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockmalfeasancePublisherPublishATXProofCall) Do(f func(context.Context, types.NodeID, []byte) error) *MockmalfeasancePublisherPublishATXProofCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockmalfeasancePublisherPublishATXProofCall) DoAndReturn(f func(context.Context, types.NodeID, []byte) error) *MockmalfeasancePublisherPublishATXProofCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // MockatxProvider is a mock of atxProvider interface. type MockatxProvider struct { ctrl *gomock.Controller @@ -2229,62 +2291,62 @@ func (c *MockpoetDbAPIValidateAndStoreCall) DoAndReturn(f func(context.Context, return c } -// MockAtxBuilder is a mock of AtxBuilder interface. -type MockAtxBuilder struct { +// MockatxBuilder is a mock of atxBuilder interface. +type MockatxBuilder struct { ctrl *gomock.Controller - recorder *MockAtxBuilderMockRecorder + recorder *MockatxBuilderMockRecorder isgomock struct{} } -// MockAtxBuilderMockRecorder is the mock recorder for MockAtxBuilder. -type MockAtxBuilderMockRecorder struct { - mock *MockAtxBuilder +// MockatxBuilderMockRecorder is the mock recorder for MockatxBuilder. +type MockatxBuilderMockRecorder struct { + mock *MockatxBuilder } -// NewMockAtxBuilder creates a new mock instance. -func NewMockAtxBuilder(ctrl *gomock.Controller) *MockAtxBuilder { - mock := &MockAtxBuilder{ctrl: ctrl} - mock.recorder = &MockAtxBuilderMockRecorder{mock} +// NewMockatxBuilder creates a new mock instance. +func NewMockatxBuilder(ctrl *gomock.Controller) *MockatxBuilder { + mock := &MockatxBuilder{ctrl: ctrl} + mock.recorder = &MockatxBuilderMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockAtxBuilder) EXPECT() *MockAtxBuilderMockRecorder { +func (m *MockatxBuilder) EXPECT() *MockatxBuilderMockRecorder { return m.recorder } // Register mocks base method. -func (m *MockAtxBuilder) Register(sig *signing.EdSigner) { +func (m *MockatxBuilder) Register(sig *signing.EdSigner) { m.ctrl.T.Helper() m.ctrl.Call(m, "Register", sig) } // Register indicates an expected call of Register. -func (mr *MockAtxBuilderMockRecorder) Register(sig any) *MockAtxBuilderRegisterCall { +func (mr *MockatxBuilderMockRecorder) Register(sig any) *MockatxBuilderRegisterCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockAtxBuilder)(nil).Register), sig) - return &MockAtxBuilderRegisterCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockatxBuilder)(nil).Register), sig) + return &MockatxBuilderRegisterCall{Call: call} } -// MockAtxBuilderRegisterCall wrap *gomock.Call -type MockAtxBuilderRegisterCall struct { +// MockatxBuilderRegisterCall wrap *gomock.Call +type MockatxBuilderRegisterCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockAtxBuilderRegisterCall) Return() *MockAtxBuilderRegisterCall { +func (c *MockatxBuilderRegisterCall) Return() *MockatxBuilderRegisterCall { c.Call = c.Call.Return() return c } // Do rewrite *gomock.Call.Do -func (c *MockAtxBuilderRegisterCall) Do(f func(*signing.EdSigner)) *MockAtxBuilderRegisterCall { +func (c *MockatxBuilderRegisterCall) Do(f func(*signing.EdSigner)) *MockatxBuilderRegisterCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockAtxBuilderRegisterCall) DoAndReturn(f func(*signing.EdSigner)) *MockAtxBuilderRegisterCall { +func (c *MockatxBuilderRegisterCall) DoAndReturn(f func(*signing.EdSigner)) *MockatxBuilderRegisterCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/activation/post_supervisor.go b/activation/post_supervisor.go index bc49dd63f1..f3ea4b6300 100644 --- a/activation/post_supervisor.go +++ b/activation/post_supervisor.go @@ -70,7 +70,7 @@ type PostSupervisor struct { provingOpts PostProvingOpts postSetupProvider postSetupProvider - atxBuilder AtxBuilder + atxBuilder atxBuilder pid atomic.Int64 // pid of the running post service, only for tests. @@ -85,7 +85,7 @@ func NewPostSupervisor( postCfg PostConfig, provingOpts PostProvingOpts, postSetupProvider postSetupProvider, - atxBuilder AtxBuilder, + atxBuilder atxBuilder, ) *PostSupervisor { return &PostSupervisor{ logger: logger, diff --git a/activation/post_supervisor_test.go b/activation/post_supervisor_test.go index 597ce47fe2..07dc303293 100644 --- a/activation/post_supervisor_test.go +++ b/activation/post_supervisor_test.go @@ -105,7 +105,7 @@ func Test_PostSupervisor_Start_FailPrepare(t *testing.T) { mgr := NewMockpostSetupProvider(ctrl) testErr := errors.New("test error") mgr.EXPECT().PrepareInitializer(gomock.Any(), postOpts, sig.NodeID()).Return(testErr) - builder := NewMockAtxBuilder(ctrl) + builder := NewMockatxBuilder(ctrl) ps := NewPostSupervisor(log.Named("supervisor"), postCfg, provingOpts, mgr, builder) require.NoError(t, ps.Start(cmdCfg, postOpts, sig)) @@ -141,7 +141,7 @@ func Test_PostSupervisor_Start_FailStartSession(t *testing.T) { mgr := NewMockpostSetupProvider(ctrl) mgr.EXPECT().PrepareInitializer(gomock.Any(), postOpts, sig.NodeID()).Return(nil) mgr.EXPECT().StartSession(gomock.Any(), sig.NodeID()).Return(errors.New("failed start session")) - builder := NewMockAtxBuilder(ctrl) + builder := NewMockatxBuilder(ctrl) ps := NewPostSupervisor(log.Named("supervisor"), postCfg, provingOpts, mgr, builder) require.NoError(t, ps.Start(cmdCfg, postOpts, sig)) @@ -160,7 +160,7 @@ func Test_PostSupervisor_StartsServiceCmd(t *testing.T) { ctrl := gomock.NewController(t) mgr := newPostManager(t, postCfg, postOpts) - builder := NewMockAtxBuilder(ctrl) + builder := NewMockatxBuilder(ctrl) builder.EXPECT().Register(sig) ps := NewPostSupervisor(log.Named("supervisor"), postCfg, provingOpts, mgr, builder) @@ -197,7 +197,7 @@ func Test_PostSupervisor_Restart_Possible(t *testing.T) { ctrl := gomock.NewController(t) mgr := newPostManager(t, postCfg, postOpts) - builder := NewMockAtxBuilder(ctrl) + builder := NewMockatxBuilder(ctrl) builder.EXPECT().Register(sig) ps := NewPostSupervisor(log.Named("supervisor"), postCfg, provingOpts, mgr, builder) @@ -228,7 +228,7 @@ func Test_PostSupervisor_LogFatalOnCrash(t *testing.T) { ctrl := gomock.NewController(t) mgr := newPostManager(t, postCfg, postOpts) - builder := NewMockAtxBuilder(ctrl) + builder := NewMockatxBuilder(ctrl) builder.EXPECT().Register(sig) ps := NewPostSupervisor(log.Named("supervisor"), postCfg, provingOpts, mgr, builder) @@ -261,7 +261,7 @@ func Test_PostSupervisor_LogFatalOnInvalidConfig(t *testing.T) { ctrl := gomock.NewController(t) mgr := newPostManager(t, postCfg, postOpts) - builder := NewMockAtxBuilder(ctrl) + builder := NewMockatxBuilder(ctrl) builder.EXPECT().Register(sig) ps := NewPostSupervisor(log.Named("supervisor"), postCfg, provingOpts, mgr, builder) @@ -301,7 +301,7 @@ func Test_PostSupervisor_StopOnError(t *testing.T) { require.NoError(t, err) return nil }) - builder := NewMockAtxBuilder(ctrl) + builder := NewMockatxBuilder(ctrl) builder.EXPECT().Register(sig) ps := NewPostSupervisor(log.Named("supervisor"), postCfg, provingOpts, mgr, builder) @@ -322,7 +322,7 @@ func Test_PostSupervisor_Providers_includesCPU(t *testing.T) { ctrl := gomock.NewController(t) mgr := NewMockpostSetupProvider(ctrl) - builder := NewMockAtxBuilder(ctrl) + builder := NewMockatxBuilder(ctrl) ps := NewPostSupervisor(log.Named("supervisor"), postCfg, provingOpts, mgr, builder) providers, err := ps.Providers() @@ -344,7 +344,7 @@ func Test_PostSupervisor_Benchmark(t *testing.T) { ctrl := gomock.NewController(t) mgr := NewMockpostSetupProvider(ctrl) - builder := NewMockAtxBuilder(ctrl) + builder := NewMockatxBuilder(ctrl) ps := NewPostSupervisor(log.Named("supervisor"), postCfg, provingOpts, mgr, builder) providers, err := ps.Providers() diff --git a/activation/post_verifier_test.go b/activation/post_verifier_test.go index c02b329387..d5bf603d5c 100644 --- a/activation/post_verifier_test.go +++ b/activation/post_verifier_test.go @@ -107,9 +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) diff --git a/activation/wire/interface.go b/activation/wire/interface.go index ba5006e3cd..9475113a60 100644 --- a/activation/wire/interface.go +++ b/activation/wire/interface.go @@ -2,6 +2,9 @@ package wire import ( "context" + "fmt" + + "github.com/spacemeshos/go-scale" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/signing" @@ -25,3 +28,15 @@ type MalfeasanceValidator interface { // Signature validates the given signature against the given message and public key. Signature(d signing.Domain, nodeID types.NodeID, m []byte, sig types.EdSignature) bool } + +// 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 and be scale encoded. +type Proof interface { + scale.Encodable + scale.Decodable + fmt.Stringer + + Type() ProofType + Info() map[string]string + Valid(ctx context.Context, malHandler MalfeasanceValidator) (types.NodeID, error) +} diff --git a/activation/wire/malfeasance.go b/activation/wire/malfeasance.go index 7e2aa97b98..dcb471bada 100644 --- a/activation/wire/malfeasance.go +++ b/activation/wire/malfeasance.go @@ -1,15 +1,11 @@ package wire import ( - "context" + "fmt" - "github.com/spacemeshos/go-scale" - - "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/codec" ) -//go:generate scalegen - // MerkleTreeIndex is the index of the leaf containing the given field in the merkle tree. type MerkleTreeIndex uint64 @@ -62,16 +58,27 @@ type ProofType byte const ( // TODO(mafa): legacy types for future migration to new malfeasance proofs. - LegacyDoublePublish ProofType = 0x00 - LegacyInvalidPost ProofType = 0x01 - LegacyInvalidPrevATX ProofType = 0x02 - - DoubleMarry ProofType = 0x10 - DoubleMerge ProofType = 0x11 - InvalidPost ProofType = 0x12 - InvalidPrevious ProofType = 0x13 + LegacyDoublePublish ProofType = 0x01 + LegacyInvalidPost ProofType = 0x02 + LegacyInvalidPrevATX ProofType = 0x03 + + DoubleMarry ProofType = 0x11 + DoubleMerge ProofType = 0x12 + InvalidPost ProofType = 0x13 + InvalidPreviousV1 ProofType = 0x14 + InvalidPreviousV2 ProofType = 0x15 ) +var proofTypes = map[ProofType]Proof{ + // TODO(mafa): legacy proofs + + DoubleMarry: &ProofDoubleMarry{}, + DoubleMerge: &ProofDoubleMerge{}, + InvalidPost: &ProofInvalidPost{}, + InvalidPreviousV1: &ProofInvalidPrevAtxV1{}, + InvalidPreviousV2: &ProofInvalidPrevAtxV2{}, +} + // ProofVersion is an identifier for the version of the proof that is encoded in the ATXProof. type ProofVersion byte @@ -80,14 +87,18 @@ type ATXProof struct { Version ProofVersion // ProofType is the type of proof that is being provided. ProofType ProofType + // Proof is the actual proof. Its type depends on the ProofType. 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 and be scale encoded. -type Proof interface { - scale.Encodable - - Valid(ctx context.Context, malHandler MalfeasanceValidator) (types.NodeID, error) +func (p *ATXProof) Decode() (Proof, error) { + rst, ok := proofTypes[p.ProofType] + if !ok { + return nil, fmt.Errorf("unknown ATX malfeasance proof type: 0x%x", p.ProofType) + } + if err := codec.Decode(p.Proof, rst); err != nil { + return nil, fmt.Errorf("decoding ATX malfeasance proof of type 0x%x: %w", p.ProofType, err) + } + return rst, nil } diff --git a/activation/wire/malfeasance_double_marry.go b/activation/wire/malfeasance_double_marry.go index fc2a98e545..1bc063c4f9 100644 --- a/activation/wire/malfeasance_double_marry.go +++ b/activation/wire/malfeasance_double_marry.go @@ -42,6 +42,24 @@ type ProofDoubleMarry struct { Proof2 MarryProof } +func (p ProofDoubleMarry) String() string { + return "DoubleMarryProof" +} + +func (p ProofDoubleMarry) Type() ProofType { + return DoubleMarry +} + +func (p ProofDoubleMarry) Info() map[string]string { + return map[string]string{ + "node_id": p.NodeID.String(), + "atx1": p.ATX1.String(), + "smesher_id1": p.SmesherID1.String(), + "atx2": p.ATX2.String(), + "smesher_id2": p.SmesherID2.String(), + } +} + var _ Proof = &ProofDoubleMarry{} func NewDoubleMarryProof(db sql.Executor, atx1, atx2 *ActivationTxV2, nodeID types.NodeID) (*ProofDoubleMarry, error) { diff --git a/activation/wire/malfeasance_double_marry_test.go b/activation/wire/malfeasance_double_marry_test.go index f52f8c8559..d53fc151ee 100644 --- a/activation/wire/malfeasance_double_marry_test.go +++ b/activation/wire/malfeasance_double_marry_test.go @@ -34,15 +34,15 @@ func Test_DoubleMarryProof(t *testing.T) { otherAtx.SmesherID = otherSig.NodeID() require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) - atx1 := newActivationTxV2( - withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), - withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + atx1 := NewTestActivationTxV2( + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) atx1.Sign(sig) - atx2 := newActivationTxV2( - withMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), - withMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), + atx2 := NewTestActivationTxV2( + WithMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + WithMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), ) atx2.Sign(otherSig) @@ -71,15 +71,15 @@ func Test_DoubleMarryProof(t *testing.T) { otherAtx.SmesherID = otherSig.NodeID() require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) - atx1 := newActivationTxV2( - withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), - withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + atx1 := NewTestActivationTxV2( + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) atx1.Sign(sig) - atx2 := newActivationTxV2( - withMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), - withMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), + atx2 := NewTestActivationTxV2( + WithMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + WithMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), ) atx2.Sign(otherSig) @@ -108,7 +108,7 @@ func Test_DoubleMarryProof(t *testing.T) { t.Parallel() db := statesql.InMemoryTest(t) - atx1 := newActivationTxV2() + atx1 := NewTestActivationTxV2() atx1.Sign(sig) proof, err := NewDoubleMarryProof(db, atx1, atx1, sig.NodeID()) @@ -138,15 +138,15 @@ func Test_DoubleMarryProof(t *testing.T) { otherAtx.SmesherID = otherSig.NodeID() require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) - atx1 := newActivationTxV2( - withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), - withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + atx1 := NewTestActivationTxV2( + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) atx1.Sign(sig) - atx2 := newActivationTxV2( - withMarriageCertificate(otherSig, types.EmptyATXID, sig.NodeID()), - withMarriageCertificate(sig, atx1.ID(), sig.NodeID()), + atx2 := NewTestActivationTxV2( + WithMarriageCertificate(otherSig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(sig, atx1.ID(), sig.NodeID()), ) atx2.Sign(otherSig) diff --git a/activation/wire/malfeasance_double_merge.go b/activation/wire/malfeasance_double_merge.go index 3b3f73194a..7b5eb97665 100644 --- a/activation/wire/malfeasance_double_merge.go +++ b/activation/wire/malfeasance_double_merge.go @@ -32,8 +32,8 @@ type ProofDoubleMerge struct { // MarriageATXSmesherID is the ID of the smesher that published the marriage ATX. MarriageATXSmesherID types.NodeID - // ATXID1 is the ID of the ATX being proven. - ATXID1 types.ATXID + // ATX1 is the ID of the ATX being proven. + ATX1 types.ATXID // SmesherID1 is the ID of the smesher that published the ATX. SmesherID1 types.NodeID // Signature1 is the signature of the ATXID by the smesher. @@ -45,8 +45,8 @@ type ProofDoubleMerge struct { // SmesherID1MarryProof is the proof that they married in MarriageATX. SmesherID1MarryProof MarryProof - // ATXID2 is the ID of the ATX being proven. - ATXID2 types.ATXID + // ATX2 is the ID of the ATX being proven. + ATX2 types.ATXID // SmesherID is the ID of the smesher that published the ATX. SmesherID2 types.NodeID // Signature2 is the signature of the ATXID by the smesher. @@ -59,6 +59,25 @@ type ProofDoubleMerge struct { SmesherID2MarryProof MarryProof } +func (p ProofDoubleMerge) String() string { + return "DoubleMergeProof" +} + +func (p ProofDoubleMerge) Type() ProofType { + return DoubleMerge +} + +func (p ProofDoubleMerge) Info() map[string]string { + return map[string]string{ + "publish_epoch": p.PublishEpoch.String(), + "marriage_atx": p.MarriageATX.String(), + "atx1": p.ATX1.String(), + "smesher_id1": p.SmesherID1.String(), + "atx2": p.ATX2.String(), + "smesher_id2": p.SmesherID2.String(), + } +} + var _ Proof = &ProofDoubleMerge{} func NewDoubleMergeProof(db sql.Executor, atx1, atx2 *ActivationTxV2) (*ProofDoubleMerge, error) { @@ -108,14 +127,14 @@ func NewDoubleMergeProof(db sql.Executor, atx1, atx2 *ActivationTxV2) (*ProofDou MarriageATX: marriageATX.ID(), MarriageATXSmesherID: marriageATX.SmesherID, - ATXID1: atx1.ID(), + ATX1: atx1.ID(), SmesherID1: atx1.SmesherID, Signature1: atx1.Signature, PublishEpochProof1: atx1.PublishEpochProof(), MarriageATXProof1: atx1.MarriageATXProof(), SmesherID1MarryProof: marriageProof1, - ATXID2: atx2.ID(), + ATX2: atx2.ID(), SmesherID2: atx2.SmesherID, Signature2: atx2.Signature, PublishEpochProof2: atx2.PublishEpochProof(), @@ -128,35 +147,35 @@ func NewDoubleMergeProof(db sql.Executor, atx1, atx2 *ActivationTxV2) (*ProofDou func (p *ProofDoubleMerge) Valid(_ context.Context, edVerifier MalfeasanceValidator) (types.NodeID, error) { // 1. The ATXs have different IDs. - if p.ATXID1 == p.ATXID2 { + if p.ATX1 == p.ATX2 { return types.EmptyNodeID, errors.New("ATXs have the same ID") } // 2. Both ATXs have a valid signature. - if !edVerifier.Signature(signing.ATX, p.SmesherID1, p.ATXID1.Bytes(), p.Signature1) { + if !edVerifier.Signature(signing.ATX, p.SmesherID1, p.ATX1.Bytes(), p.Signature1) { return types.EmptyNodeID, errors.New("ATX 1 invalid signature") } - if !edVerifier.Signature(signing.ATX, p.SmesherID2, p.ATXID2.Bytes(), p.Signature2) { + if !edVerifier.Signature(signing.ATX, p.SmesherID2, p.ATX2.Bytes(), p.Signature2) { return types.EmptyNodeID, errors.New("ATX 2 invalid signature") } // 3. and 4. publish epoch is contained in the ATXs - if !p.PublishEpochProof1.Valid(p.ATXID1, p.PublishEpoch) { + if !p.PublishEpochProof1.Valid(p.ATX1, p.PublishEpoch) { return types.EmptyNodeID, errors.New("ATX 1 invalid publish epoch proof") } - if !p.PublishEpochProof2.Valid(p.ATXID2, p.PublishEpoch) { + if !p.PublishEpochProof2.Valid(p.ATX2, p.PublishEpoch) { return types.EmptyNodeID, errors.New("ATX 2 invalid publish epoch proof") } // 5. signers are married - if !p.MarriageATXProof1.Valid(p.ATXID1, p.MarriageATX) { + if !p.MarriageATXProof1.Valid(p.ATX1, p.MarriageATX) { return types.EmptyNodeID, errors.New("ATX 1 invalid marriage ATX proof") } err := p.SmesherID1MarryProof.Valid(edVerifier, p.MarriageATX, p.MarriageATXSmesherID, p.SmesherID1) if err != nil { return types.EmptyNodeID, errors.New("ATX 1 invalid marriage ATX proof") } - if !p.MarriageATXProof2.Valid(p.ATXID2, p.MarriageATX) { + if !p.MarriageATXProof2.Valid(p.ATX2, p.MarriageATX) { return types.EmptyNodeID, errors.New("ATX 2 invalid marriage ATX proof") } err = p.SmesherID2MarryProof.Valid(edVerifier, p.MarriageATX, p.MarriageATXSmesherID, p.SmesherID2) diff --git a/activation/wire/malfeasance_double_merge_scale.go b/activation/wire/malfeasance_double_merge_scale.go index d4d38b16f3..b4d1a55736 100644 --- a/activation/wire/malfeasance_double_merge_scale.go +++ b/activation/wire/malfeasance_double_merge_scale.go @@ -31,7 +31,7 @@ func (t *ProofDoubleMerge) EncodeScale(enc *scale.Encoder) (total int, err error total += n } { - n, err := scale.EncodeByteArray(enc, t.ATXID1[:]) + n, err := scale.EncodeByteArray(enc, t.ATX1[:]) if err != nil { return total, err } @@ -73,7 +73,7 @@ func (t *ProofDoubleMerge) EncodeScale(enc *scale.Encoder) (total int, err error total += n } { - n, err := scale.EncodeByteArray(enc, t.ATXID2[:]) + n, err := scale.EncodeByteArray(enc, t.ATX2[:]) if err != nil { return total, err } @@ -141,7 +141,7 @@ func (t *ProofDoubleMerge) DecodeScale(dec *scale.Decoder) (total int, err error total += n } { - n, err := scale.DecodeByteArray(dec, t.ATXID1[:]) + n, err := scale.DecodeByteArray(dec, t.ATX1[:]) if err != nil { return total, err } @@ -185,7 +185,7 @@ func (t *ProofDoubleMerge) DecodeScale(dec *scale.Decoder) (total int, err error total += n } { - n, err := scale.DecodeByteArray(dec, t.ATXID2[:]) + n, err := scale.DecodeByteArray(dec, t.ATX2[:]) if err != nil { return total, err } diff --git a/activation/wire/malfeasance_double_merge_test.go b/activation/wire/malfeasance_double_merge_test.go index 83ba3a2b4a..0f7c43960f 100644 --- a/activation/wire/malfeasance_double_merge_test.go +++ b/activation/wire/malfeasance_double_merge_test.go @@ -29,8 +29,8 @@ func Test_DoubleMergeProof(t *testing.T) { edVerifier := signing.NewEdVerifier() setupMarriage := func(db sql.Executor) *ActivationTxV2 { - wInitialAtx1 := newActivationTxV2( - withInitial(types.RandomATXID(), PostV1{}), + wInitialAtx1 := NewTestActivationTxV2( + WithInitial(types.RandomATXID(), PostV1{}), ) wInitialAtx1.Sign(sig) initialAtx1 := &types.ActivationTx{ @@ -40,8 +40,8 @@ func Test_DoubleMergeProof(t *testing.T) { initialAtx1.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, initialAtx1, wInitialAtx1.Blob())) - wInitialAtx2 := newActivationTxV2( - withInitial(types.RandomATXID(), PostV1{}), + wInitialAtx2 := NewTestActivationTxV2( + WithInitial(types.RandomATXID(), PostV1{}), ) wInitialAtx2.Sign(otherSig) initialAtx2 := &types.ActivationTx{} @@ -49,10 +49,10 @@ func Test_DoubleMergeProof(t *testing.T) { initialAtx2.SmesherID = otherSig.NodeID() require.NoError(t, atxs.Add(db, initialAtx2, wInitialAtx2.Blob())) - wMarriageAtx := newActivationTxV2( - withMarriageCertificate(marrySig, types.EmptyATXID, marrySig.NodeID()), - withMarriageCertificate(sig, wInitialAtx1.ID(), marrySig.NodeID()), - withMarriageCertificate(otherSig, wInitialAtx2.ID(), marrySig.NodeID()), + wMarriageAtx := NewTestActivationTxV2( + WithMarriageCertificate(marrySig, types.EmptyATXID, marrySig.NodeID()), + WithMarriageCertificate(sig, wInitialAtx1.ID(), marrySig.NodeID()), + WithMarriageCertificate(otherSig, wInitialAtx2.ID(), marrySig.NodeID()), ) wMarriageAtx.Sign(marrySig) @@ -76,15 +76,15 @@ func Test_DoubleMergeProof(t *testing.T) { marriageAtx := setupMarriage(db) - atx1 := newActivationTxV2( - withMarriageATX(marriageAtx.ID()), - withPublishEpoch(marriageAtx.PublishEpoch+1), + atx1 := NewTestActivationTxV2( + WithMarriageATX(marriageAtx.ID()), + WithPublishEpoch(marriageAtx.PublishEpoch+1), ) atx1.Sign(sig) - atx2 := newActivationTxV2( - withMarriageATX(marriageAtx.ID()), - withPublishEpoch(marriageAtx.PublishEpoch+1), + atx2 := NewTestActivationTxV2( + WithMarriageATX(marriageAtx.ID()), + WithPublishEpoch(marriageAtx.PublishEpoch+1), ) atx2.Sign(otherSig) @@ -108,9 +108,9 @@ func Test_DoubleMergeProof(t *testing.T) { marriageAtx := setupMarriage(db) - atx1 := newActivationTxV2( - withMarriageATX(marriageAtx.ID()), - withPublishEpoch(marriageAtx.PublishEpoch+1), + atx1 := NewTestActivationTxV2( + WithMarriageATX(marriageAtx.ID()), + WithPublishEpoch(marriageAtx.PublishEpoch+1), ) atx1.Sign(sig) @@ -119,8 +119,8 @@ func Test_DoubleMergeProof(t *testing.T) { require.Nil(t, proof) proof = &ProofDoubleMerge{ - ATXID1: atx1.ID(), - ATXID2: atx1.ID(), + ATX1: atx1.ID(), + ATX2: atx1.ID(), } id, err := proof.Valid(context.Background(), verifier) require.EqualError(t, err, "ATXs have the same ID") @@ -130,10 +130,10 @@ func Test_DoubleMergeProof(t *testing.T) { t.Run("ATXs must have different signers", func(t *testing.T) { t.Parallel() db := statesql.InMemoryTest(t) - atx1 := newActivationTxV2() + atx1 := NewTestActivationTxV2() atx1.Sign(sig) - atx2 := newActivationTxV2() + atx2 := NewTestActivationTxV2() atx2.Sign(sig) proof, err := NewDoubleMergeProof(db, atx1, atx2) @@ -144,13 +144,13 @@ func Test_DoubleMergeProof(t *testing.T) { t.Run("ATXs must be published in the same epoch", func(t *testing.T) { t.Parallel() db := statesql.InMemoryTest(t) - atx := newActivationTxV2( - withPublishEpoch(1), + atx := NewTestActivationTxV2( + WithPublishEpoch(1), ) atx.Sign(sig) - atx2 := newActivationTxV2( - withPublishEpoch(2), + atx2 := NewTestActivationTxV2( + WithPublishEpoch(2), ) atx2.Sign(otherSig) proof, err := NewDoubleMergeProof(db, atx, atx2) @@ -162,13 +162,13 @@ func Test_DoubleMergeProof(t *testing.T) { t.Parallel() db := statesql.InMemoryTest(t) - atx := newActivationTxV2( - withPublishEpoch(1), + atx := NewTestActivationTxV2( + WithPublishEpoch(1), ) atx.Sign(sig) - atx2 := newActivationTxV2( - withPublishEpoch(1), + atx2 := NewTestActivationTxV2( + WithPublishEpoch(1), ) atx2.Sign(otherSig) @@ -211,15 +211,15 @@ func Test_DoubleMergeProof(t *testing.T) { marriageAtx := setupMarriage(db) - atx1 := newActivationTxV2( - withMarriageATX(marriageAtx.ID()), - withPublishEpoch(marriageAtx.PublishEpoch+1), + atx1 := NewTestActivationTxV2( + WithMarriageATX(marriageAtx.ID()), + WithPublishEpoch(marriageAtx.PublishEpoch+1), ) atx1.Sign(sig) - atx2 := newActivationTxV2( - withMarriageATX(marriageAtx.ID()), - withPublishEpoch(marriageAtx.PublishEpoch+1), + atx2 := NewTestActivationTxV2( + WithMarriageATX(marriageAtx.ID()), + WithPublishEpoch(marriageAtx.PublishEpoch+1), ) atx2.Sign(otherSig) @@ -243,20 +243,20 @@ func Test_DoubleMergeProof(t *testing.T) { proof.MarriageATXSmesherID = smesherID // invalid ATX1 ID - id1 := proof.ATXID1 - proof.ATXID1 = types.RandomATXID() + id1 := proof.ATX1 + proof.ATX1 = types.RandomATXID() id, err = proof.Valid(context.Background(), verifier) require.EqualError(t, err, "ATX 1 invalid signature") require.Equal(t, types.EmptyNodeID, id) - proof.ATXID1 = id1 + proof.ATX1 = id1 // invalid ATX2 ID - id2 := proof.ATXID2 - proof.ATXID2 = types.RandomATXID() + id2 := proof.ATX2 + proof.ATX2 = types.RandomATXID() id, err = proof.Valid(context.Background(), verifier) require.EqualError(t, err, "ATX 2 invalid signature") require.Equal(t, types.EmptyNodeID, id) - proof.ATXID2 = id2 + proof.ATX2 = id2 // invalid ATX1 smesher ID smesherID1 := proof.SmesherID1 diff --git a/activation/wire/malfeasance_invalid_post.go b/activation/wire/malfeasance_invalid_post.go index cb380ba88d..81d2db2bde 100644 --- a/activation/wire/malfeasance_invalid_post.go +++ b/activation/wire/malfeasance_invalid_post.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "slices" + "strconv" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/signing" @@ -21,8 +22,8 @@ import ( // 3. The commitment ATX of NodeID used for the invalid PoST based on their initial ATX. // 4. The provided Post is invalid for the given NodeID. type ProofInvalidPost struct { - // ATXID is the ID of the ATX containing the invalid PoST. - ATXID types.ATXID + // ATX is the ID of the ATX containing the invalid PoST. + ATX types.ATXID // SmesherID is the ID of the smesher that published the ATX. SmesherID types.NodeID // Signature is the signature of the ATXID by the smesher. @@ -38,6 +39,23 @@ type ProofInvalidPost struct { InvalidPostProof InvalidPostProof } +func (p ProofInvalidPost) String() string { + return "InvalidPoSTProof" +} + +func (p ProofInvalidPost) Type() ProofType { + return InvalidPost +} + +func (p ProofInvalidPost) Info() map[string]string { + return map[string]string{ + "atx": p.ATX.String(), + "index": strconv.FormatUint(uint64(p.InvalidPostProof.InvalidPostIndex), 10), + "post_node_id": p.NodeID.String(), + "smesher_id": p.SmesherID.String(), + } +} + var _ Proof = &ProofInvalidPost{} func NewInvalidPostProof( @@ -86,7 +104,7 @@ func NewInvalidPostProof( } return &ProofInvalidPost{ - ATXID: atx.ID(), + ATX: atx.ID(), SmesherID: atx.SmesherID, Signature: atx.Signature, @@ -99,7 +117,7 @@ func NewInvalidPostProof( } func (p ProofInvalidPost) Valid(ctx context.Context, malValidator MalfeasanceValidator) (types.NodeID, error) { - if !malValidator.Signature(signing.ATX, p.SmesherID, p.ATXID.Bytes(), p.Signature) { + if !malValidator.Signature(signing.ATX, p.SmesherID, p.ATX.Bytes(), p.Signature) { return types.EmptyNodeID, errors.New("invalid signature") } @@ -109,7 +127,7 @@ func (p ProofInvalidPost) Valid(ctx context.Context, malValidator MalfeasanceVal var marriageIndex *uint32 if p.MarriageProof != nil { - if err := p.MarriageProof.Valid(malValidator, p.ATXID, p.NodeID, p.SmesherID); err != nil { + if err := p.MarriageProof.Valid(malValidator, p.ATX, p.NodeID, p.SmesherID); err != nil { return types.EmptyNodeID, fmt.Errorf("invalid marriage proof: %w", err) } marriageIndex = &p.MarriageProof.NodeIDMarryProof.CertificateIndex @@ -118,7 +136,7 @@ func (p ProofInvalidPost) Valid(ctx context.Context, malValidator MalfeasanceVal if err := p.InvalidPostProof.Valid( ctx, malValidator, - p.ATXID, + p.ATX, p.NodeID, marriageIndex, ); err != nil { diff --git a/activation/wire/malfeasance_invalid_post_scale.go b/activation/wire/malfeasance_invalid_post_scale.go index ffcef75f16..0a2388b80c 100644 --- a/activation/wire/malfeasance_invalid_post_scale.go +++ b/activation/wire/malfeasance_invalid_post_scale.go @@ -10,7 +10,7 @@ import ( func (t *ProofInvalidPost) EncodeScale(enc *scale.Encoder) (total int, err error) { { - n, err := scale.EncodeByteArray(enc, t.ATXID[:]) + n, err := scale.EncodeByteArray(enc, t.ATX[:]) if err != nil { return total, err } @@ -56,7 +56,7 @@ func (t *ProofInvalidPost) EncodeScale(enc *scale.Encoder) (total int, err error func (t *ProofInvalidPost) DecodeScale(dec *scale.Decoder) (total int, err error) { { - n, err := scale.DecodeByteArray(dec, t.ATXID[:]) + n, err := scale.DecodeByteArray(dec, t.ATX[:]) if err != nil { return total, err } diff --git a/activation/wire/malfeasance_invalid_post_test.go b/activation/wire/malfeasance_invalid_post_test.go index bed03b25eb..27a6a2d4ba 100644 --- a/activation/wire/malfeasance_invalid_post_test.go +++ b/activation/wire/malfeasance_invalid_post_test.go @@ -42,10 +42,10 @@ func Test_InvalidPostProof(t *testing.T) { post PostV1, numUnits uint32, ) *ActivationTxV2 { - atx := newActivationTxV2( - withNIPost( - withNIPostChallenge(nipostChallenge), - withNIPostSubPost(SubPostV2{ + atx := NewTestActivationTxV2( + WithNIPost( + WithNIPostChallenge(nipostChallenge), + WithNIPostSubPost(SubPostV2{ Post: post, NumUnits: numUnits, }), @@ -61,8 +61,8 @@ func Test_InvalidPostProof(t *testing.T) { post PostV1, numUnits uint32, ) *ActivationTxV2 { - wInitialAtx := newActivationTxV2( - withInitial(types.RandomATXID(), PostV1{}), + wInitialAtx := NewTestActivationTxV2( + WithInitial(types.RandomATXID(), PostV1{}), ) wInitialAtx.Sign(sig) initialAtx := &types.ActivationTx{ @@ -72,8 +72,8 @@ func Test_InvalidPostProof(t *testing.T) { initialAtx.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, initialAtx, wInitialAtx.Blob())) - wPubInitialAtx := newActivationTxV2( - withInitial(types.RandomATXID(), PostV1{}), + wPubInitialAtx := NewTestActivationTxV2( + WithInitial(types.RandomATXID(), PostV1{}), ) wPubInitialAtx.Sign(pubSig) pubInitialAtx := &types.ActivationTx{} @@ -83,10 +83,10 @@ func Test_InvalidPostProof(t *testing.T) { marryInitialAtx := types.RandomATXID() - wMarriageAtx := newActivationTxV2( - withMarriageCertificate(marrySig, types.EmptyATXID, marrySig.NodeID()), - withMarriageCertificate(sig, wInitialAtx.ID(), marrySig.NodeID()), - withMarriageCertificate(pubSig, wPubInitialAtx.ID(), marrySig.NodeID()), + wMarriageAtx := NewTestActivationTxV2( + WithMarriageCertificate(marrySig, types.EmptyATXID, marrySig.NodeID()), + WithMarriageCertificate(sig, wInitialAtx.ID(), marrySig.NodeID()), + WithMarriageCertificate(pubSig, wPubInitialAtx.ID(), marrySig.NodeID()), ) wMarriageAtx.Sign(marrySig) @@ -95,24 +95,24 @@ func Test_InvalidPostProof(t *testing.T) { marriageAtx.SmesherID = marrySig.NodeID() require.NoError(t, atxs.Add(db, marriageAtx, wMarriageAtx.Blob())) - atx := newActivationTxV2( - withPreviousATXs(marryInitialAtx, wInitialAtx.ID(), wPubInitialAtx.ID()), - withMarriageATX(wMarriageAtx.ID()), - withNIPost( - withNIPostChallenge(nipostChallenge), - withNIPostMembershipProof(MerkleProofV2{}), - withNIPostSubPost(SubPostV2{ + atx := NewTestActivationTxV2( + WithPreviousATXs(marryInitialAtx, wInitialAtx.ID(), wPubInitialAtx.ID()), + WithMarriageATX(wMarriageAtx.ID()), + WithNIPost( + WithNIPostChallenge(nipostChallenge), + WithNIPostMembershipProof(MerkleProofV2{}), + WithNIPostSubPost(SubPostV2{ MarriageIndex: 0, PrevATXIndex: 0, Post: PostV1{}, }), - withNIPostSubPost(SubPostV2{ + WithNIPostSubPost(SubPostV2{ MarriageIndex: 1, PrevATXIndex: 1, Post: post, NumUnits: numUnits, }), - withNIPostSubPost(SubPostV2{ + WithNIPostSubPost(SubPostV2{ MarriageIndex: 2, PrevATXIndex: 2, Post: PostV1{}, @@ -434,11 +434,11 @@ func Test_InvalidPostProof(t *testing.T) { }).AnyTimes() // invalid ATXID - proof.ATXID = types.RandomATXID() + proof.ATX = types.RandomATXID() id, err := proof.Valid(context.Background(), verifier) require.EqualError(t, err, "invalid signature") require.Equal(t, types.EmptyNodeID, id) - proof.ATXID = atx.ID() + proof.ATX = atx.ID() // invalid smesher ID proof.SmesherID = types.RandomNodeID() @@ -618,11 +618,11 @@ func Test_InvalidPostProof(t *testing.T) { }).AnyTimes() // invalid ATXID - proof.ATXID = types.RandomATXID() + proof.ATX = types.RandomATXID() id, err := proof.Valid(context.Background(), verifier) require.EqualError(t, err, "invalid signature") require.Equal(t, types.EmptyNodeID, id) - proof.ATXID = atx.ID() + proof.ATX = atx.ID() // invalid smesher ID proof.SmesherID = types.RandomNodeID() diff --git a/activation/wire/malfeasance_invalid_prev_atx.go b/activation/wire/malfeasance_invalid_prev_atx.go index abb5acb5f6..fb5645e6d8 100644 --- a/activation/wire/malfeasance_invalid_prev_atx.go +++ b/activation/wire/malfeasance_invalid_prev_atx.go @@ -32,6 +32,25 @@ type ProofInvalidPrevAtxV2 struct { Proofs [2]InvalidPrevAtxProof } +func (p ProofInvalidPrevAtxV2) String() string { + return "InvalidPreviousATXProofV2" +} + +func (p ProofInvalidPrevAtxV2) Type() ProofType { + return InvalidPreviousV2 +} + +func (p ProofInvalidPrevAtxV2) Info() map[string]string { + return map[string]string{ + "prev_atx": p.PrevATX.String(), + "node_id": p.NodeID.String(), + "atx1": p.Proofs[0].ATXID.String(), + "smesher_id1": p.Proofs[0].SmesherID.String(), + "atx2": p.Proofs[1].ATXID.String(), + "smesher_id2": p.Proofs[1].SmesherID.String(), + } +} + var _ Proof = &ProofInvalidPrevAtxV2{} func NewInvalidPrevAtxProofV2( @@ -190,6 +209,25 @@ type ProofInvalidPrevAtxV1 struct { ATXv1 ActivationTxV1 } +func (p ProofInvalidPrevAtxV1) String() string { + return "InvalidPreviousATXProofV1" +} + +func (p ProofInvalidPrevAtxV1) Type() ProofType { + return InvalidPreviousV1 +} + +func (p ProofInvalidPrevAtxV1) Info() map[string]string { + return map[string]string{ + "prev_atx": p.PrevATX.String(), + "node_id": p.NodeID.String(), + "atx1": p.Proof.ATXID.String(), + "smesher_id1": p.Proof.SmesherID.String(), + "atx2": p.ATXv1.ID().String(), + "smesher_id2": p.ATXv1.SmesherID.String(), + } +} + var _ Proof = &ProofInvalidPrevAtxV1{} func NewInvalidPrevAtxProofV1( diff --git a/activation/wire/malfeasance_invalid_prev_atx_test.go b/activation/wire/malfeasance_invalid_prev_atx_test.go index 1c829e74eb..3047706ed0 100644 --- a/activation/wire/malfeasance_invalid_prev_atx_test.go +++ b/activation/wire/malfeasance_invalid_prev_atx_test.go @@ -37,8 +37,8 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { db sql.Executor, prevATX types.ATXID, ) *ActivationTxV2 { - wInitialAtx := newActivationTxV2( - withInitial(types.RandomATXID(), PostV1{}), + wInitialAtx := NewTestActivationTxV2( + WithInitial(types.RandomATXID(), PostV1{}), ) wInitialAtx.Sign(sig) initialAtx := &types.ActivationTx{ @@ -48,8 +48,8 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { initialAtx.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, initialAtx, wInitialAtx.Blob())) - wPubInitialAtx := newActivationTxV2( - withInitial(types.RandomATXID(), PostV1{}), + wPubInitialAtx := NewTestActivationTxV2( + WithInitial(types.RandomATXID(), PostV1{}), ) wPubInitialAtx.Sign(pubSig) pubInitialAtx := &types.ActivationTx{} @@ -59,10 +59,10 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { marryInitialAtx := types.RandomATXID() - wMarriageAtx := newActivationTxV2( - withMarriageCertificate(marrySig, types.EmptyATXID, marrySig.NodeID()), - withMarriageCertificate(sig, wInitialAtx.ID(), marrySig.NodeID()), - withMarriageCertificate(pubSig, wPubInitialAtx.ID(), marrySig.NodeID()), + wMarriageAtx := NewTestActivationTxV2( + WithMarriageCertificate(marrySig, types.EmptyATXID, marrySig.NodeID()), + WithMarriageCertificate(sig, wInitialAtx.ID(), marrySig.NodeID()), + WithMarriageCertificate(pubSig, wPubInitialAtx.ID(), marrySig.NodeID()), ) wMarriageAtx.Sign(marrySig) @@ -71,20 +71,20 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { marriageAtx.SmesherID = marrySig.NodeID() require.NoError(t, atxs.Add(db, marriageAtx, wMarriageAtx.Blob())) - atx := newActivationTxV2( - withPreviousATXs(marryInitialAtx, wPubInitialAtx.ID(), prevATX), - withMarriageATX(wMarriageAtx.ID()), - withNIPost( - withNIPostMembershipProof(MerkleProofV2{}), - withNIPostSubPost(SubPostV2{ + atx := NewTestActivationTxV2( + WithPreviousATXs(marryInitialAtx, wPubInitialAtx.ID(), prevATX), + WithMarriageATX(wMarriageAtx.ID()), + WithNIPost( + WithNIPostMembershipProof(MerkleProofV2{}), + WithNIPostSubPost(SubPostV2{ MarriageIndex: 0, PrevATXIndex: 0, }), - withNIPostSubPost(SubPostV2{ + WithNIPostSubPost(SubPostV2{ MarriageIndex: 1, PrevATXIndex: 2, }), - withNIPostSubPost(SubPostV2{ + WithNIPostSubPost(SubPostV2{ MarriageIndex: 2, PrevATXIndex: 1, }), @@ -99,14 +99,14 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { db := statesql.InMemoryTest(t) prevATXID := types.RandomATXID() - atx1 := newActivationTxV2( - withPreviousATXs(prevATXID), - withPublishEpoch(5), + atx1 := NewTestActivationTxV2( + WithPreviousATXs(prevATXID), + WithPublishEpoch(5), ) atx1.Sign(sig) - atx2 := newActivationTxV2( - withPreviousATXs(prevATXID), - withPublishEpoch(7), + atx2 := NewTestActivationTxV2( + WithPreviousATXs(prevATXID), + WithPublishEpoch(7), ) atx2.Sign(sig) @@ -135,9 +135,9 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { prevAtx.SetID(prevATXID) prevAtx.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, prevAtx, types.AtxBlob{})) - atx1 := newActivationTxV2( - withPreviousATXs(prevATXID), - withPublishEpoch(5), + atx1 := NewTestActivationTxV2( + WithPreviousATXs(prevATXID), + WithPublishEpoch(5), ) atx1.Sign(sig) atx2 := newMergedATXv2(db, prevATXID) @@ -164,8 +164,8 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { t.Parallel() db := statesql.InMemoryTest(t) - atx1 := newActivationTxV2( - withPreviousATXs(types.RandomATXID()), + atx1 := NewTestActivationTxV2( + WithPreviousATXs(types.RandomATXID()), ) atx1.Sign(sig) @@ -179,12 +179,12 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { db := statesql.InMemoryTest(t) prevATX := types.RandomATXID() - atx1 := newActivationTxV2( - withPreviousATXs(prevATX), + atx1 := NewTestActivationTxV2( + WithPreviousATXs(prevATX), ) atx1.Sign(sig) - atx2 := newActivationTxV2( - withPreviousATXs(prevATX), + atx2 := NewTestActivationTxV2( + WithPreviousATXs(prevATX), ) atx2.Sign(pubSig) @@ -209,9 +209,9 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { prevAtx.SetID(prevATXID) prevAtx.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, prevAtx, types.AtxBlob{})) - atx1 := newActivationTxV2( - withPreviousATXs(prevATXID), - withPublishEpoch(5), + atx1 := NewTestActivationTxV2( + WithPreviousATXs(prevATXID), + WithPublishEpoch(5), ) atx1.Sign(invalidSig) atx2 := newMergedATXv2(db, prevATXID) @@ -238,9 +238,9 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { prevAtx.SetID(prevATXID) prevAtx.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, prevAtx, types.AtxBlob{})) - atx1 := newActivationTxV2( - withPreviousATXs(prevATXID), - withPublishEpoch(5), + atx1 := NewTestActivationTxV2( + WithPreviousATXs(prevATXID), + WithPublishEpoch(5), ) atx1.Sign(sig) atx2 := newMergedATXv2(db, prevATXID) @@ -267,14 +267,14 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { t.Parallel() db := statesql.InMemoryTest(t) - atx1 := newActivationTxV2( - withPreviousATXs(types.RandomATXID()), - withPublishEpoch(5), + atx1 := NewTestActivationTxV2( + WithPreviousATXs(types.RandomATXID()), + WithPublishEpoch(5), ) atx1.Sign(sig) - atx2 := newActivationTxV2( - withPreviousATXs(types.RandomATXID()), - withPublishEpoch(7), + atx2 := NewTestActivationTxV2( + WithPreviousATXs(types.RandomATXID()), + WithPublishEpoch(7), ) atx2.Sign(sig) @@ -288,14 +288,14 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { db := statesql.InMemoryTest(t) prevATXID := types.RandomATXID() - atx1 := newActivationTxV2( - withPreviousATXs(prevATXID), - withPublishEpoch(5), + atx1 := NewTestActivationTxV2( + WithPreviousATXs(prevATXID), + WithPublishEpoch(5), ) atx1.Sign(sig) - atx2 := newActivationTxV2( - withPreviousATXs(prevATXID), - withPublishEpoch(7), + atx2 := NewTestActivationTxV2( + WithPreviousATXs(prevATXID), + WithPublishEpoch(7), ) atx2.Sign(sig) @@ -540,9 +540,9 @@ func Test_InvalidPrevAtxProofV2(t *testing.T) { prevAtx.SetID(prevATXID) prevAtx.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, prevAtx, types.AtxBlob{})) - atx1 := newActivationTxV2( - withPreviousATXs(prevATXID), - withPublishEpoch(5), + atx1 := NewTestActivationTxV2( + WithPreviousATXs(prevATXID), + WithPublishEpoch(5), ) atx1.Sign(sig) atx2 := newMergedATXv2(db, prevATXID) @@ -657,8 +657,8 @@ func Test_InvalidPrevAtxProofV1(t *testing.T) { db sql.Executor, prevATX types.ATXID, ) *ActivationTxV2 { - wInitialAtx := newActivationTxV2( - withInitial(types.RandomATXID(), PostV1{}), + wInitialAtx := NewTestActivationTxV2( + WithInitial(types.RandomATXID(), PostV1{}), ) wInitialAtx.Sign(sig) initialAtx := &types.ActivationTx{ @@ -668,8 +668,8 @@ func Test_InvalidPrevAtxProofV1(t *testing.T) { initialAtx.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, initialAtx, wInitialAtx.Blob())) - wPubInitialAtx := newActivationTxV2( - withInitial(types.RandomATXID(), PostV1{}), + wPubInitialAtx := NewTestActivationTxV2( + WithInitial(types.RandomATXID(), PostV1{}), ) wPubInitialAtx.Sign(pubSig) pubInitialAtx := &types.ActivationTx{} @@ -679,10 +679,10 @@ func Test_InvalidPrevAtxProofV1(t *testing.T) { marryInitialAtx := types.RandomATXID() - wMarriageAtx := newActivationTxV2( - withMarriageCertificate(marrySig, types.EmptyATXID, marrySig.NodeID()), - withMarriageCertificate(sig, wInitialAtx.ID(), marrySig.NodeID()), - withMarriageCertificate(pubSig, wPubInitialAtx.ID(), marrySig.NodeID()), + wMarriageAtx := NewTestActivationTxV2( + WithMarriageCertificate(marrySig, types.EmptyATXID, marrySig.NodeID()), + WithMarriageCertificate(sig, wInitialAtx.ID(), marrySig.NodeID()), + WithMarriageCertificate(pubSig, wPubInitialAtx.ID(), marrySig.NodeID()), ) wMarriageAtx.Sign(marrySig) @@ -691,20 +691,20 @@ func Test_InvalidPrevAtxProofV1(t *testing.T) { marriageAtx.SmesherID = marrySig.NodeID() require.NoError(t, atxs.Add(db, marriageAtx, wMarriageAtx.Blob())) - atx := newActivationTxV2( - withPreviousATXs(marryInitialAtx, wPubInitialAtx.ID(), prevATX), - withMarriageATX(wMarriageAtx.ID()), - withNIPost( - withNIPostMembershipProof(MerkleProofV2{}), - withNIPostSubPost(SubPostV2{ + atx := NewTestActivationTxV2( + WithPreviousATXs(marryInitialAtx, wPubInitialAtx.ID(), prevATX), + WithMarriageATX(wMarriageAtx.ID()), + WithNIPost( + WithNIPostMembershipProof(MerkleProofV2{}), + WithNIPostSubPost(SubPostV2{ MarriageIndex: 0, PrevATXIndex: 0, }), - withNIPostSubPost(SubPostV2{ + WithNIPostSubPost(SubPostV2{ MarriageIndex: 1, PrevATXIndex: 2, }), - withNIPostSubPost(SubPostV2{ + WithNIPostSubPost(SubPostV2{ MarriageIndex: 2, PrevATXIndex: 1, }), @@ -730,9 +730,9 @@ func Test_InvalidPrevAtxProofV1(t *testing.T) { } atxv1.Sign(sig) - atxv2 := newActivationTxV2( - withPreviousATXs(prevATX), - withPublishEpoch(7), + atxv2 := NewTestActivationTxV2( + WithPreviousATXs(prevATX), + WithPublishEpoch(7), ) atxv2.Sign(sig) @@ -802,9 +802,9 @@ func Test_InvalidPrevAtxProofV1(t *testing.T) { } atxv1.Sign(sig) - atxv2 := newActivationTxV2( - withPreviousATXs(prevATX), - withPublishEpoch(7), + atxv2 := NewTestActivationTxV2( + WithPreviousATXs(prevATX), + WithPublishEpoch(7), ) atxv2.Sign(pubSig) @@ -891,9 +891,9 @@ func Test_InvalidPrevAtxProofV1(t *testing.T) { } atxv1.Sign(sig) - atxv2 := newActivationTxV2( - withPreviousATXs(types.RandomATXID()), - withPublishEpoch(7), + atxv2 := NewTestActivationTxV2( + WithPreviousATXs(types.RandomATXID()), + WithPublishEpoch(7), ) atxv2.Sign(sig) @@ -918,9 +918,9 @@ func Test_InvalidPrevAtxProofV1(t *testing.T) { } atxv1.Sign(sig) - atxv2 := newActivationTxV2( - withPreviousATXs(prevATX), - withPublishEpoch(7), + atxv2 := NewTestActivationTxV2( + WithPreviousATXs(prevATX), + WithPublishEpoch(7), ) atxv2.Sign(sig) diff --git a/activation/wire/malfeasance_shared_test.go b/activation/wire/malfeasance_shared_test.go index 46fbccea11..b50181b1d5 100644 --- a/activation/wire/malfeasance_shared_test.go +++ b/activation/wire/malfeasance_shared_test.go @@ -34,9 +34,9 @@ func Test_MarryProof(t *testing.T) { otherAtx.SmesherID = otherSig.NodeID() require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) - atx1 := newActivationTxV2( - withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), - withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + atx1 := NewTestActivationTxV2( + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) atx1.Sign(sig) @@ -73,9 +73,9 @@ func Test_MarryProof(t *testing.T) { otherAtx.SmesherID = otherSig.NodeID() require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) - atx1 := newActivationTxV2( - withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), - withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + atx1 := NewTestActivationTxV2( + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) atx1.Sign(sig) @@ -96,9 +96,9 @@ func Test_MarryProof(t *testing.T) { otherAtx.SmesherID = otherSig.NodeID() require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) - atx1 := newActivationTxV2( - withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), - withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + atx1 := NewTestActivationTxV2( + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) atx1.Sign(sig) @@ -183,9 +183,9 @@ func Test_MarriageProof(t *testing.T) { otherAtx.SmesherID = otherSig.NodeID() require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) - wMarriageAtx := newActivationTxV2( - withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), - withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + wMarriageAtx := NewTestActivationTxV2( + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) wMarriageAtx.Sign(sig) marriageAtx := &types.ActivationTx{} @@ -193,8 +193,8 @@ func Test_MarriageProof(t *testing.T) { marriageAtx.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, marriageAtx, wMarriageAtx.Blob())) - atx := newActivationTxV2( - withMarriageATX(wMarriageAtx.ID()), + atx := NewTestActivationTxV2( + WithMarriageATX(wMarriageAtx.ID()), ) atx.Sign(sig) @@ -222,9 +222,9 @@ func Test_MarriageProof(t *testing.T) { otherAtx.SmesherID = otherSig.NodeID() require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) - wMarriageAtx := newActivationTxV2( - withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), - withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + wMarriageAtx := NewTestActivationTxV2( + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) wMarriageAtx.Sign(sig) marriageAtx := &types.ActivationTx{} @@ -232,8 +232,8 @@ func Test_MarriageProof(t *testing.T) { marriageAtx.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, marriageAtx, wMarriageAtx.Blob())) - atx := newActivationTxV2( - withMarriageATX(wMarriageAtx.ID()), + atx := NewTestActivationTxV2( + WithMarriageATX(wMarriageAtx.ID()), ) atx.Sign(sig) @@ -247,8 +247,8 @@ func Test_MarriageProof(t *testing.T) { db := statesql.InMemoryTest(t) - atx := newActivationTxV2( - withMarriageATX(types.RandomATXID()), + atx := NewTestActivationTxV2( + WithMarriageATX(types.RandomATXID()), ) atx.Sign(sig) @@ -266,9 +266,9 @@ func Test_MarriageProof(t *testing.T) { otherAtx.SmesherID = otherSig.NodeID() require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) - wMarriageAtx := newActivationTxV2( - withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), - withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + wMarriageAtx := NewTestActivationTxV2( + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) wMarriageAtx.Sign(sig) marriageAtx := &types.ActivationTx{} @@ -276,8 +276,8 @@ func Test_MarriageProof(t *testing.T) { marriageAtx.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, marriageAtx, wMarriageAtx.Blob())) - atx := newActivationTxV2( - withMarriageATX(wMarriageAtx.ID()), + atx := NewTestActivationTxV2( + WithMarriageATX(wMarriageAtx.ID()), ) atx.Sign(sig) @@ -307,9 +307,9 @@ func Test_MarriageProof(t *testing.T) { otherAtx.SmesherID = otherSig.NodeID() require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) - wMarriageAtx := newActivationTxV2( - withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), - withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + wMarriageAtx := NewTestActivationTxV2( + WithMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + WithMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), ) wMarriageAtx.Sign(sig) marriageAtx := &types.ActivationTx{} @@ -317,8 +317,8 @@ func Test_MarriageProof(t *testing.T) { marriageAtx.SmesherID = sig.NodeID() require.NoError(t, atxs.Add(db, marriageAtx, wMarriageAtx.Blob())) - atx := newActivationTxV2( - withMarriageATX(wMarriageAtx.ID()), + atx := NewTestActivationTxV2( + WithMarriageATX(wMarriageAtx.ID()), ) atx.Sign(sig) diff --git a/activation/wire/malfeasance_test.go b/activation/wire/malfeasance_test.go new file mode 100644 index 0000000000..2cd41288a4 --- /dev/null +++ b/activation/wire/malfeasance_test.go @@ -0,0 +1,89 @@ +package wire + +import ( + "testing" + + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/codec" +) + +func fuzzDecoding[T Proof](t *testing.T, data []byte, proof T) { + fuzzer := fuzz.NewFromGoFuzz(data) + fuzzer.Fuzz(proof) + + atxProof := &ATXProof{ + Version: 0x01, + ProofType: proof.Type(), + + Proof: codec.MustEncode(proof), + } + + encodedAtxProof := codec.MustEncode(atxProof) + decodedAtxProof := &ATXProof{} + codec.MustDecode(encodedAtxProof, decodedAtxProof) + + decodedProof, err := decodedAtxProof.Decode() + require.NoError(t, err) + + require.Equal(t, proof, decodedProof.(T)) +} + +func FuzzATXProofDecodeDoubleMarry(f *testing.F) { + f.Add([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}) + f.Fuzz(func(t *testing.T, data []byte) { + fuzzDecoding(t, data, &ProofDoubleMarry{}) + }) +} + +func FuzzATXProofDecodeDoubleMerge(f *testing.F) { + f.Add([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}) + f.Fuzz(func(t *testing.T, data []byte) { + fuzzDecoding(t, data, &ProofDoubleMerge{}) + }) +} + +func FuzzATXProofDecodeInvalidPost(f *testing.F) { + f.Add([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}) + f.Fuzz(func(t *testing.T, data []byte) { + fuzzDecoding(t, data, &ProofInvalidPost{}) + }) +} + +func FuzzATXProofDecodeInvalidPrevAtxV1(f *testing.F) { + f.Add([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}) + f.Fuzz(func(t *testing.T, data []byte) { + fuzzDecoding(t, data, &ProofInvalidPrevAtxV1{}) + }) +} + +func FuzzATXProofDecodeInvalidPrevAtxV2(f *testing.F) { + f.Add([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}) + f.Fuzz(func(t *testing.T, data []byte) { + fuzzDecoding(t, data, &ProofInvalidPrevAtxV2{}) + }) +} + +func TestDecode(t *testing.T) { + t.Run("unknown proof type", func(t *testing.T) { + atxProof := &ATXProof{ + Version: 0x01, + ProofType: 0x42, // unknown proof type + } + + _, err := atxProof.Decode() + require.ErrorContains(t, err, "unknown ATX malfeasance proof type") + }) + + t.Run("atx proof fails decoding", func(t *testing.T) { + atxProof := &ATXProof{ + Version: 0x01, + ProofType: DoubleMarry, + Proof: []byte{}, // invalid proof + } + + _, err := atxProof.Decode() + require.ErrorContains(t, err, "decoding ATX malfeasance proof of type 0x11") + }) +} diff --git a/activation/wire/mocks.go b/activation/wire/mocks.go index ae0fd1be61..a6f97f8087 100644 --- a/activation/wire/mocks.go +++ b/activation/wire/mocks.go @@ -13,6 +13,7 @@ import ( context "context" reflect "reflect" + scale "github.com/spacemeshos/go-scale" types "github.com/spacemeshos/go-spacemesh/common/types" signing "github.com/spacemeshos/go-spacemesh/signing" gomock "go.uber.org/mock/gomock" @@ -117,3 +118,258 @@ func (c *MockMalfeasanceValidatorSignatureCall) DoAndReturn(f func(signing.Domai c.Call = c.Call.DoAndReturn(f) return c } + +// MockProof is a mock of Proof interface. +type MockProof struct { + ctrl *gomock.Controller + recorder *MockProofMockRecorder + isgomock struct{} +} + +// MockProofMockRecorder is the mock recorder for MockProof. +type MockProofMockRecorder struct { + mock *MockProof +} + +// NewMockProof creates a new mock instance. +func NewMockProof(ctrl *gomock.Controller) *MockProof { + mock := &MockProof{ctrl: ctrl} + mock.recorder = &MockProofMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProof) EXPECT() *MockProofMockRecorder { + return m.recorder +} + +// DecodeScale mocks base method. +func (m *MockProof) DecodeScale(dec *scale.Decoder) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DecodeScale", dec) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DecodeScale indicates an expected call of DecodeScale. +func (mr *MockProofMockRecorder) DecodeScale(dec any) *MockProofDecodeScaleCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeScale", reflect.TypeOf((*MockProof)(nil).DecodeScale), dec) + return &MockProofDecodeScaleCall{Call: call} +} + +// MockProofDecodeScaleCall wrap *gomock.Call +type MockProofDecodeScaleCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockProofDecodeScaleCall) Return(arg0 int, arg1 error) *MockProofDecodeScaleCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockProofDecodeScaleCall) Do(f func(*scale.Decoder) (int, error)) *MockProofDecodeScaleCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockProofDecodeScaleCall) DoAndReturn(f func(*scale.Decoder) (int, error)) *MockProofDecodeScaleCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// EncodeScale mocks base method. +func (m *MockProof) EncodeScale(enc *scale.Encoder) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EncodeScale", enc) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EncodeScale indicates an expected call of EncodeScale. +func (mr *MockProofMockRecorder) EncodeScale(enc any) *MockProofEncodeScaleCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EncodeScale", reflect.TypeOf((*MockProof)(nil).EncodeScale), enc) + return &MockProofEncodeScaleCall{Call: call} +} + +// MockProofEncodeScaleCall wrap *gomock.Call +type MockProofEncodeScaleCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockProofEncodeScaleCall) Return(arg0 int, arg1 error) *MockProofEncodeScaleCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockProofEncodeScaleCall) Do(f func(*scale.Encoder) (int, error)) *MockProofEncodeScaleCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockProofEncodeScaleCall) DoAndReturn(f func(*scale.Encoder) (int, error)) *MockProofEncodeScaleCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Info mocks base method. +func (m *MockProof) Info() map[string]string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Info") + ret0, _ := ret[0].(map[string]string) + return ret0 +} + +// Info indicates an expected call of Info. +func (mr *MockProofMockRecorder) Info() *MockProofInfoCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockProof)(nil).Info)) + return &MockProofInfoCall{Call: call} +} + +// MockProofInfoCall wrap *gomock.Call +type MockProofInfoCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockProofInfoCall) Return(arg0 map[string]string) *MockProofInfoCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockProofInfoCall) Do(f func() map[string]string) *MockProofInfoCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockProofInfoCall) DoAndReturn(f func() map[string]string) *MockProofInfoCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// String mocks base method. +func (m *MockProof) String() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "String") + ret0, _ := ret[0].(string) + return ret0 +} + +// String indicates an expected call of String. +func (mr *MockProofMockRecorder) String() *MockProofStringCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockProof)(nil).String)) + return &MockProofStringCall{Call: call} +} + +// MockProofStringCall wrap *gomock.Call +type MockProofStringCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockProofStringCall) Return(arg0 string) *MockProofStringCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockProofStringCall) Do(f func() string) *MockProofStringCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockProofStringCall) DoAndReturn(f func() string) *MockProofStringCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Type mocks base method. +func (m *MockProof) Type() ProofType { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Type") + ret0, _ := ret[0].(ProofType) + return ret0 +} + +// Type indicates an expected call of Type. +func (mr *MockProofMockRecorder) Type() *MockProofTypeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*MockProof)(nil).Type)) + return &MockProofTypeCall{Call: call} +} + +// MockProofTypeCall wrap *gomock.Call +type MockProofTypeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockProofTypeCall) Return(arg0 ProofType) *MockProofTypeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockProofTypeCall) Do(f func() ProofType) *MockProofTypeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockProofTypeCall) DoAndReturn(f func() ProofType) *MockProofTypeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Valid mocks base method. +func (m *MockProof) Valid(ctx context.Context, malHandler MalfeasanceValidator) (types.NodeID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Valid", ctx, malHandler) + ret0, _ := ret[0].(types.NodeID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Valid indicates an expected call of Valid. +func (mr *MockProofMockRecorder) Valid(ctx, malHandler any) *MockProofValidCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Valid", reflect.TypeOf((*MockProof)(nil).Valid), ctx, malHandler) + return &MockProofValidCall{Call: call} +} + +// MockProofValidCall wrap *gomock.Call +type MockProofValidCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockProofValidCall) Return(arg0 types.NodeID, arg1 error) *MockProofValidCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockProofValidCall) Do(f func(context.Context, MalfeasanceValidator) (types.NodeID, error)) *MockProofValidCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockProofValidCall) DoAndReturn(f func(context.Context, MalfeasanceValidator) (types.NodeID, error)) *MockProofValidCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index 696c4b332d..eb7d222feb 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -465,7 +465,7 @@ type SubPostV2 struct { // Can be used to extract the nodeID and verify if it is married with the smesher of the ATX. // Must be 0 for non-merged ATXs. MarriageIndex uint32 - PrevATXIndex uint32 // Index of the previous ATX in the `InnerActivationTxV2.PreviousATXs` slice + PrevATXIndex uint32 // Index of the previous ATX in the `ActivationTxV2.PreviousATXs` slice // Index of the leaf for this ID's challenge in the poet membership tree. // IDs might shared the same index if their nipost challenges are equal. // This happens when the IDs are continuously merged (they share the previous ATX). @@ -630,6 +630,9 @@ type MarriageCertificate struct { // An ATX of the NodeID that marries. It proves that the NodeID exists. // Note: the reference ATX does not need to be from the previous epoch. // It only needs to prove the existence of the Identity. + // + // 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 diff --git a/activation/wire/wire_v2_helpers.go b/activation/wire/wire_v2_helpers.go new file mode 100644 index 0000000000..e30724d07e --- /dev/null +++ b/activation/wire/wire_v2_helpers.go @@ -0,0 +1,114 @@ +package wire + +import ( + "math/rand/v2" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" +) + +type testAtxV2Opt func(*ActivationTxV2) + +func WithMarriageCertificate(sig *signing.EdSigner, refAtx types.ATXID, atxPublisher types.NodeID) testAtxV2Opt { + return func(atx *ActivationTxV2) { + certificate := MarriageCertificate{ + ReferenceAtx: refAtx, + Signature: sig.Sign(signing.MARRIAGE, atxPublisher.Bytes()), + } + atx.Marriages = append(atx.Marriages, certificate) + } +} + +func WithMarriageATX(id types.ATXID) testAtxV2Opt { + return func(atx *ActivationTxV2) { + atx.MarriageATX = &id + } +} + +func WithPublishEpoch(epoch types.EpochID) testAtxV2Opt { + return func(atx *ActivationTxV2) { + atx.PublishEpoch = epoch + } +} + +func WithInitial(commitAtx types.ATXID, post PostV1) testAtxV2Opt { + return func(atx *ActivationTxV2) { + atx.Initial = &InitialAtxPartsV2{ + CommitmentATX: commitAtx, + Post: post, + } + } +} + +func WithPreviousATXs(atxs ...types.ATXID) testAtxV2Opt { + return func(atx *ActivationTxV2) { + atx.PreviousATXs = atxs + } +} + +func WithNIPost(opts ...testNIPostV2Opt) testAtxV2Opt { + return func(atx *ActivationTxV2) { + nipost := &NIPostV2{} + for _, opt := range opts { + opt(nipost) + } + atx.NIPosts = append(atx.NIPosts, *nipost) + } +} + +type testNIPostV2Opt func(*NIPostV2) + +func WithNIPostChallenge(challenge types.Hash32) testNIPostV2Opt { + return func(nipost *NIPostV2) { + nipost.Challenge = challenge + } +} + +func WithNIPostMembershipProof(proof MerkleProofV2) testNIPostV2Opt { + return func(nipost *NIPostV2) { + nipost.Membership = proof + } +} + +func WithNIPostSubPost(subPost SubPostV2) testNIPostV2Opt { + return func(nipost *NIPostV2) { + nipost.Posts = append(nipost.Posts, subPost) + } +} + +// NewTestActivationTxV2 creates a new ActivationTxV2 with random values. +// ONLY FOR TESTING. +func NewTestActivationTxV2(opts ...testAtxV2Opt) *ActivationTxV2 { + atx := &ActivationTxV2{ + PublishEpoch: rand.N(types.EpochID(255)), + PositioningATX: types.RandomATXID(), + } + for _, opt := range opts { + opt(atx) + } + if atx.PreviousATXs == nil { + atx.PreviousATXs = make([]types.ATXID, 1+rand.IntN(255)) + } + if atx.NIPosts == nil { + atx.NIPosts = []NIPostV2{ + { + Membership: MerkleProofV2{ + Nodes: make([]types.Hash32, 32), + }, + Challenge: types.RandomHash(), + Posts: []SubPostV2{ + { + MarriageIndex: rand.Uint32N(256), + PrevATXIndex: 0, + Post: PostV1{ + Nonce: 0, + Indices: make([]byte, 800), + Pow: 0, + }, + }, + }, + }, + } + } + return atx +} diff --git a/activation/wire/wire_v2_test.go b/activation/wire/wire_v2_test.go index f71a6fba96..329d86ce5c 100644 --- a/activation/wire/wire_v2_test.go +++ b/activation/wire/wire_v2_test.go @@ -2,7 +2,6 @@ package wire import ( "fmt" - "math/rand/v2" "testing" fuzz "github.com/google/gofuzz" @@ -10,113 +9,8 @@ import ( "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" - "github.com/spacemeshos/go-spacemesh/signing" ) -type testAtxV2Opt func(*ActivationTxV2) - -func withMarriageCertificate(sig *signing.EdSigner, refAtx types.ATXID, atxPublisher types.NodeID) testAtxV2Opt { - return func(atx *ActivationTxV2) { - certificate := MarriageCertificate{ - ReferenceAtx: refAtx, - Signature: sig.Sign(signing.MARRIAGE, atxPublisher.Bytes()), - } - atx.Marriages = append(atx.Marriages, certificate) - } -} - -func withMarriageATX(id types.ATXID) testAtxV2Opt { - return func(atx *ActivationTxV2) { - atx.MarriageATX = &id - } -} - -func withPublishEpoch(epoch types.EpochID) testAtxV2Opt { - return func(atx *ActivationTxV2) { - atx.PublishEpoch = epoch - } -} - -func withInitial(commitAtx types.ATXID, post PostV1) testAtxV2Opt { - return func(atx *ActivationTxV2) { - atx.Initial = &InitialAtxPartsV2{ - CommitmentATX: commitAtx, - Post: post, - } - } -} - -func withPreviousATXs(atxs ...types.ATXID) testAtxV2Opt { - return func(atx *ActivationTxV2) { - atx.PreviousATXs = atxs - } -} - -func withNIPost(opts ...testNIPostV2Opt) testAtxV2Opt { - return func(atx *ActivationTxV2) { - nipost := &NIPostV2{} - for _, opt := range opts { - opt(nipost) - } - atx.NIPosts = append(atx.NIPosts, *nipost) - } -} - -type testNIPostV2Opt func(*NIPostV2) - -func withNIPostChallenge(challenge types.Hash32) testNIPostV2Opt { - return func(nipost *NIPostV2) { - nipost.Challenge = challenge - } -} - -func withNIPostMembershipProof(proof MerkleProofV2) testNIPostV2Opt { - return func(nipost *NIPostV2) { - nipost.Membership = proof - } -} - -func withNIPostSubPost(subPost SubPostV2) testNIPostV2Opt { - return func(nipost *NIPostV2) { - nipost.Posts = append(nipost.Posts, subPost) - } -} - -func newActivationTxV2(opts ...testAtxV2Opt) *ActivationTxV2 { - atx := &ActivationTxV2{ - PublishEpoch: rand.N(types.EpochID(255)), - PositioningATX: types.RandomATXID(), - } - for _, opt := range opts { - opt(atx) - } - if atx.PreviousATXs == nil { - atx.PreviousATXs = make([]types.ATXID, 1+rand.IntN(255)) - } - if atx.NIPosts == nil { - atx.NIPosts = []NIPostV2{ - { - Membership: MerkleProofV2{ - Nodes: make([]types.Hash32, 32), - }, - Challenge: types.RandomHash(), - Posts: []SubPostV2{ - { - MarriageIndex: rand.Uint32N(256), - PrevATXIndex: 0, - Post: PostV1{ - Nonce: 0, - Indices: make([]byte, 800), - Pow: 0, - }, - }, - }, - }, - } - } - return atx -} - func Benchmark_ATXv2ID(b *testing.B) { f := fuzz.New() b.ResetTimer() diff --git a/api/grpcserver/activation_service.go b/api/grpcserver/activation_service.go index 4b5bb0c95e..3db73e8625 100644 --- a/api/grpcserver/activation_service.go +++ b/api/grpcserver/activation_service.go @@ -75,7 +75,6 @@ func (s *activationService) Get(ctx context.Context, request *pb.GetRequest) (*p proof, err := s.atxProvider.MalfeasanceProof(atx.SmesherID) if err != nil && !errors.Is(err, sql.ErrNotFound) { ctxzap.Error(ctx, "failed to get malfeasance proof", - zap.Stringer("smesher", atx.SmesherID), zap.Stringer("smesher", atx.SmesherID), zap.Stringer("id", atxId), zap.Error(err), diff --git a/api/grpcserver/debug_service.go b/api/grpcserver/debug_service.go index 264bb9e984..787734920d 100644 --- a/api/grpcserver/debug_service.go +++ b/api/grpcserver/debug_service.go @@ -220,7 +220,7 @@ func castEventProposal(ev *events.EventProposal) *pb.Proposal { for _, el := range ev.Proposal.Ballot.EligibilityProofs { proposal.Eligibilities = append(proposal.Eligibilities, &pb.Eligibility{ J: el.J, - Signature: el.Sig[:], + Signature: el.Sig.Bytes(), }) } return proposal diff --git a/api/grpcserver/mesh_service.go b/api/grpcserver/mesh_service.go index a256cf1f19..d27693d4fa 100644 --- a/api/grpcserver/mesh_service.go +++ b/api/grpcserver/mesh_service.go @@ -614,6 +614,7 @@ func (s *MeshService) MalfeasanceQuery( if err != nil && !errors.Is(err, sql.ErrNotFound) { return nil, status.Error(codes.Internal, err.Error()) } + // TODO(mafa): query malfeasance handler for data instead of extracting from proof bytes return &pb.MalfeasanceResponse{ Proof: events.ToMalfeasancePB(id, proof, req.IncludeProof), }, nil @@ -627,7 +628,7 @@ func (s *MeshService) MalfeasanceStream( if sub == nil { return status.Errorf(codes.FailedPrecondition, "event reporting is not enabled") } - eventch, fullch := consumeEvents[events.EventMalfeasance](stream.Context(), sub) + eventCh, fullCh := consumeEvents[events.EventMalfeasance](stream.Context(), sub) if err := stream.SendHeader(metadata.MD{}); err != nil { return status.Errorf(codes.Unavailable, "can't send header") } @@ -638,6 +639,7 @@ func (s *MeshService) MalfeasanceStream( case <-stream.Context().Done(): return nil default: + // TODO(mafa): query malfeasance handler for data instead of extracting from proof bytes res := &pb.MalfeasanceStreamResponse{ Proof: events.ToMalfeasancePB(id, proof, req.IncludeProof), } @@ -651,9 +653,10 @@ func (s *MeshService) MalfeasanceStream( select { case <-stream.Context().Done(): return nil - case <-fullch: + case <-fullCh: return status.Errorf(codes.Canceled, "buffer is full") - case ev := <-eventch: + case ev := <-eventCh: + // TODO(mafa): query malfeasance handler for data instead of extracting from proof bytes if err := stream.Send(&pb.MalfeasanceStreamResponse{ Proof: events.ToMalfeasancePB(ev.Smesher, ev.Proof, req.IncludeProof), }); err != nil { diff --git a/api/grpcserver/post_service_test.go b/api/grpcserver/post_service_test.go index df6b6b1717..856520dfa9 100644 --- a/api/grpcserver/post_service_test.go +++ b/api/grpcserver/post_service_test.go @@ -64,7 +64,7 @@ func launchPostSupervisor( require.NoError(tb, err) // start post supervisor - builder := activation.NewMockAtxBuilder(ctrl) + builder := activation.NewMockatxBuilder(ctrl) builder.EXPECT().Register(sig) ps := activation.NewPostSupervisor(log, postCfg, provingOpts, mgr, builder) require.NoError(tb, ps.Start(serviceCfg, postOpts, sig)) @@ -108,7 +108,7 @@ func launchPostSupervisorTLS( require.NoError(tb, err) // start post supervisor - builder := activation.NewMockAtxBuilder(ctrl) + builder := activation.NewMockatxBuilder(ctrl) builder.EXPECT().Register(sig) ps := activation.NewPostSupervisor(log, postCfg, provingOpts, mgr, builder) require.NoError(tb, ps.Start(serviceCfg, postOpts, sig)) diff --git a/api/grpcserver/v2alpha1/interface.go b/api/grpcserver/v2alpha1/interface.go index d0a03b528c..377d94937b 100644 --- a/api/grpcserver/v2alpha1/interface.go +++ b/api/grpcserver/v2alpha1/interface.go @@ -1,7 +1,13 @@ package v2alpha1 +import ( + "context" + + "github.com/spacemeshos/go-spacemesh/common/types" +) + //go:generate mockgen -typed -package=v2alpha1 -destination=./mocks.go -source=./interface.go type malfeasanceInfo interface { - Info(data []byte) (map[string]string, error) + Info(ctx context.Context, nodeID types.NodeID) (map[string]string, error) } diff --git a/api/grpcserver/v2alpha1/malfeasance.go b/api/grpcserver/v2alpha1/malfeasance.go index d2f9b5b41d..e2242eba7b 100644 --- a/api/grpcserver/v2alpha1/malfeasance.go +++ b/api/grpcserver/v2alpha1/malfeasance.go @@ -71,8 +71,8 @@ func (s *MalfeasanceService) List( } proofs := make([]*spacemeshv2alpha1.MalfeasanceProof, 0, request.Limit) - if err := identities.IterateOps(s.db, ops, func(id types.NodeID, proof []byte, received time.Time) bool { - rst := toProof(ctx, s.info, id, proof) + if err := identities.IterateOps(s.db, ops, func(id types.NodeID, _ []byte, _ time.Time) bool { + rst := toProof(ctx, s.info, id) if rst == nil { return true } @@ -149,7 +149,7 @@ func (s *MalfeasanceStreamService) Stream( select { // process events first case rst := <-eventsOut: - proof := toProof(stream.Context(), s.info, rst.Smesher, rst.Proof) + proof := toProof(stream.Context(), s.info, rst.Smesher) if proof == nil { continue } @@ -163,7 +163,7 @@ func (s *MalfeasanceStreamService) Stream( default: select { case rst := <-eventsOut: - proof := toProof(stream.Context(), s.info, rst.Smesher, rst.Proof) + proof := toProof(stream.Context(), s.info, rst.Smesher) if proof == nil { continue } @@ -209,22 +209,20 @@ func (s *MalfeasanceStreamService) fetchFromDB( go func() { defer close(dbChan) - if err := identities.IterateOps(s.db, ops, - func(id types.NodeID, proof []byte, received time.Time) bool { - rst := toProof(ctx, s.info, id, proof) - if rst == nil { - return true - } + if err := identities.IterateOps(s.db, ops, func(id types.NodeID, _ []byte, _ time.Time) bool { + rst := toProof(ctx, s.info, id) + if rst == nil { + return true + } - select { - case dbChan <- rst: - return true - case <-ctx.Done(): - // exit if the context is canceled - return false - } - }, - ); err != nil { + select { + case dbChan <- rst: + return true + case <-ctx.Done(): + // exit if the context is canceled + return false + } + }); err != nil { errChan <- status.Error(codes.Internal, err.Error()) } }() @@ -235,9 +233,8 @@ func toProof( ctx context.Context, info malfeasanceInfo, id types.NodeID, - proof []byte, ) *spacemeshv2alpha1.MalfeasanceProof { - properties, err := info.Info(proof) + properties, err := info.Info(ctx, id) if err != nil { ctxzap.Debug(ctx, "failed to get malfeasance info", zap.String("smesher", id.String()), diff --git a/api/grpcserver/v2alpha1/malfeasance_test.go b/api/grpcserver/v2alpha1/malfeasance_test.go index 22744ad648..00a10443cd 100644 --- a/api/grpcserver/v2alpha1/malfeasance_test.go +++ b/api/grpcserver/v2alpha1/malfeasance_test.go @@ -44,7 +44,7 @@ func TestMalfeasanceService_List(t *testing.T) { "type": strconv.FormatUint(uint64(i%4+1), 10), fmt.Sprintf("key%d", i): fmt.Sprintf("value%d", i), } - info.EXPECT().Info(proofs[i].Proof).Return(proofs[i].Properties, nil).AnyTimes() + info.EXPECT().Info(gomock.Any(), proofs[i].ID).Return(proofs[i].Properties, nil).AnyTimes() require.NoError(t, identities.SetMalicious(db, proofs[i].ID, proofs[i].Proof, time.Now())) } @@ -121,7 +121,7 @@ func TestMalfeasanceStreamService_Stream(t *testing.T) { "type": strconv.FormatUint(uint64(i%4+1), 10), fmt.Sprintf("key%d", i): fmt.Sprintf("value%d", i), } - info.EXPECT().Info(proofs[i].Proof).Return(proofs[i].Properties, nil).AnyTimes() + info.EXPECT().Info(gomock.Any(), proofs[i].ID).Return(proofs[i].Properties, nil).AnyTimes() require.NoError(t, identities.SetMalicious(db, proofs[i].ID, proofs[i].Proof, time.Now())) } @@ -182,7 +182,7 @@ func TestMalfeasanceStreamService_Stream(t *testing.T) { "type": strconv.FormatUint(uint64(i%4+1), 10), fmt.Sprintf("key%d", i): fmt.Sprintf("value%d", i), } - info.EXPECT().Info(streamed[i].Proof).Return(properties, nil).AnyTimes() + info.EXPECT().Info(gomock.Any(), streamed[i].Smesher).Return(properties, nil).AnyTimes() } request := &spacemeshv2alpha1.MalfeasanceStreamRequest{ diff --git a/api/grpcserver/v2alpha1/mocks.go b/api/grpcserver/v2alpha1/mocks.go index 6f7f88adf8..0a4529afab 100644 --- a/api/grpcserver/v2alpha1/mocks.go +++ b/api/grpcserver/v2alpha1/mocks.go @@ -10,8 +10,10 @@ package v2alpha1 import ( + context "context" reflect "reflect" + types "github.com/spacemeshos/go-spacemesh/common/types" gomock "go.uber.org/mock/gomock" ) @@ -40,18 +42,18 @@ func (m *MockmalfeasanceInfo) EXPECT() *MockmalfeasanceInfoMockRecorder { } // Info mocks base method. -func (m *MockmalfeasanceInfo) Info(data []byte) (map[string]string, error) { +func (m *MockmalfeasanceInfo) Info(ctx context.Context, nodeID types.NodeID) (map[string]string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Info", data) + ret := m.ctrl.Call(m, "Info", ctx, nodeID) ret0, _ := ret[0].(map[string]string) ret1, _ := ret[1].(error) return ret0, ret1 } // Info indicates an expected call of Info. -func (mr *MockmalfeasanceInfoMockRecorder) Info(data any) *MockmalfeasanceInfoInfoCall { +func (mr *MockmalfeasanceInfoMockRecorder) Info(ctx, nodeID any) *MockmalfeasanceInfoInfoCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockmalfeasanceInfo)(nil).Info), data) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockmalfeasanceInfo)(nil).Info), ctx, nodeID) return &MockmalfeasanceInfoInfoCall{Call: call} } @@ -67,13 +69,13 @@ func (c *MockmalfeasanceInfoInfoCall) Return(arg0 map[string]string, arg1 error) } // Do rewrite *gomock.Call.Do -func (c *MockmalfeasanceInfoInfoCall) Do(f func([]byte) (map[string]string, error)) *MockmalfeasanceInfoInfoCall { +func (c *MockmalfeasanceInfoInfoCall) Do(f func(context.Context, types.NodeID) (map[string]string, error)) *MockmalfeasanceInfoInfoCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockmalfeasanceInfoInfoCall) DoAndReturn(f func([]byte) (map[string]string, error)) *MockmalfeasanceInfoInfoCall { +func (c *MockmalfeasanceInfoInfoCall) DoAndReturn(f func(context.Context, types.NodeID) (map[string]string, error)) *MockmalfeasanceInfoInfoCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/checkpoint/recovery_test.go b/checkpoint/recovery_test.go index 4b22c57369..0f9b4e0751 100644 --- a/checkpoint/recovery_test.go +++ b/checkpoint/recovery_test.go @@ -251,6 +251,7 @@ func validateAndPreserveData( mclock := activation.NewMocklayerClock(ctrl) mFetch := smocks.NewMockFetcher(ctrl) mValidator := activation.NewMocknipostValidator(ctrl) + mMalPublisher := activation.NewMockatxMalfeasancePublisher(ctrl) mLegacyPublish := activation.NewMocklegacyMalfeasancePublisher(ctrl) mBeacon := activation.NewMockatxReceiver(ctrl) mTortoise := smocks.NewMockTortoise(ctrl) @@ -265,6 +266,7 @@ func validateAndPreserveData( mFetch, goldenAtx, mValidator, + mMalPublisher, mLegacyPublish, mBeacon, mTortoise, diff --git a/common/types/poet.go b/common/types/poet.go index 27e5764243..62cd82e087 100644 --- a/common/types/poet.go +++ b/common/types/poet.go @@ -67,7 +67,7 @@ func (p *PoetProof) MarshalLogObject(encoder zapcore.ObjectEncoder) error { type PoetProofMessage struct { PoetProof PoetServiceID []byte `scale:"max=32"` // public key of the PoET service - RoundID string `scale:"max=32"` // TODO(mafa): convert to uint64 + RoundID string `scale:"max=32"` // round ID // The input to Poet's POSW. // It's the root of a merkle tree built from all of the members // that are included in the proof. diff --git a/events/events.go b/events/events.go index 8a07fa5bc5..8827f10274 100644 --- a/events/events.go +++ b/events/events.go @@ -332,7 +332,7 @@ func EmitProposal(nodeID types.NodeID, layer types.LayerID, proposal types.Propo &pb.Event_Proposal{ Proposal: &pb.EventProposal{ Layer: layer.Uint32(), - Proposal: proposal[:], + Proposal: proposal.Bytes(), Smesher: nodeID.Bytes(), }, }, @@ -340,6 +340,7 @@ func EmitProposal(nodeID types.NodeID, layer types.LayerID, proposal types.Propo } func EmitOwnMalfeasanceProof(nodeID types.NodeID, proof []byte) { + // TODO(mafa): query malfeasance handler for data instead of extracting from proof bytes const help = "Node committed malicious behavior. Identity will be canceled." emitUserEvent( help, @@ -367,6 +368,9 @@ func emitUserEvent(help string, failure bool, details pb.IsEventDetails) { } } +// TODO(mafa): instead of passing along the proof bytes the API should query the malfeasance handler for the metadata +// of the proof if needed. +// The malfeasance handler should then take care of decoding the proof, caching if necessary and returning the metadata. func ToMalfeasancePB(nodeID types.NodeID, proof []byte, includeProof bool) *pb.MalfeasanceProof { mp := &wire.MalfeasanceProof{} if err := codec.Decode(proof, mp); err != nil { diff --git a/events/malfeasance.go b/events/malfeasance.go index 4ea0ef6ea8..ac0f15e7ce 100644 --- a/events/malfeasance.go +++ b/events/malfeasance.go @@ -8,7 +8,7 @@ import ( // EventMalfeasance includes the malfeasance proof. type EventMalfeasance struct { Smesher types.NodeID - Proof []byte + Proof []byte // TODO(mafa): remove this field and fetch metadata via malfeasance handler } // SubscribeMalfeasance subscribes malfeasance events. diff --git a/fetch/fetch_test.go b/fetch/fetch_test.go index e3ba8ba665..24748d95ff 100644 --- a/fetch/fetch_test.go +++ b/fetch/fetch_test.go @@ -156,14 +156,14 @@ func TestFetch_GetHash(t *testing.T) { hint2 := datastore.BallotDB // test hash aggregation - p0, err := f.getHash(context.TODO(), h1, hint, goodReceiver) + p0, err := f.getHash(context.Background(), h1, hint, goodReceiver) require.NoError(t, err) - p1, err := f.getHash(context.TODO(), h1, hint, goodReceiver) + p1, err := f.getHash(context.Background(), h1, hint, goodReceiver) require.NoError(t, err) require.Equal(t, p0.completed, p1.completed) h2 := types.RandomHash() - p2, err := f.getHash(context.TODO(), h2, hint2, goodReceiver) + p2, err := f.getHash(context.Background(), h2, hint2, goodReceiver) require.NoError(t, err) require.NotEqual(t, p1.completed, p2.completed) } @@ -230,10 +230,10 @@ func TestFetch_RequestHashBatchFromPeers(t *testing.T) { receiver = badReceiver } for i := 0; i < 2; i++ { - p, err := f.getHash(context.TODO(), hsh0, datastore.ProposalDB, receiver) + p, err := f.getHash(context.Background(), hsh0, datastore.ProposalDB, receiver) require.NoError(t, err) p0 = append(p0, p) - p, err = f.getHash(context.TODO(), hsh1, datastore.BlockDB, receiver) + p, err = f.getHash(context.Background(), hsh1, datastore.BlockDB, receiver) require.NoError(t, err) p1 = append(p1, p) } @@ -299,11 +299,11 @@ func TestFetch_Loop_BatchRequestMax(t *testing.T) { defer f.Stop() require.NoError(t, f.Start()) - p1, err := f.getHash(context.TODO(), h1, hint, goodReceiver) + p1, err := f.getHash(context.Background(), h1, hint, goodReceiver) require.NoError(t, err) - p2, err := f.getHash(context.TODO(), h2, hint, goodReceiver) + p2, err := f.getHash(context.Background(), h2, hint, goodReceiver) require.NoError(t, err) - p3, err := f.getHash(context.TODO(), h3, hint, goodReceiver) + p3, err := f.getHash(context.Background(), h3, hint, goodReceiver) require.NoError(t, err) for _, p := range []*promise{p1, p2, p3} { <-p.completed diff --git a/fetch/handler_test.go b/fetch/handler_test.go index b3176268a0..ae1eb64e5f 100644 --- a/fetch/handler_test.go +++ b/fetch/handler_test.go @@ -431,7 +431,7 @@ func TestHandleMaliciousIDsReq(t *testing.T) { require.NoError(t, identities.SetMalicious(th.cdb, nid, types.RandomBytes(11), time.Now())) } - out, err := th.handleMaliciousIDsReq(context.TODO(), p2p.Peer(""), []byte{}) + out, err := th.handleMaliciousIDsReq(context.Background(), p2p.Peer(""), []byte{}) require.NoError(t, err) var got MaliciousIDs require.NoError(t, codec.Decode(out, &got)) diff --git a/fetch/wire_types.go b/fetch/wire_types.go index 14c8eeafe3..bd28b52f13 100644 --- a/fetch/wire_types.go +++ b/fetch/wire_types.go @@ -29,7 +29,7 @@ func init() { // RequestMessage is sent to the peer for hash query. type RequestMessage struct { - Hint datastore.Hint `scale:"max=256"` // TODO(mafa): covert to an enum + Hint datastore.Hint `scale:"max=256"` Hash types.Hash32 } diff --git a/genvm/core/types.go b/genvm/core/types.go index 7c56487754..41d55a75cb 100644 --- a/genvm/core/types.go +++ b/genvm/core/types.go @@ -27,7 +27,7 @@ type ( // Signature is an alias to types.EdSignature. Signature = types.EdSignature - // Account is an alis to types.Account. + // Account is an alias to types.Account. Account = types.Account // Header is an alias to types.TxHeader. Header = types.TxHeader diff --git a/go.mod b/go.mod index 066f1b9327..68b252522c 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.4 require ( cloud.google.com/go/storage v1.48.0 github.com/ALTree/bigfloat v0.2.0 - github.com/chaos-mesh/chaos-mesh/api v0.0.0-20241204110417-3c631bce206f + github.com/chaos-mesh/chaos-mesh/api v0.0.0-20241207233122-cb35037e984f github.com/cosmos/btcutil v1.0.5 github.com/go-llsqlite/crawshaw v0.5.5 github.com/gofrs/flock v0.12.1 @@ -56,10 +56,10 @@ require ( github.com/zeebo/blake3 v0.2.4 go.uber.org/mock v0.5.0 go.uber.org/zap v1.27.0 - golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d + golang.org/x/exp v0.0.0-20241210194714-1829a127f884 golang.org/x/sync v0.10.0 golang.org/x/time v0.8.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 google.golang.org/grpc v1.68.1 google.golang.org/protobuf v1.36.0 k8s.io/api v0.32.0 @@ -260,3 +260,8 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +// temporary until this issue is resolved and cloud.google.com/go/storage has been updated +// https://github.com/googleapis/google-cloud-go/issues/11283 +exclude google.golang.org/grpc v1.69.0 + diff --git a/go.sum b/go.sum index d23cf42126..b176d1cbce 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMr github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chaos-mesh/chaos-mesh/api v0.0.0-20241204110417-3c631bce206f h1:N6vSz68L9EguQPpPNcbRRb8JkEuhE3T4OckRxgM49xE= -github.com/chaos-mesh/chaos-mesh/api v0.0.0-20241204110417-3c631bce206f/go.mod h1:x11iCbZV6hzzSQWMq610B6Wl5Lg1dhwqcVfeiWQQnQQ= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20241207233122-cb35037e984f h1:cRhVELxBpei+s7Lo1oZDJSHslfxQK3IxnueKyobuTWs= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20241207233122-cb35037e984f/go.mod h1:x11iCbZV6hzzSQWMq610B6Wl5Lg1dhwqcVfeiWQQnQQ= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -766,8 +766,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= -golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU= +golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -939,8 +939,8 @@ google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= diff --git a/hare3/malfeasance.go b/hare3/malfeasance.go index e7128ea3bd..30193b2f1e 100644 --- a/hare3/malfeasance.go +++ b/hare3/malfeasance.go @@ -6,7 +6,6 @@ import ( "fmt" "strconv" - "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/common/types" @@ -102,10 +101,6 @@ func (mh *MalfeasanceHandler) Validate(ctx context.Context, data wire.ProofData) return types.EmptyNodeID, errors.New("invalid hare malfeasance proof") } -func (mh *MalfeasanceHandler) ReportProof(numProofs *prometheus.CounterVec) { - numProofs.WithLabelValues(hareEquivocate).Inc() -} - -func (mh *MalfeasanceHandler) ReportInvalidProof(numInvalidProofs *prometheus.CounterVec) { - numInvalidProofs.WithLabelValues(hareEquivocate).Inc() +func (mh *MalfeasanceHandler) ReportLabel() string { + return hareEquivocate } diff --git a/malfeasance/handler.go b/malfeasance/handler.go index b15eb2aa1f..be2abbc8cb 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -86,16 +86,23 @@ func (h *Handler) reportMalfeasance(smesher types.NodeID, proof []byte) { } func (h *Handler) countProof(mp *wire.MalfeasanceProof) { - h.handlers[MalfeasanceType(mp.Proof.Type)].ReportProof(numProofs) + label := h.handlers[MalfeasanceType(mp.Proof.Type)].ReportLabel() + numProofs.WithLabelValues(label).Inc() } func (h *Handler) countInvalidProof(p *wire.MalfeasanceProof) { - h.handlers[MalfeasanceType(p.Proof.Type)].ReportInvalidProof(numInvalidProofs) + label := h.handlers[MalfeasanceType(p.Proof.Type)].ReportLabel() + numInvalidProofs.WithLabelValues(label).Inc() } -func (h *Handler) Info(data []byte) (map[string]string, error) { +func (h *Handler) Info(ctx context.Context, nodeID types.NodeID) (map[string]string, error) { + var blob sql.Blob + if err := identities.LoadMalfeasanceBlob(ctx, h.cdb, nodeID.Bytes(), &blob); err != nil { + return nil, fmt.Errorf("load malfeasance proof: %w", err) + } + var p wire.MalfeasanceProof - if err := codec.Decode(data, &p); err != nil { + if err := codec.Decode(blob.Bytes, &p); err != nil { return nil, fmt.Errorf("decode malfeasance proof: %w", err) } mh, ok := h.handlers[MalfeasanceType(p.Proof.Type)] diff --git a/malfeasance/handler_test.go b/malfeasance/handler_test.go index adb1e5af55..42d0fcc2da 100644 --- a/malfeasance/handler_test.go +++ b/malfeasance/handler_test.go @@ -20,6 +20,7 @@ import ( "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/malfeasance/wire" + "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/identities" @@ -29,13 +30,14 @@ import ( type testMalfeasanceHandler struct { *Handler - db sql.StateDatabase - mockTrt *Mocktortoise + observedLogs *observer.ObservedLogs + db sql.StateDatabase + mockTrt *Mocktortoise } func newHandler(tb testing.TB) *testMalfeasanceHandler { db := statesql.InMemoryTest(tb) - observer, _ := observer.New(zapcore.WarnLevel) + observer, observedLogs := observer.New(zapcore.WarnLevel) logger := zaptest.NewLogger(tb, zaptest.WrapOptions(zap.WrapCore( func(core zapcore.Core) zapcore.Core { return zapcore.NewTee(core, observer) @@ -58,8 +60,9 @@ func newHandler(tb testing.TB) *testMalfeasanceHandler { return &testMalfeasanceHandler{ Handler: h, - db: db, - mockTrt: trt, + observedLogs: observedLogs, + db: db, + mockTrt: trt, } } @@ -101,7 +104,7 @@ func TestHandler_HandleMalfeasanceProof(t *testing.T) { return types.EmptyNodeID, errors.New("invalid proof") }, ) - handler.EXPECT().ReportInvalidProof(gomock.Any()) + handler.EXPECT().ReportLabel().Return("multiATXs") h.RegisterHandler(MultipleATXs, handler) gossip := &wire.MalfeasanceGossip{ @@ -131,7 +134,7 @@ func TestHandler_HandleMalfeasanceProof(t *testing.T) { return nodeID, nil }, ) - handler.EXPECT().ReportProof(gomock.Any()) + handler.EXPECT().ReportLabel().Return("multiATXs") h.RegisterHandler(MultipleATXs, handler) gossip := &wire.MalfeasanceGossip{ @@ -242,7 +245,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { return nodeID, nil }, ) - handler.EXPECT().ReportProof(gomock.Any()) + handler.EXPECT().ReportLabel().Return("multiATXs") h.RegisterHandler(MultipleATXs, handler) proof := &wire.MalfeasanceProof{ @@ -253,15 +256,23 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { }, } + expectedHash := types.RandomHash() h.mockTrt.EXPECT().OnMalfeasance(nodeID) err := h.HandleSyncedMalfeasanceProof( context.Background(), - types.RandomHash(), + expectedHash, "peer", codec.MustEncode(proof), ) require.ErrorIs(t, err, errWrongHash) require.ErrorIs(t, err, pubsub.ErrValidationReject) + + require.Equal(t, 1, h.observedLogs.Len()) + log := h.observedLogs.All()[0] + require.Equal(t, zap.WarnLevel, log.Level) + require.Contains(t, log.Message, "malfeasance proof for wrong identity") + require.Equal(t, expectedHash.ShortString(), log.ContextMap()["expected"]) + require.Equal(t, p2p.Peer("peer").String(), log.ContextMap()["peer"]) }) t.Run("invalid proof", func(t *testing.T) { @@ -276,7 +287,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { return types.EmptyNodeID, errors.New("invalid proof") }, ) - handler.EXPECT().ReportInvalidProof(gomock.Any()) + handler.EXPECT().ReportLabel().Return("multiATXs") h.RegisterHandler(MultipleATXs, handler) proof := &wire.MalfeasanceProof{ @@ -309,7 +320,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { return nodeID, nil }, ) - handler.EXPECT().ReportProof(gomock.Any()) + handler.EXPECT().ReportLabel().Return("multiATXs") h.RegisterHandler(MultipleATXs, handler) proof := &wire.MalfeasanceProof{ @@ -374,11 +385,12 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { } func TestHandler_Info(t *testing.T) { - t.Run("malformed data", func(t *testing.T) { + t.Run("unknown identity", func(t *testing.T) { h := newHandler(t) - info, err := h.Info(types.RandomBytes(32)) - require.ErrorContains(t, err, "decode malfeasance proof:") + info, err := h.Info(context.Background(), types.RandomNodeID()) + require.ErrorContains(t, err, "load malfeasance proof:") + require.ErrorIs(t, err, sql.ErrNotFound) require.Nil(t, info) }) @@ -392,9 +404,11 @@ func TestHandler_Info(t *testing.T) { Data: &wire.AtxProof{}, }, } + nodeID := types.RandomNodeID() proofBytes := codec.MustEncode(proof) + identities.SetMalicious(h.db, nodeID, proofBytes, time.Now()) - info, err := h.Info(proofBytes) + info, err := h.Info(context.Background(), nodeID) require.ErrorContains(t, err, fmt.Sprintf("unknown malfeasance type %d", wire.MultipleATXs)) require.Nil(t, info) }) @@ -414,9 +428,11 @@ func TestHandler_Info(t *testing.T) { Data: &wire.AtxProof{}, }, } + nodeID := types.RandomNodeID() proofBytes := codec.MustEncode(proof) + identities.SetMalicious(h.db, nodeID, proofBytes, time.Now()) - info, err := h.Info(proofBytes) + info, err := h.Info(context.Background(), nodeID) require.ErrorContains(t, err, "invalid proof") require.Nil(t, info) }) @@ -440,7 +456,10 @@ func TestHandler_Info(t *testing.T) { Data: &wire.AtxProof{}, }, } + nodeID := types.RandomNodeID() proofBytes := codec.MustEncode(proof) + identities.SetMalicious(h.db, nodeID, proofBytes, time.Now()) + expectedProperties := map[string]string{ "domain": "0", "type": strconv.FormatUint(uint64(wire.MultipleATXs), 10), @@ -449,7 +468,7 @@ func TestHandler_Info(t *testing.T) { expectedProperties[k] = v } - info, err := h.Info(proofBytes) + info, err := h.Info(context.Background(), nodeID) require.NoError(t, err) require.Equal(t, expectedProperties, info) }) diff --git a/malfeasance/interface.go b/malfeasance/interface.go index 79883b45a8..5c01e96545 100644 --- a/malfeasance/interface.go +++ b/malfeasance/interface.go @@ -3,8 +3,6 @@ package malfeasance import ( "context" - "github.com/prometheus/client_golang/prometheus" - "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/malfeasance/wire" ) @@ -20,8 +18,12 @@ type syncer interface { } type MalfeasanceHandler interface { + // Validate the proof and return the node ID of the malicious node if the proof is valid Validate(ctx context.Context, data wire.ProofData) (types.NodeID, error) + + // Info returns a map of key-value pairs that serve as metadata for the proof Info(data wire.ProofData) (map[string]string, error) - ReportProof(vec *prometheus.CounterVec) - ReportInvalidProof(vec *prometheus.CounterVec) + + // ReportLabel returns the label for the prometheus counter of the given proof type + ReportLabel() string } diff --git a/malfeasance/mocks.go b/malfeasance/mocks.go index 18dc4e9ec5..8b001fbf7e 100644 --- a/malfeasance/mocks.go +++ b/malfeasance/mocks.go @@ -13,7 +13,6 @@ import ( context "context" reflect "reflect" - prometheus "github.com/prometheus/client_golang/prometheus" types "github.com/spacemeshos/go-spacemesh/common/types" wire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" gomock "go.uber.org/mock/gomock" @@ -204,74 +203,40 @@ func (c *MockMalfeasanceHandlerInfoCall) DoAndReturn(f func(wire.ProofData) (map return c } -// ReportInvalidProof mocks base method. -func (m *MockMalfeasanceHandler) ReportInvalidProof(vec *prometheus.CounterVec) { +// ReportLabel mocks base method. +func (m *MockMalfeasanceHandler) ReportLabel() string { m.ctrl.T.Helper() - m.ctrl.Call(m, "ReportInvalidProof", vec) -} - -// ReportInvalidProof indicates an expected call of ReportInvalidProof. -func (mr *MockMalfeasanceHandlerMockRecorder) ReportInvalidProof(vec any) *MockMalfeasanceHandlerReportInvalidProofCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportInvalidProof", reflect.TypeOf((*MockMalfeasanceHandler)(nil).ReportInvalidProof), vec) - return &MockMalfeasanceHandlerReportInvalidProofCall{Call: call} -} - -// MockMalfeasanceHandlerReportInvalidProofCall wrap *gomock.Call -type MockMalfeasanceHandlerReportInvalidProofCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockMalfeasanceHandlerReportInvalidProofCall) Return() *MockMalfeasanceHandlerReportInvalidProofCall { - c.Call = c.Call.Return() - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockMalfeasanceHandlerReportInvalidProofCall) Do(f func(*prometheus.CounterVec)) *MockMalfeasanceHandlerReportInvalidProofCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockMalfeasanceHandlerReportInvalidProofCall) DoAndReturn(f func(*prometheus.CounterVec)) *MockMalfeasanceHandlerReportInvalidProofCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// ReportProof mocks base method. -func (m *MockMalfeasanceHandler) ReportProof(vec *prometheus.CounterVec) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "ReportProof", vec) + ret := m.ctrl.Call(m, "ReportLabel") + ret0, _ := ret[0].(string) + return ret0 } -// ReportProof indicates an expected call of ReportProof. -func (mr *MockMalfeasanceHandlerMockRecorder) ReportProof(vec any) *MockMalfeasanceHandlerReportProofCall { +// ReportLabel indicates an expected call of ReportLabel. +func (mr *MockMalfeasanceHandlerMockRecorder) ReportLabel() *MockMalfeasanceHandlerReportLabelCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportProof", reflect.TypeOf((*MockMalfeasanceHandler)(nil).ReportProof), vec) - return &MockMalfeasanceHandlerReportProofCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportLabel", reflect.TypeOf((*MockMalfeasanceHandler)(nil).ReportLabel)) + return &MockMalfeasanceHandlerReportLabelCall{Call: call} } -// MockMalfeasanceHandlerReportProofCall wrap *gomock.Call -type MockMalfeasanceHandlerReportProofCall struct { +// MockMalfeasanceHandlerReportLabelCall wrap *gomock.Call +type MockMalfeasanceHandlerReportLabelCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockMalfeasanceHandlerReportProofCall) Return() *MockMalfeasanceHandlerReportProofCall { - c.Call = c.Call.Return() +func (c *MockMalfeasanceHandlerReportLabelCall) Return(arg0 string) *MockMalfeasanceHandlerReportLabelCall { + c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockMalfeasanceHandlerReportProofCall) Do(f func(*prometheus.CounterVec)) *MockMalfeasanceHandlerReportProofCall { +func (c *MockMalfeasanceHandlerReportLabelCall) Do(f func() string) *MockMalfeasanceHandlerReportLabelCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockMalfeasanceHandlerReportProofCall) DoAndReturn(f func(*prometheus.CounterVec)) *MockMalfeasanceHandlerReportProofCall { +func (c *MockMalfeasanceHandlerReportLabelCall) DoAndReturn(f func() string) *MockMalfeasanceHandlerReportLabelCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/malfeasance/publisher.go b/malfeasance/publisher.go index 6e6f222c24..76fd8111bb 100644 --- a/malfeasance/publisher.go +++ b/malfeasance/publisher.go @@ -59,7 +59,7 @@ func (p *Publisher) PublishProof(ctx context.Context, smesherID types.NodeID, pr // Only gossip the proof if we are synced (to not spam the network with proofs others probably already have). if !p.sync.ListenToATXGossip() { - p.logger.Debug("not synced, not broadcasting malfeasance proof", + p.logger.Debug("not in sync, not broadcasting malfeasance proof", zap.String("smesher_id", smesherID.ShortString()), ) return nil diff --git a/malfeasance2/handler.go b/malfeasance2/handler.go new file mode 100644 index 0000000000..3b6bff489b --- /dev/null +++ b/malfeasance2/handler.go @@ -0,0 +1,236 @@ +package malfeasance2 + +import ( + "context" + "errors" + "fmt" + "slices" + "strconv" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/events" + "github.com/spacemeshos/go-spacemesh/log" + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" +) + +var ( + ErrMalformedData = fmt.Errorf("%w: malformed data", pubsub.ErrValidationReject) + ErrWrongHash = fmt.Errorf("%w: incorrect hash", pubsub.ErrValidationReject) + ErrUnknownVersion = fmt.Errorf("%w: unknown version", pubsub.ErrValidationReject) + ErrUnknownDomain = fmt.Errorf("%w: unknown domain", pubsub.ErrValidationReject) +) + +type Handler struct { + logger *zap.Logger + db sql.Executor + self p2p.Peer + nodeIDs []types.NodeID + edVerifier *signing.EdVerifier + tortoise tortoise + + handlers map[ProofDomain]MalfeasanceHandler +} + +func NewHandler( + db sql.Executor, + lg *zap.Logger, + self p2p.Peer, + nodeIDs []types.NodeID, + edVerifier *signing.EdVerifier, + tortoise tortoise, +) *Handler { + return &Handler{ + db: db, + logger: lg, + self: self, + nodeIDs: nodeIDs, + edVerifier: edVerifier, + tortoise: tortoise, + + handlers: make(map[ProofDomain]MalfeasanceHandler), + } +} + +func (h *Handler) RegisterHandler(malfeasanceType ProofDomain, handler MalfeasanceHandler) { + if _, ok := h.handlers[malfeasanceType]; ok { + h.logger.Panic("handler already registered", zap.Int("malfeasanceType", int(malfeasanceType))) + } + h.handlers[malfeasanceType] = handler +} + +func (h *Handler) countProof(mp MalfeasanceProof) { + labels := h.handlers[mp.Domain].ReportLabels(mp.Proof) + numProofs.WithLabelValues(labels...).Inc() +} + +func (h *Handler) countInvalidProof(mp MalfeasanceProof) { + labels := h.handlers[mp.Domain].ReportLabels(mp.Proof) + numInvalidProofs.WithLabelValues(labels...).Inc() +} + +func (h *Handler) reportMalfeasance(smesher types.NodeID, proof []byte) { + h.tortoise.OnMalfeasance(smesher) + events.ReportMalfeasance(smesher, proof) + if slices.Contains(h.nodeIDs, smesher) { + events.EmitOwnMalfeasanceProof(smesher, proof) + } +} + +func (h *Handler) Info(data []byte) (map[string]string, error) { + var p MalfeasanceProof + if err := codec.Decode(data, &p); err != nil { + return nil, fmt.Errorf("decode malfeasance proof: %w", err) + } + mh, ok := h.handlers[p.Domain] + if !ok { + return nil, fmt.Errorf("unknown malfeasance domain %d", p.Domain) + } + properties, err := mh.Info(p.Proof) + if err != nil { + return nil, fmt.Errorf("malfeasance info: %w", err) + } + properties["domain"] = strconv.FormatUint(uint64(p.Domain), 10) + return properties, nil +} + +func (h *Handler) HandleSynced(ctx context.Context, expHash types.Hash32, peer p2p.Peer, msg []byte) error { + var proof MalfeasanceProof + if err := codec.Decode(msg, &proof); err != nil { + numMalformed.Inc() + return ErrMalformedData + } + + nodeIDs, err := h.handleProof(ctx, proof) + if err != nil { + return errors.Join(err, pubsub.ErrValidationReject) + } + if !slices.Contains(nodeIDs, types.NodeID(expHash)) { + // we log & return because libp2p will ignore the message if we return an error, + // but only log "validation ignored" instead of the error we return + h.logger.Warn("malfeasance proof for wrong identity", + log.ZContext(ctx), + zap.Stringer("peer", peer), + log.ZShortStringer("expected", expHash), + zap.Array("got", zapcore.ArrayMarshalerFunc(func(arr zapcore.ArrayEncoder) error { + for _, id := range nodeIDs { + arr.AppendString(id.ShortString()) + } + return nil + })), + ) + h.countInvalidProof(proof) + return fmt.Errorf( + "%w: malfeasance proof not valid for %s", + ErrWrongHash, + expHash.ShortString(), + ) + } + + if err := h.storeProof(ctx, proof.Domain, msg); err != nil { + return fmt.Errorf("store synced malfeasance proof: %w", err) + } + + for _, id := range nodeIDs { + h.reportMalfeasance(id, msg) + } + h.countProof(proof) + h.logger.Debug("synced malfeasance proof", + log.ZContext(ctx), + log.ZShortStringer("requested", expHash), + zap.Array("valid_for", zapcore.ArrayMarshalerFunc(func(arr zapcore.ArrayEncoder) error { + for _, id := range nodeIDs { + arr.AppendString(id.ShortString()) + } + return nil + })), + ) + return nil +} + +func (h *Handler) HandleGossip(ctx context.Context, peer p2p.Peer, msg []byte) error { + if peer == h.self { + // ignore messages from self, we already validate and persist proofs when publishing + return nil + } + + var proof MalfeasanceProof + if err := codec.Decode(msg, &proof); err != nil { + numMalformed.Inc() + return ErrMalformedData + } + + nodeIDs, err := h.handleProof(ctx, proof) + if err != nil { + return errors.Join(err, pubsub.ErrValidationReject) + } + + if err := h.storeProof(ctx, proof.Domain, msg); err != nil { + return fmt.Errorf("store gossiped malfeasance proof: %w", err) + } + + for _, id := range nodeIDs { + h.reportMalfeasance(id, msg) + } + h.countProof(proof) + h.logger.Debug("received gossiped malfeasance proof", + log.ZContext(ctx), + zap.Array("valid_for", zapcore.ArrayMarshalerFunc(func(arr zapcore.ArrayEncoder) error { + for _, id := range nodeIDs { + arr.AppendString(id.ShortString()) + } + return nil + })), + ) + return nil +} + +func (h *Handler) handleProof(ctx context.Context, proof MalfeasanceProof) ([]types.NodeID, error) { + if proof.Version != 0 { + // unsupported proof version + return nil, ErrUnknownVersion + } + + handler, ok := h.handlers[proof.Domain] + if !ok { + // unknown proof domain + return nil, fmt.Errorf("%w: %d", ErrUnknownDomain, proof.Domain) + } + + id, err := handler.Validate(ctx, proof.Proof) + if err != nil { + h.countInvalidProof(proof) + return nil, err + } + + validIDs := make([]types.NodeID, 0, len(proof.Certificates)+1) + validIDs = append(validIDs, id) // id has already been proven to be malfeasant + + // check certificates provided with the proof + // TODO(mafa): only works if the main identity becomes malfeasant - try different approach with merkle proofs + for _, cert := range proof.Certificates { + if id != cert.TargetID { + continue + } + if !h.edVerifier.Verify(signing.MARRIAGE, cert.TargetID, cert.SmesherID.Bytes(), cert.Signature) { + continue + } + validIDs = append(validIDs, cert.SmesherID) + } + + return validIDs, nil +} + +// TODO(mafa): store proof in db by +// - updating marriage information if needed (e.g. new smesher in the malfeasant marriage certificate set) +// - storing the proof for the identity that was proven to be malicious +func (h *Handler) storeProof(ctx context.Context, domain ProofDomain, proof []byte) error { + _ = h.db + return nil +} diff --git a/malfeasance2/handler_test.go b/malfeasance2/handler_test.go new file mode 100644 index 0000000000..ef713fd5f1 --- /dev/null +++ b/malfeasance2/handler_test.go @@ -0,0 +1,286 @@ +package malfeasance2_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest" + "go.uber.org/zap/zaptest/observer" + + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/malfeasance2" + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" + "github.com/spacemeshos/go-spacemesh/sql/statesql" +) + +type testHandler struct { + *malfeasance2.Handler + + observedLogs *observer.ObservedLogs + db sql.StateDatabase + self p2p.Peer + mockTrt *malfeasance2.Mocktortoise +} + +func newTestHandler(tb testing.TB) *testHandler { + db := statesql.InMemory() + edVerifier := signing.NewEdVerifier() + + observer, observedLogs := observer.New(zap.WarnLevel) + logger := zaptest.NewLogger(tb, zaptest.WrapOptions(zap.WrapCore( + func(core zapcore.Core) zapcore.Core { + return zapcore.NewTee(core, observer) + }, + ))) + + ctrl := gomock.NewController(tb) + mockTrt := malfeasance2.NewMocktortoise(ctrl) + + h := malfeasance2.NewHandler( + db, + logger, + "self", + []types.NodeID{types.RandomNodeID()}, + edVerifier, + mockTrt, + ) + return &testHandler{ + Handler: h, + + observedLogs: observedLogs, + db: db, + self: "self", + mockTrt: mockTrt, + } +} + +// TODO(mafa): missing tests +// - new proof for same identity is no-op +// - new proof with bigger certificate list only updates certificate list +// - all identities in certificates are marked as malicious +// - invalid certificates are ignored if proof is valid + +func TestHandler_HandleSync(t *testing.T) { + t.Run("malformed data", func(t *testing.T) { + h := newTestHandler(t) + + err := h.HandleSynced(context.Background(), types.EmptyHash32, "peer", []byte("malformed")) + require.ErrorIs(t, err, malfeasance2.ErrMalformedData) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + }) + + t.Run("unknown version", func(t *testing.T) { + h := newTestHandler(t) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 42, + } + + err := h.HandleSynced(context.Background(), types.EmptyHash32, "peer", codec.MustEncode(proof)) + require.ErrorIs(t, err, malfeasance2.ErrUnknownVersion) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + }) + + t.Run("unknown domain", func(t *testing.T) { + h := newTestHandler(t) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 0, + Domain: 42, + } + + err := h.HandleSynced(context.Background(), types.EmptyHash32, "peer", codec.MustEncode(proof)) + require.ErrorIs(t, err, malfeasance2.ErrUnknownDomain) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + }) + + t.Run("invalid proof", func(t *testing.T) { + h := newTestHandler(t) + invalidProof := []byte("invalid") + handlerError := errors.New("invalid proof") + mockHandler := malfeasance2.NewMockMalfeasanceHandler(gomock.NewController(t)) + mockHandler.EXPECT().Validate(gomock.Any(), invalidProof).Return(types.EmptyNodeID, handlerError) + mockHandler.EXPECT().ReportLabels(invalidProof).Return([]string{"ATX", "invalidPost"}) + h.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 0, + Domain: malfeasance2.InvalidActivation, + Proof: invalidProof, + } + + err := h.HandleSynced(context.Background(), types.EmptyHash32, "peer", codec.MustEncode(proof)) + require.ErrorIs(t, err, handlerError) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + }) + + t.Run("valid proof", func(t *testing.T) { + h := newTestHandler(t) + validProof := []byte("valid") + nodeID := types.RandomNodeID() + mockHandler := malfeasance2.NewMockMalfeasanceHandler(gomock.NewController(t)) + mockHandler.EXPECT().Validate(gomock.Any(), validProof).Return(nodeID, nil) + mockHandler.EXPECT().ReportLabels(validProof).Return([]string{"ATX", "invalidPost"}) + h.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) + h.mockTrt.EXPECT().OnMalfeasance(nodeID) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 0, + Domain: malfeasance2.InvalidActivation, + Proof: validProof, + } + + err := h.HandleSynced(context.Background(), types.Hash32(nodeID), "peer", codec.MustEncode(proof)) + require.NoError(t, err) + + malicious, err := malfeasance.IsMalicious(h.db, nodeID) + require.NoError(t, err) + require.True(t, malicious) + }) + + t.Run("valid proof, wrong hash", func(t *testing.T) { + h := newTestHandler(t) + validProof := []byte("valid") + nodeID := types.RandomNodeID() + mockHandler := malfeasance2.NewMockMalfeasanceHandler(gomock.NewController(t)) + mockHandler.EXPECT().Validate(gomock.Any(), validProof).Return(nodeID, nil) + mockHandler.EXPECT().ReportLabels(validProof).Return([]string{"ATX", "invalidPost"}) + h.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 0, + Domain: malfeasance2.InvalidActivation, + Proof: validProof, + } + + expectedHash := types.RandomHash() + err := h.HandleSynced(context.Background(), expectedHash, "peer", codec.MustEncode(proof)) + require.ErrorIs(t, err, malfeasance2.ErrWrongHash) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + + require.Equal(t, 1, h.observedLogs.Len()) + log := h.observedLogs.All()[0] + require.Equal(t, zap.WarnLevel, log.Level) + require.Contains(t, log.Message, "malfeasance proof for wrong identity") + require.Equal(t, expectedHash.ShortString(), log.ContextMap()["expected"]) + require.Equal(t, p2p.Peer("peer").String(), log.ContextMap()["peer"]) + }) +} + +func TestHandler_HandleGossip(t *testing.T) { + t.Run("malformed data", func(t *testing.T) { + h := newTestHandler(t) + + err := h.HandleGossip(context.Background(), "peer", []byte("malformed")) + require.ErrorIs(t, err, malfeasance2.ErrMalformedData) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + }) + + t.Run("self peer", func(t *testing.T) { + h := newTestHandler(t) + + // ignore messages from self + err := h.HandleGossip(context.Background(), h.self, []byte("malformed")) + require.NoError(t, err) + }) + + t.Run("unknown version", func(t *testing.T) { + h := newTestHandler(t) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 42, + } + + err := h.HandleGossip(context.Background(), "peer", codec.MustEncode(proof)) + require.ErrorIs(t, err, malfeasance2.ErrUnknownVersion) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + }) + + t.Run("unknown domain", func(t *testing.T) { + h := newTestHandler(t) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 0, + Domain: 42, + } + + err := h.HandleGossip(context.Background(), "peer", codec.MustEncode(proof)) + require.ErrorIs(t, err, malfeasance2.ErrUnknownDomain) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + }) + + t.Run("invalid proof", func(t *testing.T) { + h := newTestHandler(t) + invalidProof := []byte("invalid") + handlerError := errors.New("invalid proof") + mockHandler := malfeasance2.NewMockMalfeasanceHandler(gomock.NewController(t)) + mockHandler.EXPECT().Validate(gomock.Any(), invalidProof).Return(types.EmptyNodeID, handlerError) + mockHandler.EXPECT().ReportLabels(invalidProof).Return([]string{"ATX", "invalidPost"}) + h.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 0, + Domain: malfeasance2.InvalidActivation, + Proof: invalidProof, + } + + err := h.HandleGossip(context.Background(), "peer", codec.MustEncode(proof)) + require.ErrorIs(t, err, handlerError) + require.ErrorIs(t, err, pubsub.ErrValidationReject) + }) + + t.Run("valid proof", func(t *testing.T) { + h := newTestHandler(t) + validProof := []byte("valid") + nodeID := types.RandomNodeID() + mockHandler := malfeasance2.NewMockMalfeasanceHandler(gomock.NewController(t)) + mockHandler.EXPECT().Validate(gomock.Any(), validProof).Return(nodeID, nil) + mockHandler.EXPECT().ReportLabels(validProof).Return([]string{"ATX", "invalidPost"}) + h.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) + h.mockTrt.EXPECT().OnMalfeasance(nodeID) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 0, + Domain: malfeasance2.InvalidActivation, + Proof: validProof, + } + + err := h.HandleGossip(context.Background(), "peer", codec.MustEncode(proof)) + require.NoError(t, err) + }) + + t.Run("valid proof for known malicious identity", func(t *testing.T) { + h := newTestHandler(t) + validProof := []byte("valid") + nodeID := types.RandomNodeID() + mockHandler := malfeasance2.NewMockMalfeasanceHandler(gomock.NewController(t)) + mockHandler.EXPECT().Validate(gomock.Any(), validProof).Return(nodeID, nil) + mockHandler.EXPECT().ReportLabels(validProof).Return([]string{"ATX", "invalidPost"}) + h.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) + h.mockTrt.EXPECT().OnMalfeasance(nodeID) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 0, + Domain: malfeasance2.InvalidActivation, + Proof: validProof, + } + proofBytes := codec.MustEncode(proof) + + err := malfeasance.AddProof(h.db, nodeID, nil, proofBytes, byte(malfeasance2.InvalidActivation), time.Now()) + require.NoError(t, err) + + err = h.HandleGossip(context.Background(), "peer", proofBytes) + require.NoError(t, err) + }) +} diff --git a/malfeasance2/interface.go b/malfeasance2/interface.go new file mode 100644 index 0000000000..ca436ba27b --- /dev/null +++ b/malfeasance2/interface.go @@ -0,0 +1,28 @@ +package malfeasance2 + +import ( + "context" + + "github.com/spacemeshos/go-spacemesh/common/types" +) + +//go:generate mockgen -typed -package=malfeasance2 -destination=./mocks.go -source=./interface.go + +type syncer interface { + ListenToATXGossip() bool +} + +type tortoise interface { + OnMalfeasance(types.NodeID) +} + +type MalfeasanceHandler interface { + // Validate the proof and return the node ID of the malicious node if the proof is valid + Validate(ctx context.Context, data []byte) (types.NodeID, error) + + // Info returns a map of key-value pairs that serve as metadata for the proof + Info(data []byte) (map[string]string, error) + + // ReportLabel returns the label for the prometheus counter of the given proof type + ReportLabels(data []byte) []string +} diff --git a/malfeasance2/metrics.go b/malfeasance2/metrics.go new file mode 100644 index 0000000000..5d7f6b4ac4 --- /dev/null +++ b/malfeasance2/metrics.go @@ -0,0 +1,34 @@ +package malfeasance2 + +import "github.com/spacemeshos/go-spacemesh/metrics" + +const ( + namespace = "malfeasance2" + + domainLabel = "domain" + typeLabel = "type" +) + +var ( + numProofs = metrics.NewCounter( + "num_proofs", + namespace, + "number of malfeasance proofs", + []string{ + domainLabel, + typeLabel, + }, + ) + + numInvalidProofs = metrics.NewCounter( + "num_invalid_proofs", + namespace, + "number of invalid malfeasance proofs", + []string{ + domainLabel, + typeLabel, + }, + ) + + numMalformed = numInvalidProofs.WithLabelValues("mal", "unknown") +) diff --git a/malfeasance2/mocks.go b/malfeasance2/mocks.go new file mode 100644 index 0000000000..703197e020 --- /dev/null +++ b/malfeasance2/mocks.go @@ -0,0 +1,280 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./interface.go +// +// Generated by this command: +// +// mockgen -typed -package=malfeasance2 -destination=./mocks.go -source=./interface.go +// + +// Package malfeasance2 is a generated GoMock package. +package malfeasance2 + +import ( + context "context" + reflect "reflect" + + types "github.com/spacemeshos/go-spacemesh/common/types" + gomock "go.uber.org/mock/gomock" +) + +// Mocksyncer is a mock of syncer interface. +type Mocksyncer struct { + ctrl *gomock.Controller + recorder *MocksyncerMockRecorder + isgomock struct{} +} + +// MocksyncerMockRecorder is the mock recorder for Mocksyncer. +type MocksyncerMockRecorder struct { + mock *Mocksyncer +} + +// NewMocksyncer creates a new mock instance. +func NewMocksyncer(ctrl *gomock.Controller) *Mocksyncer { + mock := &Mocksyncer{ctrl: ctrl} + mock.recorder = &MocksyncerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Mocksyncer) EXPECT() *MocksyncerMockRecorder { + return m.recorder +} + +// ListenToATXGossip mocks base method. +func (m *Mocksyncer) ListenToATXGossip() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListenToATXGossip") + ret0, _ := ret[0].(bool) + return ret0 +} + +// ListenToATXGossip indicates an expected call of ListenToATXGossip. +func (mr *MocksyncerMockRecorder) ListenToATXGossip() *MocksyncerListenToATXGossipCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListenToATXGossip", reflect.TypeOf((*Mocksyncer)(nil).ListenToATXGossip)) + return &MocksyncerListenToATXGossipCall{Call: call} +} + +// MocksyncerListenToATXGossipCall wrap *gomock.Call +type MocksyncerListenToATXGossipCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MocksyncerListenToATXGossipCall) Return(arg0 bool) *MocksyncerListenToATXGossipCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MocksyncerListenToATXGossipCall) Do(f func() bool) *MocksyncerListenToATXGossipCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MocksyncerListenToATXGossipCall) DoAndReturn(f func() bool) *MocksyncerListenToATXGossipCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Mocktortoise is a mock of tortoise interface. +type Mocktortoise struct { + ctrl *gomock.Controller + recorder *MocktortoiseMockRecorder + isgomock struct{} +} + +// MocktortoiseMockRecorder is the mock recorder for Mocktortoise. +type MocktortoiseMockRecorder struct { + mock *Mocktortoise +} + +// NewMocktortoise creates a new mock instance. +func NewMocktortoise(ctrl *gomock.Controller) *Mocktortoise { + mock := &Mocktortoise{ctrl: ctrl} + mock.recorder = &MocktortoiseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Mocktortoise) EXPECT() *MocktortoiseMockRecorder { + return m.recorder +} + +// OnMalfeasance mocks base method. +func (m *Mocktortoise) OnMalfeasance(arg0 types.NodeID) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "OnMalfeasance", arg0) +} + +// OnMalfeasance indicates an expected call of OnMalfeasance. +func (mr *MocktortoiseMockRecorder) OnMalfeasance(arg0 any) *MocktortoiseOnMalfeasanceCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnMalfeasance", reflect.TypeOf((*Mocktortoise)(nil).OnMalfeasance), arg0) + return &MocktortoiseOnMalfeasanceCall{Call: call} +} + +// MocktortoiseOnMalfeasanceCall wrap *gomock.Call +type MocktortoiseOnMalfeasanceCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MocktortoiseOnMalfeasanceCall) Return() *MocktortoiseOnMalfeasanceCall { + c.Call = c.Call.Return() + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MocktortoiseOnMalfeasanceCall) Do(f func(types.NodeID)) *MocktortoiseOnMalfeasanceCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MocktortoiseOnMalfeasanceCall) DoAndReturn(f func(types.NodeID)) *MocktortoiseOnMalfeasanceCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockMalfeasanceHandler is a mock of MalfeasanceHandler interface. +type MockMalfeasanceHandler struct { + ctrl *gomock.Controller + recorder *MockMalfeasanceHandlerMockRecorder + isgomock struct{} +} + +// MockMalfeasanceHandlerMockRecorder is the mock recorder for MockMalfeasanceHandler. +type MockMalfeasanceHandlerMockRecorder struct { + mock *MockMalfeasanceHandler +} + +// NewMockMalfeasanceHandler creates a new mock instance. +func NewMockMalfeasanceHandler(ctrl *gomock.Controller) *MockMalfeasanceHandler { + mock := &MockMalfeasanceHandler{ctrl: ctrl} + mock.recorder = &MockMalfeasanceHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMalfeasanceHandler) EXPECT() *MockMalfeasanceHandlerMockRecorder { + return m.recorder +} + +// Info mocks base method. +func (m *MockMalfeasanceHandler) Info(data []byte) (map[string]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Info", data) + ret0, _ := ret[0].(map[string]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Info indicates an expected call of Info. +func (mr *MockMalfeasanceHandlerMockRecorder) Info(data any) *MockMalfeasanceHandlerInfoCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockMalfeasanceHandler)(nil).Info), data) + return &MockMalfeasanceHandlerInfoCall{Call: call} +} + +// MockMalfeasanceHandlerInfoCall wrap *gomock.Call +type MockMalfeasanceHandlerInfoCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockMalfeasanceHandlerInfoCall) Return(arg0 map[string]string, arg1 error) *MockMalfeasanceHandlerInfoCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockMalfeasanceHandlerInfoCall) Do(f func([]byte) (map[string]string, error)) *MockMalfeasanceHandlerInfoCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockMalfeasanceHandlerInfoCall) DoAndReturn(f func([]byte) (map[string]string, error)) *MockMalfeasanceHandlerInfoCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ReportLabels mocks base method. +func (m *MockMalfeasanceHandler) ReportLabels(data []byte) []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReportLabels", data) + ret0, _ := ret[0].([]string) + return ret0 +} + +// ReportLabels indicates an expected call of ReportLabels. +func (mr *MockMalfeasanceHandlerMockRecorder) ReportLabels(data any) *MockMalfeasanceHandlerReportLabelsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportLabels", reflect.TypeOf((*MockMalfeasanceHandler)(nil).ReportLabels), data) + return &MockMalfeasanceHandlerReportLabelsCall{Call: call} +} + +// MockMalfeasanceHandlerReportLabelsCall wrap *gomock.Call +type MockMalfeasanceHandlerReportLabelsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockMalfeasanceHandlerReportLabelsCall) Return(arg0 []string) *MockMalfeasanceHandlerReportLabelsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockMalfeasanceHandlerReportLabelsCall) Do(f func([]byte) []string) *MockMalfeasanceHandlerReportLabelsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockMalfeasanceHandlerReportLabelsCall) DoAndReturn(f func([]byte) []string) *MockMalfeasanceHandlerReportLabelsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Validate mocks base method. +func (m *MockMalfeasanceHandler) Validate(ctx context.Context, data []byte) (types.NodeID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validate", ctx, data) + ret0, _ := ret[0].(types.NodeID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Validate indicates an expected call of Validate. +func (mr *MockMalfeasanceHandlerMockRecorder) Validate(ctx, data any) *MockMalfeasanceHandlerValidateCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockMalfeasanceHandler)(nil).Validate), ctx, data) + return &MockMalfeasanceHandlerValidateCall{Call: call} +} + +// MockMalfeasanceHandlerValidateCall wrap *gomock.Call +type MockMalfeasanceHandlerValidateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockMalfeasanceHandlerValidateCall) Return(arg0 types.NodeID, arg1 error) *MockMalfeasanceHandlerValidateCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockMalfeasanceHandlerValidateCall) Do(f func(context.Context, []byte) (types.NodeID, error)) *MockMalfeasanceHandlerValidateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockMalfeasanceHandlerValidateCall) DoAndReturn(f func(context.Context, []byte) (types.NodeID, error)) *MockMalfeasanceHandlerValidateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/malfeasance2/publisher.go b/malfeasance2/publisher.go new file mode 100644 index 0000000000..1d6057070d --- /dev/null +++ b/malfeasance2/publisher.go @@ -0,0 +1,153 @@ +package malfeasance2 + +import ( + "context" + "errors" + "fmt" + "time" + + "go.uber.org/zap" + + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/datastore" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" + "github.com/spacemeshos/go-spacemesh/sql/marriage" +) + +type Publisher struct { + logger *zap.Logger + cdb *datastore.CachedDB + sync syncer + tortoise tortoise + publisher pubsub.Publisher +} + +func NewPublisher( + logger *zap.Logger, + cdb *datastore.CachedDB, + sync syncer, + tortoise tortoise, + publisher pubsub.Publisher, +) *Publisher { + return &Publisher{ + logger: logger, + cdb: cdb, + sync: sync, + tortoise: tortoise, + publisher: publisher, + } +} + +func (p *Publisher) PublishATXProof(ctx context.Context, nodeID types.NodeID, proof []byte) error { + marriageID, err := marriage.FindIDByNodeID(p.cdb, nodeID) + switch { + case errors.Is(err, sql.ErrNotFound): // smesher is not married + malicious, err := malfeasance.IsMalicious(p.cdb, nodeID) + if err != nil { + return fmt.Errorf("check if smesher is malicious: %w", err) + } + if malicious { + p.logger.Debug("smesher is already marked as malicious", zap.String("smesher_id", nodeID.ShortString())) + return nil + } + if err := malfeasance.AddProof(p.cdb, nodeID, nil, proof, byte(InvalidActivation), time.Now()); err != nil { + return fmt.Errorf("setting malfeasance proof: %w", err) + } + // TODO(mafa): cache proof, right now caching it would clash with legacy malfeasance proofs + // arguably this shouldn't be needed at all, API queries the handler for info about a proof + // handler can decided if this needs caching or not + // + // p.cdb.CacheMalfeasanceProof(nodeID, proof) + p.tortoise.OnMalfeasance(nodeID) + + return p.publish(ctx, nodeID, nil, proof) // pass nil for certificates + case err != nil: + return fmt.Errorf("getting equivocation set: %w", err) + default: // smesher is married + } + + // Combine IDs from the present equivocation set for atx.SmesherID and IDs in atx.Marriages. + set, err := marriage.NodeIDsByID(p.cdb, marriageID) + if err != nil { + return fmt.Errorf("getting equivocation set: %w", err) + } + + publish := false // whether to publish the proof + malicious, err := malfeasance.IsMalicious(p.cdb, nodeID) + if err != nil { + return fmt.Errorf("check if smesher is malicious: %w", err) + } + if !malicious { + err := malfeasance.AddProof(p.cdb, nodeID, &marriageID, proof, byte(InvalidActivation), time.Now()) + if err != nil { + return fmt.Errorf("setting malfeasance proof: %w", err) + } + // TODO(mafa): cache proof, right now caching it would clash with legacy malfeasance proofs + // arguably this shouldn't be needed at all, API queries the handler for info about a proof + // handler can decided if this needs caching or not + // + // p.cdb.CacheMalfeasanceProof(nodeID, proof) + p.tortoise.OnMalfeasance(nodeID) + + publish = true + } + + for _, id := range set { + if id == nodeID { + // already handled + continue + } + malicious, err := malfeasance.IsMalicious(p.cdb, id) + if err != nil { + return fmt.Errorf("check if smesher is malicious: %w", err) + } + if malicious { + p.logger.Debug("smesher is already marked as malicious", zap.String("smesher_id", id.ShortString())) + continue + } + + publish = true + if err := malfeasance.SetMalicious(p.cdb, id, marriageID, time.Now()); err != nil { + return fmt.Errorf("setting malicious: %w", err) + } + // TODO(mafa): cache proof, right now caching it would clash with legacy malfeasance proofs + // arguably this shouldn't be needed at all, API queries the handler for info about a proof + // handler can decided if this needs caching or not + // + // p.cdb.CacheMalfeasanceProof(id, proof) + p.tortoise.OnMalfeasance(id) + } + + if !publish { + // all smeshers were already marked as malicious - no gossip to void spamming the network + return nil + } + + return p.publish(ctx, nodeID, nil, proof) // TODO(mafa): do not pass nil here for certificates +} + +func (p *Publisher) publish(ctx context.Context, nodeID types.NodeID, certs []ProofCertificate, proof []byte) error { + // Only gossip the proof if we are synced (to not spam the network with proofs others probably already have). + if !p.sync.ListenToATXGossip() { + p.logger.Debug("not in sync, not broadcasting malfeasance proof", + zap.String("smesher_id", nodeID.ShortString()), + ) + return nil + } + + malfeasanceProof := &MalfeasanceProof{ + Version: 0, + Certificates: certs, + Domain: InvalidActivation, + Proof: proof, + } + if err := p.publisher.Publish(ctx, pubsub.MalfeasanceProof2, codec.MustEncode(malfeasanceProof)); err != nil { + p.logger.Error("failed to broadcast malfeasance proof", zap.Error(err)) + return fmt.Errorf("broadcast atx malfeasance proof: %w", err) + } + + return nil +} diff --git a/malfeasance2/publisher_test.go b/malfeasance2/publisher_test.go new file mode 100644 index 0000000000..a27b89ee5f --- /dev/null +++ b/malfeasance2/publisher_test.go @@ -0,0 +1,3 @@ +package malfeasance2 + +// TODO(mafa): implement me diff --git a/malfeasance2/wire.go b/malfeasance2/wire.go new file mode 100644 index 0000000000..86b0c62572 --- /dev/null +++ b/malfeasance2/wire.go @@ -0,0 +1,43 @@ +package malfeasance2 + +import "github.com/spacemeshos/go-spacemesh/common/types" + +//go:generate scalegen + +// ProofDomain encodes the type of malfeasance proof. It is used to decide which domain generated the proof. +type ProofDomain byte + +const ( + InvalidActivation ProofDomain = iota + InvalidBallot + InvalidHareMsg +) + +// ProofVersion encodes the version of the malfeasance proof. +// At the moment this will always be 0. +type ProofVersion byte + +type MalfeasanceProof struct { + // Version is the version identifier of the proof. This can be used to extend the malfeasance proof in the future. + Version ProofVersion + + // 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"` + + // Domain encodes the domain for which the proof was created + Domain ProofDomain + // Proof is the domain specific proof. Its type depends on the ProofDomain. + Proof []byte `scale:"max=1048576"` // max size of proof is 1MiB +} + +type ProofCertificate struct { + // TargetID is the identity that was married to by the smesher. + TargetID types.NodeID + // SmesherID is the identity that signed the certificate. + SmesherID types.NodeID + // Signature is the signature of the certificate. + Signature types.EdSignature +} diff --git a/malfeasance2/wire_scale.go b/malfeasance2/wire_scale.go new file mode 100644 index 0000000000..c193bf0097 --- /dev/null +++ b/malfeasance2/wire_scale.go @@ -0,0 +1,126 @@ +// Code generated by github.com/spacemeshos/go-scale/scalegen. DO NOT EDIT. + +// nolint +package malfeasance2 + +import ( + "github.com/spacemeshos/go-scale" +) + +func (t *MalfeasanceProof) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeCompact8(enc, uint8(t.Version)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.Certificates, 1024) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeCompact8(enc, uint8(t.Domain)) + 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 *MalfeasanceProof) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + field, n, err := scale.DecodeCompact8(dec) + if err != nil { + return total, err + } + total += n + t.Version = ProofVersion(field) + } + { + field, n, err := scale.DecodeStructSliceWithLimit[ProofCertificate](dec, 1024) + if err != nil { + return total, err + } + total += n + t.Certificates = field + } + { + field, n, err := scale.DecodeCompact8(dec) + if err != nil { + return total, err + } + total += n + t.Domain = ProofDomain(field) + } + { + field, n, err := scale.DecodeByteSliceWithLimit(dec, 1048576) + if err != nil { + return total, err + } + total += n + t.Proof = field + } + return total, nil +} + +func (t *ProofCertificate) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeByteArray(enc, t.TargetID[:]) + 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 *ProofCertificate) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := scale.DecodeByteArray(dec, t.TargetID[:]) + if err != nil { + return total, err + } + total += n + } + { + 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/mesh/malfeasance.go b/mesh/malfeasance.go index fc074aa221..6b9b08c76b 100644 --- a/mesh/malfeasance.go +++ b/mesh/malfeasance.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" - "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" "github.com/spacemeshos/go-spacemesh/common/types" @@ -97,10 +96,6 @@ func (mh *MalfeasanceHandler) Validate(ctx context.Context, data wire.ProofData) return types.EmptyNodeID, errors.New("invalid ballot malfeasance proof") } -func (mh *MalfeasanceHandler) ReportProof(numProofs *prometheus.CounterVec) { - numProofs.WithLabelValues(multiBallots).Inc() -} - -func (mh *MalfeasanceHandler) ReportInvalidProof(numInvalidProofs *prometheus.CounterVec) { - numInvalidProofs.WithLabelValues(multiBallots).Inc() +func (mh *MalfeasanceHandler) ReportLabel() string { + return multiBallots } diff --git a/mesh/mesh_test.go b/mesh/mesh_test.go index 78ef3e9768..f76c1a77df 100644 --- a/mesh/mesh_test.go +++ b/mesh/mesh_test.go @@ -762,7 +762,7 @@ func TestProcessLayer(t *testing.T) { tm.mockTortoise.EXPECT().Updates().Return(c.updates) ensuresDatabaseConsistent(t, tm.cdb, c.updates) - err := tm.ProcessLayer(context.TODO(), lid) + err := tm.ProcessLayer(context.Background(), lid) if len(c.err) > 0 { require.ErrorContains(t, err, c.err) } else { @@ -962,7 +962,7 @@ func TestProcessLayerPerHareOutput(t *testing.T) { if c.onHare { tm.mockTortoise.EXPECT().OnHareOutput(c.lid, c.bid) } - err := tm.ProcessLayerPerHareOutput(context.TODO(), c.lid, c.bid, false) + err := tm.ProcessLayerPerHareOutput(context.Background(), c.lid, c.bid, false) if len(c.err) > 0 { require.ErrorContains(t, err, c.err) } diff --git a/miner/proposal_builder.go b/miner/proposal_builder.go index 1bd96bc192..d17d6ba04f 100644 --- a/miner/proposal_builder.go +++ b/miner/proposal_builder.go @@ -483,8 +483,6 @@ func (pb *ProposalBuilder) initSharedData(ctx context.Context, current types.Lay // // Additionally all activesets that are older than 2 epochs are deleted at the beginning of an epoch anyway, but // maybe we should revisit this when activesets are no longer bootstrapped. - // - // TODO(mafa): I'm still seeing SQL_BUSY errors in the logs, so for now I change this back to TxImmediate. return pb.db.WithTxImmediate(ctx, func(tx sql.Transaction) error { yes, err := activesets.Has(tx, pb.shared.active.id) if err != nil { diff --git a/node/node.go b/node/node.go index 841aaade2f..0c4530b56b 100644 --- a/node/node.go +++ b/node/node.go @@ -61,6 +61,7 @@ import ( "github.com/spacemeshos/go-spacemesh/layerpatrol" "github.com/spacemeshos/go-spacemesh/log" "github.com/spacemeshos/go-spacemesh/malfeasance" + "github.com/spacemeshos/go-spacemesh/malfeasance2" "github.com/spacemeshos/go-spacemesh/mesh" "github.com/spacemeshos/go-spacemesh/metrics" "github.com/spacemeshos/go-spacemesh/metrics/public" @@ -137,6 +138,7 @@ const ( ConStateLogger = "conState" ExecutorLogger = "executor" MalfeasanceLogger = "malfeasance" + Malfeasance2Logger = "malfeasance2" BootstrapLogger = "bootstrap" ) @@ -378,50 +380,51 @@ func New(opts ...Option) *App { // App is the cli app singleton. type App struct { *cobra.Command - fileLock *flock.Flock - signers []*signing.EdSigner - Config *config.Config - db sql.StateDatabase - apiDB sql.StateDatabase - cachedDB *datastore.CachedDB - dbMetrics *dbmetrics.DBMetricsCollector - localDB sql.LocalDatabase - grpcPublicServer *grpcserver.Server - grpcPrivateServer *grpcserver.Server - grpcPostServer *grpcserver.Server - grpcTLSServer *grpcserver.Server - jsonAPIServer *grpcserver.JSONHTTPServer - grpcServices map[grpcserver.Service]grpcserver.ServiceAPI - pprofService *http.Server - profilerService *pyroscope.Profiler - syncer *syncer.Syncer - proposalBuilder *miner.ProposalBuilder - mesh *mesh.Mesh - atxsdata *atxsdata.Data - clock *timesync.NodeClock - hare3 *hare3.Hare - hare4 *hare4.Hare - hareResultsChan chan hare4.ConsensusOutput - hOracle *eligibility.Oracle - blockGen *blocks.Generator - certifier *blocks.Certifier - atxBuilder *activation.Builder - atxHandler *activation.Handler - txHandler *txs.TxHandler - validator *activation.Validator - edVerifier *signing.EdVerifier - beaconProtocol *beacon.ProtocolDriver - log log.Log - syncLogger log.Log - conState *txs.ConservativeState - fetcher *fetch.Fetch - ptimesync *peersync.Sync - updater *bootstrap.Updater - poetDb *activation.PoetDb - postVerifier activation.PostVerifier - postSupervisor *activation.PostSupervisor - malfeasanceHandler *malfeasance.Handler - errCh chan error + fileLock *flock.Flock + signers []*signing.EdSigner + Config *config.Config + db sql.StateDatabase + apiDB sql.StateDatabase + cachedDB *datastore.CachedDB + dbMetrics *dbmetrics.DBMetricsCollector + localDB sql.LocalDatabase + grpcPublicServer *grpcserver.Server + grpcPrivateServer *grpcserver.Server + grpcPostServer *grpcserver.Server + grpcTLSServer *grpcserver.Server + jsonAPIServer *grpcserver.JSONHTTPServer + grpcServices map[grpcserver.Service]grpcserver.ServiceAPI + pprofService *http.Server + profilerService *pyroscope.Profiler + syncer *syncer.Syncer + proposalBuilder *miner.ProposalBuilder + mesh *mesh.Mesh + atxsdata *atxsdata.Data + clock *timesync.NodeClock + hare3 *hare3.Hare + hare4 *hare4.Hare + hareResultsChan chan hare4.ConsensusOutput + hOracle *eligibility.Oracle + blockGen *blocks.Generator + certifier *blocks.Certifier + atxBuilder *activation.Builder + atxHandler *activation.Handler + txHandler *txs.TxHandler + validator *activation.Validator + edVerifier *signing.EdVerifier + beaconProtocol *beacon.ProtocolDriver + log log.Log + syncLogger log.Log + conState *txs.ConservativeState + fetcher *fetch.Fetch + ptimesync *peersync.Sync + updater *bootstrap.Updater + poetDb *activation.PoetDb + postVerifier activation.PostVerifier + postSupervisor *activation.PostSupervisor + malfeasanceHandler *malfeasance.Handler + malfeasance2Handler *malfeasance2.Handler + errCh chan error host *p2p.Host @@ -833,15 +836,32 @@ func (app *App) initServices(ctx context.Context) error { beaconProtocol.SetSyncState(syncer) hOracle.SetSync(syncer) - malfeasanceLogger := app.addLogger(MalfeasanceLogger, lg).Zap() + legacyMalLogger := app.addLogger(MalfeasanceLogger, lg).Zap() legacyMalPublisher := malfeasance.NewPublisher( - malfeasanceLogger, + legacyMalLogger, app.cachedDB, syncer, trtl, app.host, ) + malfeasanceLogger := app.addLogger(Malfeasance2Logger, lg).Zap() + malfeasancePublisher := malfeasance2.NewPublisher( + malfeasanceLogger, + app.cachedDB, + syncer, + trtl, + app.host, + ) + atxMalHandler := activation.NewMalfeasanceHandlerV2( + malfeasanceLogger, + malfeasancePublisher, + app.edVerifier, + validator, + ) + for _, sig := range app.signers { + atxMalHandler.Register(sig) + } atxHandler := activation.NewHandler( app.host.ID(), app.cachedDB, @@ -851,6 +871,7 @@ func (app *App) initServices(ctx context.Context) error { fetcher, goldenATXID, validator, + atxMalHandler, legacyMalPublisher, beaconProtocol, trtl, @@ -1134,18 +1155,18 @@ func (app *App) initServices(ctx context.Context) error { activationMH := activation.NewMalfeasanceHandler( app.cachedDB, - malfeasanceLogger, + legacyMalLogger, app.edVerifier, ) meshMH := mesh.NewMalfeasanceHandler( app.cachedDB, app.edVerifier, - mesh.WithMalfeasanceLogger(malfeasanceLogger), + mesh.WithMalfeasanceLogger(legacyMalLogger), ) hareMH := hare3.NewMalfeasanceHandler( app.cachedDB, app.edVerifier, - hare3.WithMalfeasanceLogger(malfeasanceLogger), + hare3.WithMalfeasanceLogger(legacyMalLogger), ) invalidPostMH := activation.NewInvalidPostIndexHandler( app.cachedDB, @@ -1158,18 +1179,28 @@ func (app *App) initServices(ctx context.Context) error { for _, s := range app.signers { nodeIDs = append(nodeIDs, s.NodeID()) } - app.malfeasanceHandler = malfeasance.NewHandler( + malHandler := malfeasance.NewHandler( + app.cachedDB, + legacyMalLogger, + app.host.ID(), + nodeIDs, + trtl, + ) + malHandler.RegisterHandler(malfeasance.MultipleATXs, activationMH) + malHandler.RegisterHandler(malfeasance.MultipleBallots, meshMH) + malHandler.RegisterHandler(malfeasance.HareEquivocation, hareMH) + malHandler.RegisterHandler(malfeasance.InvalidPostIndex, invalidPostMH) + malHandler.RegisterHandler(malfeasance.InvalidPrevATX, invalidPrevMH) + + malHandler2 := malfeasance2.NewHandler( app.cachedDB, malfeasanceLogger, app.host.ID(), nodeIDs, + app.edVerifier, trtl, ) - app.malfeasanceHandler.RegisterHandler(malfeasance.MultipleATXs, activationMH) - app.malfeasanceHandler.RegisterHandler(malfeasance.MultipleBallots, meshMH) - app.malfeasanceHandler.RegisterHandler(malfeasance.HareEquivocation, hareMH) - app.malfeasanceHandler.RegisterHandler(malfeasance.InvalidPostIndex, invalidPostMH) - app.malfeasanceHandler.RegisterHandler(malfeasance.InvalidPrevATX, invalidPrevMH) + malHandler2.RegisterHandler(malfeasance2.InvalidActivation, atxMalHandler) fetcher.SetValidators( fetch.ValidatorFunc( @@ -1214,11 +1245,19 @@ func (app *App) initServices(ctx context.Context) error { ), fetch.ValidatorFunc( pubsub.DropPeerOnSyncValidationReject( - app.malfeasanceHandler.HandleSyncedMalfeasanceProof, + malHandler.HandleSyncedMalfeasanceProof, app.host, lg.Zap(), ), ), + // TODO(mafa): add malfeasance2 handler to fetcher + // fetch.ValidatorFunc( + // pubsub.DropPeerOnSyncValidationReject( + // malHandler2.HandleSyncedMalfeasanceProof, + // app.host, + // lg.Zap(), + // ), + // ), ) checkSynced := func(_ context.Context, _ p2p.Peer, _ []byte) error { @@ -1275,7 +1314,11 @@ func (app *App) initServices(ctx context.Context) error { ) app.host.Register( pubsub.MalfeasanceProof, - pubsub.ChainGossipHandler(checkAtxSynced, app.malfeasanceHandler.HandleMalfeasanceProof), + pubsub.ChainGossipHandler(checkAtxSynced, malHandler.HandleMalfeasanceProof), + ) + app.host.Register( + pubsub.MalfeasanceProof2, + pubsub.ChainGossipHandler(checkAtxSynced, malHandler2.HandleGossip), ) app.proposalBuilder = proposalBuilder @@ -1283,6 +1326,8 @@ func (app *App) initServices(ctx context.Context) error { app.syncer = syncer app.atxBuilder = atxBuilder app.atxHandler = atxHandler + app.malfeasanceHandler = malHandler + app.malfeasance2Handler = malHandler2 app.poetDb = poetDb app.fetcher = fetcher app.beaconProtocol = beaconProtocol @@ -1570,10 +1615,14 @@ func (app *App) grpcService(svc grpcserver.Service, lg log.Log) (grpcserver.Serv app.grpcServices[svc] = service return service, nil case v2alpha1.Malfeasance: + // TODO(mafa): update to also use malfeasance2 handler + _ = app.malfeasance2Handler service := v2alpha1.NewMalfeasanceService(app.apiDB, app.malfeasanceHandler) app.grpcServices[svc] = service return service, nil case v2alpha1.MalfeasanceStream: + // TODO(mafa): update to also use malfeasance2 handler + _ = app.malfeasance2Handler service := v2alpha1.NewMalfeasanceStreamService(app.apiDB, app.malfeasanceHandler) app.grpcServices[svc] = service return service, nil diff --git a/node/node_test.go b/node/node_test.go index a777bfd0c9..835bb69fbb 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -1225,7 +1225,7 @@ func launchPostSupervisor( provingOpts := activation.DefaultPostProvingOpts() provingOpts.RandomXMode = activation.PostRandomXModeLight - builder := activation.NewMockAtxBuilder(gomock.NewController(tb)) + builder := activation.NewMockatxBuilder(gomock.NewController(tb)) builder.EXPECT().Register(sig) ps := activation.NewPostSupervisor(log, postCfg, provingOpts, mgr, builder) require.NoError(tb, ps.Start(cmdCfg, postOpts, sig)) diff --git a/p2p/pubsub/pubsub.go b/p2p/pubsub/pubsub.go index 9d181b2313..2bb8fd855f 100644 --- a/p2p/pubsub/pubsub.go +++ b/p2p/pubsub/pubsub.go @@ -76,7 +76,10 @@ const ( // BeaconFollowingVotesProtocol is the protocol id for beacon following votes. BeaconFollowingVotesProtocol = "bo1" + // MalfeasanceProof is the protocol id for malfeasance proofs (soon to be deprecated). MalfeasanceProof = "mp1" + // MalfeasanceProof2 is the protocol id for V2 malfeasance proofs. + MalfeasanceProof2 = "mp2" ) // DefaultConfig for PubSub. diff --git a/p2p/server/server.go b/p2p/server/server.go index 3a3925735c..ebc424cc24 100644 --- a/p2p/server/server.go +++ b/p2p/server/server.go @@ -138,7 +138,7 @@ func (err *ServerError) Error() string { type Response struct { // keep in line with limit of ResponseMessage.Data in `fetch/wire_types.go` Data []byte `scale:"max=272629760"` // 260 MiB > 8.0 mio ATX * 32 bytes per ID - Error string `scale:"max=1024"` // TODO(mafa): make error code instead of string + Error string `scale:"max=1024"` } // Server for the Handler. diff --git a/sql/malfeasance/malfeasance.go b/sql/malfeasance/malfeasance.go index c45c47895d..32731b60c3 100644 --- a/sql/malfeasance/malfeasance.go +++ b/sql/malfeasance/malfeasance.go @@ -28,7 +28,7 @@ func AddProof( nodeID types.NodeID, marriageID *marriage.ID, proof []byte, - domain int, + domain byte, received time.Time, ) error { _, err := db.Exec(` diff --git a/sync2/multipeer/multipeer.go b/sync2/multipeer/multipeer.go index f0b961f9d8..efadbe8d53 100644 --- a/sync2/multipeer/multipeer.go +++ b/sync2/multipeer/multipeer.go @@ -110,7 +110,7 @@ func DefaultConfig() MultiPeerReconcilerConfig { } } -// MultiPeerReconciler reconcilies the local set against multiple remote sets. +// MultiPeerReconciler reconciles the local set against multiple remote sets. type MultiPeerReconciler struct { logger *zap.Logger cfg MultiPeerReconcilerConfig diff --git a/syncer/find_fork_test.go b/syncer/find_fork_test.go index 7276cbda36..f714b82305 100644 --- a/syncer/find_fork_test.go +++ b/syncer/find_fork_test.go @@ -3,7 +3,6 @@ package syncer_test import ( "context" "encoding/binary" - "fmt" "math/rand/v2" "strconv" "testing" @@ -108,7 +107,7 @@ func serveHashReq(tb testing.TB, req *fetch.MeshHashRequest) (*fetch.MeshHashes, hashes = append(hashes, layerHash(int(req.To.Uint32()), true)) expCount := int(req.Count()) - require.Len(tb, hashes, expCount, fmt.Sprintf("%#v; count exp: %v, got %v", req, expCount, len(hashes))) + require.Lenf(tb, hashes, expCount, "%#v; count exp: %v, got %v", req, expCount, len(hashes)) mh := &fetch.MeshHashes{ Hashes: hashes, } @@ -133,7 +132,7 @@ func TestForkFinder_FindFork_Permutation(t *testing.T) { }).AnyTimes() fork, err := tf.FindFork(context.Background(), peer, types.LayerID(uint32(lid)), layerHash(lid, true)) - require.NoError(t, err, fmt.Sprintf("lid: %v", lid)) + require.NoErrorf(t, err, "lid: %v", lid) require.Equal(t, expected, int(fork)) } } diff --git a/systest/tests/distributed_post_verification_test.go b/systest/tests/distributed_post_verification_test.go index 94784843f9..513466bdde 100644 --- a/systest/tests/distributed_post_verification_test.go +++ b/systest/tests/distributed_post_verification_test.go @@ -163,7 +163,7 @@ func TestPostMalfeasanceProof(t *testing.T) { ) require.NoError(t, err) - builder := activation.NewMockAtxBuilder(ctrl) + builder := activation.NewMockatxBuilder(ctrl) builder.EXPECT().Register(signer) postSupervisor := activation.NewPostSupervisor( logger.Named("post-supervisor"), diff --git a/systest/tests/smeshing_test.go b/systest/tests/smeshing_test.go index 6ce1b74992..2d13a3f494 100644 --- a/systest/tests/smeshing_test.go +++ b/systest/tests/smeshing_test.go @@ -3,6 +3,7 @@ package tests import ( "bytes" "context" + "errors" "fmt" "os" "sort" @@ -74,7 +75,12 @@ func testSmeshing(t *testing.T, tctx *testcontext.Context, cl *cluster.Cluster) includedAll[i] = map[uint32][]*pb.Proposal{} } - eg, ctx := errgroup.WithContext(tctx) + layerDuration := testcontext.LayerDuration.Get(tctx.Parameters) + deadline := cl.Genesis().Add(time.Duration(last+2*layersPerEpoch) * layerDuration) // add 2 epochs of buffer + ctx, cancel := context.WithDeadline(tctx, deadline) + defer cancel() + + eg, ctx := errgroup.WithContext(ctx) for i := range cl.Total() { client := cl.Client(i) tctx.Log.Debugw("watching", "client", client.Name, "i", i) @@ -82,21 +88,38 @@ func testSmeshing(t *testing.T, tctx *testcontext.Context, cl *cluster.Cluster) if proposal.Layer.Number < first { return true, nil } - tctx.Log.Debugw("received proposal event", + if proposal.Layer.Number > last { + return false, nil + } + if proposal.Status == pb.Proposal_Created { + tctx.Log.Debugw("received proposal created event", + "client", client.Name, + "layer", proposal.Layer.Number, + "smesher", prettyHex(proposal.Smesher.Id), + "eligibilities", len(proposal.Eligibilities), + ) + select { + case createdCh <- proposal: + case <-ctx.Done(): + return false, ctx.Err() + default: + tctx.Log.Errorw("proposal channel is full", + "client", client.Name, + "layer", proposal.Layer.Number, + ) + return false, errors.New("proposal channel is full") + } + return true, nil + } + + tctx.Log.Debugw("received other proposal event", "client", client.Name, "layer", proposal.Layer.Number, "smesher", prettyHex(proposal.Smesher.Id), "eligibilities", len(proposal.Eligibilities), "status", pb.Proposal_Status_name[int32(proposal.Status)], ) - if proposal.Layer.Number > last { - return false, nil - } - if proposal.Status == pb.Proposal_Created { - createdCh <- proposal - } else { - includedAll[i][proposal.Layer.Number] = append(includedAll[i][proposal.Layer.Number], proposal) - } + includedAll[i][proposal.Layer.Number] = append(includedAll[i][proposal.Layer.Number], proposal) return true, nil }) } diff --git a/systest/tests/steps_test.go b/systest/tests/steps_test.go index dd1369b106..5d284ee264 100644 --- a/systest/tests/steps_test.go +++ b/systest/tests/steps_test.go @@ -189,10 +189,10 @@ func TestStepReplaceNodes(t *testing.T) { require.NoError(t, err) var ( - delete = rand.Intn(cctx.ClusterSize*2/10) + 1 + toDelete = rand.Intn(cctx.ClusterSize*2/10) + 1 deleting []*cluster.NodeClient ) - for i := cl.Bootnodes(); i < cl.Total() && len(deleting) < delete; i++ { + for i := cl.Bootnodes(); i < cl.Total() && len(deleting) < toDelete; i++ { node := cl.Client(i) // don't replace non-synced nodes if !isSynced(cctx, node) { diff --git a/timesync/peersync/sync_test.go b/timesync/peersync/sync_test.go index 11ad013e27..6654c3f1c6 100644 --- a/timesync/peersync/sync_test.go +++ b/timesync/peersync/sync_test.go @@ -51,7 +51,7 @@ func TestSyncGetOffset(t *testing.T) { require.NotNil(t, New(h, nil, WithTime(adjustedTime(peerResponse)))) } sync := New(mesh.Hosts()[0], nil, WithTime(tm)) - offset, err := sync.GetOffset(context.TODO(), 0, peers) + offset, err := sync.GetOffset(context.Background(), 0, peers) require.NoError(t, err) require.Equal(t, 5*time.Second, offset) }) @@ -70,7 +70,7 @@ func TestSyncGetOffset(t *testing.T) { } sync := New(mesh.Hosts()[0], nil, WithTime(tm)) - offset, err := sync.GetOffset(context.TODO(), 0, peers) + offset, err := sync.GetOffset(context.Background(), 0, peers) require.ErrorIs(t, err, errTimesyncFailed) require.Empty(t, offset) }) diff --git a/tortoise/model/core.go b/tortoise/model/core.go index d8bbd963d3..cd762fb102 100644 --- a/tortoise/model/core.go +++ b/tortoise/model/core.go @@ -100,7 +100,7 @@ func (c *core) OnMessage(m Messenger, event Message) { }) c.eligibilities = max(uint32(c.weight*layerSize/total), 1) } - votes, err := c.tortoise.EncodeVotes(context.TODO()) + votes, err := c.tortoise.EncodeVotes(context.Background()) if err != nil { panic(err) }