Docs · 02 / 07 · How it works
Work replaces the funding transaction.
A fresh address can't move until something pays its gas. Funding it from your main wallet links the two on a public ledger. Unlinked's paymaster pays instead, for any UserOperation that carries a valid proof of work, and asks for no ETH and no signature from a known account.
The flow#
- Epoch. The SDK polls the relay's
pow_getEpochevery 60 s in the background. An epoch (15 minutes on testnet) fixes the difficulty, fee caps, gas caps and a random seed revealed only when the epoch starts. - Build. The op's gas limits come from fixed per-template profiles, its fees from the epoch, its pre-verification gas from the relay's 30-second quote. The EntryPoint nonce key is always 0. A first op carries an EIP-7702 authorization for Simple7702Account v0.9.
- Hash.
userOpHashis the EntryPoint v0.9 EIP-712 hash, which covers the 7702 delegation. - Mine and sign, in parallel. The PoW lives in the unsigned
paymasterSignaturesuffix, so the account signs the op while the miner searches for 8 nonces. - Relay. The relay recomputes the hash, verifies the proof and every on-chain rule, reserves the op's maximum cost
against the deposit, co-signs
userOpHashand forwards the op to its own bundler (or holds it for the delay you asked for). - Bundle. The bundler sends a type-4 transaction that applies the delegation and calls
handleOps. The fresh address holds 0 ETH before and after. - Validate and account.
validatePaymasterUserOpchecks the proof, the co-signature and the policy against the epoch's write-once parameters.postOprecords the cost and emitsOpSponsored.
paymasterAndData#
196 bytes in co-signed mode (the MVP default), 131 in permissionless mode. The PoW nonces and the co-signature sit in the
EntryPoint v0.9 paymasterSignature suffix, which userOpHash excludes; the signed part only carries the version and the
epoch id.
- Solid: in
userOpHash, signed by the account - Heat: the 8 proof-of-work nonces, not signed
- Hollow: the relay co-signature and its length, not signed
paymaster20 B · @0paymasterVerificationGasLimit16 B · @20paymasterPostOpGasLimit16 B · @36version = 0x011 B · @52epochId4 B · @53nonces[0..7]64 B · @57cosig65 B · @121sigLen2 B · @186PAYMASTER_SIG_MAGIC8 B · @188
paymasterAndData in co-signed mode, one cell per byte (196 bytes). The proof lives in the unsigned suffix, so the account can sign while the miner searches.| Offset | Bytes | Field | Encoding | In userOpHash |
|---|---|---|---|---|
| 0 | 20 | paymaster | address | yes |
| 20 | 16 | paymasterVerificationGasLimit | uint128 BE, must equal the epoch's pmVerificationGasLimit | yes |
| 36 | 16 | paymasterPostOpGasLimit | uint128 BE, must equal the epoch's pmPostOpGasLimit | yes |
| 52 | 1 | version = 0x01 | uint8 | yes (signed paymasterData) |
| 53 | 4 | epochId | uint32 BE | yes (signed paymasterData) |
| 57 | 64 | nonces[0..7] | 8 × uint64 BE, strictly increasing | no (paymasterSignature) |
| 121 | 65 | cosig | r ‖ s ‖ v, v ∈ {27, 28}, low-s. Co-signed mode only | no (paymasterSignature) |
| 186 | 2 | sigLen | uint16 BE: 0x0081 (129), or 0x0040 (64) permissionless | no |
| 188 | 8 | PAYMASTER_SIG_MAGIC | 0x22e325a297439656 | yes (the magic only) |
In viem terms: paymasterData = 0x01 ‖ epochId (5 bytes) and paymasterSignature = nonces ‖ cosig (129 bytes). viem
appends the length and the magic itself. Before mining, the SDK fills the suffix with 64 zero bytes and a 65-byte stub
co-signature, so the hashed length is already final.
The challenge#
challenge = keccak256(abi.encode(
CHALLENGE_TAG, // keccak256("PowPaymaster/v1/challenge")
block.chainid,
address(entryPoint),
address(this), // the paymaster
epochId, // uint32
seed, // uint96, revealed when the epoch starts
userOpHash // EntryPoint v0.9 EIP-712 hash
))
h_i = keccak256(abi.encode(challenge, uint256(nonce_i))) // one 64-byte keccak block
accept_i ⇔ uint256(h_i) ≤ targetPerSolution- Bound to one op. The chain id, EntryPoint and paymaster are in the challenge, and the EntryPoint nonce inside
userOpHashmakes each proof usable once. - No stockpiling. The seed is unknown until the epoch starts, ops expire 5 minutes after the next roll, and the relay refuses an old epoch's ops 2 minutes after a roll.
- 8 independent solutions. The 8 nonces must be strictly increasing and each must hash under the target.
- 1 · Inputs
- CHALLENGE_TAG
0x98eb…f8c4 - chainid
46630 - entryPoint
0x4337…D009 - paymaster
0x7e57…c0de - epochId
145 - seed
0x5eed…5eed - userOpHash
0x5687…693e
- CHALLENGE_TAG
- 2 · challenge = keccak256(abi.encode(…))
0xa8796c8d7c1fd9f654bcf43531fca5be700e58a419103e600708be9ab0d0358c - 3 · h = keccak256(challenge ‖ nonce) ≤ target (12 zero bits here)
- Sub-solution 1: nonce 5,788, hash 0x000244b4…717cff0d, 14 leading zero bits.
- Sub-solution 2: nonce 11,204, hash 0x0008dfed…1b735530, 12 leading zero bits.
- Sub-solution 3: nonce 15,988, hash 0x000693b4…94e71515, 13 leading zero bits.
- Sub-solution 4: nonce 22,799, hash 0x00068e5b…56f759f3, 13 leading zero bits.
- Sub-solution 5: nonce 25,042, hash 0x00045dd5…9aac2ab5, 13 leading zero bits.
- Sub-solution 6: nonce 28,550, hash 0x000902af…588de5d5, 12 leading zero bits.
- Sub-solution 7: nonce 32,848, hash 0x0007d385…bee368b7, 13 leading zero bits.
- Sub-solution 8: nonce 33,189, hash 0x000acd24…297dd343, 12 leading zero bits.
How much work#
gasSum = verificationGasLimit + callGasLimit + paymasterVerificationGasLimit
+ paymasterPostOpGasLimit + preVerificationGas
work = max(minWork, gasSum × refFeePerGas × workPerGwei / 1e9) // expected total hashes
perSolution = ceil(work / 8)
target = (2^256 − 1) / perSolution- Priced like the gas the paymaster pays.
refFeePerGasis the base fee snapshotted at the roll, so work follows fee levels, and fee headroom inmaxFeePerGascosts no extra work. - Tight limits pay off. Every client of a template uses the same published gas profile, so the work is the same for everyone.
- K = 8. With 8 sub-solutions the 95th-percentile solve time is about 1.64 × the mean (3 × with one solution), and progress can be shown honestly as x/8.
- Bounded.
work ≤ 2^48for every epoch, far inside the 64-bit nonce space.
verificationGasLimit50,000callGasLimit105,000paymasterVerificationGasLimit70,000paymasterPostOpGasLimit25,000preVerificationGas110,000
work = 360,000 × 0.01 gwei × 10,000 / 1e9 = 36,000,000 hashes ≈ 225.1per sub-solution = ⌈work / 8⌉ = 4,500,000 → 22 zero bits
A typical first op (a claim with its delegation, gasSum ≈ 360,000) at the testnet floor costs about 2^25 hashes: around 3 s on a mid laptop and 9 s on a mid Android phone. At the 7.5× ceiling, about 21 s and 67 s (the p95 is 1.64 × these means).
Validation#
Validation reads only the op's epoch (4 storage slots, written once) and, with the delegate check, the sender's 23-byte
delegation designator. It reverts only on malformed data or an unknown epoch. Every other failure is soft: all checks run,
at constant gas, and the result is SIG_VALIDATION_FAILED. The same checks are exposed as checkUserOp(), which returns
the failure bits:
| Bit | Name | Set when |
|---|---|---|
| 1 << 0 | F_FEE_LOW | maxFeePerGas is below the epoch's minFeePerGas. |
| 1 << 1 | F_FEE_HIGH | maxFeePerGas is above the epoch's fee cap. |
| 1 << 2 | F_PRIORITY | maxPriorityFeePerGas is above the epoch's priority cap. |
| 1 << 3 | F_COST | maxCost (gasSum × maxFeePerGas) is above maxOpCost. |
| 1 << 4 | F_PM_GAS | A paymaster gas limit differs from the epoch's exact value. |
| 1 << 5 | F_NONCE_ORDER | The 8 nonces are not strictly increasing. |
| 1 << 6 | F_POW | A sub-solution hash is above the per-solution target. |
| 1 << 7 | F_COSIG | The relay co-signature is missing, wrong or high-s. |
| 1 << 8 | F_GAS_CAP | verificationGasLimit, callGasLimit or preVerificationGas is above the epoch's cap. |
| 1 << 9 | F_DELEGATE | The sender is not delegated to Simple7702Account v0.9. |
Ops are limited to single calls from an allowlist of templates, enforced by the relay before it co-signs:
| Template | Target | Call | SDK helper |
|---|---|---|---|
| claim | MockRWAFaucet | claim(address token) | claimCall(faucet, token) |
| transfer | each MockStockToken | transfer(address,uint256) | transferCall(token, to, amount) |
| approve | each MockStockToken | approve(address,uint256) | approveCall(token, spender, amount) |
Epochs, difficulty and budget#
- Write-once epochs. A
roll()(any EOA, once the epoch is over) creates the next epoch. Nothing validation reads ever changes afterwards, so a simulated op can't be invalidated by paymaster state, and the bundler never has a reason to ban it. - Step controller. At each roll, utilization
uof the epoch's budget sets the next difficulty: ×2 after two saturated epochs, ×1.25 above the 60% target, ×0.5 below half of it, ×1 otherwise, clamped to the configured range (7.5× on testnet). - Budget = deposit. The EntryPoint deposit is topped up to the epoch budget at most once per epoch, by a direct EOA roll,
and an immutable
MAX_DAILY_SPENDbounds the daily outflow. The relay paces co-signing across the epoch.