--- NEP: 645 Title: ML-DSA-65 Post-Quantum Transaction Signatures Authors: Adam Wierzbicki Status: Draft DiscussionsTo: https://github.com/near/NEPs/pull/645 Type: Protocol Version: 1.0.0 Created: 2026-06-29 LastUpdated: 2026-06-29 --- ## Summary This proposal adds a third transaction-signature scheme to NEAR: **ML-DSA-65**, the lattice-based post-quantum digital signature algorithm standardized by NIST in [FIPS 204] (2024). It is offered alongside the existing Ed25519 and ECDSA-over-secp256k1 schemes, which are currently NOT deprecated. After the `PostQuantumSignatures` protocol feature activates (stabilized at protocol version 85, shipped in the neard 2.13 release), an account may add an ML-DSA-65 access key and sign transactions, `DelegateAction`s, and `AddKey`/`DeleteKey` actions with it. The scope of this first milestone is deliberately narrow: **transaction signing and access keys only**. Validator keys, block/chunk signatures, on-chain verification host functions, and implicit-account derivation are out of scope and remain unchanged. To keep the cost of the large ML-DSA-65 public key (1952 bytes) from inflating state, access keys are stored on-trie by a 32-byte SHA3-256 hash of the public key rather than the full key. The full public key travels on the wire in the transaction, where it is needed for verification. The extra verification work is priced as additional gas burnt at transaction conversion, so post-quantum signers pay for the cost they add while classical-scheme users are unaffected. ## Motivation NEAR's two existing signature schemes, Ed25519 and ECDSA over the secp256k1 curve (used by ETH-style accounts), both rely on the hardness of the elliptic-curve discrete-logarithm problem. A sufficiently large quantum computer running Shor's algorithm would break that assumption and let an attacker recover a private key from the corresponding public key, which every NEAR access key exposes on-chain. Once a cryptographically-relevant quantum computer (CRQC) exists, any account still authenticated by a classical key could therefore have its signatures forged. There is no CRQC today, and it is unclear when one will arrive. The point of this proposal is to give users a migration path *before* that happens. The concrete goal is to **safeguard existing accounts**: a user concerned about the quantum threat can add an ML-DSA-65 access key to their account now. This requires no change to how accounts are created and no migration of existing state. ML-DSA-65 (formerly CRYSTALS-Dilithium) is the NIST Security Category 3 variant of [FIPS 204], and its security does not depend on the discrete-logarithm or factoring problems that Shor's algorithm breaks. Adding it as a third scheme, rather than replacing the classical ones, lets the network adopt post-quantum signatures incrementally without a flag day and without breaking any existing key, tool, or wallet. This is the first milestone of a larger post-quantum roadmap. Implicit-account derivation, validator keys, and on-chain verification host functions are intended follow-ups (see [Future possibilities](#future-possibilities)); none of them is required to deliver the core benefit of protecting existing accounts, so they are deferred. ## Specification The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). ### Overview A single protocol feature, `ProtocolFeature::PostQuantumSignatures`, gates the whole change. Once it is active: - A transaction MAY be signed by an ML-DSA-65 key. - An `AddKey` action MAY register an ML-DSA-65 public key as an access key (full-access or function-call). - A `DelegateAction` (meta-transaction) MAY carry an ML-DSA-65 inner signer. - `DeleteKey` and other key-referencing actions MAY reference ML-DSA-65 keys. Before the feature is active, any attempt to use an ML-DSA-65 key in these positions is rejected at validation time, so no ML-DSA-65 value can ever enter state on a pre-feature protocol version. ### Scope **In scope (this NEP):** - ML-DSA-65 as a transaction signer. - ML-DSA-65 access keys (full-access and function-call), stored on-trie by hash. - ML-DSA-65 keys as gas keys ([NEP-611]); the gas-key mechanism does not restrict the key type. - ML-DSA-65 inner signers inside `DelegateAction` meta-transactions. - Gas pricing for the additional verification cost. - Transaction-size accounting that includes the large signature. - Rosetta-RPC and view-RPC representation of the new key/signature type. **Out of scope (unchanged by this NEP):** - **Validator / staking keys.** Staking remains `ed25519`-only; `is_valid_staking_key` returns false for ML-DSA-65. - **Block and chunk signatures.** Consensus signing is unchanged. - **On-chain verification host functions.** No `ml_dsa_verify` host function is added; contracts that verify signatures continue to use the existing `ed25519_verify` / `p256_verify` / `ecrecover` host functions. - **Implicit-account derivation.** Neither NEAR-style nor ETH-style implicit accounts gain an ML-DSA-65 form. This is a separate research item (see [Future possibilities](#future-possibilities)). - **Cross-network mirroring** (`tools/mirror`, `tools/fork-network`). These tools cannot yet map ML-DSA-65 keys across networks. ### ML-DSA-65 parameters and library ML-DSA-65 is the middle of the three FIPS 204 variants (44 / 65 / 87). It targets **NIST Security Category 3** (comparable to a brute-force key search on AES-192; a Grover-style quantum search gives only a quadratic speedup and remains far out of practical reach). | Cryptographic Material | Size | | --- | --- | | Public key | 1952 bytes | | Signature | 3309 bytes | | Raw private key | 4032 bytes | | Seed | 32 bytes | | On-trie public-key hash | 32 bytes | The implementation uses `aws-lc-rs` (the Rust binding to AWS-LC), specifically `aws_lc_rs::unstable::signature::ML_DSA_65`. The crate is pinned to an exact version (`=1.16.2`) and the `unstable` cargo feature is required to expose the PQDSA APIs. The `unstable` gate reflects that AWS-LC may still evolve these APIs; implementations MUST re-benchmark on every version bump and SHOULD keep the version pinned. AWS-LC's ML-DSA implementation is built on `mldsa-native`, maintained by the PQC Alliance within the Linux Foundation. ### `PublicKey`, `Signature`, and `KeyType` The `KeyType` enum gains a third value: | KeyType | Discriminant | | --- | --- | | `ED25519` | 0 | | `SECP256K1` | 1 | | `MLDSA65` | 2 | The `PublicKey` enum gains exactly one new variant, `PublicKey::MLDSA65`, carrying the full 1952-byte key, with **borsh tag 2**. The `Signature` enum gains `Signature::MLDSA65`, carrying the 3309-byte signature, also with **borsh tag 2**. The inner key, signature, and secret-key bytes are heap-boxed (`Box<[u8; N]>`); an inline-sized variant would inflate every `PublicKey` / `Signature` / `SecretKey` value to roughly 2 KiB everywhere it is held, including the classical variants. `PublicKey::MLDSA65` is the only form that appears on the wire (in transactions and actions) and the only form against which a signature can be verified. It is never written into the trie. ### On-trie storage: `PublicKeyHandle` and hashing Storing a 1952-byte public key for every access key would be prohibitively expensive in state. Instead, ML-DSA-65 access keys are stored **by hash**. To make "a full key on the wire" and "a hashed key in the trie" distinct at the type level, the on-trie identifier lives in a separate enum, `PublicKeyHandle`, used by the trie-key layer and view-API responses: | Variant | Borsh tag | Content | | --- | --- | --- | | ED25519 | 0 | 32-byte raw key (byte-identical to `PublicKey::ED25519`) | | SECP256K1 | 1 | 64-byte raw key (byte-identical to `PublicKey::SECP256K1`) | | ML-DSA-65 | 3 | 32-byte SHA3-256 digest of the public key | The two enums use deliberately disjoint borsh tag spaces. `PublicKey` owns `{0, 1, 2}`; `PublicKeyHandle` owns `{0, 1, 3}`. Tag 2 (the full ML-DSA-65 key) is rejected by `PublicKeyHandle` decoding, and tag 3 (the hash) never appears on the wire. As a result, "a full ML-DSA-65 key in the trie" and "an ML-DSA-65 hash in a transaction" are both unrepresentable. The hash is domain-separated. The on-trie identifier of an ML-DSA-65 public key is: ``` SHA3-256( b"near:ml-dsa-65-pubkey-hash:v1" || raw_public_key ) ``` The domain tag is exposed publicly as `near_crypto::hash_domain::HashDomainTag::MlDsa65PubkeyV1`, and `MlDsa65PublicKey::to_public_key_handle()` is the public helper that computes the handle. A wallet that knows its own public key can therefore recompute the on-trie identifier itself. The trie-key encoding for an `AccessKey` entry (after the column byte and account id) is: | Variant | Encoding | | --- | --- | | ED25519 | `[tag=0] \|\| 32-byte raw key` (33 bytes) | | SECP256K1 | `[tag=1] \|\| 64-byte raw key` (65 bytes) | | ML-DSA-65 | `[tag=3] \|\| 32-byte SHA3-256 digest` (33 bytes) | An ML-DSA-65 access key occupies 33 trie-key bytes instead of the 1953 it would take if stored raw -- about a 98% reduction -- and its storage-staking cost is identical to that of an `ed25519` key, since both occupy 33 bytes. #### `trie_id_len()` vs `len()` To price storage correctly, `PublicKey` exposes two lengths: - `len()` returns the borsh-encoded length on the wire: 33 / 65 / **1953** for ED25519 / SECP256K1 / ML-DSA-65. - `trie_id_len()` returns the on-trie length: 33 / 65 / **33**. The two diverge only for ML-DSA-65. All storage-stake and trie-byte-priced fee paths (`access_key_storage_usage`, the gas-key fee helpers) MUST use `trie_id_len()`. A caller that uses `len()` for a trie-storage cost would misprice an ML-DSA-65 key by roughly 1900 bytes. ### Protocol gating The set of actions that require the feature is computed by a single predicate, `Action::post_quantum_signatures_required`, which returns true if any ML-DSA-65 key or signature appears in the action (recursing into `DelegateAction`). This predicate is checked at two choke points: 1. **Transaction validation** -- `Transaction::check_valid_for_config`. A transaction that uses ML-DSA-65 before the feature is active is rejected with `InvalidTxError::InvalidTransactionVersion`. 2. **Action validation** -- `validate_actions_with_mode`, which also covers actions emitted by contracts as receipts. Rejection here is `ActionsValidationError::UnsupportedProtocolFeature { protocol_feature: "PostQuantumSignatures", version }`. Borsh **deserialization** of `PublicKey::MLDSA65` and `Signature::MLDSA65` is intentionally NOT gated: once a value can exist in state or history, it must always parse. The action-validation gates are what guarantee that no ML-DSA-65 value enters state on a pre-feature protocol version. A useful consequence: when the feature activates, no ML-DSA-65 access key exists in state yet, so the runtime never has to reconcile a mixed population of pre-gate and post-gate keys. Every ML-DSA-65 key in state was added under the new rules, and **migration is zero**. ### Verification gas pricing ML-DSA-65 verification is more expensive than the classical schemes. Benchmarks on an Intel Core Ultra 9 185H (single core) measured mean verify times of roughly 32 µs (ed25519), 24 µs (secp256k1), and 79 µs (ML-DSA-65), with the ML-DSA-65 p99.9 around 170 µs and a worst observed single-sample around 1.3 ms. This extra cost is charged as **gas** at transaction conversion, through a new runtime parameter: - `ml_dsa_65_verification_cost = 100_000_000_000` (100 Ggas), introduced at protocol version 85. - It is wired through `RuntimeFeesConfig::signature_verification_costs`, an `EnumMap` over `SignatureKind { Ed25519, Secp256k1, MlDsa65 }`. Only `MlDsa65` is non-zero; `Ed25519` and `Secp256k1` are zero, so the classical schemes are unaffected and backwards-compatible. The charge is added to the transaction's **burnt** gas (once per ML-DSA-65 signature, including each `DelegateAction`'s inner signer) inside `calculate_tx_cost`. Because it is burnt at conversion, it raises only what the signer must pay to buy the transaction; it does NOT reduce the gas available to the resulting receipts, and it has no effect on cross-contract calls made from within a contract. Under NEAR's `1 Tgas ~= 1 ms` gas-to-time convention, 100 Ggas budgets about 100 µs, which is roughly twice the measured mean verification-time difference over the classical schemes (about 50 µs). It is an additive surcharge levied per signature, not a replacement of the existing per-transaction cost, and the number of signatures in one transaction is bounded (at most two: the outer signer plus one inner `DelegateAction` signer, since a transaction may carry at most one `DelegateAction`), so the aggregate charge per transaction is bounded. #### Verification cost is bounded Benchmarks across valid, random, and tampered signatures show ML-DSA-65 verification time in a bounded range: a fixed signature and many different signatures produce the same distribution, and the tail (p99.9 around 170 µs, occasional sub-millisecond outliers) is operating-system scheduling noise that also appears for the classical schemes. This is empirical evidence rather than a formal constant-time guarantee; the gas charge covers the mean cost difference with headroom. ### Transaction-size accounting The ML-DSA-65 signature (about 3.3 KiB) is far larger than a classical signature (64 bytes). Two transaction-size measures are distinguished: - `SignedTransaction::get_size()` counts only the inner `Transaction` body (the signature is `borsh(skip)`-ed). This historically under-counted every signed transaction by its signature size. - `SignedTransaction::wire_size()` returns the full borsh length, signature included. - `SignedTransaction::size_for_limits(protocol_version)` selects `wire_size()` from `PostQuantumSignatures` onward, and `get_size()` before it. The `max_transaction_size` limit (4 MiB) and the `combined_transactions_size_limit` (the bound on transaction bytes carried in a `ChunkStateWitness`) both use `size_for_limits`, so from protocol 85 on they count the real wire size. The version gate keeps chunk production deterministic across the protocol boundary. The mempool counts `wire_size()` unconditionally because it physically buffers the signature bytes. This accounting fix closes a witness-honesty gap: without it, the witness's transaction-byte bound would systematically undercount ML-DSA-65 transactions by their signature size. *Pricing* the extra wire bytes (a per-byte surcharge on the signature and on the public key carried by `AddKey` / `DeleteKey`) is a separate, deferred question (see [Future possibilities](#future-possibilities)); the accounting fix stands on its own. Note that the signature is not the only large payload. The signer's full 1952-byte public key is copied into the resulting `ActionReceipt.signer_public_key` (a full `PublicKey`, not the hashed handle), so it also lands in the delayed and buffered receipt queues in state and in the `ChunkStateWitness`. An ML-DSA-65 transaction therefore contributes its public key to state and witness size downstream of transaction conversion, not only on the transaction wire. ### View API and RPC representation - **`view_access_key_list`** returns `ml-dsa-65-hash:` for ML-DSA-65 entries, because the trie stores only the hash. The full public key is not recoverable from chain state. Wallets, explorers, and indexers MUST know their own public key (or recompute the handle from it) to reconcile a chain entry with a known key. This is the same model already used for ETH-style implicit accounts, whose on-chain identifier is also a hash. - **Rosetta-RPC** gains `CurveType::MlDsa65` and `SignatureType::MlDsa65` (serialized as `"ml_dsa_65"`), with full conversions to and from `KeyType::MLDSA65`. The corresponding upstream addition to the Coinbase Mesh specification is [mesh-specifications#129](https://github.com/coinbase/mesh-specifications/pull/129). Mesh clients that have not picked up the new enum value yet may reject it; this is accepted. ## Reference Implementation The feature is implemented in `nearcore` behind `ProtocolFeature::PostQuantumSignatures`, stabilized at **protocol version 85** and included in the **neard 2.13** release. - [Add FIPS 204 ML-DSA-65 as a third signature scheme](https://github.com/near/nearcore/pull/15731) (main feature PR). - [Stabilize ML-DSA-65 signatures at protocol version 85](https://github.com/near/nearcore/pull/15878). End-to-end coverage lives in `test-loop-tests/src/tests/ml_dsa_access_key.rs` (full-access and function-call keys) and `ml_dsa_verification_cost.rs` (the burnt-gas charge for an outer transaction and an inner `DelegateAction` signer). For the full set of implementation PRs and a walkthrough of the load-bearing design decisions and code layout, see the in-tree design note `docs/architecture/how/post_quantum_signatures.md`. ## Security Implications ### Quantum resistance and the threat model ML-DSA-65 is not vulnerable to Shor's algorithm; its security rests on the hardness of module lattice problems at NIST Security Category 3. Adding it lets a user remove the future forgeability risk that a CRQC would create for an account still authenticated by classical keys. This protection is opt-in and per-key. An account is only quantum-safe to the extent that it relies on ML-DSA-65 keys. Importantly, **validator/staking keys remain `ed25519`-only** under this NEP, so the consensus layer retains classical-only signing. That residual exposure is acknowledged and left to a follow-up (see [Future possibilities](#future-possibilities)). ### Hash strength of the on-trie identifier ML-DSA-65 access keys are looked up by a 32-byte (256-bit) SHA3-256 digest of the public key. An attacker who wants to bind a chosen public key to an existing access-key entry needs a second-preimage on the hash. A 256-bit digest gives roughly 128-bit security even against a Grover-style quantum search, which is secure enough for the access-key lookup. Crucially, the full public key is still carried on the wire and verified against, so the hash is an index, not the sole authenticator. The choice of 256 bits has history worth recording. An earlier iteration used SHA3-384, on the argument that matching ML-DSA-65's Category-3 keypair strength against a Grover second-preimage would want a 384-bit digest. The implementation later moved to SHA3-256 (PR [#15830](https://github.com/near/nearcore/pull/15830)). The decisive practical property is that a 32-byte digest is short enough to be encoded into a NEAR account id, which keeps the door open to deriving ML-DSA-65 implicit accounts from the account id alone in the future. The research review settled on 256-bit as acceptable for this use (and explicitly rejected anything shorter, such as truncation to 160 bits). ### Verification-cost denial of service Only transactions with valid signatures are included and charged; a transaction with an invalid signature is rejected at validation without being charged, as with the classical schemes. For included transactions the per-signature charge covers the mean verification-cost difference over the classical schemes with headroom, and a transaction carries at most two ML-DSA-65 signatures (the outer signer plus one inner `DelegateAction` signer), so the aggregate verification cost of any single transaction is bounded and paid for. Verification time also shows no signature-dependent blowup across valid, random, and tampered signatures (see [Verification cost is bounded](#verification-cost-is-bounded)). ### Witness size and DoS ML-DSA-65 signatures are about 3.3 KiB, roughly 50x a classical signature, which raises the per-transaction contribution to the `ChunkStateWitness`. The size-accounting change ensures the witness's transaction-byte bound counts the full signature, so a flood of large signatures cannot silently exceed the intended witness size. NEAR does not currently charge gas specifically for witness size for any scheme; the decision here is consistent with that status quo, and the per-byte cost of `AddKey` already imposes a (small) charge for the public-key bytes. Pricing witness bytes more precisely is deferred and tracked as future work. ### Permanence of the borsh tag assignment Borsh tag 2 is permanently assigned to `PublicKey::MLDSA65` and `Signature::MLDSA65`, and tag 3 to `PublicKeyHandle::MlDsa65`. Once any block on a network has accepted an ML-DSA-65 transaction or `AddKey`, these decodings are part of that chain's history and cannot be changed. ### Library trust The verification path depends on `aws-lc-rs`'s `unstable` ML-DSA APIs. AWS-LC is a widely used and trusted library, and Trail of Bits gave `aws-lc-rs` as their default recommendation for an ML-DSA implementation in Rust, with the caveats that they had not performed a direct audit of it and that a competitive pure-Rust implementation could emerge later. The `unstable` feature means the API surface (not necessarily the cryptography) may change between versions; the crate is pinned to an exact version and a known-answer test guards against silent behavioral changes. Implementations MUST re-benchmark and re-validate on any version bump. Because signature validity is consensus-critical, all node implementations MUST agree on it exactly. An alternative implementation that accepts or rejects an edge-case (for example malformed) signature differently from AWS-LC would fork the chain. The known-answer test guards against silent drift in the reference implementation, but exact agreement on validity semantics is a requirement any second implementation must meet, and it constrains the choice of a future pure-Rust replacement. ### Signature malleability NEAR identifies a transaction by the hash of its unsigned `Transaction` body, and the signature is verified against that same hash; the signature bytes are not part of it. Consequently, ML-DSA-65 signature malleability (if two distinct signature encodings verify for the same message and key) does not affect transaction identity, nonce consumption, or replay protection: a malleated copy has the same transaction hash and the same nonce, so it cannot be used to double-execute a transaction. ## Alternatives ### A different ML-DSA library Several Rust crates implement FIPS 204. `libcrux` (Cryspen) is formally verified but was pre-1.0 and its maintainers note production use requires contacting them. The RustCrypto `ml-dsa` crate is from trusted developers but was likewise pre-1.0 and had a recent vulnerability at evaluation time. `rust-openssl` and `ring` lacked support. `aws-lc-rs` was chosen as the most widely used and trusted option that was usable now, and is the one Trail of Bits recommended. The trade-off is the FFI boundary and the `unstable` API gate. ### Storing the full public key on-trie The 1952-byte public key could be stored directly, like `ed25519` and `secp256k1` keys. This was rejected: it would raise the storage-staking cost of an ML-DSA-65 full-access key from about 0.0008 NEAR to about 0.02 NEAR (roughly 24x) and bloat state. Since the full key is already carried in the transaction for verification, storing only its hash loses nothing functionally and aligns with how ETH-style implicit accounts already work. ### SHA3-384 instead of SHA3-256 A 384-bit digest was considered and rejected in favour of SHA3-256; see [Hash strength of the on-trie identifier](#hash-strength-of-the-on-trie-identifier) for the rationale (256 bits is sufficient for an index whose full key is verified on the wire, and a 32-byte digest fits within a NEAR account id while a 48-byte one does not). ### A different ML-DSA variant or PQ scheme ML-DSA-44 (Category 2) and ML-DSA-87 (Category 5) were available; ML-DSA-65 was chosen as a balanced Category-3 default. Other post-quantum signature families (e.g. SLH-DSA / FN-DSA) were not pursued for this milestone; the lattice-based ML-DSA was selected as the standardized, well-supported option. ### Implicit accounts or wallet-contract verification first An alternative ordering would have prioritized a wallet-contract plus an on-chain `ml_dsa_verify` host function (serving DEXs, bridges, and other contracts that rely on elliptic-curve cryptography), or shipped post-quantum implicit accounts first. This was considered and deferred. The specific, immediate goal of this milestone is to let existing accounts add a quantum-safe key; implicit accounts hit a hard constraint (the 256-bit handle does not fit the legacy `0s` 40-hex-character account format, and `AccountId` is capped at 64 characters), and host functions are sequenced after the core access-key work. Both remain on the roadmap. ### Charging post-quantum signers more (or the same) Whether ML-DSA-65 signers should pay a premium beyond the raw verification cost was discussed. The decision was to charge the additional verification cost (so signers pay for the work they add) but not to penalize them further, consistent with the general stance that validators and RPC providers reject invalid signatures for free and police abusive senders operationally. ## Future possibilities - **Post-quantum implicit accounts.** Deriving an account id directly from an ML-DSA-65 key. The SHA3-256, domain-separated hashing introduced here sets the precedent such a derivation should follow. This needs a longer account-id encoding than the legacy `0s` format and is tied to a broader "universal implicit accounts" effort. - **Validator / staking keys.** Extending post-quantum signing to consensus keys, closing the residual classical exposure at the validator layer. - **On-chain verification host function.** An `ml_dsa_verify` host function would let contracts (wallet contracts, bridges, DEXs) verify post-quantum signatures on-chain. - **Cross-network mirroring.** `tools/mirror` and `tools/fork-network` currently panic or silently skip ML-DSA-65 keys (the full key cannot be recovered from the on-trie hash). A deterministic seed-derived key mapping, analogous to the existing `ed25519`/`secp256k1` mapping, is needed before a post-quantum-bearing chain can be mirrored. The interim behavior should at least be unified to "log and skip" so failures are visible. - **Wallet / SDK support and a view endpoint.** Update `near-api-js` / `near-cli-rs`, document the hash format, and decide whether to expose a dedicated `view_access_key_hash` endpoint or keep the hashed form in the existing `public_key` field with the new prefix. - **Pricing the extra wire/witness bytes.** A per-byte gas surcharge for the large signature at conversion, and a per-byte component on `AddKey` / `DeleteKey` for the public-key bytes they carry. The accounting is already in place; only the pricing is deferred. - **Eventual deprecation of the classical schemes.** Not proposed here. The classical schemes remain first-class; deprecation would be a separate, much later decision once post-quantum adoption is broad. ## Consequences ### Positive - Users can protect existing accounts against future quantum forgery today, with no account migration and no flag day. - The classical schemes are untouched, which preserves backwards compatibility: existing keys, wallets, and tools that use them are unaffected. - Hash-based storage makes an ML-DSA-65 access key cost the same in storage staking as an `ed25519` key, despite a 1952-byte public key. - The additional verification cost is paid by the signers who incur it; classical-scheme users pay nothing extra. - Zero state migration: the validation gates guarantee no ML-DSA-65 value exists in state before activation. ### Neutral - `view_access_key_list` returns a hash, not the public key, for ML-DSA-65 entries. This matches the ETH-implicit-account model but means off-chain tools must know their own key to reconcile. - The implementation depends on the `unstable` API surface of `aws-lc-rs`, requiring re-benchmarking on version bumps. - Protocol version 85 also stabilizes unrelated features; the `PostQuantumSignatures` gate is independent of them. ### Negative - Verification is roughly 2.6x slower than the classical schemes (79 µs vs 30 µs), priced as 100 Ggas of additional burnt gas per signature. - ML-DSA-65 transactions are large (a ~3.3 KiB signature plus a ~2 KiB public key on the wire for `AddKey`), reducing how many fit in a chunk and increasing their witness footprint, though within existing limits. - The borsh tag assignments (2 for the key/signature, 3 for the handle) are permanent once any chain accepts an ML-DSA-65 value. ### Backwards Compatibility The change is backwards compatible. Before `PostQuantumSignatures` activates, ML-DSA-65 keys and signatures are rejected by transaction and action validation, so no pre-feature behavior changes and no ML-DSA-65 value can enter state. Borsh decoding of the new variants is always available, which is required so that any value, once written, always parses. The classical schemes' gas, storage, and validation behavior is unchanged (`signature_verification_costs` is zero for them). The transaction-size limit change is version-gated to keep chunk production deterministic across the activation boundary. There is no state migration. Tooling that reads `view_access_key_list` MUST be prepared for the `ml-dsa-65-hash:` form, and Rosetta clients SHOULD accept the new `ml_dsa_65` curve and signature types. Contracts that read the signer's key at runtime are affected in two ways. The `signer_account_pk` host function can now return a 1952-byte ML-DSA-65 key, so a contract that assumed a small fixed key size may need updating. Contracts compiled against an older `near-sdk` whose `PublicKey` type predates ML-DSA-65 support will fail to deserialize such a key (`UnknownCurve`); `multisig2` in `core-contracts` is one known example. These contracts keep working with classical keys and only break when handed an ML-DSA-65 key. ## Unresolved Issues - **Pricing witness/wire size.** Charging for the extra bytes the large signature and public key add is deliberately deferred. The team agreed not to implement it for this milestone; whether and how to track it for a future release was raised and not resolved in the design discussion. - **External audit.** Whether ML-DSA-65 integration warrants a dedicated external audit beyond the Trail of Bits library recommendation was raised during design and not formally resolved. - **Mirror / fork-network behavior** is inconsistent (some paths panic, others silently skip). Unifying this to a visible "log and skip" before any serious mirroring of a post-quantum-bearing chain is an open task. ## Changelog ### 1.0.0 - Initial Version > Placeholder for the context about when and who approved this NEP version. #### Benefits > List of benefits filled by the Subject Matter Experts while reviewing this version: - Benefit 1 - Benefit 2 #### Concerns > Template for Subject Matter Experts review for this version: > Status: New | Ongoing | Resolved | # | Concern | Resolution | Status | | --: | :------ | :--------- | -----: | | 1 | | | | | 2 | | | | ## Copyright Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). [FIPS 204]: https://csrc.nist.gov/pubs/fips/204/final [NEP-611]: https://github.com/near/NEPs/blob/master/neps/nep-0611.md