Files
lotus/cli/filplus.go
Peter Rabbitson 1bc8a8b32c chore: remove (deprecated) deps on build/ proxy-constants
This is a large diff, yet should have exactly zero functional changes

Ideally as a result of this some parts of the depchain will become lighter,
with downstream reaping the same benefits as the team that initiated this split.

P.S. work was done while forming better intuition of current dependency graph
2024-07-22 17:36:12 +04:00

1421 lines
38 KiB
Go

package cli
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"os"
"strconv"
"strings"
cbor "github.com/ipfs/go-ipld-cbor"
"github.com/manifoldco/promptui"
"github.com/urfave/cli/v2"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-state-types/abi"
actorstypes "github.com/filecoin-project/go-state-types/actors"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/builtin"
verifregtypes13 "github.com/filecoin-project/go-state-types/builtin/v13/verifreg"
verifregtypes8 "github.com/filecoin-project/go-state-types/builtin/v8/verifreg"
datacap2 "github.com/filecoin-project/go-state-types/builtin/v9/datacap"
verifregtypes9 "github.com/filecoin-project/go-state-types/builtin/v9/verifreg"
"github.com/filecoin-project/go-state-types/network"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/api/v0api"
"github.com/filecoin-project/lotus/blockstore"
"github.com/filecoin-project/lotus/build/buildconstants"
"github.com/filecoin-project/lotus/chain/actors"
"github.com/filecoin-project/lotus/chain/actors/adt"
"github.com/filecoin-project/lotus/chain/actors/builtin/datacap"
"github.com/filecoin-project/lotus/chain/actors/builtin/verifreg"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/lib/tablewriter"
)
var FilplusCmd = &cli.Command{
Name: "filplus",
Usage: "Interact with the verified registry actor used by Filplus",
Flags: []cli.Flag{},
Subcommands: []*cli.Command{
filplusVerifyClientCmd,
filplusListNotariesCmd,
filplusListClientsCmd,
filplusCheckClientCmd,
filplusCheckNotaryCmd,
filplusSignRemoveDataCapProposal,
filplusListAllocationsCmd,
filplusListClaimsCmd,
filplusRemoveExpiredAllocationsCmd,
filplusRemoveExpiredClaimsCmd,
filplusExtendClaimCmd,
},
}
var filplusVerifyClientCmd = &cli.Command{
Name: "grant-datacap",
Usage: "give allowance to the specified verified client address",
ArgsUsage: "[clientAddress datacap]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "from",
Usage: "specify your notary address to send the message from",
Required: true,
},
},
Action: func(cctx *cli.Context) error {
froms := cctx.String("from")
if froms == "" {
return fmt.Errorf("must specify from address with --from")
}
fromk, err := address.NewFromString(froms)
if err != nil {
return err
}
if cctx.NArg() != 2 {
return IncorrectNumArgs(cctx)
}
target, err := address.NewFromString(cctx.Args().Get(0))
if err != nil {
return err
}
allowance, err := types.BigFromString(cctx.Args().Get(1))
if err != nil {
return err
}
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
found, dcap, err := checkNotary(ctx, api, fromk)
if err != nil {
return err
}
if !found {
return xerrors.New("sender address must be a notary")
}
if dcap.Cmp(allowance.Int) < 0 {
return xerrors.Errorf("cannot allot more allowance than notary data cap: %s < %s", dcap, allowance)
}
// TODO: This should be abstracted over actor versions
params, err := actors.SerializeParams(&verifregtypes9.AddVerifiedClientParams{Address: target, Allowance: allowance})
if err != nil {
return err
}
msg := &types.Message{
To: verifreg.Address,
From: fromk,
Method: verifreg.Methods.AddVerifiedClient,
Params: params,
}
smsg, err := api.MpoolPushMessage(ctx, msg, nil)
if err != nil {
return err
}
fmt.Printf("message sent, now waiting on cid: %s\n", smsg.Cid())
mwait, err := api.StateWaitMsg(ctx, smsg.Cid(), buildconstants.MessageConfidence)
if err != nil {
return err
}
if mwait.Receipt.ExitCode.IsError() {
return fmt.Errorf("failed to add verified client: %d", mwait.Receipt.ExitCode)
}
return nil
},
}
var filplusListNotariesCmd = &cli.Command{
Name: "list-notaries",
Usage: "list all notaries",
Action: func(cctx *cli.Context) error {
if cctx.NArg() != 0 {
return IncorrectNumArgs(cctx)
}
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
act, err := api.StateGetActor(ctx, verifreg.Address, types.EmptyTSK)
if err != nil {
return err
}
apibs := blockstore.NewAPIBlockstore(api)
store := adt.WrapStore(ctx, cbor.NewCborStore(apibs))
st, err := verifreg.Load(store, act)
if err != nil {
return err
}
return st.ForEachVerifier(func(addr address.Address, dcap abi.StoragePower) error {
_, err := fmt.Printf("%s: %s\n", addr, dcap)
return err
})
},
}
var filplusListClientsCmd = &cli.Command{
Name: "list-clients",
Usage: "list all verified clients",
Action: func(cctx *cli.Context) error {
if cctx.NArg() != 0 {
return IncorrectNumArgs(cctx)
}
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
apibs := blockstore.NewAPIBlockstore(api)
store := adt.WrapStore(ctx, cbor.NewCborStore(apibs))
nv, err := api.StateNetworkVersion(ctx, types.EmptyTSK)
if err != nil {
return err
}
av, err := actorstypes.VersionForNetwork(nv)
if err != nil {
return err
}
if av <= 8 {
act, err := api.StateGetActor(ctx, verifreg.Address, types.EmptyTSK)
if err != nil {
return err
}
st, err := verifreg.Load(store, act)
if err != nil {
return err
}
return st.ForEachClient(func(addr address.Address, dcap abi.StoragePower) error {
_, err := fmt.Printf("%s: %s\n", addr, dcap)
return err
})
}
act, err := api.StateGetActor(ctx, datacap.Address, types.EmptyTSK)
if err != nil {
return err
}
st, err := datacap.Load(store, act)
if err != nil {
return err
}
return st.ForEachClient(func(addr address.Address, dcap abi.StoragePower) error {
_, err := fmt.Printf("%s: %s\n", addr, dcap)
return err
})
},
}
var filplusListAllocationsCmd = &cli.Command{
Name: "list-allocations",
Usage: "List allocations available in verified registry actor or made by a client if specified",
ArgsUsage: "clientAddress",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "expired",
Usage: "list only expired allocations",
},
&cli.BoolFlag{
Name: "json",
Usage: "output results in json format",
Value: false,
},
},
Action: func(cctx *cli.Context) error {
if cctx.NArg() > 1 {
return IncorrectNumArgs(cctx)
}
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
writeOut := func(tsHeight abi.ChainEpoch, allocations map[verifreg.AllocationId]verifreg.Allocation, json, expired bool) error {
// Map Keys. Corresponds to the standard tablewriter output
allocationID := "AllocationID"
client := "Client"
provider := "Miner"
pieceCid := "PieceCid"
pieceSize := "PieceSize"
tMin := "TermMin"
tMax := "TermMax"
expr := "Expiration"
// One-to-one mapping between tablewriter keys and JSON keys
tableKeysToJsonKeys := map[string]string{
allocationID: strings.ToLower(allocationID),
client: strings.ToLower(client),
provider: strings.ToLower(provider),
pieceCid: strings.ToLower(pieceCid),
pieceSize: strings.ToLower(pieceSize),
tMin: strings.ToLower(tMin),
tMax: strings.ToLower(tMax),
expr: strings.ToLower(expr),
}
var allocs []map[string]interface{}
for key, val := range allocations {
if tsHeight > val.Expiration || !expired {
alloc := map[string]interface{}{
allocationID: key,
client: val.Client,
provider: val.Provider,
pieceCid: val.Data,
pieceSize: val.Size,
tMin: val.TermMin,
tMax: val.TermMax,
expr: val.Expiration,
}
allocs = append(allocs, alloc)
}
}
if json {
// get a new list of allocations with json keys instead of tablewriter keys
var jsonAllocs []map[string]interface{}
for _, alloc := range allocs {
jsonAlloc := make(map[string]interface{})
for k, v := range alloc {
jsonAlloc[tableKeysToJsonKeys[k]] = v
}
jsonAllocs = append(jsonAllocs, jsonAlloc)
}
// then return this!
return PrintJson(jsonAllocs)
}
// Init the tablewriter's columns
tw := tablewriter.New(
tablewriter.Col(allocationID),
tablewriter.Col(client),
tablewriter.Col(provider),
tablewriter.Col(pieceCid),
tablewriter.Col(pieceSize),
tablewriter.Col(tMin),
tablewriter.Col(tMax),
tablewriter.Col(expr))
// populate it with content
for _, alloc := range allocs {
tw.Write(alloc)
}
// return the corresponding string
return tw.Flush(os.Stdout)
}
store := adt.WrapStore(ctx, cbor.NewCborStore(blockstore.NewAPIBlockstore(api)))
verifregActor, err := api.StateGetActor(ctx, verifreg.Address, types.EmptyTSK)
if err != nil {
return err
}
verifregState, err := verifreg.Load(store, verifregActor)
if err != nil {
return err
}
ts, err := api.ChainHead(ctx)
if err != nil {
return err
}
if cctx.NArg() == 1 {
clientAddr, err := address.NewFromString(cctx.Args().Get(0))
if err != nil {
return err
}
clientIdAddr, err := api.StateLookupID(ctx, clientAddr, types.EmptyTSK)
if err != nil {
return err
}
allocationsMap, err := verifregState.GetAllocations(clientIdAddr)
if err != nil {
return err
}
return writeOut(ts.Height(), allocationsMap, cctx.Bool("json"), cctx.Bool("expired"))
}
allocationsMap, err := verifregState.GetAllAllocations()
if err != nil {
return err
}
return writeOut(ts.Height(), allocationsMap, cctx.Bool("json"), cctx.Bool("expired"))
},
}
var filplusListClaimsCmd = &cli.Command{
Name: "list-claims",
Usage: "List claims available in verified registry actor or made by provider if specified",
ArgsUsage: "providerAddress",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "expired",
Usage: "list only expired claims",
},
&cli.BoolFlag{
Name: "json",
Usage: "output results in json format",
Value: false,
},
},
Action: func(cctx *cli.Context) error {
if cctx.NArg() > 1 {
return IncorrectNumArgs(cctx)
}
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
writeOut := func(tsHeight abi.ChainEpoch, claims map[verifreg.ClaimId]verifreg.Claim, json, expired bool) error {
// Map Keys. Corresponds to the standard tablewriter output
claimID := "ClaimID"
provider := "Provider"
client := "Client"
data := "Data"
size := "Size"
tMin := "TermMin"
tMax := "TermMax"
tStart := "TermStart"
sector := "Sector"
// One-to-one mapping between tablewriter keys and JSON keys
tableKeysToJsonKeys := map[string]string{
claimID: strings.ToLower(claimID),
provider: strings.ToLower(provider),
client: strings.ToLower(client),
data: strings.ToLower(data),
size: strings.ToLower(size),
tMin: strings.ToLower(tMin),
tMax: strings.ToLower(tMax),
tStart: strings.ToLower(tStart),
sector: strings.ToLower(sector),
}
var claimList []map[string]interface{}
for key, val := range claims {
if tsHeight > val.TermStart+val.TermMax || !expired {
claim := map[string]interface{}{
claimID: key,
provider: val.Provider,
client: val.Client,
data: val.Data,
size: val.Size,
tMin: val.TermMin,
tMax: val.TermMax,
tStart: val.TermStart,
sector: val.Sector,
}
claimList = append(claimList, claim)
}
}
if json {
// get a new list of claims with json keys instead of tablewriter keys
var jsonClaims []map[string]interface{}
for _, claim := range claimList {
jsonClaim := make(map[string]interface{})
for k, v := range claim {
jsonClaim[tableKeysToJsonKeys[k]] = v
}
jsonClaims = append(jsonClaims, jsonClaim)
}
// then return this!
return PrintJson(jsonClaims)
}
// Init the tablewriter's columns
tw := tablewriter.New(
tablewriter.Col(claimID),
tablewriter.Col(client),
tablewriter.Col(provider),
tablewriter.Col(data),
tablewriter.Col(size),
tablewriter.Col(tMin),
tablewriter.Col(tMax),
tablewriter.Col(tStart),
tablewriter.Col(sector))
// populate it with content
for _, alloc := range claimList {
tw.Write(alloc)
}
// return the corresponding string
return tw.Flush(os.Stdout)
}
store := adt.WrapStore(ctx, cbor.NewCborStore(blockstore.NewAPIBlockstore(api)))
verifregActor, err := api.StateGetActor(ctx, verifreg.Address, types.EmptyTSK)
if err != nil {
return err
}
verifregState, err := verifreg.Load(store, verifregActor)
if err != nil {
return err
}
ts, err := api.ChainHead(ctx)
if err != nil {
return err
}
if cctx.NArg() == 1 {
providerAddr, err := address.NewFromString(cctx.Args().Get(0))
if err != nil {
return err
}
providerIdAddr, err := api.StateLookupID(ctx, providerAddr, types.EmptyTSK)
if err != nil {
return err
}
claimsMap, err := verifregState.GetClaims(providerIdAddr)
if err != nil {
return err
}
return writeOut(ts.Height(), claimsMap, cctx.Bool("json"), cctx.Bool("expired"))
}
claimsMap, err := verifregState.GetAllClaims()
if err != nil {
return err
}
return writeOut(ts.Height(), claimsMap, cctx.Bool("json"), cctx.Bool("expired"))
},
}
var filplusRemoveExpiredAllocationsCmd = &cli.Command{
Name: "remove-expired-allocations",
Usage: "remove expired allocations (if no allocations are specified all eligible allocations are removed)",
ArgsUsage: "clientAddress Optional[...allocationId]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "from",
Usage: "optionally specify the account to send the message from",
},
},
Action: func(cctx *cli.Context) error {
if cctx.NArg() < 1 {
return IncorrectNumArgs(cctx)
}
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
args := cctx.Args().Slice()
clientAddr, err := address.NewFromString(args[0])
if err != nil {
return err
}
clientIdAddr, err := api.StateLookupID(ctx, clientAddr, types.EmptyTSK)
if err != nil {
return err
}
clientId, err := address.IDFromAddress(clientIdAddr)
if err != nil {
return err
}
fromAddr := clientIdAddr
if from := cctx.String("from"); from != "" {
addr, err := address.NewFromString(from)
if err != nil {
return err
}
fromAddr = addr
}
allocationIDs := make([]verifregtypes9.AllocationId, len(args)-1)
for i, allocationString := range args[1:] {
id, err := strconv.ParseUint(allocationString, 10, 64)
if err != nil {
return err
}
allocationIDs[i] = verifregtypes9.AllocationId(id)
}
params, err := actors.SerializeParams(&verifregtypes9.RemoveExpiredAllocationsParams{
Client: abi.ActorID(clientId),
AllocationIds: allocationIDs,
})
if err != nil {
return err
}
msg := &types.Message{
To: verifreg.Address,
From: fromAddr,
Method: verifreg.Methods.RemoveExpiredAllocations,
Params: params,
}
smsg, err := api.MpoolPushMessage(ctx, msg, nil)
if err != nil {
return err
}
fmt.Printf("message sent, now waiting on cid: %s\n", smsg.Cid())
mwait, err := api.StateWaitMsg(ctx, smsg.Cid(), buildconstants.MessageConfidence)
if err != nil {
return err
}
if mwait.Receipt.ExitCode.IsError() {
return fmt.Errorf("failed to remove expired allocations: %d", mwait.Receipt.ExitCode)
}
return nil
},
}
var filplusRemoveExpiredClaimsCmd = &cli.Command{
Name: "remove-expired-claims",
Usage: "remove expired claims (if no claims are specified all eligible claims are removed)",
ArgsUsage: "providerAddress Optional[...claimId]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "from",
Usage: "optionally specify the account to send the message from",
},
},
Action: func(cctx *cli.Context) error {
if cctx.NArg() < 1 {
return IncorrectNumArgs(cctx)
}
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
args := cctx.Args().Slice()
providerAddr, err := address.NewFromString(args[0])
if err != nil {
return err
}
providerIdAddr, err := api.StateLookupID(ctx, providerAddr, types.EmptyTSK)
if err != nil {
return err
}
providerId, err := address.IDFromAddress(providerIdAddr)
if err != nil {
return err
}
fromAddr := providerIdAddr
if from := cctx.String("from"); from != "" {
addr, err := address.NewFromString(from)
if err != nil {
return err
}
fromAddr = addr
}
claimIDs := make([]verifregtypes9.ClaimId, len(args)-1)
for i, claimStr := range args[1:] {
id, err := strconv.ParseUint(claimStr, 10, 64)
if err != nil {
return err
}
claimIDs[i] = verifregtypes9.ClaimId(id)
}
params, err := actors.SerializeParams(&verifregtypes9.RemoveExpiredClaimsParams{
Provider: abi.ActorID(providerId),
ClaimIds: claimIDs,
})
if err != nil {
return err
}
msg := &types.Message{
To: verifreg.Address,
From: fromAddr,
Method: verifreg.Methods.RemoveExpiredClaims,
Params: params,
}
smsg, err := api.MpoolPushMessage(ctx, msg, nil)
if err != nil {
return err
}
fmt.Printf("message sent, now waiting on cid: %s\n", smsg.Cid())
mwait, err := api.StateWaitMsg(ctx, smsg.Cid(), buildconstants.MessageConfidence)
if err != nil {
return err
}
if mwait.Receipt.ExitCode.IsError() {
return fmt.Errorf("failed to remove expired allocations: %d", mwait.Receipt.ExitCode)
}
return nil
},
}
var filplusCheckClientCmd = &cli.Command{
Name: "check-client-datacap",
Usage: "check verified client remaining bytes",
ArgsUsage: "clientAddress",
Action: func(cctx *cli.Context) error {
if cctx.NArg() != 1 {
return fmt.Errorf("must specify client address to check")
}
caddr, err := address.NewFromString(cctx.Args().First())
if err != nil {
return err
}
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
dcap, err := api.StateVerifiedClientStatus(ctx, caddr, types.EmptyTSK)
if err != nil {
return err
}
if dcap == nil {
return xerrors.Errorf("client %s is not a verified client", caddr)
}
fmt.Println(*dcap)
return nil
},
}
var filplusCheckNotaryCmd = &cli.Command{
Name: "check-notary-datacap",
Usage: "check a notary's remaining bytes",
ArgsUsage: "notaryAddress",
Action: func(cctx *cli.Context) error {
if cctx.NArg() != 1 {
return fmt.Errorf("must specify notary address to check")
}
vaddr, err := address.NewFromString(cctx.Args().First())
if err != nil {
return err
}
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
defer closer()
ctx := ReqContext(cctx)
found, dcap, err := checkNotary(ctx, api, vaddr)
if err != nil {
return err
}
if !found {
return fmt.Errorf("not found")
}
fmt.Println(dcap)
return nil
},
}
func checkNotary(ctx context.Context, api v0api.FullNode, vaddr address.Address) (bool, abi.StoragePower, error) {
vid, err := api.StateLookupID(ctx, vaddr, types.EmptyTSK)
if err != nil {
return false, big.Zero(), err
}
act, err := api.StateGetActor(ctx, verifreg.Address, types.EmptyTSK)
if err != nil {
return false, big.Zero(), err
}
apibs := blockstore.NewAPIBlockstore(api)
store := adt.WrapStore(ctx, cbor.NewCborStore(apibs))
st, err := verifreg.Load(store, act)
if err != nil {
return false, big.Zero(), err
}
return st.VerifierDataCap(vid)
}
var filplusSignRemoveDataCapProposal = &cli.Command{
Name: "sign-remove-data-cap-proposal",
Usage: "allows a notary to sign a Remove Data Cap Proposal",
ArgsUsage: "[verifierAddress clientAddress allowanceToRemove]",
Flags: []cli.Flag{
&cli.Int64Flag{
Name: "id",
Usage: "specify the RemoveDataCapProposal ID (will look up on chain if unspecified)",
Required: false,
},
},
Action: func(cctx *cli.Context) error {
if cctx.NArg() != 3 {
return IncorrectNumArgs(cctx)
}
api, closer, err := GetFullNodeAPI(cctx)
if err != nil {
return xerrors.Errorf("failed to get full node api: %w", err)
}
defer closer()
ctx := ReqContext(cctx)
act, err := api.StateGetActor(ctx, verifreg.Address, types.EmptyTSK)
if err != nil {
return xerrors.Errorf("failed to get verifreg actor: %w", err)
}
apibs := blockstore.NewAPIBlockstore(api)
store := adt.WrapStore(ctx, cbor.NewCborStore(apibs))
st, err := verifreg.Load(store, act)
if err != nil {
return xerrors.Errorf("failed to load verified registry state: %w", err)
}
verifier, err := address.NewFromString(cctx.Args().Get(0))
if err != nil {
return err
}
verifierIdAddr, err := api.StateLookupID(ctx, verifier, types.EmptyTSK)
if err != nil {
return err
}
client, err := address.NewFromString(cctx.Args().Get(1))
if err != nil {
return err
}
clientIdAddr, err := api.StateLookupID(ctx, client, types.EmptyTSK)
if err != nil {
return err
}
allowanceToRemove, err := types.BigFromString(cctx.Args().Get(2))
if err != nil {
return err
}
dataCap, err := api.StateVerifiedClientStatus(ctx, clientIdAddr, types.EmptyTSK)
if err != nil {
return xerrors.Errorf("failed to find verified client data cap: %w", err)
}
if dataCap.LessThanEqual(big.Zero()) {
return xerrors.Errorf("client data cap %s is less than amount requested to be removed %s", dataCap.String(), allowanceToRemove.String())
}
found, _, err := checkNotary(ctx, api, verifier)
if err != nil {
return xerrors.Errorf("failed to check notary status: %w", err)
}
if !found {
return xerrors.New("verifier address must be a notary")
}
id := cctx.Uint64("id")
if id == 0 {
_, id, err = st.RemoveDataCapProposalID(verifierIdAddr, clientIdAddr)
if err != nil {
return xerrors.Errorf("failed find remove data cap proposal id: %w", err)
}
}
nv, err := api.StateNetworkVersion(ctx, types.EmptyTSK)
if err != nil {
return xerrors.Errorf("failed to get network version: %w", err)
}
paramBuf := new(bytes.Buffer)
paramBuf.WriteString(verifregtypes9.SignatureDomainSeparation_RemoveDataCap)
if nv <= network.Version16 {
params := verifregtypes8.RemoveDataCapProposal{
RemovalProposalID: id,
DataCapAmount: allowanceToRemove,
VerifiedClient: clientIdAddr,
}
err = params.MarshalCBOR(paramBuf)
} else {
params := verifregtypes9.RemoveDataCapProposal{
RemovalProposalID: verifregtypes9.RmDcProposalID{ProposalID: id},
DataCapAmount: allowanceToRemove,
VerifiedClient: clientIdAddr,
}
err = params.MarshalCBOR(paramBuf)
}
if err != nil {
return xerrors.Errorf("failed to marshall paramBuf: %w", err)
}
sig, err := api.WalletSign(ctx, verifier, paramBuf.Bytes())
if err != nil {
return xerrors.Errorf("failed to sign message: %w", err)
}
sigBytes := append([]byte{byte(sig.Type)}, sig.Data...)
fmt.Println(hex.EncodeToString(sigBytes))
return nil
},
}
var filplusExtendClaimCmd = &cli.Command{
Name: "extend-claim",
Usage: "extends claim expiration (TermMax)",
UsageText: `Extends claim expiration (TermMax).
If the client is original client then claim can be extended to maximum 5 years and no Datacap is required.
If the client id different then claim can be extended up to maximum 5 years from now and Datacap is required.
`,
Flags: []cli.Flag{
&cli.Int64Flag{
Name: "term-max",
Usage: "The maximum period for which a provider can earn quality-adjusted power for the piece (epochs). Default is 5 years.",
Aliases: []string{"tmax"},
Value: verifregtypes13.MaximumVerifiedAllocationTerm,
},
&cli.StringFlag{
Name: "client",
Usage: "the client address that will used to send the message",
Required: true,
},
&cli.BoolFlag{
Name: "all",
Usage: "automatically extend TermMax of all claims for specified miner[s] to --term-max (default: 5 years from claim start epoch)",
},
&cli.StringSliceFlag{
Name: "miner",
Usage: "storage provider address[es]",
Aliases: []string{"m", "provider", "p"},
},
&cli.BoolFlag{
Name: "assume-yes",
Usage: "automatic yes to prompts; assume 'yes' as answer to all prompts and run non-interactively",
Aliases: []string{"y", "yes"},
},
&cli.IntFlag{
Name: "confidence",
Usage: "number of block confirmations to wait for",
Value: int(buildconstants.MessageConfidence),
},
&cli.IntFlag{
Name: "batch-size",
Usage: "number of extend requests per batch. If set incorrectly, this will lead to out of gas error",
Value: 500,
},
},
ArgsUsage: "<claim1> <claim2> ... or <miner1=claim1> <miner2=claims2> ...",
Action: func(cctx *cli.Context) error {
miners := cctx.StringSlice("miner")
all := cctx.Bool("all")
client := cctx.String("client")
tmax := cctx.Int64("term-max")
// No miner IDs and no arguments
if len(miners) == 0 && cctx.Args().Len() == 0 {
return xerrors.Errorf("must specify at least one miner ID or argument[s]")
}
// Single Miner with no claimID and no --all flag
if len(miners) == 1 && cctx.Args().Len() == 0 && !all {
return xerrors.Errorf("must specify either --all flag or claim IDs to extend in argument")
}
// Multiple Miner with claimIDs
if len(miners) > 1 && cctx.Args().Len() > 0 {
return xerrors.Errorf("either specify multiple miner IDs or multiple arguments")
}
// Multiple Miner with no claimID and no --all flag
if len(miners) > 1 && cctx.Args().Len() == 0 && !all {
return xerrors.Errorf("must specify --all flag with multiple miner IDs")
}
// Tmax can't be more than policy max
if tmax > verifregtypes13.MaximumVerifiedAllocationTerm {
return xerrors.Errorf("specified term-max %d is larger than %d maximum allowed by verified regirty actor policy", tmax, verifregtypes13.MaximumVerifiedAllocationTerm)
}
api, closer, err := GetFullNodeAPIV1(cctx)
if err != nil {
return xerrors.Errorf("failed to get full node api: %s", err)
}
defer closer()
ctx := ReqContext(cctx)
clientAddr, err := address.NewFromString(client)
if err != nil {
return err
}
claimMap := make(map[verifregtypes13.ClaimId]ProvInfo)
// If no miners and arguments are present
if len(miners) == 0 && cctx.Args().Len() > 0 {
for _, arg := range cctx.Args().Slice() {
detail := strings.Split(arg, "=")
if len(detail) > 2 {
return xerrors.Errorf("incorrect argument format: %s", detail)
}
n, err := strconv.ParseInt(detail[1], 10, 64)
if err != nil {
return xerrors.Errorf("failed to parse the claim ID for %s for argument %s: %s", detail[0], detail, err)
}
maddr, err := address.NewFromString(detail[0])
if err != nil {
return err
}
// Verify that minerID exists
_, err = api.StateMinerInfo(ctx, maddr, types.EmptyTSK)
if err != nil {
return err
}
mid, err := address.IDFromAddress(maddr)
if err != nil {
return err
}
pi := ProvInfo{
Addr: maddr,
ID: abi.ActorID(mid),
}
claimMap[verifregtypes13.ClaimId(n)] = pi
}
}
// If 1 miner ID and multiple arguments
if len(miners) == 1 && cctx.Args().Len() > 0 && !all {
for _, arg := range cctx.Args().Slice() {
detail := strings.Split(arg, "=")
if len(detail) > 1 {
return xerrors.Errorf("incorrect argument format %s. Must provide only claim IDs with single miner ID", detail)
}
n, err := strconv.ParseInt(detail[0], 10, 64)
if err != nil {
return xerrors.Errorf("failed to parse the claim ID for %s for argument %s: %s", detail[0], detail, err)
}
claimMap[verifregtypes13.ClaimId(n)] = ProvInfo{}
}
}
msgs, err := CreateExtendClaimMsg(ctx, api, claimMap, miners, clientAddr, abi.ChainEpoch(tmax), all, cctx.Bool("assume-yes"), cctx.Int("batch-size"))
if err != nil {
return err
}
// If not msgs are found then no claims can be extended
if msgs == nil {
fmt.Println("No eligible claims to extend")
return nil
}
// MpoolBatchPushMessage method will take care of gas estimation and funds check
smsgs, err := api.MpoolBatchPushMessage(ctx, msgs, nil)
if err != nil {
return err
}
// wait for msgs to get mined into a block
eg := errgroup.Group{}
eg.SetLimit(10)
for _, msg := range smsgs {
msg := msg
eg.Go(func() error {
wait, err := api.StateWaitMsg(ctx, msg.Cid(), uint64(cctx.Int("confidence")), 2000, true)
if err != nil {
return xerrors.Errorf("timeout waiting for message to land on chain %s", wait.Message)
}
if wait.Receipt.ExitCode.IsError() {
return xerrors.Errorf("failed to execute message %s: %s", wait.Message, wait.Receipt.ExitCode)
}
return nil
})
}
return eg.Wait()
},
}
type ProvInfo struct {
Addr address.Address
ID abi.ActorID
}
// CreateExtendClaimMsg creates extend message[s] based on the following conditions
// 1. Extend all claims for a miner ID
// 2. Extend all claims for multiple miner IDs
// 3. Extend specified claims for a miner ID
// 4. Extend specific claims for specific miner ID
// 5. Extend all claims for a miner ID with different client address (2 messages)
// 6. Extend all claims for multiple miner IDs with different client address (2 messages)
// 7. Extend specified claims for a miner ID with different client address (2 messages)
// 8. Extend specific claims for specific miner ID with different client address (2 messages)
func CreateExtendClaimMsg(ctx context.Context, api api.FullNode, pcm map[verifregtypes13.ClaimId]ProvInfo, miners []string, wallet address.Address, tmax abi.ChainEpoch, all, assumeYes bool, batchSize int) ([]*types.Message, error) {
ac, err := api.StateLookupID(ctx, wallet, types.EmptyTSK)
if err != nil {
return nil, err
}
w, err := address.IDFromAddress(ac)
if err != nil {
return nil, xerrors.Errorf("converting wallet address to ID: %w", err)
}
wid := abi.ActorID(w)
head, err := api.ChainHead(ctx)
if err != nil {
return nil, err
}
var terms []verifregtypes13.ClaimTerm
newClaims := make(map[verifregtypes13.ClaimExtensionRequest]big.Int)
rDataCap := big.NewInt(0)
// If --all is set
if all {
for _, id := range miners {
maddr, err := address.NewFromString(id)
if err != nil {
return nil, xerrors.Errorf("parsing miner %s: %s", id, err)
}
mid, err := address.IDFromAddress(maddr)
if err != nil {
return nil, xerrors.Errorf("converting miner address to miner ID: %s", err)
}
claims, err := api.StateGetClaims(ctx, maddr, types.EmptyTSK)
if err != nil {
return nil, xerrors.Errorf("getting claims for miner %s: %s", maddr, err)
}
for claimID, claim := range claims {
claimID := claimID
claim := claim
// If the client is not the original client - burn datacap
if claim.Client != wid {
// The new duration should be greater than the original deal duration and claim should not already be expired
if head.Height()+tmax-claim.TermStart > claim.TermMax-claim.TermStart && claim.TermStart+claim.TermMax > head.Height() {
req := verifregtypes13.ClaimExtensionRequest{
Claim: verifregtypes13.ClaimId(claimID),
Provider: abi.ActorID(mid),
TermMax: head.Height() + tmax - claim.TermStart,
}
newClaims[req] = big.NewInt(int64(claim.Size))
rDataCap.Add(big.NewInt(int64(claim.Size)).Int, rDataCap.Int)
}
// If new duration shorter than the original duration then do nothing
continue
}
// For original client, compare duration(TermMax) and claim should not already be expired
if claim.TermMax < tmax && claim.TermStart+claim.TermMax > head.Height() {
terms = append(terms, verifregtypes13.ClaimTerm{
ClaimId: verifregtypes13.ClaimId(claimID),
TermMax: tmax,
Provider: abi.ActorID(mid),
})
}
}
}
}
// Single miner and specific claims
if len(miners) == 1 && len(pcm) > 0 {
maddr, err := address.NewFromString(miners[0])
if err != nil {
return nil, xerrors.Errorf("parsing miner %s: %s", miners[0], err)
}
mid, err := address.IDFromAddress(maddr)
if err != nil {
return nil, xerrors.Errorf("converting miner address to miner ID: %s", err)
}
claims, err := api.StateGetClaims(ctx, maddr, types.EmptyTSK)
if err != nil {
return nil, xerrors.Errorf("getting claims for miner %s: %s", maddr, err)
}
for claimID := range pcm {
claimID := claimID
claim, ok := claims[verifregtypes9.ClaimId(claimID)]
if !ok {
return nil, xerrors.Errorf("claim %d not found for provider %s", claimID, miners[0])
}
// If the client is not the original client - burn datacap
if claim.Client != wid {
// The new duration should be greater than the original deal duration and claim should not already be expired
if head.Height()+tmax-claim.TermStart > claim.TermMax-claim.TermStart && claim.TermStart+claim.TermMax > head.Height() {
req := verifregtypes13.ClaimExtensionRequest{
Claim: claimID,
Provider: abi.ActorID(mid),
TermMax: head.Height() + tmax - claim.TermStart,
}
newClaims[req] = big.NewInt(int64(claim.Size))
rDataCap.Add(big.NewInt(int64(claim.Size)).Int, rDataCap.Int)
}
// If new duration shorter than the original duration then do nothing
continue
}
// For original client, compare duration(TermMax) and claim should not already be expired
if claim.TermMax < tmax && claim.TermStart+claim.TermMax > head.Height() {
terms = append(terms, verifregtypes13.ClaimTerm{
ClaimId: claimID,
TermMax: tmax,
Provider: abi.ActorID(mid),
})
}
}
}
if len(miners) == 0 && len(pcm) > 0 {
for claimID, prov := range pcm {
prov := prov
claimID := claimID
claim, err := api.StateGetClaim(ctx, prov.Addr, verifregtypes9.ClaimId(claimID), types.EmptyTSK)
if err != nil {
return nil, xerrors.Errorf("could not load the claim %d: %s", claimID, err)
}
if claim == nil {
return nil, xerrors.Errorf("claim %d not found in the actor state", claimID)
}
// If the client is not the original client - burn datacap
if claim.Client != wid {
// The new duration should be greater than the original deal duration and claim should not already be expired
if head.Height()+tmax-claim.TermStart > claim.TermMax-claim.TermStart && claim.TermStart+claim.TermMax > head.Height() {
req := verifregtypes13.ClaimExtensionRequest{
Claim: claimID,
Provider: prov.ID,
TermMax: head.Height() + tmax - claim.TermStart,
}
newClaims[req] = big.NewInt(int64(claim.Size))
rDataCap.Add(big.NewInt(int64(claim.Size)).Int, rDataCap.Int)
}
// If new duration shorter than the original duration then do nothing
continue
}
// For original client, compare duration(TermMax) and claim should not already be expired
if claim.TermMax < tmax && claim.TermStart+claim.TermMax > head.Height() {
terms = append(terms, verifregtypes13.ClaimTerm{
ClaimId: claimID,
TermMax: tmax,
Provider: prov.ID,
})
}
}
}
var msgs []*types.Message
if len(terms) > 0 {
// Batch in 500 to avoid running out of gas
for i := 0; i < len(terms); i += batchSize {
batchEnd := i + batchSize
if batchEnd > len(terms) {
batchEnd = len(terms)
}
batch := terms[i:batchEnd]
params, err := actors.SerializeParams(&verifregtypes13.ExtendClaimTermsParams{
Terms: batch,
})
if err != nil {
return nil, xerrors.Errorf("failed to searialise the parameters: %s", err)
}
oclaimMsg := &types.Message{
To: verifreg.Address,
From: wallet,
Method: verifreg.Methods.ExtendClaimTerms,
Params: params,
}
msgs = append(msgs, oclaimMsg)
}
}
if len(newClaims) > 0 {
if !assumeYes {
out := fmt.Sprintf("Some of the specified allocation have a different client address and will require %d Datacap to extend. Proceed? Yes [Y/y] / No [N/n], Ctrl+C (^C) to exit", rDataCap.Int)
validate := func(input string) error {
if strings.EqualFold(input, "y") || strings.EqualFold(input, "yes") {
return nil
}
if strings.EqualFold(input, "n") || strings.EqualFold(input, "no") {
return nil
}
return errors.New("incorrect input")
}
templates := &promptui.PromptTemplates{
Prompt: "{{ . }} ",
Valid: "{{ . | green }} ",
Invalid: "{{ . | red }} ",
Success: "{{ . | cyan | bold }} ",
}
prompt := promptui.Prompt{
Label: out,
Templates: templates,
Validate: validate,
}
input, err := prompt.Run()
if err != nil {
return nil, err
}
if strings.Contains(strings.ToLower(input), "n") {
fmt.Println("Dropping the extension for claims that require Datacap")
return msgs, nil
}
}
// Get datacap balance
aDataCap, err := api.StateVerifiedClientStatus(ctx, wallet, types.EmptyTSK)
if err != nil {
return nil, err
}
if aDataCap == nil {
return nil, xerrors.Errorf("wallet %s does not have any datacap", wallet)
}
// Check that we have enough data cap to make the allocation
if rDataCap.GreaterThan(big.NewInt(aDataCap.Int64())) {
return nil, xerrors.Errorf("requested datacap %s is greater then the available datacap %s", rDataCap, aDataCap)
}
// Create a map of just keys, so we can easily batch based on the numeric keys
keys := make([]verifregtypes13.ClaimExtensionRequest, 0, len(newClaims))
for k := range newClaims {
keys = append(keys, k)
}
// Batch in 500 to avoid running out of gas
for i := 0; i < len(keys); i += batchSize {
batchEnd := i + batchSize
if batchEnd > len(newClaims) {
batchEnd = len(newClaims)
}
batch := keys[i:batchEnd]
// Calculate Datacap for this batch
dcap := big.NewInt(0)
for _, k := range batch {
dc := newClaims[k]
dcap.Add(dcap.Int, dc.Int)
}
ncparams, err := actors.SerializeParams(&verifregtypes13.AllocationRequests{
Extensions: batch,
})
if err != nil {
return nil, xerrors.Errorf("failed to searialise the parameters: %s", err)
}
transferParams, err := actors.SerializeParams(&datacap2.TransferParams{
To: builtin.VerifiedRegistryActorAddr,
Amount: big.Mul(dcap, builtin.TokenPrecision),
OperatorData: ncparams,
})
if err != nil {
return nil, xerrors.Errorf("failed to serialize transfer parameters: %s", err)
}
nclaimMsg := &types.Message{
To: builtin.DatacapActorAddr,
From: wallet,
Method: datacap.Methods.TransferExported,
Params: transferParams,
Value: big.Zero(),
}
msgs = append(msgs, nclaimMsg)
}
}
return msgs, nil
}