aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRasmus Dahlberg <rasmus@mullvad.net>2022-01-25 01:04:09 +0100
committerRasmus Dahlberg <rasmus@mullvad.net>2022-01-26 00:33:44 +0100
commit72c2f39216d8d8e9bce589271b38c76d16ee2a36 (patch)
tree7bd93182471e9aa2fd06d695cb5dfda4efd42c1f
parent1594b0830d8cd18ab158dfffb64dd3c219da8f10 (diff)
state: Refactor with new tree head endpoints
For details see doc/proposals/2022-01-no-quick-tree-head-endpoint.
-rw-r--r--issues/implement-decided-proposals.md3
-rw-r--r--issues/refactor-pkg-state-single.md20
-rw-r--r--pkg/state/single.go203
-rw-r--r--pkg/state/single_test.go413
-rw-r--r--pkg/state/state_manager.go21
5 files changed, 230 insertions, 430 deletions
diff --git a/issues/implement-decided-proposals.md b/issues/implement-decided-proposals.md
index e3155c9..eecc58b 100644
--- a/issues/implement-decided-proposals.md
+++ b/issues/implement-decided-proposals.md
@@ -25,5 +25,4 @@ Current status on implementing the above:
* [ ] get-\* endpoints
* [ ] domain hint
* [ ] add-leaf
-* [ ] tree-head endpoints
- * rgdd is assigned, started on something similar in `rgdd/state`
+* [x] tree-head endpoints
diff --git a/issues/refactor-pkg-state-single.md b/issues/refactor-pkg-state-single.md
deleted file mode 100644
index 78a4e6c..0000000
--- a/issues/refactor-pkg-state-single.md
+++ /dev/null
@@ -1,20 +0,0 @@
-**Title:** Refactor pkg/state/single.go </br>
-**Date:** 2021-12-20 </br>
-
-# Summary
-Remove unwanted dependencies and resolve minor TODOs in `pkg/state/single.go`.
-
-# Description
-Some refactoring is needed in `pkg/state/single.go`. In more detail, the
-following dependencies are overkill and should ideally be removed:
-- "github.com/google/certificate-transparency-go/schedule"
-- "reflect"
-
-There are also two TODO prints in the code:
-```
-$ git g TODO
-single.go
-115: return fmt.Errorf("signature-signer pair is a duplicate") // TODO: maybe not an error
-154: sm.cosignatures = make(map[types.Hash]*types.Signature, 0) // TODO: on repeat we might want to not zero this
-```
-
diff --git a/pkg/state/single.go b/pkg/state/single.go
index d02b86f..5c69232 100644
--- a/pkg/state/single.go
+++ b/pkg/state/single.go
@@ -5,152 +5,161 @@ import (
"crypto"
"crypto/ed25519"
"fmt"
- "reflect"
"sync"
"time"
"git.sigsum.org/sigsum-lib-go/pkg/types"
"git.sigsum.org/sigsum-log-go/pkg/db"
"github.com/golang/glog"
- "github.com/google/certificate-transparency-go/schedule"
)
-// StateManagerSingle implements a single-instance StateManager. In other
-// words, there's no other state that needs to be synced on any remote machine.
+// StateManagerSingle implements a single-instance StateManager
type StateManagerSingle struct {
- client db.Client
- signer crypto.Signer
- interval time.Duration
- deadline time.Duration
+ client db.Client
+ signer crypto.Signer
+ namespace types.Hash
+ interval time.Duration
+ deadline time.Duration
+
+ // Lock-protected access to pointers. A write lock is only obtained once
+ // per interval when doing pointer rotation. All endpoints are readers.
sync.RWMutex
+ signedTreeHead *types.SignedTreeHead
+ cosignedTreeHead *types.CosignedTreeHead
- // cosigned is the current cosigned tree head that is being served
- cosigned types.CosignedTreeHead
-
- // toSign is the current tree head that is being cosigned by witnesses
- toSign types.SignedTreeHead
-
- // cosignatures keep track of all cosignatures for the toSign tree head
+ // Syncronized and deduplicated witness cosignatures for signedTreeHead
+ events chan *event
cosignatures map[types.Hash]*types.Signature
}
func NewStateManagerSingle(client db.Client, signer crypto.Signer, interval, deadline time.Duration) (*StateManagerSingle, error) {
sm := &StateManagerSingle{
- client: client,
- signer: signer,
- interval: interval,
- deadline: deadline,
+ client: client,
+ signer: signer,
+ namespace: *types.HashFn(signer.Public().(ed25519.PublicKey)),
+ interval: interval,
+ deadline: deadline,
}
- ctx, cancel := context.WithTimeout(context.Background(), sm.deadline)
- defer cancel()
-
- sth, err := sm.Latest(ctx)
- if err != nil {
- return nil, fmt.Errorf("Latest: %v", err)
- }
- sm.toSign = *sth
- sm.cosignatures = make(map[types.Hash]*types.Signature)
- sm.cosigned = types.CosignedTreeHead{
- SignedTreeHead: *sth,
- Cosignature: make([]types.Signature, 0),
- KeyHash: make([]types.Hash, 0),
- }
- return sm, nil
+ sth, err := sm.latestSTH(context.Background())
+ sm.setCosignedTreeHead()
+ sm.setToCosignTreeHead(sth)
+ return sm, err
}
func (sm *StateManagerSingle) Run(ctx context.Context) {
- schedule.Every(ctx, sm.interval, func(ctx context.Context) {
- ictx, cancel := context.WithTimeout(ctx, sm.deadline)
- defer cancel()
-
- nextSTH, err := sm.Latest(ictx)
+ rotation := func() {
+ nextSTH, err := sm.latestSTH(ctx)
if err != nil {
- glog.Warningf("rotate failed: Latest: %v", err)
+ glog.Warningf("cannot rotate without tree head: %v", err)
return
}
-
- sm.Lock()
- defer sm.Unlock()
sm.rotate(nextSTH)
- })
-}
-
-func (sm *StateManagerSingle) Latest(ctx context.Context) (*types.SignedTreeHead, error) {
- th, err := sm.client.GetTreeHead(ctx)
- if err != nil {
- return nil, fmt.Errorf("LatestTreeHead: %v", err)
}
+ sm.events = make(chan *event, 4096)
+ defer close(sm.events)
+ ticker := time.NewTicker(sm.interval)
+ defer ticker.Stop()
+
+ rotation()
+ for {
+ select {
+ case <-ticker.C:
+ rotation()
+ case ev := <-sm.events:
+ sm.handleEvent(ev)
+ case <-ctx.Done():
+ return
+ }
+ }
+}
- namespace := types.HashFn(sm.signer.Public().(ed25519.PublicKey))
- return th.Sign(sm.signer, namespace)
+func (sm *StateManagerSingle) ToCosignTreeHead(_ context.Context) (*types.SignedTreeHead, error) {
+ sm.RLock()
+ defer sm.RUnlock()
+ return sm.signedTreeHead, nil
}
-func (sm *StateManagerSingle) ToSign(_ context.Context) (*types.SignedTreeHead, error) {
+func (sm *StateManagerSingle) CosignedTreeHead(_ context.Context) (*types.CosignedTreeHead, error) {
sm.RLock()
defer sm.RUnlock()
- return &sm.toSign, nil
+ if sm.cosignedTreeHead == nil {
+ return nil, fmt.Errorf("no cosignatures available")
+ }
+ return sm.cosignedTreeHead, nil
}
-func (sm *StateManagerSingle) Cosigned(_ context.Context) (*types.CosignedTreeHead, error) {
+func (sm *StateManagerSingle) AddCosignature(ctx context.Context, pub *types.PublicKey, sig *types.Signature) error {
sm.RLock()
defer sm.RUnlock()
- if len(sm.cosigned.Cosignature) == 0 {
- return nil, fmt.Errorf("no witness cosignatures available")
+
+ msg := sm.signedTreeHead.TreeHead.ToBinary(&sm.namespace)
+ if !ed25519.Verify(ed25519.PublicKey(pub[:]), msg, sig[:]) {
+ return fmt.Errorf("invalid cosignature")
+ }
+ select {
+ case sm.events <- &event{types.HashFn(pub[:]), sig}:
+ return nil
+ case <-ctx.Done():
+ return fmt.Errorf("request timeout")
}
- return &sm.cosigned, nil
}
-func (sm *StateManagerSingle) AddCosignature(_ context.Context, vk *types.PublicKey, sig *types.Signature) error {
+func (sm *StateManagerSingle) rotate(nextSTH *types.SignedTreeHead) {
sm.Lock()
defer sm.Unlock()
- namespace := types.HashFn(sm.signer.Public().(ed25519.PublicKey))
- msg := sm.toSign.TreeHead.ToBinary(namespace)
- if !ed25519.Verify(ed25519.PublicKey(vk[:]), msg, sig[:]) {
- return fmt.Errorf("invalid tree head signature")
- }
+ glog.V(3).Infof("rotating tree heads")
+ sm.handleEvents()
+ sm.setCosignedTreeHead()
+ sm.setToCosignTreeHead(nextSTH)
+}
- witness := types.HashFn(vk[:])
- if _, ok := sm.cosignatures[*witness]; ok {
- return fmt.Errorf("signature-signer pair is a duplicate") // TODO: maybe not an error
+func (sm *StateManagerSingle) handleEvents() {
+ glog.V(3).Infof("handling any outstanding events")
+ for i, n := 0, len(sm.events); i < n; i++ {
+ sm.handleEvent(<-sm.events)
}
- sm.cosignatures[*witness] = sig
+}
- glog.V(3).Infof("accepted new cosignature from witness: %x", *witness)
- return nil
+func (sm *StateManagerSingle) handleEvent(ev *event) {
+ glog.V(3).Infof("handling event from witness %x", ev.keyHash[:])
+ sm.cosignatures[*ev.keyHash] = ev.cosignature
}
-// rotate rotates the log's cosigned and stable STH. The caller must aquire the
-// source's read-write lock if there are concurrent reads and/or writes.
-func (sm *StateManagerSingle) rotate(next *types.SignedTreeHead) {
- if reflect.DeepEqual(sm.cosigned.SignedTreeHead, sm.toSign) {
- // cosigned and toSign are the same. So, we need to merge all
- // cosignatures that we already had with the new collected ones.
- for i := 0; i < len(sm.cosigned.Cosignature); i++ {
- kh := sm.cosigned.KeyHash[i]
- sig := sm.cosigned.Cosignature[i]
-
- if _, ok := sm.cosignatures[kh]; !ok {
- sm.cosignatures[kh] = &sig
- }
- }
- glog.V(3).Infof("cosigned tree head repeated, merged signatures")
+func (sm *StateManagerSingle) setCosignedTreeHead() {
+ n := len(sm.cosignatures)
+ if n == 0 {
+ sm.cosignedTreeHead = nil
+ return
}
- var cosignatures []types.Signature
- var keyHashes []types.Hash
+ var cth types.CosignedTreeHead
+ cth.SignedTreeHead = *sm.signedTreeHead
+ cth.Cosignature = make([]types.Signature, 0, n)
+ cth.KeyHash = make([]types.Hash, 0, n)
for keyHash, cosignature := range sm.cosignatures {
- cosignatures = append(cosignatures, *cosignature)
- keyHashes = append(keyHashes, keyHash)
+ cth.KeyHash = append(cth.KeyHash, keyHash)
+ cth.Cosignature = append(cth.Cosignature, *cosignature)
}
+ sm.cosignedTreeHead = &cth
+}
- // Update cosigned tree head
- sm.cosigned.SignedTreeHead = sm.toSign
- sm.cosigned.Cosignature = cosignatures
- sm.cosigned.KeyHash = keyHashes
+func (sm *StateManagerSingle) setToCosignTreeHead(nextSTH *types.SignedTreeHead) {
+ sm.cosignatures = make(map[types.Hash]*types.Signature)
+ sm.signedTreeHead = nextSTH
+}
+
+func (sm *StateManagerSingle) latestSTH(ctx context.Context) (*types.SignedTreeHead, error) {
+ ictx, cancel := context.WithTimeout(ctx, sm.deadline)
+ defer cancel()
- // Update to-sign tree head
- sm.toSign = *next
- sm.cosignatures = make(map[types.Hash]*types.Signature, 0) // TODO: on repeat we might want to not zero this
- glog.V(3).Infof("rotated tree heads")
+ th, err := sm.client.GetTreeHead(ictx)
+ if err != nil {
+ return nil, fmt.Errorf("failed fetching tree head: %v", err)
+ }
+ sth, err := th.Sign(sm.signer, &sm.namespace)
+ if err != nil {
+ return nil, fmt.Errorf("failed signing tree head: %v", err)
+ }
+ return sth, nil
}
diff --git a/pkg/state/single_test.go b/pkg/state/single_test.go
index b315b3e..f619e2f 100644
--- a/pkg/state/single_test.go
+++ b/pkg/state/single_test.go
@@ -1,7 +1,6 @@
package state
import (
- "bytes"
"context"
"crypto"
"crypto/ed25519"
@@ -12,404 +11,204 @@ import (
"time"
"git.sigsum.org/sigsum-lib-go/pkg/types"
- mocksTrillian "git.sigsum.org/sigsum-log-go/pkg/db/mocks"
- mocksSigner "git.sigsum.org/sigsum-log-go/pkg/state/mocks"
+ db "git.sigsum.org/sigsum-log-go/pkg/db/mocks"
+ "git.sigsum.org/sigsum-log-go/pkg/state/mocks"
"github.com/golang/mock/gomock"
)
-var (
- testTH = types.TreeHead{
- Timestamp: 0,
- TreeSize: 0,
- RootHash: types.Hash{},
- }
- testSTH = types.SignedTreeHead{
- TreeHead: testTH,
- Signature: types.Signature{},
- }
- testCTH = types.CosignedTreeHead{
- SignedTreeHead: testSTH,
- Cosignature: []types.Signature{
- types.Signature{},
- },
- KeyHash: []types.Hash{
- types.Hash{},
- },
- }
-
- testSignerOK = &mocksSigner.TestSigner{types.PublicKey{}, types.Signature{}, nil}
- testSignerErr = &mocksSigner.TestSigner{types.PublicKey{}, types.Signature{}, fmt.Errorf("something went wrong")}
-)
-
func TestNewStateManagerSingle(t *testing.T) {
+ signerOk := &mocks.TestSigner{types.PublicKey{}, types.Signature{}, nil}
+ signerErr := &mocks.TestSigner{types.PublicKey{}, types.Signature{}, fmt.Errorf("err")}
for _, table := range []struct {
description string
signer crypto.Signer
- rsp *types.TreeHead
+ rsp types.TreeHead
err error
wantErr bool
- wantSth *types.SignedTreeHead
+ wantSth types.SignedTreeHead
}{
{
description: "invalid: backend failure",
- signer: testSignerOK,
+ signer: signerOk,
err: fmt.Errorf("something went wrong"),
wantErr: true,
},
{
- description: "valid",
- signer: testSignerOK,
- rsp: &testTH,
- wantSth: &testSTH,
- },
- } {
- // Run deferred functions at the end of each iteration
- func() {
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
- client := mocksTrillian.NewMockClient(ctrl)
- client.EXPECT().GetTreeHead(gomock.Any()).Return(table.rsp, table.err)
-
- sm, err := NewStateManagerSingle(client, table.signer, time.Duration(0), time.Duration(0))
- if got, want := err != nil, table.wantErr; got != want {
- t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err)
- }
- if err != nil {
- return
- }
- if got, want := &sm.cosigned.SignedTreeHead, table.wantSth; !reflect.DeepEqual(got, want) {
- t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description)
- }
- if got, want := &sm.toSign, table.wantSth; !reflect.DeepEqual(got, want) {
- t.Errorf("got toSign tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description)
- }
- // we only have log signature on startup
- if got, want := len(sm.cosignatures), 0; got != want {
- t.Errorf("got %d cosignatures but wanted %d in test %q", got, want, table.description)
- }
- }()
- }
-}
-
-func TestLatest(t *testing.T) {
- for _, table := range []struct {
- description string
- signer crypto.Signer
- rsp *types.TreeHead
- err error
- wantErr bool
- wantSth *types.SignedTreeHead
- }{
- {
- description: "invalid: backend failure",
- signer: testSignerOK,
- err: fmt.Errorf("something went wrong"),
- wantErr: true,
- },
- {
- description: "invalid: signature failure",
- rsp: &testTH,
- signer: testSignerErr,
+ description: "invalid: signer failure",
+ signer: signerErr,
+ rsp: types.TreeHead{},
wantErr: true,
},
{
description: "valid",
- signer: testSignerOK,
- rsp: &testTH,
- wantSth: &testSTH,
+ signer: signerOk,
+ rsp: types.TreeHead{},
+ wantSth: types.SignedTreeHead{},
},
} {
- // Run deferred functions at the end of each iteration
func() {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
- client := mocksTrillian.NewMockClient(ctrl)
- client.EXPECT().GetTreeHead(gomock.Any()).Return(table.rsp, table.err)
- sm := StateManagerSingle{
- client: client,
- signer: table.signer,
- }
+ client := db.NewMockClient(ctrl)
+ client.EXPECT().GetTreeHead(gomock.Any()).Return(&table.rsp, table.err)
- sth, err := sm.Latest(context.Background())
+ sm, err := NewStateManagerSingle(client, table.signer, time.Duration(0), time.Duration(0))
if got, want := err != nil, table.wantErr; got != want {
t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err)
}
if err != nil {
return
}
- if got, want := sth, table.wantSth; !reflect.DeepEqual(got, want) {
- t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description)
+ if got, want := sm.signedTreeHead, &table.wantSth; !reflect.DeepEqual(got, want) {
+ t.Errorf("got to-cosign tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description)
+ }
+ if got := sm.cosignedTreeHead; got != nil {
+ t.Errorf("got cosigned tree head but should have none in test %q", table.description)
}
}()
}
}
-func TestToSign(t *testing.T) {
- description := "valid"
+func TestToCosignTreeHead(t *testing.T) {
+ want := &types.SignedTreeHead{}
sm := StateManagerSingle{
- toSign: testSTH,
+ signedTreeHead: want,
}
- sth, err := sm.ToSign(context.Background())
+ sth, err := sm.ToCosignTreeHead(context.Background())
if err != nil {
- t.Errorf("ToSign should not fail with error: %v", err)
+ t.Errorf("should not fail with error: %v", err)
return
}
- if got, want := sth, &testSTH; !reflect.DeepEqual(got, want) {
- t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, description)
+ if got := sth; !reflect.DeepEqual(got, want) {
+ t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v", got, want)
}
}
-func TestCosigned(t *testing.T) {
- description := "valid"
+func TestCosignedTreeHead(t *testing.T) {
+ want := &types.CosignedTreeHead{
+ Cosignature: make([]types.Signature, 1),
+ KeyHash: make([]types.Hash, 1),
+ }
sm := StateManagerSingle{
- cosigned: testCTH,
+ cosignedTreeHead: want,
}
- cth, err := sm.Cosigned(context.Background())
+ cth, err := sm.CosignedTreeHead(context.Background())
if err != nil {
- t.Errorf("Cosigned should not fail with error: %v", err)
+ t.Errorf("should not fail with error: %v", err)
return
}
- if got, want := cth, &testCTH; !reflect.DeepEqual(got, want) {
- t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, description)
+ if got := cth; !reflect.DeepEqual(got, want) {
+ t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v", got, want)
}
- sm.cosigned.Cosignature = make([]types.Signature, 0)
- sm.cosigned.KeyHash = make([]types.Hash, 0)
- cth, err = sm.Cosigned(context.Background())
+ sm.cosignedTreeHead = nil
+ cth, err = sm.CosignedTreeHead(context.Background())
if err == nil {
- t.Errorf("Cosigned should fail without witness cosignatures")
+ t.Errorf("should fail without a cosigned tree head")
return
}
}
func TestAddCosignature(t *testing.T) {
- vk, sk, err := ed25519.GenerateKey(rand.Reader)
- if err != nil {
- t.Fatalf("GenerateKey: %v", err)
- }
- if bytes.Equal(vk[:], new(types.PublicKey)[:]) {
- t.Fatalf("Sampled same key as testPub, aborting...")
- }
- var vkArray types.PublicKey
- copy(vkArray[:], vk[:])
-
+ secret, public := mustKeyPair(t)
for _, table := range []struct {
- description string
- signer crypto.Signer
- vk types.PublicKey
- th types.TreeHead
- wantErr bool
+ desc string
+ signer crypto.Signer
+ vk types.PublicKey
+ wantErr bool
}{
{
- description: "invalid: signature error",
- signer: sk,
- vk: types.PublicKey{}, // wrong key for message
- th: testTH,
- wantErr: true,
+ desc: "invalid: wrong public key",
+ signer: secret,
+ vk: types.PublicKey{},
+ wantErr: true,
},
{
- description: "valid",
- signer: sk,
- vk: vkArray,
- th: testTH,
+ desc: "valid",
+ signer: secret,
+ vk: public,
},
} {
- kh := types.HashFn(testSignerOK.Public().(ed25519.PublicKey))
- sth := mustSign(t, testSignerOK, &table.th, kh)
- cth := &types.CosignedTreeHead{
- SignedTreeHead: *sth,
- Cosignature: make([]types.Signature, 0),
- KeyHash: make([]types.Hash, 0),
- }
sm := &StateManagerSingle{
- signer: testSignerOK,
- cosigned: *cth,
- toSign: *sth,
- cosignatures: make(map[types.Hash]*types.Signature, 0),
+ namespace: *types.HashFn(nil),
+ signedTreeHead: &types.SignedTreeHead{},
+ events: make(chan *event, 1),
}
+ defer close(sm.events)
- // Prepare witness signature
- var vk types.PublicKey
- copy(vk[:], table.vk[:]) //table.signer.Public().(ed25519.PublicKey))
- sth = mustSign(t, table.signer, &table.th, kh)
- kh = types.HashFn(vk[:])
-
- // Add witness signature
- err = sm.AddCosignature(context.Background(), &vk, &sth.Signature)
+ sth := mustSign(t, table.signer, &sm.signedTreeHead.TreeHead, &sm.namespace)
+ ctx := context.Background()
+ err := sm.AddCosignature(ctx, &table.vk, &sth.Signature)
if got, want := err != nil, table.wantErr; got != want {
- t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err)
+ t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err)
}
if err != nil {
continue
}
- // We should have one witness signature
- if got, want := len(sm.cosignatures), 1; got != want {
- t.Errorf("got %d cosignatures but wanted %v in test %q", got, want, table.description)
- continue
- }
- // check that witness signature is there
- sig, ok := sm.cosignatures[*kh]
- if !ok {
- t.Errorf("witness signature is missing")
- continue
+ ctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
+ defer cancel()
+ if err := sm.AddCosignature(ctx, &table.vk, &sth.Signature); err == nil {
+ t.Errorf("expected full channel in test %q", table.desc)
}
- if got, want := sig[:], sth.Signature[:]; !bytes.Equal(got, want) {
- t.Errorf("got witness sigident\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description)
- continue
- }
-
- // Adding a duplicate signature should give an error
- if err := sm.AddCosignature(context.Background(), &vk, &sth.Signature); err == nil {
- t.Errorf("duplicate witness signature accepted as valid")
+ if got, want := len(sm.events), 1; got != want {
+ t.Errorf("wanted %d cosignatures but got %d in test %q", want, got, table.desc)
}
}
}
func TestRotate(t *testing.T) {
- logSig := types.Signature{}
- wit1Sig := types.Signature{}
- wit2Sig := types.Signature{}
-
- //logKH := &types.Hash{}
- wit1KH := types.HashFn([]byte("wit1 key"))
- wit2KH := types.HashFn([]byte("wit2 key"))
+ sth := &types.SignedTreeHead{}
+ nextSTH := &types.SignedTreeHead{TreeHead: types.TreeHead{Timestamp: 1}}
+ ev := &event{
+ keyHash: &types.Hash{},
+ cosignature: &types.Signature{},
+ }
+ wantCTH := &types.CosignedTreeHead{
+ SignedTreeHead: *sth,
+ KeyHash: []types.Hash{*ev.keyHash},
+ Cosignature: []types.Signature{*ev.cosignature},
+ }
+ sm := &StateManagerSingle{
+ signedTreeHead: sth,
+ cosignatures: make(map[types.Hash]*types.Signature),
+ events: make(chan *event, 1),
+ }
+ defer close(sm.events)
- th0 := &testTH
- th1 := &types.TreeHead{
- Timestamp: 1,
- TreeSize: 1,
- RootHash: *types.HashFn([]byte("1")),
+ sm.events <- ev
+ sm.rotate(nextSTH)
+ if got, want := sm.signedTreeHead, nextSTH; !reflect.DeepEqual(got, want) {
+ t.Errorf("got to-cosign tree head\n\t%v\nbut wanted\n\t%v", got, want)
}
- th2 := &types.TreeHead{
- Timestamp: 2,
- TreeSize: 2,
- RootHash: *types.HashFn([]byte("2")),
+ if got, want := sm.cosignedTreeHead, wantCTH; !reflect.DeepEqual(got, want) {
+ t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v", got, want)
}
- for _, table := range []struct {
- description string
- before, after *StateManagerSingle
- next *types.SignedTreeHead
- }{
- {
- description: "toSign tree head repated, but got one new witnes signature",
- before: &StateManagerSingle{
- cosigned: types.CosignedTreeHead{
- SignedTreeHead: types.SignedTreeHead{
- TreeHead: *th0,
- Signature: logSig,
- },
- Cosignature: []types.Signature{wit1Sig},
- KeyHash: []types.Hash{*wit1KH},
- },
- toSign: types.SignedTreeHead{
- TreeHead: *th0,
- Signature: logSig,
- },
- cosignatures: map[types.Hash]*types.Signature{
- *wit2KH: &wit2Sig, // the new witness signature
- },
- },
- next: &types.SignedTreeHead{
- TreeHead: *th1,
- Signature: logSig,
- },
- after: &StateManagerSingle{
- cosigned: types.CosignedTreeHead{
- SignedTreeHead: types.SignedTreeHead{
- TreeHead: *th0,
- Signature: logSig,
- },
- Cosignature: []types.Signature{wit1Sig, wit2Sig},
- KeyHash: []types.Hash{*wit1KH, *wit2KH},
- },
- toSign: types.SignedTreeHead{
- TreeHead: *th1,
- Signature: logSig,
- },
- cosignatures: map[types.Hash]*types.Signature{},
- },
- },
- {
- description: "toSign tree head did not repeat, it got one witness signature",
- before: &StateManagerSingle{
- cosigned: types.CosignedTreeHead{
- SignedTreeHead: types.SignedTreeHead{
- TreeHead: *th0,
- Signature: logSig,
- },
- Cosignature: []types.Signature{wit1Sig},
- KeyHash: []types.Hash{*wit1KH},
- },
- toSign: types.SignedTreeHead{
- TreeHead: *th1,
- Signature: logSig,
- },
- cosignatures: map[types.Hash]*types.Signature{
- *wit2KH: &wit2Sig,
- },
- },
- next: &types.SignedTreeHead{
- TreeHead: *th2,
- Signature: logSig,
- },
- after: &StateManagerSingle{
- cosigned: types.CosignedTreeHead{
- SignedTreeHead: types.SignedTreeHead{
- TreeHead: *th1,
- Signature: logSig,
- },
- Cosignature: []types.Signature{wit2Sig},
- KeyHash: []types.Hash{*wit2KH},
- },
- toSign: types.SignedTreeHead{
- TreeHead: *th2,
- Signature: logSig,
- },
- cosignatures: map[types.Hash]*types.Signature{},
- },
- },
- } {
- table.before.rotate(table.next)
- if got, want := table.before.cosigned.SignedTreeHead, table.after.cosigned.SignedTreeHead; !reflect.DeepEqual(got, want) {
- t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description)
- }
- checkWitnessList(t, table.description, table.before.cosigned, table.after.cosigned)
- if got, want := table.before.toSign, table.after.toSign; !reflect.DeepEqual(got, want) {
- t.Errorf("got toSign tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description)
- }
- if got, want := table.before.cosignatures, table.after.cosignatures; !reflect.DeepEqual(got, want) {
- t.Errorf("got cosignatures map\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description)
- }
+ sth = nextSTH
+ nextSTH = &types.SignedTreeHead{TreeHead: types.TreeHead{Timestamp: 2}}
+ sm.rotate(nextSTH)
+ if got, want := sm.signedTreeHead, nextSTH; !reflect.DeepEqual(got, want) {
+ t.Errorf("got to-cosign tree head\n\t%v\nbut wanted\n\t%v", got, want)
+ }
+ if got := sm.cosignedTreeHead; got != nil {
+ t.Errorf("expected no cosignatures to be available")
}
}
-func checkWitnessList(t *testing.T, description string, got, want types.CosignedTreeHead) {
+func mustKeyPair(t *testing.T) (crypto.Signer, types.PublicKey) {
t.Helper()
- if got, want := len(got.Cosignature), len(want.Cosignature); got != want {
- t.Errorf("got %d cosignatures but wanted %d in test %q", got, want, description)
- return
- }
- if got, want := len(got.KeyHash), len(want.KeyHash); got != want {
- t.Errorf("got %d key hashes but wanted %d in test %q", got, want, description)
- return
- }
- for i := 0; i < len(got.Cosignature); i++ {
- found := false
- for j := 0; j < len(want.Cosignature); j++ {
- if bytes.Equal(got.Cosignature[i][:], want.Cosignature[j][:]) && bytes.Equal(got.KeyHash[i][:], want.KeyHash[j][:]) {
- found = true
- break
- }
- }
- if !found {
- t.Errorf("got unexpected signature-signer pair with key hash in test %q: %x", description, got.KeyHash[i][:])
- }
+ vk, sk, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatal(err)
}
+ var pub types.PublicKey
+ copy(pub[:], vk[:])
+ return sk, pub
}
func mustSign(t *testing.T, s crypto.Signer, th *types.TreeHead, kh *types.Hash) *types.SignedTreeHead {
+ t.Helper()
sth, err := th.Sign(s, kh)
if err != nil {
t.Fatal(err)
diff --git a/pkg/state/state_manager.go b/pkg/state/state_manager.go
index 5aa7609..ab0293c 100644
--- a/pkg/state/state_manager.go
+++ b/pkg/state/state_manager.go
@@ -6,11 +6,24 @@ import (
"git.sigsum.org/sigsum-lib-go/pkg/types"
)
-// StateManager coordinates access to a log's tree heads and (co)signatures
+// StateManager coordinates access to a log's tree heads and (co)signatures.
type StateManager interface {
- Latest(context.Context) (*types.SignedTreeHead, error)
- ToSign(context.Context) (*types.SignedTreeHead, error)
- Cosigned(context.Context) (*types.CosignedTreeHead, error)
+ // ToCosignTreeHead returns the log's to-cosign tree head
+ ToCosignTreeHead(context.Context) (*types.SignedTreeHead, error)
+
+ // CosignedTreeHead returns the log's cosigned tree head
+ CosignedTreeHead(context.Context) (*types.CosignedTreeHead, error)
+
+ // AddCosignature verifies that a cosignature is valid for the to-cosign
+ // tree head before adding it
AddCosignature(context.Context, *types.PublicKey, *types.Signature) error
+
+ // Run peridically rotates the log's to-cosign and cosigned tree heads
Run(context.Context)
}
+
+// event is a verified cosignature request
+type event struct {
+ keyHash *types.Hash
+ cosignature *types.Signature
+}