Skip to content

Commit

Permalink
Merge pull request #429 from spacemeshos/support-certificates-for-submit
Browse files Browse the repository at this point in the history
Support certificates for registeration
  • Loading branch information
poszu committed Nov 13, 2023
2 parents fb46c6f + 1284049 commit ba98c66
Show file tree
Hide file tree
Showing 10 changed files with 679 additions and 182 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/spacemeshos/poet

go 1.21.3
go 1.21.4

require (
github.com/c0mm4nd/go-ripemd v0.0.0-20200326052756-bd1759ad7d10
Expand Down
39 changes: 37 additions & 2 deletions registration/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package registration

import "time"
import (
"encoding/base64"
"time"

"go.uber.org/zap/zapcore"
)

func DefaultConfig() Config {
return Config{
Expand All @@ -11,9 +16,39 @@ func DefaultConfig() Config {
}

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

MaxRoundMembers int `long:"max-round-members" description:"the maximum number of members in a round"`
MaxSubmitBatchSize int `long:"max-submit-batch-size" description:"The maximum number of challenges to submit in a single batch"`
SubmitFlushInterval time.Duration `long:"submit-flush-interval" description:"The interval between flushes of the submit queue"`

Certifier *CertifierConfig
}

type Base64Enc []byte

func (k *Base64Enc) UnmarshalFlag(value string) error {
b, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return err
}
*k = b
return nil
}

func (k *Base64Enc) Bytes() []byte {
return []byte(*k)
}

type CertifierConfig struct {
URL string `long:"certifier-url" description:"The URL of the certifier service"`
PubKey Base64Enc `long:"certifier-pubkey" description:"The public key of the certifier service (base64 encoded)"`
}

// implement zap.ObjectMarshaler interface.
func (c CertifierConfig) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("url", c.URL)
enc.AddString("pubkey", base64.StdEncoding.EncodeToString(c.PubKey))
return nil
}
17 changes: 17 additions & 0 deletions registration/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package registration_test

import (
"encoding/base64"
"testing"

"github.com/stretchr/testify/require"

"github.com/spacemeshos/poet/registration"
)

func TestBase64EncDecode(t *testing.T) {
enc := base64.StdEncoding.EncodeToString([]byte("hello"))
b64 := registration.Base64Enc{}
require.NoError(t, b64.UnmarshalFlag(enc))
require.Equal(t, []byte("hello"), b64.Bytes())
}
60 changes: 53 additions & 7 deletions registration/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"sync"
"time"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"go.uber.org/zap"

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

var ErrTooLateToRegister = errors.New("too late to register for the desired round")
var (
ErrInvalidCertificate = errors.New("invalid certificate")
ErrTooLateToRegister = errors.New("too late to register for the desired round")

registerWithCertMetric = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "poet",
Subsystem: "registration",
Name: "with_cert_total",
Help: "Number of registrations with a certificate",
}, []string{"result"})

registerWithPoWMetric = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "poet",
Subsystem: "registration",
Name: "with_pow_total",
Help: "Number of registrations with a PoW",
}, []string{"result"})
)

// Registration orchestrates rounds functionality
// It is responsible for:
Expand Down Expand Up @@ -127,6 +146,13 @@ func New(
r.powVerifiers = powVerifiers{current: options.powVerifier}
}

if r.cfg.Certifier != nil && r.cfg.Certifier.PubKey != nil {
logging.FromContext(ctx).Info("configured certifier", zap.Inline(r.cfg.Certifier))
} else {
logging.FromContext(ctx).Info("disabled certificate checking")
r.cfg.Certifier = nil
}

