2020-09-05 03:01:36 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2020-11-26 21:46:19 +00:00
|
|
|
"encoding/csv"
|
2021-03-19 03:30:22 +00:00
|
|
|
"encoding/json"
|
2020-09-05 03:01:36 +00:00
|
|
|
"fmt"
|
2020-11-01 13:03:21 +00:00
|
|
|
"io"
|
2020-11-26 21:46:19 +00:00
|
|
|
"os"
|
2021-03-19 03:30:22 +00:00
|
|
|
"runtime"
|
2020-09-24 21:30:11 +00:00
|
|
|
"strconv"
|
2020-11-26 21:46:19 +00:00
|
|
|
"strings"
|
2021-03-18 22:12:57 +00:00
|
|
|
"sync"
|
2020-11-26 21:46:19 +00:00
|
|
|
"time"
|
2020-09-24 21:30:11 +00:00
|
|
|
|
|
|
|
"github.com/docker/go-units"
|
2020-09-05 03:01:36 +00:00
|
|
|
"github.com/ipfs/go-cid"
|
|
|
|
cbor "github.com/ipfs/go-ipld-cbor"
|
2020-09-24 21:30:11 +00:00
|
|
|
logging "github.com/ipfs/go-log/v2"
|
2020-09-05 03:01:36 +00:00
|
|
|
"github.com/urfave/cli/v2"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
|
|
|
|
"github.com/filecoin-project/go-address"
|
|
|
|
"github.com/filecoin-project/go-state-types/abi"
|
|
|
|
"github.com/filecoin-project/go-state-types/big"
|
2020-11-01 13:03:21 +00:00
|
|
|
|
2022-06-14 15:00:51 +00:00
|
|
|
"github.com/filecoin-project/lotus/build"
|
2020-09-21 20:43:47 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/actors/adt"
|
2022-06-14 15:00:51 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/actors/builtin"
|
|
|
|
_init "github.com/filecoin-project/lotus/chain/actors/builtin/init"
|
2020-09-21 20:43:47 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/actors/builtin/miner"
|
2022-06-14 15:00:51 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/actors/builtin/multisig"
|
|
|
|
"github.com/filecoin-project/lotus/chain/actors/builtin/power"
|
|
|
|
"github.com/filecoin-project/lotus/chain/actors/builtin/reward"
|
2022-09-27 10:21:42 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/consensus"
|
2022-06-14 15:00:51 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/consensus/filcns"
|
|
|
|
"github.com/filecoin-project/lotus/chain/gen/genesis"
|
2020-09-05 03:01:36 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/state"
|
|
|
|
"github.com/filecoin-project/lotus/chain/stmgr"
|
|
|
|
"github.com/filecoin-project/lotus/chain/store"
|
|
|
|
"github.com/filecoin-project/lotus/chain/types"
|
|
|
|
"github.com/filecoin-project/lotus/chain/vm"
|
|
|
|
lcli "github.com/filecoin-project/lotus/cli"
|
|
|
|
"github.com/filecoin-project/lotus/node/repo"
|
2022-06-14 18:03:38 +00:00
|
|
|
"github.com/filecoin-project/lotus/storage/sealer/ffiwrapper"
|
2020-09-05 03:01:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type accountInfo struct {
|
2020-10-08 00:01:01 +00:00
|
|
|
Address address.Address
|
|
|
|
Balance types.FIL
|
|
|
|
Type string
|
|
|
|
Power abi.StoragePower
|
|
|
|
Worker address.Address
|
|
|
|
Owner address.Address
|
|
|
|
InitialPledge types.FIL
|
|
|
|
PreCommits types.FIL
|
|
|
|
LockedFunds types.FIL
|
|
|
|
Sectors uint64
|
|
|
|
VestingStart abi.ChainEpoch
|
|
|
|
VestingDuration abi.ChainEpoch
|
|
|
|
VestingAmount types.FIL
|
2020-09-05 03:01:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var auditsCmd = &cli.Command{
|
|
|
|
Name: "audits",
|
|
|
|
Description: "a collection of utilities for auditing the filecoin chain",
|
|
|
|
Subcommands: []*cli.Command{
|
|
|
|
chainBalanceCmd,
|
2021-04-22 23:45:35 +00:00
|
|
|
chainBalanceSanityCheckCmd,
|
2020-09-05 03:01:36 +00:00
|
|
|
chainBalanceStateCmd,
|
2020-09-24 21:30:11 +00:00
|
|
|
chainPledgeCmd,
|
2020-11-26 21:46:19 +00:00
|
|
|
fillBalancesCmd,
|
2021-03-18 22:12:57 +00:00
|
|
|
duplicatedMessagesCmd,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var duplicatedMessagesCmd = &cli.Command{
|
2021-03-19 03:30:22 +00:00
|
|
|
Name: "duplicate-messages",
|
|
|
|
Usage: "Check for duplicate messages included in a tipset.",
|
|
|
|
UsageText: `Check for duplicate messages included in a tipset.
|
|
|
|
|
|
|
|
Due to Filecoin's expected consensus, a tipset may include the same message multiple times in
|
|
|
|
different blocks. The message will only be executed once.
|
|
|
|
|
|
|
|
This command will find such duplicate messages and print them to standard out as newline-delimited
|
|
|
|
JSON. Status messages in the form of "H: $HEIGHT ($PROGRESS%)" will be printed to standard error for
|
|
|
|
every day of chain processed.
|
|
|
|
`,
|
2021-03-18 22:12:57 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.IntFlag{
|
2021-03-19 03:30:22 +00:00
|
|
|
Name: "parallel",
|
|
|
|
Usage: "the number of parallel threads for block processing",
|
|
|
|
DefaultText: "half the number of cores",
|
|
|
|
},
|
|
|
|
&cli.IntFlag{
|
|
|
|
Name: "start",
|
|
|
|
Usage: "the first epoch to check",
|
|
|
|
DefaultText: "genesis",
|
|
|
|
},
|
|
|
|
&cli.IntFlag{
|
|
|
|
Name: "end",
|
|
|
|
Usage: "the last epoch to check",
|
|
|
|
DefaultText: "the current head",
|
|
|
|
},
|
|
|
|
&cli.IntSliceFlag{
|
|
|
|
Name: "method",
|
2021-04-29 00:41:29 +00:00
|
|
|
Usage: "filter results by method number",
|
2021-03-19 03:30:22 +00:00
|
|
|
DefaultText: "all methods",
|
2021-03-18 22:12:57 +00:00
|
|
|
},
|
2021-04-29 00:41:29 +00:00
|
|
|
&cli.StringSliceFlag{
|
|
|
|
Name: "include-to",
|
|
|
|
Usage: "include only messages to the given address (does not perform address resolution)",
|
|
|
|
DefaultText: "all recipients",
|
|
|
|
},
|
|
|
|
&cli.StringSliceFlag{
|
|
|
|
Name: "include-from",
|
|
|
|
Usage: "include only messages from the given address (does not perform address resolution)",
|
|
|
|
DefaultText: "all senders",
|
|
|
|
},
|
|
|
|
&cli.StringSliceFlag{
|
|
|
|
Name: "exclude-to",
|
|
|
|
Usage: "exclude messages to the given address (does not perform address resolution)",
|
|
|
|
},
|
|
|
|
&cli.StringSliceFlag{
|
|
|
|
Name: "exclude-from",
|
|
|
|
Usage: "exclude messages from the given address (does not perform address resolution)",
|
|
|
|
},
|
2021-03-18 22:12:57 +00:00
|
|
|
},
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
api, closer, err := lcli.GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer closer()
|
|
|
|
ctx := lcli.ReqContext(cctx)
|
|
|
|
|
2021-03-19 03:30:22 +00:00
|
|
|
var head *types.TipSet
|
|
|
|
if cctx.IsSet("end") {
|
|
|
|
epoch := abi.ChainEpoch(cctx.Int("end"))
|
|
|
|
head, err = api.ChainGetTipSetByHeight(ctx, epoch, types.EmptyTSK)
|
|
|
|
} else {
|
|
|
|
head, err = api.ChainHead(ctx)
|
|
|
|
}
|
2021-03-18 22:12:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var printLk sync.Mutex
|
|
|
|
|
2021-03-19 03:30:22 +00:00
|
|
|
threads := runtime.NumCPU() / 2
|
|
|
|
if cctx.IsSet("parallel") {
|
|
|
|
threads = cctx.Int("int")
|
|
|
|
if threads <= 0 {
|
|
|
|
return fmt.Errorf("parallelism needs to be at least 1")
|
|
|
|
}
|
|
|
|
} else if threads == 0 {
|
|
|
|
threads = 1 // if we have one core, but who are we kidding...
|
|
|
|
}
|
2021-03-18 22:12:57 +00:00
|
|
|
|
|
|
|
throttle := make(chan struct{}, threads)
|
|
|
|
|
2021-04-29 00:41:29 +00:00
|
|
|
methods := map[abi.MethodNum]bool{}
|
2021-03-19 03:30:22 +00:00
|
|
|
for _, m := range cctx.IntSlice("method") {
|
|
|
|
if m < 0 {
|
|
|
|
return fmt.Errorf("expected method numbers to be non-negative")
|
|
|
|
}
|
|
|
|
methods[abi.MethodNum(m)] = true
|
|
|
|
}
|
|
|
|
|
2021-04-29 00:41:29 +00:00
|
|
|
addressSet := func(flag string) (map[address.Address]bool, error) {
|
|
|
|
if !cctx.IsSet(flag) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
addrs := cctx.StringSlice(flag)
|
|
|
|
set := make(map[address.Address]bool, len(addrs))
|
|
|
|
for _, addrStr := range addrs {
|
|
|
|
addr, err := address.NewFromString(addrStr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to parse address %s: %w", addrStr, err)
|
|
|
|
}
|
|
|
|
set[addr] = true
|
|
|
|
}
|
|
|
|
return set, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
onlyFrom, err := addressSet("include-from")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
onlyTo, err := addressSet("include-to")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
excludeFrom, err := addressSet("exclude-from")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
excludeTo, err := addressSet("exclude-to")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-03-19 03:30:22 +00:00
|
|
|
target := abi.ChainEpoch(cctx.Int("start"))
|
2021-04-29 00:51:25 +00:00
|
|
|
if target < 0 || target > head.Height() {
|
|
|
|
return fmt.Errorf("start height must be greater than 0 and less than the end height")
|
|
|
|
}
|
2021-03-19 03:30:22 +00:00
|
|
|
totalEpochs := head.Height() - target
|
|
|
|
|
|
|
|
for target <= head.Height() {
|
2021-03-18 22:12:57 +00:00
|
|
|
select {
|
|
|
|
case throttle <- struct{}{}:
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
go func(ts *types.TipSet) {
|
|
|
|
defer func() {
|
|
|
|
<-throttle
|
|
|
|
}()
|
|
|
|
|
|
|
|
type addrNonce struct {
|
|
|
|
s address.Address
|
|
|
|
n uint64
|
|
|
|
}
|
|
|
|
anonce := func(m *types.Message) addrNonce {
|
|
|
|
return addrNonce{
|
|
|
|
s: m.From,
|
|
|
|
n: m.Nonce,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-19 03:30:22 +00:00
|
|
|
msgs := map[addrNonce]map[cid.Cid]*types.Message{}
|
2021-03-18 22:12:57 +00:00
|
|
|
|
2021-04-29 00:41:29 +00:00
|
|
|
processMessage := func(c cid.Cid, m *types.Message) {
|
|
|
|
// Filter
|
|
|
|
if len(methods) > 0 && !methods[m.Method] {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(onlyFrom) > 0 && !onlyFrom[m.From] {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(onlyTo) > 0 && !onlyTo[m.To] {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if excludeFrom[m.From] || excludeTo[m.To] {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Record
|
|
|
|
msgSet, ok := msgs[anonce(m)]
|
|
|
|
if !ok {
|
|
|
|
msgSet = make(map[cid.Cid]*types.Message, 1)
|
|
|
|
msgs[anonce(m)] = msgSet
|
|
|
|
}
|
|
|
|
msgSet[c] = m
|
|
|
|
}
|
|
|
|
|
2021-03-19 03:30:22 +00:00
|
|
|
encoder := json.NewEncoder(os.Stdout)
|
2021-03-18 22:12:57 +00:00
|
|
|
|
|
|
|
for _, bh := range ts.Blocks() {
|
|
|
|
bms, err := api.ChainGetBlockMessages(ctx, bh.Cid())
|
|
|
|
if err != nil {
|
2021-03-19 03:30:22 +00:00
|
|
|
fmt.Fprintln(os.Stderr, "ERROR: ", err)
|
2021-03-18 22:12:57 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-03-19 03:30:22 +00:00
|
|
|
for i, m := range bms.BlsMessages {
|
2021-04-29 00:41:29 +00:00
|
|
|
processMessage(bms.Cids[i], m)
|
2021-03-18 22:12:57 +00:00
|
|
|
}
|
|
|
|
|
2021-03-19 03:30:22 +00:00
|
|
|
for i, m := range bms.SecpkMessages {
|
2021-04-29 00:41:29 +00:00
|
|
|
processMessage(bms.Cids[len(bms.BlsMessages)+i], &m.Message)
|
2021-03-18 22:12:57 +00:00
|
|
|
}
|
2021-03-19 03:30:22 +00:00
|
|
|
}
|
|
|
|
for _, ms := range msgs {
|
|
|
|
if len(ms) == 1 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
type Msg struct {
|
|
|
|
Cid string
|
|
|
|
Value string
|
|
|
|
Method uint64
|
|
|
|
}
|
|
|
|
grouped := map[string][]Msg{}
|
|
|
|
for c, m := range ms {
|
|
|
|
addr := m.To.String()
|
|
|
|
grouped[addr] = append(grouped[addr], Msg{
|
|
|
|
Cid: c.String(),
|
|
|
|
Value: types.FIL(m.Value).String(),
|
|
|
|
Method: uint64(m.Method),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
printLk.Lock()
|
|
|
|
err := encoder.Encode(grouped)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, "ERROR: ", err)
|
|
|
|
}
|
|
|
|
printLk.Unlock()
|
2021-03-18 22:12:57 +00:00
|
|
|
}
|
|
|
|
}(head)
|
2021-03-19 03:30:22 +00:00
|
|
|
|
|
|
|
if head.Parents().IsEmpty() {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2021-03-18 22:12:57 +00:00
|
|
|
head, err = api.ChainGetTipSet(ctx, head.Parents())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-03-19 03:30:22 +00:00
|
|
|
if head.Height()%2880 == 0 {
|
2021-03-18 22:12:57 +00:00
|
|
|
printLk.Lock()
|
2021-03-19 03:30:22 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "H: %s (%d%%)\n", head.Height(), (100*(head.Height()-target))/totalEpochs)
|
2021-03-18 22:12:57 +00:00
|
|
|
printLk.Unlock()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := 0; i < threads; i++ {
|
|
|
|
select {
|
|
|
|
case throttle <- struct{}{}:
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-03-19 03:30:22 +00:00
|
|
|
printLk.Lock()
|
|
|
|
fmt.Fprintf(os.Stderr, "H: %s (100%%)\n", head.Height())
|
|
|
|
printLk.Unlock()
|
|
|
|
|
2021-03-18 22:12:57 +00:00
|
|
|
return nil
|
2020-09-05 03:01:36 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-04-22 23:45:35 +00:00
|
|
|
var chainBalanceSanityCheckCmd = &cli.Command{
|
|
|
|
Name: "chain-balance-sanity",
|
|
|
|
Description: "Confirms that the total balance of every actor in state is still 2 billion",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "tipset",
|
|
|
|
Usage: "specify tipset to start from",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
api, closer, err := lcli.GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer closer()
|
|
|
|
ctx := lcli.ReqContext(cctx)
|
|
|
|
|
|
|
|
ts, err := lcli.LoadTipSet(ctx, cctx, api)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
tsk := ts.Key()
|
|
|
|
actors, err := api.StateListActors(ctx, tsk)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
bal := big.Zero()
|
|
|
|
for _, addr := range actors {
|
|
|
|
act, err := api.StateGetActor(ctx, addr, tsk)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
bal = big.Add(bal, act.Balance)
|
|
|
|
}
|
|
|
|
|
|
|
|
attoBase := big.Mul(big.NewInt(int64(build.FilBase)), big.NewInt(int64(build.FilecoinPrecision)))
|
|
|
|
|
|
|
|
if big.Cmp(attoBase, bal) != 0 {
|
|
|
|
return xerrors.Errorf("sanity check failed (expected %s, actual %s)", attoBase, bal)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println("sanity check successful")
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2020-09-05 03:01:36 +00:00
|
|
|
var chainBalanceCmd = &cli.Command{
|
|
|
|
Name: "chain-balances",
|
|
|
|
Description: "Produces a csv file of all account balances",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "tipset",
|
|
|
|
Usage: "specify tipset to start from",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
api, closer, err := lcli.GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer closer()
|
|
|
|
ctx := lcli.ReqContext(cctx)
|
|
|
|
|
|
|
|
ts, err := lcli.LoadTipSet(ctx, cctx, api)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
tsk := ts.Key()
|
|
|
|
actors, err := api.StateListActors(ctx, tsk)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var infos []accountInfo
|
|
|
|
for _, addr := range actors {
|
|
|
|
act, err := api.StateGetActor(ctx, addr, tsk)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
ai := accountInfo{
|
|
|
|
Address: addr,
|
|
|
|
Balance: types.FIL(act.Balance),
|
|
|
|
Type: string(act.Code.Hash()[2:]),
|
|
|
|
}
|
|
|
|
|
2020-09-29 04:24:38 +00:00
|
|
|
if builtin.IsStorageMinerActor(act.Code) {
|
2020-09-05 03:01:36 +00:00
|
|
|
pow, err := api.StateMinerPower(ctx, addr, tsk)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to get power: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ai.Power = pow.MinerPower.RawBytePower
|
|
|
|
info, err := api.StateMinerInfo(ctx, addr, tsk)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to get miner info: %w", err)
|
|
|
|
}
|
|
|
|
ai.Worker = info.Worker
|
|
|
|
ai.Owner = info.Owner
|
|
|
|
|
|
|
|
}
|
|
|
|
infos = append(infos, ai)
|
|
|
|
}
|
|
|
|
|
2020-10-08 00:01:01 +00:00
|
|
|
printAccountInfos(infos, false)
|
|
|
|
|
2020-09-05 03:01:36 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var chainBalanceStateCmd = &cli.Command{
|
|
|
|
Name: "stateroot-balances",
|
|
|
|
Description: "Produces a csv file of all account balances from a given stateroot",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
2023-01-27 16:20:01 +00:00
|
|
|
Name: "repo",
|
|
|
|
Value: "~/.lotus",
|
|
|
|
EnvVars: []string{"LOTUS_PATH"},
|
2020-09-05 03:01:36 +00:00
|
|
|
},
|
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: "miner-info",
|
|
|
|
},
|
2020-10-15 15:51:40 +00:00
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: "robust-addresses",
|
|
|
|
},
|
2020-09-05 03:01:36 +00:00
|
|
|
},
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
ctx := context.TODO()
|
|
|
|
|
|
|
|
if !cctx.Args().Present() {
|
|
|
|
return fmt.Errorf("must pass state root")
|
|
|
|
}
|
|
|
|
|
|
|
|
sroot, err := cid.Decode(cctx.Args().First())
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to parse input: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fsrepo, err := repo.NewFS(cctx.String("repo"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
lkrepo, err := fsrepo.Lock(repo.FullNode)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer lkrepo.Close() //nolint:errcheck
|
|
|
|
|
2021-02-28 22:48:36 +00:00
|
|
|
bs, err := lkrepo.Blockstore(ctx, repo.UniversalBlockstore)
|
2020-09-05 03:01:36 +00:00
|
|
|
if err != nil {
|
2020-11-01 13:03:21 +00:00
|
|
|
return fmt.Errorf("failed to open blockstore: %w", err)
|
2020-09-05 03:01:36 +00:00
|
|
|
}
|
|
|
|
|
2020-11-01 13:03:21 +00:00
|
|
|
defer func() {
|
|
|
|
if c, ok := bs.(io.Closer); ok {
|
|
|
|
if err := c.Close(); err != nil {
|
|
|
|
log.Warnf("failed to close blockstore: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2021-01-26 10:25:34 +00:00
|
|
|
mds, err := lkrepo.Datastore(context.Background(), "/metadata")
|
2020-09-05 03:01:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-09-02 16:07:23 +00:00
|
|
|
cs := store.NewChainStore(bs, bs, mds, filcns.Weight, nil)
|
2020-11-16 22:22:08 +00:00
|
|
|
defer cs.Close() //nolint:errcheck
|
2020-09-05 03:01:36 +00:00
|
|
|
|
|
|
|
cst := cbor.NewCborStore(bs)
|
2020-09-21 20:43:47 +00:00
|
|
|
store := adt.WrapStore(ctx, cst)
|
2020-09-05 03:01:36 +00:00
|
|
|
|
2022-09-27 10:21:42 +00:00
|
|
|
sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil)
|
2021-09-02 16:07:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-09-05 03:01:36 +00:00
|
|
|
|
2020-09-14 22:43:12 +00:00
|
|
|
tree, err := state.LoadStateTree(cst, sroot)
|
2020-09-05 03:01:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
minerInfo := cctx.Bool("miner-info")
|
|
|
|
|
2020-10-15 15:51:40 +00:00
|
|
|
robustMap := make(map[address.Address]address.Address)
|
|
|
|
if cctx.Bool("robust-addresses") {
|
|
|
|
iact, err := tree.GetActor(_init.Address)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to load init actor: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ist, err := _init.Load(store, iact)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to load init actor state: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = ist.ForEachActor(func(id abi.ActorID, addr address.Address) error {
|
|
|
|
idAddr, err := address.NewIDAddress(uint64(id))
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to write to addr map: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
robustMap[idAddr] = addr
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to invert init address map: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-05 03:01:36 +00:00
|
|
|
var infos []accountInfo
|
|
|
|
err = tree.ForEach(func(addr address.Address, act *types.Actor) error {
|
|
|
|
|
|
|
|
ai := accountInfo{
|
|
|
|
Address: addr,
|
|
|
|
Balance: types.FIL(act.Balance),
|
|
|
|
Type: string(act.Code.Hash()[2:]),
|
|
|
|
Power: big.NewInt(0),
|
|
|
|
LockedFunds: types.FIL(big.NewInt(0)),
|
|
|
|
InitialPledge: types.FIL(big.NewInt(0)),
|
|
|
|
PreCommits: types.FIL(big.NewInt(0)),
|
2020-10-08 00:01:01 +00:00
|
|
|
VestingAmount: types.FIL(big.NewInt(0)),
|
2020-09-05 03:01:36 +00:00
|
|
|
}
|
|
|
|
|
2020-10-15 15:51:40 +00:00
|
|
|
if cctx.Bool("robust-addresses") {
|
|
|
|
robust, found := robustMap[addr]
|
|
|
|
if found {
|
|
|
|
ai.Address = robust
|
|
|
|
} else {
|
|
|
|
id, err := address.IDFromAddress(addr)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to get ID address: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: This is not the correctest way to determine whether a robust address should exist
|
|
|
|
if id >= genesis.MinerStart {
|
|
|
|
return xerrors.Errorf("address doesn't have a robust address: %s", addr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-29 04:24:38 +00:00
|
|
|
if minerInfo && builtin.IsStorageMinerActor(act.Code) {
|
2020-09-16 05:47:24 +00:00
|
|
|
pow, _, _, err := stmgr.GetPowerRaw(ctx, sm, sroot, addr)
|
2020-09-05 03:01:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to get power: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ai.Power = pow.RawBytePower
|
|
|
|
|
2020-09-21 20:43:47 +00:00
|
|
|
st, err := miner.Load(store, act)
|
|
|
|
if err != nil {
|
2020-09-05 03:01:36 +00:00
|
|
|
return xerrors.Errorf("failed to read miner state: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-09-21 20:43:47 +00:00
|
|
|
liveSectorCount, err := st.NumLiveSectors()
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to compute live sector count: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
lockedFunds, err := st.LockedFunds()
|
2020-09-05 03:01:36 +00:00
|
|
|
if err != nil {
|
2020-09-21 20:43:47 +00:00
|
|
|
return xerrors.Errorf("failed to compute locked funds: %w", err)
|
2020-09-05 03:01:36 +00:00
|
|
|
}
|
|
|
|
|
2020-09-21 20:43:47 +00:00
|
|
|
ai.InitialPledge = types.FIL(lockedFunds.InitialPledgeRequirement)
|
|
|
|
ai.LockedFunds = types.FIL(lockedFunds.VestingFunds)
|
|
|
|
ai.PreCommits = types.FIL(lockedFunds.PreCommitDeposits)
|
|
|
|
ai.Sectors = liveSectorCount
|
2020-09-05 03:01:36 +00:00
|
|
|
|
2020-09-21 20:43:47 +00:00
|
|
|
minfo, err := st.Info()
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to get miner info: %w", err)
|
2020-09-05 03:01:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ai.Worker = minfo.Worker
|
|
|
|
ai.Owner = minfo.Owner
|
|
|
|
}
|
2020-10-08 00:01:01 +00:00
|
|
|
|
2020-10-08 11:58:40 +00:00
|
|
|
if builtin.IsMultisigActor(act.Code) {
|
2020-10-08 00:01:01 +00:00
|
|
|
mst, err := multisig.Load(store, act)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
ai.VestingStart, err = mst.StartEpoch()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
ib, err := mst.InitialBalance()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
ai.VestingAmount = types.FIL(ib)
|
|
|
|
|
|
|
|
ai.VestingDuration, err = mst.UnlockDuration()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-09-05 03:01:36 +00:00
|
|
|
infos = append(infos, ai)
|
|
|
|
return nil
|
|
|
|
})
|
2020-09-07 23:12:44 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("failed to loop over actors: %w", err)
|
|
|
|
}
|
2020-09-05 03:01:36 +00:00
|
|
|
|
2020-10-08 00:01:01 +00:00
|
|
|
printAccountInfos(infos, minerInfo)
|
2020-09-05 03:01:36 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
2020-09-24 21:30:11 +00:00
|
|
|
|
2020-10-08 00:01:01 +00:00
|
|
|
func printAccountInfos(infos []accountInfo, minerInfo bool) {
|
|
|
|
if minerInfo {
|
|
|
|
fmt.Printf("Address,Balance,Type,Sectors,Worker,Owner,InitialPledge,Locked,PreCommits,VestingStart,VestingDuration,VestingAmount\n")
|
|
|
|
for _, acc := range infos {
|
|
|
|
fmt.Printf("%s,%s,%s,%d,%s,%s,%s,%s,%s,%d,%d,%s\n", acc.Address, acc.Balance.Unitless(), acc.Type, acc.Sectors, acc.Worker, acc.Owner, acc.InitialPledge.Unitless(), acc.LockedFunds.Unitless(), acc.PreCommits.Unitless(), acc.VestingStart, acc.VestingDuration, acc.VestingAmount.Unitless())
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fmt.Printf("Address,Balance,Type\n")
|
|
|
|
for _, acc := range infos {
|
|
|
|
fmt.Printf("%s,%s,%s\n", acc.Address, acc.Balance.Unitless(), acc.Type)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-09-24 21:30:11 +00:00
|
|
|
var chainPledgeCmd = &cli.Command{
|
|
|
|
Name: "stateroot-pledge",
|
|
|
|
Description: "Calculate sector pledge numbers",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
2023-01-27 16:20:01 +00:00
|
|
|
Name: "repo",
|
|
|
|
Value: "~/.lotus",
|
|
|
|
EnvVars: []string{"LOTUS_PATH"},
|
2020-09-24 21:30:11 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
ArgsUsage: "[stateroot epoch]",
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
logging.SetLogLevel("badger", "ERROR")
|
|
|
|
ctx := context.TODO()
|
|
|
|
|
|
|
|
if !cctx.Args().Present() {
|
|
|
|
return fmt.Errorf("must pass state root")
|
|
|
|
}
|
|
|
|
|
|
|
|
sroot, err := cid.Decode(cctx.Args().First())
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to parse input: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
epoch, err := strconv.ParseInt(cctx.Args().Get(1), 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("parsing epoch arg: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fsrepo, err := repo.NewFS(cctx.String("repo"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
lkrepo, err := fsrepo.Lock(repo.FullNode)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer lkrepo.Close() //nolint:errcheck
|
|
|
|
|
2021-02-28 22:48:36 +00:00
|
|
|
bs, err := lkrepo.Blockstore(ctx, repo.UniversalBlockstore)
|
2020-09-24 21:30:11 +00:00
|
|
|
if err != nil {
|
2020-11-01 13:03:21 +00:00
|
|
|
return xerrors.Errorf("failed to open blockstore: %w", err)
|
2020-09-24 21:30:11 +00:00
|
|
|
}
|
|
|
|
|
2020-11-01 13:03:21 +00:00
|
|
|
defer func() {
|
|
|
|
if c, ok := bs.(io.Closer); ok {
|
|
|
|
if err := c.Close(); err != nil {
|
|
|
|
log.Warnf("failed to close blockstore: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2021-01-26 10:25:34 +00:00
|
|
|
mds, err := lkrepo.Datastore(context.Background(), "/metadata")
|
2020-09-24 21:30:11 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-09-02 16:07:23 +00:00
|
|
|
cs := store.NewChainStore(bs, bs, mds, filcns.Weight, nil)
|
2020-11-16 22:22:08 +00:00
|
|
|
defer cs.Close() //nolint:errcheck
|
2020-09-24 21:30:11 +00:00
|
|
|
|
|
|
|
cst := cbor.NewCborStore(bs)
|
|
|
|
store := adt.WrapStore(ctx, cst)
|
|
|
|
|
2022-09-27 10:21:42 +00:00
|
|
|
sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil)
|
2021-09-02 16:07:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-09-24 21:30:11 +00:00
|
|
|
state, err := state.LoadStateTree(cst, sroot)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
2020-09-29 04:24:38 +00:00
|
|
|
powerSmoothed builtin.FilterEstimate
|
2020-09-24 21:30:11 +00:00
|
|
|
pledgeCollateral abi.TokenAmount
|
|
|
|
)
|
|
|
|
if act, err := state.GetActor(power.Address); err != nil {
|
|
|
|
return xerrors.Errorf("loading miner actor: %w", err)
|
|
|
|
} else if s, err := power.Load(store, act); err != nil {
|
|
|
|
return xerrors.Errorf("loading power actor state: %w", err)
|
|
|
|
} else if p, err := s.TotalPowerSmoothed(); err != nil {
|
|
|
|
return xerrors.Errorf("failed to determine total power: %w", err)
|
|
|
|
} else if c, err := s.TotalLocked(); err != nil {
|
|
|
|
return xerrors.Errorf("failed to determine pledge collateral: %w", err)
|
|
|
|
} else {
|
|
|
|
powerSmoothed = p
|
|
|
|
pledgeCollateral = c
|
|
|
|
}
|
|
|
|
|
2020-10-11 22:17:28 +00:00
|
|
|
circ, err := sm.GetVMCirculatingSupplyDetailed(ctx, abi.ChainEpoch(epoch), state)
|
2020-09-24 21:30:11 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println("(real) circulating supply: ", types.FIL(circ.FilCirculating))
|
|
|
|
if circ.FilCirculating.LessThan(big.Zero()) {
|
|
|
|
circ.FilCirculating = big.Zero()
|
|
|
|
}
|
|
|
|
|
|
|
|
rewardActor, err := state.GetActor(reward.Address)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("loading miner actor: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
rewardState, err := reward.Load(store, rewardActor)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("loading reward actor state: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println("FilVested", types.FIL(circ.FilVested))
|
|
|
|
fmt.Println("FilMined", types.FIL(circ.FilMined))
|
|
|
|
fmt.Println("FilBurnt", types.FIL(circ.FilBurnt))
|
|
|
|
fmt.Println("FilLocked", types.FIL(circ.FilLocked))
|
|
|
|
fmt.Println("FilCirculating", types.FIL(circ.FilCirculating))
|
|
|
|
|
|
|
|
for _, sectorWeight := range []abi.StoragePower{
|
|
|
|
types.NewInt(32 << 30),
|
|
|
|
types.NewInt(64 << 30),
|
|
|
|
types.NewInt(32 << 30 * 10),
|
|
|
|
types.NewInt(64 << 30 * 10),
|
|
|
|
} {
|
|
|
|
initialPledge, err := rewardState.InitialPledgeForPower(
|
|
|
|
sectorWeight,
|
|
|
|
pledgeCollateral,
|
|
|
|
&powerSmoothed,
|
|
|
|
circ.FilCirculating,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("calculating initial pledge: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println("IP ", units.HumanSize(float64(sectorWeight.Uint64())), types.FIL(initialPledge))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
2020-11-26 21:46:19 +00:00
|
|
|
|
|
|
|
const dateFmt = "1/02/06"
|
|
|
|
|
|
|
|
func parseCsv(inp string) ([]time.Time, []address.Address, error) {
|
|
|
|
fi, err := os.Open(inp)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
r := csv.NewReader(fi)
|
|
|
|
recs, err := r.ReadAll()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var addrs []address.Address
|
|
|
|
for _, rec := range recs[1:] {
|
|
|
|
a, err := address.NewFromString(rec[0])
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
addrs = append(addrs, a)
|
|
|
|
}
|
|
|
|
|
|
|
|
var dates []time.Time
|
|
|
|
for _, d := range recs[0][1:] {
|
|
|
|
if len(d) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
p := strings.Split(d, " ")
|
|
|
|
t, err := time.Parse(dateFmt, p[len(p)-1])
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
dates = append(dates, t)
|
|
|
|
}
|
|
|
|
|
|
|
|
return dates, addrs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func heightForDate(d time.Time, ts *types.TipSet) abi.ChainEpoch {
|
|
|
|
secs := d.Unix()
|
|
|
|
gents := ts.Blocks()[0].Timestamp
|
|
|
|
gents -= uint64(30 * ts.Height())
|
|
|
|
return abi.ChainEpoch((secs - int64(gents)) / 30)
|
|
|
|
}
|
|
|
|
|
|
|
|
var fillBalancesCmd = &cli.Command{
|
|
|
|
Name: "fill-balances",
|
|
|
|
Description: "fill out balances for addresses on dates in given spreadsheet",
|
|
|
|
Flags: []cli.Flag{},
|
|
|
|
Action: func(cctx *cli.Context) error {
|
|
|
|
api, closer, err := lcli.GetFullNodeAPI(cctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer closer()
|
|
|
|
ctx := lcli.ReqContext(cctx)
|
|
|
|
|
|
|
|
dates, addrs, err := parseCsv(cctx.Args().First())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
ts, err := api.ChainHead(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var tipsets []*types.TipSet
|
|
|
|
for _, d := range dates {
|
|
|
|
h := heightForDate(d, ts)
|
|
|
|
hts, err := api.ChainGetTipSetByHeight(ctx, h, ts.Key())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tipsets = append(tipsets, hts)
|
|
|
|
}
|
|
|
|
|
|
|
|
var balances [][]abi.TokenAmount
|
|
|
|
for _, a := range addrs {
|
|
|
|
var b []abi.TokenAmount
|
|
|
|
for _, hts := range tipsets {
|
|
|
|
act, err := api.StateGetActor(ctx, a, hts.Key())
|
|
|
|
if err != nil {
|
|
|
|
if !strings.Contains(err.Error(), "actor not found") {
|
|
|
|
return fmt.Errorf("error for %s at %s: %w", a, hts.Key(), err)
|
|
|
|
}
|
|
|
|
b = append(b, types.NewInt(0))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
b = append(b, act.Balance)
|
|
|
|
}
|
|
|
|
balances = append(balances, b)
|
|
|
|
}
|
|
|
|
|
|
|
|
var datestrs []string
|
|
|
|
for _, d := range dates {
|
|
|
|
datestrs = append(datestrs, "Balance at "+d.Format(dateFmt))
|
|
|
|
}
|
|
|
|
|
|
|
|
w := csv.NewWriter(os.Stdout)
|
2020-11-27 15:02:01 +00:00
|
|
|
w.Write(append([]string{"Wallet Address"}, datestrs...)) // nolint:errcheck
|
2020-11-26 21:46:19 +00:00
|
|
|
for i := 0; i < len(addrs); i++ {
|
|
|
|
row := []string{addrs[i].String()}
|
|
|
|
for _, b := range balances[i] {
|
|
|
|
row = append(row, types.FIL(b).String())
|
|
|
|
}
|
2020-11-27 15:02:01 +00:00
|
|
|
w.Write(row) // nolint:errcheck
|
2020-11-26 21:46:19 +00:00
|
|
|
}
|
|
|
|
w.Flush()
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|