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 }