diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4bc13aff92..39554550c4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,6 +10,7 @@ beacon/merkle/ @zsfelfoldi beacon/types/ @zsfelfoldi @fjl beacon/params/ @zsfelfoldi @fjl cmd/evm/ @MariusVanDerWijden @lightclient +cmd/keeper/ @gballet core/state/ @rjl493456442 crypto/ @gballet @jwasinger @fjl core/ @rjl493456442 diff --git a/beacon/blsync/engineclient.go b/beacon/blsync/engineclient.go index 9fc6a18a57..6cac2bc5a8 100644 --- a/beacon/blsync/engineclient.go +++ b/beacon/blsync/engineclient.go @@ -87,6 +87,10 @@ func (ec *engineClient) updateLoop(headCh <-chan types.ChainHeadEvent) { if status, err := ec.callForkchoiceUpdated(forkName, event); err == nil { log.Info("Successful ForkchoiceUpdated", "head", event.Block.Hash(), "status", status) } else { + if err.Error() == "beacon syncer reorging" { + log.Debug("Failed ForkchoiceUpdated", "head", event.Block.Hash(), "error", err) + continue // ignore beacon syncer reorging errors, this error can occur if the blsync is skipping a block + } log.Error("Failed ForkchoiceUpdated", "head", event.Block.Hash(), "error", err) } } diff --git a/beacon/engine/gen_blockparams.go b/beacon/engine/gen_blockparams.go index 695cdd4e32..2dbcb699a2 100644 --- a/beacon/engine/gen_blockparams.go +++ b/beacon/engine/gen_blockparams.go @@ -21,6 +21,7 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) { SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + SlotNumber *hexutil.Uint64 `json:"slotNumber"` } var enc PayloadAttributes @@ -29,6 +30,7 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) { enc.SuggestedFeeRecipient = p.SuggestedFeeRecipient enc.Withdrawals = p.Withdrawals enc.BeaconRoot = p.BeaconRoot + enc.SlotNumber = (*hexutil.Uint64)(p.SlotNumber) return json.Marshal(&enc) } @@ -40,6 +42,7 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { SuggestedFeeRecipient *common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + SlotNumber *hexutil.Uint64 `json:"slotNumber"` } var dec PayloadAttributes @@ -70,5 +73,8 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { if dec.BeaconRoot != nil { p.BeaconRoot = dec.BeaconRoot } + if dec.SlotNumber != nil { + p.SlotNumber = (*uint64)(dec.SlotNumber) + } return nil } diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go index 6893d64a16..b460368b84 100644 --- a/beacon/engine/gen_ed.go +++ b/beacon/engine/gen_ed.go @@ -34,6 +34,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { Withdrawals []*types.Withdrawal `json:"withdrawals"` BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + SlotNumber *hexutil.Uint64 `json:"slotNumber"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -58,6 +59,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { enc.Withdrawals = e.Withdrawals enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas) + enc.SlotNumber = (*hexutil.Uint64)(e.SlotNumber) return json.Marshal(&enc) } @@ -81,6 +83,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { Withdrawals []*types.Withdrawal `json:"withdrawals"` BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + SlotNumber *hexutil.Uint64 `json:"slotNumber"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -154,5 +157,8 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { if dec.ExcessBlobGas != nil { e.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas) } + if dec.SlotNumber != nil { + e.SlotNumber = (*uint64)(dec.SlotNumber) + } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index da9b6568f2..a187eb39cb 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -50,6 +50,13 @@ var ( // ExecutionPayloadV3 has the syntax of ExecutionPayloadV2 and appends the new // fields: blobGasUsed and excessBlobGas. PayloadV3 PayloadVersion = 0x3 + + // PayloadV4 is the identifier of ExecutionPayloadV4 introduced in amsterdam fork. + // + // https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md#executionpayloadv4 + // ExecutionPayloadV4 has the syntax of ExecutionPayloadV3 and appends the new + // field slotNumber. + PayloadV4 PayloadVersion = 0x4 ) //go:generate go run github.com/fjl/gencodec -type PayloadAttributes -field-override payloadAttributesMarshaling -out gen_blockparams.go @@ -62,11 +69,13 @@ type PayloadAttributes struct { SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + SlotNumber *uint64 `json:"slotNumber"` } // JSON type overrides for PayloadAttributes. type payloadAttributesMarshaling struct { - Timestamp hexutil.Uint64 + Timestamp hexutil.Uint64 + SlotNumber *hexutil.Uint64 } //go:generate go run github.com/fjl/gencodec -type ExecutableData -field-override executableDataMarshaling -out gen_ed.go @@ -90,6 +99,7 @@ type ExecutableData struct { Withdrawals []*types.Withdrawal `json:"withdrawals"` BlobGasUsed *uint64 `json:"blobGasUsed"` ExcessBlobGas *uint64 `json:"excessBlobGas"` + SlotNumber *uint64 `json:"slotNumber"` } // JSON type overrides for executableData. @@ -104,6 +114,7 @@ type executableDataMarshaling struct { Transactions []hexutil.Bytes BlobGasUsed *hexutil.Uint64 ExcessBlobGas *hexutil.Uint64 + SlotNumber *hexutil.Uint64 } // StatelessPayloadStatusV1 is the result of a stateless payload execution. @@ -313,6 +324,7 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H BlobGasUsed: data.BlobGasUsed, ParentBeaconRoot: beaconRoot, RequestsHash: requestsHash, + SlotNumber: data.SlotNumber, } return types.NewBlockWithHeader(header). WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), @@ -340,6 +352,7 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types. Withdrawals: block.Withdrawals(), BlobGasUsed: block.BlobGasUsed(), ExcessBlobGas: block.ExcessBlobGas(), + SlotNumber: block.SlotNumber(), } // Add blobs. diff --git a/build/checksums.txt b/build/checksums.txt index 98ee3a91ef..ba80a3e201 100644 --- a/build/checksums.txt +++ b/build/checksums.txt @@ -5,81 +5,102 @@ # https://github.com/ethereum/execution-spec-tests/releases/download/v5.1.0 a3192784375acec7eaec492799d5c5d0c47a2909a3cc40178898e4ecd20cc416 fixtures_develop.tar.gz -# version:golang 1.25.1 +# version:golang 1.25.7 # https://go.dev/dl/ -d010c109cee94d80efe681eab46bdea491ac906bf46583c32e9f0dbb0bd1a594 go1.25.1.src.tar.gz -1d622468f767a1b9fe1e1e67bd6ce6744d04e0c68712adc689748bbeccb126bb go1.25.1.darwin-amd64.tar.gz -68deebb214f39d542e518ebb0598a406ab1b5a22bba8ec9ade9f55fb4dd94a6c go1.25.1.darwin-arm64.tar.gz -d03cdcbc9bd8baf5cf028de390478e9e2b3e4d0afe5a6582dedc19bfe6a263b2 go1.25.1.linux-386.tar.gz -7716a0d940a0f6ae8e1f3b3f4f36299dc53e31b16840dbd171254312c41ca12e go1.25.1.linux-amd64.tar.gz -65a3e34fb2126f55b34e1edfc709121660e1be2dee6bdf405fc399a63a95a87d go1.25.1.linux-arm64.tar.gz -eb949be683e82a99e9861dafd7057e31ea40b161eae6c4cd18fdc0e8c4ae6225 go1.25.1.linux-armv6l.tar.gz -be13d5479b8c75438f2efcaa8c191fba3af684b3228abc9c99c7aa8502f34424 go1.25.1.windows-386.zip -4a974de310e7ee1d523d2fcedb114ba5fa75408c98eb3652023e55ccf3fa7cab go1.25.1.windows-amd64.zip -45ab4290adbd6ee9e7f18f0d57eaa9008fdbef590882778ed93eac3c8cca06c5 go1.25.1.aix-ppc64.tar.gz -2e3c1549bed3124763774d648f291ac42611232f48320ebbd23517c909c09b81 go1.25.1.dragonfly-amd64.tar.gz -dc0198dd4ec520e13f26798def8750544edf6448d8e9c43fd2a814e4885932af go1.25.1.freebsd-386.tar.gz -c4f1a7e7b258406e6f3b677ecdbd97bbb23ff9c0d44be4eb238a07d360f69ac8 go1.25.1.freebsd-amd64.tar.gz -7772fc5ff71ed39297ec0c1599fc54e399642c9b848eac989601040923b0de9c go1.25.1.freebsd-arm.tar.gz -5bb011d5d5b6218b12189f07aa0be618ab2002662fff1ca40afba7389735c207 go1.25.1.freebsd-arm64.tar.gz -ccac716240cb049bebfafcb7eebc3758512178a4c51fc26da9cc032035d850c8 go1.25.1.freebsd-riscv64.tar.gz -cc53910ffb9fcfdd988a9fa25b5423bae1cfa01b19616be646700e1f5453b466 go1.25.1.illumos-amd64.tar.gz -efe809f923bcedab44bf7be2b3af8d182b512b1bf9c07d302e0c45d26c8f56f3 go1.25.1.linux-loong64.tar.gz -c0de33679f6ed68991dc42dc4a602e74a666e3e166c1748ee1b5d1a7ea2ffbb2 go1.25.1.linux-mips.tar.gz -c270f7b0c0bdfbcd54fef4481227c40d41bb518f9ae38ee930870f04a0a6a589 go1.25.1.linux-mips64.tar.gz -80be871ba9c944f34d1868cdf5047e1cf2e1289fe08cdb90e2453d2f0d6965ae go1.25.1.linux-mips64le.tar.gz -9f09defa9bb22ebf2cde76162f40958564e57ce5c2b3649bc063bebcbc9294c1 go1.25.1.linux-mipsle.tar.gz -2c76b7d278c1d43ad19d478ad3f0f05e7b782b64b90870701b314fa48b5f43c6 go1.25.1.linux-ppc64.tar.gz -8b0c8d3ee5b1b5c28b6bd63dc4438792012e01d03b4bf7a61d985c87edab7d1f go1.25.1.linux-ppc64le.tar.gz -22fe934a9d0c9c57275716c55b92d46ebd887cec3177c9140705efa9f84ba1e2 go1.25.1.linux-riscv64.tar.gz -9cfe517ba423f59f3738ca5c3d907c103253cffbbcc2987142f79c5de8c1bf93 go1.25.1.linux-s390x.tar.gz -6af8a08353e76205d5b743dd7a3f0126684f96f62be0a31b75daf9837e512c46 go1.25.1.netbsd-386.tar.gz -e5d534ff362edb1bd8c8e10892b6a027c4c1482454245d1529167676498684c7 go1.25.1.netbsd-amd64.tar.gz -88bcf39254fdcea6a199c1c27d787831b652427ce60851ae9e41a3d7eb477f45 go1.25.1.netbsd-arm.tar.gz -d7c2eabe1d04ee47bcaea2816fdd90dbd25d90d4dfa756faa9786c788e4f3a4e go1.25.1.netbsd-arm64.tar.gz -14a2845977eb4dde11d929858c437a043467c427db87899935e90cee04a38d72 go1.25.1.openbsd-386.tar.gz -d27ac54b38a13a09c81e67c82ac70d387037341c85c3399291c73e13e83fdd8c go1.25.1.openbsd-amd64.tar.gz -0f4ab5f02500afa4befd51fed1e8b45e4d07ca050f641cc3acc76eaa4027b2c3 go1.25.1.openbsd-arm.tar.gz -d46c3bd156843656f7f3cb0dec27ea51cd926ec3f7b80744bf8156e67c1c812f go1.25.1.openbsd-arm64.tar.gz -c550514c67f22e409be10e40eace761e2e43069f4ef086ae6e60aac736c2b679 go1.25.1.openbsd-ppc64.tar.gz -8a09a8714a2556eb13fc1f10b7ce2553fcea4971e3330fc3be0efd24aab45734 go1.25.1.openbsd-riscv64.tar.gz -b0e1fefaf0c7abd71f139a54eee9767944aff5f0bc9d69c968234804884e552f go1.25.1.plan9-386.tar.gz -e94732c94f149690aa0ab11c26090577211b4a988137cb2c03ec0b54e750402e go1.25.1.plan9-amd64.tar.gz -7eb80e9de1e817d9089a54e8c7c5c8d8ed9e5fb4d4a012fc0f18fc422a484f0c go1.25.1.plan9-arm.tar.gz -1261dfad7c4953c0ab90381bc1242dc54e394db7485c59349428d532b2273343 go1.25.1.solaris-amd64.tar.gz -04bc3c078e9e904c4d58d6ac2532a5bdd402bd36a9ff0b5949b3c5e6006a05ee go1.25.1.windows-arm64.zip +178f2832820274b43e177d32f06a3ebb0129e427dd20a5e4c88df2c1763cf10a go1.25.7.src.tar.gz +81bf2a1f20633f62d55d826d82dde3b0570cf1408a91e15781b266037299285b go1.25.7.aix-ppc64.tar.gz +bf5050a2152f4053837b886e8d9640c829dbacbc3370f913351eb0904cb706f5 go1.25.7.darwin-amd64.tar.gz +ff18369ffad05c57d5bed888b660b31385f3c913670a83ef557cdfd98ea9ae1b go1.25.7.darwin-arm64.tar.gz +c5dccd7f192dd7b305dc209fb316ac1917776d74bd8e4d532ef2772f305bf42a go1.25.7.dragonfly-amd64.tar.gz +a2de97c8ac74bf64b0ae73fe9d379e61af530e061bc7f8f825044172ffe61a8b go1.25.7.freebsd-386.tar.gz +055f9e138787dcafa81eb0314c8ff70c6dd0f6dba1e8a6957fef5d5efd1ab8fd go1.25.7.freebsd-amd64.tar.gz +60e7f7a7c990f0b9539ac8ed668155746997d404643a4eecd47b3dee1b7e710b go1.25.7.freebsd-arm.tar.gz +631e03d5fd4c526e2f499154d8c6bf4cb081afb2fff171c428722afc9539d53a go1.25.7.freebsd-arm64.tar.gz +8a264fd685823808140672812e3ad9c43f6ad59444c0dc14cdd3a1351839ddd5 go1.25.7.freebsd-riscv64.tar.gz +57c672447d906a1bcab98f2b11492d54521a791aacbb4994a25169e59cbe289a go1.25.7.illumos-amd64.tar.gz +2866517e9ca81e6a2e85a930e9b11bc8a05cfeb2fc6dc6cb2765e7fb3c14b715 go1.25.7.linux-386.tar.gz +12e6d6a191091ae27dc31f6efc630e3a3b8ba409baf3573d955b196fdf086005 go1.25.7.linux-amd64.tar.gz +ba611a53534135a81067240eff9508cd7e256c560edd5d8c2fef54f083c07129 go1.25.7.linux-arm64.tar.gz +1ba07e0eb86b839e72467f4b5c7a5597d07f30bcf5563c951410454f7cda5266 go1.25.7.linux-armv6l.tar.gz +775753fc5952a334c415f08768df2f0b73a3228a16e8f5f63d545daacb4e3357 go1.25.7.linux-loong64.tar.gz +1a023bb367c5fbb4c637a2f6dc23ff17c6591ad929ce16ea88c74d857153b307 go1.25.7.linux-mips.tar.gz +a8e97223d8aa6fdfd45f132a4784d2f536bbac5f3d63a24b63d33b6bfe1549af go1.25.7.linux-mips64.tar.gz +eb9edb6223330d5e20275667c65dea076b064c08e595fe4eba5d7d6055cfaccf go1.25.7.linux-mips64le.tar.gz +9c1e693552a5f9bb9e0012d1c5e01456ecefbc59bef53a77305222ce10aba368 go1.25.7.linux-mipsle.tar.gz +28a788798e7329acbbc0ac2caa5e4368b1e5ede646cc24429c991214cfb45c63 go1.25.7.linux-ppc64.tar.gz +42124c0edc92464e2b37b2d7fcd3658f0c47ebd6a098732415a522be8cb88e3f go1.25.7.linux-ppc64le.tar.gz +88d59c6893c8425875d6eef8e3434bc2fa2552e5ad4c058c6cd8cd710a0301c8 go1.25.7.linux-riscv64.tar.gz +c6b77facf666dc68195ecab05dbf0ebb4e755b2a8b7734c759880557f1c29b0c go1.25.7.linux-s390x.tar.gz +f14c184d9ade0ee04c7735d4071257b90896ecbde1b32adae84135f055e6399b go1.25.7.netbsd-386.tar.gz +7e7389e404dca1088c31f0fc07f1dd60891d7182bcd621469c14f7e79eceb3ff go1.25.7.netbsd-amd64.tar.gz +70388bb3ef2f03dbf1357e9056bd09034a67e018262557354f8cf549766b3f9d go1.25.7.netbsd-arm.tar.gz +8c1cda9d25bfc9b18d24d5f95fc23949dd3ff99fa408a6cfa40e2cf12b07e362 go1.25.7.netbsd-arm64.tar.gz +42f0d1bfbe39b8401cccb84dd66b30795b97bfc9620dfdc17c5cd4fcf6495cb0 go1.25.7.openbsd-386.tar.gz +e514879c0a28bc32123cd52c4c093de912477fe83f36a6d07517d066ef55391a go1.25.7.openbsd-amd64.tar.gz +8cd22530695a0218232bf7efea8f162df1697a3106942ac4129b8c3de39ce4ef go1.25.7.openbsd-arm.tar.gz +938720f6ebc0d1c53d7840321d3a31f29fd02496e84a6538f442a9311dc1cc9a go1.25.7.openbsd-arm64.tar.gz +a4c378b73b98f89a3596c2ef51aabbb28783d9ca29f7e317d8ca07939660ce6f go1.25.7.openbsd-ppc64.tar.gz +937b58734fbeaa8c7941a0e4285e7e84b7885396e8d11c23f9ab1a8ff10ff20e go1.25.7.openbsd-riscv64.tar.gz +61a093c8c5244916f25740316386bb9f141545dcf01b06a79d1c78ece488403e go1.25.7.plan9-386.tar.gz +7fc8f6689c9de8ccb7689d2278035fa83c2d601409101840df6ddfe09ba58699 go1.25.7.plan9-amd64.tar.gz +9661dff8eaeeb62f1c3aadbc5ff189a2e6744e1ec885e32dbcb438f58a34def5 go1.25.7.plan9-arm.tar.gz +28ecba0e1d7950c8b29a4a04962dd49c3bf5221f55a44f17d98f369f82859cf4 go1.25.7.solaris-amd64.tar.gz +baa6b488291801642fa620026169e38bec2da2ac187cd3ae2145721cf826bbc3 go1.25.7.windows-386.zip +c75e5f4ff62d085cc0017be3ad19d5536f46825fa05db06ec468941f847e3228 go1.25.7.windows-amd64.zip +807033f85931bc4a589ca8497535dcbeb1f30d506e47fa200f5f04c4a71c3d9f go1.25.7.windows-arm64.zip -# version:golangci 2.4.0 +# version:golangci 2.10.1 # https://github.com/golangci/golangci-lint/releases/ -# https://github.com/golangci/golangci-lint/releases/download/v2.4.0/ -7904ce63f79db44934939cf7a063086ea0ea98e9b19eba0a9d52ccdd0d21951c golangci-lint-2.4.0-darwin-amd64.tar.gz -cd4dd53fa09b6646baff5fd22b8c64d91db02c21c7496df27992d75d34feec59 golangci-lint-2.4.0-darwin-arm64.tar.gz -d58f426ebe14cc257e81562b4bf37a488ffb4ffbbb3ec73041eb3b38bb25c0e1 golangci-lint-2.4.0-freebsd-386.tar.gz -6ec4a6177fc6c0dd541fbcb3a7612845266d020d35cc6fa92959220cdf64ca39 golangci-lint-2.4.0-freebsd-amd64.tar.gz -4d473e3e71c01feaa915a0604fb35758b41284fb976cdeac3f842118d9ee7e17 golangci-lint-2.4.0-freebsd-armv6.tar.gz -58727746c6530801a3f9a702a5945556a5eb7e88809222536dd9f9d54cafaeff golangci-lint-2.4.0-freebsd-armv7.tar.gz -fbf28c662760e24c32f82f8d16dffdb4a82de7726a52ba1fad94f890c22997ea golangci-lint-2.4.0-illumos-amd64.tar.gz -a15a000a8981ef665e971e0f67e2acda9066a9e37a59344393b7351d8fb49c81 golangci-lint-2.4.0-linux-386.tar.gz -fae792524c04424c0ac369f5b8076f04b45cf29fc945a370e55d369a8dc11840 golangci-lint-2.4.0-linux-amd64.tar.gz -70ac11f55b80ec78fd3a879249cc9255121b8dfd7f7ed4fc46ed137f4abf17e7 golangci-lint-2.4.0-linux-arm64.tar.gz -4acdc40e5cebe99e4e7ced358a05b2e71789f409b41cb4f39bbb86ccfa14b1dc golangci-lint-2.4.0-linux-armv6.tar.gz -2a68749568fa22b4a97cb88dbea655595563c795076536aa6c087f7968784bf3 golangci-lint-2.4.0-linux-armv7.tar.gz -9e3369afb023711036dcb0b4f45c9fe2792af962fa1df050c9f6ac101a6c5d73 golangci-lint-2.4.0-linux-loong64.tar.gz -bb9143d6329be2c4dbfffef9564078e7da7d88e7dde6c829b6263d98e072229e golangci-lint-2.4.0-linux-mips64.tar.gz -5ad1765b40d56cd04d4afd805b3ba6f4bfd9b36181da93c31e9b17e483d8608d golangci-lint-2.4.0-linux-mips64le.tar.gz -918936fb9c0d5ba96bef03cf4348b03938634cfcced49be1e9bb29cb5094fa73 golangci-lint-2.4.0-linux-ppc64le.tar.gz -f7474c638e1fb67ebbdc654b55ca0125377ea0bc88e8fee8d964a4f24eacf828 golangci-lint-2.4.0-linux-riscv64.tar.gz -b617a9543997c8bfceaffa88a75d4e595030c6add69fba800c1e4d8f5fe253dd golangci-lint-2.4.0-linux-s390x.tar.gz -7db027b03a9ba328f795215b04f594036837bc7dd0dd7cd16776b02a6167981c golangci-lint-2.4.0-netbsd-386.tar.gz -52d8f9393f4313df0a62b752c37775e3af0b818e43e8dd28954351542d7c60bc golangci-lint-2.4.0-netbsd-amd64.tar.gz -5c0086027fb5a4af3829e530c8115db4b35d11afe1914322eef528eb8cd38c69 golangci-lint-2.4.0-netbsd-arm64.tar.gz -6b779d6ed1aed87cefe195cc11759902b97a76551b593312c6833f2635a3488f golangci-lint-2.4.0-netbsd-armv6.tar.gz -f00d1f4b7ec3468a0f9fffd0d9ea036248b029b7621cbc9a59c449ef94356d09 golangci-lint-2.4.0-netbsd-armv7.tar.gz -3ce671b0b42b58e35066493aab75a7e2826c9e079988f1ba5d814a4029faaf87 golangci-lint-2.4.0-windows-386.zip -003112f7a56746feaabf20b744054bf9acdf900c9e77176383623c4b1d76aaa9 golangci-lint-2.4.0-windows-amd64.zip -dc0c2092af5d47fc2cd31a1dfe7b4c7e765fab22de98bd21ef2ffcc53ad9f54f golangci-lint-2.4.0-windows-arm64.zip -0263d23e20a260cb1592d35e12a388f99efe2c51b3611fdc66fbd9db1fce664d golangci-lint-2.4.0-windows-armv6.zip -9403c03bf648e6313036e0273149d44bad1b9ad53889b6d00e4ccb842ba3c058 golangci-lint-2.4.0-windows-armv7.zip +# https://github.com/golangci/golangci-lint/releases/download/v2.10.1 +66fb0da81b8033b477f97eea420d4b46b230ca172b8bb87c6610109f3772b6b6 golangci-lint-2.10.1-darwin-amd64.tar.gz +03bfadf67e52b441b7ec21305e501c717df93c959836d66c7f97312654acb297 golangci-lint-2.10.1-darwin-arm64.tar.gz +c9a44658ccc8f7b8dbbd4ae6020ba91c1a5d3987f4d91ced0f7d2bea013e57ca golangci-lint-2.10.1-freebsd-386.tar.gz +a513c5cb4e0f5bd5767001af9d5e97e7868cfc2d9c46739a4df93e713cfb24af golangci-lint-2.10.1-freebsd-amd64.tar.gz +2ef38eefc4b5cee2febacb75a30579526e5656c16338a921d80e59a8e87d4425 golangci-lint-2.10.1-freebsd-arm64.tar.gz +8fea6766318b4829e766bbe325f10191d75297dcc44ae35bf374816037878e38 golangci-lint-2.10.1-freebsd-armv6.tar.gz +30b629870574d6254f3e8804e5a74b34f98e1263c9d55465830d739c88b862ed golangci-lint-2.10.1-freebsd-armv7.tar.gz +c0db839f866ce80b1b6c96167aa101cfe50d9c936f42d942a3c1cbdc1801af68 golangci-lint-2.10.1-illumos-amd64.tar.gz +280eb56636e9175f671cd7b755d7d67f628ae2ed00a164d1e443c43c112034e5 golangci-lint-2.10.1-linux-386.deb +065a7d99da61dc7dfbfef2e2d7053dd3fa6672598f2747117aa4bb5f45e7df7f golangci-lint-2.10.1-linux-386.rpm +a55918c03bb413b2662287653ab2ae2fef4e37428b247dad6348724adde9d770 golangci-lint-2.10.1-linux-386.tar.gz +8aa9b3aa14f39745eeb7fc7ff50bcac683e785397d1e4bc9afd2184b12c4ce86 golangci-lint-2.10.1-linux-amd64.deb +62a111688e9e305032334a2cbc84f4d971b64bb3bffc99d3f80081d57fb25e32 golangci-lint-2.10.1-linux-amd64.rpm +dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09c99 golangci-lint-2.10.1-linux-amd64.tar.gz +b3f36937e8ea1660739dc0f5c892ea59c9c21ed4e75a91a25957c561f7f79a55 golangci-lint-2.10.1-linux-arm64.deb +36d50314d53683b1f1a2a6cedfb5a9468451b481c64ab9e97a8e843ea088074d golangci-lint-2.10.1-linux-arm64.rpm +6652b42ae02915eb2f9cb2a2e0cac99514c8eded8388d88ae3e06e1a52c00de8 golangci-lint-2.10.1-linux-arm64.tar.gz +a32d8d318e803496812dd3461f250e52ccc7f53c47b95ce404a9cf55778ceb6a golangci-lint-2.10.1-linux-armv6.deb +41d065f4c8ea165a1531abea644988ee2e973e4f0b49f9725ed3b979dac45112 golangci-lint-2.10.1-linux-armv6.rpm +59159a4df03aabbde69d15c7b7b3df143363cbb41f4bd4b200caffb8e34fb734 golangci-lint-2.10.1-linux-armv6.tar.gz +b2e8ec0e050a1e2251dfe1561434999d202f5a3f9fa47ce94378b0fd1662ea5a golangci-lint-2.10.1-linux-armv7.deb +28c9331429a497da27e9c77846063bd0e8275e878ffedb4eb9e9f21d24771cc0 golangci-lint-2.10.1-linux-armv7.rpm +818f33e95b273e3769284b25563b51ef6a294e9e25acf140fda5830c075a1a59 golangci-lint-2.10.1-linux-armv7.tar.gz +6b6b85ed4b7c27f51097dd681523000409dde835e86e6e314e87be4bb013e2ab golangci-lint-2.10.1-linux-loong64.deb +94050a0cf06169e2ae44afb307dcaafa7d7c3b38c0c23b5652cf9cb60f0c337f golangci-lint-2.10.1-linux-loong64.rpm +25820300fccb8c961c1cdcb1f77928040c079e04c43a3a5ceb34b1cb4a1c5c8d golangci-lint-2.10.1-linux-loong64.tar.gz +98bf39d10139fdcaa37f94950e9bbb8888660ae468847ae0bf1cb5bf67c1f68b golangci-lint-2.10.1-linux-mips64.deb +df3ce5f03808dcceaa8b683d1d06e95c885f09b59dc8e15deb840fbe2b3e3299 golangci-lint-2.10.1-linux-mips64.rpm +972508dda523067e6e6a1c8e6609d63bc7c4153819c11b947d439235cf17bac2 golangci-lint-2.10.1-linux-mips64.tar.gz +1d37f2919e183b5bf8b1777ed8c4b163d3b491d0158355a7999d647655cbbeb6 golangci-lint-2.10.1-linux-mips64le.deb +e341d031002cd09a416329ed40f674231051a38544b8f94deb2d1708ce1f4a6f golangci-lint-2.10.1-linux-mips64le.rpm +393560122b9cb5538df0c357d30eb27b6ee563533fbb9b138c8db4fd264002af golangci-lint-2.10.1-linux-mips64le.tar.gz +21ca46b6a96442e8957677a3ca059c6b93674a68a01b1c71f4e5df0ea2e96d19 golangci-lint-2.10.1-linux-ppc64le.deb +57fe0cbca0a9bbdf1547c5e8aa7d278e6896b438d72a541bae6bc62c38b43d1e golangci-lint-2.10.1-linux-ppc64le.rpm +e2883db9fa51584e5e203c64456f29993550a7faadc84e3faccdb48f0669992e golangci-lint-2.10.1-linux-ppc64le.tar.gz +aa6da0e98ab0ba3bb7582e112174c349907d5edfeff90a551dca3c6eecf92fc0 golangci-lint-2.10.1-linux-riscv64.deb +3c68d76cd884a7aad206223a980b9c20bb9ea74b560fa27ed02baf2389189234 golangci-lint-2.10.1-linux-riscv64.rpm +3bca11bfac4197205639cbd4676a5415054e629ac6c12ea10fcbe33ef852d9c3 golangci-lint-2.10.1-linux-riscv64.tar.gz +0c6aed2ce49db2586adbac72c80d871f06feb1caf4c0763a5ca98fec809a8f0b golangci-lint-2.10.1-linux-s390x.deb +16c285adfe1061d69dd8e503be69f87c7202857c6f4add74ac02e3571158fbec golangci-lint-2.10.1-linux-s390x.rpm +21011ad368eb04f024201b832095c6b5f96d0888de194cca5bfe4d9307d6364b golangci-lint-2.10.1-linux-s390x.tar.gz +7b5191e77a70485918712e31ed55159956323e4911bab1b67569c9d86e1b75eb golangci-lint-2.10.1-netbsd-386.tar.gz +07801fd38d293ebad10826f8285525a39ea91ce5ddad77d05bfa90bda9c884a9 golangci-lint-2.10.1-netbsd-amd64.tar.gz +7e7219d71c1bf33b98c328c93dc0560706dd896a1c43c44696e5222fc9d7446e golangci-lint-2.10.1-netbsd-arm64.tar.gz +92fbc90b9eec0e572269b0f5492a2895c426b086a68372fde49b7e4d4020863e golangci-lint-2.10.1-netbsd-armv6.tar.gz +f67b3ae1f47caeefa507a4ebb0c8336958a19011fe48766443212030f75d004b golangci-lint-2.10.1-netbsd-armv7.tar.gz +a40bc091c10cea84eaee1a90b84b65f5e8652113b0a600bb099e4e4d9d7caddb golangci-lint-2.10.1-windows-386.zip +c60c87695e79db8e320f0e5be885059859de52bb5ee5f11be5577828570bc2a3 golangci-lint-2.10.1-windows-amd64.zip +636ab790c8dcea8034aa34aba6031ca3893d68f7eda000460ab534341fadbab1 golangci-lint-2.10.1-windows-arm64.zip # This is the builder on PPA that will build Go itself (inception-y), don't modify! # diff --git a/build/ci.go b/build/ci.go index ddeb547177..858a9fb253 100644 --- a/build/ci.go +++ b/build/ci.go @@ -168,8 +168,6 @@ var ( "focal", // 20.04, EOL: 04/2030 "jammy", // 22.04, EOL: 04/2032 "noble", // 24.04, EOL: 04/2034 - "oracular", // 24.10, EOL: 07/2025 - "plucky", // 25.04, EOL: 01/2026 } // This is where the tests should be unpacked. diff --git a/cmd/devp2p/internal/v5test/discv5tests.go b/cmd/devp2p/internal/v5test/discv5tests.go index af54b0c4ef..bd0709ed33 100644 --- a/cmd/devp2p/internal/v5test/discv5tests.go +++ b/cmd/devp2p/internal/v5test/discv5tests.go @@ -54,7 +54,7 @@ func (s *Suite) AllTests() []utesting.Test { {Name: "Ping", Fn: s.TestPing}, {Name: "PingLargeRequestID", Fn: s.TestPingLargeRequestID}, {Name: "PingMultiIP", Fn: s.TestPingMultiIP}, - {Name: "PingHandshakeInterrupted", Fn: s.TestPingHandshakeInterrupted}, + {Name: "HandshakeResend", Fn: s.TestHandshakeResend}, {Name: "TalkRequest", Fn: s.TestTalkRequest}, {Name: "FindnodeZeroDistance", Fn: s.TestFindnodeZeroDistance}, {Name: "FindnodeResults", Fn: s.TestFindnodeResults}, @@ -167,23 +167,20 @@ the attempt from a different IP.`) } } -// TestPingHandshakeInterrupted starts a handshake, but doesn't finish it and sends a second ordinary message -// packet instead of a handshake message packet. The remote node should respond with -// another WHOAREYOU challenge for the second packet. -func (s *Suite) TestPingHandshakeInterrupted(t *utesting.T) { - t.Log(`TestPingHandshakeInterrupted starts a handshake, but doesn't finish it and sends a second ordinary message -packet instead of a handshake message packet. The remote node should respond with -another WHOAREYOU challenge for the second packet.`) - +// TestHandshakeResend starts a handshake, but doesn't finish it and sends a second ordinary message +// packet instead of a handshake message packet. The remote node should repeat the previous WHOAREYOU +// challenge for the first PING. +func (s *Suite) TestHandshakeResend(t *utesting.T) { conn, l1 := s.listen1(t) defer conn.close() // First PING triggers challenge. ping := &v5wire.Ping{ReqID: conn.nextReqID()} conn.write(l1, ping, nil) - + var challenge1 *v5wire.Whoareyou switch resp := conn.read(l1).(type) { case *v5wire.Whoareyou: + challenge1 = resp t.Logf("got WHOAREYOU for PING") default: t.Fatal("expected WHOAREYOU, got", resp) @@ -191,9 +188,16 @@ another WHOAREYOU challenge for the second packet.`) // Send second PING. ping2 := &v5wire.Ping{ReqID: conn.nextReqID()} - switch resp := conn.reqresp(l1, ping2).(type) { - case *v5wire.Pong: - checkPong(t, resp, ping2, l1) + conn.write(l1, ping2, nil) + switch resp := conn.read(l1).(type) { + case *v5wire.Whoareyou: + if resp.Nonce != challenge1.Nonce { + t.Fatalf("wrong nonce %x in WHOAREYOU (want %x)", resp.Nonce[:], challenge1.Nonce[:]) + } + if !bytes.Equal(resp.ChallengeData, challenge1.ChallengeData) { + t.Fatalf("wrong ChallengeData in resent WHOAREYOU (want %x)", resp.ChallengeData, challenge1.ChallengeData) + } + resp.Node = conn.remote default: t.Fatal("expected WHOAREYOU, got", resp) } diff --git a/cmd/evm/internal/t8ntool/block.go b/cmd/evm/internal/t8ntool/block.go index 15a4432aa4..4fa2a62162 100644 --- a/cmd/evm/internal/t8ntool/block.go +++ b/cmd/evm/internal/t8ntool/block.go @@ -57,6 +57,7 @@ type header struct { BlobGasUsed *uint64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + SlotNumber *uint64 `json:"slotNumber" rlp:"optional"` } type headerMarshaling struct { @@ -69,6 +70,7 @@ type headerMarshaling struct { BaseFee *math.HexOrDecimal256 BlobGasUsed *math.HexOrDecimal64 ExcessBlobGas *math.HexOrDecimal64 + SlotNumber *math.HexOrDecimal64 } type bbInput struct { @@ -142,6 +144,7 @@ func (i *bbInput) ToBlock() *types.Block { BlobGasUsed: i.Header.BlobGasUsed, ExcessBlobGas: i.Header.ExcessBlobGas, ParentBeaconRoot: i.Header.ParentBeaconBlockRoot, + SlotNumber: i.Header.SlotNumber, } // Fill optional values. diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 3c6790225f..0f1c00e03b 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -103,6 +103,7 @@ type stEnv struct { ParentExcessBlobGas *uint64 `json:"parentExcessBlobGas,omitempty"` ParentBlobGasUsed *uint64 `json:"parentBlobGasUsed,omitempty"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot"` + SlotNumber *uint64 `json:"slotNumber"` } type stEnvMarshaling struct { @@ -121,6 +122,7 @@ type stEnvMarshaling struct { ExcessBlobGas *math.HexOrDecimal64 ParentExcessBlobGas *math.HexOrDecimal64 ParentBlobGasUsed *math.HexOrDecimal64 + SlotNumber *math.HexOrDecimal64 } type rejectedTx struct { @@ -201,6 +203,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, ExcessBlobGas: pre.Env.ParentExcessBlobGas, BlobGasUsed: pre.Env.ParentBlobGasUsed, BaseFee: pre.Env.ParentBaseFee, + SlotNumber: pre.Env.SlotNumber, } header := &types.Header{ Time: pre.Env.Timestamp, @@ -273,6 +276,9 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, gaspool.SetGas(prevGas) continue } + if receipt.Logs == nil { + receipt.Logs = []*types.Log{} + } includedTxs = append(includedTxs, tx) if hashError != nil { diff --git a/cmd/evm/internal/t8ntool/file_tracer.go b/cmd/evm/internal/t8ntool/file_tracer.go index 38fc35bd32..d032252cba 100644 --- a/cmd/evm/internal/t8ntool/file_tracer.go +++ b/cmd/evm/internal/t8ntool/file_tracer.go @@ -56,27 +56,35 @@ func (l *fileWritingTracer) Write(p []byte) (n int, err error) { return n, nil } -// newFileWriter creates a set of hooks which wraps inner hooks (typically a logger), +// newFileWriter creates a tracer which wraps inner hooks (typically a logger), // and writes the output to a file, one file per transaction. -func newFileWriter(baseDir string, innerFn func(out io.Writer) *tracing.Hooks) *tracing.Hooks { +func newFileWriter(baseDir string, innerFn func(out io.Writer) *tracing.Hooks) *tracers.Tracer { t := &fileWritingTracer{ baseDir: baseDir, suffix: "jsonl", } t.inner = innerFn(t) // instantiate the inner tracer - return t.hooks() + return &tracers.Tracer{ + Hooks: t.hooks(), + GetResult: func() (json.RawMessage, error) { return json.RawMessage("{}"), nil }, + Stop: func(err error) {}, + } } -// newResultWriter creates a set of hooks wraps and invokes an underlying tracer, +// newResultWriter creates a tracer that wraps and invokes an underlying tracer, // and writes the result (getResult-output) to file, one per transaction. -func newResultWriter(baseDir string, tracer *tracers.Tracer) *tracing.Hooks { +func newResultWriter(baseDir string, tracer *tracers.Tracer) *tracers.Tracer { t := &fileWritingTracer{ baseDir: baseDir, getResult: tracer.GetResult, inner: tracer.Hooks, suffix: "json", } - return t.hooks() + return &tracers.Tracer{ + Hooks: t.hooks(), + GetResult: func() (json.RawMessage, error) { return json.RawMessage("{}"), nil }, + Stop: func(err error) {}, + } } // OnTxStart creates a new output-file specific for this transaction, and invokes diff --git a/cmd/evm/internal/t8ntool/flags.go b/cmd/evm/internal/t8ntool/flags.go index 90dcc2bf3f..5fd252fec6 100644 --- a/cmd/evm/internal/t8ntool/flags.go +++ b/cmd/evm/internal/t8ntool/flags.go @@ -163,6 +163,11 @@ var ( strings.Join(vm.ActivateableEips(), ", ")), Value: "GrayGlacier", } + OpcodeCountFlag = &cli.StringFlag{ + Name: "opcode.count", + Usage: "If set, opcode execution counts will be written to this file (relative to output.basedir).", + Value: "", + } VerbosityFlag = &cli.IntFlag{ Name: "verbosity", Usage: "sets the verbosity level", diff --git a/cmd/evm/internal/t8ntool/gen_header.go b/cmd/evm/internal/t8ntool/gen_header.go index a45520c26a..99ee3a5148 100644 --- a/cmd/evm/internal/t8ntool/gen_header.go +++ b/cmd/evm/internal/t8ntool/gen_header.go @@ -38,6 +38,7 @@ func (h header) MarshalJSON() ([]byte, error) { BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + SlotNumber *math.HexOrDecimal64 `json:"slotNumber" rlp:"optional"` } var enc header @@ -61,6 +62,7 @@ func (h header) MarshalJSON() ([]byte, error) { enc.BlobGasUsed = (*math.HexOrDecimal64)(h.BlobGasUsed) enc.ExcessBlobGas = (*math.HexOrDecimal64)(h.ExcessBlobGas) enc.ParentBeaconBlockRoot = h.ParentBeaconBlockRoot + enc.SlotNumber = (*math.HexOrDecimal64)(h.SlotNumber) return json.Marshal(&enc) } @@ -87,6 +89,7 @@ func (h *header) UnmarshalJSON(input []byte) error { BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + SlotNumber *math.HexOrDecimal64 `json:"slotNumber" rlp:"optional"` } var dec header @@ -175,5 +178,8 @@ func (h *header) UnmarshalJSON(input []byte) error { if dec.ParentBeaconBlockRoot != nil { h.ParentBeaconBlockRoot = dec.ParentBeaconBlockRoot } + if dec.SlotNumber != nil { + h.SlotNumber = (*uint64)(dec.SlotNumber) + } return nil } diff --git a/cmd/evm/internal/t8ntool/gen_stenv.go b/cmd/evm/internal/t8ntool/gen_stenv.go index 04503c9316..cc60bee34e 100644 --- a/cmd/evm/internal/t8ntool/gen_stenv.go +++ b/cmd/evm/internal/t8ntool/gen_stenv.go @@ -37,6 +37,7 @@ func (s stEnv) MarshalJSON() ([]byte, error) { ParentExcessBlobGas *math.HexOrDecimal64 `json:"parentExcessBlobGas,omitempty"` ParentBlobGasUsed *math.HexOrDecimal64 `json:"parentBlobGasUsed,omitempty"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot"` + SlotNumber *math.HexOrDecimal64 `json:"slotNumber"` } var enc stEnv @@ -60,6 +61,7 @@ func (s stEnv) MarshalJSON() ([]byte, error) { enc.ParentExcessBlobGas = (*math.HexOrDecimal64)(s.ParentExcessBlobGas) enc.ParentBlobGasUsed = (*math.HexOrDecimal64)(s.ParentBlobGasUsed) enc.ParentBeaconBlockRoot = s.ParentBeaconBlockRoot + enc.SlotNumber = (*math.HexOrDecimal64)(s.SlotNumber) return json.Marshal(&enc) } @@ -86,6 +88,7 @@ func (s *stEnv) UnmarshalJSON(input []byte) error { ParentExcessBlobGas *math.HexOrDecimal64 `json:"parentExcessBlobGas,omitempty"` ParentBlobGasUsed *math.HexOrDecimal64 `json:"parentBlobGasUsed,omitempty"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot"` + SlotNumber *math.HexOrDecimal64 `json:"slotNumber"` } var dec stEnv @@ -174,5 +177,8 @@ func (s *stEnv) UnmarshalJSON(input []byte) error { if dec.ParentBeaconBlockRoot != nil { s.ParentBeaconBlockRoot = dec.ParentBeaconBlockRoot } + if dec.SlotNumber != nil { + s.SlotNumber = (*uint64)(dec.SlotNumber) + } return nil } diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go index fbd78977cf..5f0903b4f8 100644 --- a/cmd/evm/internal/t8ntool/transition.go +++ b/cmd/evm/internal/t8ntool/transition.go @@ -39,6 +39,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/eth/tracers/native" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/tests" @@ -173,14 +174,15 @@ func Transition(ctx *cli.Context) error { } // Configure tracer + var tracer *tracers.Tracer if ctx.IsSet(TraceTracerFlag.Name) { // Custom tracing config := json.RawMessage(ctx.String(TraceTracerConfigFlag.Name)) - tracer, err := tracers.DefaultDirectory.New(ctx.String(TraceTracerFlag.Name), + innerTracer, err := tracers.DefaultDirectory.New(ctx.String(TraceTracerFlag.Name), nil, config, chainConfig) if err != nil { return NewError(ErrorConfig, fmt.Errorf("failed instantiating tracer: %v", err)) } - vmConfig.Tracer = newResultWriter(baseDir, tracer) + tracer = newResultWriter(baseDir, innerTracer) } else if ctx.Bool(TraceFlag.Name) { // JSON opcode tracing logConfig := &logger.Config{ DisableStack: ctx.Bool(TraceDisableStackFlag.Name), @@ -188,20 +190,45 @@ func Transition(ctx *cli.Context) error { EnableReturnData: ctx.Bool(TraceEnableReturnDataFlag.Name), } if ctx.Bool(TraceEnableCallFramesFlag.Name) { - vmConfig.Tracer = newFileWriter(baseDir, func(out io.Writer) *tracing.Hooks { + tracer = newFileWriter(baseDir, func(out io.Writer) *tracing.Hooks { return logger.NewJSONLoggerWithCallFrames(logConfig, out) }) } else { - vmConfig.Tracer = newFileWriter(baseDir, func(out io.Writer) *tracing.Hooks { + tracer = newFileWriter(baseDir, func(out io.Writer) *tracing.Hooks { return logger.NewJSONLogger(logConfig, out) }) } } + // Configure opcode counter + var opcodeTracer *tracers.Tracer + if ctx.IsSet(OpcodeCountFlag.Name) && ctx.String(OpcodeCountFlag.Name) != "" { + opcodeTracer = native.NewOpcodeCounter() + if tracer != nil { + // If we have an existing tracer, multiplex with the opcode tracer + mux, _ := native.NewMuxTracer([]string{"trace", "opcode"}, []*tracers.Tracer{tracer, opcodeTracer}) + vmConfig.Tracer = mux.Hooks + } else { + vmConfig.Tracer = opcodeTracer.Hooks + } + } else if tracer != nil { + vmConfig.Tracer = tracer.Hooks + } // Run the test and aggregate the result s, result, body, err := prestate.Apply(vmConfig, chainConfig, txIt, ctx.Int64(RewardFlag.Name)) if err != nil { return err } + // Write opcode counts if enabled + if opcodeTracer != nil { + fname := ctx.String(OpcodeCountFlag.Name) + result, err := opcodeTracer.GetResult() + if err != nil { + return NewError(ErrorJson, fmt.Errorf("failed getting opcode counts: %v", err)) + } + if err := saveFile(baseDir, fname, result); err != nil { + return err + } + } // Dump the execution result var ( collector = make(Alloc) diff --git a/cmd/evm/main.go b/cmd/evm/main.go index 3a5576437d..ec64bae8b2 100644 --- a/cmd/evm/main.go +++ b/cmd/evm/main.go @@ -162,6 +162,7 @@ var ( t8ntool.ForknameFlag, t8ntool.ChainIDFlag, t8ntool.RewardFlag, + t8ntool.OpcodeCountFlag, }, } diff --git a/cmd/evm/testdata/1/exp.json b/cmd/evm/testdata/1/exp.json index 6537c9517d..f596cb5e67 100644 --- a/cmd/evm/testdata/1/exp.json +++ b/cmd/evm/testdata/1/exp.json @@ -24,7 +24,7 @@ "status": "0x1", "cumulativeGasUsed": "0x5208", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0x0557bacce3375c98d806609b8d5043072f0b6a8bae45ae5a67a00d3a1a18d673", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0x5208", diff --git a/cmd/evm/testdata/13/exp2.json b/cmd/evm/testdata/13/exp2.json index f716289cf7..260721e22f 100644 --- a/cmd/evm/testdata/13/exp2.json +++ b/cmd/evm/testdata/13/exp2.json @@ -12,7 +12,7 @@ "status": "0x0", "cumulativeGasUsed": "0x84d0", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0xa98a24882ea90916c6a86da650fbc6b14238e46f0af04a131ce92be897507476", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0x84d0", @@ -27,7 +27,7 @@ "status": "0x0", "cumulativeGasUsed": "0x109a0", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0x36bad80acce7040c45fd32764b5c2b2d2e6f778669fb41791f73f546d56e739a", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0x84d0", diff --git a/cmd/evm/testdata/23/exp.json b/cmd/evm/testdata/23/exp.json index 2d9cd492db..803411de46 100644 --- a/cmd/evm/testdata/23/exp.json +++ b/cmd/evm/testdata/23/exp.json @@ -11,7 +11,7 @@ "status": "0x1", "cumulativeGasUsed": "0x520b", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0x72fadbef39cd251a437eea619cfeda752271a5faaaa2147df012e112159ffb81", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0x520b", diff --git a/cmd/evm/testdata/24/exp.json b/cmd/evm/testdata/24/exp.json index 0dd552e112..bbde70c631 100644 --- a/cmd/evm/testdata/24/exp.json +++ b/cmd/evm/testdata/24/exp.json @@ -27,7 +27,7 @@ "status": "0x1", "cumulativeGasUsed": "0xa861", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0x92ea4a28224d033afb20e0cc2b290d4c7c2d61f6a4800a680e4e19ac962ee941", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0xa861", @@ -41,7 +41,7 @@ "status": "0x1", "cumulativeGasUsed": "0x10306", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0x16b1d912f1d664f3f60f4e1b5f296f3c82a64a1a253117b4851d18bc03c4f1da", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0x5aa5", diff --git a/cmd/evm/testdata/25/exp.json b/cmd/evm/testdata/25/exp.json index 3dac46aa60..25cac55dc0 100644 --- a/cmd/evm/testdata/25/exp.json +++ b/cmd/evm/testdata/25/exp.json @@ -23,7 +23,7 @@ "status": "0x1", "cumulativeGasUsed": "0x5208", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0x92ea4a28224d033afb20e0cc2b290d4c7c2d61f6a4800a680e4e19ac962ee941", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0x5208", diff --git a/cmd/evm/testdata/28/exp.json b/cmd/evm/testdata/28/exp.json index 15b29bc0ac..f67ff76087 100644 --- a/cmd/evm/testdata/28/exp.json +++ b/cmd/evm/testdata/28/exp.json @@ -28,7 +28,7 @@ "status": "0x1", "cumulativeGasUsed": "0xa865", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0x7508d7139d002a4b3a26a4f12dec0d87cb46075c78bf77a38b569a133b509262", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0xa865", diff --git a/cmd/evm/testdata/29/exp.json b/cmd/evm/testdata/29/exp.json index 69c8661aa8..cb4b5eaa28 100644 --- a/cmd/evm/testdata/29/exp.json +++ b/cmd/evm/testdata/29/exp.json @@ -26,7 +26,7 @@ "status": "0x1", "cumulativeGasUsed": "0x5208", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0x84f70aba406a55628a0620f26d260f90aeb6ccc55fed6ec2ac13dd4f727032ed", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0x5208", diff --git a/cmd/evm/testdata/3/exp.json b/cmd/evm/testdata/3/exp.json index 807cdccfb4..36bf2604af 100644 --- a/cmd/evm/testdata/3/exp.json +++ b/cmd/evm/testdata/3/exp.json @@ -24,7 +24,7 @@ "status": "0x1", "cumulativeGasUsed": "0x521f", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0x72fadbef39cd251a437eea619cfeda752271a5faaaa2147df012e112159ffb81", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0x521f", diff --git a/cmd/evm/testdata/30/exp.json b/cmd/evm/testdata/30/exp.json index 9861f5a071..7df3b78820 100644 --- a/cmd/evm/testdata/30/exp.json +++ b/cmd/evm/testdata/30/exp.json @@ -25,7 +25,7 @@ "status": "0x1", "cumulativeGasUsed": "0x5208", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0xa98a24882ea90916c6a86da650fbc6b14238e46f0af04a131ce92be897507476", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0x5208", @@ -40,7 +40,7 @@ "status": "0x1", "cumulativeGasUsed": "0xa410", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "logs": null, + "logs": [], "transactionHash": "0x36bad80acce7040c45fd32764b5c2b2d2e6f778669fb41791f73f546d56e739a", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0x5208", diff --git a/cmd/evm/testdata/33/exp.json b/cmd/evm/testdata/33/exp.json index b40ca9fee2..73aaf80a28 100644 --- a/cmd/evm/testdata/33/exp.json +++ b/cmd/evm/testdata/33/exp.json @@ -44,7 +44,7 @@ "root": "0x", "status": "0x1", "cumulativeGasUsed": "0x15fa9", - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","logs": null,"transactionHash": "0x0417aab7c1d8a3989190c3167c132876ce9b8afd99262c5a0f9d06802de3d7ef", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","logs": [],"transactionHash": "0x0417aab7c1d8a3989190c3167c132876ce9b8afd99262c5a0f9d06802de3d7ef", "contractAddress": "0x0000000000000000000000000000000000000000", "gasUsed": "0x15fa9", "effectiveGasPrice": null, diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index cd9c1546f2..c15f23a98b 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -109,6 +109,7 @@ if one is set. Otherwise it prints the genesis from the datadir.`, utils.MetricsInfluxDBUsernameFlag, utils.MetricsInfluxDBPasswordFlag, utils.MetricsInfluxDBTagsFlag, + utils.MetricsInfluxDBIntervalFlag, utils.MetricsInfluxDBTokenFlag, utils.MetricsInfluxDBBucketFlag, utils.MetricsInfluxDBOrganizationFlag, diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 7fa01a737f..0760afcaf0 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -360,7 +360,9 @@ func applyMetricConfig(ctx *cli.Context, cfg *gethConfig) { if ctx.IsSet(utils.MetricsInfluxDBTagsFlag.Name) { cfg.Metrics.InfluxDBTags = ctx.String(utils.MetricsInfluxDBTagsFlag.Name) } - + if ctx.IsSet(utils.MetricsInfluxDBIntervalFlag.Name) { + cfg.Metrics.InfluxDBInterval = ctx.Duration(utils.MetricsInfluxDBIntervalFlag.Name) + } if ctx.IsSet(utils.MetricsEnableInfluxDBV2Flag.Name) { cfg.Metrics.EnableInfluxDBV2 = ctx.Bool(utils.MetricsEnableInfluxDBV2Flag.Name) } diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index 45c7eb101a..9120897b3b 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -19,6 +19,7 @@ package main import ( "bytes" "fmt" + "math" "os" "os/signal" "path/filepath" @@ -39,6 +40,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/internal/tablewriter" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" @@ -54,6 +56,23 @@ var ( Name: "remove.chain", Usage: "If set, selects the state data for removal", } + inspectTrieTopFlag = &cli.IntFlag{ + Name: "top", + Usage: "Print the top N results per ranking category", + Value: 10, + } + inspectTrieDumpPathFlag = &cli.StringFlag{ + Name: "dump-path", + Usage: "Path for the trie statistics dump file", + } + inspectTrieSummarizeFlag = &cli.StringFlag{ + Name: "summarize", + Usage: "Summarize an existing trie dump file (skip trie traversal)", + } + inspectTrieContractFlag = &cli.StringFlag{ + Name: "contract", + Usage: "Inspect only the storage of the given contract address (skips full account trie walk)", + } removedbCommand = &cli.Command{ Action: removeDB, @@ -75,6 +94,7 @@ Remove blockchain and state databases`, dbCompactCmd, dbGetCmd, dbDeleteCmd, + dbInspectTrieCmd, dbPutCmd, dbGetSlotsCmd, dbDumpFreezerIndex, @@ -93,6 +113,22 @@ Remove blockchain and state databases`, Usage: "Inspect the storage size for each type of data in the database", Description: `This commands iterates the entire database. If the optional 'prefix' and 'start' arguments are provided, then the iteration is limited to the given subset of data.`, } + dbInspectTrieCmd = &cli.Command{ + Action: inspectTrie, + Name: "inspect-trie", + ArgsUsage: "", + Flags: slices.Concat([]cli.Flag{ + utils.ExcludeStorageFlag, + inspectTrieTopFlag, + utils.OutputFileFlag, + inspectTrieDumpPathFlag, + inspectTrieSummarizeFlag, + inspectTrieContractFlag, + }, utils.NetworkFlags, utils.DatabaseFlags), + Usage: "Print detailed trie information about the structure of account trie and storage tries.", + Description: `This commands iterates the entrie trie-backed state. If the 'blocknum' is not specified, +the latest block number will be used by default.`, + } dbCheckStateContentCmd = &cli.Command{ Action: checkStateContent, Name: "check-state-content", @@ -403,6 +439,88 @@ func checkStateContent(ctx *cli.Context) error { return nil } +func inspectTrie(ctx *cli.Context) error { + topN := ctx.Int(inspectTrieTopFlag.Name) + if topN <= 0 { + return fmt.Errorf("invalid --%s value %d (must be > 0)", inspectTrieTopFlag.Name, topN) + } + config := &trie.InspectConfig{ + NoStorage: ctx.Bool(utils.ExcludeStorageFlag.Name), + TopN: topN, + Path: ctx.String(utils.OutputFileFlag.Name), + } + + if summarizePath := ctx.String(inspectTrieSummarizeFlag.Name); summarizePath != "" { + if ctx.NArg() > 0 { + return fmt.Errorf("block number argument is not supported with --%s", inspectTrieSummarizeFlag.Name) + } + config.DumpPath = summarizePath + log.Info("Summarizing trie dump", "path", summarizePath, "top", topN) + return trie.Summarize(summarizePath, config) + } + if ctx.NArg() > 1 { + return fmt.Errorf("excessive number of arguments: %v", ctx.Command.ArgsUsage) + } + + stack, _ := makeConfigNode(ctx) + db := utils.MakeChainDatabase(ctx, stack, false, false) + defer stack.Close() + defer db.Close() + + var ( + trieRoot common.Hash + hash common.Hash + number uint64 + ) + switch { + case ctx.NArg() == 0 || ctx.Args().Get(0) == "latest": + head := rawdb.ReadHeadHeaderHash(db) + n, ok := rawdb.ReadHeaderNumber(db, head) + if !ok { + return fmt.Errorf("could not load head block hash") + } + number = n + case ctx.Args().Get(0) == "snapshot": + trieRoot = rawdb.ReadSnapshotRoot(db) + number = math.MaxUint64 + default: + var err error + number, err = strconv.ParseUint(ctx.Args().Get(0), 10, 64) + if err != nil { + return fmt.Errorf("failed to parse blocknum, Args[0]: %v, err: %v", ctx.Args().Get(0), err) + } + } + + if number != math.MaxUint64 { + hash = rawdb.ReadCanonicalHash(db, number) + if hash == (common.Hash{}) { + return fmt.Errorf("canonical hash for block %d not found", number) + } + blockHeader := rawdb.ReadHeader(db, hash, number) + trieRoot = blockHeader.Root + } + if trieRoot == (common.Hash{}) { + log.Error("Empty root hash") + } + + config.DumpPath = ctx.String(inspectTrieDumpPathFlag.Name) + if config.DumpPath == "" { + config.DumpPath = stack.ResolvePath("trie-dump.bin") + } + + triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false) + defer triedb.Close() + + if contractAddr := ctx.String(inspectTrieContractFlag.Name); contractAddr != "" { + address := common.HexToAddress(contractAddr) + log.Info("Inspecting contract", "address", address, "root", trieRoot, "block", number) + return trie.InspectContract(triedb, db, trieRoot, address) + } + + log.Info("Inspecting trie", "root", trieRoot, "block", number, "dump", config.DumpPath, "top", topN) + return trie.Inspect(triedb, trieRoot, config) +} + func showDBStats(db ethdb.KeyValueStater) { stats, err := db.Stat() if err != nil { @@ -829,7 +947,7 @@ func showMetaData(ctx *cli.Context) error { data = append(data, []string{"headHeader.Root", fmt.Sprintf("%v", h.Root)}) data = append(data, []string{"headHeader.Number", fmt.Sprintf("%d (%#x)", h.Number, h.Number)}) } - table := rawdb.NewTableWriter(os.Stdout) + table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Field", "Value"}) table.AppendBulk(data) table.Render() diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 3640da493f..45769a52a3 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -218,6 +218,7 @@ var ( utils.MetricsInfluxDBUsernameFlag, utils.MetricsInfluxDBPasswordFlag, utils.MetricsInfluxDBTagsFlag, + utils.MetricsInfluxDBIntervalFlag, utils.MetricsEnableInfluxDBV2Flag, utils.MetricsInfluxDBTokenFlag, utils.MetricsInfluxDBBucketFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 2051c4b2c1..4e3688eb35 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -233,6 +233,16 @@ var ( Usage: "Max number of elements (0 = no limit)", Value: 0, } + TopFlag = &cli.IntFlag{ + Name: "top", + Usage: "Print the top N results", + Value: 5, + } + OutputFileFlag = &cli.StringFlag{ + Name: "output", + Usage: "Writes the result in json to the output", + Value: "", + } SnapshotFlag = &cli.BoolFlag{ Name: "snapshot", @@ -1048,6 +1058,13 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. Category: flags.MetricsCategory, } + MetricsInfluxDBIntervalFlag = &cli.DurationFlag{ + Name: "metrics.influxdb.interval", + Usage: "Interval between metrics reports to InfluxDB (with time unit, e.g. 10s)", + Value: metrics.DefaultConfig.InfluxDBInterval, + Category: flags.MetricsCategory, + } + MetricsEnableInfluxDBV2Flag = &cli.BoolFlag{ Name: "metrics.influxdbv2", Usage: "Enable metrics export/push to an external InfluxDB v2 database", @@ -2315,13 +2332,14 @@ func SetupMetrics(cfg *metrics.Config) { bucket = cfg.InfluxDBBucket organization = cfg.InfluxDBOrganization tagsMap = SplitTagsFlag(cfg.InfluxDBTags) + interval = cfg.InfluxDBInterval ) if enableExport { - log.Info("Enabling metrics export to InfluxDB") - go influxdb.InfluxDBWithTags(metrics.DefaultRegistry, 10*time.Second, endpoint, database, username, password, "geth.", tagsMap) + log.Info("Enabling metrics export to InfluxDB", "interval", interval) + go influxdb.InfluxDBWithTags(metrics.DefaultRegistry, interval, endpoint, database, username, password, "geth.", tagsMap) } else if enableExportV2 { - log.Info("Enabling metrics export to InfluxDB (v2)") - go influxdb.InfluxDBV2WithTags(metrics.DefaultRegistry, 10*time.Second, endpoint, token, bucket, organization, "geth.", tagsMap) + log.Info("Enabling metrics export to InfluxDB (v2)", "interval", interval) + go influxdb.InfluxDBV2WithTags(metrics.DefaultRegistry, interval, endpoint, token, bucket, organization, "geth.", tagsMap) } // Expvar exporter. diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index ac2a4c67a3..95e9ab1ed9 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -291,6 +291,17 @@ func (beacon *Beacon) verifyHeader(chain consensus.ChainHeaderReader, header, pa return err } } + + // EIP-7843 SLOTNUM: post-Amsterdam headers must carry a slotNumber, pre-Amsterdam + // must not. Amsterdam is dormant on Bor (AmsterdamBlock nil), so IsAmsterdam is + // false and headers must not carry a slotNumber. + amsterdam := chain.Config().IsAmsterdam(header.Number) + if amsterdam && header.SlotNumber == nil { + return errors.New("header is missing slotNumber") + } + if !amsterdam && header.SlotNumber != nil { + return fmt.Errorf("invalid slotNumber: have %d, expected nil", *header.SlotNumber) + } return nil } diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index 80c159bab0..aa8c1845ce 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -329,6 +329,8 @@ func (c *Clique) verifyHeader(chain consensus.ChainHeaderReader, header *types.H return fmt.Errorf("invalid blobGasUsed: have %d, expected nil", *header.BlobGasUsed) case header.ParentBeaconRoot != nil: return fmt.Errorf("invalid parentBeaconRoot, have %#x, expected nil", *header.ParentBeaconRoot) + case header.SlotNumber != nil: + return fmt.Errorf("invalid slotNumber, have %#x, expected nil", *header.SlotNumber) } // All basic checks passed, verify cascading fields return c.verifyCascadingFields(chain, header, parents) @@ -833,6 +835,9 @@ func encodeSigHeader(w io.Writer, header *types.Header) { if header.ParentBeaconRoot != nil { panic("unexpected parent beacon root value in clique") } + if header.SlotNumber != nil { + panic("unexpected slot number value in clique") + } if err := rlp.Encode(w, enc); err != nil { panic("can't encode: " + err.Error()) } diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index 3a64865f2d..de3ee00dd7 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -285,6 +285,8 @@ func (ethash *Ethash) verifyHeader(chain consensus.ChainHeaderReader, header, pa return fmt.Errorf("invalid blobGasUsed: have %d, expected nil", *header.BlobGasUsed) case header.ParentBeaconRoot != nil: return fmt.Errorf("invalid parentBeaconRoot, have %#x, expected nil", *header.ParentBeaconRoot) + case header.SlotNumber != nil: + return fmt.Errorf("invalid slotNumber, have %#x, expected nil", *header.SlotNumber) } // Add some fake checks for tests if ethash.fakeDelay != nil { @@ -566,6 +568,9 @@ func (ethash *Ethash) SealHash(header *types.Header) (hash common.Hash) { if header.ParentBeaconRoot != nil { panic("parent beacon root set on ethash") } + if header.SlotNumber != nil { + panic("slot number set on ethash") + } rlp.Encode(hasher, enc) hasher.Sum(hash[:0]) return hash diff --git a/core/evm.go b/core/evm.go index 0c16464d6d..0efd9ba02c 100644 --- a/core/evm.go +++ b/core/evm.go @@ -46,6 +46,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common baseFee *big.Int blobBaseFee *big.Int random *common.Hash + slotNum uint64 ) // If we don't have an explicit author (i.e. not mining), extract from the header @@ -91,6 +92,12 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common } } + // SlotNumber (EIP-7843 SLOTNUM) is dormant on Bor — the header field is + // nil pre-Amsterdam, so slotNum stays 0. + if header.SlotNumber != nil { + slotNum = *header.SlotNumber + } + return vm.BlockContext{ CanTransfer: CanTransfer, Transfer: transferFn, @@ -103,6 +110,7 @@ func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common BlobBaseFee: blobBaseFee, GasLimit: header.GasLimit, Random: random, + SlotNum: slotNum, } } diff --git a/core/gen_genesis.go b/core/gen_genesis.go index 9069f17dec..163701b965 100644 --- a/core/gen_genesis.go +++ b/core/gen_genesis.go @@ -34,6 +34,7 @@ func (g Genesis) MarshalJSON() ([]byte, error) { BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"` ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"` BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"` + SlotNumber *uint64 `json:"slotNumber"` } var enc Genesis @@ -59,6 +60,7 @@ func (g Genesis) MarshalJSON() ([]byte, error) { enc.BaseFee = (*math.HexOrDecimal256)(g.BaseFee) enc.ExcessBlobGas = (*math.HexOrDecimal64)(g.ExcessBlobGas) enc.BlobGasUsed = (*math.HexOrDecimal64)(g.BlobGasUsed) + enc.SlotNumber = g.SlotNumber return json.Marshal(&enc) } @@ -80,6 +82,7 @@ func (g *Genesis) UnmarshalJSON(input []byte) error { BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"` ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"` BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"` + SlotNumber *uint64 `json:"slotNumber"` } var dec Genesis @@ -152,5 +155,8 @@ func (g *Genesis) UnmarshalJSON(input []byte) error { if dec.BlobGasUsed != nil { g.BlobGasUsed = (*uint64)(dec.BlobGasUsed) } + if dec.SlotNumber != nil { + g.SlotNumber = dec.SlotNumber + } return nil } diff --git a/core/genesis.go b/core/genesis.go index f4b3e8008d..a2dd89ef07 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -78,6 +78,7 @@ type Genesis struct { BaseFee *big.Int `json:"baseFeePerGas"` // EIP-1559 ExcessBlobGas *uint64 `json:"excessBlobGas"` // EIP-4844 BlobGasUsed *uint64 `json:"blobGasUsed"` // EIP-4844 + SlotNumber *uint64 `json:"slotNumber"` // EIP-7843 } // copy copies the genesis. @@ -137,6 +138,7 @@ func ReadGenesis(db ethdb.Database) (*Genesis, error) { genesis.BaseFee = genesisHeader.BaseFee genesis.ExcessBlobGas = genesisHeader.ExcessBlobGas genesis.BlobGasUsed = genesisHeader.BlobGasUsed + genesis.SlotNumber = genesisHeader.SlotNumber return &genesis, nil } @@ -555,6 +557,14 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { if conf.IsPrague(num) && conf.Bor == nil { head.RequestsHash = &types.EmptyRequestsHash } + // EIP-7843 SLOTNUM. Amsterdam is dormant on Bor (AmsterdamBlock nil), so + // genesis headers never carry a slotNumber. + if conf.IsAmsterdam(num) { + head.SlotNumber = g.SlotNumber + if head.SlotNumber == nil { + head.SlotNumber = new(uint64) + } + } } return types.NewBlock(head, &types.Body{Withdrawals: withdrawals}, nil, trie.NewStackTrie(nil)) } diff --git a/core/parallel_state_processor_fork_parity_test.go b/core/parallel_state_processor_fork_parity_test.go index 0e8f5895ab..bdc2bee25e 100644 --- a/core/parallel_state_processor_fork_parity_test.go +++ b/core/parallel_state_processor_fork_parity_test.go @@ -73,6 +73,7 @@ var forkExpectations = map[string]forkExpect{ "IsVerkleGenesis": {inV1: false, inV2: false}, "IsEIP4762": {inV1: false, inV2: false}, "IsOsaka": {inV1: false, inV2: false}, + "IsAmsterdam": {inV1: false, inV2: false}, // EIP-7843 SLOTNUM / EIP-8024 / BAL precompile-touch are EVM/consensus-gated, not state-processor-gated. // State-processor-level forks that BOTH paths must gate. "IsByzantium": {inV1: true, inV2: true}, // selects intermediate root vs receipt status diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 1a2c78dc31..d8de5a4f26 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -580,13 +580,7 @@ func WriteBodyRLP(db ethdb.KeyValueWriter, hash common.Hash, number uint64, rlp // HasBody verifies the existence of a block body corresponding to the hash. func HasBody(db ethdb.Reader, hash common.Hash, number uint64) bool { if isCanon(db, number, hash) { - // Block is in ancient store, but bodies can be pruned. - // Check if the block number is above the pruning tail. - tail, _ := db.Tail() - if number >= tail { - return true - } - return false + return true } if has, err := db.Has(blockBodyKey(number, hash)); !has || err != nil { @@ -684,13 +678,7 @@ func DeleteTd(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { // to a block. func HasReceipts(db ethdb.Reader, hash common.Hash, number uint64) bool { if isCanon(db, number, hash) { - // Block is in ancient store, but receipts can be pruned. - // Check if the block number is above the pruning tail. - tail, _ := db.Tail() - if number >= tail { - return true - } - return false + return true } if has, err := db.Has(blockReceiptsKey(number, hash)); !has || err != nil { diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 2b88809d5a..adbbf0238b 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -32,13 +32,13 @@ import ( "sync/atomic" "time" - "github.com/olekukonko/tablewriter" "golang.org/x/sync/errgroup" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/ethereum/go-ethereum/internal/tablewriter" "github.com/ethereum/go-ethereum/log" ) @@ -906,7 +906,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { total.Add(uint64(ancient.size())) } - table := NewTableWriter(os.Stdout) + table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Database", "Category", "Size", "Items"}) table.SetFooter([]string{"", "Total", common.StorageSize(total.Load()).String(), fmt.Sprintf("%d", count.Load())}) table.AppendBulk(stats) diff --git a/core/state/state_object.go b/core/state/state_object.go index 05010f2af9..8d0a68e976 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -204,6 +204,19 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // have been handles via pendingStorage above. // 2) we don't have new values, and can deliver empty response back if _, destructed := s.db.stateObjectsDestruct[s.address]; destructed { + // Invoke the reader regardless and discard the returned value. + // The returned value may not be empty, as it could belong to a + // self-destructed contract. + // + // The read operation is still essential for correctly building + // the block-level access list. + // + // TODO(rjl493456442) the reader interface can be extended with + // Touch, recording the read access without the actual disk load. + _, err := s.db.reader.Storage(s.address, key) + if err != nil { + s.db.setError(err) + } s.originStorage[key] = common.Hash{} // track the empty slot as origin value return common.Hash{} } diff --git a/core/state_processor_test.go b/core/state_processor_test.go index cddd1e60c6..7ee2bf4f01 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -426,6 +426,9 @@ func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Tr beaconRoot := common.HexToHash("0xbeac00") header.ParentBeaconRoot = &beaconRoot } + if config.IsAmsterdam(header.Number) { + header.SlotNumber = new(uint64) + } // Assemble and return the final block for sealing body := &types.Body{Transactions: txs} if config.IsShanghai(header.Number) { diff --git a/core/state_transition.go b/core/state_transition.go index 0893fef662..9af42dc466 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -192,9 +192,9 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In msg := &Message{ Nonce: tx.Nonce(), GasLimit: tx.Gas(), - GasPrice: new(big.Int).Set(tx.GasPrice()), - GasFeeCap: new(big.Int).Set(tx.GasFeeCap()), - GasTipCap: new(big.Int).Set(tx.GasTipCap()), + GasPrice: tx.GasPrice(), + GasFeeCap: tx.GasFeeCap(), + GasTipCap: tx.GasTipCap(), To: tx.To(), Value: tx.Value(), Data: tx.Data(), diff --git a/core/stateless/stats.go b/core/stateless/stats.go index 73ce031bff..7f4473a67c 100644 --- a/core/stateless/stats.go +++ b/core/stateless/stats.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/trie" ) var accountTrieLeavesAtDepth [16]*metrics.Counter @@ -41,59 +42,68 @@ func init() { // WitnessStats aggregates statistics for account and storage trie accesses. type WitnessStats struct { - accountTrieLeaves [16]int64 - storageTrieLeaves [16]int64 + accountTrie *trie.LevelStats + storageTrie *trie.LevelStats } // NewWitnessStats creates a new WitnessStats collector. func NewWitnessStats() *WitnessStats { - return &WitnessStats{} + return &WitnessStats{ + accountTrie: trie.NewLevelStats(), + storageTrie: trie.NewLevelStats(), + } +} + +func (s *WitnessStats) init() { + if s.accountTrie == nil { + s.accountTrie = trie.NewLevelStats() + } + if s.storageTrie == nil { + s.storageTrie = trie.NewLevelStats() + } } // Add records trie access depths from the given node paths. // If `owner` is the zero hash, accesses are attributed to the account trie; // otherwise, they are attributed to the storage trie of that account. func (s *WitnessStats) Add(nodes map[string][]byte, owner common.Hash) { - // Extract paths from the nodes map + s.init() + + // Extract paths from the nodes map. paths := slices.Collect(maps.Keys(nodes)) sort.Strings(paths) + ownerStat := s.accountTrie + if owner != (common.Hash{}) { + ownerStat = s.storageTrie + } + for i, path := range paths { // If current path is a prefix of the next path, it's not a leaf. // The last path is always a leaf. if i == len(paths)-1 || !strings.HasPrefix(paths[i+1], paths[i]) { - depth := len(path) - if owner == (common.Hash{}) { - if depth >= len(s.accountTrieLeaves) { - depth = len(s.accountTrieLeaves) - 1 - } - s.accountTrieLeaves[depth] += 1 - } else { - if depth >= len(s.storageTrieLeaves) { - depth = len(s.storageTrieLeaves) - 1 - } - s.storageTrieLeaves[depth] += 1 - } + ownerStat.AddLeaf(len(path)) } } } // ReportMetrics reports the collected statistics to the global metrics registry. func (s *WitnessStats) ReportMetrics(blockNumber uint64) { - // Encode the metrics as JSON for easier consumption - accountLeavesJson, _ := json.Marshal(s.accountTrieLeaves) - storageLeavesJson, _ := json.Marshal(s.storageTrieLeaves) - - // Log account trie depth statistics - log.Info("Account trie depth stats", - "block", blockNumber, - "leavesAtDepth", string(accountLeavesJson)) - log.Info("Storage trie depth stats", - "block", blockNumber, - "leavesAtDepth", string(storageLeavesJson)) + s.init() - for i := 0; i < 16; i++ { - accountTrieLeavesAtDepth[i].Inc(s.accountTrieLeaves[i]) - storageTrieLeavesAtDepth[i].Inc(s.storageTrieLeaves[i]) + accountTrieLeaves := s.accountTrie.LeafDepths() + storageTrieLeaves := s.storageTrie.LeafDepths() + + // Encode the metrics as JSON for easier consumption. + accountLeavesJSON, _ := json.Marshal(accountTrieLeaves) + storageLeavesJSON, _ := json.Marshal(storageTrieLeaves) + + // Log account trie depth statistics. + log.Info("Account trie depth stats", "block", blockNumber, "leavesAtDepth", string(accountLeavesJSON)) + log.Info("Storage trie depth stats", "block", blockNumber, "leavesAtDepth", string(storageLeavesJSON)) + + for i := 0; i < len(accountTrieLeavesAtDepth); i++ { + accountTrieLeavesAtDepth[i].Inc(accountTrieLeaves[i]) + storageTrieLeavesAtDepth[i].Inc(storageTrieLeaves[i]) } } diff --git a/core/stateless/stats_test.go b/core/stateless/stats_test.go index e77084df5d..6dc1fa0844 100644 --- a/core/stateless/stats_test.go +++ b/core/stateless/stats_test.go @@ -17,18 +17,27 @@ package stateless import ( + "strings" "testing" "github.com/ethereum/go-ethereum/common" ) +func expectedLeaves(counts map[int]int64) [16]int64 { + var leaves [16]int64 + for depth, count := range counts { + leaves[depth] = count + } + return leaves +} + func TestWitnessStatsAdd(t *testing.T) { tests := []struct { name string nodes map[string][]byte owner common.Hash - expectedAccountLeaves map[int64]int64 - expectedStorageLeaves map[int64]int64 + expectedAccountLeaves map[int]int64 + expectedStorageLeaves map[int]int64 }{ { name: "empty nodes", @@ -41,7 +50,7 @@ func TestWitnessStatsAdd(t *testing.T) { "": []byte("data"), }, owner: common.Hash{}, - expectedAccountLeaves: map[int64]int64{0: 1}, + expectedAccountLeaves: map[int]int64{0: 1}, }, { name: "single account trie leaf", @@ -49,7 +58,7 @@ func TestWitnessStatsAdd(t *testing.T) { "abc": []byte("data"), }, owner: common.Hash{}, - expectedAccountLeaves: map[int64]int64{3: 1}, + expectedAccountLeaves: map[int]int64{3: 1}, }, { name: "account trie with internal nodes", @@ -59,7 +68,7 @@ func TestWitnessStatsAdd(t *testing.T) { "abc": []byte("data3"), }, owner: common.Hash{}, - expectedAccountLeaves: map[int64]int64{3: 1}, // Only "abc" is a leaf + expectedAccountLeaves: map[int]int64{3: 1}, // Only "abc" is a leaf }, { name: "multiple account trie branches", @@ -72,7 +81,7 @@ func TestWitnessStatsAdd(t *testing.T) { "bcd": []byte("data6"), }, owner: common.Hash{}, - expectedAccountLeaves: map[int64]int64{3: 2}, // "abc" (3) + "bcd" (3) + expectedAccountLeaves: map[int]int64{3: 2}, // "abc" (3) + "bcd" (3) }, { name: "siblings are all leaves", @@ -82,7 +91,7 @@ func TestWitnessStatsAdd(t *testing.T) { "ac": []byte("data3"), }, owner: common.Hash{}, - expectedAccountLeaves: map[int64]int64{2: 3}, + expectedAccountLeaves: map[int]int64{2: 3}, }, { name: "storage trie leaves", @@ -93,7 +102,7 @@ func TestWitnessStatsAdd(t *testing.T) { "124": []byte("data4"), }, owner: common.HexToHash("0x1234"), - expectedStorageLeaves: map[int64]int64{3: 2}, // "123" (3) + "124" (3) + expectedStorageLeaves: map[int]int64{3: 2}, // "123" (3) + "124" (3) }, { name: "complex trie structure", @@ -109,7 +118,7 @@ func TestWitnessStatsAdd(t *testing.T) { "3": []byte("data9"), }, owner: common.Hash{}, - expectedAccountLeaves: map[int64]int64{1: 1, 3: 4}, // "123"(3) + "124"(3) + "234"(3) + "235"(3) + "3"(1) + expectedAccountLeaves: map[int]int64{1: 1, 3: 4}, // "123"(3) + "124"(3) + "234"(3) + "235"(3) + "3"(1) }, } @@ -118,32 +127,59 @@ func TestWitnessStatsAdd(t *testing.T) { stats := NewWitnessStats() stats.Add(tt.nodes, tt.owner) - var expectedAccountTrieLeaves [16]int64 - for depth, count := range tt.expectedAccountLeaves { - expectedAccountTrieLeaves[depth] = count + if got, want := stats.accountTrie.LeafDepths(), expectedLeaves(tt.expectedAccountLeaves); got != want { + t.Errorf("account trie leaves = %v, want %v", got, want) } - var expectedStorageTrieLeaves [16]int64 - for depth, count := range tt.expectedStorageLeaves { - expectedStorageTrieLeaves[depth] = count + if got, want := stats.storageTrie.LeafDepths(), expectedLeaves(tt.expectedStorageLeaves); got != want { + t.Errorf("storage trie leaves = %v, want %v", got, want) } + }) + } +} - // Check account trie depth - if stats.accountTrieLeaves != expectedAccountTrieLeaves { - t.Errorf("Account trie total depth = %v, want %v", stats.accountTrieLeaves, expectedAccountTrieLeaves) - } +func TestWitnessStatsStorageTrieAggregation(t *testing.T) { + stats := NewWitnessStats() + ownerA := common.HexToHash("0xa") + ownerB := common.HexToHash("0xb") - // Check storage trie depth - if stats.storageTrieLeaves != expectedStorageTrieLeaves { - t.Errorf("Storage trie total depth = %v, want %v", stats.storageTrieLeaves, expectedStorageTrieLeaves) - } - }) + stats.Add(map[string][]byte{ + "a": []byte("data1"), + "ab": []byte("data2"), + "abc": []byte("data3"), + }, ownerA) + stats.Add(map[string][]byte{ + "xy": []byte("data4"), + }, ownerA) + stats.Add(map[string][]byte{ + "1": []byte("data5"), + "12": []byte("data6"), + "123": []byte("data7"), + "124": []byte("data8"), + }, ownerB) + + if got, want := stats.storageTrie.LeafDepths(), expectedLeaves(map[int]int64{2: 1, 3: 3}); got != want { + t.Errorf("storage leaves = %v, want %v", got, want) } + if got, want := stats.accountTrie.LeafDepths(), expectedLeaves(nil); got != want { + t.Errorf("account leaves = %v, want %v", got, want) + } +} + +func TestWitnessStatsPanicsOnDeepLeaf(t *testing.T) { + stats := NewWitnessStats() + + defer func() { + if r := recover(); r == nil { + t.Fatal("expected panic for depth >= 16") + } + }() + stats.Add(map[string][]byte{strings.Repeat("a", 16): []byte("data")}, common.Hash{}) } func TestWitnessStatsMinMax(t *testing.T) { stats := NewWitnessStats() - // Add some account trie nodes with varying depths + // Add some account trie nodes with varying depths. stats.Add(map[string][]byte{ "a": []byte("data1"), "ab": []byte("data2"), @@ -152,21 +188,21 @@ func TestWitnessStatsMinMax(t *testing.T) { "abcde": []byte("data5"), }, common.Hash{}) - // Only "abcde" is a leaf (depth 5) - for i, v := range stats.accountTrieLeaves { + // Only "abcde" is a leaf (depth 5). + for i, v := range stats.accountTrie.LeafDepths() { if v != 0 && i != 5 { t.Errorf("leaf found at invalid depth %d", i) } } - // Add more leaves with different depths + // Add more leaves with different depths. stats.Add(map[string][]byte{ "x": []byte("data6"), "yz": []byte("data7"), }, common.Hash{}) - // Now we have leaves at depths 1, 2, and 5 - for i, v := range stats.accountTrieLeaves { + // Now we have leaves at depths 1, 2, and 5. + for i, v := range stats.accountTrie.LeafDepths() { if v != 0 && (i != 5 && i != 2 && i != 1) { t.Errorf("leaf found at invalid depth %d", i) } @@ -176,7 +212,7 @@ func TestWitnessStatsMinMax(t *testing.T) { func TestWitnessStatsAverage(t *testing.T) { stats := NewWitnessStats() - // Add nodes that will create leaves at depths 2, 3, and 4 + // Add nodes that will create leaves at depths 2, 3, and 4. stats.Add(map[string][]byte{ "aa": []byte("data1"), "bb": []byte("data2"), @@ -184,22 +220,22 @@ func TestWitnessStatsAverage(t *testing.T) { "dddd": []byte("data4"), }, common.Hash{}) - // All are leaves: 2 + 2 + 3 + 4 = 11 total, 4 samples + // All are leaves: 2 + 2 + 3 + 4 = 11 total, 4 samples. expectedAvg := int64(11) / int64(4) var actualAvg, totalSamples int64 - for i, c := range stats.accountTrieLeaves { + for i, c := range stats.accountTrie.LeafDepths() { actualAvg += c * int64(i) totalSamples += c } actualAvg = actualAvg / totalSamples if actualAvg != expectedAvg { - t.Errorf("Account trie average depth = %d, want %d", actualAvg, expectedAvg) + t.Errorf("account trie average depth = %d, want %d", actualAvg, expectedAvg) } } func BenchmarkWitnessStatsAdd(b *testing.B) { - // Create a realistic trie node structure + // Create a realistic trie node structure. nodes := make(map[string][]byte) for i := 0; i < 100; i++ { base := string(rune('a' + i%26)) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 158b592169..4f1e113be6 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -105,6 +105,16 @@ const ( // maxGappedTxs is the maximum number of gapped transactions kept overall. // This is a safety limit to avoid DoS vectors. maxGapped = 128 + + // notifyThreshold is the eviction priority threshold above which a transaction + // is considered close enough to being includable to be announced to peers. + // Setting this to zero will disable announcements for anyting not immediately + // includable. Setting it to -1 allows transactions that are close to being + // includable, maybe already in the next block if fees go down, to be announced. + + // Note, this threshold is in the abstract eviction priority space, so its + // meaning depends on the current basefee/blobfee and the transaction's fees. + announceThreshold = -1 ) // blobTxMeta is the minimal subset of types.BlobTx necessary to validate and @@ -116,6 +126,8 @@ type blobTxMeta struct { vhashes []common.Hash // Blob versioned hashes to maintain the lookup table version byte // Blob transaction version to determine proof type + announced bool // Whether the tx has been announced to listeners + id uint64 // Storage ID in the pool's persistent store storageSize uint32 // Byte size in the pool's persistent store size uint64 // RLP-encoded size of transaction including the attached blob @@ -160,7 +172,7 @@ func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transac blobGas: tx.BlobGas(), } meta.basefeeJumps = dynamicFeeJumps(meta.execFeeCap) - meta.blobfeeJumps = dynamicFeeJumps(meta.blobFeeCap) + meta.blobfeeJumps = dynamicBlobFeeJumps(meta.blobFeeCap) return meta } @@ -211,6 +223,14 @@ func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transac // via a normal transaction. It should nonetheless be high enough to support // resurrecting reorged transactions. Perhaps 4-16. // +// - It is not the role of the blobpool to serve as a storage for limit orders +// below market: blob transactions with fee caps way below base fee or blob fee. +// Therefore, the propagation of blob transactions that are far from being +// includable is suppressed. The pool will only announce blob transactions that +// are close to being includable (based on the current fees and the transaction's +// fee caps), and will delay the announcement of blob transactions that are far +// from being includable until base fee and/or blob fee is reduced. +// // - Local txs are meaningless. Mining pools historically used local transactions // for payouts or for backdoor deals. With 1559 in place, the basefee usually // dominates the final price, so 0 or non-0 tip doesn't change much. Blob txs @@ -282,47 +302,54 @@ func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transac // solve after every block. // // - The first observation is that comparing 1559 base fees or 4844 blob fees -// needs to happen in the context of their dynamism. Since these fees jump -// up or down in ~1.125 multipliers (at max) across blocks, comparing fees -// in two transactions should be based on log1.125(fee) to eliminate noise. +// needs to happen in the context of their dynamism. Since base fees are +// adjusted continuously and fluctuate, and we want to optimize for effective +// miner fees, it is better to disregard small base fee cap differences. +// Instead of considering the exact fee cap values, we should group +// transactions into buckets based on fee cap values, allowing us to use +// the miner tip meaningfully as a splitter inside a bucket. // -// - The second observation is that the basefee and blobfee move independently, -// so there's no way to split mixed txs on their own (A has higher base fee, -// B has higher blob fee). Rather than look at the absolute fees, the useful -// metric is the max time it can take to exceed the transaction's fee caps. +// To create these buckets, rather than looking at the absolute fee +// differences, the useful metric is the max time it can take to exceed the +// transaction's fee caps. Base fee changes are multiplicative, so we use a +// logarithmic scale. Fees jumps up or down in ~1.125 multipliers at max +// across blocks, so we use log1.125(fee) and rounding to eliminate noise. // Specifically, we're interested in the number of jumps needed to go from // the current fee to the transaction's cap: // -// jumps = log1.125(txfee) - log1.125(basefee) +// jumps = floor(log1.125(txfee) - log1.125(basefee)) // -// - The third observation is that the base fee tends to hover around rather -// than swing wildly. The number of jumps needed from the current fee starts -// to get less relevant the higher it is. To remove the noise here too, the -// pool will use log(jumps) as the delta for comparing transactions. +// For blob fees, EIP-7892 changed the ratio of target to max blobs, and +// with that also the maximum blob fee decrease in a slot from 1.125 to +// approx 1.17. therefore, we use: // -// delta = sign(jumps) * log(abs(jumps)) +// blobfeeJumps = floor(log1.17(txBlobfee) - log1.17(blobfee)) +// +// - The second observation is that when ranking executable blob txs, it +// does not make sense to grant a later eviction priority to txs with high +// fee caps since it could enable pool wars. As such, any positive priority +// will be grouped together. +// +// priority = min(jumps, 0) +// +// - The third observation is that the basefee and blobfee move independently, +// so there's no way to split mixed txs on their own (A has higher base fee, +// B has higher blob fee). // -// - To establish a total order, we need to reduce the dimensionality of the +// To establish a total order, we need to reduce the dimensionality of the // two base fees (log jumps) to a single value. The interesting aspect from // the pool's perspective is how fast will a tx get executable (fees going // down, crossing the smaller negative jump counter) or non-executable (fees // going up, crossing the smaller positive jump counter). As such, the pool // cares only about the min of the two delta values for eviction priority. // -// priority = min(deltaBasefee, deltaBlobfee) +// priority = min(deltaBasefee, deltaBlobfee, 0) // // - The above very aggressive dimensionality and noise reduction should result // in transaction being grouped into a small number of buckets, the further // the fees the larger the buckets. This is good because it allows us to use // the miner tip meaningfully as a splitter. // -// - For the scenario where the pool does not contain non-executable blob txs -// anymore, it does not make sense to grant a later eviction priority to txs -// with high fee caps since it could enable pool wars. As such, any positive -// priority will be grouped together. -// -// priority = min(deltaBasefee, deltaBlobfee, 0) -// // Optimisation tradeoffs: // // - Eviction relies on 3 fee minimums per account (exec tip, exec cap and blob @@ -471,6 +498,20 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser } p.evict = newPriceHeap(basefee, blobfee, p.index) + // Guess what was announced. This is needed because we don't want to + // participate in the diffusion of transactions where inclusion is blocked by + // a low base fee transaction. Since we don't persist that info, the best + // we can do is to assume that anything that could have been announced + // at current prices, actually was. + for addr := range p.index { + for _, tx := range p.index[addr] { + tx.announced = p.isAnnouncable(tx) + if !tx.announced { + break + } + } + } + // Pool initialized, attach the blob limbo to it to track blobs included // recently but not yet finalized p.limbo, err = newLimbo(p.chain.Config(), limbodir) @@ -518,6 +559,7 @@ func (p *BlobPool) Close() error { // parseTransaction is a callback method on pool creation that gets called for // each transaction on disk to create the in-memory metadata index. +// Announced state is not initialized here, it needs to be iniitalized seprately. func (p *BlobPool) parseTransaction(id uint64, size uint32, blob []byte) error { tx := new(types.Transaction) if err := rlp.DecodeBytes(blob, tx); err != nil { @@ -894,6 +936,37 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) { } p.evict.reinit(basefee, blobfee, false) + // Announce transactions that became announcable due to fee changes + var announcable []*types.Transaction + for addr, txs := range p.index { + for i, meta := range txs { + if !meta.announced && (i == 0 || txs[i-1].announced) && p.isAnnouncable(meta) { + // Load the full transaction and strip the sidecar before announcing + // TODO: this is a bit ugly, as we have everything needed in meta already + data, err := p.store.Get(meta.id) + // Technically, we are supposed to set announced only if Get is successful. + // However, Get failing here indicates a more serious issue (data loss), + // so we set announced anyway to avoid repeated attempts. + meta.announced = true + if err != nil { + log.Error("Blobs missing for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) + continue + } + var tx types.Transaction + if err = rlp.DecodeBytes(data, &tx); err != nil { + log.Error("Blobs corrupted for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) + continue + } + announcable = append(announcable, tx.WithoutBlobTxSidecar()) + log.Trace("Blob transaction now announcable", "from", addr, "nonce", meta.nonce, "id", meta.id, "hash", tx.Hash()) + } + } + } + if len(announcable) > 0 { + p.discoverFeed.Send(core.NewTxsEvent{Txs: announcable}) + } + + // Update the basefee and blobfee metrics basefeeGauge.Update(int64(basefee.Uint64())) blobfeeGauge.Update(int64(blobfee.Uint64())) p.updateStorageMetrics() @@ -1477,17 +1550,12 @@ func (p *BlobPool) AvailableBlobs(vhashes []common.Hash) int { // Add inserts a set of blob transactions into the pool if they pass validation (both // consensus validity and pool restrictions). func (p *BlobPool) Add(txs []*types.Transaction, sync bool) []error { - var ( - errs = make([]error, len(txs)) - adds = make([]*types.Transaction, 0, len(txs)) - ) + errs := make([]error, len(txs)) for i, tx := range txs { if errs[i] = p.ValidateTxBasics(tx); errs[i] != nil { continue } - if errs[i] = p.add(tx); errs[i] == nil { - adds = append(adds, tx.WithoutBlobTxSidecar()) - } + errs[i] = p.add(tx) } return errs } @@ -1679,9 +1747,15 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error addValidMeter.Mark(1) - // Notify all listeners of the new arrival - p.discoverFeed.Send(core.NewTxsEvent{Txs: []*types.Transaction{tx.WithoutBlobTxSidecar()}}) - p.insertFeed.Send(core.NewTxsEvent{Txs: []*types.Transaction{tx.WithoutBlobTxSidecar()}}) + // Transaction was addded successfully, but we only announce if it is (close to being) + // includable and the previous one was already announced. + if p.isAnnouncable(meta) && (meta.nonce == next || (len(txs) > 1 && txs[offset-1].announced)) { + meta.announced = true + p.discoverFeed.Send(core.NewTxsEvent{Txs: []*types.Transaction{tx.WithoutBlobTxSidecar()}}) + p.insertFeed.Send(core.NewTxsEvent{Txs: []*types.Transaction{tx.WithoutBlobTxSidecar()}}) + } else { + log.Trace("Blob transaction not announcable yet", "hash", tx.Hash(), "nonce", tx.Nonce()) + } //check the gapped queue for this account and try to promote if gtxs, ok := p.gapped[from]; checkGapped && ok && len(gtxs) > 0 { @@ -2014,6 +2088,12 @@ func (p *BlobPool) evictGapped() { } } +// isAnnouncable checks whether a transaction is announcable based on its +// fee parameters and announceThreshold. +func (p *BlobPool) isAnnouncable(meta *blobTxMeta) bool { + return evictionPriority(p.evict.basefeeJumps, meta.basefeeJumps, p.evict.blobfeeJumps, meta.blobfeeJumps) >= announceThreshold +} + // Stats retrieves the current pool stats, namely the number of pending and the // number of queued (non-executable) transactions. func (p *BlobPool) Stats() (int, int) { diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index f02d1fd193..6c2d6cf9f4 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -848,8 +848,8 @@ func TestOpenIndex(t *testing.T) { //blobfeeJumps = []float64{34.023, 35.570, 36.879, 29.686, 26.243, 20.358} // log 1.125 (blob fee cap) evictExecTipCaps = []uint64{10, 10, 5, 5, 1, 1} - evictExecFeeJumps = []float64{39.098, 38.204, 38.204, 19.549, 19.549, 19.549} // min(log 1.125 (exec fee cap)) - evictBlobFeeJumps = []float64{34.023, 34.023, 34.023, 29.686, 26.243, 20.358} // min(log 1.125 (blob fee cap)) + evictExecFeeJumps = []float64{39.098, 38.204, 38.204, 19.549, 19.549, 19.549} // min(log 1.125 (exec fee cap)) + evictBlobFeeJumps = []float64{25.517256, 25.517256, 25.517256, 22.264502, 19.682646, 15.268934} // min(log 1.17 (blob fee cap)) totalSpent = uint256.NewInt(21000*(100+90+200+10+80+300) + blobSize*(55+66+77+33+22+11) + 100*6) // 21000 gas x price + 128KB x blobprice + value ) @@ -1781,8 +1781,8 @@ func TestAdd(t *testing.T) { // Create a blob pool out of the pre-seeded dats chain := &testBlockChain{ config: testChainConfig, - basefee: uint256.NewInt(1050), - blobfee: uint256.NewInt(105), + basefee: uint256.NewInt(1), + blobfee: uint256.NewInt(1), statedb: statedb, } pool := New(Config{Datadir: storage}, chain, nil) diff --git a/core/txpool/blobpool/evictheap.go b/core/txpool/blobpool/evictheap.go index bd7857eb92..265b5b2047 100644 --- a/core/txpool/blobpool/evictheap.go +++ b/core/txpool/blobpool/evictheap.go @@ -68,7 +68,7 @@ func newPriceHeap(basefee *uint256.Int, blobfee *uint256.Int, index map[common.A func (h *evictHeap) reinit(basefee *uint256.Int, blobfee *uint256.Int, force bool) { // If the update is mostly the same as the old, don't sort pointlessly basefeeJumps := dynamicFeeJumps(basefee) - blobfeeJumps := dynamicFeeJumps(blobfee) + blobfeeJumps := dynamicBlobFeeJumps(blobfee) if !force && math.Abs(h.basefeeJumps-basefeeJumps) < 0.01 && math.Abs(h.blobfeeJumps-blobfeeJumps) < 0.01 { // TODO(karalabe): 0.01 enough, maybe should be smaller? Maybe this optimization is moot? return @@ -96,13 +96,7 @@ func (h *evictHeap) Less(i, j int) bool { lastJ := txsJ[len(txsJ)-1] prioI := evictionPriority(h.basefeeJumps, lastI.evictionExecFeeJumps, h.blobfeeJumps, lastI.evictionBlobFeeJumps) - if prioI > 0 { - prioI = 0 - } prioJ := evictionPriority(h.basefeeJumps, lastJ.evictionExecFeeJumps, h.blobfeeJumps, lastJ.evictionBlobFeeJumps) - if prioJ > 0 { - prioJ = 0 - } if prioI == prioJ { return lastI.evictionExecTip.Lt(lastJ.evictionExecTip) } diff --git a/core/txpool/blobpool/evictheap_test.go b/core/txpool/blobpool/evictheap_test.go index 598927901c..5997087684 100644 --- a/core/txpool/blobpool/evictheap_test.go +++ b/core/txpool/blobpool/evictheap_test.go @@ -110,22 +110,22 @@ func TestPriceHeapSorting(t *testing.T) { order: []int{3, 2, 1, 0, 4, 5, 6}, }, // If both basefee and blobfee is specified, sort by the larger distance - // of the two from the current network conditions, splitting same (loglog) + // of the two from the current network conditions, splitting same // ones via the tip. // - // Basefee: 1000 - // Blobfee: 100 + // Basefee: 1000 , jumps: 888, 790, 702, 624 + // Blobfee: 100 , jumps: 85, 73, 62, 53 // - // Tx #0: (800, 80) - 2 jumps below both => priority -1 - // Tx #1: (630, 63) - 4 jumps below both => priority -2 - // Tx #2: (800, 63) - 2 jumps below basefee, 4 jumps below blobfee => priority -2 (blob penalty dominates) - // Tx #3: (630, 80) - 4 jumps below basefee, 2 jumps below blobfee => priority -2 (base penalty dominates) + // Tx #0: (800, 80) - 2 jumps below both => priority -2 + // Tx #1: (630, 55) - 4 jumps below both => priority -4 + // Tx #2: (800, 55) - 2 jumps below basefee, 4 jumps below blobfee => priority -4 (blob penalty dominates) + // Tx #3: (630, 80) - 4 jumps below basefee, 2 jumps below blobfee => priority -4 (base penalty dominates) // // Txs 1, 2, 3 share the same priority, split via tip, prefer 0 as the best { execTips: []uint64{1, 2, 3, 4}, execFees: []uint64{800, 630, 800, 630}, - blobFees: []uint64{80, 63, 63, 80}, + blobFees: []uint64{80, 55, 55, 80}, basefee: 1000, blobfee: 100, order: []int{1, 2, 3, 0}, @@ -143,7 +143,7 @@ func TestPriceHeapSorting(t *testing.T) { blobFee = uint256.NewInt(tt.blobFees[j]) basefeeJumps = dynamicFeeJumps(execFee) - blobfeeJumps = dynamicFeeJumps(blobFee) + blobfeeJumps = dynamicBlobFeeJumps(blobFee) ) index[addr] = []*blobTxMeta{{ id: uint64(j), @@ -203,7 +203,7 @@ func benchmarkPriceHeapReinit(b *testing.B, datacap uint64) { blobFee = uint256.NewInt(rnd.Uint64()) basefeeJumps = dynamicFeeJumps(execFee) - blobfeeJumps = dynamicFeeJumps(blobFee) + blobfeeJumps = dynamicBlobFeeJumps(blobFee) ) index[addr] = []*blobTxMeta{{ id: uint64(i), @@ -280,7 +280,7 @@ func benchmarkPriceHeapOverflow(b *testing.B, datacap uint64) { blobFee = uint256.NewInt(rnd.Uint64()) basefeeJumps = dynamicFeeJumps(execFee) - blobfeeJumps = dynamicFeeJumps(blobFee) + blobfeeJumps = dynamicBlobFeeJumps(blobFee) ) index[addr] = []*blobTxMeta{{ id: uint64(i), @@ -311,7 +311,7 @@ func benchmarkPriceHeapOverflow(b *testing.B, datacap uint64) { blobFee = uint256.NewInt(rnd.Uint64()) basefeeJumps = dynamicFeeJumps(execFee) - blobfeeJumps = dynamicFeeJumps(blobFee) + blobfeeJumps = dynamicBlobFeeJumps(blobFee) ) metas[i] = &blobTxMeta{ id: uint64(int(blobs) + i), diff --git a/core/txpool/blobpool/priority.go b/core/txpool/blobpool/priority.go index 7ae7f92def..d7e8789ce7 100644 --- a/core/txpool/blobpool/priority.go +++ b/core/txpool/blobpool/priority.go @@ -18,7 +18,6 @@ package blobpool import ( "math" - "math/bits" "github.com/holiman/uint256" ) @@ -26,6 +25,13 @@ import ( // log1_125 is used in the eviction priority calculation. var log1_125 = math.Log(1.125) +// log1_17 is used in the eviction priority calculation for blob fees. +// EIP-7892 (BPO) changed the ratio of target to max blobs, and with that +// also the maximum blob fee decrease in a slot from 1.125 to approx 1.17 . +// Since we want priorities to approximate time, we should change our log +// calculation for blob fees. +var log1_17 = log1_125 * 4 / 3 + // evictionPriority calculates the eviction priority based on the algorithm // described in the BlobPool docs for both fee components. // @@ -36,23 +42,20 @@ func evictionPriority(basefeeJumps float64, txBasefeeJumps, blobfeeJumps, txBlob basefeePriority = evictionPriority1D(basefeeJumps, txBasefeeJumps) blobfeePriority = evictionPriority1D(blobfeeJumps, txBlobfeeJumps) ) - if basefeePriority < blobfeePriority { - return basefeePriority - } - return blobfeePriority + return min(0, basefeePriority, blobfeePriority) } // evictionPriority1D calculates the eviction priority based on the algorithm // described in the BlobPool docs for a single fee component. func evictionPriority1D(basefeeJumps float64, txfeeJumps float64) int { jumps := txfeeJumps - basefeeJumps - if int(jumps) == 0 { - return 0 // can't log2 0 + if jumps <= 0 { + return int(math.Floor(jumps)) } - if jumps < 0 { - return -intLog2(uint(-math.Floor(jumps))) - } - return intLog2(uint(math.Ceil(jumps))) + // We only use the negative part for ordering. The positive part is only used + // for threshold comparison (with a negative threshold), so the value is almost + // irrelevant, as long as it's positive. + return int((math.Ceil(jumps))) } // dynamicFeeJumps calculates the log1.125(fee), namely the number of fee jumps @@ -70,21 +73,9 @@ func dynamicFeeJumps(fee *uint256.Int) float64 { return math.Log(fee.Float64()) / log1_125 } -// intLog2 is a helper to calculate the integral part of a log2 of an unsigned -// integer. It is a very specific calculation that's not particularly useful in -// general, but it's what we need here (it's fast). -func intLog2(n uint) int { - switch { - case n == 0: - panic("log2(0) is undefined") - - case n < 2048: - return bits.UintSize - bits.LeadingZeros(n) - 1 - - default: - // The input is log1.125(uint256) = log2(uint256) / log2(1.125). At the - // most extreme, log2(uint256) will be a bit below 257, and the constant - // log2(1.125) ~= 0.17. The larges input thus is ~257 / ~0.17 ~= ~1511. - panic("dynamic fee jump diffs cannot reach this") +func dynamicBlobFeeJumps(fee *uint256.Int) float64 { + if fee.IsZero() { + return 0 // can't log2 zero, should never happen outside tests, but don't choke } + return math.Log(fee.Float64()) / log1_17 } diff --git a/core/txpool/blobpool/priority_test.go b/core/txpool/blobpool/priority_test.go index 1eaee6d7df..47b3a5375f 100644 --- a/core/txpool/blobpool/priority_test.go +++ b/core/txpool/blobpool/priority_test.go @@ -30,12 +30,12 @@ func TestPriorityCalculation(t *testing.T) { txfee uint64 result int }{ - {basefee: 7, txfee: 10, result: 2}, // 3.02 jumps, 4 ceil, 2 log2 - {basefee: 17_200_000_000, txfee: 17_200_000_000, result: 0}, // 0 jumps, special case 0 log2 - {basefee: 9_853_941_692, txfee: 11_085_092_510, result: 0}, // 0.99 jumps, 1 ceil, 0 log2 - {basefee: 11_544_106_391, txfee: 10_356_781_100, result: 0}, // -0.92 jumps, -1 floor, 0 log2 - {basefee: 17_200_000_000, txfee: 7, result: -7}, // -183.57 jumps, -184 floor, -7 log2 - {basefee: 7, txfee: 17_200_000_000, result: 7}, // 183.57 jumps, 184 ceil, 7 log2 + {basefee: 7, txfee: 10, result: 4}, // 3.02 jumps, 4 ceil + {basefee: 17_200_000_000, txfee: 17_200_000_000, result: 0}, // 0 jumps, special case 0 + {basefee: 9_853_941_692, txfee: 11_085_092_510, result: 1}, // 0.99 jumps, 1 ceil + {basefee: 11_544_106_391, txfee: 10_356_781_100, result: -1}, // -0.92 jumps, -1 floor + {basefee: 17_200_000_000, txfee: 7, result: -184}, // -183.57 jumps, -184 floor + {basefee: 7, txfee: 17_200_000_000, result: 184}, // 183.57 jumps, 184 ceil } for i, tt := range tests { var ( @@ -69,7 +69,7 @@ func BenchmarkPriorityCalculation(b *testing.B) { blobfee := uint256.NewInt(123_456_789_000) // Completely random, no idea what this will be basefeeJumps := dynamicFeeJumps(basefee) - blobfeeJumps := dynamicFeeJumps(blobfee) + blobfeeJumps := dynamicBlobFeeJumps(blobfee) // The transaction's fee cap and blob fee cap are constant across the life // of the transaction, so we can pre-calculate and cache them. @@ -77,7 +77,7 @@ func BenchmarkPriorityCalculation(b *testing.B) { txBlobfeeJumps := make([]float64, b.N) for i := 0; i < b.N; i++ { txBasefeeJumps[i] = dynamicFeeJumps(uint256.NewInt(rnd.Uint64())) - txBlobfeeJumps[i] = dynamicFeeJumps(uint256.NewInt(rnd.Uint64())) + txBlobfeeJumps[i] = dynamicBlobFeeJumps(uint256.NewInt(rnd.Uint64())) } b.ResetTimer() b.ReportAllocs() diff --git a/core/txpool/legacypool/list_test.go b/core/txpool/legacypool/list_test.go index ce325faaeb..aca6ee91a6 100644 --- a/core/txpool/legacypool/list_test.go +++ b/core/txpool/legacypool/list_test.go @@ -78,6 +78,43 @@ func TestListAddVeryExpensive(t *testing.T) { } } +// TestPriceHeapCmp tests that the price heap comparison function works as intended. +// It also tests combinations where the basefee is higher than the gas fee cap, which +// are useful to sort in the mempool to support basefee changes. +func TestPriceHeapCmp(t *testing.T) { + key, _ := crypto.GenerateKey() + txs := []*types.Transaction{ + // nonce, gaslimit, gasfee, gastip + dynamicFeeTx(0, 1000, big.NewInt(2), big.NewInt(1), key), + dynamicFeeTx(0, 1000, big.NewInt(1), big.NewInt(2), key), + dynamicFeeTx(0, 1000, big.NewInt(1), big.NewInt(1), key), + dynamicFeeTx(0, 1000, big.NewInt(1), big.NewInt(0), key), + } + + // create priceHeap + ph := &priceHeap{} + + // now set the basefee on the heap + for _, basefee := range []uint64{0, 1, 2, 3} { + ph.baseFee = uint256.NewInt(basefee) + + for i := 0; i < len(txs); i++ { + for j := 0; j < len(txs); j++ { + switch { + case i == j: + if c := ph.cmp(txs[i], txs[j]); c != 0 { + t.Errorf("tx %d should be equal priority to tx %d with basefee %d (cmp=%d)", i, j, basefee, c) + } + case i < j: + if c := ph.cmp(txs[i], txs[j]); c != 1 { + t.Errorf("tx %d vs tx %d comparison inconsistent with basefee %d (cmp=%d)", i, j, basefee, c) + } + } + } + } + } +} + func BenchmarkListAdd(b *testing.B) { // Generate a list of transactions to insert key, _ := crypto.GenerateKey() diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go index 59753544b5..491199b1cc 100644 --- a/core/types/bal/bal.go +++ b/core/types/bal/bal.go @@ -25,13 +25,6 @@ import ( "github.com/ethereum/go-ethereum/common" ) -// CodeChange contains the runtime bytecode deployed at an address and the -// transaction index where the deployment took place. -type CodeChange struct { - TxIndex uint16 - Code []byte `json:"code,omitempty"` -} - // ConstructionAccountAccess contains post-block account state for mutations as well as // all storage keys that were read during execution. It is used when building block // access list during execution. @@ -56,9 +49,9 @@ type ConstructionAccountAccess struct { // by tx index. NonceChanges map[uint16]uint64 `json:"nonceChanges,omitempty"` - // CodeChange is only set for contract accounts which were deployed in - // the block. - CodeChange *CodeChange `json:"codeChange,omitempty"` + // CodeChange contains the post-state contract code of an account keyed + // by tx index. + CodeChange map[uint16][]byte `json:"codeChange,omitempty"` } // NewConstructionAccountAccess initializes the account access object. @@ -68,6 +61,7 @@ func NewConstructionAccountAccess() *ConstructionAccountAccess { StorageReads: make(map[common.Hash]struct{}), BalanceChanges: make(map[uint16]*uint256.Int), NonceChanges: make(map[uint16]uint64), + CodeChange: make(map[uint16][]byte), } } @@ -121,10 +115,8 @@ func (b *ConstructionBlockAccessList) CodeChange(address common.Address, txIndex if _, ok := b.Accounts[address]; !ok { b.Accounts[address] = NewConstructionAccountAccess() } - b.Accounts[address].CodeChange = &CodeChange{ - TxIndex: txIndex, - Code: bytes.Clone(code), - } + // TODO(rjl493456442) is it essential to deep-copy the code? + b.Accounts[address].CodeChange[txIndex] = bytes.Clone(code) } // NonceChange records tx post-state nonce of any contract-like accounts whose @@ -171,12 +163,11 @@ func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { aaCopy.BalanceChanges = balances aaCopy.NonceChanges = maps.Clone(aa.NonceChanges) - if aa.CodeChange != nil { - aaCopy.CodeChange = &CodeChange{ - TxIndex: aa.CodeChange.TxIndex, - Code: bytes.Clone(aa.CodeChange.Code), - } + codes := make(map[uint16][]byte, len(aa.CodeChange)) + for index, code := range aa.CodeChange { + codes[index] = bytes.Clone(code) } + aaCopy.CodeChange = codes res.Accounts[addr] = &aaCopy } return &res diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 258c6b61cc..f4b5d9faa9 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -120,6 +120,13 @@ func (e *encodingSlotWrites) validate() error { return errors.New("storage write tx indices not in order") } +// encodingCodeChange contains the runtime bytecode deployed at an address +// and the transaction index where the deployment took place. +type encodingCodeChange struct { + TxIndex uint16 `ssz-size:"2"` + Code []byte `ssz-max:"300000"` // TODO(rjl493456442) shall we put the limit here? The limit will be increased gradually +} + // AccountAccess is the encoding format of ConstructionAccountAccess. type AccountAccess struct { Address [20]byte `ssz-size:"20"` // 20-byte Ethereum address @@ -127,7 +134,7 @@ type AccountAccess struct { StorageReads [][32]byte `ssz-max:"300000"` // Read-only storage keys BalanceChanges []encodingBalanceChange `ssz-max:"300000"` // Balance changes ([tx_index -> post_balance]) NonceChanges []encodingAccountNonce `ssz-max:"300000"` // Nonce changes ([tx_index -> new_nonce]) - Code []CodeChange `ssz-max:"1"` // Code changes ([tx_index -> new_code]) + CodeChanges []encodingCodeChange `ssz-max:"300000"` // Code changes ([tx_index -> new_code]) } // validate converts the account accesses out of encoding format. @@ -167,9 +174,16 @@ func (e *AccountAccess) validate() error { return errors.New("nonce changes not in ascending order by tx index") } - // Convert code change - if len(e.Code) == 1 { - if len(e.Code[0].Code) > params.MaxCodeSize { + // Check the code changes are sorted in order + if !slices.IsSortedFunc(e.CodeChanges, func(a, b encodingCodeChange) int { + return cmp.Compare[uint16](a.TxIndex, b.TxIndex) + }) { + return errors.New("code changes not in ascending order by tx index") + } + for _, change := range e.CodeChanges { + // TODO(rjl493456442): This check should be fork-aware, since the limit may + // differ across forks. + if len(change.Code) > params.MaxCodeSize { return errors.New("code change contained oversized code") } } @@ -183,6 +197,8 @@ func (e *AccountAccess) Copy() AccountAccess { StorageReads: slices.Clone(e.StorageReads), BalanceChanges: slices.Clone(e.BalanceChanges), NonceChanges: slices.Clone(e.NonceChanges), + StorageWrites: make([]encodingSlotWrites, 0, len(e.StorageWrites)), + CodeChanges: make([]encodingCodeChange, 0, len(e.CodeChanges)), } for _, storageWrite := range e.StorageWrites { res.StorageWrites = append(res.StorageWrites, encodingSlotWrites{ @@ -190,13 +206,11 @@ func (e *AccountAccess) Copy() AccountAccess { Accesses: slices.Clone(storageWrite.Accesses), }) } - if len(e.Code) == 1 { - res.Code = []CodeChange{ - { - e.Code[0].TxIndex, - bytes.Clone(e.Code[0].Code), - }, - } + for _, codeChange := range e.CodeChanges { + res.CodeChanges = append(res.CodeChanges, encodingCodeChange{ + TxIndex: codeChange.TxIndex, + Code: bytes.Clone(codeChange.Code), + }) } return res } @@ -213,11 +227,11 @@ var _ rlp.Encoder = &ConstructionBlockAccessList{} func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAccess { res := AccountAccess{ Address: addr, - StorageWrites: make([]encodingSlotWrites, 0), - StorageReads: make([][32]byte, 0), - BalanceChanges: make([]encodingBalanceChange, 0), - NonceChanges: make([]encodingAccountNonce, 0), - Code: nil, + StorageWrites: make([]encodingSlotWrites, 0, len(a.StorageWrites)), + StorageReads: make([][32]byte, 0, len(a.StorageReads)), + BalanceChanges: make([]encodingBalanceChange, 0, len(a.BalanceChanges)), + NonceChanges: make([]encodingAccountNonce, 0, len(a.NonceChanges)), + CodeChanges: make([]encodingCodeChange, 0, len(a.CodeChange)), } // Convert write slots @@ -269,13 +283,13 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc } // Convert code change - if a.CodeChange != nil { - res.Code = []CodeChange{ - { - a.CodeChange.TxIndex, - bytes.Clone(a.CodeChange.Code), - }, - } + codeIndices := slices.Collect(maps.Keys(a.CodeChange)) + slices.SortFunc(codeIndices, cmp.Compare[uint16]) + for _, idx := range codeIndices { + res.CodeChanges = append(res.CodeChanges, encodingCodeChange{ + TxIndex: idx, + Code: a.CodeChange[idx], + }) } return res } @@ -328,9 +342,9 @@ func (e *BlockAccessList) PrettyPrint() string { printWithIndent(2, fmt.Sprintf("%d: %d", change.TxIdx, change.Nonce)) } - if len(accountDiff.Code) > 0 { - printWithIndent(1, "code:") - printWithIndent(2, fmt.Sprintf("%d: %x", accountDiff.Code[0].TxIndex, accountDiff.Code[0].Code)) + printWithIndent(1, "code changes:") + for _, change := range accountDiff.CodeChanges { + printWithIndent(2, fmt.Sprintf("%d: %x", change.TxIndex, change.Code)) } } return res.String() diff --git a/core/types/bal/bal_encoding_rlp_generated.go b/core/types/bal/bal_encoding_rlp_generated.go index 037d04f46b..05d1faafd7 100644 --- a/core/types/bal/bal_encoding_rlp_generated.go +++ b/core/types/bal/bal_encoding_rlp_generated.go @@ -52,7 +52,7 @@ func (obj *BlockAccessList) EncodeRLP(_w io.Writer) error { } w.ListEnd(_tmp15) _tmp18 := w.List() - for _, _tmp19 := range _tmp2.Code { + for _, _tmp19 := range _tmp2.CodeChanges { _tmp20 := w.List() w.WriteUint64(uint64(_tmp19.TxIndex)) w.WriteBytes(_tmp19.Code) @@ -231,13 +231,13 @@ func (obj *BlockAccessList) DecodeRLP(dec *rlp.Stream) error { return err } _tmp2.NonceChanges = _tmp17 - // Code: - var _tmp21 []CodeChange + // CodeChanges: + var _tmp21 []encodingCodeChange if _, err := dec.List(); err != nil { return err } for dec.MoreDataInList() { - var _tmp22 CodeChange + var _tmp22 encodingCodeChange { if _, err := dec.List(); err != nil { return err @@ -263,7 +263,7 @@ func (obj *BlockAccessList) DecodeRLP(dec *rlp.Stream) error { if err := dec.ListEnd(); err != nil { return err } - _tmp2.Code = _tmp21 + _tmp2.CodeChanges = _tmp21 if err := dec.ListEnd(); err != nil { return err } diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index 0aa1b9a3ab..a20390dcc6 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -61,9 +61,8 @@ func makeTestConstructionBAL() *ConstructionBlockAccessList { 1: 2, 2: 6, }, - CodeChange: &CodeChange{ - TxIndex: 0, - Code: common.Hex2Bytes("deadbeef"), + CodeChange: map[uint16][]byte{ + 0: common.Hex2Bytes("deadbeef"), }, }, common.BytesToAddress([]byte{0xff, 0xff, 0xff}): { @@ -86,6 +85,9 @@ func makeTestConstructionBAL() *ConstructionBlockAccessList { NonceChanges: map[uint16]uint64{ 1: 2, }, + CodeChange: map[uint16][]byte{ + 0: common.Hex2Bytes("deadbeef"), + }, }, }, } @@ -180,7 +182,7 @@ func makeTestAccountAccess(sort bool) AccountAccess { StorageReads: storageReads, BalanceChanges: balances, NonceChanges: nonces, - Code: []CodeChange{ + CodeChanges: []encodingCodeChange{ { TxIndex: 100, Code: testrand.Bytes(256), diff --git a/core/types/block.go b/core/types/block.go index 277903c9d2..90a0606aa0 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -110,6 +110,9 @@ type Header struct { // RequestsHash was added by EIP-7685 and is ignored in legacy headers. RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + + // SlotNumber was added by EIP-7843 and is ignored in legacy headers. + SlotNumber *uint64 `json:"slotNumber" rlp:"optional"` } // Used for Encoding and Decoding of the Extra Data Field @@ -151,6 +154,7 @@ type headerMarshaling struct { Hash common.Hash `json:"hash"` // adds call to Hash() in MarshalJSON BlobGasUsed *hexutil.Uint64 ExcessBlobGas *hexutil.Uint64 + SlotNumber *hexutil.Uint64 } // Hash returns the block hash of the header, which is simply the keccak256 hash of its @@ -407,6 +411,10 @@ func CopyHeader(h *Header) *Header { cpy.RequestsHash = new(common.Hash) *cpy.RequestsHash = *h.RequestsHash } + if h.SlotNumber != nil { + cpy.SlotNumber = new(uint64) + *cpy.SlotNumber = *h.SlotNumber + } return &cpy } @@ -632,6 +640,15 @@ func (b *Block) BlobGasUsed() *uint64 { return nil } +func (b *Block) SlotNumber() *uint64 { + var slotNum *uint64 + if b.header.SlotNumber != nil { + slotNum = new(uint64) + *slotNum = *b.header.SlotNumber + } + return slotNum +} + // Size returns the true RLP encoded storage size of the block, either by encoding // and returning it, or returning a previously cached value. func (b *Block) Size() uint64 { diff --git a/core/types/gen_header_json.go b/core/types/gen_header_json.go index 0af12500bd..16fb03f612 100644 --- a/core/types/gen_header_json.go +++ b/core/types/gen_header_json.go @@ -37,6 +37,7 @@ func (h Header) MarshalJSON() ([]byte, error) { ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"` Hash common.Hash `json:"hash"` } var enc Header @@ -61,6 +62,7 @@ func (h Header) MarshalJSON() ([]byte, error) { enc.ExcessBlobGas = (*hexutil.Uint64)(h.ExcessBlobGas) enc.ParentBeaconRoot = h.ParentBeaconRoot enc.RequestsHash = h.RequestsHash + enc.SlotNumber = (*hexutil.Uint64)(h.SlotNumber) enc.Hash = h.Hash() return json.Marshal(&enc) } @@ -89,6 +91,7 @@ func (h *Header) UnmarshalJSON(input []byte) error { ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"` } var dec Header if err := json.Unmarshal(input, &dec); err != nil { @@ -169,5 +172,8 @@ func (h *Header) UnmarshalJSON(input []byte) error { if dec.RequestsHash != nil { h.RequestsHash = dec.RequestsHash } + if dec.SlotNumber != nil { + h.SlotNumber = (*uint64)(dec.SlotNumber) + } return nil } diff --git a/core/types/gen_header_rlp.go b/core/types/gen_header_rlp.go index 6dc3481393..943815546f 100644 --- a/core/types/gen_header_rlp.go +++ b/core/types/gen_header_rlp.go @@ -46,7 +46,8 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { _tmp4 := obj.ExcessBlobGas != nil _tmp5 := obj.ParentBeaconRoot != nil _tmp6 := obj.RequestsHash != nil - if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 { + _tmp7 := obj.SlotNumber != nil + if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.BaseFee == nil { w.Write(rlp.EmptyString) } else { @@ -56,41 +57,48 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { w.WriteBigInt(obj.BaseFee) } } - if _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 { + if _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.WithdrawalsHash == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.WithdrawalsHash[:]) } } - if _tmp3 || _tmp4 || _tmp5 || _tmp6 { + if _tmp3 || _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.BlobGasUsed == nil { w.Write([]byte{0x80}) } else { w.WriteUint64((*obj.BlobGasUsed)) } } - if _tmp4 || _tmp5 || _tmp6 { + if _tmp4 || _tmp5 || _tmp6 || _tmp7 { if obj.ExcessBlobGas == nil { w.Write([]byte{0x80}) } else { w.WriteUint64((*obj.ExcessBlobGas)) } } - if _tmp5 || _tmp6 { + if _tmp5 || _tmp6 || _tmp7 { if obj.ParentBeaconRoot == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.ParentBeaconRoot[:]) } } - if _tmp6 { + if _tmp6 || _tmp7 { if obj.RequestsHash == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.RequestsHash[:]) } } + if _tmp7 { + if obj.SlotNumber == nil { + w.Write([]byte{0x80}) + } else { + w.WriteUint64((*obj.SlotNumber)) + } + } w.ListEnd(_tmp0) return w.Flush() } diff --git a/core/types/transaction.go b/core/types/transaction.go index 3c198205b5..182476d735 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -418,14 +418,37 @@ func (tx *Transaction) calcEffectiveGasTip(dst *uint256.Int, baseFee *uint256.In return err } +// EffectiveGasTipValue returns the effective gasTip value for the given base fee, +// even if it would be negative. This can be used for sorting purposes. +func (tx *Transaction) EffectiveGasTipValue(baseFee *big.Int) *big.Int { + // min(gasTipCap, gasFeeCap - baseFee) + dst := new(big.Int) + if baseFee == nil { + dst.Set(tx.inner.gasTipCap()) + return dst + } + + dst.Sub(tx.inner.gasFeeCap(), baseFee) // gasFeeCap - baseFee + gasTipCap := tx.inner.gasTipCap() + if gasTipCap.Cmp(dst) < 0 { // gasTipCap < (gasFeeCap - baseFee) + dst.Set(gasTipCap) + } + return dst +} + func (tx *Transaction) EffectiveGasTipCmp(other *Transaction, baseFee *uint256.Int) int { if baseFee == nil { return tx.GasTipCapCmp(other) } // Use more efficient internal method. txTip, otherTip := new(uint256.Int), new(uint256.Int) - tx.calcEffectiveGasTip(txTip, baseFee) - other.calcEffectiveGasTip(otherTip, baseFee) + err1 := tx.calcEffectiveGasTip(txTip, baseFee) + err2 := other.calcEffectiveGasTip(otherTip, baseFee) + if err1 != nil || err2 != nil { + // fall back to big int comparison in case of error + base := baseFee.ToBig() + return tx.EffectiveGasTipValue(base).Cmp(other.EffectiveGasTipValue(base)) + } return txTip.Cmp(otherTip) } diff --git a/core/vm/contracts.go b/core/vm/contracts.go index f75c73adaf..4fc9172949 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -419,7 +419,7 @@ func ActivePrecompiles(rules params.Rules) []common.Address { // - the returned bytes, // - the _remaining_ gas, // - any error that occurred -func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64, logger *tracing.Hooks) (ret []byte, remainingGas uint64, err error) { +func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address common.Address, input []byte, suppliedGas uint64, logger *tracing.Hooks) (ret []byte, remainingGas uint64, err error) { gasCost := p.RequiredGas(input) if suppliedGas < gasCost { return nil, 0, ErrOutOfGas @@ -428,6 +428,12 @@ func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uin logger.OnGasChange(suppliedGas, suppliedGas-gasCost, tracing.GasChangeCallPrecompiledContract) } suppliedGas -= gasCost + + // Touch the precompile for block-level accessList recording once Amsterdam + // fork is activated. + if stateDB != nil { + stateDB.Exist(address) + } output, err := p.Run(input) return output, suppliedGas, err diff --git a/core/vm/contracts_fuzz_test.go b/core/vm/contracts_fuzz_test.go index 1e5cc80074..34ed1d87fe 100644 --- a/core/vm/contracts_fuzz_test.go +++ b/core/vm/contracts_fuzz_test.go @@ -36,7 +36,7 @@ func FuzzPrecompiledContracts(f *testing.F) { return } inWant := string(input) - RunPrecompiledContract(p, input, gas, nil) + RunPrecompiledContract(nil, p, a, input, gas, nil) if inHave := string(input); inWant != inHave { t.Errorf("Precompiled %v modified input data", a) } diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index edfd7e2ae3..7a3f8d83eb 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -110,7 +110,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) { in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - if res, _, err := RunPrecompiledContract(p, in, gas, nil); err != nil { + if res, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, gas, nil); err != nil { t.Error(err) } else if common.Bytes2Hex(res) != test.Expected { t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res)) @@ -133,7 +133,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) { gas := test.Gas - 1 t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - _, _, err := RunPrecompiledContract(p, in, gas, nil) + _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, gas, nil) if err.Error() != "out of gas" { t.Errorf("Expected error [out of gas], got [%v]", err) } @@ -151,7 +151,7 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing gas := p.RequiredGas(in) t.Run(test.Name, func(t *testing.T) { - _, _, err := RunPrecompiledContract(p, in, gas, nil) + _, _, err := RunPrecompiledContract(nil, p, common.HexToAddress(addr), in, gas, nil) if err.Error() != test.ExpectedError { t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err) } @@ -184,7 +184,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) { start := time.Now() for bench.Loop() { copy(data, in) - res, _, err = RunPrecompiledContract(p, data, reqGas, nil) + res, _, err = RunPrecompiledContract(nil, p, common.HexToAddress(addr), data, reqGas, nil) } elapsed := uint64(time.Since(start)) if elapsed < 1 { @@ -534,6 +534,7 @@ func TestReinforceMultiClientPreCompilesTest(t *testing.T) { "IsCancun", "IsPrague", "IsOsaka", + "IsAmsterdam", "IsVerkle", "IsMadhugiri", "IsMadhugiriPro", diff --git a/core/vm/eips.go b/core/vm/eips.go index e68f775f56..122ffdbf48 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -44,6 +44,7 @@ var activators = map[int]func(*JumpTable){ 7702: enable7702, 7939: enable7939, 8024: enable8024, + 7843: enable7843, } // EnableEIP enables the given EIP on the config. @@ -603,3 +604,19 @@ func enable7702(jt *JumpTable) { jt[STATICCALL].dynamicGas = gasStaticCallEIP7702 jt[DELEGATECALL].dynamicGas = gasDelegateCallEIP7702 } + +// opSlotNum enables the SLOTNUM opcode +func opSlotNum(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + scope.Stack.push(uint256.NewInt(evm.Context.SlotNum)) + return nil, nil +} + +// enable7843 enables the SLOTNUM opcode as specified in EIP-7843. +func enable7843(jt *JumpTable) { + jt[SLOTNUM] = &operation{ + execute: opSlotNum, + constantGas: GasQuickStep, + minStack: minStack(0, 1), + maxStack: maxStack(0, 1), + } +} diff --git a/core/vm/evm.go b/core/vm/evm.go index f6ce516f1c..8c2bcc2c6f 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -56,17 +56,25 @@ var ecrecoverAddr = common.BytesToAddress([]byte{0x01}) // is checked before the CGo call. The prefetcher populates the cache during // warm-up so V2 workers typically hit it, saving ~1µs CGo overhead per call. func (evm *EVM) runPrecompile(p PrecompiledContract, addr common.Address, input []byte, gas uint64) ([]byte, uint64, error) { + // Touch the precompile for block-level accessList recording once Amsterdam + // is active (EIP-7928). Amsterdam is dormant on Bor networks (AmsterdamBlock + // nil on every preset), so IsAmsterdam is false and stateDB stays nil — no + // touch occurs until Amsterdam is scheduled. + var stateDB StateDB + if evm.chainRules.IsAmsterdam { + stateDB = evm.StateDB + } cache := evm.Config.EcrecoverCache if cache == nil || addr != ecrecoverAddr || len(input) > 128 { - return RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + return RunPrecompiledContract(stateDB, p, addr, input, gas, evm.Config.Tracer) } - return evm.runEcrecoverWithCache(p, input, gas, cache) + return evm.runEcrecoverWithCache(stateDB, p, addr, input, gas, cache) } // runEcrecoverWithCache handles the cached fast path for the ecrecover // precompile: input ≤ 128 bytes, cache present. Falls back to running the // precompile when the cache misses, then stores a successful result. -func (evm *EVM) runEcrecoverWithCache(p PrecompiledContract, input []byte, gas uint64, cache *sync.Map) ([]byte, uint64, error) { +func (evm *EVM) runEcrecoverWithCache(stateDB StateDB, p PrecompiledContract, addr common.Address, input []byte, gas uint64, cache *sync.Map) ([]byte, uint64, error) { // key is zero-initialised; copy fills the prefix and leaves the rest // as zeros, which matches RightPadBytes(input, 128) without the // extra heap allocation. Caller already enforced len(input) <= 128. @@ -79,12 +87,17 @@ func (evm *EVM) runEcrecoverWithCache(p PrecompiledContract, input []byte, gas u } evm.traceGasChange(gas, gas-gasCost) gas -= gasCost + // Touch the precompile even on the cache hit, matching the touch + // RunPrecompiledContract performs on the non-cached path. + if stateDB != nil { + stateDB.Exist(addr) + } if cached == nil { return nil, gas, nil } return cached.([]byte), gas, nil } - ret, remainingGas, err := RunPrecompiledContract(p, input, gas, evm.Config.Tracer) + ret, remainingGas, err := RunPrecompiledContract(stateDB, p, addr, input, gas, evm.Config.Tracer) if err == nil { cache.Store(key, ret) } @@ -120,6 +133,7 @@ type BlockContext struct { BaseFee *big.Int // Provides information for BASEFEE (0 if vm runs with NoBaseFee flag and 0 gas price) BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price) Random *common.Hash // Provides information for PREVRANDAO + SlotNum uint64 // Provides information for SLOTNUM } // TxContext provides the EVM with information about a transaction. @@ -220,6 +234,8 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon evm.table = &lisovoProInstructionSet case evm.chainRules.IsLisovo: evm.table = &lisovoInstructionSet + case evm.chainRules.IsAmsterdam: + evm.table = &amsterdamInstructionSet case evm.chainRules.IsOsaka: evm.table = &osakaInstructionSet case evm.chainRules.IsVerkle: @@ -321,14 +337,15 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if evm.depth > int(params.CallCreateDepth) { return nil, gas, ErrDepth } - // Fail if we're trying to transfer more than the available balance - if !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { + syscall := isSystemCall(caller) + + // Fail if we're trying to transfer more than the available balance. + if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) { return nil, gas, ErrInsufficientBalance } snapshot := evm.StateDB.Snapshot() p, isPrecompile := evm.precompile(addr) - if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) { // Add proof of absence to witness @@ -353,8 +370,12 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g evm.StateDB.CreateAccount(addr) } - evm.Context.Transfer(evm.StateDB, caller, addr, value) - + // Perform the value transfer only in non-syscall mode. + // Calling this is required even for zero-value transfers, + // to ensure the state clearing mechanism is applied. + if !syscall { + evm.Context.Transfer(evm.StateDB, caller, addr, value) + } if isPrecompile { ret, gas, err = evm.runPrecompile(p, addr, input, gas) } else { @@ -381,7 +402,6 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) } - gas = 0 } // TODO: consider clearing up unused snapshots: diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index a9ec4e0fe3..23d4e2a1a3 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -66,6 +66,7 @@ var ( lisovoInstructionSet = newLisovoInstructionSet() lisovoProInstructionSet = newLisovoProInstructionSet() chicagoInstructionSet = newChicagoInstructionSet() + amsterdamInstructionSet = newAmsterdamInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. @@ -108,6 +109,13 @@ func newChicagoInstructionSet() JumpTable { return validate(instructionSet) } +func newAmsterdamInstructionSet() JumpTable { + instructionSet := newOsakaInstructionSet() + enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode) + enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE) + return validate(instructionSet) +} + func newOsakaInstructionSet() JumpTable { instructionSet := newPragueInstructionSet() enable7939(&instructionSet) // EIP-7939 (CLZ opcode) diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index 9a32126a80..cb871dca6d 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -105,6 +105,7 @@ const ( BASEFEE OpCode = 0x48 BLOBHASH OpCode = 0x49 BLOBBASEFEE OpCode = 0x4a + SLOTNUM OpCode = 0x4b ) // 0x50 range - 'storage' and execution. @@ -320,6 +321,7 @@ var opCodeToString = [256]string{ BASEFEE: "BASEFEE", BLOBHASH: "BLOBHASH", BLOBBASEFEE: "BLOBBASEFEE", + SLOTNUM: "SLOTNUM", // 0x50 range - 'storage' and execution. POP: "POP", @@ -502,6 +504,7 @@ var stringToOp = map[string]OpCode{ "BASEFEE": BASEFEE, "BLOBHASH": BLOBHASH, "BLOBBASEFEE": BLOBBASEFEE, + "SLOTNUM": SLOTNUM, "DELEGATECALL": DELEGATECALL, "STATICCALL": STATICCALL, "CODESIZE": CODESIZE, diff --git a/docs/upstream-merges/v1.17.4/fork-register.md b/docs/upstream-merges/v1.17.4/fork-register.md index 63fe7267fa..844b95468f 100644 --- a/docs/upstream-merges/v1.17.4/fork-register.md +++ b/docs/upstream-merges/v1.17.4/fork-register.md @@ -18,6 +18,9 @@ gate-guarded (not ungated / unconditional). | EIP-8024 missing-immediate-byte update (#33614) | boundary `845009f68` | 10 (v1.17.0 9/12) | `core/vm/instructions.go`, `core/vm/instructions_test.go` | same as above — no gate change; spec-clarification only (missing immediate byte after DUPN/SWAPN/EXCHANGE now treated as `0x00`, matching PUSHn, instead of `ErrInvalidOpCode`) | **defer** (unchanged) | yes — re-confirmed `enable8024` registered only in the `activators` map (`eips.go:46`), never invoked by any `new*InstructionSet()`; no Bor preset sets `ExtraEips`. The opcodes stay absent from every Bor network's jump table; the behavior change is unreachable. Auto-merged clean; `TestEIP8024_Execution` (incl. new `DUPN_MISSING_IMMEDIATE`) passes. | | Fulu (Ethereum CL fork) beacon types (#33349) | boundary `228933a66` | 5 (v1.17.0 4/12) | `beacon/types/beacon_block.go`, `beacon/types/exec_header.go` | n/a — CL block/header type definitions; not a Bor execution-layer fork gate | **already-ours / inert** | yes — Fulu is an Ethereum consensus-layer fork; `beacon/types` carries CL block/header shapes used by the beacon light client, not Bor's PoA consensus path. No `params/config.go` gate, no execution behavior. Auto-merged clean, inert for Bor networks. | | Catalyst getPayload fork-timestamp checks (#32754) | boundary `228933a66` | 5 (v1.17.0 4/12) | `eth/catalyst/api.go` | n/a — Engine API `engine_getPayloadVX` version guard against configured fork timestamps | **already-ours / inert** | yes — the Engine API (`eth/catalyst`) is inherited-but-unused in Bor's PoA consensus (per repo `CLAUDE.md`). Adds a version-vs-fork-timestamp guard on getPayload; introduces no new fork and flips no gate. Auto-merged clean; the only catalyst conflicts were sync-mode (`ConfigSyncMode`→`api.eth.SyncMode()`), unrelated to this guard. | +| Amsterdam VM fork (gate) | #33742/#33589/#33928 | 14–15 (v1.17.1) | `params/config.go` | **wired block-based-nil like Osaka**: added `AmsterdamBlock *big.Int` (nil on every preset), `IsAmsterdam(num) = IsLondon(num) && isBlockForked(AmsterdamBlock, num)`, `Rules.IsAmsterdam` + `Rules()` population, and `IsAmsterdam` in `TestReinforceMultiClientPreCompilesTest`'s expected list. Corrects the v1.17.0 drop of upstream's `AmsterdamTime`/`IsAmsterdam`. | **enable-by-block** — dormant | yes — `AmsterdamBlock` nil on all presets → `isBlockForked(nil,·)` false → `IsAmsterdam` false everywhere. Enabling = set `AmsterdamBlock` on the target preset(s) (same as Osaka/Prague). | +| EIP-7843 SLOTNUM (opcode `0x4b`) | #33589 (`f811bfe4f`) | 15 (v1.17.1 1/2) | `core/vm/{opcodes,eips,jump_table,evm}.go`, `core/{evm,genesis}.go`, `core/types/block.go`, `miner/worker.go`, `consensus/beacon/consensus.go`, `core/state_processor_test.go` | opcode + `opSlotNum` + `enable7843` wired into `newAmsterdamInstructionSet`, dispatched via `case evm.chainRules.IsAmsterdam`; header `SlotNumber *uint64 rlp:"optional"` populated under `IsAmsterdam(num)` gate in genesis + miner `makeHeader`; validated under the gate in `consensus/beacon`. All gated on the (dormant) `IsAmsterdam`. | **enable-by-block** — dormant | yes — `IsAmsterdam` false on every preset → `amsterdamInstructionSet` never selected (SLOTNUM absent from active jump tables), header `SlotNumber` never set (nil → RLP unchanged, core/types tests pass), validation takes the pre-Amsterdam branch. **Caveat: enabling also requires Bor's block producer to supply `genParams.slotNum`** (Bor has no beacon slot — the miner errors "no slot number set post-amsterdam" otherwise). | +| EIP-8024 enabled in Amsterdam | #33928 (`2726c9ef9`) | 15 (v1.17.1 2/2) | `core/vm/jump_table.go` | `enable8024` invoked inside `newAmsterdamInstructionSet`, now instantiated + dispatched under the dormant `IsAmsterdam` gate | **enable-by-block** — dormant | yes — reachable only via the Amsterdam instruction set, which is selected only when `IsAmsterdam` (false on all presets). The earlier EIP-8024 rows (batches 4/5/10, via `ExtraEips`) remain separate and also unset. | ## Notes diff --git a/docs/upstream-merges/v1.17.4/ledger.md b/docs/upstream-merges/v1.17.4/ledger.md index 7cb03c81a6..a73b48e02e 100644 --- a/docs/upstream-merges/v1.17.4/ledger.md +++ b/docs/upstream-merges/v1.17.4/ledger.md @@ -285,7 +285,7 @@ Adopted (converged to upstream, Bor divergences preserved): Kept Bor divergence (declined upstream change): -- **`params.Rules.ChainID` retained** — upstream removed the `ChainID` field from `Rules` (struct-def deletion auto-merged; nothing in Bor reads `rules.ChainID`, build-confirmed). Restored it because Bor's `TestReinforceMultiClientPreCompilesTest` guard (`core/vm/contracts_test.go`) reflects over `Rules` fields and lists `ChainID` as expected — the guard exists specifically to force review of Bor↔Erigon precompile/Rules parity, so silently editing its expected list during a merge is exactly what it forbids. Restored the field + `chainID` local + `ChainID: new(big.Int).Set(chainID)` initializer; kept Bor's block-based `Rules()` (`_ uint64` param, single-arg `Is(num)`, Bor fork bools, no `IsAmsterdam`). +- **`params.Rules.ChainID` retained** — upstream removed the `ChainID` field from `Rules` (struct-def deletion auto-merged; nothing in Bor reads `rules.ChainID`, build-confirmed). Restored it because Bor's `TestReinforceMultiClientPreCompilesTest` guard (`core/vm/contracts_test.go`) reflects over `Rules` fields and lists `ChainID` as expected — the guard exists specifically to force review of Bor↔Erigon precompile/Rules parity, so silently editing its expected list during a merge is exactly what it forbids. Restored the field + `chainID` local + `ChainID: new(big.Int).Set(chainID)` initializer; kept Bor's block-based `Rules()` (`_ uint64` param, single-arg `Is(num)`, Bor fork bools, no `IsAmsterdam`). **Correction (v1.17.1 milestone 3): dropping `IsAmsterdam` here was a mistake** — every other upstream fork (Prague/Osaka/Verkle) is kept wired block-based-nil so it can be enabled by just setting the block. Amsterdam should have been converted the same way, not dropped. It was harmless in v1.17.0 (no code gated on it) but forced a strip-and-neutralize in v1.17.1 batches 14–15. Fixed by wiring `AmsterdamBlock`/`IsAmsterdam(num)`/`Rules.IsAmsterdam` block-based-nil (see the v1.17.1 batch 2/2 entry). - **`opKeccak256` `Keccak256Cache` (`core/vm/instructions.go`)** — upstream retired the reused-`evm.hasher` pattern for `hash := crypto.Keccak256Hash(data)` and deleted the `hasher`/`hasherBuf` EVM struct fields (deletion auto-merged). Kept Bor's actual optimization (the 64-byte Solidity-slot result cache) and adapted it to upstream's shape: cache stores/loads `common.Hash`, miss path uses `crypto.Keccak256Hash`, preimage recording preserved via the shared tail. `evm.go` kept Bor's `jumpDests: jd` and dropped the now-removed `hasher:` init. Deferred/declined — entangled with Bor divergence (`needs-wiring.md`): @@ -421,3 +421,123 @@ no deps). `go vet` clean except pre-existing `//nolint:govet` copylocks HEAD `2c0608fc6` (only net code change is `internal/download`): `core/state TestPDBMethodParity`, `core/vm TestAbortDuringJump`/`TestInterruptDuringExecution` (5ms flake). No leftover conflict markers; docs + `misc/` not staged. + +## v1.17.1 batch 1/2 (`9ecb6c4ae`, plan row 14) — merged `189030866` — first Amsterdam/EIP batch + +Milestone v1.17.1 opened; branch `ppatil-upstream-v1.17.1` cut off `ppatil-upstream-v1.17.0` +(HEAD `451b3b445`). 20 first-parent commits `0cf3d3ba4..9ecb6c4ae`. 14 conflicted files +(10 content + go.yml DU + version.go); the rest auto-merged. + +Fork/EIP surfaces (invariant 6 — all merged DORMANT, no activation moved on any Bor network): + +- **Amsterdam precompile touch (#33742, `cbf3d8fed`, `core/vm`)** — upstream added a + `stateDB.Exist(address)` "touch" in `RunPrecompiledContract` for EIP-7928 BAL recording, + gated on a non-nil `stateDB` that upstream passes only when `rules.IsAmsterdam`. Adapted Bor's + `runPrecompile`/`runEcrecoverWithCache` wrappers to the new + `RunPrecompiledContract(stateDB, p, address, input, gas, logger)` signature. **Gated on + `evm.chainRules.IsAmsterdam`** (see the batch-15 Amsterdam-wiring correction) — dormant on Bor + (`AmsterdamBlock` nil → `IsAmsterdam` false → `stateDB` nil → no touch). Threaded the touch onto + Bor's ecrecover-cache fast path too (dormant, faithful). Guard tests pass: + `TestReinforceMultiClientPreCompilesTest`, `TestBorHardforkPrecompileContinuityProfiles`, + `TestBorHardforkPrecompileContinuityAtNetworkBoundaries` — active precompile set unchanged. +- **Syscall value-transfer disable (#33741, `01fe1d716`, `core/vm/evm.go`)** — the + `if !syscall { Transfer(...) }` guard auto-merged cleanly; Bor's `syscall := isSystemCall(caller)` + already gates the balance check, so this is behavior-preserving for Bor's system-call + (state-sync) path. +- **BAL code-change type → list (#33774, `199ac16e0`, `core/types/bal/*`)** — auto-merged + type/encoding refactor only. BAL is not wired into Bor's `state_processor`/`blockchain` + (dormant). `core/types/bal` tests pass. +- **core/state slot-read tracking for empty storage (#33743, `e636e4e3c`)** — auto-merged; + adds a reader call on destructed-account committed-state reads for BAL readlist, but the + value is discarded and `originStorage`/return stay empty → state root unchanged + (behavior-preserving; committed-state reads are pre-block-immutable, no BlockSTM impact). + `core/state` tests pass. + +Notable resolutions: + +- **version/version.go** — declined upstream's `Patch = 1` bump (`105427690` begin v1.17.1 + cycle); kept Bor's `Patch = 0` / `Meta = "unstable"` (Bor doesn't track upstream version + numbers). Net-zero vs HEAD. +- **.github/workflows/go.yml (DU)** — deleted-by-us; Bor removed this workflow (`#1723`). + `git rm` to keep the deletion (upstream's #33866 tweak declined). +- **v5wire (#31547, `p2p/discover/v5wire/encoding.go`, `cmd/devp2p/internal/v5test/discv5tests.go`)** + — Bor had no real divergence (stray blank lines only); converged to upstream (`createMask(iv)` + + `applyMasking`; `var challenge1`). v5wire tests pass. +- **metrics influxdb interval (#33767, `cmd/geth/config.go`)** — adopted; supporting + `MetricsInfluxDBIntervalFlag`/`InfluxDBInterval` auto-merged. +- **eth_getStorageValues test (#32591, `internal/ethapi/api_test.go`)** — additive-both-sides + (Bor `TestCoinbase`/AccountAt vs upstream `TestGetStorageValues`); combined both, closing + Bor's last func and keeping the shared trailing brace for upstream's. Both tests pass. + +Deferred (recorded in needs-wiring.md): + +- **On-chain tx check in fetcher (#33607, `59ad40e56`)** — builds on the declined #33378 + (`validateMeta`/`txMetadataWithSeq`); Bor keeps `hasTx func(common.Hash) bool`. Sole + batch-14 commit in its 5 files → restored `eth/fetcher/{tx_fetcher,tx_fetcher_test,metrics}.go`, + `eth/handler.go`, `tests/fuzzers/txfetcher/txfetcher_fuzzer.go` to HEAD. No dangling refs. +- **testing_buildBlockV1 (#33656, `e40aa46e8`)** — test-only catalyst Engine API entangled + with Bor's VEBLOP `getWork` worker restructure. Sole batch-14 commit in its files → + reverted 8 modified files to HEAD, `git rm` 2 new files. No dangling refs. + +Verification (per-batch tier): `go build ./...` pass (rc=0); `go vet` clean on touched +packages (`core/vm`, `core/state`, `core/types/bal`, `p2p/discover/v5wire`, `cmd/geth`, +`internal/ethapi`, `miner`, `eth/fetcher`) except the pre-existing `//nolint:govet` copylocks. +`go test` pass: `core/vm` (guard tests green; only `TestInterruptDuringExecution` 5ms flake, +passes on isolated re-run), `core/state`, `core/types/bal`, `p2p/discover/v5wire`, `eth/fetcher` +(137s), `internal/ethapi` (`TestCoinbase` + `TestGetStorageValues` both pass). No leftover +conflict markers. 46 files net-changed vs HEAD. + +## v1.17.1 batch 2/2 (`16783c167`, plan row 15) — merged `b6175113d` — MILESTONE-CLOSING — first Amsterdam/EIP batch (2) + +20 first-parent commits `9ecb6c4ae..16783c167` (ends at the v1.17.1 release tag). 30 conflicted files. +55 files net-changed vs HEAD. + +Fork/EIP surfaces (invariant 6 — all merged DORMANT): + +- **Amsterdam fork gate WIRED block-based-nil (correction — see note below)** — `params/config.go` gains + `AmsterdamBlock *big.Int` (nil on every preset), `IsAmsterdam(num) = IsLondon(num) && isBlockForked(AmsterdamBlock, num)`, + `Rules.IsAmsterdam` + `Rules()` population, mirroring Osaka/Prague/Verkle. `IsAmsterdam` added to + `TestReinforceMultiClientPreCompilesTest`'s expected Rules-field list. This corrects the v1.17.0 drop of + upstream's `AmsterdamTime`/`IsAmsterdam` — enabling Amsterdam later is now "set `AmsterdamBlock`", like the + other dormant forks. +- **EIP-7843 SLOTNUM (#33589, `f811bfe4f`)** — opcode `SLOTNUM (0x4b)` + `opSlotNum` + `enable7843` wired into + `newAmsterdamInstructionSet`, instantiated (`amsterdamInstructionSet` var in `jump_table.go`) and dispatched + (`case evm.chainRules.IsAmsterdam` in `core/vm/evm.go`, above `IsOsaka`). Header field `SlotNumber *uint64 + \`rlp:"optional"\`` populated under the `IsAmsterdam(num)` gate in `core/genesis.go` + miner `makeHeader` + (`miner/worker.go`, with a `slotNum *uint64` field on Bor's `generateParams`), validated under the gate in + `consensus/beacon/consensus.go`, and read into `BlockContext.SlotNum` in `core/evm.go`. All dormant + (`IsAmsterdam` false → `amsterdamInstructionSet` never selected, `SlotNumber` never set → nil → RLP unchanged, + core/types tests pass). **Enabling SLOTNUM additionally needs Bor's producer to supply `genParams.slotNum` + (Bor has no beacon slot) — the miner errors otherwise.** +- **EIP-8024 in Amsterdam (#33928, `2726c9ef9`)** — `enable8024` in `newAmsterdamInstructionSet`, reachable only + via the (dormant) Amsterdam instruction set. +- **bintrie endianness (#33900, `95c6b0580`)** — 2-line fix; binary-tree is verkle-gated, dormant on Bor. +- Guards pass: `TestReinforceMultiClientPreCompilesTest`, both `TestBorHardforkPrecompileContinuity*`. +- Restore-when-Amsterdam-lands tracked in `fork-register.md`. + +Adopted: + +- **c-kzg `v2.1.5→v2.1.6` (#33901)** — auto-merged; `go mod tidy` reconciled go.sum. Keeps blob-KZG parity + with upstream. keeper module kept at Bor HEAD. +- **olekukonko/tablewriter → internal/tablewriter (#28892)** — adopted upstream's migration; removed the + duplicate import merge artifact in `core/rawdb/database.go`, fixed `MakeChainDatabase` 3→4 args (Bor's + `disableFreeze`) in `cmd/geth/dbcmd.go`. olekukonko dropped from go.mod. +- **p2p/tracker crash fix (#33940, `9962e2c9f`)** — adopted (`if t.expire == nil { return }` + schedule + refactor); applies to Bor's tracker (pre-#33835 API kept). `git rm` DU tracker_test.go. +- **trie/proof error propagation (#33898, `be92f5487`)** — adopted (`if err := tr.Update(...); err != nil`). +- **blobpool low-fee-delay test (#33893)** — combined Bor's `testChainConfig` + upstream's low-fee (1/1) values. +- **miner payload-rebuild timer (#b25080cac)** — adopted with Bor's `w.config` naming + (`timer.Reset(max(0, w.config.Recommit-time.Since(start)))`). +- **metrics influxdb interval (#33767)** — adopted (already staged in batch prep). + +Declined / kept-Bor: + +- version.go (declined `Patch=1` bump), AGENTS.md (kept Bor's), Dockerfiles (Bor already Go 1.26.4). +- **eth/68 drop (#33511, `723aae2b4`)** — deferred; full 16-file footprint reverted to HEAD (+`git rm` DU + `eth/downloader/downloader_test.go`). Piggybacked #2a4527240 (handshake metrics) + #cee751a1e (sync test) + reverted with it. See needs-wiring.md. + +Verification (per-batch tier): `go build ./...` rc=0; `go vet` clean on touched packages (bar pre-existing +copylocks); `go mod tidy` clean (go.mod diff = c-kzg bump + olekukonko removal). `go test` pass: `core/types`, +`core/vm` (guards), `core/` (182s), `miner` (196s), `eth/protocols/eth`, `trie`, `core/txpool/blobpool`, +`consensus/beacon`. No leftover conflict markers. diff --git a/docs/upstream-merges/v1.17.4/needs-wiring.md b/docs/upstream-merges/v1.17.4/needs-wiring.md index 0c2349d066..ea0c5c6acb 100644 --- a/docs/upstream-merges/v1.17.4/needs-wiring.md +++ b/docs/upstream-merges/v1.17.4/needs-wiring.md @@ -46,6 +46,9 @@ a future adoption would require. | Delayed p2p message decoding (#33835) | batch 12 (v1.17.0 12/12, `0cf3d3ba4`) | deferred | 26-file eth/snap protocol refactor: `BlockBodiesResponse`/`GetTrieNodesPacket` response fields become `rlp.RawList[...]` (decode-on-demand — limits decode work on unmatched/oversized peer messages, a DoS-hardening measure), the `p2p/tracker` API changes from `Track(peer, version, reqCode, resCode, id)` to typed `Track(req Request) error` / `Fulfil(resp Response) error` (+ `ErrNoMatchingRequest`/`ErrCodeMismatch`), the receipt interface gains `Derivable() types.DerivableList` + `EncodeForStorage() (rlp.RawValue, error)`, and every eth/snap response handler is reworked around lazy decoding. Entangled with several Bor divergences: the receipt interface's Bor-only `ExcludeStateSyncReceipt()` (state-sync consensus), Bor's `newBlockPushIntervalTimer`/`lastNewBlockPushUnix` NewBlock push metrics in `handlers.go`, the snap `requestTracker` singleton (`snap/tracker.go`), and Bor's forked downloader (`bor_downloader.go`, `bor_fetchers_concurrent_bodies.go`) + wit protocol. Security-positive but a large cross-file rework on the consensus-feeding p2p path — out of scope for a merge batch. Reverted the full footprint to HEAD (`eth/protocols/eth/*`, `eth/protocols/snap/*` incl. restoring `snap/tracker.go`, `eth/downloader/{queue,queue_test,bor_fetchers_concurrent_bodies}.go`, `eth/handler_eth{,_test}.go`, `p2p/tracker/tracker.go`, `cmd/devp2p/internal/ethtest/*`); `git rm` of `eth/downloader/downloader_test.go` (DU) + new `p2p/tracker/tracker_test.go`. | Adopt the delayed-decoding rework onto Bor's protocol layer: (1) migrate `p2p/tracker` to the typed `Track(Request)`/`Fulfil(Response)` API + update all Bor callers; (2) convert `BlockBodiesResponse`/`GetTrieNodesPacket` to `rlp.RawList` while preserving Bor's `Unpack()` semantics; (3) reconcile the receipt interface with Bor's `ExcludeStateSyncReceipt()`; (4) re-add Bor's NewBlock push metrics + snap `requestTracker` behavior on top; (5) port into Bor's forked downloader. Security-positive (DoS hardening) — good adopt candidate when the protocol layer is next revisited. | upstream boundary `0cf3d3ba4`; commit `0cba803fb` (#33835); `eth/protocols/eth/*`, `eth/protocols/snap/*`, `eth/downloader/*`, `p2p/tracker/*` | | EraE archive-format rewrite (#32157, #33827) | batch 11 (v1.17.0 11/12, `c50e5edfa`) | deferred | 15-file restructure of `internal/era`: splits the era1 monolith into `internal/era/onedb` + a new `internal/era/execdb`, adds `internal/era/proof.go`, a `types.SlimReceipt` RLP wrapper "used for era files", a `--era.format` (`era1`/`erae`) flag, and an `era.Era`/`era.Builder`/`ReadAtSeekCloser` interface layer. Entangled with two Bor divergences: (1) upstream's `ExportHistory` is built on "TD is no longer stored in the database" and recomputes initial TD by accumulating difficulty, but Bor HEAD still reads TD via `bc.GetTd`; (2) Bor's `ImportHistory(chain, db, dir, network)` adds a `db ethdb.Database` param + an explicit `InsertHeaderChain` step upstream's signature lacks. Adopting requires reworking Bor's TD storage + history-import path — out of scope for a merge batch, and TD-adjacent (consensus-sensitive). Reverted the full footprint to HEAD (`internal/era/{era,accumulator,builder,era_test,iterator}.go`, `internal/era/eradl/eradl.go`, `cmd/era/main.go`, `core/rawdb/eradb/eradb.go`, `cmd/utils/{cmd,history_test}.go`, `cmd/geth/chaincmd.go`, `core/types/receipt.go`+test); `git rm` of new `internal/era/{onedb/reader.go,onedb/builder.go,onedb/builder_test.go,onedb/iterator.go,execdb/*,proof.go}`. | Decide whether Bor adopts the EraE format: (1) reconcile `ExportHistory` with Bor's TD-in-DB (keep `bc.GetTd`, or adopt upstream's difficulty-accumulation only if Bor drops TD storage); (2) preserve Bor's `db` param + `InsertHeaderChain` in `ImportHistory` while adding the `from func(era.ReadAtSeekCloser) (era.Era, error)` format selector; (3) bring in `onedb`/`execdb` + `proof.go` + `SlimReceipt` + `--era.format`; (4) migrate `internal/era/eradl`, `cmd/era`, `core/rawdb/eradb` to the interface layer. Operational (history archive), not consensus. | upstream boundary `c50e5edfa`; commits `#32157`/`#33827`; `internal/era/*`, `cmd/utils/cmd.go`, `cmd/geth/chaincmd.go`, `core/types/receipt.go` | +| On-chain tx check in fetcher (#33607) | batch 14 (v1.17.1 1/2, `9ecb6c4ae`) | deferred | Threads a new `chain *core.BlockChain` (nil-able) through `NewTxFetcher`/`NewTxFetcherForTests`, adds a `txOnChainCache` + `chain` field to `TxFetcher`, a `txAnnounceOnchainMeter`, and an on-chain existence check in `Notify` (skip announcing txs already on chain). Builds directly on #33378's `validateMeta func(common.Hash, byte) error` + `txMetadataWithSeq` fetcher internals, which Bor **declined** (batch-6 row) — Bor keeps `hasTx func(common.Hash) bool` + `txMetadata`. The upstream `Notify` hunk even references #33378's `err` var. #33607 was the sole batch-14 commit touching all 5 of its files, so restored `eth/fetcher/{tx_fetcher,tx_fetcher_test,metrics}.go`, `eth/handler.go`, `tests/fuzzers/txfetcher/txfetcher_fuzzer.go` wholesale to Bor HEAD. | Adopt together with the batch-6 #33378 row: once Bor's fetcher migrates to `validateMeta`/`txMetadataWithSeq`, thread the nil-able `chain` param, add `txOnChainCache` population (mark-canonical / reorg) + the `Notify` on-chain skip. Perf/DoS-positive (avoids fetching already-chained txs). | upstream boundary `9ecb6c4ae`; commit `59ad40e56` (#33607); depends on the batch-6 #33378 row | +| testing_buildBlockV1 Engine API (#33656) | batch 14 (v1.17.1 1/2, `9ecb6c4ae`) | deferred | Adds a test-only `testing_buildBlockV1` catalyst Engine-API method: new `generateParams.{forceOverrides,overrideExtraData,overrideTxs}` fields, an override branch in `generateWork` (commit override txs instead of `fillTransactions`), override plumbing in `miner/payload_building.go` + `internal/ethapi/override`, `beacon/engine/types.go` + `eth/catalyst/{api,simulated_beacon}.go` wiring, and new `eth/catalyst/api_testing{,_test}.go`. Catalyst is inherited-but-unused in Bor's production consensus, and the worker.go changes collide head-on with Bor's VEBLOP `getWork`/`getWorkReq` async restructure (Bor rewrote the whole `generateWork` region — "Sanitize recommit interval" vs upstream's fillTransactions). #33656 was the sole batch-14 commit touching all its files, so reverted the 8 modified files to Bor HEAD and `git rm`'d the 2 new files. | Adopt only if the catalyst testing API is ever needed on Bor: adapt the override fields into Bor's restructured `generateParams`/`getWorkReq`, thread the override-commit branch through Bor's async work path, and re-add the catalyst wiring. Test-only; low priority. | upstream boundary `9ecb6c4ae`; commit `e40aa46e8` (#33656) | +| Drop eth/68 protocol version (#33511) | batch 15 (v1.17.1 2/2, `16783c167`) | deferred | 695-line deletion across 16 files removing eth/68 support: unifies `StatusPacket68`/`StatusPacket69`→`StatusPacket`, drops `NewBlockHashesPacket`/eth-68 receipt handling, reworks handshake/handlers/receipt. Entangled with Bor divergences: forked downloader (`bor_downloader.go`, `bor_fetchers_concurrent_bodies.go`), wit protocol, the receipt interface's Bor-only `ExcludeStateSyncReceipt()`, Bor's NewBlock push metrics (`newBlockPushIntervalTimer`/`lastNewBlockPushUnix`), and Bor's PoA block-announcement propagation (Bor still broadcasts blocks via the eth protocol, unlike post-Merge geth). Same class as the deferred #33835. Reverted the full 16-file footprint to HEAD (`eth/protocols/eth/{handler,handler_test,handlers,handshake,handshake_test,peer,protocol,protocol_test,receipt,receipt_test}.go`, `eth/handler_eth_test.go`, `eth/downloader/skeleton_test.go`, `eth/sync_test.go`, `cmd/devp2p/internal/ethtest/{conn,suite}.go`); `git rm` of DU `eth/downloader/downloader_test.go`. Two piggybacked commits reverted with it: **#2a4527240** (handshake timeout-metrics classification, 12 lines in handshake.go) and **#cee751a1e** (flaky `TestSnapSyncDisabling68` fix, sync_test.go) — both re-adoptable independently. | Adopt the eth/68 drop onto Bor's protocol layer alongside #33835: reconcile the unified `StatusPacket` + receipt interface with Bor's `ExcludeStateSyncReceipt()`, preserve NewBlock push metrics + PoA announcement path, and port into Bor's forked downloader/wit. Re-apply #2a4527240 (metrics fix) and #cee751a1e (test) at the same time. | upstream boundary `16783c167`; commit `723aae2b4` (#33511); relates to the batch-12 #33835 row | ## Notes - Fork-schedule adoptions (EIP-8024, Verkle, and the upcoming Osaka/BPO/ diff --git a/docs/upstream-merges/v1.17.4/plan.md b/docs/upstream-merges/v1.17.4/plan.md index 62dfa53274..3667c276af 100644 --- a/docs/upstream-merges/v1.17.4/plan.md +++ b/docs/upstream-merges/v1.17.4/plan.md @@ -35,8 +35,8 @@ Status legend: `pending` / `in-progress` / `merged` / `skipped`. | 11 | v1.17.0 | 10/12 | `c12959dc8` | 20 | perf/alloc sweep, keccak vendoring, Ledger Gen5, rlp RawList | merged (`f4f2c07fd`) | | 12 | v1.17.0 | 11/12 | `c50e5edfa` | 20 | EraE format, rlp iterators, HTTP/2 JSON-RPC, telemetry CLI wiring | merged (`2c0608fc6`) | | 13 | v1.17.0 | 12/12 | `0cf3d3ba4` | 7 | delayed p2p decoding, header-verification hardening, **v1.17.0 release** | merged (`70994671a`) | -| 14 | v1.17.1 | 1/2 | `9ecb6c4ae` | 20 | **syscall value-transfer disable (`core/vm`)**, BAL type changes, **Amsterdam precompile touch** | pending | -| 15 | v1.17.1 | 2/2 | `16783c167` | 20 | **eth/68 protocol drop**, **EIP-7843 SLOTNUM**, **8024 enabled in Amsterdam**, Go 1.26; release | pending | +| 14 | v1.17.1 | 1/2 | `9ecb6c4ae` | 20 | **syscall value-transfer disable (`core/vm`)**, BAL type changes, **Amsterdam precompile touch** | merged (`189030866`) | +| 15 | v1.17.1 | 2/2 | `16783c167` | 20 | **eth/68 protocol drop**, **EIP-7843 SLOTNUM**, **8024 enabled in Amsterdam**, Go 1.26; release | merged (`b6175113d`) | | 16 | v1.17.2 | 1/4 | `00540f946` | 20 | **EIP-7778 block gas accounting**, miner prefetcher, **amsterdam jump table**, default cache 4096 | pending | | 17 | v1.17.2 | 2/4 | `77e7e5ad1` | 20 | codedb refactor, **EIP-7954 max contract size**, trienode history alongside data | pending | | 18 | v1.17.2 | 3/4 | `e23b0cbc2` | 20 | stateless codedb fix, **call-variant gas measurement rework (`core/vm`)**, bintrie parallel hash | pending | diff --git a/eth/protocols/snap/sync.go b/eth/protocols/snap/sync.go index 0f427837a1..4a07e2c2b7 100644 --- a/eth/protocols/snap/sync.go +++ b/eth/protocols/snap/sync.go @@ -1824,9 +1824,13 @@ func (s *Syncer) revertAccountRequest(req *accountRequest) { } close(req.stale) - // Remove the request from the tracked set + // Remove the request from the tracked set and restore the peer to the + // idle pool so it can be reassigned work (skip if peer already left). s.lock.Lock() delete(s.accountReqs, req.id) + if _, ok := s.peers[req.peer]; ok { + s.accountIdlers[req.peer] = struct{}{} + } s.lock.Unlock() // If there's a timeout timer still running, abort it and mark the account @@ -1866,9 +1870,13 @@ func (s *Syncer) revertBytecodeRequest(req *bytecodeRequest) { } close(req.stale) - // Remove the request from the tracked set + // Remove the request from the tracked set and restore the peer to the + // idle pool so it can be reassigned work (skip if peer already left). s.lock.Lock() delete(s.bytecodeReqs, req.id) + if _, ok := s.peers[req.peer]; ok { + s.bytecodeIdlers[req.peer] = struct{}{} + } s.lock.Unlock() // If there's a timeout timer still running, abort it and mark the code @@ -1908,9 +1916,13 @@ func (s *Syncer) revertStorageRequest(req *storageRequest) { } close(req.stale) - // Remove the request from the tracked set + // Remove the request from the tracked set and restore the peer to the + // idle pool so it can be reassigned work (skip if peer already left). s.lock.Lock() delete(s.storageReqs, req.id) + if _, ok := s.peers[req.peer]; ok { + s.storageIdlers[req.peer] = struct{}{} + } s.lock.Unlock() // If there's a timeout timer still running, abort it and mark the storage @@ -1954,9 +1966,13 @@ func (s *Syncer) revertTrienodeHealRequest(req *trienodeHealRequest) { } close(req.stale) - // Remove the request from the tracked set + // Remove the request from the tracked set and restore the peer to the + // idle pool so it can be reassigned work (skip if peer already left). s.lock.Lock() delete(s.trienodeHealReqs, req.id) + if _, ok := s.peers[req.peer]; ok { + s.trienodeHealIdlers[req.peer] = struct{}{} + } s.lock.Unlock() // If there's a timeout timer still running, abort it and mark the trie node @@ -1996,9 +2012,13 @@ func (s *Syncer) revertBytecodeHealRequest(req *bytecodeHealRequest) { } close(req.stale) - // Remove the request from the tracked set + // Remove the request from the tracked set and restore the peer to the + // idle pool so it can be reassigned work (skip if peer already left). s.lock.Lock() delete(s.bytecodeHealReqs, req.id) + if _, ok := s.peers[req.peer]; ok { + s.bytecodeHealIdlers[req.peer] = struct{}{} + } s.lock.Unlock() // If there's a timeout timer still running, abort it and mark the code diff --git a/eth/tracers/native/mux.go b/eth/tracers/native/mux.go index 420773881f..c6883700f2 100644 --- a/eth/tracers/native/mux.go +++ b/eth/tracers/native/mux.go @@ -28,7 +28,7 @@ import ( ) func init() { - tracers.DefaultDirectory.Register("muxTracer", newMuxTracer, false) + tracers.DefaultDirectory.Register("muxTracer", newMuxTracerFromConfig, false) } // muxTracer is a go implementation of the Tracer interface which @@ -38,8 +38,8 @@ type muxTracer struct { tracers []*tracers.Tracer } -// newMuxTracer returns a new mux tracer. -func newMuxTracer(ctx *tracers.Context, cfg json.RawMessage, chainConfig *params.ChainConfig) (*tracers.Tracer, error) { +// newMuxTracerFromConfig returns a new mux tracer. +func newMuxTracerFromConfig(ctx *tracers.Context, cfg json.RawMessage, chainConfig *params.ChainConfig) (*tracers.Tracer, error) { var config map[string]json.RawMessage if err := json.Unmarshal(cfg, &config); err != nil { return nil, err @@ -57,7 +57,16 @@ func newMuxTracer(ctx *tracers.Context, cfg json.RawMessage, chainConfig *params objects = append(objects, t) names = append(names, k) } + return NewMuxTracer(names, objects) +} +// NewMuxTracer creates a multiplexing tracer that fans out tracing hooks to +// multiple child tracers. Each hook invocation is forwarded to all children, +// in the order they are provided. +// +// The names parameter associates a label with each tracer, used as keys in +// the aggregated JSON result returned by GetResult. +func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, error) { t := &muxTracer{names: names, tracers: objects} return &tracers.Tracer{ Hooks: &tracing.Hooks{ diff --git a/eth/tracers/native/opcode_counter.go b/eth/tracers/native/opcode_counter.go new file mode 100644 index 0000000000..d859b275f0 --- /dev/null +++ b/eth/tracers/native/opcode_counter.go @@ -0,0 +1,55 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package native + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" +) + +// opcodeCounter is a simple tracer that counts how many times each opcode is executed. +type opcodeCounter struct { + counts [256]uint64 +} + +// NewOpcodeCounter returns a new opcodeCounter tracer. +func NewOpcodeCounter() *tracers.Tracer { + c := &opcodeCounter{} + return &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnOpcode: func(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { + c.counts[op]++ + }, + }, + GetResult: c.getResult, + Stop: func(err error) {}, + } +} + +// getResult returns the opcode counts keyed by opcode name. +func (c *opcodeCounter) getResult() (json.RawMessage, error) { + out := make(map[string]uint64) + for op, count := range c.counts { + if count != 0 { + out[vm.OpCode(op).String()] = count + } + } + return json.Marshal(out) +} diff --git a/go.mod b/go.mod index 72d02e3b6a..e99c487362 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 - github.com/ethereum/c-kzg-4844/v2 v2.1.5 + github.com/ethereum/c-kzg-4844/v2 v2.1.6 github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab github.com/fatih/color v1.17.0 github.com/ferranbt/fastssz v0.1.2 @@ -70,7 +70,6 @@ require ( github.com/mitchellh/cli v1.1.5 github.com/mitchellh/go-homedir v1.1.0 github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 - github.com/olekukonko/tablewriter v0.0.5 github.com/pelletier/go-toml v1.9.5 github.com/peterh/liner v1.2.2 github.com/pion/stun/v3 v3.1.1 diff --git a/go.sum b/go.sum index f105105ecc..f17321ab15 100644 --- a/go.sum +++ b/go.sum @@ -424,8 +424,8 @@ github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9O github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= -github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= -github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -867,7 +867,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -946,8 +945,6 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/graphql/graphql.go b/graphql/graphql.go index 3cf69a5720..4e5c8c67ca 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -1093,6 +1093,18 @@ func (b *Block) ExcessBlobGas(ctx context.Context) (*hexutil.Uint64, error) { return &ret, nil } +func (b *Block) SlotNumber(ctx context.Context) (*hexutil.Uint64, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return nil, err + } + if header.SlotNumber == nil { + return nil, nil + } + ret := hexutil.Uint64(*header.SlotNumber) + return &ret, nil +} + // BlockFilterCriteria encapsulates criteria passed to a `logs` accessor inside // a block. type BlockFilterCriteria struct { diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 575f0b8bb8..8feb96725e 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -60,6 +60,10 @@ import ( // allowed to produce in order to speed up calculations. const estimateGasErrorRatio = 0.015 +// maxGetStorageSlots is the maximum total number of storage slots that can +// be requested in a single eth_getStorageValues call. +const maxGetStorageSlots = 1024 + var errBlobTxNotSupported = errors.New("signing blob transactions not supported") var errSubClosed = errors.New("chain subscription closed") @@ -729,6 +733,41 @@ func (api *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Addre return res[:], state.Error() } +// GetStorageValues returns multiple storage slot values for multiple accounts +// at the given block. +func (api *BlockChainAPI) GetStorageValues(ctx context.Context, requests map[common.Address][]common.Hash, blockNrOrHash rpc.BlockNumberOrHash) (map[common.Address][]hexutil.Bytes, error) { + // Count total slots requested. + var totalSlots int + for _, keys := range requests { + totalSlots += len(keys) + if totalSlots > maxGetStorageSlots { + return nil, &clientLimitExceededError{message: fmt.Sprintf("too many slots (max %d)", maxGetStorageSlots)} + } + } + if totalSlots == 0 { + return nil, &invalidParamsError{message: "empty request"} + } + + state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if state == nil || err != nil { + return nil, err + } + + result := make(map[common.Address][]hexutil.Bytes, len(requests)) + for addr, keys := range requests { + vals := make([]hexutil.Bytes, len(keys)) + for i, key := range keys { + v := state.GetState(addr, key) + vals[i] = v[:] + } + if err := state.Error(); err != nil { + return nil, err + } + result[addr] = vals + } + return result, nil +} + // GetBlockReceipts returns the block receipts for the given block hash or number or tag. func (api *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]map[string]interface{}, error) { var ( @@ -1261,6 +1300,9 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} { if head.RequestsHash != nil { result["requestsHash"] = head.RequestsHash } + if head.SlotNumber != nil { + result["slotNumber"] = head.SlotNumber + } return result } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 2004d4584b..90c5211ad3 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -5552,6 +5552,94 @@ func TestAccountAt(t *testing.T) { }) } +func TestGetStorageValues(t *testing.T) { + t.Parallel() + + var ( + addr1 = common.HexToAddress("0x1111") + addr2 = common.HexToAddress("0x2222") + slot0 = common.Hash{} + slot1 = common.BigToHash(big.NewInt(1)) + slot2 = common.BigToHash(big.NewInt(2)) + val0 = common.BigToHash(big.NewInt(42)) + val1 = common.BigToHash(big.NewInt(100)) + val2 = common.BigToHash(big.NewInt(200)) + + genesis = &core.Genesis{ + Config: params.MergedTestChainConfig, + Alloc: types.GenesisAlloc{ + addr1: { + Balance: big.NewInt(params.Ether), + Storage: map[common.Hash]common.Hash{ + slot0: val0, + slot1: val1, + }, + }, + addr2: { + Balance: big.NewInt(params.Ether), + Storage: map[common.Hash]common.Hash{ + slot2: val2, + }, + }, + }, + } + ) + api := NewBlockChainAPI(newTestBackend(t, 1, genesis, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) { + b.SetPoS() + })) + latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + + // Happy path: multiple addresses, multiple slots. + result, err := api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ + addr1: {slot0, slot1}, + addr2: {slot2}, + }, latest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(result) != 2 { + t.Fatalf("expected 2 addresses in result, got %d", len(result)) + } + if got := common.BytesToHash(result[addr1][0]); got != val0 { + t.Errorf("addr1 slot0: want %x, got %x", val0, got) + } + if got := common.BytesToHash(result[addr1][1]); got != val1 { + t.Errorf("addr1 slot1: want %x, got %x", val1, got) + } + if got := common.BytesToHash(result[addr2][0]); got != val2 { + t.Errorf("addr2 slot2: want %x, got %x", val2, got) + } + + // Missing slot returns zero. + result, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ + addr1: {common.HexToHash("0xff")}, + }, latest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := common.BytesToHash(result[addr1][0]); got != (common.Hash{}) { + t.Errorf("missing slot: want zero, got %x", got) + } + + // Empty request returns error. + _, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{}, latest) + if err == nil { + t.Fatal("expected error for empty request") + } + + // Exceeding slot limit returns error. + tooMany := make([]common.Hash, maxGetStorageSlots+1) + for i := range tooMany { + tooMany[i] = common.BigToHash(big.NewInt(int64(i))) + } + _, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ + addr1: tooMany, + }, latest) + if err == nil { + t.Fatal("expected error for exceeding slot limit") + } +} + func TestCallWithStateMissingHeader(t *testing.T) { t.Parallel() diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 6a597b7ae1..6ad78c1153 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -162,6 +162,7 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, config AccessList: args.AccessList, BlobFeeCap: args.BlobFeeCap, BlobHashes: args.BlobHashes, + AuthorizationList: args.AuthorizationList, } latestBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) diff --git a/core/rawdb/database_tablewriter.go b/internal/tablewriter/database_tablewriter.go similarity index 97% rename from core/rawdb/database_tablewriter.go rename to internal/tablewriter/database_tablewriter.go index 5f4054ae47..f7faaea4d9 100644 --- a/core/rawdb/database_tablewriter.go +++ b/internal/tablewriter/database_tablewriter.go @@ -16,7 +16,7 @@ // Naive stub implementation for tablewriter -package rawdb +package tablewriter import ( "errors" @@ -37,7 +37,7 @@ type Table struct { rows [][]string } -func NewTableWriter(w io.Writer) *Table { +func NewWriter(w io.Writer) *Table { return &Table{out: w} } @@ -58,12 +58,12 @@ func (t *Table) SetFooter(footer []string) { t.footer = footer } -// AppendBulk sets all data rows for the table at once, replacing any existing rows. +// AppendBulk appends one or more data rows to the table. // // Each row must have the same number of columns as the headers, or validation // will fail during Render(). func (t *Table) AppendBulk(rows [][]string) { - t.rows = rows + t.rows = append(t.rows, rows...) } // Render outputs the complete table to the configured writer. The table is rendered diff --git a/core/rawdb/database_tablewriter_test.go b/internal/tablewriter/database_tablewriter_test.go similarity index 94% rename from core/rawdb/database_tablewriter_test.go rename to internal/tablewriter/database_tablewriter_test.go index e9de5d8ce8..b915dcdda8 100644 --- a/core/rawdb/database_tablewriter_test.go +++ b/internal/tablewriter/database_tablewriter_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package rawdb +package tablewriter import ( "bytes" @@ -24,7 +24,7 @@ import ( func TestTableWriterTinyGo(t *testing.T) { var buf bytes.Buffer - table := NewTableWriter(&buf) + table := NewWriter(&buf) headers := []string{"Database", "Size", "Items", "Status"} rows := [][]string{ @@ -48,7 +48,7 @@ func TestTableWriterValidationErrors(t *testing.T) { // Test missing headers t.Run("MissingHeaders", func(t *testing.T) { var buf bytes.Buffer - table := NewTableWriter(&buf) + table := NewWriter(&buf) rows := [][]string{{"x", "y", "z"}} @@ -63,7 +63,7 @@ func TestTableWriterValidationErrors(t *testing.T) { t.Run("NotEnoughRowColumns", func(t *testing.T) { var buf bytes.Buffer - table := NewTableWriter(&buf) + table := NewWriter(&buf) headers := []string{"A", "B", "C"} badRows := [][]string{ @@ -82,7 +82,7 @@ func TestTableWriterValidationErrors(t *testing.T) { t.Run("TooManyRowColumns", func(t *testing.T) { var buf bytes.Buffer - table := NewTableWriter(&buf) + table := NewWriter(&buf) headers := []string{"A", "B", "C"} badRows := [][]string{ @@ -102,7 +102,7 @@ func TestTableWriterValidationErrors(t *testing.T) { // Test mismatched footer columns t.Run("MismatchedFooterColumns", func(t *testing.T) { var buf bytes.Buffer - table := NewTableWriter(&buf) + table := NewWriter(&buf) headers := []string{"A", "B", "C"} rows := [][]string{{"x", "y", "z"}} diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index c395a5a737..9dd8d4a745 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -639,6 +639,12 @@ web3._extend({ params: 3, inputFormatter: [web3._extend.formatters.inputAddressFormatter, null, web3._extend.formatters.inputBlockNumberFormatter] }), + new web3._extend.Method({ + name: 'getStorageValues', + call: 'eth_getStorageValues', + params: 2, + inputFormatter: [null, web3._extend.formatters.inputBlockNumberFormatter] + }), new web3._extend.Method({ name: 'createAccessList', call: 'eth_createAccessList', diff --git a/metrics/config.go b/metrics/config.go index 72f94dd194..3d3cb693fd 100644 --- a/metrics/config.go +++ b/metrics/config.go @@ -16,18 +16,21 @@ package metrics +import "time" + // Config contains the configuration for the metric collection. type Config struct { - Enabled bool `toml:",omitempty"` - EnabledExpensive bool `toml:"-"` - HTTP string `toml:",omitempty"` - Port int `toml:",omitempty"` - EnableInfluxDB bool `toml:",omitempty"` - InfluxDBEndpoint string `toml:",omitempty"` - InfluxDBDatabase string `toml:",omitempty"` - InfluxDBUsername string `toml:",omitempty"` - InfluxDBPassword string `toml:",omitempty"` - InfluxDBTags string `toml:",omitempty"` + Enabled bool `toml:",omitempty"` + EnabledExpensive bool `toml:"-"` + HTTP string `toml:",omitempty"` + Port int `toml:",omitempty"` + EnableInfluxDB bool `toml:",omitempty"` + InfluxDBEndpoint string `toml:",omitempty"` + InfluxDBDatabase string `toml:",omitempty"` + InfluxDBUsername string `toml:",omitempty"` + InfluxDBPassword string `toml:",omitempty"` + InfluxDBTags string `toml:",omitempty"` + InfluxDBInterval time.Duration `toml:",omitempty"` EnableInfluxDBV2 bool `toml:",omitempty"` InfluxDBToken string `toml:",omitempty"` @@ -47,6 +50,7 @@ var DefaultConfig = Config{ InfluxDBUsername: "test", InfluxDBPassword: "test", InfluxDBTags: "host=localhost", + InfluxDBInterval: 10 * time.Second, // influxdbv2-specific flags EnableInfluxDBV2: false, diff --git a/miner/payload_building.go b/miner/payload_building.go index a86371321d..8422d18751 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -43,6 +43,7 @@ type BuildPayloadArgs struct { Random common.Hash // The provided randomness value Withdrawals types.Withdrawals // The provided withdrawals BeaconRoot *common.Hash // The provided beaconRoot (Cancun) + SlotNum *uint64 // The provided slotNumber Version engine.PayloadVersion // Versioning byte for payload id calculation. } @@ -57,6 +58,9 @@ func (args *BuildPayloadArgs) Id() engine.PayloadID { if args.BeaconRoot != nil { hasher.Write(args.BeaconRoot[:]) } + if args.SlotNum != nil { + binary.Write(hasher, binary.BigEndian, args.SlotNum) + } var out engine.PayloadID copy(out[:], hasher.Sum(nil)[:8]) @@ -222,6 +226,7 @@ func (w *worker) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload, e random: args.Random, withdrawals: args.Withdrawals, beaconRoot: args.BeaconRoot, + slotNum: args.SlotNum, noTxs: true, } empty := w.generateWork(emptyParams, witness) @@ -252,6 +257,7 @@ func (w *worker) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload, e random: args.Random, withdrawals: args.Withdrawals, beaconRoot: args.BeaconRoot, + slotNum: args.SlotNum, noTxs: false, } @@ -265,7 +271,7 @@ func (w *worker) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload, e } else { log.Info("Error while generating work", "id", payload.id, "err", r.err) } - timer.Reset(w.config.Recommit) + timer.Reset(max(0, w.config.Recommit-time.Since(start))) case <-payload.stop: log.Info("Stopping work on payload", "id", payload.id, "reason", "delivery") return diff --git a/miner/worker.go b/miner/worker.go index 48f7d1322a..f1eb1ac9e7 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -1920,6 +1920,7 @@ type generateParams struct { random common.Hash // The randomness generated by beacon chain, empty before the merge withdrawals types.Withdrawals // List of withdrawals to include in block. beaconRoot *common.Hash // The beacon root (cancun field). + slotNum *uint64 // The slot number (EIP-7843 SLOTNUM, amsterdam field). Dormant on Bor — never populated, so header.SlotNumber stays nil. noTxs bool // Flag whether an empty block without any transaction is expected statedb *state.StateDB // The statedb to use for block generation prefetchReader state.ReaderWithStats // The prefetch reader to use for statistics @@ -2016,6 +2017,15 @@ func (w *worker) makeHeader(genParams *generateParams, waitOnPrepare bool) (*typ return nil, common.Address{}, err } + // EIP-7843 SLOTNUM. Amsterdam is dormant on Bor (AmsterdamBlock nil), so this + // never fires; when Amsterdam is scheduled, Bor's producer must supply slotNum. + if w.chainConfig.IsAmsterdam(header.Number) { + if genParams.slotNum == nil { + return nil, common.Address{}, errors.New("no slot number set post-amsterdam") + } + header.SlotNumber = genParams.slotNum + } + return header, coinbase, nil } diff --git a/node/node.go b/node/node.go index 196c66f6a7..fb7c95d6d4 100644 --- a/node/node.go +++ b/node/node.go @@ -162,8 +162,10 @@ func New(conf *Config) (*Node, error) { // Configure RPC servers. node.http = newHTTPServer(node.log, conf.HTTPTimeouts) node.httpAuth = newHTTPServer(node.log, conf.HTTPTimeouts) + node.httpAuth.disableHTTP2 = true // Engine API does not need HTTP/2 node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts) node.wsAuth = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts) + node.wsAuth.disableHTTP2 = true node.ipc = newIPCServer(node.log, conf.IPCEndpoint()) return node, nil diff --git a/node/rpcstack.go b/node/rpcstack.go index 0b792ef61a..83a9706221 100644 --- a/node/rpcstack.go +++ b/node/rpcstack.go @@ -96,6 +96,9 @@ type httpServer struct { port int handlerNames map[string]string + + // disableHTTP2 disables HTTP/2 support on this server when set to true. + disableHTTP2 bool } const ( @@ -151,7 +154,9 @@ func (h *httpServer) start() error { h.server = &http.Server{Handler: h} h.server.Protocols = new(http.Protocols) h.server.Protocols.SetHTTP1(true) - h.server.Protocols.SetUnencryptedHTTP2(true) + if !h.disableHTTP2 { + h.server.Protocols.SetUnencryptedHTTP2(true) + } if h.timeouts != (rpc.HTTPTimeouts{}) { CheckTimeouts(&h.timeouts) h.server.ReadTimeout = h.timeouts.ReadTimeout diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go index 94bbc28337..fec609a7d9 100644 --- a/p2p/discover/v5_udp_test.go +++ b/p2p/discover/v5_udp_test.go @@ -926,10 +926,10 @@ type testCodecFrame struct { } func (c *testCodec) Encode(toID enode.ID, addr string, p v5wire.Packet, _ *v5wire.Whoareyou) ([]byte, v5wire.Nonce, error) { - // To match the behavior of v5wire.Codec, we return the cached encoding of - // WHOAREYOU challenges. - if wp, ok := p.(*v5wire.Whoareyou); ok && len(wp.Encoded) > 0 { - return wp.Encoded, wp.Nonce, nil + if wp, ok := p.(*v5wire.Whoareyou); ok && len(wp.ChallengeData) > 0 { + // To match the behavior of v5wire.Codec, we return the cached encoding of + // WHOAREYOU challenges. + return wp.ChallengeData, wp.Nonce, nil } c.ctr++ @@ -946,7 +946,7 @@ func (c *testCodec) Encode(toID enode.ID, addr string, p v5wire.Packet, _ *v5wir // Store recently sent challenges. if w, ok := p.(*v5wire.Whoareyou); ok { w.Nonce = authTag - w.Encoded = frame + w.ChallengeData = frame if c.sentChallenges == nil { c.sentChallenges = make(map[enode.ID]*v5wire.Whoareyou) } @@ -985,6 +985,7 @@ func (c *testCodec) decodeFrame(input []byte) (frame testCodecFrame, p v5wire.Pa case v5wire.WhoareyouPacket: dec := new(v5wire.Whoareyou) err = rlp.DecodeBytes(frame.Packet, &dec) + dec.ChallengeData = bytes.Clone(input) p = dec default: p, err = v5wire.DecodeMessage(frame.Ptype, frame.Packet) diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go index 9ea9595eb6..d06d7d0291 100644 --- a/p2p/discover/v5wire/encoding.go +++ b/p2p/discover/v5wire/encoding.go @@ -192,10 +192,16 @@ func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoar switch { case packet.Kind() == WhoareyouPacket: - // just send the WHOAREYOU packet raw again, rather than the re-encoded challenge data w := packet.(*Whoareyou) - if len(w.Encoded) > 0 { - return w.Encoded, w.Nonce, nil + if len(w.ChallengeData) > 0 { + // This WHOAREYOU packet was encoded before, so it's a resend. + // The unmasked packet content is stored in w.ChallengeData. + // Just apply the masking again to finish encoding. + c.buf.Reset() + c.buf.Write(w.ChallengeData) + copy(head.IV[:], w.ChallengeData) + enc := applyMasking(id, head.IV, c.buf.Bytes()) + return enc, w.Nonce, nil } head, err = c.encodeWhoareyou(id, packet.(*Whoareyou)) case challenge != nil: @@ -231,7 +237,6 @@ func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoar if err != nil { return nil, Nonce{}, err } - challenge.Encoded = bytes.Clone(enc) c.sc.storeSentHandshake(id, addr, challenge) return enc, head.Nonce, err } @@ -251,14 +256,10 @@ func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoar // EncodeRaw encodes a packet with the given header. func (c *Codec) EncodeRaw(id enode.ID, head Header, msgdata []byte) ([]byte, error) { + // header c.writeHeaders(&head) - - // Apply masking. - masked := c.buf.Bytes()[sizeofMaskingIV:] - mask := head.mask(id) - mask.XORKeyStream(masked[:], masked[:]) - - // Write message data. + applyMasking(id, head.IV, c.buf.Bytes()) + // message data c.buf.Write(msgdata) return c.buf.Bytes(), nil @@ -490,7 +491,7 @@ func (c *Codec) Decode(inputData []byte, addr string) (src enode.ID, n *enode.No var head Header copy(head.IV[:], input[:sizeofMaskingIV]) - mask := head.mask(c.localnode.ID()) + mask := createMask(c.localnode.ID(), head.IV) staticHeader := input[sizeofMaskingIV:sizeofStaticPacketData] mask.XORKeyStream(staticHeader, staticHeader) @@ -736,11 +737,17 @@ func (h *StaticHeader) checkValid(packetLen int, protocolID [6]byte) error { } // mask returns a cipher for 'masking' / 'unmasking' packet headers. -func (h *Header) mask(destID enode.ID) cipher.Stream { +func createMask(destID enode.ID, iv [16]byte) cipher.Stream { block, err := aes.NewCipher(destID[:16]) if err != nil { panic("can't create cipher") } + return cipher.NewCTR(block, iv[:]) +} - return cipher.NewCTR(block, h.IV[:]) +func applyMasking(destID enode.ID, iv [16]byte, packet []byte) []byte { + masked := packet[sizeofMaskingIV:] + mask := createMask(destID, iv) + mask.XORKeyStream(masked[:], masked[:]) + return packet } diff --git a/p2p/discover/v5wire/encoding_test.go b/p2p/discover/v5wire/encoding_test.go index 96656f7e47..44cef60aa5 100644 --- a/p2p/discover/v5wire/encoding_test.go +++ b/p2p/discover/v5wire/encoding_test.go @@ -281,6 +281,35 @@ func TestHandshake_BadHandshakeAttack(t *testing.T) { net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, findnode) } +func TestEncodeWhoareyouResend(t *testing.T) { + t.Parallel() + net := newHandshakeTest() + defer net.close() + + // A -> B WHOAREYOU + challenge := &Whoareyou{ + Nonce: Nonce{1, 2, 3, 4}, + IDNonce: testIDnonce, + RecordSeq: 0, + } + enc, _ := net.nodeA.encode(t, net.nodeB, challenge) + net.nodeB.expectDecode(t, WhoareyouPacket, enc) + whoareyou1 := bytes.Clone(enc) + + if len(challenge.ChallengeData) == 0 { + t.Fatal("ChallengeData not assigned by encode") + } + + // A -> B WHOAREYOU + // Send the same challenge again. This should produce exactly + // the same bytes as the first send. + enc, _ = net.nodeA.encode(t, net.nodeB, challenge) + whoareyou2 := bytes.Clone(enc) + if !bytes.Equal(whoareyou2, whoareyou1) { + t.Fatal("re-encoded challenge not equal to first") + } +} + // This test checks some malformed packets. func TestDecodeErrorsV5(t *testing.T) { t.Parallel() diff --git a/p2p/discover/v5wire/msg.go b/p2p/discover/v5wire/msg.go index 4444c54673..482b35580f 100644 --- a/p2p/discover/v5wire/msg.go +++ b/p2p/discover/v5wire/msg.go @@ -63,19 +63,20 @@ type ( // WHOAREYOU contains the handshake challenge. Whoareyou struct { - ChallengeData []byte // Encoded challenge - Nonce Nonce // Nonce of request packet - IDNonce [16]byte // Identity proof data - RecordSeq uint64 // ENR sequence number of recipient + Nonce Nonce // Nonce of request packet + IDNonce [16]byte // Identity proof data + RecordSeq uint64 // ENR sequence number of recipient // Node is the locally known node record of recipient. // This must be set by the caller of Encode. - Node *enode.Node + Node *enode.Node `rlp:"-"` - sent mclock.AbsTime // for handshake GC. + // ChallengeData stores the unmasked encoding of the whole packet. This is the + // input data for verification. It is assigned by both Encode and Decode + // operations. + ChallengeData []byte `rlp:"-"` - // Encoded is packet raw data for sending out, but should not be include in the RLP encoding. - Encoded []byte `rlp:"-"` + sent mclock.AbsTime // for handshake GC. } // PING is sent during liveness checks. diff --git a/p2p/tracker/tracker.go b/p2p/tracker/tracker.go index 6c6665d5dd..5827694565 100644 --- a/p2p/tracker/tracker.go +++ b/p2p/tracker/tracker.go @@ -127,6 +127,10 @@ func (t *Tracker) clean() { t.lock.Lock() defer t.lock.Unlock() + if t.expire == nil { + return // Tracker was stopped. + } + // Expire anything within a certain threshold (might be no items at all if // we raced with the delivery) for t.expire.Len() > 0 { @@ -153,15 +157,15 @@ func (t *Tracker) clean() { t.schedule() } -// schedule starts a timer to trigger on the expiration of the first network -// packet. +// schedule starts a timer to trigger on the expiration of the first network packet. func (t *Tracker) schedule() { if t.expire.Len() == 0 { t.wake = nil return } - - t.wake = time.AfterFunc(time.Until(t.pending[t.expire.Front().Value.(uint64)].time.Add(t.timeout)), t.clean) + nextID := t.expire.Front().Value.(uint64) + nextTime := t.pending[nextID].time + t.wake = time.AfterFunc(time.Until(nextTime.Add(t.timeout)), t.clean) } // Fulfil fills a pending request, if any is available, reporting on various metrics. diff --git a/params/config.go b/params/config.go index 8910a0af06..48d3b8605b 100644 --- a/params/config.go +++ b/params/config.go @@ -854,11 +854,12 @@ type ChainConfig struct { // Fork scheduling was switched from blocks to timestamps here // Fork scheduling switched back to blockNumber in Bor - ShanghaiBlock *big.Int `json:"shanghaiBlock,omitempty"` // Shanghai switch Block (nil = no fork, 0 = already on shanghai) - CancunBlock *big.Int `json:"cancunBlock,omitempty"` // Cancun switch Block (nil = no fork, 0 = already on cancun) - PragueBlock *big.Int `json:"pragueBlock,omitempty"` // Prague switch Block (nil = no fork, 0 = already on prague) - VerkleBlock *big.Int `json:"verkleBlock,omitempty"` // Verkle switch Block (nil = no fork, 0 = already on verkle) - OsakaBlock *big.Int `json:"osakaBlock,omitempty"` // Osaka switch Block (nil = no fork, 0 = already on osaka) + ShanghaiBlock *big.Int `json:"shanghaiBlock,omitempty"` // Shanghai switch Block (nil = no fork, 0 = already on shanghai) + CancunBlock *big.Int `json:"cancunBlock,omitempty"` // Cancun switch Block (nil = no fork, 0 = already on cancun) + PragueBlock *big.Int `json:"pragueBlock,omitempty"` // Prague switch Block (nil = no fork, 0 = already on prague) + VerkleBlock *big.Int `json:"verkleBlock,omitempty"` // Verkle switch Block (nil = no fork, 0 = already on verkle) + OsakaBlock *big.Int `json:"osakaBlock,omitempty"` // Osaka switch Block (nil = no fork, 0 = already on osaka) + AmsterdamBlock *big.Int `json:"amsterdamBlock,omitempty"` // Amsterdam switch Block (nil = no fork, 0 = already on amsterdam) // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. @@ -1476,6 +1477,11 @@ func (c *ChainConfig) IsOsaka(num *big.Int) bool { return c.IsLondon(num) && isBlockForked(c.OsakaBlock, num) } +// IsAmsterdam returns whether num is either equal to the Amsterdam fork block or greater. +func (c *ChainConfig) IsAmsterdam(num *big.Int) bool { + return c.IsLondon(num) && isBlockForked(c.AmsterdamBlock, num) +} + // IsVerkleGenesis checks whether the verkle fork is activated at the genesis block. // // Verkle mode is considered enabled if the verkle fork time is configured, @@ -1905,6 +1911,7 @@ type Rules struct { IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool IsBerlin, IsLondon bool IsMerge, IsShanghai, IsCancun, IsPrague, IsOsaka bool + IsAmsterdam bool IsVerkle bool IsMadhugiri bool IsMadhugiriPro bool @@ -1942,6 +1949,7 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, _ uint64) Rules { IsPrague: c.IsPrague(num), IsVerkle: c.IsVerkle(num), IsOsaka: c.IsOsaka(num), + IsAmsterdam: c.IsAmsterdam(num), IsEIP4762: c.IsVerkle(num), IsMadhugiri: c.Bor != nil && c.Bor.IsMadhugiri(num), IsMadhugiriPro: c.Bor != nil && c.Bor.IsMadhugiriPro(num), diff --git a/tests/block_test_util.go b/tests/block_test_util.go index df4a635698..289b5f1007 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -97,6 +97,7 @@ type btHeader struct { BlobGasUsed *uint64 ExcessBlobGas *uint64 ParentBeaconBlockRoot *common.Hash + SlotNumber *uint64 } type btHeaderMarshaling struct { @@ -109,6 +110,7 @@ type btHeaderMarshaling struct { BaseFeePerGas *math.HexOrDecimal256 BlobGasUsed *math.HexOrDecimal64 ExcessBlobGas *math.HexOrDecimal64 + SlotNumber *math.HexOrDecimal64 } // Run executes the test against a serial state processor (the default @@ -391,6 +393,9 @@ func validateHeader(h *btHeader, h2 *types.Header) error { if !reflect.DeepEqual(h.ParentBeaconBlockRoot, h2.ParentBeaconRoot) { return fmt.Errorf("parentBeaconBlockRoot: want: %v have: %v", h.ParentBeaconBlockRoot, h2.ParentBeaconRoot) } + if !reflect.DeepEqual(h.SlotNumber, h2.SlotNumber) { + return fmt.Errorf("slotNumber: want: %v have: %v", h.SlotNumber, h2.SlotNumber) + } return nil } diff --git a/tests/gen_btheader.go b/tests/gen_btheader.go index 7213de14c1..c007fdbcd6 100644 --- a/tests/gen_btheader.go +++ b/tests/gen_btheader.go @@ -38,6 +38,7 @@ func (b btHeader) MarshalJSON() ([]byte, error) { BlobGasUsed *math.HexOrDecimal64 ExcessBlobGas *math.HexOrDecimal64 ParentBeaconBlockRoot *common.Hash + SlotNumber *math.HexOrDecimal64 } var enc btHeader @@ -62,6 +63,7 @@ func (b btHeader) MarshalJSON() ([]byte, error) { enc.BlobGasUsed = (*math.HexOrDecimal64)(b.BlobGasUsed) enc.ExcessBlobGas = (*math.HexOrDecimal64)(b.ExcessBlobGas) enc.ParentBeaconBlockRoot = b.ParentBeaconBlockRoot + enc.SlotNumber = (*math.HexOrDecimal64)(b.SlotNumber) return json.Marshal(&enc) } @@ -89,6 +91,7 @@ func (b *btHeader) UnmarshalJSON(input []byte) error { BlobGasUsed *math.HexOrDecimal64 ExcessBlobGas *math.HexOrDecimal64 ParentBeaconBlockRoot *common.Hash + SlotNumber *math.HexOrDecimal64 } var dec btHeader @@ -176,5 +179,8 @@ func (b *btHeader) UnmarshalJSON(input []byte) error { if dec.ParentBeaconBlockRoot != nil { b.ParentBeaconBlockRoot = dec.ParentBeaconBlockRoot } + if dec.SlotNumber != nil { + b.SlotNumber = (*uint64)(dec.SlotNumber) + } return nil } diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go index ecdd002331..a509c471b8 100644 --- a/trie/bintrie/trie.go +++ b/trie/bintrie/trie.go @@ -384,7 +384,7 @@ func (t *BinaryTrie) UpdateContractCode(addr common.Address, codeHash common.Has if groupOffset == 0 /* start of new group */ || chunknr == 0 /* first chunk in header group */ { values = make([][]byte, StemNodeWidth) var offset [HashSize]byte - binary.LittleEndian.PutUint64(offset[24:], chunknr+128) + binary.BigEndian.PutUint64(offset[24:], chunknr+128) key = GetBinaryTreeKey(addr, offset[:]) } values[groupOffset] = chunks[i : i+HashSize] diff --git a/trie/inspect.go b/trie/inspect.go new file mode 100644 index 0000000000..d7ca9ace63 --- /dev/null +++ b/trie/inspect.go @@ -0,0 +1,930 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package trie + +import ( + "bufio" + "bytes" + "cmp" + "container/heap" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "slices" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/internal/tablewriter" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/triedb/database" + "golang.org/x/sync/errgroup" + "golang.org/x/sync/semaphore" +) + +const ( + inspectDumpRecordSize = 32 + trieStatLevels*(3*4+8) + inspectDefaultTopN = 10 + inspectParallelism = int64(16) +) + +// inspector is used by the inner inspect function to coordinate across threads. +type inspector struct { + triedb database.NodeDatabase + root common.Hash + + config *InspectConfig + accountStat *LevelStats + + sem *semaphore.Weighted + + // Pass 1: dump file writer. + dumpMu sync.Mutex + dumpBuf *bufio.Writer + dumpFile *os.File + storageRecordsWritten atomic.Uint64 + + errMu sync.Mutex + err error +} + +// InspectConfig is a set of options to control inspection and format the output. +// TopN determines the maximum number of entries retained for each top-list. +// Path controls optional JSON output. DumpPath controls the pass-1 dump location. +type InspectConfig struct { + NoStorage bool + TopN int + Path string + DumpPath string +} + +// Inspect walks the trie with the given root and records the number and type of +// nodes at each depth. Storage trie stats are streamed to disk in fixed-size +// records, then summarized in a second pass. +func Inspect(triedb database.NodeDatabase, root common.Hash, config *InspectConfig) error { + trie, err := New(TrieID(root), triedb) + if err != nil { + return fmt.Errorf("fail to open trie %s: %w", root, err) + } + config = normalizeInspectConfig(config) + + dumpFile, err := os.OpenFile(config.DumpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("failed to create trie dump %s: %w", config.DumpPath, err) + } + in := inspector{ + triedb: triedb, + root: root, + config: config, + accountStat: NewLevelStats(), + sem: semaphore.NewWeighted(inspectParallelism), + dumpBuf: bufio.NewWriterSize(dumpFile, 1<<20), + dumpFile: dumpFile, + } + + // Start progress reporter + start := time.Now() + done := make(chan struct{}) + go func() { + ticker := time.NewTicker(8 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + accountNodes := in.accountStat.TotalNodes() + storageRecords := in.storageRecordsWritten.Load() + log.Info("Inspecting trie", + "accountNodes", accountNodes, + "storageRecords", storageRecords, + "elapsed", common.PrettyDuration(time.Since(start))) + case <-done: + return + } + } + }() + + in.recordRootSize(trie, root, in.accountStat) + in.inspect(trie, trie.root, 0, []byte{}, in.accountStat) + + // inspect is synchronous: it waits for all spawned goroutines in its + // subtree before returning, so no additional wait is needed here. + + // Persist account trie stats as the sentinel record. + in.writeDumpRecord(common.Hash{}, in.accountStat) + if err := in.closeDump(); err != nil { + in.setError(err) + } + + // Stop progress reporter + close(done) + + if err := in.getError(); err != nil { + return err + } + return Summarize(config.DumpPath, config) +} + +// InspectContract inspects the on-disk footprint of a single contract. +// It reports snapshot storage (slot count + size) and storage trie node +// statistics (node type breakdown and per-depth distribution). +func InspectContract(triedb database.NodeDatabase, db ethdb.Database, stateRoot common.Hash, address common.Address) error { + // Resolve account from the state trie. + accountHash := crypto.Keccak256Hash(address.Bytes()) + accountTrie, err := New(TrieID(stateRoot), triedb) + if err != nil { + return fmt.Errorf("failed to open account trie: %w", err) + } + accountRLP, err := accountTrie.Get(crypto.Keccak256(address.Bytes())) + if err != nil { + return fmt.Errorf("failed to read account: %w", err) + } + if accountRLP == nil { + return fmt.Errorf("account not found: %s", address) + } + var account types.StateAccount + if err := rlp.DecodeBytes(accountRLP, &account); err != nil { + return fmt.Errorf("failed to decode account: %w", err) + } + if account.Root == (common.Hash{}) || account.Root == types.EmptyRootHash { + return fmt.Errorf("account %s has no storage", address) + } + + // Look up account snapshot. + accountData := rawdb.ReadAccountSnapshot(db, accountHash) + + // Run trie walk + snap iteration in parallel. + var ( + snapSlots atomic.Uint64 + snapSize atomic.Uint64 + g errgroup.Group + start = time.Now() + ) + + // Goroutine 1: Snapshot storage iteration. + g.Go(func() error { + prefix := append(rawdb.SnapshotStoragePrefix, accountHash.Bytes()...) + it := db.NewIterator(prefix, nil) + defer it.Release() + + for it.Next() { + if !bytes.HasPrefix(it.Key(), prefix) { + break + } + snapSlots.Add(1) + snapSize.Add(uint64(len(it.Key()) + len(it.Value()))) + } + return it.Error() + }) + + // Goroutine 2: Storage trie walk using the existing inspector. + storageStat := NewLevelStats() + g.Go(func() error { + owner := accountHash + storage, err := New(StorageTrieID(stateRoot, owner, account.Root), triedb) + if err != nil { + return fmt.Errorf("failed to open storage trie: %w", err) + } + in := &inspector{ + triedb: triedb, + root: stateRoot, + config: &InspectConfig{NoStorage: true}, + accountStat: NewLevelStats(), // unused, but needed by inspector + sem: semaphore.NewWeighted(inspectParallelism), + dumpBuf: bufio.NewWriter(io.Discard), + } + + in.recordRootSize(storage, account.Root, storageStat) + in.inspect(storage, storage.root, 0, []byte{}, storageStat) + + if err := in.getError(); err != nil { + return err + } + return nil + }) + + // Progress reporter. + done := make(chan struct{}) + go func() { + ticker := time.NewTicker(8 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + log.Info("Inspecting contract", + "snapSlots", snapSlots.Load(), + "trieNodes", storageStat.TotalNodes(), + "elapsed", common.PrettyDuration(time.Since(start))) + case <-done: + return + } + } + }() + + if err := g.Wait(); err != nil { + close(done) + return err + } + close(done) + + // Display results. + fmt.Printf("\n=== Contract Inspection: %s ===\n", address) + fmt.Printf("Account hash: %s\n\n", accountHash) + + if len(accountData) == 0 { + fmt.Println("Account snapshot: not found") + } else { + fmt.Printf("Account snapshot: %s\n", common.StorageSize(len(accountData))) + } + + fmt.Printf("Snapshot storage: %d slots (%s)\n", + snapSlots.Load(), common.StorageSize(snapSize.Load())) + + // Compute trie totals from LevelStats. + var trieTotal, trieSize uint64 + for i := 0; i < trieStatLevels; i++ { + short, full, value, size := storageStat.level[i].load() + trieTotal += short + full + value + trieSize += size + } + fmt.Printf("Storage trie: %d nodes (%s)\n", trieTotal, common.StorageSize(trieSize)) + + // Depth distribution table with node type columns. + fmt.Println("\nStorage Trie Depth Distribution:") + b := new(strings.Builder) + table := tablewriter.NewWriter(b) + table.SetHeader([]string{"Depth", "Short", "Full", "Value", "Nodes", "Size"}) + for i := 0; i < trieStatLevels; i++ { + short, full, value, size := storageStat.level[i].load() + total := short + full + value + if total == 0 && size == 0 { + continue + } + table.AppendBulk([][]string{{ + fmt.Sprint(i), + fmt.Sprint(short), + fmt.Sprint(full), + fmt.Sprint(value), + fmt.Sprint(total), + common.StorageSize(size).String(), + }}) + } + table.Render() + fmt.Print(b.String()) + + return nil +} + +func normalizeInspectConfig(config *InspectConfig) *InspectConfig { + if config == nil { + config = &InspectConfig{} + } + if config.TopN <= 0 { + config.TopN = inspectDefaultTopN + } + if config.DumpPath == "" { + config.DumpPath = "trie-dump.bin" + } + return config +} + +func (in *inspector) recordRootSize(trie *Trie, root common.Hash, stat *LevelStats) { + if root == (common.Hash{}) || root == types.EmptyRootHash { + return + } + blob := trie.prevalueTracer.Get(nil) + if len(blob) == 0 { + resolved, err := trie.reader.Node(nil, root) + if err != nil { + log.Error("Failed to read trie root for size accounting", "trie", trie.Hash(), "root", root, "err", err) + return + } + blob = resolved + } + stat.addSize(0, uint64(len(blob))) +} + +func (in *inspector) closeDump() error { + var ret error + if in.dumpBuf != nil { + if err := in.dumpBuf.Flush(); err != nil { + ret = errors.Join(ret, fmt.Errorf("failed to flush trie dump %s: %w", in.config.DumpPath, err)) + } + } + if in.dumpFile != nil { + if err := in.dumpFile.Close(); err != nil { + ret = errors.Join(ret, fmt.Errorf("failed to close trie dump %s: %w", in.config.DumpPath, err)) + } + } + return ret +} + +func (in *inspector) setError(err error) { + if err == nil { + return + } + in.errMu.Lock() + defer in.errMu.Unlock() + in.err = errors.Join(in.err, err) +} + +func (in *inspector) getError() error { + in.errMu.Lock() + defer in.errMu.Unlock() + return in.err +} + +func (in *inspector) hasError() bool { + return in.getError() != nil +} + +// trySpawn attempts to run fn in a new goroutine, bounded by the semaphore. +// If a slot is available the goroutine is started and tracked via wg; the +// caller must call wg.Wait() before reading any state written by fn. +// Returns false (and does not start a goroutine) when no slot is available. +func (in *inspector) trySpawn(wg *sync.WaitGroup, fn func()) bool { + if !in.sem.TryAcquire(1) { + return false + } + wg.Add(1) + go func() { + defer in.sem.Release(1) + defer wg.Done() + fn() + }() + return true +} + +func (in *inspector) writeDumpRecord(owner common.Hash, s *LevelStats) { + if in.hasError() { + return + } + var buf [inspectDumpRecordSize]byte + copy(buf[:32], owner[:]) + + off := 32 + for i := 0; i < trieStatLevels; i++ { + binary.LittleEndian.PutUint32(buf[off:], uint32(s.level[i].short.Load())) + off += 4 + binary.LittleEndian.PutUint32(buf[off:], uint32(s.level[i].full.Load())) + off += 4 + binary.LittleEndian.PutUint32(buf[off:], uint32(s.level[i].value.Load())) + off += 4 + binary.LittleEndian.PutUint64(buf[off:], s.level[i].size.Load()) + off += 8 + } + in.dumpMu.Lock() + _, err := in.dumpBuf.Write(buf[:]) + in.dumpMu.Unlock() + if err != nil { + in.setError(fmt.Errorf("failed writing trie dump record: %w", err)) + } + + // Increment counter for storage tries only (not for account trie) + if owner != (common.Hash{}) { + in.storageRecordsWritten.Add(1) + } +} + +// inspect walks the trie rooted at n and records node statistics into stat. +// It may spawn goroutines for subtrees, but always waits for them before +// returning — the caller sees a fully-populated stat when inspect returns. +func (in *inspector) inspect(trie *Trie, n node, height uint32, path []byte, stat *LevelStats) { + if n == nil { + return + } + + // wg tracks goroutines spawned by this call so we can wait for them + // before returning. This guarantees stat is complete when we return, + // which is critical for storage tries that write their dump record + // immediately after inspect returns. + var wg sync.WaitGroup + + // Four types of nodes can be encountered: + // - short: extend path with key, inspect single value. + // - full: inspect all 17 children, spin up new threads when possible. + // - hash: need to resolve node from disk, retry inspect on result. + // - value: if account, begin inspecting storage trie. + switch n := (n).(type) { + case *shortNode: + nextPath := slices.Concat(path, n.Key) + in.inspect(trie, n.Val, height+1, nextPath, stat) + case *fullNode: + for idx, child := range n.Children { + if child == nil { + continue + } + childPath := slices.Concat(path, []byte{byte(idx)}) + childNode := child + if in.trySpawn(&wg, func() { + in.inspect(trie, childNode, height+1, childPath, stat) + }) { + continue + } + in.inspect(trie, childNode, height+1, childPath, stat) + } + case hashNode: + blob, err := trie.reader.Node(path, common.BytesToHash(n)) + if err != nil { + log.Error("Failed to resolve HashNode", "err", err, "trie", trie.Hash(), "height", height+1, "path", path) + return + } + stat.addSize(height, uint64(len(blob))) + resolved := mustDecodeNode(n, blob) + in.inspect(trie, resolved, height, path, stat) + + // Return early here so this level isn't recorded twice. + return + case valueNode: + if !hasTerm(path) { + break + } + var account types.StateAccount + if err := rlp.Decode(bytes.NewReader(n), &account); err != nil { + // Not an account value. + break + } + if account.Root == (common.Hash{}) || account.Root == types.EmptyRootHash { + // Account is empty, nothing further to inspect. + break + } + + if !in.config.NoStorage { + owner := common.BytesToHash(hexToCompact(path)) + storage, err := New(StorageTrieID(in.root, owner, account.Root), in.triedb) + if err != nil { + log.Error("Failed to open account storage trie", "node", n, "error", err, "height", height, "path", common.Bytes2Hex(path)) + break + } + storageStat := NewLevelStats() + run := func() { + in.recordRootSize(storage, account.Root, storageStat) + in.inspect(storage, storage.root, 0, []byte{}, storageStat) + in.writeDumpRecord(owner, storageStat) + } + if in.trySpawn(&wg, run) { + break + } + run() + } + default: + panic(fmt.Sprintf("%T: invalid node: %v", n, n)) + } + + // Wait for all goroutines spawned at this level before recording + // the current node. This ensures the entire subtree is counted + // before this call returns. + wg.Wait() + + // Record stats for current height. + stat.add(n, height) +} + +// Summarize performs pass 2 over a trie dump and reports account stats, +// aggregate storage statistics, and top-N rankings. +func Summarize(dumpPath string, config *InspectConfig) error { + config = normalizeInspectConfig(config) + if dumpPath == "" { + dumpPath = config.DumpPath + } + if dumpPath == "" { + return errors.New("missing dump path") + } + file, err := os.Open(dumpPath) + if err != nil { + return fmt.Errorf("failed to open trie dump %s: %w", dumpPath, err) + } + defer file.Close() + + if info, err := file.Stat(); err == nil { + if info.Size()%inspectDumpRecordSize != 0 { + return fmt.Errorf("invalid trie dump size %d (not a multiple of %d)", info.Size(), inspectDumpRecordSize) + } + } + + depthTop := newStorageStatsTopN(config.TopN, compareStorageStatsByDepth) + totalTop := newStorageStatsTopN(config.TopN, compareStorageStatsByTotal) + valueTop := newStorageStatsTopN(config.TopN, compareStorageStatsByValue) + + summary := &inspectSummary{} + reader := bufio.NewReaderSize(file, 1<<20) + var buf [inspectDumpRecordSize]byte + + for { + _, err := io.ReadFull(reader, buf[:]) + if errors.Is(err, io.EOF) { + break + } + if errors.Is(err, io.ErrUnexpectedEOF) { + return fmt.Errorf("truncated trie dump %s", dumpPath) + } + if err != nil { + return fmt.Errorf("failed reading trie dump %s: %w", dumpPath, err) + } + + record := decodeDumpRecord(buf[:]) + snapshot := newStorageStats(record.Owner, record.Levels) + if record.Owner == (common.Hash{}) { + summary.Account = snapshot + continue + } + summary.StorageCount++ + summary.DepthHistogram[snapshot.MaxDepth]++ + for i := 0; i < trieStatLevels; i++ { + summary.StorageLevels[i].Short += record.Levels[i].Short + summary.StorageLevels[i].Full += record.Levels[i].Full + summary.StorageLevels[i].Value += record.Levels[i].Value + summary.StorageLevels[i].Size += record.Levels[i].Size + } + + depthTop.TryInsert(snapshot) + totalTop.TryInsert(snapshot) + valueTop.TryInsert(snapshot) + } + if summary.Account == nil { + return fmt.Errorf("dump file %s does not contain the account trie sentinel record", dumpPath) + } + for i := 0; i < trieStatLevels; i++ { + summary.StorageTotals.Short += summary.StorageLevels[i].Short + summary.StorageTotals.Full += summary.StorageLevels[i].Full + summary.StorageTotals.Value += summary.StorageLevels[i].Value + summary.StorageTotals.Size += summary.StorageLevels[i].Size + } + summary.TopByDepth = depthTop.Sorted() + summary.TopByTotalNodes = totalTop.Sorted() + summary.TopByValueNodes = valueTop.Sorted() + + if config.Path != "" { + return summary.writeJSON(config.Path) + } + summary.display() + return nil +} + +type dumpRecord struct { + Owner common.Hash + Levels [trieStatLevels]jsonLevel +} + +func decodeDumpRecord(raw []byte) dumpRecord { + var ( + record dumpRecord + off = 32 + ) + copy(record.Owner[:], raw[:32]) + for i := 0; i < trieStatLevels; i++ { + record.Levels[i] = jsonLevel{ + Short: uint64(binary.LittleEndian.Uint32(raw[off:])), + Full: uint64(binary.LittleEndian.Uint32(raw[off+4:])), + Value: uint64(binary.LittleEndian.Uint32(raw[off+8:])), + Size: binary.LittleEndian.Uint64(raw[off+12:]), + } + off += 20 + } + return record +} + +type storageStats struct { + Owner common.Hash + Levels [trieStatLevels]jsonLevel + Summary jsonLevel + MaxDepth int + TotalNodes uint64 + TotalSize uint64 +} + +func newStorageStats(owner common.Hash, levels [trieStatLevels]jsonLevel) *storageStats { + snapshot := &storageStats{Owner: owner, Levels: levels} + for i := 0; i < trieStatLevels; i++ { + level := levels[i] + if level.Short != 0 || level.Full != 0 || level.Value != 0 { + snapshot.MaxDepth = i + } + snapshot.Summary.Short += level.Short + snapshot.Summary.Full += level.Full + snapshot.Summary.Value += level.Value + snapshot.Summary.Size += level.Size + } + snapshot.TotalNodes = snapshot.Summary.Short + snapshot.Summary.Full + snapshot.Summary.Value + snapshot.TotalSize = snapshot.Summary.Size + return snapshot +} + +func trimLevels(levels [trieStatLevels]jsonLevel) []jsonLevel { + n := len(levels) + for n > 0 && levels[n-1] == (jsonLevel{}) { + n-- + } + return levels[:n] +} + +func (s *storageStats) MarshalJSON() ([]byte, error) { + type jsonStorageSnapshot struct { + Owner common.Hash `json:"Owner"` + MaxDepth int `json:"MaxDepth"` + TotalNodes uint64 `json:"TotalNodes"` + TotalSize uint64 `json:"TotalSize"` + ValueNodes uint64 `json:"ValueNodes"` + Levels []jsonLevel `json:"Levels"` + Summary jsonLevel `json:"Summary"` + } + return json.Marshal(jsonStorageSnapshot{ + Owner: s.Owner, + MaxDepth: s.MaxDepth, + TotalNodes: s.TotalNodes, + TotalSize: s.TotalSize, + ValueNodes: s.Summary.Value, + Levels: trimLevels(s.Levels), + Summary: s.Summary, + }) +} + +func (s *storageStats) toLevelStats() *LevelStats { + stats := NewLevelStats() + for i := 0; i < trieStatLevels; i++ { + stats.level[i].short.Store(s.Levels[i].Short) + stats.level[i].full.Store(s.Levels[i].Full) + stats.level[i].value.Store(s.Levels[i].Value) + stats.level[i].size.Store(s.Levels[i].Size) + } + return stats +} + +type storageStatsCompare func(a, b *storageStats) int + +type storageStatsTopN struct { + limit int + cmp storageStatsCompare + heap storageStatsHeap +} + +type storageStatsHeap struct { + items []*storageStats + cmp storageStatsCompare +} + +func (h storageStatsHeap) Len() int { return len(h.items) } + +func (h storageStatsHeap) Less(i, j int) bool { + // Keep the weakest entry at the root (min-heap semantics). + return h.cmp(h.items[i], h.items[j]) < 0 +} + +func (h storageStatsHeap) Swap(i, j int) { h.items[i], h.items[j] = h.items[j], h.items[i] } + +func (h *storageStatsHeap) Push(x any) { + h.items = append(h.items, x.(*storageStats)) +} + +func (h *storageStatsHeap) Pop() any { + item := h.items[len(h.items)-1] + h.items = h.items[:len(h.items)-1] + return item +} + +func newStorageStatsTopN(limit int, cmp storageStatsCompare) *storageStatsTopN { + h := storageStatsHeap{cmp: cmp} + heap.Init(&h) + return &storageStatsTopN{limit: limit, cmp: cmp, heap: h} +} + +func (t *storageStatsTopN) TryInsert(item *storageStats) { + if t.limit <= 0 { + return + } + if t.heap.Len() < t.limit { + heap.Push(&t.heap, item) + return + } + if t.cmp(item, t.heap.items[0]) <= 0 { + return + } + heap.Pop(&t.heap) + heap.Push(&t.heap, item) +} + +func (t *storageStatsTopN) Sorted() []*storageStats { + out := append([]*storageStats(nil), t.heap.items...) + sort.Slice(out, func(i, j int) bool { return t.cmp(out[i], out[j]) > 0 }) + return out +} + +func compareStorageStatsByDepth(a, b *storageStats) int { + return cmp.Or( + cmp.Compare(a.MaxDepth, b.MaxDepth), + cmp.Compare(a.TotalNodes, b.TotalNodes), + cmp.Compare(a.Summary.Value, b.Summary.Value), + bytes.Compare(a.Owner[:], b.Owner[:]), + ) +} + +func compareStorageStatsByTotal(a, b *storageStats) int { + return cmp.Or( + cmp.Compare(a.TotalNodes, b.TotalNodes), + cmp.Compare(a.MaxDepth, b.MaxDepth), + cmp.Compare(a.Summary.Value, b.Summary.Value), + bytes.Compare(a.Owner[:], b.Owner[:]), + ) +} + +func compareStorageStatsByValue(a, b *storageStats) int { + return cmp.Or( + cmp.Compare(a.Summary.Value, b.Summary.Value), + cmp.Compare(a.MaxDepth, b.MaxDepth), + cmp.Compare(a.TotalNodes, b.TotalNodes), + bytes.Compare(a.Owner[:], b.Owner[:]), + ) +} + +type inspectSummary struct { + Account *storageStats + StorageCount uint64 + StorageTotals jsonLevel + StorageLevels [trieStatLevels]jsonLevel + DepthHistogram [trieStatLevels]uint64 + TopByDepth []*storageStats + TopByTotalNodes []*storageStats + TopByValueNodes []*storageStats +} + +func (s *inspectSummary) display() { + s.displayCombinedDepthTable() + s.Account.toLevelStats().display("Accounts trie") + fmt.Println("Storage trie aggregate summary") + fmt.Printf("Total storage tries: %d\n", s.StorageCount) + totalNodes := s.StorageTotals.Short + s.StorageTotals.Full + s.StorageTotals.Value + fmt.Printf("Total nodes: %d\n", totalNodes) + fmt.Printf("Total size: %s\n", common.StorageSize(s.StorageTotals.Size)) + fmt.Printf(" Short nodes: %d\n", s.StorageTotals.Short) + fmt.Printf(" Full nodes: %d\n", s.StorageTotals.Full) + fmt.Printf(" Value nodes: %d\n", s.StorageTotals.Value) + + b := new(strings.Builder) + table := tablewriter.NewWriter(b) + table.SetHeader([]string{"Max Depth", "Storage Tries"}) + for i, count := range s.DepthHistogram { + table.AppendBulk([][]string{{fmt.Sprint(i), fmt.Sprint(count)}}) + } + table.Render() + fmt.Print(b.String()) + fmt.Println() + + s.displayTop("Top storage tries by max depth", s.TopByDepth) + s.displayTop("Top storage tries by total node count", s.TopByTotalNodes) + s.displayTop("Top storage tries by value (slot) count", s.TopByValueNodes) +} + +func (s *inspectSummary) displayCombinedDepthTable() { + accountTotal := s.Account.Summary.Short + s.Account.Summary.Full + s.Account.Summary.Value + storageTotal := s.StorageTotals.Short + s.StorageTotals.Full + s.StorageTotals.Value + accountTotalSize := s.Account.Summary.Size + storageTotalSize := s.StorageTotals.Size + + fmt.Println("Trie Depth Distribution") + fmt.Printf("Account Trie: %d nodes (%s)\n", accountTotal, common.StorageSize(accountTotalSize)) + fmt.Printf("Storage Tries: %d nodes (%s) across %d tries\n", storageTotal, common.StorageSize(storageTotalSize), s.StorageCount) + + b := new(strings.Builder) + table := tablewriter.NewWriter(b) + table.SetHeader([]string{"Depth", "Account Nodes", "Account Size", "Storage Nodes", "Storage Size"}) + for i := 0; i < trieStatLevels; i++ { + accountNodes := s.Account.Levels[i].Short + s.Account.Levels[i].Full + s.Account.Levels[i].Value + accountSize := s.Account.Levels[i].Size + storageNodes := s.StorageLevels[i].Short + s.StorageLevels[i].Full + s.StorageLevels[i].Value + storageSize := s.StorageLevels[i].Size + if accountNodes == 0 && storageNodes == 0 { + continue + } + table.AppendBulk([][]string{{ + fmt.Sprint(i), + fmt.Sprint(accountNodes), + common.StorageSize(accountSize).String(), + fmt.Sprint(storageNodes), + common.StorageSize(storageSize).String(), + }}) + } + table.Render() + fmt.Print(b.String()) + fmt.Println() +} + +func (s *inspectSummary) displayTop(title string, list []*storageStats) { + fmt.Println(title) + if len(list) == 0 { + fmt.Println("No storage tries found") + fmt.Println() + return + } + for i, item := range list { + fmt.Printf("%d: %s\n", i+1, item.Owner) + item.toLevelStats().display("storage trie") + } +} + +func (s *inspectSummary) MarshalJSON() ([]byte, error) { + type jsonAccountTrie struct { + Name string `json:"Name"` + Levels []jsonLevel `json:"Levels"` + Summary jsonLevel `json:"Summary"` + } + type jsonStorageSummary struct { + TotalStorageTries uint64 `json:"TotalStorageTries"` + Totals jsonLevel `json:"Totals"` + Levels []jsonLevel `json:"Levels"` + DepthHistogram [trieStatLevels]uint64 `json:"DepthHistogram"` + } + type jsonInspectSummary struct { + AccountTrie jsonAccountTrie `json:"AccountTrie"` + StorageSummary jsonStorageSummary `json:"StorageSummary"` + TopByDepth []*storageStats `json:"TopByDepth"` + TopByTotalNodes []*storageStats `json:"TopByTotalNodes"` + TopByValueNodes []*storageStats `json:"TopByValueNodes"` + } + return json.Marshal(jsonInspectSummary{ + AccountTrie: jsonAccountTrie{ + Name: "account trie", + Levels: trimLevels(s.Account.Levels), + Summary: s.Account.Summary, + }, + StorageSummary: jsonStorageSummary{ + TotalStorageTries: s.StorageCount, + Totals: s.StorageTotals, + Levels: trimLevels(s.StorageLevels), + DepthHistogram: s.DepthHistogram, + }, + TopByDepth: s.TopByDepth, + TopByTotalNodes: s.TopByTotalNodes, + TopByValueNodes: s.TopByValueNodes, + }) +} + +func (s *inspectSummary) writeJSON(path string) error { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return err + } + defer file.Close() + + enc := json.NewEncoder(file) + enc.SetIndent("", " ") + return enc.Encode(s) +} + +// display will print a table displaying the trie's node statistics. +func (s *LevelStats) display(title string) { + // Shorten title if too long. + if len(title) > 32 { + title = title[0:8] + "..." + title[len(title)-8:] + } + + b := new(strings.Builder) + table := tablewriter.NewWriter(b) + table.SetHeader([]string{title, "Level", "Short Nodes", "Full Node", "Value Node"}) + + stat := &stat{} + for i := range s.level { + if s.level[i].empty() { + continue + } + short, full, value, _ := s.level[i].load() + table.AppendBulk([][]string{{"-", fmt.Sprint(i), fmt.Sprint(short), fmt.Sprint(full), fmt.Sprint(value)}}) + stat.add(&s.level[i]) + } + short, full, value, _ := stat.load() + table.SetFooter([]string{"Total", "", fmt.Sprint(short), fmt.Sprint(full), fmt.Sprint(value)}) + table.Render() + fmt.Print(b.String()) + fmt.Println("Max depth", s.MaxDepth()) + fmt.Println() +} + +type jsonLevel struct { + Short uint64 + Full uint64 + Value uint64 + Size uint64 +} diff --git a/trie/inspect_test.go b/trie/inspect_test.go new file mode 100644 index 0000000000..c07904c52d --- /dev/null +++ b/trie/inspect_test.go @@ -0,0 +1,256 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package trie + +import ( + "encoding/json" + "math/rand" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie/trienode" + "github.com/holiman/uint256" +) + +// TestInspect inspects a randomly generated account trie. It's useful for +// quickly verifying changes to the results display. +func TestInspect(t *testing.T) { + db := newTestDatabase(rawdb.NewMemoryDatabase(), rawdb.HashScheme) + trie, err := NewStateTrie(TrieID(types.EmptyRootHash), db) + if err != nil { + t.Fatalf("failed to create state trie: %v", err) + } + // Create a realistic looking account trie with storage. + addresses, accounts := makeAccountsWithStorage(db, 11, true) + for i := 0; i < len(addresses); i++ { + trie.MustUpdate(crypto.Keccak256(addresses[i][:]), accounts[i]) + } + // Insert the accounts into the trie and hash it + root, nodes := trie.Commit(true) + db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes)) + db.Commit(root) + + tempDir := t.TempDir() + dumpPath := filepath.Join(tempDir, "trie-dump.bin") + if err := Inspect(db, root, &InspectConfig{ + TopN: 1, + DumpPath: dumpPath, + Path: filepath.Join(tempDir, "trie-summary.json"), + }); err != nil { + t.Fatalf("inspect failed: %v", err) + } + reanalysisPath := filepath.Join(tempDir, "trie-summary-reanalysis.json") + if err := Summarize(dumpPath, &InspectConfig{ + TopN: 1, + Path: reanalysisPath, + }); err != nil { + t.Fatalf("summarize failed: %v", err) + } + + inspectSummaryPath := filepath.Join(tempDir, "trie-summary.json") + inspectOut := loadInspectJSON(t, inspectSummaryPath) + reanalysisOut := loadInspectJSON(t, reanalysisPath) + + if len(inspectOut.StorageSummary.Levels) == 0 { + t.Fatal("expected StorageSummary.Levels to be populated") + } + if inspectOut.AccountTrie.Summary.Size == 0 { + t.Fatal("expected account trie size summary to be populated") + } + if inspectOut.StorageSummary.Totals.Size == 0 { + t.Fatal("expected storage trie size summary to be populated") + } + if !reflect.DeepEqual(inspectOut.AccountTrie, reanalysisOut.AccountTrie) { + t.Fatal("account trie summary mismatch between inspect and summarize") + } + if !reflect.DeepEqual(inspectOut.StorageSummary, reanalysisOut.StorageSummary) { + t.Fatal("storage summary mismatch between inspect and summarize") + } + + assertStorageTotalsMatchLevels(t, inspectOut) + assertStorageTotalsMatchLevels(t, reanalysisOut) + assertAccountTotalsMatchLevels(t, inspectOut.AccountTrie) + assertAccountTotalsMatchLevels(t, reanalysisOut.AccountTrie) + + var histogramTotal uint64 + for _, count := range inspectOut.StorageSummary.DepthHistogram { + histogramTotal += count + } + if histogramTotal != inspectOut.StorageSummary.TotalStorageTries { + t.Fatalf("depth histogram total %d does not match total storage tries %d", histogramTotal, inspectOut.StorageSummary.TotalStorageTries) + } +} + +type inspectJSONOutput struct { + // Reuse storageStats for AccountTrie JSON to avoid introducing a parallel + // account summary test type. AccountTrie JSON includes Levels+Summary, + // which map directly; other storageStats fields remain zero-values. + AccountTrie storageStats `json:"AccountTrie"` + + StorageSummary struct { + TotalStorageTries uint64 `json:"TotalStorageTries"` + Totals jsonLevel `json:"Totals"` + Levels []jsonLevel `json:"Levels"` + DepthHistogram [trieStatLevels]uint64 `json:"DepthHistogram"` + } `json:"StorageSummary"` +} + +func loadInspectJSON(t *testing.T, path string) inspectJSONOutput { + t.Helper() + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read %s: %v", path, err) + } + var out inspectJSONOutput + if err := json.Unmarshal(raw, &out); err != nil { + t.Fatalf("failed to decode %s: %v", path, err) + } + return out +} + +func assertStorageTotalsMatchLevels(t *testing.T, out inspectJSONOutput) { + t.Helper() + var fromLevels jsonLevel + for _, level := range out.StorageSummary.Levels { + fromLevels.Short += level.Short + fromLevels.Full += level.Full + fromLevels.Value += level.Value + fromLevels.Size += level.Size + } + if fromLevels.Short != out.StorageSummary.Totals.Short || fromLevels.Full != out.StorageSummary.Totals.Full || fromLevels.Value != out.StorageSummary.Totals.Value || fromLevels.Size != out.StorageSummary.Totals.Size { + t.Fatalf("storage totals mismatch: levels=%+v totals=%+v", fromLevels, out.StorageSummary.Totals) + } +} + +func assertAccountTotalsMatchLevels(t *testing.T, account storageStats) { + t.Helper() + var fromLevels jsonLevel + for _, level := range account.Levels { + fromLevels.Short += level.Short + fromLevels.Full += level.Full + fromLevels.Value += level.Value + fromLevels.Size += level.Size + } + if fromLevels.Short != account.Summary.Short || fromLevels.Full != account.Summary.Full || fromLevels.Value != account.Summary.Value || fromLevels.Size != account.Summary.Size { + t.Fatalf("account totals mismatch: levels=%+v totals=%+v", fromLevels, account.Summary) + } +} + +// TestInspectContract tests the InspectContract function on a single account +// with storage and snapshot data. +func TestInspectContract(t *testing.T) { + diskdb := rawdb.NewMemoryDatabase() + db := newTestDatabase(diskdb, rawdb.HashScheme) + + // Create a contract address and its storage trie. + address := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + accountHash := crypto.Keccak256Hash(address.Bytes()) + + // Build a storage trie with some entries. + storageTrie := NewEmpty(db) + storageSlots := make(map[common.Hash][]byte) + for i := 0; i < 10; i++ { + k := crypto.Keccak256Hash([]byte{byte(i)}) + v := []byte{byte(i + 1)} + storageTrie.MustUpdate(k.Bytes(), v) + storageSlots[k] = v + } + storageRoot, storageNodes := storageTrie.Commit(true) + db.Update(storageRoot, types.EmptyRootHash, trienode.NewWithNodeSet(storageNodes)) + db.Commit(storageRoot) + + // Build the account trie with the contract account. + account := types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(1000), + Root: storageRoot, + CodeHash: crypto.Keccak256(nil), + } + accountRLP, err := rlp.EncodeToBytes(&account) + if err != nil { + t.Fatalf("failed to encode account: %v", err) + } + + accountTrie := NewEmpty(db) + accountTrie.MustUpdate(crypto.Keccak256(address.Bytes()), accountRLP) + stateRoot, accountNodes := accountTrie.Commit(true) + db.Update(stateRoot, types.EmptyRootHash, trienode.NewWithNodeSet(accountNodes)) + db.Commit(stateRoot) + + // Write snapshot data for the account and its storage slots. + rawdb.WriteAccountSnapshot(diskdb, accountHash, accountRLP) + for k, v := range storageSlots { + rawdb.WriteStorageSnapshot(diskdb, accountHash, k, v) + } + + // InspectContract should succeed without error. + if err := InspectContract(db, diskdb, stateRoot, address); err != nil { + t.Fatalf("InspectContract failed: %v", err) + } +} + +func makeAccountsWithStorage(db *testDb, size int, storage bool) (addresses [][20]byte, accounts [][]byte) { + // Make the random benchmark deterministic + random := rand.New(rand.NewSource(0)) + + addresses = make([][20]byte, size) + for i := 0; i < len(addresses); i++ { + data := make([]byte, 20) + random.Read(data) + copy(addresses[i][:], data) + } + accounts = make([][]byte, len(addresses)) + for i := 0; i < len(accounts); i++ { + var ( + nonce = uint64(random.Int63()) + root = types.EmptyRootHash + code = crypto.Keccak256(nil) + ) + if storage { + trie := NewEmpty(db) + for range random.Uint32()%256 + 1 { // non-zero + k, v := make([]byte, 32), make([]byte, 32) + random.Read(k) + random.Read(v) + trie.MustUpdate(k, v) + } + var nodes *trienode.NodeSet + root, nodes = trie.Commit(true) + db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes)) + db.Commit(root) + } + numBytes := random.Uint32() % 33 // [0, 32] bytes + balanceBytes := make([]byte, numBytes) + random.Read(balanceBytes) + balance := new(uint256.Int).SetBytes(balanceBytes) + data, _ := rlp.EncodeToBytes(&types.StateAccount{ + Nonce: nonce, + Balance: balance, + Root: root, + CodeHash: code, + }) + accounts[i] = data + } + return addresses, accounts +} diff --git a/trie/levelstats.go b/trie/levelstats.go new file mode 100644 index 0000000000..9168e3fbaf --- /dev/null +++ b/trie/levelstats.go @@ -0,0 +1,123 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package trie + +import ( + "fmt" + "sync/atomic" +) + +const trieStatLevels = 16 + +// LevelStats tracks the type and count of trie nodes at each level in a trie. +// +// Note: theoretically it is possible to have up to 64 trie levels, but +// LevelStats supports exactly 16 levels and panics on deeper paths. +type LevelStats struct { + level [trieStatLevels]stat +} + +// NewLevelStats creates an empty trie statistics collector. +func NewLevelStats() *LevelStats { + return &LevelStats{} +} + +// MaxDepth iterates each level and finds the deepest level with at least one +// trie node. +func (s *LevelStats) MaxDepth() int { + depth := 0 + for i := range s.level { + if s.level[i].short.Load() != 0 || s.level[i].full.Load() != 0 || s.level[i].value.Load() != 0 { + depth = i + } + } + return depth +} + +// TotalNodes returns the total number of nodes across all levels and types. +func (s *LevelStats) TotalNodes() uint64 { + var total uint64 + for i := range s.level { + total += s.level[i].short.Load() + s.level[i].full.Load() + s.level[i].value.Load() + } + return total +} + +// add increases the node count by one for the specified node type and depth. +func (s *LevelStats) add(n node, depth uint32) { + d := int(depth) + switch (n).(type) { + case *shortNode: + s.level[d].short.Add(1) + case *fullNode: + s.level[d].full.Add(1) + case valueNode: + s.level[d].value.Add(1) + default: + panic(fmt.Sprintf("%T: invalid node: %v", n, n)) + } +} + +// addSize increases the raw byte-size tally at the specified depth. +func (s *LevelStats) addSize(depth uint32, size uint64) { + s.level[depth].size.Add(size) +} + +// AddLeaf records a leaf depth. Witness collection reuses the value-node bucket +// for leaf accounting. It panics if the depth is outside [0, 15]. +func (s *LevelStats) AddLeaf(depth int) { + s.level[depth].value.Add(1) +} + +// LeafDepths returns leaf counts grouped by depth. +func (s *LevelStats) LeafDepths() [trieStatLevels]int64 { + var leaves [trieStatLevels]int64 + for i := range s.level { + leaves[i] = int64(s.level[i].value.Load()) + } + return leaves +} + +// stat is a specific level's count of each node type. +type stat struct { + short atomic.Uint64 + full atomic.Uint64 + value atomic.Uint64 + size atomic.Uint64 +} + +// empty is a helper that returns whether there are any trie nodes at the level. +func (s *stat) empty() bool { + if s.full.Load() == 0 && s.short.Load() == 0 && s.value.Load() == 0 && s.size.Load() == 0 { + return true + } + return false +} + +// load is a helper that loads each node type's value. +func (s *stat) load() (uint64, uint64, uint64, uint64) { + return s.short.Load(), s.full.Load(), s.value.Load(), s.size.Load() +} + +// add is a helper that adds two level's stats together. +func (s *stat) add(other *stat) *stat { + s.short.Add(other.short.Load()) + s.full.Add(other.full.Load()) + s.value.Add(other.value.Load()) + s.size.Add(other.size.Load()) + return s +} diff --git a/trie/levelstats_test.go b/trie/levelstats_test.go new file mode 100644 index 0000000000..90581eb1ab --- /dev/null +++ b/trie/levelstats_test.go @@ -0,0 +1,37 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package trie + +import "testing" + +func TestLevelStatsAddLeafDepthBounds(t *testing.T) { + stats := NewLevelStats() + stats.AddLeaf(15) + + if got := stats.LeafDepths()[15]; got != 1 { + t.Fatalf("leaf count at depth 15 = %d, want 1", got) + } +} + +func TestLevelStatsAddLeafPanicsOnDepth16(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("expected panic for depth >= 16") + } + }() + NewLevelStats().AddLeaf(16) +} diff --git a/trie/proof.go b/trie/proof.go index 6419b5bbe8..07b55bc3db 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -500,7 +500,7 @@ func hasRightElement(node node, key []byte) bool { // // The firstKey is paired with firstProof, not necessarily the same as keys[0] // (unless firstProof is an existent proof). Similarly, lastKey and lastProof -// are paired. +// are paired. The firstKey should be less than or equal to all keys in the list. // // Expect the normal case, this function can also be used to verify the following // range proofs: @@ -570,9 +570,14 @@ func VerifyRangeProof(rootHash common.Hash, firstKey []byte, keys [][]byte, valu return false, nil } - var lastKey = keys[len(keys)-1] + // Short circuit if the key of first element is greater than firstKey. + // A nil firstKey slice is equivalent to an empty slice. + if bytes.Compare(firstKey, keys[0]) > 0 { + return false, errors.New("unexpected key-value pairs preceding the requested range") + } // Special case, there is only one element and two edge keys are same. // In this case, we can't construct two edge paths. So handle it here. + var lastKey = keys[len(keys)-1] if len(keys) == 1 && bytes.Equal(firstKey, lastKey) { root, val, err := proofToPath(rootHash, nil, firstKey, proof, false) if err != nil { @@ -631,7 +636,9 @@ func VerifyRangeProof(rootHash common.Hash, firstKey []byte, keys [][]byte, valu } for index, key := range keys { - _ = tr.Update(key, values[index]) + if err := tr.Update(key, values[index]); err != nil { + return false, err + } } if tr.Hash() != rootHash {