From 1923cdacfd9a426f6ade9a08fcf41ead95573fb7 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Tue, 7 Apr 2026 22:30:31 +0200 Subject: [PATCH 1/2] feat: updated READMEs of core validator and specific modules --- DERIVED_TRANSACTIONS.md | 212 ++++++++++++++++++++++ app/README.md | 254 ++++++++++++++++++++++++++ precompiles/usigverifier/README.md | 122 +++++++++++-- readme.md | 20 ++- x/uexecutor/README.md | 277 ++++++++++++++++++++++++++++- x/uregistry/README.md | 115 +++++++++++- x/utss/README.md | 110 +++++++++++- x/uvalidator/README.md | 197 +++++++++++++++++++- 8 files changed, 1253 insertions(+), 54 deletions(-) create mode 100644 DERIVED_TRANSACTIONS.md create mode 100644 app/README.md diff --git a/DERIVED_TRANSACTIONS.md b/DERIVED_TRANSACTIONS.md new file mode 100644 index 00000000..73ffdb2c --- /dev/null +++ b/DERIVED_TRANSACTIONS.md @@ -0,0 +1,212 @@ +# Derived Transactions + +A primitive added in Push Chain's EVM fork ([`github.com/pushchain/evm`](https://github.com/pushchain/evm), pinned via `replace` in `go.mod`) that lets a Cosmos SDK module produce a **real EVM transaction** — one that has a real receipt, real logs, and is fully observable through the JSON-RPC layer — instead of an internal "module call" that exists only inside the SDK. + +The new EVM keeper method is `DerivedEVMCall`. Everywhere in the Push Chain codebase that needs to act on the EVM as a Cosmos module (mint PRC20s, write chain-meta, deploy a UEA, refund gas, ...) goes through this single entry point. + +## Why It Exists + +Stock cosmos-evm exposes `EVMKeeper.CallEVM`: + +```go +func (k Keeper) CallEVM( + ctx sdk.Context, + abi abi.ABI, + from, contract common.Address, + commit bool, + method string, + args ...interface{}, +) (*types.MsgEthereumTxResponse, error) +``` + +`CallEVM` is built for **internal queries**: a Cosmos module wants to read state from a contract or trigger a side effect, and the EVM layer treats it as a synthetic call. It's enough for read paths and lightweight writes, but it has hard limitations the moment a module needs to behave like a first-class EVM sender: + +| Need | `CallEVM` | +|---|---| +| Send native value (`msg.value`) | not supported (always 0) | +| Set an explicit `gasLimit` | not supported | +| Bypass gas accounting for module-initiated work | not supported | +| Act as a module account (no private key) sending a real EVM tx | not supported | +| Issue multiple calls in the same block from the same sender without nonce collisions | not supported (nonce is read from state on every call) | +| Produce a JSON-RPC-visible receipt with hash, gas used, and logs | partial — the call exists, but doesn't surface as a normal EVM tx | + +`DerivedEVMCall` is the fork's answer to all six. + +## The API + +```go +DerivedEVMCall( + ctx sdk.Context, + abi abi.ABI, + from, contract common.Address, + value, gasLimit *big.Int, + commit, gasless, isModuleSender bool, + manualNonce *uint64, + method string, + args ...interface{}, +) (*types.MsgEthereumTxResponse, error) +``` + +Defined on the Push Chain `EVMKeeper` interface in [`x/uexecutor/types/expected_keepers.go`](./x/uexecutor/types/expected_keepers.go). + +| Parameter | Purpose | +|---|---| +| `ctx` | SDK context — provides block, gas meter, store access | +| `abi` | Parsed contract ABI for encoding the call | +| `from` | The EVM address that will appear as the tx sender. Can be a derived user address or a module account address. | +| `contract` | Destination contract | +| `value` | Native value to attach (`*big.Int`, may be `nil` or `big.NewInt(0)`) | +| `gasLimit` | Explicit gas limit (`nil` -> use a sensible default). Critical for predictable receipts. | +| `commit` | `true` = real state-changing tx; `false` = simulation / static call | +| `gasless` | `true` = skip gas accounting entirely. Used when the call is initiated by the protocol itself and shouldn't bill any user. | +| `isModuleSender` | `true` = `from` is a Cosmos module account (no private key). The fork's signer logic uses a deterministic synthetic signature instead of requiring a real ECDSA signature. | +| `manualNonce` | If non-`nil`, the caller supplies the nonce explicitly. This is what makes "many EVM calls in one block from the same module" deterministic — see [Manual Nonce Management](#manual-nonce-management). | +| `method` + `args` | Standard ABI-encoded call data | + +The return type is `*evmtypes.MsgEthereumTxResponse`, the same type a normal `MsgEthereumTx` produces. Concretely: + +```go +receipt, err := k.evmKeeper.DerivedEVMCall(...) +// receipt.Hash -- 0x... tx hash, queryable via eth_getTransactionByHash +// receipt.GasUsed -- real gas used, observable in receipts +// receipt.Logs -- real EVM logs, indexable by event subscribers +// receipt.Ret -- ABI-encoded return data (for view-style commits) +``` + +## When to Use Each Mode + +The Push Chain codebase uses two distinct call patterns. Both are visible in [`x/uexecutor/keeper/evm.go`](./x/uexecutor/keeper/evm.go). + +### 1. User-derived sender (UEA-routed user actions) + +When a user submits a `MsgExecutePayload` or `MsgMigrateUEA`, the Cosmos signer is converted to its derived EVM address and the EVM call is issued from that address. The UEA contract is what authenticates the request via `verificationData`. + +```go +return k.evmKeeper.DerivedEVMCall( + ctx, + abi, + evmFromAddress, // user's derived EVM address + ueaAddr, + big.NewInt(0), + gasLimit, + true, // commit + false, // gasless = false (real user tx, gas should appear in receipt) + false, // isModuleSender = false + nil, // manualNonce = nil (read from state like a normal user) + "executeUniversalTx", + abiUniversalPayload, + verificationData, +) +``` + +Why not `CallEVM`? Two reasons: +- Real receipts. Universal Validators, indexers, and the JSON-RPC layer all need to see the tx as a normal Ethereum tx so they can observe gas used, status, and emitted events. +- Explicit `gasLimit`. The payload's gas budget must be enforceable; `CallEVM` doesn't accept one. + +### 2. Module-as-sender (protocol-initiated EVM work) + +When `x/uexecutor` itself needs to issue an EVM call (deposit PRC20s, push chain-meta, refund unused gas, ...) the sender is the `uexecutor` module account. Module accounts don't have private keys, so this would be impossible via a normal `MsgEthereumTx` — you can't sign one. `DerivedEVMCall` with `isModuleSender=true` solves it: + +```go +ueModuleAccAddress, _ := k.GetUeModuleAddress(ctx) +nonce, _ := k.GetModuleAccountNonce(ctx) +_, _ = k.IncrementModuleAccountNonce(ctx) + +return k.evmKeeper.DerivedEVMCall( + ctx, + abi, + ueModuleAccAddress, // module account as sender + handlerAddr, + big.NewInt(0), + nil, + true, // commit + false, // gasless = false (we still want gas in the receipt) + true, // isModuleSender = true + &nonce, // manualNonce = explicit + "depositPRC20Token", + prc20Address, amount, to, +) +``` + +The fork is responsible for synthesising a deterministic "signature" for the module account so the tx can be properly receipted and indexed without ever needing a real key to exist. + +## Manual Nonce Management + +Stock cosmos-evm reads the sender's nonce from EVM state on every call. That's fine for users (one user = one tx in flight at a time, the mempool serializes the rest), but it breaks for module accounts that may need to issue **several** EVM calls within the same block: + +``` +BeginBlock + uexecutor.handleInbound1 + -> CallPRC20Deposit (nonce = ?) + -> CallUniversalCoreRefundUnusedGas (nonce = ?) + uexecutor.handleInbound2 + -> CallPRC20DepositAutoSwap (nonce = ?) +EndBlock +``` + +If the keeper read the nonce from state for each of these, every call within the same block would see the same starting nonce — and they'd all collide. The fork's solution is the `manualNonce *uint64` argument: the caller passes its own counter, the fork honours it, and is responsible for incrementing it before the next call. + +`x/uexecutor` keeps that counter in its own KV store as the `ModuleAccountNonce` collection ([`x/uexecutor/keeper/keeper.go`](./x/uexecutor/keeper/keeper.go)): + +```go +nonce, err := k.GetModuleAccountNonce(ctx) // read +if _, err := k.IncrementModuleAccountNonce(ctx); err != nil { + return nil, err +} +// pass &nonce to DerivedEVMCall +``` + +The increment happens **before** the call, intentionally — if the EVM call fails, the nonce gap is benign (skipped nonces are fine in EVM), but a post-call increment would risk reusing a nonce on retry. This pre-increment is the canonical way to issue derived txs from a module. + +> ⚠️ **Single source of truth.** Only one collection in the whole codebase should ever increment `ModuleAccountNonce`. If two modules need to send derived txs as the same module account, they must coordinate through a single keeper helper. The current design has only `x/uexecutor` doing this, so the invariant holds trivially. + +## The `gasless` Flag + +`gasless=true` tells the fork: "this call is part of internal protocol bookkeeping, don't bill any account for the gas." Right now, every Push Chain call site passes `gasless=false`, with the inline comment: + +> `// gasless = false (@dev: we need gas to be emitted in the tx receipt)` + +The reason: even though the protocol pays the gas, the tx receipt still needs `gas_used` populated so off-chain services (Universal Validators, explorers, the gas-fee accounting in `x/uexecutor`) can read it back. Setting `gasless=true` would suppress the gas field and break that read path. + +The flag exists for future use — protocol housekeeping calls that don't need to be observable via receipts (e.g. genesis-time bytecode patches). For day-to-day inbound/outbound execution, `gasless` stays `false`. + +## Where It's Used + +Every derived call in Push Chain is in [`x/uexecutor/keeper/evm.go`](./x/uexecutor/keeper/evm.go). Quick map: + +| Helper | Sender | Why derived? | +|---|---|---| +| `CallFactoryToDeployUEA` | user-derived | Real tx receipt is required for the deploy; the deployer address is the source-chain user's derived EVM address. | +| `CallUEAExecutePayload` | user-derived | Carries `gasLimit` from the payload; receipt is consumed by the Universal Validator vote-back path. | +| `CallUEAMigrateUEA` | user-derived | Same — needs a real receipt. | +| `CallPRC20Deposit` | module | Mints PRC20 to recipient. Module account has no key. | +| `CallPRC20DepositAutoSwap` | module | Same, but with the auto-swap leg. | +| `CallUniversalCoreSetGasPrice` | module | Writes a single chain's gas price to the on-chain oracle. | +| `CallUniversalCoreSetChainMeta` | module | Writes gas price + block height for a chain. | +| `CallUniversalCoreRefundUnusedGas` | module | Refunds unused gas (with optional swap back to PC). | +| `CallExecuteUniversalTx` | module | Calls `executeUniversalTx` on a recipient smart contract for `isCEA` inbounds. | + +The pure read paths in the same file (`CallFactoryToGetUEAAddressForOrigin`, `CallFactoryGetOriginForUEA`, `CallUEADomainSeparator`, `GetGasPriceByChain`, `GetUniversalCoreQuoterAddress`, `GetUniversalCoreWPCAddress`, `GetDefaultFeeTierForToken`, `GetSwapQuote`) all use plain `CallEVM` with `commit=false` — they don't need a receipt because they're static. + +## Quick Reference: `CallEVM` vs `DerivedEVMCall` + +``` + CallEVM DerivedEVMCall + ------- --------------- +value 0 (implicit) explicit *big.Int +gasLimit default explicit *big.Int (or nil) +commit yes yes +gasless no (always charges) flag (default: false in PC) +isModuleSender no flag (true = synthetic signer) +manualNonce no (read from state) optional override +JSON-RPC visible receipt partial yes — same as a user MsgEthereumTx +typical use internal queries, protocol-as-sender writes, + lightweight side effects user-derived EVM-routed actions +``` + +## Caveats + +- **`isModuleSender=true` requires the synthetic signer logic in the fork.** If the upstream cosmos-evm version is bumped, that signer path must remain intact, otherwise module-originated derived calls will fail validation. +- **`manualNonce` is the caller's responsibility.** The fork trusts the supplied value verbatim. Two callers stomping each other's nonce will cause receipt collisions and confusing replays. +- **Pre-increment, never post-increment.** If you increment after the call and the call panics or errors mid-execution, you've now reused a nonce. Always increment first; treat skipped nonces as a non-issue (EVM allows nonce gaps for module accounts since no transaction sequencing depends on them). +- **`gasless=true` suppresses the gas field in the receipt.** Until there's a clear reason to drop receipts on the floor for a particular call site, leave it `false`. diff --git a/app/README.md b/app/README.md new file mode 100644 index 00000000..b99f6cca --- /dev/null +++ b/app/README.md @@ -0,0 +1,254 @@ +# Core Validator + +Push Chain's L1 node binary (`pchaind`). Four custom Cosmos SDK modules and one custom EVM precompile turn the chain into the universal-execution layer that coordinates inbounds, outbounds, and TSS-signed crosschain transactions. + +- **Produces** blocks via CometBFT consensus and runs the EVM execution engine for both standard and universal traffic +- **Coordinates** the crosschain protocol — collects votes from Universal Validators on inbounds/outbounds/chain meta, finalizes ballots, drives TSS keygen and fund migration, and rewards UV operators with a boosted fee share +- **Hosts** Universal Executor Accounts (UEAs) and the chain-meta oracle on its EVM, giving any source-chain user a deterministic Push Chain identity and predictable gas pricing across networks + +## Architecture + +``` +app/ +|-- app.go ChainApp wiring (4 custom modules + usigverifier) +|-- precompiles.go Baseline EVM precompile registration (bech32, p256, staking, ...) +|-- ante/ Custom AnteHandler chain (gasless support) +| |-- ante.go Routes Ethereum vs Cosmos txs by extension option +| |-- ante_cosmos.go Cosmos decorator chain +| |-- ante_evm.go EVM mono-decorator wrapper +| |-- fee.go Custom DeductFeeDecorator (skips fee for gasless txs) +| +-- account_init_decorator.go Creates accounts mid-pipeline for first-time gasless signers +|-- cosmos/ +| +-- min_gas_price.go MinGasPriceDecorator (skips min-fee check for gasless txs) +|-- decorators/ Generic message-filter decorator template +|-- txpolicy/ +| +-- gasless.go IsGaslessTx — single source of truth for the gasless message whitelist +|-- params/ Test encoding configuration ++-- config.go, encoding.go, genesis.go, token_pair.go, wasm.go + +x/ Custom Cosmos SDK modules (only what Push adds) +|-- uexecutor/ Universal transaction execution layer +|-- uregistry/ Chain & token registry +|-- uvalidator/ Universal validator set + ballot voting + UV reward boost ++-- utss/ TSS keygen / refresh / quorum-change / fund migration + +precompiles/ ++-- usigverifier/ Ed25519 signature verification precompile (Solana sig verification on EVM) + +cmd/pchaind/ Binary entry point, root command, key/EVM CLI wiring +proto/ Protobuf definitions for the four custom modules +config/ Per-chain JSON registry configs (mainnet/, testnet-donut/) +``` + +## What It Does + +### The Hub-and-Spoke Picture + +Push Chain is the coordination layer in a hub-and-spoke crosschain model. Universal Validators (the off-chain `puniversald` worker — see [`universalClient/README.md`](../universalClient/README.md)) watch external chains, observe events, run TSS, and vote those observations onto Push Chain. The core validator is the hub: it tallies those votes, executes the resulting Push Chain logic, and emits the next round of work. + +``` + Ethereum ----\ /---- Ethereum + Arbitrum -----\ +------------------+ /---- Arbitrum + Base ---------->---| Push Chain |--<---- Base + BSC ----------/ | (core validator) | \---- BSC + Solana ------/ +------------------+ \--- Solana + + Inbound Tally + Execute Outbound + (UV votes inbound) (PC executes UTX) (UV signs + relays) +``` + +Two primitives drive this: + +- **Inbound** — A gateway event observed on an external chain. Universal Validators wait for finality, then vote it via `MsgVoteInbound` on `x/uexecutor`. Once 2/3 vote the same observation, the core validator executes it on Push Chain (mints PRC20s, runs the user's payload through their UEA). +- **Outbound** — A transaction the core validator needs broadcast to an external chain (e.g. funds being unlocked from a vault). The pending outbound is picked up by Universal Validators, signed via TSS, broadcast, and the result is voted back via `MsgVoteOutbound`. + +A single inbound's payload can spawn multiple outbounds; each outbound's destination event can become a new inbound. The core validator is the consistency point that keeps the whole graph deterministic. + +### Custom Modules + +Push Chain registers four custom Cosmos SDK modules. + +#### `x/uexecutor` — Universal Transaction Executor + +Lifecycle owner of every crosschain transaction (`UniversalTx`). Tallies inbound/outbound/chain-meta votes from Universal Validators, executes inbound payloads through the UEA factory, tracks pending outbounds, and writes chain-meta back to the EVM oracle. + +**Messages** +- `MsgVoteInbound`, `MsgVoteOutbound`, `MsgVoteChainMeta` — bonded UV-only, gasless +- `MsgExecutePayload`, `MsgMigrateUEA` — any user, gasless (the UEA itself authenticates the request) +- `MsgUpdateParams` — gov-only + +**State** +- `UniversalTx` — the canonical UTX record (inbound, PC tx, outbounds, status) +- `PendingInbounds` — secondary index of inbounds awaiting tally/execution +- `PendingOutbounds` — secondary index of outbounds in `PENDING` status +- `ChainMetas` — aggregated gas price + block height per CAIP-2 chain +- `ModuleAccountNonce` — manually managed nonce so the module can issue `DerivedEVMCall`s +- `GasPrices` — legacy, kept only for genesis import compatibility + +**EVM integration** — Deploys the UEA factory on fresh genesis, then drives all on-chain crosschain logic (mint PRC20, swap quotes, refund gas, push chain meta) through `DerivedEVMCall` with manual nonce tracking. See [`x/uexecutor/README.md`](../x/uexecutor/README.md). + +#### `x/uregistry` — Chain & Token Registry + +Source of truth for which external chains and tokens Push Chain talks to. Admin-curated. + +**Messages** (admin-only, where admin is `params.Admin`) +- `MsgAddChainConfig`, `MsgUpdateChainConfig` +- `MsgAddTokenConfig`, `MsgUpdateTokenConfig`, `MsgRemoveTokenConfig` +- `MsgUpdateParams` — gov-only + +**State** +- `ChainConfigs` — per-CAIP-2 chain config (RPC URL, gateway, vault methods, block confirmations, inbound/outbound enabled flags, gas oracle interval) +- `TokenConfigs` — token whitelist by `chain:address`, with native representation, decimals, and liquidity cap + +Deploys the universal system contracts (UniversalGatewayPC and reserved proxy slots) on fresh genesis. See [`x/uregistry/README.md`](../x/uregistry/README.md). + +#### `x/uvalidator` — Universal Validator Management & Ballot Voting + +The consensus layer for crosschain observations. Maintains the Universal Validator set, runs the generic ballot machine that all four modules vote through, and distributes a boosted reward share to active UVs. + +**Messages** +- `MsgAddUniversalValidator`, `MsgRemoveUniversalValidator`, `MsgUpdateUniversalValidatorStatus` — admin-only +- `MsgUpdateUniversalValidator` — self (the validator updates its own crosschain identity) +- `MsgUpdateParams` — gov-only + +**State** +- `UniversalValidatorSet` — registered UVs, keyed by `sdk.ValAddress`, with lifecycle status (`PENDING_JOIN` -> `ACTIVE` -> `PENDING_LEAVE`) +- `Ballots` — every ballot ever created (vote results, status, expiry) +- `ActiveBallotIDs`, `ExpiredBallotIDs`, `FinalizedBallotIDs` — index sets for fast lookup + +**Generic ballot machine** — used by `x/uexecutor` (inbound/outbound/chain-meta) and `x/utss` (TSS events, fund migrations). A ballot is created on the first vote, finalizes as `PASSED` once `votingThreshold` matching votes are in, or `REJECTED` once enough opposite votes make the threshold unreachable. + +**UV Reward Boost (BeginBlocker)** — Before the standard distribution module runs, `x/uvalidator` intercepts the FeeCollector balance and inflates effective voting power for active UVs by a `1.148x` multiplier. The extra `0.148x` portion is allocated proportionally to UVs and forwarded to the distribution module; the remaining fees flow back to the FeeCollector for normal proposer + community-pool + delegator distribution. Net effect: validators that also run a Universal Validator earn ~14.8% more block rewards. See [`x/uvalidator/README.md`](../x/uvalidator/README.md). + +#### `x/utss` — Threshold Signature Scheme + +Coordinates the lifecycle of the TSS key that signs every outbound transaction. + +**Messages** +- `MsgInitiateTssKeyProcess`, `MsgInitiateFundMigration` — admin-only +- `MsgVoteTssKeyProcess`, `MsgVoteFundMigration` — bonded UV-only, gasless +- `MsgUpdateParams` — gov-only + +**State** +- `CurrentTssProcess` / `ProcessHistory` — active and historical keygen/refresh/quorum-change processes +- `CurrentTssKey` / `TssKeyHistory` — finalized active key + every key that has ever existed +- `TssEvents` / `PendingTssEvents` — fine-grained events emitted during a process (used for vote routing) +- `FundMigrations` / `PendingMigrations` — old-key -> new-key fund moves on each external chain + +**Process types** +- `KEYGEN` — produce a brand-new key with new on-chain addresses (triggers fund migration on every connected chain) +- `REFRESH` — redistribute fresh keyshares without changing the public key +- `QUORUM_CHANGE` — add/remove participants without changing the public key + +See [`x/utss/README.md`](../x/utss/README.md). + +### Custom EVM Precompile + +Push Chain ships exactly one custom precompile: + +| Address | Name | Purpose | +|---|---|---| +| `0x00000000000000000000000000000000000000ca` | `usigverifier` (legacy) | Ed25519 signature verification (Solana signatures over `bytes32` digests) | +| `0xEC00000000000000000000000000000000000001` | `usigverifier` (v2) | Same implementation, registered at the reserved Push range | + +Both addresses are registered simultaneously for backward compatibility with deployed contracts that have the legacy address hardcoded. Gas cost: `4000` per `verifyEd25519` call. See [`precompiles/usigverifier/README.md`](../precompiles/usigverifier/README.md). + +The baseline EVM precompiles (`bech32`, `p256`, `staking`, `distribution`, `ics20`, `bank`, `gov`, `slashing`, `evidence`) are wired in via `app/precompiles.go:NewAvailableStaticPrecompiles`. + +### Transaction Pipeline — Gasless Support + +Push Chain extends the Cosmos AnteHandler with three custom decorators that together enable **gasless transactions** for Universal Validators and UEA users. Without this, every Universal Validator would need to hold and manage gas tokens just to vote — defeating the point of having a permissioned UV set. + +**The gasless whitelist** (`app/txpolicy/gasless.go`) — only these message types qualify: + +``` +/uexecutor.v1.MsgExecutePayload +/uexecutor.v1.MsgMigrateUEA +/uexecutor.v1.MsgVoteInbound +/uexecutor.v1.MsgVoteOutbound +/uexecutor.v1.MsgVoteChainMeta +/utss.v1.MsgVoteTssKeyProcess +/utss.v1.MsgVoteFundMigration +``` + +A tx is gasless only if **every** message (including those nested inside `authz.MsgExec`) is in the whitelist. + +**Custom decorators** + +| Decorator | File | Behavior on gasless tx | +|---|---|---| +| `MinGasPriceDecorator` | `app/cosmos/min_gas_price.go` | Skips the FeeMarket minimum-fee check entirely | +| `DeductFeeDecorator` | `app/ante/fee.go` | Skips fee deduction (no balance required) | +| `AccountInitDecorator` | `app/ante/account_init_decorator.go` | If signer has no on-chain account yet, creates it mid-pipeline with `account_number=0, sequence=0`, verifies the signature against those values, and short-circuits the rest of the ante chain | + +The third decorator is what lets a freshly-keygen'd Universal Validator hot key vote on its very first tx, without anyone first having to fund it. + +## Configuration + +| | | +|---|---| +| Binary name | `pchaind` | +| Node home | `~/.pchain` | +| Bech32 prefixes | `push` (account) / `pushvaloper` (validator operator) / `pushvalcons` (consensus) | +| Coin type | `60` (Ethereum-compatible HD path) | +| Base denom | `upc` (18 decimals, EVM-aligned) | +| Default chain ID | `localchain_9000-1` (devnet); testnet uses `push_42101-1` | +| Exposed ports (Docker) | `1317` REST, `26656` P2P, `26657` Tendermint RPC, `8545` EVM JSON-RPC, `8546` EVM WS | + +`app.toml` includes the standard `[evm]`, `[json-rpc]`, `[tls]`, and `[wasm]` sections required by the embedded EVM and JSON-RPC server. There are no Push-specific configuration knobs beyond those. + +## Getting Started + +**Prerequisites** + +- [Go 1.23+](https://golang.org/dl/) +- [Docker](https://www.docker.com/) — required for `make proto-gen` and integration tests +- [Rust](https://www.rust-lang.org/tools/install) — required to build the DKLS23 native library that the Universal Validator binary links against (the core validator binary itself doesn't depend on it, but `make build` produces both) +- [jq](https://stedolan.github.io/jq/download/) — used by setup scripts + +```bash +# One-time: build the DKLS23 native library +make build-dkls23 + +# Build pchaind (and puniversald) into ./build/ +make build + +# Or install both into $GOPATH/bin +make install + +# Spin up a single-node local chain (uses scripts/test_node.sh + Cosmovisor) +make sh-testnet + +# Run unit tests (sets LD_LIBRARY_PATH for the native TSS lib) +make test-unit + +# Run with race detector +make test-race + +# Regenerate protobuf bindings (must be inside Docker) +make proto-gen +``` + +### CLI + +```bash +pchaind init --chain-id push_42101-1 # initialize node home +pchaind start # run validator/full node +pchaind status # health check +pchaind export # export app state to JSON + +# Keys (cosmos-evm flavored — uses coin type 60) +pchaind keys add +pchaind keys list +pchaind keys show + +# Custom module queries +pchaind q uexecutor params +pchaind q uregistry all-chain-configs +pchaind q uvalidator all-universal-validators +pchaind q uvalidator all-active-ballots +pchaind q utss current-key +pchaind q utss current-process +``` + +The full CLI surface is `pchaind --help` — autocli definitions live in each module's `autocli.go`. diff --git a/precompiles/usigverifier/README.md b/precompiles/usigverifier/README.md index bb4652fd..36666a5c 100644 --- a/precompiles/usigverifier/README.md +++ b/precompiles/usigverifier/README.md @@ -1,29 +1,119 @@ -# Universal Signature Verifier (USigVerifier) Precompile +# `usigverifier` — Universal Signature Verifier Precompile -This is the USigVerifier (Universal Signature Verifier) precompile, responsible for verifying cryptographic signatures from supported source chains. +The only EVM precompile Push Chain ships on top of the cosmos-evm baseline. Verifies Ed25519 signatures inside the EVM so Solidity contracts can authenticate Solana-style signatures (or any other Ed25519 input) without re-implementing the curve in EVM bytecode. -✅ Currently supported signature: **ed25519** +## Addresses -## Generate ABI encoding +| Address | Why it exists | +|---|---| +| `0x00000000000000000000000000000000000000ca` | Original "legacy" address. Hardcoded into contracts deployed before the address-range cleanup. | +| `0xEC00000000000000000000000000000000000001` | New address in the reserved Push precompile range (`0xEC...`). | + +Both addresses are registered simultaneously and point at the **same** implementation. Backward compatibility for previously-deployed contracts is the only reason the legacy address still exists. New code should target `0xEC00000000000000000000000000000000000001`. + +Wired into `app/app.go:781-795`: + +```go +usigverifierPrecompile, _ := usigverifierprecompile.NewPrecompile() +usigverifierPrecompileV2, _ := usigverifierprecompile.NewPrecompileV2() +corePrecompiles[usigverifierPrecompile.Address()] = usigverifierPrecompile +corePrecompiles[usigverifierPrecompileV2.Address()] = usigverifierPrecompileV2 +``` + +## Solidity Interface + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.18; + +address constant USigVerifier_PRECOMPILE_ADDRESS = 0x00000000000000000000000000000000000000ca; +address constant USigVerifier_PRECOMPILE_ADDRESS_V2 = 0xEC00000000000000000000000000000000000001; + +interface IUSigVerifier { + /// @notice Verifies an Ed25519 signature. + /// @param pubKey The 32-byte Ed25519 public key (Solana address bytes). + /// @param msg The message digest that was signed (bytes32). + /// @param signature The 64-byte Ed25519 signature. + /// @return isValid True iff the signature is valid for (pubKey, msg). + function verifyEd25519( + bytes calldata pubKey, + bytes32 msg, + bytes calldata signature + ) external view returns (bool); +} +``` + +| Property | Value | +|---|---| +| Method | `verifyEd25519(bytes,bytes32,bytes)` | +| State mutability | `view` (no on-chain state is touched) | +| Gas cost | `4000` per call (`VerifyEd25519Gas` in `usigverifier.go`) | + +## Verification Semantics + +The precompile is intentionally narrow. It accepts: + +- `pubKey` — 32 raw Ed25519 public key bytes (a Solana address is exactly this) +- `msg` — a single `bytes32` digest +- `signature` — 64 raw Ed25519 signature bytes + +Internally (`query.go:VerifyEd25519`), the `bytes32` digest is **rendered as a 0x-prefixed hex string** before being passed to `ed25519.Verify`: + +```go +msgStr := "0x" + hex.EncodeToString(msg) // 66 ASCII bytes +msgBytes := []byte(msgStr) +ok = ed25519.Verify(pubKeyBytes, msgBytes, signature) +``` + +In other words, the signed message that the off-chain signer must sign is the **66-byte ASCII string** `0x...` of the digest, not the raw 32 bytes. This matches the convention used by Solana wallets when signing arbitrary messages — they prefix-encode the payload — so a normal Solana wallet signature over a Push Chain message hash will verify here without any extra work on the wallet side. + +If `pubKey` is not 32 bytes or `signature` is not 64 bytes, the precompile reverts with `invalid params`. Unknown method IDs revert with the standard `unknown method` error. + +## Generating the ABI + +If `USigVerifier.sol` is changed, regenerate `abi.json` with: ```bash cd precompiles/usigverifier solcjs USigVerifier.sol --abi mv *.abi abi.json -jq --argjson abi "$(cat abi.json)" '{"_format": "hh-sol-artifact-1", "contractName": "USigVerifier", "sourceName": "precompiles/USigVerifier.sol", "bytecode": "0x", "deployedBytecode": "0x", "linkReferences": {}, "deployedLinkReferences": {}, "abi": $abi}' <<< '{}' > abi.json -cd ../../ -# jq ".abi" abi.json | abigen --abi - --pkg usigverifier --type USigVerifier --out USigVerifier.go +jq --argjson abi "$(cat abi.json)" \ + '{"_format": "hh-sol-artifact-1", "contractName": "USigVerifier", + "sourceName": "precompiles/USigVerifier.sol", + "bytecode": "0x", "deployedBytecode": "0x", + "linkReferences": {}, "deployedLinkReferences": {}, + "abi": $abi}' <<< '{}' > abi.json ``` -## Verification +The Go binary embeds `abi.json` via `//go:embed`, so a fresh `make build` will pick up the change. + +## Testing from the Command Line ```bash -# if you just get 0x, make sure the address is in the app_state["evm"]["params"]["active_static_precompiles"] - -# precompile directly -cast abi-decode "verifyEd25519(bytes,bytes32,bytes)(bool)" `cast call 0x00000000000000000000000000000000000000ca "verifyEd25519(bytes,bytes32,bytes)" \ - "5DgQvTf6BvVs5Y4vNFnB5iXvTQvZah7y2JbT1dFxN6T2" \ - 0x68656c6c6f776f726...bytes32_message_here \ - 0x6f7c...your_signature_here -` +# Make sure the precompile is enabled in the EVM params: +# app_state["evm"]["params"]["active_static_precompiles"] must include +# 0x00000000000000000000000000000000000000ca and/or 0xEC00000000000000000000000000000000000001 +# (test_node.sh installs the legacy address by default). + +cast call 0xEC00000000000000000000000000000000000001 \ + "verifyEd25519(bytes,bytes32,bytes)" \ + "<32-byte pubKey hex>" \ + "" \ + "<64-byte signature hex>" + +# Decode the boolean response +cast abi-decode "verifyEd25519(bytes,bytes32,bytes)(bool)" +``` + +If the call returns `0x` (empty), the precompile is not in `active_static_precompiles` for the current chain — that's a configuration issue, not a verification failure. + +## Layout + +``` +precompiles/usigverifier/ +|-- USigVerifier.sol Solidity interface (the source of truth for the ABI) +|-- abi.json Embedded into the binary via go:embed +|-- usigverifier.go Precompile struct, NewPrecompile / NewPrecompileV2, RequiredGas, Run +|-- query.go VerifyEd25519 method handler ++-- README.md (this file) ``` diff --git a/readme.md b/readme.md index aa37d9ac..99f18547 100755 --- a/readme.md +++ b/readme.md @@ -57,14 +57,20 @@ make sh-testnet ## Directory Structure -- `app/` – Core application logic and configuration -- `x/` – Cosmos SDK modules (UExecutor, UTxVerifier, etc.) -- `precompiles/` – EVM precompiles for universal verification -- `proto/` – Protobuf definitions -- `cmd/` – CLI entrypoints -- `deploy/` – Deployment scripts and testnet configs +- `app/` – Core validator application wiring (`pchaind`). See [`app/README.md`](./app/README.md) for what Push Chain adds on top of cosmos-evm. +- `x/` – Push Chain custom Cosmos SDK modules: + - [`uexecutor`](./x/uexecutor/README.md) – Universal transaction execution layer + - [`uregistry`](./x/uregistry/README.md) – Chain & token registry + - [`uvalidator`](./x/uvalidator/README.md) – Universal validator set, ballot voting & UV reward boost + - [`utss`](./x/utss/README.md) – Threshold signature scheme coordination +- `precompiles/` – Custom EVM precompiles ([`usigverifier`](./precompiles/usigverifier/README.md) — Ed25519 signature verification) +- `universalClient/` – The Universal Validator binary (`puniversald`). See [`universalClient/README.md`](./universalClient/README.md). +- `proto/` – Protobuf definitions for the four custom modules +- `cmd/` – CLI entrypoints (`pchaind`, `puniversald`) +- `config/` – Per-chain JSON registry configs (mainnet, testnet) +- `testnet/` – Validator setup scripts (core + universal) - `interchaintest/` – E2E and integration tests -- `utils/` – Utility functions +- `utils/` – Shared utility functions ## Contributing diff --git a/x/uexecutor/README.md b/x/uexecutor/README.md index 3c5aed2d..02a2398a 100755 --- a/x/uexecutor/README.md +++ b/x/uexecutor/README.md @@ -1,13 +1,274 @@ -# Universal Executor (UExecutor) Module +# `x/uexecutor` — Universal Transaction Executor -This is a UExecutor (Universal Executor) module, primarily responsible for executing actions originating from other source chains. This module serves as the execution layer in universal workflows. +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). -## Responsibilities +## What It Does -- Deploying Universal Executor Accounts -- Minting native tokens -- Executing payloads +- **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. -## Getting Started +## State (KV layout) -This module is intended to provide execution capabilities for actions originating from external chains. \ No newline at end of file +| 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. + +```protobuf +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. + +```protobuf +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). + +```protobuf +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`](../../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). + +```protobuf +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`: + +```protobuf +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`) | +| `MsgMigrateUEA` | any | yes | Migrate a UEA to a newer implementation (also self-authenticated) | +| `MsgUpdateParams` | gov | no | Update module params | + +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 + +```protobuf +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, MigrateUEA +| |-- 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 +``` diff --git a/x/uregistry/README.md b/x/uregistry/README.md index 50d4215a..3617e81d 100755 --- a/x/uregistry/README.md +++ b/x/uregistry/README.md @@ -1,12 +1,113 @@ -# Universal Registry (URegistry) Module +# `x/uregistry` — Chain & Token Registry -The **Universal Registry (URegistry)** module is primarily responsible for managing metadata and configurations necessary for enabling cross-chain interoperability. +The configuration layer for Push Chain's crosschain protocol. Maintains the source of truth for which external chains and which tokens on those chains the protocol talks to. Every other Push module reads from `uregistry`; nobody else writes to it. -## Responsibilities +## What It Does -- Registering and storing supported external chain configurations -- Whitelisting tokens and gateways for inbound or outbound operations +- **Stores chain configs** — for each supported external chain (CAIP-2 keyed): public RPC URL, gateway contract address, gateway/vault method identifiers, block confirmation thresholds, gas oracle fetch interval, VM type, and inbound/outbound enabled flags. +- **Stores token configs** — per (chain, token address): symbol, decimals, native PRC20 representation, liquidity cap, ERC20/SPL/etc. type. +- **Deploys reserved system contracts** — on fresh genesis, deploys `UNIVERSAL_GATEWAY_PC` and reserved proxy slots into the EVM at deterministic addresses (`0x...C1`, `0x...B0`, `0x...B1`, `0x...B2`). +- **Exposes lookup helpers** for the rest of the codebase, including `GetTokenConfigByPRC20` (reverse lookup from a PRC20 contract address to its source-chain token). -## Getting Started +## State (KV layout) -This module serves as the metadata layer for universal workflows. \ No newline at end of file +| Prefix | Collection | Type | Purpose | +|---|---|---|---| +| `0` | `Params` | `Item[Params]` | Module parameters (admin address) | +| `1` | `ChainConfigs` | `Map[string, ChainConfig]` | Per-CAIP-2 chain configuration | +| `2` | `TokenConfigs` | `Map[string, TokenConfig]` | Token configuration, keyed by `chain:address` | + +The `ChainConfig` schema (selected fields): + +```protobuf +message ChainConfig { + string chain = 1; // CAIP-2 (e.g. "eip155:11155111") + string public_rpc_url = 2; + VmType vm_type = 3; // EVM | SVM | MOVE_VM | WASM_VM | ... + string gateway_address = 4; + repeated GatewayMethods gateway_methods = 5; + repeated VaultMethods vault_methods = 6; + BlockConfirmation block_confirmation = 7; // fast & standard inbound counts + uint64 gas_oracle_fetch_interval = 8; + ChainEnabled enabled = 9; // is_inbound_enabled, is_outbound_enabled +} +``` + +## Messages (`MsgServer`) + +| Message | Authority | Purpose | +|---|---|---| +| `MsgAddChainConfig` | admin (`params.Admin`) | Register a new external chain | +| `MsgUpdateChainConfig` | admin | Modify an existing chain config | +| `MsgAddTokenConfig` | admin | Whitelist a token on a chain | +| `MsgUpdateTokenConfig` | admin | Modify a token config | +| `MsgRemoveTokenConfig` | admin | Remove a token from the whitelist | +| `MsgUpdateParams` | gov | Rotate the admin or update other params | + +There is no validator-vote path here — chain and token additions are intentionally admin-curated. The expected workflow is gov passes `MsgUpdateParams` to install an admin key, and the admin executes config changes day-to-day. + +## Queries + +- `Params` +- `ChainConfig` — by CAIP-2 ID +- `AllChainConfigs` — paginated list +- `TokenConfig` — by (chain, address) +- `AllTokenConfigs` — paginated list +- `TokenConfigsByChain` — filter by chain + +## Inter-module Dependencies + +The keeper holds: +- `evmKeeper` — for deploying system contracts on genesis + +It exports no hooks. `x/uexecutor` and `x/utss` call its lookup helpers (`GetChainConfig`, `IsChainInboundEnabled`, `IsChainOutboundEnabled`, `GetTokenConfig`, `GetTokenConfigByPRC20`) but never write. + +## EVM Integration + +On fresh genesis (`Exported=false`), `InitGenesis` calls `deploySystemContracts` to install: + +| Slot | Address | +|---|---| +| `UNIVERSAL_GATEWAY_PC` | `0x00000000000000000000000000000000000000C1` (proxy) | +| `RESERVED_0` | `0x00000000000000000000000000000000000000B0` | +| `RESERVED_1` | `0x00000000000000000000000000000000000000B1` | +| `RESERVED_2` | `0x00000000000000000000000000000000000000B2` | +| `UNIVERSAL_BATCH_CALL` | `0x00000000000000000000000000000000000000Bc` | + +These are EIP-1967 transparent proxies — runtime-deployed bytecode is committed verbatim in `keeper.go`. Helper functions `ReserveUGPC` and `FixReservedBytecode` exist for in-place upgrade migrations to (re)install bytecode without redeploying through normal EVM calls. + +## Genesis + +```protobuf +GenesisState { + Params params + repeated ChainConfigEntry chain_configs + repeated TokenConfigEntry token_configs + bool exported // skip system-contract deploy if true +} +``` + +Default admin in `params.go`: `push1negskcfqu09j5zvpk7nhvacnwyy2mafffy7r6a`. + +## Configuration Files + +The on-disk JSON registry under `/config/{mainnet,testnet-donut}//` is what operators use to seed `uregistry` at genesis or via admin txs. Each chain has a `chain.json` plus a `tokens/` directory of per-token JSONs. See `/config/testnet-donut/eth_sepolia/` for the canonical example. + +## Layout + +``` +x/uregistry/ +|-- keeper/ +| |-- keeper.go State, lookups, system-contract deployment +| |-- msg_server.go AddChainConfig, AddTokenConfig, ... +| +-- query_server.go gRPC queries +|-- types/ +| |-- types.pb.go ChainConfig, TokenConfig, GatewayMethods, VaultMethods, enums +| |-- params.go Admin field +| |-- keys.go Store prefixes +| |-- chain_config.go, block_confirmation.go, gateway_methods.go, chain_enabled.go +| +-- expected_keepers.go EVMKeeper interface +|-- module.go +|-- autocli.go ++-- depinject.go +``` diff --git a/x/utss/README.md b/x/utss/README.md index de166ff6..7f08737c 100755 --- a/x/utss/README.md +++ b/x/utss/README.md @@ -1,13 +1,107 @@ -# Universal Transaction Verification (utss) Module +# `x/utss` — Threshold Signature Scheme -This is utss (Universal Transaction Verification) module. +The on-chain coordination layer for Push Chain's TSS key. The actual DKLS protocol runs off-chain inside the Universal Validator binary (`puniversald`); this module is the deterministic state machine that schedules processes, tallies validator votes about what happened off-chain, and serves as the canonical record of which TSS key is active. -## Responsibilities +## What It Does -- Verifying transaction hashes of funds locked on source chains -- Performing RPC calls to external chains -- Storing verified transaction hashes for reference and validation +- **Schedules TSS key processes** — admin-initiated keygen, refresh, and quorum-change events. Each process is given a deterministic `process_id` and tracked through history. +- **Stores the active TSS key** — `CurrentTssKey` is the single source of truth for which key signs outbound transactions. `TssKeyHistory` retains every key that has ever existed (never deleted, used by fund migration). +- **Tallies UV votes on TSS events** — every fine-grained step of an off-chain DKLS run (setup message produced, key derived, vote-to-finalize) is voted onto chain via `MsgVoteTssKeyProcess`. The module finalizes events through the generic ballot machine in `x/uvalidator`. +- **Coordinates fund migration** — when a `KEYGEN` produces a new public key, funds locked under the old key on every external chain need to move to the new key. Each (old_key, chain) pair becomes a `FundMigration` record; UVs broadcast the migration tx off-chain and vote success/failure on chain. -## Overview +## State (KV layout) -The utss module acts as the verification layer in a universal system, ensuring the authenticity of transactions before execution on the destination chain. \ No newline at end of file +| Prefix | Collection | Type | Purpose | +|---|---|---|---| +| `0` | `Params` | `Item[Params]` | Module parameters (admin address) | +| `1` | `NextProcessId` | `Sequence` | Auto-increment for process IDs | +| `2` | `CurrentTssProcess` | `Item[TssKeyProcess]` | Active in-flight process (may be empty) | +| `3` | `ProcessHistory` | `Map[uint64, TssKeyProcess]` | All past processes by ID | +| `4` | `CurrentTssKey` | `Item[TssKey]` | Currently active finalized key | +| `5` | `TssKeyHistory` | `Map[string, TssKey]` | All keys ever finalized, keyed by `key_id` | +| `6` | `TssEvents` | `Map[uint64, TssEvent]` | Per-event records produced during a process | +| `7` | `NextTssEventId` | `Sequence` | Auto-increment for event IDs | +| `8` | `PendingTssEvents` | `Map[uint64, uint64]` | `process_id -> event_id` index of in-flight events | +| `9` | `FundMigrations` | `Map[uint64, FundMigration]` | Migration records by ID | +| `10` | `NextMigrationId` | `Sequence` | Auto-increment for migration IDs | +| `11` | `PendingMigrations` | `Map[uint64, uint64]` | `migration_id -> migration_id` pending index | + +`PendingTssEvents` and `PendingMigrations` are deliberately structured as `uint64 -> uint64` indexes so the keeper can iterate "everything currently in flight" without scanning the full history. + +## Process Types + +| Type | Public key | On-chain addresses | Triggers fund migration? | +|---|---|---|---| +| `KEYGEN` | new | new | yes — funds must move to the new addresses on every chain | +| `REFRESH` | unchanged | unchanged | no — only keyshares are redistributed | +| `QUORUM_CHANGE` | unchanged | unchanged | no — only the participant set changes | + +`KEYGEN` is the heaviest operation: it lets the protocol periodically rotate the master key as a security uplift, but it forces a coordinated migration of every locked balance on every connected chain. + +## Messages (`MsgServer`) + +| Message | Authority | Gasless? | Purpose | +|---|---|---|---| +| `MsgInitiateTssKeyProcess` | admin | no | Start a new keygen / refresh / quorum-change | +| `MsgVoteTssKeyProcess` | bonded UV | yes | Vote on a TSS event during an active process | +| `MsgInitiateFundMigration` | admin | no | Open a migration record for an old key on a specific chain | +| `MsgVoteFundMigration` | bonded UV | yes | Vote success or failure on a fund migration tx | +| `MsgUpdateParams` | gov | no | Rotate admin or update other params | + +Vote messages gate on `IsBondedUniversalValidator` and `IsTombstonedUniversalValidator` from `x/uvalidator`. The two vote messages are gasless so UVs can participate without holding gas tokens. + +## Queries + +- `Params` +- `CurrentProcess`, `ProcessById`, `AllProcesses` +- `CurrentKey`, `KeyById` +- Plus event and migration queries (see `keeper/query_server.go`) + +## Inter-module Dependencies + +The keeper holds: +- `uvalidatorKeeper` — bonded/tombstoned checks, generic ballot machine +- `uregistryKeeper` — chain lookups for fund migration +- `uexecutorKeeper` — to update UTX state when migration affects in-flight outbounds + +It exports no hooks; other modules read `CurrentTssKey` to know what address signs outbounds. + +## Genesis + +```protobuf +GenesisState { + Params params + TssKeyProcess? current_tss_process + repeated TssKeyProcessEntry process_history + TssKey? current_tss_key + repeated TssKeyEntry tss_key_history + uint64 next_process_id + repeated TssEvent tss_events + uint64 next_tss_event_id + repeated FundMigrationEntry fund_migrations + uint64 next_migration_id +} +``` + +`PendingMigrations` is reconstructed from `FundMigrations` during `InitGenesis` by re-indexing every entry whose status is `FUND_MIGRATION_STATUS_PENDING`. + +Default admin in `params.go`: `push1negskcfqu09j5zvpk7nhvacnwyy2mafffy7r6a`. + +## Layout + +``` +x/utss/ +|-- keeper/ +| |-- keeper.go State + lifecycle +| |-- msg_server.go InitiateTssKeyProcess, VoteTssKeyProcess, InitiateFundMigration, VoteFundMigration +| +-- query_server.go gRPC queries +|-- types/ +| |-- types.pb.go TssKeyProcess, TssKey, TssEvent, FundMigration, enums +| |-- params.go Admin field +| |-- keys.go Store prefixes + ballot key generators (sha256 of canonical inputs) +| |-- tss_key.go, tss_key_process.go, msg_tss_key_process.go +| +-- expected_keepers.go UValidatorKeeper, URegistryKeeper, UExecutorKeeper interfaces +|-- module.go +|-- autocli.go ++-- depinject.go +``` diff --git a/x/uvalidator/README.md b/x/uvalidator/README.md index a08dfcf3..bc22f7ab 100755 --- a/x/uvalidator/README.md +++ b/x/uvalidator/README.md @@ -1,13 +1,194 @@ -# Universal Validator (UValidator) Module +# `x/uvalidator` — Universal Validator Set, Ballot Voting & Reward Boost -The **Universal Validator (UValidator)** module is responsible for managing the validator set and coordinating votes related to cross-chain operations. +The consensus coordination layer for Push Chain's crosschain protocol. Three responsibilities live here: -## Responsibilities +1. **Maintain the Universal Validator (UV) set** — the subset of standard Cosmos validators that have been approved to additionally run a `puniversald` worker and participate in crosschain consensus. +2. **Run the generic ballot machine** that every other Push module votes through (inbound, outbound, chain meta, TSS events, fund migrations all use it). +3. **Boost UV rewards** — in `BeginBlocker`, intercept the FeeCollector balance and allocate an extra `0.148x` portion to active UVs so running a Universal Validator is economically attractive. -- Managing the universal validator set across supported chains -- Creating and tracking ballots for voting on external chain operations -- Coordinating and recording validator votes on observed events +## What It Does -## Getting Started +### Universal Validator Lifecycle -This module serves as the consensus layer for verifying and approving cross-chain messages and actions. +A standard Cosmos validator becomes a UV by being added by the admin. Lifecycle: + +``` + AddUniversalValidator RemoveUniversalValidator +PENDING_JOIN ---------------> ACTIVE ---------------------------> PENDING_LEAVE -----> LEFT + (admin) (admin) (gradual) + +Slashing-driven side states: TOMBSTONED (terminal — can never return) +``` + +The status is stored as `LifecycleInfo` on the `UniversalValidator` record. `UpdateUniversalValidator` lets the validator self-update its crosschain identity (network info, public keys for external chains) without needing admin approval. `UpdateUniversalValidatorStatus` is admin-gated for everything else. + +Bonded check (`IsBondedUniversalValidator`) requires: +1. The validator is in `UniversalValidatorSet` +2. The validator exists in the staking module +3. The validator's status is `BONDED` + +Tombstone check (`IsTombstonedUniversalValidator`) consults the slashing keeper directly, so any double-sign by the underlying core validator immediately removes their UV from the eligible voter set. + +### Generic Ballot Machine + +Every crosschain observation (in `x/uexecutor` and `x/utss`) is voted through this single mechanism: + +```go +ballot, finalized, isNew, err := k.VoteOnBallot( + ctx, + ballotId, // canonical hash of the observation + ballotType, // INBOUND | OUTBOUND | CHAIN_META | TSS_EVENT | FUND_MIGRATION + voter, // signer's bech32 address + voteResult, // SUCCESS | FAILURE + eligibleVoters, // snapshot of UVs at ballot creation + votesNeeded, // threshold (caller decides 2/3, 100%, simple majority, ...) + expiryAfterBlocks, // ballot auto-expires after this many blocks +) +``` + +A ballot is created lazily on the first vote, indexed in `ActiveBallotIDs`, and finalizes the moment either: +- `yesVotes >= votingThreshold` -> `BALLOT_STATUS_PASSED` +- `eligibleVoters - noVotes < votingThreshold` (the threshold is now mathematically unreachable) -> `BALLOT_STATUS_REJECTED` + +On finalization, the ballot is moved from `ActiveBallotIDs` -> `FinalizedBallotIDs`. Expired ballots that never reached threshold are moved to `ExpiredBallotIDs`. + +The ballot type is opaque — `x/uvalidator` doesn't care what's being voted on. The ballot ID is a `sha256` of the canonical observation, so two validators voting on the same observation hit the same ballot deterministically. + +### UV Reward Boost (BeginBlocker) + +`x/uvalidator`'s `BeginBlocker` runs **before** the standard distribution module's `BeginBlocker` and reshapes the fee distribution: + +``` + 1.148x effective power + for active UVs +fees collected +-------------------------------------------+ +in previous ---->| uvalidator BeginBlocker | +block ---->| | + | 1. Compute effective_total_power: | + | sum( vote.power * 1.148 if UV | + | vote.power else ) | + | | + | 2. For each UV vote, allocate | + | fees * (vote.power * 0.148) | + | / effective_total_power | + | to the validator via distribution | + | module's AllocateTokensToValidator | + | | + | 3. Forward the boost coins to the | + | distribution module account so | + | accounting matches | + | | + | 4. Send the remaining coins back to | + | the FeeCollector | + +-------------------------------------------+ + | + v + standard distribution BeginBlocker + runs as usual on the remaining fees +``` + +Constants in `abci.go`: + +```go +const BoostMultiplier = "1.148" // applied to UV power when computing the denominator +const ExtraBoostPortion = "0.148" // numerator for the UV-specific allocation +``` + +Net effect: a validator that runs a UV earns ~14.8% more block rewards than a non-UV with the same stake. This is the only economic incentive baked into the protocol for running a UV — it has to make sense as a business for permissioned operators. + +> **Note on community tax** — The boost math is correct only when community tax is `0`. With a non-zero community tax, the UV boost is taken from the full fee amount before tax is applied to the remainder, so the community pool sees a slightly smaller share than configured. This is documented inline in `abci.go`. + +## State (KV layout) + +| Prefix | Collection | Type | Purpose | +|---|---|---|---| +| `0` | `Params` | `Item[Params]` | Module parameters (admin address) | +| `2` | `UniversalValidatorSet` | `Map[sdk.ValAddress, UniversalValidator]` | Registered UVs with lifecycle info and crosschain identity | +| `3` | `Ballots` | `Map[string, Ballot]` | All ballots ever created | +| `4` | `ActiveBallotIDs` | `KeySet[string]` | Ballots currently collecting votes | +| `5` | `ExpiredBallotIDs` | `KeySet[string]` | Expired (not yet pruned) ballots | +| `6` | `FinalizedBallotIDs` | `KeySet[string]` | `PASSED` or `REJECTED` ballots | + +(Prefix `1` was historically used by an obsolete `core_to_universal` mapping and is left unused for migration compatibility.) + +## Messages (`MsgServer`) + +| Message | Authority | Purpose | +|---|---|---| +| `MsgAddUniversalValidator` | admin | Register a core validator as a UV (`PENDING_JOIN`) | +| `MsgRemoveUniversalValidator` | admin | Begin removing a UV (`PENDING_LEAVE`) | +| `MsgUpdateUniversalValidatorStatus` | admin | Force-set lifecycle status (escape hatch) | +| `MsgUpdateUniversalValidator` | self | The UV updates its own crosschain identity (network info / external pubkeys) | +| `MsgUpdateParams` | gov | Rotate admin or update other params | + +## Queries + +- `Params` +- `AllUniversalValidators`, `UniversalValidator` +- `Ballot`, `AllBallots` +- `AllActiveBallotIDs`, `AllActiveBallots` + +## Hooks + +`x/uvalidator` exports `UValidatorHooks`: + +```go +type UValidatorHooks interface { + AfterValidatorAdded(ctx, valAddr) error + AfterValidatorRemoved(ctx, valAddr) error + AfterValidatorStatusChanged(ctx, valAddr, oldStatus, newStatus) error +} +``` + +A `MultiUValidatorHooks` dispatcher (`keeper/hooks.go`) lets multiple consumers subscribe. As of today, no other module installs hooks, but the interface is present for future use. + +## Inter-module Dependencies + +The keeper holds: +- `StakingKeeper` — to look up validators by operator/consensus address and to gate `IsBondedUniversalValidator` +- `SlashingKeeper` — to check tombstone status (`IsTombstoned` by consensus address) +- `BankKeeper` — to move fees between FeeCollector / `uvalidator` / `distribution` module accounts during the boost +- `AuthKeeper` (`AccountKeeper`) — to resolve the FeeCollector module account +- `DistributionKeeper` — to call `AllocateTokensToValidator` for the UV boost +- `UtssKeeper` — used during validator lifecycle transitions when TSS quorum changes are needed + +## Genesis + +```protobuf +GenesisState { + Params params + repeated UniversalValidatorEntry universal_validators + repeated Ballot ballots + repeated string active_ballot_ids + repeated string expired_ballot_ids + repeated string finalized_ballot_ids +} +``` + +Default admin in `params.go`: `push1negskcfqu09j5zvpk7nhvacnwyy2mafffy7r6a`. + +## Layout + +``` +x/uvalidator/ +|-- abci.go BeginBlocker — UV reward boost (this is the interesting one) +|-- keeper/ +| |-- keeper.go State + dependencies +| |-- voting.go IsBondedUV, IsTombstonedUV, AddVoteToBallot, VoteOnBallot, CheckIfFinalizingVote +| |-- ballot.go CreateBallot, GetOrCreateBallot, ExpireBallotsBeforeHeight +| |-- validator.go UV set CRUD and bonded/tombstone helpers +| |-- hooks.go MultiUValidatorHooks dispatcher +| |-- msg_server.go + msg_*.go for each message type +| +-- query_server.go gRPC queries +|-- types/ +| |-- ballot.go, ballot.pb.go Ballot lifecycle (ShouldPass, ShouldReject, IsExpired, AddVote) +| |-- universal_validator.go, types.pb.go UV record + UVStatus enum +| |-- identity_info.go, network_info.go Per-chain identity +| |-- lifecyle_info.go, lifecyle_event.go Status tracking +| |-- params.go, keys.go +| +-- expected_keepers.go Staking, Slashing, Bank, Distribution, Account, Utss interfaces +|-- migrations/ Consensus version 2 — one prior breaking change +|-- module.go +|-- autocli.go ++-- depinject.go +``` From ffe6f96c2ebb054f2ce050304b604d4cf79344a0 Mon Sep 17 00:00:00 2001 From: Nilesh Gupta Date: Tue, 7 Apr 2026 22:51:48 +0200 Subject: [PATCH 2/2] feat: updated READMEs of core validator and specific modules --- DERIVED_TRANSACTIONS.md | 5 ++--- app/README.md | 3 +-- x/uexecutor/README.md | 5 +++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/DERIVED_TRANSACTIONS.md b/DERIVED_TRANSACTIONS.md index 73ffdb2c..db0132ae 100644 --- a/DERIVED_TRANSACTIONS.md +++ b/DERIVED_TRANSACTIONS.md @@ -79,7 +79,7 @@ The Push Chain codebase uses two distinct call patterns. Both are visible in [`x ### 1. User-derived sender (UEA-routed user actions) -When a user submits a `MsgExecutePayload` or `MsgMigrateUEA`, the Cosmos signer is converted to its derived EVM address and the EVM call is issued from that address. The UEA contract is what authenticates the request via `verificationData`. +When a user submits a `MsgExecutePayload`, the Cosmos signer is converted to its derived EVM address and the EVM call is issued from that address. The UEA contract is what authenticates the request via `verificationData`. UEA migration takes the same path — there is no separate migration message; an upgrade is just an `executePayload` whose payload calls the UEA's migration entry point. ```go return k.evmKeeper.DerivedEVMCall( @@ -177,8 +177,7 @@ Every derived call in Push Chain is in [`x/uexecutor/keeper/evm.go`](./x/uexecut | Helper | Sender | Why derived? | |---|---|---| | `CallFactoryToDeployUEA` | user-derived | Real tx receipt is required for the deploy; the deployer address is the source-chain user's derived EVM address. | -| `CallUEAExecutePayload` | user-derived | Carries `gasLimit` from the payload; receipt is consumed by the Universal Validator vote-back path. | -| `CallUEAMigrateUEA` | user-derived | Same — needs a real receipt. | +| `CallUEAExecutePayload` | user-derived | Carries `gasLimit` from the payload; receipt is consumed by the Universal Validator vote-back path. UEA migration also flows through this path now (the migration is just a payload that calls the UEA's migrate entry point). | | `CallPRC20Deposit` | module | Mints PRC20 to recipient. Module account has no key. | | `CallPRC20DepositAutoSwap` | module | Same, but with the auto-swap leg. | | `CallUniversalCoreSetGasPrice` | module | Writes a single chain's gas price to the on-chain oracle. | diff --git a/app/README.md b/app/README.md index b99f6cca..0b470db6 100644 --- a/app/README.md +++ b/app/README.md @@ -74,7 +74,7 @@ Lifecycle owner of every crosschain transaction (`UniversalTx`). Tallies inbound **Messages** - `MsgVoteInbound`, `MsgVoteOutbound`, `MsgVoteChainMeta` — bonded UV-only, gasless -- `MsgExecutePayload`, `MsgMigrateUEA` — any user, gasless (the UEA itself authenticates the request) +- `MsgExecutePayload` — any user, gasless (the UEA itself authenticates the request) - `MsgUpdateParams` — gov-only **State** @@ -163,7 +163,6 @@ Push Chain extends the Cosmos AnteHandler with three custom decorators that toge ``` /uexecutor.v1.MsgExecutePayload -/uexecutor.v1.MsgMigrateUEA /uexecutor.v1.MsgVoteInbound /uexecutor.v1.MsgVoteOutbound /uexecutor.v1.MsgVoteChainMeta diff --git a/x/uexecutor/README.md b/x/uexecutor/README.md index 02a2398a..4b35784a 100755 --- a/x/uexecutor/README.md +++ b/x/uexecutor/README.md @@ -201,9 +201,10 @@ At every step the UTX is mutated **append-only**: new entries are added to `pc_t | `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`) | -| `MsgMigrateUEA` | any | yes | Migrate a UEA to a newer implementation (also self-authenticated) | | `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 @@ -257,7 +258,7 @@ GenesisState { x/uexecutor/ |-- keeper/ | |-- keeper.go State + dependencies -| |-- msg_server.go MsgVoteInbound, MsgVoteOutbound, MsgVoteChainMeta, ExecutePayload, MigrateUEA +| |-- 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