Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dist/
.env
.env.*
.secrets/
probe/out/
7 changes: 4 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ Chains resolve by id, name, or slug (`200010`, `hadrian`, `Rome Hadrian`) — by
| `fund <chain> --from <src> --amount <usdc>` | `ROME_EVM_KEY` | bridge USDC → Rome **gas** (CCTP); the "from home" on-ramp |
| `bridge <chain> --from <src> --amount <usdc> [--intent gas\|wrapper]` | `ROME_EVM_KEY` | bridge USDC in as gas or wUSDC |
| `verify <chain> [--path solidity]` | `ROME_EVM_KEY` + `ROME_SOLANA_KEY` | the **both-lane works-gate**: deploy a probe, drive it from the EVM lane *and* the Solana lane, assert parity |
| `verify <chain> --path solana-program` | `ROME_EVM_KEY` | the **cross-VM works-gate**: deploy a thin CPI wrapper; an EVM-lane call drives a Solana program (SPL Memo) via CPI. `--solana-rpc` adds the Solana-log deep check |

`fund`/`bridge` orchestrate `@rome-protocol/sdk/bridge` (quote → sign the source burn → settle → register → poll); `verify` orchestrates `submitRomeTx` (EVM lane) + `submitRomeTxSolanaLane` (Solana lane). Every action prints what it did; `fund`/`bridge` preview with `--dry-run`.
`fund`/`bridge` orchestrate `@rome-protocol/sdk/bridge` (quote → sign the source burn → settle → register → poll); `verify --path solidity` orchestrates `submitRomeTx` (EVM lane) + `submitRomeTxSolanaLane` (Solana lane); `verify --path solana-program` orchestrates `submitRomeTx` into a wrapper that CPIs a Solana program via the CPI precompile (`0xff…08`). Every action prints what it did; `fund`/`bridge` preview with `--dry-run`.

## Grounding — why the facts are trustworthy

