lotus/cmd/lotus-shed/balances.go
2023-11-15 13:06:51 +01:00

931 lines
23 KiB
Go

package main
import (
"context"
"encoding/csv"
"encoding/json"
"fmt"
"io"
"os"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/docker/go-units"
"github.com/ipfs/go-cid"
cbor "github.com/ipfs/go-ipld-cbor"
logging "github.com/ipfs/go-log/v2"
"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"
"github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain/actors/adt"
"github.com/filecoin-project/lotus/chain/actors/builtin"
_init "github.com/filecoin-project/lotus/chain/actors/builtin/init"
"github.com/filecoin-project/lotus/chain/actors/builtin/miner"
"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"
"github.com/filecoin-project/lotus/chain/consensus"
"github.com/filecoin-project/lotus/chain/consensus/filcns"
"github.com/filecoin-project/lotus/chain/gen/genesis"
"github.com/filecoin-project/lotus/chain/index"
"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"
"github.com/filecoin-project/lotus/storage/sealer/ffiwrapper"
)
type accountInfo struct {
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
}
var auditsCmd = &cli.Command{
Name: "audits",
Description: "a collection of utilities for auditing the filecoin chain",
Subcommands: []*cli.Command{
chainBalanceCmd,
chainBalanceSanityCheckCmd,
chainBalanceStateCmd,
chainPledgeCmd,
fillBalancesCmd,
duplicatedMessagesCmd,
},
}
var duplicatedMessagesCmd = &cli.Command{
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.
`,
Flags: []cli.Flag{
&cli.IntFlag{
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",
Usage: "filter results by method number",
DefaultText: "all methods",
},
&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)",
},
},
Action: func(cctx *cli.Context) error {
api, closer, err := lcli.GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := lcli.ReqContext(cctx)
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)
}
if err != nil {
return err
}
var printLk sync.Mutex
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...
}
throttle := make(chan struct{}, threads)
methods := map[abi.MethodNum]bool{}
for _, m := range cctx.IntSlice("method") {
if m < 0 {
return fmt.Errorf("expected method numbers to be non-negative")
}
methods[abi.MethodNum(m)] = true
}
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
}
target := abi.ChainEpoch(cctx.Int("start"))
if target < 0 || target > head.Height() {
return fmt.Errorf("start height must be greater than 0 and less than the end height")
}
totalEpochs := head.Height() - target
for target <= head.Height() {
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,
}
}
msgs := map[addrNonce]map[cid.Cid]*types.Message{}
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
}
encoder := json.NewEncoder(os.Stdout)
for _, bh := range ts.Blocks() {
bms, err := api.ChainGetBlockMessages(ctx, bh.Cid())
if err != nil {
fmt.Fprintln(os.Stderr, "ERROR: ", err)
return
}
for i, m := range bms.BlsMessages {
processMessage(bms.Cids[i], m)
}
for i, m := range bms.SecpkMessages {
processMessage(bms.Cids[len(bms.BlsMessages)+i], &m.Message)
}
}
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()
}
}(head)
if head.Parents().IsEmpty() {
break
}
head, err = api.ChainGetTipSet(ctx, head.Parents())
if err != nil {
return err
}
if head.Height()%2880 == 0 {
printLk.Lock()
fmt.Fprintf(os.Stderr, "H: %s (%d%%)\n", head.Height(), (100*(head.Height()-target))/totalEpochs)
printLk.Unlock()
}
}
for i := 0; i < threads; i++ {
select {
case throttle <- struct{}{}:
case <-ctx.Done():
return ctx.Err()
}
}
printLk.Lock()
fmt.Fprintf(os.Stderr, "H: %s (100%%)\n", head.Height())
printLk.Unlock()
return nil
},
}
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
},
}
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:]),
}
if builtin.IsStorageMinerActor(act.Code) {
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)
}
printAccountInfos(infos, false)
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{
Name: "repo",
Value: "~/.lotus",
},
&cli.BoolFlag{
Name: "miner-info",
},
&cli.BoolFlag{
Name: "robust-addresses",
},
},
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
bs, err := lkrepo.Blockstore(ctx, repo.UniversalBlockstore)
if err != nil {
return fmt.Errorf("failed to open blockstore: %w", err)
}
defer func() {
if c, ok := bs.(io.Closer); ok {
if err := c.Close(); err != nil {
log.Warnf("failed to close blockstore: %s", err)
}
}
}()
mds, err := lkrepo.Datastore(context.Background(), "/metadata")
if err != nil {
return err
}
cs := store.NewChainStore(bs, bs, mds, filcns.Weight, nil)
defer cs.Close() //nolint:errcheck
cst := cbor.NewCborStore(bs)
store := adt.WrapStore(ctx, cst)
sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil, mds, index.DummyMsgIndex)
if err != nil {
return err
}
tree, err := state.LoadStateTree(cst, sroot)
if err != nil {
return err
}
minerInfo := cctx.Bool("miner-info")
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)
}
}
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)),
VestingAmount: types.FIL(big.NewInt(0)),
}
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)
}
}
}
if minerInfo && builtin.IsStorageMinerActor(act.Code) {
pow, _, _, err := stmgr.GetPowerRaw(ctx, sm, sroot, addr)
if err != nil {
return xerrors.Errorf("failed to get power: %w", err)
}
ai.Power = pow.RawBytePower
st, err := miner.Load(store, act)
if err != nil {
return xerrors.Errorf("failed to read miner state: %w", err)
}
liveSectorCount, err := st.NumLiveSectors()
if err != nil {
return xerrors.Errorf("failed to compute live sector count: %w", err)
}
lockedFunds, err := st.LockedFunds()
if err != nil {
return xerrors.Errorf("failed to compute locked funds: %w", err)
}
ai.InitialPledge = types.FIL(lockedFunds.InitialPledgeRequirement)
ai.LockedFunds = types.FIL(lockedFunds.VestingFunds)
ai.PreCommits = types.FIL(lockedFunds.PreCommitDeposits)
ai.Sectors = liveSectorCount
minfo, err := st.Info()
if err != nil {
return xerrors.Errorf("failed to get miner info: %w", err)
}
ai.Worker = minfo.Worker
ai.Owner = minfo.Owner
}
if builtin.IsMultisigActor(act.Code) {
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
}
}
infos = append(infos, ai)
return nil
})
if err != nil {
return xerrors.Errorf("failed to loop over actors: %w", err)
}
printAccountInfos(infos, minerInfo)
return nil
},
}
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)
}
}
}
var chainPledgeCmd = &cli.Command{
Name: "stateroot-pledge",
Description: "Calculate sector pledge numbers",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "repo",
Value: "~/.lotus",
},
},
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
bs, err := lkrepo.Blockstore(ctx, repo.UniversalBlockstore)
if err != nil {
return xerrors.Errorf("failed to open blockstore: %w", err)
}
defer func() {
if c, ok := bs.(io.Closer); ok {
if err := c.Close(); err != nil {
log.Warnf("failed to close blockstore: %s", err)
}
}
}()
mds, err := lkrepo.Datastore(context.Background(), "/metadata")
if err != nil {
return err
}
cs := store.NewChainStore(bs, bs, mds, filcns.Weight, nil)
defer cs.Close() //nolint:errcheck
cst := cbor.NewCborStore(bs)
store := adt.WrapStore(ctx, cst)
sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil, mds, index.DummyMsgIndex)
if err != nil {
return err
}
state, err := state.LoadStateTree(cst, sroot)
if err != nil {
return err
}
var (
powerSmoothed builtin.FilterEstimate
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
}
circ, err := sm.GetVMCirculatingSupplyDetailed(ctx, abi.ChainEpoch(epoch), state)
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
},
}
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)
w.Write(append([]string{"Wallet Address"}, datestrs...)) // nolint:errcheck
for i := 0; i < len(addrs); i++ {
row := []string{addrs[i].String()}
for _, b := range balances[i] {
row = append(row, types.FIL(b).String())
}
w.Write(row) // nolint:errcheck
}
w.Flush()
return nil
},
}