diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eeb44b7dd5..a2401a17021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This Lotus release candidate introduces the new `ChainIndexer` subsystem, enhanc - Implement `EthGetTransactionByBlockNumberAndIndex` (`eth_getTransactionByBlockNumberAndIndex`) and `EthGetTransactionByBlockHashAndIndex` (`eth_getTransactionByBlockHashAndIndex`) methods. ([filecoin-project/lotus#12618](https://github.com/filecoin-project/lotus/pull/12618)) - `lotus-shed indexes inspect-indexes` now performs a comprehensive comparison of the event index data for each message by comparing the AMT root CID from the message receipt with the root of a reconstructed AMT. Previously `inspect-indexes` simply compared event counts. Comparing AMT roots instead confirms all the event data is byte-perfect. ([filecoin-project/lotus#12570](https://github.com/filecoin-project/lotus/pull/12570)) - Reduce size of embedded genesis CAR files by removing WASM actor blocks and compressing with zstd. This reduces the `lotus` binary size by approximately 10 MiB. ([filecoin-project/lotus#12439](https://github.com/filecoin-project/lotus/pull/12439)) +- Implement F3 utility CLIs to list the power table for a given instance and sum the proportional power of a set of actors that participate in a given instance. ([filecoin-project/lotus#12698](https://github.com/filecoin-project/lotus/pull/12698)) ## 🐛 Bug Fix Highlights - Add logic to check if the miner's owner address is delegated (f4 address). If it is delegated, the `lotus-shed sectors termination-estimate` command now sends the termination state call using the worker ID. This fix resolves the issue where termination-estimate did not function correctly for miners with delegated owner addresses. ([filecoin-project/lotus#12569](https://github.com/filecoin-project/lotus/pull/12569)) diff --git a/cli/f3.go b/cli/f3.go index 7ffd8bae419..02874750711 100644 --- a/cli/f3.go +++ b/cli/f3.go @@ -1,6 +1,7 @@ package cli import ( + "context" "embed" "encoding/json" "errors" @@ -10,119 +11,350 @@ import ( "strings" "text/template" + "github.com/ipfs/go-cid" "github.com/urfave/cli/v2" "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-f3/certs" "github.com/filecoin-project/go-f3/gpbft" "github.com/filecoin-project/go-f3/manifest" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/lotus/api/v1api" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/lib/tablewriter" ) -var ( - F3Cmd = &cli.Command{ - Name: "f3", - Usage: "Manages Filecoin Fast Finality (F3) interactions", - Subcommands: []*cli.Command{ - { - Name: "list-miners", - Aliases: []string{"lm"}, - Usage: "Lists the miners that currently participate in F3 via this node.", - Action: func(cctx *cli.Context) error { - api, closer, err := GetFullNodeAPIV1(cctx) - if err != nil { - return err - } - defer closer() +var F3Cmd = &cli.Command{ + Name: "f3", + Usage: "Manages Filecoin Fast Finality (F3) interactions", + Subcommands: []*cli.Command{ + f3SbCmdListMiners, + f3SubCmdPowerTable, + f3SubCmdCerts, + f3SubCmdManifest, + f3SubCmdStatus, + }, +} +var f3SbCmdListMiners = &cli.Command{ + Name: "list-miners", + Aliases: []string{"lm"}, + Usage: `Lists the miners that currently participate in F3 via this node.`, + Action: func(cctx *cli.Context) error { + api, closer, err := GetFullNodeAPIV1(cctx) + if err != nil { + return err + } + defer closer() + + miners, err := api.F3ListParticipants(cctx.Context) + if err != nil { + return fmt.Errorf("listing participants: %w", err) + } + if len(miners) == 0 { + _, err = fmt.Fprintln(cctx.App.Writer, "No miners.") + return err + } + const ( + miner = "Miner" + from = "From" + to = "To" + ) + tw := tablewriter.New( + tablewriter.Col(miner), + tablewriter.Col(from), + tablewriter.Col(to), + ) + for _, participant := range miners { + addr, err := address.NewIDAddress(participant.MinerID) + if err != nil { + return fmt.Errorf("converting miner ID to address: %w", err) + } - miners, err := api.F3ListParticipants(cctx.Context) + tw.Write(map[string]interface{}{ + miner: addr, + from: participant.FromInstance, + to: participant.FromInstance + participant.ValidityTerm, + }) + } + return tw.Flush(cctx.App.Writer) + }, +} +var f3SubCmdPowerTable = &cli.Command{ + Name: "powertable", + Aliases: []string{"pt"}, + Subcommands: []*cli.Command{ + { + Name: "get", + Aliases: []string{"g"}, + Usage: `Get F3 power table at a specific instance ID or latest instance if none is specified.`, + ArgsUsage: "[instance]", + Flags: []cli.Flag{f3FlagPowerTableFromEC}, + Before: func(cctx *cli.Context) error { + if cctx.Args().Len() > 1 { + return fmt.Errorf("too many arguments") + } + return nil + }, + Action: func(cctx *cli.Context) error { + api, closer, err := GetFullNodeAPIV1(cctx) + if err != nil { + return err + } + defer closer() + + progress, err := api.F3GetProgress(cctx.Context) + if err != nil { + return fmt.Errorf("getting progress: %w", err) + } + + var instance uint64 + if cctx.Args().Present() { + instance, err = strconv.ParseUint(cctx.Args().First(), 10, 64) if err != nil { - return fmt.Errorf("listing participants: %w", err) + return fmt.Errorf("parsing instance: %w", err) } - if len(miners) == 0 { - _, err = fmt.Fprintln(cctx.App.Writer, "No miners.") - return err + if instance > progress.ID { + // TODO: Technically we can return power table for instances ahead as long as + // instance is within lookback. Implement it. + return fmt.Errorf("instance is ahead the current instance in progress: %d > %d", instance, progress.ID) } - const ( - miner = "Miner" - from = "From" - to = "To" - ) - tw := tablewriter.New( - tablewriter.Col(miner), - tablewriter.Col(from), - tablewriter.Col(to), - ) - for _, participant := range miners { - addr, err := address.NewIDAddress(participant.MinerID) + } else { + instance = progress.ID + } + + ltsk, expectedPowerTableCID, err := f3GetPowerTableTSKByInstance(cctx.Context, api, instance) + if err != nil { + return fmt.Errorf("getting power table tsk for instance %d: %w", instance, err) + } + + var result = struct { + Instance uint64 + FromEC bool + PowerTable struct { + CID string + Entries gpbft.PowerEntries + Total gpbft.StoragePower + ScaledTotal int64 + } + }{ + Instance: instance, + FromEC: cctx.Bool(f3FlagPowerTableFromEC.Name), + } + if result.FromEC { + result.PowerTable.Entries, err = api.F3GetECPowerTable(cctx.Context, ltsk) + } else { + result.PowerTable.Entries, err = api.F3GetF3PowerTable(cctx.Context, ltsk) + } + if err != nil { + return fmt.Errorf("getting f3 power table at instance %d: %w", instance, err) + } + + pt := gpbft.NewPowerTable() + if err := pt.Add(result.PowerTable.Entries...); err != nil { + // Sanity check the entries returned by the API. + return fmt.Errorf("retrieved power table is not valid for instance %d: %w", instance, err) + } + result.PowerTable.Total = pt.Total + result.PowerTable.ScaledTotal = pt.ScaledTotal + + actualPowerTableCID, err := certs.MakePowerTableCID(result.PowerTable.Entries) + if err != nil { + return fmt.Errorf("gettingh power table CID at instance %d: %w", instance, err) + } + if !expectedPowerTableCID.Equals(actualPowerTableCID) { + return fmt.Errorf("expected power table CID %s at instance %d, got: %s", expectedPowerTableCID, instance, actualPowerTableCID) + } + result.PowerTable.CID = actualPowerTableCID.String() + + output, err := json.MarshalIndent(result, "", " ") + if err != nil { + return fmt.Errorf("marshalling f3 power table at instance %d: %w", instance, err) + } + _, _ = fmt.Fprint(cctx.App.Writer, string(output)) + return nil + }, + }, + { + Name: "get-proportion", + Aliases: []string{"gp"}, + Usage: `Gets the total proportion of power for a list of actors at a given instance.`, + ArgsUsage: " [actor-id] ...", + Flags: []cli.Flag{ + f3FlagPowerTableFromEC, + f3FlagInstanceID, + }, + Before: func(cctx *cli.Context) error { + if cctx.Args().Len() < 1 { + return fmt.Errorf("at least one actor ID must be specified") + } + return nil + }, + Action: func(cctx *cli.Context) error { + api, closer, err := GetFullNodeAPIV1(cctx) + if err != nil { + return err + } + defer closer() + + progress, err := api.F3GetProgress(cctx.Context) + if err != nil { + return fmt.Errorf("getting progress: %w", err) + } + + var instance uint64 + if cctx.IsSet(f3FlagInstanceID.Name) { + instance = cctx.Uint64(f3FlagInstanceID.Name) + if instance > progress.ID { + // TODO: Technically we can return power table for instances ahead as long as + // instance is within lookback. Implement it. + return fmt.Errorf("instance is ahead the current instance in progress: %d > %d", instance, progress.ID) + } + } else { + instance = progress.ID + } + + ltsk, expectedPowerTableCID, err := f3GetPowerTableTSKByInstance(cctx.Context, api, instance) + if err != nil { + return fmt.Errorf("getting power table tsk for instance %d: %w", instance, err) + } + + var result = struct { + Instance uint64 + FromEC bool + PowerTable struct { + CID string + ScaledTotal int64 + } + ScaledSum int64 + Proportion float64 + }{ + Instance: instance, + FromEC: cctx.Bool(f3FlagPowerTableFromEC.Name), + } + + var powerEntries gpbft.PowerEntries + if result.FromEC { + powerEntries, err = api.F3GetECPowerTable(cctx.Context, ltsk) + } else { + powerEntries, err = api.F3GetF3PowerTable(cctx.Context, ltsk) + } + if err != nil { + return fmt.Errorf("getting f3 power table at instance %d: %w", instance, err) + } + + actualPowerTableCID, err := certs.MakePowerTableCID(powerEntries) + if err != nil { + return fmt.Errorf("gettingh power table CID at instance %d: %w", instance, err) + } + if !expectedPowerTableCID.Equals(actualPowerTableCID) { + return fmt.Errorf("expected power table CID %s at instance %d, got: %s", expectedPowerTableCID, instance, actualPowerTableCID) + } + result.PowerTable.CID = actualPowerTableCID.String() + + pt := gpbft.NewPowerTable() + if err := pt.Add(powerEntries...); err != nil { + return fmt.Errorf("constructing power table from entries: %w", err) + } + result.PowerTable.ScaledTotal = pt.ScaledTotal + + inputActorIDs := cctx.Args().Slice() + seenIDs := map[gpbft.ActorID]struct{}{} + for _, stringID := range inputActorIDs { + var actorID gpbft.ActorID + switch addr, err := address.NewFromString(stringID); { + case err == nil: + idAddr, err := address.IDFromAddress(addr) if err != nil { - return fmt.Errorf("converting miner ID to address: %w", err) + return fmt.Errorf("parsing ID from address %q: %w", stringID, err) } - - tw.Write(map[string]interface{}{ - miner: addr, - from: participant.FromInstance, - to: participant.FromInstance + participant.ValidityTerm, - }) + actorID = gpbft.ActorID(idAddr) + case errors.Is(err, address.ErrUnknownNetwork), + errors.Is(err, address.ErrUnknownProtocol): + // Try parsing as uint64 straight up. + id, err := strconv.ParseUint(stringID, 10, 64) + if err != nil { + return fmt.Errorf("parsing as uint64 %q: %w", stringID, err) + } + actorID = gpbft.ActorID(id) + default: + return fmt.Errorf("parsing address %q: %w", stringID, err) + } + // Prune duplicate IDs. + if _, ok := seenIDs[actorID]; ok { + continue + } + seenIDs[actorID] = struct{}{} + scaled, key := pt.Get(actorID) + if key == nil { + return fmt.Errorf("actor ID %q not found in power table", actorID) + } + result.ScaledSum += scaled + } + result.Proportion = float64(result.ScaledSum) / float64(result.PowerTable.ScaledTotal) + output, err := json.MarshalIndent(result, "", " ") + if err != nil { + return fmt.Errorf("marshalling f3 power table at instance %d: %w", instance, err) + } + _, _ = fmt.Fprint(cctx.App.Writer, string(output)) + return nil + }, + }, + }, +} +var f3SubCmdCerts = &cli.Command{ + Name: "certs", + Aliases: []string{"c"}, + Usage: "Manages interactions with F3 finality certificates.", + Subcommands: []*cli.Command{ + { + Name: "get", + Usage: "Gets an F3 finality certificate to a given instance ID, " + + "or the latest certificate if no instance is specified.", + ArgsUsage: "[instance]", + Flags: []cli.Flag{ + f3FlagOutput, + }, + Before: func(cctx *cli.Context) error { + if count := cctx.NArg(); count > 1 { + return fmt.Errorf("too many arguments: expected at most 1 but got %d", count) + } + return nil + }, + Action: func(cctx *cli.Context) error { + api, closer, err := GetFullNodeAPIV1(cctx) + if err != nil { + return err + } + defer closer() + + // Get the certificate, either for the given instance or the latest if no + // instance is specified. + var cert *certs.FinalityCertificate + if cctx.Args().Present() { + var instance uint64 + instance, err = strconv.ParseUint(cctx.Args().First(), 10, 64) + if err != nil { + return fmt.Errorf("parsing instance: %w", err) } - return tw.Flush(cctx.App.Writer) - }, + cert, err = api.F3GetCertificate(cctx.Context, instance) + } else { + cert, err = api.F3GetLatestCertificate(cctx.Context) + } + if err != nil { + return fmt.Errorf("getting finality certificate: %w", err) + } + if cert == nil { + _, _ = fmt.Fprintln(cctx.App.ErrWriter, "No certificate.") + return nil + } + + return outputFinalityCertificate(cctx, cert) }, - { - Name: "certs", - Aliases: []string{"c"}, - Usage: "Manages interactions with F3 finality certificates.", - Subcommands: []*cli.Command{ - { - Name: "get", - Usage: "Gets an F3 finality certificate to a given instance ID, " + - "or the latest certificate if no instance is specified.", - ArgsUsage: "[instance]", - Flags: []cli.Flag{ - f3FlagOutput, - }, - Before: func(cctx *cli.Context) error { - if count := cctx.NArg(); count > 1 { - return fmt.Errorf("too many arguments: expected at most 1 but got %d", count) - } - return nil - }, - Action: func(cctx *cli.Context) error { - api, closer, err := GetFullNodeAPIV1(cctx) - if err != nil { - return err - } - defer closer() - - // Get the certificate, either for the given instance or the latest if no - // instance is specified. - var cert *certs.FinalityCertificate - if cctx.Args().Present() { - var instance uint64 - instance, err = strconv.ParseUint(cctx.Args().First(), 10, 64) - if err != nil { - return fmt.Errorf("parsing instance: %w", err) - } - cert, err = api.F3GetCertificate(cctx.Context, instance) - } else { - cert, err = api.F3GetLatestCertificate(cctx.Context) - } - if err != nil { - return fmt.Errorf("getting finality certificate: %w", err) - } - if cert == nil { - _, _ = fmt.Fprintln(cctx.App.ErrWriter, "No certificate.") - return nil - } - - return outputFinalityCertificate(cctx, cert) - }, - }, - { - Name: "list", - Usage: `Lists a range of F3 finality certificates. + }, + { + Name: "list", + Usage: `Lists a range of F3 finality certificates. By default the certificates are listed in newest to oldest order, i.e. descending instance IDs. The order may be reversed using the @@ -158,179 +390,217 @@ Examples: * All certificates from instance 3 to 1413 in order of newest to oldest: $ lotus f3 certs list 3..1413 `, - ArgsUsage: "[range]", - Flags: []cli.Flag{ - f3FlagOutput, - f3FlagInstanceLimit, - f3FlagReverseOrder, - }, - Before: func(cctx *cli.Context) error { - if count := cctx.NArg(); count > 1 { - return fmt.Errorf("too many arguments: expected at most 1 but got %d", count) - } - return nil - }, - Action: func(cctx *cli.Context) error { - api, closer, err := GetFullNodeAPIV1(cctx) - if err != nil { - return err - } - defer closer() - - limit := cctx.Int(f3FlagInstanceLimit.Name) - reverse := cctx.Bool(f3FlagReverseOrder.Name) - fromTo := cctx.Args().First() - if fromTo == "" { - fromTo = "0.." - if !cctx.IsSet(f3FlagInstanceLimit.Name) { - // Default to limit of 10 if no explicit range and limit is given. - limit = 10 - } - } - r, err := newRanger(fromTo, limit, reverse, func() (uint64, error) { - latest, err := api.F3GetLatestCertificate(cctx.Context) - if err != nil { - return 0, fmt.Errorf("getting latest finality certificate: %w", err) - } - if latest == nil { - return 0, errors.New("no latest finality certificate") - } - return latest.GPBFTInstance, nil - }) - if err != nil { - return err - } - - var cert *certs.FinalityCertificate - for cctx.Context.Err() == nil { - next, proceed := r.next() - if !proceed { - return nil - } - cert, err = api.F3GetCertificate(cctx.Context, next) - if err != nil { - return fmt.Errorf("getting finality certificate for instance %d: %w", next, err) - } - if cert == nil { - // This is unexpected, because the range of iteration was determined earlier and - // certstore should to have all the certs. Error out. - return fmt.Errorf("nil finality certificate for instance %d", next) - } - if err := outputFinalityCertificate(cctx, cert); err != nil { - return err - } - _, _ = fmt.Fprintln(cctx.App.Writer) - } - return nil - }, - }, - }, + ArgsUsage: "[range]", + Flags: []cli.Flag{ + f3FlagOutput, + f3FlagInstanceLimit, + f3FlagReverseOrder, }, - { - Name: "manifest", - Usage: "Gets the current manifest used by F3.", - Flags: []cli.Flag{f3FlagOutput}, - Action: func(cctx *cli.Context) error { - api, closer, err := GetFullNodeAPIV1(cctx) - if err != nil { - return err - } - defer closer() - - manifest, err := api.F3GetManifest(cctx.Context) - if err != nil { - return fmt.Errorf("getting manifest: %w", err) - } - switch output := cctx.String(f3FlagOutput.Name); strings.ToLower(output) { - case "text": - return prettyPrintManifest(cctx.App.Writer, manifest) - case "json": - encoder := json.NewEncoder(cctx.App.Writer) - encoder.SetIndent("", " ") - return encoder.Encode(manifest) - default: - return fmt.Errorf("unknown output format: %s", output) - } - }, + Before: func(cctx *cli.Context) error { + if count := cctx.NArg(); count > 1 { + return fmt.Errorf("too many arguments: expected at most 1 but got %d", count) + } + return nil }, - { - Name: "status", - Usage: "Checks the F3 status.", - Action: func(cctx *cli.Context) error { - api, closer, err := GetFullNodeAPIV1(cctx) - if err != nil { - return err + Action: func(cctx *cli.Context) error { + api, closer, err := GetFullNodeAPIV1(cctx) + if err != nil { + return err + } + defer closer() + + limit := cctx.Int(f3FlagInstanceLimit.Name) + reverse := cctx.Bool(f3FlagReverseOrder.Name) + fromTo := cctx.Args().First() + if fromTo == "" { + fromTo = "0.." + if !cctx.IsSet(f3FlagInstanceLimit.Name) { + // Default to limit of 10 if no explicit range and limit is given. + limit = 10 } - defer closer() - - running, err := api.F3IsRunning(cctx.Context) + } + r, err := newRanger(fromTo, limit, reverse, func() (uint64, error) { + latest, err := api.F3GetLatestCertificate(cctx.Context) if err != nil { - return fmt.Errorf("getting running state: %w", err) + return 0, fmt.Errorf("getting latest finality certificate: %w", err) } - _, _ = fmt.Fprintf(cctx.App.Writer, "Running: %t\n", running) - if !running { + if latest == nil { + return 0, errors.New("no latest finality certificate") + } + return latest.GPBFTInstance, nil + }) + if err != nil { + return err + } + + var cert *certs.FinalityCertificate + for cctx.Context.Err() == nil { + next, proceed := r.next() + if !proceed { return nil } - - progress, err := api.F3GetProgress(cctx.Context) + cert, err = api.F3GetCertificate(cctx.Context, next) if err != nil { - return fmt.Errorf("getting progress: %w", err) + return fmt.Errorf("getting finality certificate for instance %d: %w", next, err) } - - _, _ = fmt.Fprintln(cctx.App.Writer, "Progress:") - _, _ = fmt.Fprintf(cctx.App.Writer, " Instance: %d\n", progress.ID) - _, _ = fmt.Fprintf(cctx.App.Writer, " Round: %d\n", progress.Round) - _, _ = fmt.Fprintf(cctx.App.Writer, " Phase: %s\n", progress.Phase) - - manifest, err := api.F3GetManifest(cctx.Context) - if err != nil { - return fmt.Errorf("getting manifest: %w", err) + if cert == nil { + // This is unexpected, because the range of iteration was determined earlier and + // certstore should to have all the certs. Error out. + return fmt.Errorf("nil finality certificate for instance %d", next) + } + if err := outputFinalityCertificate(cctx, cert); err != nil { + return err } - return prettyPrintManifest(cctx.App.Writer, manifest) - }, + _, _ = fmt.Fprintln(cctx.App.Writer) + } + return nil }, }, + }, +} +var f3SubCmdManifest = &cli.Command{ + Name: "manifest", + Usage: "Gets the current manifest used by F3.", + Flags: []cli.Flag{f3FlagOutput}, + Action: func(cctx *cli.Context) error { + api, closer, err := GetFullNodeAPIV1(cctx) + if err != nil { + return err + } + defer closer() + + manifest, err := api.F3GetManifest(cctx.Context) + if err != nil { + return fmt.Errorf("getting manifest: %w", err) + } + switch output := cctx.String(f3FlagOutput.Name); strings.ToLower(output) { + case "text": + return prettyPrintManifest(cctx.App.Writer, manifest) + case "json": + encoder := json.NewEncoder(cctx.App.Writer) + encoder.SetIndent("", " ") + return encoder.Encode(manifest) + default: + return fmt.Errorf("unknown output format: %s", output) + } + }, +} +var f3SubCmdStatus = &cli.Command{ + Name: "status", + Usage: "Checks the F3 status.", + Action: func(cctx *cli.Context) error { + api, closer, err := GetFullNodeAPIV1(cctx) + if err != nil { + return err + } + defer closer() + + running, err := api.F3IsRunning(cctx.Context) + if err != nil { + return fmt.Errorf("getting running state: %w", err) + } + _, _ = fmt.Fprintf(cctx.App.Writer, "Running: %t\n", running) + if !running { + return nil + } + + progress, err := api.F3GetProgress(cctx.Context) + if err != nil { + return fmt.Errorf("getting progress: %w", err) + } + + _, _ = fmt.Fprintln(cctx.App.Writer, "Progress:") + _, _ = fmt.Fprintf(cctx.App.Writer, " Instance: %d\n", progress.ID) + _, _ = fmt.Fprintf(cctx.App.Writer, " Round: %d\n", progress.Round) + _, _ = fmt.Fprintf(cctx.App.Writer, " Phase: %s\n", progress.Phase) + + manifest, err := api.F3GetManifest(cctx.Context) + if err != nil { + return fmt.Errorf("getting manifest: %w", err) + } + return prettyPrintManifest(cctx.App.Writer, manifest) + }, +} + +// TODO: we should standardise format as a top level flag. For now, here is an f3 +// +// specific one. +// See: https://github.com/filecoin-project/lotus/issues/12616 +var f3FlagOutput = &cli.StringFlag{ + Name: "output", + Usage: "The output format. Supported formats: text, json", + Value: "text", + Action: func(cctx *cli.Context, output string) error { + switch output { + case "text", "json": + return nil + default: + return fmt.Errorf("unknown output format: %s", output) + } + }, +} +var f3FlagInstanceLimit = &cli.IntFlag{ + Name: "limit", + Usage: "The maximum number of instances. A value less than 0 indicates no limit.", + DefaultText: "10 when no range is specified. Otherwise, unlimited.", + Value: -1, +} +var f3FlagReverseOrder = &cli.BoolFlag{ + Name: "reverse", + Usage: "Reverses the default order of output. ", +} +var f3FlagPowerTableFromEC = &cli.BoolFlag{ + Name: "ec", + Usage: "Whether to get the power table from EC.", +} +var f3FlagInstanceID = &cli.Uint64Flag{ + Name: "instance", + Aliases: []string{"i"}, + Usage: "The F3 instance ID.", + DefaultText: "Latest Instance", +} + +//go:embed templates/f3_*.go.tmpl +var f3TemplatesFS embed.FS +var f3Templates = template.Must( + template.New(""). + Funcs(template.FuncMap{ + "ptDiffToString": f3PowerTableDiffsToString, + "tipSetKeyToLotusTipSetKey": types.TipSetKeyFromBytes, + "add": func(a, b int) int { return a + b }, + "sub": func(a, b int) int { return a - b }, + }). + ParseFS(f3TemplatesFS, "templates/f3_*.go.tmpl"), +) + +func f3GetPowerTableTSKByInstance(ctx context.Context, api v1api.FullNode, instance uint64) (types.TipSetKey, cid.Cid, error) { + mfst, err := api.F3GetManifest(ctx) + if err != nil { + return types.EmptyTSK, cid.Undef, fmt.Errorf("getting manifest: %w", err) } - // TODO: we should standardise format as a top level flag. For now, here is an f3 - // specific one. - // See: https://github.com/filecoin-project/lotus/issues/12616 - f3FlagOutput = &cli.StringFlag{ - Name: "output", - Usage: "The output format. Supported formats: text, json", - Value: "text", - Action: func(cctx *cli.Context, output string) error { - switch output { - case "text", "json": - return nil - default: - return fmt.Errorf("unknown output format: %s", output) - } - }, + if instance < mfst.InitialInstance+mfst.CommitteeLookback { + ts, err := api.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(mfst.BootstrapEpoch-mfst.EC.Finality), types.EmptyTSK) + if err != nil { + return types.EmptyTSK, cid.Undef, fmt.Errorf("getting bootstrap epoch tipset: %w", err) + } + return ts.Key(), mfst.InitialPowerTable, nil } - f3FlagInstanceLimit = &cli.IntFlag{ - Name: "limit", - Usage: "The maximum number of instances. A value less than 0 indicates no limit.", - DefaultText: "10 when no range is specified. Otherwise, unlimited.", - Value: -1, + + previous, err := api.F3GetCertificate(ctx, instance-1) + if err != nil { + return types.EmptyTSK, cid.Undef, fmt.Errorf("getting certificate for previous instance: %w", err) } - f3FlagReverseOrder = &cli.BoolFlag{ - Name: "reverse", - Usage: "Reverses the default order of output. ", + lookback, err := api.F3GetCertificate(ctx, instance-mfst.CommitteeLookback) + if err != nil { + return types.EmptyTSK, cid.Undef, fmt.Errorf("getting certificate for lookback instance: %w", err) } - //go:embed templates/f3_*.go.tmpl - f3TemplatesFS embed.FS - f3Templates = template.Must( - template.New(""). - Funcs(template.FuncMap{ - "ptDiffToString": f3PowerTableDiffsToString, - "tipSetKeyToLotusTipSetKey": types.TipSetKeyFromBytes, - "add": func(a, b int) int { return a + b }, - "sub": func(a, b int) int { return a - b }, - }). - ParseFS(f3TemplatesFS, "templates/f3_*.go.tmpl"), - ) -) + ltsk, err := types.TipSetKeyFromBytes(lookback.ECChain.Head().Key) + if err != nil { + return types.EmptyTSK, cid.Undef, fmt.Errorf("getting lotus tipset key from head of lookback certificate: %w", err) + } + return ltsk, previous.SupplementalData.PowerTable, nil +} func outputFinalityCertificate(cctx *cli.Context, cert *certs.FinalityCertificate) error { diff --git a/documentation/en/cli-lotus.md b/documentation/en/cli-lotus.md index a6e5b701225..e8e6f8d7004 100644 --- a/documentation/en/cli-lotus.md +++ b/documentation/en/cli-lotus.md @@ -2799,6 +2799,7 @@ USAGE: COMMANDS: list-miners, lm Lists the miners that currently participate in F3 via this node. + powertable, pt certs, c Manages interactions with F3 finality certificates. manifest Gets the current manifest used by F3. status Checks the F3 status. @@ -2820,6 +2821,50 @@ OPTIONS: --help, -h show help ``` +### lotus f3 powertable +``` +NAME: + lotus f3 powertable + +USAGE: + lotus f3 powertable command [command options] [arguments...] + +COMMANDS: + get, g Get F3 power table at a specific instance ID or latest instance if none is specified. + get-proportion, gp Gets the total proportion of power for a list of actors at a given instance. + help, h Shows a list of commands or help for one command + +OPTIONS: + --help, -h show help +``` + +#### lotus f3 powertable get +``` +NAME: + lotus f3 powertable get - Get F3 power table at a specific instance ID or latest instance if none is specified. + +USAGE: + lotus f3 powertable get [command options] [instance] + +OPTIONS: + --ec Whether to get the power table from EC. (default: false) + --help, -h show help +``` + +#### lotus f3 powertable get-proportion +``` +NAME: + lotus f3 powertable get-proportion - Gets the total proportion of power for a list of actors at a given instance. + +USAGE: + lotus f3 powertable get-proportion [command options] [actor-id] ... + +OPTIONS: + --ec Whether to get the power table from EC. (default: false) + --instance value, -i value The F3 instance ID. (default: Latest Instance) + --help, -h show help +``` + ### lotus f3 certs ``` NAME: