diff --git a/activation/activation.go b/activation/activation.go index 6ee9ed80c0..3a15621e68 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -45,13 +45,15 @@ type PoetConfig struct { RequestTimeout time.Duration `mapstructure:"poet-request-timeout"` RequestRetryDelay time.Duration `mapstructure:"retry-delay"` PositioningATXSelectionTimeout time.Duration `mapstructure:"positioning-atx-selection-timeout"` + CertifierInfoCacheTTL time.Duration `mapstructure:"certifier-info-cache-ttl"` MaxRequestRetries int `mapstructure:"retry-max"` } func DefaultPoetConfig() PoetConfig { return PoetConfig{ - RequestRetryDelay: 400 * time.Millisecond, - MaxRequestRetries: 10, + RequestRetryDelay: 400 * time.Millisecond, + MaxRequestRetries: 10, + CertifierInfoCacheTTL: 5 * time.Minute, } } diff --git a/activation/e2e/poet_test.go b/activation/e2e/poet_test.go index a3c8cc0d8c..3aef153168 100644 --- a/activation/e2e/poet_test.go +++ b/activation/e2e/poet_test.go @@ -40,7 +40,7 @@ func (h *HTTPPoetTestHarness) Client( db *activation.PoetDb, cfg activation.PoetConfig, logger *zap.Logger, - opts ...activation.PoetClientOpt, + opts ...activation.PoetServiceOpt, ) (activation.PoetService, error) { return activation.NewPoetService( db, diff --git a/activation/poet.go b/activation/poet.go index b7302b51a6..174e3b66c1 100644 --- a/activation/poet.go +++ b/activation/poet.go @@ -24,6 +24,8 @@ import ( "github.com/spacemeshos/go-spacemesh/sql/localsql/certifier" ) +//go:generate mockgen -typed -package=activation -destination=poet_mocks.go -source=./poet.go + var ( ErrInvalidRequest = errors.New("invalid request") ErrUnauthorized = errors.New("unauthorized") @@ -329,6 +331,12 @@ func (c *HTTPPoetClient) req(ctx context.Context, method, path string, reqBody, return nil } +type certifierInfo struct { + obtained time.Time + url *url.URL + pubkey []byte +} + // poetService is a higher-level interface to communicate with a PoET service. // It wraps the HTTP client, adding additional functionality. type poetService struct { @@ -343,11 +351,15 @@ type poetService struct { proofMembers map[string][]types.Hash32 certifier certifierService + + certifierInfoTTL time.Duration + certifierInfo certifierInfo + certifierInfoMutex sync.Mutex } -type PoetClientOpt func(*poetService) +type PoetServiceOpt func(*poetService) -func WithCertifier(certifier certifierService) PoetClientOpt { +func WithCertifier(certifier certifierService) PoetServiceOpt { return func(c *poetService) { c.certifier = certifier } @@ -358,7 +370,7 @@ func NewPoetService( server types.PoetServer, cfg PoetConfig, logger *zap.Logger, - opts ...PoetClientOpt, + opts ...PoetServiceOpt, ) (*poetService, error) { client, err := NewHTTPPoetClient(server, cfg, WithLogger(logger)) if err != nil { @@ -378,14 +390,15 @@ func NewPoetServiceWithClient( client PoetClient, cfg PoetConfig, logger *zap.Logger, - opts ...PoetClientOpt, + opts ...PoetServiceOpt, ) *poetService { poetClient := &poetService{ - db: db, - logger: logger, - client: client, - requestTimeout: cfg.RequestTimeout, - proofMembers: make(map[string][]types.Hash32, 1), + db: db, + logger: logger, + client: client, + requestTimeout: cfg.RequestTimeout, + certifierInfoTTL: cfg.CertifierInfoCacheTTL, + proofMembers: make(map[string][]types.Hash32, 1), } for _, opt := range opts { @@ -519,9 +532,9 @@ 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.client.CertifierInfo(ctx) + url, pubkey, err := c.getCertifierInfo(ctx) if err != nil { - return nil, fmt.Errorf("getting certifier info: %w", err) + return nil, err } return c.certifier.Certificate(ctx, id, url, pubkey) } @@ -530,9 +543,27 @@ func (c *poetService) recertify(ctx context.Context, id types.NodeID) (*certifie if c.certifier == nil { return nil, errors.New("certifier not configured") } - url, pubkey, err := c.client.CertifierInfo(ctx) + url, pubkey, err := c.getCertifierInfo(ctx) if err != nil { - return nil, fmt.Errorf("getting certifier info: %w", err) + return nil, err } return c.certifier.Recertify(ctx, id, url, pubkey) } + +func (c *poetService) getCertifierInfo(ctx context.Context) (*url.URL, []byte, error) { + c.certifierInfoMutex.Lock() + defer c.certifierInfoMutex.Unlock() + if time.Since(c.certifierInfo.obtained) < c.certifierInfoTTL { + return c.certifierInfo.url, c.certifierInfo.pubkey, nil + } + url, pubkey, err := c.client.CertifierInfo(ctx) + if err != nil { + return nil, nil, fmt.Errorf("getting certifier info: %w", err) + } + c.certifierInfo = certifierInfo{ + obtained: time.Now(), + url: url, + pubkey: pubkey, + } + return c.certifierInfo.url, c.certifierInfo.pubkey, nil +} diff --git a/activation/poet_client_test.go b/activation/poet_client_test.go index 32c6287e78..25a6b7603e 100644 --- a/activation/poet_client_test.go +++ b/activation/poet_client_test.go @@ -20,6 +20,7 @@ import ( "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/localsql/certifier" ) @@ -387,3 +388,36 @@ func TestPoetClient_RecertifiesOnAuthFailure(t *testing.T) { require.NoError(t, err) require.Equal(t, 2, submitCount) } + +func TestPoetService_CachesCertifierInfo(t *testing.T) { + t.Parallel() + type test struct { + name string + ttl time.Duration + } + for _, tc := range []test{ + {name: "cache enabled", ttl: time.Hour}, + {name: "cache disabled"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + cfg := DefaultPoetConfig() + cfg.CertifierInfoCacheTTL = tc.ttl + client := NewMockPoetClient(gomock.NewController(t)) + db := NewPoetDb(sql.InMemory(), zaptest.NewLogger(t)) + 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) + if tc.ttl == 0 { + exp.Times(5) + } + for range 5 { + gotUrl, gotPubkey, err := poet.getCertifierInfo(context.Background()) + require.NoError(t, err) + require.Equal(t, url, gotUrl) + require.Equal(t, pubkey, gotPubkey) + } + }) + } +} diff --git a/activation/poet_mocks.go b/activation/poet_mocks.go new file mode 100644 index 0000000000..ff746b4de6 --- /dev/null +++ b/activation/poet_mocks.go @@ -0,0 +1,277 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./poet.go +// +// Generated by this command: +// +// mockgen -typed -package=activation -destination=poet_mocks.go -source=./poet.go +// + +// Package activation is a generated GoMock package. +package activation + +import ( + context "context" + url "net/url" + reflect "reflect" + time "time" + + types "github.com/spacemeshos/go-spacemesh/common/types" + gomock "go.uber.org/mock/gomock" +) + +// MockPoetClient is a mock of PoetClient interface. +type MockPoetClient struct { + ctrl *gomock.Controller + recorder *MockPoetClientMockRecorder +} + +// MockPoetClientMockRecorder is the mock recorder for MockPoetClient. +type MockPoetClientMockRecorder struct { + mock *MockPoetClient +} + +// NewMockPoetClient creates a new mock instance. +func NewMockPoetClient(ctrl *gomock.Controller) *MockPoetClient { + mock := &MockPoetClient{ctrl: ctrl} + mock.recorder = &MockPoetClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPoetClient) EXPECT() *MockPoetClientMockRecorder { + return m.recorder +} + +// Address mocks base method. +func (m *MockPoetClient) Address() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Address") + ret0, _ := ret[0].(string) + return ret0 +} + +// Address indicates an expected call of Address. +func (mr *MockPoetClientMockRecorder) Address() *MockPoetClientAddressCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Address", reflect.TypeOf((*MockPoetClient)(nil).Address)) + return &MockPoetClientAddressCall{Call: call} +} + +// MockPoetClientAddressCall wrap *gomock.Call +type MockPoetClientAddressCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockPoetClientAddressCall) Return(arg0 string) *MockPoetClientAddressCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockPoetClientAddressCall) Do(f func() string) *MockPoetClientAddressCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockPoetClientAddressCall) DoAndReturn(f func() string) *MockPoetClientAddressCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// CertifierInfo mocks base method. +func (m *MockPoetClient) CertifierInfo(ctx context.Context) (*url.URL, []byte, 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 +} + +// CertifierInfo indicates an expected call of CertifierInfo. +func (mr *MockPoetClientMockRecorder) CertifierInfo(ctx any) *MockPoetClientCertifierInfoCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CertifierInfo", reflect.TypeOf((*MockPoetClient)(nil).CertifierInfo), ctx) + return &MockPoetClientCertifierInfoCall{Call: call} +} + +// MockPoetClientCertifierInfoCall wrap *gomock.Call +type MockPoetClientCertifierInfoCall struct { + *gomock.Call +} + +// 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) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockPoetClientCertifierInfoCall) Do(f func(context.Context) (*url.URL, []byte, 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 { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Id mocks base method. +func (m *MockPoetClient) Id() []byte { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Id") + ret0, _ := ret[0].([]byte) + return ret0 +} + +// Id indicates an expected call of Id. +func (mr *MockPoetClientMockRecorder) Id() *MockPoetClientIdCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Id", reflect.TypeOf((*MockPoetClient)(nil).Id)) + return &MockPoetClientIdCall{Call: call} +} + +// MockPoetClientIdCall wrap *gomock.Call +type MockPoetClientIdCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockPoetClientIdCall) Return(arg0 []byte) *MockPoetClientIdCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockPoetClientIdCall) Do(f func() []byte) *MockPoetClientIdCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockPoetClientIdCall) DoAndReturn(f func() []byte) *MockPoetClientIdCall { + 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() + ret := m.ctrl.Call(m, "PowParams", ctx) + ret0, _ := ret[0].(*PoetPowParams) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PowParams indicates an expected call of PowParams. +func (mr *MockPoetClientMockRecorder) PowParams(ctx any) *MockPoetClientPowParamsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PowParams", reflect.TypeOf((*MockPoetClient)(nil).PowParams), ctx) + return &MockPoetClientPowParamsCall{Call: call} +} + +// MockPoetClientPowParamsCall wrap *gomock.Call +type MockPoetClientPowParamsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockPoetClientPowParamsCall) Return(arg0 *PoetPowParams, arg1 error) *MockPoetClientPowParamsCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockPoetClientPowParamsCall) Do(f func(context.Context) (*PoetPowParams, error)) *MockPoetClientPowParamsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockPoetClientPowParamsCall) DoAndReturn(f func(context.Context) (*PoetPowParams, error)) *MockPoetClientPowParamsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Proof mocks base method. +func (m *MockPoetClient) Proof(ctx context.Context, roundID string) (*types.PoetProofMessage, []types.Hash32, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Proof", ctx, roundID) + ret0, _ := ret[0].(*types.PoetProofMessage) + ret1, _ := ret[1].([]types.Hash32) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Proof indicates an expected call of Proof. +func (mr *MockPoetClientMockRecorder) Proof(ctx, roundID any) *MockPoetClientProofCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Proof", reflect.TypeOf((*MockPoetClient)(nil).Proof), ctx, roundID) + return &MockPoetClientProofCall{Call: call} +} + +// MockPoetClientProofCall wrap *gomock.Call +type MockPoetClientProofCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockPoetClientProofCall) Return(arg0 *types.PoetProofMessage, arg1 []types.Hash32, arg2 error) *MockPoetClientProofCall { + c.Call = c.Call.Return(arg0, arg1, arg2) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockPoetClientProofCall) Do(f func(context.Context, string) (*types.PoetProofMessage, []types.Hash32, error)) *MockPoetClientProofCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockPoetClientProofCall) DoAndReturn(f func(context.Context, string) (*types.PoetProofMessage, []types.Hash32, error)) *MockPoetClientProofCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Submit mocks base method. +func (m *MockPoetClient) Submit(ctx context.Context, deadline time.Time, prefix, challenge []byte, signature types.EdSignature, nodeID types.NodeID, auth PoetAuth) (*types.PoetRound, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Submit", ctx, deadline, prefix, challenge, signature, nodeID, auth) + ret0, _ := ret[0].(*types.PoetRound) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Submit indicates an expected call of Submit. +func (mr *MockPoetClientMockRecorder) Submit(ctx, deadline, prefix, challenge, signature, nodeID, auth any) *MockPoetClientSubmitCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Submit", reflect.TypeOf((*MockPoetClient)(nil).Submit), ctx, deadline, prefix, challenge, signature, nodeID, auth) + return &MockPoetClientSubmitCall{Call: call} +} + +// MockPoetClientSubmitCall wrap *gomock.Call +type MockPoetClientSubmitCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockPoetClientSubmitCall) Return(arg0 *types.PoetRound, arg1 error) *MockPoetClientSubmitCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockPoetClientSubmitCall) Do(f func(context.Context, time.Time, []byte, []byte, types.EdSignature, types.NodeID, PoetAuth) (*types.PoetRound, error)) *MockPoetClientSubmitCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockPoetClientSubmitCall) DoAndReturn(f func(context.Context, time.Time, []byte, []byte, types.EdSignature, types.NodeID, PoetAuth) (*types.PoetRound, error)) *MockPoetClientSubmitCall { + c.Call = c.Call.DoAndReturn(f) + return c +}