From 8263c092d6ab56610373f9e850e71424266ff3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Tue, 11 Jun 2024 16:20:17 +0200 Subject: [PATCH 01/25] Syntactical validation of merged ATX --- activation/e2e/atx_merge_test.go | 416 ++++++++++++++++++++++++ activation/e2e/certifier_client_test.go | 1 + activation/handler_v2.go | 117 +++++-- activation/handler_v2_test.go | 391 +++++++++++++++++++++- activation/post.go | 8 + activation/wire/challenge_v2.go | 1 + sql/identities/identities.go | 59 +++- sql/identities/identities_test.go | 71 +++- sql/migrations/state/0019_marriages.sql | 1 + 9 files changed, 1024 insertions(+), 41 deletions(-) create mode 100644 activation/e2e/atx_merge_test.go diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go new file mode 100644 index 0000000000..ac1af716f2 --- /dev/null +++ b/activation/e2e/atx_merge_test.go @@ -0,0 +1,416 @@ +package activation_test + +import ( + "context" + "net/url" + "slices" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/peer" + "github.com/spacemeshos/merkle-tree" + "github.com/spacemeshos/poet/registration" + "github.com/spacemeshos/poet/shared" + "github.com/spacemeshos/post/verifying" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" + "golang.org/x/exp/maps" + "golang.org/x/sync/errgroup" + + "github.com/spacemeshos/go-spacemesh/activation" + "github.com/spacemeshos/go-spacemesh/activation/wire" + "github.com/spacemeshos/go-spacemesh/api/grpcserver" + "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/p2p/pubsub" + "github.com/spacemeshos/go-spacemesh/p2p/pubsub/mocks" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/sql/identities" + "github.com/spacemeshos/go-spacemesh/sql/localsql" + "github.com/spacemeshos/go-spacemesh/sql/localsql/nipost" + smocks "github.com/spacemeshos/go-spacemesh/system/mocks" + "github.com/spacemeshos/go-spacemesh/timesync" +) + +func constructMerkleProof(t *testing.T, members []types.Hash32) wire.MerkleProofV2 { + t.Helper() + ids := make(map[uint64]bool, len(members)) + for i := range members { + ids[uint64(i)] = true + } + + tree, err := merkle.NewTreeBuilder(). + WithLeavesToProve(ids). + WithHashFunc(shared.HashMembershipTreeNode). + Build() + require.NoError(t, err) + for _, member := range members { + require.NoError(t, tree.AddLeaf(member[:])) + } + nodes := tree.Proof() + nodesH32 := make([]types.Hash32, 0, len(nodes)) + for _, n := range nodes { + nodesH32 = append(nodesH32, types.BytesToHash(n)) + } + indicies := maps.Keys(ids) + slices.Sort(indicies) + return wire.MerkleProofV2{ + LeafIndices: indicies, + Nodes: nodesH32, + } +} + +func buildNipost(nb *activation.NIPostBuilder, sig *signing.EdSigner, publish types.EpochID, previous, positioning types.ATXID) (*nipost.NIPostState, error) { + challenge := wire.NIPostChallengeV2{ + PublishEpoch: publish, + PrevATXID: previous, + PositioningATXID: positioning, + } + nipost, err := nb.BuildNIPost(context.Background(), sig, challenge.PublishEpoch, challenge.Hash()) + nb.ResetState(sig.NodeID()) + return nipost, err +} + +func Test_MarryAndMerge(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + signers := []*signing.EdSigner{} + for range 2 { + sig, err := signing.NewEdSigner() + require.NoError(t, err) + signers = append(signers, sig) + } + + logger := zaptest.NewLogger(t) + goldenATX := types.ATXID{2, 3, 4} + cfg := activation.DefaultPostConfig() + db := sql.InMemory() + cdb := datastore.NewCachedDB(db, logger) + localDB := localsql.InMemory() + + syncer := activation.NewMocksyncer(ctrl) + syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { + synced := make(chan struct{}) + close(synced) + return synced + }).AnyTimes() + + svc := grpcserver.NewPostService(logger) + svc.AllowConnections(true) + grpcCfg, cleanup := launchServer(t, svc) + t.Cleanup(cleanup) + + opts := testPostSetupOpts(t) + verifyingOpts := activation.DefaultTestPostVerifyingOpts() + verifier, err := activation.NewPostVerifier(cfg, logger, activation.WithVerifyingOpts(verifyingOpts)) + require.NoError(t, err) + t.Cleanup(func() { assert.NoError(t, verifier.Close()) }) + poetDb := activation.NewPoetDb(db, logger.Named("poetDb")) + validator := activation.NewValidator(db, poetDb, cfg, opts.Scrypt, verifier) + + var eg errgroup.Group + var totalNumUnits uint32 + for i, sig := range signers { + opts := opts + opts.DataDir = t.TempDir() + opts.NumUnits = uint32(i + 1) + totalNumUnits += opts.NumUnits + + eg.Go(func() error { + mgr, err := activation.NewPostSetupManager(cfg, logger, db, atxsdata.New(), goldenATX, syncer, validator) + require.NoError(t, err) + + t.Cleanup(launchPostSupervisor(t, zap.NewNop(), mgr, sig, grpcCfg, opts)) + + require.Eventually(t, func() bool { + _, err := svc.Client(sig.NodeID()) + return err == nil + }, 10*time.Second, 100*time.Millisecond, "timed out waiting for connection") + return nil + }) + } + require.NoError(t, eg.Wait()) + + // ensure that genesis aligns with layer timings + genesis := time.Now().Add(layerDuration).Round(layerDuration) + layerDuration := 2 * time.Second + epoch := layersPerEpoch * layerDuration + poetCfg := activation.PoetConfig{ + PhaseShift: epoch, + CycleGap: epoch / 2, + GracePeriod: epoch / 5, + RequestTimeout: epoch / 5, + RequestRetryDelay: epoch / 50, + MaxRequestRetries: 10, + } + + pubkey, address := spawnTestCertifier(t, cfg, nil, verifying.WithLabelScryptParams(opts.Scrypt)) + certClient := activation.NewCertifierClient(db, localDB, logger.Named("certifier")) + certifier := activation.NewCertifier(localDB, logger, certClient) + poet := spawnPoet( + t, + WithGenesis(genesis), + WithEpochDuration(epoch), + WithPhaseShift(poetCfg.PhaseShift), + WithCycleGap(poetCfg.CycleGap), + WithCertifier(®istration.CertifierConfig{ + URL: (&url.URL{Scheme: "http", Host: address.String()}).String(), + PubKey: registration.Base64Enc(pubkey), + }), + ) + poetClient, err := poet.Client(poetDb, poetCfg, logger, activation.WithCertifier(certifier)) + require.NoError(t, err) + + clock, err := timesync.NewClock( + timesync.WithGenesisTime(genesis), + timesync.WithLayerDuration(layerDuration), + timesync.WithTickInterval(100*time.Millisecond), + timesync.WithLogger(zap.NewNop()), + ) + require.NoError(t, err) + t.Cleanup(clock.Close) + + nb, err := activation.NewNIPostBuilder( + localDB, + svc, + logger.Named("nipostBuilder"), + poetCfg, + clock, + activation.WithPoetClients(poetClient), + ) + require.NoError(t, err) + + conf := activation.Config{ + GoldenATXID: goldenATX, + RegossipInterval: 0, + } + + data := atxsdata.New() + atxVersions := activation.AtxVersions{postGenesisEpoch: types.AtxV2} + edVerifier := signing.NewEdVerifier() + mpub := mocks.NewMockPublisher(ctrl) + mFetch := smocks.NewMockFetcher(ctrl) + mBeacon := activation.NewMockAtxReceiver(ctrl) + mTortoise := smocks.NewMockTortoise(ctrl) + + tickSize := uint64(3) + atxHdlr := activation.NewHandler( + "local", + cdb, + data, + edVerifier, + clock, + mpub, + mFetch, + goldenATX, + validator, + mBeacon, + mTortoise, + logger, + activation.WithAtxVersions(atxVersions), + activation.WithTickSize(tickSize), + ) + + mpub.EXPECT().Publish(gomock.Any(), pubsub.AtxProtocol, gomock.Any()).DoAndReturn( + func(ctx context.Context, p string, got []byte) error { + mFetch.EXPECT().RegisterPeerHashes(peer.ID(p), gomock.Any()) + mFetch.EXPECT().GetPoetProof(gomock.Any(), gomock.Any()) + mBeacon.EXPECT().OnAtx(gomock.Any()) + mTortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + return atxHdlr.HandleGossipAtx(ctx, peer.ID(p), got) + }, + ).Times(2) + + v := activation.NewValidator(db, poetDb, cfg, opts.Scrypt, verifier) + builder := activation.NewBuilder( + conf, + db, + data, + localDB, + mpub, + nb, + clock, + syncer, + logger, + activation.WithPoetConfig(poetCfg), + activation.WithValidator(v), + ) + + // Step 1. Publish initial ATXs for each signer + eg = errgroup.Group{} + for _, signer := range signers { + eg.Go(func() error { + post, postInfo, err := nb.Proof(context.Background(), signer.NodeID(), types.EmptyHash32[:]) + if err != nil { + return err + } + initialPost := nipost.Post{ + Nonce: post.Nonce, + Indices: post.Indices, + Pow: post.Pow, + Challenge: types.EmptyHash32[:], + NumUnits: postInfo.NumUnits, + CommitmentATX: postInfo.CommitmentATX, + VRFNonce: *postInfo.Nonce, + } + if err := nipost.AddPost(localDB, signer.NodeID(), initialPost); err != nil { + return err + } + return builder.PublishActivationTx(context.Background(), signer) + }) + } + require.NoError(t, eg.Wait()) + + // Step 2. Marry + mainID, mergedID := signers[0], signers[1] + + prevMergedIDATX, err := atxs.GetLastIDByNodeID(db, mergedID.NodeID()) + require.NoError(t, err) + prevATXID, err := atxs.GetLastIDByNodeID(db, mainID.NodeID()) + require.NoError(t, err) + prev, err := atxs.Get(db, prevATXID) + require.NoError(t, err) + + challenge := wire.NIPostChallengeV2{ + PublishEpoch: prev.PublishEpoch + 1, + PrevATXID: prevATXID, + PositioningATXID: prevATXID, + } + + nipostState, err := nb.BuildNIPost(context.Background(), mainID, challenge.PublishEpoch, challenge.Hash()) + require.NoError(t, err) + require.NoError(t, nb.ResetState(mainID.NodeID())) + + marriageATX := &wire.ActivationTxV2{ + PublishEpoch: challenge.PublishEpoch, + PositioningATX: challenge.PositioningATXID, + PreviousATXs: []types.ATXID{challenge.PrevATXID}, + Coinbase: builder.Coinbase(), + VRFNonce: (uint64)(nipostState.VRFNonce), + NiPosts: []wire.NiPostsV2{ + { + Membership: wire.MerkleProofV2{ + Nodes: nipostState.Membership.Nodes, + LeafIndices: []uint64{nipostState.Membership.LeafIndex}, + }, + Challenge: types.Hash32(nipostState.NIPost.PostMetadata.Challenge), + Posts: []wire.SubPostV2{ + { + Post: *wire.PostToWireV1(nipostState.Post), + NumUnits: nipostState.NumUnits, + }, + }, + }, + }, + Marriages: []wire.MarriageCertificate{ + { + ReferenceAtx: prevMergedIDATX, + Signature: mergedID.Sign(signing.MARRIAGE, mainID.NodeID().Bytes()), + }, + }, + } + marriageATX.Sign(mainID) + logger.Info("publishing marriage ATX", zap.Inline(marriageATX)) + + mFetch.EXPECT().RegisterPeerHashes(peer.ID(""), gomock.Any()) + mFetch.EXPECT().GetPoetProof(gomock.Any(), gomock.Any()) + mFetch.EXPECT().GetAtxs(gomock.Any(), gomock.Any(), gomock.Any()) + mBeacon.EXPECT().OnAtx(gomock.Any()) + mTortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + err = atxHdlr.HandleGossipAtx(context.Background(), "", codec.MustEncode(marriageATX)) + require.NoError(t, err) + + // Verify marriage + for i, signer := range signers { + marriage, idx, err := identities.MarriageInfo(db, signer.NodeID()) + require.NoError(t, err) + require.NotNil(t, marriage) + require.Equal(t, marriageATX.ID(), *marriage) + require.Equal(t, i, idx) + } + + // Step 3. Publish merged ATX together + eg = errgroup.Group{} + var niposts [2]*nipost.NIPostState + // 3.1. NiPOST for main ID (the publisher) + eg.Go(func() error { + niposts[0], err = buildNipost(nb, mainID, marriageATX.PublishEpoch+2, marriageATX.ID(), marriageATX.ID()) + return err + }) + + // 3.2. NiPOST for merged ID + prevATXID, err = atxs.GetLastIDByNodeID(db, mergedID.NodeID()) + require.NoError(t, err) + eg.Go(func() error { + niposts[1], err = buildNipost(nb, mergedID, marriageATX.PublishEpoch+2, prevATXID, marriageATX.ID()) + return err + }) + require.NoError(t, eg.Wait()) + + // 3.3 Construct a multi-ID poet membership merkle proof for both IDs + poetRef := niposts[0].PostMetadata.Challenge + poetProof, members, err := poetClient.Proof(context.Background(), "2") + require.NoError(t, err) + membershipProof := constructMerkleProof(t, members) + + mATXID := marriageATX.ID() + mergedATX := &wire.ActivationTxV2{ + PublishEpoch: marriageATX.PublishEpoch + 2, + PreviousATXs: []types.ATXID{ + mATXID, + prevATXID, + }, + MarriageATX: &mATXID, + PositioningATX: mATXID, + Coinbase: builder.Coinbase(), + VRFNonce: marriageATX.VRFNonce, + NiPosts: []wire.NiPostsV2{ + { + Membership: membershipProof, + Challenge: types.Hash32(poetRef), + Posts: make([]wire.SubPostV2, 2), + }, + }, + } + // 3.4 Append PoSTs for both IDs + // PoSTs must be ordered by their leaf index in the poet membership proof + for marriageIdx, nipost := range niposts { + mergedATX.NiPosts[0].Posts[nipost.Membership.LeafIndex] = wire.SubPostV2{ + MarriageIndex: uint32(marriageIdx), + PrevATXIndex: uint32(marriageIdx), + Post: *wire.PostToWireV1(nipost.Post), + NumUnits: nipost.NumUnits, + } + } + + // 3.5 Publish + mergedATX.Sign(mainID) + logger.Info("publishing merged ATX", zap.Inline(mergedATX)) + + mFetch.EXPECT().RegisterPeerHashes(peer.ID(""), gomock.Any()) + mFetch.EXPECT().GetPoetProof(gomock.Any(), gomock.Any()) + mFetch.EXPECT().GetAtxs(gomock.Any(), gomock.Any(), gomock.Any()) + mBeacon.EXPECT().OnAtx(gomock.Any()) + mTortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + err = atxHdlr.HandleGossipAtx(context.Background(), "", codec.MustEncode(mergedATX)) + require.NoError(t, err) + + // Step 4. verify the merged ATX + atx, err := atxs.Get(db, mergedATX.ID()) + require.NoError(t, err) + require.Equal(t, totalNumUnits, atx.NumUnits) + require.Equal(t, mainID.NodeID(), atx.SmesherID) + require.Equal(t, poetProof.LeafCount/tickSize, atx.TickCount) + + mATX, err := atxs.Get(db, mATXID) + require.NoError(t, err) + require.Equal(t, mATX.TickHeight(), atx.BaseTickHeight) // mATX was used as positioning ATX +} diff --git a/activation/e2e/certifier_client_test.go b/activation/e2e/certifier_client_test.go index 26da2c7df4..ea313e0cd7 100644 --- a/activation/e2e/certifier_client_test.go +++ b/activation/e2e/certifier_client_test.go @@ -197,6 +197,7 @@ func spawnTestCertifier( postVerifier, err := activation.NewPostVerifier( cfg, zaptest.NewLogger(t), + activation.WithVerifyingOpts(activation.DefaultTestPostVerifyingOpts()), ) require.NoError(t, err) var eg errgroup.Group diff --git a/activation/handler_v2.go b/activation/handler_v2.go index a369c50aaf..6613473a68 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -152,8 +152,6 @@ func (h *HandlerV2) processATX( } // Syntactically validate an ATX. -// TODOs: -// 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) @@ -230,8 +228,9 @@ func (h *HandlerV2) syntacticallyValidate(ctx context.Context, atx *wire.Activat if len(atx.Marriages) != 0 { return errors.New("merged atx cannot have marriages") } - // TODO: support merged ATXs - return errors.New("atx merge is not supported") + if err := h.verifyIncludedIDsUniqueness(atx); err != nil { + return err + } default: // Solo chained (non-initial) ATX if len(atx.PreviousATXs) != 1 { @@ -367,13 +366,28 @@ func (h *HandlerV2) validatePreviousAtx(id types.NodeID, post *wire.SubPostV2, p } return min(prev.NumUnits, post.NumUnits), nil case *wire.ActivationTxV2: - // TODO: support previous merged-ATX - - // previous is solo ATX - if prev.SmesherID == id { - return min(prev.NiPosts[0].Posts[0].NumUnits, post.NumUnits), nil + if prev.MarriageATX != nil { + // Previous is a merged ATX + // need to find out if the given ID was present in the previous ATX + _, idx, err := identities.MarriageInfo(h.cdb, id) + if err != nil { + return 0, fmt.Errorf("fetching marriage info for ID %s: %w", id, err) + } + for _, nipost := range prev.NiPosts { + for _, post := range nipost.Posts { + if post.MarriageIndex == uint32(idx) { + return min(post.NumUnits, post.NumUnits), nil + } + } + } + } else { + // Previous is a solo ATX + if prev.SmesherID == id { + return min(prev.NiPosts[0].Posts[0].NumUnits, post.NumUnits), nil + } } - return 0, fmt.Errorf("previous solo ATX V2 has different owner: %s (expected %s)", prev.SmesherID, id) + + return 0, fmt.Errorf("previous ATX V2 doesn't contain %s", id) } return 0, fmt.Errorf("unexpected previous ATX type: %T", prev) } @@ -440,11 +454,56 @@ func (h *HandlerV2) validateMarriages(atx *wire.ActivationTxV2) ([]types.NodeID, return marryingIDs, nil } +// Validate marriage ATX and return the full equivocation set. +func (h *HandlerV2) equivocationSet(atx *wire.ActivationTxV2) ([]types.NodeID, error) { + if atx.MarriageATX == nil { + return []types.NodeID{atx.SmesherID}, nil + } + marriageAtxID, _, err := identities.MarriageInfo(h.cdb, atx.SmesherID) + switch { + case errors.Is(err, sql.ErrNotFound) || marriageAtxID == nil: + return nil, errors.New("smesher is not married") + case err != nil: + return nil, fmt.Errorf("fetching smesher's marriage atx ID: %w", err) + } + + if *atx.MarriageATX != *marriageAtxID { + return nil, fmt.Errorf("smesher's marriage ATX ID mismatch: %s != %s", *atx.MarriageATX, *marriageAtxID) + } + + marriageAtx, err := atxs.Get(h.cdb, *atx.MarriageATX) + if err != nil { + return nil, fmt.Errorf("fetching marriage atx: %w", err) + } + if !(marriageAtx.PublishEpoch <= atx.PublishEpoch-2) { + return nil, fmt.Errorf( + "marriage atx must be published at least 2 epochs before %v (is %v)", + atx.PublishEpoch, + marriageAtx.PublishEpoch, + ) + } + + return identities.EquivocationSetByMarriageATX(h.cdb, *atx.MarriageATX) +} + type atxParts struct { leaves uint64 effectiveUnits uint32 } +func (h *HandlerV2) verifyIncludedIDsUniqueness(atx *wire.ActivationTxV2) error { + seen := make(map[uint32]struct{}) + for _, niposts := range atx.NiPosts { + for _, post := range niposts.Posts { + if _, ok := seen[post.MarriageIndex]; ok { + return fmt.Errorf("ID present twice (duplicated marriage index): %d", post.MarriageIndex) + } + seen[post.MarriageIndex] = struct{}{} + } + } + return nil +} + // Syntactically validate the ATX with its dependencies. func (h *HandlerV2) syntacticallyValidateDeps( ctx context.Context, @@ -469,33 +528,42 @@ func (h *HandlerV2) syntacticallyValidateDeps( previousAtxs[i] = prevAtx } - // validate all niposts - // TODO: support merged ATXs - // For a merged ATX we need to fetch the equivocation this smesher is part of. - equivocationSet := []types.NodeID{atx.SmesherID} + equivocationSet, err := h.equivocationSet(atx) + if err != nil { + return nil, nil, fmt.Errorf("validating marriages: %w", err) + } + + // validate previous ATXs var totalEffectiveNumUnits uint32 - var minLeaves uint64 = math.MaxUint64 - var smesherCommitment *types.ATXID for _, niposts := range atx.NiPosts { - // verify PoET memberships in a single go - var poetChallenges [][]byte - for _, post := range niposts.Posts { if post.MarriageIndex >= uint32(len(equivocationSet)) { err := fmt.Errorf("marriage index out of bounds: %d > %d", post.MarriageIndex, len(equivocationSet)-1) return nil, nil, err } + id := equivocationSet[post.MarriageIndex] effectiveNumUnits := post.NumUnits if atx.Initial == nil { var err error effectiveNumUnits, err = h.validatePreviousAtx(id, &post, previousAtxs) if err != nil { - return nil, nil, fmt.Errorf("validating previous atx for ID %s: %w", id, err) + return nil, nil, fmt.Errorf("validating previous atx: %w", err) } } totalEffectiveNumUnits += effectiveNumUnits + } + } + // validate all niposts + var minLeaves uint64 = math.MaxUint64 + var smesherCommitment *types.ATXID + for _, niposts := range atx.NiPosts { + // verify PoET memberships in a single go + var poetChallenges [][]byte + + for _, post := range niposts.Posts { + id := equivocationSet[post.MarriageIndex] var commitment types.ATXID if atx.Initial != nil { commitment = atx.Initial.CommitmentATX @@ -505,7 +573,7 @@ func (h *HandlerV2) syntacticallyValidateDeps( if err != nil { return nil, nil, fmt.Errorf("commitment atx not found for ID %s: %w", id, err) } - if smesherCommitment == nil { + if id == atx.SmesherID { smesherCommitment = &commitment } } @@ -561,6 +629,9 @@ func (h *HandlerV2) syntacticallyValidateDeps( } if atx.Initial == nil { + if smesherCommitment == nil { + return nil, nil, errors.New("ATX signer not present in merged ATX") + } err := h.nipostValidator.VRFNonceV2(atx.SmesherID, *smesherCommitment, atx.VRFNonce, atx.TotalNumUnits()) if err != nil { return nil, nil, fmt.Errorf("validating VRF nonce: %w", err) @@ -645,8 +716,8 @@ func (h *HandlerV2) storeAtx( } if len(marrying) != 0 { - for _, id := range marrying { - if err := identities.SetMarriage(tx, id, atx.ID()); err != nil { + for i, id := range marrying { + if err := identities.SetMarriage(tx, id, atx.ID(), i); err != nil { return err } } diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 3de4a113eb..5592e6dea6 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -3,6 +3,7 @@ package activation import ( "context" "errors" + "fmt" "testing" "time" @@ -31,6 +32,11 @@ type v2TestHandler struct { handlerMocks } +type marriedId struct { + signer *signing.EdSigner + refAtx *wire.ActivationTxV2 +} + const poetLeaves = 200 func newV2TestHandler(tb testing.TB, golden types.ATXID) *v2TestHandler { @@ -83,6 +89,28 @@ func (h *handlerMocks) expectVerifyNIPoST(atx *wire.ActivationTxV2) { ).Return(poetLeaves, nil) } +func (h *handlerMocks) expectVerifyNIPoSTs(atx *wire.ActivationTxV2, equivocationSet []types.NodeID) { + for _, nipost := range atx.NiPosts { + for _, post := range nipost.Posts { + h.mValidator.EXPECT().PostV2( + gomock.Any(), + equivocationSet[post.MarriageIndex], + gomock.Any(), + wire.PostFromWireV1(&post.Post), + nipost.Challenge.Bytes(), + post.NumUnits, + gomock.Any(), + ) + } + h.mValidator.EXPECT().PoetMembership( + gomock.Any(), + gomock.Any(), + nipost.Challenge, + gomock.Any(), + ) + } +} + func (h *handlerMocks) expectStoreAtxV2(atx *wire.ActivationTxV2) { h.mbeacon.EXPECT().OnAtx(gomock.Any()) h.mtortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) @@ -125,6 +153,19 @@ func (h *handlerMocks) expectAtxV2(atx *wire.ActivationTxV2) { h.expectStoreAtxV2(atx) } +func (h *handlerMocks) expectMergedAtxV2(atx *wire.ActivationTxV2, equivocationSet []types.NodeID) { + h.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) + h.expectFetchDeps(atx) + h.mValidator.EXPECT().VRFNonceV2( + atx.SmesherID, + gomock.Any(), + atx.VRFNonce, + atx.TotalNumUnits(), + ) + h.expectVerifyNIPoSTs(atx, equivocationSet) + h.expectStoreAtxV2(atx) +} + func (h *v2TestHandler) createAndProcessInitial(t *testing.T, sig *signing.EdSigner) *wire.ActivationTxV2 { t.Helper() atx := newInitialATXv2(t, h.handlerMocks.goldenATXID) @@ -374,15 +415,19 @@ func TestHandlerV2_SyntacticallyValidate_MergedAtx(t *testing.T) { sig, err := signing.NewEdSigner() require.NoError(t, err) - t.Run("merged ATXs are not supported yet", func(t *testing.T) { + t.Run("cannot have marriage", func(t *testing.T) { t.Parallel() + atx := newSoloATXv2(t, 0, types.RandomATXID(), types.RandomATXID()) atx.MarriageATX = &golden + atx.Marriages = []wire.MarriageCertificate{{ + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }} atx.Sign(sig) atxHandler.mclock.EXPECT().CurrentLayer() - err := atxHandler.syntacticallyValidate(context.Background(), atx) - require.ErrorContains(t, err, "atx merge is not supported") + err = atxHandler.syntacticallyValidate(context.Background(), atx) + require.ErrorContains(t, err, "merged atx cannot have marriages") }) } @@ -587,6 +632,207 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { }) } +func marryIDs( + t *testing.T, + atxHandler *v2TestHandler, + sig *signing.EdSigner, + golden types.ATXID, + num int, +) (marriage *wire.ActivationTxV2, other []*wire.ActivationTxV2) { + var ( + marriedIds []marriedId + equivocationSet = []types.NodeID{sig.NodeID()} + ) + for range num { + signer, err := signing.NewEdSigner() + require.NoError(t, err) + atx := atxHandler.createAndProcessInitial(t, signer) + marriedIds = append(marriedIds, marriedId{signer, atx}) + } + + var atxs []*wire.ActivationTxV2 + mATX := newInitialATXv2(t, golden) + mATX.Marriages = []wire.MarriageCertificate{{ + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }} + for _, id := range marriedIds { + mATX.Marriages = append(mATX.Marriages, wire.MarriageCertificate{ + ReferenceAtx: id.refAtx.ID(), + Signature: id.signer.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }) + equivocationSet = append(equivocationSet, id.signer.NodeID()) + } + mATX.Sign(sig) + atxHandler.expectInitialAtxV2(mATX) + p, err := atxHandler.processATX(context.Background(), "", mATX, codec.MustEncode(mATX), time.Now()) + require.NoError(t, err) + require.Nil(t, p) + + // Other IDs publish their first ATXs. + for _, id := range marriedIds { + atx := newInitialATXv2(t, golden) + atx.Sign(id.signer) + atxHandler.expectInitialAtxV2(atx) + _, err := atxHandler.processATX(context.Background(), "", atx, codec.MustEncode(atx), time.Now()) + require.NoError(t, err) + atxs = append(atxs, atx) + } + + return mATX, atxs +} + +func TestHandlerV2_ProcessMergedATX(t *testing.T) { + t.Parallel() + golden := types.RandomATXID() + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + t.Run("happy case", func(t *testing.T) { + atxHandler := newV2TestHandler(t, golden) + + // Marry IDs + mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 2) + previousATXs := []types.ATXID{mATX.ID()} + equivocationSet := []types.NodeID{sig.NodeID()} + for _, atx := range otherATXs { + previousATXs = append(previousATXs, atx.ID()) + equivocationSet = append(equivocationSet, atx.SmesherID) + } + + // Process a merged ATX + merged := newSoloATXv2(t, mATX.PublishEpoch+2, mATX.ID(), mATX.ID()) + totalNumUnits := merged.NiPosts[0].Posts[0].NumUnits + for i, atx := range otherATXs { + post := wire.SubPostV2{ + MarriageIndex: uint32(i + 1), + NumUnits: atx.TotalNumUnits(), + PrevATXIndex: uint32(i + 1), + } + totalNumUnits += post.NumUnits + merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post) + } + mATXID := mATX.ID() + merged.MarriageATX = &mATXID + + merged.PreviousATXs = previousATXs + merged.Sign(sig) + + atxHandler.expectMergedAtxV2(merged, equivocationSet) + p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) + require.NoError(t, err) + require.Nil(t, p) + + atx, err := atxs.Get(atxHandler.cdb, merged.ID()) + require.NoError(t, err) + require.Equal(t, totalNumUnits, atx.NumUnits) + require.Equal(t, sig.NodeID(), atx.SmesherID) + }) + t.Run("signer must be included merged ATX", func(t *testing.T) { + atxHandler := newV2TestHandler(t, golden) + + // Marry IDs + mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 2) + previousATXs := []types.ATXID{} + equivocationSet := []types.NodeID{sig.NodeID()} + for _, atx := range otherATXs { + previousATXs = append(previousATXs, atx.ID()) + equivocationSet = append(equivocationSet, atx.SmesherID) + } + + // Process a merged ATX + merged := newSoloATXv2(t, mATX.PublishEpoch+2, mATX.ID(), mATX.ID()) + merged.NiPosts[0].Posts = []wire.SubPostV2{} // remove signer's PoST + for i, atx := range otherATXs { + post := wire.SubPostV2{ + MarriageIndex: uint32(i + 1), + NumUnits: atx.TotalNumUnits(), + PrevATXIndex: uint32(i), + } + merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post) + } + mATXID := mATX.ID() + merged.MarriageATX = &mATXID + + merged.PreviousATXs = previousATXs + merged.Sign(sig) + + atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) + atxHandler.expectFetchDeps(merged) + atxHandler.expectVerifyNIPoSTs(merged, equivocationSet) + + p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) + require.ErrorContains(t, err, "ATX signer not present in merged ATX") + require.Nil(t, p) + }) + t.Run("ID must be present max 1 times", func(t *testing.T) { + atxHandler := newV2TestHandler(t, golden) + + // Marry IDs + mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 1) + previousATXs := []types.ATXID{mATX.ID()} + equivocationSet := []types.NodeID{sig.NodeID()} + for _, atx := range otherATXs { + previousATXs = append(previousATXs, atx.ID()) + equivocationSet = append(equivocationSet, atx.SmesherID) + } + + // Process a merged ATX + merged := newSoloATXv2(t, mATX.PublishEpoch+2, mATX.ID(), mATX.ID()) + // Insert the same ID twice + for range 2 { + post := wire.SubPostV2{ + MarriageIndex: 1, + PrevATXIndex: 1, + NumUnits: otherATXs[0].TotalNumUnits(), + } + merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post) + } + mATXID := mATX.ID() + merged.MarriageATX = &mATXID + + merged.PreviousATXs = previousATXs + merged.Sign(sig) + + atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) + p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) + require.ErrorContains(t, err, "ID present twice (duplicated marriage index)") + require.Nil(t, p) + }) + t.Run("ID must use previous ATX containing itself", func(t *testing.T) { + atxHandler := newV2TestHandler(t, golden) + + // Marry IDs + mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 1) + previousATXs := []types.ATXID{mATX.ID()} + equivocationSet := []types.NodeID{sig.NodeID()} + for _, atx := range otherATXs { + previousATXs = append(previousATXs, atx.ID()) + equivocationSet = append(equivocationSet, atx.SmesherID) + } + + // Process a merged ATX + merged := newSoloATXv2(t, mATX.PublishEpoch+2, mATX.ID(), mATX.ID()) + post := wire.SubPostV2{ + MarriageIndex: 1, + PrevATXIndex: 0, // use wrong previous ATX + NumUnits: otherATXs[0].TotalNumUnits(), + } + merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post) + + mATXID := mATX.ID() + merged.MarriageATX = &mATXID + + merged.PreviousATXs = previousATXs + merged.Sign(sig) + + atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) + atxHandler.expectFetchDeps(merged) + p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) + require.ErrorContains(t, err, fmt.Sprintf("previous ATX V2 doesn't contain %s", otherATXs[0].SmesherID)) + require.Nil(t, p) + }) +} + func TestCollectDeps_AtxV2(t *testing.T) { goldenATX := types.RandomATXID() prev0 := types.RandomATXID() @@ -788,6 +1034,143 @@ func Test_ValidatePositioningAtx(t *testing.T) { }) } +func Test_ValidateMarriages(t *testing.T) { + t.Parallel() + golden := types.RandomATXID() + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + t.Run("marriage ATX not set (solo ATX)", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) + atx := newInitialATXv2(t, golden) + atx.Sign(sig) + + set, err := atxHandler.equivocationSet(atx) + require.NoError(t, err) + require.Equal(t, []types.NodeID{atx.SmesherID}, set) + }) + t.Run("smesher is not married", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) + atx := newSoloATXv2(t, 0, types.RandomATXID(), golden) + atx.MarriageATX = &golden + atx.Sign(sig) + + _, err := atxHandler.equivocationSet(atx) + require.ErrorContains(t, err, "smesher is not married") + }) + t.Run("marriage ATX must be published 2 epochs prior merging IDs", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) + otherSigner, err := signing.NewEdSigner() + require.NoError(t, err) + otherAtx := atxHandler.createAndProcessInitial(t, otherSigner) + + marriage := newInitialATXv2(t, golden) + marriage.PublishEpoch = 1 + marriage.Marriages = []wire.MarriageCertificate{ + { + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + { + ReferenceAtx: otherAtx.ID(), + Signature: otherSigner.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + } + marriage.Sign(sig) + + atxHandler.expectInitialAtxV2(marriage) + p, err := atxHandler.processATX(context.Background(), "", marriage, codec.MustEncode(marriage), time.Now()) + require.NoError(t, err) + require.Nil(t, p) + + atx := newSoloATXv2(t, marriage.PublishEpoch+1, types.RandomATXID(), golden) + marriageATXID := marriage.ID() + atx.MarriageATX = &marriageATXID + atx.Sign(sig) + + _, err = atxHandler.equivocationSet(atx) + require.ErrorContains(t, err, "marriage atx must be published at least 2 epochs before") + }) + t.Run("can't use somebody else's marriage ATX", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) + + otherSigner, err := signing.NewEdSigner() + require.NoError(t, err) + otherAtx := atxHandler.createAndProcessInitial(t, otherSigner) + + marriage := newInitialATXv2(t, golden) + marriage.PublishEpoch = 1 + marriage.Marriages = []wire.MarriageCertificate{ + { + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + { + ReferenceAtx: otherAtx.ID(), + Signature: otherSigner.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + } + marriage.Sign(sig) + + atxHandler.expectInitialAtxV2(marriage) + p, err := atxHandler.processATX(context.Background(), "", marriage, codec.MustEncode(marriage), time.Now()) + require.NoError(t, err) + require.Nil(t, p) + + atx := newSoloATXv2(t, marriage.PublishEpoch+1, types.RandomATXID(), golden) + marriageATXID := types.RandomATXID() + atx.MarriageATX = &marriageATXID + atx.Sign(sig) + + _, err = atxHandler.equivocationSet(atx) + require.ErrorContains(t, err, "smesher's marriage ATX ID mismatch") + }) + t.Run("smesher is married", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) + marriage := newInitialATXv2(t, golden) + marriage.Marriages = []wire.MarriageCertificate{{ + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }} + + var otherIds []marriedId + for range 5 { + signer, err := signing.NewEdSigner() + require.NoError(t, err) + atx := atxHandler.createAndProcessInitial(t, signer) + otherIds = append(otherIds, marriedId{signer, atx}) + } + + expectedSet := []types.NodeID{sig.NodeID()} + + for _, id := range otherIds { + cert := wire.MarriageCertificate{ + ReferenceAtx: id.refAtx.ID(), + Signature: id.signer.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + } + marriage.Marriages = append(marriage.Marriages, cert) + expectedSet = append(expectedSet, id.signer.NodeID()) + } + marriage.Sign(sig) + + p, err := atxHandler.processInitial(marriage) + require.NoError(t, err) + require.Nil(t, p) + + atx := newSoloATXv2(t, 0, marriage.ID(), golden) + atx.PublishEpoch = marriage.PublishEpoch + 2 + marriageATXID := marriage.ID() + atx.MarriageATX = &marriageATXID + atx.Sign(sig) + + set, err := atxHandler.equivocationSet(atx) + require.NoError(t, err) + require.Equal(t, expectedSet, set) + }) +} + func Test_LoadPreviousATX(t *testing.T) { t.Parallel() t.Run("not found", func(t *testing.T) { @@ -909,7 +1292,7 @@ func Test_ValidatePreviousATX(t *testing.T) { prev := newInitialATXv2(t, golden) prev.SmesherID = types.RandomNodeID() _, err := atxHandler.validatePreviousAtx(types.RandomNodeID(), &wire.SubPostV2{}, []opaqueAtx{prev}) - require.ErrorContains(t, err, "previous solo ATX V2 has different owner") + require.ErrorContains(t, err, "previous ATX V2 doesn't contain") }) t.Run("previous golden, valid", func(t *testing.T) { t.Parallel() diff --git a/activation/post.go b/activation/post.go index 08f083592a..7e1664df89 100644 --- a/activation/post.go +++ b/activation/post.go @@ -110,6 +110,14 @@ func DefaultPostVerifyingOpts() PostProofVerifyingOpts { } } +func DefaultTestPostVerifyingOpts() PostProofVerifyingOpts { + return PostProofVerifyingOpts{ + MinWorkers: 1, + Workers: 1, + Flags: PostPowFlags(config.DefaultVerifyingPowFlags()), + } +} + // PostSetupStatus represents a status snapshot of the Post setup. type PostSetupStatus struct { State PostSetupState diff --git a/activation/wire/challenge_v2.go b/activation/wire/challenge_v2.go index 257f7093e1..198edbd556 100644 --- a/activation/wire/challenge_v2.go +++ b/activation/wire/challenge_v2.go @@ -31,6 +31,7 @@ func (c *NIPostChallengeV2) MarshalLogObject(encoder zapcore.ObjectEncoder) erro if c == nil { return nil } + encoder.AddString("Hash", c.Hash().String()) encoder.AddUint32("PublishEpoch", c.PublishEpoch.Uint32()) encoder.AddString("PrevATXID", c.PrevATXID.String()) encoder.AddString("PositioningATX", c.PositioningATXID.String()) diff --git a/sql/identities/identities.go b/sql/identities/identities.go index 613e19326f..90327eb203 100644 --- a/sql/identities/identities.go +++ b/sql/identities/identities.go @@ -5,6 +5,8 @@ import ( "fmt" "time" + sqlite "github.com/go-llsqlite/crawshaw" + "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/malfeasance/wire" @@ -143,17 +145,45 @@ func Married(db sql.Executor, id types.NodeID) (bool, error) { return rows > 0, nil } +// MarriageInfo obtains the marriage ATX and index for given ID. +func MarriageInfo(db sql.Executor, id types.NodeID) (*types.ATXID, int, error) { + var ( + atx *types.ATXID + index int + ) + rows, err := db.Exec("select marriage_atx, marriage_idx from identities where pubkey = ?1;", + func(stmt *sql.Statement) { + stmt.BindBytes(1, id.Bytes()) + }, func(stmt *sql.Statement) bool { + if stmt.ColumnType(0) != sqlite.SQLITE_NULL { + atx = new(types.ATXID) + stmt.ColumnBytes(0, atx[:]) + + index = int(stmt.ColumnInt64(1)) + } + return false + }) + if err != nil { + return nil, 0, fmt.Errorf("getting marriage ATX for %v: %w", id, err) + } + if rows == 0 { + return nil, 0, sql.ErrNotFound + } + return atx, index, nil +} + // Set marriage inserts marriage ATX for given identity. // If identitty doesn't exist - create it. -func SetMarriage(db sql.Executor, id types.NodeID, atx types.ATXID) error { +func SetMarriage(db sql.Executor, id types.NodeID, atx types.ATXID, marriageIndex int) error { _, err := db.Exec(` - INSERT INTO identities (pubkey, marriage_atx) - values (?1, ?2) - ON CONFLICT(pubkey) DO UPDATE SET marriage_atx = excluded.marriage_atx + INSERT INTO identities (pubkey, marriage_atx, marriage_idx) + values (?1, ?2, ?3) + ON CONFLICT(pubkey) DO UPDATE SET marriage_atx = excluded.marriage_atx, marriage_idx = excluded.marriage_idx WHERE marriage_atx IS NULL;`, func(stmt *sql.Statement) { stmt.BindBytes(1, id.Bytes()) stmt.BindBytes(2, atx.Bytes()) + stmt.BindInt64(3, int64(marriageIndex)) }, nil, ) if err != nil { @@ -188,3 +218,24 @@ func EquivocationSet(db sql.Executor, id types.NodeID) ([]types.NodeID, error) { return ids, nil } + +func EquivocationSetByMarriageATX(db sql.Executor, atx types.ATXID) ([]types.NodeID, error) { + var ids []types.NodeID + + _, err := db.Exec(` + SELECT pubkey FROM identities WHERE marriage_atx = ?1 ORDER BY marriage_idx ASC;`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, atx.Bytes()) + }, + func(stmt *sql.Statement) bool { + var nid types.NodeID + stmt.ColumnBytes(0, nid[:]) + ids = append(ids, nid) + return true + }) + if err != nil { + return nil, fmt.Errorf("getting equivocation set by ID %s: %w", atx, err) + } + + return ids, nil +} diff --git a/sql/identities/identities_test.go b/sql/identities/identities_test.go index 0feaeb018f..a8b77cd71f 100644 --- a/sql/identities/identities_test.go +++ b/sql/identities/identities_test.go @@ -131,7 +131,7 @@ func TestMarried(t *testing.T) { require.False(t, married) atx := types.RandomATXID() - require.NoError(t, SetMarriage(db, id, atx)) + require.NoError(t, SetMarriage(db, id, atx, 0)) married, err = Married(db, id) require.NoError(t, err) @@ -149,7 +149,7 @@ func TestMarried(t *testing.T) { require.NoError(t, err) require.False(t, married) - require.NoError(t, SetMarriage(db, id, types.RandomATXID())) + require.NoError(t, SetMarriage(db, id, types.RandomATXID(), 0)) married, err = Married(db, id) require.NoError(t, err) @@ -157,6 +157,30 @@ func TestMarried(t *testing.T) { }) } +func TestMarriageATX(t *testing.T) { + t.Parallel() + t.Run("not married", func(t *testing.T) { + t.Parallel() + db := sql.InMemory() + + id := types.RandomNodeID() + _, _, err := MarriageInfo(db, id) + require.ErrorIs(t, err, sql.ErrNotFound) + }) + t.Run("married", func(t *testing.T) { + t.Parallel() + db := sql.InMemory() + + id := types.RandomNodeID() + atx := types.RandomATXID() + require.NoError(t, SetMarriage(db, id, atx, 5)) + got, idx, err := MarriageInfo(db, id) + require.NoError(t, err) + require.Equal(t, atx, *got) + require.Equal(t, 5, idx) + }) +} + func TestEquivocationSet(t *testing.T) { t.Parallel() t.Run("equivocation set of married IDs", func(t *testing.T) { @@ -169,8 +193,8 @@ func TestEquivocationSet(t *testing.T) { types.RandomNodeID(), types.RandomNodeID(), } - for _, id := range ids { - require.NoError(t, SetMarriage(db, id, atx)) + for i, id := range ids { + require.NoError(t, SetMarriage(db, id, atx, i)) } for _, id := range ids { @@ -198,8 +222,8 @@ func TestEquivocationSet(t *testing.T) { types.RandomNodeID(), types.RandomNodeID(), } - for _, id := range ids { - require.NoError(t, SetMarriage(db, id, atx)) + for i, id := range ids { + require.NoError(t, SetMarriage(db, id, atx, i)) } for _, id := range ids { @@ -210,7 +234,7 @@ func TestEquivocationSet(t *testing.T) { // try to marry via another random ATX // the set should remain intact - require.NoError(t, SetMarriage(db, ids[0], types.RandomATXID())) + require.NoError(t, SetMarriage(db, ids[0], types.RandomATXID(), 0)) for _, id := range ids { set, err := EquivocationSet(db, id) require.NoError(t, err) @@ -221,7 +245,7 @@ func TestEquivocationSet(t *testing.T) { db := sql.InMemory() atx := types.RandomATXID() id := types.RandomNodeID() - require.NoError(t, SetMarriage(db, id, atx)) + require.NoError(t, SetMarriage(db, id, atx, 0)) malicious, err := IsMalicious(db, id) require.NoError(t, err) @@ -243,8 +267,8 @@ func TestEquivocationSet(t *testing.T) { types.RandomNodeID(), types.RandomNodeID(), } - for _, id := range ids { - require.NoError(t, SetMarriage(db, id, atx)) + for i, id := range ids { + require.NoError(t, SetMarriage(db, id, atx, i)) } require.NoError(t, SetMalicious(db, ids[0], []byte("proof"), time.Now())) @@ -256,3 +280,30 @@ func TestEquivocationSet(t *testing.T) { } }) } + +func TestEquivocationSetByMarriageATX(t *testing.T) { + t.Parallel() + + t.Run("married IDs", func(t *testing.T) { + db := sql.InMemory() + ids := []types.NodeID{ + types.RandomNodeID(), + types.RandomNodeID(), + types.RandomNodeID(), + types.RandomNodeID(), + } + atx := types.RandomATXID() + for i, id := range ids { + require.NoError(t, SetMarriage(db, id, atx, i)) + } + set, err := EquivocationSetByMarriageATX(db, atx) + require.NoError(t, err) + require.Equal(t, ids, set) + }) + t.Run("empty set", func(t *testing.T) { + db := sql.InMemory() + set, err := EquivocationSetByMarriageATX(db, types.RandomATXID()) + require.NoError(t, err) + require.Empty(t, set) + }) +} diff --git a/sql/migrations/state/0019_marriages.sql b/sql/migrations/state/0019_marriages.sql index 66bc7d1128..799f36c7d0 100644 --- a/sql/migrations/state/0019_marriages.sql +++ b/sql/migrations/state/0019_marriages.sql @@ -1 +1,2 @@ ALTER TABLE identities ADD COLUMN marriage_atx CHAR(32); +ALTER TABLE identities ADD COLUMN marriage_idx INTEGER; From 43710956ca92637fadfa3cdf5b33cfb05f18a0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 12 Jun 2024 17:19:43 +0200 Subject: [PATCH 02/25] Test publishing merged ATX by another ID --- activation/e2e/atx_merge_test.go | 210 ++++++++++++++++++++++++------- activation/handler_v2.go | 52 ++++---- 2 files changed, 190 insertions(+), 72 deletions(-) diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index ac1af716f2..cdf626245c 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -2,6 +2,8 @@ package activation_test import ( "context" + "encoding/hex" + "fmt" "net/url" "slices" "testing" @@ -41,7 +43,7 @@ import ( func constructMerkleProof(t *testing.T, members []types.Hash32) wire.MerkleProofV2 { t.Helper() - ids := make(map[uint64]bool, len(members)) + ids := make(map[uint64]bool) for i := range members { ids[uint64(i)] = true } @@ -67,7 +69,17 @@ func constructMerkleProof(t *testing.T, members []types.Hash32) wire.MerkleProof } } -func buildNipost(nb *activation.NIPostBuilder, sig *signing.EdSigner, publish types.EpochID, previous, positioning types.ATXID) (*nipost.NIPostState, error) { +type nipostData struct { + previous types.ATXID + *nipost.NIPostState +} + +func buildNipost( + nb *activation.NIPostBuilder, + sig *signing.EdSigner, + publish types.EpochID, + previous, positioning types.ATXID, +) (nipostData, error) { challenge := wire.NIPostChallengeV2{ PublishEpoch: publish, PrevATXID: previous, @@ -75,19 +87,82 @@ func buildNipost(nb *activation.NIPostBuilder, sig *signing.EdSigner, publish ty } nipost, err := nb.BuildNIPost(context.Background(), sig, challenge.PublishEpoch, challenge.Hash()) nb.ResetState(sig.NodeID()) - return nipost, err + return nipostData{previous, nipost}, err } -func Test_MarryAndMerge(t *testing.T) { - t.Parallel() - ctrl := gomock.NewController(t) +func createMerged( + niposts []nipostData, + publish types.EpochID, + marriage, positioning types.ATXID, + previous []types.ATXID, + membership wire.MerkleProofV2, +) *wire.ActivationTxV2 { + atx := &wire.ActivationTxV2{ + PublishEpoch: publish, + PreviousATXs: previous, + MarriageATX: &marriage, + PositioningATX: positioning, + NiPosts: []wire.NiPostsV2{ + { + Membership: membership, + Challenge: types.Hash32(niposts[0].PostMetadata.Challenge), + Posts: make([]wire.SubPostV2, 2), + }, + }, + } + // Append PoSTs for all IDs + // PoSTs must be ordered by their leaf index in the poet membership proof + for i, nipost := range niposts { + idx := slices.IndexFunc(previous, func(a types.ATXID) bool { return a == nipost.previous }) + if idx == -1 { + panic(fmt.Sprintf("previous ATX %s not found in %s", nipost.previous, previous)) + } + atx.NiPosts[0].Posts[nipost.Membership.LeafIndex] = wire.SubPostV2{ + MarriageIndex: uint32(i), + PrevATXIndex: uint32(idx), + Post: *wire.PostToWireV1(nipost.Post), + NumUnits: nipost.NumUnits, + } + } + return atx +} + +func signers(t *testing.T, keysHex []string) []*signing.EdSigner { + t.Helper() + + var keys [][]byte + for _, k := range keysHex { + key, err := hex.DecodeString(k) + require.NoError(t, err) + keys = append(keys, key) + } signers := []*signing.EdSigner{} - for range 2 { - sig, err := signing.NewEdSigner() + for _, key := range keys { + sig, err := signing.NewEdSigner(signing.WithPrivateKey(key)) require.NoError(t, err) signers = append(signers, sig) } + return signers +} + +var units = [2]uint32{2, 3} + +// Keys were preselected to give IDs whose VRF nonces satisfy the combined storage requirement for the above `units`. +// +//nolint:lll +var singerKeys = [2]string{ + "1f2b77052ecc193038156d5c32f08d449742e7dda81fa172f8ac90839d34c76935a5d9365d1317c3002838126409e138321c57a5651d758485336c1e7e5af101", + "6f385445a53d8af57874acd2dd98023858df7aa62f0b6e91ffdd51198036e2c331d2a7c55ba1e29312ac71dd419b4edc019b6406960cfc8ffb3d7550dde2ca1b", +} + +func Test_MarryAndMerge(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + signers := signers(t, singerKeys[:]) + + var totalNumUnits uint32 + var nonces [2]uint64 logger := zaptest.NewLogger(t) goldenATX := types.ATXID{2, 3, 4} @@ -117,12 +192,11 @@ func Test_MarryAndMerge(t *testing.T) { validator := activation.NewValidator(db, poetDb, cfg, opts.Scrypt, verifier) var eg errgroup.Group - var totalNumUnits uint32 for i, sig := range signers { opts := opts opts.DataDir = t.TempDir() - opts.NumUnits = uint32(i + 1) - totalNumUnits += opts.NumUnits + opts.NumUnits = units[i] + totalNumUnits += units[i] eg.Go(func() error { mgr, err := activation.NewPostSetupManager(cfg, logger, db, atxsdata.New(), goldenATX, syncer, validator) @@ -246,7 +320,7 @@ func Test_MarryAndMerge(t *testing.T) { // Step 1. Publish initial ATXs for each signer eg = errgroup.Group{} - for _, signer := range signers { + for i, signer := range signers { eg.Go(func() error { post, postInfo, err := nb.Proof(context.Background(), signer.NodeID(), types.EmptyHash32[:]) if err != nil { @@ -261,6 +335,12 @@ func Test_MarryAndMerge(t *testing.T) { CommitmentATX: postInfo.CommitmentATX, VRFNonce: *postInfo.Nonce, } + // make sure that the vrf nonce is good enough for the combined storage + err = v.VRFNonceV2(signer.NodeID(), postInfo.CommitmentATX, uint64(*postInfo.Nonce), totalNumUnits) + if err != nil { + return err + } + nonces[i] = uint64(*postInfo.Nonce) if err := nipost.AddPost(localDB, signer.NodeID(), initialPost); err != nil { return err } @@ -338,11 +418,14 @@ func Test_MarryAndMerge(t *testing.T) { } // Step 3. Publish merged ATX together + publish := marriageATX.PublishEpoch + 2 eg = errgroup.Group{} - var niposts [2]*nipost.NIPostState + + var niposts [2]nipostData // 3.1. NiPOST for main ID (the publisher) eg.Go(func() error { - niposts[0], err = buildNipost(nb, mainID, marriageATX.PublishEpoch+2, marriageATX.ID(), marriageATX.ID()) + niposts[0], err = buildNipost(nb, mainID, publish, marriageATX.ID(), marriageATX.ID()) + logger.Info("built NiPoST", zap.Any("post", niposts[0])) return err }) @@ -350,49 +433,30 @@ func Test_MarryAndMerge(t *testing.T) { prevATXID, err = atxs.GetLastIDByNodeID(db, mergedID.NodeID()) require.NoError(t, err) eg.Go(func() error { - niposts[1], err = buildNipost(nb, mergedID, marriageATX.PublishEpoch+2, prevATXID, marriageATX.ID()) + niposts[1], err = buildNipost(nb, mergedID, publish, prevATXID, marriageATX.ID()) + logger.Info("built NiPoST", zap.Any("post", niposts[1])) return err }) require.NoError(t, eg.Wait()) // 3.3 Construct a multi-ID poet membership merkle proof for both IDs - poetRef := niposts[0].PostMetadata.Challenge poetProof, members, err := poetClient.Proof(context.Background(), "2") require.NoError(t, err) membershipProof := constructMerkleProof(t, members) - mATXID := marriageATX.ID() - mergedATX := &wire.ActivationTxV2{ - PublishEpoch: marriageATX.PublishEpoch + 2, - PreviousATXs: []types.ATXID{ - mATXID, - prevATXID, - }, - MarriageATX: &mATXID, - PositioningATX: mATXID, - Coinbase: builder.Coinbase(), - VRFNonce: marriageATX.VRFNonce, - NiPosts: []wire.NiPostsV2{ - { - Membership: membershipProof, - Challenge: types.Hash32(poetRef), - Posts: make([]wire.SubPostV2, 2), - }, - }, - } - // 3.4 Append PoSTs for both IDs - // PoSTs must be ordered by their leaf index in the poet membership proof - for marriageIdx, nipost := range niposts { - mergedATX.NiPosts[0].Posts[nipost.Membership.LeafIndex] = wire.SubPostV2{ - MarriageIndex: uint32(marriageIdx), - PrevATXIndex: uint32(marriageIdx), - Post: *wire.PostToWireV1(nipost.Post), - NumUnits: nipost.NumUnits, - } - } + mergedATX := createMerged( + niposts[:], + publish, + marriageATX.ID(), + marriageATX.ID(), + []types.ATXID{marriageATX.ID(), prevATXID}, + membershipProof, + ) + mergedATX.Coinbase = builder.Coinbase() + mergedATX.VRFNonce = nonces[0] + mergedATX.Sign(mainID) // 3.5 Publish - mergedATX.Sign(mainID) logger.Info("publishing merged ATX", zap.Inline(mergedATX)) mFetch.EXPECT().RegisterPeerHashes(peer.ID(""), gomock.Any()) @@ -410,7 +474,57 @@ func Test_MarryAndMerge(t *testing.T) { require.Equal(t, mainID.NodeID(), atx.SmesherID) require.Equal(t, poetProof.LeafCount/tickSize, atx.TickCount) - mATX, err := atxs.Get(db, mATXID) + posATX, err := atxs.Get(db, marriageATX.ID()) + require.NoError(t, err) + require.Equal(t, posATX.TickHeight(), atx.BaseTickHeight) + + // Step 5. Publish merged using the same previous now + // Publish by the other signer this time. + publish = mergedATX.PublishEpoch + 1 + eg = errgroup.Group{} + for i, sig := range signers { + eg.Go(func() error { + niposts[i], err = buildNipost(nb, sig, publish, mergedATX.ID(), mergedATX.ID()) + logger.Info("built NiPoST", zap.Any("post", niposts[i])) + return err + }) + } + require.NoError(t, eg.Wait()) + poetProof, members, err = poetClient.Proof(context.Background(), "3") + require.NoError(t, err) + membershipProof = constructMerkleProof(t, members) + + // Both IDs have the same nipost challenge, we need to fix the leaf index of the 2nd ID + niposts[1].Membership.LeafIndex = 1 + + mergedATX2 := createMerged( + niposts[:], + publish, + marriageATX.ID(), + mergedATX.ID(), + []types.ATXID{mergedATX.ID()}, + membershipProof, + ) + mergedATX2.Coinbase = builder.Coinbase() + mergedATX2.VRFNonce = nonces[1] + mergedATX2.Sign(signers[1]) + + logger.Info("publishing second merged ATX", zap.Inline(mergedATX2)) + mFetch.EXPECT().RegisterPeerHashes(peer.ID(""), gomock.Any()) + mFetch.EXPECT().GetPoetProof(gomock.Any(), gomock.Any()) + mFetch.EXPECT().GetAtxs(gomock.Any(), gomock.Any(), gomock.Any()) + mBeacon.EXPECT().OnAtx(gomock.Any()) + mTortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + err = atxHdlr.HandleGossipAtx(context.Background(), "", codec.MustEncode(mergedATX2)) + require.NoError(t, err) + + atx, err = atxs.Get(db, mergedATX2.ID()) + require.NoError(t, err) + require.Equal(t, totalNumUnits, atx.NumUnits) + require.Equal(t, signers[1].NodeID(), atx.SmesherID) + require.Equal(t, poetProof.LeafCount/tickSize, atx.TickCount) + + posATX, err = atxs.Get(db, mergedATX.ID()) require.NoError(t, err) - require.Equal(t, mATX.TickHeight(), atx.BaseTickHeight) // mATX was used as positioning ATX + require.Equal(t, posATX.TickHeight(), atx.BaseTickHeight) } diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 6613473a68..662078c0ca 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -346,7 +346,7 @@ func (h *HandlerV2) previous(ctx context.Context, id types.ATXID) (opaqueAtx, er // Validate the previous ATX for the given PoST and return the effective numunits. func (h *HandlerV2) validatePreviousAtx(id types.NodeID, post *wire.SubPostV2, prevAtxs []opaqueAtx) (uint32, error) { - if post.PrevATXIndex > uint32(len(prevAtxs)) { + if post.PrevATXIndex >= uint32(len(prevAtxs)) { return 0, fmt.Errorf("prevATXIndex out of bounds: %d > %d", post.PrevATXIndex, len(prevAtxs)) } prev := prevAtxs[post.PrevATXIndex] @@ -555,13 +555,38 @@ func (h *HandlerV2) syntacticallyValidateDeps( } } - // validate all niposts + // validate poet membership proofs var minLeaves uint64 = math.MaxUint64 - var smesherCommitment *types.ATXID for _, niposts := range atx.NiPosts { // verify PoET memberships in a single go var poetChallenges [][]byte + for _, post := range niposts.Posts { + nipostChallenge := wire.NIPostChallengeV2{ + PublishEpoch: atx.PublishEpoch, + PositioningATXID: atx.PositioningATX, + } + if atx.Initial != nil { + nipostChallenge.InitialPost = &atx.Initial.Post + } else { + nipostChallenge.PrevATXID = atx.PreviousATXs[post.PrevATXIndex] + } + poetChallenges = append(poetChallenges, nipostChallenge.Hash().Bytes()) + } + membership := types.MultiMerkleProof{ + Nodes: niposts.Membership.Nodes, + LeafIndices: niposts.Membership.LeafIndices, + } + leaves, err := h.nipostValidator.PoetMembership(ctx, &membership, niposts.Challenge, poetChallenges) + if err != nil { + return nil, nil, fmt.Errorf("invalid poet membership: %w", err) + } + minLeaves = min(leaves, minLeaves) + } + + // validate all niposts + var smesherCommitment *types.ATXID + for _, niposts := range atx.NiPosts { for _, post := range niposts.Posts { id := equivocationSet[post.MarriageIndex] var commitment types.ATXID @@ -599,28 +624,7 @@ func (h *HandlerV2) syntacticallyValidateDeps( if err != nil { return nil, nil, fmt.Errorf("invalid post for ID %s: %w", id, err) } - - nipostChallenge := wire.NIPostChallengeV2{ - PublishEpoch: atx.PublishEpoch, - PositioningATXID: atx.PositioningATX, - } - if atx.Initial != nil { - nipostChallenge.InitialPost = &atx.Initial.Post - } else { - nipostChallenge.PrevATXID = atx.PreviousATXs[post.PrevATXIndex] - } - - poetChallenges = append(poetChallenges, nipostChallenge.Hash().Bytes()) - } - membership := types.MultiMerkleProof{ - Nodes: niposts.Membership.Nodes, - LeafIndices: niposts.Membership.LeafIndices, - } - leaves, err := h.nipostValidator.PoetMembership(ctx, &membership, niposts.Challenge, poetChallenges) - if err != nil { - return nil, nil, fmt.Errorf("invalid poet membership: %w", err) } - minLeaves = min(leaves, minLeaves) } parts := &atxParts{ From 6414f3d4246651603bfd5cba249dce0769ae5aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 13 Jun 2024 12:24:27 +0200 Subject: [PATCH 03/25] Allow sharing nipostchallenge in merged ATX --- Makefile | 2 +- activation/activation.go | 8 ++--- activation/e2e/atx_merge_test.go | 57 +++++++++++++------------------- activation/handler_v2.go | 19 +++++++++-- activation/handler_v2_test.go | 10 +----- activation/wire/wire_v2.go | 18 +++++++--- activation/wire/wire_v2_scale.go | 30 ++++++++--------- activation/wire/wire_v2_test.go | 9 ++--- 8 files changed, 76 insertions(+), 77 deletions(-) diff --git a/Makefile b/Makefile index 4198474b82..d94176b782 100644 --- a/Makefile +++ b/Makefile @@ -102,7 +102,7 @@ clear-test-cache: .PHONY: clear-test-cache test: get-libs - @$(ULIMIT) CGO_LDFLAGS="$(CGO_TEST_LDFLAGS)" gotestsum -- -race -timeout 5m -p 1 $(UNIT_TESTS) + @$(ULIMIT) CGO_LDFLAGS="$(CGO_TEST_LDFLAGS)" gotestsum -- -race -timeout 8m -p 1 $(UNIT_TESTS) .PHONY: test generate: get-libs diff --git a/activation/activation.go b/activation/activation.go index 5e85d24aab..f58f9df674 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -801,14 +801,14 @@ func (b *Builder) createAtx( NiPosts: []wire.NiPostsV2{ { Membership: wire.MerkleProofV2{ - Nodes: nipostState.Membership.Nodes, - LeafIndices: []uint64{nipostState.Membership.LeafIndex}, + Nodes: nipostState.Membership.Nodes, }, Challenge: types.Hash32(nipostState.NIPost.PostMetadata.Challenge), Posts: []wire.SubPostV2{ { - Post: *wire.PostToWireV1(nipostState.Post), - NumUnits: nipostState.NumUnits, + Post: *wire.PostToWireV1(nipostState.Post), + NumUnits: nipostState.NumUnits, + MembershipLeafIndex: nipostState.Membership.LeafIndex, }, }, }, diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index cdf626245c..9cb3f5e74e 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -19,7 +19,6 @@ import ( "go.uber.org/mock/gomock" "go.uber.org/zap" "go.uber.org/zap/zaptest" - "golang.org/x/exp/maps" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/activation" @@ -41,12 +40,8 @@ import ( "github.com/spacemeshos/go-spacemesh/timesync" ) -func constructMerkleProof(t *testing.T, members []types.Hash32) wire.MerkleProofV2 { +func constructMerkleProof(t *testing.T, members []types.Hash32, ids map[uint64]bool) wire.MerkleProofV2 { t.Helper() - ids := make(map[uint64]bool) - for i := range members { - ids[uint64(i)] = true - } tree, err := merkle.NewTreeBuilder(). WithLeavesToProve(ids). @@ -61,12 +56,7 @@ func constructMerkleProof(t *testing.T, members []types.Hash32) wire.MerkleProof for _, n := range nodes { nodesH32 = append(nodesH32, types.BytesToHash(n)) } - indicies := maps.Keys(ids) - slices.Sort(indicies) - return wire.MerkleProofV2{ - LeafIndices: indicies, - Nodes: nodesH32, - } + return wire.MerkleProofV2{Nodes: nodesH32} } type nipostData struct { @@ -106,23 +96,22 @@ func createMerged( { Membership: membership, Challenge: types.Hash32(niposts[0].PostMetadata.Challenge), - Posts: make([]wire.SubPostV2, 2), }, }, } // Append PoSTs for all IDs - // PoSTs must be ordered by their leaf index in the poet membership proof for i, nipost := range niposts { idx := slices.IndexFunc(previous, func(a types.ATXID) bool { return a == nipost.previous }) if idx == -1 { panic(fmt.Sprintf("previous ATX %s not found in %s", nipost.previous, previous)) } - atx.NiPosts[0].Posts[nipost.Membership.LeafIndex] = wire.SubPostV2{ - MarriageIndex: uint32(i), - PrevATXIndex: uint32(idx), - Post: *wire.PostToWireV1(nipost.Post), - NumUnits: nipost.NumUnits, - } + atx.NiPosts[0].Posts = append(atx.NiPosts[0].Posts, wire.SubPostV2{ + MarriageIndex: uint32(i), + PrevATXIndex: uint32(idx), + MembershipLeafIndex: nipost.Membership.LeafIndex, + Post: *wire.PostToWireV1(nipost.Post), + NumUnits: nipost.NumUnits, + }) } return atx } @@ -378,14 +367,14 @@ func Test_MarryAndMerge(t *testing.T) { NiPosts: []wire.NiPostsV2{ { Membership: wire.MerkleProofV2{ - Nodes: nipostState.Membership.Nodes, - LeafIndices: []uint64{nipostState.Membership.LeafIndex}, + Nodes: nipostState.Membership.Nodes, }, Challenge: types.Hash32(nipostState.NIPost.PostMetadata.Challenge), Posts: []wire.SubPostV2{ { - Post: *wire.PostToWireV1(nipostState.Post), - NumUnits: nipostState.NumUnits, + Post: *wire.PostToWireV1(nipostState.Post), + NumUnits: nipostState.NumUnits, + MembershipLeafIndex: nipostState.Membership.LeafIndex, }, }, }, @@ -424,8 +413,9 @@ func Test_MarryAndMerge(t *testing.T) { var niposts [2]nipostData // 3.1. NiPOST for main ID (the publisher) eg.Go(func() error { - niposts[0], err = buildNipost(nb, mainID, publish, marriageATX.ID(), marriageATX.ID()) + n, err := buildNipost(nb, mainID, publish, marriageATX.ID(), marriageATX.ID()) logger.Info("built NiPoST", zap.Any("post", niposts[0])) + niposts[0] = n return err }) @@ -433,8 +423,9 @@ func Test_MarryAndMerge(t *testing.T) { prevATXID, err = atxs.GetLastIDByNodeID(db, mergedID.NodeID()) require.NoError(t, err) eg.Go(func() error { - niposts[1], err = buildNipost(nb, mergedID, publish, prevATXID, marriageATX.ID()) - logger.Info("built NiPoST", zap.Any("post", niposts[1])) + n, err := buildNipost(nb, mergedID, publish, prevATXID, marriageATX.ID()) + logger.Info("built NiPoST", zap.Any("post", n)) + niposts[1] = n return err }) require.NoError(t, eg.Wait()) @@ -442,7 +433,7 @@ func Test_MarryAndMerge(t *testing.T) { // 3.3 Construct a multi-ID poet membership merkle proof for both IDs poetProof, members, err := poetClient.Proof(context.Background(), "2") require.NoError(t, err) - membershipProof := constructMerkleProof(t, members) + membershipProof := constructMerkleProof(t, members, map[uint64]bool{0: true, 1: true}) mergedATX := createMerged( niposts[:], @@ -484,18 +475,16 @@ func Test_MarryAndMerge(t *testing.T) { eg = errgroup.Group{} for i, sig := range signers { eg.Go(func() error { - niposts[i], err = buildNipost(nb, sig, publish, mergedATX.ID(), mergedATX.ID()) - logger.Info("built NiPoST", zap.Any("post", niposts[i])) + n, err := buildNipost(nb, sig, publish, mergedATX.ID(), mergedATX.ID()) + logger.Info("built NiPoST", zap.Any("post", n)) + niposts[i] = n return err }) } require.NoError(t, eg.Wait()) poetProof, members, err = poetClient.Proof(context.Background(), "3") require.NoError(t, err) - membershipProof = constructMerkleProof(t, members) - - // Both IDs have the same nipost challenge, we need to fix the leaf index of the 2nd ID - niposts[1].Membership.LeafIndex = 1 + membershipProof = constructMerkleProof(t, members, map[uint64]bool{0: true}) mergedATX2 := createMerged( niposts[:], diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 662078c0ca..476820acfa 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -559,7 +559,7 @@ func (h *HandlerV2) syntacticallyValidateDeps( var minLeaves uint64 = math.MaxUint64 for _, niposts := range atx.NiPosts { // verify PoET memberships in a single go - var poetChallenges [][]byte + indexedChallenges := make(map[uint64][]byte) for _, post := range niposts.Posts { nipostChallenge := wire.NIPostChallengeV2{ @@ -571,11 +571,24 @@ func (h *HandlerV2) syntacticallyValidateDeps( } else { nipostChallenge.PrevATXID = atx.PreviousATXs[post.PrevATXIndex] } - poetChallenges = append(poetChallenges, nipostChallenge.Hash().Bytes()) + if _, ok := indexedChallenges[post.MembershipLeafIndex]; !ok { + indexedChallenges[post.MembershipLeafIndex] = nipostChallenge.Hash().Bytes() + } + } + + leafIndicies := make([]uint64, 0, len(indexedChallenges)) + for i := range indexedChallenges { + leafIndicies = append(leafIndicies, i) } + slices.Sort(leafIndicies) + poetChallenges := make([][]byte, 0, len(indexedChallenges)) + for _, i := range leafIndicies { + poetChallenges = append(poetChallenges, indexedChallenges[i]) + } + membership := types.MultiMerkleProof{ Nodes: niposts.Membership.Nodes, - LeafIndices: niposts.Membership.LeafIndices, + LeafIndices: leafIndicies, } leaves, err := h.nipostValidator.PoetMembership(ctx, &membership, niposts.Challenge, poetChallenges) if err != nil { diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 5592e6dea6..0ee0643f71 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -1402,6 +1402,7 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { atx := newInitialATXv2(t, golden) atx.Sign(sig) + atxHandler.mValidator.EXPECT().PoetMembership(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) atxHandler.mValidator.EXPECT(). PostV2( gomock.Any(), @@ -1445,15 +1446,6 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { atx := newInitialATXv2(t, golden) atx.Sign(sig) - atxHandler.mValidator.EXPECT().PostV2( - gomock.Any(), - sig.NodeID(), - golden, - wire.PostFromWireV1(&atx.NiPosts[0].Posts[0].Post), - atx.NiPosts[0].Challenge.Bytes(), - atx.TotalNumUnits(), - gomock.Any(), - ) atxHandler.mValidator.EXPECT(). PoetMembership(gomock.Any(), gomock.Any(), atx.NiPosts[0].Challenge, gomock.Any()). Return(0, errors.New("poet failure")) diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index db3bf9d022..c844ae7809 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -196,8 +196,7 @@ func (mc *MarriageCertificate) Root() []byte { // MerkleProofV2 proves membership of multiple challenges in a PoET membership merkle tree. type MerkleProofV2 struct { // Nodes on path from leaf to root (not including leaf) - Nodes []types.Hash32 `scale:"max=32"` - LeafIndices []uint64 `scale:"max=256"` // support merging up to 256 IDs + Nodes []types.Hash32 `scale:"max=32"` } type SubPostV2 struct { @@ -206,8 +205,12 @@ type SubPostV2 struct { // Must be 0 for non-merged ATXs. MarriageIndex uint32 PrevATXIndex uint32 // Index of the previous ATX in the `InnerActivationTxV2.PreviousATXs` slice - Post PostV1 - NumUnits uint32 + // Index of the leaf for this ID's challenge in the poet membership tree. + // IDs might shared the same index if their nipost challenges are equal. + // This happens when the IDs are continuously merged (they share the previous ATX). + MembershipLeafIndex uint64 + Post PostV1 + NumUnits uint32 } func (sp *SubPostV2) Root(prevATXs []types.ATXID) []byte { @@ -225,6 +228,11 @@ func (sp *SubPostV2) Root(prevATXs []types.ATXID) []byte { return nil // invalid index, root cannot be generated } tree.AddLeaf(prevATXs[sp.PrevATXIndex].Bytes()) + + var leafIndex [8]byte + binary.LittleEndian.PutUint64(leafIndex[:], sp.MembershipLeafIndex) + tree.AddLeaf(leafIndex[:]) + tree.AddLeaf(sp.Post.Root()) numUnits := make([]byte, 4) @@ -235,7 +243,6 @@ func (sp *SubPostV2) Root(prevATXs []types.ATXID) []byte { type NiPostsV2 struct { // Single membership proof for all IDs in `Posts`. - // The index of ID in `Posts` is the index of the challenge in the proof (`LeafIndices`). Membership MerkleProofV2 // The root of the PoET proof, that serves as the challenge for PoSTs. Challenge types.Hash32 @@ -336,6 +343,7 @@ func (post *SubPostV2) MarshalLogObject(encoder zapcore.ObjectEncoder) error { } encoder.AddUint32("MarriageIndex", post.MarriageIndex) encoder.AddUint32("PrevATXIndex", post.PrevATXIndex) + encoder.AddUint64("MembershipLeafIndex", post.MembershipLeafIndex) encoder.AddObject("Post", &post.Post) encoder.AddUint32("NumUnits", post.NumUnits) return nil diff --git a/activation/wire/wire_v2_scale.go b/activation/wire/wire_v2_scale.go index 286a200428..4c5404a34c 100644 --- a/activation/wire/wire_v2_scale.go +++ b/activation/wire/wire_v2_scale.go @@ -257,13 +257,6 @@ func (t *MerkleProofV2) EncodeScale(enc *scale.Encoder) (total int, err error) { } total += n } - { - n, err := scale.EncodeUint64SliceWithLimit(enc, t.LeafIndices, 256) - if err != nil { - return total, err - } - total += n - } return total, nil } @@ -276,14 +269,6 @@ func (t *MerkleProofV2) DecodeScale(dec *scale.Decoder) (total int, err error) { total += n t.Nodes = field } - { - field, n, err := scale.DecodeUint64SliceWithLimit(dec, 256) - if err != nil { - return total, err - } - total += n - t.LeafIndices = field - } return total, nil } @@ -302,6 +287,13 @@ func (t *SubPostV2) EncodeScale(enc *scale.Encoder) (total int, err error) { } total += n } + { + n, err := scale.EncodeCompact64(enc, uint64(t.MembershipLeafIndex)) + if err != nil { + return total, err + } + total += n + } { n, err := t.Post.EncodeScale(enc) if err != nil { @@ -336,6 +328,14 @@ func (t *SubPostV2) DecodeScale(dec *scale.Decoder) (total int, err error) { total += n t.PrevATXIndex = uint32(field) } + { + field, n, err := scale.DecodeCompact64(dec) + if err != nil { + return total, err + } + total += n + t.MembershipLeafIndex = uint64(field) + } { n, err := t.Post.DecodeScale(dec) if err != nil { diff --git a/activation/wire/wire_v2_test.go b/activation/wire/wire_v2_test.go index f56ae7423e..596be06091 100644 --- a/activation/wire/wire_v2_test.go +++ b/activation/wire/wire_v2_test.go @@ -37,16 +37,14 @@ func Benchmark_ATXv2ID_WorstScenario(b *testing.B) { NiPosts: []NiPostsV2{ { Membership: MerkleProofV2{ - Nodes: make([]types.Hash32, 32), - LeafIndices: make([]uint64, 256), + Nodes: make([]types.Hash32, 32), }, Challenge: types.RandomHash(), Posts: make([]SubPostV2, 256), }, { Membership: MerkleProofV2{ - Nodes: make([]types.Hash32, 32), - LeafIndices: make([]uint64, 256), + Nodes: make([]types.Hash32, 32), }, Challenge: types.RandomHash(), Posts: make([]SubPostV2, 256), // actually the sum of all posts in `NiPosts` should be 256 @@ -96,8 +94,7 @@ func Test_GenerateDoublePublishProof(t *testing.T) { NiPosts: []NiPostsV2{ { Membership: MerkleProofV2{ - Nodes: make([]types.Hash32, 32), - LeafIndices: make([]uint64, 256), + Nodes: make([]types.Hash32, 32), }, Challenge: types.RandomHash(), Posts: []SubPostV2{ From b51d401c1054071e940aea137e07af0314afaa0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 13 Jun 2024 13:04:27 +0200 Subject: [PATCH 04/25] Test emergency split --- activation/e2e/atx_merge_test.go | 104 ++++++++++++++++++++----------- activation/handler_v2_test.go | 2 +- 2 files changed, 70 insertions(+), 36 deletions(-) diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index 9cb3f5e74e..7170b12a94 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -80,6 +80,30 @@ func buildNipost( return nipostData{previous, nipost}, err } +func createSoloAtx(publish types.EpochID, prev, pos types.ATXID, nipost *nipost.NIPostState) *wire.ActivationTxV2 { + return &wire.ActivationTxV2{ + PublishEpoch: publish, + PreviousATXs: []types.ATXID{prev}, + PositioningATX: pos, + VRFNonce: uint64(nipost.VRFNonce), + NiPosts: []wire.NiPostsV2{ + { + Membership: wire.MerkleProofV2{ + Nodes: nipost.Membership.Nodes, + }, + Challenge: types.Hash32(nipost.PostMetadata.Challenge), + Posts: []wire.SubPostV2{ + { + Post: *wire.PostToWireV1(nipost.Post), + NumUnits: nipost.NumUnits, + MembershipLeafIndex: nipost.Membership.LeafIndex, + }, + }, + }, + }, + } +} + func createMerged( niposts []nipostData, publish types.EpochID, @@ -348,42 +372,18 @@ func Test_MarryAndMerge(t *testing.T) { prev, err := atxs.Get(db, prevATXID) require.NoError(t, err) - challenge := wire.NIPostChallengeV2{ - PublishEpoch: prev.PublishEpoch + 1, - PrevATXID: prevATXID, - PositioningATXID: prevATXID, - } - - nipostState, err := nb.BuildNIPost(context.Background(), mainID, challenge.PublishEpoch, challenge.Hash()) + publish := prev.PublishEpoch + 1 + nipostState, err := buildNipost(nb, mainID, publish, prevATXID, prevATXID) require.NoError(t, err) - require.NoError(t, nb.ResetState(mainID.NodeID())) - - marriageATX := &wire.ActivationTxV2{ - PublishEpoch: challenge.PublishEpoch, - PositioningATX: challenge.PositioningATXID, - PreviousATXs: []types.ATXID{challenge.PrevATXID}, - Coinbase: builder.Coinbase(), - VRFNonce: (uint64)(nipostState.VRFNonce), - NiPosts: []wire.NiPostsV2{ - { - Membership: wire.MerkleProofV2{ - Nodes: nipostState.Membership.Nodes, - }, - Challenge: types.Hash32(nipostState.NIPost.PostMetadata.Challenge), - Posts: []wire.SubPostV2{ - { - Post: *wire.PostToWireV1(nipostState.Post), - NumUnits: nipostState.NumUnits, - MembershipLeafIndex: nipostState.Membership.LeafIndex, - }, - }, - }, + + marriageATX := createSoloAtx(publish, prevATXID, prevATXID, nipostState.NIPostState) + marriageATX.Marriages = []wire.MarriageCertificate{ + { + Signature: mainID.Sign(signing.MARRIAGE, mainID.NodeID().Bytes()), }, - Marriages: []wire.MarriageCertificate{ - { - ReferenceAtx: prevMergedIDATX, - Signature: mergedID.Sign(signing.MARRIAGE, mainID.NodeID().Bytes()), - }, + { + ReferenceAtx: prevMergedIDATX, + Signature: mergedID.Sign(signing.MARRIAGE, mainID.NodeID().Bytes()), }, } marriageATX.Sign(mainID) @@ -407,7 +407,7 @@ func Test_MarryAndMerge(t *testing.T) { } // Step 3. Publish merged ATX together - publish := marriageATX.PublishEpoch + 2 + publish = marriageATX.PublishEpoch + 2 eg = errgroup.Group{} var niposts [2]nipostData @@ -516,4 +516,38 @@ func Test_MarryAndMerge(t *testing.T) { posATX, err = atxs.Get(db, mergedATX.ID()) require.NoError(t, err) require.Equal(t, posATX.TickHeight(), atx.BaseTickHeight) + + // Step 6. Make an emergency split and publish separately + publish = mergedATX2.PublishEpoch + 1 + eg = errgroup.Group{} + for i, sig := range signers { + eg.Go(func() error { + n, err := buildNipost(nb, sig, publish, mergedATX2.ID(), mergedATX2.ID()) + logger.Info("built NiPoST", zap.Any("post", n)) + niposts[i] = n + return err + }) + } + require.NoError(t, eg.Wait()) + + for i, signer := range signers { + atx := createSoloAtx(publish, mergedATX2.ID(), mergedATX2.ID(), niposts[i].NIPostState) + atx.Sign(signer) + logger.Info("publishing split ATX", zap.Inline(atx)) + + mFetch.EXPECT().RegisterPeerHashes(peer.ID(""), gomock.Any()) + mFetch.EXPECT().GetPoetProof(gomock.Any(), gomock.Any()) + mFetch.EXPECT().GetAtxs(gomock.Any(), gomock.Any(), gomock.Any()) + mBeacon.EXPECT().OnAtx(gomock.Any()) + mTortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + err = atxHdlr.HandleGossipAtx(context.Background(), "", codec.MustEncode(atx)) + require.NoError(t, err) + + atxFromDb, err := atxs.Get(db, atx.ID()) + require.NoError(t, err) + require.Equal(t, units[i], atxFromDb.NumUnits) + require.Equal(t, signer.NodeID(), atxFromDb.SmesherID) + require.Equal(t, publish, atxFromDb.PublishEpoch) + require.Equal(t, mergedATX2.ID(), atxFromDb.PrevATXID) + } } diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 0ee0643f71..01bf43166f 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -1637,7 +1637,7 @@ func Test_MarryingMalicious(t *testing.T) { atxHandler.mtortoise.EXPECT().OnMalfeasance(sig.NodeID()) atxHandler.mtortoise.EXPECT().OnMalfeasance(otherSig.NodeID()) - _, err = atxHandler.processATX(context.Background(), "", atx, codec.MustEncode(atx), time.Now()) + _, err := atxHandler.processATX(context.Background(), "", atx, codec.MustEncode(atx), time.Now()) require.NoError(t, err) equiv, err := identities.EquivocationSet(atxHandler.cdb, sig.NodeID()) From 29301a284de1b04a7e655ede9182d620e178a2b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 19 Jun 2024 17:24:31 +0200 Subject: [PATCH 05/25] Marry using reference ATX from the same epoch --- activation/e2e/atx_merge_test.go | 159 +++++++++++++++---------------- 1 file changed, 79 insertions(+), 80 deletions(-) diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index 7170b12a94..a47582f8a2 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -28,7 +28,6 @@ import ( "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" - "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "github.com/spacemeshos/go-spacemesh/p2p/pubsub/mocks" "github.com/spacemeshos/go-spacemesh/signing" "github.com/spacemeshos/go-spacemesh/sql" @@ -36,6 +35,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/identities" "github.com/spacemeshos/go-spacemesh/sql/localsql" "github.com/spacemeshos/go-spacemesh/sql/localsql/nipost" + "github.com/spacemeshos/go-spacemesh/system" smocks "github.com/spacemeshos/go-spacemesh/system/mocks" "github.com/spacemeshos/go-spacemesh/timesync" ) @@ -80,6 +80,38 @@ func buildNipost( return nipostData{previous, nipost}, err } +func createInitialAtx( + publish types.EpochID, + commitment, pos types.ATXID, + nipost *nipost.NIPostState, + initial *types.Post, +) *wire.ActivationTxV2 { + return &wire.ActivationTxV2{ + PublishEpoch: publish, + PositioningATX: pos, + Initial: &wire.InitialAtxPartsV2{ + CommitmentATX: commitment, + Post: *wire.PostToWireV1(initial), + }, + VRFNonce: uint64(nipost.VRFNonce), + NiPosts: []wire.NiPostsV2{ + { + Membership: wire.MerkleProofV2{ + Nodes: nipost.Membership.Nodes, + }, + Challenge: types.Hash32(nipost.PostMetadata.Challenge), + Posts: []wire.SubPostV2{ + { + Post: *wire.PostToWireV1(nipost.Post), + NumUnits: nipost.NumUnits, + MembershipLeafIndex: nipost.Membership.LeafIndex, + }, + }, + }, + }, + } +} + func createSoloAtx(publish types.EpochID, prev, pos types.ATXID, nipost *nipost.NIPostState) *wire.ActivationTxV2 { return &wire.ActivationTxV2{ PublishEpoch: publish, @@ -179,6 +211,7 @@ func Test_MarryAndMerge(t *testing.T) { logger := zaptest.NewLogger(t) goldenATX := types.ATXID{2, 3, 4} + coinbase := types.Address{1, 2, 3, 4, 5, 6, 7} cfg := activation.DefaultPostConfig() db := sql.InMemory() cdb := datastore.NewCachedDB(db, logger) @@ -275,14 +308,6 @@ func Test_MarryAndMerge(t *testing.T) { ) require.NoError(t, err) - conf := activation.Config{ - GoldenATXID: goldenATX, - RegossipInterval: 0, - } - - data := atxsdata.New() - atxVersions := activation.AtxVersions{postGenesisEpoch: types.AtxV2} - edVerifier := signing.NewEdVerifier() mpub := mocks.NewMockPublisher(ctrl) mFetch := smocks.NewMockFetcher(ctrl) mBeacon := activation.NewMockAtxReceiver(ctrl) @@ -292,8 +317,8 @@ func Test_MarryAndMerge(t *testing.T) { atxHdlr := activation.NewHandler( "local", cdb, - data, - edVerifier, + atxsdata.New(), + signing.NewEdVerifier(), clock, mpub, mFetch, @@ -302,36 +327,14 @@ func Test_MarryAndMerge(t *testing.T) { mBeacon, mTortoise, logger, - activation.WithAtxVersions(atxVersions), + activation.WithAtxVersions(activation.AtxVersions{0: types.AtxV2}), activation.WithTickSize(tickSize), ) - mpub.EXPECT().Publish(gomock.Any(), pubsub.AtxProtocol, gomock.Any()).DoAndReturn( - func(ctx context.Context, p string, got []byte) error { - mFetch.EXPECT().RegisterPeerHashes(peer.ID(p), gomock.Any()) - mFetch.EXPECT().GetPoetProof(gomock.Any(), gomock.Any()) - mBeacon.EXPECT().OnAtx(gomock.Any()) - mTortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) - return atxHdlr.HandleGossipAtx(ctx, peer.ID(p), got) - }, - ).Times(2) - - v := activation.NewValidator(db, poetDb, cfg, opts.Scrypt, verifier) - builder := activation.NewBuilder( - conf, - db, - data, - localDB, - mpub, - nb, - clock, - syncer, - logger, - activation.WithPoetConfig(poetCfg), - activation.WithValidator(v), - ) - - // Step 1. Publish initial ATXs for each signer + // Step 1. Marry + publish := types.EpochID(1) + var niposts [2]nipostData + var initialPosts [2]*types.Post eg = errgroup.Group{} for i, signer := range signers { eg.Go(func() error { @@ -339,50 +342,39 @@ func Test_MarryAndMerge(t *testing.T) { if err != nil { return err } - initialPost := nipost.Post{ - Nonce: post.Nonce, - Indices: post.Indices, - Pow: post.Pow, - Challenge: types.EmptyHash32[:], - NumUnits: postInfo.NumUnits, - CommitmentATX: postInfo.CommitmentATX, - VRFNonce: *postInfo.Nonce, + + challenge := wire.NIPostChallengeV2{ + PublishEpoch: publish, + PositioningATXID: goldenATX, + InitialPost: wire.PostToWireV1(post), } - // make sure that the vrf nonce is good enough for the combined storage - err = v.VRFNonceV2(signer.NodeID(), postInfo.CommitmentATX, uint64(*postInfo.Nonce), totalNumUnits) + nipost, err := nb.BuildNIPost(context.Background(), signer, challenge.PublishEpoch, challenge.Hash()) if err != nil { return err } + nb.ResetState(signer.NodeID()) + + initialPosts[i] = post nonces[i] = uint64(*postInfo.Nonce) - if err := nipost.AddPost(localDB, signer.NodeID(), initialPost); err != nil { - return err - } - return builder.PublishActivationTx(context.Background(), signer) + niposts[i] = nipostData{types.EmptyATXID, nipost} + return nil }) } require.NoError(t, eg.Wait()) - // Step 2. Marry + // mainID will create marriage ATX mainID, mergedID := signers[0], signers[1] - prevMergedIDATX, err := atxs.GetLastIDByNodeID(db, mergedID.NodeID()) - require.NoError(t, err) - prevATXID, err := atxs.GetLastIDByNodeID(db, mainID.NodeID()) - require.NoError(t, err) - prev, err := atxs.Get(db, prevATXID) - require.NoError(t, err) - - publish := prev.PublishEpoch + 1 - nipostState, err := buildNipost(nb, mainID, publish, prevATXID, prevATXID) - require.NoError(t, err) + mergedIdAtx := createInitialAtx(publish, goldenATX, goldenATX, niposts[1].NIPostState, initialPosts[1]) + mergedIdAtx.Sign(mergedID) - marriageATX := createSoloAtx(publish, prevATXID, prevATXID, nipostState.NIPostState) + marriageATX := createInitialAtx(publish, goldenATX, goldenATX, niposts[0].NIPostState, initialPosts[0]) marriageATX.Marriages = []wire.MarriageCertificate{ { Signature: mainID.Sign(signing.MARRIAGE, mainID.NodeID().Bytes()), }, { - ReferenceAtx: prevMergedIDATX, + ReferenceAtx: mergedIdAtx.ID(), Signature: mergedID.Sign(signing.MARRIAGE, mainID.NodeID().Bytes()), }, } @@ -391,7 +383,15 @@ func Test_MarryAndMerge(t *testing.T) { mFetch.EXPECT().RegisterPeerHashes(peer.ID(""), gomock.Any()) mFetch.EXPECT().GetPoetProof(gomock.Any(), gomock.Any()) - mFetch.EXPECT().GetAtxs(gomock.Any(), gomock.Any(), gomock.Any()) + mFetch.EXPECT().GetAtxs(gomock.Any(), []types.ATXID{mergedIdAtx.ID()}, gomock.Any()). + DoAndReturn(func(_ context.Context, _ []types.ATXID, _ ...system.GetAtxOpt) error { + // Provide the referenced ATX for the married ID + mFetch.EXPECT().RegisterPeerHashes(peer.ID(""), gomock.Any()) + mFetch.EXPECT().GetPoetProof(gomock.Any(), gomock.Any()) + mBeacon.EXPECT().OnAtx(gomock.Any()) + mTortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + return atxHdlr.HandleGossipAtx(context.Background(), "", codec.MustEncode(mergedIdAtx)) + }) mBeacon.EXPECT().OnAtx(gomock.Any()) mTortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) err = atxHdlr.HandleGossipAtx(context.Background(), "", codec.MustEncode(marriageATX)) @@ -406,12 +406,11 @@ func Test_MarryAndMerge(t *testing.T) { require.Equal(t, i, idx) } - // Step 3. Publish merged ATX together + // Step 2. Publish merged ATX together publish = marriageATX.PublishEpoch + 2 eg = errgroup.Group{} - var niposts [2]nipostData - // 3.1. NiPOST for main ID (the publisher) + // 2.1. NiPOST for main ID (the publisher) eg.Go(func() error { n, err := buildNipost(nb, mainID, publish, marriageATX.ID(), marriageATX.ID()) logger.Info("built NiPoST", zap.Any("post", niposts[0])) @@ -419,8 +418,8 @@ func Test_MarryAndMerge(t *testing.T) { return err }) - // 3.2. NiPOST for merged ID - prevATXID, err = atxs.GetLastIDByNodeID(db, mergedID.NodeID()) + // 2.2. NiPOST for merged ID + prevATXID, err := atxs.GetLastIDByNodeID(db, mergedID.NodeID()) require.NoError(t, err) eg.Go(func() error { n, err := buildNipost(nb, mergedID, publish, prevATXID, marriageATX.ID()) @@ -430,8 +429,8 @@ func Test_MarryAndMerge(t *testing.T) { }) require.NoError(t, eg.Wait()) - // 3.3 Construct a multi-ID poet membership merkle proof for both IDs - poetProof, members, err := poetClient.Proof(context.Background(), "2") + // 2.3 Construct a multi-ID poet membership merkle proof for both IDs + poetProof, members, err := poetClient.Proof(context.Background(), "1") require.NoError(t, err) membershipProof := constructMerkleProof(t, members, map[uint64]bool{0: true, 1: true}) @@ -443,11 +442,11 @@ func Test_MarryAndMerge(t *testing.T) { []types.ATXID{marriageATX.ID(), prevATXID}, membershipProof, ) - mergedATX.Coinbase = builder.Coinbase() + mergedATX.Coinbase = coinbase mergedATX.VRFNonce = nonces[0] mergedATX.Sign(mainID) - // 3.5 Publish + // 2.4 Publish logger.Info("publishing merged ATX", zap.Inline(mergedATX)) mFetch.EXPECT().RegisterPeerHashes(peer.ID(""), gomock.Any()) @@ -458,7 +457,7 @@ func Test_MarryAndMerge(t *testing.T) { err = atxHdlr.HandleGossipAtx(context.Background(), "", codec.MustEncode(mergedATX)) require.NoError(t, err) - // Step 4. verify the merged ATX + // Step 3. verify the merged ATX atx, err := atxs.Get(db, mergedATX.ID()) require.NoError(t, err) require.Equal(t, totalNumUnits, atx.NumUnits) @@ -469,7 +468,7 @@ func Test_MarryAndMerge(t *testing.T) { require.NoError(t, err) require.Equal(t, posATX.TickHeight(), atx.BaseTickHeight) - // Step 5. Publish merged using the same previous now + // Step 4. Publish merged using the same previous now // Publish by the other signer this time. publish = mergedATX.PublishEpoch + 1 eg = errgroup.Group{} @@ -482,7 +481,7 @@ func Test_MarryAndMerge(t *testing.T) { }) } require.NoError(t, eg.Wait()) - poetProof, members, err = poetClient.Proof(context.Background(), "3") + poetProof, members, err = poetClient.Proof(context.Background(), "2") require.NoError(t, err) membershipProof = constructMerkleProof(t, members, map[uint64]bool{0: true}) @@ -494,7 +493,7 @@ func Test_MarryAndMerge(t *testing.T) { []types.ATXID{mergedATX.ID()}, membershipProof, ) - mergedATX2.Coinbase = builder.Coinbase() + mergedATX2.Coinbase = coinbase mergedATX2.VRFNonce = nonces[1] mergedATX2.Sign(signers[1]) @@ -517,7 +516,7 @@ func Test_MarryAndMerge(t *testing.T) { require.NoError(t, err) require.Equal(t, posATX.TickHeight(), atx.BaseTickHeight) - // Step 6. Make an emergency split and publish separately + // Step 5. Make an emergency split and publish separately publish = mergedATX2.PublishEpoch + 1 eg = errgroup.Group{} for i, sig := range signers { From 7d0e2c676e81e1eff0f7d35d7424e3981b8ec489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 20 Jun 2024 12:38:07 +0200 Subject: [PATCH 06/25] Calculate and persist ATX weight in DB --- activation/e2e/atx_merge_test.go | 2 + activation/e2e/builds_atx_v2_test.go | 2 +- activation/handler_test.go | 4 +- activation/handler_v1.go | 6 + activation/handler_v2.go | 71 +++++-- activation/handler_v2_test.go | 242 +++++++++++++++++------ activation/post_test.go | 22 ++- api/grpcserver/grpcserver_test.go | 35 ++-- api/grpcserver/v2alpha1/activation.go | 2 +- atxsdata/data.go | 2 +- beacon/beacon.go | 2 +- beacon/beacon_test.go | 31 ++- beacon/handlers.go | 4 +- blocks/generator_test.go | 15 +- cmd/activeset/activeset.go | 2 +- common/types/activation.go | 50 +---- fetch/mesh_data_test.go | 10 +- hare3/eligibility/oracle_test.go | 9 +- hare3/hare_test.go | 1 + malfeasance/wire/malfeasance_test.go | 7 +- mesh/executor_test.go | 17 +- miner/proposal_builder_test.go | 1 + proposals/eligibility_validator_test.go | 1 + sql/atxs/atxs.go | 10 +- sql/migrations/state/0020_atx_weight.sql | 2 + tortoise/model/core.go | 17 +- tortoise/sim/generator.go | 17 +- tortoise/sim/layer.go | 4 +- tortoise/tortoise_test.go | 5 +- 29 files changed, 379 insertions(+), 214 deletions(-) create mode 100644 sql/migrations/state/0020_atx_weight.sql diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index a47582f8a2..a0cf900b48 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -463,6 +463,7 @@ func Test_MarryAndMerge(t *testing.T) { require.Equal(t, totalNumUnits, atx.NumUnits) require.Equal(t, mainID.NodeID(), atx.SmesherID) require.Equal(t, poetProof.LeafCount/tickSize, atx.TickCount) + require.Equal(t, uint64(totalNumUnits)*atx.TickCount, atx.Weight) posATX, err := atxs.Get(db, marriageATX.ID()) require.NoError(t, err) @@ -511,6 +512,7 @@ func Test_MarryAndMerge(t *testing.T) { require.Equal(t, totalNumUnits, atx.NumUnits) require.Equal(t, signers[1].NodeID(), atx.SmesherID) require.Equal(t, poetProof.LeafCount/tickSize, atx.TickCount) + require.Equal(t, uint64(totalNumUnits)*atx.TickCount, atx.Weight) posATX, err = atxs.Get(db, mergedATX.ID()) require.NoError(t, err) diff --git a/activation/e2e/builds_atx_v2_test.go b/activation/e2e/builds_atx_v2_test.go index 057b45beec..9bc896cad7 100644 --- a/activation/e2e/builds_atx_v2_test.go +++ b/activation/e2e/builds_atx_v2_test.go @@ -216,7 +216,7 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { require.NotZero(t, atx.BaseTickHeight) require.NotZero(t, atx.TickCount) - require.NotZero(t, atx.GetWeight()) + require.NotZero(t, atx.Weight) require.NotZero(t, atx.TickHeight()) require.Equal(t, opts.NumUnits, atx.NumUnits) previous = atx diff --git a/activation/handler_test.go b/activation/handler_test.go index d5b03b9be0..30c0681dc2 100644 --- a/activation/handler_test.go +++ b/activation/handler_test.go @@ -642,7 +642,7 @@ func TestHandler_AtxWeight(t *testing.T) { require.Equal(t, uint64(0), stored1.BaseTickHeight) require.Equal(t, leaves/tickSize, stored1.TickCount) require.Equal(t, leaves/tickSize, stored1.TickHeight()) - require.Equal(t, (leaves/tickSize)*units, stored1.GetWeight()) + require.Equal(t, (leaves/tickSize)*units, stored1.Weight) atx2 := newChainedActivationTxV1(t, atx1, atx1.ID()) atx2.Sign(sig) @@ -657,7 +657,7 @@ func TestHandler_AtxWeight(t *testing.T) { require.Equal(t, stored1.TickHeight(), stored2.BaseTickHeight) require.Equal(t, leaves/tickSize, stored2.TickCount) require.Equal(t, stored1.TickHeight()+leaves/tickSize, stored2.TickHeight()) - require.Equal(t, int(leaves/tickSize)*units, int(stored2.GetWeight())) + require.Equal(t, int(leaves/tickSize)*units, int(stored2.Weight)) } func TestHandler_WrongHash(t *testing.T) { diff --git a/activation/handler_v1.go b/activation/handler_v1.go index cefff79e1b..0126ee9832 100644 --- a/activation/handler_v1.go +++ b/activation/handler_v1.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math/bits" "sync" "time" @@ -683,6 +684,11 @@ func (h *HandlerV1) processATX( atx.NumUnits = effectiveNumUnits atx.BaseTickHeight = baseTickHeight atx.TickCount = leaves / h.tickSize + hi, weight := bits.Mul64(uint64(atx.NumUnits), atx.TickCount) + if hi != 0 { + return nil, errors.New("atx weight would overflow uint64") + } + atx.Weight = weight proof, err = h.storeAtx(ctx, atx, watx) if err != nil { diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 476820acfa..20851b3f5a 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -1,10 +1,11 @@ package activation import ( + "cmp" "context" "errors" "fmt" - "math" + "math/bits" "slices" "time" @@ -121,9 +122,10 @@ func (h *HandlerV2) processATX( atx := &types.ActivationTx{ PublishEpoch: watx.PublishEpoch, Coinbase: watx.Coinbase, - NumUnits: parts.effectiveUnits, BaseTickHeight: baseTickHeight, - TickCount: parts.leaves / h.tickSize, + NumUnits: parts.effectiveUnits, + TickCount: parts.ticks, + Weight: parts.weight, VRFNonce: types.VRFPostIndex(watx.VRFNonce), SmesherID: watx.SmesherID, AtxBlob: types.AtxBlob{Blob: blob, Version: types.AtxV2}, @@ -487,10 +489,50 @@ func (h *HandlerV2) equivocationSet(atx *wire.ActivationTxV2) ([]types.NodeID, e } type atxParts struct { - leaves uint64 + ticks uint64 + weight uint64 effectiveUnits uint32 } +type nipostSize struct { + units uint32 + ticks uint64 +} + +func (n *nipostSize) addUnits(units uint32) error { + sum, carry := bits.Add32(n.units, units, 0) + if carry != 0 { + return errors.New("units overflow") + } + n.units = sum + return nil +} + +type nipostSizes []*nipostSize + +func (n nipostSizes) minTicks() uint64 { + return slices.MinFunc(n, func(a, b *nipostSize) int { return cmp.Compare(a.ticks, b.ticks) }).ticks +} + +func (n nipostSizes) sumUp() (units uint32, weight uint64, err error) { + var totalEffectiveNumUnits uint32 + var totalWeight uint64 + for _, ns := range n { + sum, carry := bits.Add32(totalEffectiveNumUnits, ns.units, 0) + if carry != 0 { + return 0, 0, fmt.Errorf("total units overflow (%d + %d)", totalEffectiveNumUnits, ns.units) + } + totalEffectiveNumUnits = sum + + hi, weight := bits.Mul64(uint64(ns.units), ns.ticks) + if hi != 0 { + return 0, 0, fmt.Errorf("weight overflow (%d * %d)", ns.units, ns.ticks) + } + totalWeight += weight + } + return totalEffectiveNumUnits, totalWeight, nil +} + func (h *HandlerV2) verifyIncludedIDsUniqueness(atx *wire.ActivationTxV2) error { seen := make(map[uint32]struct{}) for _, niposts := range atx.NiPosts { @@ -534,8 +576,9 @@ func (h *HandlerV2) syntacticallyValidateDeps( } // validate previous ATXs - var totalEffectiveNumUnits uint32 - for _, niposts := range atx.NiPosts { + nipostSizes := make(nipostSizes, len(atx.NiPosts)) + for i, niposts := range atx.NiPosts { + nipostSizes[i] = new(nipostSize) for _, post := range niposts.Posts { if post.MarriageIndex >= uint32(len(equivocationSet)) { err := fmt.Errorf("marriage index out of bounds: %d > %d", post.MarriageIndex, len(equivocationSet)-1) @@ -551,13 +594,13 @@ func (h *HandlerV2) syntacticallyValidateDeps( return nil, nil, fmt.Errorf("validating previous atx: %w", err) } } - totalEffectiveNumUnits += effectiveNumUnits + nipostSizes[i].addUnits(effectiveNumUnits) + } } // validate poet membership proofs - var minLeaves uint64 = math.MaxUint64 - for _, niposts := range atx.NiPosts { + for i, niposts := range atx.NiPosts { // verify PoET memberships in a single go indexedChallenges := make(map[uint64][]byte) @@ -594,7 +637,12 @@ func (h *HandlerV2) syntacticallyValidateDeps( if err != nil { return nil, nil, fmt.Errorf("invalid poet membership: %w", err) } - minLeaves = min(leaves, minLeaves) + nipostSizes[i].ticks = leaves / h.tickSize + } + + totalEffectiveNumUnits, totalWeight, err := nipostSizes.sumUp() + if err != nil { + return nil, nil, err } // validate all niposts @@ -641,8 +689,9 @@ func (h *HandlerV2) syntacticallyValidateDeps( } parts := &atxParts{ - leaves: minLeaves, + ticks: nipostSizes.minTicks(), effectiveUnits: totalEffectiveNumUnits, + weight: totalWeight, } if atx.Initial == nil { diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 01bf43166f..ffa53f3fbd 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "math" + "slices" "testing" "time" @@ -37,7 +39,10 @@ type marriedId struct { refAtx *wire.ActivationTxV2 } -const poetLeaves = 200 +const ( + tickSize = 20 + poetLeaves = 200 +) func newV2TestHandler(tb testing.TB, golden types.ATXID) *v2TestHandler { lg := zaptest.NewLogger(tb) @@ -50,7 +55,7 @@ func newV2TestHandler(tb testing.TB, golden types.ATXID) *v2TestHandler { atxsdata: atxsdata.New(), edVerifier: signing.NewEdVerifier(), clock: mocks.mclock, - tickSize: 1, + tickSize: tickSize, goldenATXID: golden, nipostValidator: mocks.mValidator, logger: lg, @@ -89,8 +94,12 @@ func (h *handlerMocks) expectVerifyNIPoST(atx *wire.ActivationTxV2) { ).Return(poetLeaves, nil) } -func (h *handlerMocks) expectVerifyNIPoSTs(atx *wire.ActivationTxV2, equivocationSet []types.NodeID) { - for _, nipost := range atx.NiPosts { +func (h *handlerMocks) expectVerifyNIPoSTs( + atx *wire.ActivationTxV2, + equivocationSet []types.NodeID, + poetLeaves []uint64, +) { + for i, nipost := range atx.NiPosts { for _, post := range nipost.Posts { h.mValidator.EXPECT().PostV2( gomock.Any(), @@ -107,7 +116,7 @@ func (h *handlerMocks) expectVerifyNIPoSTs(atx *wire.ActivationTxV2, equivocatio gomock.Any(), nipost.Challenge, gomock.Any(), - ) + ).Return(poetLeaves[i], nil) } } @@ -153,7 +162,11 @@ func (h *handlerMocks) expectAtxV2(atx *wire.ActivationTxV2) { h.expectStoreAtxV2(atx) } -func (h *handlerMocks) expectMergedAtxV2(atx *wire.ActivationTxV2, equivocationSet []types.NodeID) { +func (h *handlerMocks) expectMergedAtxV2( + atx *wire.ActivationTxV2, + equivocationSet []types.NodeID, + poetLeaves []uint64, +) { h.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) h.expectFetchDeps(atx) h.mValidator.EXPECT().VRFNonceV2( @@ -162,7 +175,7 @@ func (h *handlerMocks) expectMergedAtxV2(atx *wire.ActivationTxV2, equivocationS atx.VRFNonce, atx.TotalNumUnits(), ) - h.expectVerifyNIPoSTs(atx, equivocationSet) + h.expectVerifyNIPoSTs(atx, equivocationSet, poetLeaves) h.expectStoreAtxV2(atx) } @@ -445,6 +458,7 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { blob := codec.MustEncode(atx) atxHandler := newV2TestHandler(t, golden) + atxHandler.tickSize = tickSize atxHandler.expectInitialAtxV2(atx) proof, err := atxHandler.processATX(context.Background(), peer, atx, blob, time.Now()) @@ -456,9 +470,10 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { require.NotNil(t, atx) require.Equal(t, atx.ID(), atxFromDb.ID()) require.Equal(t, atx.Coinbase, atxFromDb.Coinbase) - require.EqualValues(t, poetLeaves, atxFromDb.TickCount) - require.EqualValues(t, poetLeaves, atxFromDb.TickHeight()) + require.EqualValues(t, poetLeaves/tickSize, atxFromDb.TickCount) + require.EqualValues(t, 0+atxFromDb.TickCount, atxFromDb.TickHeight()) // positioning is golden require.Equal(t, atx.NiPosts[0].Posts[0].NumUnits, atxFromDb.NumUnits) + require.EqualValues(t, atx.NiPosts[0].Posts[0].NumUnits*poetLeaves/tickSize, atxFromDb.Weight) // processing ATX for the second time should skip checks proof, err = atxHandler.processATX(context.Background(), peer, atx, blob, time.Now()) @@ -471,9 +486,10 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { prev := newInitialATXv1(t, golden) prev.Sign(sig) - atxs.Add(atxHandler.cdb, toAtx(t, prev)) + prevAtx := toAtx(t, prev) + atxs.Add(atxHandler.cdb, prevAtx) - atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), golden) + atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), prevAtx.ID()) atx.Sign(sig) blob := codec.MustEncode(atx) atxHandler.expectAtxV2(atx) @@ -486,34 +502,39 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { require.NoError(t, err) require.Nil(t, atxFromDb.CommitmentATX) - // copies coinbase and VRF nonce from the previous ATX - require.Equal(t, prev.Coinbase, atxFromDb.Coinbase) - require.EqualValues(t, *prev.VRFNonce, atxFromDb.VRFNonce) + + require.Equal(t, atx.Coinbase, atxFromDb.Coinbase) + require.EqualValues(t, atx.VRFNonce, atxFromDb.VRFNonce) + require.EqualValues(t, poetLeaves/tickSize, atxFromDb.TickCount) + require.EqualValues(t, prevAtx.TickHeight(), atxFromDb.BaseTickHeight) + require.EqualValues(t, prevAtx.TickHeight()+atxFromDb.TickCount, atxFromDb.TickHeight()) + require.Equal(t, atx.NiPosts[0].Posts[0].NumUnits, atxFromDb.NumUnits) + require.EqualValues(t, atx.NiPosts[0].Posts[0].NumUnits*poetLeaves/tickSize, atxFromDb.Weight) }) t.Run("second ATX, previous V2", func(t *testing.T) { t.Parallel() atxHandler := newV2TestHandler(t, golden) - prev := newInitialATXv2(t, golden) - prev.Sign(sig) - blob := codec.MustEncode(prev) - - atxHandler.expectInitialAtxV2(prev) - proof, err := atxHandler.processATX(context.Background(), peer, prev, blob, time.Now()) - require.NoError(t, err) - require.Nil(t, proof) + prev := atxHandler.createAndProcessInitial(t, sig) - atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), golden) + atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), prev.ID()) atx.Sign(sig) - blob = codec.MustEncode(atx) - atxHandler.expectAtxV2(atx) + blob := codec.MustEncode(atx) - proof, err = atxHandler.processATX(context.Background(), peer, atx, blob, time.Now()) + atxHandler.expectAtxV2(atx) + proof, err := atxHandler.processATX(context.Background(), peer, atx, blob, time.Now()) require.NoError(t, err) require.Nil(t, proof) - _, err = atxs.Get(atxHandler.cdb, atx.ID()) + prevAtx, err := atxs.Get(atxHandler.cdb, prev.ID()) + require.NoError(t, err) + atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) require.NoError(t, err) + require.EqualValues(t, poetLeaves/tickSize, atxFromDb.TickCount) + require.EqualValues(t, prevAtx.TickHeight(), atxFromDb.BaseTickHeight) + require.EqualValues(t, prevAtx.TickHeight()+atxFromDb.TickCount, atxFromDb.TickHeight()) + require.Equal(t, atx.NiPosts[0].Posts[0].NumUnits, atxFromDb.NumUnits) + require.EqualValues(t, atx.NiPosts[0].Posts[0].NumUnits*poetLeaves/tickSize, atxFromDb.Weight) }) t.Run("second ATX, previous checkpointed", func(t *testing.T) { t.Parallel() @@ -639,46 +660,29 @@ func marryIDs( golden types.ATXID, num int, ) (marriage *wire.ActivationTxV2, other []*wire.ActivationTxV2) { - var ( - marriedIds []marriedId - equivocationSet = []types.NodeID{sig.NodeID()} - ) - for range num { - signer, err := signing.NewEdSigner() - require.NoError(t, err) - atx := atxHandler.createAndProcessInitial(t, signer) - marriedIds = append(marriedIds, marriedId{signer, atx}) - } - - var atxs []*wire.ActivationTxV2 mATX := newInitialATXv2(t, golden) mATX.Marriages = []wire.MarriageCertificate{{ Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), }} - for _, id := range marriedIds { + + for range num { + signer, err := signing.NewEdSigner() + require.NoError(t, err) + atx := atxHandler.createAndProcessInitial(t, signer) + other = append(other, atx) mATX.Marriages = append(mATX.Marriages, wire.MarriageCertificate{ - ReferenceAtx: id.refAtx.ID(), - Signature: id.signer.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + ReferenceAtx: atx.ID(), + Signature: signer.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), }) - equivocationSet = append(equivocationSet, id.signer.NodeID()) } + mATX.Sign(sig) atxHandler.expectInitialAtxV2(mATX) p, err := atxHandler.processATX(context.Background(), "", mATX, codec.MustEncode(mATX), time.Now()) require.NoError(t, err) require.Nil(t, p) - // Other IDs publish their first ATXs. - for _, id := range marriedIds { - atx := newInitialATXv2(t, golden) - atx.Sign(id.signer) - atxHandler.expectInitialAtxV2(atx) - _, err := atxHandler.processATX(context.Background(), "", atx, codec.MustEncode(atx), time.Now()) - require.NoError(t, err) - atxs = append(atxs, atx) - } - - return mATX, atxs + return mATX, other } func TestHandlerV2_ProcessMergedATX(t *testing.T) { @@ -717,7 +721,7 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { merged.PreviousATXs = previousATXs merged.Sign(sig) - atxHandler.expectMergedAtxV2(merged, equivocationSet) + atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{poetLeaves}) p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) require.NoError(t, err) require.Nil(t, p) @@ -726,6 +730,81 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { require.NoError(t, err) require.Equal(t, totalNumUnits, atx.NumUnits) require.Equal(t, sig.NodeID(), atx.SmesherID) + require.EqualValues(t, totalNumUnits*poetLeaves/tickSize, atx.Weight) + }) + t.Run("merged IDs on 2 poets", func(t *testing.T) { + const tickSize = 33 + atxHandler := newV2TestHandler(t, golden) + atxHandler.tickSize = tickSize + + // Marry IDs + mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 4) + previousATXs := []types.ATXID{mATX.ID()} + equivocationSet := []types.NodeID{sig.NodeID()} + for _, atx := range otherATXs { + previousATXs = append(previousATXs, atx.ID()) + equivocationSet = append(equivocationSet, atx.SmesherID) + } + + // Process a merged ATX + merged := &wire.ActivationTxV2{ + PublishEpoch: mATX.PublishEpoch + 2, + PreviousATXs: previousATXs, + PositioningATX: mATX.ID(), + Coinbase: types.GenerateAddress([]byte("aaaa")), + VRFNonce: uint64(999), + NiPosts: make([]wire.NiPostsV2, 2), + } + atxsPerPoet := [][]*wire.ActivationTxV2{ + append([]*wire.ActivationTxV2{mATX}, otherATXs[:2]...), + otherATXs[2:], + } + var totalNumUnits uint32 + unitsPerPoet := make([]uint32, 2) + var idx uint32 + for nipostId := range 2 { + for _, atx := range atxsPerPoet[nipostId] { + post := wire.SubPostV2{ + MarriageIndex: idx, + NumUnits: atx.TotalNumUnits(), + PrevATXIndex: idx, + } + unitsPerPoet[nipostId] += post.NumUnits + totalNumUnits += post.NumUnits + merged.NiPosts[nipostId].Posts = append(merged.NiPosts[nipostId].Posts, post) + idx++ + } + } + + mATXID := mATX.ID() + merged.MarriageATX = &mATXID + + merged.PreviousATXs = previousATXs + merged.Sign(sig) + + poetLeaves := []uint64{100, 500} + minPoetLeaves := slices.Min(poetLeaves) + + atxHandler.expectMergedAtxV2(merged, equivocationSet, poetLeaves) + p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) + require.NoError(t, err) + require.Nil(t, p) + + marriageATX, err := atxs.Get(atxHandler.cdb, mATX.ID()) + require.NoError(t, err) + atx, err := atxs.Get(atxHandler.cdb, merged.ID()) + require.NoError(t, err) + require.Equal(t, totalNumUnits, atx.NumUnits) + require.Equal(t, sig.NodeID(), atx.SmesherID) + require.Equal(t, minPoetLeaves/tickSize, atx.TickCount) + require.Equal(t, marriageATX.TickHeight()+atx.TickCount, atx.TickHeight()) + // the total weight is summed weight on each poet + var weight uint64 + for i := range unitsPerPoet { + ticks := poetLeaves[i] / tickSize + weight += uint64(unitsPerPoet[i]) * ticks + } + require.EqualValues(t, weight, atx.Weight) }) t.Run("signer must be included merged ATX", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) @@ -758,7 +837,7 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) atxHandler.expectFetchDeps(merged) - atxHandler.expectVerifyNIPoSTs(merged, equivocationSet) + atxHandler.expectVerifyNIPoSTs(merged, equivocationSet, []uint64{200}) p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) require.ErrorContains(t, err, "ATX signer not present in merged ATX") @@ -1653,6 +1732,57 @@ func Test_MarryingMalicious(t *testing.T) { } } +func Test_CalculatingUnits(t *testing.T) { + t.Parallel() + t.Run("units on 1 nipost must not overflow", func(t *testing.T) { + t.Parallel() + ns := nipostSize{} + require.NoError(t, ns.addUnits(1)) + require.EqualValues(t, 1, ns.units) + require.Error(t, ns.addUnits(math.MaxUint32)) + }) + t.Run("total units on all niposts must not overflow", func(t *testing.T) { + t.Parallel() + ns := make(nipostSizes, 0) + ns = append(ns, &nipostSize{units: 11}, &nipostSize{units: math.MaxUint32 - 10}) + _, _, err := ns.sumUp() + require.Error(t, err) + }) + t.Run("units = sum of units on every nipost", func(t *testing.T) { + t.Parallel() + ns := make(nipostSizes, 0) + ns = append(ns, &nipostSize{units: 1}, &nipostSize{units: 10}) + u, _, err := ns.sumUp() + require.NoError(t, err) + require.EqualValues(t, 1+10, u) + }) +} + +func Test_CalculatingWeight(t *testing.T) { + t.Parallel() + t.Run("total weight must not overflow uint64", func(t *testing.T) { + t.Parallel() + ns := make(nipostSizes, 0) + ns = append(ns, &nipostSize{units: 1, ticks: 100}, &nipostSize{units: 10, ticks: math.MaxUint64}) + _, _, err := ns.sumUp() + require.Error(t, err) + }) + t.Run("weight = sum of weight on every nipost", func(t *testing.T) { + t.Parallel() + ns := make(nipostSizes, 0) + ns = append(ns, &nipostSize{units: 1, ticks: 100}, &nipostSize{units: 10, ticks: 1000}) + _, w, err := ns.sumUp() + require.NoError(t, err) + require.EqualValues(t, 1*100+10*1000, w) + }) +} + +func Test_CalculatingTicks(t *testing.T) { + ns := make(nipostSizes, 0) + ns = append(ns, &nipostSize{units: 1, ticks: 100}, &nipostSize{units: 10, ticks: 1000}) + require.EqualValues(t, 100, ns.minTicks()) +} + func newInitialATXv2(t testing.TB, golden types.ATXID) *wire.ActivationTxV2 { t.Helper() atx := &wire.ActivationTxV2{ diff --git a/activation/post_test.go b/activation/post_test.go index c0273a6369..de51d599df 100644 --- a/activation/post_test.go +++ b/activation/post_test.go @@ -273,15 +273,15 @@ func TestPostSetupManager_findCommitmentAtx_UsesLatestAtx(t *testing.T) { signer, err := signing.NewEdSigner() require.NoError(t, err) - challenge := types.NIPostChallenge{ + atx := &types.ActivationTx{ PublishEpoch: 1, + NumUnits: 2, + Weight: 2, + SmesherID: signer.NodeID(), + TickCount: 1, } - atx := types.NewActivationTx(challenge, types.Address{}, 2) - atx.SmesherID = signer.NodeID() atx.SetID(types.RandomATXID()) atx.SetReceived(time.Now()) - atx.TickCount = 1 - require.NoError(t, err) require.NoError(t, atxs.Add(mgr.db, atx)) mgr.atxsdata.AddFromAtx(atx, false) @@ -323,12 +323,16 @@ func TestPostSetupManager_getCommitmentAtx_getsCommitmentAtxFromInitialAtx(t *te // add an atx by the same node commitmentAtx := types.RandomATXID() - atx := types.NewActivationTx(types.NIPostChallenge{}, types.Address{}, 1) - atx.CommitmentATX = &commitmentAtx - atx.SmesherID = signer.NodeID() + atx := &types.ActivationTx{ + NumUnits: 1, + Weight: 1, + SmesherID: signer.NodeID(), + TickCount: 1, + CommitmentATX: &commitmentAtx, + } + atx.SetID(types.RandomATXID()) atx.SetReceived(time.Now()) - atx.TickCount = 1 require.NoError(t, atxs.Add(mgr.cdb, atx)) atxid, err := mgr.commitmentAtx(context.Background(), mgr.opts.DataDir, signer.NodeID()) diff --git a/api/grpcserver/grpcserver_test.go b/api/grpcserver/grpcserver_test.go index 7bb2eacf57..c434396896 100644 --- a/api/grpcserver/grpcserver_test.go +++ b/api/grpcserver/grpcserver_test.go @@ -88,8 +88,6 @@ var ( addr1 types.Address addr2 types.Address rewardSmesherID = types.RandomNodeID() - prevAtxID = types.ATXID(types.HexToHash32("44444")) - challenge = newChallenge(1, prevAtxID, prevAtxID, postGenesisEpoch) globalAtx *types.ActivationTx globalAtx2 *types.ActivationTx globalTx *types.Transaction @@ -165,12 +163,28 @@ func TestMain(m *testing.M) { addr1 = wallet.Address(signer1.PublicKey().Bytes()) addr2 = wallet.Address(signer2.PublicKey().Bytes()) - globalAtx = types.NewActivationTx(challenge, addr1, numUnits) + globalAtx = &types.ActivationTx{ + PublishEpoch: postGenesisEpoch, + Sequence: 1, + PrevATXID: types.ATXID{4, 4, 4, 4}, + Coinbase: addr1, + NumUnits: numUnits, + Weight: numUnits, + TickCount: 1, + SmesherID: signer.NodeID(), + } globalAtx.SetReceived(time.Now()) - globalAtx.SmesherID = signer.NodeID() - globalAtx.TickCount = 1 - globalAtx2 = types.NewActivationTx(challenge, addr2, numUnits) + globalAtx2 = &types.ActivationTx{ + PublishEpoch: postGenesisEpoch, + Sequence: 1, + PrevATXID: types.ATXID{5, 5, 5, 5}, + Coinbase: addr2, + NumUnits: numUnits, + Weight: numUnits, + TickCount: 1, + SmesherID: signer.NodeID(), + } globalAtx2.SetReceived(time.Now()) globalAtx2.SmesherID = signer.NodeID() globalAtx2.TickCount = 1 @@ -391,15 +405,6 @@ func NewTx(nonce uint64, recipient types.Address, signer *signing.EdSigner) *typ return &tx } -func newChallenge(sequence uint64, prevAtxID, posAtxID types.ATXID, epoch types.EpochID) types.NIPostChallenge { - return types.NIPostChallenge{ - Sequence: sequence, - PrevATXID: prevAtxID, - PublishEpoch: epoch, - PositioningATX: posAtxID, - } -} - func launchServer(tb testing.TB, services ...ServiceAPI) (Config, func()) { cfg := DefaultTestConfig() grpcService, err := NewWithServices(cfg.PublicListener, zaptest.NewLogger(tb).Named("grpc"), cfg, services) diff --git a/api/grpcserver/v2alpha1/activation.go b/api/grpcserver/v2alpha1/activation.go index fc8fd2f424..3019125996 100644 --- a/api/grpcserver/v2alpha1/activation.go +++ b/api/grpcserver/v2alpha1/activation.go @@ -149,7 +149,7 @@ func toAtx(atx *types.ActivationTx) *spacemeshv2alpha1.Activation { PublishEpoch: atx.PublishEpoch.Uint32(), PreviousAtx: atx.PrevATXID[:], Coinbase: atx.Coinbase.String(), - Weight: atx.GetWeight(), + Weight: atx.Weight, Height: atx.TickHeight(), } } diff --git a/atxsdata/data.go b/atxsdata/data.go index f8eae4794c..94c4cfc89d 100644 --- a/atxsdata/data.go +++ b/atxsdata/data.go @@ -76,7 +76,7 @@ func (d *Data) AddFromAtx(atx *types.ActivationTx, malicious bool) *ATX { atx.SmesherID, atx.Coinbase, atx.ID(), - atx.GetWeight(), + atx.Weight, atx.BaseTickHeight, atx.TickHeight(), atx.VRFNonce, diff --git a/beacon/beacon.go b/beacon/beacon.go index 8160597c0f..3a0effae27 100644 --- a/beacon/beacon.go +++ b/beacon/beacon.go @@ -604,7 +604,7 @@ func (pd *ProtocolDriver) initEpochStateIfNotPresent(logger *zap.Logger, target ) err := atxs.IterateAtxsWithMalfeasance(pd.cdb, target-1, func(atx *types.ActivationTx, malicious bool) bool { if !malicious { - epochWeight += atx.GetWeight() + epochWeight += atx.Weight } else { logger.Debug("malicious miner get 0 weight", zap.Stringer("smesher", atx.SmesherID)) } diff --git a/beacon/beacon_test.go b/beacon/beacon_test.go index c72776f17a..bdc1c54fa7 100644 --- a/beacon/beacon_test.go +++ b/beacon/beacon_test.go @@ -114,22 +114,25 @@ func createATX( numUnits uint32, received time.Time, ) types.ATXID { - nonce := types.VRFPostIndex(1) - atx := types.NewActivationTx( - types.NIPostChallenge{PublishEpoch: lid.GetEpoch()}, - types.GenerateAddress(types.RandomBytes(types.AddressLength)), - numUnits, - ) - atx.VRFNonce = nonce + tb.Helper() + atx := types.ActivationTx{ + PublishEpoch: lid.GetEpoch(), + Coinbase: types.GenerateAddress(types.RandomBytes(types.AddressLength)), + NumUnits: numUnits, + VRFNonce: 1, + TickCount: 1, + Weight: uint64(numUnits), + SmesherID: sig.NodeID(), + } + atx.SetReceived(received) - atx.SmesherID = sig.NodeID() atx.SetID(types.RandomATXID()) - atx.TickCount = 1 - require.NoError(tb, atxs.Add(db, atx)) + require.NoError(tb, atxs.Add(db, &atx)) return atx.ID() } func createRandomATXs(tb testing.TB, db *datastore.CachedDB, lid types.LayerID, num int) { + tb.Helper() for i := 0; i < num; i++ { sig, err := signing.NewEdSigner() require.NoError(tb, err) @@ -187,12 +190,8 @@ func TestBeacon_MultipleNodes(t *testing.T) { require.NoError(t, err) require.Equal(t, bootstrap, got) } - for i, node := range testNodes { - if i == 0 { - // make the first node non-smeshing node - continue - } - + // make the first node non-smeshing node + for _, node := range testNodes[1:] { for _, db := range dbs { for _, s := range node.signers { createATX(t, db, atxPublishLid, s, 1, time.Now().Add(-1*time.Second)) diff --git a/beacon/handlers.go b/beacon/handlers.go index 7234572101..89d838e985 100644 --- a/beacon/handlers.go +++ b/beacon/handlers.go @@ -331,7 +331,7 @@ func (pd *ProtocolDriver) storeFirstVotes(m FirstVotingMessage, nodeID types.Nod } voteWeight := new(big.Int) if !malicious { - voteWeight.SetUint64(atx.GetWeight()) + voteWeight.SetUint64(atx.Weight) } else { pd.logger.Debug("malicious miner get 0 weight", zap.Stringer("smesher", nodeID)) } @@ -457,7 +457,7 @@ func (pd *ProtocolDriver) storeFollowingVotes(m FollowingVotingMessage, nodeID t } voteWeight := new(big.Int) if !malicious { - voteWeight.SetUint64(atx.GetWeight()) + voteWeight.SetUint64(atx.Weight) } else { pd.logger.Debug("malicious miner get 0 weight", zap.Stringer("smesher", nodeID)) } diff --git a/blocks/generator_test.go b/blocks/generator_test.go index 0d4d206464..4145f3ff29 100644 --- a/blocks/generator_test.go +++ b/blocks/generator_test.go @@ -154,14 +154,15 @@ func createModifiedATXs( signer, err := signing.NewEdSigner() require.NoError(tb, err) signers = append(signers, signer) - address := types.GenerateAddress(signer.PublicKey().Bytes()) - atx := types.NewActivationTx( - types.NIPostChallenge{PublishEpoch: lid.GetEpoch()}, - address, - numUnit, - ) + atx := &types.ActivationTx{ + PublishEpoch: lid.GetEpoch(), + Coinbase: types.GenerateAddress(signer.PublicKey().Bytes()), + NumUnits: numUnit, + SmesherID: signer.NodeID(), + TickCount: 1, + Weight: uint64(numUnit), + } atx.SetReceived(time.Now()) - atx.SmesherID = signer.NodeID() atx.SetID(types.RandomATXID()) onAtx(atx) data.AddFromAtx(atx, false) diff --git a/cmd/activeset/activeset.go b/cmd/activeset/activeset.go index 6c3acd6d0c..1046916b02 100644 --- a/cmd/activeset/activeset.go +++ b/cmd/activeset/activeset.go @@ -39,7 +39,7 @@ Example: for _, id := range ids { atx, err := atxs.Get(db, id) must(err, "get id %v: %s\n", id, err) - weight += atx.GetWeight() + weight += atx.Weight } fmt.Printf("count = %d\nweight = %d\n", len(ids), weight) } diff --git a/common/types/activation.go b/common/types/activation.go index ca99b151c4..0bc7d65076 100644 --- a/common/types/activation.go +++ b/common/types/activation.go @@ -185,6 +185,13 @@ type ActivationTx struct { TickCount uint64 VRFNonce VRFPostIndex SmesherID NodeID + // Weight of the ATX. The total weight of the epoch is expected to fit in a uint64. + // The total ATX weight is sum(NumUnits * TickCount) for identity it holds. + // Space Units sizes are chosen such that NumUnits for all ATXs in an epoch is expected to be < 10^6. + // PoETs should produce ~10k ticks at genesis, but are expected due to technological advances + // to produce more over time. A uint64 should be large enough to hold the total weight of an epoch, + // for at least the first few years. + Weight uint64 AtxBlob @@ -194,25 +201,6 @@ type ActivationTx struct { validity Validity // whether the chain is fully verified and OK } -// NewActivationTx returns a new activation transaction. The ATXID is calculated and cached. -// NOTE: this function is deprecated and used in a few tests only. -// Create a new ActivationTx with ActivationTx{...}, setting the fields manually. -func NewActivationTx( - challenge NIPostChallenge, - coinbase Address, - numUnits uint32, -) *ActivationTx { - atx := &ActivationTx{ - PublishEpoch: challenge.PublishEpoch, - Sequence: challenge.Sequence, - PrevATXID: challenge.PrevATXID, - CommitmentATX: challenge.CommitmentATX, - Coinbase: coinbase, - NumUnits: numUnits, - } - return atx -} - // TargetEpoch returns the target epoch of the ATX. This is the epoch in which the miner is eligible // to participate thanks to the ATX. func (atx *ActivationTx) TargetEpoch() EpochID { @@ -238,16 +226,6 @@ func (atx *ActivationTx) SetGolden() { atx.golden = true } -// Weight of the ATX. The total weight of the epoch is expected to fit in a uint64 and is -// sum(atx.NumUnits * atx.TickCount for each ATX in a given epoch). -// Space Units sizes are chosen such that NumUnits for all ATXs in an epoch is expected to be < 10^6. -// PoETs should produce ~10k ticks at genesis, but are expected due to technological advances -// to produce more over time. A uint64 should be large enough to hold the total weight of an epoch, -// for at least the first few years. -func (atx *ActivationTx) GetWeight() uint64 { - return getWeight(uint64(atx.NumUnits), atx.TickCount) -} - // TickHeight returns a sum of base tick height and tick count. func (atx *ActivationTx) TickHeight() uint64 { return atx.BaseTickHeight + atx.TickCount @@ -270,7 +248,7 @@ func (atx *ActivationTx) MarshalLogObject(encoder log.ObjectEncoder) error { encoder.AddUint64("sequence_number", atx.Sequence) encoder.AddUint64("base_tick_height", atx.BaseTickHeight) encoder.AddUint64("tick_count", atx.TickCount) - encoder.AddUint64("weight", atx.GetWeight()) + encoder.AddUint64("weight", atx.Weight) encoder.AddUint64("height", atx.TickHeight()) return nil } @@ -400,15 +378,3 @@ type EpochActiveSet struct { } var MaxEpochActiveSetSize = scale.MustGetMaxElements[EpochActiveSet]("Set") - -func getWeight(numUnits, tickCount uint64) uint64 { - return safeMul(numUnits, tickCount) -} - -func safeMul(a, b uint64) uint64 { - c := a * b - if a > 1 && b > 1 && c/b != a { - panic("uint64 overflow") - } - return c -} diff --git a/fetch/mesh_data_test.go b/fetch/mesh_data_test.go index a83ef09c69..b1916a4a8a 100644 --- a/fetch/mesh_data_test.go +++ b/fetch/mesh_data_test.go @@ -458,11 +458,11 @@ func genATXs(tb testing.TB, num uint32) []*types.ActivationTx { require.NoError(tb, err) atxs := make([]*types.ActivationTx, 0, num) for i := uint32(0); i < num; i++ { - atx := types.NewActivationTx( - types.NIPostChallenge{}, - types.Address{1, 2, 3}, - i, - ) + atx := &types.ActivationTx{ + Coinbase: types.Address{1, 2, 3}, + NumUnits: i, + Weight: uint64(i), + } atx.SmesherID = sig.NodeID() atx.SetID(types.RandomATXID()) atxs = append(atxs, atx) diff --git a/hare3/eligibility/oracle_test.go b/hare3/eligibility/oracle_test.go index 60652a04a7..e299ea2f02 100644 --- a/hare3/eligibility/oracle_test.go +++ b/hare3/eligibility/oracle_test.go @@ -143,8 +143,7 @@ func (t *testOracle) createActiveSet( miners = append(miners, nodeID) atx := &types.ActivationTx{ PublishEpoch: lid.GetEpoch(), - NumUnits: uint32(i + 1), - TickCount: 1, + Weight: uint64(i + 1), SmesherID: nodeID, } atx.SetID(id) @@ -371,8 +370,7 @@ func Test_VrfSignVerify(t *testing.T) { activeSet := types.RandomActiveSet(numMiners) atx1 := &types.ActivationTx{ PublishEpoch: prevEpoch, - NumUnits: 1 * 1024, - TickCount: 1, + Weight: 1 * 1024, SmesherID: signer.NodeID(), } atx1.SetID(activeSet[0]) @@ -384,9 +382,8 @@ func Test_VrfSignVerify(t *testing.T) { atx2 := &types.ActivationTx{ PublishEpoch: prevEpoch, - NumUnits: 9 * 1024, + Weight: 9 * 1024, SmesherID: signer2.NodeID(), - TickCount: 1, } atx2.SetID(activeSet[1]) atx2.SetReceived(time.Now()) diff --git a/hare3/hare_test.go b/hare3/hare_test.go index c53f78fbe5..acdaa7f398 100644 --- a/hare3/hare_test.go +++ b/hare3/hare_test.go @@ -163,6 +163,7 @@ func (n *node) withAtx(min, max int) *node { } else { atx.NumUnits = uint32(min) } + atx.Weight = uint64(atx.NumUnits) * atx.TickCount id := types.ATXID{} n.t.rng.Read(id[:]) atx.SetID(id) diff --git a/malfeasance/wire/malfeasance_test.go b/malfeasance/wire/malfeasance_test.go index b367d24ee0..df927e2145 100644 --- a/malfeasance/wire/malfeasance_test.go +++ b/malfeasance/wire/malfeasance_test.go @@ -25,14 +25,11 @@ func TestMain(m *testing.M) { func TestCodec_MultipleATXs(t *testing.T) { epoch := types.EpochID(11) - a1 := types.NewActivationTx(types.NIPostChallenge{PublishEpoch: epoch}, types.Address{1, 2, 3}, 10) - a2 := types.NewActivationTx(types.NIPostChallenge{PublishEpoch: epoch}, types.Address{3, 2, 1}, 11) - var atxProof wire.AtxProof - for i, a := range []*types.ActivationTx{a1, a2} { + for i := range atxProof.Messages { atxProof.Messages[i] = wire.AtxProofMsg{ InnerMsg: types.ATXMetadata{ - PublishEpoch: a.PublishEpoch, + PublishEpoch: epoch, MsgHash: types.RandomHash(), }, SmesherID: types.RandomNodeID(), diff --git a/mesh/executor_test.go b/mesh/executor_test.go index 01330cfb6e..01645d640f 100644 --- a/mesh/executor_test.go +++ b/mesh/executor_test.go @@ -69,16 +69,17 @@ func makeResults(lid types.LayerID, txs ...types.Transaction) []types.Transactio func (t *testExecutor) createATX(epoch types.EpochID, cb types.Address) (types.ATXID, types.NodeID) { sig, err := signing.NewEdSigner() require.NoError(t.tb, err) - atx := types.NewActivationTx( - types.NIPostChallenge{PublishEpoch: epoch}, - cb, - 11, - ) - atx.VRFNonce = 1 + atx := &types.ActivationTx{ + PublishEpoch: epoch, + Coinbase: cb, + NumUnits: 11, + Weight: 11, + VRFNonce: 1, + TickCount: 1, + SmesherID: sig.NodeID(), + } atx.SetReceived(time.Now()) - atx.SmesherID = sig.NodeID() atx.SetID(types.RandomATXID()) - atx.TickCount = 1 require.NoError(t.tb, atxs.Add(t.db, atx)) t.atxsdata.AddFromAtx(atx, false) return atx.ID(), sig.NodeID() diff --git a/miner/proposal_builder_test.go b/miner/proposal_builder_test.go index 542927cc2c..3fd3fa2457 100644 --- a/miner/proposal_builder_test.go +++ b/miner/proposal_builder_test.go @@ -75,6 +75,7 @@ func gatx( PublishEpoch: epoch, TickCount: ticks, SmesherID: smesher, + Weight: uint64(units) * ticks, } atx.SetID(id) atx.SetReceived(time.Time{}.Add(1)) diff --git a/proposals/eligibility_validator_test.go b/proposals/eligibility_validator_test.go index 6030327d4f..acdcc9203c 100644 --- a/proposals/eligibility_validator_test.go +++ b/proposals/eligibility_validator_test.go @@ -27,6 +27,7 @@ func gatx( VRFNonce: nonce, TickCount: 100, SmesherID: smesher, + Weight: uint64(units) * 100, } atx.SetID(id) atx.SetReceived(time.Time{}.Add(1)) diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 4f41ab4a68..423d28df65 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -22,7 +22,7 @@ const ( // filters that refer to the id column. const fieldsQuery = `select atxs.id, atxs.nonce, atxs.base_tick_height, atxs.tick_count, atxs.pubkey, atxs.effective_num_units, -atxs.received, atxs.epoch, atxs.sequence, atxs.coinbase, atxs.validity, atxs.prev_id, atxs.commitment_atx` +atxs.received, atxs.epoch, atxs.sequence, atxs.coinbase, atxs.validity, atxs.prev_id, atxs.commitment_atx, atxs.weight` const fullQuery = fieldsQuery + ` from atxs` @@ -61,6 +61,7 @@ func decoder(fn decoderCallback) sql.Decoder { a.CommitmentATX = new(types.ATXID) stmt.ColumnBytes(12, a.CommitmentATX[:]) } + a.Weight = uint64(stmt.ColumnInt64(13)) return fn(&a) } @@ -440,13 +441,14 @@ func Add(db sql.Executor, atx *types.ActivationTx) error { } else { stmt.BindNull(13) } + stmt.BindInt64(14, int64(atx.Weight)) } _, err := db.Exec(` insert into atxs (id, epoch, effective_num_units, commitment_atx, nonce, pubkey, received, base_tick_height, tick_count, sequence, coinbase, - validity, prev_id) - values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)`, enc, nil) + validity, prev_id, weight) + values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)`, enc, nil) if err != nil { return fmt.Errorf("insert ATX ID %v: %w", atx.ID(), err) } @@ -776,7 +778,7 @@ func IterateAtxsWithMalfeasance( func(s *sql.Statement) { s.BindInt64(1, int64(publish)) }, func(s *sql.Statement) bool { return decoder(func(atx *types.ActivationTx) bool { - return fn(atx, s.ColumnInt(13) != 0) + return fn(atx, s.ColumnInt(14) != 0) })(s) }, ) diff --git a/sql/migrations/state/0020_atx_weight.sql b/sql/migrations/state/0020_atx_weight.sql new file mode 100644 index 0000000000..4504bd4320 --- /dev/null +++ b/sql/migrations/state/0020_atx_weight.sql @@ -0,0 +1,2 @@ +ALTER TABLE atxs ADD COLUMN weight INTEGER; +INSERT INTO atxs (weight) SELECT effective_num_units * tick_count FROM atxs; diff --git a/tortoise/model/core.go b/tortoise/model/core.go index ce7022fa33..04381a2aa1 100644 --- a/tortoise/model/core.go +++ b/tortoise/model/core.go @@ -147,19 +147,20 @@ func (c *core) OnMessage(m Messenger, event Message) { return } - nipost := types.NIPostChallenge{ - PublishEpoch: ev.LayerID.GetEpoch(), + atx := &types.ActivationTx{ + PublishEpoch: ev.LayerID.GetEpoch(), + NumUnits: c.units, + Coinbase: types.GenerateAddress(c.signer.PublicKey().Bytes()), + SmesherID: c.signer.NodeID(), + BaseTickHeight: 1, + TickCount: 2, + Weight: uint64(c.units) * 2, } - addr := types.GenerateAddress(c.signer.PublicKey().Bytes()) - atx := types.NewActivationTx(nipost, addr, c.units) - atx.SmesherID = c.signer.NodeID() atx.SetID(types.RandomATXID()) atx.SetReceived(time.Now()) - atx.BaseTickHeight = 1 - atx.TickCount = 2 c.refBallot = nil c.atx = atx.ID() - c.weight = atx.GetWeight() + c.weight = atx.Weight m.Send(MessageAtx{Atx: atx}) case MessageBlock: diff --git a/tortoise/sim/generator.go b/tortoise/sim/generator.go index 3ebc5a82c4..d89a3be918 100644 --- a/tortoise/sim/generator.go +++ b/tortoise/sim/generator.go @@ -229,23 +229,24 @@ func (g *Generator) generateAtxs() { if err != nil { panic(err) } - address := types.GenerateAddress(sig.PublicKey().Bytes()) - nipost := types.NIPostChallenge{ - PublishEpoch: g.nextLayer.Sub(1).GetEpoch(), - } - atx := types.NewActivationTx(nipost, address, units) var ticks uint64 if g.ticks != nil { ticks = g.ticks[i] } else { ticks = uint64(intInRange(g.rng, g.ticksRange)) } - atx.SmesherID = sig.NodeID() + atx := &types.ActivationTx{ + PublishEpoch: g.nextLayer.Sub(1).GetEpoch(), + Coinbase: types.GenerateAddress(sig.PublicKey().Bytes()), + NumUnits: units, + SmesherID: sig.NodeID(), + BaseTickHeight: g.prevHeight[i], + TickCount: ticks, + Weight: uint64(units) * ticks, + } atx.SetID(types.RandomATXID()) atx.SetReceived(time.Now()) - atx.BaseTickHeight = g.prevHeight[i] - atx.TickCount = ticks g.prevHeight[i] += ticks g.activations[i] = atx for _, state := range g.states { diff --git a/tortoise/sim/layer.go b/tortoise/sim/layer.go index 5a1ba8a7d6..5bc6b74d2d 100644 --- a/tortoise/sim/layer.go +++ b/tortoise/sim/layer.go @@ -159,7 +159,7 @@ func (g *Generator) genLayer(cfg nextConf) types.LayerID { } var total uint64 for _, atx := range g.activations { - total += atx.GetWeight() + total += atx.Weight } miners := make([]uint32, len(g.activations)) @@ -182,7 +182,7 @@ func (g *Generator) genLayer(cfg nextConf) types.LayerID { if err != nil { g.logger.Panic("failed to get a beacon", zap.Error(err)) } - n, err := util.GetNumEligibleSlots(atx.GetWeight(), 0, total, g.conf.LayerSize, g.conf.LayersPerEpoch) + n, err := util.GetNumEligibleSlots(atx.Weight, 0, total, g.conf.LayerSize, g.conf.LayersPerEpoch) if err != nil { g.logger.Panic("eligible slots", zap.Error(err)) } diff --git a/tortoise/tortoise_test.go b/tortoise/tortoise_test.go index d8b2de1b9f..9d1ec119c0 100644 --- a/tortoise/tortoise_test.go +++ b/tortoise/tortoise_test.go @@ -475,8 +475,7 @@ func TestComputeExpectedWeight(t *testing.T) { eid := first + types.EpochID(i) atx := &types.ActivationTx{ PublishEpoch: eid - 1, - NumUnits: uint32(weight), - TickCount: 1, + Weight: weight, } atx.SetID(types.RandomATXID()) atx.SetReceived(time.Now()) @@ -500,7 +499,7 @@ func extractAtxsData(db sql.Executor, target types.EpochID) (uint64, uint64, err heights []uint64 ) if err := atxs.IterateAtxsOps(db, builder.FilterEpochOnly(target-1), func(atx *types.ActivationTx) bool { - weight += atx.GetWeight() + weight += atx.Weight heights = append(heights, atx.TickHeight()) return true }); err != nil { From 72f8ec30e728cb7f68c654a1c6650fa45242a9d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 20 Jun 2024 19:01:42 +0200 Subject: [PATCH 07/25] Review feedback --- activation/e2e/atx_merge_test.go | 29 +++++++++-------- activation/handler_v2.go | 33 ++++++++++--------- activation/handler_v2_test.go | 55 +++++++++++++++++++++++++++----- activation/wire/wire_v2.go | 2 +- common/fixture/atxs.go | 18 +++++++++++ 5 files changed, 98 insertions(+), 39 deletions(-) diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index a0cf900b48..86c74fa2ed 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -66,17 +66,18 @@ type nipostData struct { func buildNipost( nb *activation.NIPostBuilder, - sig *signing.EdSigner, + signer *signing.EdSigner, publish types.EpochID, previous, positioning types.ATXID, ) (nipostData, error) { - challenge := wire.NIPostChallengeV2{ - PublishEpoch: publish, - PrevATXID: previous, - PositioningATXID: positioning, + postChallenge := &types.NIPostChallenge{ + PublishEpoch: publish, + PrevATXID: previous, + PositioningATX: positioning, } - nipost, err := nb.BuildNIPost(context.Background(), sig, challenge.PublishEpoch, challenge.Hash()) - nb.ResetState(sig.NodeID()) + challenge := wire.NIPostChallengeToWireV2(postChallenge).Hash() + nipost, err := nb.BuildNIPost(context.Background(), signer, challenge, postChallenge) + nb.ResetState(signer.NodeID()) return nipostData{previous, nipost}, err } @@ -304,6 +305,7 @@ func Test_MarryAndMerge(t *testing.T) { logger.Named("nipostBuilder"), poetCfg, clock, + validator, activation.WithPoetClients(poetClient), ) require.NoError(t, err) @@ -338,17 +340,18 @@ func Test_MarryAndMerge(t *testing.T) { eg = errgroup.Group{} for i, signer := range signers { eg.Go(func() error { - post, postInfo, err := nb.Proof(context.Background(), signer.NodeID(), types.EmptyHash32[:]) + post, postInfo, err := nb.Proof(context.Background(), signer.NodeID(), types.EmptyHash32[:], nil) if err != nil { return err } - challenge := wire.NIPostChallengeV2{ - PublishEpoch: publish, - PositioningATXID: goldenATX, - InitialPost: wire.PostToWireV1(post), + postChallenge := &types.NIPostChallenge{ + PublishEpoch: publish, + PositioningATX: goldenATX, + InitialPost: post, } - nipost, err := nb.BuildNIPost(context.Background(), signer, challenge.PublishEpoch, challenge.Hash()) + challenge := wire.NIPostChallengeToWireV2(postChallenge).Hash() + nipost, err := nb.BuildNIPost(context.Background(), signer, challenge, postChallenge) if err != nil { return err } diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 20851b3f5a..72b46d7a49 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "math" "math/bits" "slices" "time" @@ -435,7 +436,8 @@ func (h *HandlerV2) validateMarriages(atx *wire.ActivationTxV2) ([]types.NodeID, if len(atx.Marriages) == 0 { return nil, nil } - var marryingIDs []types.NodeID + marryingIDsSet := make(map[types.NodeID]struct{}, len(atx.Marriages)) + var marryingIDs []types.NodeID // for deterministic order for i, m := range atx.Marriages { var id types.NodeID if m.ReferenceAtx == types.EmptyATXID { @@ -451,6 +453,10 @@ func (h *HandlerV2) validateMarriages(atx *wire.ActivationTxV2) ([]types.NodeID, if !h.edVerifier.Verify(signing.MARRIAGE, id, atx.SmesherID.Bytes(), m.Signature) { return nil, fmt.Errorf("invalid marriage[%d] signature", i) } + if _, ok := marryingIDsSet[id]; ok { + return nil, fmt.Errorf("more than 1 marriage certificate for ID %s", id) + } + marryingIDsSet[id] = struct{}{} marryingIDs = append(marryingIDs, id) } return marryingIDs, nil @@ -515,14 +521,10 @@ func (n nipostSizes) minTicks() uint64 { } func (n nipostSizes) sumUp() (units uint32, weight uint64, err error) { - var totalEffectiveNumUnits uint32 + var totalUnits uint64 var totalWeight uint64 for _, ns := range n { - sum, carry := bits.Add32(totalEffectiveNumUnits, ns.units, 0) - if carry != 0 { - return 0, 0, fmt.Errorf("total units overflow (%d + %d)", totalEffectiveNumUnits, ns.units) - } - totalEffectiveNumUnits = sum + totalUnits += uint64(ns.units) hi, weight := bits.Mul64(uint64(ns.units), ns.ticks) if hi != 0 { @@ -530,7 +532,10 @@ func (n nipostSizes) sumUp() (units uint32, weight uint64, err error) { } totalWeight += weight } - return totalEffectiveNumUnits, totalWeight, nil + if totalUnits > math.MaxUint32 { + return 0, 0, fmt.Errorf("total units overflow: %d", totalUnits) + } + return uint32(totalUnits), totalWeight, nil } func (h *HandlerV2) verifyIncludedIDsUniqueness(atx *wire.ActivationTxV2) error { @@ -595,7 +600,6 @@ func (h *HandlerV2) syntacticallyValidateDeps( } } nipostSizes[i].addUnits(effectiveNumUnits) - } } @@ -708,7 +712,6 @@ func (h *HandlerV2) syntacticallyValidateDeps( } func (h *HandlerV2) checkMalicious( - ctx context.Context, tx *sql.Tx, watx *wire.ActivationTxV2, marrying []types.NodeID, @@ -721,7 +724,7 @@ func (h *HandlerV2) checkMalicious( return true, nil, nil } - proof, err := h.checkDoubleMarry(tx, watx, marrying) + proof, err := h.checkDoubleMarry(tx, marrying) if err != nil { return false, nil, fmt.Errorf("checking double marry: %w", err) } @@ -739,11 +742,7 @@ func (h *HandlerV2) checkMalicious( return false, nil, nil } -func (h *HandlerV2) checkDoubleMarry( - tx *sql.Tx, - watx *wire.ActivationTxV2, - marrying []types.NodeID, -) (*mwire.MalfeasanceProof, error) { +func (h *HandlerV2) checkDoubleMarry(tx *sql.Tx, marrying []types.NodeID) (*mwire.MalfeasanceProof, error) { for _, id := range marrying { married, err := identities.Married(tx, id) if err != nil { @@ -776,7 +775,7 @@ func (h *HandlerV2) storeAtx( ) if err := h.cdb.WithTx(ctx, func(tx *sql.Tx) error { var err error - malicious, proof, err = h.checkMalicious(ctx, tx, watx, marrying) + malicious, proof, err = h.checkMalicious(tx, watx, marrying) if err != nil { return fmt.Errorf("check malicious: %w", err) } diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index ffa53f3fbd..1c84c353fd 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -19,6 +19,7 @@ import ( "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/fixture" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" mwire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" @@ -121,8 +122,8 @@ func (h *handlerMocks) expectVerifyNIPoSTs( } func (h *handlerMocks) expectStoreAtxV2(atx *wire.ActivationTxV2) { - h.mbeacon.EXPECT().OnAtx(gomock.Any()) - h.mtortoise.EXPECT().OnAtx(gomock.Any(), gomock.Any(), gomock.Any()) + h.mbeacon.EXPECT().OnAtx(fixture.MatchId(atx.ID())) + h.mtortoise.EXPECT().OnAtx(atx.PublishEpoch+1, atx.ID(), gomock.Any()) h.mValidator.EXPECT().IsVerifyingFullPost().Return(false) } @@ -183,17 +184,24 @@ func (h *v2TestHandler) createAndProcessInitial(t *testing.T, sig *signing.EdSig t.Helper() atx := newInitialATXv2(t, h.handlerMocks.goldenATXID) atx.Sign(sig) - p, err := h.processInitial(atx) + p, err := h.processInitial(t, atx) require.NoError(t, err) require.Nil(t, p) return atx } -func (h *v2TestHandler) processInitial(atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) { +func (h *v2TestHandler) processInitial(t *testing.T, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) { + t.Helper() h.expectInitialAtxV2(atx) return h.processATX(context.Background(), peer.ID("peer"), atx, codec.MustEncode(atx), time.Now()) } +func (h *v2TestHandler) processSoloAtx(t *testing.T, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) { + t.Helper() + h.expectAtxV2(atx) + return h.processATX(context.Background(), peer.ID("peer"), atx, codec.MustEncode(atx), time.Now()) +} + func TestHandlerV2_SyntacticallyValidate(t *testing.T) { t.Parallel() golden := types.RandomATXID() @@ -1234,7 +1242,7 @@ func Test_ValidateMarriages(t *testing.T) { } marriage.Sign(sig) - p, err := atxHandler.processInitial(marriage) + p, err := atxHandler.processInitial(t, marriage) require.NoError(t, err) require.Nil(t, p) @@ -1569,10 +1577,9 @@ func Test_Marriages(t *testing.T) { Signature: otherSig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), }, } - atx.Sign(sig) - p, err := atxHandler.processInitial(atx) + p, err := atxHandler.processInitial(t, atx) require.NoError(t, err) require.Nil(t, p) @@ -1588,7 +1595,39 @@ func Test_Marriages(t *testing.T) { require.NoError(t, err) require.ElementsMatch(t, []types.NodeID{sig.NodeID(), otherSig.NodeID()}, set) }) - t.Run("can't marry twice", func(t *testing.T) { + t.Run("can't marry twice in the same marriage ATX", func(t *testing.T) { + t.Parallel() + atxHandler := newV2TestHandler(t, golden) + + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + othersAtx := atxHandler.createAndProcessInitial(t, otherSig) + + othersSecondAtx := newSoloATXv2(t, othersAtx.PublishEpoch+1, othersAtx.ID(), othersAtx.ID()) + othersSecondAtx.Sign(otherSig) + _, err = atxHandler.processSoloAtx(t, othersSecondAtx) + require.NoError(t, err) + + atx := newInitialATXv2(t, golden) + atx.Marriages = []wire.MarriageCertificate{ + { + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + { + ReferenceAtx: othersAtx.ID(), + Signature: otherSig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + { + ReferenceAtx: othersSecondAtx.ID(), + Signature: otherSig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }, + } + atx.Sign(sig) + + _, err = atxHandler.validateMarriages(atx) + require.ErrorContains(t, err, "more than 1 marriage certificate for ID") + }) + t.Run("can't marry twice (separate marriages)", func(t *testing.T) { t.Parallel() atxHandler := newV2TestHandler(t, golden) diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index c844ae7809..a320351152 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -229,7 +229,7 @@ func (sp *SubPostV2) Root(prevATXs []types.ATXID) []byte { } tree.AddLeaf(prevATXs[sp.PrevATXIndex].Bytes()) - var leafIndex [8]byte + var leafIndex types.Hash32 binary.LittleEndian.PutUint64(leafIndex[:], sp.MembershipLeafIndex) tree.AddLeaf(leafIndex[:]) diff --git a/common/fixture/atxs.go b/common/fixture/atxs.go index af3ead08f2..74226aca21 100644 --- a/common/fixture/atxs.go +++ b/common/fixture/atxs.go @@ -77,3 +77,21 @@ func ToAtx(t testing.TB, watx *wire.ActivationTxV1) *types.ActivationTx { atx.TickCount = 1 return atx } + +type idMatcher types.ATXID + +func MatchId(id types.ATXID) idMatcher { + return idMatcher(id) +} + +func (m idMatcher) Matches(x any) bool { + type hasID interface { + ID() types.ATXID + } + v, ok := x.(hasID) + return ok && v.ID() == types.ATXID(m) +} + +func (m idMatcher) String() string { + return "is ATX ID " + types.ATXID(m).String() +} From 568a5a13911164c351b168124e62439a871c8114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Sun, 23 Jun 2024 17:26:31 +0200 Subject: [PATCH 08/25] Fix atxs weight migration --- sql/migrations/state/0020_atx_weight.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/migrations/state/0020_atx_weight.sql b/sql/migrations/state/0020_atx_weight.sql index 4504bd4320..9fa69ec669 100644 --- a/sql/migrations/state/0020_atx_weight.sql +++ b/sql/migrations/state/0020_atx_weight.sql @@ -1,2 +1,2 @@ ALTER TABLE atxs ADD COLUMN weight INTEGER; -INSERT INTO atxs (weight) SELECT effective_num_units * tick_count FROM atxs; +UPDATE atxs SET weight = effective_num_units * tick_count; From cdda582d83deac0b4a4f0603e40a11bcca1c70a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 28 Jun 2024 10:00:20 +0200 Subject: [PATCH 09/25] review feedback --- activation/e2e/atx_merge_test.go | 10 +++++----- activation/handler_v2.go | 6 +++--- sql/identities/identities.go | 15 +++++++-------- sql/identities/identities_test.go | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index 86c74fa2ed..d8d1b22892 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -3,7 +3,6 @@ package activation_test import ( "context" "encoding/hex" - "fmt" "net/url" "slices" "testing" @@ -138,6 +137,7 @@ func createSoloAtx(publish types.EpochID, prev, pos types.ATXID, nipost *nipost. } func createMerged( + t testing.TB, niposts []nipostData, publish types.EpochID, marriage, positioning types.ATXID, @@ -159,9 +159,7 @@ func createMerged( // Append PoSTs for all IDs for i, nipost := range niposts { idx := slices.IndexFunc(previous, func(a types.ATXID) bool { return a == nipost.previous }) - if idx == -1 { - panic(fmt.Sprintf("previous ATX %s not found in %s", nipost.previous, previous)) - } + require.NotEqual(t, -1, idx) atx.NiPosts[0].Posts = append(atx.NiPosts[0].Posts, wire.SubPostV2{ MarriageIndex: uint32(i), PrevATXIndex: uint32(idx), @@ -405,7 +403,7 @@ func Test_MarryAndMerge(t *testing.T) { marriage, idx, err := identities.MarriageInfo(db, signer.NodeID()) require.NoError(t, err) require.NotNil(t, marriage) - require.Equal(t, marriageATX.ID(), *marriage) + require.Equal(t, marriageATX.ID(), marriage) require.Equal(t, i, idx) } @@ -438,6 +436,7 @@ func Test_MarryAndMerge(t *testing.T) { membershipProof := constructMerkleProof(t, members, map[uint64]bool{0: true, 1: true}) mergedATX := createMerged( + t, niposts[:], publish, marriageATX.ID(), @@ -490,6 +489,7 @@ func Test_MarryAndMerge(t *testing.T) { membershipProof = constructMerkleProof(t, members, map[uint64]bool{0: true}) mergedATX2 := createMerged( + t, niposts[:], publish, marriageATX.ID(), diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 72b46d7a49..2f65814c1c 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -469,14 +469,14 @@ func (h *HandlerV2) equivocationSet(atx *wire.ActivationTxV2) ([]types.NodeID, e } marriageAtxID, _, err := identities.MarriageInfo(h.cdb, atx.SmesherID) switch { - case errors.Is(err, sql.ErrNotFound) || marriageAtxID == nil: + case errors.Is(err, sql.ErrNotFound): return nil, errors.New("smesher is not married") case err != nil: return nil, fmt.Errorf("fetching smesher's marriage atx ID: %w", err) } - if *atx.MarriageATX != *marriageAtxID { - return nil, fmt.Errorf("smesher's marriage ATX ID mismatch: %s != %s", *atx.MarriageATX, *marriageAtxID) + if *atx.MarriageATX != marriageAtxID { + return nil, fmt.Errorf("smesher's marriage ATX ID mismatch: %s != %s", *atx.MarriageATX, marriageAtxID) } marriageAtx, err := atxs.Get(h.cdb, *atx.MarriageATX) diff --git a/sql/identities/identities.go b/sql/identities/identities.go index 90327eb203..c610724017 100644 --- a/sql/identities/identities.go +++ b/sql/identities/identities.go @@ -35,8 +35,9 @@ func SetMalicious(db sql.Executor, nodeID types.NodeID, proof []byte, received t func IsMalicious(db sql.Executor, nodeID types.NodeID) (bool, error) { rows, err := db.Exec(` SELECT 1 FROM identities - WHERE (marriage_atx = ( - SELECT marriage_atx FROM identities WHERE pubkey = ?1 AND marriage_atx IS NOT NULL) AND proof IS NOT NULL + WHERE ( + marriage_atx = (SELECT marriage_atx FROM identities WHERE pubkey = ?1 AND marriage_atx IS NOT NULL) + AND proof IS NOT NULL ) OR (pubkey = ?1 AND marriage_atx IS NULL AND proof IS NOT NULL);`, func(stmt *sql.Statement) { @@ -146,9 +147,9 @@ func Married(db sql.Executor, id types.NodeID) (bool, error) { } // MarriageInfo obtains the marriage ATX and index for given ID. -func MarriageInfo(db sql.Executor, id types.NodeID) (*types.ATXID, int, error) { +func MarriageInfo(db sql.Executor, id types.NodeID) (types.ATXID, int, error) { var ( - atx *types.ATXID + atx types.ATXID index int ) rows, err := db.Exec("select marriage_atx, marriage_idx from identities where pubkey = ?1;", @@ -156,18 +157,16 @@ func MarriageInfo(db sql.Executor, id types.NodeID) (*types.ATXID, int, error) { stmt.BindBytes(1, id.Bytes()) }, func(stmt *sql.Statement) bool { if stmt.ColumnType(0) != sqlite.SQLITE_NULL { - atx = new(types.ATXID) stmt.ColumnBytes(0, atx[:]) - index = int(stmt.ColumnInt64(1)) } return false }) if err != nil { - return nil, 0, fmt.Errorf("getting marriage ATX for %v: %w", id, err) + return atx, 0, fmt.Errorf("getting marriage ATX for %v: %w", id, err) } if rows == 0 { - return nil, 0, sql.ErrNotFound + return atx, 0, sql.ErrNotFound } return atx, index, nil } diff --git a/sql/identities/identities_test.go b/sql/identities/identities_test.go index a8b77cd71f..9b3be5e8e1 100644 --- a/sql/identities/identities_test.go +++ b/sql/identities/identities_test.go @@ -176,7 +176,7 @@ func TestMarriageATX(t *testing.T) { require.NoError(t, SetMarriage(db, id, atx, 5)) got, idx, err := MarriageInfo(db, id) require.NoError(t, err) - require.Equal(t, atx, *got) + require.Equal(t, atx, got) require.Equal(t, 5, idx) }) } From ee1d0bc528fe0a5ed2e6547db29e4811ac36d728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Mon, 1 Jul 2024 11:43:11 +0200 Subject: [PATCH 10/25] use fast poet in merged-atx e2e test --- activation/e2e/atx_merge_test.go | 43 ++++++++++---------------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index d8d1b22892..b061460933 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -3,16 +3,13 @@ package activation_test import ( "context" "encoding/hex" - "net/url" "slices" "testing" "time" "github.com/libp2p/go-libp2p/core/peer" "github.com/spacemeshos/merkle-tree" - "github.com/spacemeshos/poet/registration" "github.com/spacemeshos/poet/shared" - "github.com/spacemeshos/post/verifying" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -21,6 +18,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/activation" + ae2e "github.com/spacemeshos/go-spacemesh/activation/e2e" "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/api/grpcserver" "github.com/spacemeshos/go-spacemesh/atxsdata" @@ -259,34 +257,16 @@ func Test_MarryAndMerge(t *testing.T) { require.NoError(t, eg.Wait()) // ensure that genesis aligns with layer timings - genesis := time.Now().Add(layerDuration).Round(layerDuration) - layerDuration := 2 * time.Second + genesis := time.Now().Round(layerDuration) epoch := layersPerEpoch * layerDuration poetCfg := activation.PoetConfig{ - PhaseShift: epoch, - CycleGap: epoch / 2, - GracePeriod: epoch / 5, - RequestTimeout: epoch / 5, - RequestRetryDelay: epoch / 50, - MaxRequestRetries: 10, + PhaseShift: epoch, + CycleGap: epoch / 2, + GracePeriod: epoch / 5, } - pubkey, address := spawnTestCertifier(t, cfg, nil, verifying.WithLabelScryptParams(opts.Scrypt)) - certClient := activation.NewCertifierClient(db, localDB, logger.Named("certifier")) - certifier := activation.NewCertifier(localDB, logger, certClient) - poet := spawnPoet( - t, - WithGenesis(genesis), - WithEpochDuration(epoch), - WithPhaseShift(poetCfg.PhaseShift), - WithCycleGap(poetCfg.CycleGap), - WithCertifier(®istration.CertifierConfig{ - URL: (&url.URL{Scheme: "http", Host: address.String()}).String(), - PubKey: registration.Base64Enc(pubkey), - }), - ) - poetClient, err := poet.Client(poetDb, poetCfg, logger, activation.WithCertifier(certifier)) - require.NoError(t, err) + client := ae2e.NewTestPoetClient(2) + poetSvc := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) clock, err := timesync.NewClock( timesync.WithGenesisTime(genesis), @@ -304,7 +284,7 @@ func Test_MarryAndMerge(t *testing.T) { poetCfg, clock, validator, - activation.WithPoetClients(poetClient), + activation.WithPoetServices(poetSvc), ) require.NoError(t, err) @@ -431,7 +411,7 @@ func Test_MarryAndMerge(t *testing.T) { require.NoError(t, eg.Wait()) // 2.3 Construct a multi-ID poet membership merkle proof for both IDs - poetProof, members, err := poetClient.Proof(context.Background(), "1") + poetProof, members, err := poetSvc.Proof(context.Background(), "1") require.NoError(t, err) membershipProof := constructMerkleProof(t, members, map[uint64]bool{0: true, 1: true}) @@ -449,6 +429,7 @@ func Test_MarryAndMerge(t *testing.T) { mergedATX.Sign(mainID) // 2.4 Publish + <-clock.AwaitLayer(mergedATX.PublishEpoch.FirstLayer()) logger.Info("publishing merged ATX", zap.Inline(mergedATX)) mFetch.EXPECT().RegisterPeerHashes(peer.ID(""), gomock.Any()) @@ -484,7 +465,7 @@ func Test_MarryAndMerge(t *testing.T) { }) } require.NoError(t, eg.Wait()) - poetProof, members, err = poetClient.Proof(context.Background(), "2") + poetProof, members, err = poetSvc.Proof(context.Background(), "2") require.NoError(t, err) membershipProof = constructMerkleProof(t, members, map[uint64]bool{0: true}) @@ -501,6 +482,7 @@ func Test_MarryAndMerge(t *testing.T) { mergedATX2.VRFNonce = nonces[1] mergedATX2.Sign(signers[1]) + <-clock.AwaitLayer(mergedATX2.PublishEpoch.FirstLayer()) logger.Info("publishing second merged ATX", zap.Inline(mergedATX2)) mFetch.EXPECT().RegisterPeerHashes(peer.ID(""), gomock.Any()) mFetch.EXPECT().GetPoetProof(gomock.Any(), gomock.Any()) @@ -534,6 +516,7 @@ func Test_MarryAndMerge(t *testing.T) { } require.NoError(t, eg.Wait()) + <-clock.AwaitLayer(publish.FirstLayer()) for i, signer := range signers { atx := createSoloAtx(publish, mergedATX2.ID(), mergedATX2.ID(), niposts[i].NIPostState) atx.Sign(signer) From 68e191cbc99a0a9811d4880b19d4be29bdc70310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 26 Jun 2024 10:09:30 +0200 Subject: [PATCH 11/25] Persist actual PoST units of every ATX member in DB across a checkpoint --- activation/handler.go | 2 - activation/handler_v1.go | 54 +- activation/handler_v2.go | 121 +-- activation/handler_v2_test.go | 211 +--- api/grpcserver/admin_service_test.go | 1 + checkpoint/checkpointdata.json | 1304 ++++++++++++++++++----- checkpoint/recovery.go | 1 + checkpoint/recovery_test.go | 1 + checkpoint/runner.go | 1 + checkpoint/runner_test.go | 9 +- common/types/checkpoint.go | 5 +- common/types/nodeid.go | 2 +- sql/atxs/atxs.go | 66 +- sql/atxs/atxs_test.go | 39 + sql/migrations/state/0021_atx_posts.sql | 12 + 15 files changed, 1245 insertions(+), 584 deletions(-) create mode 100644 sql/migrations/state/0021_atx_posts.sql diff --git a/activation/handler.go b/activation/handler.go index f79daa678f..71f11ff72c 100644 --- a/activation/handler.go +++ b/activation/handler.go @@ -237,8 +237,6 @@ func (h *Handler) determineVersion(msg []byte) (*types.AtxVersion, error) { type opaqueAtx interface { ID() types.ATXID - Published() types.EpochID - TotalNumUnits() uint32 } func (h *Handler) decodeATX(msg []byte) (opaqueAtx, error) { diff --git a/activation/handler_v1.go b/activation/handler_v1.go index 0126ee9832..30c994752a 100644 --- a/activation/handler_v1.go +++ b/activation/handler_v1.go @@ -168,47 +168,6 @@ func (h *HandlerV1) commitment(atx *wire.ActivationTxV1) (types.ATXID, error) { return atxs.CommitmentATX(h.cdb, atx.SmesherID) } -// Obtain the previous ATX for the given ATX. -// We need to decode it from the blob because we are interested in the true NumUnits value -// that was declared by the previous ATX and the `atxs` table only holds the effective NumUnits. -// However, in case of a golden ATX, the blob is not available and we fallback to fetching the ATX from the DB -// to use the effective num units. -func (h *HandlerV1) previous(ctx context.Context, atx *wire.ActivationTxV1) (*types.ActivationTx, error) { - var blob sql.Blob - v, err := atxs.LoadBlob(ctx, h.cdb, atx.PrevATXID[:], &blob) - if err != nil { - return nil, err - } - - if len(blob.Bytes) == 0 { - // An empty blob indicates a golden ATX (after a checkpoint-recovery). - // Fallback to fetching it from the DB to get the effective NumUnits. - atx, err := atxs.Get(h.cdb, atx.PrevATXID) - if err != nil { - return nil, fmt.Errorf("fetching golden previous atx: %w", err) - } - return atx, nil - } - if v != types.AtxV1 { - return nil, fmt.Errorf("previous atx %s is not of version 1", atx.PrevATXID) - } - - var prev wire.ActivationTxV1 - if err := codec.Decode(blob.Bytes, &prev); err != nil { - return nil, fmt.Errorf("decoding previous atx: %w", err) - } - prev.SetID(atx.PrevATXID) - if prev.VRFNonce == nil { - nonce, err := atxs.NonceByID(h.cdb, prev.ID()) - if err != nil { - return nil, fmt.Errorf("failed to get nonce of previous ATX %s: %w", prev.ID(), err) - } - prev.VRFNonce = (*uint64)(&nonce) - } - - return wire.ActivationTxFromWireV1(&prev, blob.Bytes...), nil -} - func (h *HandlerV1) syntacticallyValidateDeps( ctx context.Context, atx *wire.ActivationTxV1, @@ -224,14 +183,18 @@ func (h *HandlerV1) syntacticallyValidateDeps( } effectiveNumUnits = atx.NumUnits } else { - previous, err := h.previous(ctx, atx) + previous, err := atxs.Get(h.cdb, atx.PrevATXID) if err != nil { return 0, 0, nil, fmt.Errorf("fetching previous atx %s: %w", atx.PrevATXID, err) } if err := h.validateNonInitialAtx(ctx, atx, previous, commitmentATX); err != nil { return 0, 0, nil, err } - effectiveNumUnits = min(previous.NumUnits, atx.NumUnits) + prevUnits, err := atxs.Units(h.cdb, atx.PrevATXID, atx.SmesherID) + if err != nil { + return 0, 0, nil, fmt.Errorf("fetching previous atx units: %w", err) + } + effectiveNumUnits = min(prevUnits, atx.NumUnits) } err = h.nipostValidator.PositioningAtx(atx.PositioningATXID, h.cdb, h.goldenATXID, atx.PublishEpoch) @@ -591,6 +554,11 @@ func (h *HandlerV1) storeAtx( if err != nil && !errors.Is(err, sql.ErrObjectExists) { return fmt.Errorf("add atx to db: %w", err) } + err = atxs.SetUnits(tx, atx.ID(), map[types.NodeID]uint32{atx.SmesherID: watx.NumUnits}) + if err != nil && !errors.Is(err, sql.ErrObjectExists) { + return fmt.Errorf("set atx units: %w", err) + } + return nil }); err != nil { return nil, fmt.Errorf("store atx: %w", err) diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 2f65814c1c..20c891aeb5 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -131,6 +131,7 @@ func (h *HandlerV2) processATX( SmesherID: watx.SmesherID, AtxBlob: types.AtxBlob{Blob: blob, Version: types.AtxV2}, } + if watx.Initial == nil { // FIXME: update to keep many previous ATXs to support merged ATXs atx.PrevATXID = watx.PreviousATXs[0] @@ -144,7 +145,7 @@ func (h *HandlerV2) processATX( atx.SetID(watx.ID()) atx.SetReceived(received) - proof, err = h.storeAtx(ctx, atx, watx, marrying) + proof, err = h.storeAtx(ctx, atx, watx, marrying, parts.units) if err != nil { return nil, fmt.Errorf("cannot store atx %s: %w", atx.ShortString(), err) } @@ -313,86 +314,22 @@ func (h *HandlerV2) collectAtxDeps(atx *wire.ActivationTxV2) ([]types.Hash32, [] return maps.Keys(poetRefs), maps.Keys(filtered) } -func (h *HandlerV2) previous(ctx context.Context, id types.ATXID) (opaqueAtx, error) { - var blob sql.Blob - version, err := atxs.LoadBlob(ctx, h.cdb, id[:], &blob) - if err != nil { - return nil, err - } - - if len(blob.Bytes) == 0 { - // An empty blob indicates a golden ATX (after a checkpoint-recovery). - // Fallback to fetching it from the DB to get the effective NumUnits. - atx, err := atxs.Get(h.cdb, id) - if err != nil { - return nil, fmt.Errorf("fetching golden previous atx: %w", err) - } - return atx, nil - } - - switch version { - case types.AtxV1: - var prev wire.ActivationTxV1 - if err := codec.Decode(blob.Bytes, &prev); err != nil { - return nil, fmt.Errorf("decoding previous atx v1: %w", err) - } - return &prev, nil - case types.AtxV2: - var prev wire.ActivationTxV2 - if err := codec.Decode(blob.Bytes, &prev); err != nil { - return nil, fmt.Errorf("decoding previous atx v2: %w", err) - } - return &prev, nil - } - return nil, fmt.Errorf("unexpected previous ATX version: %d", version) -} - // Validate the previous ATX for the given PoST and return the effective numunits. -func (h *HandlerV2) validatePreviousAtx(id types.NodeID, post *wire.SubPostV2, prevAtxs []opaqueAtx) (uint32, error) { +func (h *HandlerV2) validatePreviousAtx( + id types.NodeID, + post *wire.SubPostV2, + prevAtxs []*types.ActivationTx, +) (uint32, error) { if post.PrevATXIndex >= uint32(len(prevAtxs)) { return 0, fmt.Errorf("prevATXIndex out of bounds: %d > %d", post.PrevATXIndex, len(prevAtxs)) } prev := prevAtxs[post.PrevATXIndex] - - switch prev := prev.(type) { - case *types.ActivationTx: - // A golden ATX - // TODO: support merged golden ATX - if prev.SmesherID != id { - return 0, fmt.Errorf("prev golden ATX has different owner: %s (expected %s)", prev.SmesherID, id) - } - return min(prev.NumUnits, post.NumUnits), nil - - case *wire.ActivationTxV1: - if prev.SmesherID != id { - return 0, fmt.Errorf("prev ATX V1 has different owner: %s (expected %s)", prev.SmesherID, id) - } - return min(prev.NumUnits, post.NumUnits), nil - case *wire.ActivationTxV2: - if prev.MarriageATX != nil { - // Previous is a merged ATX - // need to find out if the given ID was present in the previous ATX - _, idx, err := identities.MarriageInfo(h.cdb, id) - if err != nil { - return 0, fmt.Errorf("fetching marriage info for ID %s: %w", id, err) - } - for _, nipost := range prev.NiPosts { - for _, post := range nipost.Posts { - if post.MarriageIndex == uint32(idx) { - return min(post.NumUnits, post.NumUnits), nil - } - } - } - } else { - // Previous is a solo ATX - if prev.SmesherID == id { - return min(prev.NiPosts[0].Posts[0].NumUnits, post.NumUnits), nil - } - } - - return 0, fmt.Errorf("previous ATX V2 doesn't contain %s", id) + prevUnits, err := atxs.Units(h.cdb, prev.ID(), id) + if err != nil { + return 0, fmt.Errorf("fetching previous atx %s units for ID %s: %w", prev.ID(), id, err) } - return 0, fmt.Errorf("unexpected previous ATX type: %T", prev) + + return min(prevUnits, post.NumUnits), nil } func (h *HandlerV2) validateCommitmentAtx(golden, commitmentAtxId types.ATXID, publish types.EpochID) error { @@ -498,6 +435,7 @@ type atxParts struct { ticks uint64 weight uint64 effectiveUnits uint32 + units map[types.NodeID]uint32 } type nipostSize struct { @@ -556,23 +494,26 @@ func (h *HandlerV2) syntacticallyValidateDeps( ctx context.Context, atx *wire.ActivationTxV2, ) (*atxParts, *mwire.MalfeasanceProof, error) { + parts := atxParts{ + units: make(map[types.NodeID]uint32), + } if atx.Initial != nil { if err := h.validateCommitmentAtx(h.goldenATXID, atx.Initial.CommitmentATX, atx.PublishEpoch); err != nil { return nil, nil, fmt.Errorf("verifying commitment ATX: %w", err) } } - previousAtxs := make([]opaqueAtx, len(atx.PreviousATXs)) + prevAtxs := make([]*types.ActivationTx, len(atx.PreviousATXs)) for i, prev := range atx.PreviousATXs { - prevAtx, err := h.previous(ctx, prev) + prevAtx, err := atxs.Get(h.cdb, prev) if err != nil { return nil, nil, fmt.Errorf("fetching previous atx: %w", err) } - if prevAtx.Published() >= atx.PublishEpoch { - err := fmt.Errorf("previous atx is too new (%d >= %d) (%s) ", prevAtx.Published(), atx.PublishEpoch, prev) + if prevAtx.PublishEpoch >= atx.PublishEpoch { + err := fmt.Errorf("previous atx is too new (%d >= %d) (%s) ", prevAtx.PublishEpoch, atx.PublishEpoch, prev) return nil, nil, err } - previousAtxs[i] = prevAtx + prevAtxs[i] = prevAtx } equivocationSet, err := h.equivocationSet(atx) @@ -594,7 +535,7 @@ func (h *HandlerV2) syntacticallyValidateDeps( effectiveNumUnits := post.NumUnits if atx.Initial == nil { var err error - effectiveNumUnits, err = h.validatePreviousAtx(id, &post, previousAtxs) + effectiveNumUnits, err = h.validatePreviousAtx(id, &post, prevAtxs) if err != nil { return nil, nil, fmt.Errorf("validating previous atx: %w", err) } @@ -644,7 +585,7 @@ func (h *HandlerV2) syntacticallyValidateDeps( nipostSizes[i].ticks = leaves / h.tickSize } - totalEffectiveNumUnits, totalWeight, err := nipostSizes.sumUp() + parts.effectiveUnits, parts.weight, err = nipostSizes.sumUp() if err != nil { return nil, nil, err } @@ -689,15 +630,10 @@ func (h *HandlerV2) syntacticallyValidateDeps( if err != nil { return nil, nil, fmt.Errorf("invalid post for ID %s: %w", id, err) } + parts.units[id] = post.NumUnits } } - parts := &atxParts{ - ticks: nipostSizes.minTicks(), - effectiveUnits: totalEffectiveNumUnits, - weight: totalWeight, - } - if atx.Initial == nil { if smesherCommitment == nil { return nil, nil, errors.New("ATX signer not present in merged ATX") @@ -708,7 +644,9 @@ func (h *HandlerV2) syntacticallyValidateDeps( } } - return parts, nil, nil + parts.ticks = nipostSizes.minTicks() + + return &parts, nil, nil } func (h *HandlerV2) checkMalicious( @@ -768,6 +706,7 @@ func (h *HandlerV2) storeAtx( atx *types.ActivationTx, watx *wire.ActivationTxV2, marrying []types.NodeID, + units map[types.NodeID]uint32, ) (*mwire.MalfeasanceProof, error) { var ( malicious bool @@ -799,6 +738,10 @@ func (h *HandlerV2) storeAtx( if err != nil && !errors.Is(err, sql.ErrObjectExists) { return fmt.Errorf("add atx to db: %w", err) } + err = atxs.SetUnits(tx, atx.ID(), units) + if err != nil && !errors.Is(err, sql.ErrObjectExists) { + return fmt.Errorf("set atx units: %w", err) + } return nil }); err != nil { return nil, fmt.Errorf("store atx: %w", err) diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 1c84c353fd..8e86c51977 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -3,7 +3,6 @@ package activation import ( "context" "errors" - "fmt" "math" "slices" "testing" @@ -488,38 +487,7 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { require.NoError(t, err) require.Nil(t, proof) }) - t.Run("second ATX, previous V1", func(t *testing.T) { - t.Parallel() - atxHandler := newV2TestHandler(t, golden) - - prev := newInitialATXv1(t, golden) - prev.Sign(sig) - prevAtx := toAtx(t, prev) - atxs.Add(atxHandler.cdb, prevAtx) - - atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), prevAtx.ID()) - atx.Sign(sig) - blob := codec.MustEncode(atx) - atxHandler.expectAtxV2(atx) - - proof, err := atxHandler.processATX(context.Background(), peer, atx, blob, time.Now()) - require.NoError(t, err) - require.Nil(t, proof) - - atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) - require.NoError(t, err) - - require.Nil(t, atxFromDb.CommitmentATX) - - require.Equal(t, atx.Coinbase, atxFromDb.Coinbase) - require.EqualValues(t, atx.VRFNonce, atxFromDb.VRFNonce) - require.EqualValues(t, poetLeaves/tickSize, atxFromDb.TickCount) - require.EqualValues(t, prevAtx.TickHeight(), atxFromDb.BaseTickHeight) - require.EqualValues(t, prevAtx.TickHeight()+atxFromDb.TickCount, atxFromDb.TickHeight()) - require.Equal(t, atx.NiPosts[0].Posts[0].NumUnits, atxFromDb.NumUnits) - require.EqualValues(t, atx.NiPosts[0].Posts[0].NumUnits*poetLeaves/tickSize, atxFromDb.Weight) - }) - t.Run("second ATX, previous V2", func(t *testing.T) { + t.Run("second ATX", func(t *testing.T) { t.Parallel() atxHandler := newV2TestHandler(t, golden) @@ -551,8 +519,9 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { prev := atxs.CheckpointAtx{ ID: types.RandomATXID(), CommitmentATX: types.RandomATXID(), - NumUnits: 100, SmesherID: sig.NodeID(), + NumUnits: 100, + Units: map[types.NodeID]uint32{sig.NodeID(): 100}, } require.NoError(t, atxs.AddCheckpointed(atxHandler.cdb, &prev)) @@ -561,8 +530,12 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atxHandler.expectAtxV2(atx) _, err := atxHandler.processATX(context.Background(), peer, atx, codec.MustEncode(atx), time.Now()) require.NoError(t, err) + + atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) + require.NoError(t, err) + require.Equal(t, atx.TotalNumUnits(), atxFromDb.TotalNumUnits()) }) - t.Run("second ATX, previous V2, increases space (no nonce, previous valid)", func(t *testing.T) { + t.Run("second ATX, increases space (nonce valid)", func(t *testing.T) { t.Parallel() atxHandler := newV2TestHandler(t, golden) prev := newInitialATXv2(t, golden) @@ -575,6 +548,7 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), golden) atx.NiPosts[0].Posts[0].NumUnits *= 10 + atx.VRFNonce = 7779989 atx.Sign(sig) atxHandler.expectAtxV2(atx) @@ -582,12 +556,11 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { require.NoError(t, err) require.Nil(t, proof) - // picks the VRF nonce from the previous ATX atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) require.NoError(t, err) - require.EqualValues(t, prev.VRFNonce, atxFromDb.VRFNonce) + require.EqualValues(t, atx.VRFNonce, atxFromDb.VRFNonce) }) - t.Run("second ATX, previous V2, increases space (no nonce, previous invalid)", func(t *testing.T) { + t.Run("second ATX, increases space (nonce invalid)", func(t *testing.T) { t.Parallel() atxHandler := newV2TestHandler(t, golden) prev := newInitialATXv2(t, golden) @@ -600,6 +573,7 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), golden) atx.NiPosts[0].Posts[0].NumUnits *= 10 + atx.VRFNonce = 7779989 atx.Sign(sig) atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) atxHandler.expectFetchDeps(atx) @@ -607,7 +581,7 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atxHandler.mValidator.EXPECT().VRFNonceV2( sig.NodeID(), prev.Initial.CommitmentATX, - prev.VRFNonce, + atx.VRFNonce, atx.TotalNumUnits(), ).Return(errors.New("vrf nonce is not valid")) @@ -619,19 +593,13 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { }) t.Run("second ATX, increases space (new nonce)", func(t *testing.T) { t.Parallel() - lowerNumUnits := uint32(10) atxHandler := newV2TestHandler(t, golden) - prev := &types.ActivationTx{ - NumUnits: lowerNumUnits, - SmesherID: sig.NodeID(), - CommitmentATX: &golden, - } - prev.SetID(types.RandomATXID()) - require.NoError(t, atxs.Add(atxHandler.cdb, prev)) + prev := atxHandler.createAndProcessInitial(t, sig) + lowerNumUnits := prev.TotalNumUnits() atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), golden) atx.VRFNonce = uint64(123) - atx.NiPosts[0].Posts[0].NumUnits *= 10 + atx.NiPosts[0].Posts[0].NumUnits = lowerNumUnits * 10 atx.Sign(sig) atxHandler.expectAtxV2(atx) @@ -915,7 +883,7 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) atxHandler.expectFetchDeps(merged) p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) - require.ErrorContains(t, err, fmt.Sprintf("previous ATX V2 doesn't contain %s", otherATXs[0].SmesherID)) + require.Error(t, err) require.Nil(t, p) }) } @@ -1258,60 +1226,6 @@ func Test_ValidateMarriages(t *testing.T) { }) } -func Test_LoadPreviousATX(t *testing.T) { - t.Parallel() - t.Run("not found", func(t *testing.T) { - t.Parallel() - atxHandler := newV2TestHandler(t, types.RandomATXID()) - _, err := atxHandler.previous(context.Background(), types.RandomATXID()) - require.ErrorContains(t, err, "not found") - }) - t.Run("golden not found", func(t *testing.T) { - t.Parallel() - atxHandler := newV2TestHandler(t, types.RandomATXID()) - golden := &types.ActivationTx{} - golden.SetID(types.RandomATXID()) - _, err := atxHandler.previous(context.Background(), golden.ID()) - require.ErrorContains(t, err, "not found") - }) - t.Run("golden", func(t *testing.T) { - t.Parallel() - atxHandler := newV2TestHandler(t, types.RandomATXID()) - golden := &types.ActivationTx{} - golden.SetID(types.RandomATXID()) - require.NoError(t, atxs.Add(atxHandler.cdb, golden)) - atx, err := atxHandler.previous(context.Background(), golden.ID()) - require.NoError(t, err) - require.Equal(t, golden.ID(), atx.ID()) - }) - t.Run("v1", func(t *testing.T) { - t.Parallel() - atxHandler := newV2TestHandler(t, types.RandomATXID()) - prevWire := newInitialATXv1(t, types.RandomATXID()) - prev := toAtx(t, prevWire) - require.NoError(t, atxs.Add(atxHandler.cdb, prev)) - atx, err := atxHandler.previous(context.Background(), prev.ID()) - require.NoError(t, err) - require.Equal(t, prev.ID(), atx.ID()) - }) - t.Run("v2", func(t *testing.T) { - t.Parallel() - atxHandler := newV2TestHandler(t, types.RandomATXID()) - prevWire := newInitialATXv2(t, types.RandomATXID()) - prev := &types.ActivationTx{ - AtxBlob: types.AtxBlob{ - Blob: codec.MustEncode(prevWire), - Version: types.AtxV2, - }, - } - prev.SetID(prevWire.ID()) - require.NoError(t, atxs.Add(atxHandler.cdb, prev)) - atx, err := atxHandler.previous(context.Background(), prev.ID()) - require.NoError(t, err) - require.Equal(t, prev.ID(), atx.ID()) - }) -} - func Test_ValidateCommitmentAtx(t *testing.T) { t.Parallel() golden := types.RandomATXID() @@ -1361,68 +1275,45 @@ func Test_ValidatePreviousATX(t *testing.T) { _, err := atxHandler.validatePreviousAtx(types.RandomNodeID(), post, nil) require.ErrorContains(t, err, "out of bounds") }) - t.Run("previous golden, wrong smesher ID", func(t *testing.T) { - t.Parallel() - prev := &types.ActivationTx{SmesherID: types.RandomNodeID()} - _, err := atxHandler.validatePreviousAtx(types.RandomNodeID(), &wire.SubPostV2{}, []opaqueAtx{prev}) - require.ErrorContains(t, err, "prev golden ATX has different owner") - }) - t.Run("previous V1, wrong smesher ID", func(t *testing.T) { - t.Parallel() - prev := newInitialATXv1(t, golden) - prev.SmesherID = types.RandomNodeID() - _, err := atxHandler.validatePreviousAtx(types.RandomNodeID(), &wire.SubPostV2{}, []opaqueAtx{prev}) - require.ErrorContains(t, err, "prev ATX V1 has different owner") - }) - t.Run("previous V2, wrong smesher ID", func(t *testing.T) { + t.Run("smesher ID not present", func(t *testing.T) { t.Parallel() - prev := newInitialATXv2(t, golden) - prev.SmesherID = types.RandomNodeID() - _, err := atxHandler.validatePreviousAtx(types.RandomNodeID(), &wire.SubPostV2{}, []opaqueAtx{prev}) - require.ErrorContains(t, err, "previous ATX V2 doesn't contain") + prev := &types.ActivationTx{} + prev.SetID(types.RandomATXID()) + require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), map[types.NodeID]uint32{types.RandomNodeID(): 13})) + + _, err := atxHandler.validatePreviousAtx(types.RandomNodeID(), &wire.SubPostV2{}, []*types.ActivationTx{prev}) + require.Error(t, err) }) - t.Run("previous golden, valid", func(t *testing.T) { + t.Run("effective units is min(previous, atx) for given smesher", func(t *testing.T) { t.Parallel() id := types.RandomNodeID() - prev := &types.ActivationTx{ - SmesherID: id, - NumUnits: 20, - } - units, err := atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 100}, []opaqueAtx{prev}) - require.NoError(t, err) - require.Equal(t, uint32(20), units) + other := types.RandomNodeID() + prev := &types.ActivationTx{} + prev.SetID(types.RandomATXID()) + require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), map[types.NodeID]uint32{id: 7, other: 13})) - units, err = atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 10}, []opaqueAtx{prev}) + units, err := atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 100}, []*types.ActivationTx{prev}) require.NoError(t, err) - require.Equal(t, uint32(10), units) - }) - t.Run("previous V1, valid", func(t *testing.T) { - t.Parallel() - id := types.RandomNodeID() - prev := newInitialATXv1(t, golden) - prev.SmesherID = id - prev.NumUnits = 20 - units, err := atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 100}, []opaqueAtx{prev}) + require.EqualValues(t, 7, units) + + units, err = atxHandler.validatePreviousAtx(other, &wire.SubPostV2{NumUnits: 100}, []*types.ActivationTx{prev}) require.NoError(t, err) - require.Equal(t, uint32(20), units) + require.EqualValues(t, 13, units) - units, err = atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 10}, []opaqueAtx{prev}) + units, err = atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 2}, []*types.ActivationTx{prev}) require.NoError(t, err) - require.Equal(t, uint32(10), units) + require.EqualValues(t, 2, units) }) - t.Run("previous V2, valid - owner is same ID", func(t *testing.T) { + t.Run("previous merged, doesn't contain ID", func(t *testing.T) { t.Parallel() id := types.RandomNodeID() - prev := newInitialATXv2(t, golden) - prev.SmesherID = id - prev.NiPosts[0].Posts[0].NumUnits = 20 - units, err := atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 100}, []opaqueAtx{prev}) - require.NoError(t, err) - require.Equal(t, uint32(20), units) + other := types.RandomNodeID() + prev := &types.ActivationTx{} + prev.SetID(types.RandomATXID()) + require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), map[types.NodeID]uint32{other: 13})) - units, err = atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 10}, []opaqueAtx{prev}) - require.NoError(t, err) - require.Equal(t, uint32(10), units) + _, err := atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 100}, []*types.ActivationTx{prev}) + require.Error(t, err) }) } @@ -1450,15 +1341,13 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { atx.Sign(sig) _, proof, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) - require.ErrorContains(t, err, "fetching previous atx: database: not found") + require.ErrorContains(t, err, "fetching previous atx") require.Nil(t, proof) }) t.Run("previous ATX too new", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) - prev := &types.ActivationTx{} - prev.SetID(types.RandomATXID()) - require.NoError(t, atxs.Add(atxHandler.cdb, prev)) + prev := atxHandler.createAndProcessInitial(t, sig) atx := newSoloATXv2(t, 0, prev.ID(), golden) atx.Sign(sig) @@ -1470,17 +1359,15 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { t.Run("previous ATX by different smesher", func(t *testing.T) { atxHandler := newV2TestHandler(t, golden) - prev := &types.ActivationTx{ - SmesherID: types.RandomNodeID(), - } - prev.SetID(types.RandomATXID()) - require.NoError(t, atxs.Add(atxHandler.cdb, prev)) + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + prev := atxHandler.createAndProcessInitial(t, otherSig) atx := newSoloATXv2(t, 2, prev.ID(), golden) atx.Sign(sig) _, proof, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) - require.ErrorContains(t, err, "has different owner") + require.Error(t, err) require.Nil(t, proof) }) t.Run("invalid PoST", func(t *testing.T) { diff --git a/api/grpcserver/admin_service_test.go b/api/grpcserver/admin_service_test.go index 3be5b22351..641a5a6b07 100644 --- a/api/grpcserver/admin_service_test.go +++ b/api/grpcserver/admin_service_test.go @@ -38,6 +38,7 @@ func newAtx(tb testing.TB, db *sql.Database) { atx.SmesherID = types.BytesToNodeID(types.RandomBytes(20)) atx.SetReceived(time.Now().Local()) require.NoError(tb, atxs.Add(db, atx)) + require.NoError(tb, atxs.SetUnits(db, atx.ID(), map[types.NodeID]uint32{atx.SmesherID: atx.NumUnits})) } func createMesh(tb testing.TB, db *sql.Database) { diff --git a/checkpoint/checkpointdata.json b/checkpoint/checkpointdata.json index 7ae778cc74..a2082ae276 100644 --- a/checkpoint/checkpointdata.json +++ b/checkpoint/checkpointdata.json @@ -1,421 +1,1157 @@ { - "command": "grpcurl -plaintext -d '{\"snapshot_layer\":15,\"num_atxs\":2}' 0.0.0.0:9093 spacemesh.v1.AdminService.CheckpointStream", + "command": "grpcurl -plaintext -d '{\"snapshot_layer\":1152,\"num_atxs\":2}' 0.0.0.0:9093 spacemesh.v1.AdminService.CheckpointStream", "version": "https://spacemesh.io/checkpoint.schema.json.1.0", "data": { - "id": "snapshot-15", + "id": "snapshot-1152", "atxs": [ { - "id": "mORyeMH1is/StnCnMPKImPdOsUBIKge5H/gfn/C32fQ=", + "id": "u8oX7y0gk80G3d5Omcs5f8KcooWZT8wxSV5ZWyxTzgU=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 6637, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "ACC97STWCRc+fWqHI0wJub1eOJ8BrBY6gA67kDGPm6Y=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "ACC97STWCRc+fWqHI0wJub1eOJ8BrBY6gA67kDGPm6Y=": 33 + } + }, + { + "id": "st5me/GMizi7orCtvY5EHAeaRiL1NQt2Xk3uIyFezXQ=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 114, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "AjDF111CuE+YgA7OtHvJzE2AMFiQClA0agn/YdVrZYI=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 6637, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "ACC97STWCRc+fWqHI0wJub1eOJ8BrBY6gA67kDGPm6Y=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "ACC97STWCRc+fWqHI0wJub1eOJ8BrBY6gA67kDGPm6Y=": 33 + } + }, + { + "id": "4O/GjoomBlNJawBIT5fPI+/h0ngDWHJFtKLhFgin4p8=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 21089, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "AUv/xfD3TEfFuh4fAUv1w3SEvxu8j2nRcaLThno0AW8=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "AUv/xfD3TEfFuh4fAUv1w3SEvxu8j2nRcaLThno0AW8=": 33 + } }, { - "id": "e7QJXNX9Xv/HGaWkXbBh7zJLjDAH05LJbn6ETVkaqJM=", + "id": "X1RItKn2DqhLr2pHiJ6GM+atK7yFzW0K/BuJhcmFTU8=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 118, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "BEFt4bjPnzI3hKjLqtnTqT0DAZARRHkvtTvWZG5fGWo=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 21089, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "AUv/xfD3TEfFuh4fAUv1w3SEvxu8j2nRcaLThno0AW8=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "AUv/xfD3TEfFuh4fAUv1w3SEvxu8j2nRcaLThno0AW8=": 33 + } }, { - "id": "Ucq8mQbeucxEOwwUjljhuoO+zbqJl5rFQF+oSVYiKM4=", + "id": "+0cYFPMex+gSsTwNAtWuYz4WkE3m1KvaGWId9xdjLUs=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 13207, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "AooC1fiukLEwg1MVyMzBvSPAU3fv1ofIKVASTPqjZ2k=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "AooC1fiukLEwg1MVyMzBvSPAU3fv1ofIKVASTPqjZ2k=": 33 + } + }, + { + "id": "wzqJRGHhn3Cn3ua/FUgidDFTo9C05Ch2R4KHFJhlNR0=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 36, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "DWOFdx7D7nm+pb836Ke59MPfPb21ZHLMqPFwsh6r5TA=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 13207, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "AooC1fiukLEwg1MVyMzBvSPAU3fv1ofIKVASTPqjZ2k=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "AooC1fiukLEwg1MVyMzBvSPAU3fv1ofIKVASTPqjZ2k=": 33 + } + }, + { + "id": "oSKEDPcSMxMfVtk+xi0z/NcVplprMO/iu2+fpvPC+qs=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 12343, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "ArEQOhpAwiUNpATNhXPIY3HIHkabho1kASKsNNdKdrE=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "ArEQOhpAwiUNpATNhXPIY3HIHkabho1kASKsNNdKdrE=": 33 + } }, { - "id": "a3E203WsejzivHgXnnTFo5d+wTs4bXlpuk8C7cUtcVI=", + "id": "OJ9XD6fJUQGtsUPnYw7NAN1Yyhj17PzmWEGcM9gc6aA=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 118, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "DmlnvWmNXbU/spggGtx9Eopqmcgj8XxNrLN9YDPMLs4=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 12343, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "ArEQOhpAwiUNpATNhXPIY3HIHkabho1kASKsNNdKdrE=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "ArEQOhpAwiUNpATNhXPIY3HIHkabho1kASKsNNdKdrE=": 33 + } }, { - "id": "7zwIpqALLONL+8vgJDxQE+P9W0ObYGpXzJymFmC1HPM=", + "id": "BQECMwRr+6EkiU4GkbOuC7RC5aFLjcLh4khqQ5r8LAc=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 15243, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "A+FBnUB2mBCTBkZ/E0JuONQ4xEsXKJmvj0ubJjZ/zBU=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "A+FBnUB2mBCTBkZ/E0JuONQ4xEsXKJmvj0ubJjZ/zBU=": 33 + } + }, + { + "id": "lJ/Pl6ZuWhuObUqz77txuOwqL7L/Ip4GSOBh8oKmJkg=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 225, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "GymS9e2sTLJoHAThUEkYG57LpwESILJIqNL7AgGhR3k=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 15243, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "A+FBnUB2mBCTBkZ/E0JuONQ4xEsXKJmvj0ubJjZ/zBU=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "A+FBnUB2mBCTBkZ/E0JuONQ4xEsXKJmvj0ubJjZ/zBU=": 33 + } + }, + { + "id": "9KDG7LaXqdeNr1u0qtHbov1hxNNy+J+zsy+68eHesPY=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 6570, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "A/2tBH5znWAlCDJngpx8t/JZuLyiMANppmocJrYpPkM=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "A/2tBH5znWAlCDJngpx8t/JZuLyiMANppmocJrYpPkM=": 33 + } }, { - "id": "m1OXiG6whx9bSgDFZEwRDzVhjYFlUo1jakPd9gP+ix0=", + "id": "M7ot7AaQ6eBYyWTivSrMFy6zNnYHry5s2It+/pgv9s0=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 28, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "JBASUKxLLO/PeKJQRCk+hObdHINqpRm0k/GfwZsGpoU=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 6570, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "A/2tBH5znWAlCDJngpx8t/JZuLyiMANppmocJrYpPkM=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "A/2tBH5znWAlCDJngpx8t/JZuLyiMANppmocJrYpPkM=": 33 + } + }, + { + "id": "SgQflaI1iTZIqSErreZLz9/Tm2sUPl6DRW4K3WbK5YE=", + "epoch": 1, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 30840, + "baseTickHeight": 0, + "tickCount": 5909, + "publicKey": "BAFb70vXDTH7UXJFIYnz5kL5PUYHK8BlP4zDaVUM2r4=", + "sequence": 0, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "BAFb70vXDTH7UXJFIYnz5kL5PUYHK8BlP4zDaVUM2r4=": 33 + } + }, + { + "id": "YX15FFQSwwMOge1A9KtyvNyn5kIyJiS/UQJmDbm9J0Y=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 22475, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "BRIqQP4O16gY5ddbIfToK0vESEwpcO3Ky0RIb72D3QE=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "BRIqQP4O16gY5ddbIfToK0vESEwpcO3Ky0RIb72D3QE=": 33 + } }, { - "id": "EybL87BLqJPeoOESnohH35v+cwpjKKPIoojCYD1XOyQ=", + "id": "r9zPzkVaV+uZ8M7AToXGrcod09dYEtpyzFgaygtycHA=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 169, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "JLJQBNA9gK3mc4YUNeeV7uLxbl1/kU9/Eb4bx40UBRE=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 22475, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "BRIqQP4O16gY5ddbIfToK0vESEwpcO3Ky0RIb72D3QE=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "BRIqQP4O16gY5ddbIfToK0vESEwpcO3Ky0RIb72D3QE=": 33 + } + }, + { + "id": "VDSd1HWFRa8XZRKMCCNWzNR0j8hau+KkfsCfI/AMhL8=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 39956, + "baseTickHeight": 11821, + "tickCount": 4787, + "publicKey": "Bcl1FXmzkjAUmyac546RFNGqyrtDI7dki9L8nW7rTcs=", + "sequence": 0, + "coinbase": "AAAAAAHAd/lY2IYJI8eC1f9WpvJAJXEY", + "numUnits": 100, + "units": { + "Bcl1FXmzkjAUmyac546RFNGqyrtDI7dki9L8nW7rTcs=": 100 + } + }, + { + "id": "/fZWW/5Nd9Ml3eYPamECbO5xtA3I97UBtam9sqB41S8=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 30668, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "BdGU5/LH7x6kyR8lmYf3o3zyxr+P3ijeE+5gPBSLY/0=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "BdGU5/LH7x6kyR8lmYf3o3zyxr+P3ijeE+5gPBSLY/0=": 33 + } }, { - "id": "+NsAaWCfgNnM14YBaiRr0awmIxFEfSbwQDSJY/GHEMY=", + "id": "F0O6F6OAZLLpsnZMQROAiWyT08TPuIKELrV8y9M23xY=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 162, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "JL5BCzlK9IKVctKhJuSp2H+bP5iwb4/PBXH5enXud+8=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 30668, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "BdGU5/LH7x6kyR8lmYf3o3zyxr+P3ijeE+5gPBSLY/0=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "BdGU5/LH7x6kyR8lmYf3o3zyxr+P3ijeE+5gPBSLY/0=": 33 + } + }, + { + "id": "dLjvOPj0aop/LxSxdNhkcbfD6Z8pKYK2EjJnF+J0EjM=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 3885, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "Bhe+OatEVxQ0TpMcowLWe1ZGA90JMzVfk3XEKwsupT4=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "Bhe+OatEVxQ0TpMcowLWe1ZGA90JMzVfk3XEKwsupT4=": 33 + } }, { - "id": "8/YqDIi87zSct9QRcEKEsF7H4y/FkbDQuAnaHLDkpSI=", + "id": "+pupHbOeRd9JLAxO/QMpXOtQiB8U3Dm33jhd5R4SxK4=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 251, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "Jz2niGSkirbv7nOyGZ3+8heGVbx1q1YdANiljTiCAGQ=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 3885, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "Bhe+OatEVxQ0TpMcowLWe1ZGA90JMzVfk3XEKwsupT4=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "Bhe+OatEVxQ0TpMcowLWe1ZGA90JMzVfk3XEKwsupT4=": 33 + } }, { - "id": "bxGrD1Nsk/bHeuMrmvLjLBmcbaRYyT2M1q/x1RYYpnA=", + "id": "W+frEvfwgB9baZvcqFLuhc97p8OnsNUYbrqL+IQR0hw=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 690, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "BmuZafq227zV95QKGpdWNqfV+lniAndpD7gXfaMtNCg=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "BmuZafq227zV95QKGpdWNqfV+lniAndpD7gXfaMtNCg=": 33 + } + }, + { + "id": "atRH414FjVvScYRMcL1hu3WXEX92hnXoCiwEe39QL28=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 35, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "R1QpJYjIYJSauXfIlWtzPRC/eP2PJMuy+iDEc/wxnFw=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 690, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "BmuZafq227zV95QKGpdWNqfV+lniAndpD7gXfaMtNCg=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "BmuZafq227zV95QKGpdWNqfV+lniAndpD7gXfaMtNCg=": 33 + } + }, + { + "id": "SM7ffYm7hPVjOLxRMyirQm2Pa/JldcQeZY2F7+pth18=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 11475, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "B9JKogxn8v0o9xHuJhsd2tH3eI4npW6nB1fKOpZLwfM=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "B9JKogxn8v0o9xHuJhsd2tH3eI4npW6nB1fKOpZLwfM=": 33 + } }, { - "id": "rIcqVvi+kSuaqF/g47dbYrI9rx993oRsiqkjkvmSuy4=", + "id": "hW2Dij7tm4iyr1WFDfjmiywR5R8BHD8MOo2cWk1tiiw=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 163, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "UWcgflkxKJ2mtXopUjmQzfntrlUFP2Qly70Y+13ALFY=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 11475, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "B9JKogxn8v0o9xHuJhsd2tH3eI4npW6nB1fKOpZLwfM=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "B9JKogxn8v0o9xHuJhsd2tH3eI4npW6nB1fKOpZLwfM=": 33 + } }, { - "id": "OOhu1255ZmVPnesTnJ7/Zo4e7k1FqsBWJ4lIRk9PtDk=", + "id": "D1mUFuc07Wi+JP72xYpdIwaqPiei6klyGTaFEIJCTWA=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 23399, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "CEgz/V3ovWdQTQXGNmP8sfzZo8TwwLkXlABmvBMEwuw=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "CEgz/V3ovWdQTQXGNmP8sfzZo8TwwLkXlABmvBMEwuw=": 33 + } + }, + { + "id": "7Q/RHUg5jd0aZ2tuViK6IWShAEe1YEhC/AxbME6JSZY=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 224, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "XkdK5+lrPxDLpmVgwOklWj+zATbPq355uBvfT5+Imrg=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 23399, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "CEgz/V3ovWdQTQXGNmP8sfzZo8TwwLkXlABmvBMEwuw=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "CEgz/V3ovWdQTQXGNmP8sfzZo8TwwLkXlABmvBMEwuw=": 33 + } + }, + { + "id": "yMaWck6Yvg5xpjxk4OwDmaCai1SboG6U6YNmlE49gQU=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 32241, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "Cob8IKp/r+AviRD6k5aqEhiNL3DB8yl0lXeCRi8bvcE=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "Cob8IKp/r+AviRD6k5aqEhiNL3DB8yl0lXeCRi8bvcE=": 33 + } }, { - "id": "fpDfLfjSZMhB8fSADJpBwKecaz454cW54UO3QyAIATI=", + "id": "PlACWchG//hb6n4u8VvPfpjDriqICwWSBScRJUbLs/g=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 58, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "YTtPPMQkuHKGUzOu7OyuJo1gKgFNsGvuebF41EXfq98=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 32241, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "Cob8IKp/r+AviRD6k5aqEhiNL3DB8yl0lXeCRi8bvcE=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "Cob8IKp/r+AviRD6k5aqEhiNL3DB8yl0lXeCRi8bvcE=": 33 + } }, { - "id": "CxWR7O6gq0PAlkF7sMCKpiFX1m38DPU9o63GqJY1tlI=", + "id": "OT+i7D09UNI51Q6Oan7TxoH6TN7P+54RFVbnD02N8nc=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 18075, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "Co0dYdJWOa/oWQ7sNnFpd0txWWf7X4imqTBBaFTG1NE=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "Co0dYdJWOa/oWQ7sNnFpd0txWWf7X4imqTBBaFTG1NE=": 33 + } + }, + { + "id": "glQtNfR9Bv2f4yRGxwjD+mrLAU+sQRMPPmVLcPJTNEE=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 160, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "dQaH3Xu/EmSOD/YsGwERP/b2WSNwNXP3cEmFODF8OKc=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 18075, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "Co0dYdJWOa/oWQ7sNnFpd0txWWf7X4imqTBBaFTG1NE=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "Co0dYdJWOa/oWQ7sNnFpd0txWWf7X4imqTBBaFTG1NE=": 33 + } + }, + { + "id": "r/b9KmCZoagOu9tgLiHtOrRDNbVKLuSbjGJokP5Wweg=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 24726, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "CpJgf2aYfGvPzj71QNxuUeaQKbGXDh7XJbGiju5toIU=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "CpJgf2aYfGvPzj71QNxuUeaQKbGXDh7XJbGiju5toIU=": 33 + } }, { - "id": "/4mpsMHE/b/7aaiZjercT32tr+DiYG6ZuusfAjiXYL8=", + "id": "89TwNgoRK2oMlazZuIPohlfaO91EHrB492Vy5PcA1/4=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 175, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "d1V8P6y/q8zzcmTL3oNTEcwp1mRvnY44h4/gMTbrUVE=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 24726, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "CpJgf2aYfGvPzj71QNxuUeaQKbGXDh7XJbGiju5toIU=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "CpJgf2aYfGvPzj71QNxuUeaQKbGXDh7XJbGiju5toIU=": 33 + } }, { - "id": "2fVXJwJE1eDCXCwVR+A+Fs6jSkfq9xCG5QHSw+26v1Q=", + "id": "aFvhy5FtqoLN51s+IBC/OyD3Cutj2mXMEsjpdB2zTsI=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 23717, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "Cr02ZM2IWdxToYodz2vXvZQqEIVt8tm9mTbCzv1lPmA=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "Cr02ZM2IWdxToYodz2vXvZQqEIVt8tm9mTbCzv1lPmA=": 33 + } + }, + { + "id": "4VTCYV91Bthl96aPzDWymWtCE9fTpxt9NbHkvWC9IhA=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 66, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "eAg30tcT7v2wAKlImtz9+gGB9RZwOUu/oBRS3CQsDIM=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 23717, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "Cr02ZM2IWdxToYodz2vXvZQqEIVt8tm9mTbCzv1lPmA=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "Cr02ZM2IWdxToYodz2vXvZQqEIVt8tm9mTbCzv1lPmA=": 33 + } + }, + { + "id": "WTP9dTB28LDjXBaHSKmDjRFa2J4ydiYCBBYOgfFDVNg=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 22040, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "C0pHznKOU/MbCW3pfUBlkR7QMmOCIoYPs+KZRPd7k/k=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "C0pHznKOU/MbCW3pfUBlkR7QMmOCIoYPs+KZRPd7k/k=": 33 + } }, { - "id": "pSCDIQkwMJw4lNWmLYoX7AxrFaln42Gx3bl+zWEBJDw=", + "id": "gI7iRDf7Ma0d21dwgkBfy9CRMlJGVJR0lXTSLCsuM1U=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 63, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "gGO0GpK9K+/jK91DJBJtDz0dAFgeVrvi7CeneExQeW8=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 22040, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "C0pHznKOU/MbCW3pfUBlkR7QMmOCIoYPs+KZRPd7k/k=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "C0pHznKOU/MbCW3pfUBlkR7QMmOCIoYPs+KZRPd7k/k=": 33 + } }, { - "id": "DNW3RHxjCwbXtcJqvZ2myYQusfI4NPQM+K1oufhkOJA=", + "id": "kQrjQRsN3rNRr+hXz795Ift4JEUQo2bjHOZCP6FreKs=", + "epoch": 1, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 15322, + "baseTickHeight": 0, + "tickCount": 5909, + "publicKey": "C4iiN5H06+0Y2w7mGqdxUPSlDtOxqA+gp6r6nDVoD8s=", + "sequence": 0, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "C4iiN5H06+0Y2w7mGqdxUPSlDtOxqA+gp6r6nDVoD8s=": 33 + } + }, + { + "id": "ag/AMzHZ6Fcm6jcrbAsolZvzx5Fg1JYVn76a/+C+hvM=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 23673, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "C5nht7r1IApif0neIQwWNzBMajX7uXRMxt05giYXhnE=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "C5nht7r1IApif0neIQwWNzBMajX7uXRMxt05giYXhnE=": 33 + } + }, + { + "id": "MHwh+kPx5G/ViJl8YSLBJcqYvF6R9BHWTgl5I6QLdM4=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 122, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "lDtxQqzy1DPpFJHb/w0I/QKkfST0iF4iULkkYuhLjB0=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 23673, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "C5nht7r1IApif0neIQwWNzBMajX7uXRMxt05giYXhnE=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "C5nht7r1IApif0neIQwWNzBMajX7uXRMxt05giYXhnE=": 33 + } }, { - "id": "k1eMLxhzJ4XWuxKLED8PNpgWohFIs6bu84qIPGsdwPI=", + "id": "2RSj0KjzJ1ipTMXpB8fFfD63h6OMCmIqnDVLiO+56S8=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 26008, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "C+PrXQcTh3wbdmMpLi37vRhAoZAJE4f6/XstKX//9oU=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "C+PrXQcTh3wbdmMpLi37vRhAoZAJE4f6/XstKX//9oU=": 33 + } + }, + { + "id": "cYVW4HDB7vE8CyfUG5IyfZCWlfn9OLcTqYMCI1y8iz4=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 111, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "ne6Xsdv9hyMPzm/EFk+iVvHwhe/qIKNlsnJz4+LneMY=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 26008, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "C+PrXQcTh3wbdmMpLi37vRhAoZAJE4f6/XstKX//9oU=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "C+PrXQcTh3wbdmMpLi37vRhAoZAJE4f6/XstKX//9oU=": 33 + } }, { - "id": "CUUdaKPTxKbboLrH5JAv9/g8eTdXgrWhomHr8ozxjO0=", + "id": "EdAf9/F8T1dLeEOivsqrPKRxbvtUasCk0VsvQBQMKI0=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 22684, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "DAKMJyRfCnl2MeJvu87cTq4gC7PVtTd7V7k4W174hWU=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "DAKMJyRfCnl2MeJvu87cTq4gC7PVtTd7V7k4W174hWU=": 33 + } + }, + { + "id": "1Phn8kpmA5vySWdUGYtnK/lLTonJqKcHA/QbdXlFDro=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 9, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "nuoQ7Ji1PSoe6TWDxea2Wd+Q3zN/CM7UsN6jzLo8E6o=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 22684, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "DAKMJyRfCnl2MeJvu87cTq4gC7PVtTd7V7k4W174hWU=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "DAKMJyRfCnl2MeJvu87cTq4gC7PVtTd7V7k4W174hWU=": 33 + } + }, + { + "id": "NRNoQkQ0rOg3Bl0IoAC6lR+iJmHoN7IWBhJnh8QnJn0=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 12265, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "DN9/1ySs9MOgeU0E/5qOz/JiQxjxssb+f+5SKK+qL9Q=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "DN9/1ySs9MOgeU0E/5qOz/JiQxjxssb+f+5SKK+qL9Q=": 33 + } }, { - "id": "p0sG4uz9PgsjocZdge6IcsZk1PEEhCGiuGhZ7WhmX08=", + "id": "b753T1CB7r6FM8GZq4BicT1wwV/5BgRT42h9SqatXSk=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 100, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "tYesWSTOwZMS9NWkGNmNtMSSyONl0aqvuE0RaFnpa2g=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 12265, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "DN9/1ySs9MOgeU0E/5qOz/JiQxjxssb+f+5SKK+qL9Q=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "DN9/1ySs9MOgeU0E/5qOz/JiQxjxssb+f+5SKK+qL9Q=": 33 + } }, { - "id": "CTtqOhleEEUjf4PQ/g4cAIUNE88oIuBbkkaXH367mEM=", + "id": "XTRGnwx42zE4s5aXT5Ll2WfeKf/rKyCg/jjBbJdycs0=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 22674, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "DOYm61QwL7dRW1jbLSJqWRz0f+qa0CWCsFy2nPBLjno=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "DOYm61QwL7dRW1jbLSJqWRz0f+qa0CWCsFy2nPBLjno=": 33 + } + }, + { + "id": "6yxuZcIVgZ5cBsJ5UDw7Y+Y5thZ67eLeZX4IAba5GcY=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 100, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "vopmBzDRKMEACuU5/vgwJhLX36KOz26fUXvTpth0Hj8=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 22674, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "DOYm61QwL7dRW1jbLSJqWRz0f+qa0CWCsFy2nPBLjno=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "DOYm61QwL7dRW1jbLSJqWRz0f+qa0CWCsFy2nPBLjno=": 33 + } + }, + { + "id": "kR2yHkBpUVCh3Jc9b2XuAJRe76/bjyftydFKoSl091Q=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 4922, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "DT5rESAWYlrwdWb6yxNdzmBz8h+r2pT5ZcR1C24Tx/k=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "DT5rESAWYlrwdWb6yxNdzmBz8h+r2pT5ZcR1C24Tx/k=": 33 + } }, { - "id": "WeYO5DzkmT+LOD+LPkMTpbG36nacu+MkWIFrdn4iAR8=", + "id": "/Ezt/WGbXcCb2FYvrx3BzW1bKVmVXTLjX3GsSsEeCwo=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 187, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "yZgoeiL/ZUh/UK29rZLj/SCuF6oRyBu8qFJlIOYM+FA=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 4922, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "DT5rESAWYlrwdWb6yxNdzmBz8h+r2pT5ZcR1C24Tx/k=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "DT5rESAWYlrwdWb6yxNdzmBz8h+r2pT5ZcR1C24Tx/k=": 33 + } }, { - "id": "9R1RfLd9HtlV8dPOgMRbkSXJ4SDXHELrt89UiU9UYww=", + "id": "+AxJ02FsmpZiIuZcFedSmyHmFg3hEYe2971HQHrpZAM=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 16077, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "DciYS9tzU+k6iEdTuFRbWsP+XyXQsjZc+VnU6+H9u4s=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "DciYS9tzU+k6iEdTuFRbWsP+XyXQsjZc+VnU6+H9u4s=": 33 + } + }, + { + "id": "ocKpf3AJ1UJ3bugthZk9VeA6I0YP6M6eE9d0FjQEjik=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 55, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "y6/LmwlUGbzaZ2Hp1x83HBIpG4K8OQOVKAFeT6aQ1g0=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 16077, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "DciYS9tzU+k6iEdTuFRbWsP+XyXQsjZc+VnU6+H9u4s=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "DciYS9tzU+k6iEdTuFRbWsP+XyXQsjZc+VnU6+H9u4s=": 33 + } + }, + { + "id": "pNrY2xN/+6lcyUu+XuRSv3tSZSwddWKNdCRHRzYHdsk=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 9675, + "baseTickHeight": 11821, + "tickCount": 4787, + "publicKey": "Dcket7piCC4pYPLmSW+kyOSJVMJwSVOK0tjL9YVzPCo=", + "sequence": 0, + "coinbase": "AAAAAAHAd/lY2IYJI8eC1f9WpvJAJXEY", + "numUnits": 100, + "units": { + "Dcket7piCC4pYPLmSW+kyOSJVMJwSVOK0tjL9YVzPCo=": 100 + } }, { - "id": "F5O01qw5QxzJkV5V0MSsAposs732b79qJZbr4Umm2qM=", + "id": "Nrxz9O92lsiTaa3T+VlLCPtKrdtAI+qTZgEHltmQ7KU=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 58506, + "baseTickHeight": 11821, + "tickCount": 4787, + "publicKey": "DiFP9U4LWkak2kEvWlv/vq0oJzq1rVaA9R1tF4peXlQ=", + "sequence": 2, + "coinbase": "AAAAAGESmyCRIgdK34zMqo6c3mPx08gk", + "numUnits": 100, + "units": { + "DiFP9U4LWkak2kEvWlv/vq0oJzq1rVaA9R1tF4peXlQ=": 100 + } + }, + { + "id": "yjupkOAImAMe3C8qPw4BTBOzsvfMwz4cOJq/JiIaz8Q=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 184, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "12zcRs1oRwsXdKOrF6p4SPrirOm02R2hwJudh28f1c8=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 58506, + "baseTickHeight": 5909, + "tickCount": 4738, + "publicKey": "DiFP9U4LWkak2kEvWlv/vq0oJzq1rVaA9R1tF4peXlQ=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" + "coinbase": "AAAAAGESmyCRIgdK34zMqo6c3mPx08gk", + "numUnits": 100, + "units": { + "DiFP9U4LWkak2kEvWlv/vq0oJzq1rVaA9R1tF4peXlQ=": 100 + } }, { - "id": "sc3vQd4SIonpamSsxl+tksvz94Wljju2FncHGI4GWM8=", + "id": "ywRGivZT8yhB8iYsCIZk6yQn5N8nvdaaGr4q/wjKPfg=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 7966, + "baseTickHeight": 11821, + "tickCount": 4787, + "publicKey": "Dv1yHWybFg0snsR7l2+ytj9oTMWRW7TeZJ5zI4oaBSo=", + "sequence": 0, + "coinbase": "AAAAAAHAd/lY2IYJI8eC1f9WpvJAJXEY", + "numUnits": 100, + "units": { + "Dv1yHWybFg0snsR7l2+ytj9oTMWRW7TeZJ5zI4oaBSo=": 100 + } + }, + { + "id": "3+bYfDia1apZVRmuB+WkMF1qh9yw1h3Rm2k3wjweF/0=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 23535, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "D6p6WsfM/IgCUSUPAA0/SXMl1TQypSD9y/YBgRnP9GM=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "D6p6WsfM/IgCUSUPAA0/SXMl1TQypSD9y/YBgRnP9GM=": 33 + } + }, + { + "id": "APFYbXP2uO0snHwAy+mKmi1IdeTTaFIUHpIs4edv3uU=", "epoch": 2, - "commitmentAtx": "iXlw/UMbcz+h/Bm+l90zA2GnKWVg3dEEOV3fP8vdKfE=", - "vrfNonce": 18, - "numUnits": 2, - "baseTickHeight": 6162, - "tickCount": 6159, - "publicKey": "9UMrQ+L5i51d9ik3SQ5202QsOa4AnDWzTj+QQC3mNg4=", + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 23535, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "D6p6WsfM/IgCUSUPAA0/SXMl1TQypSD9y/YBgRnP9GM=", "sequence": 1, - "coinbase": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA" - } - ], - "accounts": [ + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "D6p6WsfM/IgCUSUPAA0/SXMl1TQypSD9y/YBgRnP9GM=": 33 + } + }, { - "address": "AAAAAAc6977AGOjS43n6R99qn6B6aoNE", - "balance": 100000000000000000, - "nonce": 0, - "template": null, - "state": null + "id": "NuovdYlFeOOsueYMWaT1xeOusUYjORqUp/GqSXdV2tQ=", + "epoch": 1, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 32634, + "baseTickHeight": 0, + "tickCount": 5909, + "publicKey": "EDfbWmIKHweeRzVwqKazchfvA5D6peR5SrwzW1Kspq8=", + "sequence": 0, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "EDfbWmIKHweeRzVwqKazchfvA5D6peR5SrwzW1Kspq8=": 33 + } }, { - "address": "AAAAAAsBAQAAAAAAAAAAAAAAAAAAAAAA", - "balance": 1000, - "nonce": 0, - "template": null, - "state": null + "id": "TxlYswDgeBY0JdM1VvJe+i/leedBJaJcEn9HpLoCawA=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 7705, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "EGstlX/VPlNT4hQ9KX60ZgrOiuVlJxON/3bn3TVnlL0=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "EGstlX/VPlNT4hQ9KX60ZgrOiuVlJxON/3bn3TVnlL0=": 33 + } }, { - "address": "AAAAABsxTRfAWikF+RjQ1y8vaYlkD7tD", - "balance": 100000000000000000, - "nonce": 0, - "template": null, - "state": null + "id": "hP4uH1FcWDtUR3QWV3KX7gzPl1HzGOKIeFVwj41tMrI=", + "epoch": 2, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 7705, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "EGstlX/VPlNT4hQ9KX60ZgrOiuVlJxON/3bn3TVnlL0=", + "sequence": 1, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "EGstlX/VPlNT4hQ9KX60ZgrOiuVlJxON/3bn3TVnlL0=": 33 + } }, { - "address": "AAAAABuplkSS7uc5nwy4I5Og9PhuwFMS", - "balance": 99999999999863378, - "nonce": 2, - "template": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", - "state": "yFk7fs/6xYbisUDOENMsr1Rt3CseVQkcSKA409KnyDE=" + "id": "Zvdd89yxP4UQZ4+M1Lkuc2+Dtz6VXrpLNNP9wHLPMis=", + "epoch": 3, + "commitmentAtx": "H247+9mKgBUSZX1jLEHY9u08VVli005amReDskZOb18=", + "vrfNonce": 17002, + "baseTickHeight": 11821, + "tickCount": 4787, + "publicKey": "EIcsZQ566PgTeHCHZuL/2DU5wJga8CYgplfaHI7dm74=", + "sequence": 0, + "coinbase": "AAAAAAHAd/lY2IYJI8eC1f9WpvJAJXEY", + "numUnits": 100, + "units": { + "EIcsZQ566PgTeHCHZuL/2DU5wJga8CYgplfaHI7dm74=": 100 + } }, { - "address": "AAAAADEAAAAAAAAAAAAAAAAAAAAAAAAA", - "balance": 3343327309466, - "nonce": 0, - "template": null, - "state": null + "id": "mZT9Em8gRsemZFG9SVetBuLBZLWSfSY50y/5HcodOKM=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 94029, + "baseTickHeight": 11821, + "tickCount": 4787, + "publicKey": "EMAIBBWiUbOVERLDoqxCJ0cR77AbE5rL7oS/zYn/QcI=", + "sequence": 0, + "coinbase": "AAAAAAHAd/lY2IYJI8eC1f9WpvJAJXEY", + "numUnits": 100, + "units": { + "EMAIBBWiUbOVERLDoqxCJ0cR77AbE5rL7oS/zYn/QcI=": 100 + } + }, + { + "id": "etp5lgS3T4FZglszYwBBB0NXHNwLqOyPvQeL3fnpeUo=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 14750, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "EQPH+3yXmod4ytUJ6e+dm+bhS4xy//ILsy32RQqlX2s=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "EQPH+3yXmod4ytUJ6e+dm+bhS4xy//ILsy32RQqlX2s=": 33 + } + }, + { + "id": "as7Adv9h7Nw00D3ohpZQq3fHUOICr9aAB1PICyeXpUo=", + "epoch": 2, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 14750, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "EQPH+3yXmod4ytUJ6e+dm+bhS4xy//ILsy32RQqlX2s=", + "sequence": 1, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "EQPH+3yXmod4ytUJ6e+dm+bhS4xy//ILsy32RQqlX2s=": 33 + } + }, + { + "id": "15qh75bMf2XKR1IzJp2Ahjf/xneLQqMydtdfbrgRFYE=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 32319, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "ESjbBN+t7xbFXgctEIJ4feolbXXSFk8i5q3h++Mrq9U=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "ESjbBN+t7xbFXgctEIJ4feolbXXSFk8i5q3h++Mrq9U=": 33 + } }, { - "address": "AAAAAE/bRPsGcLFsZXAvgtq06B0goSxS", - "balance": 99999999999863378, - "nonce": 2, - "template": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", - "state": "N5hEV9rrr/0l6EH/jd5RRi4uDv4zJPok9vMVAloCCjo=" + "id": "5zCdf/LGXUKBBn9IydWCmHu6a366xZ04hugxY5wtgG8=", + "epoch": 2, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 32319, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "ESjbBN+t7xbFXgctEIJ4feolbXXSFk8i5q3h++Mrq9U=", + "sequence": 1, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "ESjbBN+t7xbFXgctEIJ4feolbXXSFk8i5q3h++Mrq9U=": 33 + } }, { - "address": "AAAAAFoCrsQ+F8gKsTPgDIOBVD0IaOHP", - "balance": 99999999999863378, - "nonce": 2, - "template": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", - "state": "9u1uvfA/aIeKs3wHABX5v0cuw41e88dT97J/7/rgWys=" + "id": "+OJn66G9Xcs48UT/fFIQPimo+3/vZ/YQdscLn8kaFyY=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 16699, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "ETG4uCa9tKcGWH0H2d7g5wPVglAm7Zhqb240LblwCa4=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "ETG4uCa9tKcGWH0H2d7g5wPVglAm7Zhqb240LblwCa4=": 33 + } }, { - "address": "AAAAAHYI4uxyryMeLuLHHDEHSdNZn+Uc", - "balance": 99999999999863378, - "nonce": 2, - "template": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", - "state": "HqsEi7+vZ7oqINxH7lYKDpyQVerJhSvKfhSHDcI6JMg=" + "id": "4g2KCJyGkCMtZoIlpFQTcr4Hpc+VjYoW5KKDpxwEAQ4=", + "epoch": 2, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 16699, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "ETG4uCa9tKcGWH0H2d7g5wPVglAm7Zhqb240LblwCa4=", + "sequence": 1, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "ETG4uCa9tKcGWH0H2d7g5wPVglAm7Zhqb240LblwCa4=": 33 + } }, { - "address": "AAAAAIEtzQqCaLIJQfBXw1DuOPpQZv+y", - "balance": 99999999999863378, - "nonce": 2, - "template": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", - "state": "UC65cj7HOPJqYagaeVy3SXy8weDmtvKRVZu+WQYqhXM=" + "id": "0HUPRSUoUJvrQB/mma6+PAJ7EgwlQANVDN/IX2xRXrY=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 31738, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "EYAQ4iynbSb5We+vPV727FlL5Exdl6scHAoROHb65Oo=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "EYAQ4iynbSb5We+vPV727FlL5Exdl6scHAoROHb65Oo=": 33 + } }, { - "address": "AAAAAINzk5402WXIT+3ti89stMmZQiAI", - "balance": 99999999999863378, - "nonce": 2, - "template": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", - "state": "CPvzEMLPa3bWbNTo02pqjHyI7PEEZ2lv/lGkYLwuHIs=" + "id": "Y95gHvl1XVzNXJiEAsmJzOVTLFwXDoz7a46yQT23Tw8=", + "epoch": 2, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 31738, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "EYAQ4iynbSb5We+vPV727FlL5Exdl6scHAoROHb65Oo=", + "sequence": 1, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "EYAQ4iynbSb5We+vPV727FlL5Exdl6scHAoROHb65Oo=": 33 + } }, { - "address": "AAAAALOIq50BKZxlVKEZqONtsNlyOUeJ", - "balance": 99999999999863378, - "nonce": 2, - "template": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", - "state": "Gfmjt6Kd+wJN6Pa4hTapZaOYJC7V/9YUodrzkVPYWeg=" + "id": "DDLvIZCjunktKsaKUzlKj4YGeVX52go8fWxYFpgklkM=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 6091, + "baseTickHeight": 11821, + "tickCount": 4787, + "publicKey": "EoRzhZBmKrGknTfXtE8MKdfCyc6cr9Z0FVxkqj17QLA=", + "sequence": 0, + "coinbase": "AAAAAAHAd/lY2IYJI8eC1f9WpvJAJXEY", + "numUnits": 100, + "units": { + "EoRzhZBmKrGknTfXtE8MKdfCyc6cr9Z0FVxkqj17QLA=": 100 + } }, { - "address": "AAAAAL1XWyitwtnCf75AjQ3alv/cOsTJ", - "balance": 99999999999863378, - "nonce": 2, - "template": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", - "state": "epKOw7UakXzPuNQ5UmwrmC5c5VH5J8Wd/OGd++ut4KE=" + "id": "MEqcCKRces6Fe6grGC0R1TlU3nsw3Q/bMtj7MX6B6Ck=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 1323, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "EpllxhigTIVayGrULrE5AFb85bwBsXmsS0WeBBsQ48w=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "EpllxhigTIVayGrULrE5AFb85bwBsXmsS0WeBBsQ48w=": 33 + } }, { - "address": "AAAAANmtySRp9InS6YkIFZZLhRARq80t", - "balance": 99999999999863378, - "nonce": 2, - "template": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", - "state": "6CGvkwPVkE3oRCtwM/KnS4qrsF37w40z6tE7HsQDD0E=" + "id": "goHEKaMCErnmhv6aXkXvPNogWRK8NdYJApnY8ACv+Ek=", + "epoch": 2, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 1323, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "EpllxhigTIVayGrULrE5AFb85bwBsXmsS0WeBBsQ48w=", + "sequence": 1, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "EpllxhigTIVayGrULrE5AFb85bwBsXmsS0WeBBsQ48w=": 33 + } }, { - "address": "AAAAAPRFrccwTVI1jHzxBoZzFLhZqFYo", - "balance": 99999999999863378, - "nonce": 2, - "template": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB", - "state": "Q9PuZ7NKMTO5fUIgdCG0tptm7AEUjonCKEeKsAWXALk=" + "id": "BbJerrvnt3Em7hhaynF8kCnNy8e4p9k1DELAv9Ia0P0=", + "epoch": 3, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 33164, + "baseTickHeight": 11821, + "tickCount": 5913, + "publicKey": "Ew1eFmCcpVSCD37NB6JQ1OU6dLWTASB78E03u2xO+hI=", + "sequence": 2, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "Ew1eFmCcpVSCD37NB6JQ1OU6dLWTASB78E03u2xO+hI=": 33 + } + }, + { + "id": "2g6fNDQ6BQN19n8En0PEzw+B3JXtfUr3ToFX2y40Ykk=", + "epoch": 2, + "commitmentAtx": "zZeDLsOumFHmJ32L2IvBe0UI4rKmq5Co7km6jk8Y0cg=", + "vrfNonce": 33164, + "baseTickHeight": 5909, + "tickCount": 5912, + "publicKey": "Ew1eFmCcpVSCD37NB6JQ1OU6dLWTASB78E03u2xO+hI=", + "sequence": 1, + "coinbase": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "numUnits": 33, + "units": { + "Ew1eFmCcpVSCD37NB6JQ1OU6dLWTASB78E03u2xO+hI=": 33 + } + } + ], + "accounts": [ + { + "address": "AAAAAAHAd/lY2IYJI8eC1f9WpvJAJXEY", + "balance": 6450295402459, + "nonce": 0, + "template": null, + "state": null + }, + { + "address": "AAAAAEWFYdzEv+mfQmEIRhtXR7IavXQl", + "balance": 253981759502464, + "nonce": 0, + "template": null, + "state": null + }, + { + "address": "AAAAAGESmyCRIgdK34zMqo6c3mPx08gk", + "balance": 10939325155211, + "nonce": 0, + "template": null, + "state": null } ] } -} \ No newline at end of file +} diff --git a/checkpoint/recovery.go b/checkpoint/recovery.go index f14f7142b7..626e8cc39a 100644 --- a/checkpoint/recovery.go +++ b/checkpoint/recovery.go @@ -350,6 +350,7 @@ func checkpointData(fs afero.Fs, file string, newGenesis types.LayerID) (*recove cAtx.TickCount = atx.TickCount cAtx.Sequence = atx.Sequence copy(cAtx.Coinbase[:], atx.Coinbase) + cAtx.Units = atx.Units allAtxs = append(allAtxs, &cAtx) } return &recoveryData{ diff --git a/checkpoint/recovery_test.go b/checkpoint/recovery_test.go index b0f9e28b69..527e113ee7 100644 --- a/checkpoint/recovery_test.go +++ b/checkpoint/recovery_test.go @@ -836,6 +836,7 @@ func TestRecover_OwnAtxNotInCheckpoint_Preserve_DepIsGolden(t *testing.T) { SmesherID: golden.SmesherID, Sequence: golden.Sequence, Coinbase: golden.Coinbase, + Units: map[types.NodeID]uint32{golden.SmesherID: golden.NumUnits}, })) validateAndPreserveData(t, oldDB, vAtxs[1:]) // the proofs are not valid, but save them anyway for the purpose of testing diff --git a/checkpoint/runner.go b/checkpoint/runner.go index 89e7797cfe..688410cca8 100644 --- a/checkpoint/runner.go +++ b/checkpoint/runner.go @@ -88,6 +88,7 @@ func checkpointDB( PublicKey: catx.SmesherID.Bytes(), Sequence: catx.Sequence, Coinbase: catx.Coinbase.Bytes(), + Units: catx.Units, }) } diff --git a/checkpoint/runner_test.go b/checkpoint/runner_test.go index c23fa63813..ede06468f1 100644 --- a/checkpoint/runner_test.go +++ b/checkpoint/runner_test.go @@ -249,14 +249,21 @@ func asAtxSnapshot(v *types.ActivationTx, cmt *types.ATXID) types.AtxSnapshot { PublicKey: v.SmesherID.Bytes(), Sequence: v.Sequence, Coinbase: v.Coinbase.Bytes(), + Units: map[types.NodeID]uint32{v.SmesherID: v.NumUnits}, } } +func addAtx(t testing.TB, db sql.Executor, atx *types.ActivationTx) { + t.Helper() + require.NoError(t, atxs.Add(db, atx)) + require.NoError(t, atxs.SetUnits(db, atx.ID(), map[types.NodeID]uint32{atx.SmesherID: atx.NumUnits})) +} + func createMesh(t testing.TB, db *sql.Database, miners []miner, accts []*types.Account) { t.Helper() for _, miner := range miners { for _, atx := range miner.atxs { - require.NoError(t, atxs.Add(db, atx)) + addAtx(t, db, atx) } if proof := miner.malfeasanceProof; len(proof) > 0 { require.NoError(t, identities.SetMalicious(db, miner.atxs[0].SmesherID, proof, time.Now())) diff --git a/common/types/checkpoint.go b/common/types/checkpoint.go index 5dfba2cd43..a5ff07a164 100644 --- a/common/types/checkpoint.go +++ b/common/types/checkpoint.go @@ -17,12 +17,15 @@ type AtxSnapshot struct { Epoch uint32 `json:"epoch"` CommitmentAtx []byte `json:"commitmentAtx"` VrfNonce uint64 `json:"vrfNonce"` - NumUnits uint32 `json:"numUnits"` BaseTickHeight uint64 `json:"baseTickHeight"` TickCount uint64 `json:"tickCount"` PublicKey []byte `json:"publicKey"` Sequence uint64 `json:"sequence"` Coinbase []byte `json:"coinbase"` + // total effective units + NumUnits uint32 `json:"numUnits"` + // actual units per smesher + Units map[NodeID]uint32 `json:"units"` } type AccountSnapshot struct { diff --git a/common/types/nodeid.go b/common/types/nodeid.go index 13d16aaf2a..887fe14a8c 100644 --- a/common/types/nodeid.go +++ b/common/types/nodeid.go @@ -55,7 +55,7 @@ func (id *NodeID) DecodeScale(d *scale.Decoder) (int, error) { return scale.DecodeByteArray(d, id[:]) } -func (id *NodeID) MarshalText() ([]byte, error) { +func (id NodeID) MarshalText() ([]byte, error) { return util.Base64Encode(id[:]), nil } diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 423d28df65..7d266f95c0 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -537,12 +537,15 @@ type CheckpointAtx struct { Epoch types.EpochID CommitmentATX types.ATXID VRFNonce types.VRFPostIndex - NumUnits uint32 BaseTickHeight uint64 TickCount uint64 SmesherID types.NodeID Sequence uint64 Coinbase types.Address + // total effective units + NumUnits uint32 + // actual units of each included smesher + Units map[types.NodeID]uint32 } // LatestN returns the latest N ATXs per smesher. @@ -565,6 +568,7 @@ func LatestN(db sql.Executor, n int) ([]CheckpointAtx, error) { catx.Sequence = uint64(stmt.ColumnInt64(6)) stmt.ColumnBytes(7, catx.Coinbase[:]) catx.VRFNonce = types.VRFPostIndex(stmt.ColumnInt64(8)) + catx.Units = make(map[types.NodeID]uint32) rst = append(rst, catx) return true } @@ -583,6 +587,24 @@ func LatestN(db sql.Executor, n int) ([]CheckpointAtx, error) { } else if ierr != nil { return nil, ierr } + + for i := range rst { + enc := func(stmt *sql.Statement) { + stmt.BindBytes(1, rst[i].ID.Bytes()) + } + if rows, err := db.Exec(` + SELECT pubkey, units FROM posts WHERE atxid = ?1;`, enc, func(stmt *sql.Statement) bool { + var nid types.NodeID + stmt.ColumnBytes(0, nid[:]) + rst[i].Units[nid] = uint32(stmt.ColumnInt64(1)) + return true + }); err != nil { + return nil, fmt.Errorf("fetching units for checkpoint ATX: %w", err) + } else if rows == 0 { + return nil, fmt.Errorf("fetching units for checkpoint ATX: %w", sql.ErrNotFound) + } + } + return rst, nil } @@ -614,6 +636,11 @@ func AddCheckpointed(db sql.Executor, catx *CheckpointAtx) error { if err != nil { return fmt.Errorf("insert checkpoint ATX blob %v: %w", catx.ID, err) } + + if err := SetUnits(db, catx.ID, catx.Units); err != nil { + return fmt.Errorf("insert checkpoint ATX units %v: %w", catx.ID, err) + } + return nil } @@ -846,3 +873,40 @@ func PrevATXCollisions(db sql.Executor) ([]PrevATXCollision, error) { return result, nil } + +func Units(db sql.Executor, atxID types.ATXID, nodeID types.NodeID) (uint32, error) { + var units uint32 + rows, err := db.Exec(` + SELECT units FROM posts WHERE atxid = ?1 AND pubkey = ?2;`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, atxID.Bytes()) + stmt.BindBytes(2, nodeID.Bytes()) + }, + func(stmt *sql.Statement) bool { + units = uint32(stmt.ColumnInt64(0)) + return false + }, + ) + if rows == 0 { + return 0, sql.ErrNotFound + } + return units, err +} + +func SetUnits(db sql.Executor, atxID types.ATXID, units map[types.NodeID]uint32) error { + for nodeID, u := range units { + _, err := db.Exec(` + INSERT INTO posts (atxid, pubkey, units) VALUES (?1, ?2, ?3);`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, atxID.Bytes()) + stmt.BindBytes(2, nodeID.Bytes()) + stmt.BindInt64(3, int64(u)) + }, nil, + ) + if err != nil { + return fmt.Errorf("set units for ID %s in ATX %s: %w", nodeID, atxID, err) + } + } + + return nil +} diff --git a/sql/atxs/atxs_test.go b/sql/atxs/atxs_test.go index af55b5473e..fc2e5fd7fa 100644 --- a/sql/atxs/atxs_test.go +++ b/sql/atxs/atxs_test.go @@ -173,6 +173,7 @@ func TestLatestN(t *testing.T) { for _, atx := range []*types.ActivationTx{atx1, atx2, atx3, atx4, atx5, atx6} { require.NoError(t, atxs.Add(db, atx)) + require.NoError(t, atxs.SetUnits(db, atx.ID(), map[types.NodeID]uint32{atx.SmesherID: atx.NumUnits})) } for _, tc := range []struct { @@ -1121,3 +1122,41 @@ func TestCoinbase(t *testing.T) { require.Equal(t, atx2.Coinbase, cb) }) } + +func TestUnits(t *testing.T) { + t.Parallel() + t.Run("ATX not found", func(t *testing.T) { + t.Parallel() + db := sql.InMemory() + _, err := atxs.Units(db, types.RandomATXID(), types.RandomNodeID()) + require.ErrorIs(t, err, sql.ErrNotFound) + }) + t.Run("smesher has no units in ATX", func(t *testing.T) { + t.Parallel() + db := sql.InMemory() + atxID := types.RandomATXID() + require.NoError(t, atxs.SetUnits(db, atxID, map[types.NodeID]uint32{{1, 2, 3}: 10})) + _, err := atxs.Units(db, types.RandomATXID(), types.RandomNodeID()) + require.ErrorIs(t, err, sql.ErrNotFound) + }) + t.Run("returns units for given smesher in given ATX", func(t *testing.T) { + t.Parallel() + db := sql.InMemory() + atxID := types.RandomATXID() + units := map[types.NodeID]uint32{ + {1, 2, 3}: 10, + {4, 5, 6}: 20, + } + require.NoError(t, atxs.SetUnits(db, atxID, units)) + + nodeID := types.NodeID{1, 2, 3} + got, err := atxs.Units(db, atxID, nodeID) + require.NoError(t, err) + require.Equal(t, units[nodeID], got) + + nodeID = types.NodeID{4, 5, 6} + got, err = atxs.Units(db, atxID, nodeID) + require.NoError(t, err) + require.Equal(t, units[nodeID], got) + }) +} diff --git a/sql/migrations/state/0021_atx_posts.sql b/sql/migrations/state/0021_atx_posts.sql new file mode 100644 index 0000000000..e741638f4c --- /dev/null +++ b/sql/migrations/state/0021_atx_posts.sql @@ -0,0 +1,12 @@ +-- Table showing the exact number of PoST units commited by smesher in given ATX. +-- TODO(poszu): Migrate data for existing ATXs (require decoding blobs to be correct). +-- Alternatively, we could take the effective numUnits from `atxs` table, +-- which would be faster but it could cause temporary harm for ATXs growing in size. +CREATE TABLE posts ( + atxid CHAR(32) NOT NULL, + units INT NOT NULL, + pubkey CHAR(32) NOT NULL, + UNIQUE (atxid, pubkey) +); + +CREATE INDEX posts_by_atxid_by_pubkey ON posts (atxid, pubkey); From 9bdaf94bd3a505ba682e2b0bac168db4fcccc677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 26 Jun 2024 13:15:10 +0200 Subject: [PATCH 12/25] Add unit tests for checkpointed previous ATX --- activation/handler_v2_test.go | 99 +++++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 21 deletions(-) diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 8e86c51977..e59f056f7f 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -538,41 +538,30 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { t.Run("second ATX, increases space (nonce valid)", func(t *testing.T) { t.Parallel() atxHandler := newV2TestHandler(t, golden) - prev := newInitialATXv2(t, golden) - prev.Sign(sig) - - atxHandler.expectInitialAtxV2(prev) - proof, err := atxHandler.processATX(context.Background(), peer, prev, codec.MustEncode(prev), time.Now()) - require.NoError(t, err) - require.Nil(t, proof) + prev := atxHandler.createAndProcessInitial(t, sig) atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), golden) - atx.NiPosts[0].Posts[0].NumUnits *= 10 + atx.NiPosts[0].Posts[0].NumUnits = prev.TotalNumUnits() * 10 atx.VRFNonce = 7779989 atx.Sign(sig) atxHandler.expectAtxV2(atx) - proof, err = atxHandler.processATX(context.Background(), peer, atx, codec.MustEncode(atx), time.Now()) + proof, err := atxHandler.processATX(context.Background(), peer, atx, codec.MustEncode(atx), time.Now()) require.NoError(t, err) require.Nil(t, proof) atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) require.NoError(t, err) require.EqualValues(t, atx.VRFNonce, atxFromDb.VRFNonce) + require.Equal(t, min(prev.TotalNumUnits(), atx.TotalNumUnits()), atxFromDb.TotalNumUnits()) }) t.Run("second ATX, increases space (nonce invalid)", func(t *testing.T) { t.Parallel() atxHandler := newV2TestHandler(t, golden) - prev := newInitialATXv2(t, golden) - prev.Sign(sig) - - atxHandler.expectInitialAtxV2(prev) - proof, err := atxHandler.processATX(context.Background(), peer, prev, codec.MustEncode(prev), time.Now()) - require.NoError(t, err) - require.Nil(t, proof) + prev := atxHandler.createAndProcessInitial(t, sig) atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), golden) - atx.NiPosts[0].Posts[0].NumUnits *= 10 + atx.NiPosts[0].Posts[0].NumUnits = prev.TotalNumUnits() * 10 atx.VRFNonce = 7779989 atx.Sign(sig) atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) @@ -591,15 +580,14 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { _, err = atxs.Get(atxHandler.cdb, atx.ID()) require.ErrorIs(t, err, sql.ErrNotFound) }) - t.Run("second ATX, increases space (new nonce)", func(t *testing.T) { + t.Run("second ATX, decreases space", func(t *testing.T) { t.Parallel() atxHandler := newV2TestHandler(t, golden) prev := atxHandler.createAndProcessInitial(t, sig) - lowerNumUnits := prev.TotalNumUnits() atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), golden) atx.VRFNonce = uint64(123) - atx.NiPosts[0].Posts[0].NumUnits = lowerNumUnits * 10 + atx.NiPosts[0].Posts[0].NumUnits = prev.TotalNumUnits() - 1 atx.Sign(sig) atxHandler.expectAtxV2(atx) @@ -610,7 +598,7 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { // verify that the ATX was added to the DB and it has the lower effective num units atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) require.NoError(t, err) - require.Equal(t, lowerNumUnits, atxFromDb.TotalNumUnits()) + require.Equal(t, min(prev.TotalNumUnits(), atx.TotalNumUnits()), atxFromDb.TotalNumUnits()) require.EqualValues(t, atx.VRFNonce, atxFromDb.VRFNonce) }) t.Run("can't find positioning ATX", func(t *testing.T) { @@ -886,6 +874,75 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { require.Error(t, err) require.Nil(t, p) }) + t.Run("previous checkpointed ATX must include every ID", func(t *testing.T) { + atxHandler := newV2TestHandler(t, golden) + + // Marry IDs + mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 1) + equivocationSet := []types.NodeID{sig.NodeID()} + for _, atx := range otherATXs { + equivocationSet = append(equivocationSet, atx.SmesherID) + } + + prev := atxs.CheckpointAtx{ + ID: types.RandomATXID(), + CommitmentATX: types.RandomATXID(), + SmesherID: sig.NodeID(), + NumUnits: 10, + Units: make(map[types.NodeID]uint32), + } + for _, id := range equivocationSet { + prev.Units[id] = 10 + } + require.NoError(t, atxs.AddCheckpointed(atxHandler.cdb, &prev)) + + // Process a merged ATX + merged := newSoloATXv2(t, prev.Epoch+1, prev.ID, golden) + merged.NiPosts[0].Posts = []wire.SubPostV2{} + for marriageIdx := range equivocationSet { + post := wire.SubPostV2{ + MarriageIndex: uint32(marriageIdx), + NumUnits: 7, + } + merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post) + } + + mATXID := mATX.ID() + merged.MarriageATX = &mATXID + merged.Sign(sig) + + atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100}) + p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) + require.NoError(t, err) + require.Nil(t, p) + + // checkpoint again but not inslude one of the IDs + prev.ID = types.RandomATXID() + prev.Epoch = merged.PublishEpoch + 1 + clear(prev.Units) + for _, id := range equivocationSet[:1] { + prev.Units[id] = 10 + } + require.NoError(t, atxs.AddCheckpointed(atxHandler.cdb, &prev)) + + merged = newSoloATXv2(t, prev.Epoch+1, prev.ID, golden) + merged.NiPosts[0].Posts = []wire.SubPostV2{} + for marriageIdx := range equivocationSet { + post := wire.SubPostV2{ + MarriageIndex: uint32(marriageIdx), + NumUnits: 7, + } + merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post) + } + merged.MarriageATX = &mATXID + merged.Sign(sig) + + atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) + atxHandler.expectFetchDeps(merged) + p, err = atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) + require.Error(t, err) + require.Nil(t, p) + }) } func TestCollectDeps_AtxV2(t *testing.T) { From 752b5d4a3e90332637fadd7e495ca393cceb6222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 26 Jun 2024 13:19:06 +0200 Subject: [PATCH 13/25] Cleanup unused methods --- activation/handler_v2_test.go | 6 +++--- activation/wire/wire_v1.go | 8 -------- activation/wire/wire_v2.go | 4 ---- api/grpcserver/v2alpha1/activation.go | 2 +- common/types/activation.go | 8 -------- 5 files changed, 4 insertions(+), 24 deletions(-) diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index e59f056f7f..579e080a1a 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -533,7 +533,7 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) require.NoError(t, err) - require.Equal(t, atx.TotalNumUnits(), atxFromDb.TotalNumUnits()) + require.Equal(t, atx.TotalNumUnits(), atxFromDb.NumUnits) }) t.Run("second ATX, increases space (nonce valid)", func(t *testing.T) { t.Parallel() @@ -553,7 +553,7 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) require.NoError(t, err) require.EqualValues(t, atx.VRFNonce, atxFromDb.VRFNonce) - require.Equal(t, min(prev.TotalNumUnits(), atx.TotalNumUnits()), atxFromDb.TotalNumUnits()) + require.Equal(t, min(prev.TotalNumUnits(), atx.TotalNumUnits()), atxFromDb.NumUnits) }) t.Run("second ATX, increases space (nonce invalid)", func(t *testing.T) { t.Parallel() @@ -598,7 +598,7 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { // verify that the ATX was added to the DB and it has the lower effective num units atxFromDb, err := atxs.Get(atxHandler.cdb, atx.ID()) require.NoError(t, err) - require.Equal(t, min(prev.TotalNumUnits(), atx.TotalNumUnits()), atxFromDb.TotalNumUnits()) + require.Equal(t, min(prev.TotalNumUnits(), atx.TotalNumUnits()), atxFromDb.NumUnits) require.EqualValues(t, atx.VRFNonce, atxFromDb.VRFNonce) }) t.Run("can't find positioning ATX", func(t *testing.T) { diff --git a/activation/wire/wire_v1.go b/activation/wire/wire_v1.go index 2ad6892527..e0fe9506a3 100644 --- a/activation/wire/wire_v1.go +++ b/activation/wire/wire_v1.go @@ -106,14 +106,6 @@ func (atx *ActivationTxV1) SetID(id types.ATXID) { atx.id = id } -func (atx *ActivationTxV1) Published() types.EpochID { - return atx.PublishEpoch -} - -func (atx *ActivationTxV1) TotalNumUnits() uint32 { - return atx.NumUnits -} - func (atx *ActivationTxV1) Sign(signer *signing.EdSigner) { if atx.PrevATXID == types.EmptyATXID { nodeID := signer.NodeID() diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index a320351152..d439ffa20d 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -136,10 +136,6 @@ func (atx *ActivationTxV2) Sign(signer *signing.EdSigner) { atx.Signature = signer.Sign(signing.ATX, atx.SignedBytes()) } -func (atx *ActivationTxV2) Published() types.EpochID { - return atx.PublishEpoch -} - func (atx *ActivationTxV2) TotalNumUnits() uint32 { var total uint32 for _, post := range atx.NiPosts { diff --git a/api/grpcserver/v2alpha1/activation.go b/api/grpcserver/v2alpha1/activation.go index f85cee3922..d87e6ed399 100644 --- a/api/grpcserver/v2alpha1/activation.go +++ b/api/grpcserver/v2alpha1/activation.go @@ -153,7 +153,7 @@ func toAtx(atx *types.ActivationTx) *spacemeshv2alpha1.Activation { Coinbase: atx.Coinbase.String(), Weight: atx.Weight, Height: atx.TickHeight(), - NumUnits: atx.TotalNumUnits(), + NumUnits: atx.NumUnits, } } diff --git a/common/types/activation.go b/common/types/activation.go index 0bc7d65076..84af7ac196 100644 --- a/common/types/activation.go +++ b/common/types/activation.go @@ -207,14 +207,6 @@ func (atx *ActivationTx) TargetEpoch() EpochID { return atx.PublishEpoch + 1 } -func (atx *ActivationTx) Published() EpochID { - return atx.PublishEpoch -} - -func (atx *ActivationTx) TotalNumUnits() uint32 { - return atx.NumUnits -} - // Golden returns true if atx is from a checkpoint snapshot. // a golden ATX is not verifiable, and is only allowed to be prev atx or positioning atx. func (atx *ActivationTx) Golden() bool { From b7cba29272560b6e6f1da223a7318b326fdb0f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Fri, 28 Jun 2024 20:25:28 +0200 Subject: [PATCH 14/25] in-code migration to fill posts table --- node/node.go | 5 +- sql/atxs/atxs.go | 17 ++- sql/migrations/state/0021_atx_posts.sql | 5 +- sql/migrations/state_0021_migration.go | 156 ++++++++++++++++++++ sql/migrations/state_0021_migration_test.go | 96 ++++++++++++ 5 files changed, 267 insertions(+), 12 deletions(-) create mode 100644 sql/migrations/state_0021_migration.go create mode 100644 sql/migrations/state_0021_migration_test.go diff --git a/node/node.go b/node/node.go index ab76d0ef3c..a44a6bc68d 100644 --- a/node/node.go +++ b/node/node.go @@ -76,6 +76,7 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/layers" "github.com/spacemeshos/go-spacemesh/sql/localsql" dbmetrics "github.com/spacemeshos/go-spacemesh/sql/metrics" + "github.com/spacemeshos/go-spacemesh/sql/migrations" "github.com/spacemeshos/go-spacemesh/syncer" "github.com/spacemeshos/go-spacemesh/syncer/atxsync" "github.com/spacemeshos/go-spacemesh/syncer/blockssync" @@ -1901,14 +1902,16 @@ func (app *App) setupDBs(ctx context.Context, lg log.Log) error { if err := os.MkdirAll(dbPath, os.ModePerm); err != nil { return fmt.Errorf("failed to create %s: %w", dbPath, err) } + dbLog := app.addLogger(StateDbLogger, lg) + m21 := migrations.New0021Migration(dbLog.Zap(), 100_000) migrations, err := sql.StateMigrations() if err != nil { return fmt.Errorf("failed to load migrations: %w", err) } - dbLog := app.addLogger(StateDbLogger, lg) dbopts := []sql.Opt{ sql.WithLogger(dbLog.Zap()), sql.WithMigrations(migrations), + sql.WithMigration(m21), sql.WithConnections(app.Config.DatabaseConnections), sql.WithLatencyMetering(app.Config.DatabaseLatencyMetering), sql.WithVacuumState(app.Config.DatabaseVacuumState), diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 7d266f95c0..154a5e5231 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -453,16 +453,19 @@ func Add(db sql.Executor, atx *types.ActivationTx) error { return fmt.Errorf("insert ATX ID %v: %w", atx.ID(), err) } - enc = func(stmt *sql.Statement) { - stmt.BindBytes(1, atx.ID().Bytes()) - stmt.BindBytes(2, atx.Blob) - stmt.BindInt64(3, int64(atx.Version)) + return AddBlob(db, atx.ID(), atx.Blob, atx.Version) +} + +func AddBlob(db sql.Executor, id types.ATXID, blob []byte, version types.AtxVersion) error { + enc := func(stmt *sql.Statement) { + stmt.BindBytes(1, id.Bytes()) + stmt.BindBytes(2, blob) + stmt.BindInt64(3, int64(version)) } - _, err = db.Exec("insert into atx_blobs (id, atx, version) values (?1, ?2, ?3)", enc, nil) + _, err := db.Exec("insert into atx_blobs (id, atx, version) values (?1, ?2, ?3)", enc, nil) if err != nil { - return fmt.Errorf("insert ATX blob %v: %w", atx.ID(), err) + return fmt.Errorf("insert ATX blob %v: %w", id, err) } - return nil } diff --git a/sql/migrations/state/0021_atx_posts.sql b/sql/migrations/state/0021_atx_posts.sql index e741638f4c..25ec2e2ca5 100644 --- a/sql/migrations/state/0021_atx_posts.sql +++ b/sql/migrations/state/0021_atx_posts.sql @@ -1,11 +1,8 @@ -- Table showing the exact number of PoST units commited by smesher in given ATX. --- TODO(poszu): Migrate data for existing ATXs (require decoding blobs to be correct). --- Alternatively, we could take the effective numUnits from `atxs` table, --- which would be faster but it could cause temporary harm for ATXs growing in size. CREATE TABLE posts ( atxid CHAR(32) NOT NULL, - units INT NOT NULL, pubkey CHAR(32) NOT NULL, + units INT NOT NULL, UNIQUE (atxid, pubkey) ); diff --git a/sql/migrations/state_0021_migration.go b/sql/migrations/state_0021_migration.go new file mode 100644 index 0000000000..c3d89ca27a --- /dev/null +++ b/sql/migrations/state_0021_migration.go @@ -0,0 +1,156 @@ +package migrations + +import ( + "errors" + "fmt" + + "go.uber.org/zap" + + "github.com/spacemeshos/go-spacemesh/activation/wire" + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" +) + +type migration0021 struct { + batch int + logger *zap.Logger +} + +func New0021Migration(log *zap.Logger, batch int) *migration0021 { + return &migration0021{ + logger: log, + batch: batch, + } +} + +func (*migration0021) Name() string { + return "populate posts table with units for each ATX" +} + +func (*migration0021) Order() int { + return 21 +} + +func (*migration0021) Rollback() error { + return nil +} + +func (m *migration0021) Apply(db sql.Executor) error { + if err := m.createTable(db); err != nil { + return err + } + var total int + _, err := db.Exec("SELECT count(*) FROM atx_blobs", nil, func(s *sql.Statement) bool { + total = s.ColumnInt(0) + return false + }) + if err != nil { + return fmt.Errorf("counting all ATXs %w", err) + } + m.logger.Info("applying migration 21", zap.Int("total", total)) + + for offset := 0; ; offset += m.batch { + n, err := m.processBatch(db, offset, m.batch) + if err != nil { + return err + } + + processed := offset + n + progress := float64(processed) * 100.0 / float64(total) + m.logger.Info("processed ATXs", zap.Float64("progress [%]", progress)) + if processed >= total { + return nil + } + } +} + +func (m *migration0021) createTable(db sql.Executor) error { + query := `CREATE TABLE posts ( + atxid CHAR(32) NOT NULL, + pubkey CHAR(32) NOT NULL, + units INT NOT NULL, + UNIQUE (atxid, pubkey) + );` + _, err := db.Exec(query, nil, nil) + if err != nil { + return fmt.Errorf("creating posts table: %w", err) + } + + query = "CREATE INDEX posts_by_atxid_by_pubkey ON posts (atxid, pubkey);" + _, err = db.Exec(query, nil, nil) + if err != nil { + return fmt.Errorf("creating index `posts_by_atxid_by_pubkey`: %w", err) + } + return nil +} + +type update struct { + id types.NodeID + units uint32 +} + +func (m *migration0021) processBatch(db sql.Executor, offset, size int) (int, error) { + var blob sql.Blob + var id types.ATXID + var procErr error + updates := make(map[types.ATXID]*update) + rows, err := db.Exec("SELECT id, atx, version FROM atx_blobs LIMIT ?1 OFFSET ?2", + func(s *sql.Statement) { + s.BindInt64(1, int64(size)) + s.BindInt64(2, int64(offset)) + }, + func(stmt *sql.Statement) bool { + _, procErr = stmt.ColumnReader(0).Read(id[:]) + if procErr != nil { + return false + } + + blob.FromColumn(stmt, 1) + version := types.AtxVersion(stmt.ColumnInt(2)) + + upd, err := processATX(types.AtxBlob{Blob: blob.Bytes, Version: version}) + if err != nil { + procErr = fmt.Errorf("processing ATX %s: %w", id, err) + return false + } + updates[id] = upd + return true + }, + ) + + if err := errors.Join(err, procErr); err != nil { + return 0, fmt.Errorf("getting ATX blobs: %w", err) + } + if rows == 0 { + return 0, nil + } + + if err := m.applyPendingUpdates(db, updates); err != nil { + return 0, fmt.Errorf("applying updates: %w", err) + } + return rows, nil +} + +func (m *migration0021) applyPendingUpdates(db sql.Executor, updates map[types.ATXID]*update) error { + for id, upd := range updates { + atxs.SetUnits(db, id, map[types.NodeID]uint32{upd.id: upd.units}) + } + return nil +} + +func processATX(blob types.AtxBlob) (*update, error) { + switch blob.Version { + case 0: + fallthrough + case types.AtxV1: + var watx wire.ActivationTxV1 + if err := codec.Decode(blob.Blob, &watx); err != nil { + return nil, fmt.Errorf("decoding ATX V1: %w", err) + } + return &update{watx.SmesherID, watx.NumUnits}, nil + default: + return nil, fmt.Errorf("unsupported ATX version: %d", blob.Version) + } +} diff --git a/sql/migrations/state_0021_migration_test.go b/sql/migrations/state_0021_migration_test.go new file mode 100644 index 0000000000..76f50d6d1c --- /dev/null +++ b/sql/migrations/state_0021_migration_test.go @@ -0,0 +1,96 @@ +package migrations + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + + "github.com/spacemeshos/go-spacemesh/activation/wire" + "github.com/spacemeshos/go-spacemesh/codec" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" +) + +// Test that in-code migration results in the same schema as the .sql one. +func Test0021Migration_CompatibleSchema(t *testing.T) { + db := sql.InMemory( + sql.WithLogger(zaptest.NewLogger(t)), + sql.WithMigration(New0021Migration(zaptest.NewLogger(t), 1000)), + ) + + var schemasInCode []string + _, err := db.Exec("SELECT sql FROM sqlite_schema;", nil, func(stmt *sql.Statement) bool { + sql := stmt.ColumnText(0) + sql = strings.Join(strings.Fields(sql), " ") // remove whitespace + schemasInCode = append(schemasInCode, sql) + return true + }) + require.NoError(t, err) + require.NoError(t, db.Close()) + + db = sql.InMemory() + + var schemasInFile []string + _, err = db.Exec("SELECT sql FROM sqlite_schema;", nil, func(stmt *sql.Statement) bool { + sql := stmt.ColumnText(0) + sql = strings.Join(strings.Fields(sql), " ") // remove whitespace + schemasInFile = append(schemasInFile, sql) + return true + }) + require.NoError(t, err) + require.NoError(t, db.Close()) + + require.Equal(t, schemasInFile, schemasInCode) +} + +func Test0021Migration(t *testing.T) { + db := sql.InMemory( + sql.WithLogger(zaptest.NewLogger(t)), + sql.WithSkipMigrations(21), + ) + + var signers [177]*signing.EdSigner + for i := range signers { + var err error + signers[i], err = signing.NewEdSigner() + require.NoError(t, err) + } + type post struct { + id types.NodeID + units uint32 + } + allPosts := make(map[types.EpochID]map[types.ATXID]post) + for epoch := range types.EpochID(40) { + allPosts[epoch] = make(map[types.ATXID]post) + for _, signer := range signers { + watx := wire.ActivationTxV1{ + InnerActivationTxV1: wire.InnerActivationTxV1{ + NumUnits: epoch.Uint32() * 10, + Coinbase: types.Address(types.RandomBytes(24)), + }, + SmesherID: signer.NodeID(), + } + require.NoError(t, atxs.AddBlob(db, watx.ID(), codec.MustEncode(&watx), 0)) + allPosts[epoch][watx.ID()] = post{ + id: signer.NodeID(), + units: watx.NumUnits, + } + } + } + + m := New0021Migration(zaptest.NewLogger(t), 1000) + require.Equal(t, 21, m.Order()) + require.NoError(t, m.Apply(db)) + + for _, posts := range allPosts { + for atx, post := range posts { + units, err := atxs.Units(db, atx, post.id) + require.NoError(t, err) + require.Equal(t, post.units, units) + } + } +} From 7fcfedc6c1161837dd062ddd5162a54f60a529b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Mon, 1 Jul 2024 12:13:04 +0200 Subject: [PATCH 15/25] remove redundant helper --- checkpoint/runner_test.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/checkpoint/runner_test.go b/checkpoint/runner_test.go index ede06468f1..7957766dd5 100644 --- a/checkpoint/runner_test.go +++ b/checkpoint/runner_test.go @@ -253,17 +253,12 @@ func asAtxSnapshot(v *types.ActivationTx, cmt *types.ATXID) types.AtxSnapshot { } } -func addAtx(t testing.TB, db sql.Executor, atx *types.ActivationTx) { - t.Helper() - require.NoError(t, atxs.Add(db, atx)) - require.NoError(t, atxs.SetUnits(db, atx.ID(), map[types.NodeID]uint32{atx.SmesherID: atx.NumUnits})) -} - func createMesh(t testing.TB, db *sql.Database, miners []miner, accts []*types.Account) { t.Helper() for _, miner := range miners { for _, atx := range miner.atxs { - addAtx(t, db, atx) + require.NoError(t, atxs.Add(db, atx)) + require.NoError(t, atxs.SetUnits(db, atx.ID(), map[types.NodeID]uint32{atx.SmesherID: atx.NumUnits})) } if proof := miner.malfeasanceProof; len(proof) > 0 { require.NoError(t, identities.SetMalicious(db, miner.atxs[0].SmesherID, proof, time.Now())) From a3e79200acc79b15579c7663aece6d26b9bed3c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Mon, 1 Jul 2024 12:27:28 +0200 Subject: [PATCH 16/25] avoid underflow --- activation/handler_v2.go | 2 +- activation/handler_v2_test.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 20c891aeb5..b3fba2c79a 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -420,7 +420,7 @@ func (h *HandlerV2) equivocationSet(atx *wire.ActivationTxV2) ([]types.NodeID, e if err != nil { return nil, fmt.Errorf("fetching marriage atx: %w", err) } - if !(marriageAtx.PublishEpoch <= atx.PublishEpoch-2) { + if marriageAtx.PublishEpoch+2 > atx.PublishEpoch { return nil, fmt.Errorf( "marriage atx must be published at least 2 epochs before %v (is %v)", atx.PublishEpoch, diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 579e080a1a..d68529db67 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -799,7 +799,7 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { merged.PreviousATXs = previousATXs merged.Sign(sig) - atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) + atxHandler.mclock.EXPECT().CurrentLayer().Return(merged.PublishEpoch.FirstLayer()) atxHandler.expectFetchDeps(merged) atxHandler.expectVerifyNIPoSTs(merged, equivocationSet, []uint64{200}) @@ -836,7 +836,7 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { merged.PreviousATXs = previousATXs merged.Sign(sig) - atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) + atxHandler.mclock.EXPECT().CurrentLayer().Return(merged.PublishEpoch.FirstLayer()) p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) require.ErrorContains(t, err, "ID present twice (duplicated marriage index)") require.Nil(t, p) @@ -868,7 +868,7 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { merged.PreviousATXs = previousATXs merged.Sign(sig) - atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) + atxHandler.mclock.EXPECT().CurrentLayer().Return(merged.PublishEpoch.FirstLayer()) atxHandler.expectFetchDeps(merged) p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) require.Error(t, err) @@ -885,6 +885,7 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { } prev := atxs.CheckpointAtx{ + Epoch: mATX.PublishEpoch + 1, ID: types.RandomATXID(), CommitmentATX: types.RandomATXID(), SmesherID: sig.NodeID(), @@ -937,7 +938,7 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) { merged.MarriageATX = &mATXID merged.Sign(sig) - atxHandler.mclock.EXPECT().CurrentLayer().Return(postGenesisEpoch.FirstLayer()) + atxHandler.mclock.EXPECT().CurrentLayer().Return(merged.PublishEpoch.FirstLayer()) atxHandler.expectFetchDeps(merged) p, err = atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) require.Error(t, err) From 8899c8ec7b97f27d472170c49872b7bbaa7721c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Mon, 1 Jul 2024 12:33:13 +0200 Subject: [PATCH 17/25] small cleanups --- activation/handler_v2.go | 8 ++++---- sql/atxs/atxs_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/activation/handler_v2.go b/activation/handler_v2.go index b3fba2c79a..981107b840 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -503,7 +503,7 @@ func (h *HandlerV2) syntacticallyValidateDeps( } } - prevAtxs := make([]*types.ActivationTx, len(atx.PreviousATXs)) + previousAtxs := make([]*types.ActivationTx, len(atx.PreviousATXs)) for i, prev := range atx.PreviousATXs { prevAtx, err := atxs.Get(h.cdb, prev) if err != nil { @@ -513,12 +513,12 @@ func (h *HandlerV2) syntacticallyValidateDeps( err := fmt.Errorf("previous atx is too new (%d >= %d) (%s) ", prevAtx.PublishEpoch, atx.PublishEpoch, prev) return nil, nil, err } - prevAtxs[i] = prevAtx + previousAtxs[i] = prevAtx } equivocationSet, err := h.equivocationSet(atx) if err != nil { - return nil, nil, fmt.Errorf("validating marriages: %w", err) + return nil, nil, fmt.Errorf("calculating equivocation set: %w", err) } // validate previous ATXs @@ -535,7 +535,7 @@ func (h *HandlerV2) syntacticallyValidateDeps( effectiveNumUnits := post.NumUnits if atx.Initial == nil { var err error - effectiveNumUnits, err = h.validatePreviousAtx(id, &post, prevAtxs) + effectiveNumUnits, err = h.validatePreviousAtx(id, &post, previousAtxs) if err != nil { return nil, nil, fmt.Errorf("validating previous atx: %w", err) } diff --git a/sql/atxs/atxs_test.go b/sql/atxs/atxs_test.go index fc2e5fd7fa..fd1a828f4f 100644 --- a/sql/atxs/atxs_test.go +++ b/sql/atxs/atxs_test.go @@ -1136,7 +1136,7 @@ func TestUnits(t *testing.T) { db := sql.InMemory() atxID := types.RandomATXID() require.NoError(t, atxs.SetUnits(db, atxID, map[types.NodeID]uint32{{1, 2, 3}: 10})) - _, err := atxs.Units(db, types.RandomATXID(), types.RandomNodeID()) + _, err := atxs.Units(db, atxID, types.RandomNodeID()) require.ErrorIs(t, err, sql.ErrNotFound) }) t.Run("returns units for given smesher in given ATX", func(t *testing.T) { From 69d7e58064cf5262975546ffd73ccc42e53694b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Mon, 1 Jul 2024 12:56:19 +0200 Subject: [PATCH 18/25] increase migration 21 batch size to 1M ATXs --- node/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/node.go b/node/node.go index a44a6bc68d..e54058d44d 100644 --- a/node/node.go +++ b/node/node.go @@ -1903,7 +1903,7 @@ func (app *App) setupDBs(ctx context.Context, lg log.Log) error { return fmt.Errorf("failed to create %s: %w", dbPath, err) } dbLog := app.addLogger(StateDbLogger, lg) - m21 := migrations.New0021Migration(dbLog.Zap(), 100_000) + m21 := migrations.New0021Migration(dbLog.Zap(), 1_000_000) migrations, err := sql.StateMigrations() if err != nil { return fmt.Errorf("failed to load migrations: %w", err) From 6bf6bd2f37b239bc79bdade49eb5cfdf46ea6231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Mon, 1 Jul 2024 13:02:55 +0200 Subject: [PATCH 19/25] update checkpoint JSON schema --- checkpoint/schema.json | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/checkpoint/schema.json b/checkpoint/schema.json index b3cafdb124..88254a0b80 100644 --- a/checkpoint/schema.json +++ b/checkpoint/schema.json @@ -49,9 +49,6 @@ "vrfNonce": { "type": "integer" }, - "numUnits": { - "type": "integer" - }, "baseTickHeight": { "type": "integer" }, @@ -66,8 +63,18 @@ }, "coinbase": { "type": "string" + }, + "numUnits": { + "type": "integer" + }, + "units": { + "type": "object", + "additionalProperties": { + "type": "integer" + } } - } + }, + "required": ["id", "epoch", "commitmentAtx", "vrfNonce", "baseTickHeight", "tickCount", "publicKey", "sequence", "coinbase", "numUnits", "units"] }, "accounts": { "description": "accounts snapshot", @@ -99,4 +106,3 @@ } } } - From a7089fbe0f770024512a7761f2a22002693c5bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Tue, 2 Jul 2024 14:44:14 +0200 Subject: [PATCH 20/25] review feedback --- activation/e2e/atx_merge_test.go | 17 ++++--------- activation/handler_v1.go | 2 +- activation/handler_v2.go | 10 ++++---- activation/handler_v2_test.go | 15 ++++++------ api/grpcserver/admin_service_test.go | 2 +- checkpoint/runner_test.go | 2 +- sql/atxs/atxs.go | 33 ++++++++++++-------------- sql/atxs/atxs_test.go | 8 ++++--- sql/migrations/state_0021_migration.go | 8 +++++-- 9 files changed, 47 insertions(+), 50 deletions(-) diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index b061460933..52c38f246d 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -37,7 +37,7 @@ import ( "github.com/spacemeshos/go-spacemesh/timesync" ) -func constructMerkleProof(t *testing.T, members []types.Hash32, ids map[uint64]bool) wire.MerkleProofV2 { +func constructMerkleProof(t testing.TB, members []types.Hash32, ids map[uint64]bool) wire.MerkleProofV2 { t.Helper() tree, err := merkle.NewTreeBuilder(). @@ -169,18 +169,14 @@ func createMerged( return atx } -func signers(t *testing.T, keysHex []string) []*signing.EdSigner { +func signers(t testing.TB, keysHex []string) []*signing.EdSigner { t.Helper() - var keys [][]byte + signers := make([]*signing.EdSigner, 0, len(keysHex)) for _, k := range keysHex { key, err := hex.DecodeString(k) require.NoError(t, err) - keys = append(keys, key) - } - signers := []*signing.EdSigner{} - for _, key := range keys { sig, err := signing.NewEdSigner(signing.WithPrivateKey(key)) require.NoError(t, err) signers = append(signers, sig) @@ -315,7 +311,6 @@ func Test_MarryAndMerge(t *testing.T) { publish := types.EpochID(1) var niposts [2]nipostData var initialPosts [2]*types.Post - eg = errgroup.Group{} for i, signer := range signers { eg.Go(func() error { post, postInfo, err := nb.Proof(context.Background(), signer.NodeID(), types.EmptyHash32[:], nil) @@ -389,12 +384,10 @@ func Test_MarryAndMerge(t *testing.T) { // Step 2. Publish merged ATX together publish = marriageATX.PublishEpoch + 2 - eg = errgroup.Group{} - // 2.1. NiPOST for main ID (the publisher) eg.Go(func() error { n, err := buildNipost(nb, mainID, publish, marriageATX.ID(), marriageATX.ID()) - logger.Info("built NiPoST", zap.Any("post", niposts[0])) + logger.Info("built NiPoST", zap.Any("post", n)) niposts[0] = n return err }) @@ -455,7 +448,6 @@ func Test_MarryAndMerge(t *testing.T) { // Step 4. Publish merged using the same previous now // Publish by the other signer this time. publish = mergedATX.PublishEpoch + 1 - eg = errgroup.Group{} for i, sig := range signers { eg.Go(func() error { n, err := buildNipost(nb, sig, publish, mergedATX.ID(), mergedATX.ID()) @@ -505,7 +497,6 @@ func Test_MarryAndMerge(t *testing.T) { // Step 5. Make an emergency split and publish separately publish = mergedATX2.PublishEpoch + 1 - eg = errgroup.Group{} for i, sig := range signers { eg.Go(func() error { n, err := buildNipost(nb, sig, publish, mergedATX2.ID(), mergedATX2.ID()) diff --git a/activation/handler_v1.go b/activation/handler_v1.go index 30c994752a..847739699e 100644 --- a/activation/handler_v1.go +++ b/activation/handler_v1.go @@ -554,7 +554,7 @@ func (h *HandlerV1) storeAtx( if err != nil && !errors.Is(err, sql.ErrObjectExists) { return fmt.Errorf("add atx to db: %w", err) } - err = atxs.SetUnits(tx, atx.ID(), map[types.NodeID]uint32{atx.SmesherID: watx.NumUnits}) + err = atxs.SetUnits(tx, atx.ID(), atx.SmesherID, watx.NumUnits) if err != nil && !errors.Is(err, sql.ErrObjectExists) { return fmt.Errorf("set atx units: %w", err) } diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 981107b840..6993b05751 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -726,7 +726,7 @@ func (h *HandlerV2) storeAtx( } } if !malicious && proof == nil { - // We check for malfeasance again becase the marriage increased the equivocation set. + // We check for malfeasance again because the marriage increased the equivocation set. malicious, err = identities.IsMalicious(tx, atx.SmesherID) if err != nil { return fmt.Errorf("re-checking if smesherID is malicious: %w", err) @@ -738,9 +738,11 @@ func (h *HandlerV2) storeAtx( if err != nil && !errors.Is(err, sql.ErrObjectExists) { return fmt.Errorf("add atx to db: %w", err) } - err = atxs.SetUnits(tx, atx.ID(), units) - if err != nil && !errors.Is(err, sql.ErrObjectExists) { - return fmt.Errorf("set atx units: %w", err) + for id, units := range units { + err = atxs.SetUnits(tx, atx.ID(), id, units) + if err != nil && !errors.Is(err, sql.ErrObjectExists) { + return fmt.Errorf("setting atx units for ID %s: %w", id, err) + } } return nil }); err != nil { diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index d68529db67..fe54df6413 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -179,7 +179,7 @@ func (h *handlerMocks) expectMergedAtxV2( h.expectStoreAtxV2(atx) } -func (h *v2TestHandler) createAndProcessInitial(t *testing.T, sig *signing.EdSigner) *wire.ActivationTxV2 { +func (h *v2TestHandler) createAndProcessInitial(t testing.TB, sig *signing.EdSigner) *wire.ActivationTxV2 { t.Helper() atx := newInitialATXv2(t, h.handlerMocks.goldenATXID) atx.Sign(sig) @@ -189,13 +189,13 @@ func (h *v2TestHandler) createAndProcessInitial(t *testing.T, sig *signing.EdSig return atx } -func (h *v2TestHandler) processInitial(t *testing.T, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) { +func (h *v2TestHandler) processInitial(t testing.TB, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) { t.Helper() h.expectInitialAtxV2(atx) return h.processATX(context.Background(), peer.ID("peer"), atx, codec.MustEncode(atx), time.Now()) } -func (h *v2TestHandler) processSoloAtx(t *testing.T, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) { +func (h *v2TestHandler) processSoloAtx(t testing.TB, atx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) { t.Helper() h.expectAtxV2(atx) return h.processATX(context.Background(), peer.ID("peer"), atx, codec.MustEncode(atx), time.Now()) @@ -618,7 +618,7 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { } func marryIDs( - t *testing.T, + t testing.TB, atxHandler *v2TestHandler, sig *signing.EdSigner, golden types.ATXID, @@ -1337,7 +1337,7 @@ func Test_ValidatePreviousATX(t *testing.T) { t.Parallel() prev := &types.ActivationTx{} prev.SetID(types.RandomATXID()) - require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), map[types.NodeID]uint32{types.RandomNodeID(): 13})) + require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), types.RandomNodeID(), 13)) _, err := atxHandler.validatePreviousAtx(types.RandomNodeID(), &wire.SubPostV2{}, []*types.ActivationTx{prev}) require.Error(t, err) @@ -1348,7 +1348,8 @@ func Test_ValidatePreviousATX(t *testing.T) { other := types.RandomNodeID() prev := &types.ActivationTx{} prev.SetID(types.RandomATXID()) - require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), map[types.NodeID]uint32{id: 7, other: 13})) + require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), id, 7)) + require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), other, 13)) units, err := atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 100}, []*types.ActivationTx{prev}) require.NoError(t, err) @@ -1368,7 +1369,7 @@ func Test_ValidatePreviousATX(t *testing.T) { other := types.RandomNodeID() prev := &types.ActivationTx{} prev.SetID(types.RandomATXID()) - require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), map[types.NodeID]uint32{other: 13})) + require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), other, 13)) _, err := atxHandler.validatePreviousAtx(id, &wire.SubPostV2{NumUnits: 100}, []*types.ActivationTx{prev}) require.Error(t, err) diff --git a/api/grpcserver/admin_service_test.go b/api/grpcserver/admin_service_test.go index 641a5a6b07..526b5420af 100644 --- a/api/grpcserver/admin_service_test.go +++ b/api/grpcserver/admin_service_test.go @@ -38,7 +38,7 @@ func newAtx(tb testing.TB, db *sql.Database) { atx.SmesherID = types.BytesToNodeID(types.RandomBytes(20)) atx.SetReceived(time.Now().Local()) require.NoError(tb, atxs.Add(db, atx)) - require.NoError(tb, atxs.SetUnits(db, atx.ID(), map[types.NodeID]uint32{atx.SmesherID: atx.NumUnits})) + require.NoError(tb, atxs.SetUnits(db, atx.ID(), atx.SmesherID, atx.NumUnits)) } func createMesh(tb testing.TB, db *sql.Database) { diff --git a/checkpoint/runner_test.go b/checkpoint/runner_test.go index 7957766dd5..676b993f20 100644 --- a/checkpoint/runner_test.go +++ b/checkpoint/runner_test.go @@ -258,7 +258,7 @@ func createMesh(t testing.TB, db *sql.Database, miners []miner, accts []*types.A for _, miner := range miners { for _, atx := range miner.atxs { require.NoError(t, atxs.Add(db, atx)) - require.NoError(t, atxs.SetUnits(db, atx.ID(), map[types.NodeID]uint32{atx.SmesherID: atx.NumUnits})) + require.NoError(t, atxs.SetUnits(db, atx.ID(), atx.SmesherID, atx.NumUnits)) } if proof := miner.malfeasanceProof; len(proof) > 0 { require.NoError(t, identities.SetMalicious(db, miner.atxs[0].SmesherID, proof, time.Now())) diff --git a/sql/atxs/atxs.go b/sql/atxs/atxs.go index 154a5e5231..f1a8ba5450 100644 --- a/sql/atxs/atxs.go +++ b/sql/atxs/atxs.go @@ -640,8 +640,10 @@ func AddCheckpointed(db sql.Executor, catx *CheckpointAtx) error { return fmt.Errorf("insert checkpoint ATX blob %v: %w", catx.ID, err) } - if err := SetUnits(db, catx.ID, catx.Units); err != nil { - return fmt.Errorf("insert checkpoint ATX units %v: %w", catx.ID, err) + for id, units := range catx.Units { + if err := SetUnits(db, catx.ID, id, units); err != nil { + return fmt.Errorf("insert checkpoint ATX units %v: %w", catx.ID, err) + } } return nil @@ -896,20 +898,15 @@ func Units(db sql.Executor, atxID types.ATXID, nodeID types.NodeID) (uint32, err return units, err } -func SetUnits(db sql.Executor, atxID types.ATXID, units map[types.NodeID]uint32) error { - for nodeID, u := range units { - _, err := db.Exec(` - INSERT INTO posts (atxid, pubkey, units) VALUES (?1, ?2, ?3);`, - func(stmt *sql.Statement) { - stmt.BindBytes(1, atxID.Bytes()) - stmt.BindBytes(2, nodeID.Bytes()) - stmt.BindInt64(3, int64(u)) - }, nil, - ) - if err != nil { - return fmt.Errorf("set units for ID %s in ATX %s: %w", nodeID, atxID, err) - } - } - - return nil +func SetUnits(db sql.Executor, atxID types.ATXID, id types.NodeID, units uint32) error { + _, err := db.Exec( + `INSERT INTO posts (atxid, pubkey, units) VALUES (?1, ?2, ?3);`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, atxID.Bytes()) + stmt.BindBytes(2, id.Bytes()) + stmt.BindInt64(3, int64(units)) + }, + nil, + ) + return err } diff --git a/sql/atxs/atxs_test.go b/sql/atxs/atxs_test.go index fd1a828f4f..b856db4190 100644 --- a/sql/atxs/atxs_test.go +++ b/sql/atxs/atxs_test.go @@ -173,7 +173,7 @@ func TestLatestN(t *testing.T) { for _, atx := range []*types.ActivationTx{atx1, atx2, atx3, atx4, atx5, atx6} { require.NoError(t, atxs.Add(db, atx)) - require.NoError(t, atxs.SetUnits(db, atx.ID(), map[types.NodeID]uint32{atx.SmesherID: atx.NumUnits})) + require.NoError(t, atxs.SetUnits(db, atx.ID(), atx.SmesherID, atx.NumUnits)) } for _, tc := range []struct { @@ -1135,7 +1135,7 @@ func TestUnits(t *testing.T) { t.Parallel() db := sql.InMemory() atxID := types.RandomATXID() - require.NoError(t, atxs.SetUnits(db, atxID, map[types.NodeID]uint32{{1, 2, 3}: 10})) + require.NoError(t, atxs.SetUnits(db, atxID, types.RandomNodeID(), 10)) _, err := atxs.Units(db, atxID, types.RandomNodeID()) require.ErrorIs(t, err, sql.ErrNotFound) }) @@ -1147,7 +1147,9 @@ func TestUnits(t *testing.T) { {1, 2, 3}: 10, {4, 5, 6}: 20, } - require.NoError(t, atxs.SetUnits(db, atxID, units)) + for id, units := range units { + require.NoError(t, atxs.SetUnits(db, atxID, id, units)) + } nodeID := types.NodeID{1, 2, 3} got, err := atxs.Units(db, atxID, nodeID) diff --git a/sql/migrations/state_0021_migration.go b/sql/migrations/state_0021_migration.go index c3d89ca27a..8a98145e57 100644 --- a/sql/migrations/state_0021_migration.go +++ b/sql/migrations/state_0021_migration.go @@ -134,13 +134,17 @@ func (m *migration0021) processBatch(db sql.Executor, offset, size int) (int, er } func (m *migration0021) applyPendingUpdates(db sql.Executor, updates map[types.ATXID]*update) error { - for id, upd := range updates { - atxs.SetUnits(db, id, map[types.NodeID]uint32{upd.id: upd.units}) + for atxID, upd := range updates { + if err := atxs.SetUnits(db, atxID, upd.id, upd.units); err != nil { + return err + } } return nil } func processATX(blob types.AtxBlob) (*update, error) { + // The migration adding the version column does not set it to 1 for existing ATXs. + // Thus, both values 0 and 1 mean V1. switch blob.Version { case 0: fallthrough From 8f09fb4d19adecc30596f378007c0441e35eb427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Tue, 2 Jul 2024 15:32:37 +0200 Subject: [PATCH 21/25] Simpify code around poet membership proof validation --- activation/handler_v2.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 6993b05751..0ba311584e 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -550,6 +550,9 @@ func (h *HandlerV2) syntacticallyValidateDeps( indexedChallenges := make(map[uint64][]byte) for _, post := range niposts.Posts { + if _, ok := indexedChallenges[post.MembershipLeafIndex]; ok { + continue + } nipostChallenge := wire.NIPostChallengeV2{ PublishEpoch: atx.PublishEpoch, PositioningATXID: atx.PositioningATX, @@ -559,17 +562,12 @@ func (h *HandlerV2) syntacticallyValidateDeps( } else { nipostChallenge.PrevATXID = atx.PreviousATXs[post.PrevATXIndex] } - if _, ok := indexedChallenges[post.MembershipLeafIndex]; !ok { - indexedChallenges[post.MembershipLeafIndex] = nipostChallenge.Hash().Bytes() - } + indexedChallenges[post.MembershipLeafIndex] = nipostChallenge.Hash().Bytes() } - leafIndicies := make([]uint64, 0, len(indexedChallenges)) - for i := range indexedChallenges { - leafIndicies = append(leafIndicies, i) - } + leafIndicies := maps.Keys(indexedChallenges) slices.Sort(leafIndicies) - poetChallenges := make([][]byte, 0, len(indexedChallenges)) + poetChallenges := make([][]byte, 0, len(leafIndicies)) for _, i := range leafIndicies { poetChallenges = append(poetChallenges, indexedChallenges[i]) } From 7ec660268bbd3f6a8ffbd836b7963112bb2747a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 3 Jul 2024 11:01:41 +0200 Subject: [PATCH 22/25] review feedback --- activation/e2e/atx_merge_test.go | 17 +++++++++++------ activation/handler_v2_test.go | 3 +-- common/fixture/atxs.go | 18 ------------------ 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index 52c38f246d..06ca20b50c 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -62,6 +62,7 @@ type nipostData struct { } func buildNipost( + ctx context.Context, nb *activation.NIPostBuilder, signer *signing.EdSigner, publish types.EpochID, @@ -73,7 +74,7 @@ func buildNipost( PositioningATX: positioning, } challenge := wire.NIPostChallengeToWireV2(postChallenge).Hash() - nipost, err := nb.BuildNIPost(context.Background(), signer, challenge, postChallenge) + nipost, err := nb.BuildNIPost(ctx, signer, challenge, postChallenge) nb.ResetState(signer.NodeID()) return nipostData{previous, nipost}, err } @@ -230,7 +231,7 @@ func Test_MarryAndMerge(t *testing.T) { poetDb := activation.NewPoetDb(db, logger.Named("poetDb")) validator := activation.NewValidator(db, poetDb, cfg, opts.Scrypt, verifier) - var eg errgroup.Group + eg, ctx := errgroup.WithContext(context.Background()) for i, sig := range signers { opts := opts opts.DataDir = t.TempDir() @@ -311,6 +312,7 @@ func Test_MarryAndMerge(t *testing.T) { publish := types.EpochID(1) var niposts [2]nipostData var initialPosts [2]*types.Post + eg, ctx = errgroup.WithContext(context.Background()) for i, signer := range signers { eg.Go(func() error { post, postInfo, err := nb.Proof(context.Background(), signer.NodeID(), types.EmptyHash32[:], nil) @@ -384,9 +386,10 @@ func Test_MarryAndMerge(t *testing.T) { // Step 2. Publish merged ATX together publish = marriageATX.PublishEpoch + 2 + eg, ctx = errgroup.WithContext(context.Background()) // 2.1. NiPOST for main ID (the publisher) eg.Go(func() error { - n, err := buildNipost(nb, mainID, publish, marriageATX.ID(), marriageATX.ID()) + n, err := buildNipost(ctx, nb, mainID, publish, marriageATX.ID(), marriageATX.ID()) logger.Info("built NiPoST", zap.Any("post", n)) niposts[0] = n return err @@ -396,7 +399,7 @@ func Test_MarryAndMerge(t *testing.T) { prevATXID, err := atxs.GetLastIDByNodeID(db, mergedID.NodeID()) require.NoError(t, err) eg.Go(func() error { - n, err := buildNipost(nb, mergedID, publish, prevATXID, marriageATX.ID()) + n, err := buildNipost(ctx, nb, mergedID, publish, prevATXID, marriageATX.ID()) logger.Info("built NiPoST", zap.Any("post", n)) niposts[1] = n return err @@ -447,10 +450,11 @@ func Test_MarryAndMerge(t *testing.T) { // Step 4. Publish merged using the same previous now // Publish by the other signer this time. + eg, ctx = errgroup.WithContext(context.Background()) publish = mergedATX.PublishEpoch + 1 for i, sig := range signers { eg.Go(func() error { - n, err := buildNipost(nb, sig, publish, mergedATX.ID(), mergedATX.ID()) + n, err := buildNipost(ctx, nb, sig, publish, mergedATX.ID(), mergedATX.ID()) logger.Info("built NiPoST", zap.Any("post", n)) niposts[i] = n return err @@ -497,9 +501,10 @@ func Test_MarryAndMerge(t *testing.T) { // Step 5. Make an emergency split and publish separately publish = mergedATX2.PublishEpoch + 1 + eg, ctx = errgroup.WithContext(context.Background()) for i, sig := range signers { eg.Go(func() error { - n, err := buildNipost(nb, sig, publish, mergedATX2.ID(), mergedATX2.ID()) + n, err := buildNipost(ctx, nb, sig, publish, mergedATX2.ID(), mergedATX2.ID()) logger.Info("built NiPoST", zap.Any("post", n)) niposts[i] = n return err diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index fe54df6413..3718f2bd25 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -18,7 +18,6 @@ import ( "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/fixture" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" mwire "github.com/spacemeshos/go-spacemesh/malfeasance/wire" @@ -121,7 +120,7 @@ func (h *handlerMocks) expectVerifyNIPoSTs( } func (h *handlerMocks) expectStoreAtxV2(atx *wire.ActivationTxV2) { - h.mbeacon.EXPECT().OnAtx(fixture.MatchId(atx.ID())) + h.mbeacon.EXPECT().OnAtx(gomock.Cond(func(a any) bool { return a.(*types.ActivationTx).ID() == atx.ID() })) h.mtortoise.EXPECT().OnAtx(atx.PublishEpoch+1, atx.ID(), gomock.Any()) h.mValidator.EXPECT().IsVerifyingFullPost().Return(false) } diff --git a/common/fixture/atxs.go b/common/fixture/atxs.go index 74226aca21..af3ead08f2 100644 --- a/common/fixture/atxs.go +++ b/common/fixture/atxs.go @@ -77,21 +77,3 @@ func ToAtx(t testing.TB, watx *wire.ActivationTxV1) *types.ActivationTx { atx.TickCount = 1 return atx } - -type idMatcher types.ATXID - -func MatchId(id types.ATXID) idMatcher { - return idMatcher(id) -} - -func (m idMatcher) Matches(x any) bool { - type hasID interface { - ID() types.ATXID - } - v, ok := x.(hasID) - return ok && v.ID() == types.ATXID(m) -} - -func (m idMatcher) String() string { - return "is ATX ID " + types.ATXID(m).String() -} From eddbd13d0f3999e6f13c3518e8213260a6fbc3fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Wed, 3 Jul 2024 14:21:07 +0200 Subject: [PATCH 23/25] Fix after merge with develop --- activation/e2e/atx_merge_test.go | 19 ++----------------- activation/e2e/nipost_test.go | 5 ----- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index 06ca20b50c..f93885498e 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -206,18 +206,11 @@ func Test_MarryAndMerge(t *testing.T) { logger := zaptest.NewLogger(t) goldenATX := types.ATXID{2, 3, 4} coinbase := types.Address{1, 2, 3, 4, 5, 6, 7} - cfg := activation.DefaultPostConfig() + cfg := testPostConfig() db := sql.InMemory() cdb := datastore.NewCachedDB(db, logger) localDB := localsql.InMemory() - syncer := activation.NewMocksyncer(ctrl) - syncer.EXPECT().RegisterForATXSynced().DoAndReturn(func() <-chan struct{} { - synced := make(chan struct{}) - close(synced) - return synced - }).AnyTimes() - svc := grpcserver.NewPostService(logger) svc.AllowConnections(true) grpcCfg, cleanup := launchServer(t, svc) @@ -239,15 +232,7 @@ func Test_MarryAndMerge(t *testing.T) { totalNumUnits += units[i] eg.Go(func() error { - mgr, err := activation.NewPostSetupManager(cfg, logger, db, atxsdata.New(), goldenATX, syncer, validator) - require.NoError(t, err) - - t.Cleanup(launchPostSupervisor(t, zap.NewNop(), mgr, sig, grpcCfg, opts)) - - require.Eventually(t, func() bool { - _, err := svc.Client(sig.NodeID()) - return err == nil - }, 10*time.Second, 100*time.Millisecond, "timed out waiting for connection") + initPost(t, cfg, opts, sig, goldenATX, grpcCfg, svc) return nil }) } diff --git a/activation/e2e/nipost_test.go b/activation/e2e/nipost_test.go index 8f959fa16a..defbd852c4 100644 --- a/activation/e2e/nipost_test.go +++ b/activation/e2e/nipost_test.go @@ -140,11 +140,6 @@ func initPost( mgr, err := activation.NewPostSetupManager(cfg, logger, db, atxsdata.New(), golden, syncer, nil) require.NoError(tb, err) - // Create data. - require.NoError(tb, mgr.PrepareInitializer(context.Background(), opts, sig.NodeID())) - require.NoError(tb, mgr.StartSession(context.Background(), sig.NodeID())) - require.Equal(tb, activation.PostSetupStateComplete, mgr.Status().State) - stop := launchPostSupervisor(tb, logger, mgr, sig, grpcCfg, cfg, opts) tb.Cleanup(stop) require.Eventually(tb, func() bool { From f7454c3d34a5573887c1dfdc7d7140779c8f1549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 4 Jul 2024 09:35:39 +0200 Subject: [PATCH 24/25] small cleanup of test --- activation/e2e/atx_merge_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index f93885498e..e597983bc9 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -205,7 +205,6 @@ func Test_MarryAndMerge(t *testing.T) { logger := zaptest.NewLogger(t) goldenATX := types.ATXID{2, 3, 4} - coinbase := types.Address{1, 2, 3, 4, 5, 6, 7} cfg := testPostConfig() db := sql.InMemory() cdb := datastore.NewCachedDB(db, logger) @@ -405,7 +404,6 @@ func Test_MarryAndMerge(t *testing.T) { []types.ATXID{marriageATX.ID(), prevATXID}, membershipProof, ) - mergedATX.Coinbase = coinbase mergedATX.VRFNonce = nonces[0] mergedATX.Sign(mainID) @@ -459,7 +457,6 @@ func Test_MarryAndMerge(t *testing.T) { []types.ATXID{mergedATX.ID()}, membershipProof, ) - mergedATX2.Coinbase = coinbase mergedATX2.VRFNonce = nonces[1] mergedATX2.Sign(signers[1]) From ec6e1d5ed6a50b106d42cf7131099b497c98571a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 4 Jul 2024 09:36:11 +0200 Subject: [PATCH 25/25] don't modify released migration --- sql/migrations/state/0019_marriages.sql | 1 - .../state/{0020_atx_weight.sql => 0020_atx_merge.sql} | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) rename sql/migrations/state/{0020_atx_weight.sql => 0020_atx_merge.sql} (50%) diff --git a/sql/migrations/state/0019_marriages.sql b/sql/migrations/state/0019_marriages.sql index 799f36c7d0..66bc7d1128 100644 --- a/sql/migrations/state/0019_marriages.sql +++ b/sql/migrations/state/0019_marriages.sql @@ -1,2 +1 @@ ALTER TABLE identities ADD COLUMN marriage_atx CHAR(32); -ALTER TABLE identities ADD COLUMN marriage_idx INTEGER; diff --git a/sql/migrations/state/0020_atx_weight.sql b/sql/migrations/state/0020_atx_merge.sql similarity index 50% rename from sql/migrations/state/0020_atx_weight.sql rename to sql/migrations/state/0020_atx_merge.sql index 9fa69ec669..5c8f03afd7 100644 --- a/sql/migrations/state/0020_atx_weight.sql +++ b/sql/migrations/state/0020_atx_merge.sql @@ -1,2 +1,6 @@ +-- Changes required to handle merged ATXs + ALTER TABLE atxs ADD COLUMN weight INTEGER; UPDATE atxs SET weight = effective_num_units * tick_count; + +ALTER TABLE identities ADD COLUMN marriage_idx INTEGER;