diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md new file mode 100644 index 000000000..7d81564c9 --- /dev/null +++ b/docs/foreign_chain_transactions.md @@ -0,0 +1,249 @@ +# Foreign Chain Transaction Verification (Design Proposal) + +Status: Draft (based on PR #1851 / branch `read-foreign-chain`) + +## Purpose & Motivation + +This feature lets the MPC network sign payloads only after verifying a specific foreign-chain transaction, so NEAR contracts can react to external chain events without a trusted relayer. Primary use cases: + +- Omnibridge inbound flow (foreign chain -> NEAR) where Chain Signatures are required to attest that a foreign transaction finalized successfully. +- Broader chain abstraction: a single MPC network verifies foreign chain state and signs conditional payloads. + +## Scope + +- In scope: contract-level API for verify+sign requests, node-side verification via configured RPC providers, deterministic provider selection, and extensible per-chain verifiers. +- Out of scope: on-chain light clients / cryptographic proofs, multi-round MPC consensus on verification results, and non-ECDSA schemes for verify_foreign_transaction (initially ECDSA only). + +## Overview + +At a high level: + +1. A user submits a `verify_foreign_transaction` request with a chain-specific verification config. +2. MPC nodes verify the foreign transaction via configured RPC providers. +3. If verified, MPC signs `sha256(tx_id_bytes)` with the derived domain key and returns the signature on-chain. + +### User Flow: Verify a Foreign Transaction + +```mermaid +--- +title: Foreign Chain Verification - System Context +--- +flowchart TD + DEV["**Developer / Bridge Service** + _Submits verify_foreign_transaction requests._"] + + SC["**MPC Signer Contract** + _On-chain policy + pending requests._"] + + MPC["**MPC Nodes** + _Verify foreign tx status and sign._"] + + RPC["**RPC Providers** + _JSON-RPC endpoints._"] + + FC["**Foreign Chain** + _Solana, future chains._"] + + DEV -->|"1. verify_foreign_transaction()"| SC + MPC -->|"3. respond_verify_foreign_tx()"| SC + MPC -->|"2. query tx status"| RPC + RPC -->|"read chain state"| FC + + DEV@{ shape: manual-input} + SC@{ shape: db} + MPC@{ shape: proc} + RPC@{ shape: proc} + FC@{ shape: cylinder} +``` + +With the user flow in mind, the on-chain interface is: + +### Contract Interface (Request/Response) + +```rust +// Contract methods +verify_foreign_transaction(request: VerifyForeignTxRequestArgs) -> VerifyForeignTxResponse // Through a promise +respond_verify_foreign_tx({ request, response }) // Respond method for signers +``` + +```rust +// Contract DTOs +pub struct VerifyForeignTxRequestArgs { + pub chain: ForeignChain, + pub tx_id: TransactionId, // TxID is the payload we're signing + pub path: String, // Key derivation path + pub domain_id: Option, // Defaults to 0 (legacy ECDSA) +} + +pub struct VerifyForeignTxRequest { + // Constructed from the args + pub chain: ForeignChainRpcRequest, + pub tx_id: TransactionId, + pub tweak: Tweak, + pub domain_id: DomainId, +} + +pub struct VerifyForeignTxResponse { + pub verified_at_block: BlockId, + pub signature: SignatureResponse, // Signature over `sha256(tx_id_bytes)` where `tx_id_bytes` are chain-native bytes (e.g., Solana 64-byte signature). +} + +pub enum ForeignChainRpcRequest { + Solana(SolanaRpcRequest), + Bitcoin(BitcoinRpcRequest), + // Future chains... +} + +pub struct SolanaRpcRequest { + pub tx_id: SolanaTxId, + pub finality: Finality, // Optimistic or Final +} + +pub struct BitcoinRpcRequest { + pub tx_id: BitcoinTxId, + pub confirmations: usize, // required confirmations before considering final +} + +pub enum Finality{ + Optimistic, + Final, +} +``` + +### Contract state (Foreign Chain Policy) + +The contract maintains a *foreign chain policy* that defines which chains and RPC providers are allowed. + +```rust +pub struct ForeignChainPolicy { + pub chains: BTreeSet, +} + +pub struct ForeignChainConfig { + pub chain: ForeignChain, + pub providers: NonEmptyVec, +} + +pub enum ForeignChain { + Solana, + Bitcoin, + // Future chains... +} + +pub struct RpcProviderName(String); + +pub struct ForeignChainPolicyVotes { + // Each authenticated participant has one active vote for a proposal. + pub proposal_by_account: BTreeMap, +} +``` + +### Failure and Timeout Behavior + +- Nodes **abstain** if verification fails (RPC error, tx not found, or not finalized). +- A failed verification does **not** produce an on-chain failure response. The request eventually times out and fails with the standard timeout error. + +For operators, policy updates control which chains/providers are allowed: + +### Operator Flow: Policy Updates (New Chains / Providers) + +```mermaid +--- +title: Foreign Chain Policy Updates - High Level +--- +flowchart TD + NODE["**MPC Node** + _Local config + API keys._"] + + SC["**MPC Signer Contract** + _Foreign chain policy._"] + + COMP["**Compare** + _Local config vs policy._"] + + UPDATED["**Policy Updated** + _Unanimous vote reached._"] + + NODE -->|"1. read policy"| SC + NODE -->|"2. compare"| COMP + COMP -->|"3. vote if different"| SC + SC -->|"4. update policy on unanimity"| UPDATED + + NODE@{ shape: proc} + SC@{ shape: db} + COMP@{ shape: proc} + UPDATED@{ shape: proc} +``` + +### Contract Policy State (Types) +See "Contract state (Foreign Chain Policy)" above. + +### Node Configuration and Policy Updates + +- Node config contains chain RPC providers and timeouts (API keys stay local). +- On startup, nodes compare local config to the on-chain policy. +- If different, a node submits a vote for the policy derived from its local config. +- Policy updates are applied only when all current participants vote for the same proposal. +- Pending proposals and vote counts are visible via `get_foreign_chain_policy_proposals()`. + +Provider selection is deterministic across nodes: + +### Deterministic Provider Selection + +Each node selects a provider using a deterministic hash of: + +``` +hash = sha256(participant_id || request_id || provider_name) +``` + +Providers are sorted by this hash to build a deterministic ordering: + +- **Primary provider** = first in the ordering. +- **Fallback** = subsequent providers in order. +- Each provider can include backup URLs for failover. + +This ensures different nodes query different providers for the same request while preserving determinism. + +### Configuration (Node) + +Example config snippet: + +```yaml +foreign_chains: + solana: + timeout_sec: 30 + max_retries: 3 + providers: + alchemy: + rpc_url: "https://solana-mainnet.g.alchemy.com/v2/" + api_key: + env: ALCHEMY_API_KEY + quicknode: + rpc_url: "https://your-endpoint.solana-mainnet.quiknode.pro/" + api_key: + val: "" +``` + +The contract policy references providers by **name**, and nodes must have matching +provider entries in config (including API keys) to satisfy the policy. + +## Risks + +- **RPC trust and correctness**: Verification relies on centralized RPC providers. A malicious + or faulty provider could return incorrect status for a subset of nodes. +- **No additional consensus**: Nodes independently verify and abstain on failure. If a threshold + of nodes are misled by providers, the network could sign invalid payloads. +- **Provider availability**: Outages or rate limits can cause verification failures and reduced + signing availability. +- **Finality semantics**: Finality definitions differ across chains; mapping them correctly is critical. +- **Operational friction**: Unanimous voting for policy updates may slow rollouts and hot fixes. +- **Config drift**: Nodes missing required provider keys will fail startup validation. + +## Discussion points +- Why do we return a signature? Can't we just return a bool. + - A signature suggests this is a "proof" that can be validated by someone else than the caller, but currently it seems like this proof could easily be forged by just calling the normal "sign" method. +- Finality interface right now diverges from the original PR. Are we okay with this new structure? +- Can we assume all RPC providers take API keys as bearer tokens? +- Should we identify RPC providers by a base URL instead of an arbitrary name? +- Should the policy vote threshold stay **unanimous**, or be configurable (e.g., threshold)? +- Startup validation: when policy is empty, nodes skip config validation and can still boot/vote an initial policy. Is this the desired operational behavior? diff --git a/prompt.md b/prompt.md new file mode 100644 index 000000000..e5b941e53 --- /dev/null +++ b/prompt.md @@ -0,0 +1,50 @@ +Hey! Can you help draft an initial design doc in `docs/foreign_chain_transactions.md`? Heres a description of the issue: + +### Background + +In https://github.com/near/mpc/pull/1851 a new feature has been proposed to extend the MPC network to allow MPC nodes to verify foreign chain transactions. + +Since this is a big feature, it would be very helpful to compile a design proposal to facilitate effective design conversations and ensure we can make effective progress on getting this merged. + +### User Story + +As a developer I'd like to have key design decisions documented to ensure we're aligned and allowing us to proceed and focus on implementation details. + +### Acceptance Criteria + +We have a design doc for the foreign transaction validation feature. The design doc should contain the following: + +1. **Motivation.** Why is this feature important? What are the use-cases we want to support? +2. **High level component design.** What are the major components we're implementing, and how are they interacting? How does the flow look end to end when using this feature? Mermaid charts following the c4 model would be helpful here, as well as sequence diagrams for user flows and the voting flows (config updates + proposing new chains) etc. +3. **Risks**. What are the major risks if we implement this feature? Can we migrate pieces of it, or will this cause a big maintenance burden going forward. +4. **Alternatives considered.** Outline some of the alternatives to the design we've considered, and why we chose to proceed with the existing design. + +### Resources & Additional Notes + +Prototype implementation PR: https://github.com/near/mpc/pull/1851 + +Meeting notes from a discussion on this: +- We'll start small with only supporting foreign transaction status verification. + - This is sufficient for the bridge use cases. + - This will not help us migrate the Hot wallet use case. + - Hot bridge should be able to work with this, but it would require significant refactors on their end. +- Supported RPC providers should be configured in the MPC contract. + - We'll require a threshold number of votes to add a new RPC provider. + - Nodes, not operators, will vote for the RPC providers as soon as they see a proposal they have configured API keys for. +- Each MPC node will call a single RPC provider, determined using consistent hashing similar to how we do leader election. + +**Use case: Omnibridge** +See this quote from Bowen - this feature is key to allow using the MPC network to move assets from other chains to near. + +> Chain Signatures is used in Omnibridge starting from Day 1. Near → Foreign Chain always uses chain signatures, whether the destination chain is Bitcoin, Zcash, Solana, Ethereum, etc. The other direction (foreign chain to Near) uses a variety of proving mechanisms including light clients and wormhole. However, we are also working on migrating that entirely to chain signatures. + +PR description summary +This PR introduces a new MPC signing flow that conditionally signs only after independently verifying that a foreign-chain transaction has succeeded. Users submit a verification request containing a transaction hash, target chain, and finality level. Each MPC node independently verifies the transaction via RPC before participating in signing, with no additional consensus round required—nodes simply abstain if verification fails. + +The initial implementation supports Solana and is designed to be easily extensible to additional chains. The contract exposes a new `verify_foreign_transaction` function that derives the signing payload from the transaction ID (SHA-256) and supports ECDSA domains. On-chain policy controls which foreign chains and RPC providers are allowed, with unanimous voting required for policy changes. Nodes automatically validate and synchronize their local configuration against this policy on startup. + +Verification uses deterministic RPC provider selection based on participant ID and request ID, ensuring that different nodes query different providers for the same request, reducing reliance on any single RPC endpoint. Fallback to alternate providers is deterministic, improving resilience against faulty or malicious RPC responses. + +This design enables secure, trust-minimized cross-chain signing and significantly extends Omnibridge’s multi-chain capabilities, providing a robust foundation for supporting additional foreign chains in the future. + +The #1851 PR os on branch `read-foreign-chain`. Please inspect the diff to understand the current idea better. Also please look at key files to understand how the system works around these changes.