github.com/klaytn/klaytn@v1.10.2/node/cn/api.go (about) 1 // Modifications Copyright 2018 The klaytn Authors 2 // Copyright 2015 The go-ethereum Authors 3 // This file is part of go-ethereum. 4 // 5 // The go-ethereum library is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Lesser General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // The go-ethereum library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public License 16 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 17 // 18 // This file is derived from eth/api.go (2018/06/04). 19 // Modified and improved for the klaytn development. 20 21 package cn 22 23 import ( 24 "bytes" 25 "compress/gzip" 26 "context" 27 "errors" 28 "fmt" 29 "io" 30 "os" 31 "strings" 32 "time" 33 34 "github.com/klaytn/klaytn/blockchain" 35 "github.com/klaytn/klaytn/blockchain/state" 36 "github.com/klaytn/klaytn/blockchain/types" 37 "github.com/klaytn/klaytn/common" 38 "github.com/klaytn/klaytn/common/hexutil" 39 "github.com/klaytn/klaytn/networks/rpc" 40 "github.com/klaytn/klaytn/params" 41 "github.com/klaytn/klaytn/rlp" 42 "github.com/klaytn/klaytn/storage/statedb" 43 "github.com/klaytn/klaytn/work" 44 ) 45 46 // PublicKlayAPI provides an API to access Klaytn CN-related 47 // information. 48 type PublicKlayAPI struct { 49 cn *CN 50 } 51 52 // NewPublicKlayAPI creates a new Klaytn protocol API for full nodes. 53 func NewPublicKlayAPI(e *CN) *PublicKlayAPI { 54 return &PublicKlayAPI{e} 55 } 56 57 // Rewardbase is the address that consensus rewards will be send to 58 func (api *PublicKlayAPI) Rewardbase() (common.Address, error) { 59 return api.cn.Rewardbase() 60 } 61 62 // PrivateAdminAPI is the collection of CN full node-related APIs 63 // exposed over the private admin endpoint. 64 type PrivateAdminAPI struct { 65 cn *CN 66 } 67 68 // NewPrivateAdminAPI creates a new API definition for the full node private 69 // admin methods of the CN service. 70 func NewPrivateAdminAPI(cn *CN) *PrivateAdminAPI { 71 return &PrivateAdminAPI{cn: cn} 72 } 73 74 // ExportChain exports the current blockchain into a local file. 75 func (api *PrivateAdminAPI) ExportChain(file string) (bool, error) { 76 if _, err := os.Stat(file); err == nil { 77 // File already exists. Allowing overwrite could be a DoS vecotor, 78 // since the 'file' may point to arbitrary paths on the drive 79 return false, errors.New("location would overwrite an existing file") 80 } 81 82 // Make sure we can create the file to export into 83 out, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) 84 if err != nil { 85 return false, err 86 } 87 defer out.Close() 88 89 var writer io.Writer = out 90 if strings.HasSuffix(file, ".gz") { 91 writer = gzip.NewWriter(writer) 92 defer writer.(*gzip.Writer).Close() 93 } 94 95 // Export the blockchain 96 if err := api.cn.BlockChain().Export(writer); err != nil { 97 return false, err 98 } 99 return true, nil 100 } 101 102 func hasAllBlocks(chain work.BlockChain, bs []*types.Block) bool { 103 for _, b := range bs { 104 if !chain.HasBlock(b.Hash(), b.NumberU64()) { 105 return false 106 } 107 } 108 109 return true 110 } 111 112 // ImportChain imports a blockchain from a local file. 113 func (api *PrivateAdminAPI) ImportChain(file string) (bool, error) { 114 // Make sure the can access the file to import 115 in, err := os.Open(file) 116 if err != nil { 117 return false, err 118 } 119 defer in.Close() 120 121 var reader io.Reader = in 122 if strings.HasSuffix(file, ".gz") { 123 if reader, err = gzip.NewReader(reader); err != nil { 124 return false, err 125 } 126 } 127 stream := rlp.NewStream(reader, 0) 128 129 return api.importChain(stream) 130 } 131 132 func (api *PrivateAdminAPI) ImportChainFromString(blockRlp string) (bool, error) { 133 // Run actual the import in pre-configured batches 134 stream := rlp.NewStream(bytes.NewReader(common.FromHex(blockRlp)), 0) 135 136 return api.importChain(stream) 137 } 138 139 func (api *PrivateAdminAPI) importChain(stream *rlp.Stream) (bool, error) { 140 blocks, index := make([]*types.Block, 0, 2500), 0 141 for batch := 0; ; batch++ { 142 // Load a batch of blocks from the input file 143 for len(blocks) < cap(blocks) { 144 block := new(types.Block) 145 if err := stream.Decode(block); err == io.EOF { 146 break 147 } else if err != nil { 148 return false, fmt.Errorf("block %d: failed to parse: %v", index, err) 149 } 150 blocks = append(blocks, block) 151 index++ 152 } 153 if len(blocks) == 0 { 154 break 155 } 156 157 if hasAllBlocks(api.cn.BlockChain(), blocks) { 158 blocks = blocks[:0] 159 continue 160 } 161 // Import the batch and reset the buffer 162 if _, err := api.cn.BlockChain().InsertChain(blocks); err != nil { 163 return false, fmt.Errorf("batch %d: failed to insert: %v", batch, err) 164 } 165 blocks = blocks[:0] 166 } 167 return true, nil 168 } 169 170 // StartStateMigration starts state migration. 171 func (api *PrivateAdminAPI) StartStateMigration() error { 172 return api.cn.blockchain.PrepareStateMigration() 173 } 174 175 // StopStateMigration stops state migration and removes stateMigrationDB. 176 func (api *PrivateAdminAPI) StopStateMigration() error { 177 return api.cn.BlockChain().StopStateMigration() 178 } 179 180 // StateMigrationStatus returns the status information of state trie migration. 181 func (api *PrivateAdminAPI) StateMigrationStatus() map[string]interface{} { 182 isMigration, blkNum, read, committed, pending, progress, err := api.cn.BlockChain().StateMigrationStatus() 183 184 errStr := "null" 185 if err != nil { 186 errStr = err.Error() 187 } 188 189 return map[string]interface{}{ 190 "isMigration": isMigration, 191 "migrationBlockNumber": blkNum, 192 "read": read, 193 "committed": committed, 194 "pending": pending, 195 "progress": progress, 196 "err": errStr, 197 } 198 } 199 200 func (api *PrivateAdminAPI) SaveTrieNodeCacheToDisk() error { 201 return api.cn.BlockChain().SaveTrieNodeCacheToDisk() 202 } 203 204 func (api *PrivateAdminAPI) SpamThrottlerConfig(ctx context.Context) (*blockchain.ThrottlerConfig, error) { 205 throttler := blockchain.GetSpamThrottler() 206 if throttler == nil { 207 return nil, errors.New("spam throttler is not running") 208 } 209 return throttler.GetConfig(), nil 210 } 211 212 func (api *PrivateAdminAPI) StopSpamThrottler(ctx context.Context) error { 213 throttler := blockchain.GetSpamThrottler() 214 if throttler == nil { 215 return errors.New("spam throttler was already stopped") 216 } 217 api.cn.txPool.StopSpamThrottler() 218 return nil 219 } 220 221 func (api *PrivateAdminAPI) StartSpamThrottler(ctx context.Context, config *blockchain.ThrottlerConfig) error { 222 throttler := blockchain.GetSpamThrottler() 223 if throttler != nil { 224 return errors.New("spam throttler is already running") 225 } 226 return api.cn.txPool.StartSpamThrottler(config) 227 } 228 229 func (api *PrivateAdminAPI) SetSpamThrottlerWhiteList(ctx context.Context, addrs []common.Address) error { 230 throttler := blockchain.GetSpamThrottler() 231 if throttler == nil { 232 return errors.New("spam throttler is not running") 233 } 234 throttler.SetAllowed(addrs) 235 return nil 236 } 237 238 func (api *PrivateAdminAPI) GetSpamThrottlerWhiteList(ctx context.Context) ([]common.Address, error) { 239 throttler := blockchain.GetSpamThrottler() 240 if throttler == nil { 241 return nil, errors.New("spam throttler is not running") 242 } 243 return throttler.GetAllowed(), nil 244 } 245 246 func (api *PrivateAdminAPI) GetSpamThrottlerThrottleList(ctx context.Context) ([]common.Address, error) { 247 throttler := blockchain.GetSpamThrottler() 248 if throttler == nil { 249 return nil, errors.New("spam throttler is not running") 250 } 251 return throttler.GetThrottled(), nil 252 } 253 254 func (api *PrivateAdminAPI) GetSpamThrottlerCandidateList(ctx context.Context) (map[common.Address]int, error) { 255 throttler := blockchain.GetSpamThrottler() 256 if throttler == nil { 257 return nil, errors.New("spam throttler is not running") 258 } 259 return throttler.GetCandidates(), nil 260 } 261 262 // PublicDebugAPI is the collection of Klaytn full node APIs exposed 263 // over the public debugging endpoint. 264 type PublicDebugAPI struct { 265 cn *CN 266 } 267 268 // NewPublicDebugAPI creates a new API definition for the full node- 269 // related public debug methods of the Klaytn service. 270 func NewPublicDebugAPI(cn *CN) *PublicDebugAPI { 271 return &PublicDebugAPI{cn: cn} 272 } 273 274 // DumpBlock retrieves the entire state of the database at a given block. 275 func (api *PublicDebugAPI) DumpBlock(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (state.Dump, error) { 276 if *blockNrOrHash.BlockNumber == rpc.PendingBlockNumber { 277 // If we're dumping the pending state, we need to request 278 // both the pending block as well as the pending state from 279 // the miner and operate on those 280 _, stateDb := api.cn.miner.Pending() 281 return stateDb.RawDump(), nil 282 } 283 284 var block *types.Block 285 var err error 286 if *blockNrOrHash.BlockNumber == rpc.LatestBlockNumber { 287 block = api.cn.APIBackend.CurrentBlock() 288 } else { 289 block, err = api.cn.APIBackend.BlockByNumberOrHash(ctx, blockNrOrHash) 290 if err != nil { 291 blockNrOrHashString, _ := blockNrOrHash.NumberOrHashString() 292 return state.Dump{}, fmt.Errorf("block %v not found", blockNrOrHashString) 293 } 294 } 295 stateDb, err := api.cn.BlockChain().StateAtWithPersistent(block.Root()) 296 if err != nil { 297 return state.Dump{}, err 298 } 299 return stateDb.RawDump(), nil 300 } 301 302 type Trie struct { 303 Type string `json:"type"` 304 Hash string `json:"hash"` 305 Parent string `json:"parent"` 306 Path string `json:"path"` 307 } 308 309 type DumpStateTrieResult struct { 310 Root string `json:"root"` 311 Tries []Trie `json:"tries"` 312 } 313 314 // DumpStateTrie retrieves all state/storage tries of the given state root. 315 func (api *PublicDebugAPI) DumpStateTrie(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (DumpStateTrieResult, error) { 316 block, err := api.cn.APIBackend.BlockByNumberOrHash(ctx, blockNrOrHash) 317 if err != nil { 318 blockNrOrHashString, _ := blockNrOrHash.NumberOrHashString() 319 return DumpStateTrieResult{}, fmt.Errorf("block #%v not found", blockNrOrHashString) 320 } 321 322 result := DumpStateTrieResult{ 323 Root: block.Root().String(), 324 Tries: make([]Trie, 0), 325 } 326 327 db := state.NewDatabaseWithExistingCache(api.cn.chainDB, api.cn.blockchain.StateCache().TrieDB().TrieNodeCache()) 328 stateDB, err := state.New(block.Root(), db, nil) 329 if err != nil { 330 return DumpStateTrieResult{}, err 331 } 332 it := state.NewNodeIterator(stateDB) 333 for it.Next() { 334 t := Trie{ 335 it.Type, 336 it.Hash.String(), 337 it.Parent.String(), 338 statedb.HexPathToString(it.Path), 339 } 340 341 result.Tries = append(result.Tries, t) 342 } 343 return result, nil 344 } 345 346 // TODO-klaytn: Rearrange PublicDebugAPI and PrivateDebugAPI receivers 347 // StartWarmUp retrieves all state/storage tries of the latest committed state root and caches the tries. 348 func (api *PrivateDebugAPI) StartWarmUp() error { 349 return api.cn.blockchain.StartWarmUp() 350 } 351 352 // TODO-klaytn: Rearrange PublicDebugAPI and PrivateDebugAPI receivers 353 // StartContractWarmUp retrieves a storage trie of the latest state root and caches the trie 354 // corresponding to the given contract address. 355 func (api *PrivateDebugAPI) StartContractWarmUp(contractAddr common.Address) error { 356 return api.cn.blockchain.StartContractWarmUp(contractAddr) 357 } 358 359 // TODO-klaytn: Rearrange PublicDebugAPI and PrivateDebugAPI receivers 360 // StopWarmUp stops the warming up process. 361 func (api *PrivateDebugAPI) StopWarmUp() error { 362 return api.cn.blockchain.StopWarmUp() 363 } 364 365 // TODO-klaytn: Rearrange PublicDebugAPI and PrivateDebugAPI receivers 366 // StartCollectingTrieStats collects state/storage trie statistics and print in the log. 367 func (api *PrivateDebugAPI) StartCollectingTrieStats(contractAddr common.Address) error { 368 return api.cn.blockchain.StartCollectingTrieStats(contractAddr) 369 } 370 371 // PrivateDebugAPI is the collection of CN full node APIs exposed over 372 // the private debugging endpoint. 373 type PrivateDebugAPI struct { 374 config *params.ChainConfig 375 cn *CN 376 } 377 378 // NewPrivateDebugAPI creates a new API definition for the full node-related 379 // private debug methods of the CN service. 380 func NewPrivateDebugAPI(config *params.ChainConfig, cn *CN) *PrivateDebugAPI { 381 return &PrivateDebugAPI{config: config, cn: cn} 382 } 383 384 // Preimage is a debug API function that returns the preimage for a sha3 hash, if known. 385 func (api *PrivateDebugAPI) Preimage(ctx context.Context, hash common.Hash) (hexutil.Bytes, error) { 386 if preimage := api.cn.ChainDB().ReadPreimage(hash); preimage != nil { 387 return preimage, nil 388 } 389 return nil, errors.New("unknown preimage") 390 } 391 392 // TODO-klaytn: Rearrange PublicDebugAPI and PrivateDebugAPI receivers 393 // GetBadBLocks returns a list of the last 'bad blocks' that the client has seen on the network 394 // and returns them as a JSON list of block-hashes 395 func (api *PublicDebugAPI) GetBadBlocks(ctx context.Context) ([]blockchain.BadBlockArgs, error) { 396 return api.cn.BlockChain().BadBlocks() 397 } 398 399 // StorageRangeResult is the result of a debug_storageRangeAt API call. 400 type StorageRangeResult struct { 401 Storage storageMap `json:"storage"` 402 NextKey *common.Hash `json:"nextKey"` // nil if Storage includes the last key in the statedb. 403 } 404 405 type storageMap map[common.Hash]storageEntry 406 407 type storageEntry struct { 408 Key *common.Hash `json:"key"` 409 Value common.Hash `json:"value"` 410 } 411 412 // StorageRangeAt returns the storage at the given block height and transaction index. 413 func (api *PrivateDebugAPI) StorageRangeAt(ctx context.Context, blockHash common.Hash, txIndex int, contractAddress common.Address, keyStart hexutil.Bytes, maxResult int) (StorageRangeResult, error) { 414 // Retrieve the block 415 block := api.cn.blockchain.GetBlockByHash(blockHash) 416 if block == nil { 417 return StorageRangeResult{}, fmt.Errorf("block %#x not found", blockHash) 418 } 419 _, _, statedb, err := api.cn.stateAtTransaction(block, txIndex, 0) 420 if err != nil { 421 return StorageRangeResult{}, err 422 } 423 st := statedb.StorageTrie(contractAddress) 424 if st == nil { 425 return StorageRangeResult{}, fmt.Errorf("account %x doesn't exist", contractAddress) 426 } 427 return storageRangeAt(st, keyStart, maxResult) 428 } 429 430 func storageRangeAt(st state.Trie, start []byte, maxResult int) (StorageRangeResult, error) { 431 it := statedb.NewIterator(st.NodeIterator(start)) 432 result := StorageRangeResult{Storage: storageMap{}} 433 for i := 0; i < maxResult && it.Next(); i++ { 434 _, content, _, err := rlp.Split(it.Value) 435 if err != nil { 436 return StorageRangeResult{}, err 437 } 438 e := storageEntry{Value: common.BytesToHash(content)} 439 if preimage := st.GetKey(it.Key); preimage != nil { 440 preimage := common.BytesToHash(preimage) 441 e.Key = &preimage 442 } 443 result.Storage[common.BytesToHash(it.Key)] = e 444 } 445 // Add the 'next key' so clients can continue downloading. 446 if it.Next() { 447 next := common.BytesToHash(it.Key) 448 result.NextKey = &next 449 } 450 return result, nil 451 } 452 453 // TODO-klaytn: Rearrange PublicDebugAPI and PrivateDebugAPI receivers 454 // GetModifiedAccountsByNumber returns all accounts that have changed between the 455 // two blocks specified. A change is defined as a difference in nonce, balance, 456 // code hash, or storage hash. 457 // 458 // With one parameter, returns the list of accounts modified in the specified block. 459 func (api *PublicDebugAPI) GetModifiedAccountsByNumber(ctx context.Context, startNum rpc.BlockNumber, endNum *rpc.BlockNumber) ([]common.Address, error) { 460 startBlock, endBlock, err := api.getStartAndEndBlock(ctx, startNum, endNum) 461 if err != nil { 462 return nil, err 463 } 464 return api.getModifiedAccounts(startBlock, endBlock) 465 } 466 467 // TODO-klaytn: Rearrange PublicDebugAPI and PrivateDebugAPI receivers 468 // GetModifiedAccountsByHash returns all accounts that have changed between the 469 // two blocks specified. A change is defined as a difference in nonce, balance, 470 // code hash, or storage hash. 471 // 472 // With one parameter, returns the list of accounts modified in the specified block. 473 func (api *PublicDebugAPI) GetModifiedAccountsByHash(startHash common.Hash, endHash *common.Hash) ([]common.Address, error) { 474 var startBlock, endBlock *types.Block 475 startBlock = api.cn.blockchain.GetBlockByHash(startHash) 476 if startBlock == nil { 477 return nil, fmt.Errorf("start block %x not found", startHash) 478 } 479 480 if endHash == nil { 481 endBlock = startBlock 482 startBlock = api.cn.blockchain.GetBlockByHash(startBlock.ParentHash()) 483 if startBlock == nil { 484 return nil, fmt.Errorf("block %x has no parent", startHash) 485 } 486 } else { 487 endBlock = api.cn.blockchain.GetBlockByHash(*endHash) 488 if endBlock == nil { 489 return nil, fmt.Errorf("end block %x not found", *endHash) 490 } 491 } 492 return api.getModifiedAccounts(startBlock, endBlock) 493 } 494 495 // TODO-klaytn: Rearrange PublicDebugAPI and PrivateDebugAPI receivers 496 func (api *PublicDebugAPI) getModifiedAccounts(startBlock, endBlock *types.Block) ([]common.Address, error) { 497 trieDB := api.cn.blockchain.StateCache().TrieDB() 498 499 oldTrie, err := statedb.NewSecureTrie(startBlock.Root(), trieDB) 500 if err != nil { 501 return nil, err 502 } 503 newTrie, err := statedb.NewSecureTrie(endBlock.Root(), trieDB) 504 if err != nil { 505 return nil, err 506 } 507 508 diff, _ := statedb.NewDifferenceIterator(oldTrie.NodeIterator([]byte{}), newTrie.NodeIterator([]byte{})) 509 iter := statedb.NewIterator(diff) 510 511 var dirty []common.Address 512 for iter.Next() { 513 key := newTrie.GetKey(iter.Key) 514 if key == nil { 515 return nil, fmt.Errorf("no preimage found for hash %x", iter.Key) 516 } 517 dirty = append(dirty, common.BytesToAddress(key)) 518 } 519 return dirty, nil 520 } 521 522 // TODO-klaytn: Rearrange PublicDebugAPI and PrivateDebugAPI receivers 523 // getStartAndEndBlock returns start and end block based on the given startNum and endNum. 524 func (api *PublicDebugAPI) getStartAndEndBlock(ctx context.Context, startNum rpc.BlockNumber, endNum *rpc.BlockNumber) (*types.Block, *types.Block, error) { 525 var startBlock, endBlock *types.Block 526 527 startBlock, err := api.cn.APIBackend.BlockByNumber(ctx, startNum) 528 if err != nil { 529 return nil, nil, fmt.Errorf("start block number #%d not found", startNum.Uint64()) 530 } 531 532 if endNum == nil { 533 endBlock = startBlock 534 startBlock, err = api.cn.APIBackend.BlockByHash(ctx, startBlock.ParentHash()) 535 if err != nil { 536 return nil, nil, fmt.Errorf("block number #%d has no parent", startNum.Uint64()) 537 } 538 } else { 539 endBlock, err = api.cn.APIBackend.BlockByNumber(ctx, *endNum) 540 if err != nil { 541 return nil, nil, fmt.Errorf("end block number #%d not found", (*endNum).Uint64()) 542 } 543 } 544 545 if startBlock.Number().Uint64() >= endBlock.Number().Uint64() { 546 return nil, nil, fmt.Errorf("start block height (%d) must be less than end block height (%d)", startBlock.Number().Uint64(), endBlock.Number().Uint64()) 547 } 548 549 return startBlock, endBlock, nil 550 } 551 552 // TODO-klaytn: Rearrange PublicDebugAPI and PrivateDebugAPI receivers 553 // GetModifiedStorageNodesByNumber returns the number of storage nodes of a contract account 554 // that have been changed between the two blocks specified. 555 // 556 // With the first two parameters, it returns the number of storage trie nodes modified in the specified block. 557 func (api *PublicDebugAPI) GetModifiedStorageNodesByNumber(ctx context.Context, contractAddr common.Address, startNum rpc.BlockNumber, endNum *rpc.BlockNumber, printDetail *bool) (int, error) { 558 startBlock, endBlock, err := api.getStartAndEndBlock(ctx, startNum, endNum) 559 if err != nil { 560 return 0, err 561 } 562 return api.getModifiedStorageNodes(contractAddr, startBlock, endBlock, printDetail) 563 } 564 565 // TODO-klaytn: Rearrange PublicDebugAPI and PrivateDebugAPI receivers 566 func (api *PublicDebugAPI) getModifiedStorageNodes(contractAddr common.Address, startBlock, endBlock *types.Block, printDetail *bool) (int, error) { 567 startBlockRoot, err := api.cn.blockchain.GetContractStorageRoot(startBlock, api.cn.blockchain.StateCache(), contractAddr) 568 if err != nil { 569 return 0, err 570 } 571 endBlockRoot, err := api.cn.blockchain.GetContractStorageRoot(endBlock, api.cn.blockchain.StateCache(), contractAddr) 572 if err != nil { 573 return 0, err 574 } 575 576 trieDB := api.cn.blockchain.StateCache().TrieDB() 577 oldTrie, err := statedb.NewSecureTrie(startBlockRoot, trieDB) 578 if err != nil { 579 return 0, err 580 } 581 newTrie, err := statedb.NewSecureTrie(endBlockRoot, trieDB) 582 if err != nil { 583 return 0, err 584 } 585 586 diff, _ := statedb.NewDifferenceIterator(oldTrie.NodeIterator([]byte{}), newTrie.NodeIterator([]byte{})) 587 iter := statedb.NewIterator(diff) 588 589 logger.Info("Start collecting the modified storage nodes", "contractAddr", contractAddr.String(), 590 "startBlock", startBlock.NumberU64(), "endBlock", endBlock.NumberU64()) 591 start := time.Now() 592 numModifiedNodes := 0 593 for iter.Next() { 594 numModifiedNodes++ 595 if printDetail != nil && *printDetail { 596 logger.Info("modified storage trie nodes", "contractAddr", contractAddr.String(), 597 "nodeHash", common.BytesToHash(iter.Key).String()) 598 } 599 } 600 logger.Info("Finished collecting the modified storage nodes", "contractAddr", contractAddr.String(), 601 "startBlock", startBlock.NumberU64(), "endBlock", endBlock.NumberU64(), "numModifiedNodes", numModifiedNodes, "elapsed", time.Since(start)) 602 return numModifiedNodes, nil 603 }