epoch := r.roundCfg.OpenRoundId(r.genesis, time.Now())
round, err := newRound(epoch, r.dbdir, r.newRoundOpts()...)
if err != nil {
Expand All @@ -138,6 +164,10 @@ func New(
return r, nil
}

func (r *Registration) CertifierInfo() *CertifierConfig {
return r.cfg.Certifier
}

func (r *Registration) Pubkey() ed25519.PublicKey {
return r.privKey.Public().(ed25519.PublicKey)
}
Expand Down Expand Up @@ -287,18 +317,34 @@ func (r *Registration) newRoundOpts() []newRoundOptionFunc {
func (r *Registration) Submit(
ctx context.Context,
challenge, nodeID []byte,
// TODO: remove deprecated PoW
nonce uint64,
powParams PowParams,
certificate []byte,
deadline time.Time,
) (epoch uint, roundEnd time.Time, err error) {
logger := logging.FromContext(ctx)

err = r.powVerifiers.VerifyWithParams(challenge, nodeID, nonce, powParams)
if err != nil {
logger.Debug("challenge verification failed", zap.Error(err))
return 0, time.Time{}, err
// Verify if the node is allowed to register.
// Support both a certificate and a PoW while
// the certificate path is being stabilized.
if r.cfg.Certifier != nil && certificate != nil {
if !ed25519.Verify(r.cfg.Certifier.PubKey.Bytes(), nodeID, certificate) {
registerWithCertMetric.WithLabelValues("invalid").Inc()
return 0, time.Time{}, ErrInvalidCertificate
}
registerWithCertMetric.WithLabelValues("valid").Inc()
} else {
// FIXME: PoW is deprecated
// Remove once certificate path is stabilized and mandatory.
err := r.powVerifiers.VerifyWithParams(challenge, nodeID, nonce, powParams)
if err != nil {
registerWithPoWMetric.WithLabelValues("invalid").Inc()
logger.Debug("PoW verification failed", zap.Error(err))
return 0, time.Time{}, err
}
registerWithPoWMetric.WithLabelValues("valid").Inc()
logger.Debug("verified PoW", zap.String("node_id", hex.EncodeToString(nodeID)))
}
logger.Debug("verified challenge", zap.String("node_id", hex.EncodeToString(nodeID)))

r.openRoundMutex.RLock()
epoch = r.openRound.epoch
Expand Down
110 changes: 108 additions & 2 deletions registration/registration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package registration_test
import (
"bytes"
"context"
"crypto/ed25519"
"testing"
"time"

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

// Submit challenge
epoch, _, err := r.Submit(context.Background(), challenge, nodeID, nonce, registration.PowParams{}, time.Time{})
epoch, _, err := r.Submit(
context.Background(),
challenge,
nodeID,
nonce,
registration.PowParams{},
nil,
time.Time{},
)
req.NoError(err)
req.Equal(uint(0), epoch)

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

Expand Down Expand Up @@ -270,3 +279,100 @@ func TestRecoveringRoundInProgress(t *testing.T) {
)
req.NoError(r.Run(ctx))
}

func Test_GetCertifierInfo(t *testing.T) {
certifier := &registration.CertifierConfig{
PubKey: registration.Base64Enc("pubkey"),
URL: "http://the-certifier.org",
}

r, err := registration.New(
context.Background(),
time.Now(),
t.TempDir(),
nil,
server.DefaultRoundConfig(),
registration.WithConfig(registration.Config{
MaxRoundMembers: 10,
Certifier: certifier,
}),
)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, r.Close()) })
require.Equal(t, r.CertifierInfo(), certifier)
}

func Test_CheckCertificate(t *testing.T) {
challenge := []byte("challenge")
nodeID := []byte("nodeID00nodeID00nodeID00nodeID00")

t.Run("certification check disabled (default config)", func(t *testing.T) {
powVerifier := mocks.NewMockPowVerifier(gomock.NewController(t))
powVerifier.EXPECT().Params().Return(registration.PowParams{}).AnyTimes()
r, err := registration.New(
context.Background(),
time.Now(),
t.TempDir(),
nil,
server.DefaultRoundConfig(),
registration.WithPowVerifier(powVerifier),
)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, r.Close()) })

// missing certificate - fallback to PoW
powVerifier.EXPECT().Verify(challenge, nodeID, uint64(5)).Return(nil)
_, _, err = r.Submit(context.Background(), challenge, nodeID, 5, registration.PowParams{}, nil, time.Time{})
require.NoError(t, err)

// passed certificate - still fallback to PoW
powVerifier.EXPECT().Verify(challenge, nodeID, uint64(7)).Return(nil)
_, _, err = r.Submit(
context.Background(),
challenge,
nodeID,
7,
registration.PowParams{},
[]byte{1, 2, 3, 4},
time.Time{},
)
require.NoError(t, err)
})
t.Run("certification check enabled", func(t *testing.T) {
pub, private, err := ed25519.GenerateKey(nil)
require.NoError(t, err)
powVerifier := mocks.NewMockPowVerifier(gomock.NewController(t))

r, err := registration.New(
context.Background(),
time.Now(),
t.TempDir(),
nil,
server.DefaultRoundConfig(),
registration.WithPowVerifier(powVerifier),
registration.WithConfig(registration.Config{
MaxRoundMembers: 10,
Certifier: &registration.CertifierConfig{
PubKey: registration.Base64Enc(pub),
},
}),
)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, r.Close()) })

// missing certificate - fallback to PoW
powVerifier.EXPECT().Params().Return(registration.PowParams{}).AnyTimes()
powVerifier.EXPECT().Verify(challenge, nodeID, uint64(7)).Return(nil)
_, _, err = r.Submit(context.Background(), challenge, nodeID, 7, r.PowParams(), nil, time.Time{})
require.NoError(t, err)

// valid certificate
signature := ed25519.Sign(private, nodeID)
_, _, err = r.Submit(context.Background(), challenge, nodeID, 0, r.PowParams(), signature, time.Time{})
require.NoError(t, err)

// invalid certificate
_, _, err = r.Submit(context.Background(), challenge, nodeID, 0, r.PowParams(), []byte{1, 2, 3, 4}, time.Time{})
require.ErrorIs(t, err, registration.ErrInvalidCertificate)
})
}
Loading

0 comments on commit ba98c66

Please sign in to comment.