Skip to content

Commit 66049ed

Browse files
Amxxluiz-lvjernestognw
authored
Do not remove prefix zeros when RLP encoding bytes32 values (#6167)
Co-authored-by: Luiz Vasconcelos Júnior <64055364+luiz-lvj@users.noreply.github.com> Co-authored-by: ernestognw <ernestognw@gmail.com>
1 parent 9d96210 commit 66049ed

File tree

4 files changed

+45
-25
lines changed

4 files changed

+45
-25
lines changed

.changeset/young-corners-help.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`RLP`: Encode `bytes32` as a fixed size item and not as a scalar in `encode(bytes32)`. Scalar RLP encoding remains available by casting to a `uint256` and using the `encode(uint256)` function.

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
### Breaking changes
4+
5+
- `RLP`: The `encode(bytes32)` function now encodes `bytes32` as a fixed size item and not as a scalar in `encode(uint256)`. Users must replace calls to `encode(bytes32)` with `encode(uint256(bytes32))` to preserve the same behavior.
6+
37
## 5.5.0 (2025-10-31)
48

59
### Bug fixes

contracts/utils/RLP.sol

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ library RLP {
151151
}
152152

153153
/**
154-
* @dev Encode an address as RLP.
154+
* @dev Encode an address as an RLP item of fixed size (20 bytes).
155155
*
156156
* The address is encoded with its leading zeros (if it has any). If someone wants to encode the address as a scalar,
157157
* they can cast it to an uint256 and then call the corresponding {encode} function.
@@ -165,7 +165,11 @@ library RLP {
165165
}
166166
}
167167

168-
/// @dev Encode a uint256 as RLP.
168+
/**
169+
* @dev Encode an uint256 as an RLP scalar.
170+
*
171+
* Unlike {encode-bytes32-}, this function uses scalar encoding that removes the prefix zeros.
172+
*/
169173
function encode(uint256 input) internal pure returns (bytes memory result) {
170174
if (input < SHORT_OFFSET) {
171175
assembly ("memory-safe") {
@@ -186,9 +190,19 @@ library RLP {
186190
}
187191
}
188192

189-
/// @dev Encode a bytes32 as RLP. Type alias for {encode-uint256-}.
190-
function encode(bytes32 input) internal pure returns (bytes memory) {
191-
return encode(uint256(input));
193+
/**
194+
* @dev Encode a bytes32 as an RLP item of fixed size (32 bytes).
195+
*
196+
* Unlike {encode-uint256-}, this function uses array encoding that preserves the prefix zeros.
197+
*/
198+
function encode(bytes32 input) internal pure returns (bytes memory result) {
199+
assembly ("memory-safe") {
200+
result := mload(0x40)
201+
mstore(result, 0x21) // length of the encoded data: 1 (prefix) + 0x20
202+
mstore8(add(result, 0x20), 0xa0) // prefix: SHORT_OFFSET + 0x20
203+
mstore(add(result, 0x21), input)
204+
mstore(0x40, add(result, 0x41)) // reserve memory
205+
}
192206
}
193207

194208
/// @dev Encode a bytes buffer as RLP.

test/utils/RLP.test.js

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -91,36 +91,33 @@ describe('RLP', function () {
9191
});
9292

9393
it('encode/decode bytes32', async function () {
94-
for (const { input, expected } of [
95-
{ input: '0x0000000000000000000000000000000000000000000000000000000000000000', expected: '0x80' },
96-
{ input: '0x0000000000000000000000000000000000000000000000000000000000000001', expected: '0x01' },
97-
{
98-
input: '0x1000000000000000000000000000000000000000000000000000000000000000',
99-
expected: '0xa01000000000000000000000000000000000000000000000000000000000000000',
100-
},
94+
for (const input of [
95+
'0x0000000000000000000000000000000000000000000000000000000000000000',
96+
'0x0000000000000000000000000000000000000000000000000000000000000001',
97+
'0x1000000000000000000000000000000000000000000000000000000000000000',
98+
generators.bytes32(),
10199
]) {
102-
await expect(this.mock.$encode_bytes32(input)).to.eventually.equal(expected);
103-
await expect(this.mock.$decodeBytes32(expected)).to.eventually.equal(input);
100+
const encoded = ethers.encodeRlp(input);
101+
await expect(this.mock.$encode_bytes32(input)).to.eventually.equal(encoded);
102+
await expect(this.mock.$decodeBytes32(encoded)).to.eventually.equal(input);
104103
}
105104

105+
// Compact encoding for 1234
106106
await expect(this.mock.$decodeBytes32('0x8204d2')).to.eventually.equal(
107107
'0x00000000000000000000000000000000000000000000000000000000000004d2',
108-
); // Canonical encoding for 1234
108+
);
109+
// Encoding with one leading zero
109110
await expect(this.mock.$decodeBytes32('0x830004d2')).to.eventually.equal(
110111
'0x00000000000000000000000000000000000000000000000000000000000004d2',
111-
); // Non-canonical encoding with leading zero
112+
);
113+
// Encoding with two leading zeros
112114
await expect(this.mock.$decodeBytes32('0x84000004d2')).to.eventually.equal(
113115
'0x00000000000000000000000000000000000000000000000000000000000004d2',
114-
); // Non-canonical encoding with two leading zeros
115-
116-
// Canonical encoding for zero and non-canonical encodings with leading zeros
117-
await expect(this.mock.$decodeBytes32('0x80')).to.eventually.equal(
118-
'0x0000000000000000000000000000000000000000000000000000000000000000',
119116
);
120-
// 1 leading zero is not allowed for single bytes less than 0x80, they must be encoded as themselves
121-
await expect(this.mock.$decodeBytes32('0x820000')).to.eventually.equal(
122-
'0x0000000000000000000000000000000000000000000000000000000000000000',
123-
); // Non-canonical encoding with two leading zeros
117+
// Encoding for the value
118+
await expect(this.mock.$decodeBytes32('0x80')).to.eventually.equal(ethers.ZeroHash);
119+
// Encoding for two zeros (and nothing else)
120+
await expect(this.mock.$decodeBytes32('0x820000')).to.eventually.equal(ethers.ZeroHash);
124121
});
125122

126123
it('encode/decode empty byte', async function () {

0 commit comments

Comments
 (0)