-
Notifications
You must be signed in to change notification settings - Fork 71
Add native merkle sigs to the Account #388
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ import {LibNonce} from "./libraries/LibNonce.sol"; | |
| import {TokenTransferLib} from "./libraries/TokenTransferLib.sol"; | ||
| import {LibTStack} from "./libraries/LibTStack.sol"; | ||
| import {IIthacaAccount} from "./interfaces/IIthacaAccount.sol"; | ||
| import {MerkleProofLib} from "solady/utils/MerkleProofLib.sol"; | ||
|
|
||
| /// @title Account | ||
| /// @notice A account contract for EOAs with EIP7702. | ||
|
|
@@ -490,9 +491,43 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { | |
| return isMultichain ? _hashTypedDataSansChainId(structHash) : _hashTypedData(structHash); | ||
| } | ||
|
|
||
| /// @dev Verifies the merkle sig | ||
| /// - Note: Each leaf of the merkle tree should be a standard digest. | ||
| /// - The signature for using merkle verification is encoded as: | ||
| /// - bytes signature = abi.encode(bytes32[] proof, bytes32 root, bytes rootSig) | ||
| function _verifyMerkleSig(bytes32 digest, bytes calldata signature) | ||
| internal | ||
| view | ||
| returns (bool isValid, bytes32 keyHash) | ||
| { | ||
| bytes32[] calldata proof; | ||
| bytes32 root; | ||
| bytes calldata rootSig; | ||
|
|
||
| assembly ("memory-safe") { | ||
| let proofOffset := add(signature.offset, calldataload(signature.offset)) | ||
| proof.length := calldataload(proofOffset) | ||
| proof.offset := add(proofOffset, 0x20) | ||
|
|
||
| root := calldataload(add(signature.offset, 0x20)) | ||
|
|
||
| let rootSigOffset := add(signature.offset, calldataload(add(signature.offset, 0x40))) | ||
| rootSig.length := calldataload(rootSigOffset) | ||
| rootSig.offset := add(rootSigOffset, 0x20) | ||
| } | ||
|
|
||
| if (MerkleProofLib.verifyCalldata(proof, root, digest)) { | ||
| (isValid, keyHash) = unwrapAndValidateSignature(root, rootSig); | ||
|
|
||
| return (isValid, keyHash); | ||
| } | ||
|
|
||
| return (false, bytes32(0)); | ||
| } | ||
|
|
||
| /// @dev Returns if the signature is valid, along with its `keyHash`. | ||
| /// The `signature` is a wrapped signature, given by | ||
| /// `abi.encodePacked(bytes(innerSignature), bytes32(keyHash), bool(prehash))`. | ||
| /// `abi.encode(bytes(innerSignature), bytes32(keyHash), bool(prehash), bool(merkle))`. | ||
| function unwrapAndValidateSignature(bytes32 digest, bytes calldata signature) | ||
| public | ||
| view | ||
|
|
@@ -507,14 +542,20 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { | |
| return (ECDSA.recoverCalldata(digest, signature) == address(this), 0); | ||
| } | ||
|
|
||
| bool merkle; | ||
| unchecked { | ||
| uint256 n = signature.length - 0x21; | ||
| uint256 n = signature.length - 0x22; | ||
| keyHash = LibBytes.loadCalldata(signature, n); | ||
| signature = LibBytes.truncatedCalldata(signature, n); | ||
| // Do the prehash if last byte is non-zero. | ||
| if (uint256(LibBytes.loadCalldata(signature, n + 1)) & 0xff != 0) { | ||
| digest = EfficientHashLib.sha2(digest); // `sha256(abi.encode(digest))`. | ||
| } | ||
| merkle = uint256(LibBytes.loadCalldata(signature, n + 2)) & 0xff != 0; | ||
| } | ||
|
|
||
| if (merkle) { | ||
| return _verifyMerkleSig(digest, signature); | ||
|
Contributor
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. looks like we're skipping some checks by short circuiting here like key expiry are we checking expiry anywhere else? otherwise this might allow an expired session key to execute valid transactions
Contributor
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. oh nvm, i just saw that
Contributor
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. hmm, we use the user input and would there be issues here? e.g. could a user pass in the owner keyHash in the sig, but use a session key to validate the merkle sig?
Contributor
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. also, i see that we truncate the sig before calling
why do we not require two instances of
Collaborator
Author
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. The outer keyhash is ignored right now, because we do a recurisve call into the
Contributor
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.
Contributor
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. Actually, on second review, the outer keyhash is not used at all, there shouldn’t be a security issue, just a redundancy In the first call frame for unwrapAndValidate, it parses out the outer keyHash from the sig but they’re not used, since it enters the if(merkle) branch and returns after The second call frame does all the checks using the inner keyHash. That’s the keyhash that’s returned here, and is bubbled up into the top level call frame and out If we move the isMerkle check and the merkle = loadCalldata to right after calculating n, we would only need to duplicate the isMerkle bool and not the keyhash/prehash
Collaborator
Author
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. The outer keyhash is used for the
Collaborator
Author
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. We could implement your suggestion of directly picking the inner keyhash, if we detect an So everyone would basically have to do an Will think about it some more.
Collaborator
Author
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. Actually, we only ignore the |
||
| } | ||
|
|
||
| Key memory key = getKey(keyHash); | ||
|
|
@@ -755,6 +796,6 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { | |
| returns (string memory name, string memory version) | ||
| { | ||
| name = "IthacaAccount"; | ||
| version = "0.5.10"; | ||
| version = "0.5.11"; | ||
| } | ||
| } | ||
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.
encodewould result in this:[0-0x20] - sig offset, likely pointing to 0x60
[0x20-0x40] - keyhash
[0x40-0x60] - bool
[0x60-...] sig
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.
in tests we're doing
abi.encodePackedso we just need to update this comment