Skip to content

Commit 703333c

Browse files
authored
feat: implement Lotus F3 CLI finality cert get and list (#12627)
Implement `lotus f3` CLI sub commands to: * Get a specific finality certificate, either latest or by instance ID. * List a range of finality certificates Part of #12607
1 parent e04dbe1 commit 703333c

File tree

5 files changed

+472
-44
lines changed

5 files changed

+472
-44
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- `lotus chain head` now supports a `--height` flag to print just the epoch number of the current chain head ([filecoin-project/lotus#12609](https://github.com/filecoin-project/lotus/pull/12609))
1111
- `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 confirms all the event data is byte-perfect. ([filecoin-project/lotus#12570](https://github.com/filecoin-project/lotus/pull/12570))
1212
- Expose APIs to list the miner IDs that are currently participating in F3 via node. ([filecoin-project/lotus#12608](https://github.com/filecoin-project/lotus/pull/12608))
13-
- Implement new `lotus f3` CLI commands to list F3 participants, dump manifest and check the F3 status. ([filecoin-project/lotus#12617](https://github.com/filecoin-project/lotus/pull/12617))
13+
- Implement new `lotus f3` CLI commands to list F3 participants, dump manifest, get/list finality certificates and check the F3 status. ([filecoin-project/lotus#12617](https://github.com/filecoin-project/lotus/pull/12617), [filecoin-project/lotus#12627](https://github.com/filecoin-project/lotus/pull/12627))
1414

1515
## Bug Fixes
1616
- Fix a bug in the `lotus-shed indexes backfill-events` command that may result in either duplicate events being backfilled where there are existing events (such an operation *should* be idempotent) or events erroneously having duplicate `logIndex` values when queried via ETH APIs. ([filecoin-project/lotus#12567](https://github.com/filecoin-project/lotus/pull/12567))

cli/f3.go

Lines changed: 291 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
package cli
22

33
import (
4+
"embed"
45
"encoding/json"
6+
"errors"
57
"fmt"
68
"io"
9+
"strconv"
710
"strings"
811
"text/template"
912

1013
"github.com/urfave/cli/v2"
1114

15+
"github.com/filecoin-project/go-address"
16+
"github.com/filecoin-project/go-f3/certs"
17+
"github.com/filecoin-project/go-f3/gpbft"
1218
"github.com/filecoin-project/go-f3/manifest"
1319

20+
"github.com/filecoin-project/lotus/chain/types"
1421
"github.com/filecoin-project/lotus/lib/tablewriter"
1522
)
1623

@@ -21,7 +28,7 @@ var (
2128
Subcommands: []*cli.Command{
2229
{
2330
Name: "list-miners",
24-
Aliases: []string{"lp"},
31+
Aliases: []string{"lm"},
2532
Usage: "Lists the miners that currently participate in F3 via this node.",
2633
Action: func(cctx *cli.Context) error {
2734
api, closer, err := GetFullNodeAPIV1(cctx)
@@ -39,25 +46,181 @@ var (
3946
return err
4047
}
4148
const (
42-
idColumn = "ID"
43-
fromColumn = "From"
44-
ToColumn = "To"
49+
miner = "Miner"
50+
from = "From"
51+
to = "To"
4552
)
4653
tw := tablewriter.New(
47-
tablewriter.Col(idColumn),
48-
tablewriter.Col(fromColumn),
49-
tablewriter.Col(ToColumn),
54+
tablewriter.Col(miner),
55+
tablewriter.Col(from),
56+
tablewriter.Col(to),
5057
)
5158
for _, participant := range miners {
59+
addr, err := address.NewIDAddress(participant.MinerID)
60+
if err != nil {
61+
return fmt.Errorf("converting miner ID to address: %w", err)
62+
}
63+
5264
tw.Write(map[string]interface{}{
53-
idColumn: participant.MinerID,
54-
fromColumn: participant.FromInstance,
55-
ToColumn: participant.FromInstance + participant.ValidityTerm,
65+
miner: addr,
66+
from: participant.FromInstance,
67+
to: participant.FromInstance + participant.ValidityTerm,
5668
})
5769
}
5870
return tw.Flush(cctx.App.Writer)
5971
},
6072
},
73+
{
74+
Name: "certs",
75+
Aliases: []string{"c"},
76+
Usage: "Manages interactions with F3 finality certificates.",
77+
Subcommands: []*cli.Command{
78+
{
79+
Name: "get",
80+
Usage: "Gets an F3 finality certificate to a given instance ID, " +
81+
"or the latest certificate if no instance is specified.",
82+
ArgsUsage: "[instance]",
83+
Flags: []cli.Flag{
84+
f3FlagOutput,
85+
},
86+
Before: func(cctx *cli.Context) error {
87+
if count := cctx.NArg(); count > 1 {
88+
return fmt.Errorf("too many arguments: expected at most 1 but got %d", count)
89+
}
90+
return nil
91+
},
92+
Action: func(cctx *cli.Context) error {
93+
api, closer, err := GetFullNodeAPIV1(cctx)
94+
if err != nil {
95+
return err
96+
}
97+
defer closer()
98+
99+
// Get the certificate, either for the given instance or the latest if no
100+
// instance is specified.
101+
var cert *certs.FinalityCertificate
102+
if cctx.Args().Present() {
103+
var instance uint64
104+
instance, err = strconv.ParseUint(cctx.Args().First(), 10, 64)
105+
if err != nil {
106+
return fmt.Errorf("parsing instance: %w", err)
107+
}
108+
cert, err = api.F3GetCertificate(cctx.Context, instance)
109+
} else {
110+
cert, err = api.F3GetLatestCertificate(cctx.Context)
111+
}
112+
if err != nil {
113+
return fmt.Errorf("getting finality certificate: %w", err)
114+
}
115+
if cert == nil {
116+
_, _ = fmt.Fprintln(cctx.App.ErrWriter, "No certificate.")
117+
return nil
118+
}
119+
120+
return outputFinalityCertificate(cctx, cert)
121+
},
122+
},
123+
{
124+
Name: "list",
125+
Usage: `Lists a range of F3 finality certificates.
126+
127+
By default the certificates are listed in newest to oldest order,
128+
i.e. descending instance IDs. The order may be reversed using the
129+
'--reverse' flag.
130+
131+
A range may optionally be specified as the first argument to indicate
132+
inclusive range of 'from' and 'to' instances in following notation:
133+
'<from>..<to>'. Either <from> or <to> may be omitted, but not both.
134+
An omitted <from> value is always interpreted as 0, and an omitted
135+
<to> value indicates the latest instance. If both are specified, <from>
136+
must never exceed <to>.
137+
138+
If no range is specified all certificates are listed, i.e. the range
139+
of '0..'.
140+
141+
Examples:
142+
* All certificates from newest to oldest:
143+
$ lotus f3 certs list 0..
144+
145+
* Three newest certificates:
146+
$ lotus f3 certs list --limit 3 0..
147+
148+
* Three oldest certificates:
149+
$ lotus f3 certs list --limit 3 --reverse 0..
150+
151+
* Up to three certificates starting from instance 1413 to the oldest:
152+
$ lotus f3 certs list --limit 3 ..1413
153+
154+
* Up to 3 certificates starting from instance 1413 to the newest:
155+
$ lotus f3 certs list --limit 3 --reverse 1413..
156+
157+
* All certificates from instance 3 to 1413 in order of newest to oldest:
158+
$ lotus f3 certs list 3..1413
159+
`,
160+
ArgsUsage: "[range]",
161+
Flags: []cli.Flag{
162+
f3FlagOutput,
163+
f3FlagInstanceLimit,
164+
f3FlagReverseOrder,
165+
},
166+
Before: func(cctx *cli.Context) error {
167+
if count := cctx.NArg(); count > 1 {
168+
return fmt.Errorf("too many arguments: expected at most 1 but got %d", count)
169+
}
170+
return nil
171+
},
172+
Action: func(cctx *cli.Context) error {
173+
api, closer, err := GetFullNodeAPIV1(cctx)
174+
if err != nil {
175+
return err
176+
}
177+
defer closer()
178+
179+
limit := cctx.Int(f3FlagInstanceLimit.Name)
180+
reverse := cctx.Bool(f3FlagReverseOrder.Name)
181+
fromTo := cctx.Args().First()
182+
if fromTo == "" {
183+
fromTo = "0.."
184+
}
185+
r, err := newRanger(fromTo, limit, reverse, func() (uint64, error) {
186+
latest, err := api.F3GetLatestCertificate(cctx.Context)
187+
if err != nil {
188+
return 0, fmt.Errorf("getting latest finality certificate: %w", err)
189+
}
190+
if latest == nil {
191+
return 0, errors.New("no latest finality certificate")
192+
}
193+
return latest.GPBFTInstance, nil
194+
})
195+
if err != nil {
196+
return err
197+
}
198+
199+
var cert *certs.FinalityCertificate
200+
for cctx.Context.Err() == nil {
201+
next, proceed := r.next()
202+
if !proceed {
203+
return nil
204+
}
205+
cert, err = api.F3GetCertificate(cctx.Context, next)
206+
if err != nil {
207+
return fmt.Errorf("getting finality certificate for instance %d: %w", next, err)
208+
}
209+
if cert == nil {
210+
// This is unexpected, because the range of iteration was determined earlier and
211+
// certstore should to have all the certs. Error out.
212+
return fmt.Errorf("nil finality certificate for instance %d", next)
213+
}
214+
if err := outputFinalityCertificate(cctx, cert); err != nil {
215+
return err
216+
}
217+
_, _ = fmt.Fprintln(cctx.App.Writer)
218+
}
219+
return nil
220+
},
221+
},
222+
},
223+
},
61224
{
62225
Name: "manifest",
63226
Usage: "Gets the current manifest used by F3.",
@@ -140,46 +303,132 @@ var (
140303
}
141304
},
142305
}
306+
f3FlagInstanceLimit = &cli.IntFlag{
307+
Name: "limit",
308+
Usage: "The maximum number of instances. A value less than 0 indicates no limit.",
309+
DefaultText: "No limit",
310+
Value: -1,
311+
}
312+
f3FlagReverseOrder = &cli.BoolFlag{
313+
Name: "reverse",
314+
Usage: "Reverses the default order of output. ",
315+
}
316+
//go:embed templates/f3_*.go.tmpl
317+
f3TemplatesFS embed.FS
318+
f3Templates = template.Must(
319+
template.New("").
320+
Funcs(template.FuncMap{
321+
"ptDiffToString": f3PowerTableDiffsToString,
322+
"tipSetKeyToLotusTipSetKey": types.TipSetKeyFromBytes,
323+
"add": func(a, b int) int { return a + b },
324+
"sub": func(a, b int) int { return a - b },
325+
}).
326+
ParseFS(f3TemplatesFS, "templates/f3_*.go.tmpl"),
327+
)
143328
)
144329

330+
func outputFinalityCertificate(cctx *cli.Context, cert *certs.FinalityCertificate) error {
331+
332+
switch output := cctx.String(f3FlagOutput.Name); strings.ToLower(output) {
333+
case "text":
334+
return prettyPrintFinalityCertificate(cctx.App.Writer, cert)
335+
case "json":
336+
encoder := json.NewEncoder(cctx.App.Writer)
337+
encoder.SetIndent("", " ")
338+
return encoder.Encode(cert)
339+
default:
340+
return fmt.Errorf("unknown output format: %s", output)
341+
}
342+
}
343+
145344
func prettyPrintManifest(out io.Writer, manifest *manifest.Manifest) error {
146345
if manifest == nil {
147346
_, err := fmt.Fprintln(out, "Manifest: None")
148347
return err
149348
}
150349

151-
const manifestTemplate = `Manifest:
152-
Protocol Version: {{.ProtocolVersion}}
153-
Paused: {{.Pause}}
154-
Initial Instance: {{.InitialInstance}}
155-
Initial Power Table: {{if .InitialPowerTable.Defined}}{{.InitialPowerTable}}{{else}}unknown{{end}}
156-
Bootstrap Epoch: {{.BootstrapEpoch}}
157-
Network Name: {{.NetworkName}}
158-
Ignore EC Power: {{.IgnoreECPower}}
159-
Committee Lookback: {{.CommitteeLookback}}
160-
Catch Up Alignment: {{.CatchUpAlignment}}
161-
162-
GPBFT Delta: {{.Gpbft.Delta}}
163-
GPBFT Delta BackOff Exponent: {{.Gpbft.DeltaBackOffExponent}}
164-
GPBFT Max Lookahead Rounds: {{.Gpbft.MaxLookaheadRounds}}
165-
GPBFT Rebroadcast Backoff Base: {{.Gpbft.RebroadcastBackoffBase}}
166-
GPBFT Rebroadcast Backoff Spread: {{.Gpbft.RebroadcastBackoffSpread}}
167-
GPBFT Rebroadcast Backoff Max: {{.Gpbft.RebroadcastBackoffMax}}
168-
169-
EC Period: {{.EC.Period}}
170-
EC Finality: {{.EC.Finality}}
171-
EC Delay Multiplier: {{.EC.DelayMultiplier}}
172-
EC Head Lookback: {{.EC.HeadLookback}}
173-
EC Finalize: {{.EC.Finalize}}
174-
175-
Certificate Exchange Client Timeout: {{.CertificateExchange.ClientRequestTimeout}}
176-
Certificate Exchange Server Timeout: {{.CertificateExchange.ServerRequestTimeout}}
177-
Certificate Exchange Min Poll Interval: {{.CertificateExchange.MinimumPollInterval}}
178-
Certificate Exchange Max Poll Interval: {{.CertificateExchange.MaximumPollInterval}}
179-
`
180-
t, err := template.New("manifest").Parse(manifestTemplate)
350+
return f3Templates.ExecuteTemplate(out, "f3_manifest.go.tmpl", manifest)
351+
}
352+
353+
func prettyPrintFinalityCertificate(out io.Writer, cert *certs.FinalityCertificate) error {
354+
if cert == nil {
355+
_, _ = fmt.Fprintln(out, "Certificate: None")
356+
return nil
357+
}
358+
return f3Templates.ExecuteTemplate(out, "f3_finality_cert.go.tmpl", cert)
359+
}
360+
361+
func f3PowerTableDiffsToString(diff certs.PowerTableDiff) (string, error) {
362+
if len(diff) == 0 {
363+
return "None", nil
364+
}
365+
totalDiff := gpbft.NewStoragePower(0).Int
366+
for _, delta := range diff {
367+
if !delta.IsZero() {
368+
totalDiff = totalDiff.Add(totalDiff, delta.PowerDelta.Int)
369+
}
370+
}
371+
if totalDiff.Cmp(gpbft.NewStoragePower(0).Int) == 0 {
372+
return "None", nil
373+
}
374+
return fmt.Sprintf("Total of %s storage power across %d miner(s).", totalDiff, len(diff)), nil
375+
}
376+
377+
type ranger struct {
378+
from, to uint64
379+
limit int
380+
ascend bool
381+
}
382+
383+
func newRanger(fromTo string, limit int, ascend bool, latest func() (uint64, error)) (*ranger, error) {
384+
parts := strings.Split(strings.TrimSpace(fromTo), "..")
385+
if len(parts) != 2 || (parts[0] == "" && parts[1] == "") {
386+
return nil, fmt.Errorf("invalid range format: expected '<from>..<to>', got: %s", fromTo)
387+
}
388+
389+
r := ranger{
390+
limit: limit,
391+
ascend: ascend,
392+
}
393+
var err error
394+
if parts[0] != "" {
395+
r.from, err = strconv.ParseUint(parts[0], 10, 64)
396+
if err != nil {
397+
return nil, fmt.Errorf("determining 'from' instance: %v", err)
398+
}
399+
}
400+
if parts[1] != "" {
401+
r.to, err = strconv.ParseUint(parts[1], 10, 64)
402+
} else {
403+
r.to, err = latest()
404+
}
181405
if err != nil {
182-
return fmt.Errorf("failed to parse manifest template: %w", err)
406+
return nil, fmt.Errorf("determining 'to' instance: %v", err)
407+
}
408+
if r.from > r.to {
409+
return nil, fmt.Errorf("invalid range: 'from' cannot exceed 'to': %d > %d", r.from, r.to)
410+
}
411+
if !ascend {
412+
r.from, r.to = r.to, r.from
413+
}
414+
return &r, nil
415+
}
416+
417+
func (r *ranger) next() (uint64, bool) {
418+
if r.limit == 0 {
419+
return 0, false
420+
}
421+
422+
next := r.from
423+
if r.from == r.to {
424+
r.limit = 0
425+
return next, true
426+
}
427+
if r.ascend {
428+
r.from++
429+
} else if r.from > 0 {
430+
r.from--
183431
}
184-
return t.ExecuteTemplate(out, "manifest", manifest)
432+
r.limit--
433+
return next, true
185434
}

0 commit comments

Comments
 (0)