diff --git a/cmd/f3/fake_ec.go b/cmd/f3/fake_ec.go new file mode 100644 index 00000000..40d9c1ee --- /dev/null +++ b/cmd/f3/fake_ec.go @@ -0,0 +1,133 @@ +package main + +import ( + "context" + "encoding/binary" + "time" + + "golang.org/x/crypto/blake2b" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-f3" + "github.com/filecoin-project/go-f3/gpbft" +) + +type FakeEC struct { + seed []byte + initialPowerTable gpbft.PowerEntries + + ecPeriod time.Duration + ecStart time.Time +} + +type Tipset struct { + tsk []byte + epoch int64 + timestamp time.Time +} + +func (ts *Tipset) Key() gpbft.TipSetKey { + return ts.tsk +} + +func (ts *Tipset) Epoch() int64 { + return ts.epoch +} +func (ts *Tipset) Beacon() []byte { + h, err := blake2b.New256([]byte("beacon")) + if err != nil { + panic(err) + } + h.Write(ts.tsk) + return h.Sum(nil) +} + +func (ts *Tipset) Timestamp() time.Time { + return ts.timestamp +} + +func NewFakeEC(seed uint64, m f3.Manifest) *FakeEC { + return &FakeEC{ + seed: binary.BigEndian.AppendUint64(nil, seed), + initialPowerTable: m.InitialPowerTable, + + ecPeriod: m.ECPeriod, + ecStart: m.ECBoostrapTimestamp, + } +} + +func (ec *FakeEC) genTipset(epoch int64) *Tipset { + h, err := blake2b.New256(ec.seed) + if err != nil { + panic(err) + } + h.Write(binary.BigEndian.AppendUint64(nil, uint64(epoch))) + rng := h.Sum(nil) + var size uint8 + size, rng = rng[0]%8, rng[1:] + _ = rng + tsk := make([]byte, 0, size*gpbft.CID_MAX_LEN) + + if size == 0 { + return nil + } + + for i := uint8(0); i < size; i++ { + h.Write([]byte{1}) + digest := h.Sum(nil) + if i == 0 { + //encode epoch in the first block hash + binary.BigEndian.PutUint64(digest[32-8:], uint64(epoch)) + } + tsk = append(tsk, gpbft.DigestToCid(digest)...) + } + return &Tipset{ + tsk: tsk, + epoch: epoch, + timestamp: ec.ecStart.Add(time.Duration(epoch) * ec.ecPeriod), + } +} + +func (ec *FakeEC) currentEpoch() int64 { + return int64(time.Since(ec.ecStart) / ec.ecPeriod) +} + +// GetTipsetByHeight should return a tipset or nil/empty byte array if it does not exists +func (ec *FakeEC) GetTipsetByEpoch(ctx context.Context, epoch int64) (f3.TipSet, error) { + if ec.currentEpoch() < epoch { + return nil, xerrors.Errorf("does not yet exist") + } + ts := ec.genTipset(epoch) + for ts == nil { + epoch-- + ts = ec.genTipset(epoch - 1) + } + return ts, nil +} + +func (ec *FakeEC) GetParent(ctx context.Context, ts f3.TipSet) (f3.TipSet, error) { + + for epoch := ts.Epoch() - 1; epoch > 0; epoch-- { + ts, err := ec.GetTipsetByEpoch(ctx, epoch) + if err != nil { + return nil, xerrors.Errorf("walking back tipsets: %w", err) + } + if ts != nil { + return ts, nil + } + } + return nil, xerrors.Errorf("parent not found") +} + +func (ec *FakeEC) GetHead(ctx context.Context) (f3.TipSet, error) { + return ec.GetTipsetByEpoch(ctx, ec.currentEpoch()) +} + +func (ec *FakeEC) GetPowerTable(ctx context.Context, tsk gpbft.TipSetKey) (gpbft.PowerEntries, error) { + return ec.initialPowerTable, nil +} + +func (ec *FakeEC) GetTipset(ctx context.Context, tsk gpbft.TipSetKey) (f3.TipSet, error) { + epoch := binary.BigEndian.Uint64(tsk[6+32-8 : 6+32]) + return ec.genTipset(int64(epoch)), nil +} diff --git a/cmd/f3/manifest.go b/cmd/f3/manifest.go index bb481710..b7c94e28 100644 --- a/cmd/f3/manifest.go +++ b/cmd/f3/manifest.go @@ -1,9 +1,7 @@ package main import ( - "crypto/rand" "encoding/json" - "fmt" "math/big" "os" @@ -30,12 +28,11 @@ var manifestGenCmd = cli.Command{ Value: 2, }, }, + Action: func(c *cli.Context) error { path := c.String("manifest") - rng := make([]byte, 4) - _, _ = rand.Read(rng) - var m f3.Manifest - m.NetworkName = gpbft.NetworkName(fmt.Sprintf("localnet-%X", rng)) + m := f3.LocalnetManifest() + fsig := signing.NewFakeBackend() for i := 0; i < c.Int("N"); i++ { pubkey, _ := fsig.GenerateKey() @@ -43,9 +40,10 @@ var manifestGenCmd = cli.Command{ m.InitialPowerTable = append(m.InitialPowerTable, gpbft.PowerEntry{ ID: gpbft.ActorID(i), PubKey: pubkey, - Power: big.NewInt(1), + Power: big.NewInt(1000), }) } + f, err := os.OpenFile(path, os.O_WRONLY, 0666) if err != nil { return xerrors.Errorf("opening manifest file for writing: %w", err) diff --git a/cmd/f3/run.go b/cmd/f3/run.go index ebe04035..7a78c54b 100644 --- a/cmd/f3/run.go +++ b/cmd/f3/run.go @@ -76,7 +76,10 @@ var runCmd = cli.Command{ signingBackend := &fakeSigner{*signing.NewFakeBackend()} id := c.Uint64("id") signingBackend.Allow(int(id)) - module, err := f3.New(ctx, gpbft.ActorID(id), m, ds, h, ps, signingBackend, signingBackend, nil, log) + + ec := NewFakeEC(1, m) + module, err := f3.New(ctx, gpbft.ActorID(id), m, ds, h, ps, + signingBackend, signingBackend, ec, log) if err != nil { return xerrors.Errorf("creating module: %w", err) } diff --git a/f3.go b/f3.go index 31a9a52c..1505193c 100644 --- a/f3.go +++ b/f3.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "time" "github.com/filecoin-project/go-f3/certstore" "github.com/filecoin-project/go-f3/gpbft" @@ -36,6 +37,7 @@ type client struct { certstore *certstore.Store id gpbft.ActorID nn gpbft.NetworkName + ec ECBackend gpbft.Verifier gpbft.SignerWithMarshaler @@ -110,6 +112,7 @@ func New(ctx context.Context, id gpbft.ActorID, manifest Manifest, ds datastore. client: &client{ certstore: cs, + ec: ec, nn: manifest.NetworkName, id: id, Verifier: verif, @@ -231,7 +234,23 @@ loop: return multierr.Append(err, ctx.Err()) } -type ECBackend interface{} +type ECBackend interface { + // GetTipsetByEpoch should return a tipset before the one requested if the requested + // tipset does not exist due to null epochs + GetTipsetByEpoch(ctx context.Context, epoch int64) (TipSet, error) + GetTipset(context.Context, gpbft.TipSetKey) (TipSet, error) + GetHead(context.Context) (TipSet, error) + GetParent(context.Context, TipSet) (TipSet, error) + + GetPowerTable(context.Context, gpbft.TipSetKey) (gpbft.PowerEntries, error) +} + +type TipSet interface { + Key() gpbft.TipSetKey + Beacon() []byte + Epoch() int64 + Timestamp() time.Time +} type Logger interface { Debug(args ...interface{}) diff --git a/gpbft/chain.go b/gpbft/chain.go index 326ce279..3c713c30 100644 --- a/gpbft/chain.go +++ b/gpbft/chain.go @@ -2,8 +2,8 @@ package gpbft import ( "bytes" + "encoding/base32" "encoding/binary" - "encoding/hex" "errors" "fmt" "strings" @@ -36,9 +36,17 @@ func MakeCid(data []byte) []byte { // We construct this CID manually to avoid depending on go-cid (it's also a _bit_ faster). digest := blake2b.Sum256(data) - out := make([]byte, 0, 38) + return DigestToCid(digest[:]) +} + +// DigestToCid turns a digest into CBOR + blake2b-256 CID +func DigestToCid(digest []byte) []byte { + if len(digest) != 32 { + panic(fmt.Sprintf("wrong length of digest, expected 32, got %d", len(digest))) + } + out := make([]byte, 0, CID_MAX_LEN) out = append(out, cidPrefix...) - out = append(out, digest[:]...) + out = append(out, digest...) return out } @@ -102,8 +110,9 @@ func (ts *TipSet) String() string { if ts == nil { return "" } + encTs := base32.StdEncoding.EncodeToString(ts.Key) - return fmt.Sprintf("%d@%s", ts.Epoch, hex.EncodeToString(ts.Key)) + return fmt.Sprintf("%s@%d", encTs[:max(16, len(encTs))], ts.Epoch) } // A chain of tipsets comprising a base (the last finalised tipset from which the chain extends). @@ -263,7 +272,7 @@ func (c ECChain) Validate() error { return fmt.Errorf("tipset %d: %w", i, err) } if ts.Epoch <= lastEpoch { - return errors.New("chain must have increasing epochs") + return fmt.Errorf("chain must have increasing epochs %d <= %d", ts.Epoch, lastEpoch) } lastEpoch = ts.Epoch } @@ -300,5 +309,9 @@ func (c ECChain) String() string { } } b.WriteString("]") - return b.String() + str := b.String() + if len(str) > 77 { + str = str[:77] + "..." + } + return str } diff --git a/gpbft/powertable.go b/gpbft/powertable.go index 4323c659..def2d9e5 100644 --- a/gpbft/powertable.go +++ b/gpbft/powertable.go @@ -84,7 +84,6 @@ func (p PowerEntries) Scaled() (scaled []uint16, total uint16, err error) { // NewPowerTable creates a new PowerTable from a slice of PowerEntry . // It is more efficient than Add, as it only needs to sort the entries once. -// Note that the function takes ownership of the slice - it must not be modified afterwards. func NewPowerTable() *PowerTable { return &PowerTable{ Lookup: make(map[ActorID]int), diff --git a/host.go b/host.go index a8af6b8f..4960180b 100644 --- a/host.go +++ b/host.go @@ -1,12 +1,13 @@ package f3 import ( + "bytes" "context" + "slices" "time" "github.com/filecoin-project/go-f3/certs" "github.com/filecoin-project/go-f3/gpbft" - "github.com/filecoin-project/go-f3/sim" "golang.org/x/xerrors" ) @@ -100,6 +101,22 @@ func (h *gpbftRunner) ValidateMessage(msg *gpbft.GMessage) (gpbft.ValidatedMessa return h.participant.ValidateMessage(msg) } +func (h *gpbftHost) collectChain(base TipSet, head TipSet) ([]TipSet, error) { + // TODO: optimize when head is way beyond base + res := make([]TipSet, 0, 2*gpbft.CHAIN_MAX_LEN) + res = append(res, head) + for !bytes.Equal(head.Key(), base.Key()) { + var err error + head, err = h.client.ec.GetParent(h.runningCtx, head) + if err != nil { + return nil, xerrors.Errorf("walking back the chain: %w", err) + } + res = append(res, head) + } + slices.Reverse(res) + return res[1:], nil +} + // Returns inputs to the next GPBFT instance. // These are: // - the supplemental data. @@ -108,40 +125,129 @@ func (h *gpbftRunner) ValidateMessage(msg *gpbft.GMessage) (gpbft.ValidatedMessa // The chain should be a suffix of the last chain notified to the host via // ReceiveDecision (or known to be final via some other channel). func (h *gpbftHost) GetProposalForInstance(instance uint64) (*gpbft.SupplementalData, gpbft.ECChain, error) { - // TODO: this is just a complete fake + var baseTsk gpbft.TipSetKey + if instance == 0 { + ts, err := h.client.ec.GetTipsetByEpoch(h.runningCtx, + h.manifest.BootstrapEpoch-h.manifest.ECFinality) + if err != nil { + return nil, nil, xerrors.Errorf("getting boostrap base: %w", err) + } + baseTsk = ts.Key() + } else { + cert, err := h.client.certstore.Get(h.runningCtx, instance-1) + if err != nil { + return nil, nil, xerrors.Errorf("getting cert for previous instance(%d): %w", instance-1, err) + } + baseTsk = cert.ECChain.Head().Key + } - pt, _, err := h.GetCommitteeForInstance(0) + baseTs, err := h.client.ec.GetTipset(h.runningCtx, baseTsk) if err != nil { - return nil, nil, xerrors.Errorf("getting power table: %w", err) + return nil, nil, xerrors.Errorf("getting base TS: %w", err) + } + headTs, err := h.client.ec.GetHead(h.runningCtx) + if err != nil { + return nil, nil, xerrors.Errorf("getting head TS: %w", err) + } + + collectedChain, err := h.collectChain(baseTs, headTs) + if err != nil { + return nil, nil, xerrors.Errorf("collecting chain: %w", err) + } + + base := gpbft.TipSet{ + Epoch: baseTs.Epoch(), + Key: baseTs.Key(), + } + pte, err := h.client.ec.GetPowerTable(h.runningCtx, baseTs.Key()) + if err != nil { + return nil, nil, xerrors.Errorf("getting power table for base: %w", err) + } + base.PowerTable, err = certs.MakePowerTableCID(pte) + if err != nil { + return nil, nil, xerrors.Errorf("computing powertable CID for base: %w", err) + } + + suffix := make([]gpbft.TipSet, min(gpbft.CHAIN_MAX_LEN-1, len(collectedChain))) // -1 because of base + for i := range suffix { + suffix[i].Key = collectedChain[i].Key() + suffix[i].Epoch = collectedChain[i].Epoch() + + pte, err = h.client.ec.GetPowerTable(h.runningCtx, suffix[i].Key) + if err != nil { + return nil, nil, xerrors.Errorf("getting power table for suffix %d: %w", i, err) + } + suffix[i].PowerTable, err = certs.MakePowerTableCID(pte) + if err != nil { + return nil, nil, xerrors.Errorf("computing powertable CID for base: %w", err) + } } - ptCid, err := certs.MakePowerTableCID(pt.Entries) + chain, err := gpbft.NewChain(base, suffix...) if err != nil { - return nil, nil, xerrors.Errorf("computing power table CID: %w", err) + return nil, nil, xerrors.Errorf("making new chain: %w", err) } - ts := sim.NewTipSetGenerator(1) - chain, err := gpbft.NewChain( - gpbft.TipSet{Epoch: 0, Key: ts.Sample(), PowerTable: ptCid}, - gpbft.TipSet{Epoch: 1, Key: ts.Sample(), PowerTable: ptCid}, - ) + var supplData gpbft.SupplementalData + pt, _, err := h.GetCommitteeForInstance(instance + 1) if err != nil { - return nil, nil, xerrors.Errorf("geenrating chain: %w", err) + return nil, nil, xerrors.Errorf("getting commite for %d: %w", instance+1, err) } - sd := &gpbft.SupplementalData{ - PowerTable: ptCid, + + supplData.PowerTable, err = certs.MakePowerTableCID(pt.Entries) + if err != nil { + return nil, nil, xerrors.Errorf("making power table cid for supplemental data: %w", err) } - // TODO: use lookback to return the correct next power table commitment and commitments hash. - return sd, chain, nil + return &supplData, chain, nil } func (h *gpbftHost) GetCommitteeForInstance(instance uint64) (*gpbft.PowerTable, []byte, error) { + var powerTsk gpbft.TipSetKey + var powerEntries gpbft.PowerEntries + var err error + + if instance < h.manifest.CommiteeLookback { + //boostrap phase + ts, err := h.client.ec.GetTipsetByEpoch(h.runningCtx, h.manifest.BootstrapEpoch-h.manifest.ECFinality) + if err != nil { + return nil, nil, xerrors.Errorf("getting tipset for boostrap epoch with lookback: %w", err) + } + powerTsk = ts.Key() + powerEntries, err = h.client.ec.GetPowerTable(h.runningCtx, powerTsk) + if err != nil { + return nil, nil, xerrors.Errorf("getting power table: %w", err) + } + } else { + cert, err := h.client.certstore.Get(h.runningCtx, instance-h.manifest.CommiteeLookback) + if err != nil { + return nil, nil, xerrors.Errorf("getting finality certificate: %w", err) + } + powerTsk = cert.ECChain.Head().Key + + powerEntries, err = h.client.certstore.GetPowerTable(h.runningCtx, instance) + if err != nil { + // this fires every round, is this correct? + h.log.Infof("failed getting power table from certstore: %v, falling back to EC", err) + + powerEntries, err = h.client.ec.GetPowerTable(h.runningCtx, powerTsk) + if err != nil { + return nil, nil, xerrors.Errorf("getting power table: %w", err) + } + } + } + + ts, err := h.client.ec.GetTipset(h.runningCtx, powerTsk) + if err != nil { + return nil, nil, xerrors.Errorf("getting tipset: %w", err) + } + table := gpbft.NewPowerTable() - err := table.Add(h.manifest.InitialPowerTable...) + err = table.Add(powerEntries...) if err != nil { - return nil, nil, err + return nil, nil, xerrors.Errorf("adding entries to power table: %w", err) } - return table, []byte{'A'}, nil + + return table, ts.Beacon(), nil } // Returns the network's name (for signature separation) @@ -184,13 +290,18 @@ func (h *gpbftHost) SetAlarm(at time.Time) { // based on the decision received (which may be in the past). // E.g. this might be: finalised tipset timestamp + epoch duration + stabilisation delay. func (h *gpbftHost) ReceiveDecision(decision *gpbft.Justification) time.Time { - h.log.Infof("got decision: %+v", decision) + h.log.Infof("got decision, finalized head at epoch: %d", decision.Vote.Value.Head().Epoch) err := h.saveDecision(decision) if err != nil { h.log.Errorf("error while saving decision: %+v", err) } + ts, err := h.client.ec.GetTipset(h.runningCtx, decision.Vote.Value.Head().Key) + if err != nil { + h.log.Errorf("could not get timestamp of just finalized tipset: %+v", err) + return time.Now().Add(h.manifest.ECDelay) + } - return time.Now().Add(2 * time.Second) + return ts.Timestamp().Add(h.manifest.ECDelay) } func (h *gpbftHost) saveDecision(decision *gpbft.Justification) error { diff --git a/manifest.go b/manifest.go index 85c5ba44..32f32ee1 100644 --- a/manifest.go +++ b/manifest.go @@ -1,8 +1,39 @@ package f3 -import "github.com/filecoin-project/go-f3/gpbft" +import ( + "crypto/rand" + "fmt" + "time" + + "github.com/filecoin-project/go-f3/gpbft" +) type Manifest struct { NetworkName gpbft.NetworkName - InitialPowerTable []gpbft.PowerEntry + InitialPowerTable gpbft.PowerEntries + BootstrapEpoch int64 + + ECFinality int64 + ECDelay time.Duration + CommiteeLookback uint64 + + //Temporary + ECPeriod time.Duration + ECBoostrapTimestamp time.Time +} + +func LocalnetManifest() Manifest { + rng := make([]byte, 4) + _, _ = rand.Read(rng) + m := Manifest{ + NetworkName: gpbft.NetworkName(fmt.Sprintf("localnet-%X", rng)), + BootstrapEpoch: 1000, + ECFinality: 900, + CommiteeLookback: 5, + ECDelay: 30 * time.Second, + + ECPeriod: 30 * time.Second, + } + m.ECBoostrapTimestamp = time.Now().Add(-time.Duration(m.BootstrapEpoch) * m.ECPeriod) + return m }