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  }