Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BytesParsing lib #24

Merged
merged 3 commits into from
Jan 10, 2024
Merged

Add BytesParsing lib #24

merged 3 commits into from
Jan 10, 2024

Conversation

nik-suri
Copy link
Collaborator

@nik-suri nik-suri commented Jan 5, 2024

No description provided.

@nik-suri nik-suri requested a review from nonergodic January 5, 2024 21:39
uint offset
) internal pure returns (bool, uint) {
(uint8 ret, uint nextOffset) = asUint8(encoded, offset);
return (ret != 0, nextOffset);
Copy link
Contributor

Choose a reason for hiding this comment

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

can we check that ret == 0 || ret == 1 here? or equivalently ret == ret & 1 which might be marginally cheaper

Copy link

Choose a reason for hiding this comment

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

Wouldn't ret == ret & 1 return true for all odd numbers though, so it only half does what you're looking for. Agree though that it would be nice to enforce 0/1, good call

Copy link
Contributor

Choose a reason for hiding this comment

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

hmm, I think it does what we want 🤔 are you thinking of ret | 1?

Copy link

Choose a reason for hiding this comment

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

Unless I'm being stupid I think that's right since it's a bitwise AND. If a is always 1, this little Remix test return true for b = 1,3,5,etc

pragma solidity 0.8.0;

contract Test {
    function testAnd(uint8 a, uint8 b) public pure returns (bool) {
        uint8 x = a & b;
        return (x != 0);
    }
}

Copy link

@djb15 djb15 Jan 6, 2024

Choose a reason for hiding this comment

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

Ohhh I see what you're saying now. You're suggesting we return ret == ret & 1 and forget about the !=0 part? That would work although then anything >1 returns false, but wouldn't we rather it reverted instead?

Copy link
Collaborator

Choose a reason for hiding this comment

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

assert is the wrong choice since it should indicate situations that should never happen (implementation errors) and consumes all remaining gas. So it should be either a custom error or require.

Copy link
Collaborator

@nonergodic nonergodic Jan 8, 2024

Choose a reason for hiding this comment

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

I propose the following implementation:

function asBoolUnchecked(
  bytes memory encoded,
  uint offset
) internal pure returns (bool, uint) {
  (uint8 val, uint nextOffset) = asUint8(encoded, offset);
  if (val & 0xfe != 0)
    revert InvalidBoolVal(val);
   
  bool ret;
  assembly ("memory-safe") {
    ret := val
  }
  return (ret, nextOffset);
}

this should translates to:

  • 1 PUSH for the 0xfe constant
  • 1 AND for &
  • 2 ISZERO for != comparison
  • 1 JUMPI

and the assembly section skips the 2 ISZERO opcodes that the solidity compiler would otherwise throw in there to cleanly convert a uint8 into a bool

Copy link
Collaborator

Choose a reason for hiding this comment

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

exactly, though thinking about it more, assert(ret == 0 || ret == 1) is a lot more readable and costs marginally more gas 😅

I think in this part of the code we should strongly prioritize gas efficiency over readability. solc will turn the || here into another conditional jump because of short-circuiting which makes the entire thing a lot less gas and code efficient.

Copy link
Collaborator

Choose a reason for hiding this comment

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

fixing my sloppy oversight of using asUint8 instead of asUint8Unchecked here

Copy link
Collaborator

@nonergodic nonergodic Jan 11, 2024

Choose a reason for hiding this comment

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

And as it turns out the inline assembly is not safe because solc is actually no cleaning up the upper 31 bytes of ret (as I expected it would), so there's no way to avoid the cost of the two ISZERO opcodes after all.

Also fixed in the above PR.

My bad 😿

Copy link
Collaborator

@nonergodic nonergodic left a comment

Choose a reason for hiding this comment

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

lgtm!

@nik-suri nik-suri requested a review from djb15 January 5, 2024 23:58
@nik-suri nik-suri merged commit 374a016 into main Jan 10, 2024
1 check passed
@nik-suri nik-suri deleted the bytes-parsing branch January 10, 2024 19:33
kcsongor added a commit that referenced this pull request Jan 15, 2024
this was added to the libraries folder in #24, so the one in testing can
be deleted now
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.

5 participants