From bbcfad4bb4eaad121a8163c2aa6b67addbdeea77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20R=C3=B3=C5=BCa=C5=84ski?= Date: Thu, 1 Aug 2024 11:41:37 +0000 Subject: [PATCH] fallback to PoW if cannot recertify for poet (#6197) ## Motivation Fallback to PoW if cannot recertify after poet registration failed with 401 (unauthorized). --- activation/certifier.go | 28 ++++++------ activation/interface.go | 7 +-- activation/mocks.go | 31 +++++++------ activation/poet.go | 36 +++++++++------- activation/poet_client_test.go | 79 +++++++++++++++++++++++++++++++++- sql/localsql/certifier/db.go | 13 ++++++ 6 files changed, 140 insertions(+), 54 deletions(-) diff --git a/activation/certifier.go b/activation/certifier.go index 8aecbb47e5..bbd7cf60f6 100644 --- a/activation/certifier.go +++ b/activation/certifier.go @@ -117,7 +117,15 @@ func (c *Certifier) Certificate( case !errors.Is(err, sql.ErrNotFound): return nil, fmt.Errorf("getting certificate from DB for: %w", err) } - return c.Recertify(ctx, id, certifier, pubkey) + cert, err = c.client.Certify(ctx, id, certifier, pubkey) + if err != nil { + return nil, fmt.Errorf("certifying POST at %v: %w", certifier, err) + } + + if err := certifierdb.AddCertificate(c.db, id, *cert, pubkey); err != nil { + c.logger.Warn("failed to persist poet cert", zap.Error(err)) + } + return cert, nil }) if err != nil { @@ -126,21 +134,11 @@ func (c *Certifier) Certificate( return cert.(*certifierdb.PoetCert), nil } -func (c *Certifier) Recertify( - ctx context.Context, - id types.NodeID, - certifier *url.URL, - pubkey []byte, -) (*certifierdb.PoetCert, error) { - cert, err := c.client.Certify(ctx, id, certifier, pubkey) - if err != nil { - return nil, fmt.Errorf("certifying POST at %v: %w", certifier, err) - } - - if err := certifierdb.AddCertificate(c.db, id, *cert, pubkey); err != nil { - c.logger.Warn("failed to persist poet cert", zap.Error(err)) +func (c *Certifier) DeleteCertificate(id types.NodeID, pubkey []byte) error { + if err := certifierdb.DeleteCertificate(c.db, id, pubkey); err != nil { + return err } - return cert, nil + return nil } type CertifierClient struct { diff --git a/activation/interface.go b/activation/interface.go index 5d38204697..437d208c91 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -163,12 +163,7 @@ type certifierService interface { pubkey []byte, ) (*certifier.PoetCert, error) - Recertify( - ctx context.Context, - id types.NodeID, - certifierAddress *url.URL, - pubkey []byte, - ) (*certifier.PoetCert, error) + DeleteCertificate(id types.NodeID, pubkey []byte) error } type poetDbAPI interface { diff --git a/activation/mocks.go b/activation/mocks.go index 8f7bd80c76..ac208e8efe 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -1874,41 +1874,40 @@ func (c *MockcertifierServiceCertificateCall) DoAndReturn(f func(context.Context return c } -// Recertify mocks base method. -func (m *MockcertifierService) Recertify(ctx context.Context, id types.NodeID, certifierAddress *url.URL, pubkey []byte) (*certifier.PoetCert, error) { +// DeleteCertificate mocks base method. +func (m *MockcertifierService) DeleteCertificate(id types.NodeID, pubkey []byte) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Recertify", ctx, id, certifierAddress, pubkey) - ret0, _ := ret[0].(*certifier.PoetCert) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "DeleteCertificate", id, pubkey) + ret0, _ := ret[0].(error) + return ret0 } -// Recertify indicates an expected call of Recertify. -func (mr *MockcertifierServiceMockRecorder) Recertify(ctx, id, certifierAddress, pubkey any) *MockcertifierServiceRecertifyCall { +// DeleteCertificate indicates an expected call of DeleteCertificate. +func (mr *MockcertifierServiceMockRecorder) DeleteCertificate(id, pubkey any) *MockcertifierServiceDeleteCertificateCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recertify", reflect.TypeOf((*MockcertifierService)(nil).Recertify), ctx, id, certifierAddress, pubkey) - return &MockcertifierServiceRecertifyCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCertificate", reflect.TypeOf((*MockcertifierService)(nil).DeleteCertificate), id, pubkey) + return &MockcertifierServiceDeleteCertificateCall{Call: call} } -// MockcertifierServiceRecertifyCall wrap *gomock.Call -type MockcertifierServiceRecertifyCall struct { +// MockcertifierServiceDeleteCertificateCall wrap *gomock.Call +type MockcertifierServiceDeleteCertificateCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockcertifierServiceRecertifyCall) Return(arg0 *certifier.PoetCert, arg1 error) *MockcertifierServiceRecertifyCall { - c.Call = c.Call.Return(arg0, arg1) +func (c *MockcertifierServiceDeleteCertificateCall) Return(arg0 error) *MockcertifierServiceDeleteCertificateCall { + c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockcertifierServiceRecertifyCall) Do(f func(context.Context, types.NodeID, *url.URL, []byte) (*certifier.PoetCert, error)) *MockcertifierServiceRecertifyCall { +func (c *MockcertifierServiceDeleteCertificateCall) Do(f func(types.NodeID, []byte) error) *MockcertifierServiceDeleteCertificateCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockcertifierServiceRecertifyCall) DoAndReturn(f func(context.Context, types.NodeID, *url.URL, []byte) (*certifier.PoetCert, error)) *MockcertifierServiceRecertifyCall { +func (c *MockcertifierServiceDeleteCertificateCall) DoAndReturn(f func(types.NodeID, []byte) error) *MockcertifierServiceDeleteCertificateCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/activation/poet.go b/activation/poet.go index 46b124ceb3..08ea9b4fe1 100644 --- a/activation/poet.go +++ b/activation/poet.go @@ -451,8 +451,7 @@ func (c *poetService) authorize( } // Fallback to PoW // TODO: remove this fallback once we migrate to certificates fully. - - logger.Debug("querying for poet pow parameters") + logger.Info("falling back to PoW authorization") powCtx, cancel := withConditionalTimeout(ctx, c.requestTimeout) defer cancel() powParams, err := c.powParams(powCtx) @@ -480,6 +479,22 @@ func (c *poetService) authorize( }}, nil } +func (c *poetService) reauthorize( + ctx context.Context, + id types.NodeID, + challange []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 { + return nil, fmt.Errorf("deleting cert: %w", err) + } + } + } + return c.authorize(ctx, id, challange, c.logger) +} + func (c *poetService) Submit( ctx context.Context, deadline time.Time, @@ -508,10 +523,10 @@ func (c *poetService) Submit( case err == nil: return round, nil case errors.Is(err, ErrUnauthorized): - logger.Warn("failed to submit challenge as unathorized - recertifying", zap.Error(err)) - auth.PoetCert, err = c.recertify(ctx, nodeID) + logger.Warn("failed to submit challenge as unathorized - authorizing again", zap.Error(err)) + auth, err := c.reauthorize(ctx, nodeID, challenge, logger) if err != nil { - return nil, fmt.Errorf("recertifying: %w", err) + return nil, fmt.Errorf("authorizing: %w", err) } return c.client.Submit(submitCtx, deadline, prefix, challenge, signature, nodeID, *auth) } @@ -560,17 +575,6 @@ func (c *poetService) Certify(ctx context.Context, id types.NodeID) (*certifier. return c.certifier.Certificate(ctx, id, url, pubkey) } -func (c *poetService) recertify(ctx context.Context, id types.NodeID) (*certifier.PoetCert, error) { - if c.certifier == nil { - return nil, errors.New("certifier not configured") - } - url, pubkey, err := c.getCertifierInfo(ctx) - if err != nil { - return nil, err - } - return c.certifier.Recertify(ctx, id, url, 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) diff --git a/activation/poet_client_test.go b/activation/poet_client_test.go index 3c67f828db..0f0b1d82b6 100644 --- a/activation/poet_client_test.go +++ b/activation/poet_client_test.go @@ -1,7 +1,9 @@ package activation import ( + "bytes" "context" + "errors" "io" "net/http" "net/http/httptest" @@ -375,8 +377,9 @@ func TestPoetClient_RecertifiesOnAuthFailure(t *testing.T) { mCertifier.EXPECT(). Certificate(gomock.Any(), sig.NodeID(), certifierAddress, certifierPubKey). Return(&certifier.PoetCert{Data: []byte("first")}, nil), + mCertifier.EXPECT().DeleteCertificate(sig.NodeID(), certifierPubKey), mCertifier.EXPECT(). - Recertify(gomock.Any(), sig.NodeID(), certifierAddress, certifierPubKey). + Certificate(gomock.Any(), sig.NodeID(), certifierAddress, certifierPubKey). Return(&certifier.PoetCert{Data: []byte("second")}, nil), ) @@ -389,6 +392,80 @@ func TestPoetClient_RecertifiesOnAuthFailure(t *testing.T) { require.Equal(t, 2, submitCount) } +func TestPoetClient_FallbacksToPowWhenCannotRecertify(t *testing.T) { + t.Parallel() + + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + certifierAddress := &url.URL{Scheme: "http", Host: "certifier"} + certifierPubKey := []byte("certifier-pubkey") + + mux := http.NewServeMux() + infoResp, err := protojson.Marshal(&rpcapi.InfoResponse{ + ServicePubkey: []byte("pubkey"), + Certifier: &rpcapi.InfoResponse_Cerifier{ + Url: certifierAddress.String(), + Pubkey: certifierPubKey, + }, + }) + require.NoError(t, err) + mux.HandleFunc("GET /v1/info", func(w http.ResponseWriter, r *http.Request) { w.Write(infoResp) }) + + powChallenge := []byte("challenge") + powResp, err := protojson.Marshal(&rpcapi.PowParamsResponse{PowParams: &rpcapi.PowParams{Challenge: powChallenge}}) + require.NoError(t, err) + mux.HandleFunc("GET /v1/pow_params", func(w http.ResponseWriter, r *http.Request) { w.Write(powResp) }) + + submitResp, err := protojson.Marshal(&rpcapi.SubmitResponse{}) + require.NoError(t, err) + submitCount := 0 + mux.HandleFunc("POST /v1/submit", func(w http.ResponseWriter, r *http.Request) { + req := rpcapi.SubmitRequest{} + body, _ := io.ReadAll(r.Body) + protojson.Unmarshal(body, &req) + + switch { + case submitCount == 0: + w.WriteHeader(http.StatusUnauthorized) + case submitCount == 1 && req.Certificate == nil && bytes.Equal(req.PowParams.Challenge, powChallenge): + w.Write(submitResp) + default: + w.WriteHeader(http.StatusUnauthorized) + } + submitCount++ + }) + + ts := httptest.NewServer(mux) + defer ts.Close() + + server := types.PoetServer{ + Address: ts.URL, + Pubkey: types.NewBase64Enc([]byte("pubkey")), + } + cfg := PoetConfig{CertifierInfoCacheTTL: time.Hour} + + ctrl := gomock.NewController(t) + mCertifier := NewMockcertifierService(ctrl) + gomock.InOrder( + mCertifier.EXPECT(). + Certificate(gomock.Any(), sig.NodeID(), certifierAddress, certifierPubKey). + Return(&certifier.PoetCert{Data: []byte("first")}, nil), + mCertifier.EXPECT().DeleteCertificate(sig.NodeID(), certifierPubKey), + mCertifier.EXPECT(). + Certificate(gomock.Any(), sig.NodeID(), certifierAddress, certifierPubKey). + Return(nil, errors.New("cannot recertify")), + ) + + 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()) + require.NoError(t, err) + require.Equal(t, 2, submitCount) +} + func TestPoetService_CachesCertifierInfo(t *testing.T) { t.Parallel() type test struct { diff --git a/sql/localsql/certifier/db.go b/sql/localsql/certifier/db.go index cf74c50c99..202d93e555 100644 --- a/sql/localsql/certifier/db.go +++ b/sql/localsql/certifier/db.go @@ -28,6 +28,19 @@ func AddCertificate(db sql.Executor, nodeID types.NodeID, cert PoetCert, cerifie return nil } +func DeleteCertificate(db sql.Executor, nodeID types.NodeID, certifierID []byte) error { + enc := func(stmt *sql.Statement) { + stmt.BindBytes(1, nodeID.Bytes()) + stmt.BindBytes(2, certifierID) + } + if _, err := db.Exec(` + DELETE FROM poet_certificates WHERE node_id = ?1 AND certifier_id = ?2;`, enc, nil, + ); err != nil { + return fmt.Errorf("deleting poet certificate for (%s; %x): %w", nodeID.ShortString(), certifierID, err) + } + return nil +} + func Certificate(db sql.Executor, nodeID types.NodeID, certifierID []byte) (*PoetCert, error) { enc := func(stmt *sql.Statement) { stmt.BindBytes(1, nodeID.Bytes())