x/uexecutor — Universal Transaction Executor
The execution layer for Push Chain's crosschain protocol. Owns the lifecycle of every UniversalTx (UTX) — from inbound observation through Push Chain execution to outbound completion — and is the only module that drives the EVM-side universal contracts (UEA factory, gateway PC, chain meta oracle).
What It Does
- Tally inbound votes from Universal Validators (UVs). Once 2/3+ vote the same observation, finalize the inbound and execute it on Push Chain (deposit funds, run the user's payload through their UEA).
- Track pending outbounds created as a side-effect of Push Chain execution, and tally UV votes on whether they were successfully broadcast on the destination chain (or have permanently failed and need a refund).
- Maintain the chain meta oracle (gas price + block height per external chain) by tallying votes from UVs and writing the result back to the EVM so contracts can read it.
- Issue derived EVM calls as the
uexecutor module account, with a manually managed nonce, so the module can deploy and call universal contracts on behalf of itself.
State (KV layout)
| Prefix |
Collection |
Type |
Purpose |
0 |
Params |
Item[Params] |
Module parameters |
2 |
PendingInbounds |
KeySet[string] |
UTX IDs of inbounds awaiting tally / execution |
3 |
UniversalTx |
Map[string, UniversalTx] |
Canonical UTX record. Key = sha256(sourceChain:txHash:logIndex) |
4 |
ModuleAccountNonce |
Item[uint64] |
Manual nonce for DerivedEVMCall from the module account |
5 |
GasPrices |
Map[string, GasPrice] |
Deprecated — replaced by ChainMetas, kept only for genesis import |
6 |
ChainMetas |
Map[string, ChainMeta] |
Aggregated gas price + block height per CAIP-2 chain |
7 |
PendingOutbounds |
Map[string, PendingOutboundEntry] |
Secondary index of outbounds in PENDING status |
The UniversalTx Record
UniversalTx (UTX) is the canonical, end-to-end record of a single crosschain transaction as it travels through Push Chain. One UTX is created per observed inbound and lives forever (it is never deleted, only mutated as new pieces of evidence arrive). It is the only object in the module that the rest of the protocol — Universal Validators, the JSON-RPC layer, indexers, the explorer — needs to read in order to know what's happening with a given crosschain action.
message UniversalTx {
string id = 1; // sha256(sourceChain:txHash:logIndex)
Inbound inbound_tx = 2; // the source-chain observation that opened this UTX
repeated PCTx pc_tx = 3; // every Push Chain execution this UTX produced
repeated OutboundTx outbound_tx = 4; // every outbound this UTX spawned (and their results)
string revert_error = 6; // non-empty if revert-outbound attachment failed
}
The UTX is intentionally append-mostly. Components are filled in over time as the protocol progresses; nothing is overwritten. Field 5 is reserved (a removed UniversalTxStatus enum field — see below for why status is computed instead of stored).
The Three Components
1. Inbound — the source-chain observation
Filled in once, when the inbound vote is finalized. After that, it is read-only.
message Inbound {
string source_chain = 1; // CAIP-2, e.g. "eip155:11155111"
string tx_hash = 2; // unique source-chain tx hash
string sender = 3; // source-chain sender address
string recipient = 4; // destination address on Push Chain (UEA or contract)
string amount = 5; // bridged amount (synthetic token, uint256 as string)
string asset_addr = 6; // source-chain ERC20 / native token address
string log_index = 7; // log index that emitted this inbound (uniqueness within tx)
TxType tx_type = 8; // see TxType table below
UniversalPayload universal_payload = 9; // the user's intent (decoded from raw_payload)
string verification_data = 10; // bytes the UEA uses to authenticate the payload
RevertInstructions revert_instructions = 11; // where funds go on revert
bool isCEA = 12; // recipient is a contract (CEA) instead of a UEA
string raw_payload = 13; // hex-encoded raw event bytes (decoded by core validator)
}
2. PCTx — Push Chain execution
A list, because a single inbound can spawn multiple Push Chain executions (the deposit tx, the payload-execution tx, and possibly a revert tx all live as separate PCTx entries on the same UTX).
message PCTx {
string tx_hash = 1; // hash of the EVM tx the core validator produced (DerivedEVMCall)
string sender = 2; // who initiated it (user-derived address, or uexecutor module)
uint64 gas_used = 3; // populated from the tx receipt
uint64 block_height = 4; // Push Chain block this was committed in
string status = 6; // "SUCCESS" or "FAILED"
string error_msg = 7; // populated when status == "FAILED"
}
These hashes correspond to real EVM transactions you can fetch from eth_getTransactionByHash — see DERIVED_TRANSACTIONS.md for why module-originated calls produce real receipts.
3. OutboundTx — outbounds spawned by Push Chain execution
A list, because one inbound's payload can fan out into multiple destination-chain transactions (e.g. a multi-hop cross-chain swap or a batched refund).
message OutboundTx {
string destination_chain = 1; // CAIP-2 of the destination
string recipient = 2;
string amount = 3;
string external_asset_addr = 4;
string prc20_asset_addr = 5;
string sender = 6;
string payload = 7;
string gas_limit = 8;
TxType tx_type = 9;
OriginatingPcTx pc_tx = 10; // which PCTx (and log) created this outbound
OutboundObservation observed_tx = 11; // populated once UVs vote the destination-chain result
string id = 12; // deterministic outbound ID
Status outbound_status = 13; // PENDING -> OBSERVED | REVERTED | ABORTED
RevertInstructions revert_instructions = 14;
PCTx pc_revert_execution = 15; // PC tx that ran the revert path (nil if not reverted)
string gas_price = 16; // destination-chain gas price snapshot
string gas_fee = 17; // amount paid to relayer
PCTx pc_refund_execution = 18; // PC tx that ran the unused-gas refund (nil if no refund)
string refund_swap_error = 19; // non-empty if the swap-refund leg failed
string gas_token = 20; // PRC20 used to pay relayer
string abort_reason = 21; // human-readable reason if outbound was aborted
}
OutboundObservation is what UVs vote in via MsgVoteOutbound:
message OutboundObservation {
bool success = 1;
uint64 block_height = 2;
string tx_hash = 3;
string error_msg = 4;
string gas_fee_used = 5; // actual gas spent on destination — used to compute refund
}
TxType — what flavour of crosschain action
The same enum is used on both Inbound and OutboundTx to describe what the message is for.
TxType |
Inbound semantics |
Outbound semantics |
GAS |
User pre-paid gas on the source chain. Mints PC to the recipient as a gas top-up. |
Refund of unused gas back to a source chain. |
GAS_AND_PAYLOAD |
Gas top-up + executes a payload through the recipient's UEA in the same Push Chain tx. |
Same combo on the destination side. |
FUNDS |
Pure synthetic transfer — mints PRC20 representation of an external token. |
Pure transfer of a PRC20 back out of Push Chain. |
FUNDS_AND_PAYLOAD |
Mints funds + runs a payload (e.g. deposit + DEX swap atomically). |
Funds delivery with a destination-side call. |
PAYLOAD |
Pure payload execution, no value movement. |
Pure call on the destination chain. |
INBOUND_REVERT |
Reverts a previously-executed inbound (returns funds to the source-chain sender). |
— |
RESCUE_FUNDS |
Admin-driven rescue path for stuck funds. |
Outbound that delivers the rescue. |
Status is derived from component state, not stored
The current UniversalTx record has no status field at all. Field 5 is reserved precisely because the old UniversalTxStatus enum field was removed in favour of computing status on the fly from the underlying components. This avoids the staleness class of bugs where a stored status gets out of sync with the actual outbounds/PC txs after a partial update.
Instead, callers ask "what's the state of this UTX?" by inspecting:
- whether
OutboundTx[] is non-empty, and the per-entry outbound_status (PENDING / OBSERVED / REVERTED / ABORTED)
- whether
PcTx[] is non-empty, and each entry's status string ("SUCCESS" / "FAILED")
- whether
InboundTx is set
The priority for any rollup view is outbounds > PC txs > inbound presence: as soon as an outbound exists, the UTX is "in the outbound phase" regardless of how the PC txs went; before that, PC tx state dominates; before that, the UTX is just a recorded inbound waiting to be executed.
Note on UniversalTxStatus (legacy enum). The UniversalTxStatus proto enum (PENDING_INBOUND_EXECUTION, PC_EXECUTED_SUCCESS, OUTBOUND_PENDING, ...) is only used by the legacy query response shape UniversalTxLegacy. The v1 GetUniversalTx query converts the current record into UniversalTxLegacy and synthesises the status field via computeUniversalStatus in keeper/query_server.go purely for client backward compatibility. Anything new built against x/uexecutor should consume the live components on UniversalTx directly and compute the status it cares about, instead of depending on the legacy enum.
Status — per-outbound status
OutboundTx.outbound_status uses a separate, narrower enum:
Status |
Meaning |
PENDING |
Outbound created on Push Chain, waiting for UVs to broadcast and vote |
OBSERVED |
UVs voted the outbound was successfully broadcast on the destination chain |
REVERTED |
UVs voted the outbound permanently failed; revert path triggered |
ABORTED |
Finalization or revert attachment failed and requires manual intervention |
Lifecycle Walkthrough
A typical FUNDS_AND_PAYLOAD inbound, end to end:
1. UV observes a source-chain gateway event.
2. UV submits MsgVoteInbound. The UTX is created the moment the first vote
arrives, with id = sha256(sourceChain:txHash:logIndex). Only the
InboundTx field is populated; PcTx and OutboundTx are empty.
(UTX id is also added to PendingInbounds.)
3. Threshold of UV votes reached. The keeper executes the inbound:
a. Mints the PRC20 to the recipient's UEA address.
A new PCTx (deposit) is appended to UTX.PcTx.
b. Runs the universal payload through the UEA.
A second PCTx (executeUniversalTx) is appended.
(UTX id removed from PendingInbounds.)
4. The payload triggered a destination-chain call (e.g. release funds on
another chain). An OutboundTx is created with Status_PENDING and
appended to UTX.OutboundTx. It is also indexed in PendingOutbounds.
5. UVs sign the outbound via TSS, broadcast it, and vote the result back
via MsgVoteOutbound. The OutboundTx.observed_tx is filled in and
outbound_status flips to OBSERVED. The PendingOutbounds entry is
removed.
6. If the destination chain refunds excess gas, a refund PCTx runs on
Push Chain. PCTx.pc_refund_execution is set on the OutboundTx. The
refund is just additional evidence attached to the existing OutboundTx.
At every step the UTX is mutated append-only: new entries are added to pc_tx and outbound_tx, existing entries are updated in place, and the live state of those slices is the only source of truth for "what's happening" with this UTX.
Messages (MsgServer)
| Message |
Authority |
Gasless? |
Purpose |
MsgVoteInbound |
bonded UV |
yes |
Vote an observed source-chain inbound |
MsgVoteOutbound |
bonded UV |
yes |
Vote that an outbound was broadcast (or failed) on the destination chain |
MsgVoteChainMeta |
bonded UV |
yes |
Vote on observed gas price + block height for a chain |
MsgExecutePayload |
any |
yes |
Execute a payload on a UEA (the UEA itself authenticates via verificationData) |
MsgUpdateParams |
gov |
no |
Update module params |
UEA migration is now part of payload execution. There used to be a separate MsgMigrateUEA message; that path has been removed. UEAs are upgraded by submitting a normal MsgExecutePayload whose payload calls the UEA's migration entry point on the EVM side. The Cosmos layer no longer has a dedicated migration message — the UEA contract is the source of truth for who is allowed to migrate it and to what implementation.
Vote messages check IsBondedUniversalValidator and IsTombstonedUniversalValidator on x/uvalidator before accepting the vote. Tombstoned validators are silently rejected.
Queries
Params
GetUniversalTx — fetch a single UTX by ID. The v1 endpoint returns the legacy UniversalTxLegacy shape (with a synthesised UniversalTxStatus for backward compatibility); the v2 endpoint returns the live UniversalTx directly.
- v2 query server (
query_server_v2.go) provides additional iterators over UTX state
See keeper/query_server.go and keeper/query_server_v2.go for the full surface.
Inter-module Dependencies
The keeper holds references to:
evmKeeper — for DerivedEVMCall (deploy contracts, mint, refund, push chain meta)
feemarketKeeper — for current Push Chain gas price
bankKeeper — for native transfers
accountKeeper — for the uexecutor module account
uregistryKeeper — to look up chain configs and token configs
uvalidatorKeeper — to gate votes on bonded/tombstoned status, and to drive the generic ballot machine
It does not export any hooks; other modules call into it (not the other way around).
EVM Integration
x/uexecutor is unusual in that it issues EVM calls as a Cosmos module. On fresh genesis (Exported=false) it deploys the UEA factory contract. Thereafter, every inbound execution, refund, swap quote, and chain-meta update flows through DerivedEVMCall with the manually tracked ModuleAccountNonce so successive calls in the same block don't collide.
Re-deploying the factory on genesis import is explicitly skipped — see keeper.go:155-159 — because that would overwrite live EVM state and shift the deterministic addresses of every UEA on chain.
Genesis
GenesisState {
Params params
repeated string pending_inbounds
repeated UTXEntry universal_txs
uint64 module_account_nonce
repeated GasPrice gas_prices // legacy
repeated ChainMeta chain_metas
repeated Outbound pending_outbounds
bool exported // skip factory deploy if true
}
Block Lifecycle
x/uexecutor does not implement a BeginBlocker or EndBlocker — the module is listed in the manager's order arrays as a placeholder, but all real work happens synchronously in the message handlers. Vote tallying, inbound execution, outbound creation, and chain-meta updates are all triggered by incoming Msg* calls.
Layout
x/uexecutor/
|-- keeper/
| |-- keeper.go State + dependencies
| |-- msg_server.go MsgVoteInbound, MsgVoteOutbound, MsgVoteChainMeta, ExecutePayload
| |-- query_server.go v1 queries
| |-- query_server_v2.go v2 queries
| +-- ... inbound execution, outbound creation, chain meta, derived EVM calls
|-- types/
| |-- types.pb.go UniversalTx, Inbound, ChainMeta, PendingOutboundEntry, enums
| |-- params.go Params (currently a single placeholder field)
| |-- keys.go Store prefixes + ID generators
| |-- abi.go, decode_payload.go, gateway_pc_event_decode.go, caip2.go
| +-- expected_keepers.go Interfaces for evm/feemarket/bank/account/uregistry/uvalidator
|-- migrations/ v2, v4, v5 — params shape, UTX restructure, GasPrices -> ChainMetas
|-- module.go AppModule wiring
|-- autocli.go CLI auto-registration
+-- depinject.go Dependency injection