Merge pull request #5847 from filecoin-project/steb/audit-dups
shed: command to list duplicate messages in tipsets (steb)
This commit is contained in:
commit
b23837a7d6
@ -3,11 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/filecoin-project/lotus/build"
|
"github.com/filecoin-project/lotus/build"
|
||||||
@ -70,6 +73,266 @@ var auditsCmd = &cli.Command{
|
|||||||
chainBalanceStateCmd,
|
chainBalanceStateCmd,
|
||||||
chainPledgeCmd,
|
chainPledgeCmd,
|
||||||
fillBalancesCmd,
|
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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user