diff --git a/activation/activation.go b/activation/activation.go index 7923f4ef46..46f7d9ae34 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -15,10 +15,10 @@ import ( "go.uber.org/zap/zapcore" "golang.org/x/exp/maps" "golang.org/x/sync/errgroup" + "golang.org/x/sync/singleflight" "github.com/spacemeshos/go-spacemesh/activation/metrics" "github.com/spacemeshos/go-spacemesh/activation/wire" - "github.com/spacemeshos/go-spacemesh/atxsdata" "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/events" @@ -27,14 +27,11 @@ import ( "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "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/localsql/localatxs" "github.com/spacemeshos/go-spacemesh/sql/localsql/nipost" ) -var ( - ErrNotFound = errors.New("not found") - errNilVrfNonce = errors.New("nil VRF nonce") -) +var errNilVrfNonce = errors.New("nil VRF nonce") // PoetConfig is the configuration to interact with the poet server. type PoetConfig struct { @@ -81,11 +78,11 @@ type Config struct { // it is responsible for initializing post, receiving poet proof and orchestrating nipost after which it will // calculate total weight and providing relevant view as proof. type Builder struct { - accountLock sync.RWMutex - coinbaseAccount types.Address - conf Config - db sql.Executor - atxsdata *atxsdata.Data + accountLock sync.RWMutex + coinbaseAccount types.Address + conf Config + atxSvc AtxService + localDB sql.LocalDatabase publisher pubsub.Publisher nipostBuilder nipostBuilder @@ -97,8 +94,6 @@ type Builder struct { poets []PoetService poetCfg PoetConfig poetRetryInterval time.Duration - // delay before PoST in ATX is considered valid (counting from the time it was received) - postValidityDelay time.Duration // ATX versions versions []atxVersion @@ -115,22 +110,19 @@ type Builder struct { stop context.CancelFunc } +type foundPosAtx struct { + id types.ATXID + forPublish types.EpochID +} + type positioningAtxFinder struct { - finding sync.Mutex - found *struct { - id types.ATXID - forPublish types.EpochID - } + finding singleflight.Group + found foundPosAtx + logger *zap.Logger } type BuilderOption func(*Builder) -func WithPostValidityDelay(delay time.Duration) BuilderOption { - return func(b *Builder) { - b.postValidityDelay = delay - } -} - // WithPoetRetryInterval modifies time that builder will have to wait before retrying ATX build process // if it failed due to issues with PoET server. func WithPoetRetryInterval(interval time.Duration) BuilderOption { @@ -160,12 +152,6 @@ func WithPoets(poets ...PoetService) BuilderOption { } } -func WithValidator(v nipostValidator) BuilderOption { - return func(b *Builder) { - b.validator = v - } -} - func WithPostStates(ps PostStates) BuilderOption { return func(b *Builder) { b.postStates = ps @@ -181,10 +167,10 @@ func BuilderAtxVersions(v AtxVersions) BuilderOption { // NewBuilder returns an atx builder that will start a routine that will attempt to create an atx upon each new layer. func NewBuilder( conf Config, - db sql.Executor, - atxsdata *atxsdata.Data, localDB sql.LocalDatabase, + atxService AtxService, publisher pubsub.Publisher, + nipostValidator nipostValidator, nipostBuilder nipostBuilder, layerClock layerClock, syncer syncer, @@ -195,18 +181,20 @@ func NewBuilder( parentCtx: context.Background(), signers: make(map[types.NodeID]*signing.EdSigner), conf: conf, - db: db, - atxsdata: atxsdata, localDB: localDB, publisher: publisher, + atxSvc: atxService, + validator: nipostValidator, nipostBuilder: nipostBuilder, layerClock: layerClock, syncer: syncer, logger: log, poetRetryInterval: defaultPoetRetryInterval, - postValidityDelay: 12 * time.Hour, postStates: NewPostStates(log), versions: []atxVersion{{0, types.AtxV1}}, + posAtxFinder: positioningAtxFinder{ + logger: log, + }, } for _, opt := range opts { opt(b) @@ -349,7 +337,7 @@ func (b *Builder) SmesherIDs() []types.NodeID { func (b *Builder) BuildInitialPost(ctx context.Context, nodeID types.NodeID) error { // Generate the initial POST if we don't have an ATX... - if _, err := atxs.GetLastIDByNodeID(b.db, nodeID); err == nil { + if _, err := b.atxSvc.LastATX(ctx, nodeID); err == nil { return nil } // ...and if we haven't stored an initial post yet. @@ -530,11 +518,11 @@ func (b *Builder) BuildNIPostChallenge(ctx context.Context, nodeID types.NodeID) // Start building new challenge: // 1. get previous ATX - prevAtx, err := b.GetPrevAtx(nodeID) + prevAtx, err := b.atxSvc.LastATX(ctx, nodeID) switch { case err == nil: currentEpochId = max(currentEpochId, prevAtx.PublishEpoch) - case errors.Is(err, sql.ErrNotFound): + case errors.Is(err, ErrNotFound): // no previous ATX case err != nil: return nil, fmt.Errorf("get last ATX: %w", err) @@ -580,11 +568,11 @@ func (b *Builder) BuildNIPostChallenge(ctx context.Context, nodeID types.NodeID) // 4. build new challenge logger.Info("building new NiPOST challenge", zap.Uint32("current_epoch", currentEpochId.Uint32())) - prevAtx, err = b.GetPrevAtx(nodeID) + prevAtx, err = b.atxSvc.LastATX(ctx, nodeID) var challenge *types.NIPostChallenge switch { - case errors.Is(err, sql.ErrNotFound): + case errors.Is(err, ErrNotFound): logger.Info("no previous ATX found, creating an initial nipost challenge") challenge, err = b.buildInitialNIPostChallenge(ctx, logger, nodeID, publishEpochId) if err != nil { @@ -700,14 +688,6 @@ func (b *Builder) buildInitialNIPostChallenge( }, nil } -func (b *Builder) GetPrevAtx(nodeID types.NodeID) (*types.ActivationTx, error) { - id, err := atxs.GetLastIDByNodeID(b.db, nodeID) - if err != nil { - return nil, fmt.Errorf("getting last ATXID: %w", err) - } - return atxs.Get(b.db, id) -} - // SetCoinbase sets the address rewardAddress to be the coinbase account written into the activation transaction // the rewards for blocks made by this miner will go to this address. func (b *Builder) SetCoinbase(rewardAddress types.Address) { @@ -755,6 +735,11 @@ func (b *Builder) PublishActivationTx(ctx context.Context, sig *signing.EdSigner case <-b.layerClock.AwaitLayer(challenge.PublishEpoch.FirstLayer()): } + err = localatxs.AddBlob(b.localDB, challenge.PublishEpoch, atx.ID(), sig.NodeID(), codec.MustEncode(atx)) + if err != nil { + b.logger.Warn("failed to persist built ATX into the local DB - regossiping won't work", zap.Error(err)) + } + for { b.logger.Info( "broadcasting ATX", @@ -847,7 +832,7 @@ func (b *Builder) createAtx( case challenge.PrevATXID == types.EmptyATXID: atx.VRFNonce = (*uint64)(&nipostState.VRFNonce) default: - oldNonce, err := atxs.NonceByID(b.db, challenge.PrevATXID) + prevAtx, err := b.atxSvc.Atx(ctx, challenge.PrevATXID) if err != nil { b.logger.Warn("failed to get VRF nonce for ATX", zap.Error(err), @@ -855,12 +840,12 @@ func (b *Builder) createAtx( ) break } - if nipostState.VRFNonce != oldNonce { + if nipostState.VRFNonce != prevAtx.VRFNonce { b.logger.Info( "attaching a new VRF nonce in ATX", log.ZShortStringer("smesherID", sig.NodeID()), zap.Uint64("new nonce", uint64(nipostState.VRFNonce)), - zap.Uint64("old nonce", uint64(oldNonce)), + zap.Uint64("old nonce", uint64(prevAtx.VRFNonce)), ) atx.VRFNonce = (*uint64)(&nipostState.VRFNonce) } @@ -919,54 +904,36 @@ func (b *Builder) broadcast(ctx context.Context, atx scale.Encodable) (int, erro return len(buf), nil } -// searchPositioningAtx returns atx id with the highest tick height. -// Publish epoch is used for caching the positioning atx. -func (b *Builder) searchPositioningAtx( +// find returns atx id with the highest tick height. +// The publish epoch (of the built ATX) is used for: +// - caching the positioning atx, +// - filtering candidates for positioning atx (it must be published in an earlier epoch than built ATX). +// +// It always returns an ATX, falling back to the golden one as the last resort. +func (f *positioningAtxFinder) find( ctx context.Context, - nodeID types.NodeID, + atxs AtxService, publish types.EpochID, ) (types.ATXID, error) { - logger := b.logger.With(log.ZShortStringer("smesherID", nodeID), zap.Uint32("publish epoch", publish.Uint32())) - - b.posAtxFinder.finding.Lock() - defer b.posAtxFinder.finding.Unlock() - - if found := b.posAtxFinder.found; found != nil && found.forPublish == publish { - logger.Debug("using cached positioning atx", log.ZShortStringer("atx_id", found.id)) - return found.id, nil - } + logger := f.logger.With(zap.Uint32("publish epoch", publish.Uint32())) - latestPublished, err := atxs.LatestEpoch(b.db) - if err != nil { - return types.EmptyATXID, fmt.Errorf("get latest epoch: %w", err) - } + atx, err, _ := f.finding.Do(publish.String(), func() (any, error) { + if f.found.forPublish == publish { + logger.Debug("using cached positioning atx", log.ZShortStringer("atx_id", f.found.id)) + return f.found.id, nil + } - logger.Info("searching for positioning atx", zap.Uint32("latest_epoch", latestPublished.Uint32())) - - // positioning ATX publish epoch must be lower than the publish epoch of built ATX - positioningAtxPublished := min(latestPublished, publish-1) - id, err := findFullyValidHighTickAtx( - ctx, - b.atxsdata, - positioningAtxPublished, - b.conf.GoldenATXID, - b.validator, - logger, - VerifyChainOpts.AssumeValidBefore(time.Now().Add(-b.postValidityDelay)), - VerifyChainOpts.WithTrustedID(nodeID), - VerifyChainOpts.WithLogger(b.logger), - ) - if err != nil { - logger.Info("search failed - using golden atx as positioning atx", zap.Error(err)) - id = b.conf.GoldenATXID - } + id, err := atxs.PositioningATX(ctx, publish-1) + if err != nil { + return types.EmptyATXID, err + } - b.posAtxFinder.found = &struct { - id types.ATXID - forPublish types.EpochID - }{id, publish} + logger.Debug("found candidate positioning atx", log.ZShortStringer("id", id)) + f.found = foundPosAtx{id, publish} + return id, nil + }) - return id, nil + return atx.(types.ATXID), err } // getPositioningAtx returns the positioning ATX. @@ -978,16 +945,12 @@ func (b *Builder) getPositioningAtx( publish types.EpochID, previous *types.ActivationTx, ) (types.ATXID, error) { - id, err := b.searchPositioningAtx(ctx, nodeID, publish) + id, err := b.posAtxFinder.find(ctx, b.atxSvc, publish) if err != nil { - return types.EmptyATXID, err + b.logger.Warn("failed to find positioning ATX - falling back to golden", zap.Error(err)) + id = b.conf.GoldenATXID } - b.logger.Debug("found candidate positioning atx", - log.ZShortStringer("id", id), - log.ZShortStringer("smesherID", nodeID), - ) - if previous == nil { b.logger.Info("selected positioning atx", log.ZShortStringer("id", id), @@ -1004,9 +967,10 @@ func (b *Builder) getPositioningAtx( return id, nil } - candidate, err := atxs.Get(b.db, id) + candidate, err := b.atxSvc.Atx(ctx, id) if err != nil { - return types.EmptyATXID, fmt.Errorf("get candidate pos ATX %s: %w", id.ShortString(), err) + b.logger.Warn("failed to get candidate pos ATX - falling back to previous", zap.Error(err)) + return previous.ID(), nil } if previous.TickHeight() >= candidate.TickHeight() { @@ -1024,23 +988,17 @@ func (b *Builder) getPositioningAtx( func (b *Builder) Regossip(ctx context.Context, nodeID types.NodeID) error { epoch := b.layerClock.CurrentLayer().GetEpoch() - atx, err := atxs.GetIDByEpochAndNodeID(b.db, epoch, nodeID) + id, blob, err := localatxs.AtxBlob(b.localDB, epoch, nodeID) if errors.Is(err, sql.ErrNotFound) { return nil } else if err != nil { return err } - var blob sql.Blob - if _, err := atxs.LoadBlob(ctx, b.db, atx.Bytes(), &blob); err != nil { - return fmt.Errorf("get blob %s: %w", atx.ShortString(), err) - } - if len(blob.Bytes) == 0 { - return nil // checkpoint - } - if err := b.publisher.Publish(ctx, pubsub.AtxProtocol, blob.Bytes); err != nil { - return fmt.Errorf("republish %s: %w", atx.ShortString(), err) + + if err := b.publisher.Publish(ctx, pubsub.AtxProtocol, blob); err != nil { + return fmt.Errorf("republishing ATX %s: %w", id, err) } - b.logger.Debug("re-gossipped atx", log.ZShortStringer("smesherID", nodeID), log.ZShortStringer("atx", atx)) + b.logger.Debug("re-gossipped atx", log.ZShortStringer("smesherID", nodeID), log.ZShortStringer("atx ID", id)) return nil } @@ -1053,41 +1011,3 @@ func (b *Builder) version(publish types.EpochID) types.AtxVersion { } return version } - -func findFullyValidHighTickAtx( - ctx context.Context, - atxdata *atxsdata.Data, - publish types.EpochID, - goldenATXID types.ATXID, - validator nipostValidator, - logger *zap.Logger, - opts ...VerifyChainOption, -) (types.ATXID, error) { - var found *types.ATXID - - // iterate trough epochs, to get first valid, not malicious ATX with the biggest height - atxdata.IterateHighTicksInEpoch(publish+1, func(id types.ATXID) (contSearch bool) { - logger.Debug("found candidate for high-tick atx", log.ZShortStringer("id", id)) - if ctx.Err() != nil { - return false - } - // verify ATX-candidate by getting their dependencies (previous Atx, positioning ATX etc.) - // and verifying PoST for every dependency - if err := validator.VerifyChain(ctx, id, goldenATXID, opts...); err != nil { - logger.Debug("rejecting candidate for high-tick atx", zap.Error(err), log.ZShortStringer("id", id)) - return true - } - found = &id - return false - }) - - if ctx.Err() != nil { - return types.ATXID{}, ctx.Err() - } - - if found == nil { - return types.ATXID{}, ErrNotFound - } - - return *found, nil -} diff --git a/activation/activation_multi_test.go b/activation/activation_multi_test.go index ce795e53b5..064dc4a98d 100644 --- a/activation/activation_multi_test.go +++ b/activation/activation_multi_test.go @@ -18,7 +18,7 @@ import ( "github.com/spacemeshos/go-spacemesh/p2p/pubsub" "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/localsql/localatxs" "github.com/spacemeshos/go-spacemesh/sql/localsql/nipost" ) @@ -188,46 +188,22 @@ func TestRegossip(t *testing.T) { }) t.Run("success", func(t *testing.T) { - goldenATXID := types.RandomATXID() tab := newTestBuilder(t, 5) - var refAtx *types.ActivationTx - + var ( + smesher types.NodeID + blob []byte + ) for _, sig := range tab.signers { - atx := newInitialATXv1(t, goldenATXID) - atx.PublishEpoch = layer.GetEpoch() - atx.Sign(sig) - vAtx := toAtx(t, atx) - require.NoError(t, atxs.Add(tab.db, vAtx, atx.Blob())) - - if refAtx == nil { - refAtx = vAtx - } + smesher = sig.NodeID() + blob = types.RandomBytes(20) + localatxs.AddBlob(tab.localDb, layer.GetEpoch(), types.RandomATXID(), smesher, blob) } - var blob sql.Blob - ver, err := atxs.LoadBlob(context.Background(), tab.db, refAtx.ID().Bytes(), &blob) - require.NoError(t, err) - require.Equal(t, types.AtxV1, ver) - // atx will be regossiped once (by the smesher) tab.mclock.EXPECT().CurrentLayer().Return(layer) ctx := context.Background() - tab.mpub.EXPECT().Publish(ctx, pubsub.AtxProtocol, blob.Bytes) - require.NoError(t, tab.Regossip(ctx, refAtx.SmesherID)) - }) - - t.Run("checkpointed", func(t *testing.T) { - tab := newTestBuilder(t, 5) - for _, sig := range tab.signers { - atx := atxs.CheckpointAtx{ - ID: types.RandomATXID(), - Epoch: layer.GetEpoch(), - SmesherID: sig.NodeID(), - } - require.NoError(t, atxs.AddCheckpointed(tab.db, &atx)) - tab.mclock.EXPECT().CurrentLayer().Return(layer) - require.NoError(t, tab.Regossip(context.Background(), sig.NodeID())) - } + tab.mpub.EXPECT().Publish(ctx, pubsub.AtxProtocol, blob) + require.NoError(t, tab.Regossip(ctx, smesher)) }) } diff --git a/activation/activation_test.go b/activation/activation_test.go index ba1c6dd7ea..78cb0b0eac 100644 --- a/activation/activation_test.go +++ b/activation/activation_test.go @@ -91,20 +91,26 @@ func newTestBuilder(tb testing.TB, numSigners int, opts ...BuilderOption) *testA mValidator: NewMocknipostValidator(ctrl), } - opts = append(opts, WithValidator(tab.mValidator)) - cfg := Config{ GoldenATXID: tab.goldenATXID, } tab.msync.EXPECT().RegisterForATXSynced().DoAndReturn(closedChan).AnyTimes() - b := NewBuilder( - cfg, + atxService := NewDBAtxService( tab.db, + tab.goldenATXID, atxsdata.New(), + tab.mValidator, + logger, + ) + + b := NewBuilder( + cfg, tab.localDb, + atxService, tab.mpub, + tab.mValidator, tab.mnipost, tab.mclock, tab.msync, @@ -141,7 +147,6 @@ func publishAtxV1( tb, atxs.SetPost(tab.db, watx.ID(), watx.PrevATXID, 0, watx.SmesherID, watx.NumUnits, watx.PublishEpoch), ) - tab.atxsdata.AddFromAtx(toAtx(tb, &watx), false) return &watx } @@ -353,14 +358,17 @@ func TestBuilder_PublishActivationTx_HappyFlow(t *testing.T) { posEpoch := postGenesisEpoch currLayer := posEpoch.FirstLayer() - prevAtx := newInitialATXv1(t, tab.goldenATXID) - prevAtx.Sign(sig) - require.NoError(t, atxs.Add(tab.db, toAtx(t, prevAtx), prevAtx.Blob())) - tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) + prevAtx := &types.ActivationTx{ + CommitmentATX: &tab.goldenATXID, + Coinbase: tab.Coinbase(), + NumUnits: 100, + TickCount: 10, + SmesherID: sig.NodeID(), + } + require.NoError(t, atxs.Add(tab.db, prevAtx, types.AtxBlob{})) // create and publish ATX tab.mclock.EXPECT().CurrentLayer().Return(currLayer).Times(4) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), prevAtx.ID(), tab.goldenATXID, gomock.Any()) atx1 := publishAtxV1(t, tab, sig.NodeID(), posEpoch, &currLayer, layersPerEpoch) require.NotNil(t, atx1) require.Equal(t, prevAtx.ID(), atx1.PositioningATXID) @@ -368,7 +376,6 @@ func TestBuilder_PublishActivationTx_HappyFlow(t *testing.T) { // create and publish another ATX currLayer = (posEpoch + 1).FirstLayer() tab.mclock.EXPECT().CurrentLayer().Return(currLayer).Times(4) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), atx1.ID(), tab.goldenATXID, gomock.Any()) atx2 := publishAtxV1(t, tab, sig.NodeID(), atx1.PublishEpoch, &currLayer, layersPerEpoch) require.NotNil(t, atx2) require.NotEqual(t, atx1, atx2) @@ -392,7 +399,6 @@ func TestBuilder_Loop_WaitsOnStaleChallenge(t *testing.T) { prevAtx := newInitialATXv1(t, tab.goldenATXID) prevAtx.Sign(sig) require.NoError(t, atxs.Add(tab.db, toAtx(t, prevAtx), prevAtx.Blob())) - tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) tab.mclock.EXPECT().CurrentLayer().Return(currLayer).AnyTimes() tab.mclock.EXPECT().LayerToTime(gomock.Any()).DoAndReturn( @@ -416,8 +422,6 @@ func TestBuilder_Loop_WaitsOnStaleChallenge(t *testing.T) { return ch }) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - // Act & Verify var eg errgroup.Group eg.Go(func() error { @@ -441,7 +445,6 @@ func TestBuilder_PublishActivationTx_FaultyNet(t *testing.T) { prevAtx := newInitialATXv1(t, tab.goldenATXID) prevAtx.Sign(sig) require.NoError(t, atxs.Add(tab.db, toAtx(t, prevAtx), prevAtx.Blob())) - tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) publishEpoch := posEpoch + 1 tab.mclock.EXPECT().CurrentLayer().DoAndReturn(func() types.LayerID { return currLayer }).AnyTimes() @@ -488,7 +491,6 @@ func TestBuilder_PublishActivationTx_FaultyNet(t *testing.T) { // after successful publish, state is cleaned up tab.mnipost.EXPECT().ResetState(sig.NodeID()).Return(nil) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) tab.mpub.EXPECT().Publish(gomock.Any(), pubsub.AtxProtocol, gomock.Any()).DoAndReturn( // second publish succeeds func(_ context.Context, _ string, got []byte) error { @@ -516,7 +518,6 @@ func TestBuilder_PublishActivationTx_UsesExistingChallengeOnLatePublish(t *testi prevAtx.Sign(sig) vPrevAtx := toAtx(t, prevAtx) require.NoError(t, atxs.Add(tab.db, vPrevAtx, prevAtx.Blob())) - tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) publishEpoch := currLayer.GetEpoch() tab.mclock.EXPECT().CurrentLayer().DoAndReturn(func() types.LayerID { return currLayer }).AnyTimes() @@ -536,12 +537,14 @@ func TestBuilder_PublishActivationTx_UsesExistingChallengeOnLatePublish(t *testi NumUnits: DefaultPostSetupOpts().NumUnits, LabelsPerUnit: DefaultPostConfig().LabelsPerUnit, }, nil).AnyTimes() - tab.mnipost.EXPECT().BuildNIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, _ *signing.EdSigner, _ types.Hash32, _ *types.NIPostChallenge, - ) (*nipost.NIPostState, error) { - currLayer = currLayer.Add(1) - return newNIPostWithPoet(t, []byte("66666")), nil - }) + tab.mnipost.EXPECT(). + BuildNIPost(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(_ context.Context, _ *signing.EdSigner, _ types.Hash32, _ *types.NIPostChallenge, + ) (*nipost.NIPostState, error) { + currLayer = currLayer.Add(1) + return newNIPostWithPoet(t, []byte("66666")), nil + }) done := make(chan struct{}) close(done) tab.mclock.EXPECT().AwaitLayer(publishEpoch.FirstLayer()).DoAndReturn( @@ -593,7 +596,6 @@ func TestBuilder_PublishActivationTx_RebuildNIPostWhenTargetEpochPassed(t *testi prevAtx.Sign(sig) vPrevAtx := toAtx(t, prevAtx) require.NoError(t, atxs.Add(tab.db, vPrevAtx, prevAtx.Blob())) - tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) publishEpoch := posEpoch + 1 tab.mclock.EXPECT().CurrentLayer().DoAndReturn( @@ -622,7 +624,6 @@ func TestBuilder_PublishActivationTx_RebuildNIPostWhenTargetEpochPassed(t *testi } return done }) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) ctx, cancel := context.WithCancel(context.Background()) defer cancel() var built *wire.ActivationTxV1 @@ -654,10 +655,8 @@ func TestBuilder_PublishActivationTx_RebuildNIPostWhenTargetEpochPassed(t *testi posAtx := newInitialATXv1(t, tab.goldenATXID, func(atx *wire.ActivationTxV1) { atx.PublishEpoch = posEpoch }) posAtx.Sign(sig) require.NoError(t, atxs.Add(tab.db, toAtx(t, posAtx), posAtx.Blob())) - tab.atxsdata.AddFromAtx(toAtx(t, posAtx), false) tab.mclock.EXPECT().CurrentLayer().DoAndReturn(func() types.LayerID { return currLayer }).AnyTimes() tab.mnipost.EXPECT().ResetState(sig.NodeID()).Return(nil) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), posAtx.ID(), tab.goldenATXID, gomock.Any()) built2 := publishAtxV1(t, tab, sig.NodeID(), posEpoch, &currLayer, layersPerEpoch) require.NotNil(t, built2) require.NotEqual(t, built.NIPostChallengeV1, built2.NIPostChallengeV1) @@ -843,31 +842,35 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { r := require.New(t) // Arrange - tab := newTestBuilder(t, 1, WithPoetConfig(PoetConfig{PhaseShift: layerDuration * 4})) + actSvc := NewMockAtxService(gomock.NewController(t)) + tab := newTestBuilder(t, 1, + WithPoetConfig(PoetConfig{PhaseShift: layerDuration * 4}), + ) + tab.atxSvc = actSvc sig := maps.Values(tab.signers)[0] - otherSigner, err := signing.NewEdSigner() - r.NoError(err) - poetBytes := []byte("poet") - currentLayer := postGenesisEpoch.FirstLayer().Add(3) - posAtx := newInitialATXv1(t, tab.goldenATXID) - posAtx.Sign(otherSigner) - vPosAtx := toAtx(t, posAtx) - vPosAtx.TickCount = 100 - r.NoError(atxs.Add(tab.db, vPosAtx, posAtx.Blob())) - tab.atxsdata.AddFromAtx(vPosAtx, false) - - nonce := types.VRFPostIndex(123) - prevAtx := newInitialATXv1(t, tab.goldenATXID, func(atx *wire.ActivationTxV1) { - atx.VRFNonce = (*uint64)(&nonce) - }) - prevAtx.Sign(sig) - vPrevAtx := toAtx(t, prevAtx) - r.NoError(atxs.Add(tab.db, vPrevAtx, prevAtx.Blob())) - tab.atxsdata.AddFromAtx(vPrevAtx, false) + posAtx := &types.ActivationTx{ + PublishEpoch: 1, + TickCount: 100, + SmesherID: types.RandomNodeID(), + } + posAtx.SetID(types.RandomATXID()) + actSvc.EXPECT().PositioningATX(gomock.Any(), gomock.Any()).Return(posAtx.ID(), nil) + actSvc.EXPECT().Atx(gomock.Any(), posAtx.ID()).Return(posAtx, nil) + + prevAtx := &types.ActivationTx{ + PublishEpoch: 1, + TickCount: 10, + SmesherID: types.RandomNodeID(), + VRFNonce: types.VRFPostIndex(123), + } + prevAtx.SetID(types.RandomATXID()) + actSvc.EXPECT().LastATX(gomock.Any(), sig.NodeID()).Return(prevAtx, nil).Times(2) + actSvc.EXPECT().Atx(gomock.Any(), prevAtx.ID()).Return(prevAtx, nil) // Act + currentLayer := prevAtx.PublishEpoch.FirstLayer().Add(3) tab.msync.EXPECT().RegisterForATXSynced().DoAndReturn(closedChan).AnyTimes() tab.mclock.EXPECT().CurrentLayer().Return(currentLayer).AnyTimes() @@ -877,7 +880,7 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { genesis := time.Now().Add(-time.Duration(currentLayer) * layerDuration) return genesis.Add(layerDuration * time.Duration(layer)) }).AnyTimes() - tab.mclock.EXPECT().AwaitLayer(vPosAtx.PublishEpoch.FirstLayer().Add(layersPerEpoch)).DoAndReturn( + tab.mclock.EXPECT().AwaitLayer(prevAtx.PublishEpoch.FirstLayer().Add(layersPerEpoch)).DoAndReturn( func(layer types.LayerID) <-chan struct{} { ch := make(chan struct{}) close(ch) @@ -889,7 +892,7 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { tab.mpostClient.EXPECT().Info(gomock.Any()).Return(&types.PostInfo{ NodeID: sig.NodeID(), CommitmentATX: commitmentATX, - Nonce: &nonce, + Nonce: &prevAtx.VRFNonce, NumUnits: DefaultPostSetupOpts().NumUnits, LabelsPerUnit: DefaultPostConfig().LabelsPerUnit, @@ -904,8 +907,6 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { return newNIPostWithPoet(t, poetBytes), nil }) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - tab.mpub.EXPECT(). Publish(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, _ string, msg []byte) error { @@ -929,7 +930,7 @@ func TestBuilder_PublishActivationTx_PrevATXWithoutPrevATX(t *testing.T) { r.NoError(tab.PublishActivationTx(context.Background(), sig)) // state is cleaned up - _, err = nipost.Challenge(tab.localDB, sig.NodeID()) + _, err := nipost.Challenge(tab.localDB, sig.NodeID()) require.ErrorIs(t, err, sql.ErrNotFound) } @@ -937,19 +938,23 @@ func TestBuilder_PublishActivationTx_TargetsEpochBasedOnPosAtx(t *testing.T) { r := require.New(t) // Arrange - tab := newTestBuilder(t, 1, WithPoetConfig(PoetConfig{PhaseShift: layerDuration * 4})) + atxSvc := NewMockAtxService(gomock.NewController(t)) + tab := newTestBuilder(t, 1, + WithPoetConfig(PoetConfig{PhaseShift: layerDuration * 4}), + ) + tab.atxSvc = atxSvc sig := maps.Values(tab.signers)[0] - otherSigner, err := signing.NewEdSigner() - r.NoError(err) - poetBytes := []byte("poet") currentLayer := postGenesisEpoch.FirstLayer().Add(3) - posEpoch := postGenesisEpoch - posAtx := newInitialATXv1(t, tab.goldenATXID) - posAtx.Sign(otherSigner) - r.NoError(atxs.Add(tab.db, toAtx(t, posAtx), posAtx.Blob())) - tab.atxsdata.AddFromAtx(toAtx(t, posAtx), false) + posAtx := &types.ActivationTx{ + PublishEpoch: 2, + TickCount: 100, + SmesherID: types.RandomNodeID(), + } + posAtx.SetID(types.RandomATXID()) + atxSvc.EXPECT().PositioningATX(gomock.Any(), gomock.Any()).Return(posAtx.ID(), nil) + atxSvc.EXPECT().LastATX(gomock.Any(), sig.NodeID()).Return(nil, ErrNotFound).Times(2) // Act & Assert tab.msync.EXPECT().RegisterForATXSynced().DoAndReturn(closedChan).AnyTimes() @@ -987,7 +992,6 @@ func TestBuilder_PublishActivationTx_TargetsEpochBasedOnPosAtx(t *testing.T) { return newNIPostWithPoet(t, poetBytes), nil }) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) tab.mpub.EXPECT(). Publish(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, _ string, msg []byte) error { @@ -999,7 +1003,7 @@ func TestBuilder_PublishActivationTx_TargetsEpochBasedOnPosAtx(t *testing.T) { r.Equal(types.EmptyATXID, atx.PrevATXID) r.NotNil(atx.InitialPost) r.Equal(posAtx.ID(), atx.PositioningATXID) - r.Equal(posEpoch+1, atx.PublishEpoch) + r.Equal(posAtx.PublishEpoch+1, atx.PublishEpoch) r.Equal(poetBytes, atx.NIPost.PostMetadata.Challenge) return nil @@ -1029,7 +1033,7 @@ func TestBuilder_PublishActivationTx_TargetsEpochBasedOnPosAtx(t *testing.T) { r.NoError(tab.PublishActivationTx(context.Background(), sig)) // state is cleaned up - _, err = nipost.Challenge(tab.localDB, sig.NodeID()) + _, err := nipost.Challenge(tab.localDB, sig.NodeID()) require.ErrorIs(t, err, sql.ErrNotFound) } @@ -1042,7 +1046,6 @@ func TestBuilder_PublishActivationTx_FailsWhenNIPostBuilderFails(t *testing.T) { prevAtx := newInitialATXv1(t, tab.goldenATXID) prevAtx.Sign(sig) require.NoError(t, atxs.Add(tab.db, toAtx(t, prevAtx), prevAtx.Blob())) - tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) tab.mclock.EXPECT().CurrentLayer().Return(posEpoch.FirstLayer()).AnyTimes() tab.mclock.EXPECT().LayerToTime(gomock.Any()).DoAndReturn( @@ -1055,7 +1058,6 @@ func TestBuilder_PublishActivationTx_FailsWhenNIPostBuilderFails(t *testing.T) { tab.mnipost.EXPECT(). BuildNIPost(gomock.Any(), sig, gomock.Any(), gomock.Any()). Return(nil, nipostErr) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) require.ErrorIs(t, tab.PublishActivationTx(context.Background(), sig), nipostErr) // state is preserved @@ -1100,7 +1102,6 @@ func TestBuilder_RetryPublishActivationTx(t *testing.T) { prevAtx := newInitialATXv1(t, tab.goldenATXID) prevAtx.Sign(sig) require.NoError(t, atxs.Add(tab.db, toAtx(t, prevAtx), prevAtx.Blob())) - tab.atxsdata.AddFromAtx(toAtx(t, prevAtx), false) currLayer := prevAtx.PublishEpoch.FirstLayer() tab.mclock.EXPECT().CurrentLayer().DoAndReturn(func() types.LayerID { return currLayer }).AnyTimes() @@ -1148,7 +1149,6 @@ func TestBuilder_RetryPublishActivationTx(t *testing.T) { ) tab.mnipost.EXPECT().ResetState(sig.NodeID()).Return(nil) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) nonce := types.VRFPostIndex(123) commitmentATX := types.RandomATXID() @@ -1395,74 +1395,36 @@ func TestWaitPositioningAtx(t *testing.T) { } } -// Test if GetPositioningAtx disregards ATXs with invalid POST in their chain. -// It should pick an ATX with valid POST even though it's a lower height. -func TestGetPositioningAtxPicksAtxWithValidChain(t *testing.T) { - tab := newTestBuilder(t, 1) - sig := maps.Values(tab.signers)[0] - - // Invalid chain with high height - sigInvalid, err := signing.NewEdSigner() - require.NoError(t, err) - invalidAtx := newInitialATXv1(t, tab.goldenATXID) - invalidAtx.Sign(sigInvalid) - vInvalidAtx := toAtx(t, invalidAtx) - vInvalidAtx.TickCount = 100 - require.NoError(t, err) - require.NoError(t, atxs.Add(tab.db, vInvalidAtx, invalidAtx.Blob())) - tab.atxsdata.AddFromAtx(vInvalidAtx, false) - - // Valid chain with lower height - sigValid, err := signing.NewEdSigner() - require.NoError(t, err) - validAtx := newInitialATXv1(t, tab.goldenATXID) - validAtx.NumUnits += 10 - validAtx.Sign(sigValid) - vValidAtx := toAtx(t, validAtx) - require.NoError(t, atxs.Add(tab.db, vValidAtx, validAtx.Blob())) - tab.atxsdata.AddFromAtx(vValidAtx, false) - - tab.mValidator.EXPECT(). - VerifyChain(gomock.Any(), invalidAtx.ID(), tab.goldenATXID, gomock.Any()). - Return(errors.New("")) - tab.mValidator.EXPECT(). - VerifyChain(gomock.Any(), validAtx.ID(), tab.goldenATXID, gomock.Any()) - - posAtxID, err := tab.getPositioningAtx(context.Background(), sig.NodeID(), 77, nil) - require.NoError(t, err) - require.Equal(t, posAtxID, vValidAtx.ID()) - - // should use the cached positioning ATX when asked for the same publish epoch - posAtxID, err = tab.getPositioningAtx(context.Background(), sig.NodeID(), 77, nil) - require.NoError(t, err) - require.Equal(t, posAtxID, vValidAtx.ID()) - - // should lookup again when asked for a different publish epoch - tab.mValidator.EXPECT(). - VerifyChain(gomock.Any(), invalidAtx.ID(), tab.goldenATXID, gomock.Any()). - Return(errors.New("")) - tab.mValidator.EXPECT(). - VerifyChain(gomock.Any(), validAtx.ID(), tab.goldenATXID, gomock.Any()) - - posAtxID, err = tab.getPositioningAtx(context.Background(), sig.NodeID(), 99, nil) - require.NoError(t, err) - require.Equal(t, posAtxID, vValidAtx.ID()) -} - func TestGetPositioningAtx(t *testing.T) { t.Parallel() - t.Run("db failed", func(t *testing.T) { + t.Run("picks golden when failed", func(t *testing.T) { t.Parallel() + atxSvc := NewMockAtxService(gomock.NewController(t)) tab := newTestBuilder(t, 1) + tab.atxSvc = atxSvc - db := sql.NewMockExecutor(gomock.NewController(t)) - tab.Builder.db = db - expected := errors.New("db error") - db.EXPECT().Exec(gomock.Any(), gomock.Any(), gomock.Any()).Return(0, expected) + expected := errors.New("expected error") + atxSvc.EXPECT().PositioningATX(gomock.Any(), gomock.Any()).Return(types.ATXID{}, expected) - none, err := tab.getPositioningAtx(context.Background(), types.EmptyNodeID, 99, nil) - require.ErrorIs(t, err, expected) - require.Equal(t, types.ATXID{}, none) + posATX, err := tab.getPositioningAtx(context.Background(), types.EmptyNodeID, 99, nil) + require.NoError(t, err) + require.Equal(t, tab.goldenATXID, posATX) + }) + t.Run("picks previous when querying candidate fails and previous is available", func(t *testing.T) { + t.Parallel() + atxSvc := NewMockAtxService(gomock.NewController(t)) + tab := newTestBuilder(t, 1) + tab.atxSvc = atxSvc + + atxID := types.RandomATXID() + atxSvc.EXPECT().PositioningATX(gomock.Any(), types.EpochID(98)).Return(atxID, nil) + atxSvc.EXPECT().Atx(context.Background(), atxID).Return(nil, errors.New("failed")) + + previous := types.ActivationTx{} + previous.SetID(types.RandomATXID()) + posATX, err := tab.getPositioningAtx(context.Background(), types.EmptyNodeID, 99, &previous) + require.NoError(t, err) + require.Equal(t, previous.ID(), posATX) }) t.Run("picks golden if no ATXs", func(t *testing.T) { tab := newTestBuilder(t, 1) @@ -1479,21 +1441,18 @@ func TestGetPositioningAtx(t *testing.T) { require.Equal(t, prev.ID(), atx) }) t.Run("prefers own previous when it has GTE ticks", func(t *testing.T) { + atxSvc := NewMockAtxService(gomock.NewController(t)) tab := newTestBuilder(t, 1) + tab.atxSvc = atxSvc atxInDb := &types.ActivationTx{TickCount: 10} atxInDb.SetID(types.RandomATXID()) - require.NoError(t, atxs.Add(tab.db, atxInDb, types.AtxBlob{})) - tab.atxsdata.AddFromAtx(atxInDb, false) + atxSvc.EXPECT().PositioningATX(gomock.Any(), types.EpochID(98)).Return(atxInDb.ID(), nil) + atxSvc.EXPECT().Atx(context.Background(), atxInDb.ID()).Return(atxInDb, nil).Times(2) prev := &types.ActivationTx{TickCount: 100} prev.SetID(types.RandomATXID()) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), atxInDb.ID(), tab.goldenATXID, gomock.Any()) - found, err := tab.searchPositioningAtx(context.Background(), types.EmptyNodeID, 99) - require.NoError(t, err) - require.Equal(t, atxInDb.ID(), found) - // prev.Height > found.Height selected, err := tab.getPositioningAtx(context.Background(), types.EmptyNodeID, 99, prev) require.NoError(t, err) @@ -1505,67 +1464,6 @@ func TestGetPositioningAtx(t *testing.T) { require.NoError(t, err) require.Equal(t, prev.ID(), selected) }) - t.Run("prefers own previous or golded when positioning ATX selection timout expired", func(t *testing.T) { - tab := newTestBuilder(t, 1) - - atxInDb := &types.ActivationTx{TickCount: 100} - atxInDb.SetID(types.RandomATXID()) - require.NoError(t, atxs.Add(tab.db, atxInDb, types.AtxBlob{})) - tab.atxsdata.AddFromAtx(atxInDb, false) - - prev := &types.ActivationTx{TickCount: 90} - prev.SetID(types.RandomATXID()) - - // no timeout set up - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), atxInDb.ID(), tab.goldenATXID, gomock.Any()) - found, err := tab.getPositioningAtx(context.Background(), types.EmptyNodeID, 99, prev) - require.NoError(t, err) - require.Equal(t, atxInDb.ID(), found) - - tab.posAtxFinder.found = nil - - // timeout set up, prev ATX exists - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - selected, err := tab.getPositioningAtx(ctx, types.EmptyNodeID, 99, prev) - require.NoError(t, err) - require.Equal(t, prev.ID(), selected) - - tab.posAtxFinder.found = nil - - // timeout set up, prev ATX do not exists - ctx, cancel = context.WithCancel(context.Background()) - cancel() - - selected, err = tab.getPositioningAtx(ctx, types.EmptyNodeID, 99, nil) - require.NoError(t, err) - require.Equal(t, tab.goldenATXID, selected) - }) -} - -func TestFindFullyValidHighTickAtx(t *testing.T) { - t.Parallel() - golden := types.RandomATXID() - - t.Run("skips malicious ATXs", func(t *testing.T) { - data := atxsdata.New() - atxMal := &types.ActivationTx{TickCount: 100, SmesherID: types.RandomNodeID()} - atxMal.SetID(types.RandomATXID()) - data.AddFromAtx(atxMal, true) - - atxLower := &types.ActivationTx{TickCount: 10, SmesherID: types.RandomNodeID()} - atxLower.SetID(types.RandomATXID()) - data.AddFromAtx(atxLower, false) - - mValidator := NewMocknipostValidator(gomock.NewController(t)) - mValidator.EXPECT().VerifyChain(gomock.Any(), atxLower.ID(), golden, gomock.Any()) - - lg := zaptest.NewLogger(t) - found, err := findFullyValidHighTickAtx(context.Background(), data, 0, golden, mValidator, lg) - require.NoError(t, err) - require.Equal(t, atxLower.ID(), found) - }) } // Test_Builder_RegenerateInitialPost tests the coverage for the edge case diff --git a/activation/atx_service_db.go b/activation/atx_service_db.go new file mode 100644 index 0000000000..26e11ad7ae --- /dev/null +++ b/activation/atx_service_db.go @@ -0,0 +1,150 @@ +package activation + +import ( + "context" + "errors" + "fmt" + "time" + + "go.uber.org/zap" + + "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/log" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" +) + +// dbAtxService implements AtxService by accessing the state database. +type dbAtxService struct { + golden types.ATXID + logger *zap.Logger + db sql.Executor + atxsdata *atxsdata.Data + validator nipostValidator + cfg dbAtxServiceConfig +} + +type dbAtxServiceConfig struct { + // delay before PoST in ATX is considered valid (counting from the time it was received) + postValidityDelay time.Duration + trusted []types.NodeID +} + +type dbAtxServiceOption func(*dbAtxServiceConfig) + +func WithPostValidityDelay(delay time.Duration) dbAtxServiceOption { + return func(cfg *dbAtxServiceConfig) { + cfg.postValidityDelay = delay + } +} + +func WithTrustedIDs(ids ...types.NodeID) dbAtxServiceOption { + return func(cfg *dbAtxServiceConfig) { + cfg.trusted = ids + } +} + +func NewDBAtxService( + db sql.Executor, + golden types.ATXID, + atxsdata *atxsdata.Data, + validator nipostValidator, + logger *zap.Logger, + opts ...dbAtxServiceOption, +) *dbAtxService { + cfg := dbAtxServiceConfig{ + postValidityDelay: time.Hour * 12, + } + + for _, opt := range opts { + opt(&cfg) + } + + return &dbAtxService{ + golden: golden, + logger: logger, + db: db, + atxsdata: atxsdata, + validator: validator, + cfg: cfg, + } +} + +func (s *dbAtxService) Atx(_ context.Context, id types.ATXID) (*types.ActivationTx, error) { + atx, err := atxs.Get(s.db, id) + if errors.Is(err, sql.ErrNotFound) { + return nil, ErrNotFound + } + return atx, err +} + +func (s *dbAtxService) LastATX(ctx context.Context, id types.NodeID) (*types.ActivationTx, error) { + atxid, err := atxs.GetLastIDByNodeID(s.db, id) + switch { + case errors.Is(err, sql.ErrNotFound): + return nil, ErrNotFound + case err != nil: + return nil, fmt.Errorf("getting last ATXID: %w", err) + } + return atxs.Get(s.db, atxid) +} + +func (s *dbAtxService) PositioningATX(ctx context.Context, maxPublish types.EpochID) (types.ATXID, error) { + latestPublished, err := atxs.LatestEpoch(s.db) + if err != nil { + return types.EmptyATXID, fmt.Errorf("get latest epoch: %w", err) + } + s.logger.Info("searching for positioning atx", zap.Uint32("latest_epoch", latestPublished.Uint32())) + + // positioning ATX publish epoch must be lower than the publish epoch of built ATX + positioningAtxPublished := min(latestPublished, maxPublish) + return findFullyValidHighTickAtx( + ctx, + s.atxsdata, + positioningAtxPublished, + s.golden, + s.validator, s.logger, + VerifyChainOpts.AssumeValidBefore(time.Now().Add(-s.cfg.postValidityDelay)), + VerifyChainOpts.WithTrustedIDs(s.cfg.trusted...), + VerifyChainOpts.WithLogger(s.logger), + ) +} + +func findFullyValidHighTickAtx( + ctx context.Context, + atxdata *atxsdata.Data, + publish types.EpochID, + goldenATXID types.ATXID, + validator nipostValidator, + logger *zap.Logger, + opts ...VerifyChainOption, +) (types.ATXID, error) { + var found *types.ATXID + + // iterate trough epochs, to get first valid, not malicious ATX with the biggest height + atxdata.IterateHighTicksInEpoch(publish+1, func(id types.ATXID) (contSearch bool) { + logger.Debug("found candidate for high-tick atx", log.ZShortStringer("id", id)) + if ctx.Err() != nil { + return false + } + // verify ATX-candidate by getting their dependencies (previous Atx, positioning ATX etc.) + // and verifying PoST for every dependency + if err := validator.VerifyChain(ctx, id, goldenATXID, opts...); err != nil { + logger.Debug("rejecting candidate for high-tick atx", zap.Error(err), log.ZShortStringer("id", id)) + return true + } + found = &id + return false + }) + + if ctx.Err() != nil { + return types.ATXID{}, ctx.Err() + } + + if found == nil { + return types.ATXID{}, ErrNotFound + } + + return *found, nil +} diff --git a/activation/atx_service_db_test.go b/activation/atx_service_db_test.go new file mode 100644 index 0000000000..9ed32db65c --- /dev/null +++ b/activation/atx_service_db_test.go @@ -0,0 +1,102 @@ +package activation + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" + + "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql/atxs" + "github.com/spacemeshos/go-spacemesh/sql/statesql" +) + +func newTestDbAtxService(t *testing.T) *dbAtxService { + return NewDBAtxService( + statesql.InMemoryTest(t), + types.RandomATXID(), + atxsdata.New(), + NewMocknipostValidator(gomock.NewController(t)), + zaptest.NewLogger(t), + ) +} + +// Test if PositioningAtx disregards ATXs with invalid POST in their chain. +// It should pick an ATX with valid POST even though it's a lower height. +func TestGetPositioningAtxPicksAtxWithValidChain(t *testing.T) { + atxSvc := newTestDbAtxService(t) + + // Invalid chain with high height + sigInvalid, err := signing.NewEdSigner() + require.NoError(t, err) + invalidAtx := newInitialATXv1(t, atxSvc.golden) + invalidAtx.Sign(sigInvalid) + vInvalidAtx := toAtx(t, invalidAtx) + vInvalidAtx.TickCount = 100 + require.NoError(t, err) + require.NoError(t, atxs.Add(atxSvc.db, vInvalidAtx, invalidAtx.Blob())) + atxSvc.atxsdata.AddFromAtx(vInvalidAtx, false) + + // Valid chain with lower height + sigValid, err := signing.NewEdSigner() + require.NoError(t, err) + validAtx := newInitialATXv1(t, atxSvc.golden) + validAtx.NumUnits += 10 + validAtx.Sign(sigValid) + vValidAtx := toAtx(t, validAtx) + require.NoError(t, atxs.Add(atxSvc.db, vValidAtx, validAtx.Blob())) + atxSvc.atxsdata.AddFromAtx(vValidAtx, false) + + atxSvc.validator.(*MocknipostValidator).EXPECT(). + VerifyChain(gomock.Any(), invalidAtx.ID(), atxSvc.golden, gomock.Any()). + Return(errors.New("this is invalid")) + atxSvc.validator.(*MocknipostValidator).EXPECT(). + VerifyChain(gomock.Any(), validAtx.ID(), atxSvc.golden, gomock.Any()) + + posAtxID, err := atxSvc.PositioningATX(context.Background(), validAtx.PublishEpoch) + require.NoError(t, err) + require.Equal(t, vValidAtx.ID(), posAtxID) + + // look in a later epoch, it should return the same one (there is no newer one). + atxSvc.validator.(*MocknipostValidator).EXPECT(). + VerifyChain(gomock.Any(), invalidAtx.ID(), atxSvc.golden, gomock.Any()). + Return(errors.New("")) + atxSvc.validator.(*MocknipostValidator).EXPECT(). + VerifyChain(gomock.Any(), validAtx.ID(), atxSvc.golden, gomock.Any()) + + posAtxID, err = atxSvc.PositioningATX(context.Background(), validAtx.PublishEpoch+1) + require.NoError(t, err) + require.Equal(t, vValidAtx.ID(), posAtxID) + + _, err = atxSvc.PositioningATX(context.Background(), validAtx.PublishEpoch-1) + require.ErrorIs(t, err, ErrNotFound) +} + +func TestFindFullyValidHighTickAtx(t *testing.T) { + t.Parallel() + golden := types.RandomATXID() + + t.Run("skips malicious ATXs", func(t *testing.T) { + data := atxsdata.New() + atxMal := &types.ActivationTx{TickCount: 100, SmesherID: types.RandomNodeID()} + atxMal.SetID(types.RandomATXID()) + data.AddFromAtx(atxMal, true) + + atxLower := &types.ActivationTx{TickCount: 10, SmesherID: types.RandomNodeID()} + atxLower.SetID(types.RandomATXID()) + data.AddFromAtx(atxLower, false) + + mValidator := NewMocknipostValidator(gomock.NewController(t)) + mValidator.EXPECT().VerifyChain(gomock.Any(), atxLower.ID(), golden, gomock.Any()) + + lg := zaptest.NewLogger(t) + found, err := findFullyValidHighTickAtx(context.Background(), data, 0, golden, mValidator, lg) + require.NoError(t, err) + require.Equal(t, atxLower.ID(), found) + }) +} diff --git a/activation/builder_v2_test.go b/activation/builder_v2_test.go index 0054147f4e..d7170c12bf 100644 --- a/activation/builder_v2_test.go +++ b/activation/builder_v2_test.go @@ -93,7 +93,6 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { posEpoch += 1 layer = posEpoch.FirstLayer() tab.mclock.EXPECT().CurrentLayer().Return(layer).Times(4) - tab.mValidator.EXPECT().VerifyChain(gomock.Any(), atx1.ID(), tab.goldenATXID, gomock.Any()) var atx2 wire.ActivationTxV2 publishAtx(t, tab, sig.NodeID(), posEpoch, &layer, layersPerEpoch, func(_ context.Context, _ string, got []byte) error { diff --git a/activation/e2e/activation_test.go b/activation/e2e/activation_test.go index d0c86e5785..d4d65a2a5b 100644 --- a/activation/e2e/activation_test.go +++ b/activation/e2e/activation_test.go @@ -189,18 +189,26 @@ func Test_BuilderWithMultipleClients(t *testing.T) { ).Times(totalAtxs) t.Cleanup(func() { assert.NoError(t, verifier.Close()) }) - tab := activation.NewBuilder( - conf, + + atxService := activation.NewDBAtxService( db, + conf.GoldenATXID, data, + validator, + logger, + ) + + tab := activation.NewBuilder( + conf, localDB, + atxService, mpub, + validator, nb, clock, syncedSyncer(t), logger, activation.WithPoetConfig(poetCfg), - activation.WithValidator(validator), activation.WithPoets(client), ) for _, sig := range signers { diff --git a/activation/e2e/builds_atx_v2_test.go b/activation/e2e/builds_atx_v2_test.go index 93832608eb..1621329847 100644 --- a/activation/e2e/builds_atx_v2_test.go +++ b/activation/e2e/builds_atx_v2_test.go @@ -200,18 +200,25 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { ).Times(2), ) - tab := activation.NewBuilder( - conf, + atxService := activation.NewDBAtxService( db, + conf.GoldenATXID, atxsdata, + validator, + logger, + ) + + tab := activation.NewBuilder( + conf, localDB, + atxService, mpub, + validator, nb, clock, syncedSyncer(t), logger, activation.WithPoetConfig(poetCfg), - activation.WithValidator(validator), activation.BuilderAtxVersions(atxVersions), ) tab.Register(sig) diff --git a/activation/e2e/checkpoint_test.go b/activation/e2e/checkpoint_test.go index b7a2596cae..e06ce214af 100644 --- a/activation/e2e/checkpoint_test.go +++ b/activation/e2e/checkpoint_test.go @@ -123,18 +123,25 @@ func TestCheckpoint_PublishingSoloATXs(t *testing.T) { activation.WithAtxVersions(atxVersions), ) - tab := activation.NewBuilder( - activation.Config{GoldenATXID: goldenATX}, + atxService := activation.NewDBAtxService( db, + goldenATX, atxdata, + validator, + logger, + ) + + tab := activation.NewBuilder( + activation.Config{GoldenATXID: goldenATX}, localDB, + atxService, mpub, + validator, nb, clock, syncer, logger, activation.WithPoetConfig(poetCfg), - activation.WithValidator(validator), activation.BuilderAtxVersions(atxVersions), ) tab.Register(sig) @@ -223,18 +230,25 @@ func TestCheckpoint_PublishingSoloATXs(t *testing.T) { ) require.NoError(t, err) - tab = activation.NewBuilder( - activation.Config{GoldenATXID: goldenATX}, + atxService = activation.NewDBAtxService( newDB, + goldenATX, atxdata, + validator, + logger, + ) + + tab = activation.NewBuilder( + activation.Config{GoldenATXID: goldenATX}, localDB, + atxService, mpub, + validator, nb, clock, syncer, logger, activation.WithPoetConfig(poetCfg), - activation.WithValidator(validator), activation.BuilderAtxVersions(atxVersions), ) tab.Register(sig) diff --git a/activation/interface.go b/activation/interface.go index c9c3359091..7a5066a8d7 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -19,6 +19,8 @@ import ( //go:generate mockgen -typed -package=activation -destination=./mocks.go -source=./interface.go +var ErrNotFound = errors.New("not found") + type AtxReceiver interface { OnAtx(*types.ActivationTx) } @@ -108,6 +110,22 @@ type atxProvider interface { GetAtx(id types.ATXID) (*types.ActivationTx, error) } +// AtxService provides ATXs needed by the ATX Builder. +type AtxService interface { + // Get ATX with given ID + // + // Returns ErrNotFound if couldn't get the ATX. + Atx(ctx context.Context, id types.ATXID) (*types.ActivationTx, error) + // Get the last ATX of the given identitity. + // + // Returns ErrNotFound if couldn't get the ATX. + LastATX(ctx context.Context, nodeID types.NodeID) (*types.ActivationTx, error) + // PositioningATX returns atx id with the highest tick height. + // + // The maxPublish epoch is the maximum publish epoch of the returned ATX. + PositioningATX(ctx context.Context, maxPublish types.EpochID) (types.ATXID, error) +} + // PostSetupProvider defines the functionality required for Post setup. // This interface is used by the atx builder and currently implemented by the PostSetupManager. // Eventually most of the functionality will be moved to the PoSTClient. diff --git a/activation/mocks.go b/activation/mocks.go index 985f6a05f3..4e1fe49cef 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -1217,6 +1217,147 @@ func (c *MockatxProviderGetAtxCall) DoAndReturn(f func(types.ATXID) (*types.Acti return c } +// MockAtxService is a mock of AtxService interface. +type MockAtxService struct { + ctrl *gomock.Controller + recorder *MockAtxServiceMockRecorder + isgomock struct{} +} + +// MockAtxServiceMockRecorder is the mock recorder for MockAtxService. +type MockAtxServiceMockRecorder struct { + mock *MockAtxService +} + +// NewMockAtxService creates a new mock instance. +func NewMockAtxService(ctrl *gomock.Controller) *MockAtxService { + mock := &MockAtxService{ctrl: ctrl} + mock.recorder = &MockAtxServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAtxService) EXPECT() *MockAtxServiceMockRecorder { + return m.recorder +} + +// Atx mocks base method. +func (m *MockAtxService) Atx(ctx context.Context, id types.ATXID) (*types.ActivationTx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Atx", ctx, id) + ret0, _ := ret[0].(*types.ActivationTx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Atx indicates an expected call of Atx. +func (mr *MockAtxServiceMockRecorder) Atx(ctx, id any) *MockAtxServiceAtxCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Atx", reflect.TypeOf((*MockAtxService)(nil).Atx), ctx, id) + return &MockAtxServiceAtxCall{Call: call} +} + +// MockAtxServiceAtxCall wrap *gomock.Call +type MockAtxServiceAtxCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAtxServiceAtxCall) Return(arg0 *types.ActivationTx, arg1 error) *MockAtxServiceAtxCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAtxServiceAtxCall) Do(f func(context.Context, types.ATXID) (*types.ActivationTx, error)) *MockAtxServiceAtxCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAtxServiceAtxCall) DoAndReturn(f func(context.Context, types.ATXID) (*types.ActivationTx, error)) *MockAtxServiceAtxCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// LastATX mocks base method. +func (m *MockAtxService) LastATX(ctx context.Context, nodeID types.NodeID) (*types.ActivationTx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LastATX", ctx, nodeID) + ret0, _ := ret[0].(*types.ActivationTx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LastATX indicates an expected call of LastATX. +func (mr *MockAtxServiceMockRecorder) LastATX(ctx, nodeID any) *MockAtxServiceLastATXCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastATX", reflect.TypeOf((*MockAtxService)(nil).LastATX), ctx, nodeID) + return &MockAtxServiceLastATXCall{Call: call} +} + +// MockAtxServiceLastATXCall wrap *gomock.Call +type MockAtxServiceLastATXCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAtxServiceLastATXCall) Return(arg0 *types.ActivationTx, arg1 error) *MockAtxServiceLastATXCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAtxServiceLastATXCall) Do(f func(context.Context, types.NodeID) (*types.ActivationTx, error)) *MockAtxServiceLastATXCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAtxServiceLastATXCall) DoAndReturn(f func(context.Context, types.NodeID) (*types.ActivationTx, error)) *MockAtxServiceLastATXCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PositioningATX mocks base method. +func (m *MockAtxService) PositioningATX(ctx context.Context, maxPublish types.EpochID) (types.ATXID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PositioningATX", ctx, maxPublish) + ret0, _ := ret[0].(types.ATXID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PositioningATX indicates an expected call of PositioningATX. +func (mr *MockAtxServiceMockRecorder) PositioningATX(ctx, maxPublish any) *MockAtxServicePositioningATXCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PositioningATX", reflect.TypeOf((*MockAtxService)(nil).PositioningATX), ctx, maxPublish) + return &MockAtxServicePositioningATXCall{Call: call} +} + +// MockAtxServicePositioningATXCall wrap *gomock.Call +type MockAtxServicePositioningATXCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAtxServicePositioningATXCall) Return(arg0 types.ATXID, arg1 error) *MockAtxServicePositioningATXCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAtxServicePositioningATXCall) Do(f func(context.Context, types.EpochID) (types.ATXID, error)) *MockAtxServicePositioningATXCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAtxServicePositioningATXCall) DoAndReturn(f func(context.Context, types.EpochID) (types.ATXID, error)) *MockAtxServicePositioningATXCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // MockpostSetupProvider is a mock of postSetupProvider interface. type MockpostSetupProvider struct { ctrl *gomock.Controller diff --git a/activation/validation.go b/activation/validation.go index d6d070d895..b31a3ec786 100644 --- a/activation/validation.go +++ b/activation/validation.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "time" "github.com/spacemeshos/merkle-tree" @@ -360,7 +361,7 @@ func (v *Validator) PositioningAtx( type verifyChainOpts struct { assumedValidTime time.Time - trustedNodeID types.NodeID + trustedNodeID []types.NodeID logger *zap.Logger } @@ -377,8 +378,8 @@ func (verifyChainOptsNs) AssumeValidBefore(val time.Time) VerifyChainOption { } } -// WithTrustedID configures the validator to assume that ATXs created by the given node ID are valid. -func (verifyChainOptsNs) WithTrustedID(val types.NodeID) VerifyChainOption { +// WithTrustedIDs configures the validator to assume that ATXs created by the given node IDs are valid. +func (verifyChainOptsNs) WithTrustedIDs(val ...types.NodeID) VerifyChainOption { return func(o *verifyChainOpts) { o.trustedNodeID = val } @@ -533,7 +534,7 @@ func (v *Validator) verifyChainWithOpts( zap.Time("valid_before", opts.assumedValidTime), ) return nil - case atx.SmesherID == opts.trustedNodeID: + case slices.Contains(opts.trustedNodeID, atx.SmesherID): log.Debug("not verifying ATX chain", zap.Stringer("atx_id", id), zap.String("reason", "trusted")) return nil } diff --git a/activation/validation_test.go b/activation/validation_test.go index 59278548dd..41ebbc5642 100644 --- a/activation/validation_test.go +++ b/activation/validation_test.go @@ -543,7 +543,7 @@ func TestVerifyChainDeps(t *testing.T) { ctrl := gomock.NewController(t) v := NewMockPostVerifier(ctrl) validator := NewValidator(db, nil, DefaultPostConfig(), config.ScryptParams{}, v) - err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID, VerifyChainOpts.WithTrustedID(signer.NodeID())) + err = validator.VerifyChain(ctx, vAtx.ID(), goldenATXID, VerifyChainOpts.WithTrustedIDs(signer.NodeID())) require.NoError(t, err) }) diff --git a/node/node.go b/node/node.go index 11076d235b..d5fc4089ed 100644 --- a/node/node.go +++ b/node/node.go @@ -1078,22 +1078,36 @@ func (app *App) initServices(ctx context.Context) error { GoldenATXID: goldenATXID, RegossipInterval: app.Config.RegossipAtxInterval, } - atxBuilder := activation.NewBuilder( - builderConfig, + + atxBuilderLog := app.addLogger(ATXBuilderLogger, lg).Zap() + trustedIDs := make([]types.NodeID, 0, len(app.signers)) + for _, sig := range app.signers { + trustedIDs = append(trustedIDs, sig.NodeID()) + } + + atxService := activation.NewDBAtxService( app.db, + goldenATXID, app.atxsdata, + app.validator, + atxBuilderLog, + activation.WithPostValidityDelay(app.Config.PostValidDelay), + activation.WithTrustedIDs(trustedIDs...), + ) + atxBuilder := activation.NewBuilder( + builderConfig, app.localDB, + atxService, app.host, + app.validator, nipostBuilder, app.clock, newSyncer, - app.addLogger(ATXBuilderLogger, lg).Zap(), + atxBuilderLog, activation.WithContext(ctx), activation.WithPoetConfig(app.Config.POET), // TODO(dshulyak) makes no sense. how we ended using it? activation.WithPoetRetryInterval(app.Config.HARE3.PreroundDelay), - activation.WithValidator(app.validator), - activation.WithPostValidityDelay(app.Config.PostValidDelay), activation.WithPostStates(postStates), activation.WithPoets(poetClients...), activation.BuilderAtxVersions(app.Config.AtxVersions), diff --git a/sql/localsql/localatxs/atxs.go b/sql/localsql/localatxs/atxs.go new file mode 100644 index 0000000000..de9eaadee0 --- /dev/null +++ b/sql/localsql/localatxs/atxs.go @@ -0,0 +1,37 @@ +package localatxs + +import ( + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/sql" +) + +func AddBlob(db sql.LocalDatabase, epoch types.EpochID, id types.ATXID, nodeID types.NodeID, blob []byte) error { + _, err := db.Exec("INSERT INTO atx_blobs (epoch, id, pubkey, atx) VALUES (?1, ?2, ?3, ?4)", + func(s *sql.Statement) { + s.BindInt64(1, int64(epoch)) + s.BindBytes(2, id[:]) + s.BindBytes(3, nodeID[:]) + s.BindBytes(4, blob) + }, nil) + return err +} + +func AtxBlob(db sql.LocalDatabase, epoch types.EpochID, nodeID types.NodeID) (id types.ATXID, blob []byte, err error) { + rows, err := db.Exec("select id, atx from atx_blobs where epoch = ?1 and pubkey = ?2", + func(s *sql.Statement) { + s.BindInt64(1, int64(epoch)) + s.BindBytes(2, nodeID[:]) + }, + func(s *sql.Statement) bool { + s.ColumnBytes(0, id[:]) + blob = make([]byte, s.ColumnLen(1)) + s.ColumnBytes(1, blob) + return false + }, + ) + if rows == 0 { + return id, blob, sql.ErrNotFound + } + + return id, blob, err +} diff --git a/sql/localsql/localatxs/atxs_test.go b/sql/localsql/localatxs/atxs_test.go new file mode 100644 index 0000000000..23e77d402e --- /dev/null +++ b/sql/localsql/localatxs/atxs_test.go @@ -0,0 +1,41 @@ +package localatxs_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/localsql" + "github.com/spacemeshos/go-spacemesh/sql/localsql/localatxs" +) + +func Test_Blobs(t *testing.T) { + t.Run("not found", func(t *testing.T) { + db := localsql.InMemoryTest(t) + _, _, err := localatxs.AtxBlob(db, types.EpochID(0), types.NodeID{}) + require.ErrorIs(t, err, sql.ErrNotFound) + }) + t.Run("found", func(t *testing.T) { + db := localsql.InMemoryTest(t) + epoch := types.EpochID(2) + atxid := types.RandomATXID() + nodeID := types.RandomNodeID() + blob := types.RandomBytes(10) + err := localatxs.AddBlob(db, epoch, atxid, nodeID, blob) + require.NoError(t, err) + gotID, gotBlob, err := localatxs.AtxBlob(db, epoch, nodeID) + require.NoError(t, err) + require.Equal(t, atxid, gotID) + require.Equal(t, blob, gotBlob) + + // different ID + _, _, err = localatxs.AtxBlob(db, epoch, types.RandomNodeID()) + require.ErrorIs(t, err, sql.ErrNotFound) + + // different epoch + _, _, err = localatxs.AtxBlob(db, types.EpochID(3), nodeID) + require.ErrorIs(t, err, sql.ErrNotFound) + }) +} diff --git a/sql/localsql/schema/migrations/0010_atxs.sql b/sql/localsql/schema/migrations/0010_atxs.sql new file mode 100644 index 0000000000..97ead34ce4 --- /dev/null +++ b/sql/localsql/schema/migrations/0010_atxs.sql @@ -0,0 +1,11 @@ +--- Table for storing blobs of published ATX for regossiping purposes. +CREATE TABLE atx_blobs +( + id CHAR(32) PRIMARY KEY, + pubkey CHAR(32) NOT NULL, + epoch INT NOT NULL, + atx BLOB, + version INTEGER +); + +CREATE UNIQUE INDEX atx_blobs_epoch_pubkey ON atx_blobs (epoch, pubkey); diff --git a/sql/localsql/schema/schema.sql b/sql/localsql/schema/schema.sql index 0b6a1c00f7..a3a7333b18 100755 --- a/sql/localsql/schema/schema.sql +++ b/sql/localsql/schema/schema.sql @@ -1,4 +1,12 @@ -PRAGMA user_version = 9; +PRAGMA user_version = 10; +CREATE TABLE atx_blobs +( + id CHAR(32) PRIMARY KEY, + pubkey CHAR(32) NOT NULL, + epoch INT NOT NULL, + atx BLOB, + version INTEGER +); CREATE TABLE atx_sync_requests ( epoch INT NOT NULL, @@ -80,4 +88,5 @@ CREATE TABLE prepared_activeset data BLOB NOT NULL, PRIMARY KEY (kind, epoch) ) WITHOUT ROWID; +CREATE UNIQUE INDEX atx_blobs_epoch_pubkey ON atx_blobs (epoch, pubkey); CREATE UNIQUE INDEX idx_poet_certificates ON poet_certificates (node_id, certifier_id);