Overview
Core Lane’s intent system enables asynchronous execution and cross-chain coordination through a marketplace model where users express desired outcomes (intents) and solvers compete to fulfill them.
What are Intents?
An intent is a user’s declaration of a desired outcome with locked value, without specifying the execution path. The system manages the lifecycle from submission to fulfillment.
Intent Types
Core Lane supports multiple intent types (src/intents.rs:42-47):
pub enum IntentType {
AnchorBitcoinFill = 1 , // Bitcoin-to-Core Lane bridge
RiscVProgram = 2 , // RISC-V program execution with custom logic
}
AnchorBitcoinFill Cross-chain fills: User requests Bitcoin payment, solver delivers BTC for locked Core Lane tokens
RiscVProgram Programmable intents: Custom verification logic running in Cartesi VM
Intent Structure
The Intent struct tracks intent state (src/intents.rs:411-423):
pub struct Intent {
pub data : Bytes , // CBOR-encoded intent data
pub value : u64 , // Locked value in wei (currently u64, max ~18.4 ETH)
pub status : IntentStatus , // Current lifecycle state
pub last_command : IntentCommandType , // Last operation performed
pub creator : Address , // Intent creator's address
}
Intent Status
Intents transition through states (src/intents.rs:386-396):
pub enum IntentStatus {
Submitted , // Intent created, awaiting solver
Locked ( Address ), // Solver has locked for solving
Solved , // Successfully fulfilled
Cancelled , // Cancelled by creator
}
Intent System Interface
The intent system is accessed via a special contract address (src/intents.rs:12-29):
interface IntentSystem {
// Blob storage (for large data)
function storeBlob ( bytes data , uint256 expiryTime ) payable ;
function prolongBlob ( bytes32 blobHash ) payable ;
function blobStored ( bytes32 blobHash ) view returns ( bool );
// Intent creation
function intent ( bytes intentData , uint256 nonce ) payable returns ( bytes32 intentId );
function intentFromBlob ( bytes32 blobHash , uint256 nonce , bytes extraData ) payable returns ( bytes32 );
function createIntentAndLock ( bytes eip712sig , bytes lockData ) returns ( bytes32 intentId );
// Intent lifecycle
function cancelIntent ( bytes32 intentId , bytes data ) payable ;
function lockIntentForSolving ( bytes32 intentId , bytes data ) payable ;
function solveIntent ( bytes32 intentId , bytes data ) payable ;
function cancelIntentLock ( bytes32 intentId , bytes data ) payable ;
// Query functions
function isIntentSolved ( bytes32 intentId ) view returns ( bool );
function intentLocker ( bytes32 intentId ) view returns ( address );
function valueStoredInIntent ( bytes32 intentId ) view returns ( uint256 );
}
Accessed at: 0x0000000000000000000000000000000000000045 (src/transaction.rs:122-128)
Intent Creation
Method 1: Direct Intent Submission
Submit intent with inline data (src/transaction.rs:599-653):
IntentCall :: Intent { intent_data , .. } => {
// Check balance
if bundle_state . get_balance ( state . state_manager (), sender ) < value {
return Err ( "Insufficient balance" );
}
// Lock value
bundle_state . sub_balance ( state . state_manager (), sender , value ) ? ;
bundle_state . increment_nonce ( state . state_manager (), sender ) ? ;
// Calculate intent ID
let intent_id = calculate_intent_id ( sender , nonce , Bytes :: from ( intent_data . clone ()));
// Create intent
bundle_state . insert_intent (
intent_id ,
Intent {
data : Bytes :: from ( intent_data ),
value : value_u64 ,
status : IntentStatus :: Submitted ,
last_command : IntentCommandType :: Created ,
creator : sender ,
},
);
}
Intent ID Calculation (prevents collisions):
fn calculate_intent_id ( creator : Address , nonce : u64 , data : Bytes ) -> B256 {
keccak256 ( abi . encode ( creator , nonce , data ))
}
Method 2: Intent from Blob
For large intent data, store in blob first (src/transaction.rs:480-519):
// Step 1: Store blob
IntentCall :: StoreBlob { data , .. } => {
let blob_hash = keccak256 ( & data );
bundle_state . insert_blob ( blob_hash , data . clone ());
bundle_state . increment_nonce ( state . state_manager (), sender ) ? ;
}
// Step 2: Create intent from blob
IntentCall :: IntentFromBlob { blob_hash , extra_data , .. } => {
if ! bundle_state . contains_blob ( state . state_manager (), & blob_hash ) {
return Err ( "Blob not stored" );
}
// Intent ID includes blob hash
let mut preimage = Vec :: new ();
preimage . extend_from_slice ( blob_hash . as_slice ());
preimage . extend_from_slice ( & extra_data );
let intent_id = calculate_intent_id ( sender , nonce , Bytes :: from ( preimage ));
bundle_state . insert_intent (
intent_id ,
Intent {
data : Bytes :: from ( extra_data ), // Only extra_data stored
value : value_u64 ,
status : IntentStatus :: Submitted ,
last_command : IntentCommandType :: Created ,
creator : sender ,
},
);
}
Blob storage is useful for RISC-V programs where the program binary is large but the execution parameters are small.
Method 3: EIP-712 Signed Intent
Create and lock in one transaction using a signature (src/transaction.rs:323-478):
IntentCall :: CreateIntentAndLock { eip712sig , lock_data } => {
// Parse signed payload (CBOR format)
let signed_payload : SignedPayload = ciborium :: de :: from_reader ( lock_data . as_slice ()) ? ;
// Verify EIP-712 signature
let domain = Eip712Domain {
name : Some ( Cow :: Borrowed ( "CoreLaneIntent" )),
version : Some ( Cow :: Borrowed ( "1" )),
chain_id : Some ( U256 :: from ( 1281453634 u64 )),
verifying_contract : Some ( CoreLaneAddresses :: exit_marketplace ()),
salt : None ,
};
let digest = eip712_digest ( & domain , & signed_payload . intent,
signed_payload . nonce, signed_payload . value);
let signer = recover_signer_from_digest ( & digest , & eip712sig ) ? ;
// Validate nonce and balance
let expected_nonce = bundle_state . get_nonce ( state . state_manager (), signer );
if expected_nonce != signed_payload . nonce {
return Err ( "Invalid nonce" );
}
if bundle_state . get_balance ( state . state_manager (), signer ) < signed_payload . value {
return Err ( "Insufficient balance" );
}
// Lock value and create intent in Locked state
bundle_state . sub_balance ( state . state_manager (), signer , signed_payload . value) ? ;
let intent_id = calculate_intent_id ( signer , nonce_u64 , Bytes :: from ( signed_payload . intent . clone ()));
bundle_state . insert_intent (
intent_id ,
Intent {
data : Bytes :: from ( signed_payload . intent),
value : value_u64 ,
status : IntentStatus :: Locked ( signer ), // Already locked by creator
last_command : IntentCommandType :: LockIntentForSolving ,
creator : signer ,
},
);
}
EIP-712 signed intents allow gasless submission - someone else can submit the transaction on behalf of the signer.
Intent Lifecycle Operations
Locking for Solving
Solvers lock intents to prevent competition (src/transaction.rs:673-792):
IntentCall :: LockIntentForSolving { intent_id , .. } => {
let intent = bundle_state . get_intent ( state . state_manager (), & intent_id )
. ok_or ( "Intent not found" ) ? ;
match intent . status {
IntentStatus :: Submitted => {
// Lock for solving
if let Some ( intent ) = bundle_state . get_intent_mut ( state . state_manager (), & intent_id ) {
intent . status = IntentStatus :: Locked ( sender );
intent . last_command = IntentCommandType :: LockIntentForSolving ;
}
bundle_state . increment_nonce ( state . state_manager (), sender ) ? ;
}
IntentStatus :: Locked ( _ ) => return Err ( "Already locked" ),
IntentStatus :: Solved => return Err ( "Already solved" ),
IntentStatus :: Cancelled => return Err ( "Cancelled" ),
}
}
Solving Intents
Solvers provide proof of fulfillment:
AnchorBitcoinFill
Requires Bitcoin L1 verification (src/transaction.rs:849-928):
IntentType :: AnchorBitcoinFill => {
// data contains: block_height (8 bytes) + txid (32 bytes)
let block_number = u64 :: from_le_bytes ( data [ .. 8 ]);
let txid_bytes : [ u8 ; 32 ] = data [ 8 .. 40 ];
// Verify Bitcoin transaction exists and pays to correct address
if verify_intent_fill_on_bitcoin ( state , intent_id , block_number , txid_bytes ) ? {
// Release intent value to solver
bundle_state . add_balance ( state . state_manager (), sender , U256 :: from ( intent_value )) ? ;
// Mark as solved
if let Some ( intent ) = bundle_state . get_intent_mut ( state . state_manager (), & intent_id ) {
intent . status = IntentStatus :: Solved ;
intent . last_command = IntentCommandType :: SolveIntent ;
}
bundle_state . increment_nonce ( state . state_manager (), sender ) ? ;
} else {
return Err ( "L1 fill not found in block" );
}
}
Bitcoin verification uses the Bitcoin RPC client to fetch blocks and scan for matching payment transactions.
RiscVProgram
Executes program for custom verification (src/transaction.rs:930-987):
IntentType :: RiscVProgram => {
// Release value to solver
bundle_state . add_balance ( state . state_manager (), sender , U256 :: from ( intent_value )) ? ;
// Mark as solved
if let Some ( intent ) = bundle_state . get_intent_mut ( state . state_manager (), & intent_id ) {
intent . status = IntentStatus :: Solved ;
intent . last_command = IntentCommandType :: SolveIntent ;
}
bundle_state . increment_nonce ( state . state_manager (), sender ) ? ;
// Run RISC-V program for permission check (if Cartesi enabled)
#[cfg(feature = "cartesi-runner" )]
{
let permission = check_riscv_intent_permission (
bundle_state , state , & cbor_intent , intent_id
) ? ;
if permission == 1 {
return Err ( "Permission denied by RISC-V program" );
}
}
}
RISC-V programs can reject solve attempts even after state changes, so permission checks happen last.
Cancelling Intents
Creators can cancel unsolved intents (src/transaction.rs:990-1088):
IntentCall :: CancelIntent { intent_id , .. } => {
let intent = bundle_state . get_intent ( state . state_manager (), & intent_id )
. ok_or ( "Intent not found" ) ? ;
// Verify caller is creator
if intent . creator != sender {
return Err ( "Not creator" );
}
// Cannot cancel solved intents
if matches! ( intent . status, IntentStatus :: Solved ) {
return Err ( "Already solved" );
}
// For RiscVProgram intents, run permission check
#[cfg(feature = "cartesi-runner" )]
if intent_type == IntentType :: RiscVProgram {
let permission = check_riscv_intent_permission ( bundle_state , state , & intent_data , intent_id ) ? ;
if permission == 1 {
return Err ( "Permission denied by RISC-V program" );
}
}
// Refund value to creator
bundle_state . add_balance ( state . state_manager (), intent . creator, U256 :: from ( intent . value)) ? ;
// Mark as cancelled
if let Some ( intent_mut ) = bundle_state . get_intent_mut ( state . state_manager (), & intent_id ) {
intent_mut . status = IntentStatus :: Cancelled ;
intent_mut . last_command = IntentCommandType :: CancelIntent ;
}
bundle_state . increment_nonce ( state . state_manager (), sender ) ? ;
}
Unlocking Intents
Solvers can release locks if unable to solve (src/transaction.rs:1090-1157):
IntentCall :: CancelIntentLock { intent_id , .. } => {
let intent = bundle_state . get_intent ( state . state_manager (), & intent_id )
. ok_or ( "Intent not found" ) ? ;
match intent . status {
IntentStatus :: Locked ( locker ) => {
if locker != sender {
return Err ( "Not current locker" );
}
// For RiscVProgram, run permission check
#[cfg(feature = "cartesi-runner" )]
if intent_type == IntentType :: RiscVProgram {
let permission = check_riscv_intent_permission ( bundle_state , state , & intent_data , intent_id ) ? ;
if permission == 1 {
return Err ( "Permission denied by RISC-V program" );
}
}
// Unlock intent
if let Some ( intent_mut ) = bundle_state . get_intent_mut ( state . state_manager (), & intent_id ) {
intent_mut . status = IntentStatus :: Submitted ;
intent_mut . last_command = IntentCommandType :: CancelIntentLock ;
}
bundle_state . increment_nonce ( state . state_manager (), sender ) ? ;
}
_ => return Err ( "Not locked" ),
}
}
AnchorBitcoinFill
CBOR-encoded structure (src/intents.rs:87-92):
pub struct AnchorBitcoinFill {
pub bitcoin_address : Vec < u8 >, // Bitcoin address (UTF-8 string bytes)
pub amount : U256 , // Requested BTC amount in sats
pub max_fee : U256 , // Maximum acceptable fee
pub expire_by : u64 , // Expiration timestamp
}
Creation helper (src/intents.rs:163-177):
pub fn create_anchor_bitcoin_fill_intent (
bitcoin_address : & str ,
amount : U256 ,
max_fee : U256 ,
expire_by : u64 ,
) -> Result < IntentData > {
let fill_data = AnchorBitcoinFill :: from_bitcoin_address (
bitcoin_address , amount , max_fee , expire_by
) ? ;
let fill_cbor = fill_data . to_cbor () ? ;
Ok ( IntentData {
intent_type : IntentType :: AnchorBitcoinFill ,
data : fill_cbor ,
})
}
RiscVProgramIntent
CBOR-encoded structure (src/intents.rs:94-98):
pub struct RiscVProgramIntent {
pub blob_hash : [ u8 ; 32 ], // Hash of stored RISC-V program
pub extra_data : Vec < u8 >, // Program execution parameters
}
The blob_hash references a program stored via storeBlob(). The extra_data contains execution-specific parameters.
Intent Queries
Read-only queries for intent state (src/transaction.rs:655-671):
IntentCall :: IsIntentSolved { intent_id } => {
let solved = match bundle_state . get_intent ( state . state_manager (), & intent_id ) {
Some ( intent ) => matches! ( intent . status, IntentStatus :: Solved ),
None => false ,
};
// Return as 32-byte boolean (0 or 1 in last byte)
let mut ret = vec! [ 0 u8 ; 32 ];
if solved {
ret [ 31 ] = 1 ;
}
return Ok ( ExecutionResult {
success : true ,
output : Bytes :: from ( ret ),
...
});
}
Similar implementations exist for intentLocker() and valueStoredInIntent().
State Storage
Intents are stored in both state managers:
StateManager - Persistent intent storage (src/state.rs:91)
BundleStateManager - Temporary intent changes during bundle execution (src/state.rs:102)
Intent Retrieval
Bundle state provides copy-on-write semantics (src/state.rs:140-168):
pub fn get_intent_mut (
& mut self ,
original : & StateManager ,
intent_id : & B256 ,
) -> Option < & mut Intent > {
// Ensure intent exists in bundle before getting mutable reference
if ! self . intents . contains_key ( intent_id ) {
if let Some ( orig_intent ) = original . get_intent ( intent_id ) {
self . intents . insert ( * intent_id , orig_intent . clone ());
} else {
return None ;
}
}
self . intents . get_mut ( intent_id )
}
Security Considerations
Value Limits
Intent values are currently limited to u64::MAX (~18.4 ETH in wei). Values exceeding this are rejected (src/transaction.rs:418-430, 553-567, 613-625). Future versions may support U256 for arbitrary values.
Nonce Validation
Intent creation increments nonce to prevent replay:
bundle_state . increment_nonce ( state . state_manager (), sender ) ? ;
Permission Checks
RISC-V programs can enforce custom permissions on lock/solve/cancel operations, preventing unauthorized state changes even by intent creators.
Example: Bitcoin Bridge Intent
// User wants to receive 0.001 BTC
let intent_data = create_anchor_bitcoin_fill_intent (
"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh" , // User's BTC address
U256 :: from ( 100_000 ), // 100,000 sats = 0.001 BTC
U256 :: from ( 1000 ), // Max 1000 sat fee
1735689600 , // Expires Jan 1, 2025
) ? ;
// User submits intent with 0.001 laneBTC locked
// (0.001 BTC * 10^8 sats/BTC * 10^10 wei/sat = 10^15 wei)
let call_data = IntentSystem :: intentCall :: new (
intent_data . to_cbor () ? ,
U256 :: from ( 0 ), // nonce (fetched from state)
) . abi_encode ();
let tx = /* EIP-1559 transaction to 0x...0045 with value=10^15 wei */ ;
// Solver sees intent, sends 0.001 BTC to user's address
// Solver then calls solveIntent with Bitcoin txid proof
let solve_data = [ block_height_bytes , txid_bytes ] . concat ();
let solve_call = IntentSystem :: solveIntentCall :: new ( intent_id , solve_data ) . abi_encode ();
// If verified, solver receives 10^15 wei locked in intent
Next Steps
Transaction Processing See how intent calls are executed
State Management Learn how intents are stored