diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index 8b1fdab98c..fecd29ae76 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -271,7 +271,7 @@ func Test_MarryAndMerge(t *testing.T) { mpub := mocks.NewMockPublisher(ctrl) mFetch := smocks.NewMockFetcher(ctrl) - mBeacon := activation.NewMockAtxReceiver(ctrl) + mBeacon := activation.NewMockatxReceiver(ctrl) mTortoise := smocks.NewMockTortoise(ctrl) tickSize := uint64(3) diff --git a/activation/e2e/builds_atx_v2_test.go b/activation/e2e/builds_atx_v2_test.go index af8fad6a55..791c8674c8 100644 --- a/activation/e2e/builds_atx_v2_test.go +++ b/activation/e2e/builds_atx_v2_test.go @@ -116,7 +116,7 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { edVerifier := signing.NewEdVerifier() mpub := mocks.NewMockPublisher(ctrl) mFetch := smocks.NewMockFetcher(ctrl) - mBeacon := activation.NewMockAtxReceiver(ctrl) + mBeacon := activation.NewMockatxReceiver(ctrl) mTortoise := smocks.NewMockTortoise(ctrl) atxHdlr := activation.NewHandler( diff --git a/activation/e2e/checkpoint_merged_test.go b/activation/e2e/checkpoint_merged_test.go index 970ac803b3..c280f4a1f0 100644 --- a/activation/e2e/checkpoint_merged_test.go +++ b/activation/e2e/checkpoint_merged_test.go @@ -106,7 +106,7 @@ func Test_CheckpointAfterMerge(t *testing.T) { mpub := mocks.NewMockPublisher(ctrl) mFetch := smocks.NewMockFetcher(ctrl) - mBeacon := activation.NewMockAtxReceiver(ctrl) + mBeacon := activation.NewMockatxReceiver(ctrl) mTortoise := smocks.NewMockTortoise(ctrl) atxHdlr := activation.NewHandler( diff --git a/activation/e2e/checkpoint_test.go b/activation/e2e/checkpoint_test.go index 0e28ee72d9..b398c7c575 100644 --- a/activation/e2e/checkpoint_test.go +++ b/activation/e2e/checkpoint_test.go @@ -102,7 +102,7 @@ func TestCheckpoint_PublishingSoloATXs(t *testing.T) { edVerifier := signing.NewEdVerifier() mpub := mocks.NewMockPublisher(ctrl) mFetch := smocks.NewMockFetcher(ctrl) - mBeacon := activation.NewMockAtxReceiver(ctrl) + mBeacon := activation.NewMockatxReceiver(ctrl) mTortoise := smocks.NewMockTortoise(ctrl) atxHdlr := activation.NewHandler( diff --git a/activation/handler.go b/activation/handler.go index da99dd999d..1dd084c59a 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -106,7 +106,7 @@ func NewHandler( fetcher system.Fetcher, goldenATXID types.ATXID, nipostValidator nipostValidator, - beacon AtxReceiver, + beacon atxReceiver, tortoise system.Tortoise, lg *zap.Logger, opts ...HandlerOption, diff --git a/activation/handler_test.go b/activation/handler_test.go index 29a0f6e3e7..75712ed707 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -124,7 +124,7 @@ type handlerMocks struct { mpub *pubsubmocks.MockPublisher mockFetch *mocks.MockFetcher mValidator *MocknipostValidator - mbeacon *MockAtxReceiver + mbeacon *MockatxReceiver mtortoise *mocks.MockTortoise mMalPublish *MockmalfeasancePublisher } @@ -188,7 +188,7 @@ func newTestHandlerMocks(tb testing.TB, golden types.ATXID) handlerMocks { mpub: pubsubmocks.NewMockPublisher(ctrl), mockFetch: mocks.NewMockFetcher(ctrl), mValidator: NewMocknipostValidator(ctrl), - mbeacon: NewMockAtxReceiver(ctrl), + mbeacon: NewMockatxReceiver(ctrl), mtortoise: mocks.NewMockTortoise(ctrl), mMalPublish: NewMockmalfeasancePublisher(ctrl), } diff --git a/activation/handler_v1.go b/activation/handler_v1.go index a6bb961ec3..0db3b0a35e 100644 --- a/activation/handler_v1.go +++ b/activation/handler_v1.go @@ -76,7 +76,7 @@ type HandlerV1 struct { tickSize uint64 goldenATXID types.ATXID nipostValidator nipostValidatorV1 - beacon AtxReceiver + beacon atxReceiver tortoise system.Tortoise logger *zap.Logger fetcher system.Fetcher diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 880d9ca6cb..776170a415 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -64,7 +64,7 @@ type HandlerV2 struct { tickSize uint64 goldenATXID types.ATXID nipostValidator nipostValidatorV2 - beacon AtxReceiver + beacon atxReceiver tortoise system.Tortoise logger *zap.Logger fetcher system.Fetcher diff --git a/activation/interface.go b/activation/interface.go index 5bd649f5ae..e036b39394 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -19,7 +19,7 @@ import ( //go:generate mockgen -typed -package=activation -destination=./mocks.go -source=./interface.go -type AtxReceiver interface { +type atxReceiver interface { OnAtx(*types.ActivationTx) } diff --git a/activation/mocks.go b/activation/mocks.go index 38a1a47206..7809780388 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -24,61 +24,61 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockAtxReceiver is a mock of AtxReceiver interface. -type MockAtxReceiver struct { +// MockatxReceiver is a mock of atxReceiver interface. +type MockatxReceiver struct { ctrl *gomock.Controller - recorder *MockAtxReceiverMockRecorder + recorder *MockatxReceiverMockRecorder } -// MockAtxReceiverMockRecorder is the mock recorder for MockAtxReceiver. -type MockAtxReceiverMockRecorder struct { - mock *MockAtxReceiver +// MockatxReceiverMockRecorder is the mock recorder for MockatxReceiver. +type MockatxReceiverMockRecorder struct { + mock *MockatxReceiver } -// NewMockAtxReceiver creates a new mock instance. -func NewMockAtxReceiver(ctrl *gomock.Controller) *MockAtxReceiver { - mock := &MockAtxReceiver{ctrl: ctrl} - mock.recorder = &MockAtxReceiverMockRecorder{mock} +// NewMockatxReceiver creates a new mock instance. +func NewMockatxReceiver(ctrl *gomock.Controller) *MockatxReceiver { + mock := &MockatxReceiver{ctrl: ctrl} + mock.recorder = &MockatxReceiverMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockAtxReceiver) EXPECT() *MockAtxReceiverMockRecorder { +func (m *MockatxReceiver) EXPECT() *MockatxReceiverMockRecorder { return m.recorder } // OnAtx mocks base method. -func (m *MockAtxReceiver) OnAtx(arg0 *types.ActivationTx) { +func (m *MockatxReceiver) OnAtx(arg0 *types.ActivationTx) { m.ctrl.T.Helper() m.ctrl.Call(m, "OnAtx", arg0) } // OnAtx indicates an expected call of OnAtx. -func (mr *MockAtxReceiverMockRecorder) OnAtx(arg0 any) *MockAtxReceiverOnAtxCall { +func (mr *MockatxReceiverMockRecorder) OnAtx(arg0 any) *MockatxReceiverOnAtxCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnAtx", reflect.TypeOf((*MockAtxReceiver)(nil).OnAtx), arg0) - return &MockAtxReceiverOnAtxCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnAtx", reflect.TypeOf((*MockatxReceiver)(nil).OnAtx), arg0) + return &MockatxReceiverOnAtxCall{Call: call} } -// MockAtxReceiverOnAtxCall wrap *gomock.Call -type MockAtxReceiverOnAtxCall struct { +// MockatxReceiverOnAtxCall wrap *gomock.Call +type MockatxReceiverOnAtxCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockAtxReceiverOnAtxCall) Return() *MockAtxReceiverOnAtxCall { +func (c *MockatxReceiverOnAtxCall) Return() *MockatxReceiverOnAtxCall { c.Call = c.Call.Return() return c } // Do rewrite *gomock.Call.Do -func (c *MockAtxReceiverOnAtxCall) Do(f func(*types.ActivationTx)) *MockAtxReceiverOnAtxCall { +func (c *MockatxReceiverOnAtxCall) Do(f func(*types.ActivationTx)) *MockatxReceiverOnAtxCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockAtxReceiverOnAtxCall) DoAndReturn(f func(*types.ActivationTx)) *MockAtxReceiverOnAtxCall { +func (c *MockatxReceiverOnAtxCall) DoAndReturn(f func(*types.ActivationTx)) *MockatxReceiverOnAtxCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/checkpoint/recovery_test.go b/checkpoint/recovery_test.go index eb0e6d0a58..ae01570c86 100644 --- a/checkpoint/recovery_test.go +++ b/checkpoint/recovery_test.go @@ -251,7 +251,7 @@ func validateAndPreserveData( mclock := activation.NewMocklayerClock(ctrl) mfetch := smocks.NewMockFetcher(ctrl) mvalidator := activation.NewMocknipostValidator(ctrl) - mreceiver := activation.NewMockAtxReceiver(ctrl) + mreceiver := activation.NewMockatxReceiver(ctrl) mtrtl := smocks.NewMockTortoise(ctrl) cdb := datastore.NewCachedDB(db, lg) atxHandler := activation.NewHandler( diff --git a/malfeasance/handler.go b/malfeasance/handler.go index 45acdfcd54..161292ab60 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -32,31 +32,22 @@ var ( type MalfeasanceType byte const ( - // V1 types. MultipleATXs MalfeasanceType = MalfeasanceType(wire.MultipleATXs) MultipleBallots = MalfeasanceType(wire.MultipleBallots) HareEquivocation = MalfeasanceType(wire.HareEquivocation) InvalidPostIndex = MalfeasanceType(wire.InvalidPostIndex) InvalidPrevATX = MalfeasanceType(wire.InvalidPrevATX) - - // V2 types - // TODO(mafa): for future use. - InvalidActivation MalfeasanceType = iota + 10 - InvalidBallot - InvalidHareMsg ) // Handler processes MalfeasanceProof from gossip and, if deems it valid, propagates it to peers. type Handler struct { - logger *zap.Logger - cdb *datastore.CachedDB - - handlersV1 map[MalfeasanceType]HandlerV1 - handlersV2 map[MalfeasanceType]HandlerV2 - + logger *zap.Logger + cdb *datastore.CachedDB self p2p.Peer nodeIDs []types.NodeID tortoise tortoise + + handlers map[MalfeasanceType]MalfeasanceHandler } func NewHandler( @@ -73,17 +64,12 @@ func NewHandler( nodeIDs: nodeID, tortoise: tortoise, - handlersV1: make(map[MalfeasanceType]HandlerV1), - handlersV2: make(map[MalfeasanceType]HandlerV2), + handlers: make(map[MalfeasanceType]MalfeasanceHandler), } } -func (h *Handler) RegisterHandlerV1(malfeasanceType MalfeasanceType, handler HandlerV1) { - h.handlersV1[malfeasanceType] = handler -} - -func (h *Handler) RegisterHandlerV2(malfeasanceType MalfeasanceType, handler HandlerV2) { - h.handlersV2[malfeasanceType] = handler +func (h *Handler) RegisterHandler(malfeasanceType MalfeasanceType, handler MalfeasanceHandler) { + h.handlers[malfeasanceType] = handler } func (h *Handler) reportMalfeasance(smesher types.NodeID, mp *wire.MalfeasanceProof) { @@ -95,11 +81,11 @@ func (h *Handler) reportMalfeasance(smesher types.NodeID, mp *wire.MalfeasancePr } func (h *Handler) countProof(mp *wire.MalfeasanceProof) { - h.handlersV1[MalfeasanceType(mp.Proof.Type)].ReportProof(numProofs) + h.handlers[MalfeasanceType(mp.Proof.Type)].ReportProof(numProofs) } func (h *Handler) countInvalidProof(p *wire.MalfeasanceProof) { - h.handlersV1[MalfeasanceType(p.Proof.Type)].ReportInvalidProof(numInvalidProofs) + h.handlers[MalfeasanceType(p.Proof.Type)].ReportInvalidProof(numInvalidProofs) } // HandleSyncedMalfeasanceProof is the sync validator for MalfeasanceProof. @@ -115,7 +101,7 @@ func (h *Handler) HandleSyncedMalfeasanceProof( h.logger.Error("malformed message (sync)", log.ZContext(ctx), zap.Error(err)) return errMalformedData } - nodeID, err := h.validateAndSave(ctx, &wire.MalfeasanceGossip{MalfeasanceProof: p}) + nodeID, err := h.validateAndSave(ctx, &p) if err == nil && types.Hash32(nodeID) != expHash { return fmt.Errorf( "%w: malfeasance proof want %s, got %s", @@ -135,52 +121,33 @@ func (h *Handler) HandleMalfeasanceProof(ctx context.Context, peer p2p.Peer, dat h.logger.Error("malformed message", log.ZContext(ctx), zap.Error(err)) return errMalformedData } - if peer == h.self { - id, err := h.Validate(ctx, &p) - if err != nil { - h.countInvalidProof(&p.MalfeasanceProof) - return err - } - h.reportMalfeasance(id, &p.MalfeasanceProof) - // node saves malfeasance proof eagerly/atomically with the malicious data. - // it has validated the proof before saving to db. - h.countProof(&p.MalfeasanceProof) - return nil + if p.Eligibility != nil { + return fmt.Errorf("%w: eligibility field was deprecated with hare3", pubsub.ErrValidationReject) } - _, err := h.validateAndSave(ctx, &p) + _, err := h.validateAndSave(ctx, &p.MalfeasanceProof) return err } -func (h *Handler) validateAndSave(ctx context.Context, p *wire.MalfeasanceGossip) (types.NodeID, error) { - if p.Eligibility != nil { - numMalformed.Inc() - return types.EmptyNodeID, fmt.Errorf( - "%w: eligibility field was deprecated with hare3", - pubsub.ErrValidationReject, - ) - } +func (h *Handler) validateAndSave(ctx context.Context, p *wire.MalfeasanceProof) (types.NodeID, error) { nodeID, err := h.Validate(ctx, p) switch { case errors.Is(err, errUnknownProof): numMalformed.Inc() return types.EmptyNodeID, err case err != nil: - h.countInvalidProof(&p.MalfeasanceProof) + h.countInvalidProof(p) return types.EmptyNodeID, errors.Join(err, pubsub.ErrValidationReject) } if err := h.cdb.WithTx(ctx, func(dbtx sql.Transaction) error { malicious, err := identities.IsMalicious(dbtx, nodeID) if err != nil { return fmt.Errorf("check known malicious: %w", err) - } else if malicious { + } + if malicious { h.logger.Debug("known malicious identity", log.ZContext(ctx), zap.Stringer("smesher", nodeID)) return ErrKnownProof } - encoded, err := codec.Encode(&p.MalfeasanceProof) - if err != nil { - h.logger.Panic("failed to encode MalfeasanceProof", zap.Error(err)) - } - if err := identities.SetMalicious(dbtx, nodeID, encoded, time.Now()); err != nil { + if err := identities.SetMalicious(dbtx, nodeID, codec.MustEncode(p), time.Now()); err != nil { return fmt.Errorf("add malfeasance proof: %w", err) } return nil @@ -193,11 +160,11 @@ func (h *Handler) validateAndSave(ctx context.Context, p *wire.MalfeasanceGossip zap.Error(err), ) } - return types.EmptyNodeID, err + return nodeID, err } - h.reportMalfeasance(nodeID, &p.MalfeasanceProof) - h.cdb.CacheMalfeasanceProof(nodeID, &p.MalfeasanceProof) - h.countProof(&p.MalfeasanceProof) + h.reportMalfeasance(nodeID, p) + h.cdb.CacheMalfeasanceProof(nodeID, p) + h.countProof(p) h.logger.Debug("new malfeasance proof", log.ZContext(ctx), zap.Stringer("smesher", nodeID), @@ -206,8 +173,8 @@ func (h *Handler) validateAndSave(ctx context.Context, p *wire.MalfeasanceGossip return nodeID, nil } -func (h *Handler) Validate(ctx context.Context, p *wire.MalfeasanceGossip) (types.NodeID, error) { - mh, ok := h.handlersV1[MalfeasanceType(p.Proof.Type)] +func (h *Handler) Validate(ctx context.Context, p *wire.MalfeasanceProof) (types.NodeID, error) { + mh, ok := h.handlers[MalfeasanceType(p.Proof.Type)] if !ok { return types.EmptyNodeID, fmt.Errorf("%w: unknown malfeasance type", errUnknownProof) } diff --git a/malfeasance/handler_test.go b/malfeasance/handler_test.go index 86532ce677..da1e1ebe15 100644 --- a/malfeasance/handler_test.go +++ b/malfeasance/handler_test.go @@ -92,7 +92,7 @@ func TestHandler_HandleMalfeasanceProof(t *testing.T) { h := newHandler(t) ctrl := gomock.NewController(t) - handler := NewMockHandlerV1(ctrl) + handler := NewMockMalfeasanceHandler(ctrl) handler.EXPECT().Validate(gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, data wire.ProofData) (types.NodeID, error) { require.IsType(t, &wire.AtxProof{}, data) @@ -100,7 +100,7 @@ func TestHandler_HandleMalfeasanceProof(t *testing.T) { }, ) handler.EXPECT().ReportInvalidProof(gomock.Any()) - h.RegisterHandlerV1(MultipleATXs, handler) + h.RegisterHandler(MultipleATXs, handler) gossip := &wire.MalfeasanceGossip{ MalfeasanceProof: wire.MalfeasanceProof{ @@ -122,7 +122,7 @@ func TestHandler_HandleMalfeasanceProof(t *testing.T) { nodeID := types.RandomNodeID() ctrl := gomock.NewController(t) - handler := NewMockHandlerV1(ctrl) + handler := NewMockMalfeasanceHandler(ctrl) handler.EXPECT().Validate(gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, data wire.ProofData) (types.NodeID, error) { require.IsType(t, &wire.AtxProof{}, data) @@ -130,7 +130,7 @@ func TestHandler_HandleMalfeasanceProof(t *testing.T) { }, ) handler.EXPECT().ReportProof(gomock.Any()) - h.RegisterHandlerV1(MultipleATXs, handler) + h.RegisterHandler(MultipleATXs, handler) gossip := &wire.MalfeasanceGossip{ MalfeasanceProof: wire.MalfeasanceProof{ @@ -165,14 +165,14 @@ func TestHandler_HandleMalfeasanceProof(t *testing.T) { identities.SetMalicious(h.db, nodeID, codec.MustEncode(proof), time.Now()) ctrl := gomock.NewController(t) - handler := NewMockHandlerV1(ctrl) + handler := NewMockMalfeasanceHandler(ctrl) handler.EXPECT().Validate(gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, data wire.ProofData) (types.NodeID, error) { require.IsType(t, &wire.AtxProof{}, data) return nodeID, nil }, ) - h.RegisterHandlerV1(MultipleATXs, handler) + h.RegisterHandler(MultipleATXs, handler) gossip := &wire.MalfeasanceGossip{ MalfeasanceProof: wire.MalfeasanceProof{ @@ -234,7 +234,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { nodeID := types.RandomNodeID() ctrl := gomock.NewController(t) - handler := NewMockHandlerV1(ctrl) + handler := NewMockMalfeasanceHandler(ctrl) handler.EXPECT().Validate(gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, data wire.ProofData) (types.NodeID, error) { require.IsType(t, &wire.AtxProof{}, data) @@ -242,7 +242,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { }, ) handler.EXPECT().ReportProof(gomock.Any()) - h.RegisterHandlerV1(MultipleATXs, handler) + h.RegisterHandler(MultipleATXs, handler) proof := &wire.MalfeasanceProof{ Layer: types.LayerID(22), @@ -268,7 +268,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { nodeID := types.RandomNodeID() ctrl := gomock.NewController(t) - handler := NewMockHandlerV1(ctrl) + handler := NewMockMalfeasanceHandler(ctrl) handler.EXPECT().Validate(gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, data wire.ProofData) (types.NodeID, error) { require.IsType(t, &wire.AtxProof{}, data) @@ -276,7 +276,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { }, ) handler.EXPECT().ReportInvalidProof(gomock.Any()) - h.RegisterHandlerV1(MultipleATXs, handler) + h.RegisterHandler(MultipleATXs, handler) proof := &wire.MalfeasanceProof{ Layer: types.LayerID(22), @@ -301,7 +301,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { nodeID := types.RandomNodeID() ctrl := gomock.NewController(t) - handler := NewMockHandlerV1(ctrl) + handler := NewMockMalfeasanceHandler(ctrl) handler.EXPECT().Validate(gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, data wire.ProofData) (types.NodeID, error) { require.IsType(t, &wire.AtxProof{}, data) @@ -309,7 +309,7 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { }, ) handler.EXPECT().ReportProof(gomock.Any()) - h.RegisterHandlerV1(MultipleATXs, handler) + h.RegisterHandler(MultipleATXs, handler) proof := &wire.MalfeasanceProof{ Layer: types.LayerID(22), @@ -347,14 +347,14 @@ func TestHandler_HandleSyncedMalfeasanceProof(t *testing.T) { identities.SetMalicious(h.db, nodeID, codec.MustEncode(proof), time.Now()) ctrl := gomock.NewController(t) - handler := NewMockHandlerV1(ctrl) + handler := NewMockMalfeasanceHandler(ctrl) handler.EXPECT().Validate(gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, data wire.ProofData) (types.NodeID, error) { require.IsType(t, &wire.AtxProof{}, data) return nodeID, nil }, ) - h.RegisterHandlerV1(MultipleATXs, handler) + h.RegisterHandler(MultipleATXs, handler) newProof := &wire.MalfeasanceProof{ Layer: types.LayerID(22), diff --git a/malfeasance/interface.go b/malfeasance/interface.go index 46b3cdb9f9..ab6d8bcf6c 100644 --- a/malfeasance/interface.go +++ b/malfeasance/interface.go @@ -15,12 +15,8 @@ type tortoise interface { OnMalfeasance(types.NodeID) } -type HandlerV1 interface { +type MalfeasanceHandler interface { Validate(ctx context.Context, data wire.ProofData) (types.NodeID, error) ReportProof(vec *prometheus.CounterVec) ReportInvalidProof(vec *prometheus.CounterVec) } - -type HandlerV2 interface { - Validate(ctx context.Context, data []byte) (types.NodeID, error) -} diff --git a/malfeasance/mocks.go b/malfeasance/mocks.go index d0be0c3a1a..093bf9a91d 100644 --- a/malfeasance/mocks.go +++ b/malfeasance/mocks.go @@ -78,103 +78,103 @@ func (c *MocktortoiseOnMalfeasanceCall) DoAndReturn(f func(types.NodeID)) *Mockt return c } -// MockHandlerV1 is a mock of HandlerV1 interface. -type MockHandlerV1 struct { +// MockMalfeasanceHandler is a mock of MalfeasanceHandler interface. +type MockMalfeasanceHandler struct { ctrl *gomock.Controller - recorder *MockHandlerV1MockRecorder + recorder *MockMalfeasanceHandlerMockRecorder } -// MockHandlerV1MockRecorder is the mock recorder for MockHandlerV1. -type MockHandlerV1MockRecorder struct { - mock *MockHandlerV1 +// MockMalfeasanceHandlerMockRecorder is the mock recorder for MockMalfeasanceHandler. +type MockMalfeasanceHandlerMockRecorder struct { + mock *MockMalfeasanceHandler } -// NewMockHandlerV1 creates a new mock instance. -func NewMockHandlerV1(ctrl *gomock.Controller) *MockHandlerV1 { - mock := &MockHandlerV1{ctrl: ctrl} - mock.recorder = &MockHandlerV1MockRecorder{mock} +// 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 *MockHandlerV1) EXPECT() *MockHandlerV1MockRecorder { +func (m *MockMalfeasanceHandler) EXPECT() *MockMalfeasanceHandlerMockRecorder { return m.recorder } // ReportInvalidProof mocks base method. -func (m *MockHandlerV1) ReportInvalidProof(vec *prometheus.CounterVec) { +func (m *MockMalfeasanceHandler) ReportInvalidProof(vec *prometheus.CounterVec) { m.ctrl.T.Helper() m.ctrl.Call(m, "ReportInvalidProof", vec) } // ReportInvalidProof indicates an expected call of ReportInvalidProof. -func (mr *MockHandlerV1MockRecorder) ReportInvalidProof(vec any) *MockHandlerV1ReportInvalidProofCall { +func (mr *MockMalfeasanceHandlerMockRecorder) ReportInvalidProof(vec any) *MockMalfeasanceHandlerReportInvalidProofCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportInvalidProof", reflect.TypeOf((*MockHandlerV1)(nil).ReportInvalidProof), vec) - return &MockHandlerV1ReportInvalidProofCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportInvalidProof", reflect.TypeOf((*MockMalfeasanceHandler)(nil).ReportInvalidProof), vec) + return &MockMalfeasanceHandlerReportInvalidProofCall{Call: call} } -// MockHandlerV1ReportInvalidProofCall wrap *gomock.Call -type MockHandlerV1ReportInvalidProofCall struct { +// MockMalfeasanceHandlerReportInvalidProofCall wrap *gomock.Call +type MockMalfeasanceHandlerReportInvalidProofCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockHandlerV1ReportInvalidProofCall) Return() *MockHandlerV1ReportInvalidProofCall { +func (c *MockMalfeasanceHandlerReportInvalidProofCall) Return() *MockMalfeasanceHandlerReportInvalidProofCall { c.Call = c.Call.Return() return c } // Do rewrite *gomock.Call.Do -func (c *MockHandlerV1ReportInvalidProofCall) Do(f func(*prometheus.CounterVec)) *MockHandlerV1ReportInvalidProofCall { +func (c *MockMalfeasanceHandlerReportInvalidProofCall) Do(f func(*prometheus.CounterVec)) *MockMalfeasanceHandlerReportInvalidProofCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockHandlerV1ReportInvalidProofCall) DoAndReturn(f func(*prometheus.CounterVec)) *MockHandlerV1ReportInvalidProofCall { +func (c *MockMalfeasanceHandlerReportInvalidProofCall) DoAndReturn(f func(*prometheus.CounterVec)) *MockMalfeasanceHandlerReportInvalidProofCall { c.Call = c.Call.DoAndReturn(f) return c } // ReportProof mocks base method. -func (m *MockHandlerV1) ReportProof(vec *prometheus.CounterVec) { +func (m *MockMalfeasanceHandler) ReportProof(vec *prometheus.CounterVec) { m.ctrl.T.Helper() m.ctrl.Call(m, "ReportProof", vec) } // ReportProof indicates an expected call of ReportProof. -func (mr *MockHandlerV1MockRecorder) ReportProof(vec any) *MockHandlerV1ReportProofCall { +func (mr *MockMalfeasanceHandlerMockRecorder) ReportProof(vec any) *MockMalfeasanceHandlerReportProofCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportProof", reflect.TypeOf((*MockHandlerV1)(nil).ReportProof), vec) - return &MockHandlerV1ReportProofCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportProof", reflect.TypeOf((*MockMalfeasanceHandler)(nil).ReportProof), vec) + return &MockMalfeasanceHandlerReportProofCall{Call: call} } -// MockHandlerV1ReportProofCall wrap *gomock.Call -type MockHandlerV1ReportProofCall struct { +// MockMalfeasanceHandlerReportProofCall wrap *gomock.Call +type MockMalfeasanceHandlerReportProofCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockHandlerV1ReportProofCall) Return() *MockHandlerV1ReportProofCall { +func (c *MockMalfeasanceHandlerReportProofCall) Return() *MockMalfeasanceHandlerReportProofCall { c.Call = c.Call.Return() return c } // Do rewrite *gomock.Call.Do -func (c *MockHandlerV1ReportProofCall) Do(f func(*prometheus.CounterVec)) *MockHandlerV1ReportProofCall { +func (c *MockMalfeasanceHandlerReportProofCall) Do(f func(*prometheus.CounterVec)) *MockMalfeasanceHandlerReportProofCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockHandlerV1ReportProofCall) DoAndReturn(f func(*prometheus.CounterVec)) *MockHandlerV1ReportProofCall { +func (c *MockMalfeasanceHandlerReportProofCall) DoAndReturn(f func(*prometheus.CounterVec)) *MockMalfeasanceHandlerReportProofCall { c.Call = c.Call.DoAndReturn(f) return c } // Validate mocks base method. -func (m *MockHandlerV1) Validate(ctx context.Context, data wire.ProofData) (types.NodeID, error) { +func (m *MockMalfeasanceHandler) Validate(ctx context.Context, data wire.ProofData) (types.NodeID, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Validate", ctx, data) ret0, _ := ret[0].(types.NodeID) @@ -183,93 +183,31 @@ func (m *MockHandlerV1) Validate(ctx context.Context, data wire.ProofData) (type } // Validate indicates an expected call of Validate. -func (mr *MockHandlerV1MockRecorder) Validate(ctx, data any) *MockHandlerV1ValidateCall { +func (mr *MockMalfeasanceHandlerMockRecorder) Validate(ctx, data any) *MockMalfeasanceHandlerValidateCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockHandlerV1)(nil).Validate), ctx, data) - return &MockHandlerV1ValidateCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockMalfeasanceHandler)(nil).Validate), ctx, data) + return &MockMalfeasanceHandlerValidateCall{Call: call} } -// MockHandlerV1ValidateCall wrap *gomock.Call -type MockHandlerV1ValidateCall struct { +// MockMalfeasanceHandlerValidateCall wrap *gomock.Call +type MockMalfeasanceHandlerValidateCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockHandlerV1ValidateCall) Return(arg0 types.NodeID, arg1 error) *MockHandlerV1ValidateCall { +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 *MockHandlerV1ValidateCall) Do(f func(context.Context, wire.ProofData) (types.NodeID, error)) *MockHandlerV1ValidateCall { +func (c *MockMalfeasanceHandlerValidateCall) Do(f func(context.Context, wire.ProofData) (types.NodeID, error)) *MockMalfeasanceHandlerValidateCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockHandlerV1ValidateCall) DoAndReturn(f func(context.Context, wire.ProofData) (types.NodeID, error)) *MockHandlerV1ValidateCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// MockHandlerV2 is a mock of HandlerV2 interface. -type MockHandlerV2 struct { - ctrl *gomock.Controller - recorder *MockHandlerV2MockRecorder -} - -// MockHandlerV2MockRecorder is the mock recorder for MockHandlerV2. -type MockHandlerV2MockRecorder struct { - mock *MockHandlerV2 -} - -// NewMockHandlerV2 creates a new mock instance. -func NewMockHandlerV2(ctrl *gomock.Controller) *MockHandlerV2 { - mock := &MockHandlerV2{ctrl: ctrl} - mock.recorder = &MockHandlerV2MockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockHandlerV2) EXPECT() *MockHandlerV2MockRecorder { - return m.recorder -} - -// Validate mocks base method. -func (m *MockHandlerV2) 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 *MockHandlerV2MockRecorder) Validate(ctx, data any) *MockHandlerV2ValidateCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockHandlerV2)(nil).Validate), ctx, data) - return &MockHandlerV2ValidateCall{Call: call} -} - -// MockHandlerV2ValidateCall wrap *gomock.Call -type MockHandlerV2ValidateCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockHandlerV2ValidateCall) Return(arg0 types.NodeID, arg1 error) *MockHandlerV2ValidateCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockHandlerV2ValidateCall) Do(f func(context.Context, []byte) (types.NodeID, error)) *MockHandlerV2ValidateCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockHandlerV2ValidateCall) DoAndReturn(f func(context.Context, []byte) (types.NodeID, error)) *MockHandlerV2ValidateCall { +func (c *MockMalfeasanceHandlerValidateCall) DoAndReturn(f func(context.Context, wire.ProofData) (types.NodeID, error)) *MockMalfeasanceHandlerValidateCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/malfeasance/publisher.go b/malfeasance/publisher.go new file mode 100644 index 0000000000..14398fc922 --- /dev/null +++ b/malfeasance/publisher.go @@ -0,0 +1,56 @@ +package malfeasance + +import ( + "context" + "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/malfeasance/wire" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub" + "github.com/spacemeshos/go-spacemesh/sql/identities" +) + +type Publisher struct { + logger *zap.Logger + cdb *datastore.CachedDB + tortoise tortoise + publisher pubsub.Publisher +} + +func NewPublisher( + logger *zap.Logger, + cdb *datastore.CachedDB, + tortoise tortoise, + publisher pubsub.Publisher, +) *Publisher { + return &Publisher{ + logger: logger, + cdb: cdb, + tortoise: tortoise, + publisher: publisher, + } +} + +// Publishes a malfeasance proof to the network. +func (p *Publisher) PublishProof(ctx context.Context, smesherID types.NodeID, proof *wire.MalfeasanceProof) error { + err := identities.SetMalicious(p.cdb, smesherID, codec.MustEncode(proof), time.Now()) + if err != nil { + return fmt.Errorf("adding malfeasance proof: %w", err) + } + p.cdb.CacheMalfeasanceProof(smesherID, proof) + p.tortoise.OnMalfeasance(smesherID) + + gossip := wire.MalfeasanceGossip{ + MalfeasanceProof: *proof, + } + if err = p.publisher.Publish(ctx, pubsub.MalfeasanceProof, codec.MustEncode(&gossip)); 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/handler.go b/malfeasance2/handler.go new file mode 100644 index 0000000000..f9490228da --- /dev/null +++ b/malfeasance2/handler.go @@ -0,0 +1,140 @@ +package malfeasance2 + +import ( + "context" + "fmt" + "slices" + + "go.uber.org/zap" + + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub" + "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 + tortoise tortoise + + handlers map[ProofDomain]MalfeasanceHandler +} + +func NewHandler( + db sql.Executor, + lg *zap.Logger, + self p2p.Peer, + tortoise tortoise, +) *Handler { + return &Handler{ + db: db, + logger: lg, + self: self, + tortoise: tortoise, + + handlers: make(map[ProofDomain]MalfeasanceHandler), + } +} + +func (h *Handler) RegisterHandler(malfeasanceType ProofDomain, handler MalfeasanceHandler) { + h.handlers[malfeasanceType] = handler +} + +func (h *Handler) HandleSynced(ctx context.Context, expHash types.Hash32, _ p2p.Peer, msg []byte) error { + var proof MalfeasanceProof + if err := codec.Decode(msg, &proof); err != nil { + h.logger.Warn("failed to decode malfeasance proof", + zap.Stringer("exp_hash", expHash), + zap.Error(err), + ) + return ErrMalformedData + } + + nodeIDs, err := h.handleProof(ctx, proof) + if err != nil { + h.logger.Warn("failed to handle synced malfeasance proof", + zap.Stringer("exp_hash", expHash), + zap.Error(err), + ) + return err + } + + if !slices.Contains(nodeIDs, types.NodeID(expHash)) { + h.logger.Warn("synced malfeasance proof invalid for requested nodeID", + zap.Stringer("exp_hash", expHash), + ) + return ErrWrongHash + } + + if err := h.storeProof(ctx, proof.Domain, msg); err != nil { + h.logger.Warn("failed to store synced malfeasance proof", + zap.Stringer("exp_hash", expHash), + zap.Error(err), + ) + return err + } + 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 { + h.logger.Warn("failed to decode malfeasance proof", + zap.Stringer("peer", peer), + zap.Error(err), + ) + return ErrMalformedData + } + + _, err := h.handleProof(ctx, proof) + if err != nil { + h.logger.Warn("failed to handle gossiped malfeasance proof", + zap.Stringer("peer", peer), + zap.Error(err), + ) + return err + } + + if err := h.storeProof(ctx, proof.Domain, msg); err != nil { + h.logger.Warn("failed to store synced malfeasance proof", + zap.Error(err), + ) + return err + } + + 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) + } + + return handler.Validate(ctx, proof.Proof) +} + +func (h *Handler) storeProof(ctx context.Context, domain ProofDomain, proof []byte) error { + return nil +} diff --git a/malfeasance2/handler_test.go b/malfeasance2/handler_test.go new file mode 100644 index 0000000000..de4d4c7f3d --- /dev/null +++ b/malfeasance2/handler_test.go @@ -0,0 +1,226 @@ +package malfeasance2_test + +import ( + "context" + "errors" + "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/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/sql" + "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() + 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", + 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) + }) + + 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) + }) + + 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) + }) + + 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(nil, handlerError) + 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) + }) + + 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([]types.NodeID{nodeID}, nil) + h.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) + + 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) + }) + + 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([]types.NodeID{nodeID}, nil) + h.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 0, + Domain: malfeasance2.InvalidActivation, + Proof: validProof, + } + + err := h.HandleSynced(context.Background(), types.Hash32(types.RandomNodeID()), "peer", codec.MustEncode(proof)) + require.ErrorIs(t, err, malfeasance2.ErrWrongHash) + }) +} + +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) + }) + + 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) + }) + + 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) + }) + + 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(nil, handlerError) + 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) + }) + + 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([]types.NodeID{nodeID}, nil) + h.RegisterHandler(malfeasance2.InvalidActivation, mockHandler) + + proof := &malfeasance2.MalfeasanceProof{ + Version: 0, + Domain: malfeasance2.InvalidActivation, + Proof: validProof, + } + + err := h.HandleGossip(context.Background(), "peer", codec.MustEncode(proof)) + require.NoError(t, err) + }) +} diff --git a/malfeasance2/interface.go b/malfeasance2/interface.go new file mode 100644 index 0000000000..33f68921e5 --- /dev/null +++ b/malfeasance2/interface.go @@ -0,0 +1,17 @@ +package malfeasance2 + +import ( + "context" + + "github.com/spacemeshos/go-spacemesh/common/types" +) + +//go:generate mockgen -typed -package=malfeasance2 -destination=./mocks.go -source=./interface.go + +type tortoise interface { + OnMalfeasance(types.NodeID) +} + +type MalfeasanceHandler interface { + Validate(ctx context.Context, data []byte) ([]types.NodeID, error) +} diff --git a/malfeasance2/mocks.go b/malfeasance2/mocks.go new file mode 100644 index 0000000000..b8f89fa37b --- /dev/null +++ b/malfeasance2/mocks.go @@ -0,0 +1,139 @@ +// 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" +) + +// Mocktortoise is a mock of tortoise interface. +type Mocktortoise struct { + ctrl *gomock.Controller + recorder *MocktortoiseMockRecorder +} + +// 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 +} + +// 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 +} + +// 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/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/node/node.go b/node/node.go index 70ea73a48f..8722734560 100644 --- a/node/node.go +++ b/node/node.go @@ -1140,11 +1140,11 @@ func (app *App) initServices(ctx context.Context) error { nodeIDs, trtl, ) - malfeasanceHandler.RegisterHandlerV1(malfeasance.MultipleATXs, activationMH) - malfeasanceHandler.RegisterHandlerV1(malfeasance.MultipleBallots, meshMH) - malfeasanceHandler.RegisterHandlerV1(malfeasance.HareEquivocation, hareMH) - malfeasanceHandler.RegisterHandlerV1(malfeasance.InvalidPostIndex, invalidPostMH) - malfeasanceHandler.RegisterHandlerV1(malfeasance.InvalidPrevATX, invalidPrevMH) + malfeasanceHandler.RegisterHandler(malfeasance.MultipleATXs, activationMH) + malfeasanceHandler.RegisterHandler(malfeasance.MultipleBallots, meshMH) + malfeasanceHandler.RegisterHandler(malfeasance.HareEquivocation, hareMH) + malfeasanceHandler.RegisterHandler(malfeasance.InvalidPostIndex, invalidPostMH) + malfeasanceHandler.RegisterHandler(malfeasance.InvalidPrevATX, invalidPrevMH) fetcher.SetValidators( fetch.ValidatorFunc( @@ -1196,13 +1196,13 @@ func (app *App) initServices(ctx context.Context) error { ), ) - syncHandler := func(_ context.Context, _ p2p.Peer, _ []byte) error { + checkSynced := func(_ context.Context, _ p2p.Peer, _ []byte) error { if newSyncer.ListenToGossip() { return nil } return errors.New("not synced for gossip") } - atxSyncHandler := func(_ context.Context, _ p2p.Peer, _ []byte) error { + checkAtxSynced := func(_ context.Context, _ p2p.Peer, _ []byte) error { if newSyncer.ListenToATXGossip() { return nil } @@ -1212,45 +1212,49 @@ func (app *App) initServices(ctx context.Context) error { if app.Config.Beacon.RoundsNumber > 0 { app.host.Register( pubsub.BeaconWeakCoinProtocol, - pubsub.ChainGossipHandler(syncHandler, beaconProtocol.HandleWeakCoinProposal), + pubsub.ChainGossipHandler(checkSynced, beaconProtocol.HandleWeakCoinProposal), pubsub.WithValidatorInline(true), ) app.host.Register( pubsub.BeaconProposalProtocol, - pubsub.ChainGossipHandler(syncHandler, beaconProtocol.HandleProposal), + pubsub.ChainGossipHandler(checkSynced, beaconProtocol.HandleProposal), pubsub.WithValidatorInline(true), ) app.host.Register( pubsub.BeaconFirstVotesProtocol, - pubsub.ChainGossipHandler(syncHandler, beaconProtocol.HandleFirstVotes), + pubsub.ChainGossipHandler(checkSynced, beaconProtocol.HandleFirstVotes), pubsub.WithValidatorInline(true), ) app.host.Register( pubsub.BeaconFollowingVotesProtocol, - pubsub.ChainGossipHandler(syncHandler, beaconProtocol.HandleFollowingVotes), + pubsub.ChainGossipHandler(checkSynced, beaconProtocol.HandleFollowingVotes), pubsub.WithValidatorInline(true), ) } app.host.Register( pubsub.ProposalProtocol, - pubsub.ChainGossipHandler(syncHandler, proposalListener.HandleProposal), + pubsub.ChainGossipHandler(checkSynced, proposalListener.HandleProposal), ) app.host.Register( pubsub.AtxProtocol, - pubsub.ChainGossipHandler(atxSyncHandler, atxHandler.HandleGossipAtx), + pubsub.ChainGossipHandler(checkAtxSynced, atxHandler.HandleGossipAtx), pubsub.WithValidatorConcurrency(app.Config.P2P.GossipAtxValidationThrottle), ) app.host.Register( pubsub.TxProtocol, - pubsub.ChainGossipHandler(syncHandler, app.txHandler.HandleGossipTransaction), + pubsub.ChainGossipHandler(checkSynced, app.txHandler.HandleGossipTransaction), ) app.host.Register( pubsub.BlockCertify, - pubsub.ChainGossipHandler(syncHandler, app.certifier.HandleCertifyMessage), + pubsub.ChainGossipHandler(checkSynced, app.certifier.HandleCertifyMessage), ) app.host.Register( pubsub.MalfeasanceProof, - pubsub.ChainGossipHandler(atxSyncHandler, malfeasanceHandler.HandleMalfeasanceProof), + pubsub.ChainGossipHandler(checkAtxSynced, malfeasanceHandler.HandleMalfeasanceProof), + ) + app.host.Register( + pubsub.MalfeasanceProof2, + pubsub.ChainGossipHandler(checkAtxSynced, malfeasanceHandler.HandleMalfeasanceProof), ) app.proposalBuilder = proposalBuilder diff --git a/p2p/pubsub/pubsub.go b/p2p/pubsub/pubsub.go index b64dd2f859..db67c4904b 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/sql/database.go b/sql/database.go index a647086f4a..599a775f3a 100644 --- a/sql/database.go +++ b/sql/database.go @@ -309,7 +309,10 @@ func prepareDB(logger *zap.Logger, db *sqliteDatabase, config *conf, freshDB boo if config.enableLatency { db.latency = newQueryLatency() } - + if _, err := db.Exec("PRAGMA foreign_keys = ON;", nil, nil); err != nil { + db.Close() + return nil, fmt.Errorf("enable foreign keys: %w", err) + } if config.temp { // Temporary database is used for migration and is deleted if migrations // fail, so we make it faster by disabling journaling and synchronous diff --git a/sql/identities/identities.go b/sql/identities/identities.go index ff4d9d804e..4fdf0ff935 100644 --- a/sql/identities/identities.go +++ b/sql/identities/identities.go @@ -186,7 +186,7 @@ func Marriage(db sql.Executor, id types.NodeID) (*MarriageData, error) { } // Set marriage inserts marriage data for given identity. -// If identitty doesn't exist - create it. +// If identity doesn't exist - create it. func SetMarriage(db sql.Executor, id types.NodeID, m *MarriageData) error { _, err := db.Exec(` INSERT INTO identities (pubkey, marriage_atx, marriage_idx, marriage_target, marriage_signature) diff --git a/sql/malfeasance/malfeasance.go b/sql/malfeasance/malfeasance.go new file mode 100644 index 0000000000..28809cf544 --- /dev/null +++ b/sql/malfeasance/malfeasance.go @@ -0,0 +1,172 @@ +package malfeasance + +import ( + "context" + "fmt" + "time" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/sql" +) + +func Add(db sql.Executor, nodeID types.NodeID, domain byte, proof []byte, received time.Time) error { + _, err := db.Exec(` + INSERT INTO malfeasance (pubkey, received, domain, proof) + VALUES (?1, ?2, ?3, ?4);`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, nodeID.Bytes()) + stmt.BindInt64(2, received.UnixNano()) + stmt.BindInt64(3, int64(domain)) + stmt.BindBytes(4, proof) + }, nil, + ) + if err != nil { + return fmt.Errorf("add malfeasance %s: %w", nodeID, err) + } + return nil +} + +func AddMarried(db sql.Executor, nodeID, marriedTo types.NodeID, received time.Time) error { + _, err := db.Exec(` + INSERT INTO malfeasance (pubkey, received, married_to) + VALUES (?1, ?2, ?3);`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, nodeID.Bytes()) + stmt.BindInt64(2, received.UnixNano()) + stmt.BindBytes(3, marriedTo.Bytes()) + }, nil, + ) + if err != nil { + return fmt.Errorf("add married %s: %w", nodeID, err) + } + return nil +} + +func IsMalicious(db sql.Executor, nodeID types.NodeID) (bool, error) { + rows, err := db.Exec(` + SELECT 1 FROM malfeasance + WHERE pubkey = ?1;`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, nodeID.Bytes()) + }, nil, + ) + if err != nil { + return false, fmt.Errorf("is malicious %s: %w", nodeID, err) + } + return rows > 0, nil +} + +// Proof returns a proof for the given node ID. It will not necessarily return the proof for the given node ID, +// but might return the proof for the node ID the given node ID is married to. +func Proof(db sql.Executor, nodeID types.NodeID) (types.NodeID, byte, []byte, error) { + var domain byte + var proof []byte + _, err := db.Exec(` + SELECT domain, proof FROM malfeasance + WHERE pubkey = ?1;`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, nodeID.Bytes()) + }, func(stmt *sql.Statement) bool { + domain = byte(stmt.ColumnInt(0)) + proof = make([]byte, stmt.ColumnLen(1)) + stmt.ColumnBytes(1, proof) + return true + }, + ) + if err != nil { + return types.EmptyNodeID, 0, nil, fmt.Errorf("proof %v: %w", nodeID, err) + } + if len(proof) > 0 { + return nodeID, domain, proof, nil + } + + _, err = db.Exec(` + SELECT pubkey, domain, proof FROM malfeasance + WHERE pubkey = ( + SELECT married_to FROM malfeasance + WHERE pubkey = ?1 + );`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, nodeID.Bytes()) + }, func(stmt *sql.Statement) bool { + stmt.ColumnBytes(0, nodeID[:]) + domain = byte(stmt.ColumnInt(1)) + proof = make([]byte, stmt.ColumnLen(2)) + stmt.ColumnBytes(2, proof) + return true + }, + ) + if err != nil { + return types.EmptyNodeID, 0, nil, fmt.Errorf("proof %v: %w", nodeID, err) + } + if proof == nil { + return types.EmptyNodeID, 0, nil, fmt.Errorf("proof %v: %w", nodeID, sql.ErrNotFound) + } + + return nodeID, domain, proof, nil +} + +// TODO(mafa): it seems that this is again needed by the fetcher. +// +// The problem here is that iterate will iterate over all identities known to be malfeasant, +// independent of their marriage set. I believe we have to stick with this behavior, +// since we might have a different view on the marriage set than our peers, so we have to include +// ALL known malicious identities. +func Iterate(db sql.Executor, callback func(total int, id types.NodeID) error) error { + var callbackErr error + dec := func(stmt *sql.Statement) bool { + var id types.NodeID + total := stmt.ColumnInt(0) + stmt.ColumnBytes(1, id[:]) + if err := callback(total, id); err != nil { + return false + } + return true + } + + _, err := db.Exec(` + SELECT (SELECT count(*) FROM malfeasance) as total, + pubkey FROM malfeasance;`, + nil, dec, + ) + if err != nil { + return fmt.Errorf("iterate malfeasance: %w", err) + } + return callbackErr +} + +// All retrieves all malicious node IDs from the database. +func All(db sql.Executor) ([]types.NodeID, error) { + var nodeIDs []types.NodeID + err := Iterate(db, func(total int, id types.NodeID) error { + if nodeIDs == nil { + nodeIDs = make([]types.NodeID, 0, total) + } + nodeIDs = append(nodeIDs, id) + return nil + }) + if err != nil { + return nil, err + } + if len(nodeIDs) != cap(nodeIDs) { + panic("BUG: bad malicious node ID count") + } + return nodeIDs, nil +} + +// TODO(mafa): it looks like the fetcher needs this function? +// Implementing this is not trivial, as the blob size depends on how many identities are in the marriage set +// and the encoded proof might be for a different identity then requested. +// +// This query could be significantly slower than other "GetBlobSizes" queries. +func BlobSizes(db sql.Executor, ids [][]byte) (sizes []int, err error) { + panic("implement me") +} + +// TODO(mafa): it looks like the fetcher needs this function? +// +// Same as above - loading the blob from DB is not trivial, since it requires re-encoding a +// possibly different identity's proof with certificates from the current knowledge about the marriage set. +func LoadBlob(ctx context.Context, db sql.Executor, nodeID []byte, blob *sql.Blob) error { + panic("implement me") +} diff --git a/sql/malfeasance/malfeasance_test.go b/sql/malfeasance/malfeasance_test.go new file mode 100644 index 0000000000..889a219e99 --- /dev/null +++ b/sql/malfeasance/malfeasance_test.go @@ -0,0 +1,156 @@ +package malfeasance_test + +import ( + "testing" + "time" + + sqlite "github.com/go-llsqlite/crawshaw" + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/malfeasance" + "github.com/spacemeshos/go-spacemesh/sql/statesql" +) + +func TestAdd(t *testing.T) { + t.Parallel() + + db := statesql.InMemoryTest(t) + + id := types.RandomNodeID() + domain := byte(1) + proof := []byte{1, 2, 3} + received := time.Now() + + require.NoError(t, malfeasance.Add(db, id, domain, proof, received)) + + mal, err := malfeasance.IsMalicious(db, id) + require.NoError(t, err) + require.True(t, mal) +} + +func TestAddMarried(t *testing.T) { + t.Parallel() + + db := statesql.InMemoryTest(t) + + id := types.RandomNodeID() + marriedTo := types.RandomNodeID() + received := time.Now() + + require.NoError(t, malfeasance.Add(db, marriedTo, 1, []byte{1, 2, 3}, received)) + + require.NoError(t, malfeasance.AddMarried(db, id, marriedTo, received)) + + mal, err := malfeasance.IsMalicious(db, id) + require.NoError(t, err) + require.True(t, mal) + + mal, err = malfeasance.IsMalicious(db, marriedTo) + require.NoError(t, err) + require.True(t, mal) +} + +func TestAddMarriedMissing(t *testing.T) { + t.Parallel() + + db := statesql.InMemoryTest(t) + + id := types.RandomNodeID() + marriedTo := types.RandomNodeID() + received := time.Now() + + err := malfeasance.AddMarried(db, id, marriedTo, received) + sqlError := &sqlite.Error{} + require.ErrorAs(t, err, sqlError) + require.Equal(t, sqlite.SQLITE_CONSTRAINT_FOREIGNKEY, sqlError.Code) + + mal, err := malfeasance.IsMalicious(db, id) + require.NoError(t, err) + require.False(t, mal) + + mal, err = malfeasance.IsMalicious(db, marriedTo) + require.NoError(t, err) + require.False(t, mal) +} + +func TestProof(t *testing.T) { + t.Parallel() + + db := statesql.InMemoryTest(t) + + id := types.RandomNodeID() + domain := byte(1) + proof := []byte{1, 2, 3} + received := time.Now() + + gotId, gotDomain, gotProof, err := malfeasance.Proof(db, id) + require.ErrorIs(t, err, sql.ErrNotFound) + require.Zero(t, gotId) + require.Zero(t, gotDomain) + require.Nil(t, gotProof) + + require.NoError(t, malfeasance.Add(db, id, domain, proof, received)) + + gotId, gotDomain, gotProof, err = malfeasance.Proof(db, id) + require.NoError(t, err) + require.Equal(t, id, gotId) + require.Equal(t, domain, gotDomain) + require.Equal(t, proof, gotProof) +} + +func TestProofMarried(t *testing.T) { + t.Parallel() + + db := statesql.InMemoryTest(t) + + id := types.RandomNodeID() + marriedTo := types.RandomNodeID() + domain := byte(1) + proof := []byte{1, 2, 3} + received := time.Now() + + require.NoError(t, malfeasance.Add(db, marriedTo, domain, proof, received)) + + require.NoError(t, malfeasance.AddMarried(db, id, marriedTo, received)) + + gotId, gotDomain, gotProof, err := malfeasance.Proof(db, marriedTo) + require.NoError(t, err) + require.Equal(t, marriedTo, gotId) + require.Equal(t, domain, gotDomain) + require.Equal(t, proof, gotProof) + + gotId, gotDomain, gotProof, err = malfeasance.Proof(db, id) + require.NoError(t, err) + require.Equal(t, marriedTo, gotId) + require.Equal(t, domain, gotDomain) + require.Equal(t, proof, gotProof) +} + +func TestAll(t *testing.T) { + t.Parallel() + + db := statesql.InMemoryTest(t) + + ids := make([]types.NodeID, 3) + domain := byte(1) + proof := []byte{1, 2, 3} + received := time.Now() + + for i := range ids { + ids[i] = types.RandomNodeID() + require.NoError(t, malfeasance.Add(db, ids[i], domain, proof, received)) + } + + marriedIds := make([]types.NodeID, 3) + for i := range marriedIds { + marriedIds[i] = types.RandomNodeID() + require.NoError(t, malfeasance.AddMarried(db, marriedIds[i], ids[i], received)) + } + + expected := append(ids, marriedIds...) + all, err := malfeasance.All(db) + require.NoError(t, err) + require.ElementsMatch(t, expected, all) +} diff --git a/sql/statesql/schema/migrations/0023_malfeasance.sql b/sql/statesql/schema/migrations/0023_malfeasance.sql new file mode 100644 index 0000000000..83d824ed56 --- /dev/null +++ b/sql/statesql/schema/migrations/0023_malfeasance.sql @@ -0,0 +1,20 @@ +-- adds new table for v2 malfeasance proofs +-- TODO(mafa): in the future add a migration to convert old malfeasance proofs to the new format +-- and then remove proof, received from the old table + +CREATE TABLE malfeasance +( + pubkey CHAR(32) PRIMARY KEY, + received INT NOT NULL, -- unix timestamp + + -- if the following field is not null, then domain and proof are null + married_to CHAR(32), -- the pubkey of identity in the marriage set that was proven to be malicious + + -- if the following fields are not null, then married_to is null + domain INT, -- domain of the proof + proof BLOB, -- proof of the identity to be malicious + + -- ensure the identity referenced already exists in this table + FOREIGN KEY (married_to) REFERENCES malfeasance(pubkey) +); + diff --git a/sql/statesql/schema/schema.sql b/sql/statesql/schema/schema.sql index 4e115bd24a..2756d7469c 100755 --- a/sql/statesql/schema/schema.sql +++ b/sql/statesql/schema/schema.sql @@ -1,4 +1,4 @@ -PRAGMA user_version = 22; +PRAGMA user_version = 23; CREATE TABLE accounts ( address CHAR(24), @@ -96,6 +96,21 @@ CREATE TABLE layers aggregated_hash CHAR(32) ) WITHOUT ROWID; CREATE INDEX layers_by_processed ON layers (processed); +CREATE TABLE malfeasance +( + pubkey CHAR(32) PRIMARY KEY, + received INT NOT NULL, + + + married_to CHAR(32), + + + domain INT, + proof BLOB, + + + FOREIGN KEY (married_to) REFERENCES malfeasance(pubkey) +); CREATE TABLE poets ( ref VARCHAR PRIMARY KEY,