on Robinhood Chain · testnet

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#

  1. Epoch. The SDK polls the relay's pow_getEpoch every 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.
  2. 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.
  3. Hash. userOpHash is the EntryPoint v0.9 EIP-712 hash, which covers the 7702 delegation.
  4. Mine and sign, in parallel. The PoW lives in the unsigned paymasterSignature suffix, so the account signs the op while the miner searches for 8 nonces.
  5. 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 userOpHash and forwards the op to its own bundler (or holds it for the delay you asked for).
  6. Bundle. The bundler sends a type-4 transaction that applies the delegation and calls handleOps. The fresh address holds 0 ETH before and after.
  7. Validate and account. validatePaymasterUserOp checks the proof, the co-signature and the policy against the epoch's write-once parameters. postOp records the cost and emits OpSponsored.

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
  1. paymaster20 B · @0
  2. paymasterVerificationGasLimit16 B · @20
  3. paymasterPostOpGasLimit16 B · @36
  4. version = 0x011 B · @52
  5. epochId4 B · @53
  6. nonces[0..7]64 B · @57
  7. cosig65 B · @121
  8. sigLen2 B · @186
  9. PAYMASTER_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.
paymasterAndData layout (co-signed mode, 196 bytes)
OffsetBytesFieldEncodingIn userOpHash
020paymasteraddressyes
2016paymasterVerificationGasLimituint128 BE, must equal the epoch's pmVerificationGasLimityes
3616paymasterPostOpGasLimituint128 BE, must equal the epoch's pmPostOpGasLimityes
521version = 0x01uint8yes (signed paymasterData)
534epochIduint32 BEyes (signed paymasterData)
5764nonces[0..7]8 × uint64 BE, strictly increasingno (paymasterSignature)
12165cosigr ‖ s ‖ v, v ∈ {27, 28}, low-s. Co-signed mode onlyno (paymasterSignature)
1862sigLenuint16 BE: 0x0081 (129), or 0x0040 (64) permissionlessno
1888PAYMASTER_SIG_MAGIC0x22e325a297439656yes (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#

Solidity
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 userOpHash makes 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. 1 · Inputs
    • CHALLENGE_TAG0x98eb…f8c4
    • chainid46630
    • entryPoint0x4337…D009
    • paymaster0x7e57…c0de
    • epochId145
    • seed0x5eed…5eed
    • userOpHash0x5687…693e
  2. 2 · challenge = keccak256(abi.encode(…))0xa8796c8d7c1fd9f654bcf43531fca5be700e58a419103e600708be9ab0d0358c
  3. 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.
A worked proof with real values (example inputs, not a deployment): the challenge binds the chain, the EntryPoint, the paymaster, the epoch's seed and the op, then 8 strictly increasing nonces each hash under the target. White-hot digits are the leading zeros that make a hash valid. Real ops need about 22 zero bits each.

How much work#

text
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. refFeePerGas is the base fee snapshotted at the roll, so work follows fee levels, and fee headroom in maxFeePerGas costs 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^48 for every epoch, far inside the 64-bit nonce space.
gasSum = 360,000 · 1 cell = 5,000 gas
  • verificationGasLimit50,000
  • callGasLimit105,000
  • paymasterVerificationGasLimit70,000
  • paymasterPostOpGasLimit25,000
  • preVerificationGas110,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

Target · 22 bits8 sub-solutions · 1 cell = 1 bit
A standard first op (a claim with its EIP-7702 delegation) at the testnet floor: 360,000 gas priced at 0.01 gwei and 10,000 hashes per gwei is 36,000,000 expected hashes, split into 8 sub-solutions of about 22 leading zero bits each.

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:

Soft-failure bits (failMask)
BitNameSet when
1 << 0F_FEE_LOWmaxFeePerGas is below the epoch's minFeePerGas.
1 << 1F_FEE_HIGHmaxFeePerGas is above the epoch's fee cap.
1 << 2F_PRIORITYmaxPriorityFeePerGas is above the epoch's priority cap.
1 << 3F_COSTmaxCost (gasSum × maxFeePerGas) is above maxOpCost.
1 << 4F_PM_GASA paymaster gas limit differs from the epoch's exact value.
1 << 5F_NONCE_ORDERThe 8 nonces are not strictly increasing.
1 << 6F_POWA sub-solution hash is above the per-solution target.
1 << 7F_COSIGThe relay co-signature is missing, wrong or high-s.
1 << 8F_GAS_CAPverificationGasLimit, callGasLimit or preVerificationGas is above the epoch's cap.
1 << 9F_DELEGATEThe 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:

Sponsored call templates (testnet demo allowlist)
TemplateTargetCallSDK helper
claimMockRWAFaucetclaim(address token)claimCall(faucet, token)
transfereach MockStockTokentransfer(address,uint256)transferCall(token, to, amount)
approveeach MockStockTokenapprove(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 u of 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_SPEND bounds the daily outflow. The relay paces co-signing across the epoch.