diff --git a/README.md b/README.md index 3f2bfde5a2..2f3fb38e7c 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ the build folder you need to ensure that you have the gpu setup dynamic library binary. The simplest way to do this is just copy the library file to be in the same directory as the go-spacemesh binary. Alternatively you can modify your system's library search paths (e.g. LD_LIBRARY_PATH) to ensure that the -library is found._ +library is found. go-spacemesh is p2p software which is designed to form a decentralized network by connecting to other instances of go-spacemesh running on remote computers. diff --git a/activation/activation_errors.go b/activation/activation_errors.go index 63027b0c81..e2e2215e5f 100644 --- a/activation/activation_errors.go +++ b/activation/activation_errors.go @@ -3,6 +3,7 @@ package activation import ( "errors" "fmt" + "strings" ) var ( @@ -21,8 +22,31 @@ type PoetSvcUnstableError struct { source error } -func (e *PoetSvcUnstableError) Error() string { +func (e PoetSvcUnstableError) Error() string { return fmt.Sprintf("poet service is unstable: %s (%v)", e.msg, e.source) } func (e *PoetSvcUnstableError) Unwrap() error { return e.source } + +type PoetRegistrationMismatchError struct { + registrations []string + configuredPoets []string +} + +func (e PoetRegistrationMismatchError) Error() string { + var sb strings.Builder + sb.WriteString("builder: none of configured poets matches the existing registrations.\n") + sb.WriteString("registrations:\n") + for _, r := range e.registrations { + sb.WriteString("\t") + sb.WriteString(r) + sb.WriteString("\n") + } + sb.WriteString("\nconfigured poets:\n") + for _, p := range e.configuredPoets { + sb.WriteString("\t") + sb.WriteString(p) + sb.WriteString("\n") + } + return sb.String() +} diff --git a/activation/e2e/atx_merge_test.go b/activation/e2e/atx_merge_test.go index fda7593427..49f3f6d0a0 100644 --- a/activation/e2e/atx_merge_test.go +++ b/activation/e2e/atx_merge_test.go @@ -246,7 +246,7 @@ func Test_MarryAndMerge(t *testing.T) { GracePeriod: epoch / 4, } - client := ae2e.NewTestPoetClient(2) + client := ae2e.NewTestPoetClient(2, poetCfg) poetSvc := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) clock, err := timesync.NewClock( diff --git a/activation/e2e/builds_atx_v2_test.go b/activation/e2e/builds_atx_v2_test.go index 9c56b565be..2baf326060 100644 --- a/activation/e2e/builds_atx_v2_test.go +++ b/activation/e2e/builds_atx_v2_test.go @@ -92,7 +92,7 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { require.NoError(t, err) t.Cleanup(clock.Close) - client := ae2e.NewTestPoetClient(1) + client := ae2e.NewTestPoetClient(1, poetCfg) poetClient := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) localDB := localsql.InMemory() diff --git a/activation/e2e/checkpoint_merged_test.go b/activation/e2e/checkpoint_merged_test.go index 3984d926f8..f06c319f3d 100644 --- a/activation/e2e/checkpoint_merged_test.go +++ b/activation/e2e/checkpoint_merged_test.go @@ -81,7 +81,7 @@ func Test_CheckpointAfterMerge(t *testing.T) { GracePeriod: epoch / 4, } - client := ae2e.NewTestPoetClient(2) + client := ae2e.NewTestPoetClient(2, poetCfg) poetSvc := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) clock, err := timesync.NewClock( diff --git a/activation/e2e/checkpoint_test.go b/activation/e2e/checkpoint_test.go index 048469b2ff..4e825d944c 100644 --- a/activation/e2e/checkpoint_test.go +++ b/activation/e2e/checkpoint_test.go @@ -71,7 +71,7 @@ func TestCheckpoint_PublishingSoloATXs(t *testing.T) { CycleGap: 3 * epoch / 4, GracePeriod: epoch / 4, } - client := ae2e.NewTestPoetClient(1) + client := ae2e.NewTestPoetClient(1, poetCfg) poetService := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) // ensure that genesis aligns with layer timings diff --git a/activation/e2e/nipost_test.go b/activation/e2e/nipost_test.go index 2a135e3256..74a9e29b57 100644 --- a/activation/e2e/nipost_test.go +++ b/activation/e2e/nipost_test.go @@ -198,7 +198,7 @@ func TestNIPostBuilderWithClients(t *testing.T) { err = nipost.AddPost(localDb, sig.NodeID(), *fullPost(post, info, shared.ZeroChallenge)) require.NoError(t, err) - client := ae2e.NewTestPoetClient(1) + client := ae2e.NewTestPoetClient(1, poetCfg) poetService := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) localDB := localsql.InMemory() @@ -272,7 +272,7 @@ func Test_NIPostBuilderWithMultipleClients(t *testing.T) { } poetDb := activation.NewPoetDb(db, logger.Named("poetDb")) - client := ae2e.NewTestPoetClient(len(signers)) + client := ae2e.NewTestPoetClient(len(signers), poetCfg) poetService := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) mclock := activation.NewMocklayerClock(ctrl) diff --git a/activation/e2e/poet_client.go b/activation/e2e/poet_client.go index c025ed5302..01fee564ec 100644 --- a/activation/e2e/poet_client.go +++ b/activation/e2e/poet_client.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "net/url" "strconv" "sync" "time" @@ -20,15 +19,17 @@ import ( ) type TestPoet struct { - mu sync.Mutex - round int + mu sync.Mutex + round int + poetCfg activation.PoetConfig expectedMembers int registrations chan []byte } -func NewTestPoetClient(expectedMembers int) *TestPoet { +func NewTestPoetClient(expectedMembers int, poetCfg activation.PoetConfig) *TestPoet { return &TestPoet{ + poetCfg: poetCfg, expectedMembers: expectedMembers, registrations: make(chan []byte, expectedMembers), } @@ -66,8 +67,15 @@ func (p *TestPoet) Submit( return &types.PoetRound{ID: strconv.Itoa(round), End: time.Now()}, nil } -func (p *TestPoet) CertifierInfo(ctx context.Context) (*url.URL, []byte, error) { - return nil, nil, errors.New("not supported") +func (p *TestPoet) CertifierInfo(ctx context.Context) (*types.CertifierInfo, error) { + return nil, errors.New("CertifierInfo: not supported") +} + +func (p *TestPoet) Info(ctx context.Context) (*types.PoetInfo, error) { + return &types.PoetInfo{ + PhaseShift: p.poetCfg.PhaseShift, + CycleGap: p.poetCfg.CycleGap, + }, nil } // Build a proof. diff --git a/activation/e2e/poet_test.go b/activation/e2e/poet_test.go index 3aef153168..27d50f18e0 100644 --- a/activation/e2e/poet_test.go +++ b/activation/e2e/poet_test.go @@ -259,10 +259,10 @@ func TestCertifierInfo(t *testing.T) { ) require.NoError(t, err) - url, pubkey, err := client.CertifierInfo(context.Background()) + certInfo, err := client.CertifierInfo(context.Background()) r.NoError(err) - r.Equal("http://localhost:8080", url.String()) - r.Equal([]byte("pubkey"), pubkey) + r.Equal("http://localhost:8080", certInfo.Url.String()) + r.Equal([]byte("pubkey"), certInfo.Pubkey) } func TestNoCertifierInfo(t *testing.T) { @@ -291,6 +291,6 @@ func TestNoCertifierInfo(t *testing.T) { ) require.NoError(t, err) - _, _, err = client.CertifierInfo(context.Background()) + _, err = client.CertifierInfo(context.Background()) r.ErrorContains(err, "poet doesn't support certificates") } diff --git a/activation/e2e/validation_test.go b/activation/e2e/validation_test.go index 124a5762f3..dde1ff6162 100644 --- a/activation/e2e/validation_test.go +++ b/activation/e2e/validation_test.go @@ -51,7 +51,7 @@ func TestValidator_Validate(t *testing.T) { } poetDb := activation.NewPoetDb(sql.InMemory(), logger.Named("poetDb")) - client := ae2e.NewTestPoetClient(1) + client := ae2e.NewTestPoetClient(1, poetCfg) poetService := activation.NewPoetServiceWithClient(poetDb, client, poetCfg, logger) mclock := activation.NewMocklayerClock(ctrl) diff --git a/activation/nipost.go b/activation/nipost.go index bb29387a14..ec9f8ac9c5 100644 --- a/activation/nipost.go +++ b/activation/nipost.go @@ -12,6 +12,7 @@ import ( "github.com/spacemeshos/poet/shared" postshared "github.com/spacemeshos/post/shared" "go.uber.org/zap" + "golang.org/x/exp/maps" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/activation/metrics" @@ -86,8 +87,7 @@ func NewNIPostBuilder( opts ...NIPostBuilderOption, ) (*NIPostBuilder, error) { b := &NIPostBuilder{ - localDB: db, - + localDB: db, postService: postService, logger: lg, poetCfg: poetCfg, @@ -210,7 +210,7 @@ func (nb *NIPostBuilder) BuildNIPost( poetRoundStart := nb.layerClock.LayerToTime((postChallenge.PublishEpoch - 1).FirstLayer()). Add(nb.poetCfg.PhaseShift) - poetRoundEnd := nb.layerClock.LayerToTime(postChallenge.PublishEpoch.FirstLayer()). + curPoetRoundEnd := nb.layerClock.LayerToTime(postChallenge.PublishEpoch.FirstLayer()). Add(nb.poetCfg.PhaseShift). Add(-nb.poetCfg.CycleGap) @@ -224,41 +224,31 @@ func (nb *NIPostBuilder) BuildNIPost( logger.Info("building nipost", zap.Time("poet round start", poetRoundStart), - zap.Time("poet round end", poetRoundEnd), + zap.Time("poet round end", curPoetRoundEnd), zap.Time("publish epoch end", publishEpochEnd), zap.Uint32("publish epoch", postChallenge.PublishEpoch.Uint32()), ) // Phase 0: Submit challenge to PoET services. - count, err := nipost.PoetRegistrationCount(nb.localDB, signer.NodeID()) - if err != nil { - return nil, fmt.Errorf("failed to get poet registration count: %w", err) - } - if count == 0 { - now := time.Now() - // Deadline: start of PoET round for publish epoch. PoET won't accept registrations after that. - if poetRoundStart.Before(now) { - return nil, fmt.Errorf( - "%w: poet round has already started at %s (now: %s)", - ErrATXChallengeExpired, - poetRoundStart, - now, - ) - } - - submitCtx, cancel := context.WithDeadline(ctx, poetRoundStart) - defer cancel() - err := nb.submitPoetChallenges(submitCtx, signer, poetProofDeadline, challenge.Bytes()) - if err != nil { - return nil, fmt.Errorf("submitting to poets: %w", err) - } - count, err := nipost.PoetRegistrationCount(nb.localDB, signer.NodeID()) - if err != nil { - return nil, fmt.Errorf("failed to get poet registration count: %w", err) - } - if count == 0 { - return nil, &PoetSvcUnstableError{msg: "failed to submit challenge to any PoET", source: submitCtx.Err()} - } + // Deadline: start of PoET round: we will not accept registrations after that + submittedRegistrations, err := nb.submitPoetChallenges( + ctx, + signer, + poetProofDeadline, + poetRoundStart, challenge.Bytes(), + ) + regErr := &PoetRegistrationMismatchError{} + switch { + case errors.As(err, ®Err): + logger.Fatal( + "None of the poets listed in the config matches the existing registrations. "+ + "Verify your config and local database state.", + zap.Strings("registrations", regErr.registrations), + zap.Strings("configured_poets", regErr.configuredPoets), + ) + return nil, err + case err != nil: + return nil, fmt.Errorf("submitting to poets: %w", err) } // Phase 1: query PoET services for proofs @@ -280,8 +270,8 @@ func (nb *NIPostBuilder) BuildNIPost( ) } - events.EmitPoetWaitProof(signer.NodeID(), postChallenge.PublishEpoch, poetRoundEnd) - poetProofRef, membership, err = nb.getBestProof(ctx, signer.NodeID(), challenge, postChallenge.PublishEpoch) + events.EmitPoetWaitProof(signer.NodeID(), postChallenge.PublishEpoch, curPoetRoundEnd) + poetProofRef, membership, err = nb.getBestProof(ctx, signer.NodeID(), challenge, submittedRegistrations) if err != nil { return nil, &PoetSvcUnstableError{msg: "getBestProof failed", source: err} } @@ -315,13 +305,17 @@ func (nb *NIPostBuilder) BuildNIPost( defer cancel() nb.logger.Info("starting post execution", zap.Binary("challenge", poetProofRef[:])) + startTime := time.Now() proof, postInfo, err := nb.Proof(postCtx, signer.NodeID(), poetProofRef[:], postChallenge) if err != nil { return nil, fmt.Errorf("failed to generate Post: %w", err) } + postGenDuration := time.Since(startTime) + nb.logger.Info("finished post execution", zap.Duration("duration", postGenDuration)) + metrics.PostDuration.Set(float64(postGenDuration.Nanoseconds())) public.PostSeconds.Set(postGenDuration.Seconds()) @@ -363,7 +357,7 @@ func (nb *NIPostBuilder) submitPoetChallenge( client PoetService, prefix, challenge []byte, signature types.EdSignature, -) error { +) (nipost.PoETRegistration, error) { logger := nb.logger.With( log.ZContext(ctx), zap.String("poet", client.Address()), @@ -377,64 +371,143 @@ func (nb *NIPostBuilder) submitPoetChallenge( round, err := client.Submit(submitCtx, deadline, prefix, challenge, signature, nodeID) if err != nil { - return &PoetSvcUnstableError{msg: "failed to submit challenge to poet service", source: err} + return nipost.PoETRegistration{}, + &PoetSvcUnstableError{msg: "failed to submit challenge to poet service", source: err} } logger.Info("challenge submitted to poet proving service", zap.String("round", round.ID)) - return nipost.AddPoetRegistration(nb.localDB, nodeID, nipost.PoETRegistration{ + + registration := nipost.PoETRegistration{ ChallengeHash: types.Hash32(challenge), Address: client.Address(), RoundID: round.ID, RoundEnd: round.End, - }) + } + + if err := nipost.AddPoetRegistration(nb.localDB, nodeID, registration); err != nil { + return nipost.PoETRegistration{}, err + } + + return registration, err } -// Submit the challenge to all registered PoETs. +// submitPoetChallenges submit the challenge to registered PoETs +// if some registrations are missing and PoET round didn't start. func (nb *NIPostBuilder) submitPoetChallenges( ctx context.Context, signer *signing.EdSigner, - deadline time.Time, + poetProofDeadline time.Time, + curPoetRoundStartDeadline time.Time, challenge []byte, -) error { - signature := signer.Sign(signing.POET, challenge) - prefix := bytes.Join([][]byte{signer.Prefix(), {byte(signing.POET)}}, nil) +) ([]nipost.PoETRegistration, error) { + // check if some registrations missing or were removed nodeID := signer.NodeID() - g, ctx := errgroup.WithContext(ctx) - errChan := make(chan error, len(nb.poetProvers)) - for _, client := range nb.poetProvers { - g.Go(func() error { - errChan <- nb.submitPoetChallenge(ctx, nodeID, deadline, client, prefix, challenge, signature) - return nil - }) + registrations, err := nipost.PoetRegistrations(nb.localDB, nodeID) + if err != nil { + return nil, fmt.Errorf("failed to get poet registrations from db: %w", err) } - g.Wait() - close(errChan) - allInvalid := true - for err := range errChan { - if err == nil { - allInvalid = false - continue + registrationsMap := make(map[string]nipost.PoETRegistration) + for _, reg := range registrations { + registrationsMap[reg.Address] = reg + } + + existingRegistrationsMap := make(map[string]nipost.PoETRegistration) + var missingRegistrations []PoetService + for addr, poet := range nb.poetProvers { + if val, ok := registrationsMap[addr]; ok { + existingRegistrationsMap[addr] = val + } else { + missingRegistrations = append(missingRegistrations, poet) } + } - nb.logger.Warn("failed to submit challenge to poet", zap.Error(err), log.ZShortStringer("smesherID", nodeID)) - if !errors.Is(err, ErrInvalidRequest) { - allInvalid = false + misconfiguredRegistrations := make(map[string]struct{}) + for addr := range registrationsMap { + if _, ok := existingRegistrationsMap[addr]; !ok { + misconfiguredRegistrations[addr] = struct{}{} } } - if allInvalid { - nb.logger.Warn("all poet submits were too late. ATX challenge expires", log.ZShortStringer("smesherID", nodeID)) - return ErrATXChallengeExpired + + if len(misconfiguredRegistrations) != 0 { + nb.logger.Warn( + "Found existing registrations for poets not listed in the config. Will not fetch proof from them.", + zap.Strings("registrations_addresses", maps.Keys(misconfiguredRegistrations)), + log.ZShortStringer("smesherID", nodeID), + ) } - return nil -} -func (nb *NIPostBuilder) getPoetService(ctx context.Context, address string) PoetService { - for _, service := range nb.poetProvers { - if address == service.Address() { - return service + existingRegistrations := maps.Values(existingRegistrationsMap) + if len(missingRegistrations) == 0 { + return existingRegistrations, nil + } + + now := time.Now() + + if curPoetRoundStartDeadline.Before(now) { + switch { + case len(existingRegistrations) == 0 && len(registrations) == 0: + // no existing registration at all, drop current registration challenge + return nil, fmt.Errorf( + "%w: poet round has already started at %s (now: %s)", + ErrATXChallengeExpired, + curPoetRoundStartDeadline, + now, + ) + case len(existingRegistrations) == 0: + // no existing registration for given poets set + return nil, &PoetRegistrationMismatchError{ + registrations: maps.Keys(registrationsMap), + configuredPoets: maps.Keys(nb.poetProvers), + } + default: + return existingRegistrations, nil } } - return nil + + // send registrations to missing addresses + signature := signer.Sign(signing.POET, challenge) + prefix := bytes.Join([][]byte{signer.Prefix(), {byte(signing.POET)}}, nil) + + submitCtx, cancel := context.WithDeadline(ctx, curPoetRoundStartDeadline) + defer cancel() + + eg, ctx := errgroup.WithContext(submitCtx) + submittedRegistrationsChan := make(chan nipost.PoETRegistration, len(missingRegistrations)) + + for _, client := range missingRegistrations { + eg.Go(func() error { + registration, err := nb.submitPoetChallenge( + ctx, nodeID, + poetProofDeadline, + client, prefix, challenge, signature, + ) + if err != nil { + nb.logger.Warn("failed to submit challenge to poet", + zap.Error(err), + log.ZShortStringer("smesherID", nodeID), + ) + } else { + submittedRegistrationsChan <- registration + } + return nil + }) + } + + eg.Wait() + close(submittedRegistrationsChan) + + for registration := range submittedRegistrationsChan { + existingRegistrations = append(existingRegistrations, registration) + } + + if len(existingRegistrations) == 0 { + if curPoetRoundStartDeadline.Before(time.Now()) { + return nil, ErrATXChallengeExpired + } + return nil, &PoetSvcUnstableError{msg: "failed to submit challenge to any PoET", source: ctx.Err()} + } + + return existingRegistrations, nil } // membersContainChallenge verifies that the challenge is included in proof's members. @@ -451,16 +524,12 @@ func (nb *NIPostBuilder) getBestProof( ctx context.Context, nodeID types.NodeID, challenge types.Hash32, - publishEpoch types.EpochID, + registrations []nipost.PoETRegistration, ) (types.PoetProofRef, *types.MerkleProof, error) { type poetProof struct { poet *types.PoetProof membership *types.MerkleProof } - registrations, err := nipost.PoetRegistrations(nb.localDB, nodeID) - if err != nil { - return types.PoetProofRef{}, nil, fmt.Errorf("getting poet registrations: %w", err) - } proofs := make(chan *poetProof, len(registrations)) var eg errgroup.Group @@ -471,11 +540,13 @@ func (nb *NIPostBuilder) getBestProof( zap.String("poet_address", r.Address), zap.String("round", r.RoundID), ) - client := nb.getPoetService(ctx, r.Address) - if client == nil { + + client, ok := nb.poetProvers[r.Address] + if !ok { logger.Warn("poet client not found") continue } + round := r.RoundID waitDeadline := proofDeadline(r.RoundEnd, nb.poetCfg.CycleGap) eg.Go(func() error { diff --git a/activation/nipost_test.go b/activation/nipost_test.go index 95c3cf8a60..41c5b8c782 100644 --- a/activation/nipost_test.go +++ b/activation/nipost_test.go @@ -723,7 +723,7 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { require.ErrorAs(t, err, &poetErr) require.Nil(t, nipst) }) - t.Run("Submit hangs", func(t *testing.T) { + t.Run("Submit hangs, no registrations submitted", func(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) mclock := defaultLayerClockMock(ctrl) @@ -761,8 +761,7 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { challenge, &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}, ) - poetErr := &PoetSvcUnstableError{} - require.ErrorAs(t, err, &poetErr) + require.ErrorIs(t, err, ErrATXChallengeExpired) require.Nil(t, nipst) }) t.Run("GetProof fails", func(t *testing.T) { @@ -817,6 +816,302 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { }) } +// TestNIPoSTBuilder_PoETConfigChange checks if +// it properly detects added/deleted PoET services and re-registers if needed. +func TestNIPoSTBuilder_PoETConfigChange(t *testing.T) { + t.Parallel() + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + challenge := types.NIPostChallenge{ + PublishEpoch: postGenesisEpoch + 2, + } + + challengeHash := wire.NIPostChallengeToWireV1(&challenge).Hash() + + const ( + poetProverAddr = "http://localhost:9999" + poetProverAddr2 = "http://localhost:9988" + ) + + t.Run("1 poet deleted BEFORE round started -> continue with submitted registration", func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + poet := NewMockPoetService(ctrl) + poet.EXPECT(). + Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + Return(&types.PoetRound{}, nil) + poet.EXPECT().Address().Return(poetProverAddr).AnyTimes() + + // successfully registered to 2 poets + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr2, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + + nb, err := NewNIPostBuilder( + db, + nil, + zaptest.NewLogger(t), + PoetConfig{}, + nil, + nil, + WithPoetServices(poet), // add only 1 poet prover + ) + require.NoError(t, err) + + existingRegistrations, err := nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(5*time.Second), + challengeHash.Bytes()) + + require.NoError(t, err) + require.Len(t, existingRegistrations, 1) + require.Equal(t, poetProverAddr, existingRegistrations[0].Address) + }) + + t.Run("1 poet added BEFORE round started -> register to missing poet", func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + poetProver := NewMockPoetService(ctrl) + poetProver.EXPECT().Address().Return(poetProverAddr).AnyTimes() + + addedPoetProver := NewMockPoetService(ctrl) + addedPoetProver.EXPECT(). + Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(&types.PoetRound{}, nil) + addedPoetProver.EXPECT().Address().Return(poetProverAddr2).AnyTimes() + + // successfully registered to 1 poet + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + // successful post exec + nb, err := NewNIPostBuilder( + db, + nil, + zaptest.NewLogger(t), + PoetConfig{}, + nil, + nil, + WithPoetServices(poetProver, addedPoetProver), // add both poet provers + ) + require.NoError(t, err) + + existingRegistrations, err := nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(5*time.Second), + challengeHash.Bytes()) + + require.NoError(t, err) + require.Len(t, existingRegistrations, 2) + require.Equal(t, poetProverAddr, existingRegistrations[0].Address) + require.Equal(t, poetProverAddr2, existingRegistrations[1].Address) + }) + + t.Run("completely changed poet service BEFORE round started -> register new poet", func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + addedPoetProver := NewMockPoetService(ctrl) + addedPoetProver.EXPECT(). + Submit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(&types.PoetRound{}, nil) + addedPoetProver.EXPECT().Address().Return(poetProverAddr2).AnyTimes() + + // successfully registered to removed poet + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + nb, err := NewNIPostBuilder( + db, + nil, + zaptest.NewLogger(t), + PoetConfig{}, + nil, + nil, + WithPoetServices(addedPoetProver), // add new poet + ) + require.NoError(t, err) + + existingRegistrations, err := nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(5*time.Second), + challengeHash.Bytes()) + + require.NoError(t, err) + require.Len(t, existingRegistrations, 1) + require.Equal(t, poetProverAddr2, existingRegistrations[0].Address) + }) + + t.Run("1 poet added AFTER round started -> too late to register to added poet", + func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + poetProver := NewMockPoetService(ctrl) + poetProver.EXPECT().Address().Return(poetProverAddr).AnyTimes() + + addedPoetProver := NewMockPoetService(ctrl) + addedPoetProver.EXPECT().Address().Return(poetProverAddr2).AnyTimes() + + // successfully registered to 1 poet + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + // successful post exec + nb, err := NewNIPostBuilder( + db, + nil, + zaptest.NewLogger(t), + PoetConfig{}, + nil, + nil, + WithPoetServices(poetProver, addedPoetProver), + ) + require.NoError(t, err) + + existingRegistrations, err := nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(-5*time.Second), // poet round started + challengeHash.Bytes()) + + require.NoError(t, err) + require.Len(t, existingRegistrations, 1) + require.Equal(t, poetProverAddr, existingRegistrations[0].Address) + }) + + t.Run("1 poet removed AFTER round started -> too late to register to added poet", + func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + poetProver := NewMockPoetService(ctrl) + poetProver.EXPECT().Address().Return(poetProverAddr).AnyTimes() + + addedPoetProver := NewMockPoetService(ctrl) + addedPoetProver.EXPECT().Address().Return(poetProverAddr2).AnyTimes() + + // successfully registered to 2 poets + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr2, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + + nb, err := NewNIPostBuilder( + db, + nil, + zaptest.NewLogger(t), + PoetConfig{}, + nil, + nil, + WithPoetServices(poetProver), + ) + require.NoError(t, err) + + existingRegistrations, err := nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(-5*time.Second), // poet round started + challengeHash.Bytes()) + + require.NoError(t, err) + require.Len(t, existingRegistrations, 1) + require.Equal(t, poetProverAddr, existingRegistrations[0].Address) + }) + + t.Run("completely changed poet service AFTER round started -> fail, too late to register again", + func(t *testing.T) { + db := localsql.InMemory() + ctrl := gomock.NewController(t) + + poetProver := NewMockPoetService(ctrl) + poetProver.EXPECT().Address().Return(poetProverAddr).AnyTimes() + + // successfully registered to removed poet + err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ + ChallengeHash: challengeHash, + Address: poetProverAddr2, + RoundID: "1", + RoundEnd: time.Now().Add(1 * time.Second), + }) + require.NoError(t, err) + + logger := zaptest.NewLogger(t) + + nb, err := NewNIPostBuilder( + db, + nil, + logger, + PoetConfig{}, + nil, + nil, + WithPoetServices(poetProver), + ) + require.NoError(t, err) + + _, err = nb.submitPoetChallenges( + context.Background(), + sig, + time.Now().Add(10*time.Second), + time.Now().Add(-5*time.Second), // poet round started + challengeHash.Bytes(), + ) + poetErr := &PoetRegistrationMismatchError{} + require.ErrorAs(t, err, &poetErr) + require.ElementsMatch(t, poetErr.configuredPoets, []string{poetProverAddr}) + require.ElementsMatch(t, poetErr.registrations, []string{poetProverAddr2}) + }) +} + // TestNIPoSTBuilder_StaleChallenge checks if // it properly detects that the challenge is stale and the poet round has already started. func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { @@ -828,12 +1123,14 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { sig, err := signing.NewEdSigner() require.NoError(t, err) + const poetAddr = "http://localhost:9999" + // Act & Verify t.Run("no requests, poet round started", func(t *testing.T) { ctrl := gomock.NewController(t) mclock := NewMocklayerClock(ctrl) poetProver := NewMockPoetService(ctrl) - poetProver.EXPECT().Address().Return("http://localhost:9999") + poetProver.EXPECT().Address().Return(poetAddr).AnyTimes() mclock.EXPECT().LayerToTime(gomock.Any()).DoAndReturn( func(got types.LayerID) time.Time { return genesis.Add(layerDuration * time.Duration(got)) @@ -862,7 +1159,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { ctrl := gomock.NewController(t) mclock := NewMocklayerClock(ctrl) poetProver := NewMockPoetService(ctrl) - poetProver.EXPECT().Address().Return("http://localhost:9999") + poetProver.EXPECT().Address().Return(poetAddr).AnyTimes() mclock.EXPECT().LayerToTime(gomock.Any()).DoAndReturn( func(got types.LayerID) time.Time { return genesis.Add(layerDuration * time.Duration(got)) @@ -889,7 +1186,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { // successfully registered to at least one poet err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ ChallengeHash: challengeHash, - Address: "http://poet1.com", + Address: poetAddr, RoundID: "1", RoundEnd: time.Now().Add(10 * time.Second), }) @@ -904,7 +1201,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { ctrl := gomock.NewController(t) mclock := NewMocklayerClock(ctrl) poetProver := NewMockPoetService(ctrl) - poetProver.EXPECT().Address().Return("http://localhost:9999") + poetProver.EXPECT().Address().Return(poetAddr).AnyTimes() mclock.EXPECT().LayerToTime(gomock.Any()).DoAndReturn( func(got types.LayerID) time.Time { return genesis.Add(layerDuration * time.Duration(got)) @@ -931,7 +1228,7 @@ func TestNIPoSTBuilder_StaleChallenge(t *testing.T) { // successfully registered to at least one poet err = nipost.AddPoetRegistration(db, sig.NodeID(), nipost.PoETRegistration{ ChallengeHash: challengeHash, - Address: "http://poet1.com", + Address: poetAddr, RoundID: "1", RoundEnd: time.Now().Add(10 * time.Second), }) diff --git a/activation/poet.go b/activation/poet.go index 0a30b32953..af9f1f09a1 100644 --- a/activation/poet.go +++ b/activation/poet.go @@ -31,6 +31,7 @@ var ( ErrInvalidRequest = errors.New("invalid request") ErrUnauthorized = errors.New("unauthorized") ErrCertificatesNotSupported = errors.New("poet doesn't support certificates") + ErrIncompatiblePhaseShift = errors.New("fetched poet phase_shift is incompatible with configured phase_shift") ) type PoetPowParams struct { @@ -53,7 +54,7 @@ type PoetClient interface { Address() string PowParams(ctx context.Context) (*PoetPowParams, error) - CertifierInfo(ctx context.Context) (*url.URL, []byte, error) + CertifierInfo(ctx context.Context) (*types.CertifierInfo, error) Submit( ctx context.Context, deadline time.Time, @@ -63,6 +64,7 @@ type PoetClient interface { auth PoetAuth, ) (*types.PoetRound, error) Proof(ctx context.Context, roundID string) (*types.PoetProofMessage, []types.Hash32, error) + Info(ctx context.Context) (*types.PoetInfo, error) } // HTTPPoetClient implements PoetProvingServiceClient interface. @@ -184,20 +186,15 @@ func (c *HTTPPoetClient) PowParams(ctx context.Context) (*PoetPowParams, error) }, nil } -func (c *HTTPPoetClient) CertifierInfo(ctx context.Context) (*url.URL, []byte, error) { - info, err := c.info(ctx) +func (c *HTTPPoetClient) CertifierInfo(ctx context.Context) (*types.CertifierInfo, error) { + info, err := c.Info(ctx) if err != nil { - return nil, nil, err - } - certifierInfo := info.GetCertifier() - if certifierInfo == nil { - return nil, nil, ErrCertificatesNotSupported + return nil, err } - url, err := url.Parse(certifierInfo.Url) - if err != nil { - return nil, nil, fmt.Errorf("parsing certifier address: %w", err) + if info.Certifier == nil { + return nil, ErrCertificatesNotSupported } - return url, certifierInfo.Pubkey, nil + return info.Certifier, nil } // Submit registers a challenge in the proving service current open round. @@ -242,12 +239,30 @@ func (c *HTTPPoetClient) Submit( return &types.PoetRound{ID: resBody.RoundId, End: roundEnd}, nil } -func (c *HTTPPoetClient) info(ctx context.Context) (*rpcapi.InfoResponse, error) { +func (c *HTTPPoetClient) Info(ctx context.Context) (*types.PoetInfo, error) { resBody := rpcapi.InfoResponse{} if err := c.req(ctx, http.MethodGet, "/v1/info", nil, &resBody); err != nil { return nil, fmt.Errorf("getting poet info: %w", err) } - return &resBody, nil + + var certifierInfo *types.CertifierInfo + if resBody.GetCertifier() != nil { + url, err := url.Parse(resBody.GetCertifier().Url) + if err != nil { + return nil, fmt.Errorf("parsing certifier address: %w", err) + } + certifierInfo = &types.CertifierInfo{ + Url: url, + Pubkey: resBody.GetCertifier().Pubkey, + } + } + + return &types.PoetInfo{ + ServicePubkey: resBody.ServicePubkey, + PhaseShift: resBody.PhaseShift.AsDuration(), + CycleGap: resBody.CycleGap.AsDuration(), + Certifier: certifierInfo, + }, nil } // Proof implements PoetProvingServiceClient. @@ -332,11 +347,6 @@ func (c *HTTPPoetClient) req(ctx context.Context, method, path string, reqBody, return nil } -type certifierInfo struct { - url *url.URL - pubkey []byte -} - type cachedData[T any] struct { mu sync.Mutex data T @@ -373,7 +383,10 @@ type poetService struct { certifier certifierService - certifierInfoCache cachedData[*certifierInfo] + certifierInfoCache cachedData[*types.CertifierInfo] + mtx sync.Mutex + expectedPhaseShift time.Duration + fetchedPhaseShift time.Duration powParamsCache cachedData[*PoetPowParams] } @@ -394,7 +407,7 @@ func NewPoetService( ) (*poetService, error) { client, err := NewHTTPPoetClient(server, cfg, WithLogger(logger)) if err != nil { - return nil, fmt.Errorf("creating HTTP poet client %s: %w", server.Address, err) + return nil, err } return NewPoetServiceWithClient( db, @@ -412,21 +425,50 @@ func NewPoetServiceWithClient( logger *zap.Logger, opts ...PoetServiceOpt, ) *poetService { - poetClient := &poetService{ + service := &poetService{ db: db, logger: logger, client: client, requestTimeout: cfg.RequestTimeout, - certifierInfoCache: cachedData[*certifierInfo]{ttl: cfg.CertifierInfoCacheTTL}, + certifierInfoCache: cachedData[*types.CertifierInfo]{ttl: cfg.CertifierInfoCacheTTL}, powParamsCache: cachedData[*PoetPowParams]{ttl: cfg.PowParamsCacheTTL}, proofMembers: make(map[string][]types.Hash32, 1), + expectedPhaseShift: cfg.PhaseShift, } - for _, opt := range opts { - opt(poetClient) + opt(service) + } + + err := service.verifyPhaseShiftConfiguration(context.Background()) + switch { + case errors.Is(err, ErrIncompatiblePhaseShift): + logger.Fatal("failed to create poet service", zap.String("poet", client.Address())) + return nil + case err != nil: + logger.Warn("failed to fetch poet phase shift", + zap.String("poet", client.Address()), + zap.Error(err), + ) } + return service +} + +func (c *poetService) verifyPhaseShiftConfiguration(ctx context.Context) error { + c.mtx.Lock() + defer c.mtx.Unlock() - return poetClient + if c.fetchedPhaseShift != 0 { + return nil + } + resp, err := c.client.Info(ctx) + if err != nil { + return err + } else if resp.PhaseShift != c.expectedPhaseShift { + return ErrIncompatiblePhaseShift + } + + c.fetchedPhaseShift = resp.PhaseShift + return nil } func (c *poetService) Address() string { @@ -452,6 +494,7 @@ func (c *poetService) authorize( // Fallback to PoW // TODO: remove this fallback once we migrate to certificates fully. logger.Info("falling back to PoW authorization") + powCtx, cancel := withConditionalTimeout(ctx, c.requestTimeout) defer cancel() powParams, err := c.powParams(powCtx) @@ -483,11 +526,10 @@ func (c *poetService) reauthorize( ctx context.Context, id types.NodeID, challenge []byte, - logger *zap.Logger, ) (*PoetAuth, error) { if c.certifier != nil { - if _, pubkey, err := c.getCertifierInfo(ctx); err == nil { - if err := c.certifier.DeleteCertificate(id, pubkey); err != nil { + if info, err := c.getCertifierInfo(ctx); err == nil { + if err := c.certifier.DeleteCertificate(id, info.Pubkey); err != nil { return nil, fmt.Errorf("deleting cert: %w", err) } } @@ -508,7 +550,16 @@ func (c *poetService) Submit( log.ZShortStringer("smesherID", nodeID), ) - // Try obtain a certificate + err := c.verifyPhaseShiftConfiguration(ctx) + switch { + case errors.Is(err, ErrIncompatiblePhaseShift): + logger.Fatal("failed to submit challenge", zap.String("poet", c.client.Address())) + return nil, err + case err != nil: + return nil, err + } + + // Try to obtain a certificate auth, err := c.authorize(ctx, nodeID, challenge, logger) if err != nil { return nil, fmt.Errorf("authorizing: %w", err) @@ -524,7 +575,7 @@ func (c *poetService) Submit( return round, nil case errors.Is(err, ErrUnauthorized): logger.Warn("failed to submit challenge as unauthorized - authorizing again", zap.Error(err)) - auth, err := c.reauthorize(ctx, nodeID, challenge, logger) + auth, err := c.reauthorize(ctx, nodeID, challenge) if err != nil { return nil, fmt.Errorf("authorizing: %w", err) } @@ -568,26 +619,25 @@ func (c *poetService) Certify(ctx context.Context, id types.NodeID) (*certifier. if c.certifier == nil { return nil, errors.New("certifier not configured") } - url, pubkey, err := c.getCertifierInfo(ctx) + info, err := c.getCertifierInfo(ctx) if err != nil { return nil, err } - return c.certifier.Certificate(ctx, id, url, pubkey) + return c.certifier.Certificate(ctx, id, info.Url, info.Pubkey) } -func (c *poetService) getCertifierInfo(ctx context.Context) (*url.URL, []byte, error) { - info, err := c.certifierInfoCache.get(func() (*certifierInfo, error) { - url, pubkey, err := c.client.CertifierInfo(ctx) +func (c *poetService) getCertifierInfo(ctx context.Context) (*types.CertifierInfo, error) { + info, err := c.certifierInfoCache.get(func() (*types.CertifierInfo, error) { + certifierInfo, err := c.client.CertifierInfo(ctx) if err != nil { return nil, fmt.Errorf("getting certifier info: %w", err) } - return &certifierInfo{url: url, pubkey: pubkey}, nil + return certifierInfo, nil }) if err != nil { - return nil, nil, err + return nil, err } - - return info.url, info.pubkey, nil + return info, nil } func (c *poetService) powParams(ctx context.Context) (*PoetPowParams, error) { diff --git a/activation/poet_client_test.go b/activation/poet_client_test.go index 782872d347..2ed127a069 100644 --- a/activation/poet_client_test.go +++ b/activation/poet_client_test.go @@ -16,6 +16,7 @@ import ( "github.com/spacemeshos/poet/server" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "go.uber.org/zap" "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" "google.golang.org/protobuf/encoding/protojson" @@ -199,26 +200,27 @@ func TestPoetClient_CachesProof(t *testing.T) { func TestPoetClient_QueryProofTimeout(t *testing.T) { t.Parallel() - block := make(chan struct{}) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - <-block - })) - defer ts.Close() - defer close(block) - - server := types.PoetServer{ - Address: ts.URL, - Pubkey: types.NewBase64Enc([]byte("pubkey")), - } cfg := PoetConfig{ RequestTimeout: time.Millisecond * 100, + PhaseShift: 10 * time.Second, } - client, err := NewHTTPPoetClient(server, cfg, withCustomHttpClient(ts.Client())) - require.NoError(t, err) + client := NewMockPoetClient(gomock.NewController(t)) + // first call on info returns the expected value + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{ + PhaseShift: cfg.PhaseShift, + }, nil) poet := NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) + // any additional call on Info will block + client.EXPECT().Proof(gomock.Any(), "1").DoAndReturn( + func(ctx context.Context, _ string) (*types.PoetProofMessage, []types.Hash32, error) { + <-ctx.Done() + return nil, nil, ctx.Err() + }, + ).AnyTimes() + start := time.Now() - eg := errgroup.Group{} + var eg errgroup.Group for range 50 { eg.Go(func() error { _, _, err := poet.Proof(context.Background(), "1") @@ -452,6 +454,7 @@ func TestPoetClient_FallbacksToPowWhenCannotRecertify(t *testing.T) { client, err := NewHTTPPoetClient(server, cfg, withCustomHttpClient(ts.Client())) require.NoError(t, err) + poet := NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t), WithCertifier(mCertifier)) _, err = poet.Submit(context.Background(), time.Time{}, nil, nil, types.RandomEdSignature(), sig.NodeID()) @@ -475,18 +478,24 @@ func TestPoetService_CachesCertifierInfo(t *testing.T) { cfg.CertifierInfoCacheTTL = tc.ttl client := NewMockPoetClient(gomock.NewController(t)) db := NewPoetDb(sql.InMemory(), zaptest.NewLogger(t)) + + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{}, nil) + poet := NewPoetServiceWithClient(db, client, cfg, zaptest.NewLogger(t)) + url := &url.URL{Host: "certifier.hello"} pubkey := []byte("pubkey") - exp := client.EXPECT().CertifierInfo(gomock.Any()).Return(url, pubkey, nil) + exp := client.EXPECT().CertifierInfo(gomock.Any()). + Return(&types.CertifierInfo{Url: url, Pubkey: pubkey}, nil) if tc.ttl == 0 { exp.Times(5) } for range 5 { - gotUrl, gotPubkey, err := poet.getCertifierInfo(context.Background()) + info, err := poet.getCertifierInfo(context.Background()) require.NoError(t, err) - require.Equal(t, url, gotUrl) - require.Equal(t, pubkey, gotPubkey) + require.Equal(t, url, info.Url) + require.Equal(t, pubkey, info.Pubkey) } }) } @@ -507,6 +516,10 @@ func TestPoetService_CachesPowParams(t *testing.T) { cfg := DefaultPoetConfig() cfg.PowParamsCacheTTL = tc.ttl client := NewMockPoetClient(gomock.NewController(t)) + + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{}, nil) + client.EXPECT().Address().Return("some_address").AnyTimes() + poet := NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) params := PoetPowParams{ @@ -525,3 +538,115 @@ func TestPoetService_CachesPowParams(t *testing.T) { }) } } + +func TestPoetService_FetchPoetPhaseShift(t *testing.T) { + t.Parallel() + const phaseShift = time.Second + + t.Run("poet service created: expected and fetched phase shift are matching", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{ + PhaseShift: phaseShift, + }, nil) + + NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) + }) + + t.Run("poet service created: phase shift is not fetched", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(nil, errors.New("some error")) + + NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) + }) + + t.Run("poet service creation failed: expected and fetched phase shift are not matching", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{ + PhaseShift: phaseShift * 2, + }, nil) + + log := zaptest.NewLogger(t).WithOptions(zap.WithFatalHook(calledFatal(t))) + NewPoetServiceWithClient(nil, client, cfg, log) + }) + + t.Run("fetch phase shift before submitting challenge: success", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(nil, errors.New("some error")) + + poet := NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{PhaseShift: phaseShift}, nil) + client.EXPECT().PowParams(gomock.Any()).Return(&PoetPowParams{}, nil) + client.EXPECT(). + Submit( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + gomock.Any(), gomock.Any(), gomock.Any()). + Return(&types.PoetRound{}, nil) + + _, err = poet.Submit(context.Background(), time.Time{}, nil, nil, types.RandomEdSignature(), sig.NodeID()) + require.NoError(t, err) + }) + + t.Run("fetch phase shift before submitting challenge: failed to fetch poet info", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(nil, errors.New("some error")) + + poet := NewPoetServiceWithClient(nil, client, cfg, zaptest.NewLogger(t)) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + expectedErr := errors.New("some error") + client.EXPECT().Info(gomock.Any()).Return(nil, expectedErr) + + _, err = poet.Submit(context.Background(), time.Time{}, nil, nil, types.RandomEdSignature(), sig.NodeID()) + require.ErrorIs(t, err, expectedErr) + }) + + t.Run("fetch phase shift before submitting challenge: fetched and expected phase shift do not match", + func(t *testing.T) { + cfg := DefaultPoetConfig() + cfg.PhaseShift = phaseShift + + client := NewMockPoetClient(gomock.NewController(t)) + client.EXPECT().Address().Return("some_addr").AnyTimes() + client.EXPECT().Info(gomock.Any()).Return(nil, errors.New("some error")) + + log := zaptest.NewLogger(t).WithOptions(zap.WithFatalHook(calledFatal(t))) + poet := NewPoetServiceWithClient(nil, client, cfg, log) + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + client.EXPECT().Info(gomock.Any()).Return(&types.PoetInfo{ + PhaseShift: phaseShift * 2, + }, nil) + + poet.Submit(context.Background(), time.Time{}, nil, nil, types.RandomEdSignature(), sig.NodeID()) + }) +} diff --git a/activation/poet_mocks.go b/activation/poet_mocks.go index ff746b4de6..885105fef3 100644 --- a/activation/poet_mocks.go +++ b/activation/poet_mocks.go @@ -11,7 +11,6 @@ package activation import ( context "context" - url "net/url" reflect "reflect" time "time" @@ -81,13 +80,12 @@ func (c *MockPoetClientAddressCall) DoAndReturn(f func() string) *MockPoetClient } // CertifierInfo mocks base method. -func (m *MockPoetClient) CertifierInfo(ctx context.Context) (*url.URL, []byte, error) { +func (m *MockPoetClient) CertifierInfo(ctx context.Context) (*types.CertifierInfo, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CertifierInfo", ctx) - ret0, _ := ret[0].(*url.URL) - ret1, _ := ret[1].([]byte) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 + ret0, _ := ret[0].(*types.CertifierInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 } // CertifierInfo indicates an expected call of CertifierInfo. @@ -103,19 +101,19 @@ type MockPoetClientCertifierInfoCall struct { } // Return rewrite *gomock.Call.Return -func (c *MockPoetClientCertifierInfoCall) Return(arg0 *url.URL, arg1 []byte, arg2 error) *MockPoetClientCertifierInfoCall { - c.Call = c.Call.Return(arg0, arg1, arg2) +func (c *MockPoetClientCertifierInfoCall) Return(arg0 *types.CertifierInfo, arg1 error) *MockPoetClientCertifierInfoCall { + c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *MockPoetClientCertifierInfoCall) Do(f func(context.Context) (*url.URL, []byte, error)) *MockPoetClientCertifierInfoCall { +func (c *MockPoetClientCertifierInfoCall) Do(f func(context.Context) (*types.CertifierInfo, error)) *MockPoetClientCertifierInfoCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockPoetClientCertifierInfoCall) DoAndReturn(f func(context.Context) (*url.URL, []byte, error)) *MockPoetClientCertifierInfoCall { +func (c *MockPoetClientCertifierInfoCall) DoAndReturn(f func(context.Context) (*types.CertifierInfo, error)) *MockPoetClientCertifierInfoCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -158,6 +156,45 @@ func (c *MockPoetClientIdCall) DoAndReturn(f func() []byte) *MockPoetClientIdCal return c } +// Info mocks base method. +func (m *MockPoetClient) Info(ctx context.Context) (*types.PoetInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Info", ctx) + ret0, _ := ret[0].(*types.PoetInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Info indicates an expected call of Info. +func (mr *MockPoetClientMockRecorder) Info(ctx any) *MockPoetClientInfoCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockPoetClient)(nil).Info), ctx) + return &MockPoetClientInfoCall{Call: call} +} + +// MockPoetClientInfoCall wrap *gomock.Call +type MockPoetClientInfoCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockPoetClientInfoCall) Return(arg0 *types.PoetInfo, arg1 error) *MockPoetClientInfoCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockPoetClientInfoCall) Do(f func(context.Context) (*types.PoetInfo, error)) *MockPoetClientInfoCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockPoetClientInfoCall) DoAndReturn(f func(context.Context) (*types.PoetInfo, error)) *MockPoetClientInfoCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // PowParams mocks base method. func (m *MockPoetClient) PowParams(ctx context.Context) (*PoetPowParams, error) { m.ctrl.T.Helper() diff --git a/common/types/poet.go b/common/types/poet.go index 0d1d1177ea..f04b613517 100644 --- a/common/types/poet.go +++ b/common/types/poet.go @@ -3,6 +3,7 @@ package types import ( "encoding/hex" "fmt" + "net/url" "time" poetShared "github.com/spacemeshos/poet/shared" @@ -97,3 +98,15 @@ type PoetRound struct { ID string `scale:"max=32"` End time.Time } + +type PoetInfo struct { + ServicePubkey []byte + PhaseShift time.Duration + CycleGap time.Duration + Certifier *CertifierInfo +} + +type CertifierInfo struct { + Url *url.URL + Pubkey []byte +} diff --git a/node/node.go b/node/node.go index a512018bd0..95a0b05dec 100644 --- a/node/node.go +++ b/node/node.go @@ -1042,7 +1042,7 @@ func (app *App) initServices(ctx context.Context) error { activation.WithCertifier(certifier), ) if err != nil { - app.log.Panic("failed to create poet client: %v", err) + app.log.Panic("failed to create poet client with address %v: %v", server.Address, err) } poetClients = append(poetClients, client) } diff --git a/node/node_test.go b/node/node_test.go index 3a8f5a2407..274ed57ca7 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -997,7 +997,7 @@ func TestAdminEvents(t *testing.T) { select { case <-app.Started(): - case <-time.After(10 * time.Second): + case <-time.After(15 * time.Second): require.Fail(t, "app did not start in time") } @@ -1090,7 +1090,7 @@ func TestAdminEvents_MultiSmesher(t *testing.T) { select { case <-app.Started(): - case <-time.After(10 * time.Second): + case <-time.After(15 * time.Second): require.Fail(t, "app did not start in time") } diff --git a/sql/localsql/nipost/poet_registration.go b/sql/localsql/nipost/poet_registration.go index 50aa83ac10..743de7b4cc 100644 --- a/sql/localsql/nipost/poet_registration.go +++ b/sql/localsql/nipost/poet_registration.go @@ -37,23 +37,6 @@ func AddPoetRegistration( return nil } -func PoetRegistrationCount(db sql.Executor, nodeID types.NodeID) (int, error) { - var count int - enc := func(stmt *sql.Statement) { - stmt.BindBytes(1, nodeID.Bytes()) - } - dec := func(stmt *sql.Statement) bool { - count = int(stmt.ColumnInt64(0)) - return true - } - query := `select count(*) from poet_registration where id = ?1;` - _, err := db.Exec(query, enc, dec) - if err != nil { - return 0, fmt.Errorf("get poet registration count for node id %s: %w", nodeID.ShortString(), err) - } - return count, nil -} - func ClearPoetRegistrations(db sql.Executor, nodeID types.NodeID) error { enc := func(stmt *sql.Statement) { stmt.BindBytes(1, nodeID.Bytes()) @@ -66,9 +49,11 @@ func ClearPoetRegistrations(db sql.Executor, nodeID types.NodeID) error { func PoetRegistrations(db sql.Executor, nodeID types.NodeID) ([]PoETRegistration, error) { var registrations []PoETRegistration + enc := func(stmt *sql.Statement) { stmt.BindBytes(1, nodeID.Bytes()) } + dec := func(stmt *sql.Statement) bool { registration := PoETRegistration{ Address: stmt.ColumnText(1), @@ -79,10 +64,13 @@ func PoetRegistrations(db sql.Executor, nodeID types.NodeID) ([]PoETRegistration registrations = append(registrations, registration) return true } - query := `select hash, address, round_id, round_end from poet_registration where id = ?1;` + + query := `SELECT hash, address, round_id, round_end FROM poet_registration WHERE id = ?1;` + _, err := db.Exec(query, enc, dec) if err != nil { return nil, fmt.Errorf("get poet registrations for node id %s: %w", nodeID.ShortString(), err) } + return registrations, nil } diff --git a/sql/localsql/nipost/poet_registration_test.go b/sql/localsql/nipost/poet_registration_test.go index 9d130043ae..a4228a6371 100644 --- a/sql/localsql/nipost/poet_registration_test.go +++ b/sql/localsql/nipost/poet_registration_test.go @@ -31,18 +31,14 @@ func Test_AddPoetRegistration(t *testing.T) { err := AddPoetRegistration(db, nodeID, reg1) require.NoError(t, err) - count, err := PoetRegistrationCount(db, nodeID) + registrations, err := PoetRegistrations(db, nodeID) require.NoError(t, err) - require.Equal(t, 1, count) + require.Len(t, registrations, 1) err = AddPoetRegistration(db, nodeID, reg2) require.NoError(t, err) - count, err = PoetRegistrationCount(db, nodeID) - require.NoError(t, err) - require.Equal(t, 2, count) - - registrations, err := PoetRegistrations(db, nodeID) + registrations, err = PoetRegistrations(db, nodeID) require.NoError(t, err) require.Len(t, registrations, 2) require.Equal(t, reg1, registrations[0]) @@ -51,9 +47,9 @@ func Test_AddPoetRegistration(t *testing.T) { err = ClearPoetRegistrations(db, nodeID) require.NoError(t, err) - count, err = PoetRegistrationCount(db, nodeID) + registrations, err = PoetRegistrations(db, nodeID) require.NoError(t, err) - require.Equal(t, 0, count) + require.Empty(t, registrations) } func Test_AddPoetRegistration_NoDuplicates(t *testing.T) { @@ -70,14 +66,14 @@ func Test_AddPoetRegistration_NoDuplicates(t *testing.T) { err := AddPoetRegistration(db, nodeID, reg) require.NoError(t, err) - count, err := PoetRegistrationCount(db, nodeID) + registrations, err := PoetRegistrations(db, nodeID) require.NoError(t, err) - require.Equal(t, 1, count) + require.Len(t, registrations, 1) err = AddPoetRegistration(db, nodeID, reg) require.ErrorIs(t, err, sql.ErrObjectExists) - count, err = PoetRegistrationCount(db, nodeID) + registrations, err = PoetRegistrations(db, nodeID) require.NoError(t, err) - require.Equal(t, 1, count) + require.Len(t, registrations, 1) }