Skip to main content
The Local Prover handles same-chain intent fulfillment where both intent creation and execution occur on the same blockchain. Unlike cross-chain provers, it requires no message passing infrastructure.

Architecture

Same Chain
━━━━━━━━━━
Portal (creates intent)

Portal (fulfills intent)

LocalProver (reads claimant immediately)

Purpose

Local Prover enables intents to be fulfilled on their origin chain:
  • No cross-chain messaging required
  • Zero additional fees beyond gas
  • Instant proof availability
  • Useful for same-chain swaps, transfers, or operations

Configuration

constructor(address inbox)
Parameters:
  • inbox: Portal contract address (provides Inbox functionality)
Initialization:
  • Stores Portal reference as immutable
  • Records chain ID for proof data validation
  • Validates chain ID fits in uint64

How It Works

Immediate Proof Creation

Unlike cross-chain provers, Local Prover doesn’t create proofs—it reads them:
  1. Intent fulfilled on Portal → claimant stored in Portal.claimants mapping
  2. provenIntents() queries Portal’s claimant mapping directly
  3. Returns proof data with current chain ID and claimant address

Proof Lookup

function provenIntents(bytes32 intentHash) 
    public view returns (ProofData memory)
Returns:
  • claimant: Address that fulfilled the intent (converted from bytes32)
  • destination: Current chain ID
If not fulfilled:
  • Returns ProofData(address(0), 0)

No-Op Functions

prove()

function prove(
    address sender,
    uint64 sourceChainId,
    bytes calldata encodedProofs,
    bytes calldata data
) external payable
Intentionally empty - no action needed because:
  • Claimant already stored by Portal during fulfillment
  • No cross-chain message to send
  • Proof is immediately available
Can be called without reverting to support fulfillAndProve() pattern.

challengeIntentProof()

function challengeIntentProof(
    uint64 destination,
    bytes32 routeHash,
    bytes32 rewardHash
) external pure
Intentionally empty - challenges not applicable because:
  • Same-chain intents cannot be proven on wrong chain
  • No cross-chain discrepancy possible

Usage Patterns

Creating Same-Chain Intent

Intent memory intent = Intent({
    destination: block.chainid,  // Same as current chain
    route: Route({
        portal: address(portal),
        // ... other route params
    }),
    reward: Reward({
        prover: address(localProver),  // Use LocalProver
        // ... other reward params
    })
});

portal.publishAndFund{value: amount}(intent, false);

Fulfilling Same-Chain Intent

// Fulfill without proving
portal.fulfill{value: route.nativeAmount}(
    intentHash,
    route,
    rewardHash,
    claimant
);

// OR fulfill and prove (prove is no-op but doesn't revert)
portal.fulfillAndProve{value: route.nativeAmount}(
    intentHash,
    route,
    rewardHash,
    claimant,
    address(localProver),
    uint64(block.chainid),
    "" // Empty data
);
Both patterns work identically—prove() is a no-op.

Claiming Rewards

// Check if intent is proven (fulfilled)
IProver.ProofData memory proof = localProver.provenIntents(intentHash);
require(proof.claimant != address(0), "Not fulfilled");

// Withdraw rewards
portal.withdraw(
    uint64(block.chainid),
    routeHash,
    reward
);

Fee Structure

Zero additional fees:
  • No cross-chain messaging costs
  • No bridge fees
  • Only standard gas costs for fulfillment and withdrawal

When to Use Local Prover

Ideal Use Cases

Same-Chain Operations:
  • Token swaps on same chain
  • Transfers between accounts
  • DeFi operations on single chain
  • Testing and development
Advantages:
  • Instant settlement (no message delays)
  • Zero bridging costs
  • Simpler integration
  • No external dependencies

Not Suitable For

  • Cross-chain transfers
  • Multi-chain operations
  • Bridging assets between chains
For cross-chain intents, use:
  • HyperProver (Hyperlane)
  • LayerZeroProver (LayerZero)
  • MetaProver (Metalayer)

Integration Example

// Deploy Local Prover
LocalProver localProver = new LocalProver(address(portal));

// Create same-chain intent
Intent memory intent = Intent({
    destination: uint64(block.chainid),
    route: Route({
        salt: keccak256(abi.encode(user, nonce)),
        deadline: block.timestamp + 1 hours,
        portal: address(portal),
        nativeAmount: 0,
        tokens: tokenAmounts,
        calls: calls
    }),
    reward: Reward({
        creator: msg.sender,
        prover: address(localProver),
        deadline: block.timestamp + 1 hours,
        nativeAmount: 0.01 ether,
        tokens: rewardTokens
    })
});

// Publish and fund
portal.publishAndFund{value: 0.01 ether}(intent, false);

// Solver fulfills
portal.fulfill{value: route.nativeAmount}(
    intentHash,
    route,
    rewardHash,
    bytes32(uint256(uint160(solverAddress)))
);

// Solver claims rewards immediately
portal.withdraw(
    uint64(block.chainid),
    routeHash,
    reward
);

Comparison with Cross-Chain Provers

FeatureLocalProverCross-Chain Provers
Proof AvailabilityImmediateAfter message delivery
Additional FeesNone$5-50 per message
Message DelaysNoneSeconds to minutes
InfrastructureNone requiredBridge dependencies
ComplexityMinimalDomain IDs, hooks, etc.
Use CaseSame chain onlyCross-chain operations

Best Practices

Intent Creation

Set destination correctly:
destination: uint64(block.chainid)  // Must match current chain
Use Local Prover:
prover: address(localProver)  // Not a cross-chain prover

Fulfillment

Either pattern works:
// Simple: fulfill only
portal.fulfill(intentHash, route, rewardHash, claimant);

// With prove (no-op): works with fulfillAndProve
portal.fulfillAndProve(
    intentHash, route, rewardHash, claimant,
    address(localProver), uint64(block.chainid), ""
);

Validation

Check destination matches:
require(
    intent.destination == block.chainid,
    "Not same-chain intent"
);
Verify prover is LocalProver:
require(
    intent.reward.prover == address(localProver),
    "Wrong prover"
);

Error Handling

  • ChainIdTooLarge(uint256): Chain ID exceeds uint64.max during construction
Note: LocalProver functions (prove, challengeIntentProof) never revert—they are intentionally no-ops.

Technical Notes

Why prove() is Empty

The prove() function is empty because:
  1. Portal stores claimant in claimants mapping during fulfillment
  2. provenIntents() queries this mapping directly
  3. No cross-chain message needed
  4. Proof is instantly available

Why No Fees

No fees because:
  • No cross-chain message to send
  • No bridge infrastructure to pay
  • No external services required
  • Only standard EVM gas costs

Claimant Storage

// Portal stores during fulfill()
claimants[intentHash] = claimant;

// LocalProver reads it
bytes32 claimant = _PORTAL.claimants(intentHash);
Direct storage access eliminates need for proving.

Constant

function getProofType() external pure returns (string memory) {
    return "Same chain";
}
Identifies this prover handles same-chain intents.
I