From 6c106837379f54c12edaf7a6357f4312d8c19022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 12:02:23 +0100 Subject: [PATCH 01/20] chore: Add prompt --- prompt.md | 373 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 prompt.md diff --git a/prompt.md b/prompt.md new file mode 100644 index 000000000..e1727c54b --- /dev/null +++ b/prompt.md @@ -0,0 +1,373 @@ +Hey! Can you help me with this 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 **concise** design proposal (target **< 500 lines**) for “foreign transaction status verification” based on PR #1851. It must: + +1. **State scope and non-scope** (tx status verification only; what is explicitly not handled). +2. **Describe the end-to-end flow** from request → policy lookup → provider selection → RPC query → returned result (or contract interaction), matching the PR. +3. **List the key design decisions + open questions** (explicitly mark TBDs rather than guessing). +4. **List 2–3 alternatives** *as placeholders* (no invented details), only to frame discussion. +5. **Propose a PR slicing plan**: 5–8 small PRs that map to components already present/implicit in #1851. + +### 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. + + +Here are some notes for how I'd like to tackle this: + +Goal: Produce a concise (1–2 pages) design proposal for “foreign transaction status verification” based on: +(A) the PR description below, and +(B) the diff between `main` (or PR base) and branch `read-foreign-chain`. + +Step 1 — Gather context (mandatory): +- Checkout branch `read-foreign-chain`. +- Compute and inspect the diff vs `main` (or the merge base). + Example commands (use whatever is available): + git fetch origin + git checkout read-foreign-chain + git diff --stat origin/main...HEAD + git diff origin/main...HEAD +- Skim the key touched files to understand data types, contract methods, indexer flow, and node RPC verification logic. + +Step 2 — Write the design proposal using ONLY what you can ground in: +- PR description below +- code seen in the diff +If something is unclear or not present, write “TBD” rather than guessing. + +Step 3 — Add grounding: +- For every important statement about behavior or architecture, add “(source: )” referencing the relevant changed file(s). + +Document format (use these headings exactly, bullet-heavy, minimal prose): + +1) Summary (5–8 bullets) +- What feature does, what it enables (Omnibridge), what is explicitly NOT solved. + +2) Scope / Non-goals +- In scope (tx status verification + conditional signing flow, Solana first, etc.) +- Out of scope (general oracle reads, Hot wallet migration, proofs/light clients, etc.) — only if stated. + +3) Proposed Design (as implemented in `read-foreign-chain`) +- Contract surface: new methods + request/response types + payload derivation (SHA-256(tx_id)) + domain limitation to ECDSA (source: …) +- Node behavior: verification step before signing; how “no explicit consensus round” is achieved; failure semantics (“don’t sign”) (source: …) +- Indexer flow: how receipts become requests; where pending requests are stored; how response resumes yield promise (source: …) +- Foreign chain policy voting: where config lives on-chain; how voting works (unanimous vs threshold); when nodes vote; contract enforcement when policy empty (source: …) +- Provider selection: deterministic assignment function and failover ordering (source: …) +- RPC verification: Solana JSON-RPC handling; finality levels supported; success/failed mapping (source: …) +- Config validation: local config validation + against policy on startup (source: …) + +4) End-to-end Flow (numbered steps, copy PR flow but verify in code) +- Request → contract → indexer → node selection/provider selection → verify → sign → respond → contract resume (source: …) + +5) Decisions / Open Questions (table) +Columns: Topic | What PR does | Why (if stated) | TBD / Questions +Must include: +- Unanimous vs threshold voting (PR says unanimous; meeting notes may differ) — call out discrepancy explicitly +- 1-provider-per-node vs multi-provider queries +- API key handling: env vars vs config; restart/rotation considerations (only what is stated) +- Trust model for RPC data / compromised providers + +6) Alternatives (placeholders ONLY, 2–3 bullets each, no new details) +- e.g. “separate oracle network”, “query k providers + cross-check”, “light clients/proofs” (do not elaborate beyond placeholders) + +7) PR Slicing Plan (5–8 small PRs) +- Use the diff to propose a clean breakdown with file lists per slice. + +PR description (for context): + +This PR implements a new MPC signing flow that conditionally signs based on verification that a foreign chain transaction was successful. Users submit a verification request + specifying the transaction hash, chain, and finality level. MPC nodes independently verify the transaction via RPC before participating in MPC signing. + This feature would be greatly useful to extend Omnibridge to support more chains. Initial implementation supports Solana; other chains can be added later. + ## Key Features + ### 1. New contract function `verify_foreign_transaction` + - Users submit a foreign chain transaction ID instead of an arbitrary payload + - The contract derives the signing payload from the transaction ID (SHA-256 hash) + - Only ECDSA domains are supported for this feature + ### 2. Independent verification by MPC nodes + - Each node verifies the foreign transaction via RPC before signing + - No explicit consensus round needed - if verification fails, the node simply doesn't sign + - Supports two finality levels: Optimistic (confirmed) and Final (finalized) + ### 3. Solana RPC integration + - Full Solana verifier implementation using JSON-RPC + - Configurable primary and backup RPC endpoints + - Proper handling of transaction status (success/failed) + ### 4. Foreign Chain Policy Voting System + - On-chain configuration for supported foreign chains and their RPC providers + - Unanimous voting required for policy changes + - Nodes automatically vote when their local configuration changes on startup + - Contract validates policy before allowing `verify_foreign_transaction` + - When policy is empty, `verify_foreign_transaction` returns error (safe no-op) + ### 5. Deterministic Provider Selection + - Each MPC node is assigned a specific RPC provider based on `hash(participant_id, request_id, provider_name)` + - Different nodes query different providers for the same request + - Reduces risk of a single bad provider affecting verification + - Fallback to other providers in deterministic order if primary fails + ### 6. Enhanced Validation + - Node startup validates local config against contract policy + - Policy validation ensures required providers have usable endpoints (non-empty rpc_url) + - Clear error messages when configuration doesn't match policy + ## Request Flow + 1. User calls `verify_foreign_transaction(chain=Solana, tx_id=..., finality=Final, path=...)` + 2. Contract validates args, checks policy allows the chain, stores in `pending_verify_foreign_tx_requests`, creates yield promise + 3. Indexer detects the receipt, creates `VerifyForeignTxRequest` + 4. MpcClient selects RPC provider deterministically based on participant ID and request ID + 5. MpcClient verifies foreign transaction via Solana RPC + 6. If verified & tx succeeded, proceed with MPC signing (same as regular signature) + 7. Submit `respond_verify_foreign_tx` with verification proof + signature + 8. Contract validates signature, resumes yield promise + 9. User receives `VerifyForeignTxResponse` with `block_id` and `signature` + ## Node Configuration + ```yaml + # config.yaml format for foreign chains + foreign_chains: + solana: + timeout_sec: 30 + max_retries: 3 + providers: + alchemy: + rpc_url: "https://solana-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}" + quicknode: + rpc_url: "https://your-endpoint.solana-mainnet.quiknode.pro/${QN_API_KEY}" + ``` +## Important Implementation Details + 1. Payload derivation: The signing payload is SHA-256(tx_id), matching the contract's near_sdk::env::sha256() + 2. Atomic writes: Both VerifyForeignTxRequest and SignatureRequest are written atomically to prevent inconsistent state on crash + 3. Config validation: ForeignChainConfig::validate() is called on startup, and validate_against_policy() checks required providers have usable endpoints + 4. Policy enforcement: Contract checks policy before processing requests; empty policy or unsupported chain returns error + 5. Provider distribution: Hash-based provider selection ensures different nodes query different providers, improving resilience against bad RPC data + + +Another document that could be helpful (take with a pinch of salt): +# Foreign Chain Policy Implementation Plan + +## Overview + +Add on-chain configuration for supported foreign chains and their RPC providers, with unanimous voting required for policy changes. Nodes automatically vote when their local configuration changes. + +## Data Structures + +### Contract Types (crates/contract/src/primitives/foreign_chain.rs) + +```rust +/// RPC provider identifier (e.g., "alchemy", "quicknode", "helius") +#[near(serializers=[borsh, json])] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RpcProviderName(pub String); + +/// Configuration for a supported foreign chain +#[near(serializers=[borsh, json])] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ForeignChainEntry { + pub chain: ForeignChain, + pub required_providers: Vec, // At least 1 required +} + +/// Complete foreign chain policy stored in contract state +#[near(serializers=[borsh, json])] +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ForeignChainPolicy { + pub chains: Vec, +} +``` + +### Contract Voting (new file: crates/contract/src/primitives/foreign_chain_policy_votes.rs) + +```rust +/// Tracks votes for ForeignChainPolicy changes (follows ThresholdParametersVotes pattern) +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ForeignChainPolicyVotes { + proposal_by_account: BTreeMap, +} +``` + +### Node Config Changes (crates/node/src/config.rs) + +```yaml +# New config.yaml format +foreign_chains: + solana: + timeout_sec: 30 + max_retries: 3 + providers: + alchemy: + rpc_url: "https://solana-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}" + quicknode: + rpc_url: "https://your-endpoint.solana-mainnet.quiknode.pro/${QN_API_KEY}" +``` + +## Contract Changes + +### 1. Add to RunningContractState (crates/contract/src/state/running.rs) + +- Add `foreign_chain_policy: ForeignChainPolicy` field +- Add `foreign_chain_policy_votes: ForeignChainPolicyVotes` field +- Add `vote_foreign_chain_policy()` method requiring unanimous agreement + +### 2. Add Contract Methods (crates/contract/src/lib.rs) + +```rust +/// Vote for a new foreign chain policy (creates proposal if none exists) +#[handle_result] +pub fn vote_foreign_chain_policy(&mut self, proposal: ForeignChainPolicy) -> Result<(), Error> + +/// Get current policy (view method) +pub fn get_foreign_chain_policy(&self) -> Result + +/// Get pending proposals with vote counts (view method) +pub fn get_foreign_chain_policy_proposals(&self) -> Result, Error> +``` + +### 3. Validation + +- Each chain in policy must have at least 1 provider +- No duplicate chains in policy +- Voting uses `AuthenticatedAccountId` pattern from votes.rs + +## Node Changes + +### 1. Config Structure (crates/node/src/config.rs) + +```rust +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ForeignChainConfig { + pub solana: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SolanaProviderConfig { + pub providers: HashMap, // provider_name -> endpoint + #[serde(default = "default_solana_timeout")] + pub timeout_sec: u64, + #[serde(default = "default_solana_retries")] + pub max_retries: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SolanaRpcEndpoint { + pub rpc_url: String, + #[serde(default)] + pub backup_urls: Vec, +} +``` + +Add methods: +- `validate_against_policy(&ForeignChainPolicy) -> Result<(), String>` - check all required providers are configured +- `to_policy() -> ForeignChainPolicy` - convert local config to policy for voting + +### 2. Startup Validation (crates/node/src/cli.rs) + +After indexer sync, before coordinator starts: +1. Fetch `foreign_chain_policy` from contract +2. Call `config.foreign_chains.validate_against_policy(&policy)` +3. Fail with clear error message if providers are missing + +### 3. Automatic Voting (new: crates/node/src/foreign_chain_policy_voter.rs) + +Runs once during node startup (before coordinator enters main loop): +1. Compare local config to contract policy +2. If local config matches current policy, no action needed +3. If different, check for matching pending proposal +4. If matching proposal exists, vote for it +5. If no matching proposal, create one (cast first vote) + +**Note**: Config changes require node restart. This matches the existing pattern for other node configuration. + +### 4. Indexer Changes (crates/node/src/indexer/participants.rs) + +- Add `foreign_chain_policy: ForeignChainPolicy` to `ContractRunningState` +- Parse from contract state response + +### 5. Verifier Registry (crates/node/src/foreign_chain_verifier/mod.rs) + +- Update `ForeignChainVerifierRegistry::new()` to work with new provider-based config +- Select first available provider for each chain (or implement provider rotation) + +## File-by-File Implementation Order + +### Phase 1: Contract Data Structures +1. `crates/contract/src/primitives/foreign_chain.rs` - Add `RpcProviderName`, `ForeignChainEntry`, `ForeignChainPolicy` +2. `crates/contract/src/primitives/foreign_chain_policy_votes.rs` (new) - Voting structure +3. `crates/contract/src/primitives.rs` - Export new module +4. `crates/contract/src/errors.rs` - Add `ForeignChainPolicyError` + +### Phase 2: Contract State & Methods +5. `crates/contract/src/state/running.rs` - Add policy fields and voting method +6. `crates/contract/src/state.rs` - Add delegation methods +7. `crates/contract/src/lib.rs` - Add contract methods + +### Phase 3: Contract Interface +8. `crates/contract-interface/src/types/foreign_chain.rs` (new or extend) - DTO types for node + +### Phase 4: Node Config +9. `crates/node/src/config.rs` - Refactor to provider-based config +10. `crates/node/src/foreign_chain_verifier/mod.rs` - Update registry initialization + +### Phase 5: Node Integration +11. `crates/node/src/indexer/participants.rs` - Add policy to contract state +12. `crates/node/src/cli.rs` - Add startup validation +13. `crates/node/src/foreign_chain_policy_voter.rs` (new) - Automatic voting task +14. `crates/node/src/indexer/types.rs` - Add `VoteForeignChainPolicy` transaction type +15. `crates/node/src/coordinator.rs` - Spawn voter task + +### Phase 6: Tests +16. Unit tests for voting logic +17. Unit tests for config validation +18. Integration tests for full voting flow + +## Migration Considerations + +- Initial deployment: Contract starts with empty `ForeignChainPolicy` +- **When policy is empty, `verify_foreign_transaction` returns error** "Foreign chain verification not enabled" - safe no-op behavior +- When policy exists but requested chain is not in policy, return error "Chain not supported by policy" +- Existing nodes need config file updates to new format +- First policy must be established via unanimous vote to enable foreign tx verification +- Node startup validation is skipped if contract policy is empty (allows nodes to start and vote for initial policy) + +## Verification + +1. **Unit tests** (`cargo test --profile test-release`): + - Voting logic: vote counting, unanimous agreement detection + - Config validation: `validate_against_policy()` with missing/present providers + - Policy conversion: `to_policy()` from node config + +2. **Rust integration tests** (crates/contract/tests/sandbox/): + - Test unanimous voting completion across multiple participants + - Test vote replacement when participant changes their vote + - Test policy validation (at least 1 provider per chain) + +3. **Python system tests** (pytest/tests/): + - `test_foreign_chain_policy_voting.py`: + - Test node startup fails when provider missing from policy + - Test node startup succeeds with correct config + - Test automatic vote is cast when config differs from policy + - Test unanimous voting updates policy + - Test `verify_foreign_transaction` returns error when policy is empty + - Test `verify_foreign_transaction` returns error when chain not in policy + - Test `verify_foreign_transaction` succeeds when chain is in policy From d89665327a1ae546a45eb4b577f53bd3f4d0cb13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 12:08:33 +0100 Subject: [PATCH 02/20] Update prompt --- prompt.md | 344 +----------------------------------------------------- 1 file changed, 5 insertions(+), 339 deletions(-) diff --git a/prompt.md b/prompt.md index e1727c54b..7ced8ce90 100644 --- a/prompt.md +++ b/prompt.md @@ -12,13 +12,12 @@ As a developer I'd like to have key design decisions documented to ensure we're ### Acceptance Criteria -We have a **concise** design proposal (target **< 500 lines**) for “foreign transaction status verification” based on PR #1851. It must: +We have a design doc for the foreign transaction validation feature. The design doc should contain the following: -1. **State scope and non-scope** (tx status verification only; what is explicitly not handled). -2. **Describe the end-to-end flow** from request → policy lookup → provider selection → RPC query → returned result (or contract interaction), matching the PR. -3. **List the key design decisions + open questions** (explicitly mark TBDs rather than guessing). -4. **List 2–3 alternatives** *as placeholders* (no invented details), only to frame discussion. -5. **Propose a PR slicing plan**: 5–8 small PRs that map to components already present/implicit in #1851. +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? Charts following the c4 model would be helpful here. +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 @@ -38,336 +37,3 @@ Meeting notes from a discussion on this: 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. - - -Here are some notes for how I'd like to tackle this: - -Goal: Produce a concise (1–2 pages) design proposal for “foreign transaction status verification” based on: -(A) the PR description below, and -(B) the diff between `main` (or PR base) and branch `read-foreign-chain`. - -Step 1 — Gather context (mandatory): -- Checkout branch `read-foreign-chain`. -- Compute and inspect the diff vs `main` (or the merge base). - Example commands (use whatever is available): - git fetch origin - git checkout read-foreign-chain - git diff --stat origin/main...HEAD - git diff origin/main...HEAD -- Skim the key touched files to understand data types, contract methods, indexer flow, and node RPC verification logic. - -Step 2 — Write the design proposal using ONLY what you can ground in: -- PR description below -- code seen in the diff -If something is unclear or not present, write “TBD” rather than guessing. - -Step 3 — Add grounding: -- For every important statement about behavior or architecture, add “(source: )” referencing the relevant changed file(s). - -Document format (use these headings exactly, bullet-heavy, minimal prose): - -1) Summary (5–8 bullets) -- What feature does, what it enables (Omnibridge), what is explicitly NOT solved. - -2) Scope / Non-goals -- In scope (tx status verification + conditional signing flow, Solana first, etc.) -- Out of scope (general oracle reads, Hot wallet migration, proofs/light clients, etc.) — only if stated. - -3) Proposed Design (as implemented in `read-foreign-chain`) -- Contract surface: new methods + request/response types + payload derivation (SHA-256(tx_id)) + domain limitation to ECDSA (source: …) -- Node behavior: verification step before signing; how “no explicit consensus round” is achieved; failure semantics (“don’t sign”) (source: …) -- Indexer flow: how receipts become requests; where pending requests are stored; how response resumes yield promise (source: …) -- Foreign chain policy voting: where config lives on-chain; how voting works (unanimous vs threshold); when nodes vote; contract enforcement when policy empty (source: …) -- Provider selection: deterministic assignment function and failover ordering (source: …) -- RPC verification: Solana JSON-RPC handling; finality levels supported; success/failed mapping (source: …) -- Config validation: local config validation + against policy on startup (source: …) - -4) End-to-end Flow (numbered steps, copy PR flow but verify in code) -- Request → contract → indexer → node selection/provider selection → verify → sign → respond → contract resume (source: …) - -5) Decisions / Open Questions (table) -Columns: Topic | What PR does | Why (if stated) | TBD / Questions -Must include: -- Unanimous vs threshold voting (PR says unanimous; meeting notes may differ) — call out discrepancy explicitly -- 1-provider-per-node vs multi-provider queries -- API key handling: env vars vs config; restart/rotation considerations (only what is stated) -- Trust model for RPC data / compromised providers - -6) Alternatives (placeholders ONLY, 2–3 bullets each, no new details) -- e.g. “separate oracle network”, “query k providers + cross-check”, “light clients/proofs” (do not elaborate beyond placeholders) - -7) PR Slicing Plan (5–8 small PRs) -- Use the diff to propose a clean breakdown with file lists per slice. - -PR description (for context): - -This PR implements a new MPC signing flow that conditionally signs based on verification that a foreign chain transaction was successful. Users submit a verification request - specifying the transaction hash, chain, and finality level. MPC nodes independently verify the transaction via RPC before participating in MPC signing. - This feature would be greatly useful to extend Omnibridge to support more chains. Initial implementation supports Solana; other chains can be added later. - ## Key Features - ### 1. New contract function `verify_foreign_transaction` - - Users submit a foreign chain transaction ID instead of an arbitrary payload - - The contract derives the signing payload from the transaction ID (SHA-256 hash) - - Only ECDSA domains are supported for this feature - ### 2. Independent verification by MPC nodes - - Each node verifies the foreign transaction via RPC before signing - - No explicit consensus round needed - if verification fails, the node simply doesn't sign - - Supports two finality levels: Optimistic (confirmed) and Final (finalized) - ### 3. Solana RPC integration - - Full Solana verifier implementation using JSON-RPC - - Configurable primary and backup RPC endpoints - - Proper handling of transaction status (success/failed) - ### 4. Foreign Chain Policy Voting System - - On-chain configuration for supported foreign chains and their RPC providers - - Unanimous voting required for policy changes - - Nodes automatically vote when their local configuration changes on startup - - Contract validates policy before allowing `verify_foreign_transaction` - - When policy is empty, `verify_foreign_transaction` returns error (safe no-op) - ### 5. Deterministic Provider Selection - - Each MPC node is assigned a specific RPC provider based on `hash(participant_id, request_id, provider_name)` - - Different nodes query different providers for the same request - - Reduces risk of a single bad provider affecting verification - - Fallback to other providers in deterministic order if primary fails - ### 6. Enhanced Validation - - Node startup validates local config against contract policy - - Policy validation ensures required providers have usable endpoints (non-empty rpc_url) - - Clear error messages when configuration doesn't match policy - ## Request Flow - 1. User calls `verify_foreign_transaction(chain=Solana, tx_id=..., finality=Final, path=...)` - 2. Contract validates args, checks policy allows the chain, stores in `pending_verify_foreign_tx_requests`, creates yield promise - 3. Indexer detects the receipt, creates `VerifyForeignTxRequest` - 4. MpcClient selects RPC provider deterministically based on participant ID and request ID - 5. MpcClient verifies foreign transaction via Solana RPC - 6. If verified & tx succeeded, proceed with MPC signing (same as regular signature) - 7. Submit `respond_verify_foreign_tx` with verification proof + signature - 8. Contract validates signature, resumes yield promise - 9. User receives `VerifyForeignTxResponse` with `block_id` and `signature` - ## Node Configuration - ```yaml - # config.yaml format for foreign chains - foreign_chains: - solana: - timeout_sec: 30 - max_retries: 3 - providers: - alchemy: - rpc_url: "https://solana-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}" - quicknode: - rpc_url: "https://your-endpoint.solana-mainnet.quiknode.pro/${QN_API_KEY}" - ``` -## Important Implementation Details - 1. Payload derivation: The signing payload is SHA-256(tx_id), matching the contract's near_sdk::env::sha256() - 2. Atomic writes: Both VerifyForeignTxRequest and SignatureRequest are written atomically to prevent inconsistent state on crash - 3. Config validation: ForeignChainConfig::validate() is called on startup, and validate_against_policy() checks required providers have usable endpoints - 4. Policy enforcement: Contract checks policy before processing requests; empty policy or unsupported chain returns error - 5. Provider distribution: Hash-based provider selection ensures different nodes query different providers, improving resilience against bad RPC data - - -Another document that could be helpful (take with a pinch of salt): -# Foreign Chain Policy Implementation Plan - -## Overview - -Add on-chain configuration for supported foreign chains and their RPC providers, with unanimous voting required for policy changes. Nodes automatically vote when their local configuration changes. - -## Data Structures - -### Contract Types (crates/contract/src/primitives/foreign_chain.rs) - -```rust -/// RPC provider identifier (e.g., "alchemy", "quicknode", "helius") -#[near(serializers=[borsh, json])] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RpcProviderName(pub String); - -/// Configuration for a supported foreign chain -#[near(serializers=[borsh, json])] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ForeignChainEntry { - pub chain: ForeignChain, - pub required_providers: Vec, // At least 1 required -} - -/// Complete foreign chain policy stored in contract state -#[near(serializers=[borsh, json])] -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct ForeignChainPolicy { - pub chains: Vec, -} -``` - -### Contract Voting (new file: crates/contract/src/primitives/foreign_chain_policy_votes.rs) - -```rust -/// Tracks votes for ForeignChainPolicy changes (follows ThresholdParametersVotes pattern) -#[near(serializers=[borsh, json])] -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct ForeignChainPolicyVotes { - proposal_by_account: BTreeMap, -} -``` - -### Node Config Changes (crates/node/src/config.rs) - -```yaml -# New config.yaml format -foreign_chains: - solana: - timeout_sec: 30 - max_retries: 3 - providers: - alchemy: - rpc_url: "https://solana-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}" - quicknode: - rpc_url: "https://your-endpoint.solana-mainnet.quiknode.pro/${QN_API_KEY}" -``` - -## Contract Changes - -### 1. Add to RunningContractState (crates/contract/src/state/running.rs) - -- Add `foreign_chain_policy: ForeignChainPolicy` field -- Add `foreign_chain_policy_votes: ForeignChainPolicyVotes` field -- Add `vote_foreign_chain_policy()` method requiring unanimous agreement - -### 2. Add Contract Methods (crates/contract/src/lib.rs) - -```rust -/// Vote for a new foreign chain policy (creates proposal if none exists) -#[handle_result] -pub fn vote_foreign_chain_policy(&mut self, proposal: ForeignChainPolicy) -> Result<(), Error> - -/// Get current policy (view method) -pub fn get_foreign_chain_policy(&self) -> Result - -/// Get pending proposals with vote counts (view method) -pub fn get_foreign_chain_policy_proposals(&self) -> Result, Error> -``` - -### 3. Validation - -- Each chain in policy must have at least 1 provider -- No duplicate chains in policy -- Voting uses `AuthenticatedAccountId` pattern from votes.rs - -## Node Changes - -### 1. Config Structure (crates/node/src/config.rs) - -```rust -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ForeignChainConfig { - pub solana: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SolanaProviderConfig { - pub providers: HashMap, // provider_name -> endpoint - #[serde(default = "default_solana_timeout")] - pub timeout_sec: u64, - #[serde(default = "default_solana_retries")] - pub max_retries: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SolanaRpcEndpoint { - pub rpc_url: String, - #[serde(default)] - pub backup_urls: Vec, -} -``` - -Add methods: -- `validate_against_policy(&ForeignChainPolicy) -> Result<(), String>` - check all required providers are configured -- `to_policy() -> ForeignChainPolicy` - convert local config to policy for voting - -### 2. Startup Validation (crates/node/src/cli.rs) - -After indexer sync, before coordinator starts: -1. Fetch `foreign_chain_policy` from contract -2. Call `config.foreign_chains.validate_against_policy(&policy)` -3. Fail with clear error message if providers are missing - -### 3. Automatic Voting (new: crates/node/src/foreign_chain_policy_voter.rs) - -Runs once during node startup (before coordinator enters main loop): -1. Compare local config to contract policy -2. If local config matches current policy, no action needed -3. If different, check for matching pending proposal -4. If matching proposal exists, vote for it -5. If no matching proposal, create one (cast first vote) - -**Note**: Config changes require node restart. This matches the existing pattern for other node configuration. - -### 4. Indexer Changes (crates/node/src/indexer/participants.rs) - -- Add `foreign_chain_policy: ForeignChainPolicy` to `ContractRunningState` -- Parse from contract state response - -### 5. Verifier Registry (crates/node/src/foreign_chain_verifier/mod.rs) - -- Update `ForeignChainVerifierRegistry::new()` to work with new provider-based config -- Select first available provider for each chain (or implement provider rotation) - -## File-by-File Implementation Order - -### Phase 1: Contract Data Structures -1. `crates/contract/src/primitives/foreign_chain.rs` - Add `RpcProviderName`, `ForeignChainEntry`, `ForeignChainPolicy` -2. `crates/contract/src/primitives/foreign_chain_policy_votes.rs` (new) - Voting structure -3. `crates/contract/src/primitives.rs` - Export new module -4. `crates/contract/src/errors.rs` - Add `ForeignChainPolicyError` - -### Phase 2: Contract State & Methods -5. `crates/contract/src/state/running.rs` - Add policy fields and voting method -6. `crates/contract/src/state.rs` - Add delegation methods -7. `crates/contract/src/lib.rs` - Add contract methods - -### Phase 3: Contract Interface -8. `crates/contract-interface/src/types/foreign_chain.rs` (new or extend) - DTO types for node - -### Phase 4: Node Config -9. `crates/node/src/config.rs` - Refactor to provider-based config -10. `crates/node/src/foreign_chain_verifier/mod.rs` - Update registry initialization - -### Phase 5: Node Integration -11. `crates/node/src/indexer/participants.rs` - Add policy to contract state -12. `crates/node/src/cli.rs` - Add startup validation -13. `crates/node/src/foreign_chain_policy_voter.rs` (new) - Automatic voting task -14. `crates/node/src/indexer/types.rs` - Add `VoteForeignChainPolicy` transaction type -15. `crates/node/src/coordinator.rs` - Spawn voter task - -### Phase 6: Tests -16. Unit tests for voting logic -17. Unit tests for config validation -18. Integration tests for full voting flow - -## Migration Considerations - -- Initial deployment: Contract starts with empty `ForeignChainPolicy` -- **When policy is empty, `verify_foreign_transaction` returns error** "Foreign chain verification not enabled" - safe no-op behavior -- When policy exists but requested chain is not in policy, return error "Chain not supported by policy" -- Existing nodes need config file updates to new format -- First policy must be established via unanimous vote to enable foreign tx verification -- Node startup validation is skipped if contract policy is empty (allows nodes to start and vote for initial policy) - -## Verification - -1. **Unit tests** (`cargo test --profile test-release`): - - Voting logic: vote counting, unanimous agreement detection - - Config validation: `validate_against_policy()` with missing/present providers - - Policy conversion: `to_policy()` from node config - -2. **Rust integration tests** (crates/contract/tests/sandbox/): - - Test unanimous voting completion across multiple participants - - Test vote replacement when participant changes their vote - - Test policy validation (at least 1 provider per chain) - -3. **Python system tests** (pytest/tests/): - - `test_foreign_chain_policy_voting.py`: - - Test node startup fails when provider missing from policy - - Test node startup succeeds with correct config - - Test automatic vote is cast when config differs from policy - - Test unanimous voting updates policy - - Test `verify_foreign_transaction` returns error when policy is empty - - Test `verify_foreign_transaction` returns error when chain not in policy - - Test `verify_foreign_transaction` succeeds when chain is in policy From 80b216d7a1180706ecc46c5dac8e49b51b145072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 12:11:59 +0100 Subject: [PATCH 03/20] Update prompt again --- prompt.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/prompt.md b/prompt.md index 7ced8ce90..ec6b50002 100644 --- a/prompt.md +++ b/prompt.md @@ -1,4 +1,4 @@ -Hey! Can you help me with this issue: +Hey! Can you help draft an initial design doc in `docs/foreign_chain_transactions.md`? Heres a description of the issue: ### Background @@ -15,7 +15,7 @@ As a developer I'd like to have key design decisions documented to ensure we're 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? Charts following the c4 model would be helpful here. +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. @@ -37,3 +37,5 @@ Meeting notes from a discussion on this: 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. + +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. From cb19c048408024ab916631a45e742dbd32a34af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 12:16:22 +0100 Subject: [PATCH 04/20] PR description summary in design doc --- prompt.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/prompt.md b/prompt.md index ec6b50002..e5b941e53 100644 --- a/prompt.md +++ b/prompt.md @@ -38,4 +38,13 @@ See this quote from Bowen - this feature is key to allow using the MPC network t > 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. From 1d68430ec34a4352e4eb6db59ba3a7bd68521205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 12:23:16 +0100 Subject: [PATCH 05/20] docs: Draft design doc for foreign_chain_transactions --- docs/foreign_chain_transactions.md | 248 +++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 docs/foreign_chain_transactions.md diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md new file mode 100644 index 000000000..7a010705f --- /dev/null +++ b/docs/foreign_chain_transactions.md @@ -0,0 +1,248 @@ +# Foreign Chain Transaction Verification (Design Proposal) + +Status: Draft (based on PR #1851 / branch `read-foreign-chain`) + +## Motivation + +The MPC network currently signs payloads that are already known to be valid within NEAR. For cross-chain use cases, we need to ensure that a foreign-chain transaction has actually succeeded before the MPC network signs a message derived from it. + +Primary motivations and use cases: + +- Omnibridge inbound flow (foreign chain -> NEAR) where Chain Signatures are required to attest that a foreign transaction finalized successfully. +- Broader chain abstraction efforts where a single MPC network can verify foreign chain state and sign conditional payloads. +- Reduce reliance on centralized oracles by having MPC nodes independently verify foreign-chain transaction status. + +Scope notes from the initial discussion: + +- Start with **foreign transaction status verification** only. +- This enables bridge use cases but does **not** migrate the hot-wallet use case. +- "Hot bridge" can work with this but would require significant refactors on their end. + +## Goals + +- Provide a **contract-level API** to request verification + signing for a foreign transaction. +- Ensure each MPC node independently verifies the transaction using **configured RPC providers**. +- Avoid extra consensus rounds: nodes that cannot verify simply **abstain**. +- Support deterministic provider selection to reduce reliance on any single RPC endpoint. +- Make it easy to extend support to additional foreign chains over time. + +## Non-Goals + +- Provide on-chain light client verification (too heavy for the contract). +- Provide cryptographic proofs of foreign chain state. +- Support non-ECDSA signature schemes for verify_foreign_transaction (initially ECDSA only). +- Support hot-wallet migration or generalized signing conditioned on arbitrary predicates. + +## High-Level Design + +### C4 Context Diagram + +```mermaid +C4Context +title Foreign Chain Transaction Verification - Context + +Person(dev, "Developer / Bridge Service") +System_Boundary(near, "NEAR MPC Network") { + System(mpc_contract, "MPC Signer Contract", "NEAR smart contract") + System(mpc_nodes, "MPC Nodes", "Threshold signing + foreign tx verification") +} +System_Ext(foreign_chain, "Foreign Chain", "Solana, future chains") +System_Ext(rpc_providers, "RPC Providers", "JSON-RPC endpoints") + +Rel(dev, mpc_contract, "verify_foreign_transaction()") +Rel(mpc_nodes, mpc_contract, "respond_verify_foreign_tx()") +Rel(mpc_nodes, rpc_providers, "Query tx status") +Rel(rpc_providers, foreign_chain, "Read chain state") +``` + +### C4 Container Diagram + +```mermaid +C4Container +title Foreign Chain Transaction Verification - Containers + +System_Boundary(near, "NEAR MPC Network") { + Container(mpc_contract, "MPC Signer Contract", "Rust / NEAR", "Stores policy + pending requests; verifies signatures") + + Container(mpc_node, "MPC Node", "Rust", "Indexer + MPC client + foreign verifier") + Container(indexer, "Indexer", "Rust", "Watches contract receipts") + ContainerDb(rocksdb, "RocksDB", "Keyshare + request storage") + + Container(foreign_verifier, "Foreign Chain Verifier", "Rust", "RPC-based verification per chain") +} + +System_Ext(rpc_providers, "RPC Providers", "JSON-RPC / HTTP") +Person(dev, "Developer / Bridge Service") + +Rel(dev, mpc_contract, "verify_foreign_transaction()", "NEAR tx") +Rel(indexer, mpc_contract, "index receipts / state") +Rel(mpc_node, indexer, "block updates") +Rel(mpc_node, foreign_verifier, "verify(tx_id, finality)") +Rel(foreign_verifier, rpc_providers, "getSignatureStatuses / chain-specific RPC") +Rel(mpc_node, rocksdb, "persist requests") +Rel(mpc_node, mpc_contract, "respond_verify_foreign_tx()", "NEAR tx") +``` + +### Core Flow: Verify Foreign Transaction + +```mermaid +sequenceDiagram + participant User as Developer / Bridge + participant Contract as MPC Signer Contract + participant Indexer as Node Indexer + participant MPC as MPC Client + participant Verifier as Foreign Chain Verifier + participant RPC as RPC Provider + + User->>Contract: verify_foreign_transaction(chain, tx_id, finality, path, domain_id?) + Contract->>Contract: validate policy, scheme (ECDSA only), deposit, gas + Contract-->>User: promise yield (return_verify_foreign_tx_on_success) + + Indexer-->>MPC: new VerifyForeignTx request (from receipts) + MPC->>Verifier: verify(chain, tx_id, finality, provider_context) + Verifier->>RPC: query tx status + RPC-->>Verifier: status + block/slot + Verifier-->>MPC: VerificationOutput(success, block_id) + + alt verification succeeds + MPC->>MPC: run MPC signing with payload = sha256(tx_id) + MPC->>Contract: respond_verify_foreign_tx(request, {verified_at_block, signature}) + Contract->>Contract: verify signature against derived payload + Contract-->>User: resolve promise with VerifyForeignTxResponse + else verification fails / not found / not final + MPC-->>Contract: no response (node abstains) + end +``` + +### Core Flow: Foreign Chain Policy Updates (New Chains / Providers) + +```mermaid +sequenceDiagram + participant Node as MPC Node + participant Contract as MPC Signer Contract + + Node->>Contract: get_foreign_chain_policy() + Node->>Node: validate local config against policy + + alt policy differs from local config + Node->>Contract: vote_foreign_chain_policy(proposal from local config) + Contract->>Contract: record vote + Contract-->>Node: ok + Contract->>Contract: if unanimous, update policy + clear votes + else policy matches + Node-->>Node: no vote needed + end +``` + +### Key Components and Responsibilities + +**On-chain (mpc-contract)** + +- New API: + - `verify_foreign_transaction(request)` - stores request, yields a callback. + - `respond_verify_foreign_tx(request, response)` - validates signature + resolves the callback. + - `vote_foreign_chain_policy(proposal)` - unanimous vote to update supported chains/providers. + - `get_foreign_chain_policy()` and `get_foreign_chain_policy_proposals()`. +- Policy gating: + - If policy is empty, verification is **disabled**. + - Request chain must be in policy. + - Policy includes **provider names only** (no secrets). +- Payload derivation: + - `payload = sha256(tx_id)` (ECDSA only). +- Finality levels: + - `Optimistic` (e.g., Solana confirmed) + - `Final` (e.g., Solana finalized) + +**Off-chain (mpc-node)** + +- **ForeignChainVerifierRegistry** + - Dispatches to chain-specific verifiers (initially Solana). + - Uses deterministic provider selection. +- **Foreign Chain Policy Voter** + - On startup: validates local config vs on-chain policy. + - Auto-votes if policy differs from local config. +- **MPC Client** + - Indexes verify_foreign_tx requests. + - Verifies transaction via RPC, then runs MPC signing. + - Responds to contract with `verified_at_block` + signature. +- **Storage** + - `VerifyForeignTxStorage` persists verification requests. + - Atomic write with `SignRequestStorage` to avoid crash inconsistencies. + +### 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/${ALCHEMY_API_KEY}" + quicknode: + rpc_url: "https://your-endpoint.solana-mainnet.quiknode.pro/${QN_API_KEY}" + backup_urls: + - "https://backup.solana.quiknode.pro/${QN_API_KEY}" +``` + +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. + +## Alternatives Considered + +1. **On-chain light clients / proofs** + - Strong security but high on-chain cost and complexity. + - Not practical for multiple chains and frequent requests. + +2. **Dedicated oracle / relayer** + - Simpler operational model but introduces a new trusted party. + +3. **Explicit MPC consensus on verification result** + - Adds a round of agreement on foreign tx status. + - Increases latency and protocol complexity. + +4. **Each node queries multiple providers and cross-checks** + - More robust to bad data but higher latency and RPC costs. + - Could be added later as a hardening option. + +5. **Sign full transaction data or proofs instead of tx_id hash** + - Larger payloads, chain-specific parsing, and validation logic. + - Current design keeps payload small and stable. + +## Open Questions / Follow-ups + +- Should the policy vote threshold stay **unanimous**, or be configurable (e.g., threshold)? +- Should nodes keep a minimum number of independent providers per chain? +- Should we add optional multi-provider verification for high-value requests? +- How do we standardize finality mapping for additional chains (Ethereum, Bitcoin, etc.)? + From ceb06760263471aa018035d1c8211fadbc1634a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 12:30:49 +0100 Subject: [PATCH 06/20] Simplify charts --- docs/foreign_chain_transactions.md | 119 +++++++++++++++++++---------- 1 file changed, 78 insertions(+), 41 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 7a010705f..007bb4f23 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -35,57 +35,92 @@ Scope notes from the initial discussion: ## High-Level Design -### C4 Context Diagram +### System Context Diagram ```mermaid -C4Context -title Foreign Chain Transaction Verification - Context - -Person(dev, "Developer / Bridge Service") -System_Boundary(near, "NEAR MPC Network") { - System(mpc_contract, "MPC Signer Contract", "NEAR smart contract") - System(mpc_nodes, "MPC Nodes", "Threshold signing + foreign tx verification") -} -System_Ext(foreign_chain, "Foreign Chain", "Solana, future chains") -System_Ext(rpc_providers, "RPC Providers", "JSON-RPC endpoints") - -Rel(dev, mpc_contract, "verify_foreign_transaction()") -Rel(mpc_nodes, mpc_contract, "respond_verify_foreign_tx()") -Rel(mpc_nodes, rpc_providers, "Query tx status") -Rel(rpc_providers, foreign_chain, "Read chain state") +--- +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} ``` -### C4 Container Diagram +### Component Diagram ```mermaid -C4Container -title Foreign Chain Transaction Verification - Containers - -System_Boundary(near, "NEAR MPC Network") { - Container(mpc_contract, "MPC Signer Contract", "Rust / NEAR", "Stores policy + pending requests; verifies signatures") - - Container(mpc_node, "MPC Node", "Rust", "Indexer + MPC client + foreign verifier") - Container(indexer, "Indexer", "Rust", "Watches contract receipts") - ContainerDb(rocksdb, "RocksDB", "Keyshare + request storage") - - Container(foreign_verifier, "Foreign Chain Verifier", "Rust", "RPC-based verification per chain") -} - -System_Ext(rpc_providers, "RPC Providers", "JSON-RPC / HTTP") -Person(dev, "Developer / Bridge Service") - -Rel(dev, mpc_contract, "verify_foreign_transaction()", "NEAR tx") -Rel(indexer, mpc_contract, "index receipts / state") -Rel(mpc_node, indexer, "block updates") -Rel(mpc_node, foreign_verifier, "verify(tx_id, finality)") -Rel(foreign_verifier, rpc_providers, "getSignatureStatuses / chain-specific RPC") -Rel(mpc_node, rocksdb, "persist requests") -Rel(mpc_node, mpc_contract, "respond_verify_foreign_tx()", "NEAR tx") +--- +title: Foreign Chain Verification - Components +--- +flowchart TD + DEV["**Developer / Bridge Service** + _Calls verify_foreign_transaction._"] + + SC["**MPC Signer Contract** + _Stores policy + pending requests. + Verifies signatures._"] + + IDX["**Indexer** + _Watches contract receipts._"] + + MPC["**MPC Client** + _Coordinates verification + signing._"] + + VER["**Foreign Chain Verifier** + _RPC-based chain verification._"] + + DB["**RocksDB** + _Request + keyshare storage._"] + + RPC["**RPC Providers** + _Chain JSON-RPC endpoints._"] + + DEV -->|"1. verify_foreign_transaction()"| SC + SC -->|"2. receipts / state"| IDX + IDX -->|"3. block updates"| MPC + MPC -->|"4. verify(tx_id, finality)"| VER + VER -->|"5. chain RPC calls"| RPC + MPC -->|"6. persist requests"| DB + MPC -->|"7. respond_verify_foreign_tx()"| SC + + DEV@{ shape: manual-input} + SC@{ shape: db} + IDX@{ shape: proc} + MPC@{ shape: proc} + VER@{ shape: proc} + DB@{ shape: cylinder} + RPC@{ shape: proc} ``` ### Core Flow: Verify Foreign Transaction ```mermaid +--- +title: Verify Foreign Transaction - Sequence +--- sequenceDiagram participant User as Developer / Bridge participant Contract as MPC Signer Contract @@ -117,6 +152,9 @@ sequenceDiagram ### Core Flow: Foreign Chain Policy Updates (New Chains / Providers) ```mermaid +--- +title: Foreign Chain Policy Updates - Sequence +--- sequenceDiagram participant Node as MPC Node participant Contract as MPC Signer Contract @@ -245,4 +283,3 @@ provider entries in config (including API keys) to satisfy the policy. - Should nodes keep a minimum number of independent providers per chain? - Should we add optional multi-provider verification for high-value requests? - How do we standardize finality mapping for additional chains (Ethereum, Bitcoin, etc.)? - From 7806bdf8912d9eb9cb59631caf16a81b2b20b30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 12:34:33 +0100 Subject: [PATCH 07/20] fix: Simplifications --- docs/foreign_chain_transactions.md | 101 ++++++++--------------------- 1 file changed, 26 insertions(+), 75 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 007bb4f23..6793b118f 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -69,107 +69,58 @@ flowchart TD FC@{ shape: cylinder} ``` -### Component Diagram +### Core Flow: Verify Foreign Transaction ```mermaid --- -title: Foreign Chain Verification - Components +title: Verify Foreign Transaction - High Level --- flowchart TD DEV["**Developer / Bridge Service** - _Calls verify_foreign_transaction._"] + _Submits verification request._"] SC["**MPC Signer Contract** - _Stores policy + pending requests. - Verifies signatures._"] - - IDX["**Indexer** - _Watches contract receipts._"] - - MPC["**MPC Client** - _Coordinates verification + signing._"] - - VER["**Foreign Chain Verifier** - _RPC-based chain verification._"] + _Validates policy + enqueues request._"] - DB["**RocksDB** - _Request + keyshare storage._"] + MPC["**MPC Nodes** + _Verify foreign tx + sign._"] RPC["**RPC Providers** - _Chain JSON-RPC endpoints._"] + _Return tx status._"] DEV -->|"1. verify_foreign_transaction()"| SC - SC -->|"2. receipts / state"| IDX - IDX -->|"3. block updates"| MPC - MPC -->|"4. verify(tx_id, finality)"| VER - VER -->|"5. chain RPC calls"| RPC - MPC -->|"6. persist requests"| DB - MPC -->|"7. respond_verify_foreign_tx()"| SC + SC -->|"2. request observed by nodes"| MPC + MPC -->|"3. verify tx status"| RPC + MPC -->|"4. sign payload = sha256(tx_id)"| MPC + MPC -->|"5. respond_verify_foreign_tx()"| SC + SC -->|"6. resolve promise"| DEV DEV@{ shape: manual-input} SC@{ shape: db} - IDX@{ shape: proc} MPC@{ shape: proc} - VER@{ shape: proc} - DB@{ shape: cylinder} RPC@{ shape: proc} ``` -### Core Flow: Verify Foreign Transaction +### Core Flow: Foreign Chain Policy Updates (New Chains / Providers) ```mermaid --- -title: Verify Foreign Transaction - Sequence +title: Foreign Chain Policy Updates - High Level --- -sequenceDiagram - participant User as Developer / Bridge - participant Contract as MPC Signer Contract - participant Indexer as Node Indexer - participant MPC as MPC Client - participant Verifier as Foreign Chain Verifier - participant RPC as RPC Provider - - User->>Contract: verify_foreign_transaction(chain, tx_id, finality, path, domain_id?) - Contract->>Contract: validate policy, scheme (ECDSA only), deposit, gas - Contract-->>User: promise yield (return_verify_foreign_tx_on_success) - - Indexer-->>MPC: new VerifyForeignTx request (from receipts) - MPC->>Verifier: verify(chain, tx_id, finality, provider_context) - Verifier->>RPC: query tx status - RPC-->>Verifier: status + block/slot - Verifier-->>MPC: VerificationOutput(success, block_id) - - alt verification succeeds - MPC->>MPC: run MPC signing with payload = sha256(tx_id) - MPC->>Contract: respond_verify_foreign_tx(request, {verified_at_block, signature}) - Contract->>Contract: verify signature against derived payload - Contract-->>User: resolve promise with VerifyForeignTxResponse - else verification fails / not found / not final - MPC-->>Contract: no response (node abstains) - end -``` +flowchart TD + NODE["**MPC Node** + _Local config + API keys._"] -### Core Flow: Foreign Chain Policy Updates (New Chains / Providers) + SC["**MPC Signer Contract** + _Foreign chain policy._"] -```mermaid ---- -title: Foreign Chain Policy Updates - Sequence ---- -sequenceDiagram - participant Node as MPC Node - participant Contract as MPC Signer Contract - - Node->>Contract: get_foreign_chain_policy() - Node->>Node: validate local config against policy - - alt policy differs from local config - Node->>Contract: vote_foreign_chain_policy(proposal from local config) - Contract->>Contract: record vote - Contract-->>Node: ok - Contract->>Contract: if unanimous, update policy + clear votes - else policy matches - Node-->>Node: no vote needed - end + NODE -->|"1. read policy"| SC + NODE -->|"2. compare to local config"| NODE + NODE -->|"3. vote if different"| SC + SC -->|"4. update policy on unanimity"| SC + + NODE@{ shape: proc} + SC@{ shape: db} ``` ### Key Components and Responsibilities From 4282c29a39c2de3c19f12ce1c7550292137218f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 13:05:15 +0100 Subject: [PATCH 08/20] Further simplify charts --- docs/foreign_chain_transactions.md | 46 +++++++----------------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 6793b118f..d93109e2a 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -69,38 +69,6 @@ flowchart TD FC@{ shape: cylinder} ``` -### Core Flow: Verify Foreign Transaction - -```mermaid ---- -title: Verify Foreign Transaction - High Level ---- -flowchart TD - DEV["**Developer / Bridge Service** - _Submits verification request._"] - - SC["**MPC Signer Contract** - _Validates policy + enqueues request._"] - - MPC["**MPC Nodes** - _Verify foreign tx + sign._"] - - RPC["**RPC Providers** - _Return tx status._"] - - DEV -->|"1. verify_foreign_transaction()"| SC - SC -->|"2. request observed by nodes"| MPC - MPC -->|"3. verify tx status"| RPC - MPC -->|"4. sign payload = sha256(tx_id)"| MPC - MPC -->|"5. respond_verify_foreign_tx()"| SC - SC -->|"6. resolve promise"| DEV - - DEV@{ shape: manual-input} - SC@{ shape: db} - MPC@{ shape: proc} - RPC@{ shape: proc} -``` - ### Core Flow: Foreign Chain Policy Updates (New Chains / Providers) ```mermaid @@ -114,13 +82,21 @@ flowchart TD 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 to local config"| NODE - NODE -->|"3. vote if different"| SC - SC -->|"4. update policy on unanimity"| 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} ``` ### Key Components and Responsibilities From ad5caebbc1d65b8e728c3984c0af758d162a7cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 13:12:29 +0100 Subject: [PATCH 09/20] Purpose & motivation update --- docs/foreign_chain_transactions.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index d93109e2a..1590f3018 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -2,9 +2,13 @@ Status: Draft (based on PR #1851 / branch `read-foreign-chain`) +## Purpose + +This document describes the design for a proposed feature that allows the MPC network to sign payloads attesting to the presence of transactions on other chains. + ## Motivation -The MPC network currently signs payloads that are already known to be valid within NEAR. For cross-chain use cases, we need to ensure that a foreign-chain transaction has actually succeeded before the MPC network signs a message derived from it. +The MPC network supports signing arbitrary payloads for NEAR users. This allows NEAR contracts to manage custody of funds on other chains. However, there is no way for these contracts to respond to or interact with events happening on other chains (for example, proving that a specific foreign-chain transaction finalized). This feature adds a way to request signatures that are conditional on foreign-chain transaction status. Primary motivations and use cases: From b681d7e715a23e460fc4ead7fd67368d61dd4c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 13:19:32 +0100 Subject: [PATCH 10/20] Remove more slop --- docs/foreign_chain_transactions.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 1590f3018..fececc818 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -16,12 +16,6 @@ Primary motivations and use cases: - Broader chain abstraction efforts where a single MPC network can verify foreign chain state and sign conditional payloads. - Reduce reliance on centralized oracles by having MPC nodes independently verify foreign-chain transaction status. -Scope notes from the initial discussion: - -- Start with **foreign transaction status verification** only. -- This enables bridge use cases but does **not** migrate the hot-wallet use case. -- "Hot bridge" can work with this but would require significant refactors on their end. - ## Goals - Provide a **contract-level API** to request verification + signing for a foreign transaction. @@ -35,7 +29,6 @@ Scope notes from the initial discussion: - Provide on-chain light client verification (too heavy for the contract). - Provide cryptographic proofs of foreign chain state. - Support non-ECDSA signature schemes for verify_foreign_transaction (initially ECDSA only). -- Support hot-wallet migration or generalized signing conditioned on arbitrary predicates. ## High-Level Design From ca299769a9eb072573849eaf46c9c0abf4a82eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 13:51:50 +0100 Subject: [PATCH 11/20] More simplifications and updates --- docs/foreign_chain_transactions.md | 75 ++++++++++++------------------ 1 file changed, 29 insertions(+), 46 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index fececc818..768d80020 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -2,33 +2,17 @@ Status: Draft (based on PR #1851 / branch `read-foreign-chain`) -## Purpose +## Purpose & Motivation -This document describes the design for a proposed feature that allows the MPC network to sign payloads attesting to the presence of transactions on other chains. - -## Motivation - -The MPC network supports signing arbitrary payloads for NEAR users. This allows NEAR contracts to manage custody of funds on other chains. However, there is no way for these contracts to respond to or interact with events happening on other chains (for example, proving that a specific foreign-chain transaction finalized). This feature adds a way to request signatures that are conditional on foreign-chain transaction status. - -Primary motivations and use cases: +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 efforts where a single MPC network can verify foreign chain state and sign conditional payloads. -- Reduce reliance on centralized oracles by having MPC nodes independently verify foreign-chain transaction status. - -## Goals - -- Provide a **contract-level API** to request verification + signing for a foreign transaction. -- Ensure each MPC node independently verifies the transaction using **configured RPC providers**. -- Avoid extra consensus rounds: nodes that cannot verify simply **abstain**. -- Support deterministic provider selection to reduce reliance on any single RPC endpoint. -- Make it easy to extend support to additional foreign chains over time. +- Broader chain abstraction: a single MPC network verifies foreign chain state and signs conditional payloads. -## Non-Goals +## Scope -- Provide on-chain light client verification (too heavy for the contract). -- Provide cryptographic proofs of foreign chain state. -- Support non-ECDSA signature schemes for verify_foreign_transaction (initially ECDSA only). +- 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). ## High-Level Design @@ -101,8 +85,8 @@ flowchart TD **On-chain (mpc-contract)** - New API: - - `verify_foreign_transaction(request)` - stores request, yields a callback. - - `respond_verify_foreign_tx(request, response)` - validates signature + resolves the callback. + - `verify_foreign_transaction(request)` - stores request, yields a callback. Request includes `chain`, `tx_id`, `finality`, `path`, and optional `domain_id`. + - `respond_verify_foreign_tx(request, response)` - validates signature + resolves the callback. Response includes `verified_at_block` and the signature. - `vote_foreign_chain_policy(proposal)` - unanimous vote to update supported chains/providers. - `get_foreign_chain_policy()` and `get_foreign_chain_policy_proposals()`. - Policy gating: @@ -110,7 +94,9 @@ flowchart TD - Request chain must be in policy. - Policy includes **provider names only** (no secrets). - Payload derivation: - - `payload = sha256(tx_id)` (ECDSA only). + - `payload = sha256(tx_id_bytes)` (ECDSA only). + - For Solana, `tx_id` is a base58 signature in JSON, but the hash uses the raw 64-byte signature bytes. + - Tweak derivation: `tweak = derive_tweak(predecessor_account_id, path)`. - Finality levels: - `Optimistic` (e.g., Solana confirmed) - `Final` (e.g., Solana finalized) @@ -131,6 +117,23 @@ flowchart TD - `VerifyForeignTxStorage` persists verification requests. - Atomic write with `SignRequestStorage` to avoid crash inconsistencies. +### Request/Response Summary (Contract) + +``` +verify_foreign_transaction({ + chain, tx_id, finality, path, domain_id? +}) -> promise (callback on success) + +respond_verify_foreign_tx({ + request, response: { verified_at_block, signature } +}) +``` + +### 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. + ### Deterministic Provider Selection Each node selects a provider using a deterministic hash of: @@ -180,30 +183,10 @@ provider entries in config (including API keys) to satisfy the policy. - **Operational friction**: Unanimous voting for policy updates may slow rollouts and hot fixes. - **Config drift**: Nodes missing required provider keys will fail startup validation. -## Alternatives Considered - -1. **On-chain light clients / proofs** - - Strong security but high on-chain cost and complexity. - - Not practical for multiple chains and frequent requests. - -2. **Dedicated oracle / relayer** - - Simpler operational model but introduces a new trusted party. - -3. **Explicit MPC consensus on verification result** - - Adds a round of agreement on foreign tx status. - - Increases latency and protocol complexity. - -4. **Each node queries multiple providers and cross-checks** - - More robust to bad data but higher latency and RPC costs. - - Could be added later as a hardening option. - -5. **Sign full transaction data or proofs instead of tx_id hash** - - Larger payloads, chain-specific parsing, and validation logic. - - Current design keeps payload small and stable. - ## Open Questions / Follow-ups - Should the policy vote threshold stay **unanimous**, or be configurable (e.g., threshold)? - Should nodes keep a minimum number of independent providers per chain? - Should we add optional multi-provider verification for high-value requests? - How do we standardize finality mapping for additional chains (Ethereum, Bitcoin, etc.)? +- Startup validation: when policy is empty, nodes skip config validation and can still boot/vote an initial policy. Is this the desired operational behavior? From b73462705e7944471d15803d06b1d9e3bc1ba9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 14:08:09 +0100 Subject: [PATCH 12/20] docs: Elaborate on key types --- docs/foreign_chain_transactions.md | 63 ++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 768d80020..05bb5f7a5 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -96,10 +96,8 @@ flowchart TD - Payload derivation: - `payload = sha256(tx_id_bytes)` (ECDSA only). - For Solana, `tx_id` is a base58 signature in JSON, but the hash uses the raw 64-byte signature bytes. - - Tweak derivation: `tweak = derive_tweak(predecessor_account_id, path)`. -- Finality levels: - - `Optimistic` (e.g., Solana confirmed) - - `Final` (e.g., Solana finalized) + - The signed key is derived from the domain key and `tweak`. +- Chain-specific verification parameters (e.g., Solana finality or Bitcoin confirmations). **Off-chain (mpc-node)** @@ -117,18 +115,52 @@ flowchart TD - `VerifyForeignTxStorage` persists verification requests. - Atomic write with `SignRequestStorage` to avoid crash inconsistencies. -### Request/Response Summary (Contract) - +### New public types + +```rust +pub struct VerifyForeignTxRequest { + pub chain: ForeignChain, + pub tx_id: TransactionId, + pub tweak: Tweak, + pub domain_id: DomainId, +} + +pub enum ForeignChain { + Solana(SolanaConfig), + Bitcoin(BitcoinConfig), + // Future chains... +} + +pub struct SolanaConfig { + pub finality: SolanaFinality, // Optimistic or Final +} + +pub enum SolanaFinality { + Optimistic, + Final, +} + +pub struct BitcoinConfig { + pub confirmations: usize, // required confirmations before considering final +} + +pub struct VerifyForeignTxResponse { + pub verified_at_block: BlockId, + pub signature: SignatureResponse, +} ``` -verify_foreign_transaction({ - chain, tx_id, finality, path, domain_id? -}) -> promise (callback on success) -respond_verify_foreign_tx({ - request, response: { verified_at_block, signature } -}) +```rust +verify_foreign_transaction(request) -> promise (callback on success) +respond_verify_foreign_tx({ request, response }) ``` +**What is signed and over what key** + +- Payload is `sha256(tx_id_bytes)`, where `tx_id_bytes` are chain-native bytes (e.g., Solana 64-byte signature). +- Signature is ECDSA over that payload using the domain key derived with `tweak` (i.e., the derived key for `domain_id` + `tweak`). +- `tweak` should be derived deterministically (prototype uses `derive_tweak(predecessor_account_id, path)`), unless we explicitly move to passing raw tweaks. + ### Failure and Timeout Behavior - Nodes **abstain** if verification fails (RPC error, tx not found, or not finalized). @@ -183,10 +215,7 @@ provider entries in config (including API keys) to satisfy the policy. - **Operational friction**: Unanimous voting for policy updates may slow rollouts and hot fixes. - **Config drift**: Nodes missing required provider keys will fail startup validation. -## Open Questions / Follow-ups - +## Discussion points +- Finality interface right now diverges from the original PR. Are we okay with this new structure? - Should the policy vote threshold stay **unanimous**, or be configurable (e.g., threshold)? -- Should nodes keep a minimum number of independent providers per chain? -- Should we add optional multi-provider verification for high-value requests? -- How do we standardize finality mapping for additional chains (Ethereum, Bitcoin, etc.)? - Startup validation: when policy is empty, nodes skip config validation and can still boot/vote an initial policy. Is this the desired operational behavior? From f7a38c52c79c95950b3cf34af4da27301405829c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 14:16:58 +0100 Subject: [PATCH 13/20] Some minor updates --- docs/foreign_chain_transactions.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 05bb5f7a5..7e9e9e8d5 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -115,10 +115,24 @@ flowchart TD - `VerifyForeignTxStorage` persists verification requests. - Atomic write with `SignRequestStorage` to avoid crash inconsistencies. -### New public types +### New contract methods +```rust +verify_foreign_transaction(request: VerifyForeignTxRequestArgs) -> VerifyForeignTxResponse // Through a promise +respond_verify_foreign_tx({ request, response }) // Respond method for signers +``` + +### New contract types ```rust +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: ForeignChain, pub tx_id: TransactionId, pub tweak: Tweak, @@ -150,11 +164,6 @@ pub struct VerifyForeignTxResponse { } ``` -```rust -verify_foreign_transaction(request) -> promise (callback on success) -respond_verify_foreign_tx({ request, response }) -``` - **What is signed and over what key** - Payload is `sha256(tx_id_bytes)`, where `tx_id_bytes` are chain-native bytes (e.g., Solana 64-byte signature). From 1c159bea7ae544018678db6441ed21a38a5830ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 14:36:29 +0100 Subject: [PATCH 14/20] More readability improvements --- docs/foreign_chain_transactions.md | 151 ++++++++++++++++------------- 1 file changed, 82 insertions(+), 69 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 7e9e9e8d5..ea1d1760c 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -14,9 +14,15 @@ This feature lets the MPC network sign payloads only after verifying a specific - 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). -## High-Level Design +## Overview -### System Context Diagram +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 --- @@ -50,79 +56,16 @@ flowchart TD FC@{ shape: cylinder} ``` -### Core Flow: Foreign Chain 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._"] +With the user flow in mind, the on-chain interface is: - COMP["**Compare** - _Local config vs policy._"] +### Contract Interface (Request/Response) - 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} -``` - -### Key Components and Responsibilities - -**On-chain (mpc-contract)** - -- New API: - - `verify_foreign_transaction(request)` - stores request, yields a callback. Request includes `chain`, `tx_id`, `finality`, `path`, and optional `domain_id`. - - `respond_verify_foreign_tx(request, response)` - validates signature + resolves the callback. Response includes `verified_at_block` and the signature. - - `vote_foreign_chain_policy(proposal)` - unanimous vote to update supported chains/providers. - - `get_foreign_chain_policy()` and `get_foreign_chain_policy_proposals()`. -- Policy gating: - - If policy is empty, verification is **disabled**. - - Request chain must be in policy. - - Policy includes **provider names only** (no secrets). -- Payload derivation: - - `payload = sha256(tx_id_bytes)` (ECDSA only). - - For Solana, `tx_id` is a base58 signature in JSON, but the hash uses the raw 64-byte signature bytes. - - The signed key is derived from the domain key and `tweak`. -- Chain-specific verification parameters (e.g., Solana finality or Bitcoin confirmations). - -**Off-chain (mpc-node)** - -- **ForeignChainVerifierRegistry** - - Dispatches to chain-specific verifiers (initially Solana). - - Uses deterministic provider selection. -- **Foreign Chain Policy Voter** - - On startup: validates local config vs on-chain policy. - - Auto-votes if policy differs from local config. -- **MPC Client** - - Indexes verify_foreign_tx requests. - - Verifies transaction via RPC, then runs MPC signing. - - Responds to contract with `verified_at_block` + signature. -- **Storage** - - `VerifyForeignTxStorage` persists verification requests. - - Atomic write with `SignRequestStorage` to avoid crash inconsistencies. - -### New contract methods ```rust +// Contract methods verify_foreign_transaction(request: VerifyForeignTxRequestArgs) -> VerifyForeignTxResponse // Through a promise respond_verify_foreign_tx({ request, response }) // Respond method for signers ``` -### New contract types - ```rust pub struct VerifyForeignTxRequestArgs { pub chain: ForeignChain, @@ -164,7 +107,15 @@ pub struct VerifyForeignTxResponse { } ``` -**What is signed and over what key** +**Contract behavior** + +- Policy gating: + - If policy is empty, verification is **disabled**. + - Request chain must be in policy. + - Policy includes **provider names only** (no secrets). +- Verification parameters are chain-specific (e.g., Solana finality or Bitcoin confirmations). + +**Signing semantics** - Payload is `sha256(tx_id_bytes)`, where `tx_id_bytes` are chain-native bytes (e.g., Solana 64-byte signature). - Signature is ECDSA over that payload using the domain key derived with `tweak` (i.e., the derived key for `domain_id` + `tweak`). @@ -175,6 +126,68 @@ pub struct VerifyForeignTxResponse { - 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) + +```rust +pub struct ForeignChainPolicy { + pub chains: Vec, +} + +pub struct ForeignChainEntry { + pub chain: ForeignChain, + pub required_providers: Vec, +} + +pub struct RpcProviderName(pub String); + +pub struct ForeignChainPolicyVotes { + // Each authenticated participant has one active vote for a proposal. + pub proposal_by_account: BTreeMap, +} +``` + +### 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: From f804f06d0224304ff220e939ddf26091f3c07bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 14:45:00 +0100 Subject: [PATCH 15/20] docs: Document contract state --- docs/foreign_chain_transactions.md | 51 ++++++++++++++++++------------ 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index ea1d1760c..7d505af21 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -76,13 +76,13 @@ pub struct VerifyForeignTxRequestArgs { pub struct VerifyForeignTxRequest { // Constructed from the args - pub chain: ForeignChain, + pub chain: ForeignChainConfig, pub tx_id: TransactionId, pub tweak: Tweak, pub domain_id: DomainId, } -pub enum ForeignChain { +pub enum ForeignChainConfig { Solana(SolanaConfig), Bitcoin(BitcoinConfig), // Future chains... @@ -107,6 +107,34 @@ pub struct VerifyForeignTxResponse { } ``` +### 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: Vec, +} + +pub struct ForeignChainProviders { + pub chain: ForeignChain, + pub providers: Vec, +} + +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, +} +``` + **Contract behavior** - Policy gating: @@ -159,24 +187,7 @@ flowchart TD ``` ### Contract Policy State (Types) - -```rust -pub struct ForeignChainPolicy { - pub chains: Vec, -} - -pub struct ForeignChainEntry { - pub chain: ForeignChain, - pub required_providers: Vec, -} - -pub struct RpcProviderName(pub String); - -pub struct ForeignChainPolicyVotes { - // Each authenticated participant has one active vote for a proposal. - pub proposal_by_account: BTreeMap, -} -``` +See "Contract state (Foreign Chain Policy)" above. ### Node Configuration and Policy Updates From 43fd020557fa91b9631349b51c48c8b4b40513a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 14:48:44 +0100 Subject: [PATCH 16/20] clear out some slop --- docs/foreign_chain_transactions.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 7d505af21..8893d1d35 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -103,7 +103,7 @@ pub struct BitcoinConfig { pub struct VerifyForeignTxResponse { pub verified_at_block: BlockId, - pub signature: SignatureResponse, + pub signature: SignatureResponse, // Signature over `sha256(tx_id_bytes)` where `tx_id_bytes` are chain-native bytes (e.g., Solana 64-byte signature). } ``` @@ -135,20 +135,6 @@ pub struct ForeignChainPolicyVotes { } ``` -**Contract behavior** - -- Policy gating: - - If policy is empty, verification is **disabled**. - - Request chain must be in policy. - - Policy includes **provider names only** (no secrets). -- Verification parameters are chain-specific (e.g., Solana finality or Bitcoin confirmations). - -**Signing semantics** - -- Payload is `sha256(tx_id_bytes)`, where `tx_id_bytes` are chain-native bytes (e.g., Solana 64-byte signature). -- Signature is ECDSA over that payload using the domain key derived with `tweak` (i.e., the derived key for `domain_id` + `tweak`). -- `tweak` should be derived deterministically (prototype uses `derive_tweak(predecessor_account_id, path)`), unless we explicitly move to passing raw tweaks. - ### Failure and Timeout Behavior - Nodes **abstain** if verification fails (RPC error, tx not found, or not finalized). From 12e973da7c619f0664fce2dc73983d0ac7d3b9d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 14:50:00 +0100 Subject: [PATCH 17/20] Small touch --- docs/foreign_chain_transactions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 8893d1d35..84ab4a8c0 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -67,6 +67,7 @@ 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 From 5bda32465dc0e55b91cc7ce4afade5a634382d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 15:16:26 +0100 Subject: [PATCH 18/20] Design update --- docs/foreign_chain_transactions.md | 37 ++++++++++++++++-------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 84ab4a8c0..7101d4533 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -77,34 +77,36 @@ pub struct VerifyForeignTxRequestArgs { pub struct VerifyForeignTxRequest { // Constructed from the args - pub chain: ForeignChainConfig, + pub chain: ForeignChainRpcRequest, pub tx_id: TransactionId, pub tweak: Tweak, pub domain_id: DomainId, } -pub enum ForeignChainConfig { - Solana(SolanaConfig), - Bitcoin(BitcoinConfig), - // Future chains... +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 struct SolanaConfig { - pub finality: SolanaFinality, // Optimistic or Final +pub enum ForeignChainRpcRequest { + Solana(SolanaRpcRequest), + Bitcoin(BitcoinRpcRequest), + // Future chains... } -pub enum SolanaFinality { - Optimistic, - Final, +pub struct SolanaRpcRequest { + pub tx_id: SolanaTxId, + pub finality: Finality, // Optimistic or Final } -pub struct BitcoinConfig { +pub struct BitcoinRpcRequest { + pub tx_id: BitcoinTxId, pub confirmations: usize, // required confirmations before considering final } -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 Finality{ + Optimistic, + Final, } ``` @@ -114,12 +116,12 @@ The contract maintains a *foreign chain policy* that defines which chains and RP ```rust pub struct ForeignChainPolicy { - pub chains: Vec, + pub chains: BTreeSet, } -pub struct ForeignChainProviders { +pub struct ForeignChainConfig { pub chain: ForeignChain, - pub providers: Vec, + pub providers: NonEmptyVec, } pub enum ForeignChain { @@ -237,5 +239,6 @@ provider entries in config (including API keys) to satisfy the policy. ## Discussion points - Finality interface right now diverges from the original PR. Are we okay with this new structure? +- 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? From 5ef5cfbf65aa6201ccaf5ee657fc39f4852ed662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 15:22:52 +0100 Subject: [PATCH 19/20] Expand on API keys --- docs/foreign_chain_transactions.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 7101d4533..3581260f4 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -215,11 +215,13 @@ foreign_chains: max_retries: 3 providers: alchemy: - rpc_url: "https://solana-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}" + 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/${QN_API_KEY}" - backup_urls: - - "https://backup.solana.quiknode.pro/${QN_API_KEY}" + rpc_url: "https://your-endpoint.solana-mainnet.quiknode.pro/" + api_key: + val: "" ``` The contract policy references providers by **name**, and nodes must have matching @@ -239,6 +241,7 @@ provider entries in config (including API keys) to satisfy the policy. ## Discussion points - 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? From 3889265bd3bd7758ec1eaaf51c9496344626dbc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 30 Jan 2026 15:33:05 +0100 Subject: [PATCH 20/20] Another discussion question --- docs/foreign_chain_transactions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/foreign_chain_transactions.md b/docs/foreign_chain_transactions.md index 3581260f4..7d81564c9 100644 --- a/docs/foreign_chain_transactions.md +++ b/docs/foreign_chain_transactions.md @@ -240,6 +240,8 @@ provider entries in config (including API keys) to satisfy the policy. - **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?