Common MPC Pitfalls

NEAR MPC `DomainPurpose` tagging

The NEAR MPC node exposes a threshold key to three different methods on the contract: sign() for arbitrary user-supplied payloads, verify_foreign_transaction() for foreign-chain (Bitcoin, Ethereum) transaction attestation used by bridges, and request_app_private_key() for confidential key derivation (CKD). All three call paths can route to the same set of distributed keys. Before the fix, the contract enforced only that the curve matched the call: any Secp256k1 key could back either sign() or verify_foreign_transaction(). A caller could therefore submit a foreign-chain transaction payload to the generic sign() method, collect a threshold signature, and then replay it to a bridge calling verify_foreign_transaction() against the same key; the bridge would accept the signature as proof that the foreign transaction had been attested.

The fix (PR #2163) introduces an explicit per-domain DomainPurpose enum:

 1// FILE: crates/contract-interface/src/types/state.rs — near/mpc (after PR #2163)
 2pub enum DomainPurpose {
 3    /// Domain is used by `sign()`.
 4    Sign,
 5    /// Domain is used by `verify_foreign_transaction()`.
 6    ForeignTx,
 7    /// Domain is used by `request_app_private_key()` (Confidential Key Derivation).
 8    CKD,
 9}
10
11pub struct DomainConfig {
12    pub id: DomainId,
13    pub scheme: SignatureScheme,
14    pub purpose: Option<DomainPurpose>, // new: purpose tag per domain
15}

Each contract entry-point now requires the target domain to carry the matching purpose (crates/contract/src/lib.rs):

 1// FILE: crates/contract/src/lib.rs — near/mpc (after PR #2163)
 2
 3// in sign(...)
 4if domain_config.purpose != DomainPurpose::Sign {
 5    env::panic_str(
 6        &InvalidParameters::WrongDomainPurpose { /* ... */ }
 7            .message("sign() may only target domains with purpose Sign")
 8            .to_string(),
 9    );
10}
11
12// in verify_foreign_transaction(...)
13if domain_config.purpose != DomainPurpose::ForeignTx {
14    env::panic_str(
15        &InvalidParameters::WrongDomainPurpose { /* ... */ }
16            .message("verify_foreign_transaction() requires a domain with purpose ForeignTx")
17            .to_string(),
18    );
19}
20
21// in request_app_private_key(...)
22if domain_config.purpose != DomainPurpose::CKD {
23    env::panic_str(
24        &InvalidParameters::WrongDomainPurpose { /* ... */ }
25            .message("request_app_private_key() may only target domains with purpose CKD")
26            .to_string(),
27    );
28}