Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/young-corners-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`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.
24 changes: 19 additions & 5 deletions contracts/utils/RLP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ library RLP {
}

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

/// @dev Encode a uint256 as RLP.
/**
* @dev Encode an uint256 as an RLP scalar.
*
* Unlike {encode-bytes32-}, this function uses scalar encoding that removes the prefix zeros.
*/
function encode(uint256 input) internal pure returns (bytes memory result) {
if (input < SHORT_OFFSET) {
assembly ("memory-safe") {
Expand All @@ -186,9 +190,19 @@ library RLP {
}
}

/// @dev Encode a bytes32 as RLP. Type alias for {encode-uint256-}.
function encode(bytes32 input) internal pure returns (bytes memory) {
return encode(uint256(input));
/**
* @dev Encode a bytes32 as an RLP item of fixed size (32 bytes).
*
* Unlike {encode-uint256-}, this function uses array encoding that preserves the prefix zeros.
*/
function encode(bytes32 input) internal pure returns (bytes memory result) {
assembly ("memory-safe") {
result := mload(0x40)
mstore(result, 0x21) // length of the encoded data: 1 (prefix) + 0x20
mstore8(add(result, 0x20), 0xa0) // prefix: SHORT_OFFSET + 0x20
mstore(add(result, 0x21), input)
mstore(0x40, add(result, 0x41)) // reserve memory
}
}

/// @dev Encode a bytes buffer as RLP.
Expand Down
37 changes: 17 additions & 20 deletions test/utils/RLP.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,36 +91,33 @@ describe('RLP', function () {
});

it('encode/decode bytes32', async function () {
for (const { input, expected } of [
{ input: '0x0000000000000000000000000000000000000000000000000000000000000000', expected: '0x80' },
{ input: '0x0000000000000000000000000000000000000000000000000000000000000001', expected: '0x01' },
{
input: '0x1000000000000000000000000000000000000000000000000000000000000000',
expected: '0xa01000000000000000000000000000000000000000000000000000000000000000',
},
for (const input of [
'0x0000000000000000000000000000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000000000000000000000000000000001',
'0x1000000000000000000000000000000000000000000000000000000000000000',
generators.bytes32(),
]) {
await expect(this.mock.$encode_bytes32(input)).to.eventually.equal(expected);
await expect(this.mock.$decodeBytes32(expected)).to.eventually.equal(input);
const encoded = ethers.encodeRlp(input);
await expect(this.mock.$encode_bytes32(input)).to.eventually.equal(encoded);
await expect(this.mock.$decodeBytes32(encoded)).to.eventually.equal(input);
}

// Compact encoding for 1234
await expect(this.mock.$decodeBytes32('0x8204d2')).to.eventually.equal(
'0x00000000000000000000000000000000000000000000000000000000000004d2',
); // Canonical encoding for 1234
);
// Encoding with one leading zero
await expect(this.mock.$decodeBytes32('0x830004d2')).to.eventually.equal(
'0x00000000000000000000000000000000000000000000000000000000000004d2',
); // Non-canonical encoding with leading zero
);
// Encoding with two leading zeros
await expect(this.mock.$decodeBytes32('0x84000004d2')).to.eventually.equal(
'0x00000000000000000000000000000000000000000000000000000000000004d2',
); // Non-canonical encoding with two leading zeros

// Canonical encoding for zero and non-canonical encodings with leading zeros
await expect(this.mock.$decodeBytes32('0x80')).to.eventually.equal(
'0x0000000000000000000000000000000000000000000000000000000000000000',
);
// 1 leading zero is not allowed for single bytes less than 0x80, they must be encoded as themselves
await expect(this.mock.$decodeBytes32('0x820000')).to.eventually.equal(
'0x0000000000000000000000000000000000000000000000000000000000000000',
); // Non-canonical encoding with two leading zeros
// Encoding for the value
await expect(this.mock.$decodeBytes32('0x80')).to.eventually.equal(ethers.ZeroHash);
// Encoding for two zeros (and nothing else)
await expect(this.mock.$decodeBytes32('0x820000')).to.eventually.equal(ethers.ZeroHash);
});

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