Expand Down Expand Up @@ -102,7 +103,7 @@ src/
bridge.ts fund · bridge — the inbound CCTP flow engine (orchestrates the SDK)
doctor.ts preflight checklist
tx.ts cross-VM diagnosis (rome_solanaTxForEvmTx; no debug_trace)
verify.ts the both-lane works-gate (+ probe.ts, the bundled Store probe)
verify.ts the path-aware works-gate (+ probe.ts: bundled Store + CPI-Memo probes)
presets.ts foundry/hardhat config
keys.ts requireEvmKey / requireSolanaKey (env-only)
eip1193.ts Node EIP-1193 shim for submitRomeTx
Expand All @@ -115,4 +116,4 @@ test/ per-module unit tests + alignment + behavioral CLI (+ mcp-

## Roadmap

Shipped: the full four-paths surface above (reads + actions), all funded-verified on a live Rome chain. Next: `verify --path solana-program` / `--path from-home` slices · bridge ETH (Wormhole) + outbound (`from-rome`) · `new` (wraps `create-rome-app`). Deploy/build stays orchestrated (Foundry/Hardhat/create-rome-app) — `rome` is the connective tissue + the Rome-unique gaps, not a re-implementation of the EVM toolchain.
Shipped: the full four-paths surface above (reads + actions), all funded-verified on a live Rome chain — including `verify --path solana-program` (an EVM-lane call driving a Solana program via CPI). Next: `verify --path from-home` · bridge ETH (Wormhole) + outbound (`from-rome`) · `new` (wraps `create-rome-app`). Deploy/build stays orchestrated (Foundry/Hardhat/create-rome-app) — `rome` is the connective tissue + the Rome-unique gaps, not a re-implementation of the EVM toolchain.
20 changes: 19 additions & 1 deletion docs/GUIDES.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,25 @@ $ rome verify hadrian --path solidity
}
```

`ok: true` means one contract on Rome answered correctly whether an EVM wallet *or* a Solana wallet drove it — the dual-lane promise, verified on-chain. (`--path solana-program` and `--path from-home` follow.)
`ok: true` means one contract on Rome answered correctly whether an EVM wallet *or* a Solana wallet drove it — the dual-lane promise, verified on-chain.

### `--path solana-program` — an EVM user drives your Solana program via CPI

Brought a **Solana program**? The gate is different: prove an EVM-lane call reaches it through the CPI precompile. `verify --path solana-program` deploys a thin CPI wrapper (its constructor self-provisions the contract's external-auth PDA), then an EVM-lane `ping` drives the SPL Memo program via CPI. A successful EVM receipt is the proof — a failed CPI reverts the tx. This path needs **only `ROME_EVM_KEY`** (no Solana key — the EVM lane is the whole story).

```console
$ rome verify hadrian --path solana-program
{
"path": "solana-program",
"probe": "0x206d7101…",
"program": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
"evmTx": "0x82aae391…",
"cpiLanded": true,
"ok": true
}
```

Add `--solana-rpc <url>` for the deep check: `verify` resolves the EVM tx to its Solana settlement (`rome_solanaTxForEvmTx`) and confirms the memo landed in the program's logs (`"memoConfirmed": true`). (`--path from-home` follows.)

---

Expand Down
41 changes: 41 additions & 0 deletions probe/CpiMemoProbe.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;

// Minimal CPI wrapper for `rome verify --path solana-program`. Proves an EVM-lane
// call drives a Solana program (SPL Memo) through the CPI precompile (0xff..08),
// signing as this contract's own external-auth PDA — the "bring your Solana
// program" pattern, reduced to its smallest honest form. Compiled with solc 0.8.36.

interface ICpi {
struct AccountMeta {
bytes32 pubkey;
bool is_signer;
bool is_writable;
}
function invoke(bytes32 program_id, AccountMeta[] memory accounts, bytes memory data) external;
}

interface IHelper {
function pda(address user) external view returns (bytes32);
function create_pda(address user) external;
}

contract CpiMemoProbe {
ICpi private constant CPI = ICpi(0xFF00000000000000000000000000000000000008);
IHelper private constant HELPER = IHelper(0xff00000000000000000000000000000000000009);
// SPL Memo v2 (MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr) as bytes32.
bytes32 private constant MEMO = 0x054a535a992921064d24e87160da387c7c35b5ddbc92bb81e41fa8404105448d;

constructor() {
// Provision this contract's external-auth PDA so the CPI precompile can sign as it.
HELPER.create_pda(address(this));
}

// CPI into SPL Memo with this contract's PDA as the sole (required) signer account.
// A failed CPI reverts this call — that revert is the works-gate's negative signal.
function ping(string calldata memo) external {
ICpi.AccountMeta[] memory accts = new ICpi.AccountMeta[](1);
accts[0] = ICpi.AccountMeta({ pubkey: HELPER.pda(address(this)), is_signer: true, is_writable: false });
CPI.invoke(MEMO, accts, bytes(memo));
}
}
10 changes: 7 additions & 3 deletions src/core/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,16 @@ export const CAPABILITIES: Capability[] = [
(a) => bridgeHandler(a),
),

// ── verify: the both-lane works-gate (CLI-only action; EVM key + Solana key) ──
// ── verify: the path-aware works-gate (CLI-only action; keys vary by path) ──
verbAction(
"verify",
"verify",
"The both-lane works-gate: deploy a probe and prove the SAME contract answers on the EVM lane AND the Solana lane. Needs ROME_EVM_KEY + ROME_SOLANA_KEY. e.g. rome verify hadrian --path solidity",
[chainArg, { name: "path", required: false, description: "builder path (solidity; solana-program + from-home follow)" }],
"The path-aware works-gate. solidity: prove the SAME contract answers on the EVM lane AND the Solana lane (ROME_EVM_KEY + ROME_SOLANA_KEY). solana-program: an EVM-lane call drives a Solana program via CPI (ROME_EVM_KEY). e.g. rome verify hadrian --path solana-program",
[
chainArg,
{ name: "path", required: false, description: "builder path: solidity (default) | solana-program (from-home follows)" },
{ name: "solana-rpc", required: false, description: "Solana RPC for the opt-in deep check (confirm the CPI effect in Solana logs)" },
],
(a) => verifyHandler(a),
),
];
Expand Down
17 changes: 17 additions & 0 deletions src/core/probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,20 @@ export const STORE_PROBE = {
bytecode:
"0x6080604052348015600e575f5ffd5b5060a580601a5f395ff3fe6080604052348015600e575f5ffd5b50600436106030575f3560e01c806360fe47b11460345780636d4ce63c146045575b5f5ffd5b6043603f3660046059565b5f55565b005b5f5460405190815260200160405180910390f35b5f602082840312156068575f5ffd5b503591905056fea2646970667358221220960bbcb62c68f5e98b5bca2ceac80403075d01943318403b5e8bcf09cff64a0b64736f6c63430008240033",
} as const;

// A minimal CPI wrapper probe (solc 0.8.36, optimized). The constructor self-provisions
// this contract's external-auth PDA (`HelperProgram.create_pda`); `ping(string)` CPIs the
// SPL Memo program via the CPI precompile (0xff..08), signing as that PDA. `rome verify
// --path solana-program` deploys it and calls `ping` — a successful EVM tx proves an
// EVM-lane caller drove a Solana program via CPI (a failed CPI reverts the tx).
// Source: probe/CpiMemoProbe.sol. The driven program is SPL Memo v2.
export const CPI_MEMO_PROBE = {
abi: [
{ inputs: [], stateMutability: "nonpayable", type: "constructor" },
{ inputs: [{ internalType: "string", name: "memo", type: "string" }], name: "ping", outputs: [], stateMutability: "nonpayable", type: "function" },
],
bytecode:
"0x6080604052348015600e575f5ffd5b5060405160016265549b60e11b0319815230600482015273ff000000000000000000000000000000000000099063ff3556ca906024015f604051808303815f87803b158015605a575f5ffd5b505af1158015606b573d5f5f3e3d5ffd5b5050505061031d8061007c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c80633adb191b1461002d575b5f5ffd5b61004061003b3660046101a4565b610042565b005b6040805160018082528183019092525f91816020015b604080516060810182525f80825260208083018290529282015282525f1990920191018161005857905050604080516060810191829052638854a29960e01b90915230606482015290915080600960ff60981b01638854a29960848301602060405180830381865afa1580156100d0573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906100f49190610212565b8152600160208201525f6040909101819052825183919061011757610117610229565b6020908102919091010152604051633a4065c360e11b8152600860ff60981b0190637480cb8690610172907f054a535a992921064d24e87160da387c7c35b5ddbc92bb81e41fa8404105448d90859088908890600401610265565b5f604051808303815f87803b158015610189575f5ffd5b505af115801561019b573d5f5f3e3d5ffd5b50505050505050565b5f5f602083850312156101b5575f5ffd5b823567ffffffffffffffff8111156101cb575f5ffd5b8301601f810185136101db575f5ffd5b803567ffffffffffffffff8111156101f1575f5ffd5b856020828401011115610202575f5ffd5b6020919091019590945092505050565b5f60208284031215610222575f5ffd5b5051919050565b634e487b7160e01b5f52603260045260245ffd5b81835281816020850137505f828201602090810191909152601f909101601f19169091010190565b5f60608201868352606060208401528086518083526080850191506020880192505f5b818110156102c65783518051845260208101511515602085015260408101511515604085015250606083019250602084019350600181019050610288565b505083810360408501526102db81868861023d565b9897505050505050505056fea26469706673582212209834cec8d28a7f7526419d3efe61137125ae270d973218e633513339f7a8d35764736f6c63430008240033",
// The Solana program the probe drives via CPI (SPL Memo v2).
memoProgram: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
} as const;
108 changes: 101 additions & 7 deletions src/core/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { submitRomeTx, submitRomeTxSolanaLane } from "@rome-protocol/sdk";
import { getChainFacts } from "./facts.js";
import { requireEvmKey, requireSolanaKey } from "./keys.js";
import { eip1193FromAccount } from "./eip1193.js";
import { STORE_PROBE } from "./probe.js";
import { STORE_PROBE, CPI_MEMO_PROBE } from "./probe.js";

