Skip to content

Commit ba98c66

Browse files
authoredNov 13, 2023
Merge pull request #429 from spacemeshos/support-certificates-for-submit
Support certificates for registeration
2 parents fb46c6f + 1284049 commit ba98c66

File tree

10 files changed

+679
-182
lines changed

10 files changed

+679
-182
lines changed
 

‎go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/spacemeshos/poet
22

3-
go 1.21.3
3+
go 1.21.4
44

55
require (
66
github.com/c0mm4nd/go-ripemd v0.0.0-20200326052756-bd1759ad7d10

‎registration/config.go

+37-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package registration
22

3-
import "time"
3+
import (
4+
"encoding/base64"
5+
"time"
6+
7+
"go.uber.org/zap/zapcore"
8+
)
49

510
func DefaultConfig() Config {
611
return Config{
@@ -11,9 +16,39 @@ func DefaultConfig() Config {
1116
}
1217

1318
type Config struct {
14-
PowDifficulty uint `long:"pow-difficulty" description:"PoW difficulty (in the number of leading zero bits)"`
19+
// FIXME: remove deprecated PoW
20+
PowDifficulty uint `long:"pow-difficulty" description:"(DEPRECATED) PoW difficulty (in the number of leading zero bits)"`
1521

1622
MaxRoundMembers int `long:"max-round-members" description:"the maximum number of members in a round"`
1723
MaxSubmitBatchSize int `long:"max-submit-batch-size" description:"The maximum number of challenges to submit in a single batch"`
1824
SubmitFlushInterval time.Duration `long:"submit-flush-interval" description:"The interval between flushes of the submit queue"`
25+
26+
Certifier *CertifierConfig
27+
}
28+
29+
type Base64Enc []byte
30+
31+
func (k *Base64Enc) UnmarshalFlag(value string) error {
32+
b, err := base64.StdEncoding.DecodeString(value)
33+
if err != nil {
34+
return err
35+
}
36+
*k = b
37+
return nil
38+
}
39+
40+
func (k *Base64Enc) Bytes() []byte {
41+
return []byte(*k)
42+
}
43+
44+
type CertifierConfig struct {
45+
URL string `long:"certifier-url" description:"The URL of the certifier service"`
46+
PubKey Base64Enc `long:"certifier-pubkey" description:"The public key of the certifier service (base64 encoded)"`
47+
}
48+
49+
// implement zap.ObjectMarshaler interface.
50+
func (c CertifierConfig) MarshalLogObject(enc zapcore.ObjectEncoder) error {
51+
enc.AddString("url", c.URL)
52+
enc.AddString("pubkey", base64.StdEncoding.EncodeToString(c.PubKey))
53+
return nil
1954
}

‎registration/config_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package registration_test
2+
3+
import (
4+
"encoding/base64"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/spacemeshos/poet/registration"
10+
)
11+
12+
func TestBase64EncDecode(t *testing.T) {
13+
enc := base64.StdEncoding.EncodeToString([]byte("hello"))
14+
b64 := registration.Base64Enc{}
15+
require.NoError(t, b64.UnmarshalFlag(enc))
16+
require.Equal(t, []byte("hello"), b64.Bytes())
17+
}

‎registration/registration.go

+53-7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"sync"
1414
"time"
1515

16+
"github.com/prometheus/client_golang/prometheus"
17+
"github.com/prometheus/client_golang/prometheus/promauto"
1618
"go.uber.org/zap"
1719

1820
"github.com/spacemeshos/poet/logging"
@@ -32,7 +34,24 @@ type roundConfig interface {
3234
RoundEnd(genesis time.Time, epoch uint) time.Time
3335
}
3436

35-
var ErrTooLateToRegister = errors.New("too late to register for the desired round")
37+
var (
38+
ErrInvalidCertificate = errors.New("invalid certificate")
39+
ErrTooLateToRegister = errors.New("too late to register for the desired round")
40+
41+
registerWithCertMetric = promauto.NewCounterVec(prometheus.CounterOpts{
42+
Namespace: "poet",
43+
Subsystem: "registration",
44+
Name: "with_cert_total",
45+
Help: "Number of registrations with a certificate",
46+
}, []string{"result"})
47+
48+
registerWithPoWMetric = promauto.NewCounterVec(prometheus.CounterOpts{
49+
Namespace: "poet",
50+
Subsystem: "registration",
51+
Name: "with_pow_total",
52+
Help: "Number of registrations with a PoW",
53+
}, []string{"result"})
54+
)
3655

3756
// Registration orchestrates rounds functionality
3857
// It is responsible for:
@@ -127,6 +146,13 @@ func New(
127146
r.powVerifiers = powVerifiers{current: options.powVerifier}
128147
}
129148

149+
if r.cfg.Certifier != nil && r.cfg.Certifier.PubKey != nil {
150+
logging.FromContext(ctx).Info("configured certifier", zap.Inline(r.cfg.Certifier))
151+
} else {
152+
logging.FromContext(ctx).Info("disabled certificate checking")
153+
r.cfg.Certifier = nil
154+
}
155+
130156
epoch := r.roundCfg.OpenRoundId(r.genesis, time.Now())
131157
round, err := newRound(epoch, r.dbdir, r.newRoundOpts()...)
132158
if err != nil {
@@ -138,6 +164,10 @@ func New(
138164
return r, nil
139165
}
140166

167+
func (r *Registration) CertifierInfo() *CertifierConfig {
168+
return r.cfg.Certifier
169+
}
170+
141171
func (r *Registration) Pubkey() ed25519.PublicKey {
142172
return r.privKey.Public().(ed25519.PublicKey)
143173
}
@@ -287,18 +317,34 @@ func (r *Registration) newRoundOpts() []newRoundOptionFunc {
287317
func (r *Registration) Submit(
288318
ctx context.Context,
289319
challenge, nodeID []byte,
320+
// TODO: remove deprecated PoW
290321
nonce uint64,
291322
powParams PowParams,
323+
certificate []byte,
292324
deadline time.Time,
293325
) (epoch uint, roundEnd time.Time, err error) {
294326
logger := logging.FromContext(ctx)
295-
296-
err = r.powVerifiers.VerifyWithParams(challenge, nodeID, nonce, powParams)
297-
if err != nil {
298-
logger.Debug("challenge verification failed", zap.Error(err))
299-
return 0, time.Time{}, err
327+
// Verify if the node is allowed to register.
328+
// Support both a certificate and a PoW while
329+
// the certificate path is being stabilized.
330+
if r.cfg.Certifier != nil && certificate != nil {
331+
if !ed25519.Verify(r.cfg.Certifier.PubKey.Bytes(), nodeID, certificate) {
332+
registerWithCertMetric.WithLabelValues("invalid").Inc()
333+
return 0, time.Time{}, ErrInvalidCertificate
334+
}
335+
registerWithCertMetric.WithLabelValues("valid").Inc()
336+
} else {
337+
// FIXME: PoW is deprecated
338+
// Remove once certificate path is stabilized and mandatory.
339+
err := r.powVerifiers.VerifyWithParams(challenge, nodeID, nonce, powParams)
340+
if err != nil {
341+
registerWithPoWMetric.WithLabelValues("invalid").Inc()
342+
logger.Debug("PoW verification failed", zap.Error(err))
343+
return 0, time.Time{}, err
344+
}
345+
registerWithPoWMetric.WithLabelValues("valid").Inc()
346+
logger.Debug("verified PoW", zap.String("node_id", hex.EncodeToString(nodeID)))
300347
}
301-
logger.Debug("verified challenge", zap.String("node_id", hex.EncodeToString(nodeID)))
302348

303349
r.openRoundMutex.RLock()
304350
epoch = r.openRound.epoch

‎registration/registration_test.go

+108-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package registration_test
33
import (
44
"bytes"
55
"context"
6+
"crypto/ed25519"
67
"testing"
78
"time"
89

@@ -57,12 +58,20 @@ func TestSubmitIdempotence(t *testing.T) {
5758
eg.Go(func() error { return r.Run(ctx) })
5859

5960
// Submit challenge
60-
epoch, _, err := r.Submit(context.Background(), challenge, nodeID, nonce, registration.PowParams{}, time.Time{})
61+
epoch, _, err := r.Submit(
62+
context.Background(),
63+
challenge,
64+
nodeID,
65+
nonce,
66+
registration.PowParams{},
67+
nil,
68+
time.Time{},
69+
)
6170
req.NoError(err)
6271
req.Equal(uint(0), epoch)
6372

6473
// Try again - it should return the same result
65-
epoch, _, err = r.Submit(context.Background(), challenge, nodeID, nonce, registration.PowParams{}, time.Time{})
74+
epoch, _, err = r.Submit(context.Background(), challenge, nodeID, nonce, registration.PowParams{}, nil, time.Time{})
6675
req.NoError(err)
6776
req.Equal(uint(0), epoch)
6877

@@ -270,3 +279,100 @@ func TestRecoveringRoundInProgress(t *testing.T) {
270279
)
271280
req.NoError(r.Run(ctx))
272281
}
282+
283+
func Test_GetCertifierInfo(t *testing.T) {
284+
certifier := &registration.CertifierConfig{
285+
PubKey: registration.Base64Enc("pubkey"),
286+
URL: "http://the-certifier.org",
287+
}
288+
289+
r, err := registration.New(
290+
context.Background(),
291+
time.Now(),
292+
t.TempDir(),
293+
nil,
294+
server.DefaultRoundConfig(),
295+
registration.WithConfig(registration.Config{
296+
MaxRoundMembers: 10,
297+
Certifier: certifier,
298+
}),
299+
)
300+
require.NoError(t, err)
301+
t.Cleanup(func() { require.NoError(t, r.Close()) })
302+
require.Equal(t, r.CertifierInfo(), certifier)
303+
}
304+
305+
func Test_CheckCertificate(t *testing.T) {
306+
challenge := []byte("challenge")
307+
nodeID := []byte("nodeID00nodeID00nodeID00nodeID00")
308+
309+
t.Run("certification check disabled (default config)", func(t *testing.T) {
310+
powVerifier := mocks.NewMockPowVerifier(gomock.NewController(t))
311+
powVerifier.EXPECT().Params().Return(registration.PowParams{}).AnyTimes()
312+
r, err := registration.New(
313+
context.Background(),
314+
time.Now(),
315+
t.TempDir(),
316+
nil,
317+
server.DefaultRoundConfig(),
318+
registration.WithPowVerifier(powVerifier),
319+
)
320+
require.NoError(t, err)
321+
t.Cleanup(func() { require.NoError(t, r.Close()) })
322+
323+
// missing certificate - fallback to PoW
324+
powVerifier.EXPECT().Verify(challenge, nodeID, uint64(5)).Return(nil)
325+
_, _, err = r.Submit(context.Background(), challenge, nodeID, 5, registration.PowParams{}, nil, time.Time{})
326+
require.NoError(t, err)
327+
328+
// passed certificate - still fallback to PoW
329+
powVerifier.EXPECT().Verify(challenge, nodeID, uint64(7)).Return(nil)
330+
_, _, err = r.Submit(
331+
context.Background(),
332+
challenge,
333+
nodeID,
334+
7,
335+
registration.PowParams{},
336+
[]byte{1, 2, 3, 4},
337+
time.Time{},
338+
)
339+
require.NoError(t, err)
340+
})
341+
t.Run("certification check enabled", func(t *testing.T) {
342+
pub, private, err := ed25519.GenerateKey(nil)
343+
require.NoError(t, err)
344+
powVerifier := mocks.NewMockPowVerifier(gomock.NewController(t))
345+
346+
r, err := registration.New(
347+
context.Background(),
348+
time.Now(),
349+
t.TempDir(),
350+
nil,
351+
server.DefaultRoundConfig(),
352+
registration.WithPowVerifier(powVerifier),
353+
registration.WithConfig(registration.Config{
354+
MaxRoundMembers: 10,
355+
Certifier: &registration.CertifierConfig{
356+
PubKey: registration.Base64Enc(pub),
357+
},
358+
}),
359+
)
360+
require.NoError(t, err)
361+
t.Cleanup(func() { require.NoError(t, r.Close()) })
362+
363+
// missing certificate - fallback to PoW
364+
powVerifier.EXPECT().Params().Return(registration.PowParams{}).AnyTimes()
365+
powVerifier.EXPECT().Verify(challenge, nodeID, uint64(7)).Return(nil)
366+
_, _, err = r.Submit(context.Background(), challenge, nodeID, 7, r.PowParams(), nil, time.Time{})
367+
require.NoError(t, err)
368+
369+
// valid certificate
370+
signature := ed25519.Sign(private, nodeID)
371+
_, _, err = r.Submit(context.Background(), challenge, nodeID, 0, r.PowParams(), signature, time.Time{})
372+
require.NoError(t, err)
373+
374+
// invalid certificate
375+
_, _, err = r.Submit(context.Background(), challenge, nodeID, 0, r.PowParams(), []byte{1, 2, 3, 4}, time.Time{})
376+
require.ErrorIs(t, err, registration.ErrInvalidCertificate)
377+
})
378+
}

0 commit comments

Comments
 (0)
Please sign in to comment.