Near Learn

How Chain Signatures work

Inside NEAR Chain Signatures: the v1.signer MPC contract, key derivation per account and path, secp256k1 and ed25519 domains, yield/resume, deposit and gas.

Advanced7 min read3-question check

Chain Signatures let any NEAR account, including a smart contract, get signatures from a key that no single machine holds. A network of MPC nodes holds shares of one root key per signature scheme. Every NEAR account gets its own child keys, derived from the root key, its account ID and a free-form path string. Only that account can ask for signatures with them.

You never talk to the nodes. You call one contract and wait for the answer. Official docs: What are Chain Signatures?; contract and node source: near/mpc.

The moving parts#

PartDetails (checked October 2026)
Signer contractMainnet v1.signer, testnet v1.signer-prod.testnet. Receives sign requests, verifies the nodes’ answers, returns the signature to the caller.
MPC nodesWatch the contract, run the threshold signing protocol and call respond. The live state view on both networks lists 17 participants with a threshold of 11 (some docs pages still say 8 nodes; the contract is the source of truth). Nodes also submit TEE attestations that the contract checks.
Root keysOne per *domain*. Read them with the public_key view (domain_id argument, default 0).
DerivationA child key per (calling account, path), computed from the root public key, so anyone can derive the address offline.

Domains: picking a signature scheme#

A request names a domain_id, not just a curve. The registry on mainnet and testnet (from the contract’s state view, October 2026):

domain_idProtocolPurposeUse it for
0CaitSith (ECDSA secp256k1)SignBitcoin, Ethereum and all EVM chains, Cosmos, XRP, Dogecoin…
1FROST (EdDSA ed25519)SignSolana, Aptos, Sui, TON, Stellar, NEAR
2Confidential Key Derivation (BLS12-381)CKDDeterministic app secrets via request_app_private_key, not signing
3CaitSith (secp256k1)ForeignTxverify_foreign_transaction: nodes check a foreign-chain transaction before signing. Newer feature; read the design doc before relying on it

Derivation: one root key, a child per account and path#

The contract computes a 32-byte tweak from the caller and the path, then adds tweak · G to the root public key. The caller is env::predecessor_account_id() of the sign call, so a path is only meaningful together with the account that calls: alice.near + ethereum-1 and bob.near + ethereum-1 are unrelated keys, and nobody but alice.near can sign for the first one.

MoreThe exact derivation formula
  • tweak = sha3_256("near-mpc-recovery v0.1.0 epsilon derivation:" + predecessor_id + "," + path)
  • secp256k1: child_pk = root_pk + tweak · G (tweak read as a big-endian scalar)
  • ed25519: child_pk = root_pk + (tweak mod L) · B (tweak read little-endian)

This is additive key derivation: the nodes sign with root_sk + tweak, which needs no new key generation per account. Source: derive_tweak in crates/near-mpc-crypto-types/src/kdf.rs of near/mpc, mirrored by deriveChildPublicKey in chainsig.js.

The sign request#

Arguments of v1.signer `sign` (current wire format)
JSON
{
  "request": {
    "path": "ethereum-1",
    "payload_v2": { "Ecdsa": "<64 hex chars: the 32-byte hash to sign>" },
    "domain_id": 0
  }
}

// ed25519: the payload is the message itself (up to 1232 bytes), not a hash
{
  "request": {
    "path": "solana-1",
    "payload_v2": { "Eddsa": "<hex of the serialized Solana message>" },
    "domain_id": 1
  }
}
RuleDetail
DepositAt least 1 yoctoNEAR (SIGN_DEPOSIT_YOCTONEAR = 1); any excess is refunded to the caller.
GasPrepaid gas on the sign receipt must be at least sign_call_gas_attachment_requirement_tera_gas from the config view: 15 Tgas in October 2026. Attaching more is fine; unused gas is refunded.
ECDSA payloadExactly 32 bytes and a valid secp256k1 scalar. The contract does not hash it: you pass the chain-specific hash (keccak256 of the unsigned EVM tx, the BIP-143 sighash for Bitcoin…).
Legacy formatOld clients send payload (raw 32-byte array) and key_version. Both are still accepted as aliases of payload_v2 and domain_id, but never in the same request.
TimeoutThe request waits at most the protocol yield timeout, 200 blocks. Then the call fails with Request has timed out.

Yield and resume#

sign does not return immediately. It validates the request, stores it as pending and yields: it creates a promise that will be resumed with data later (NEAR’s yield/resume host functions). The nodes see the pending request, sign, and call respond. respond checks the signature against the derived public key and resumes the yielded promise, whose callback returns the signature as the result of your original sign call.

From the caller’s point of view it is just a slow cross-contract call: a few seconds to a few blocks. A user transaction gets its result in the final outcome; a contract gets it in its callback (next lessons).

Return value of `sign`, per scheme (JSON, `scheme` is the tag)
JSON
{
  "scheme": "Secp256k1",
  "big_r": { "affine_point": "02a1…(33-byte compressed point, hex)" },
  "s": { "scalar": "4f1c…(32 bytes, hex)" },
  "recovery_id": 0
}

{
  "scheme": "Ed25519",
  "signature": [12, 201, 7, "… 64 bytes as a JSON array of numbers"]
}

For ECDSA, r is the x coordinate of big_r (drop the first byte of the compressed point), s is s.scalar, and the recovery bit is recovery_id (EVM yParity; legacy v = recovery_id + 27). chainsig.js does this conversion for you.

Check yourself

3 questions · progress saved in this browser

  1. 1.Who can request a signature for the key derived from alice.near + path btc-1?
  2. 2.Which domain_id do you use to sign a Solana transaction?
  3. 3.You pass the raw RLP bytes of an unsigned EVM transaction as an Ecdsa payload. What happens?