251 lines
6.7 KiB
Go
251 lines
6.7 KiB
Go
package drand
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"time"
|
|
|
|
dchain "github.com/drand/drand/chain"
|
|
dclient "github.com/drand/drand/client"
|
|
hclient "github.com/drand/drand/client/http"
|
|
dcrypto "github.com/drand/drand/crypto"
|
|
dlog "github.com/drand/drand/log"
|
|
gclient "github.com/drand/drand/lp2p/client"
|
|
"github.com/drand/kyber"
|
|
lru "github.com/hashicorp/golang-lru/v2"
|
|
logging "github.com/ipfs/go-log/v2"
|
|
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
|
"go.uber.org/zap"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/filecoin-project/go-state-types/abi"
|
|
"github.com/filecoin-project/go-state-types/network"
|
|
|
|
"github.com/filecoin-project/lotus/build"
|
|
"github.com/filecoin-project/lotus/chain/beacon"
|
|
"github.com/filecoin-project/lotus/chain/types"
|
|
"github.com/filecoin-project/lotus/node/modules/dtypes"
|
|
)
|
|
|
|
var log = logging.Logger("drand")
|
|
|
|
// DrandBeacon connects Lotus with a drand network in order to provide
|
|
// randomness to the system in a way that's aligned with Filecoin rounds/epochs.
|
|
//
|
|
// We connect to drand peers via their public HTTP endpoints. The peers are
|
|
// enumerated in the drandServers variable.
|
|
//
|
|
// The root trust for the Drand chain is configured from build.DrandChain.
|
|
type DrandBeacon struct {
|
|
isChained bool
|
|
client dclient.Client
|
|
|
|
pubkey kyber.Point
|
|
|
|
// seconds
|
|
interval time.Duration
|
|
|
|
drandGenTime uint64
|
|
filGenTime uint64
|
|
filRoundTime uint64
|
|
scheme *dcrypto.Scheme
|
|
|
|
localCache *lru.Cache[uint64, *types.BeaconEntry]
|
|
}
|
|
|
|
// IsChained tells us whether this particular beacon operates in "chained mode". Prior to Drand
|
|
// quicknet, beacons form a chain. After the introduction of quicknet, they do not, so we need to
|
|
// change how we interact with beacon entries. (See FIP-0063)
|
|
func (db *DrandBeacon) IsChained() bool {
|
|
return db.isChained
|
|
}
|
|
|
|
// DrandHTTPClient interface overrides the user agent used by drand
|
|
type DrandHTTPClient interface {
|
|
SetUserAgent(string)
|
|
}
|
|
|
|
type logger struct {
|
|
*zap.SugaredLogger
|
|
}
|
|
|
|
func (l *logger) With(args ...interface{}) dlog.Logger {
|
|
return &logger{l.SugaredLogger.With(args...)}
|
|
}
|
|
|
|
func (l *logger) Named(s string) dlog.Logger {
|
|
return &logger{l.SugaredLogger.Named(s)}
|
|
}
|
|
|
|
func (l *logger) AddCallerSkip(skip int) dlog.Logger {
|
|
return &logger{l.SugaredLogger.With(zap.AddCallerSkip(skip))}
|
|
}
|
|
|
|
func NewDrandBeacon(genesisTs, interval uint64, ps *pubsub.PubSub, config dtypes.DrandConfig) (*DrandBeacon, error) {
|
|
if genesisTs == 0 {
|
|
panic("what are you doing this cant be zero")
|
|
}
|
|
|
|
drandChain, err := dchain.InfoFromJSON(bytes.NewReader([]byte(config.ChainInfoJSON)))
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("unable to unmarshal drand chain info: %w", err)
|
|
}
|
|
|
|
var clients []dclient.Client
|
|
for _, url := range config.Servers {
|
|
hc, err := hclient.NewWithInfo(url, drandChain, nil)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("could not create http drand client: %w", err)
|
|
}
|
|
hc.(DrandHTTPClient).SetUserAgent("drand-client-lotus/" + build.NodeBuildVersion)
|
|
clients = append(clients, hc)
|
|
|
|
}
|
|
|
|
opts := []dclient.Option{
|
|
dclient.WithChainInfo(drandChain),
|
|
dclient.WithCacheSize(1024),
|
|
dclient.WithLogger(&logger{&log.SugaredLogger}),
|
|
}
|
|
|
|
if ps != nil {
|
|
opts = append(opts, gclient.WithPubsub(ps))
|
|
} else {
|
|
log.Info("drand beacon without pubsub")
|
|
}
|
|
|
|
client, err := dclient.Wrap(clients, opts...)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("creating drand client: %w", err)
|
|
}
|
|
|
|
lc, err := lru.New[uint64, *types.BeaconEntry](1024)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
db := &DrandBeacon{
|
|
isChained: config.IsChained,
|
|
client: client,
|
|
localCache: lc,
|
|
}
|
|
|
|
sch, err := dcrypto.GetSchemeByIDWithDefault(drandChain.Scheme)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
db.scheme = sch
|
|
db.pubkey = drandChain.PublicKey
|
|
db.interval = drandChain.Period
|
|
db.drandGenTime = uint64(drandChain.GenesisTime)
|
|
db.filRoundTime = interval
|
|
db.filGenTime = genesisTs
|
|
|
|
return db, nil
|
|
}
|
|
|
|
func (db *DrandBeacon) Entry(ctx context.Context, round uint64) <-chan beacon.Response {
|
|
out := make(chan beacon.Response, 1)
|
|
if round != 0 {
|
|
be := db.getCachedValue(round)
|
|
if be != nil {
|
|
out <- beacon.Response{Entry: *be}
|
|
close(out)
|
|
return out
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
start := build.Clock.Now()
|
|
log.Debugw("start fetching randomness", "round", round)
|
|
resp, err := db.client.Get(ctx, round)
|
|
|
|
var br beacon.Response
|
|
if err != nil {
|
|
br.Err = xerrors.Errorf("drand failed Get request: %w", err)
|
|
} else {
|
|
br.Entry.Round = resp.Round()
|
|
br.Entry.Data = resp.Signature()
|
|
}
|
|
log.Debugw("done fetching randomness", "round", round, "took", build.Clock.Since(start))
|
|
out <- br
|
|
close(out)
|
|
}()
|
|
|
|
return out
|
|
}
|
|
func (db *DrandBeacon) cacheValue(e types.BeaconEntry) {
|
|
db.localCache.Add(e.Round, &e)
|
|
}
|
|
|
|
func (db *DrandBeacon) getCachedValue(round uint64) *types.BeaconEntry {
|
|
v, _ := db.localCache.Get(round)
|
|
return v
|
|
}
|
|
|
|
func (db *DrandBeacon) VerifyEntry(entry types.BeaconEntry, prevEntrySig []byte) error {
|
|
if be := db.getCachedValue(entry.Round); be != nil {
|
|
if !bytes.Equal(entry.Data, be.Data) {
|
|
return xerrors.New("invalid beacon value, does not match cached good value")
|
|
}
|
|
// return no error if the value is in the cache already
|
|
return nil
|
|
}
|
|
b := &dchain.Beacon{
|
|
PreviousSig: prevEntrySig,
|
|
Round: entry.Round,
|
|
Signature: entry.Data,
|
|
}
|
|
|
|
err := db.scheme.VerifyBeacon(b, db.pubkey)
|
|
if err != nil {
|
|
return xerrors.Errorf("failed to verify beacon: %w", err)
|
|
}
|
|
|
|
db.cacheValue(entry)
|
|
return nil
|
|
}
|
|
|
|
func (db *DrandBeacon) MaxBeaconRoundForEpoch(nv network.Version, filEpoch abi.ChainEpoch) uint64 {
|
|
// TODO: sometimes the genesis time for filecoin is zero and this goes negative
|
|
latestTs := ((uint64(filEpoch) * db.filRoundTime) + db.filGenTime) - db.filRoundTime
|
|
|
|
if nv <= network.Version15 {
|
|
return db.maxBeaconRoundV1(latestTs)
|
|
}
|
|
|
|
return db.maxBeaconRoundV2(latestTs)
|
|
}
|
|
|
|
func (db *DrandBeacon) maxBeaconRoundV1(latestTs uint64) uint64 {
|
|
dround := (latestTs - db.drandGenTime) / uint64(db.interval.Seconds())
|
|
return dround
|
|
}
|
|
|
|
func (db *DrandBeacon) maxBeaconRoundV2(latestTs uint64) uint64 {
|
|
if latestTs < db.drandGenTime {
|
|
return 1
|
|
}
|
|
|
|
fromGenesis := latestTs - db.drandGenTime
|
|
// we take the time from genesis divided by the periods in seconds, that
|
|
// gives us the number of periods since genesis. We also add +1 because
|
|
// round 1 starts at genesis time.
|
|
return fromGenesis/uint64(db.interval.Seconds()) + 1
|
|
}
|
|
|
|
var _ beacon.RandomBeacon = (*DrandBeacon)(nil)
|
|
|
|
func BeaconScheduleFromDrandSchedule(dcs dtypes.DrandSchedule, genesisTime uint64, ps *pubsub.PubSub) (beacon.Schedule, error) {
|
|
shd := beacon.Schedule{}
|
|
for _, dc := range dcs {
|
|
bc, err := NewDrandBeacon(genesisTime, build.BlockDelaySecs, ps, dc.Config)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("creating drand beacon: %w", err)
|
|
}
|
|
shd = append(shd, beacon.BeaconPoint{Start: dc.Start, Beacon: bc})
|
|
}
|
|
|
|
return shd, nil
|
|
}
|