diff --git a/activation/handler.go b/activation/handler.go index 8a3bf8ce24..5561c60c43 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -232,6 +232,12 @@ func (h *Handler) decodeATX(msg []byte) (opaqueAtx, error) { return nil, fmt.Errorf("%w: %w", errMalformedData, err) } return &atx, nil + case types.AtxV2: + var atx wire.ActivationTxV2 + if err := codec.Decode(msg, &atx); err != nil { + return nil, fmt.Errorf("%w: %w", errMalformedData, err) + } + return &atx, nil } return nil, fmt.Errorf("unsupported ATX version: %v", *version) @@ -280,6 +286,9 @@ func (h *Handler) handleAtx( switch atx := opaqueAtx.(type) { case *wire.ActivationTxV1: proof, err = h.v1.processATX(ctx, peer, atx, msg, receivedTime) + case *wire.ActivationTxV2: + // proof, err = h.v2.processATX(ctx, peer, atx, msg, receivedTime) + return nil, errors.New("ATX V2 is not supported yet") default: panic("unreachable") } diff --git a/activation/handler_test.go b/activation/handler_test.go index df8e7b7ceb..ee5190f544 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -119,12 +119,13 @@ func toAtx(t testing.TB, watx *wire.ActivationTxV1) *types.ActivationTx { type handlerMocks struct { goldenATXID types.ATXID - mclock *MocklayerClock - mpub *pubsubmocks.MockPublisher - mockFetch *mocks.MockFetcher - mValidator *MocknipostValidator - mbeacon *MockAtxReceiver - mtortoise *mocks.MockTortoise + mclock *MocklayerClock + mpub *pubsubmocks.MockPublisher + mockFetch *mocks.MockFetcher + mValidator *MocknipostValidator + mValidatorV2 *MocknipostValidatorV2 + mbeacon *MockAtxReceiver + mtortoise *mocks.MockTortoise } type testHandler struct { @@ -181,13 +182,14 @@ func (h *handlerMocks) expectAtxV1(atx *wire.ActivationTxV1, nodeId types.NodeID func newTestHandlerMocks(tb testing.TB, golden types.ATXID) handlerMocks { ctrl := gomock.NewController(tb) return handlerMocks{ - goldenATXID: golden, - mclock: NewMocklayerClock(ctrl), - mpub: pubsubmocks.NewMockPublisher(ctrl), - mockFetch: mocks.NewMockFetcher(ctrl), - mValidator: NewMocknipostValidator(ctrl), - mbeacon: NewMockAtxReceiver(ctrl), - mtortoise: mocks.NewMockTortoise(ctrl), + goldenATXID: golden, + mclock: NewMocklayerClock(ctrl), + mpub: pubsubmocks.NewMockPublisher(ctrl), + mockFetch: mocks.NewMockFetcher(ctrl), + mValidator: NewMocknipostValidator(ctrl), + mValidatorV2: NewMocknipostValidatorV2(ctrl), + mbeacon: NewMockAtxReceiver(ctrl), + mtortoise: mocks.NewMockTortoise(ctrl), } } @@ -520,6 +522,18 @@ func TestHandler_HandleSyncedAtx(t *testing.T) { require.ErrorIs(t, err, errMalformedData) require.ErrorContains(t, err, "invalid atx signature") }) + t.Run("initial atx V2", func(t *testing.T) { + t.Skip("atx V2 is not supported yet") + t.Parallel() + + atx := newInitialATXv2(t, goldenATXID) + atx.Sign(sig) + + atxHdlr := newTestHandler(t, goldenATXID, WithAtxVersions(AtxVersions{0: types.AtxV2})) + atxHdlr.expectInitialAtxV2(atx) + err := atxHdlr.HandleSyncedAtx(context.Background(), atx.ID().Hash32(), p2p.NoPeer, codec.MustEncode(atx)) + require.NoError(t, err) + }) } func TestCollectDeps(t *testing.T) { @@ -867,13 +881,25 @@ func TestHandler_DecodeATX(t *testing.T) { require.NoError(t, err) require.Equal(t, atx, decoded) }) - t.Run("v2 not supported", func(t *testing.T) { - // TODO: change this test when v2 is supported + t.Run("v2", func(t *testing.T) { t.Parallel() versions := map[types.EpochID]types.AtxVersion{10: types.AtxV2} atxHdlr := newTestHandler(t, types.RandomATXID(), WithAtxVersions(versions)) - _, err := atxHdlr.decodeATX(codec.MustEncode(types.EpochID(20))) - require.ErrorContains(t, err, "unsupported ATX version") + atx := newInitialATXv2(t, atxHdlr.goldenATXID) + atx.PublishEpoch = 10 + decoded, err := atxHdlr.decodeATX(codec.MustEncode(atx)) + require.NoError(t, err) + require.Equal(t, atx, decoded) + }) + t.Run("rejects v2 in epoch when it's not supported", func(t *testing.T) { + t.Parallel() + versions := map[types.EpochID]types.AtxVersion{10: types.AtxV2} + atxHdlr := newTestHandler(t, types.RandomATXID(), WithAtxVersions(versions)) + + atx := newInitialATXv2(t, atxHdlr.goldenATXID) + atx.PublishEpoch = 9 + _, err := atxHdlr.decodeATX(codec.MustEncode(atx)) + require.ErrorIs(t, err, errMalformedData) }) } diff --git a/activation/handler_v2.go b/activation/handler_v2.go new file mode 100644 index 0000000000..e465ea616c --- /dev/null +++ b/activation/handler_v2.go @@ -0,0 +1,177 @@ +package activation + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/spacemeshos/post/shared" + "go.uber.org/zap" + + "github.com/spacemeshos/go-spacemesh/activation/wire" + "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/datastore" + "github.com/spacemeshos/go-spacemesh/log" + mwire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/system" +) + +//go:generate mockgen -typed -source=./handler_v2.go -destination=mocks_handler_v2.go -package=activation + +type nipostValidatorV2 interface { + IsVerifyingFullPost() bool + VRFNonceV2(smesherID types.NodeID, commitment types.ATXID, vrfNonce types.VRFPostIndex, numUnits uint32) error + PostV2( + ctx context.Context, + smesherID types.NodeID, + commitment types.ATXID, + post *wire.PostV1, + challenge []byte, + numUnits uint32, + opts ...validatorOption, + ) error +} + +type HandlerV2 struct { + local p2p.Peer + cdb *datastore.CachedDB + atxsdata *atxsdata.Data + edVerifier *signing.EdVerifier + clock layerClock + tickSize uint64 + goldenATXID types.ATXID + nipostValidator nipostValidatorV2 + beacon AtxReceiver + tortoise system.Tortoise + log *zap.Logger + fetcher system.Fetcher +} + +func (h *HandlerV2) processATX( + ctx context.Context, + peer p2p.Peer, + watx *wire.ActivationTxV2, + blob []byte, + received time.Time, +) (*mwire.MalfeasanceProof, error) { + exists, err := atxs.Has(h.cdb, watx.ID()) + if err != nil { + return nil, fmt.Errorf("failed to check if atx exists: %w", err) + } + if exists { + return nil, nil + } + + h.log.Debug( + "processing atx", + log.ZContext(ctx), + zap.Stringer("atx_id", watx.ID()), + zap.Uint32("publish", watx.PublishEpoch.Uint32()), + zap.Stringer("smesherID", watx.SmesherID), + ) + + if err := h.syntacticallyValidate(ctx, watx); err != nil { + return nil, fmt.Errorf("atx %s syntactically invalid: %w", watx.ID(), err) + } + + // TODO: + // 1. fetch dependencies + // 2. syntactically validate dependencies + // 3. contextually validate ATX + // 4. store ATX + return nil, nil +} + +// Syntactically validate an ATX. +// TODOs: +// 1. support marriages +// 2. support merged ATXs. +func (h *HandlerV2) syntacticallyValidate(ctx context.Context, atx *wire.ActivationTxV2) error { + if !h.edVerifier.Verify(signing.ATX, atx.SmesherID, atx.SignedBytes(), atx.Signature) { + return fmt.Errorf("invalid atx signature: %w", errMalformedData) + } + if atx.PositioningATX == types.EmptyATXID { + return errors.New("empty positioning atx") + } + // TODO: support marriages + if len(atx.Marriages) != 0 { + return errors.New("marriages are not supported") + } + + current := h.clock.CurrentLayer().GetEpoch() + if atx.PublishEpoch > current+1 { + return fmt.Errorf("atx publish epoch is too far in the future: %d > %d", atx.PublishEpoch, current+1) + } + + if atx.MarriageATX == nil { + if len(atx.NiPosts) != 1 { + return errors.New("solo atx must have one nipost") + } + if len(atx.NiPosts[0].Posts) != 1 { + return errors.New("solo atx must have one post") + } + if atx.NiPosts[0].Posts[0].PrevATXIndex != 0 { + return errors.New("solo atx post must have prevATXIndex 0") + } + } + + if atx.Initial != nil { + if atx.MarriageATX != nil { + return errors.New("initial atx cannot reference a marriage atx") + } + if atx.Initial.CommitmentATX == types.EmptyATXID { + return errors.New("initial atx missing commitment atx") + } + if atx.VRFNonce == nil { + return errors.New("initial atx missing vrf nonce") + } + if len(atx.PreviousATXs) != 0 { + return errors.New("initial atx must not have previous atxs") + } + + if atx.Coinbase == nil { + return errors.New("initial atx missing coinbase") + } + + numUnits := atx.NiPosts[0].Posts[0].NumUnits + if err := h.nipostValidator.VRFNonceV2( + atx.SmesherID, atx.Initial.CommitmentATX, types.VRFPostIndex(*atx.VRFNonce), numUnits, + ); err != nil { + return fmt.Errorf("invalid vrf nonce: %w", err) + } + if err := h.nipostValidator.PostV2( + ctx, atx.SmesherID, atx.Initial.CommitmentATX, &atx.Initial.Post, shared.ZeroChallenge, numUnits, + ); err != nil { + return fmt.Errorf("invalid initial post: %w", err) + } + return nil + } + + for i, prev := range atx.PreviousATXs { + if prev == types.EmptyATXID { + return fmt.Errorf("previous atx[%d] is empty", i) + } + if prev == h.goldenATXID { + return fmt.Errorf("previous atx[%d] is the golden ATX", i) + } + } + + switch { + case atx.MarriageATX != nil: + // Merged ATX + // TODO: support merged ATXs + return errors.New("atx merge is not supported") + default: + // Solo chained (non-initial) ATX + if len(atx.PreviousATXs) != 1 { + return errors.New("solo atx must have one previous atx") + } + } + + return nil +} diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go new file mode 100644 index 0000000000..a32f625a1a --- /dev/null +++ b/activation/handler_v2_test.go @@ -0,0 +1,428 @@ +package activation + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/peer" + "github.com/spacemeshos/post/shared" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/spacemeshos/go-spacemesh/activation/wire" + "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/datastore" + "github.com/spacemeshos/go-spacemesh/log/logtest" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" +) + +type v2TestHandler struct { + *HandlerV2 + + handlerMocks +} + +func newV2TestHandler(tb testing.TB, golden types.ATXID) *v2TestHandler { + lg := logtest.New(tb) + cdb := datastore.NewCachedDB(sql.InMemory(), lg) + mocks := newTestHandlerMocks(tb, golden) + return &v2TestHandler{ + HandlerV2: &HandlerV2{ + local: "localID", + cdb: cdb, + atxsdata: atxsdata.New(), + edVerifier: signing.NewEdVerifier(), + clock: mocks.mclock, + tickSize: 1, + goldenATXID: golden, + nipostValidator: mocks.mValidatorV2, + log: lg.Zap(), + fetcher: mocks.mockFetch, + beacon: mocks.mbeacon, + tortoise: mocks.mtortoise, + }, + handlerMocks: mocks, + } +} + +func (h *handlerMocks) expectInitialAtxV2(atx *wire.ActivationTxV2) { + h.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) + h.mValidatorV2.EXPECT().VRFNonceV2( + atx.SmesherID, + atx.Initial.CommitmentATX, + types.VRFPostIndex(*atx.VRFNonce), + atx.NiPosts[0].Posts[0].NumUnits, + ) + h.mValidatorV2.EXPECT().PostV2( + gomock.Any(), + atx.SmesherID, + atx.Initial.CommitmentATX, + &atx.Initial.Post, + shared.ZeroChallenge, + atx.NiPosts[0].Posts[0].NumUnits, + gomock.Any(), + ) + + // TODO: + // 1. expect fetching dependencies + // 2. expect verifying nipost + // 3. expect storing ATX +} + +func TestHandlerV2_SyntacticallyValidate(t *testing.T) { + t.Parallel() + golden := types.RandomATXID() + sig, err := signing.NewEdSigner() + require.NoError(t, err) + t.Run("rejects invalid signature", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + + atxHandler := newV2TestHandler(t, golden) + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "signature") + }) + t.Run("rejects from future", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.PublishEpoch = 100 + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.mclock.EXPECT().CurrentLayer().Return(0) + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "atx publish epoch is too far in the future") + }) + t.Run("rejects empty positioning ATX", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.PositioningATX = types.EmptyATXID + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "empty positioning atx") + }) + t.Run("marriages are not supported (yet)", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.Marriages = []wire.MarriageCertificate{{}} + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "marriages are not supported") + }) + t.Run("reject golden previous ATX", func(t *testing.T) { + t.Parallel() + atx := newSoloATXv2(t, 0, golden, golden) + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "previous atx[0] is the golden ATX") + }) + t.Run("reject empty previous ATX", func(t *testing.T) { + t.Parallel() + atx := newSoloATXv2(t, 0, types.EmptyATXID, golden) + atx.PreviousATXs = append(atx.PreviousATXs, types.EmptyATXID) + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "previous atx[0] is empty") + }) +} + +func TestHandlerV2_SyntacticallyValidate_InitialAtx(t *testing.T) { + t.Parallel() + golden := types.RandomATXID() + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + t.Run("valid", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.mclock.EXPECT().CurrentLayer() + atxHandler.mValidatorV2.EXPECT().VRFNonceV2( + sig.NodeID(), + atx.Initial.CommitmentATX, + types.VRFPostIndex(*atx.VRFNonce), + atx.NiPosts[0].Posts[0].NumUnits, + ) + atxHandler.mValidatorV2.EXPECT().PostV2( + context.Background(), + sig.NodeID(), + atx.Initial.CommitmentATX, + &atx.Initial.Post, + shared.ZeroChallenge, + atx.NiPosts[0].Posts[0].NumUnits, + ) + require.NoError(t, atxHandler.syntacticallyValidate(context.Background(), atx)) + }) + t.Run("rejects previous ATXs", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.PreviousATXs = []types.ATXID{types.RandomATXID()} + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "initial atx must not have previous atxs") + + atx.PreviousATXs = []types.ATXID{types.EmptyATXID} + atx.Sign(sig) + + atxHandler.mclock.EXPECT().CurrentLayer() + err = atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "initial atx must not have previous atxs") + }) + t.Run("rejects when VRF nonce is nil", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.VRFNonce = nil + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "initial atx missing vrf nonce") + }) + t.Run("rejects when Coinbase is nil", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.Coinbase = nil + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "initial atx missing coinbase") + }) + t.Run("rejects when marriage ATX ref is set", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.MarriageATX = &golden + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "initial atx cannot reference a marriage atx") + }) + t.Run("rejects when commitment ATX is missing", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.Initial.CommitmentATX = types.EmptyATXID + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "initial atx missing commitment atx") + }) + t.Run("invalid VRF nonce", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.mclock.EXPECT().CurrentLayer() + atxHandler.mValidatorV2.EXPECT(). + VRFNonceV2( + sig.NodeID(), + atx.Initial.CommitmentATX, + types.VRFPostIndex(*atx.VRFNonce), + atx.NiPosts[0].Posts[0].NumUnits, + ). + Return(errors.New("invalid nonce")) + + require.ErrorContains(t, atxHandler.syntacticallyValidate(context.Background(), atx), "invalid nonce") + }) + t.Run("invalid initial PoST", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.Sign(sig) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.mclock.EXPECT().CurrentLayer() + atxHandler.mValidatorV2.EXPECT().VRFNonceV2( + sig.NodeID(), + atx.Initial.CommitmentATX, + types.VRFPostIndex(*atx.VRFNonce), + atx.NiPosts[0].Posts[0].NumUnits, + ) + atxHandler.mValidatorV2.EXPECT(). + PostV2( + context.Background(), + sig.NodeID(), + atx.Initial.CommitmentATX, + &atx.Initial.Post, + shared.ZeroChallenge, + atx.NiPosts[0].Posts[0].NumUnits, + ). + Return(errors.New("invalid post")) + require.ErrorContains(t, atxHandler.syntacticallyValidate(context.Background(), atx), "invalid post") + }) +} + +func TestHandlerV2_SyntacticallyValidate_SoloAtx(t *testing.T) { + t.Parallel() + golden := types.RandomATXID() + atxHandler := newV2TestHandler(t, golden) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + t.Run("valid", func(t *testing.T) { + atx := newSoloATXv2(t, 0, types.RandomATXID(), types.RandomATXID()) + atx.Sign(sig) + + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.NoError(t, err) + }) + t.Run("must have 1 previous ATX", func(t *testing.T) { + atx := newSoloATXv2(t, 0, types.RandomATXID(), types.RandomATXID()) + atx.PreviousATXs = append(atx.PreviousATXs, types.RandomATXID()) + atx.Sign(sig) + + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "solo atx must have one previous atx") + }) + t.Run("rejects when len(NIPoSTs) != 1", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.NiPosts = append(atx.NiPosts, wire.NiPostsV2{}) + atx.Sign(sig) + + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "solo atx must have one nipost") + }) + t.Run("rejects when contains more than 1 ID", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.NiPosts[0].Posts = append(atx.NiPosts[0].Posts, wire.SubPostV2{}) + atx.Sign(sig) + + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "solo atx must have one post") + }) + t.Run("rejects when PrevATXIndex != 0", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.NiPosts[0].Posts[0].PrevATXIndex = 1 + atx.Sign(sig) + + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "solo atx post must have prevATXIndex 0") + }) +} + +func TestHandlerV2_SyntacticallyValidate_MergedAtx(t *testing.T) { + t.Parallel() + golden := types.RandomATXID() + atxHandler := newV2TestHandler(t, golden) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + t.Run("merged ATXs are not supported yet", func(t *testing.T) { + t.Parallel() + atx := newSoloATXv2(t, 0, types.RandomATXID(), types.RandomATXID()) + atx.MarriageATX = &golden + atx.Sign(sig) + + atxHandler.mclock.EXPECT().CurrentLayer() + err := atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "atx merge is not supported") + }) +} + +func TestHandlerV2_ProcessSoloATX(t *testing.T) { + t.Parallel() + golden := types.RandomATXID() + peer := peer.ID("other") + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + t.Run("initial ATX", func(t *testing.T) { + t.Parallel() + atx := newInitialATXv2(t, golden) + atx.Sign(sig) + blob := codec.MustEncode(atx) + + atxHandler := newV2TestHandler(t, golden) + atxHandler.expectInitialAtxV2(atx) + + proof, err := atxHandler.processATX(context.Background(), peer, atx, blob, time.Now()) + require.NoError(t, err) + require.Nil(t, proof) + + // TODO: verify that the ATX was added to the DB + // TODO: processing ATX for the second time should skip checks + // proof, err = atxHandler.processATX(context.Background(), peer, atx, blob, time.Now()) + // require.NoError(t, err) + // require.Nil(t, proof) + }) + // TODO: more tests +} + +func newInitialATXv2(t testing.TB, golden types.ATXID) *wire.ActivationTxV2 { + t.Helper() + nonce := uint64(999) + coinbase := types.GenerateAddress([]byte("aaaa")) + atx := &wire.ActivationTxV2{ + PositioningATX: golden, + Initial: &wire.InitialAtxPartsV2{CommitmentATX: golden}, + NiPosts: []wire.NiPostsV2{ + { + Challenge: types.RandomHash(), + Posts: []wire.SubPostV2{ + { + NumUnits: 4, + }, + }, + }, + }, + Coinbase: &coinbase, + VRFNonce: &nonce, + } + + return atx +} + +func newSoloATXv2(t testing.TB, publish types.EpochID, prev, pos types.ATXID) *wire.ActivationTxV2 { + t.Helper() + + atx := &wire.ActivationTxV2{ + PublishEpoch: publish, + PreviousATXs: []types.ATXID{prev}, + PositioningATX: pos, + NiPosts: []wire.NiPostsV2{ + { + Challenge: types.RandomHash(), + Posts: []wire.SubPostV2{ + { + NumUnits: 4, + }, + }, + }, + }, + } + + return atx +} diff --git a/activation/mocks_handler_v2.go b/activation/mocks_handler_v2.go new file mode 100644 index 0000000000..30130f7b4a --- /dev/null +++ b/activation/mocks_handler_v2.go @@ -0,0 +1,161 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./handler_v2.go +// +// Generated by this command: +// +// mockgen -typed -source=./handler_v2.go -destination=mocks_handler_v2.go -package=activation +// + +// Package activation is a generated GoMock package. +package activation + +import ( + context "context" + reflect "reflect" + + wire "github.com/spacemeshos/go-spacemesh/activation/wire" + types "github.com/spacemeshos/go-spacemesh/common/types" + gomock "go.uber.org/mock/gomock" +) + +// MocknipostValidatorV2 is a mock of nipostValidatorV2 interface. +type MocknipostValidatorV2 struct { + ctrl *gomock.Controller + recorder *MocknipostValidatorV2MockRecorder +} + +// MocknipostValidatorV2MockRecorder is the mock recorder for MocknipostValidatorV2. +type MocknipostValidatorV2MockRecorder struct { + mock *MocknipostValidatorV2 +} + +// NewMocknipostValidatorV2 creates a new mock instance. +func NewMocknipostValidatorV2(ctrl *gomock.Controller) *MocknipostValidatorV2 { + mock := &MocknipostValidatorV2{ctrl: ctrl} + mock.recorder = &MocknipostValidatorV2MockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MocknipostValidatorV2) EXPECT() *MocknipostValidatorV2MockRecorder { + return m.recorder +} + +// IsVerifyingFullPost mocks base method. +func (m *MocknipostValidatorV2) IsVerifyingFullPost() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsVerifyingFullPost") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsVerifyingFullPost indicates an expected call of IsVerifyingFullPost. +func (mr *MocknipostValidatorV2MockRecorder) IsVerifyingFullPost() *MocknipostValidatorV2IsVerifyingFullPostCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsVerifyingFullPost", reflect.TypeOf((*MocknipostValidatorV2)(nil).IsVerifyingFullPost)) + return &MocknipostValidatorV2IsVerifyingFullPostCall{Call: call} +} + +// MocknipostValidatorV2IsVerifyingFullPostCall wrap *gomock.Call +type MocknipostValidatorV2IsVerifyingFullPostCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MocknipostValidatorV2IsVerifyingFullPostCall) Return(arg0 bool) *MocknipostValidatorV2IsVerifyingFullPostCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MocknipostValidatorV2IsVerifyingFullPostCall) Do(f func() bool) *MocknipostValidatorV2IsVerifyingFullPostCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MocknipostValidatorV2IsVerifyingFullPostCall) DoAndReturn(f func() bool) *MocknipostValidatorV2IsVerifyingFullPostCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PostV2 mocks base method. +func (m *MocknipostValidatorV2) PostV2(ctx context.Context, smesherID types.NodeID, commitment types.ATXID, post *wire.PostV1, challenge []byte, numUnits uint32, opts ...validatorOption) error { + m.ctrl.T.Helper() + varargs := []any{ctx, smesherID, commitment, post, challenge, numUnits} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PostV2", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// PostV2 indicates an expected call of PostV2. +func (mr *MocknipostValidatorV2MockRecorder) PostV2(ctx, smesherID, commitment, post, challenge, numUnits any, opts ...any) *MocknipostValidatorV2PostV2Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, smesherID, commitment, post, challenge, numUnits}, opts...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostV2", reflect.TypeOf((*MocknipostValidatorV2)(nil).PostV2), varargs...) + return &MocknipostValidatorV2PostV2Call{Call: call} +} + +// MocknipostValidatorV2PostV2Call wrap *gomock.Call +type MocknipostValidatorV2PostV2Call struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MocknipostValidatorV2PostV2Call) Return(arg0 error) *MocknipostValidatorV2PostV2Call { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MocknipostValidatorV2PostV2Call) Do(f func(context.Context, types.NodeID, types.ATXID, *wire.PostV1, []byte, uint32, ...validatorOption) error) *MocknipostValidatorV2PostV2Call { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MocknipostValidatorV2PostV2Call) DoAndReturn(f func(context.Context, types.NodeID, types.ATXID, *wire.PostV1, []byte, uint32, ...validatorOption) error) *MocknipostValidatorV2PostV2Call { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// VRFNonceV2 mocks base method. +func (m *MocknipostValidatorV2) VRFNonceV2(smesherID types.NodeID, commitment types.ATXID, vrfNonce types.VRFPostIndex, numUnits uint32) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VRFNonceV2", smesherID, commitment, vrfNonce, numUnits) + ret0, _ := ret[0].(error) + return ret0 +} + +// VRFNonceV2 indicates an expected call of VRFNonceV2. +func (mr *MocknipostValidatorV2MockRecorder) VRFNonceV2(smesherID, commitment, vrfNonce, numUnits any) *MocknipostValidatorV2VRFNonceV2Call { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VRFNonceV2", reflect.TypeOf((*MocknipostValidatorV2)(nil).VRFNonceV2), smesherID, commitment, vrfNonce, numUnits) + return &MocknipostValidatorV2VRFNonceV2Call{Call: call} +} + +// MocknipostValidatorV2VRFNonceV2Call wrap *gomock.Call +type MocknipostValidatorV2VRFNonceV2Call struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MocknipostValidatorV2VRFNonceV2Call) Return(arg0 error) *MocknipostValidatorV2VRFNonceV2Call { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MocknipostValidatorV2VRFNonceV2Call) Do(f func(types.NodeID, types.ATXID, types.VRFPostIndex, uint32) error) *MocknipostValidatorV2VRFNonceV2Call { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MocknipostValidatorV2VRFNonceV2Call) DoAndReturn(f func(types.NodeID, types.ATXID, types.VRFPostIndex, uint32) error) *MocknipostValidatorV2VRFNonceV2Call { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index 545873d05b..455b8b337a 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -1,6 +1,10 @@ package wire -import "github.com/spacemeshos/go-spacemesh/common/types" +import ( + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" +) //go:generate scalegen @@ -40,6 +44,25 @@ type ActivationTxV2 struct { Signature types.EdSignature } +// TODO: This is dummy implementation for testing. +// It needs to be implemented properly. +func (atx *ActivationTxV2) SignedBytes() []byte { + atxCopy := *atx + atxCopy.Signature = types.EdSignature{} + return codec.MustEncode(&atxCopy) +} + +// TODO: This is dummy implementation for testing. +// It needs to be implemented properly. +func (atx *ActivationTxV2) ID() types.ATXID { + return types.BytesToATXID(atx.SignedBytes()) +} + +func (atx *ActivationTxV2) Sign(signer *signing.EdSigner) { + atx.SmesherID = signer.NodeID() + atx.Signature = signer.Sign(signing.ATX, atx.SignedBytes()) +} + type InitialAtxPartsV2 struct { CommitmentATX types.ATXID Post PostV1