package signer
import (
"flag"
"fmt"
"sync"
"time"
"github.com/google/trillian/docs/storage/commit_log/simelection"
"github.com/google/trillian/docs/storage/commit_log/simkafka"
"k8s.io/klog/v2"
)
var (
batchSize = flag.Int("batch_size", 5, "Maximum leaves to sign in one run")
pessimizeInterval = flag.Duration("signer_pessimize", 10*time.Millisecond, "Pause interval in signing to induce inter-signer problems")
)
type Signer struct {
mu sync.RWMutex
Name string
election *simelection.Election
epoch int64
dbSTHInfo STHInfo
db FakeDatabase
}
func New(name string, election *simelection.Election, epoch int64) *Signer {
return &Signer{
Name: name,
election: election,
epoch: epoch,
dbSTHInfo: STHInfo{
treeRevision: -1,
sthOffset: -1,
},
}
}
func (s *Signer) String() string {
s.mu.RLock()
defer s.mu.RUnlock()
prefix := " "
if s.IsMaster() {
prefix = "**"
}
return fmt.Sprintf("%s Signer %s up to STH{offset=%d, rev=%d} = %s\n", prefix, s.Name, s.dbSTHInfo.sthOffset, s.dbSTHInfo.treeRevision, s.dbSTHInfo.sth.String())
}
func (s *Signer) LatestSTHInfo() STHInfo {
return s.dbSTHInfo
}
func (s *Signer) StoreSTHInfo(info STHInfo) {
s.dbSTHInfo = info
}
func (s *Signer) IsMaster() bool {
if s.election == nil {
return true
}
return s.election.IsMaster(s.Name)
}
func (s *Signer) Run() {
s.mu.Lock()
defer s.mu.Unlock()
dbSTHInfo := s.LatestSTHInfo()
klog.V(2).Infof("%s: our DB has data upto STH at %d", s.Name, dbSTHInfo.sthOffset)
if dbSTHInfo.sth.TreeSize > 0 {
ourSTH, err := sthFromString(simkafka.Read("STHs/<treeID>", dbSTHInfo.sthOffset))
if err != nil {
klog.Errorf("%s: got an error unpacking STH: %v", s.Name, err)
}
if ourSTH == nil {
klog.Errorf("%s: local DB has data ahead of STHs topic!!", s.Name)
return
}
if ourSTH.Offset != dbSTHInfo.sthOffset {
klog.Errorf("%s: local DB recorded offset %d but that has inconsistent STH %s!!", s.Name, dbSTHInfo.sthOffset, ourSTH)
return
}
if ourSTH.TimeStamp != dbSTHInfo.sth.TimeStamp || ourSTH.TreeSize != dbSTHInfo.sth.TreeSize {
klog.Errorf("%s: local DB has different data than STHs topic!!", s.Name)
return
}
klog.V(2).Infof("%s: our DB at %v, matches STH at that offset", s.Name, dbSTHInfo.sthOffset)
}
nextOffset := dbSTHInfo.sthOffset
var nextSTH *STH
for {
nextOffset++
sth, err := sthFromString(simkafka.Read("STHs/<treeID>", nextOffset))
if err != nil {
klog.Errorf("%s: got an error unpacking STH: %v", s.Name, err)
}
nextSTH = sth
if nextSTH == nil {
break
}
if nextSTH.Offset < nextOffset {
klog.V(2).Infof("%s: ignoring inconsistent STH %s at offset %d", s.Name, nextSTH.String(), nextOffset)
continue
}
if nextSTH.Offset > nextOffset {
klog.Errorf("%s: STH %s is stored at offset %d, earlier than its writer expected!!", s.Name, nextSTH.String(), nextOffset)
return
}
if nextSTH.TimeStamp < dbSTHInfo.sth.TimeStamp || nextSTH.TreeSize < dbSTHInfo.sth.TreeSize {
klog.Errorf("%s: next STH %s has earlier timestamp than in local DB (%s)!!", s.Name, nextSTH.String(), dbSTHInfo.sth.String())
return
}
break
}
time.Sleep(*pessimizeInterval)
if nextSTH == nil {
if !s.IsMaster() {
klog.V(2).Infof("%s: up-to-date with STHs but not master, so exit", s.Name)
return
}
offset := dbSTHInfo.sth.TreeSize
batch := simkafka.ReadMultiple("Leaves/<treeID>", offset, *batchSize)
klog.V(2).Infof("%s: nothing at next offset %d and we are master, so have read %d more leaves", s.Name, nextOffset, len(batch))
if len(batch) == 0 {
klog.V(2).Infof("%s: nothing to do", s.Name)
return
}
timestamp := (time.Now().UnixNano() / int64(time.Millisecond)) - s.epoch
newSTHInfo := STHInfo{
sth: STH{
TreeSize: s.db.Size() + len(batch),
TimeStamp: timestamp,
Offset: nextOffset,
},
treeRevision: dbSTHInfo.treeRevision + 1,
}
newSTHInfo.sthOffset = simkafka.Append("STHs/<treeID>", newSTHInfo.sth.String())
if newSTHInfo.sthOffset > nextOffset {
klog.Warningf("%s: stored new STH %s at offset %d, which is unexpected; give up", s.Name, newSTHInfo.sth.String(), newSTHInfo.sthOffset)
return
}
if newSTHInfo.sthOffset < nextOffset {
klog.Errorf("%s: stored new STH %s at offset %d, which is earlier than expected!!", s.Name, newSTHInfo.sth.String(), newSTHInfo.sthOffset)
return
}
klog.V(2).Infof("%s: stored new STH %s at expected offset, including %d new leaves", s.Name, newSTHInfo.sth.String(), len(batch))
s.db.AddLeaves(timestamp, nextOffset, batch)
s.StoreSTHInfo(newSTHInfo)
} else {
count := nextSTH.TreeSize - dbSTHInfo.sth.TreeSize
klog.V(2).Infof("%s: our DB is %d leaves behind the next STH at %s, so update it", s.Name, count, nextSTH.String())
batch := simkafka.ReadMultiple("Leaves/<treeID>", dbSTHInfo.sth.TreeSize, count)
if len(batch) != count {
klog.Errorf("%s: expected to read leaves [%d, %d) but only got %d!!", s.Name, dbSTHInfo.sth.TreeSize, dbSTHInfo.sth.TreeSize+count, len(batch))
return
}
newSTHInfo := STHInfo{
sth: s.db.AddLeaves(nextSTH.TimeStamp, nextOffset, batch),
treeRevision: dbSTHInfo.treeRevision + 1,
sthOffset: nextOffset,
}
klog.V(2).Infof("%s: update our DB to %s", s.Name, newSTHInfo.sth.String())
s.StoreSTHInfo(newSTHInfo)
}
}