Skip to content

Commit

Permalink
AtxBuilder verification of initial PoST (#6031)
Browse files Browse the repository at this point in the history
## Motivation
In #5921, an edge case is described where a node may start with a certain amount of SUs, generate an initial PoST but for possible reasons, the number of SUs may change before the first ATX is broadcasted. This means that the initial PoST is no longer valid and needs to be regenerated (costing the miner another epoch before being able to submit the initial ATX).
  • Loading branch information
acud committed Jun 13, 2024
1 parent d4597f0 commit 7efcf58
Show file tree
Hide file tree
Showing 15 changed files with 390 additions and 139 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ operating your own PoET and want to use certificate authentication please refer
ATXs. This vulnerability allows an attacker to claim rewards for a full tick amount although they should not be
eligible for them.

* [#6031](https://github.com/spacemeshos/go-spacemesh/pull/6031) Fixed an edge case where the storage units might have
changed after the initial PoST was generated but before the first ATX has been emitted, invalidating the initial PoST.
The node will now try to verify the initial PoST and regenerate it if necessary.

## Release v1.5.7

### Improvements
Expand Down
47 changes: 36 additions & 11 deletions activation/activation.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ import (
"github.com/spacemeshos/go-spacemesh/sql/localsql/nipost"
)

var ErrNotFound = errors.New("not found")
var (
ErrNotFound = errors.New("not found")
errNilVrfNonce = errors.New("nil VRF nonce")
)

// PoetConfig is the configuration to interact with the poet server.
type PoetConfig struct {
Expand Down Expand Up @@ -351,19 +354,19 @@ func (b *Builder) buildInitialPost(ctx context.Context, nodeID types.NodeID) err
default:
return fmt.Errorf("get initial post: %w", err)
}

// Create the initial post and save it.
startTime := time.Now()
post, postInfo, err := b.nipostBuilder.Proof(ctx, nodeID, shared.ZeroChallenge)
post, postInfo, err := b.nipostBuilder.Proof(ctx, nodeID, shared.ZeroChallenge, nil)
if err != nil {
return fmt.Errorf("post execution: %w", err)
}
if postInfo.Nonce == nil {
b.logger.Error("initial PoST is invalid: missing VRF nonce. Check your PoST data",
log.ZShortStringer("smesherID", nodeID),
)
return errors.New("nil VRF nonce")
return errNilVrfNonce
}

initialPost := nipost.Post{
Nonce: post.Nonce,
Indices: post.Indices,
Expand All @@ -390,22 +393,28 @@ func (b *Builder) buildInitialPost(ctx context.Context, nodeID types.NodeID) err
return nipost.AddPost(b.localDB, nodeID, initialPost)
}

func (b *Builder) run(ctx context.Context, sig *signing.EdSigner) {
defer b.logger.Info("atx builder stopped")

func (b *Builder) buildPost(ctx context.Context, nodeID types.NodeID) error {
for {
err := b.buildInitialPost(ctx, sig.NodeID())
err := b.buildInitialPost(ctx, nodeID)
if err == nil {
break
return nil
}
b.logger.Error("failed to generate initial proof:", zap.Error(err))
currentLayer := b.layerClock.CurrentLayer()
select {
case <-ctx.Done():
return
return ctx.Err()
case <-b.layerClock.AwaitLayer(currentLayer.Add(1)):
}
}
}

func (b *Builder) run(ctx context.Context, sig *signing.EdSigner) {
defer b.logger.Info("atx builder stopped")
if err := b.buildPost(ctx, sig.NodeID()); err != nil {
b.logger.Error("failed to build initial post:", zap.Error(err))
return
}
var eg errgroup.Group
for _, poet := range b.poets {
eg.Go(func() error {
Expand Down Expand Up @@ -451,6 +460,22 @@ func (b *Builder) run(ctx context.Context, sig *signing.EdSigner) {
return
case <-time.After(b.poetRetryInterval):
}
case errors.Is(err, ErrInvalidInitialPost):
// delete the existing db post
// call build initial post again
b.logger.Error("initial post is no longer valid. regenerating initial post")
if err := b.nipostBuilder.ResetState(sig.NodeID()); err != nil {
b.logger.Error("failed to reset nipost builder state", zap.Error(err))
}
if err := nipost.RemoveChallenge(b.localDB, sig.NodeID()); err != nil {
b.logger.Error("failed to discard challenge", zap.Error(err))
}
if err := nipost.RemovePost(b.localDB, sig.NodeID()); err != nil {
b.logger.Error("failed to remove existing post from db", zap.Error(err))
}
if err := b.buildPost(ctx, sig.NodeID()); err != nil {
b.logger.Error("failed to regenerate initial post:", zap.Error(err))
}
default:
b.logger.Warn("unknown error", zap.Error(err))
// other failures are related to in-process software. we may as well panic here
Expand Down Expand Up @@ -718,7 +743,7 @@ func (b *Builder) createAtx(
return nil, fmt.Errorf("unknown ATX version: %v", version)
}
b.logger.Info("building ATX", zap.Stringer("smesherID", sig.NodeID()), zap.Stringer("version", version))
nipostState, err := b.nipostBuilder.BuildNIPost(ctx, sig, challenge.PublishEpoch, challengeHash)
nipostState, err := b.nipostBuilder.BuildNIPost(ctx, sig, challengeHash, challenge)
if err != nil {
return nil, fmt.Errorf("build NIPost: %w", err)
}
Expand Down
72 changes: 41 additions & 31 deletions activation/activation_multi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/spacemeshos/go-spacemesh/codec"
"github.com/spacemeshos/go-spacemesh/common/types"
"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/nipost"
Expand All @@ -26,8 +27,9 @@ func Test_Builder_Multi_StartSmeshingCoinbase(t *testing.T) {
coinbase := types.Address{1, 1, 1}

for _, sig := range tab.signers {
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) {
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge,
) (*types.Post, *types.PostInfo, error) {
<-ctx.Done()
return nil, nil, ctx.Err()
})
Expand All @@ -51,8 +53,9 @@ func Test_Builder_Multi_RestartSmeshing(t *testing.T) {
tab := newTestBuilder(t, 5)

for _, sig := range tab.signers {
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).AnyTimes().DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) {
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).AnyTimes().DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge,
) (*types.Post, *types.PostInfo, error) {
<-ctx.Done()
return nil, nil, ctx.Err()
})
Expand Down Expand Up @@ -109,8 +112,9 @@ func Test_Builder_Multi_StopSmeshing_Delete(t *testing.T) {
tab.mclock.EXPECT().AwaitLayer(gomock.Any()).Return(make(chan struct{})).Times(numIds)

for _, sig := range tab.signers {
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) {
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge,
) (*types.Post, *types.PostInfo, error) {
<-ctx.Done()
return nil, nil, ctx.Err()
})
Expand All @@ -132,8 +136,9 @@ func Test_Builder_Multi_StopSmeshing_Delete(t *testing.T) {
require.Equal(t, refChallenge, challenge) // challenge still present

tab.mnipost.EXPECT().ResetState(sig.NodeID()).Return(nil)
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) {
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge,
) (*types.Post, *types.PostInfo, error) {
<-ctx.Done()
return nil, nil, ctx.Err()
})
Expand All @@ -152,8 +157,9 @@ func Test_Builder_Multi_StopSmeshing_Delete(t *testing.T) {
require.Nil(t, challenge) // challenge deleted

tab.mnipost.EXPECT().ResetState(sig.NodeID()).Return(nil)
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) {
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge,
) (*types.Post, *types.PostInfo, error) {
<-ctx.Done()
return nil, nil, ctx.Err()
})
Expand Down Expand Up @@ -241,7 +247,7 @@ func Test_Builder_Multi_InitialPost(t *testing.T) {
commitmentATX := types.RandomATXID()
tab.mValidator.EXPECT().
PostV2(gomock.Any(), sig.NodeID(), commitmentATX, post, shared.ZeroChallenge, numUnits)
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).Return(
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).Return(
post,
&types.PostInfo{
CommitmentATX: commitmentATX,
Expand All @@ -252,12 +258,10 @@ func Test_Builder_Multi_InitialPost(t *testing.T) {
},
nil,
)
err := tab.buildInitialPost(context.Background(), sig.NodeID())
require.NoError(t, err)
require.NoError(t, tab.buildInitialPost(context.Background(), sig.NodeID()))

// postClient.Proof() should not be called again
err = tab.buildInitialPost(context.Background(), sig.NodeID())
require.NoError(t, err)
require.NoError(t, tab.buildInitialPost(context.Background(), sig.NodeID()))
return nil
})
}
Expand All @@ -277,7 +281,7 @@ func Test_Builder_Multi_HappyPath(t *testing.T) {
ch := make(chan struct{})
initialPostStep[sig.NodeID()] = ch

nipost := nipost.Post{
dbPost := nipost.Post{
Indices: types.RandomBytes(10),
Nonce: rand.Uint32(),
Pow: rand.Uint64(),
Expand All @@ -286,28 +290,29 @@ func Test_Builder_Multi_HappyPath(t *testing.T) {
CommitmentATX: types.RandomATXID(),
VRFNonce: types.VRFPostIndex(rand.Uint64()),
}
initialPost[sig.NodeID()] = &nipost
initialPost[sig.NodeID()] = &dbPost

post := &types.Post{
Indices: nipost.Indices,
Nonce: nipost.Nonce,
Pow: nipost.Pow,
Indices: dbPost.Indices,
Nonce: dbPost.Nonce,
Pow: dbPost.Pow,
}
tab.mValidator.EXPECT().
PostV2(gomock.Any(), sig.NodeID(), nipost.CommitmentATX, post, shared.ZeroChallenge, nipost.NumUnits)
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge).DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte) (*types.Post, *types.PostInfo, error) {
PostV2(gomock.Any(), sig.NodeID(), dbPost.CommitmentATX, post, shared.ZeroChallenge, dbPost.NumUnits)
tab.mnipost.EXPECT().Proof(gomock.Any(), sig.NodeID(), shared.ZeroChallenge, nil).DoAndReturn(
func(ctx context.Context, _ types.NodeID, _ []byte, _ *types.NIPostChallenge,
) (*types.Post, *types.PostInfo, error) {
<-initialPostChan
close(ch)
post := &types.Post{
Indices: nipost.Indices,
Nonce: nipost.Nonce,
Pow: nipost.Pow,
Indices: dbPost.Indices,
Nonce: dbPost.Nonce,
Pow: dbPost.Pow,
}
postInfo := &types.PostInfo{
NumUnits: nipost.NumUnits,
CommitmentATX: nipost.CommitmentATX,
Nonce: &nipost.VRFNonce,
NumUnits: dbPost.NumUnits,
CommitmentATX: dbPost.CommitmentATX,
Nonce: &dbPost.VRFNonce,
LabelsPerUnit: DefaultPostConfig().LabelsPerUnit,
}

Expand Down Expand Up @@ -400,8 +405,13 @@ func Test_Builder_Multi_HappyPath(t *testing.T) {
}
nipostState[sig.NodeID()] = state
tab.mnipost.EXPECT().
BuildNIPost(gomock.Any(), sig, ref.PublishEpoch, ref.Hash()).
Return(state, nil)
BuildNIPost(gomock.Any(), sig, ref.Hash(), gomock.Any()).
DoAndReturn(func(_ context.Context, _ *signing.EdSigner, _ types.Hash32,
postChallenge *types.NIPostChallenge,
) (*nipost.NIPostState, error) {
require.Equal(t, postChallenge.PublishEpoch, ref.PublishEpoch, "publish epoch mismatch")
return state, nil
})

// awaiting atx publication epoch log
tab.mclock.EXPECT().CurrentLayer().DoAndReturn(
Expand Down
Loading

0 comments on commit 7efcf58

Please sign in to comment.