Skip to content

Commit

Permalink
Pass proof deadline when registering in poet (#5103)
Browse files Browse the repository at this point in the history
## Motivation
Avoid slipping into the next poet round when submitting a challenge late. It's a prerequisite to support phased poets. It will let the nodes avoid wrongly registering in poets with earlier phases (their round has already started). 

Depends on spacemeshos/poet#410

## Changes
Pass the required time (deadline) by which the poet proof must be ready. If poet decides that the current open round ends after that time - it will reject challenge registration.

## Test Plan
- existing tests should pass
- added poet client unit test to see if the error is properly detected and handled
- the functionality itself is tested in poet tests
  • Loading branch information
poszu committed Oct 5, 2023
1 parent 0d8bb75 commit c050854
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 47 deletions.
47 changes: 34 additions & 13 deletions activation/nipost.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ type PoetProvingServiceClient interface {
PowParams(ctx context.Context) (*PoetPowParams, error)

// Submit registers a challenge in the proving service current open round.
Submit(ctx context.Context, prefix, challenge []byte, signature types.EdSignature, nodeID types.NodeID, pow PoetPoW) (*types.PoetRound, error)
Submit(ctx context.Context, deadline time.Time, prefix, challenge []byte, signature types.EdSignature, nodeID types.NodeID, pow PoetPoW) (*types.PoetRound, error)

// PoetServiceID returns the public key of the PoET proving service.
PoetServiceID(context.Context) (types.PoetServiceID, error)
Expand Down Expand Up @@ -237,7 +237,10 @@ func (nb *NIPostBuilder) BuildNIPost(ctx context.Context, challenge *types.NIPos
prefix := bytes.Join([][]byte{nb.signer.Prefix(), {byte(signing.POET)}}, nil)
submitCtx, cancel := context.WithDeadline(ctx, poetRoundStart)
defer cancel()
poetRequests := nb.submitPoetChallenges(submitCtx, prefix, challengeHash.Bytes(), signature, nb.signer.NodeID())
poetRequests, err := nb.submitPoetChallenges(submitCtx, poetProofDeadline, prefix, challengeHash.Bytes(), signature, nb.signer.NodeID())
if err != nil {
return nil, fmt.Errorf("submitting to poets: %w", err)
}
if len(poetRequests) == 0 {
return nil, &PoetSvcUnstableError{msg: "failed to submit challenge to any PoET", source: submitCtx.Err()}
}
Expand Down Expand Up @@ -328,7 +331,7 @@ func withConditionalTimeout(ctx context.Context, timeout time.Duration) (context
}

// Submit the challenge to a single PoET.
func (nb *NIPostBuilder) submitPoetChallenge(ctx context.Context, client PoetProvingServiceClient, prefix, challenge []byte, signature types.EdSignature, nodeID types.NodeID) (*types.PoetRequest, error) {
func (nb *NIPostBuilder) submitPoetChallenge(ctx context.Context, deadline time.Time, client PoetProvingServiceClient, prefix, challenge []byte, signature types.EdSignature, nodeID types.NodeID) (*types.PoetRequest, error) {
poetServiceID, err := client.PoetServiceID(ctx)
if err != nil {
return nil, &PoetSvcUnstableError{msg: "failed to get PoET service ID", source: err}
Expand All @@ -355,7 +358,7 @@ func (nb *NIPostBuilder) submitPoetChallenge(ctx context.Context, client PoetPro

submitCtx, cancel := withConditionalTimeout(ctx, nb.poetCfg.RequestTimeout)
defer cancel()
round, err := client.Submit(submitCtx, prefix, challenge, signature, nodeID, PoetPoW{
round, err := client.Submit(submitCtx, deadline, prefix, challenge, signature, nodeID, PoetPoW{
Nonce: nonce,
Params: *powParams,
})
Expand All @@ -372,28 +375,46 @@ func (nb *NIPostBuilder) submitPoetChallenge(ctx context.Context, client PoetPro
}

// Submit the challenge to all registered PoETs.
func (nb *NIPostBuilder) submitPoetChallenges(ctx context.Context, prefix, challenge []byte, signature types.EdSignature, nodeID types.NodeID) []types.PoetRequest {
func (nb *NIPostBuilder) submitPoetChallenges(ctx context.Context, deadline time.Time, prefix, challenge []byte, signature types.EdSignature, nodeID types.NodeID) ([]types.PoetRequest, error) {
g, ctx := errgroup.WithContext(ctx)
poetRequestsChannel := make(chan types.PoetRequest, len(nb.poetProvers))
type submitResult struct {
request *types.PoetRequest
err error
}
poetRequestsChannel := make(chan submitResult, len(nb.poetProvers))
for _, poetProver := range nb.poetProvers {
poet := poetProver
g.Go(func() error {
if poetRequest, err := nb.submitPoetChallenge(ctx, poet, prefix, challenge, signature, nodeID); err == nil {
poetRequestsChannel <- *poetRequest
} else {
nb.log.With().Warning("failed to submit challenge to PoET", log.Err(err))
poetRequest, err := nb.submitPoetChallenge(ctx, deadline, poet, prefix, challenge, signature, nodeID)
poetRequestsChannel <- submitResult{
request: poetRequest,
err: err,
}
return nil
})
}
g.Wait()
close(poetRequestsChannel)

allInvalid := true
poetRequests := make([]types.PoetRequest, 0, len(nb.poetProvers))
for request := range poetRequestsChannel {
poetRequests = append(poetRequests, request)
for result := range poetRequestsChannel {
if result.err == nil {
poetRequests = append(poetRequests, *result.request)
allInvalid = false
continue
}

nb.log.With().Warning("failed to submit challenge to poet", log.Err(result.err))
if !errors.Is(result.err, ErrInvalidRequest) {
allInvalid = false
}
}
if allInvalid {
nb.log.Warning("all poet submits were too late. ATX challenge expires")
return nil, ErrATXChallengeExpired
}
return poetRequests
return poetRequests, nil
}

func (nb *NIPostBuilder) getPoetClient(ctx context.Context, id types.PoetServiceID) PoetProvingServiceClient {
Expand Down
13 changes: 7 additions & 6 deletions activation/nipost_mocks.go

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

137 changes: 126 additions & 11 deletions activation/nipost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
func defaultPoetServiceMock(tb testing.TB, id []byte, address string) *MockPoetProvingServiceClient {
tb.Helper()
poetClient := NewMockPoetProvingServiceClient(gomock.NewController(tb))
poetClient.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&types.PoetRound{}, nil)
poetClient.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(&types.PoetRound{}, nil)
poetClient.EXPECT().PoetServiceID(gomock.Any()).AnyTimes().Return(types.PoetServiceID{ServiceID: id}, nil)
poetClient.EXPECT().PowParams(gomock.Any()).AnyTimes().Return(&PoetPowParams{}, nil)
poetClient.EXPECT().Address().AnyTimes().Return(address).AnyTimes()
Expand Down Expand Up @@ -450,8 +450,8 @@ func TestNIPostBuilder_ManyPoETs_SubmittingChallenge_DeadlineReached(t *testing.
{
poet := NewMockPoetProvingServiceClient(ctrl)
poet.EXPECT().PoetServiceID(gomock.Any()).AnyTimes().Return(types.PoetServiceID{ServiceID: []byte("poet0")}, nil)
poet.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, _, _ []byte, _ types.EdSignature, _ types.NodeID, _ PoetPoW) (*types.PoetRound, error) {
poet.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, _ time.Time, _, _ []byte, _ types.EdSignature, _ types.NodeID, _ PoetPoW) (*types.PoetRound, error) {
<-ctx.Done()
return nil, ctx.Err()
},
Expand All @@ -463,7 +463,7 @@ func TestNIPostBuilder_ManyPoETs_SubmittingChallenge_DeadlineReached(t *testing.
{
poet := NewMockPoetProvingServiceClient(ctrl)
poet.EXPECT().PoetServiceID(gomock.Any()).AnyTimes().Return(types.PoetServiceID{ServiceID: []byte("poet1")}, nil)
poet.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.PoetRound{}, nil)
poet.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.PoetRound{}, nil)
poet.EXPECT().Proof(gomock.Any(), gomock.Any()).Return(proof, []types.Member{types.Member(challenge.Hash())}, nil)
poet.EXPECT().PowParams(gomock.Any()).Return(&PoetPowParams{}, nil)
poet.EXPECT().Address().Return("http://localhost:9998")
Expand Down Expand Up @@ -511,6 +511,121 @@ func TestNIPostBuilder_ManyPoETs_SubmittingChallenge_DeadlineReached(t *testing.
req.EqualValues(ref[:], nipost.PostMetadata.Challenge)
}

func TestNIPostBuilder_PoetInvalidRequest(t *testing.T) {
t.Parallel()
// Arrange
req := require.New(t)
challenge := types.NIPostChallenge{
PublishEpoch: postGenesisEpoch + 1,
}

ctrl := gomock.NewController(t)

poet := NewMockPoetProvingServiceClient(ctrl)
poet.EXPECT().PoetServiceID(gomock.Any()).AnyTimes().Return(types.PoetServiceID{ServiceID: []byte("poet0")}, nil)
poet.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.PoetRound{}, ErrInvalidRequest)
poet.EXPECT().PowParams(gomock.Any()).Return(&PoetPowParams{}, nil)
poet.EXPECT().Address().Return("http://localhost:9999")

sig, err := signing.NewEdSigner()
req.NoError(err)
postProvider := NewMockpostSetupProvider(ctrl)
postProvider.EXPECT().Status().Return(&PostSetupStatus{State: PostSetupStateComplete})
postProvider.EXPECT().CommitmentAtx().Return(types.EmptyATXID, nil).AnyTimes()
postProvider.EXPECT().LastOpts().Return(&PostSetupOpts{}).AnyTimes()

nb, err := NewNIPostBuilder(
types.NodeID{1},
postProvider,
NewMockpoetDbAPI(ctrl),
[]string{},
t.TempDir(),
logtest.New(t),
sig,
PoetConfig{PhaseShift: layerDuration * layersPerEpoch / 2},
defaultLayerClockMock(t),
WithNipostValidator(NewMocknipostValidator(ctrl)),
withPoetClients([]PoetProvingServiceClient{poet}),
)
req.NoError(err)

// Act
_, err = nb.BuildNIPost(context.Background(), &challenge)
req.ErrorIs(err, ErrATXChallengeExpired)
}

func TestNIPostBuilder_OnePoetInvalidRequest(t *testing.T) {
t.Parallel()
// Arrange
req := require.New(t)
challenge := types.NIPostChallenge{
PublishEpoch: postGenesisEpoch + 1,
}

proof := &types.PoetProofMessage{PoetProof: types.PoetProof{}}

ctrl := gomock.NewController(t)
nipostValidator := NewMocknipostValidator(ctrl)
poetDb := NewMockpoetDbAPI(ctrl)
poetDb.EXPECT().ValidateAndStore(gomock.Any(), gomock.Any()).Return(nil)

poets := make([]PoetProvingServiceClient, 0, 2)
{
poet := NewMockPoetProvingServiceClient(ctrl)
poet.EXPECT().PoetServiceID(gomock.Any()).AnyTimes().Return(types.PoetServiceID{ServiceID: []byte("poet0")}, nil)
poet.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.PoetRound{}, ErrInvalidRequest)
poet.EXPECT().PowParams(gomock.Any()).Return(&PoetPowParams{}, nil)
poet.EXPECT().Address().Return("http://localhost:9999")
poets = append(poets, poet)
}
{
poet := NewMockPoetProvingServiceClient(ctrl)
poet.EXPECT().PoetServiceID(gomock.Any()).AnyTimes().Return(types.PoetServiceID{ServiceID: []byte("poet1")}, nil)
poet.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.PoetRound{}, nil)
poet.EXPECT().Proof(gomock.Any(), gomock.Any()).Return(proof, []types.Member{types.Member(challenge.Hash())}, nil)
poet.EXPECT().PowParams(gomock.Any()).Return(&PoetPowParams{}, nil)
poet.EXPECT().Address().Return("http://localhost:9998")
poets = append(poets, poet)
}

sig, err := signing.NewEdSigner()
req.NoError(err)
postProvider := NewMockpostSetupProvider(ctrl)
postProvider.EXPECT().Status().Return(&PostSetupStatus{State: PostSetupStateComplete})
postProvider.EXPECT().CommitmentAtx().Return(types.EmptyATXID, nil).AnyTimes()
postProvider.EXPECT().LastOpts().Return(&PostSetupOpts{}).AnyTimes()
postProvider.EXPECT().GenerateProof(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, challenge []byte, _ ...proving.OptionFunc) (*types.Post, *types.PostMetadata, error) {
return &types.Post{}, &types.PostMetadata{
Challenge: challenge,
}, nil
},
)
nipostValidator.EXPECT().Post(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil)
nb, err := NewNIPostBuilder(
types.NodeID{1},
postProvider,
poetDb,
[]string{},
t.TempDir(),
logtest.New(t),
sig,
PoetConfig{PhaseShift: layerDuration * layersPerEpoch / 2},
defaultLayerClockMock(t),
WithNipostValidator(nipostValidator),
withPoetClients(poets),
)
req.NoError(err)

// Act
nipost, err := nb.BuildNIPost(context.Background(), &challenge)
req.NoError(err)

// Verify
ref, _ := proof.Ref()
req.EqualValues(ref[:], nipost.PostMetadata.Challenge)
}

func TestNIPostBuilder_ManyPoETs_AllFinished(t *testing.T) {
t.Parallel()
// Arrange
Expand Down Expand Up @@ -673,7 +788,7 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) {
postProver.EXPECT().Status().Return(&PostSetupStatus{State: PostSetupStateComplete})
poetProver := NewMockPoetProvingServiceClient(ctrl)
poetProver.EXPECT().PoetServiceID(gomock.Any()).AnyTimes().Return(types.PoetServiceID{ServiceID: []byte{}}, nil)
poetProver.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("test"))
poetProver.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("test"))
poetProver.EXPECT().PowParams(gomock.Any()).Return(&PoetPowParams{}, nil)
poetProver.EXPECT().Address().Return("http://localhost:9999")

Expand Down Expand Up @@ -704,8 +819,8 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) {
postProver.EXPECT().Status().Return(&PostSetupStatus{State: PostSetupStateComplete})
poetProver := NewMockPoetProvingServiceClient(ctrl)
poetProver.EXPECT().PoetServiceID(gomock.Any()).AnyTimes().Return(types.PoetServiceID{ServiceID: []byte{}}, nil)
poetProver.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, _, _ []byte, _ types.EdSignature, _ types.NodeID, _ PoetPoW) (*types.PoetRound, error) {
poetProver.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, _ time.Time, _, _ []byte, _ types.EdSignature, _ types.NodeID, _ PoetPoW) (*types.PoetRound, error) {
<-ctx.Done()
return nil, ctx.Err()
},
Expand Down Expand Up @@ -939,8 +1054,8 @@ func TestNIPoSTBuilder_Continues_After_Interrupted(t *testing.T) {

poet := NewMockPoetProvingServiceClient(ctrl)
poet.EXPECT().PoetServiceID(gomock.Any()).AnyTimes().Return(types.PoetServiceID{ServiceID: []byte("poet0")}, nil)
poet.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, _, _ []byte, _ types.EdSignature, _ types.NodeID, _ PoetPoW) (*types.PoetRound, error) {
poet.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, _ time.Time, _, _ []byte, _ types.EdSignature, _ types.NodeID, _ PoetPoW) (*types.PoetRound, error) {
cancel()
return &types.PoetRound{}, context.Canceled
},
Expand Down Expand Up @@ -988,7 +1103,7 @@ func TestNIPoSTBuilder_Continues_After_Interrupted(t *testing.T) {
req.Nil(nipost)

// allow to submit in the second try
poet.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.PoetRound{}, nil)
poet.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.PoetRound{}, nil)

nipost, err = nb.BuildNIPost(context.Background(), &challenge)
req.NoError(err)
Expand Down Expand Up @@ -1088,7 +1203,7 @@ func TestNIPostBuilder_Mainnet_PoetRound3_Workaround(t *testing.T) {
poetProvider.EXPECT().PowParams(gomock.Any()).AnyTimes().Return(&PoetPowParams{}, nil)

// PoET succeeds to submit
poetProvider.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.PoetRound{}, nil)
poetProvider.EXPECT().Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.PoetRound{}, nil)

// proof is fetched from PoET
poetProvider.EXPECT().Proof(gomock.Any(), "").Return(&types.PoetProofMessage{
Expand Down
Loading

0 comments on commit c050854

Please sign in to comment.