Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Verify self-generated POST proofs to catch errors early #4721

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 23 additions & 9 deletions activation/activation.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/spacemeshos/post/proving"
"github.com/spacemeshos/post/shared"
"github.com/spacemeshos/post/verifying"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"

Expand Down Expand Up @@ -211,6 +212,7 @@ func (b *Builder) StartSmeshing(coinbase types.Address, opts PostSetupOpts) erro
return nil
case err != nil:
b.log.Panic("initialization failed: %v", err)
return err
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this is only needed to pass the unit test that mocks log.Panic... (and doesn't actually panic, allowing the code to proceed)

}

b.run(ctx)
Expand Down Expand Up @@ -254,11 +256,21 @@ func (b *Builder) SmesherID() types.NodeID {
}

func (b *Builder) run(ctx context.Context) {
if err := b.generateInitialPost(ctx); err != nil {
post, metadata, err := b.generateInitialPost(ctx)
if err != nil {
b.log.Error("Failed to generate proof: %s", err)
return
}

if post != nil {
b.log.With().Info("initial post created", log.Object("post", post), log.Object("metadata", metadata))
if err := b.postSetupProvider.VerifyProof(ctx, post, metadata, verifying.WithPowCreator(b.nodeID.Bytes())); err != nil {
events.EmitInvalidPostProof()
b.log.With().Fatal("initial POST proof is invalid. Probably initialized POST data is corrupted. Please verify the data with postcli and regenerate the corrupted files.", log.Err(err))
}
b.initialPost = post
}
poszu marked this conversation as resolved.
Show resolved Hide resolved

select {
case <-ctx.Done():
return
Expand All @@ -267,33 +279,35 @@ func (b *Builder) run(ctx context.Context) {
b.loop(ctx)
}

func (b *Builder) generateInitialPost(ctx context.Context) error {
func (b *Builder) generateInitialPost(ctx context.Context) (*types.Post, *types.PostMetadata, error) {
// Generate the initial POST if we don't have an ATX...
if _, err := b.cdb.GetLastAtx(b.nodeID); err == nil {
return nil
return nil, nil, nil
}
// ...and if we don't have an initial POST persisted already.
if post, err := loadPost(b.nipostBuilder.DataDir()); err == nil {
b.initialPost = post
return nil
return post, &types.PostMetadata{
Challenge: shared.ZeroChallenge,
LabelsPerUnit: b.postSetupProvider.Config().LabelsPerUnit,
}, nil
}

// Create the initial post and save it.
startTime := time.Now()
var err error
events.EmitPostStart(shared.ZeroChallenge)
b.initialPost, _, err = b.postSetupProvider.GenerateProof(ctx, shared.ZeroChallenge, proving.WithPowCreator(b.nodeID.Bytes()))
post, metadata, err := b.postSetupProvider.GenerateProof(ctx, shared.ZeroChallenge, proving.WithPowCreator(b.nodeID.Bytes()))
if err != nil {
events.EmitPostFailure()
return fmt.Errorf("post execution: %w", err)
return nil, nil, fmt.Errorf("post execution: %w", err)
}
events.EmitPostComplete(shared.ZeroChallenge)
metrics.PostDuration.Set(float64(time.Since(startTime).Nanoseconds()))

if err := savePost(b.nipostBuilder.DataDir(), b.initialPost); err != nil {
if err := savePost(b.nipostBuilder.DataDir(), post); err != nil {
b.log.With().Warning("failed to save initial post: %w", log.Err(err))
}
return nil
return post, metadata, nil
}

func (b *Builder) receivePendingPoetClients() *[]PoetProvingServiceClient {
Expand Down
38 changes: 31 additions & 7 deletions activation/activation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ func TestBuilder_StartSmeshingCoinbase(t *testing.T) {
tab.mpost.EXPECT().PrepareInitializer(gomock.Any(), gomock.Any()).AnyTimes()
tab.mpost.EXPECT().StartSession(gomock.Any()).AnyTimes()
tab.mpost.EXPECT().GenerateProof(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&types.Post{}, &types.PostMetadata{}, nil)
tab.mpost.EXPECT().VerifyProof(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
tab.mclock.EXPECT().AwaitLayer(gomock.Any()).Return(make(chan struct{})).AnyTimes()
require.NoError(t, tab.StartSmeshing(coinbase, postSetupOpts))
require.Equal(t, coinbase, tab.Coinbase())
Expand All @@ -272,8 +273,12 @@ func TestBuilder_RestartSmeshing(t *testing.T) {
tab := newTestBuilder(t)
tab.mpost.EXPECT().PrepareInitializer(gomock.Any(), gomock.Any()).AnyTimes()
tab.mpost.EXPECT().StartSession(gomock.Any()).AnyTimes()
tab.mpost.EXPECT().GenerateProof(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&types.Post{}, &types.PostMetadata{}, nil)
tab.mpost.EXPECT().GenerateProof(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&types.Post{}, &types.PostMetadata{
Challenge: shared.ZeroChallenge,
}, nil)
tab.mpost.EXPECT().Reset().AnyTimes()
tab.mpost.EXPECT().VerifyProof(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
tab.mpost.EXPECT().Config().AnyTimes()
ch := make(chan struct{})
close(ch)
tab.mclock.EXPECT().AwaitLayer(gomock.Any()).Return(ch).AnyTimes()
Expand Down Expand Up @@ -387,6 +392,7 @@ func TestBuilder_StopSmeshing_OnPoSTError(t *testing.T) {
tab.mpost.EXPECT().PrepareInitializer(gomock.Any(), gomock.Any()).AnyTimes()
tab.mpost.EXPECT().StartSession(gomock.Any()).Return(nil).AnyTimes()
tab.mpost.EXPECT().GenerateProof(gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.Post{}, &types.PostMetadata{}, nil).AnyTimes()
tab.mpost.EXPECT().VerifyProof(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
ch := make(chan struct{})
close(ch)
now := time.Now()
Expand Down Expand Up @@ -1084,7 +1090,10 @@ func TestBuilder_RetryPublishActivationTx(t *testing.T) {
func TestBuilder_InitialProofGeneratedOnce(t *testing.T) {
tab := newTestBuilder(t, WithPoetConfig(PoetConfig{PhaseShift: layerDuration * 4}))
tab.mpost.EXPECT().GenerateProof(gomock.Any(), shared.ZeroChallenge, gomock.Any()).Return(&types.Post{}, &types.PostMetadata{}, nil)
require.NoError(t, tab.generateInitialPost(context.Background()))
p, m, err := tab.generateInitialPost(context.Background())
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, m)

posEpoch := postGenesisEpoch + 1
challenge := newChallenge(1, types.ATXID{1, 2, 3}, types.ATXID{1, 2, 3}, posEpoch, nil)
Expand All @@ -1104,21 +1113,36 @@ func TestBuilder_InitialProofGeneratedOnce(t *testing.T) {
assertLastAtx(require.New(t), tab.nodeID, types.BytesToHash(poetByte), atx, vPrevAtx, vPrevAtx, layersPerEpoch)

// GenerateProof() should not be called again
require.NoError(t, tab.generateInitialPost(context.Background()))
p, m, err = tab.generateInitialPost(context.Background())
require.NoError(t, err)
require.Nil(t, p)
require.Nil(t, m)
}

func TestBuilder_InitialPostIsPersisted(t *testing.T) {
tab := newTestBuilder(t, WithPoetConfig(PoetConfig{PhaseShift: layerDuration * 4}))
tab.mpost.EXPECT().GenerateProof(gomock.Any(), shared.ZeroChallenge, gomock.Any()).Return(&types.Post{}, &types.PostMetadata{}, nil)
require.NoError(t, tab.generateInitialPost(context.Background()))
tab.mpost.EXPECT().Config().AnyTimes().Return(PostConfig{})
tab.mpost.EXPECT().GenerateProof(gomock.Any(), shared.ZeroChallenge, gomock.Any()).Return(&types.Post{}, &types.PostMetadata{
Challenge: shared.ZeroChallenge,
}, nil)
p, m, err := tab.generateInitialPost(context.Background())
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, m)
poszu marked this conversation as resolved.
Show resolved Hide resolved

// GenerateProof() should not be called again
require.NoError(t, tab.generateInitialPost(context.Background()))
p2, m2, err := tab.generateInitialPost(context.Background())
require.NoError(t, err)
require.EqualValues(t, p, p2)
require.EqualValues(t, m, m2)

// Remove the persisted post file and try again
require.NoError(t, os.Remove(filepath.Join(tab.nipostBuilder.DataDir(), postFilename)))
tab.mpost.EXPECT().GenerateProof(gomock.Any(), shared.ZeroChallenge, gomock.Any()).Return(&types.Post{}, &types.PostMetadata{}, nil)
require.NoError(t, tab.generateInitialPost(context.Background()))
p, m, err = tab.generateInitialPost(context.Background())
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, m)
}

func TestBuilder_UpdatePoets(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions activation/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ type postSetupProvider interface {
StartSession(context context.Context) error
Reset() error
GenerateProof(ctx context.Context, challenge []byte, options ...proving.OptionFunc) (*types.Post, *types.PostMetadata, error)
VerifyProof(ctx context.Context, proof *types.Post, metadata *types.PostMetadata, options ...verifying.OptionFunc) error
CommitmentAtx() (types.ATXID, error)
VRFNonce() (*types.VRFPostIndex, error)
LastOpts() *PostSetupOpts
Expand Down
19 changes: 19 additions & 0 deletions activation/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions activation/nipost.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/spacemeshos/merkle-tree"
"github.com/spacemeshos/poet/shared"
"github.com/spacemeshos/post/proving"
"github.com/spacemeshos/post/verifying"
"golang.org/x/sync/errgroup"

"github.com/spacemeshos/go-spacemesh/activation/metrics"
Expand Down Expand Up @@ -217,6 +218,10 @@ func (nb *NIPostBuilder) BuildNIPost(ctx context.Context, challenge *types.NIPos
events.EmitPostFailure()
return nil, 0, fmt.Errorf("failed to generate Post: %v", err)
}
if err := nb.postSetupProvider.VerifyProof(ctx, proof, proofMetadata, verifying.WithPowCreator(nb.nodeID.Bytes())); err != nil {
events.EmitInvalidPostProof()
return nil, 0, fmt.Errorf("failed to verify Post: %v", err)
}
events.EmitPostComplete(nb.state.PoetProofRef[:])
postGenDuration = time.Since(startTime)
nb.log.With().Info("finished post execution", log.Duration("duration", postGenDuration))
Expand Down
8 changes: 8 additions & 0 deletions activation/nipost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func TestNIPostBuilderWithMocks(t *testing.T) {
postProvider := NewMockpostSetupProvider(ctrl)
postProvider.EXPECT().Status().Return(&PostSetupStatus{State: PostSetupStateComplete})
postProvider.EXPECT().GenerateProof(gomock.Any(), gomock.Any(), gomock.Any())
postProvider.EXPECT().VerifyProof(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)

poetProvider := defaultPoetServiceMock(t, []byte("poet"))
poetProvider.EXPECT().Proof(gomock.Any(), "").Return(&types.PoetProofMessage{
Expand Down Expand Up @@ -277,6 +278,7 @@ func TestNIPostBuilder_BuildNIPost(t *testing.T) {
poetDb, dir, logtest.New(t), sig, PoetConfig{}, mclock)

postProvider.EXPECT().GenerateProof(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
postProvider.EXPECT().VerifyProof(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
nipost, _, err := nb.BuildNIPost(context.Background(), &challenge)
req.NoError(err)
req.NotNil(nipost)
Expand All @@ -301,6 +303,7 @@ func TestNIPostBuilder_BuildNIPost(t *testing.T) {
req.NoError(err)
nb = NewNIPostBuilder(nodeID, postProvider, []PoetProvingServiceClient{poetProver}, poetDb, dir, logtest.New(t), sig, PoetConfig{}, mclock)
postProvider.EXPECT().GenerateProof(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
postProvider.EXPECT().VerifyProof(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
// check that proof ref is not called again
nipost, _, err = nb.BuildNIPost(context.Background(), &challenge2)
req.NoError(err)
Expand All @@ -309,6 +312,7 @@ func TestNIPostBuilder_BuildNIPost(t *testing.T) {
// test state not loading if other challenge provided
poetDb.EXPECT().ValidateAndStore(gomock.Any(), gomock.Any()).Return(nil)
postProvider.EXPECT().GenerateProof(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
postProvider.EXPECT().VerifyProof(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
nipost, _, err = nb.BuildNIPost(context.Background(), &challenge3)
req.NoError(err)
req.NotNil(nipost)
Expand Down Expand Up @@ -365,6 +369,7 @@ func TestNIPostBuilder_ManyPoETs_SubmittingChallenge_DeadlineReached(t *testing.
}, nil
},
)
postProvider.EXPECT().VerifyProof(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
nb := NewNIPostBuilder(types.NodeID{1}, postProvider, poets, poetDb, t.TempDir(), logtest.New(t), sig, poetCfg, mclock)

// Act
Expand Down Expand Up @@ -423,6 +428,7 @@ func TestNIPostBuilder_ManyPoETs_WaitingForProof_DeadlineReached(t *testing.T) {
}, nil
},
)
postProvider.EXPECT().VerifyProof(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
nb := NewNIPostBuilder(types.NodeID{1}, postProvider, poets, poetDb, t.TempDir(), logtest.New(t), sig, poetCfg, mclock)

// Act
Expand Down Expand Up @@ -482,6 +488,7 @@ func TestNIPostBuilder_ManyPoETs_AllFinished(t *testing.T) {
}, nil
},
)
postProvider.EXPECT().VerifyProof(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
nb := NewNIPostBuilder(types.NodeID{1}, postProvider, poets, poetDb, t.TempDir(), logtest.New(t), sig, PoetConfig{}, mclock)

// Act
Expand Down Expand Up @@ -726,6 +733,7 @@ func TestNIPoSTBuilder_Continues_After_Interrupted(t *testing.T) {
}, nil
},
)
postProvider.EXPECT().VerifyProof(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
nb := NewNIPostBuilder(types.NodeID{1}, postProvider, []PoetProvingServiceClient{poet}, poetDb, t.TempDir(), logtest.New(t), sig, poetCfg, mclock)

// Act
Expand Down
30 changes: 30 additions & 0 deletions activation/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/spacemeshos/post/config"
"github.com/spacemeshos/post/initialization"
"github.com/spacemeshos/post/proving"
"github.com/spacemeshos/post/shared"
"github.com/spacemeshos/post/verifying"

"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spacemeshos/go-spacemesh/datastore"
Expand Down Expand Up @@ -489,6 +491,34 @@ func (mgr *PostSetupManager) GenerateProof(ctx context.Context, challenge []byte
return p, m, nil
}

func (mgr *PostSetupManager) VerifyProof(ctx context.Context, proof *types.Post, metadata *types.PostMetadata, options ...verifying.OptionFunc) error {
poszu marked this conversation as resolved.
Show resolved Hide resolved
commitmentAtx, err := mgr.CommitmentAtx()
if err != nil {
return fmt.Errorf("failed to get commitment ATX: %w", err)
}

p := (*shared.Proof)(proof)

m := &shared.ProofMetadata{
NodeId: mgr.id.Bytes(),
CommitmentAtxId: commitmentAtx.Bytes(),
NumUnits: mgr.lastOpts.NumUnits,
Challenge: metadata.Challenge,
LabelsPerUnit: metadata.LabelsPerUnit,
}

verifier, err := NewPostVerifier(mgr.cfg, mgr.logger)
if err != nil {
return fmt.Errorf("creating post verifier: %w", err)
}
opts := []verifying.OptionFunc{
verifying.WithLabelScryptParams(mgr.lastOpts.Scrypt),
}
opts = append(opts, options...)

return verifier.Verify(ctx, p, m, opts...)
}

// VRFNonce returns the VRF nonce found during initialization.
func (mgr *PostSetupManager) VRFNonce() (*types.VRFPostIndex, error) {
mgr.mu.Lock()
Expand Down
9 changes: 9 additions & 0 deletions events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ func EmitPostFailure() {
)
}

func EmitInvalidPostProof() {
const help = "Node generated invalid POST proof. Please verify your POST data."
emitUserEvent(
help,
true,
&pb.Event_PostComplete{PostComplete: &pb.EventPostComplete{}},
)
}

func EmitAtxPublished(
current, target types.EpochID,
id types.ATXID,
Expand Down