2024-03-15 21:38:13 +00:00
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"
2024-05-23 11:03:59 +00:00
"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"
2024-05-23 11:03:59 +00:00
"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"
2024-05-23 11:03:59 +00:00
"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"
2024-05-23 11:03:59 +00:00
"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
)
2024-03-15 21:38:13 +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
2024-05-23 11:03:59 +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
}
2024-05-23 11:03:59 +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 ,
2024-05-23 11:03:59 +00:00
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 ,
}
2023-12-15 13:37:22 +00:00
persistNoWin := func ( ) ( bool , error ) {
n , err := t . db . Exec ( ctx , ` UPDATE mining_base_block SET no_win = true WHERE task_id = $1 ` , taskID )
2023-11-11 13:45:58 +00:00
if err != nil {
2023-12-15 13:37:22 +00:00
return false , xerrors . Errorf ( "marking base as not-won: %w" , err )
2023-11-11 13:45:58 +00:00
}
2023-12-15 13:37:22 +00:00
log . Debugw ( "persisted no-win" , "rows" , n )
2023-11-11 13:45:58 +00:00
2023-12-15 13:37:22 +00:00
if n == 0 {
return false , xerrors . Errorf ( "persist no win: no rows updated" )
}
2024-01-03 16:11:10 +00:00
return true , nil
2023-11-11 13:45:58 +00:00
}
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!!
2024-02-24 10:52:23 +00:00
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 {
2023-11-11 13:45:58 +00:00
return false , xerrors . Errorf ( "failed to get mining base info: %w" , err )
2023-11-10 19:01:17 +00:00
}
if mbi == nil {
2023-11-11 13:45:58 +00:00
// 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 ( ) ) )
2023-12-15 13:37:22 +00:00
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 ( ) ) )
2023-12-15 13:37:22 +00:00
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 ( ) ) )
2023-12-15 13:37:22 +00:00
return persistNoWin ( )
2023-11-10 19:01:17 +00:00
}
}
2024-02-24 10:52:23 +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 ,
}
}
2024-05-23 11:03:59 +00:00
_ , 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
}
}
2024-02-24 10:52:23 +00:00
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 )
}
2024-02-24 10:52:23 +00:00
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
}
}
}
2024-02-24 10:52:23 +00:00
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 )
}
}
2024-02-24 10:52:23 +00:00
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
{
2024-02-24 10:52:23 +00:00
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!!
{
2024-02-24 10:52:23 +00:00
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
}
2024-05-23 11:03:59 +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
}
2023-11-11 11:44:36 +00:00
// 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
}
2023-11-10 10:40:56 +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 )
2023-11-10 10:40:56 +00:00
return tsTime
}
func ( mb MiningBase ) afterPropDelay ( ) time . Time {
2024-02-24 10:52:23 +00:00
return mb . baseTime ( ) . Add ( time . Duration ( build . PropagationDelaySecs ) * time . Second ) . Add ( randTimeOffset ( time . Second ) )
2023-11-10 10:40:56 +00:00
}
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
2023-11-10 10:40:56 +00:00
{
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 ,
2023-11-10 10:40:56 +00:00
ComputeTime : time . Now ( ) ,
}
}
2023-11-10 17:00:21 +00:00
/ *
/ - T + 0 == workBase . baseTime
|
> -- -- -- -- * -- -- -- * -- -- -- -- [ wait until next round ] -- -- - >
|
| - T + PD == workBase . afterPropDelay + ( ~ 1 s )
| - Here we acquire the new workBase , and start a new round task
\ - Then we loop around , and wait for the next head
time -- >
* /
2023-11-10 10:40:56 +00:00
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
2023-11-10 10:40:56 +00:00
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" )
2023-11-10 10:40:56 +00:00
} 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" )
2023-11-10 10:40:56 +00:00
} 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" )
2023-11-10 10:40:56 +00:00
workBase = MiningBase {
TipSet : maybeBase ,
2023-11-10 17:00:21 +00:00
AddRounds : 0 ,
2023-11-10 10:40:56 +00:00
ComputeTime : time . Now ( ) ,
}
}
}
2023-11-10 17:00:21 +00:00
// dispatch mining task
// (note equivocation prevention is handled by the mining code)
2023-11-27 22:18:28 +00:00
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 ) {
2023-11-27 22:18:28 +00:00
// 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 {
2023-11-27 22:18:28 +00:00
return false , xerrors . Errorf ( "querying mining_tasks: %w" , err )
}
2023-12-05 16:51:42 +00:00
if len ( baseBlockCids ) >= 1 {
baseBlockCid := baseBlockCids [ 0 ]
2023-11-27 22:18:28 +00:00
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 ( ) {
2023-11-11 13:45:58 +00:00
_ , 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 10:40:56 +00:00
}
}
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 )
2024-05-09 02:15:35 +00:00
_ , _ = 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 { }