diff --git a/Makefile b/Makefile index b883b15b6a..f6af69b1ef 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,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 6ee9ed80c0..217217e3d4 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -809,14 +809,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 new file mode 100644 index 0000000000..e597983bc9 --- /dev/null +++ b/activation/e2e/atx_merge_test.go @@ -0,0 +1,518 @@ +package activation_test + +import ( + "context" + "encoding/hex" + "slices" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/peer" + "github.com/spacemeshos/merkle-tree" + "github.com/spacemeshos/poet/shared" + "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/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" + "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/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" + "github.com/spacemeshos/go-spacemesh/system" + smocks "github.com/spacemeshos/go-spacemesh/system/mocks" + "github.com/spacemeshos/go-spacemesh/timesync" +) + +func constructMerkleProof(t testing.TB, members []types.Hash32, ids map[uint64]bool) wire.MerkleProofV2 { + t.Helper() + + 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)) + } + return wire.MerkleProofV2{Nodes: nodesH32} +} + +type nipostData struct { + previous types.ATXID + *nipost.NIPostState +} + +func buildNipost( + ctx context.Context, + nb *activation.NIPostBuilder, + signer *signing.EdSigner, + publish types.EpochID, + previous, positioning types.ATXID, +) (nipostData, error) { + postChallenge := &types.NIPostChallenge{ + PublishEpoch: publish, + PrevATXID: previous, + PositioningATX: positioning, + } + challenge := wire.NIPostChallengeToWireV2(postChallenge).Hash() + nipost, err := nb.BuildNIPost(ctx, signer, challenge, postChallenge) + nb.ResetState(signer.NodeID()) + 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, + 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( + t testing.TB, + 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), + }, + }, + } + // Append PoSTs for all IDs + for i, nipost := range niposts { + idx := slices.IndexFunc(previous, func(a types.ATXID) bool { return a == nipost.previous }) + require.NotEqual(t, -1, idx) + 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 +} + +func signers(t testing.TB, keysHex []string) []*signing.EdSigner { + t.Helper() + + signers := make([]*signing.EdSigner, 0, len(keysHex)) + for _, k := range keysHex { + key, err := hex.DecodeString(k) + require.NoError(t, err) + + 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} + cfg := testPostConfig() + db := sql.InMemory() + cdb := datastore.NewCachedDB(db, logger) + localDB := localsql.InMemory() + + 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) + + eg, ctx := errgroup.WithContext(context.Background()) + for i, sig := range signers { + opts := opts + opts.DataDir = t.TempDir() + opts.NumUnits = units[i] + totalNumUnits += units[i] + + eg.Go(func() error { + initPost(t, cfg, opts, sig, goldenATX, grpcCfg, svc) + return nil + }) + } + require.NoError(t, eg.Wait()) + + // ensure that genesis aligns with layer timings + genesis := time.Now().Round(layerDuration) + epoch := layersPerEpoch * layerDuration + poetCfg := activation.PoetConfig{ + PhaseShift: epoch, + CycleGap: epoch / 2, + GracePeriod: epoch / 5, + } + + client := ae2e.NewTestPoetClient(2) + poetSvc := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) + + 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, + validator, + activation.WithPoetServices(poetSvc), + ) + require.NoError(t, err) + + mpub := mocks.NewMockPublisher(ctrl) + mFetch := smocks.NewMockFetcher(ctrl) + mBeacon := activation.NewMockAtxReceiver(ctrl) + mTortoise := smocks.NewMockTortoise(ctrl) + + tickSize := uint64(3) + atxHdlr := activation.NewHandler( + "local", + cdb, + atxsdata.New(), + signing.NewEdVerifier(), + clock, + mpub, + mFetch, + goldenATX, + validator, + mBeacon, + mTortoise, + logger, + activation.WithAtxVersions(activation.AtxVersions{0: types.AtxV2}), + activation.WithTickSize(tickSize), + ) + + // Step 1. Marry + 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) + if err != nil { + return err + } + + postChallenge := &types.NIPostChallenge{ + PublishEpoch: publish, + PositioningATX: goldenATX, + InitialPost: post, + } + challenge := wire.NIPostChallengeToWireV2(postChallenge).Hash() + nipost, err := nb.BuildNIPost(context.Background(), signer, challenge, postChallenge) + if err != nil { + return err + } + nb.ResetState(signer.NodeID()) + + initialPosts[i] = post + nonces[i] = uint64(*postInfo.Nonce) + niposts[i] = nipostData{types.EmptyATXID, nipost} + return nil + }) + } + require.NoError(t, eg.Wait()) + + // mainID will create marriage ATX + mainID, mergedID := signers[0], signers[1] + + mergedIdAtx := createInitialAtx(publish, goldenATX, goldenATX, niposts[1].NIPostState, initialPosts[1]) + mergedIdAtx.Sign(mergedID) + + marriageATX := createInitialAtx(publish, goldenATX, goldenATX, niposts[0].NIPostState, initialPosts[0]) + marriageATX.Marriages = []wire.MarriageCertificate{ + { + Signature: mainID.Sign(signing.MARRIAGE, mainID.NodeID().Bytes()), + }, + { + ReferenceAtx: mergedIdAtx.ID(), + 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(), []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)) + 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 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(ctx, nb, mainID, publish, marriageATX.ID(), marriageATX.ID()) + logger.Info("built NiPoST", zap.Any("post", n)) + niposts[0] = n + return err + }) + + // 2.2. NiPOST for merged ID + prevATXID, err := atxs.GetLastIDByNodeID(db, mergedID.NodeID()) + require.NoError(t, err) + eg.Go(func() error { + n, err := buildNipost(ctx, nb, mergedID, publish, prevATXID, marriageATX.ID()) + logger.Info("built NiPoST", zap.Any("post", n)) + niposts[1] = n + return err + }) + require.NoError(t, eg.Wait()) + + // 2.3 Construct a multi-ID poet membership merkle proof for both IDs + poetProof, members, err := poetSvc.Proof(context.Background(), "1") + require.NoError(t, err) + membershipProof := constructMerkleProof(t, members, map[uint64]bool{0: true, 1: true}) + + mergedATX := createMerged( + t, + niposts[:], + publish, + marriageATX.ID(), + marriageATX.ID(), + []types.ATXID{marriageATX.ID(), prevATXID}, + membershipProof, + ) + mergedATX.VRFNonce = nonces[0] + 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()) + 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 3. 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) + require.Equal(t, uint64(totalNumUnits)*atx.TickCount, atx.Weight) + + posATX, err := atxs.Get(db, marriageATX.ID()) + require.NoError(t, err) + require.Equal(t, posATX.TickHeight(), atx.BaseTickHeight) + + // 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(ctx, 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 = poetSvc.Proof(context.Background(), "2") + require.NoError(t, err) + membershipProof = constructMerkleProof(t, members, map[uint64]bool{0: true}) + + mergedATX2 := createMerged( + t, + niposts[:], + publish, + marriageATX.ID(), + mergedATX.ID(), + []types.ATXID{mergedATX.ID()}, + membershipProof, + ) + 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()) + 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) + require.Equal(t, uint64(totalNumUnits)*atx.TickCount, atx.Weight) + + posATX, err = atxs.Get(db, mergedATX.ID()) + require.NoError(t, err) + require.Equal(t, posATX.TickHeight(), atx.BaseTickHeight) + + // 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(ctx, 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()) + + <-clock.AwaitLayer(publish.FirstLayer()) + 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/e2e/builds_atx_v2_test.go b/activation/e2e/builds_atx_v2_test.go index dd9864b534..f4d8060af0 100644 --- a/activation/e2e/builds_atx_v2_test.go +++ b/activation/e2e/builds_atx_v2_test.go @@ -184,7 +184,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/e2e/certifier_client_test.go b/activation/e2e/certifier_client_test.go index 66105de4e7..809ce2cd94 100644 --- a/activation/e2e/certifier_client_test.go +++ b/activation/e2e/certifier_client_test.go @@ -183,6 +183,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/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 { 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_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..847739699e 100644 --- a/activation/handler_v1.go +++ b/activation/handler_v1.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math/bits" "sync" "time" @@ -167,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, @@ -223,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) @@ -590,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(), 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) @@ -683,6 +652,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 a369c50aaf..0ba311584e 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -1,10 +1,12 @@ package activation import ( + "cmp" "context" "errors" "fmt" "math" + "math/bits" "slices" "time" @@ -121,13 +123,15 @@ 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}, } + if watx.Initial == nil { // FIXME: update to keep many previous ATXs to support merged ATXs atx.PrevATXID = watx.PreviousATXs[0] @@ -141,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) } @@ -152,8 +156,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 +232,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 { @@ -311,71 +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) { - if post.PrevATXIndex > uint32(len(prevAtxs)) { +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: - // TODO: support previous merged-ATX - - // previous is 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) + 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 { @@ -419,7 +373,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 { @@ -435,14 +390,103 @@ 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 } +// 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): + 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+2 > atx.PublishEpoch { + 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 + ticks uint64 + weight uint64 effectiveUnits uint32 + units map[types.NodeID]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 totalUnits uint64 + var totalWeight uint64 + for _, ns := range n { + totalUnits += uint64(ns.units) + + 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 + } + 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 { + 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. @@ -450,52 +494,105 @@ 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)) + previousAtxs := 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 } - // 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} - 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 + equivocationSet, err := h.equivocationSet(atx) + if err != nil { + return nil, nil, fmt.Errorf("calculating equivocation set: %w", err) + } + // validate previous ATXs + 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) 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 + nipostSizes[i].addUnits(effectiveNumUnits) + } + } + + // validate poet membership proofs + for i, niposts := range atx.NiPosts { + // verify PoET memberships in a single go + 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, + } + if atx.Initial != nil { + nipostChallenge.InitialPost = &atx.Initial.Post + } else { + nipostChallenge.PrevATXID = atx.PreviousATXs[post.PrevATXIndex] + } + indexedChallenges[post.MembershipLeafIndex] = nipostChallenge.Hash().Bytes() + } + leafIndicies := maps.Keys(indexedChallenges) + slices.Sort(leafIndicies) + poetChallenges := make([][]byte, 0, len(leafIndicies)) + for _, i := range leafIndicies { + poetChallenges = append(poetChallenges, indexedChallenges[i]) + } + + membership := types.MultiMerkleProof{ + Nodes: niposts.Membership.Nodes, + LeafIndices: leafIndicies, + } + leaves, err := h.nipostValidator.PoetMembership(ctx, &membership, niposts.Challenge, poetChallenges) + if err != nil { + return nil, nil, fmt.Errorf("invalid poet membership: %w", err) + } + nipostSizes[i].ticks = leaves / h.tickSize + } + + parts.effectiveUnits, parts.weight, err = nipostSizes.sumUp() + if err != nil { + return nil, nil, err + } + + // 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 if atx.Initial != nil { commitment = atx.Initial.CommitmentATX @@ -505,7 +602,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 } } @@ -531,47 +628,26 @@ 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()) + parts.units[id] = post.NumUnits } - 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{ - leaves: minLeaves, - effectiveUnits: totalEffectiveNumUnits, } 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) } } - return parts, nil, nil + parts.ticks = nipostSizes.minTicks() + + return &parts, nil, nil } func (h *HandlerV2) checkMalicious( - ctx context.Context, tx *sql.Tx, watx *wire.ActivationTxV2, marrying []types.NodeID, @@ -584,7 +660,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) } @@ -602,11 +678,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 { @@ -632,6 +704,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 @@ -639,19 +712,19 @@ 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) } 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 } } 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) @@ -663,6 +736,12 @@ func (h *HandlerV2) storeAtx( if err != nil && !errors.Is(err, sql.ErrObjectExists) { return fmt.Errorf("add atx to db: %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 { return nil, fmt.Errorf("store atx: %w", err) diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 3de4a113eb..3718f2bd25 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -3,6 +3,8 @@ package activation import ( "context" "errors" + "math" + "slices" "testing" "time" @@ -31,7 +33,15 @@ type v2TestHandler struct { handlerMocks } -const poetLeaves = 200 +type marriedId struct { + signer *signing.EdSigner + refAtx *wire.ActivationTxV2 +} + +const ( + tickSize = 20 + poetLeaves = 200 +) func newV2TestHandler(tb testing.TB, golden types.ATXID) *v2TestHandler { lg := zaptest.NewLogger(tb) @@ -44,7 +54,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, @@ -83,9 +93,35 @@ func (h *handlerMocks) expectVerifyNIPoST(atx *wire.ActivationTxV2) { ).Return(poetLeaves, nil) } +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(), + 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(), + ).Return(poetLeaves[i], nil) + } +} + 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(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) } @@ -125,21 +161,45 @@ func (h *handlerMocks) expectAtxV2(atx *wire.ActivationTxV2) { h.expectStoreAtxV2(atx) } -func (h *v2TestHandler) createAndProcessInitial(t *testing.T, sig *signing.EdSigner) *wire.ActivationTxV2 { +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( + atx.SmesherID, + gomock.Any(), + atx.VRFNonce, + atx.TotalNumUnits(), + ) + h.expectVerifyNIPoSTs(atx, equivocationSet, poetLeaves) + h.expectStoreAtxV2(atx) +} + +func (h *v2TestHandler) createAndProcessInitial(t testing.TB, sig *signing.EdSigner) *wire.ActivationTxV2 { 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.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.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()) +} + func TestHandlerV2_SyntacticallyValidate(t *testing.T) { t.Parallel() golden := types.RandomATXID() @@ -374,15 +434,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") }) } @@ -400,6 +464,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()) @@ -411,64 +476,40 @@ 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()) require.NoError(t, err) require.Nil(t, proof) }) - t.Run("second ATX, previous V1", func(t *testing.T) { + t.Run("second ATX", func(t *testing.T) { t.Parallel() atxHandler := newV2TestHandler(t, golden) - prev := newInitialATXv1(t, golden) - prev.Sign(sig) - atxs.Add(atxHandler.cdb, toAtx(t, prev)) + 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) + 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) - // copies coinbase and VRF nonce from the previous ATX - require.Equal(t, prev.Coinbase, atxFromDb.Coinbase) - require.EqualValues(t, *prev.VRFNonce, atxFromDb.VRFNonce) - }) - 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) - - atx := newSoloATXv2(t, prev.PublishEpoch+1, prev.ID(), golden) - atx.Sign(sig) - blob = codec.MustEncode(atx) - atxHandler.expectAtxV2(atx) - - proof, err = atxHandler.processATX(context.Background(), peer, atx, blob, time.Now()) + prevAtx, err := atxs.Get(atxHandler.cdb, prev.ID()) require.NoError(t, err) - require.Nil(t, proof) - - _, err = atxs.Get(atxHandler.cdb, atx.ID()) + 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() @@ -477,8 +518,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)) @@ -487,45 +529,39 @@ 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.NumUnits) }) - 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) - 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) - // 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) + require.Equal(t, min(prev.TotalNumUnits(), atx.TotalNumUnits()), atxFromDb.NumUnits) }) - 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) - 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()) atxHandler.expectFetchDeps(atx) @@ -533,7 +569,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")) @@ -543,21 +579,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() - 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) 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 = prev.TotalNumUnits() - 1 atx.Sign(sig) atxHandler.expectAtxV2(atx) @@ -568,7 +597,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.NumUnits) require.EqualValues(t, atx.VRFNonce, atxFromDb.VRFNonce) }) t.Run("can't find positioning ATX", func(t *testing.T) { @@ -587,6 +616,335 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) { }) } +func marryIDs( + t testing.TB, + atxHandler *v2TestHandler, + sig *signing.EdSigner, + golden types.ATXID, + num int, +) (marriage *wire.ActivationTxV2, other []*wire.ActivationTxV2) { + mATX := newInitialATXv2(t, golden) + mATX.Marriages = []wire.MarriageCertificate{{ + Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }} + + 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: atx.ID(), + Signature: signer.Sign(signing.MARRIAGE, sig.NodeID().Bytes()), + }) + } + + 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) + + return mATX, other +} + +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, []uint64{poetLeaves}) + 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) + 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) + + // 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(merged.PublishEpoch.FirstLayer()) + atxHandler.expectFetchDeps(merged) + 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") + 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(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) + }) + 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(merged.PublishEpoch.FirstLayer()) + atxHandler.expectFetchDeps(merged) + p, err := atxHandler.processATX(context.Background(), "", merged, codec.MustEncode(merged), time.Now()) + 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{ + Epoch: mATX.PublishEpoch + 1, + 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(merged.PublishEpoch.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) { goldenATX := types.RandomATXID() prev0 := types.RandomATXID() @@ -788,57 +1146,140 @@ func Test_ValidatePositioningAtx(t *testing.T) { }) } -func Test_LoadPreviousATX(t *testing.T) { +func Test_ValidateMarriages(t *testing.T) { t.Parallel() - t.Run("not found", func(t *testing.T) { + 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, types.RandomATXID()) - _, err := atxHandler.previous(context.Background(), types.RandomATXID()) - require.ErrorContains(t, err, "not found") + 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("golden not found", func(t *testing.T) { + t.Run("smesher is not married", 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") + 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("golden", func(t *testing.T) { + t.Run("marriage ATX must be published 2 epochs prior merging IDs", 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()) + 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.Equal(t, golden.ID(), atx.ID()) + 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("v1", func(t *testing.T) { + t.Run("can't use somebody else's marriage ATX", 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()) + atxHandler := newV2TestHandler(t, golden) + + otherSigner, err := signing.NewEdSigner() require.NoError(t, err) - require.Equal(t, prev.ID(), atx.ID()) + 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("v2", func(t *testing.T) { + t.Run("smesher is married", 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, - }, + 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()) } - prev.SetID(prevWire.ID()) - require.NoError(t, atxs.Add(atxHandler.cdb, prev)) - atx, err := atxHandler.previous(context.Background(), prev.ID()) + marriage.Sign(sig) + + p, err := atxHandler.processInitial(t, 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, prev.ID(), atx.ID()) + require.Equal(t, expectedSet, set) }) } @@ -891,68 +1332,46 @@ 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.Run("smesher ID not present", 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.Parallel() - 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") + prev := &types.ActivationTx{} + prev.SetID(types.RandomATXID()) + 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) }) - 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(), id, 7)) + require.NoError(t, atxs.SetUnits(atxHandler.cdb, prev.ID(), 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(), 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) }) } @@ -980,15 +1399,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) @@ -1000,17 +1417,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) { @@ -1019,6 +1434,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(), @@ -1062,15 +1478,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")) @@ -1115,10 +1522,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) @@ -1134,7 +1540,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) @@ -1262,7 +1700,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()) @@ -1278,6 +1716,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.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/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/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/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 db3bf9d022..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 { @@ -196,8 +192,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 +201,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 +224,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 types.Hash32 + binary.LittleEndian.PutUint64(leafIndex[:], sp.MembershipLeafIndex) + tree.AddLeaf(leafIndex[:]) + tree.AddLeaf(sp.Post.Root()) numUnits := make([]byte, 4) @@ -235,7 +239,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 +339,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{ diff --git a/api/grpcserver/admin_service_test.go b/api/grpcserver/admin_service_test.go index 3be5b22351..526b5420af 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(), atx.SmesherID, atx.NumUnits)) } func createMesh(tb testing.TB, db *sql.Database) { 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 65adf72805..d87e6ed399 100644 --- a/api/grpcserver/v2alpha1/activation.go +++ b/api/grpcserver/v2alpha1/activation.go @@ -151,9 +151,9 @@ func toAtx(atx *types.ActivationTx) *spacemeshv2alpha1.Activation { SmesherId: atx.SmesherID.Bytes(), PublishEpoch: atx.PublishEpoch.Uint32(), Coinbase: atx.Coinbase.String(), - Weight: atx.GetWeight(), + Weight: atx.Weight, Height: atx.TickHeight(), - NumUnits: atx.TotalNumUnits(), + NumUnits: atx.NumUnits, } } 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/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 7d985a1275..826622405f 100644 --- a/checkpoint/recovery.go +++ b/checkpoint/recovery.go @@ -354,6 +354,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 ec37e8db19..bd171d9d60 100644 --- a/checkpoint/recovery_test.go +++ b/checkpoint/recovery_test.go @@ -830,6 +830,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..676b993f20 100644 --- a/checkpoint/runner_test.go +++ b/checkpoint/runner_test.go @@ -249,6 +249,7 @@ 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}, } } @@ -257,6 +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(), 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/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 @@ } } } - 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..84af7ac196 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,39 +201,12 @@ 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 { 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 { @@ -238,16 +218,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 +240,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 +370,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/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/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 9f7ae21941..7c24a673b5 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) @@ -368,8 +367,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]) @@ -381,9 +379,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 9b5ce40a79..0ab3093f05 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/node/node.go b/node/node.go index 3aa50a879f..bbd457f3b7 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" @@ -1902,14 +1903,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(), 1_000_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/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..f1a8ba5450 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,27 +441,31 @@ 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) } - 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 } @@ -535,12 +540,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. @@ -563,6 +571,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 } @@ -581,6 +590,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 } @@ -612,6 +639,13 @@ func AddCheckpointed(db sql.Executor, catx *CheckpointAtx) error { if err != nil { return fmt.Errorf("insert checkpoint ATX blob %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 } @@ -776,7 +810,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) }, ) @@ -844,3 +878,35 @@ 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, 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 af55b5473e..b856db4190 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(), atx.SmesherID, atx.NumUnits)) } for _, tc := range []struct { @@ -1121,3 +1122,43 @@ 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, types.RandomNodeID(), 10)) + _, 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) { + t.Parallel() + db := sql.InMemory() + atxID := types.RandomATXID() + units := map[types.NodeID]uint32{ + {1, 2, 3}: 10, + {4, 5, 6}: 20, + } + 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) + 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/identities/identities.go b/sql/identities/identities.go index 613e19326f..c610724017 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" @@ -33,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) { @@ -143,17 +146,43 @@ 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 { + stmt.ColumnBytes(0, atx[:]) + index = int(stmt.ColumnInt64(1)) + } + return false + }) + if err != nil { + return atx, 0, fmt.Errorf("getting marriage ATX for %v: %w", id, err) + } + if rows == 0 { + return atx, 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 +217,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..9b3be5e8e1 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/0020_atx_merge.sql b/sql/migrations/state/0020_atx_merge.sql new file mode 100644 index 0000000000..5c8f03afd7 --- /dev/null +++ b/sql/migrations/state/0020_atx_merge.sql @@ -0,0 +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; diff --git a/sql/migrations/state/0021_atx_posts.sql b/sql/migrations/state/0021_atx_posts.sql new file mode 100644 index 0000000000..25ec2e2ca5 --- /dev/null +++ b/sql/migrations/state/0021_atx_posts.sql @@ -0,0 +1,9 @@ +-- Table showing the exact number of PoST units commited by smesher in given ATX. +CREATE TABLE posts ( + atxid CHAR(32) NOT NULL, + pubkey CHAR(32) NOT NULL, + units INT NOT NULL, + UNIQUE (atxid, pubkey) +); + +CREATE INDEX posts_by_atxid_by_pubkey ON posts (atxid, pubkey); diff --git a/sql/migrations/state_0021_migration.go b/sql/migrations/state_0021_migration.go new file mode 100644 index 0000000000..8a98145e57 --- /dev/null +++ b/sql/migrations/state_0021_migration.go @@ -0,0 +1,160 @@ +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 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 + 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) + } + } +} 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 {