feat: CLI: add claim-extend cli (#11711)

* add claim-extend cli

* fix arg usage

* add missing question

* fix client addr, datacap prompt

* replace waitGrp with errGrp

* use promptUI

* replace fmt.ErrorF with xerror

* apply var name suggestion

* GST rc3, update types

* add itest

* make gen

* add changelog
This commit is contained in:
LexLuthr
2024-03-20 13:34:52 +04:00
committed by GitHub
parent 2e75f3b796
commit 02a8848b54
4 changed files with 640 additions and 38 deletions
+435
View File
@@ -4,23 +4,30 @@ 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"
@@ -47,6 +54,7 @@ var filplusCmd = &cli.Command{
filplusListClaimsCmd,
filplusRemoveExpiredAllocationsCmd,
filplusRemoveExpiredClaimsCmd,
filplusExtendClaimCmd,
},
}
@@ -923,3 +931,430 @@ var filplusSignRemoveDataCapProposal = &cli.Command{
return nil
},
}
var filplusExtendClaimCmd = &cli.Command{
Name: "extend-claim",
Usage: "extend claim expiration (TermMax)",
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(build.MessageConfidence),
},
},
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"))
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) ([]*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
var newClaims []verifregtypes13.ClaimExtensionRequest
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 claim.TermMax < tmax && claim.TermStart+claim.TermMax > head.Height() {
// If client is not same - needs to burn datacap
if claim.Client != wid {
newClaims = append(newClaims, verifregtypes13.ClaimExtensionRequest{
Claim: verifregtypes13.ClaimId(claimID),
Provider: abi.ActorID(mid),
TermMax: tmax,
})
rDataCap.Add(big.NewInt(int64(claim.Size)).Int, rDataCap.Int)
continue
}
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 claim.TermMax < tmax && claim.TermStart+claim.TermMax > head.Height() {
// If client is not same - needs to burn datacap
if claim.Client != wid {
newClaims = append(newClaims, verifregtypes13.ClaimExtensionRequest{
Claim: claimID,
Provider: abi.ActorID(mid),
TermMax: tmax,
})
rDataCap.Add(big.NewInt(int64(claim.Size)).Int, rDataCap.Int)
continue
}
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 claim.TermMax < tmax && claim.TermStart+claim.TermMax > head.Height() {
// If client is not same - needs to burn datacap
if claim.Client != wid {
newClaims = append(newClaims, verifregtypes13.ClaimExtensionRequest{
Claim: claimID,
Provider: prov.ID,
TermMax: tmax,
})
rDataCap.Add(big.NewInt(int64(claim.Size)).Int, rDataCap.Int)
continue
}
terms = append(terms, verifregtypes13.ClaimTerm{
ClaimId: claimID,
TermMax: tmax,
Provider: prov.ID,
})
}
}
}
var msgs []*types.Message
if len(terms) > 0 {
params, err := actors.SerializeParams(&verifregtypes13.ExtendClaimTermsParams{
Terms: terms,
})
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 {
// 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)
}
ncparams, err := actors.SerializeParams(&verifregtypes13.AllocationRequests{
Extensions: newClaims,
})
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(rDataCap, 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(),
}
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
}
}
msgs = append(msgs, nclaimMsg)
}
return msgs, nil
}