Skip to content

Commit

Permalink
ATX V2 syntactical validation (no deps) (#5936)
Browse files Browse the repository at this point in the history
  • Loading branch information
poszu committed May 23, 2024
1 parent c865710 commit 794aa17
Show file tree
Hide file tree
Showing 6 changed files with 842 additions and 18 deletions.
9 changes: 9 additions & 0 deletions activation/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
}
Expand Down
60 changes: 43 additions & 17 deletions activation/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
})
}
177 changes: 177 additions & 0 deletions activation/handler_v2.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 794aa17

Please sign in to comment.