github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/scripts/compare-dumps/compare-dumps.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/binary"
     7  	"encoding/hex"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"sort"
    14  
    15  	"github.com/urfave/cli"
    16  )
    17  
    18  var ledgerContractID = -4
    19  
    20  type dump []blockDump
    21  
    22  type blockDump struct {
    23  	Block   uint32      `json:"block"`
    24  	Size    int         `json:"size"`
    25  	Storage []storageOp `json:"storage"`
    26  }
    27  
    28  type storageOp struct {
    29  	State string `json:"state"`
    30  	Key   string `json:"key"`
    31  	Value string `json:"value,omitempty"`
    32  }
    33  
    34  func readFile(path string) (dump, error) {
    35  	data, err := os.ReadFile(path)
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  	d := make(dump, 0)
    40  	if err := json.Unmarshal(data, &d); err != nil {
    41  		return nil, err
    42  	}
    43  	return d, err
    44  }
    45  
    46  func (d dump) normalize() {
    47  	ledgerIDBytes := make([]byte, 4)
    48  	binary.LittleEndian.PutUint32(ledgerIDBytes, uint32(ledgerContractID))
    49  	for i := range d {
    50  		var newStorage []storageOp
    51  		for j := range d[i].Storage {
    52  			keyBytes, err := base64.StdEncoding.DecodeString(d[i].Storage[j].Key)
    53  			if err != nil {
    54  				panic(fmt.Errorf("invalid key encoding: %w", err))
    55  			}
    56  			if bytes.HasPrefix(keyBytes, ledgerIDBytes) {
    57  				continue
    58  			}
    59  			if d[i].Storage[j].State == "Changed" {
    60  				d[i].Storage[j].State = "Added"
    61  			}
    62  			newStorage = append(newStorage, d[i].Storage[j])
    63  		}
    64  		sort.Slice(newStorage, func(k, l int) bool {
    65  			return newStorage[k].Key < newStorage[l].Key
    66  		})
    67  		d[i].Storage = newStorage
    68  	}
    69  	// assume that d is already sorted by Block
    70  }
    71  
    72  func compare(a, b string) error {
    73  	dumpA, err := readFile(a)
    74  	if err != nil {
    75  		return fmt.Errorf("reading file %s: %w", a, err)
    76  	}
    77  	dumpB, err := readFile(b)
    78  	if err != nil {
    79  		return fmt.Errorf("reading file %s: %w", b, err)
    80  	}
    81  	dumpA.normalize()
    82  	dumpB.normalize()
    83  	if len(dumpA) != len(dumpB) {
    84  		return fmt.Errorf("dump files differ in size: %d vs %d", len(dumpA), len(dumpB))
    85  	}
    86  	for i := range dumpA {
    87  		blockA := &dumpA[i]
    88  		blockB := &dumpB[i]
    89  		if blockA.Block != blockB.Block {
    90  			return fmt.Errorf("block number mismatch: %d vs %d", blockA.Block, blockB.Block)
    91  		}
    92  		if len(blockA.Storage) != len(blockB.Storage) {
    93  			return fmt.Errorf("block %d, changes length mismatch: %d vs %d", blockA.Block, len(blockA.Storage), len(blockB.Storage))
    94  		}
    95  		fail := false
    96  		for j := range blockA.Storage {
    97  			if blockA.Storage[j].Key != blockB.Storage[j].Key {
    98  				idA, prefixA := parseKey(blockA.Storage[j].Key)
    99  				idB, prefixB := parseKey(blockB.Storage[j].Key)
   100  				return fmt.Errorf("block %d: key mismatch:\n\tKey: %s\n\tContract ID: %d\n\tItem key (base64): %s\n\tItem key (hex): %s\n\tItem key (bytes): %v\nvs\n\tKey: %s\n\tContract ID: %d\n\tItem key (base64): %s\n\tItem key (hex): %s\n\tItem key (bytes): %v", blockA.Block, blockA.Storage[j].Key, idA, base64.StdEncoding.EncodeToString(prefixA), hex.EncodeToString(prefixA), prefixA, blockB.Storage[j].Key, idB, base64.StdEncoding.EncodeToString(prefixB), hex.EncodeToString(prefixB), prefixB)
   101  			}
   102  			if blockA.Storage[j].State != blockB.Storage[j].State {
   103  				id, prefix := parseKey(blockA.Storage[j].Key)
   104  				return fmt.Errorf("block %d: state mismatch for key %s:\n\tContract ID: %d\n\tItem key (base64): %s\n\tItem key (hex): %s\n\tItem key (bytes): %v\n\tDiff: %s vs %s", blockA.Block, blockA.Storage[j].Key, id, base64.StdEncoding.EncodeToString(prefix), hex.EncodeToString(prefix), prefix, blockA.Storage[j].State, blockB.Storage[j].State)
   105  			}
   106  			if blockA.Storage[j].Value != blockB.Storage[j].Value {
   107  				fail = true
   108  				id, prefix := parseKey(blockA.Storage[j].Key)
   109  				fmt.Printf("block %d: value mismatch for key %s:\n\tContract ID: %d\n\tItem key (base64): %s\n\tItem key (hex): %s\n\tItem key (bytes): %v\n\tDiff: %s vs %s\n", blockA.Block, blockA.Storage[j].Key, id, base64.StdEncoding.EncodeToString(prefix), hex.EncodeToString(prefix), prefix, blockA.Storage[j].Value, blockB.Storage[j].Value)
   110  			}
   111  		}
   112  		if fail {
   113  			return errors.New("fail")
   114  		}
   115  	}
   116  	return nil
   117  }
   118  
   119  // parseKey splits the provided storage item key into contract ID and contract storage item prefix.
   120  func parseKey(key string) (int32, []byte) {
   121  	keyBytes, _ := base64.StdEncoding.DecodeString(key) // ignore error, rely on proper storage dump state.
   122  	id := int32(binary.LittleEndian.Uint32(keyBytes[:4]))
   123  	prefix := keyBytes[4:]
   124  	return id, prefix
   125  }
   126  
   127  func cliMain(c *cli.Context) error {
   128  	a := c.Args().Get(0)
   129  	b := c.Args().Get(1)
   130  	if a == "" {
   131  		return errors.New("no arguments given")
   132  	}
   133  	if b == "" {
   134  		return errors.New("missing second argument")
   135  	}
   136  	fa, err := os.Open(a)
   137  	if err != nil {
   138  		return err
   139  	}
   140  	defer fa.Close()
   141  	fb, err := os.Open(b)
   142  	if err != nil {
   143  		return err
   144  	}
   145  	defer fb.Close()
   146  
   147  	astat, err := fa.Stat()
   148  	if err != nil {
   149  		return err
   150  	}
   151  	bstat, err := fb.Stat()
   152  	if err != nil {
   153  		return err
   154  	}
   155  	if astat.Mode().IsRegular() && bstat.Mode().IsRegular() {
   156  		return compare(a, b)
   157  	}
   158  	if astat.Mode().IsDir() && bstat.Mode().IsDir() {
   159  		for i := 0; i <= 6000000; i += 100000 {
   160  			dir := fmt.Sprintf("BlockStorage_%d", i)
   161  			fmt.Println("Processing directory", dir)
   162  			for j := i - 99000; j <= i; j += 1000 {
   163  				if j < 0 {
   164  					continue
   165  				}
   166  				fname := fmt.Sprintf("%s/dump-block-%d.json", dir, j)
   167  
   168  				aname := filepath.Join(a, fname)
   169  				bname := filepath.Join(b, fname)
   170  				err := compare(aname, bname)
   171  				if err != nil {
   172  					return fmt.Errorf("file %s: %w", fname, err)
   173  				}
   174  			}
   175  		}
   176  		return nil
   177  	}
   178  	return errors.New("both parameters must be either dump files or directories")
   179  }
   180  
   181  func main() {
   182  	ctl := cli.NewApp()
   183  	ctl.Name = "compare-dumps"
   184  	ctl.Version = "1.0"
   185  	ctl.Usage = "compare-dumps dumpDirA dumpDirB"
   186  	ctl.Action = cliMain
   187  
   188  	if err := ctl.Run(os.Args); err != nil {
   189  		fmt.Fprintln(os.Stderr, err)
   190  		fmt.Fprintln(os.Stderr, ctl.Usage)
   191  		os.Exit(1)
   192  	}
   193  }