Post-quantum keys

Sign NEAR transactions with post-quantum ML-DSA-65 keys.

A sufficiently large quantum computer would break the elliptic-curve signatures (Ed25519, secp256k1) that protect blockchain accounts today. NEAR's answer is ML-DSA-65 — the NIST FIPS 204 lattice signature scheme also known as Dilithium — added as an account access-key and transaction-signing type starting at protocol version 85. FastNear ships it opt-in in the standalone @fastnear/ml-dsa-65 package (Node.js 20.19+ or a modern browser), so apps that only use classical keys never download the post-quantum implementation.

How enrollment works
  • Generate an in-memory signer with generateSigner() — keys never touch disk unless you export them, and signer.destroy() zeroizes package-owned buffers when the lifecycle ends.
  • Enroll the signer's full public key with a classical full-access AddKey — enrollment always starts from an existing classical signer.
  • Sign and submit through sendTx({ signerId, signer, … }) in @fastnear/api; the signer is bridged in via a structural TransactionSigner, and queryProtocolVersion() gates on v85.
Two key forms
  • Full form ml-dsa-65:<base58> — use it for AddKey, direct access-key lookup, signing, and DeleteKey.
  • Compact handle ml-dsa-65-hash:<base58> — what NEAR stores on-chain and what access-key list responses expose: the SHA3-256 digest of a domain tag (near:ml-dsa-65-pubkey-hash:v1) followed by the raw public key.
  • Reconcile the two with publicKeyToHandle() before comparing against list responses.
Wire facts
  • Seed: 32 bytes. Public key: 1,952 bytes. Expanded secret key: 4,032 bytes. Signature: 3,309 bytes.
  • NEAR charges 100 Ggas for each outer or delegated ML-DSA-65 signature verification, so transactions are substantially larger and costlier than classical equivalents.
  • Scope: account access keys and transaction signatures only — validator and staking keys remain Ed25519.
Quickstarts

Generate a signer, then send with it.

The two core flows from the catalog (ESM). The full set — generate, explicit-send, enroll-delete, reconcile — lives in the mlDsa65 key of recipes.json.

Generate an in-memory signer

Retain only public recovery metadata, and always destroy the signer when its lifecycle ends.

import { generateSigner } from "@fastnear/ml-dsa-65";

const signer = generateSigner();

try {
  // Public values are safe to retain for enrollment and cleanup.
  const recovery = {
    network: "testnet",
    accountId: "device.testnet",
    publicKey: signer.publicKey,
    publicKeyHandle: signer.publicKeyHandle,
  };

  console.log(recovery);
  // Never log or persist signer.exportSeed() or signer.exportSecretKey().
} finally {
  signer.destroy();
}
Send with an enrolled signer

The explicit-signer branch of sendTx, after the full public key has been enrolled on the account with a classical full-access AddKey.

import {
  actions,
  queryProtocolVersion,
  sendTx,
} from "@fastnear/api";

export async function sendOneYoctoWithMlDsa65({ accountId, signer }) {
  const protocolVersion = await queryProtocolVersion({ network: "testnet" });
  if (protocolVersion < 85) {
    throw new Error(`testnet protocol ${protocolVersion} does not support ML-DSA-65`);
  }

  return sendTx({
    signerId: accountId,
    signer,
    receiverId: accountId,
    actions: [actions.transfer("1")],
    waitUntil: "FINAL",
    network: "testnet",
  });
}
Safety rules

Treat key material and activation signals strictly.

These rules come from the machine-readable catalog — agents and humans should apply the same ones.

Activation and lifecycle
  • Check the selected RPC's active protocol_version and require 85 or later before adding or using an ML-DSA-65 key; do not use node software versions or latest_protocol_version as activation signals.
  • Never print or persist generated seeds or expanded secret keys. Keep a temporary recovery record public-only: network, account ID, full public key, and hash handle.
  • After an AddKey attempt, do not trust a single absence read: submit a finalized classical DeleteKey nonce barrier, confirm absence at finality, and only then remove the record.
Backend and runtime
  • The reference backend (@noble/post-quantum) describes itself as self-audited and does not claim constant-time side-channel protection — prefer a native, WASM, HSM, or hardware TransactionSigner when the threat model requires one.
  • destroy() provides best-effort zeroization of package-owned JavaScript buffers, not a hard memory-erasure guarantee.
  • Constrained QuickJS and MCU runtimes are not a v1 compatibility target.
Go deeper

AI agents: read the mlDsa65 key in recipes.json — it carries all four quickstarts plus the sizes, key forms, and safety rules on this page. The scheme is defined in NIST FIPS 204, and the package lives on npm.