diff --git a/go.mod b/go.mod index fe965286d..ede2d7788 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ require ( github.com/abiosoft/ishell/v2 v2.0.2 github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db github.com/btcsuite/btcd v0.22.0-beta + github.com/davecgh/go-spew v1.1.1 github.com/gorilla/websocket v1.4.2 github.com/hashicorp/golang-lru v0.5.4 github.com/holiman/uint256 v1.2.0 @@ -13,6 +14,7 @@ require ( github.com/nspcc-dev/neofs-sdk-go v0.0.0-20220113123743-7f3162110659 github.com/nspcc-dev/rfc6979 v0.2.0 github.com/pierrec/lz4 v2.6.1+incompatible + github.com/pmezard/go-difflib v1.0.0 github.com/prometheus/client_golang v1.11.0 github.com/stretchr/testify v1.7.0 github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 diff --git a/scripts/compare-dumps_test.go b/scripts/compare-dumps/compare-dumps_test.go similarity index 100% rename from scripts/compare-dumps_test.go rename to scripts/compare-dumps/compare-dumps_test.go diff --git a/scripts/compare-states/compare-states.go b/scripts/compare-states/compare-states.go new file mode 100644 index 000000000..0fdb12181 --- /dev/null +++ b/scripts/compare-states/compare-states.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/davecgh/go-spew/spew" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/pmezard/go-difflib/difflib" + "github.com/urfave/cli" +) + +func initClient(addr string, name string) (*client.Client, uint32, error) { + c, err := client.New(context.Background(), addr, client.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 *client.Client, cb *client.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 *client.Client, cb *client.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("state matches at %d", 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 + } + if ha != hb { + return errors.New("chains have different heights") + } + h, err := bisectState(ca, cb, ha-1) + if err != 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()) + for _, t := range blk.Transactions { + fmt.Printf("transaction %s:\n", t.Hash().StringLE()) + la, err := ca.GetApplicationLog(t.Hash(), nil) + if err != nil { + return err + } + lb, err := cb.GetApplicationLog(t.Hash(), 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 + + if err := ctl.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(os.Stderr, ctl.Usage) + os.Exit(1) + } +}