Merge pull request #1557 from filecoin-project/feat/power-table-lookback
Use lookback to get sector set and power for miners
This commit is contained in:
commit
9c5cda0efa
@ -71,7 +71,7 @@ type FullNode interface {
|
||||
|
||||
// miner
|
||||
|
||||
MinerGetBaseInfo(context.Context, address.Address, types.TipSetKey) (*MiningBaseInfo, error)
|
||||
MinerGetBaseInfo(context.Context, address.Address, abi.ChainEpoch, types.TipSetKey) (*MiningBaseInfo, error)
|
||||
MinerCreateBlock(context.Context, *BlockTemplate) (*types.BlockMsg, error)
|
||||
|
||||
// // UX ?
|
||||
@ -382,7 +382,7 @@ type ComputeStateOutput struct {
|
||||
type MiningBaseInfo struct {
|
||||
MinerPower types.BigInt
|
||||
NetworkPower types.BigInt
|
||||
Sectors []*ChainSectorInfo
|
||||
Sectors []abi.SectorInfo
|
||||
WorkerKey address.Address
|
||||
SectorSize abi.SectorSize
|
||||
PrevBeaconEntry types.BeaconEntry
|
||||
|
@ -87,8 +87,8 @@ type FullNodeStruct struct {
|
||||
MpoolGetNonce func(context.Context, address.Address) (uint64, error) `perm:"read"`
|
||||
MpoolSub func(context.Context) (<-chan api.MpoolUpdate, error) `perm:"read"`
|
||||
|
||||
MinerGetBaseInfo func(context.Context, address.Address, types.TipSetKey) (*api.MiningBaseInfo, error) `perm:"read"`
|
||||
MinerCreateBlock func(context.Context, *api.BlockTemplate) (*types.BlockMsg, error) `perm:"write"`
|
||||
MinerGetBaseInfo func(context.Context, address.Address, abi.ChainEpoch, types.TipSetKey) (*api.MiningBaseInfo, error) `perm:"read"`
|
||||
MinerCreateBlock func(context.Context, *api.BlockTemplate) (*types.BlockMsg, error) `perm:"write"`
|
||||
|
||||
WalletNew func(context.Context, crypto.SigType) (address.Address, error) `perm:"write"`
|
||||
WalletHas func(context.Context, address.Address) (bool, error) `perm:"write"`
|
||||
@ -332,8 +332,8 @@ func (c *FullNodeStruct) MpoolSub(ctx context.Context) (<-chan api.MpoolUpdate,
|
||||
return c.Internal.MpoolSub(ctx)
|
||||
}
|
||||
|
||||
func (c *FullNodeStruct) MinerGetBaseInfo(ctx context.Context, maddr address.Address, tsk types.TipSetKey) (*api.MiningBaseInfo, error) {
|
||||
return c.Internal.MinerGetBaseInfo(ctx, maddr, tsk)
|
||||
func (c *FullNodeStruct) MinerGetBaseInfo(ctx context.Context, maddr address.Address, epoch abi.ChainEpoch, tsk types.TipSetKey) (*api.MiningBaseInfo, error) {
|
||||
return c.Internal.MinerGetBaseInfo(ctx, maddr, epoch, tsk)
|
||||
}
|
||||
|
||||
func (c *FullNodeStruct) MinerCreateBlock(ctx context.Context, bt *api.BlockTemplate) (*types.BlockMsg, error) {
|
||||
|
@ -71,6 +71,8 @@ const MaxSealLookback = SealRandomnessLookbackLimit + 2000 // TODO: Get from spe
|
||||
// Epochs
|
||||
const TicketRandomnessLookback = 1
|
||||
|
||||
const WinningPoStSectorSetLookback = 10
|
||||
|
||||
// /////
|
||||
// Devnet settings
|
||||
|
||||
|
@ -277,9 +277,9 @@ func CarWalkFunc(nd format.Node) (out []*format.Link, err error) {
|
||||
}
|
||||
|
||||
func (cg *ChainGen) nextBlockProof(ctx context.Context, pts *types.TipSet, m address.Address, round abi.ChainEpoch) ([]types.BeaconEntry, *types.ElectionProof, *types.Ticket, error) {
|
||||
mc := &mca{w: cg.w, sm: cg.sm}
|
||||
mc := &mca{w: cg.w, sm: cg.sm, pv: ffiwrapper.ProofVerifier}
|
||||
|
||||
mbi, err := mc.MinerGetBaseInfo(ctx, m, pts.Key())
|
||||
mbi, err := mc.MinerGetBaseInfo(ctx, m, round, pts.Key())
|
||||
if err != nil {
|
||||
return nil, nil, nil, xerrors.Errorf("get miner base info: %w", err)
|
||||
}
|
||||
@ -476,7 +476,7 @@ func (cg *ChainGen) YieldRepo() (repo.Repo, error) {
|
||||
type MiningCheckAPI interface {
|
||||
ChainGetRandomness(ctx context.Context, tsk types.TipSetKey, personalization crypto.DomainSeparationTag, randEpoch abi.ChainEpoch, entropy []byte) (abi.Randomness, error)
|
||||
|
||||
MinerGetBaseInfo(context.Context, address.Address, types.TipSetKey) (*api.MiningBaseInfo, error)
|
||||
MinerGetBaseInfo(context.Context, address.Address, abi.ChainEpoch, types.TipSetKey) (*api.MiningBaseInfo, error)
|
||||
|
||||
WalletSign(context.Context, address.Address, []byte) (*crypto.Signature, error)
|
||||
}
|
||||
@ -484,6 +484,7 @@ type MiningCheckAPI interface {
|
||||
type mca struct {
|
||||
w *wallet.Wallet
|
||||
sm *stmgr.StateManager
|
||||
pv ffiwrapper.Verifier
|
||||
}
|
||||
|
||||
func (mca mca) ChainGetRandomness(ctx context.Context, tsk types.TipSetKey, personalization crypto.DomainSeparationTag, randEpoch abi.ChainEpoch, entropy []byte) (abi.Randomness, error) {
|
||||
@ -495,8 +496,8 @@ func (mca mca) ChainGetRandomness(ctx context.Context, tsk types.TipSetKey, pers
|
||||
return mca.sm.ChainStore().GetRandomness(ctx, pts.Cids(), personalization, randEpoch, entropy)
|
||||
}
|
||||
|
||||
func (mca mca) MinerGetBaseInfo(ctx context.Context, maddr address.Address, tsk types.TipSetKey) (*api.MiningBaseInfo, error) {
|
||||
return stmgr.MinerGetBaseInfo(ctx, mca.sm, tsk, maddr)
|
||||
func (mca mca) MinerGetBaseInfo(ctx context.Context, maddr address.Address, epoch abi.ChainEpoch, tsk types.TipSetKey) (*api.MiningBaseInfo, error) {
|
||||
return stmgr.MinerGetBaseInfo(ctx, mca.sm, tsk, epoch, maddr, mca.pv)
|
||||
}
|
||||
|
||||
func (mca mca) WalletSign(ctx context.Context, a address.Address, v []byte) (*crypto.Signature, error) {
|
||||
|
@ -2,6 +2,7 @@ package stmgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
amt "github.com/filecoin-project/go-amt-ipld/v2"
|
||||
"github.com/filecoin-project/sector-storage/ffiwrapper"
|
||||
"github.com/filecoin-project/specs-actors/actors/abi"
|
||||
@ -16,6 +17,7 @@ import (
|
||||
"github.com/filecoin-project/go-address"
|
||||
|
||||
"github.com/filecoin-project/lotus/api"
|
||||
"github.com/filecoin-project/lotus/build"
|
||||
"github.com/filecoin-project/lotus/chain/state"
|
||||
"github.com/filecoin-project/lotus/chain/store"
|
||||
"github.com/filecoin-project/lotus/chain/types"
|
||||
@ -56,10 +58,10 @@ func GetMinerWorkerRaw(ctx context.Context, sm *StateManager, st cid.Cid, maddr
|
||||
}
|
||||
|
||||
func GetPower(ctx context.Context, sm *StateManager, ts *types.TipSet, maddr address.Address) (power.Claim, power.Claim, error) {
|
||||
return getPowerRaw(ctx, sm, ts.ParentState(), maddr)
|
||||
return GetPowerRaw(ctx, sm, ts.ParentState(), maddr)
|
||||
}
|
||||
|
||||
func getPowerRaw(ctx context.Context, sm *StateManager, st cid.Cid, maddr address.Address) (power.Claim, power.Claim, error) {
|
||||
func GetPowerRaw(ctx context.Context, sm *StateManager, st cid.Cid, maddr address.Address) (power.Claim, power.Claim, error) {
|
||||
var ps power.State
|
||||
_, err := sm.LoadActorStateRaw(ctx, builtin.StoragePowerActorAddr, &ps, st)
|
||||
if err != nil {
|
||||
@ -133,16 +135,11 @@ func GetMinerSectorSet(ctx context.Context, sm *StateManager, ts *types.TipSet,
|
||||
return LoadSectorsFromSet(ctx, sm.ChainStore().Blockstore(), mas.Sectors, filter)
|
||||
}
|
||||
|
||||
func GetSectorsForWinningPoSt(ctx context.Context, pv ffiwrapper.Verifier, sm *StateManager, ts *types.TipSet, maddr address.Address) ([]abi.SectorInfo, error) {
|
||||
pts, err := sm.cs.LoadTipSet(ts.Parents()) // TODO: Review: check correct lookback for winningPost sector set
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("loading parent tipset: %w", err)
|
||||
}
|
||||
|
||||
func GetSectorsForWinningPoSt(ctx context.Context, pv ffiwrapper.Verifier, sm *StateManager, st cid.Cid, maddr address.Address, rand abi.PoStRandomness) ([]abi.SectorInfo, error) {
|
||||
var mas miner.State
|
||||
_, err = sm.LoadActorStateRaw(ctx, maddr, &mas, pts.ParentState())
|
||||
_, err := sm.LoadActorStateRaw(ctx, maddr, &mas, st)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("(get ssize) failed to load miner actor state: %w", err)
|
||||
return nil, xerrors.Errorf("(get sectors) failed to load miner actor state: %w", err)
|
||||
}
|
||||
|
||||
// TODO: Optimization: we could avoid loaditg the whole proving set here if we had AMT.GetNth with bitfield filtering
|
||||
@ -166,12 +163,6 @@ func GetSectorsForWinningPoSt(ctx context.Context, pv ffiwrapper.Verifier, sm *S
|
||||
return nil, xerrors.Errorf("getting miner ID: %w", err)
|
||||
}
|
||||
|
||||
// TODO: use the right dst, also NB: not using any 'entropy' in this call because nicola really didnt want it
|
||||
rand, err := sm.cs.GetRandomness(ctx, ts.Cids(), crypto.DomainSeparationTag_ElectionPoStChallengeSeed, ts.Height()-1, nil)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to get randomness for winning post: %w", err)
|
||||
}
|
||||
|
||||
ids, err := pv.GenerateWinningPoStSectorChallenge(ctx, wpt, abi.ActorID(mid), rand, uint64(len(sectorSet)))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("generating winning post challenges: %w", err)
|
||||
@ -424,29 +415,58 @@ func GetProvingSetRaw(ctx context.Context, sm *StateManager, mas miner.State) ([
|
||||
return provset, nil
|
||||
}
|
||||
|
||||
func MinerGetBaseInfo(ctx context.Context, sm *StateManager, tsk types.TipSetKey, maddr address.Address) (*api.MiningBaseInfo, error) {
|
||||
func GetLookbackTipSetForRound(ctx context.Context, sm *StateManager, ts *types.TipSet, round abi.ChainEpoch) (*types.TipSet, error) {
|
||||
var lbr abi.ChainEpoch
|
||||
if round > build.WinningPoStSectorSetLookback {
|
||||
lbr = round - build.WinningPoStSectorSetLookback
|
||||
}
|
||||
|
||||
// more null blocks than our lookback
|
||||
if lbr > ts.Height() {
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
lbts, err := sm.ChainStore().GetTipsetByHeight(ctx, lbr, ts)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to get lookback tipset: %w", err)
|
||||
}
|
||||
|
||||
return lbts, nil
|
||||
}
|
||||
|
||||
func MinerGetBaseInfo(ctx context.Context, sm *StateManager, tsk types.TipSetKey, round abi.ChainEpoch, maddr address.Address, pv ffiwrapper.Verifier) (*api.MiningBaseInfo, error) {
|
||||
ts, err := sm.ChainStore().LoadTipSet(tsk)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to load tipset for mining base: %w", err)
|
||||
}
|
||||
|
||||
st, _, err := sm.TipSetState(ctx, ts)
|
||||
lbts, err := GetLookbackTipSetForRound(ctx, sm, ts, round)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("getting lookback miner actor state: %w", err)
|
||||
}
|
||||
|
||||
lbst, _, err := sm.TipSetState(ctx, lbts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var mas miner.State
|
||||
_, err = sm.LoadActorState(ctx, maddr, &mas, ts)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("(get sset) failed to load miner actor state: %w", err)
|
||||
}
|
||||
|
||||
provset, err := GetProvingSetRaw(ctx, sm, mas)
|
||||
if err != nil {
|
||||
if _, err := sm.LoadActorStateRaw(ctx, maddr, &mas, lbst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mpow, tpow, err := getPowerRaw(ctx, sm, st, maddr)
|
||||
// TODO: use the right dst, also NB: not using any 'entropy' in this call because nicola really didnt want it
|
||||
prand, err := sm.cs.GetRandomness(ctx, ts.Cids(), crypto.DomainSeparationTag_ElectionPoStChallengeSeed, round-1, nil)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to get randomness for winning post: %w", err)
|
||||
}
|
||||
|
||||
sectors, err := GetSectorsForWinningPoSt(ctx, pv, sm, lbst, maddr, prand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("getting wpost proving set: %w", err)
|
||||
}
|
||||
|
||||
mpow, tpow, err := GetPowerRaw(ctx, sm, lbst, maddr)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to get power: %w", err)
|
||||
}
|
||||
@ -464,7 +484,7 @@ func MinerGetBaseInfo(ctx context.Context, sm *StateManager, tsk types.TipSetKey
|
||||
return &api.MiningBaseInfo{
|
||||
MinerPower: mpow.QualityAdjPower,
|
||||
NetworkPower: tpow.QualityAdjPower,
|
||||
Sectors: provset,
|
||||
Sectors: sectors,
|
||||
WorkerKey: worker,
|
||||
SectorSize: mas.Info.SectorSize,
|
||||
PrevBeaconEntry: *prev,
|
||||
|
@ -942,6 +942,10 @@ func (cs *ChainStore) GetTipsetByHeight(ctx context.Context, h abi.ChainEpoch, t
|
||||
return nil, xerrors.Errorf("looking for tipset with height less than start point")
|
||||
}
|
||||
|
||||
if h == ts.Height() {
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
if ts.Height()-h > build.ForkLengthThreshold {
|
||||
log.Warnf("expensive call to GetTipsetByHeight, seeking %d levels", ts.Height()-h)
|
||||
}
|
||||
@ -955,6 +959,9 @@ func (cs *ChainStore) GetTipsetByHeight(ctx context.Context, h abi.ChainEpoch, t
|
||||
if h > pts.Height() {
|
||||
return ts, nil
|
||||
}
|
||||
if h == pts.Height() {
|
||||
return pts, nil
|
||||
}
|
||||
|
||||
ts = pts
|
||||
}
|
||||
|
@ -519,6 +519,16 @@ func (syncer *Syncer) ValidateBlock(ctx context.Context, b *types.FullBlock) err
|
||||
return xerrors.Errorf("load parent tipset failed (%s): %w", h.Parents, err)
|
||||
}
|
||||
|
||||
lbts, err := stmgr.GetLookbackTipSetForRound(ctx, syncer.sm, baseTs, h.Height)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to get lookback tipset for block: %w", err)
|
||||
}
|
||||
|
||||
lbst, _, err := syncer.sm.TipSetState(ctx, lbts)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to compute lookback tipset state: %w", err)
|
||||
}
|
||||
|
||||
prevBeacon, err := syncer.store.GetLatestBeaconEntry(baseTs)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to get latest beacon entry: %w", err)
|
||||
@ -585,7 +595,7 @@ func (syncer *Syncer) ValidateBlock(ctx context.Context, b *types.FullBlock) err
|
||||
return xerrors.Errorf("parent receipts root did not match computed value (%s != %s)", precp, h.ParentMessageReceipts)
|
||||
}
|
||||
|
||||
waddr, err := stmgr.GetMinerWorkerRaw(ctx, syncer.sm, stateroot, h.Miner)
|
||||
waddr, err := stmgr.GetMinerWorkerRaw(ctx, syncer.sm, lbst, h.Miner)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("GetMinerWorkerRaw failed: %w", err)
|
||||
}
|
||||
@ -620,7 +630,7 @@ func (syncer *Syncer) ValidateBlock(ctx context.Context, b *types.FullBlock) err
|
||||
return xerrors.Errorf("received block was from slashed or invalid miner")
|
||||
}
|
||||
|
||||
mpow, tpow, err := stmgr.GetPower(ctx, syncer.sm, baseTs, h.Miner)
|
||||
mpow, tpow, err := stmgr.GetPowerRaw(ctx, syncer.sm, lbst, h.Miner)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed getting power: %w", err)
|
||||
}
|
||||
@ -666,7 +676,7 @@ func (syncer *Syncer) ValidateBlock(ctx context.Context, b *types.FullBlock) err
|
||||
})
|
||||
|
||||
wproofCheck := async.Err(func() error {
|
||||
if err := syncer.VerifyWinningPoStProof(ctx, h, waddr); err != nil {
|
||||
if err := syncer.VerifyWinningPoStProof(ctx, h, lbst, waddr); err != nil {
|
||||
return xerrors.Errorf("invalid election post: %w", err)
|
||||
}
|
||||
return nil
|
||||
@ -709,7 +719,7 @@ func (syncer *Syncer) ValidateBlock(ctx context.Context, b *types.FullBlock) err
|
||||
return merr
|
||||
}
|
||||
|
||||
func (syncer *Syncer) VerifyWinningPoStProof(ctx context.Context, h *types.BlockHeader, waddr address.Address) error {
|
||||
func (syncer *Syncer) VerifyWinningPoStProof(ctx context.Context, h *types.BlockHeader, lbst cid.Cid, waddr address.Address) error {
|
||||
if build.InsecurePoStValidation {
|
||||
if len(h.WinPoStProof) == 0 {
|
||||
return xerrors.Errorf("[TESTING] No winning post proof given")
|
||||
@ -737,7 +747,7 @@ func (syncer *Syncer) VerifyWinningPoStProof(ctx context.Context, h *types.Block
|
||||
return xerrors.Errorf("failed to get ID from miner address %s: %w", h.Miner, err)
|
||||
}
|
||||
|
||||
sectors, err := stmgr.GetSectorsForWinningPoSt(ctx, syncer.verifier, syncer.sm, curTs, h.Miner)
|
||||
sectors, err := stmgr.GetSectorsForWinningPoSt(ctx, syncer.verifier, syncer.sm, lbst, h.Miner, rand)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("getting winning post sector set: %w", err)
|
||||
}
|
||||
|
@ -294,14 +294,15 @@ func (m *Miner) mineOne(ctx context.Context, addr address.Address, base *MiningB
|
||||
log.Debugw("attempting to mine a block", "tipset", types.LogCids(base.ts.Cids()))
|
||||
start := time.Now()
|
||||
|
||||
mbi, err := m.api.MinerGetBaseInfo(ctx, addr, base.ts.Key())
|
||||
round := base.ts.Height() + base.nullRounds + 1
|
||||
|
||||
mbi, err := m.api.MinerGetBaseInfo(ctx, addr, round, base.ts.Key())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to get mining base info: %w", err)
|
||||
}
|
||||
|
||||
beaconPrev := mbi.PrevBeaconEntry
|
||||
|
||||
round := base.ts.Height() + base.nullRounds + 1
|
||||
bvals, err := beacon.BeaconEntriesForBlock(ctx, m.beacon, round, beaconPrev)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get beacon entries failed: %w", err)
|
||||
@ -347,14 +348,7 @@ func (m *Miner) mineOne(ctx context.Context, addr address.Address, base *MiningB
|
||||
|
||||
prand := abi.PoStRandomness(rand)
|
||||
|
||||
sx, err := m.epp.GenerateCandidates(ctx, prand, uint64(len(mbi.Sectors)))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to generate candidates for winning post: %w", err)
|
||||
}
|
||||
|
||||
si := mbi.Sectors[sx[0]]
|
||||
postInp := []abi.SectorInfo{si.Info.AsSectorInfo()}
|
||||
postProof, err := m.epp.ComputeProof(ctx, postInp, prand)
|
||||
postProof, err := m.epp.ComputeProof(ctx, mbi.Sectors, prand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to compute winning post proof: %w", err)
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/filecoin-project/lotus/chain/wallet"
|
||||
"github.com/filecoin-project/lotus/lib/bufbstore"
|
||||
"github.com/filecoin-project/lotus/node/modules/dtypes"
|
||||
"github.com/filecoin-project/sector-storage/ffiwrapper"
|
||||
"github.com/filecoin-project/specs-actors/actors/abi"
|
||||
"github.com/filecoin-project/specs-actors/actors/abi/big"
|
||||
"github.com/filecoin-project/specs-actors/actors/builtin"
|
||||
@ -40,8 +41,9 @@ type StateAPI struct {
|
||||
// API attached to the state API. It probably should live somewhere better
|
||||
Wallet *wallet.Wallet
|
||||
|
||||
StateManager *stmgr.StateManager
|
||||
Chain *store.ChainStore
|
||||
ProofVerifier ffiwrapper.Verifier
|
||||
StateManager *stmgr.StateManager
|
||||
Chain *store.ChainStore
|
||||
}
|
||||
|
||||
func (a *StateAPI) StateNetworkName(ctx context.Context) (dtypes.NetworkName, error) {
|
||||
@ -252,8 +254,8 @@ func (a *StateAPI) StateReadState(ctx context.Context, act *types.Actor, tsk typ
|
||||
}
|
||||
|
||||
// This is on StateAPI because miner.Miner requires this, and MinerAPI requires miner.Miner
|
||||
func (a *StateAPI) MinerGetBaseInfo(ctx context.Context, maddr address.Address, tsk types.TipSetKey) (*api.MiningBaseInfo, error) {
|
||||
return stmgr.MinerGetBaseInfo(ctx, a.StateManager, tsk, maddr)
|
||||
func (a *StateAPI) MinerGetBaseInfo(ctx context.Context, maddr address.Address, epoch abi.ChainEpoch, tsk types.TipSetKey) (*api.MiningBaseInfo, error) {
|
||||
return stmgr.MinerGetBaseInfo(ctx, a.StateManager, tsk, epoch, maddr, a.ProofVerifier)
|
||||
}
|
||||
|
||||
func (a *StateAPI) MinerCreateBlock(ctx context.Context, bt *api.BlockTemplate) (*types.BlockMsg, error) {
|
||||
|
Loading…
Reference in New Issue
Block a user