lotus/curiosrc/winning/winning_task.go

726 lines
23 KiB
Go
Raw Normal View History

package winning
2023-11-09 22:28:36 +00:00
import (
2023-11-10 19:01:17 +00:00
"bytes"
2023-11-09 22:28:36 +00:00
"context"
"crypto/rand"
"encoding/binary"
2023-11-10 19:01:17 +00:00
"encoding/json"
2023-11-10 19:36:41 +00:00
"time"
"github.com/ipfs/go-cid"
logging "github.com/ipfs/go-log/v2"
"golang.org/x/sync/errgroup"
2023-11-10 19:36:41 +00:00
"golang.org/x/xerrors"
ffi "github.com/filecoin-project/filecoin-ffi"
2023-11-10 17:00:21 +00:00
"github.com/filecoin-project/go-address"
2023-11-09 22:28:36 +00:00
"github.com/filecoin-project/go-state-types/abi"
2023-11-10 19:01:17 +00:00
"github.com/filecoin-project/go-state-types/crypto"
"github.com/filecoin-project/go-state-types/network"
"github.com/filecoin-project/go-state-types/proof"
2023-11-10 19:01:17 +00:00
prooftypes "github.com/filecoin-project/go-state-types/proof"
2023-11-10 19:36:41 +00:00
2023-11-10 19:01:17 +00:00
"github.com/filecoin-project/lotus/api"
2023-11-09 22:28:36 +00:00
"github.com/filecoin-project/lotus/build"
2023-11-10 19:01:17 +00:00
"github.com/filecoin-project/lotus/chain/gen"
lrand "github.com/filecoin-project/lotus/chain/rand"
2023-11-09 22:28:36 +00:00
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/lib/ffiselect"
2023-11-10 17:00:21 +00:00
"github.com/filecoin-project/lotus/lib/harmony/harmonydb"
2023-11-09 22:28:36 +00:00
"github.com/filecoin-project/lotus/lib/harmony/harmonytask"
"github.com/filecoin-project/lotus/lib/harmony/resources"
2023-11-10 17:00:21 +00:00
"github.com/filecoin-project/lotus/lib/promise"
"github.com/filecoin-project/lotus/node/modules/dtypes"
"github.com/filecoin-project/lotus/storage/paths"
2023-11-10 19:17:05 +00:00
"github.com/filecoin-project/lotus/storage/sealer/storiface"
2023-11-09 22:28:36 +00:00
)
var log = logging.Logger("curio/winning")
2023-11-09 22:28:36 +00:00
type WinPostTask struct {
2023-11-10 17:04:06 +00:00
max int
2023-11-10 19:01:17 +00:00
db *harmonydb.DB
2023-11-10 19:17:05 +00:00
paths *paths.Local
2023-11-10 19:17:05 +00:00
verifier storiface.Verifier
2023-11-09 22:28:36 +00:00
2023-11-10 17:00:21 +00:00
api WinPostAPI
2024-02-13 01:03:45 +00:00
actors map[dtypes.MinerAddress]bool
2023-11-10 17:00:21 +00:00
mineTF promise.Promise[harmonytask.AddTaskFunc]
2023-11-09 22:28:36 +00:00
}
type WinPostAPI interface {
ChainHead(context.Context) (*types.TipSet, error)
ChainTipSetWeight(context.Context, types.TipSetKey) (types.BigInt, error)
2023-11-10 19:01:17 +00:00
ChainGetTipSet(context.Context, types.TipSetKey) (*types.TipSet, error)
2023-11-09 22:28:36 +00:00
StateGetBeaconEntry(context.Context, abi.ChainEpoch) (*types.BeaconEntry, error)
SyncSubmitBlock(context.Context, *types.BlockMsg) error
2023-11-10 19:01:17 +00:00
StateGetRandomnessFromBeacon(ctx context.Context, personalization crypto.DomainSeparationTag, randEpoch abi.ChainEpoch, entropy []byte, tsk types.TipSetKey) (abi.Randomness, error)
StateGetRandomnessFromTickets(ctx context.Context, personalization crypto.DomainSeparationTag, randEpoch abi.ChainEpoch, entropy []byte, tsk types.TipSetKey) (abi.Randomness, error)
StateNetworkVersion(context.Context, types.TipSetKey) (network.Version, error)
2023-11-10 19:17:05 +00:00
StateMinerInfo(context.Context, address.Address, types.TipSetKey) (api.MinerInfo, error)
2023-11-10 19:01:17 +00:00
MinerGetBaseInfo(context.Context, address.Address, abi.ChainEpoch, types.TipSetKey) (*api.MiningBaseInfo, error)
MinerCreateBlock(context.Context, *api.BlockTemplate) (*types.BlockMsg, error)
MpoolSelect(context.Context, types.TipSetKey, float64) ([]*types.SignedMessage, error)
WalletSign(context.Context, address.Address, []byte) (*crypto.Signature, error)
2023-11-09 22:28:36 +00:00
}
func NewWinPostTask(max int, db *harmonydb.DB, pl *paths.Local, verifier storiface.Verifier, api WinPostAPI, actors map[dtypes.MinerAddress]bool) *WinPostTask {
2023-11-11 13:16:26 +00:00
t := &WinPostTask{
2023-11-10 19:17:05 +00:00
max: max,
db: db,
paths: pl,
2023-11-10 19:17:05 +00:00
verifier: verifier,
api: api,
actors: actors,
2023-11-10 19:01:17 +00:00
}
// TODO: run warmup
2023-11-11 13:16:26 +00:00
go t.mineBasic(context.TODO())
return t
2023-11-09 22:28:36 +00:00
}
func (t *WinPostTask) Do(taskID harmonytask.TaskID, stillOwned func() bool) (done bool, err error) {
2023-11-14 13:03:36 +00:00
log.Debugw("WinPostTask.Do()", "taskID", taskID)
2023-11-10 19:01:17 +00:00
ctx := context.TODO()
type BlockCID struct {
CID string
}
type MiningTaskDetails struct {
SpID uint64
Epoch uint64
BlockCIDs []BlockCID
CompTime time.Time
}
var details MiningTaskDetails
2023-11-09 22:28:36 +00:00
2023-11-10 19:01:17 +00:00
// First query to fetch from mining_tasks
err = t.db.QueryRow(ctx, `SELECT sp_id, epoch, base_compute_time FROM mining_tasks WHERE task_id = $1`, taskID).Scan(&details.SpID, &details.Epoch, &details.CompTime)
if err != nil {
2023-12-19 11:39:25 +00:00
return false, xerrors.Errorf("query mining base info fail: %w", err)
2023-11-10 19:01:17 +00:00
}
// Second query to fetch from mining_base_block
rows, err := t.db.Query(ctx, `SELECT block_cid FROM mining_base_block WHERE task_id = $1`, taskID)
if err != nil {
2023-12-19 11:39:25 +00:00
return false, xerrors.Errorf("query mining base blocks fail: %w", err)
2023-11-10 19:01:17 +00:00
}
defer rows.Close()
for rows.Next() {
var cid BlockCID
if err := rows.Scan(&cid.CID); err != nil {
return false, err
}
details.BlockCIDs = append(details.BlockCIDs, cid)
}
if err := rows.Err(); err != nil {
2023-12-19 11:39:25 +00:00
return false, xerrors.Errorf("query mining base blocks fail (rows.Err): %w", err)
2023-11-10 19:01:17 +00:00
}
// construct base
maddr, err := address.NewIDAddress(details.SpID)
if err != nil {
return false, err
}
var bcids []cid.Cid
for _, c := range details.BlockCIDs {
bcid, err := cid.Parse(c.CID)
if err != nil {
return false, err
}
bcids = append(bcids, bcid)
}
tsk := types.NewTipSetKey(bcids...)
baseTs, err := t.api.ChainGetTipSet(ctx, tsk)
if err != nil {
return false, xerrors.Errorf("loading base tipset: %w", err)
}
base := MiningBase{
TipSet: baseTs,
AddRounds: abi.ChainEpoch(details.Epoch) - baseTs.Height() - 1,
ComputeTime: details.CompTime,
}
persistNoWin := func() (bool, error) {
n, err := t.db.Exec(ctx, `UPDATE mining_base_block SET no_win = true WHERE task_id = $1`, taskID)
if err != nil {
return false, xerrors.Errorf("marking base as not-won: %w", err)
}
log.Debugw("persisted no-win", "rows", n)
if n == 0 {
return false, xerrors.Errorf("persist no win: no rows updated")
}
return true, nil
}
2023-11-10 19:01:17 +00:00
// ensure we have a beacon entry for the epoch we're mining on
round := base.epoch()
_ = retry1(func() (*types.BeaconEntry, error) {
return t.api.StateGetBeaconEntry(ctx, round)
})
// MAKE A MINING ATTEMPT!!
log.Debugw("attempting to mine a block", "tipset", types.LogCids(base.TipSet.Cids()), "null-rounds", base.AddRounds)
2023-11-10 19:01:17 +00:00
mbi, err := t.api.MinerGetBaseInfo(ctx, maddr, round, base.TipSet.Key())
if err != nil {
return false, xerrors.Errorf("failed to get mining base info: %w", err)
2023-11-10 19:01:17 +00:00
}
if mbi == nil {
// not eligible to mine on this base, we're done here
2023-11-14 13:03:36 +00:00
log.Debugw("WinPoSt not eligible to mine on this base", "tipset", types.LogCids(base.TipSet.Cids()))
return persistNoWin()
2023-11-10 19:01:17 +00:00
}
if !mbi.EligibleForMining {
// slashed or just have no power yet, we're done here
2023-11-14 13:03:36 +00:00
log.Debugw("WinPoSt not eligible for mining", "tipset", types.LogCids(base.TipSet.Cids()))
return persistNoWin()
2023-11-10 19:01:17 +00:00
}
2023-11-11 13:16:26 +00:00
if len(mbi.Sectors) == 0 {
2023-11-14 13:03:36 +00:00
log.Warnw("WinPoSt no sectors to mine", "tipset", types.LogCids(base.TipSet.Cids()))
2023-11-11 13:16:26 +00:00
return false, xerrors.Errorf("no sectors selected for winning PoSt")
}
2023-11-10 19:01:17 +00:00
var rbase types.BeaconEntry
var bvals []types.BeaconEntry
var eproof *types.ElectionProof
// winner check
{
bvals = mbi.BeaconEntries
rbase = mbi.PrevBeaconEntry
if len(bvals) > 0 {
rbase = bvals[len(bvals)-1]
}
eproof, err = gen.IsRoundWinner(ctx, round, maddr, rbase, mbi, t.api)
if err != nil {
2023-11-14 13:03:36 +00:00
log.Warnw("WinPoSt failed to check if we win next round", "error", err)
2023-11-10 19:01:17 +00:00
return false, xerrors.Errorf("failed to check if we win next round: %w", err)
}
if eproof == nil {
// not a winner, we're done here
2023-11-14 13:03:36 +00:00
log.Debugw("WinPoSt not a winner", "tipset", types.LogCids(base.TipSet.Cids()))
return persistNoWin()
2023-11-10 19:01:17 +00:00
}
}
log.Infow("WinPostTask won election", "tipset", types.LogCids(base.TipSet.Cids()), "miner", maddr, "round", round, "eproof", eproof)
2023-11-10 19:01:17 +00:00
// winning PoSt
var wpostProof []prooftypes.PoStProof
{
buf := new(bytes.Buffer)
if err := maddr.MarshalCBOR(buf); err != nil {
err = xerrors.Errorf("failed to marshal miner address: %w", err)
return false, err
}
2023-11-10 19:17:05 +00:00
brand, err := lrand.DrawRandomnessFromBase(rbase.Data, crypto.DomainSeparationTag_WinningPoStChallengeSeed, round, buf.Bytes())
2023-11-10 19:01:17 +00:00
if err != nil {
err = xerrors.Errorf("failed to get randomness for winning post: %w", err)
return false, err
}
2023-11-10 19:17:05 +00:00
prand := abi.PoStRandomness(brand)
2023-11-11 13:16:26 +00:00
prand[31] &= 0x3f // make into fr
2023-11-10 19:01:17 +00:00
2023-11-10 19:36:41 +00:00
sectorNums := make([]abi.SectorNumber, len(mbi.Sectors))
for i, s := range mbi.Sectors {
sectorNums[i] = s.SectorNumber
}
2023-11-11 13:16:26 +00:00
ppt, err := mbi.Sectors[0].SealProof.RegisteredWinningPoStProof()
if err != nil {
return false, xerrors.Errorf("mapping sector seal proof type to post proof type: %w", err)
}
postChallenges, err := ffi.GeneratePoStFallbackSectorChallenges(ppt, abi.ActorID(details.SpID), prand, sectorNums)
2023-11-10 19:36:41 +00:00
if err != nil {
2023-11-11 13:16:26 +00:00
return false, xerrors.Errorf("generating election challenges: %v", err)
2023-11-10 19:36:41 +00:00
}
sectorChallenges := make([]storiface.PostSectorChallenge, len(mbi.Sectors))
for i, s := range mbi.Sectors {
sectorChallenges[i] = storiface.PostSectorChallenge{
SealProof: s.SealProof,
SectorNumber: s.SectorNumber,
SealedCID: s.SealedCID,
Challenge: postChallenges.Challenges[s.SectorNumber],
Update: s.SectorKey != nil,
}
}
_, err = t.generateWinningPost(ctx, ppt, abi.ActorID(details.SpID), sectorChallenges, prand)
//wpostProof, err = t.prover.GenerateWinningPoSt(ctx, ppt, abi.ActorID(details.SpID), sectorChallenges, prand)
2023-11-10 19:01:17 +00:00
if err != nil {
err = xerrors.Errorf("failed to compute winning post proof: %w", err)
return false, err
}
}
log.Infow("WinPostTask winning PoSt computed", "tipset", types.LogCids(base.TipSet.Cids()), "miner", maddr, "round", round, "proofs", wpostProof)
2023-11-10 19:01:17 +00:00
ticket, err := t.computeTicket(ctx, maddr, &rbase, round, base.TipSet.MinTicket(), mbi)
if err != nil {
return false, xerrors.Errorf("scratching ticket failed: %w", err)
}
// get pending messages early,
msgs, err := t.api.MpoolSelect(ctx, base.TipSet.Key(), ticket.Quality())
if err != nil {
return false, xerrors.Errorf("failed to select messages for block: %w", err)
}
log.Infow("WinPostTask selected messages", "tipset", types.LogCids(base.TipSet.Cids()), "miner", maddr, "round", round, "messages", len(msgs))
2023-11-10 19:01:17 +00:00
// equivocation handling
{
// This next block exists to "catch" equivocating miners,
// who submit 2 blocks at the same height at different times in order to split the network.
// To safeguard against this, we make sure it's been EquivocationDelaySecs since our base was calculated,
// then re-calculate it.
// If the daemon detected equivocated blocks, those blocks will no longer be in the new base.
time.Sleep(time.Until(base.ComputeTime.Add(time.Duration(build.EquivocationDelaySecs) * time.Second)))
bestTs, err := t.api.ChainHead(ctx)
if err != nil {
return false, xerrors.Errorf("failed to get chain head: %w", err)
}
headWeight, err := t.api.ChainTipSetWeight(ctx, bestTs.Key())
if err != nil {
return false, xerrors.Errorf("failed to get chain head weight: %w", err)
}
baseWeight, err := t.api.ChainTipSetWeight(ctx, base.TipSet.Key())
if err != nil {
return false, xerrors.Errorf("failed to get base weight: %w", err)
}
if types.BigCmp(headWeight, baseWeight) <= 0 {
bestTs = base.TipSet
}
// If the base has changed, we take the _intersection_ of our old base and new base,
// thus ejecting blocks from any equivocating miners, without taking any new blocks.
if bestTs.Height() == base.TipSet.Height() && !bestTs.Equals(base.TipSet) {
log.Warnf("base changed from %s to %s, taking intersection", base.TipSet.Key(), bestTs.Key())
newBaseMap := map[cid.Cid]struct{}{}
for _, newBaseBlk := range bestTs.Cids() {
newBaseMap[newBaseBlk] = struct{}{}
}
refreshedBaseBlocks := make([]*types.BlockHeader, 0, len(base.TipSet.Cids()))
for _, baseBlk := range base.TipSet.Blocks() {
if _, ok := newBaseMap[baseBlk.Cid()]; ok {
refreshedBaseBlocks = append(refreshedBaseBlocks, baseBlk)
}
}
if len(refreshedBaseBlocks) != 0 && len(refreshedBaseBlocks) != len(base.TipSet.Blocks()) {
refreshedBase, err := types.NewTipSet(refreshedBaseBlocks)
if err != nil {
return false, xerrors.Errorf("failed to create new tipset when refreshing: %w", err)
}
if !base.TipSet.MinTicket().Equals(refreshedBase.MinTicket()) {
log.Warn("recomputing ticket due to base refresh")
ticket, err = t.computeTicket(ctx, maddr, &rbase, round, refreshedBase.MinTicket(), mbi)
if err != nil {
return false, xerrors.Errorf("failed to refresh ticket: %w", err)
}
}
log.Warn("re-selecting messages due to base refresh")
// refresh messages, as the selected messages may no longer be valid
msgs, err = t.api.MpoolSelect(ctx, refreshedBase.Key(), ticket.Quality())
if err != nil {
return false, xerrors.Errorf("failed to re-select messages for block: %w", err)
}
base.TipSet = refreshedBase
}
}
}
log.Infow("WinPostTask base ready", "tipset", types.LogCids(base.TipSet.Cids()), "miner", maddr, "round", round, "ticket", ticket)
2023-11-10 19:01:17 +00:00
// block construction
var blockMsg *types.BlockMsg
{
uts := base.TipSet.MinTimestamp() + build.BlockDelaySecs*(uint64(base.AddRounds)+1)
blockMsg, err = t.api.MinerCreateBlock(context.TODO(), &api.BlockTemplate{
Miner: maddr,
Parents: base.TipSet.Key(),
Ticket: ticket,
Eproof: eproof,
BeaconValues: bvals,
Messages: msgs,
Epoch: round,
Timestamp: uts,
WinningPoStProof: wpostProof,
})
if err != nil {
return false, xerrors.Errorf("failed to create block: %w", err)
}
}
log.Infow("WinPostTask block ready", "tipset", types.LogCids(base.TipSet.Cids()), "miner", maddr, "round", round, "block", blockMsg.Header.Cid(), "timestamp", blockMsg.Header.Timestamp)
2023-11-10 19:01:17 +00:00
// persist in db
{
bhjson, err := json.Marshal(blockMsg.Header)
if err != nil {
return false, xerrors.Errorf("failed to marshal block header: %w", err)
}
_, err = t.db.Exec(ctx, `UPDATE mining_tasks
SET won = true, mined_cid = $2, mined_header = $3, mined_at = $4
WHERE task_id = $1`, taskID, blockMsg.Header.Cid(), string(bhjson), time.Now().UTC())
if err != nil {
return false, xerrors.Errorf("failed to update mining task: %w", err)
}
}
2023-11-10 19:17:05 +00:00
// wait until block timestamp
{
log.Infow("WinPostTask waiting for block timestamp", "tipset", types.LogCids(base.TipSet.Cids()), "miner", maddr, "round", round, "block", blockMsg.Header.Cid(), "until", time.Unix(int64(blockMsg.Header.Timestamp), 0))
2023-11-10 19:17:05 +00:00
time.Sleep(time.Until(time.Unix(int64(blockMsg.Header.Timestamp), 0)))
}
2023-11-10 19:01:17 +00:00
// submit block!!
{
log.Infow("WinPostTask submitting block", "tipset", types.LogCids(base.TipSet.Cids()), "miner", maddr, "round", round, "block", blockMsg.Header.Cid())
2023-11-10 19:01:17 +00:00
if err := t.api.SyncSubmitBlock(ctx, blockMsg); err != nil {
return false, xerrors.Errorf("failed to submit block: %w", err)
}
}
log.Infow("mined a block", "tipset", types.LogCids(blockMsg.Header.Parents), "height", blockMsg.Header.Height, "miner", maddr, "cid", blockMsg.Header.Cid())
// persist that we've submitted the block
{
_, err = t.db.Exec(ctx, `UPDATE mining_tasks
SET submitted_at = $2
WHERE task_id = $1`, taskID, time.Now().UTC())
if err != nil {
return false, xerrors.Errorf("failed to update mining task: %w", err)
}
}
return true, nil
2023-11-09 22:28:36 +00:00
}
func (t *WinPostTask) generateWinningPost(
ctx context.Context,
ppt abi.RegisteredPoStProof,
mid abi.ActorID,
sectors []storiface.PostSectorChallenge,
randomness abi.PoStRandomness) ([]proof.PoStProof, error) {
// don't throttle winningPoSt
// * Always want it done asap
// * It's usually just one sector
vproofs := make([][]byte, len(sectors))
eg := errgroup.Group{}
for i, s := range sectors {
i, s := i, s
eg.Go(func() error {
vanilla, err := t.paths.GenerateSingleVanillaProof(ctx, mid, s, ppt)
if err != nil {
return xerrors.Errorf("get winning sector:%d,vanila failed: %w", s.SectorNumber, err)
}
if vanilla == nil {
return xerrors.Errorf("get winning sector:%d,vanila is nil", s.SectorNumber)
}
vproofs[i] = vanilla
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
return ffiselect.FFISelect{}.GenerateWinningPoStWithVanilla(ppt, mid, randomness, vproofs)
}
2023-11-09 22:28:36 +00:00
func (t *WinPostTask) CanAccept(ids []harmonytask.TaskID, engine *harmonytask.TaskEngine) (*harmonytask.TaskID, error) {
2023-11-10 17:04:06 +00:00
if len(ids) == 0 {
// probably can't happen, but panicking is bad
return nil, nil
}
// select lowest epoch
var lowestEpoch abi.ChainEpoch
var lowestEpochID = ids[0]
for _, id := range ids {
var epoch uint64
err := t.db.QueryRow(context.Background(), `SELECT epoch FROM mining_tasks WHERE task_id = $1`, id).Scan(&epoch)
if err != nil {
return nil, err
}
if lowestEpoch == 0 || abi.ChainEpoch(epoch) < lowestEpoch {
lowestEpoch = abi.ChainEpoch(epoch)
lowestEpochID = id
}
}
return &lowestEpochID, nil
2023-11-09 22:28:36 +00:00
}
func (t *WinPostTask) TypeDetails() harmonytask.TaskTypeDetails {
return harmonytask.TaskTypeDetails{
Name: "WinPost",
2023-11-10 17:04:06 +00:00
Max: t.max,
2023-11-09 22:28:36 +00:00
MaxFailures: 3,
Follows: nil,
Cost: resources.Resources{
Cpu: 1,
// todo set to something for 32/64G sector sizes? Technically windowPoSt is happy on a CPU
// but it will use a GPU if available
Gpu: 0,
Ram: 1 << 30, // todo arbitrary number
},
}
}
func (t *WinPostTask) Adder(taskFunc harmonytask.AddTaskFunc) {
2023-11-10 17:00:21 +00:00
t.mineTF.Set(taskFunc)
2023-11-09 22:28:36 +00:00
}
// MiningBase is the tipset on top of which we plan to construct our next block.
// Refer to godocs on GetBestMiningCandidate.
type MiningBase struct {
TipSet *types.TipSet
ComputeTime time.Time
2023-11-10 17:00:21 +00:00
AddRounds abi.ChainEpoch
}
func (mb MiningBase) epoch() abi.ChainEpoch {
// return the epoch that will result from mining on this base
return mb.TipSet.Height() + mb.AddRounds + 1
2023-11-09 22:28:36 +00:00
}
func (mb MiningBase) baseTime() time.Time {
tsTime := time.Unix(int64(mb.TipSet.MinTimestamp()), 0)
2023-11-10 17:00:21 +00:00
roundDelay := build.BlockDelaySecs * uint64(mb.AddRounds+1)
tsTime = tsTime.Add(time.Duration(roundDelay) * time.Second)
return tsTime
}
func (mb MiningBase) afterPropDelay() time.Time {
return mb.baseTime().Add(time.Duration(build.PropagationDelaySecs) * time.Second).Add(randTimeOffset(time.Second))
}
func (t *WinPostTask) mineBasic(ctx context.Context) {
var workBase MiningBase
2023-11-10 17:00:21 +00:00
taskFn := t.mineTF.Val(ctx)
2023-11-10 19:17:05 +00:00
// initialize workbase
{
head := retry1(func() (*types.TipSet, error) {
return t.api.ChainHead(ctx)
})
workBase = MiningBase{
TipSet: head,
2023-11-10 17:00:21 +00:00
AddRounds: 0,
ComputeTime: time.Now(),
}
}
2023-11-10 17:00:21 +00:00
/*
/- T+0 == workBase.baseTime
|
>--------*------*--------[wait until next round]----->
|
|- T+PD == workBase.afterPropDelay+(~1s)
|- Here we acquire the new workBase, and start a new round task
\- Then we loop around, and wait for the next head
time -->
*/
for {
2023-11-14 13:03:36 +00:00
// limit the rate at which we mine blocks to at least EquivocationDelaySecs
// this is to prevent races on devnets in catch up mode. Acts as a minimum
// delay for the sleep below.
time.Sleep(time.Duration(build.EquivocationDelaySecs)*time.Second + time.Second)
2023-11-10 17:00:21 +00:00
// wait for *NEXT* propagation delay
time.Sleep(time.Until(workBase.afterPropDelay()))
// check current best candidate
maybeBase := retry1(func() (*types.TipSet, error) {
return t.api.ChainHead(ctx)
})
if workBase.TipSet.Equals(maybeBase) {
2023-11-10 17:00:21 +00:00
// workbase didn't change in the new round so we have a null round here
workBase.AddRounds++
log.Debugw("workbase update", "tipset", workBase.TipSet.Cids(), "nulls", workBase.AddRounds, "lastUpdate", time.Since(workBase.ComputeTime), "type", "same-tipset")
} else {
btsw := retry1(func() (types.BigInt, error) {
return t.api.ChainTipSetWeight(ctx, maybeBase.Key())
})
ltsw := retry1(func() (types.BigInt, error) {
return t.api.ChainTipSetWeight(ctx, workBase.TipSet.Key())
})
if types.BigCmp(btsw, ltsw) <= 0 {
2023-11-10 17:00:21 +00:00
// new tipset for some reason has less weight than the old one, assume null round here
// NOTE: the backing node may have reorged, or manually changed head
workBase.AddRounds++
log.Debugw("workbase update", "tipset", workBase.TipSet.Cids(), "nulls", workBase.AddRounds, "lastUpdate", time.Since(workBase.ComputeTime), "type", "prefer-local-weight")
} else {
2023-11-10 17:00:21 +00:00
// new tipset has more weight, so we should mine on it, no null round here
log.Debugw("workbase update", "tipset", workBase.TipSet.Cids(), "nulls", workBase.AddRounds, "lastUpdate", time.Since(workBase.ComputeTime), "type", "prefer-new-tipset")
workBase = MiningBase{
TipSet: maybeBase,
2023-11-10 17:00:21 +00:00
AddRounds: 0,
ComputeTime: time.Now(),
}
}
}
2023-11-10 17:00:21 +00:00
// dispatch mining task
// (note equivocation prevention is handled by the mining code)
baseEpoch := workBase.TipSet.Height()
2024-02-13 01:03:45 +00:00
for act := range t.actors {
2023-11-10 17:00:21 +00:00
spID, err := address.IDFromAddress(address.Address(act))
if err != nil {
log.Errorf("failed to get spID from address %s: %s", act, err)
continue
}
taskFn(func(id harmonytask.TaskID, tx *harmonydb.Tx) (shouldCommit bool, seriousError error) {
// First we check if the mining base includes blocks we may have mined previously to avoid getting slashed
// select mining_tasks where epoch==base_epoch if win=true to maybe get base block cid which has to be included in our tipset
2023-12-05 16:51:42 +00:00
var baseBlockCids []string
err := tx.Select(&baseBlockCids, `SELECT mined_cid FROM mining_tasks WHERE epoch = $1 AND sp_id = $2 AND won = true`, baseEpoch, spID)
if err != nil {
return false, xerrors.Errorf("querying mining_tasks: %w", err)
}
2023-12-05 16:51:42 +00:00
if len(baseBlockCids) >= 1 {
baseBlockCid := baseBlockCids[0]
c, err := cid.Parse(baseBlockCid)
if err != nil {
return false, xerrors.Errorf("parsing mined_cid: %w", err)
}
// we have mined in the previous round, make sure that our block is included in the tipset
// if it's not we risk getting slashed
var foundOurs bool
for _, c2 := range workBase.TipSet.Cids() {
if c == c2 {
foundOurs = true
break
}
}
if !foundOurs {
log.Errorw("our block was not included in the tipset, aborting", "tipset", workBase.TipSet.Cids(), "ourBlock", c)
return false, xerrors.Errorf("our block was not included in the tipset, aborting")
}
}
_, err = tx.Exec(`INSERT INTO mining_tasks (task_id, sp_id, epoch, base_compute_time) VALUES ($1, $2, $3, $4)`, id, spID, workBase.epoch(), workBase.ComputeTime.UTC())
2023-11-10 17:00:21 +00:00
if err != nil {
return false, xerrors.Errorf("inserting mining_tasks: %w", err)
}
for _, c := range workBase.TipSet.Cids() {
_, err = tx.Exec(`INSERT INTO mining_base_block (task_id, sp_id, block_cid) VALUES ($1, $2, $3)`, id, spID, c)
2023-11-10 17:00:21 +00:00
if err != nil {
return false, xerrors.Errorf("inserting mining base blocks: %w", err)
}
}
return true, nil // no errors, commit the transaction
})
}
}
}
2023-11-10 19:01:17 +00:00
func (t *WinPostTask) computeTicket(ctx context.Context, maddr address.Address, brand *types.BeaconEntry, round abi.ChainEpoch, chainRand *types.Ticket, mbi *api.MiningBaseInfo) (*types.Ticket, error) {
buf := new(bytes.Buffer)
if err := maddr.MarshalCBOR(buf); err != nil {
return nil, xerrors.Errorf("failed to marshal address to cbor: %w", err)
}
if round > build.UpgradeSmokeHeight {
buf.Write(chainRand.VRFProof)
}
input, err := lrand.DrawRandomnessFromBase(brand.Data, crypto.DomainSeparationTag_TicketProduction, round-build.TicketRandomnessLookback, buf.Bytes())
if err != nil {
return nil, err
}
vrfOut, err := gen.ComputeVRF(ctx, t.api.WalletSign, mbi.WorkerKey, input)
if err != nil {
return nil, err
}
return &types.Ticket{
VRFProof: vrfOut,
}, nil
}
2023-11-09 22:28:36 +00:00
func randTimeOffset(width time.Duration) time.Duration {
buf := make([]byte, 8)
_, _ = rand.Reader.Read(buf)
2023-11-09 22:28:36 +00:00
val := time.Duration(binary.BigEndian.Uint64(buf) % uint64(width))
return val - (width / 2)
}
2023-11-10 17:04:06 +00:00
func retry1[R any](f func() (R, error)) R {
for {
r, err := f()
if err == nil {
return r
}
log.Errorw("error in mining loop, retrying", "error", err)
time.Sleep(time.Second)
}
}
2023-11-09 22:28:36 +00:00
var _ harmonytask.TaskInterface = &WinPostTask{}