Skip to content

fix: right-pad short bytesN values in solidity_keccak - #3912

Open
haybinalim wants to merge 1 commit into
ApeWorX:mainfrom
haybinalim:fix/solidity-keccak-short-bytesn
Open

haybinalim wants to merge 1 commit into
ApeWorX:mainfrom
haybinalim:fix/solidity-keccak-short-bytesn

Conversation

@haybinalim

@haybinalim haybinalim commented Oct 31, 2026 •

Copy link
Copy Markdown

What I did

Web3.solidity_keccak now right-pads a standalone bytesN value that is shorter than N bytes to N bytes, the same way Solidity's abi.encodePacked and eth_abi.packed.encode_packed encode it. Before this change, solidity_keccak(["bytes2"], [b"a"]) hashed 0x61 instead of 0x6100. Since #3882, validate_abi_value accepts these short values, so they passed validation and gave a hash that differs from Solidity.

Closes #3906

How I did it

  • hex_encode_abi_type (web3/_utils/encoding.py): for a standalone bytesN (no force_size), right-pad the hex value with zero bytes to N bytes. Dynamic bytes is unchanged.
  • Values longer than N bytes still raise Web3TypeError. validate_abi_value already rejects them (since fix: reject out-of-range integers and junk hex in validate_abi_value #3882), before the encoder runs, and this PR keeps that. I did not truncate them, because there's no single right way to do it: Solidity won't implicitly convert a longer value to bytesN, an explicit bytesN(x) keeps the leftmost bytes, and eth_abi's encode_packed raises ValueOutOfBounds. Hashing a cut-down value without warning would bring back the same kind of mismatch this PR fixes. The new tests cover this.
  • bytesN array elements are left alone. Fix solidity_keccak for bytesN array elements #3886 fixes those (they have to be padded to 32 bytes) in the same branch of hex_encode_abi_type. This change only touches the force_size is None path, so the two PRs don't overlap in behavior. Whichever one lands second is a one-line rebase (an if force_size is not None / elif abi_type != "bytes" pair), and I'm happy to do it.
  • Updated the existing ["bytes3"], ["0x5402"] case in test_solidity_keccak. It expected the same hash as bytes2(0x5402), but Solidity hashes bytes3(0x540200) to 0xed37fb74…. I also updated the hex_encode_abi_type unit case for bytes3 (0x5402 → 0x540200).
  • Documented the padding and the longer-value error in Web3.solidity_keccak (docs/web3.main.rst), with a bytes4(hex"1234") example.

There's no newsfragment because the repo now uses GitHub-generated release notes, per the contributing guide.

Note: #3907 and #3911 also target #3906, but they produce different hashes from Solidity for bytes2(b"a"). #3907 pads to 32 bytes (0x294587bf…), and #3911 left-pads and encodes hex strings as UTF-8 (0x97222015…). Solidity gives 0xa3fe1181…. #3911 also includes unrelated Wei/Gwei changes.

How to verify it

from web3 import Web3
from eth_abi.packed import encode_packed

Web3.solidity_keccak(["bytes2"], [b"a"]) == Web3.keccak(encode_packed(["bytes2"], [b"a"]))  # True now
Web3.solidity_keccak(["bytes4"], ["0x1234"]).to_0x_hex()
# '0x83cdd5d8e9eedfbb36b0f6748705833df28b0a094f672873aea9e21eda67aed9'
Web3.solidity_keccak(["bytes2"], ["0x010203"])  # Web3TypeError, unchanged

I took every new expected hash from keccak256(abi.encodePacked(...)) in a contract compiled with solc 0.8.26 and called on py-evm (EthereumTesterProvider). Each one also matches keccak(eth_abi.packed.encode_packed(...)):

types values hash (Solidity = eth_abi = this PR)
bytes2 b"a" / "0x61" 0xa3fe1181…
bytes3 "0x5402" 0xed37fb74…
bytes4 "0x1234" 0x83cdd5d8…
bytes32 "0x01" 0x48078cfe…
bytes4, bytes1 b"\x01\x02", b"\x03" 0x84afedaa…
string, bool, uint16, bytes4, address "testing a string!", False, 299, "0x5402", 0x49Ed…5614 0x9f22f3fb…

Local runs (Python 3.12):

  • pytest tests/core/utilities (604 passed) and pytest tests/core/web3-module (all passed)
  • pytest tests/integration/test_ethereum_tester.py -k "solidity_keccak or Web3Module": 42 passed, 1 skipped (batch requests, as on main)
  • The new and updated cases fail on main (10 failures) and pass with this change
  • ruff check ., ruff format --check . and mdformat --check *.md pass. mypy -p web3 -p ens reports nothing in the touched files.

Checklist

  • All changes are completed
  • Change is covered in tests
  • Documentation is complete

Solidity's abi.encodePacked encodes a bytesN value as exactly N bytes.
hex_encode_abi_type returned standalone bytesN values unpadded, so a
value shorter than N bytes (accepted by validate_abi_value since ApeWorX#3882)
produced a hash that differs from Solidity and eth_abi.encode_packed.

Right-pad standalone bytesN values to N bytes. Values longer than N
bytes are still rejected by validate_abi_value. bytesN array elements
are left unchanged (see ApeWorX#3886).

Closes ApeWorX#3906
@haybinalim haybinalim closed this Oct 31, 2026
@haybinalim haybinalim reopened this Oct 31, 2026
@haybinalim haybinalim changed the title bytes4"], ["0x1234"]).to_0x_hex()fix: right-pad short bytesN values in solidity_keccak fix: right-pad short bytesN values in solidity_keccak Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

solidity_keccak hashes short bytesN values without padding

1 participant