// `rome verify --path solidity` — the both-lane works-gate. Deploy a probe, then prove
// the SAME contract answers when set from the EVM lane (submitRomeTx) AND the Solana lane
Expand Down Expand Up @@ -106,12 +106,106 @@ export function defaultVerifyDeps(chain: string | number): VerifyDeps {
};
}

/** `rome verify --path solidity` handler — validates the path, then runs the gate. */
export async function verifyHandler(args: Record<string, string>): Promise<VerifyResult> {
const path = (args.path ?? "solidity").toLowerCase();
if (path !== "solidity") {
throw new Error(`Only --path solidity is supported in this slice (got "${path}"). solana-program + from-home follow.`);
// ─────────────────────────────────────────────────────────────────────────────
// `rome verify --path solana-program` — the cross-VM works-gate. Deploy a thin CPI
// wrapper (its constructor self-provisions the contract's external-auth PDA), then an
// EVM-lane `ping` drives the SPL Memo program via the CPI precompile (0xff..08). A
// successful EVM receipt IS the proof — a failed CPI reverts the tx. EVM-lane ONLY
// (ROME_EVM_KEY; no Solana key). The Solana-log confirmation is an opt-in deep check,
// gated on a Solana RPC (`--solana-rpc`), since the public CLI's default Solana RPC is
// substituted to public devnet and the core gate needs no RPC at all.

export interface VerifySolanaProgramResult {
path: "solana-program";
probe: `0x${string}`;
program: string;
memo: string;
evmTx: `0x${string}`;
cpiLanded: boolean;
solanaSettlement?: string;
memoConfirmed?: boolean;
ok: boolean;
}

export interface VerifySolanaProgramDeps {
deployProbe(): Promise<`0x${string}`>;
ping(probe: `0x${string}`, memo: string): Promise<{ hash: `0x${string}`; success: boolean }>;
/** Opt-in deep check: confirm the memo landed in the Solana settlement logs. */
confirmMemo?(evmTxHash: `0x${string}`, memo: string): Promise<{ settlement?: string; found: boolean }>;
}

/** Deploy the CPI wrapper, drive it from the EVM lane, and (optionally) confirm the effect. */
export async function runVerifySolanaProgram(deps: VerifySolanaProgramDeps): Promise<VerifySolanaProgramResult> {
const probe = await deps.deployProbe();
const memo = `rome-verify:${probe}`;
const { hash, success } = await deps.ping(probe, memo);

let solanaSettlement: string | undefined;
let memoConfirmed: boolean | undefined;
if (deps.confirmMemo) {
const c = await deps.confirmMemo(hash, memo);
solanaSettlement = c.settlement;
memoConfirmed = c.found;
}
// Core gate = the EVM tx landed (a failed CPI reverts it). When the deep check runs,
// it must also find the memo we sent in the Solana settlement logs.
const ok = success && (memoConfirmed ?? true);
return { path: "solana-program", probe, program: CPI_MEMO_PROBE.memoProgram, memo, evmTx: hash, cpiLanded: success, solanaSettlement, memoConfirmed, ok };
}

/** Real deps: viem deploy of the CPI probe + `ping` via submitRomeTx. EVM key ONLY. */
export function defaultVerifySolanaProgramDeps(chain: string | number, opts?: { solanaRpc?: string }): VerifySolanaProgramDeps {
const c = getChainFacts(chain);
const account = privateKeyToAccount(requireEvmKey()); // fail fast — no network, no Solana key required
const pub = createPublicClient({ transport: http(c.rpcUrl) });
const provider = eip1193FromAccount(account, c.rpcUrl, c.chainId);

return {
async deployProbe() {
const wallet = createWalletClient({ account, transport: http(c.rpcUrl) });
const gp = await pub.getGasPrice();
const hash = await wallet.deployContract({
abi: CPI_MEMO_PROBE.abi as never,
bytecode: CPI_MEMO_PROBE.bytecode,
chain: null,
gas: 26_000_000n,
maxFeePerGas: (gp * 3n) / 2n,
maxPriorityFeePerGas: 0n,
});
const rcpt = await pub.waitForTransactionReceipt({ hash });
if (!rcpt.contractAddress) throw new Error("CPI probe deploy produced no contract address");
return rcpt.contractAddress;
},
async ping(probe, memo) {
const data = encodeFunctionData({ abi: CPI_MEMO_PROBE.abi, functionName: "ping", args: [memo] });
const hash = await submitRomeTx(provider, { from: account.address, to: probe, data });
const rcpt = await pub.waitForTransactionReceipt({ hash });
return { hash, success: rcpt.status === "success" };
},
confirmMemo: opts?.solanaRpc
? async (evmTxHash, memo) => {
const res = await fetch(c.rpcUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "rome_solanaTxForEvmTx", params: [evmTxHash] }),
});
const sigs: string[] = ((await res.json()) as { result?: string[] })?.result ?? [];
const connection = new Connection(opts.solanaRpc!, "confirmed");
for (const sig of sigs) {
const stx = await connection.getTransaction(sig, { maxSupportedTransactionVersion: 0 });
if ((stx?.meta?.logMessages ?? []).some((l) => l.includes(memo))) return { settlement: sig, found: true };
}
return { settlement: sigs[0], found: false };
}
: undefined,
};
}

/** `rome verify --path <solidity|solana-program>` handler — validates the path, runs the gate. */
export async function verifyHandler(args: Record<string, string>): Promise<VerifyResult | VerifySolanaProgramResult> {
const path = (args.path ?? "solidity").toLowerCase();
// async so a synchronous key/config throw surfaces as a rejected promise, not a throw.
return runVerifySolidity(defaultVerifyDeps(args.chain));
if (path === "solidity") return runVerifySolidity(defaultVerifyDeps(args.chain));
if (path === "solana-program") return runVerifySolanaProgram(defaultVerifySolanaProgramDeps(args.chain, { solanaRpc: args["solana-rpc"] }));
throw new Error(`--path ${path} is not supported yet (available: solidity, solana-program; from-home follows).`);
}
Loading
Loading