a32217fac8
Should be a part of #3435. Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
180 lines
4.2 KiB
Go
180 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/pmezard/go-difflib/difflib"
|
|
"github.com/urfave/cli"
|
|
)
|
|
|
|
var errStateMatches = errors.New("state matches")
|
|
|
|
func initClient(addr string, name string) (*rpcclient.Client, uint32, error) {
|
|
c, err := rpcclient.New(context.Background(), addr, rpcclient.Options{})
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("RPC %s: %w", name, err)
|
|
}
|
|
err = c.Init()
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("RPC %s init: %w", name, err)
|
|
}
|
|
h, err := c.GetBlockCount()
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("RPC %s block count: %w", name, err)
|
|
}
|
|
return c, h, nil
|
|
}
|
|
|
|
func getRoots(ca *rpcclient.Client, cb *rpcclient.Client, h uint32) (util.Uint256, util.Uint256, error) {
|
|
ra, err := ca.GetStateRootByHeight(h)
|
|
if err != nil {
|
|
return util.Uint256{}, util.Uint256{}, fmt.Errorf("getstateroot from A for %d: %w", h, err)
|
|
}
|
|
rb, err := cb.GetStateRootByHeight(h)
|
|
if err != nil {
|
|
return util.Uint256{}, util.Uint256{}, fmt.Errorf("getstateroot from B for %d: %w", h, err)
|
|
}
|
|
return ra.Root, rb.Root, nil
|
|
}
|
|
|
|
func bisectState(ca *rpcclient.Client, cb *rpcclient.Client, h uint32) (uint32, error) {
|
|
ra, rb, err := getRoots(ca, cb, 0)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
fmt.Printf("at %d: %s vs %s\n", 0, ra.StringLE(), rb.StringLE())
|
|
if ra != rb {
|
|
return 0, nil
|
|
}
|
|
good := uint32(0)
|
|
ra, rb, err = getRoots(ca, cb, h)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
fmt.Printf("at %d: %s vs %s\n", h, ra.StringLE(), rb.StringLE())
|
|
if ra.Equals(rb) {
|
|
return 0, fmt.Errorf("%w at %d", errStateMatches, h)
|
|
}
|
|
bad := h
|
|
for bad-good > 1 {
|
|
next := good + (bad-good)/2
|
|
ra, rb, err = getRoots(ca, cb, next)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
fmt.Printf("at %d: %s vs %s\n", next, ra.StringLE(), rb.StringLE())
|
|
if ra == rb {
|
|
good = next
|
|
} else {
|
|
bad = next
|
|
}
|
|
}
|
|
return bad, nil
|
|
}
|
|
|
|
func cliMain(c *cli.Context) error {
|
|
a := c.Args().Get(0)
|
|
b := c.Args().Get(1)
|
|
if a == "" {
|
|
return errors.New("no arguments given")
|
|
}
|
|
if b == "" {
|
|
return errors.New("missing second argument")
|
|
}
|
|
ca, ha, err := initClient(a, "A")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cb, hb, err := initClient(b, "B")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var refHeight = ha
|
|
if ha != hb {
|
|
var diff = hb - ha
|
|
if ha > hb {
|
|
refHeight = hb
|
|
diff = ha - hb
|
|
}
|
|
if diff > 10 && !c.Bool("ignore-height") { // Allow some height drift.
|
|
return fmt.Errorf("chains have different heights: %d vs %d", ha, hb)
|
|
}
|
|
}
|
|
h, err := bisectState(ca, cb, refHeight-1)
|
|
if err != nil {
|
|
if errors.Is(err, errStateMatches) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
blk, err := ca.GetBlockByIndex(h)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("state differs at %d, block %s\n", h, blk.Hash().StringLE())
|
|
err = dumpApplogDiff(true, blk.Hash(), a, b, ca, cb)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to dump block application log: %w", err)
|
|
}
|
|
for _, t := range blk.Transactions {
|
|
err = dumpApplogDiff(false, t.Hash(), a, b, ca, cb)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to dump application log for tx %s: %w", t.Hash().StringLE(), err)
|
|
}
|
|
}
|
|
return errors.New("different state found")
|
|
}
|
|
|
|
func dumpApplogDiff(isBlock bool, container util.Uint256, a string, b string, ca *rpcclient.Client, cb *rpcclient.Client) error {
|
|
if isBlock {
|
|
fmt.Printf("block %s:\n", container.StringLE())
|
|
} else {
|
|
fmt.Printf("transaction %s:\n", container.StringLE())
|
|
}
|
|
la, err := ca.GetApplicationLog(container, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lb, err := cb.GetApplicationLog(container, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
da := spew.Sdump(la)
|
|
db := spew.Sdump(lb)
|
|
diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
|
|
A: difflib.SplitLines(da),
|
|
B: difflib.SplitLines(db),
|
|
FromFile: a,
|
|
FromDate: "",
|
|
ToFile: b,
|
|
ToDate: "",
|
|
Context: 1,
|
|
})
|
|
fmt.Println(diff)
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
ctl := cli.NewApp()
|
|
ctl.Name = "compare-states"
|
|
ctl.Version = "1.0"
|
|
ctl.Usage = "compare-states RPC_A RPC_B"
|
|
ctl.Action = cliMain
|
|
ctl.Flags = []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "ignore-height, g",
|
|
Usage: "ignore height difference",
|
|
},
|
|
}
|
|
|
|
if err := ctl.Run(os.Args); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|