Skip to content
This repository has been archived by the owner on Nov 5, 2023. It is now read-only.

Pseudo floats #519

Merged
merged 5 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 17 additions & 16 deletions contracts/contracts/FallbackExpander.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

import "./lib/VLQ.sol";
import "./lib/PseudoFloat.sol";
import "./interfaces/IExpander.sol";
import "./interfaces/IWallet.sol";

Expand All @@ -22,25 +23,25 @@ import "./interfaces/IWallet.sol";
* 24f1fc8a1f7256dc2914e524966309df2226fd329373aaaae1881bf5cd0c62f4 // BLS key
*
* 00 // nonce: 0
* 868d20 // gas: 100,000
* 3100 // gas: 100,000
* 02 // two actions
*
* // Action 1
* 95ecd98ed5f38000 // ethValue: 12300000000000000 (0.0123 ETH)
* 7b0f // ethValue: 12300000000000000 (0.0123 ETH)
* 70997970c51812dc3a010c7d01b50e0d17dc79c8 // contractAddress
* 00 // encodedFunction: (empty)
*
* // Action 2
* 82dd9fbdf38000 // ethValue: 12000000000000 (0.000012 ETH)
* 6c01 // ethValue: 12000000000000 (0.000012 ETH)
* 4bd2e4e99b50a2a9e6b9dabfa3c8dcd1f885f008 // contractAddress (AggUtils)
* 04 // 4 bytes for encodedFunction
* 1dfea6a0 // sendEthToTxOrigin
*
* The proposal doc for the new expander lists the same example ("Example of an
* Expanded User Operation" https://hackmd.io/0q7H3Ad0Su-I4RWWK8wQPA) using the
* solidity ABI, which uses 608 bytes. Here we've encoded the same thing (plus
* gas) in 194 bytes, which is (about) 70% smaller. (If you account for the
* zero-byte discount, the saving is still over 30%.)
* gas) in 182 bytes, which is 70% smaller. (If you account for the zero-byte
* discount, the saving is still over 30%.)
*/
contract FallbackExpander is IExpander {
function expand(bytes calldata stream) external pure returns (
Expand All @@ -49,30 +50,30 @@ contract FallbackExpander is IExpander {
uint256 bytesRead
) {
uint256 originalStreamLen = stream.length;
uint256 vlqValue;
uint256 decodedValue;

senderPublicKey = abi.decode(stream[:128], (uint256[4]));
stream = stream[128:];

(vlqValue, stream) = VLQ.decode(stream);
operation.nonce = vlqValue;
(decodedValue, stream) = VLQ.decode(stream);
operation.nonce = decodedValue;

(vlqValue, stream) = VLQ.decode(stream);
operation.gas = vlqValue;
(decodedValue, stream) = PseudoFloat.decode(stream);
operation.gas = decodedValue;

(vlqValue, stream) = VLQ.decode(stream);
operation.actions = new IWallet.ActionData[](vlqValue);
(decodedValue, stream) = VLQ.decode(stream);
operation.actions = new IWallet.ActionData[](decodedValue);

for (uint256 i = 0; i < operation.actions.length; i++) {
uint256 ethValue;
(ethValue, stream) = VLQ.decode(stream);
(ethValue, stream) = PseudoFloat.decode(stream);

address contractAddress = address(bytes20(stream[:20]));
stream = stream[20:];

(vlqValue, stream) = VLQ.decode(stream);
bytes memory encodedFunction = stream[:vlqValue];
stream = stream[vlqValue:];
(decodedValue, stream) = VLQ.decode(stream);
bytes memory encodedFunction = stream[:decodedValue];
stream = stream[decodedValue:];

operation.actions[i] = IWallet.ActionData({
ethValue: ethValue,
Expand Down
117 changes: 117 additions & 0 deletions contracts/contracts/lib/PseudoFloat.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking if this was the original version range (VLQ.sol too)? Was the code audited at a particular compiler version? We should ensure the project is configured to compile this lib with it (like the BLS lib PR comment).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been just copying that version range, I think it originates with BLSExpander.sol.

Was the code audited at a particular compiler version?

Nah this is new code.

We should ensure the project is configured to compile this lib with it (like the BLS lib PR comment).

I'm not sure what you mean by this. I believe that hardhat will compile it with whatever latest matching solidity version is available in the project, which is currently 0.8.15 (hardhat.config.ts has 0.6.12, 0.7.6, 0.8.15, so 0.8.15 is the latest matching version).

pragma abicoder v2;

import "./VLQ.sol";

/**
* Like a float, but technically for integers. Also base 10.
*
* The pseudo-float is an encoding that can represent any uint256 value but
* efficiently represents values with a small number of significant figures
* (just 2 bytes for 3 significant figures).
*
* Zero is a special case, it's just 0x00.
*
* Otherwise, start with the value in scientific notation:
*
* 1.23 * 10^16 (e.g. 0.0123 ETH)
*
* Make the mantissa (1.23) a whole number by adjusting the exponent:
*
* 123 * 10^14
*
* We add 1 to the exponent and encode it in 5 bits:
*
* 01111 (=15)
*
* Note: The maximum value we can encode here is 31 (11111). This means the
* maximum exponent is 30. Adjust the left side of the previous equation if
* needed.
*
* Encode the left side in binary:
*
* 1111011 (=123)
*
* Our first byte is the 5-bit exponent followed by the three lowest bits of the
* mantissa:
*
* 01111011
* ^^^^^-------- 15 => exponent is 14
* ^^^----- lowest 3 bits of the mantissa
*
* Encode the remaining bits of the mantissa as a VLQ:
*
* 00001111
* ^------------ special VLQ bit, zero indicates this is the last byte
* ^^^^^^^----- bits to use, put them together with 011 above to get
* 0001111011, which is 123.
*
* Putting it together is two bytes:
*
* 0x7b0f
*
* Example 2:
*
* 0.883887085 ETH uses 5 bytes: 0x55b4d7c27d
* 883887085 * 10^9
* For exponent 9 we encode 10 as 5 bits: 01010
* 883887085 is 110100101011110000101111101(101)
*
* 01010101 10110100 11010111 11000010 01111101
* ^^^^^------------------------------------------- 10 => exponent is 9
* ^^^---------------------------------------- lowest 3 bits
* ^^^^^^^--^^^^^^^--^^^^^^^--^^^^^^^ higher bits
* ^--------^--------^-------------------- 1 => not the last byte
* ^----------- 0 => the last byte
*
* Note that the *encode* process is described above for explanatory purposes.
* On-chain we need to *decode* to recover the value from the encoded binary
* instead.
*/
library PseudoFloat {
function decode(
bytes calldata stream
) internal pure returns (uint256, bytes calldata) {
uint8 firstByte = uint8(stream[0]);

if (firstByte == 0) {
return (0, stream[1:]);
}

uint8 exponent = ((firstByte & 0xf8) >> 3) - 1;

uint256 value;
(value, stream) = VLQ.decode(stream[1:]);

value <<= 3;
value += firstByte & 0x07;

// TODO (merge-ok): Exponentiation by squaring might be better here.
// Counterpoints:
// - The gas used is pretty low anyway
// - For these low exponents (typically ~15), the benefit is unclear
for (uint256 i = 0; i < exponent; i++) {
value *= 10;
}

return (value, stream);
}

/**
* Same as decode, but public.
*
* This is here because when a library function that is not internal
* requires linking when used in other contracts. This avoids including a
* copy of that function in the contract but it's complexity that we don't
* want right now.
*
* What we do want though, is a public version so that we can call it
* statically for testing.
*/
function decodePublic(
bytes calldata stream
) public pure returns (uint256, bytes calldata) {
return decode(stream);
}
}
2 changes: 1 addition & 1 deletion contracts/contracts/lib/VLQ.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

Expand Down
30 changes: 27 additions & 3 deletions contracts/shared/helpers/bundleCompression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ export function compressAsFallback(
);

result.push(encodeVLQ(operation.nonce));
result.push(encodeVLQ(operation.gas));
result.push(encodePseudoFloat(operation.gas));

result.push(encodeVLQ(operation.actions.length));

for (const action of operation.actions) {
result.push(encodeVLQ(action.ethValue));
result.push(encodePseudoFloat(action.ethValue));
result.push(action.contractAddress);

const fnHex = ethers.utils.hexlify(action.encodedFunction);
Expand All @@ -56,7 +56,7 @@ function remove0x(hexString: string) {
return hexString.slice(2);
}

function encodeVLQ(x: BigNumberish) {
export function encodeVLQ(x: BigNumberish) {
x = BigNumber.from(x);

const segments: number[] = [];
Expand All @@ -83,3 +83,27 @@ function encodeVLQ(x: BigNumberish) {

return result;
}

export function encodePseudoFloat(x: BigNumberish) {
x = BigNumber.from(x);

if (x.eq(0)) {
return "0x00";
}

let exponent = 0;

while (x.mod(10).eq(0) && exponent < 30) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this stop at 30? (2^256 = 10^77-ish) Is it from their code?

EDIT: I noticed that an example show's 5 bits being used for the exponent (2^5 = 32), so it looks to be by design. Most whole ethers and gas values are 10^9 to begin, so another 21 on top of that is a lot of headroom for values being compressed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's to make sure the exponent fits into a small space.

If you go above that range, you can still encode it using the VLQ part:

console.log(encodePseudoFloat(ethers.constants.MaxUint256));
// 0x0f81ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f

x = x.div(10);
exponent++;
}

const exponentBits = (exponent + 1).toString(2).padStart(5, "0");
const lowest3Bits = x.mod(8).toNumber().toString(2).padStart(3, "0");

const firstByte = parseInt(`${exponentBits}${lowest3Bits}`, 2)
.toString(16)
.padStart(2, "0");

return hexJoin([`0x${firstByte}`, encodeVLQ(x.div(8))]);
}
Loading