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}