This repository has been archived by the owner on Nov 5, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 46
Pseudo floats #519
Merged
Merged
Pseudo floats #519
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
|
@@ -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[] = []; | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
|
||
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))]); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
.Nah this is new code.
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).