UserOperation Signature
The signature field on a UserOperation encodes an arbitrary signature to validate that the transaction is legit.
All User Operation signatures should be a signed message of the userOpHash
. This userOpHash
is a keccak256
hash of:
- The entire User Operation (minus the signature field).
- The EntryPoint address.
- The chain ID.
To optimize gas, the EntryPoint will generate this hash once and pass it to both validateUserOp
and validatePaymasterUserOp
during the verification phase.
Example: generating a userOpHash
userOpHash
const packedData = ethers.utils.defaultAbiCoder.encode(
[
"address",
"uint256",
"bytes32",
"bytes32",
"uint256",
"uint256",
"uint256",
"uint256",
"uint256",
"bytes32",
],
[
sender,
nonce,
ethers.utils.keccak256(initCode),
ethers.utils.keccak256(callData),
callGasLimit,
verificationGasLimit,
preVerificationGas,
maxFeePerGas,
maxPriorityFeePerGas,
ethers.utils.keccak256(paymasterAndData),
]
);
const enc = ethers.utils.defaultAbiCoder.encode(
["bytes32", "address", "uint256"],
[ethers.utils.keccak256(packedData), entryPoint, chainId]
);
const userOpHash = ethers.utils.keccak256(enc);
// See the EntryPoint contract at
// https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/core/EntryPoint.sol#L267-L269
function getUserOpHash(
UserOperation calldata userOp
) public view returns (bytes32) {
return
keccak256(abi.encode(userOp.hash(), address(this), block.chainid));
}
// See the Stackup bundler code at
// https://github.com/stackup-wallet/stackup-bundler/blob/34cd6b4ebc7a8c0b6a135f947f68911fd475c33a/pkg/userop/object.go#L178-L184
func (op *UserOperation) GetUserOpHash(entryPoint common.Address, chainID *big.Int) common.Hash {
return crypto.Keccak256Hash(
crypto.Keccak256(op.PackForSignature()),
common.LeftPadBytes(entryPoint.Bytes(), 32),
common.LeftPadBytes(chainID.Bytes(), 32),
)
}
The User Operation signature
signature
ERC-4337 is agnostic to any specific signature scheme. This means users can validate with anything from ECDSA, multi-sig, or WebAuthn to name a few examples. No matter the scheme used, the end result should be a signature of the userOpHash
for maximum security. Once a signature of the userOpHash
is generated, no field can be changed without going through the signing process again.
In the case of a SimpleAccount.sol
, the signature is required to be signed by a trusted EOA owner
. However the signer
in the example below could just as well be an abstraction for a biometric device and verified according to that WebAuthn standard during validateUserOp
.
const signature = signer.signMessage(
ethers.utils.arrayify(userOpHash)
);
Updated about 1 year ago