2019-09-06 06:26:02 +00:00
|
|
|
package stmgr
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2019-10-08 05:51:34 +00:00
|
|
|
"fmt"
|
2019-09-06 20:03:28 +00:00
|
|
|
"sync"
|
2019-09-06 06:26:02 +00:00
|
|
|
|
2020-08-18 01:54:49 +00:00
|
|
|
"github.com/filecoin-project/specs-actors/actors/builtin/power"
|
|
|
|
|
2020-08-06 17:09:03 +00:00
|
|
|
"github.com/filecoin-project/specs-actors/actors/builtin/multisig"
|
|
|
|
|
2019-12-19 20:13:17 +00:00
|
|
|
"github.com/filecoin-project/go-address"
|
2020-02-08 02:18:32 +00:00
|
|
|
"github.com/filecoin-project/lotus/api"
|
2020-07-17 17:49:55 +00:00
|
|
|
"github.com/filecoin-project/lotus/build"
|
2020-03-04 21:21:24 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/actors"
|
2019-10-18 04:47:41 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/state"
|
|
|
|
"github.com/filecoin-project/lotus/chain/store"
|
|
|
|
"github.com/filecoin-project/lotus/chain/types"
|
|
|
|
"github.com/filecoin-project/lotus/chain/vm"
|
2020-07-23 02:05:11 +00:00
|
|
|
|
2020-09-07 03:49:10 +00:00
|
|
|
"github.com/filecoin-project/go-state-types/abi"
|
|
|
|
"github.com/filecoin-project/go-state-types/big"
|
2020-02-11 01:43:26 +00:00
|
|
|
"github.com/filecoin-project/specs-actors/actors/builtin"
|
2020-02-25 20:35:15 +00:00
|
|
|
"github.com/filecoin-project/specs-actors/actors/builtin/market"
|
2020-03-04 21:21:24 +00:00
|
|
|
"github.com/filecoin-project/specs-actors/actors/builtin/reward"
|
2020-02-08 02:18:32 +00:00
|
|
|
"github.com/filecoin-project/specs-actors/actors/util/adt"
|
2020-07-23 02:05:11 +00:00
|
|
|
|
2019-09-06 20:03:28 +00:00
|
|
|
"golang.org/x/xerrors"
|
2019-09-06 06:26:02 +00:00
|
|
|
|
|
|
|
"github.com/ipfs/go-cid"
|
2020-02-04 22:19:05 +00:00
|
|
|
cbor "github.com/ipfs/go-ipld-cbor"
|
2020-01-08 19:10:57 +00:00
|
|
|
logging "github.com/ipfs/go-log/v2"
|
2020-07-23 02:05:11 +00:00
|
|
|
cbg "github.com/whyrusleeping/cbor-gen"
|
2019-10-10 11:13:26 +00:00
|
|
|
"go.opencensus.io/trace"
|
2019-09-06 06:26:02 +00:00
|
|
|
)
|
|
|
|
|
2019-09-09 16:18:27 +00:00
|
|
|
var log = logging.Logger("statemgr")
|
2019-09-06 06:26:02 +00:00
|
|
|
|
|
|
|
type StateManager struct {
|
|
|
|
cs *store.ChainStore
|
2019-09-06 20:03:28 +00:00
|
|
|
|
2020-07-28 21:36:59 +00:00
|
|
|
stCache map[string][]cid.Cid
|
|
|
|
compWait map[string]chan struct{}
|
|
|
|
stlk sync.Mutex
|
|
|
|
genesisMsigLk sync.Mutex
|
2020-08-06 17:09:03 +00:00
|
|
|
newVM func(*vm.VMOpts) (*vm.VM, error)
|
2020-08-09 23:31:00 +00:00
|
|
|
genInfo *genesisInfo
|
2019-09-06 06:26:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewStateManager(cs *store.ChainStore) *StateManager {
|
2019-09-06 20:03:28 +00:00
|
|
|
return &StateManager{
|
2020-01-31 23:51:15 +00:00
|
|
|
newVM: vm.NewVM,
|
2019-12-08 17:07:44 +00:00
|
|
|
cs: cs,
|
|
|
|
stCache: make(map[string][]cid.Cid),
|
|
|
|
compWait: make(map[string]chan struct{}),
|
2019-09-06 20:03:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func cidsToKey(cids []cid.Cid) string {
|
|
|
|
var out string
|
|
|
|
for _, c := range cids {
|
|
|
|
out += c.KeyString()
|
|
|
|
}
|
|
|
|
return out
|
2019-09-06 06:26:02 +00:00
|
|
|
}
|
|
|
|
|
2019-12-08 17:07:44 +00:00
|
|
|
func (sm *StateManager) TipSetState(ctx context.Context, ts *types.TipSet) (st cid.Cid, rec cid.Cid, err error) {
|
2019-10-12 09:44:56 +00:00
|
|
|
ctx, span := trace.StartSpan(ctx, "tipSetState")
|
|
|
|
defer span.End()
|
2019-11-13 06:44:29 +00:00
|
|
|
if span.IsRecordingEvents() {
|
|
|
|
span.AddAttributes(trace.StringAttribute("tipset", fmt.Sprint(ts.Cids())))
|
|
|
|
}
|
2019-09-19 20:25:18 +00:00
|
|
|
|
2019-10-02 20:03:27 +00:00
|
|
|
ck := cidsToKey(ts.Cids())
|
2019-09-06 20:03:28 +00:00
|
|
|
sm.stlk.Lock()
|
2019-12-08 17:07:44 +00:00
|
|
|
cw, cwok := sm.compWait[ck]
|
|
|
|
if cwok {
|
|
|
|
sm.stlk.Unlock()
|
|
|
|
span.AddAttributes(trace.BoolAttribute("waited", true))
|
|
|
|
select {
|
|
|
|
case <-cw:
|
|
|
|
sm.stlk.Lock()
|
|
|
|
case <-ctx.Done():
|
|
|
|
return cid.Undef, cid.Undef, ctx.Err()
|
|
|
|
}
|
|
|
|
}
|
2019-09-06 20:03:28 +00:00
|
|
|
cached, ok := sm.stCache[ck]
|
|
|
|
if ok {
|
2019-12-08 17:07:44 +00:00
|
|
|
sm.stlk.Unlock()
|
2019-10-13 01:05:43 +00:00
|
|
|
span.AddAttributes(trace.BoolAttribute("cache", true))
|
2019-09-27 23:55:15 +00:00
|
|
|
return cached[0], cached[1], nil
|
2019-09-06 20:03:28 +00:00
|
|
|
}
|
2019-12-08 17:07:44 +00:00
|
|
|
ch := make(chan struct{})
|
|
|
|
sm.compWait[ck] = ch
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
sm.stlk.Lock()
|
|
|
|
delete(sm.compWait, ck)
|
|
|
|
if st != cid.Undef {
|
|
|
|
sm.stCache[ck] = []cid.Cid{st, rec}
|
|
|
|
}
|
|
|
|
sm.stlk.Unlock()
|
|
|
|
close(ch)
|
|
|
|
}()
|
|
|
|
|
|
|
|
sm.stlk.Unlock()
|
2019-09-06 20:03:28 +00:00
|
|
|
|
2019-10-02 18:00:08 +00:00
|
|
|
if ts.Height() == 0 {
|
2019-10-02 20:27:41 +00:00
|
|
|
// NB: This is here because the process that executes blocks requires that the
|
|
|
|
// block miner reference a valid miner in the state tree. Unless we create some
|
|
|
|
// magical genesis miner, this won't work properly, so we short circuit here
|
|
|
|
// This avoids the question of 'who gets paid the genesis block reward'
|
2019-10-02 18:00:08 +00:00
|
|
|
return ts.Blocks()[0].ParentStateRoot, ts.Blocks()[0].ParentMessageReceipts, nil
|
|
|
|
}
|
|
|
|
|
2020-08-09 01:37:49 +00:00
|
|
|
st, rec, err = sm.computeTipSetState(ctx, ts, nil)
|
2019-09-06 20:03:28 +00:00
|
|
|
if err != nil {
|
2019-09-27 23:55:15 +00:00
|
|
|
return cid.Undef, cid.Undef, err
|
2019-09-06 20:03:28 +00:00
|
|
|
}
|
|
|
|
|
2019-09-27 23:55:15 +00:00
|
|
|
return st, rec, nil
|
2019-09-06 20:03:28 +00:00
|
|
|
}
|
|
|
|
|
2020-03-08 02:31:36 +00:00
|
|
|
func (sm *StateManager) ExecutionTrace(ctx context.Context, ts *types.TipSet) (cid.Cid, []*api.InvocResult, error) {
|
|
|
|
var trace []*api.InvocResult
|
2020-08-09 01:37:49 +00:00
|
|
|
st, _, err := sm.computeTipSetState(ctx, ts, func(mcid cid.Cid, msg *types.Message, ret *vm.ApplyRet) error {
|
2020-03-08 02:31:36 +00:00
|
|
|
ir := &api.InvocResult{
|
2020-06-11 00:47:28 +00:00
|
|
|
Msg: msg,
|
|
|
|
MsgRct: &ret.MessageReceipt,
|
|
|
|
ExecutionTrace: ret.ExecutionTrace,
|
|
|
|
Duration: ret.Duration,
|
2020-03-08 02:31:36 +00:00
|
|
|
}
|
|
|
|
if ret.ActorErr != nil {
|
|
|
|
ir.Error = ret.ActorErr.Error()
|
|
|
|
}
|
|
|
|
trace = append(trace, ir)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return cid.Undef, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return st, trace, nil
|
|
|
|
}
|
|
|
|
|
2020-03-03 01:24:07 +00:00
|
|
|
type ExecCallback func(cid.Cid, *types.Message, *vm.ApplyRet) error
|
2019-09-20 02:54:52 +00:00
|
|
|
|
2020-08-09 01:37:49 +00:00
|
|
|
func (sm *StateManager) ApplyBlocks(ctx context.Context, parentEpoch abi.ChainEpoch, pstate cid.Cid, bms []store.BlockMessages, epoch abi.ChainEpoch, r vm.Rand, cb ExecCallback, baseFee abi.TokenAmount) (cid.Cid, cid.Cid, error) {
|
2020-08-06 17:09:03 +00:00
|
|
|
|
|
|
|
vmopt := &vm.VMOpts{
|
2020-08-09 22:49:38 +00:00
|
|
|
StateBase: pstate,
|
|
|
|
Epoch: epoch,
|
|
|
|
Rand: r,
|
|
|
|
Bstore: sm.cs.Blockstore(),
|
|
|
|
Syscalls: sm.cs.VMSys(),
|
|
|
|
CircSupplyCalc: sm.GetCirculatingSupply,
|
|
|
|
BaseFee: baseFee,
|
2020-08-06 17:09:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
vmi, err := sm.newVM(vmopt)
|
2019-09-06 06:26:02 +00:00
|
|
|
if err != nil {
|
2019-09-27 23:55:15 +00:00
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("instantiating VM failed: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-07-28 00:28:22 +00:00
|
|
|
runCron := func() error {
|
2020-07-28 00:25:04 +00:00
|
|
|
// TODO: this nonce-getting is a tiny bit ugly
|
|
|
|
ca, err := vmi.StateTree().GetActor(builtin.SystemActorAddr)
|
|
|
|
if err != nil {
|
2020-07-28 00:28:22 +00:00
|
|
|
return err
|
2020-07-28 00:25:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cronMsg := &types.Message{
|
2020-08-06 21:08:42 +00:00
|
|
|
To: builtin.CronActorAddr,
|
|
|
|
From: builtin.SystemActorAddr,
|
|
|
|
Nonce: ca.Nonce,
|
|
|
|
Value: types.NewInt(0),
|
|
|
|
GasFeeCap: types.NewInt(0),
|
|
|
|
GasPremium: types.NewInt(0),
|
|
|
|
GasLimit: build.BlockGasLimit * 10000, // Make super sure this is never too little
|
|
|
|
Method: builtin.MethodsCron.EpochTick,
|
|
|
|
Params: nil,
|
2020-07-28 00:25:04 +00:00
|
|
|
}
|
|
|
|
ret, err := vmi.ApplyImplicitMessage(ctx, cronMsg)
|
|
|
|
if err != nil {
|
2020-07-28 00:28:22 +00:00
|
|
|
return err
|
2020-07-28 00:25:04 +00:00
|
|
|
}
|
|
|
|
if cb != nil {
|
|
|
|
if err := cb(cronMsg.Cid(), cronMsg, ret); err != nil {
|
2020-07-28 00:28:22 +00:00
|
|
|
return xerrors.Errorf("callback failed on cron message: %w", err)
|
2020-07-28 00:25:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if ret.ExitCode != 0 {
|
2020-07-28 00:28:22 +00:00
|
|
|
return xerrors.Errorf("CheckProofSubmissions exit was non-zero: %d", ret.ExitCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-28 12:31:13 +00:00
|
|
|
for i := parentEpoch; i < epoch; i++ {
|
|
|
|
// handle state forks
|
|
|
|
err = sm.handleStateForks(ctx, vmi.StateTree(), i)
|
|
|
|
if err != nil {
|
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("error handling state forks: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if i > parentEpoch {
|
|
|
|
// run cron for null rounds if any
|
|
|
|
if err := runCron(); err != nil {
|
|
|
|
return cid.Cid{}, cid.Cid{}, err
|
|
|
|
}
|
2020-07-28 00:25:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
vmi.SetBlockHeight(i + 1)
|
|
|
|
}
|
|
|
|
|
2019-09-27 23:55:15 +00:00
|
|
|
var receipts []cbg.CBORMarshaler
|
2020-03-25 10:48:17 +00:00
|
|
|
processedMsgs := map[cid.Cid]bool{}
|
2020-03-03 01:24:07 +00:00
|
|
|
for _, b := range bms {
|
2020-03-04 21:21:24 +00:00
|
|
|
penalty := types.NewInt(0)
|
2020-03-24 06:50:20 +00:00
|
|
|
gasReward := big.Zero()
|
2019-09-06 06:26:02 +00:00
|
|
|
|
2020-03-04 21:21:24 +00:00
|
|
|
for _, cm := range append(b.BlsMessages, b.SecpkMessages...) {
|
2019-09-27 23:55:15 +00:00
|
|
|
m := cm.VMMessage()
|
2020-03-25 10:48:17 +00:00
|
|
|
if _, found := processedMsgs[m.Cid()]; found {
|
|
|
|
continue
|
|
|
|
}
|
2020-03-25 19:13:09 +00:00
|
|
|
r, err := vmi.ApplyMessage(ctx, cm)
|
2019-09-06 06:26:02 +00:00
|
|
|
if err != nil {
|
2019-09-27 23:55:15 +00:00
|
|
|
return cid.Undef, cid.Undef, err
|
2019-09-06 06:26:02 +00:00
|
|
|
}
|
2019-09-19 20:25:18 +00:00
|
|
|
|
2019-09-27 23:55:15 +00:00
|
|
|
receipts = append(receipts, &r.MessageReceipt)
|
2020-08-06 19:05:16 +00:00
|
|
|
gasReward = big.Add(gasReward, r.MinerTip)
|
2020-03-25 08:46:42 +00:00
|
|
|
penalty = big.Add(penalty, r.Penalty)
|
2019-09-27 23:55:15 +00:00
|
|
|
|
2019-09-19 20:25:18 +00:00
|
|
|
if cb != nil {
|
2019-09-27 23:55:15 +00:00
|
|
|
if err := cb(cm.Cid(), m, r); err != nil {
|
|
|
|
return cid.Undef, cid.Undef, err
|
2019-09-19 20:25:18 +00:00
|
|
|
}
|
|
|
|
}
|
2020-03-25 10:48:17 +00:00
|
|
|
processedMsgs[m.Cid()] = true
|
2019-09-06 06:26:02 +00:00
|
|
|
}
|
2020-03-04 21:21:24 +00:00
|
|
|
|
2020-03-05 21:02:33 +00:00
|
|
|
var err error
|
2020-03-04 21:21:24 +00:00
|
|
|
params, err := actors.SerializeParams(&reward.AwardBlockRewardParams{
|
2020-06-15 16:30:49 +00:00
|
|
|
Miner: b.Miner,
|
|
|
|
Penalty: penalty,
|
|
|
|
GasReward: gasReward,
|
2020-06-26 18:27:23 +00:00
|
|
|
WinCount: b.WinCount,
|
2020-03-04 21:21:24 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("failed to serialize award params: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
sysAct, err := vmi.StateTree().GetActor(builtin.SystemActorAddr)
|
|
|
|
if err != nil {
|
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("failed to get system actor: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-03-08 02:31:36 +00:00
|
|
|
rwMsg := &types.Message{
|
2020-08-06 21:08:42 +00:00
|
|
|
From: builtin.SystemActorAddr,
|
|
|
|
To: builtin.RewardActorAddr,
|
|
|
|
Nonce: sysAct.Nonce,
|
|
|
|
Value: types.NewInt(0),
|
|
|
|
GasFeeCap: types.NewInt(0),
|
|
|
|
GasPremium: types.NewInt(0),
|
|
|
|
GasLimit: 1 << 30,
|
|
|
|
Method: builtin.MethodsReward.AwardBlockReward,
|
|
|
|
Params: params,
|
2020-03-08 02:31:36 +00:00
|
|
|
}
|
2020-03-25 08:46:42 +00:00
|
|
|
ret, err := vmi.ApplyImplicitMessage(ctx, rwMsg)
|
2020-03-04 21:21:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("failed to apply reward message for miner %s: %w", b.Miner, err)
|
|
|
|
}
|
2020-03-08 02:31:36 +00:00
|
|
|
if cb != nil {
|
|
|
|
if err := cb(rwMsg.Cid(), rwMsg, ret); err != nil {
|
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("callback failed on reward message: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-04 21:21:24 +00:00
|
|
|
if ret.ExitCode != 0 {
|
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("reward application message failed (exit %d): %s", ret.ExitCode, ret.ActorErr)
|
|
|
|
}
|
2019-11-14 00:02:24 +00:00
|
|
|
}
|
|
|
|
|
2020-07-28 00:28:22 +00:00
|
|
|
if err := runCron(); err != nil {
|
|
|
|
return cid.Cid{}, cid.Cid{}, err
|
2019-11-13 23:14:02 +00:00
|
|
|
}
|
|
|
|
|
2020-07-23 00:14:54 +00:00
|
|
|
rectarr := adt.MakeEmptyArray(sm.cs.Store(ctx))
|
|
|
|
for i, receipt := range receipts {
|
|
|
|
if err := rectarr.Set(uint64(i), receipt); err != nil {
|
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("failed to build receipts amt: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
rectroot, err := rectarr.Root()
|
2019-09-27 23:55:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("failed to build receipts amt: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
st, err := vmi.Flush(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("vm flush failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return st, rectroot, nil
|
2020-03-03 01:24:07 +00:00
|
|
|
}
|
|
|
|
|
2020-08-09 01:37:49 +00:00
|
|
|
func (sm *StateManager) computeTipSetState(ctx context.Context, ts *types.TipSet, cb ExecCallback) (cid.Cid, cid.Cid, error) {
|
2020-03-03 01:24:07 +00:00
|
|
|
ctx, span := trace.StartSpan(ctx, "computeTipSetState")
|
|
|
|
defer span.End()
|
|
|
|
|
2020-08-09 01:37:49 +00:00
|
|
|
blks := ts.Blocks()
|
|
|
|
|
2020-03-03 01:24:07 +00:00
|
|
|
for i := 0; i < len(blks); i++ {
|
|
|
|
for j := i + 1; j < len(blks); j++ {
|
|
|
|
if blks[i].Miner == blks[j].Miner {
|
|
|
|
return cid.Undef, cid.Undef,
|
|
|
|
xerrors.Errorf("duplicate miner in a tipset (%s %s)",
|
|
|
|
blks[i].Miner, blks[j].Miner)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-28 00:25:04 +00:00
|
|
|
var parentEpoch abi.ChainEpoch
|
2020-03-03 01:24:07 +00:00
|
|
|
pstate := blks[0].ParentStateRoot
|
2020-08-18 01:54:49 +00:00
|
|
|
if blks[0].Height > 0 {
|
2020-03-03 01:24:07 +00:00
|
|
|
parent, err := sm.cs.GetBlock(blks[0].Parents[0])
|
|
|
|
if err != nil {
|
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("getting parent block: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-07-28 00:25:04 +00:00
|
|
|
parentEpoch = parent.Height
|
2020-03-03 01:24:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cids := make([]cid.Cid, len(blks))
|
|
|
|
for i, v := range blks {
|
|
|
|
cids[i] = v.Cid()
|
|
|
|
}
|
|
|
|
|
2020-09-01 19:48:16 +00:00
|
|
|
r := store.NewChainRand(sm.cs, cids)
|
2020-03-03 01:24:07 +00:00
|
|
|
|
2020-08-09 01:37:49 +00:00
|
|
|
blkmsgs, err := sm.cs.BlockMsgsForTipset(ts)
|
|
|
|
if err != nil {
|
|
|
|
return cid.Undef, cid.Undef, xerrors.Errorf("getting block messages for tipset: %w", err)
|
2020-03-03 01:24:07 +00:00
|
|
|
}
|
2020-08-09 01:37:49 +00:00
|
|
|
|
2020-08-06 17:09:03 +00:00
|
|
|
baseFee := blks[0].ParentBaseFee
|
2020-03-03 01:24:07 +00:00
|
|
|
|
2020-08-06 17:09:03 +00:00
|
|
|
return sm.ApplyBlocks(ctx, parentEpoch, pstate, blkmsgs, blks[0].Height, r, cb, baseFee)
|
2019-09-06 06:26:02 +00:00
|
|
|
}
|
|
|
|
|
2020-02-11 02:33:27 +00:00
|
|
|
func (sm *StateManager) parentState(ts *types.TipSet) cid.Cid {
|
2019-09-17 22:43:47 +00:00
|
|
|
if ts == nil {
|
|
|
|
ts = sm.cs.GetHeaviestTipSet()
|
|
|
|
}
|
|
|
|
|
2020-02-11 02:33:27 +00:00
|
|
|
return ts.ParentState()
|
|
|
|
}
|
|
|
|
|
2019-09-06 06:26:02 +00:00
|
|
|
func (sm *StateManager) ChainStore() *store.ChainStore {
|
|
|
|
return sm.cs
|
|
|
|
}
|
|
|
|
|
2020-06-02 16:38:41 +00:00
|
|
|
// ResolveToKeyAddress is similar to `vm.ResolveToKeyAddr` but does not allow `Actor` type of addresses.
|
2020-06-02 14:29:39 +00:00
|
|
|
// Uses the `TipSet` `ts` to generate the VM state.
|
2019-10-09 02:58:49 +00:00
|
|
|
func (sm *StateManager) ResolveToKeyAddress(ctx context.Context, addr address.Address, ts *types.TipSet) (address.Address, error) {
|
|
|
|
switch addr.Protocol() {
|
|
|
|
case address.BLS, address.SECP256K1:
|
|
|
|
return addr, nil
|
|
|
|
case address.Actor:
|
|
|
|
return address.Undef, xerrors.New("cannot resolve actor address to key address")
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
if ts == nil {
|
|
|
|
ts = sm.cs.GetHeaviestTipSet()
|
|
|
|
}
|
|
|
|
|
2019-10-10 11:13:26 +00:00
|
|
|
st, _, err := sm.TipSetState(ctx, ts)
|
2019-10-09 02:58:49 +00:00
|
|
|
if err != nil {
|
|
|
|
return address.Undef, xerrors.Errorf("resolve address failed to get tipset state: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-02-04 22:19:05 +00:00
|
|
|
cst := cbor.NewCborStore(sm.cs.Blockstore())
|
2019-10-09 02:58:49 +00:00
|
|
|
tree, err := state.LoadStateTree(cst, st)
|
|
|
|
if err != nil {
|
|
|
|
return address.Undef, xerrors.Errorf("failed to load state tree")
|
|
|
|
}
|
|
|
|
|
|
|
|
return vm.ResolveToKeyAddr(tree, cst, addr)
|
|
|
|
}
|
|
|
|
|
2020-07-26 01:38:18 +00:00
|
|
|
func (sm *StateManager) GetBlsPublicKey(ctx context.Context, addr address.Address, ts *types.TipSet) (pubk []byte, err error) {
|
2019-10-09 02:58:49 +00:00
|
|
|
kaddr, err := sm.ResolveToKeyAddress(ctx, addr, ts)
|
|
|
|
if err != nil {
|
|
|
|
return pubk, xerrors.Errorf("failed to resolve address to key address: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if kaddr.Protocol() != address.BLS {
|
|
|
|
return pubk, xerrors.Errorf("address must be BLS address to load bls public key")
|
|
|
|
}
|
|
|
|
|
2020-07-26 01:38:18 +00:00
|
|
|
return kaddr.Payload(), nil
|
2019-10-09 02:58:49 +00:00
|
|
|
}
|
2019-10-08 05:51:34 +00:00
|
|
|
|
2020-02-23 15:50:36 +00:00
|
|
|
func (sm *StateManager) LookupID(ctx context.Context, addr address.Address, ts *types.TipSet) (address.Address, error) {
|
|
|
|
cst := cbor.NewCborStore(sm.cs.Blockstore())
|
|
|
|
state, err := state.LoadStateTree(cst, sm.parentState(ts))
|
|
|
|
if err != nil {
|
|
|
|
return address.Undef, xerrors.Errorf("load state tree: %w", err)
|
|
|
|
}
|
|
|
|
return state.LookupID(addr)
|
|
|
|
}
|
|
|
|
|
2019-11-19 21:27:25 +00:00
|
|
|
func (sm *StateManager) GetReceipt(ctx context.Context, msg cid.Cid, ts *types.TipSet) (*types.MessageReceipt, error) {
|
2019-11-24 19:16:18 +00:00
|
|
|
m, err := sm.cs.GetCMessage(msg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to load message: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-08-10 12:55:27 +00:00
|
|
|
r, _, err := sm.tipsetExecutedMessage(ts, msg, m.VMMessage())
|
2019-11-19 21:27:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if r != nil {
|
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
|
2020-08-10 12:55:27 +00:00
|
|
|
_, r, _, err = sm.searchBackForMsg(ctx, ts, m)
|
2019-11-19 21:27:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to look back through chain for message: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
|
2020-06-03 19:38:37 +00:00
|
|
|
// WaitForMessage blocks until a message appears on chain. It looks backwards in the chain to see if this has already
|
|
|
|
// happened. It guarantees that the message has been on chain for at least confidence epochs without being reverted
|
|
|
|
// before returning.
|
2020-08-10 12:55:27 +00:00
|
|
|
func (sm *StateManager) WaitForMessage(ctx context.Context, mcid cid.Cid, confidence uint64) (*types.TipSet, *types.MessageReceipt, cid.Cid, error) {
|
2019-10-08 05:51:34 +00:00
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
msg, err := sm.cs.GetCMessage(mcid)
|
|
|
|
if err != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, fmt.Errorf("failed to load message: %w", err)
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
tsub := sm.cs.SubHeadChanges(ctx)
|
|
|
|
|
|
|
|
head, ok := <-tsub
|
|
|
|
if !ok {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, fmt.Errorf("SubHeadChanges stream was invalid")
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(head) != 1 {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, fmt.Errorf("SubHeadChanges first entry should have been one item")
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if head[0].Type != store.HCCurrent {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, fmt.Errorf("expected current head on SHC stream (got %s)", head[0].Type)
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
2020-08-10 12:55:27 +00:00
|
|
|
r, foundMsg, err := sm.tipsetExecutedMessage(head[0].Val, mcid, msg.VMMessage())
|
2019-10-08 05:51:34 +00:00
|
|
|
if err != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, err
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if r != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return head[0].Val, r, foundMsg, nil
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var backTs *types.TipSet
|
|
|
|
var backRcp *types.MessageReceipt
|
2020-08-10 12:55:27 +00:00
|
|
|
var backFm cid.Cid
|
2019-10-08 05:51:34 +00:00
|
|
|
backSearchWait := make(chan struct{})
|
|
|
|
go func() {
|
2020-08-10 12:55:27 +00:00
|
|
|
fts, r, foundMsg, err := sm.searchBackForMsg(ctx, head[0].Val, msg)
|
2019-10-08 05:51:34 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Warnf("failed to look back through chain for message: %w", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
backTs = fts
|
|
|
|
backRcp = r
|
2020-08-10 12:55:27 +00:00
|
|
|
backFm = foundMsg
|
2019-10-08 05:51:34 +00:00
|
|
|
close(backSearchWait)
|
|
|
|
}()
|
|
|
|
|
2020-06-03 19:38:37 +00:00
|
|
|
var candidateTs *types.TipSet
|
|
|
|
var candidateRcp *types.MessageReceipt
|
2020-08-10 12:55:27 +00:00
|
|
|
var candidateFm cid.Cid
|
2020-06-03 19:38:37 +00:00
|
|
|
heightOfHead := head[0].Val.Height()
|
|
|
|
reverts := map[types.TipSetKey]bool{}
|
|
|
|
|
2019-10-08 05:51:34 +00:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case notif, ok := <-tsub:
|
|
|
|
if !ok {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, ctx.Err()
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
for _, val := range notif {
|
|
|
|
switch val.Type {
|
|
|
|
case store.HCRevert:
|
2020-06-03 19:38:37 +00:00
|
|
|
if val.Val.Equals(candidateTs) {
|
|
|
|
candidateTs = nil
|
|
|
|
candidateRcp = nil
|
2020-08-10 12:55:27 +00:00
|
|
|
candidateFm = cid.Undef
|
2020-06-03 19:38:37 +00:00
|
|
|
}
|
|
|
|
if backSearchWait != nil {
|
|
|
|
reverts[val.Val.Key()] = true
|
|
|
|
}
|
2019-10-08 05:51:34 +00:00
|
|
|
case store.HCApply:
|
2020-06-04 02:30:09 +00:00
|
|
|
if candidateTs != nil && val.Val.Height() >= candidateTs.Height()+abi.ChainEpoch(confidence) {
|
2020-08-10 12:55:27 +00:00
|
|
|
return candidateTs, candidateRcp, candidateFm, nil
|
2020-06-03 19:38:37 +00:00
|
|
|
}
|
2020-08-10 12:55:27 +00:00
|
|
|
r, foundMsg, err := sm.tipsetExecutedMessage(val.Val, mcid, msg.VMMessage())
|
2019-10-08 05:51:34 +00:00
|
|
|
if err != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, err
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
if r != nil {
|
2020-06-03 19:38:37 +00:00
|
|
|
if confidence == 0 {
|
2020-08-10 12:55:27 +00:00
|
|
|
return val.Val, r, foundMsg, err
|
2020-06-03 19:38:37 +00:00
|
|
|
}
|
|
|
|
candidateTs = val.Val
|
|
|
|
candidateRcp = r
|
2020-08-10 12:55:27 +00:00
|
|
|
candidateFm = foundMsg
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
2020-06-03 19:38:37 +00:00
|
|
|
heightOfHead = val.Val.Height()
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
case <-backSearchWait:
|
2020-06-03 19:38:37 +00:00
|
|
|
// check if we found the message in the chain and that is hasn't been reverted since we started searching
|
|
|
|
if backTs != nil && !reverts[backTs.Key()] {
|
|
|
|
// if head is at or past confidence interval, return immediately
|
2020-06-04 02:30:09 +00:00
|
|
|
if heightOfHead >= backTs.Height()+abi.ChainEpoch(confidence) {
|
2020-08-10 12:55:27 +00:00
|
|
|
return backTs, backRcp, backFm, nil
|
2020-06-03 19:38:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// wait for confidence interval
|
|
|
|
candidateTs = backTs
|
|
|
|
candidateRcp = backRcp
|
2020-08-10 12:55:27 +00:00
|
|
|
candidateFm = backFm
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
2020-06-03 19:38:37 +00:00
|
|
|
reverts = nil
|
2019-10-08 05:51:34 +00:00
|
|
|
backSearchWait = nil
|
|
|
|
case <-ctx.Done():
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, ctx.Err()
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-10 12:55:27 +00:00
|
|
|
func (sm *StateManager) SearchForMessage(ctx context.Context, mcid cid.Cid) (*types.TipSet, *types.MessageReceipt, cid.Cid, error) {
|
2020-03-18 23:06:53 +00:00
|
|
|
msg, err := sm.cs.GetCMessage(mcid)
|
|
|
|
if err != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, fmt.Errorf("failed to load message: %w", err)
|
2020-03-18 23:06:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
head := sm.cs.GetHeaviestTipSet()
|
|
|
|
|
2020-08-10 12:55:27 +00:00
|
|
|
r, foundMsg, err := sm.tipsetExecutedMessage(head, mcid, msg.VMMessage())
|
2020-03-18 23:06:53 +00:00
|
|
|
if err != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, err
|
2020-03-18 23:06:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if r != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return head, r, foundMsg, nil
|
2020-03-18 23:06:53 +00:00
|
|
|
}
|
|
|
|
|
2020-08-10 12:55:27 +00:00
|
|
|
fts, r, foundMsg, err := sm.searchBackForMsg(ctx, head, msg)
|
2020-03-18 23:06:53 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Warnf("failed to look back through chain for message %s", mcid)
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, err
|
2020-03-18 23:06:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if fts == nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, nil
|
2020-03-18 23:06:53 +00:00
|
|
|
}
|
|
|
|
|
2020-08-10 12:55:27 +00:00
|
|
|
return fts, r, foundMsg, nil
|
2020-03-18 23:06:53 +00:00
|
|
|
}
|
|
|
|
|
2020-08-10 12:55:27 +00:00
|
|
|
func (sm *StateManager) searchBackForMsg(ctx context.Context, from *types.TipSet, m types.ChainMsg) (*types.TipSet, *types.MessageReceipt, cid.Cid, error) {
|
2019-10-08 05:51:34 +00:00
|
|
|
|
|
|
|
cur := from
|
|
|
|
for {
|
2019-10-09 08:56:47 +00:00
|
|
|
if cur.Height() == 0 {
|
|
|
|
// it ain't here!
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, nil
|
2019-10-09 08:56:47 +00:00
|
|
|
}
|
|
|
|
|
2019-10-08 05:51:34 +00:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, nil
|
2019-10-08 05:51:34 +00:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
2020-07-14 11:45:45 +00:00
|
|
|
var act types.Actor
|
|
|
|
err := sm.WithParentState(cur, sm.WithActor(m.VMMessage().From, GetActor(&act)))
|
2019-10-08 05:51:34 +00:00
|
|
|
if err != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, err
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
2020-06-03 18:08:05 +00:00
|
|
|
// we either have no messages from the sender, or the latest message we found has a lower nonce than the one being searched for,
|
|
|
|
// either way, no reason to lookback, it ain't there
|
|
|
|
if act.Nonce == 0 || act.Nonce < m.VMMessage().Nonce {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, nil
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ts, err := sm.cs.LoadTipSet(cur.Parents())
|
|
|
|
if err != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, fmt.Errorf("failed to load tipset during msg wait searchback: %w", err)
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
2020-08-10 12:55:27 +00:00
|
|
|
r, foundMsg, err := sm.tipsetExecutedMessage(ts, m.Cid(), m.VMMessage())
|
2019-10-08 05:51:34 +00:00
|
|
|
if err != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, nil, cid.Undef, fmt.Errorf("checking for message execution during lookback: %w", err)
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if r != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return ts, r, foundMsg, nil
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cur = ts
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-10 12:55:27 +00:00
|
|
|
func (sm *StateManager) tipsetExecutedMessage(ts *types.TipSet, msg cid.Cid, vmm *types.Message) (*types.MessageReceipt, cid.Cid, error) {
|
2019-10-08 05:51:34 +00:00
|
|
|
// The genesis block did not execute any messages
|
|
|
|
if ts.Height() == 0 {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, cid.Undef, nil
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pts, err := sm.cs.LoadTipSet(ts.Parents())
|
|
|
|
if err != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, cid.Undef, err
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cm, err := sm.cs.MessagesForTipset(pts)
|
|
|
|
if err != nil {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, cid.Undef, err
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
|
2019-11-24 22:42:51 +00:00
|
|
|
for ii := range cm {
|
|
|
|
// iterate in reverse because we going backwards through the chain
|
|
|
|
i := len(cm) - ii - 1
|
|
|
|
m := cm[i]
|
|
|
|
|
2019-11-24 19:16:18 +00:00
|
|
|
if m.VMMessage().From == vmm.From { // cheaper to just check origin first
|
|
|
|
if m.VMMessage().Nonce == vmm.Nonce {
|
2020-08-10 12:55:27 +00:00
|
|
|
if m.VMMessage().EqualCall(vmm) {
|
|
|
|
if m.Cid() != msg {
|
|
|
|
log.Warnw("found message with equal nonce and call params but different CID",
|
|
|
|
"wanted", msg, "found", m.Cid(), "nonce", vmm.Nonce, "from", vmm.From)
|
|
|
|
}
|
|
|
|
|
|
|
|
pr, err := sm.cs.GetParentReceipt(ts.Blocks()[0], i)
|
|
|
|
if err != nil {
|
|
|
|
return nil, cid.Undef, err
|
|
|
|
}
|
|
|
|
return pr, m.Cid(), nil
|
2019-11-24 19:16:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// this should be that message
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, cid.Undef, xerrors.Errorf("found message with equal nonce as the one we are looking for (F:%s n %d, TS: %s n%d)",
|
2019-11-24 19:16:18 +00:00
|
|
|
msg, vmm.Nonce, m.Cid(), m.VMMessage().Nonce)
|
|
|
|
}
|
|
|
|
if m.VMMessage().Nonce < vmm.Nonce {
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, cid.Undef, nil // don't bother looking further
|
2019-11-24 19:16:18 +00:00
|
|
|
}
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-10 12:55:27 +00:00
|
|
|
return nil, cid.Undef, nil
|
2019-10-08 05:51:34 +00:00
|
|
|
}
|
2019-10-12 06:45:48 +00:00
|
|
|
|
|
|
|
func (sm *StateManager) ListAllActors(ctx context.Context, ts *types.TipSet) ([]address.Address, error) {
|
|
|
|
if ts == nil {
|
2019-11-14 06:34:54 +00:00
|
|
|
ts = sm.cs.GetHeaviestTipSet()
|
2019-10-12 06:45:48 +00:00
|
|
|
}
|
|
|
|
st, _, err := sm.TipSetState(ctx, ts)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-07-23 00:14:54 +00:00
|
|
|
r, err := adt.AsMap(sm.cs.Store(ctx), st)
|
2019-10-12 06:45:48 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var out []address.Address
|
2020-07-23 00:14:54 +00:00
|
|
|
err = r.ForEach(nil, func(k string) error {
|
2019-10-12 06:45:48 +00:00
|
|
|
addr, err := address.NewFromBytes([]byte(k))
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("address in state tree was not valid: %w", err)
|
|
|
|
}
|
|
|
|
out = append(out, addr)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|
2019-10-23 17:39:14 +00:00
|
|
|
|
2020-02-08 02:18:32 +00:00
|
|
|
func (sm *StateManager) MarketBalance(ctx context.Context, addr address.Address, ts *types.TipSet) (api.MarketBalance, error) {
|
2020-02-25 20:35:15 +00:00
|
|
|
var state market.State
|
2020-02-25 20:54:58 +00:00
|
|
|
_, err := sm.LoadActorState(ctx, builtin.StorageMarketActorAddr, &state, ts)
|
2020-02-08 02:18:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return api.MarketBalance{}, err
|
2019-10-23 17:39:14 +00:00
|
|
|
}
|
2020-02-08 02:18:32 +00:00
|
|
|
|
2020-02-23 15:50:36 +00:00
|
|
|
addr, err = sm.LookupID(ctx, addr, ts)
|
|
|
|
if err != nil {
|
|
|
|
return api.MarketBalance{}, err
|
|
|
|
}
|
|
|
|
|
2020-02-08 02:18:32 +00:00
|
|
|
var out api.MarketBalance
|
2020-02-23 15:50:36 +00:00
|
|
|
|
2020-04-13 21:05:34 +00:00
|
|
|
et, err := adt.AsBalanceTable(sm.cs.Store(ctx), state.EscrowTable)
|
|
|
|
if err != nil {
|
|
|
|
return api.MarketBalance{}, err
|
|
|
|
}
|
2020-07-28 14:36:32 +00:00
|
|
|
out.Escrow, err = et.Get(addr)
|
2019-10-23 17:39:14 +00:00
|
|
|
if err != nil {
|
2020-07-28 14:36:32 +00:00
|
|
|
return api.MarketBalance{}, xerrors.Errorf("getting escrow balance: %w", err)
|
2019-10-23 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
2020-04-13 21:05:34 +00:00
|
|
|
lt, err := adt.AsBalanceTable(sm.cs.Store(ctx), state.LockedTable)
|
|
|
|
if err != nil {
|
|
|
|
return api.MarketBalance{}, err
|
|
|
|
}
|
2020-07-28 14:36:32 +00:00
|
|
|
out.Locked, err = lt.Get(addr)
|
2020-02-08 02:18:32 +00:00
|
|
|
if err != nil {
|
2020-07-28 14:36:32 +00:00
|
|
|
return api.MarketBalance{}, xerrors.Errorf("getting locked balance: %w", err)
|
2020-02-08 02:18:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
2019-10-23 17:39:14 +00:00
|
|
|
}
|
2020-01-21 01:53:55 +00:00
|
|
|
|
|
|
|
func (sm *StateManager) ValidateChain(ctx context.Context, ts *types.TipSet) error {
|
|
|
|
tschain := []*types.TipSet{ts}
|
|
|
|
for ts.Height() != 0 {
|
|
|
|
next, err := sm.cs.LoadTipSet(ts.Parents())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
tschain = append(tschain, next)
|
|
|
|
ts = next
|
|
|
|
}
|
|
|
|
|
|
|
|
lastState := tschain[len(tschain)-1].ParentState()
|
|
|
|
for i := len(tschain) - 1; i >= 0; i-- {
|
|
|
|
cur := tschain[i]
|
|
|
|
log.Infof("computing state (height: %d, ts=%s)", cur.Height(), cur.Cids())
|
|
|
|
if cur.ParentState() != lastState {
|
|
|
|
return xerrors.Errorf("tipset chain had state mismatch at height %d", cur.Height())
|
|
|
|
}
|
|
|
|
st, _, err := sm.TipSetState(ctx, cur)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
lastState = st
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2020-01-31 23:51:15 +00:00
|
|
|
|
2020-08-06 17:09:03 +00:00
|
|
|
func (sm *StateManager) SetVMConstructor(nvm func(*vm.VMOpts) (*vm.VM, error)) {
|
2020-01-31 23:51:15 +00:00
|
|
|
sm.newVM = nvm
|
|
|
|
}
|
2020-07-25 22:29:33 +00:00
|
|
|
|
2020-08-09 23:31:00 +00:00
|
|
|
type genesisInfo struct {
|
|
|
|
genesisMsigs []multisig.State
|
|
|
|
// info about the Accounts in the genesis state
|
|
|
|
genesisActors []genesisActor
|
|
|
|
genesisPledge abi.TokenAmount
|
|
|
|
genesisMarketFunds abi.TokenAmount
|
2020-07-25 22:29:33 +00:00
|
|
|
}
|
|
|
|
|
2020-08-09 21:45:45 +00:00
|
|
|
type genesisActor struct {
|
|
|
|
addr address.Address
|
|
|
|
initBal abi.TokenAmount
|
|
|
|
}
|
|
|
|
|
2020-08-18 03:44:56 +00:00
|
|
|
// sets up information about the actors in the genesis state
|
2020-08-09 21:45:45 +00:00
|
|
|
func (sm *StateManager) setupGenesisActors(ctx context.Context) error {
|
2020-08-09 23:31:00 +00:00
|
|
|
|
|
|
|
gi := genesisInfo{}
|
|
|
|
|
2020-07-25 22:29:33 +00:00
|
|
|
gb, err := sm.cs.GetGenesis()
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("getting genesis block: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
gts, err := types.NewTipSet([]*types.BlockHeader{gb})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("getting genesis tipset: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
st, _, err := sm.TipSetState(ctx, gts)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("getting genesis tipset state: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-08-09 21:45:45 +00:00
|
|
|
cst := cbor.NewCborStore(sm.cs.Blockstore())
|
|
|
|
sTree, err := state.LoadStateTree(cst, st)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("loading state tree: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-08-09 23:31:00 +00:00
|
|
|
gi.genesisMarketFunds, err = getFilMarketLocked(ctx, sTree)
|
2020-08-09 23:21:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("setting up genesis market funds: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-08-09 23:31:00 +00:00
|
|
|
gi.genesisPledge, err = getFilPowerLocked(ctx, sTree)
|
2020-08-09 23:21:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("setting up genesis pledge: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-07-25 22:29:33 +00:00
|
|
|
r, err := adt.AsMap(sm.cs.Store(ctx), st)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("getting genesis actors: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
totalsByEpoch := make(map[abi.ChainEpoch]abi.TokenAmount)
|
|
|
|
var act types.Actor
|
|
|
|
err = r.ForEach(&act, func(k string) error {
|
|
|
|
if act.Code == builtin.MultisigActorCodeID {
|
|
|
|
var s multisig.State
|
|
|
|
err := sm.cs.Store(ctx).Get(ctx, act.Head, &s)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if s.StartEpoch != 0 {
|
|
|
|
return xerrors.New("genesis multisig doesn't start vesting at epoch 0!")
|
|
|
|
}
|
|
|
|
|
|
|
|
ot, f := totalsByEpoch[s.UnlockDuration]
|
|
|
|
if f {
|
|
|
|
totalsByEpoch[s.UnlockDuration] = big.Add(ot, s.InitialBalance)
|
|
|
|
} else {
|
|
|
|
totalsByEpoch[s.UnlockDuration] = s.InitialBalance
|
|
|
|
}
|
|
|
|
|
2020-08-09 21:45:45 +00:00
|
|
|
} else if act.Code == builtin.AccountActorCodeID {
|
2020-08-18 10:01:48 +00:00
|
|
|
// should exclude burnt funds actor and "remainder account actor"
|
2020-08-12 19:24:30 +00:00
|
|
|
// should only ever be "faucet" accounts in testnets
|
2020-08-09 21:45:45 +00:00
|
|
|
kaddr, err := address.NewFromBytes([]byte(k))
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("decoding address: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-08-14 00:19:47 +00:00
|
|
|
if kaddr != builtin.BurntFundsActorAddr {
|
|
|
|
kid, err := sTree.LookupID(kaddr)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("resolving address: %w", err)
|
|
|
|
}
|
2020-08-09 21:45:45 +00:00
|
|
|
|
2020-08-14 00:19:47 +00:00
|
|
|
gi.genesisActors = append(gi.genesisActors, genesisActor{
|
|
|
|
addr: kid,
|
|
|
|
initBal: act.Balance,
|
|
|
|
})
|
|
|
|
}
|
2020-07-25 22:29:33 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
2020-08-09 21:45:45 +00:00
|
|
|
return xerrors.Errorf("error setting up genesis infos: %w", err)
|
2020-07-25 22:29:33 +00:00
|
|
|
}
|
|
|
|
|
2020-08-09 23:31:00 +00:00
|
|
|
gi.genesisMsigs = make([]multisig.State, 0, len(totalsByEpoch))
|
2020-07-25 22:29:33 +00:00
|
|
|
for k, v := range totalsByEpoch {
|
|
|
|
ns := multisig.State{
|
|
|
|
InitialBalance: v,
|
|
|
|
UnlockDuration: k,
|
|
|
|
PendingTxns: cid.Undef,
|
|
|
|
}
|
2020-08-09 23:31:00 +00:00
|
|
|
gi.genesisMsigs = append(gi.genesisMsigs, ns)
|
2020-07-25 22:29:33 +00:00
|
|
|
}
|
|
|
|
|
2020-08-09 23:31:00 +00:00
|
|
|
sm.genInfo = &gi
|
|
|
|
|
2020-07-25 22:29:33 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-18 03:44:56 +00:00
|
|
|
// sets up information about the actors in the genesis state
|
|
|
|
// For testnet we use a hardcoded set of multisig states, instead of what's actually in the genesis multisigs
|
|
|
|
// We also do not consider ANY account actors (including the faucet)
|
|
|
|
func (sm *StateManager) setupGenesisActorsTestnet(ctx context.Context) error {
|
|
|
|
|
|
|
|
gi := genesisInfo{}
|
|
|
|
|
|
|
|
gb, err := sm.cs.GetGenesis()
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("getting genesis block: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
gts, err := types.NewTipSet([]*types.BlockHeader{gb})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("getting genesis tipset: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
st, _, err := sm.TipSetState(ctx, gts)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("getting genesis tipset state: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
cst := cbor.NewCborStore(sm.cs.Blockstore())
|
|
|
|
sTree, err := state.LoadStateTree(cst, st)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("loading state tree: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
gi.genesisMarketFunds, err = getFilMarketLocked(ctx, sTree)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("setting up genesis market funds: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
gi.genesisPledge, err = getFilPowerLocked(ctx, sTree)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("setting up genesis pledge: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
totalsByEpoch := make(map[abi.ChainEpoch]abi.TokenAmount)
|
|
|
|
|
|
|
|
// 6 months
|
|
|
|
sixMonths := abi.ChainEpoch(183 * builtin.EpochsInDay)
|
|
|
|
totalsByEpoch[sixMonths] = big.NewInt(49_929_341)
|
|
|
|
totalsByEpoch[sixMonths] = big.Add(totalsByEpoch[sixMonths], big.NewInt(32_787_700))
|
|
|
|
|
|
|
|
// 1 year
|
|
|
|
oneYear := abi.ChainEpoch(365 * builtin.EpochsInDay)
|
|
|
|
totalsByEpoch[oneYear] = big.NewInt(22_421_712)
|
|
|
|
|
|
|
|
// 2 years
|
|
|
|
twoYears := abi.ChainEpoch(2 * 365 * builtin.EpochsInDay)
|
|
|
|
totalsByEpoch[twoYears] = big.NewInt(7_223_364)
|
|
|
|
|
|
|
|
// 3 years
|
|
|
|
threeYears := abi.ChainEpoch(3 * 365 * builtin.EpochsInDay)
|
|
|
|
totalsByEpoch[threeYears] = big.NewInt(87_637_883)
|
|
|
|
|
|
|
|
// 6 years
|
|
|
|
sixYears := abi.ChainEpoch(6 * 365 * builtin.EpochsInDay)
|
|
|
|
totalsByEpoch[sixYears] = big.NewInt(100_000_000)
|
|
|
|
totalsByEpoch[sixYears] = big.Add(totalsByEpoch[sixYears], big.NewInt(300_000_000))
|
|
|
|
|
|
|
|
gi.genesisMsigs = make([]multisig.State, 0, len(totalsByEpoch))
|
|
|
|
for k, v := range totalsByEpoch {
|
|
|
|
ns := multisig.State{
|
|
|
|
InitialBalance: v,
|
|
|
|
UnlockDuration: k,
|
|
|
|
PendingTxns: cid.Undef,
|
|
|
|
}
|
|
|
|
gi.genesisMsigs = append(gi.genesisMsigs, ns)
|
|
|
|
}
|
|
|
|
|
|
|
|
sm.genInfo = &gi
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-09 21:45:45 +00:00
|
|
|
// GetVestedFunds returns all funds that have "left" actors that are in the genesis state:
|
|
|
|
// - For Multisigs, it counts the actual amounts that have vested at the given epoch
|
|
|
|
// - For Accounts, it counts max(currentBalance - genesisBalance, 0).
|
2020-08-09 22:49:38 +00:00
|
|
|
func (sm *StateManager) GetFilVested(ctx context.Context, height abi.ChainEpoch, st *state.StateTree) (abi.TokenAmount, error) {
|
2020-07-25 22:29:33 +00:00
|
|
|
vf := big.Zero()
|
2020-08-09 23:31:00 +00:00
|
|
|
for _, v := range sm.genInfo.genesisMsigs {
|
2020-07-25 22:29:33 +00:00
|
|
|
au := big.Sub(v.InitialBalance, v.AmountLocked(height))
|
|
|
|
vf = big.Add(vf, au)
|
|
|
|
}
|
2020-08-09 21:45:45 +00:00
|
|
|
|
2020-08-18 03:44:56 +00:00
|
|
|
// there should not be any such accounts in testnet (and also none in mainnet?)
|
2020-08-09 23:31:00 +00:00
|
|
|
for _, v := range sm.genInfo.genesisActors {
|
2020-08-09 21:45:45 +00:00
|
|
|
act, err := st.GetActor(v.addr)
|
|
|
|
if err != nil {
|
|
|
|
return big.Zero(), xerrors.Errorf("failed to get actor: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
diff := big.Sub(v.initBal, act.Balance)
|
|
|
|
if diff.GreaterThan(big.Zero()) {
|
|
|
|
vf = big.Add(vf, diff)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-12 19:24:30 +00:00
|
|
|
vf = big.Add(vf, sm.genInfo.genesisPledge)
|
|
|
|
vf = big.Add(vf, sm.genInfo.genesisMarketFunds)
|
|
|
|
|
2020-07-25 22:29:33 +00:00
|
|
|
return vf, nil
|
|
|
|
}
|
2020-08-09 22:49:38 +00:00
|
|
|
|
|
|
|
func GetFilMined(ctx context.Context, st *state.StateTree) (abi.TokenAmount, error) {
|
2020-08-10 20:13:09 +00:00
|
|
|
ractor, err := st.GetActor(builtin.RewardActorAddr)
|
2020-08-09 22:49:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return big.Zero(), xerrors.Errorf("failed to load reward actor state: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-08-10 20:13:09 +00:00
|
|
|
var rst reward.State
|
|
|
|
if err := st.Store.Get(ctx, ractor.Head, &rst); err != nil {
|
|
|
|
return big.Zero(), xerrors.Errorf("failed to load reward state: %w", err)
|
2020-08-09 22:49:38 +00:00
|
|
|
}
|
2020-08-10 20:13:09 +00:00
|
|
|
|
|
|
|
return rst.TotalMined, nil
|
2020-08-09 22:49:38 +00:00
|
|
|
}
|
|
|
|
|
2020-08-09 23:21:21 +00:00
|
|
|
func getFilMarketLocked(ctx context.Context, st *state.StateTree) (abi.TokenAmount, error) {
|
2020-08-09 22:49:38 +00:00
|
|
|
mactor, err := st.GetActor(builtin.StorageMarketActorAddr)
|
|
|
|
if err != nil {
|
|
|
|
return big.Zero(), xerrors.Errorf("failed to load market actor: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var mst market.State
|
|
|
|
if err := st.Store.Get(ctx, mactor.Head, &mst); err != nil {
|
|
|
|
return big.Zero(), xerrors.Errorf("failed to load market state: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-08-09 23:21:21 +00:00
|
|
|
fml := types.BigAdd(mst.TotalClientLockedCollateral, mst.TotalProviderLockedCollateral)
|
|
|
|
fml = types.BigAdd(fml, mst.TotalClientStorageFee)
|
|
|
|
return fml, nil
|
|
|
|
}
|
2020-08-09 22:49:38 +00:00
|
|
|
|
2020-08-09 23:21:21 +00:00
|
|
|
func getFilPowerLocked(ctx context.Context, st *state.StateTree) (abi.TokenAmount, error) {
|
2020-08-09 22:49:38 +00:00
|
|
|
pactor, err := st.GetActor(builtin.StoragePowerActorAddr)
|
|
|
|
if err != nil {
|
|
|
|
return big.Zero(), xerrors.Errorf("failed to load power actor: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var pst power.State
|
|
|
|
if err := st.Store.Get(ctx, pactor.Head, &pst); err != nil {
|
|
|
|
return big.Zero(), xerrors.Errorf("failed to load power state: %w", err)
|
|
|
|
}
|
2020-08-09 23:21:21 +00:00
|
|
|
return pst.TotalPledgeCollateral, nil
|
|
|
|
}
|
2020-08-09 22:49:38 +00:00
|
|
|
|
2020-08-09 23:21:21 +00:00
|
|
|
func (sm *StateManager) GetFilLocked(ctx context.Context, st *state.StateTree) (abi.TokenAmount, error) {
|
|
|
|
|
|
|
|
filMarketLocked, err := getFilMarketLocked(ctx, st)
|
|
|
|
if err != nil {
|
|
|
|
return big.Zero(), xerrors.Errorf("failed to get filMarketLocked: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
filPowerLocked, err := getFilPowerLocked(ctx, st)
|
|
|
|
if err != nil {
|
|
|
|
return big.Zero(), xerrors.Errorf("failed to get filPowerLocked: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return types.BigAdd(filMarketLocked, filPowerLocked), nil
|
2020-08-09 22:49:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func GetFilBurnt(ctx context.Context, st *state.StateTree) (abi.TokenAmount, error) {
|
|
|
|
burnt, err := st.GetActor(builtin.BurntFundsActorAddr)
|
|
|
|
if err != nil {
|
|
|
|
return big.Zero(), xerrors.Errorf("failed to load burnt actor: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return burnt.Balance, nil
|
|
|
|
}
|
|
|
|
|
2020-08-14 20:44:33 +00:00
|
|
|
func (sm *StateManager) GetCirculatingSupplyDetailed(ctx context.Context, height abi.ChainEpoch, st *state.StateTree) (api.CirculatingSupply, error) {
|
2020-08-09 23:21:21 +00:00
|
|
|
sm.genesisMsigLk.Lock()
|
|
|
|
defer sm.genesisMsigLk.Unlock()
|
2020-08-09 23:31:00 +00:00
|
|
|
if sm.genInfo == nil {
|
2020-08-18 03:44:56 +00:00
|
|
|
err := sm.setupGenesisActorsTestnet(ctx)
|
2020-08-09 23:21:21 +00:00
|
|
|
if err != nil {
|
2020-08-14 20:44:33 +00:00
|
|
|
return api.CirculatingSupply{}, xerrors.Errorf("failed to setup genesis information: %w", err)
|
2020-08-09 23:21:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-09 22:49:38 +00:00
|
|
|
filVested, err := sm.GetFilVested(ctx, height, st)
|
|
|
|
if err != nil {
|
2020-08-14 20:44:33 +00:00
|
|
|
return api.CirculatingSupply{}, xerrors.Errorf("failed to calculate filVested: %w", err)
|
2020-08-09 22:49:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
filMined, err := GetFilMined(ctx, st)
|
|
|
|
if err != nil {
|
2020-08-14 20:44:33 +00:00
|
|
|
return api.CirculatingSupply{}, xerrors.Errorf("failed to calculate filMined: %w", err)
|
2020-08-09 22:49:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
filBurnt, err := GetFilBurnt(ctx, st)
|
|
|
|
if err != nil {
|
2020-08-14 20:44:33 +00:00
|
|
|
return api.CirculatingSupply{}, xerrors.Errorf("failed to calculate filBurnt: %w", err)
|
2020-08-09 22:49:38 +00:00
|
|
|
}
|
|
|
|
|
2020-08-09 23:21:21 +00:00
|
|
|
filLocked, err := sm.GetFilLocked(ctx, st)
|
2020-08-09 22:49:38 +00:00
|
|
|
if err != nil {
|
2020-08-14 20:44:33 +00:00
|
|
|
return api.CirculatingSupply{}, xerrors.Errorf("failed to calculate filLocked: %w", err)
|
2020-08-09 22:49:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ret := types.BigAdd(filVested, filMined)
|
|
|
|
ret = types.BigSub(ret, filBurnt)
|
|
|
|
ret = types.BigSub(ret, filLocked)
|
|
|
|
|
|
|
|
if ret.LessThan(big.Zero()) {
|
|
|
|
ret = big.Zero()
|
|
|
|
}
|
|
|
|
|
2020-08-14 20:44:33 +00:00
|
|
|
return api.CirculatingSupply{
|
|
|
|
FilVested: filVested,
|
|
|
|
FilMined: filMined,
|
|
|
|
FilBurnt: filBurnt,
|
|
|
|
FilLocked: filLocked,
|
|
|
|
FilCirculating: ret,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sm *StateManager) GetCirculatingSupply(ctx context.Context, height abi.ChainEpoch, st *state.StateTree) (abi.TokenAmount, error) {
|
|
|
|
csi, err := sm.GetCirculatingSupplyDetailed(ctx, height, st)
|
|
|
|
if err != nil {
|
|
|
|
return big.Zero(), err
|
|
|
|
}
|
|
|
|
|
|
|
|
return csi.FilCirculating, nil
|
2020-08-09 22:49:38 +00:00
|
|
|
}
|