github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/scripts/compare-states/compare-states.go (about) 1 package main 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 9 "github.com/davecgh/go-spew/spew" 10 "github.com/nspcc-dev/neo-go/pkg/rpcclient" 11 "github.com/nspcc-dev/neo-go/pkg/util" 12 "github.com/pmezard/go-difflib/difflib" 13 "github.com/urfave/cli" 14 ) 15 16 var errStateMatches = errors.New("state matches") 17 18 func initClient(addr string, name string) (*rpcclient.Client, uint32, error) { 19 c, err := rpcclient.New(context.Background(), addr, rpcclient.Options{}) 20 if err != nil { 21 return nil, 0, fmt.Errorf("RPC %s: %w", name, err) 22 } 23 err = c.Init() 24 if err != nil { 25 return nil, 0, fmt.Errorf("RPC %s init: %w", name, err) 26 } 27 h, err := c.GetBlockCount() 28 if err != nil { 29 return nil, 0, fmt.Errorf("RPC %s block count: %w", name, err) 30 } 31 return c, h, nil 32 } 33 34 func getRoots(ca *rpcclient.Client, cb *rpcclient.Client, h uint32) (util.Uint256, util.Uint256, error) { 35 ra, err := ca.GetStateRootByHeight(h) 36 if err != nil { 37 return util.Uint256{}, util.Uint256{}, fmt.Errorf("getstateroot from A for %d: %w", h, err) 38 } 39 rb, err := cb.GetStateRootByHeight(h) 40 if err != nil { 41 return util.Uint256{}, util.Uint256{}, fmt.Errorf("getstateroot from B for %d: %w", h, err) 42 } 43 return ra.Root, rb.Root, nil 44 } 45 46 func bisectState(ca *rpcclient.Client, cb *rpcclient.Client, h uint32) (uint32, error) { 47 ra, rb, err := getRoots(ca, cb, 0) 48 if err != nil { 49 return 0, err 50 } 51 fmt.Printf("at %d: %s vs %s\n", 0, ra.StringLE(), rb.StringLE()) 52 if ra != rb { 53 return 0, nil 54 } 55 good := uint32(0) 56 ra, rb, err = getRoots(ca, cb, h) 57 if err != nil { 58 return 0, err 59 } 60 fmt.Printf("at %d: %s vs %s\n", h, ra.StringLE(), rb.StringLE()) 61 if ra.Equals(rb) { 62 return 0, fmt.Errorf("%w at %d", errStateMatches, h) 63 } 64 bad := h 65 for bad-good > 1 { 66 next := good + (bad-good)/2 67 ra, rb, err = getRoots(ca, cb, next) 68 if err != nil { 69 return 0, err 70 } 71 fmt.Printf("at %d: %s vs %s\n", next, ra.StringLE(), rb.StringLE()) 72 if ra == rb { 73 good = next 74 } else { 75 bad = next 76 } 77 } 78 return bad, nil 79 } 80 81 func cliMain(c *cli.Context) error { 82 a := c.Args().Get(0) 83 b := c.Args().Get(1) 84 if a == "" { 85 return errors.New("no arguments given") 86 } 87 if b == "" { 88 return errors.New("missing second argument") 89 } 90 ca, ha, err := initClient(a, "A") 91 if err != nil { 92 return err 93 } 94 cb, hb, err := initClient(b, "B") 95 if err != nil { 96 return err 97 } 98 var refHeight = ha 99 if ha != hb { 100 var diff = hb - ha 101 if ha > hb { 102 refHeight = hb 103 diff = ha - hb 104 } 105 if diff > 10 && !c.Bool("ignore-height") { // Allow some height drift. 106 return fmt.Errorf("chains have different heights: %d vs %d", ha, hb) 107 } 108 } 109 h, err := bisectState(ca, cb, refHeight-1) 110 if err != nil { 111 if errors.Is(err, errStateMatches) { 112 return nil 113 } 114 return err 115 } 116 blk, err := ca.GetBlockByIndex(h) 117 if err != nil { 118 return err 119 } 120 fmt.Printf("state differs at %d, block %s\n", h, blk.Hash().StringLE()) 121 err = dumpApplogDiff(true, blk.Hash(), a, b, ca, cb) 122 if err != nil { 123 return fmt.Errorf("failed to dump block application log: %w", err) 124 } 125 for _, t := range blk.Transactions { 126 err = dumpApplogDiff(false, t.Hash(), a, b, ca, cb) 127 if err != nil { 128 return fmt.Errorf("failed to dump application log for tx %s: %w", t.Hash().StringLE(), err) 129 } 130 } 131 return errors.New("different state found") 132 } 133 134 func dumpApplogDiff(isBlock bool, container util.Uint256, a string, b string, ca *rpcclient.Client, cb *rpcclient.Client) error { 135 if isBlock { 136 fmt.Printf("block %s:\n", container.StringLE()) 137 } else { 138 fmt.Printf("transaction %s:\n", container.StringLE()) 139 } 140 la, err := ca.GetApplicationLog(container, nil) 141 if err != nil { 142 return err 143 } 144 lb, err := cb.GetApplicationLog(container, nil) 145 if err != nil { 146 return err 147 } 148 da := spew.Sdump(la) 149 db := spew.Sdump(lb) 150 diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ 151 A: difflib.SplitLines(da), 152 B: difflib.SplitLines(db), 153 FromFile: a, 154 FromDate: "", 155 ToFile: b, 156 ToDate: "", 157 Context: 1, 158 }) 159 fmt.Println(diff) 160 return nil 161 } 162 163 func main() { 164 ctl := cli.NewApp() 165 ctl.Name = "compare-states" 166 ctl.Version = "1.0" 167 ctl.Usage = "compare-states RPC_A RPC_B" 168 ctl.Action = cliMain 169 ctl.Flags = []cli.Flag{ 170 cli.BoolFlag{ 171 Name: "ignore-height, g", 172 Usage: "ignore height difference", 173 }, 174 } 175 176 if err := ctl.Run(os.Args); err != nil { 177 fmt.Fprintln(os.Stderr, err) 178 os.Exit(1) 179 } 180 }