diff options
| -rw-r--r-- | issues/implement-decided-proposals.md | 3 | ||||
| -rw-r--r-- | issues/refactor-pkg-state-single.md | 20 | ||||
| -rw-r--r-- | pkg/state/single.go | 203 | ||||
| -rw-r--r-- | pkg/state/single_test.go | 413 | ||||
| -rw-r--r-- | pkg/state/state_manager.go | 21 | 
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 +} | 
