Messages
GenLayer has three types of interaction, each operating at a different layer of the architecture.
Transactions (→ Intelligent Contract)
A transaction is the entry point for all IC execution. The caller signs an EVM transaction that calls addTransaction() on ConsensusMain or on the IC's ghost contract (which relays to ConsensusMain). This is submitted via eth_sendRawTransaction.
The caller can be an EOA or an EVM contract on GenLayer Chain.
gl.message.sender_address= caller addressgl.message.origin_address= caller address
The transaction enters the consensus pipeline: activation → proposal → commit → reveal → finalization.
Studio: The SDK sends the same signed addTransaction EVM transaction via eth_sendRawTransaction, but Studio intercepts and simulates the consensus pipeline rather than executing on a real EVM layer.
Internal Messages (IC → IC)
An internal message is an asynchronous call from one Intelligent Contract to another. It stays within the GenVM layer. During execution, emit() records the message as part of the execution result — the message is not sent immediately. The actual child transaction is only created when the parent transaction reaches the specified state (on='accepted' or on='finalized'). The child transaction then goes through consensus independently.
other = gl.get_contract_at(addr)
# Call a write method on another IC
other.emit(on='finalized').update_status("active")
# Call with value (recipient must be payable)
other.emit(value=u256(100), on='finalized').deposit()
# Pure value transfer (triggers __receive__ on recipient)
other.emit_transfer(value=u256(100), on='finalized')
# Deploy a child contract
addr = gl.deploy_contract(
code=contract_code,
args=[],
salt_nonce=u256(1),
on='finalized',
)In the child transaction's execution context:
gl.message.sender_address= calling contract's addressgl.message.origin_address= original caller (preserved through the chain)
See Interacting with Intelligent Contracts for full syntax reference.
External Messages (IC → Chain Layer)
An external message crosses from the GenVM layer back to the GenLayer Chain (EVM layer). This is how an IC sends value or calls an EOA or EVM contract.
External messages are executed by the IC's ghost contract via handleOp(), so the msg.sender seen by the recipient is the ghost contract address (which is the same as the IC's address).
@gl.evm.contract_interface
class ERC20:
class View:
def balance_of(self, owner: Address) -> u256: ...
class Write:
def transfer(self, to: Address, amount: u256) -> None: ...
# Read from an EVM contract (gl.message.contract_address is your own address)
balance = ERC20(token_address).view().balance_of(gl.message.contract_address)
# Write to an EVM contract (value is optional, defaults to 0)
ERC20(token_address).emit().transfer(recipient, amount)
# Send value to an EOA
@gl.evm.contract_interface
class _Recipient:
class View:
pass
class Write:
pass
_Recipient(Address(eoa_address)).emit_transfer(value=u256(amount))External messages can only be emitted on='finalized'. Using on='accepted' for external messages is not supported.
Studio: EVM contract interaction beyond value transfers to EOAs is not implemented. The @gl.evm.contract_interface calls are not functional in Studio.
Ghost Contracts
Every Intelligent Contract has a corresponding ghost contract on GenLayer Chain (the EVM layer). The ghost and IC share the same address.
What Ghost Contracts Do
- Hold the IC's GEN balance — the native balance on the ghost contract on-chain is the source of truth for
self.balance - Relay transactions —
addTransaction()on the ghost forwards to ConsensusMain - Execute external messages — on finalization,
handleOp()forwards calls to recipients somsg.senderis the IC's address - Bridge the two layers — ghost on EVM ↔ IC on GenVM, same address
Lifecycle
- A deploy transaction is submitted
- GhostFactory deploys a ghost contract at address
0xABCon GenLayer Chain - Consensus processes the deployment
- If successful: IC deploys at the same address
0xABCon GenVM - If deployment is appealed and reverted: the ghost remains but has no IC behind it — it has no purpose or effect, but may be reused by consensus if deployment succeeds later
Ghost contract existence does not guarantee the IC is deployed. A reverted deployment leaves an empty ghost.
Studio: Ghost contracts are not implemented. The IC address is used directly, and balance is stored in a database table.
Message Context (gl.message)
Every contract execution has access to gl.message:
| Field | Type | Description |
|---|---|---|
sender_address | Address | Immediate caller — EOA, EVM contract, or IC depending on how the call was initiated |
origin_address | Address | Original transaction submitter — preserved through internal message chains |
contract_address | Address | The current contract's own address |
value | u256 | GEN sent with the call (only in @gl.public.write.payable methods) |
chain_id | u256 | Current chain ID |
Timing: Accepted vs Finalized
Internal messages and contract deployments can execute at two different points:
on='finalized' (default)
The message executes after the parent transaction is fully finalized (appeal window has closed). This is the safe default.
on='accepted'
The message executes as soon as the parent transaction is accepted by initial consensus, before the appeal window closes.
Appeal risks with on='accepted':
- The re-execution may emit the message again — potentially multiple times across appeal rounds.
- If the appeal changes the outcome, the message may be "invalid" (would not have been emitted with the final outcome), but it cannot be taken back — it was already sent and executed.
- The receiving contract must be idempotent and must handle duplicate or unexpected messages gracefully.
- Contract design must account for the possibility that accepted messages may not reflect the final state.
External messages always use on='finalized' and cannot be emitted on acceptance.
Interfaces
IC Interfaces (@gl.contract_interface)
Define typed stubs for calling other Intelligent Contracts:
@gl.contract_interface
class Token:
class View:
def balance_of(self, owner: Address) -> u256: ...
class Write:
def transfer(self, to: Address, amount: u256) -> None: ...
token = Token(token_address)
balance = token.view().balance_of(user) # typed view call
token.emit(on='finalized').transfer(to, amount) # typed write callThis is purely for type safety and IDE autocompletion — at runtime it behaves identically to gl.get_contract_at(). No overhead.
EVM Interfaces (@gl.evm.contract_interface)
Define typed stubs matching Solidity function signatures:
@gl.evm.contract_interface
class IERC20:
class View:
def balance_of(self, owner: Address) -> u256: ...
def total_supply(self) -> u256: ...
class Write:
def transfer(self, to: Address, amount: u256) -> bool: ...
def approve(self, spender: Address, amount: u256) -> bool: ...Parameters are automatically ABI-encoded. Type mapping:
| GenLayer | Solidity |
|---|---|
u256, u128, u64, ... | uint256, uint128, uint64, ... |
i256, i128, i64, ... | int256, int128, int64, ... |
Address | address |
bool | bool |
str | string |
bytes | bytes |