feat: post-quantum (ML-DSA-65) key generation#605
Conversation
Add a top-level `near generate-keypair` command that creates a key pair offline for either Ed25519 (classic) or the post-quantum ML-DSA-65 scheme (FIPS 204), printing it to the terminal or saving it to a JSON file. Post-quantum access keys require nearcore 2.13 (nearcore#15731), which is not yet published to crates.io, so the near-* crates are temporarily git-pinned to the nearcore 2.13-release branch and the matching jsonrpc/socialdb draft branches so they resolve to a single version. This also adapts near-cli-rs to the 2.13 API changes: the new DelegateV2 action and its gas-key/delegate access-key error variants, the ViewState after_key/limit query fields, the PublicKeyHandle returned by the access-key list, and the removal of KeyType: PartialEq.
near-crypto's post-quantum support pulls in aws-lc-sys, which needs NASM to assemble its x86_64 code; the x86_64-pc-windows-msvc release build has no NASM on the runner. Set AWS_LC_SYS_PREBUILT_NASM=1 to use the crate's pregenerated NASM objects instead. (release.yml is allow-dirty=["ci"], so the manual edit is fine.)
Add a --signature-scheme (ed25519 | ml-dsa-65) option to the autogenerate-new-keypair paths of `account add-key`, `account create-account fund-myself`, and `account create-account sponsor-by-faucet-service`, so a post-quantum access key can be generated and put on-chain (valid once a network upgrades to protocol 85 / 2.13). Factor the shared SignatureScheme enum and a GeneratedKeyPair helper into common.rs and reuse them from the standalone `generate-keypair` command. The flag defaults to ed25519 when omitted (prompting only in an interactive terminal), so existing non-interactive invocations are unchanged. KeyPairProperties and the seed-phrase flows are untouched.
There was a problem hiding this comment.
Pull request overview
Adds CLI support for NEAR protocol 2.13’s post-quantum ML-DSA-65 key type by introducing an offline keypair generator and extending existing autogeneration flows to optionally emit ML-DSA-65 keys, alongside the existing Ed25519 behavior. Also includes required nearcore 2.13 API adaptations (new delegate action variants, error kinds, and ViewState query fields) and updates release packaging to accommodate new crypto dependencies.
Changes:
- Introduce a shared
SignatureScheme+GeneratedKeyPairabstraction to generate/store/display Ed25519 vs ML-DSA-65 keys and reuse it across autogeneration flows. - Add a new offline
near generate-keypairtop-level command with output modes (print vs JSON file) plus unit tests for ML-DSA-65 key parsing/signing. - Update multiple code paths to compile against nearcore 2.13 APIs (DelegateV2, new errors, ViewState fields, access-key list
PublicKeyHandle) and adjust release workflow for aws-lc/Windows build needs.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/transaction_signature_options/sign_with_mpc/mod.rs |
Reject ML-DSA-65 for MPC signing and document/guard MPC domain-id mapping. |
src/common.rs |
Add SignatureScheme, interactive resolver, and GeneratedKeyPair (Ed25519 + ML-DSA-65) plus 2.13 API adaptations. |
src/commands/transaction/reconstruct_transaction/mod.rs |
Block reconstructing DelegateV2 transactions with a clear error. |
src/commands/mod.rs |
Register new top-level generate-keypair command module/variant. |
src/commands/generate_keypair/mod.rs |
Implement offline keypair generation command, output modes, and unit tests. |
src/commands/contract/view_storage/output_format/mod.rs |
Populate new ViewState fields (after_key, limit) for 2.13 compatibility. |
src/commands/account/delete_key/public_keys_to_delete.rs |
Adapt to PublicKeyHandle and skip ML-DSA-65 entries that can’t provide full pubkeys for DeleteKey. |
src/commands/account/create_account/sponsor_by_faucet_service/add_key/autogenerate_new_keypair/mod.rs |
Add --signature-scheme to autogenerate flow and use GeneratedKeyPair. |
src/commands/account/create_account/fund_myself_create_account/add_key/autogenerate_new_keypair/mod.rs |
Add --signature-scheme to autogenerate flow and use GeneratedKeyPair. |
src/commands/account/add_key/autogenerate_new_keypair/mod.rs |
Add --signature-scheme to autogenerate flow and switch to GeneratedKeyPair. |
src/commands/account/add_key/autogenerate_new_keypair/save_keypair_to_legacy_keychain/mod.rs |
Save ML-DSA-65-compatible credentials JSON via GeneratedKeyPair. |
src/commands/account/add_key/autogenerate_new_keypair/save_keypair_to_keychain/mod.rs |
Save ML-DSA-65-compatible credentials JSON via GeneratedKeyPair. |
src/commands/account/add_key/autogenerate_new_keypair/print_keypair_to_terminal/mod.rs |
Print keypair info via GeneratedKeyPair (supports ML-DSA-65). |
Cargo.toml |
Bump nearcore-related deps to 0.37.0-rc.2 + git-pin draft client libs. |
Cargo.lock |
Lockfile updates reflecting new dependency graph (aws-lc, etc.). |
.github/workflows/release.yml |
Set AWS_LC_SYS_PREBUILT_NASM to avoid NASM requirement on Windows MSVC builds. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e170e55345
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
## Cause nearcore 2.13 (protocol v85) introduces gas keys: an access-key permission that carries its own NEAR balance and `N` parallel nonces. near-cli has no way to create one, fund it, or inspect its nonces. ## Fix Stacked on the nearcore-2.13 branch (#605): - **Create** — `account add-key` gains two permission variants, `grant-gas-key-full-access` and `grant-gas-key-function-call`, that build an `AddKey` action with `AccessKeyPermission::GasKeyFullAccess` / `GasKeyFunctionCall`. Gas keys are created **empty** (the protocol rejects an `AddKey` with a non-zero gas-key balance), so there is no balance argument; fund the key afterwards with `fund-gas-key`. The function-call variant takes a contract id and method names but **no allowance** (the protocol rejects an allowance on a gas-key function-call key). The parallel-nonce count is collected as `u64` (interactive-clap has no `ToCli for u16`) and narrowed to `NonceIndex` with a `1..=MAX_NONCES_FOR_GAS_KEY` bound check. - **Fund** — `account fund-gas-key` transfers NEAR from the account into a gas key's balance. - **Withdraw** — `account withdraw-from-gas-key` moves NEAR from a gas key's balance back to the account. - **View** — `account view-gas-key-nonces <account> <public-key>` issues the `view_gas_key_nonces` query (via the generic `QueryRequest::ViewGasKeyNonces` path) and prints the per-index nonces from `GasKeyNoncesView`. Signing a transaction with a specific `GasKeyNonce` (DelegateV2 / parallel-nonce signing) remains a follow-up increment.
## 🤖 New release
* `near-cli-rs`: 0.27.0 -> 0.28.0 (⚠ API breaking changes)
### ⚠ `near-cli-rs` breaking changes
```text
--- failure constructible_struct_adds_field: externally-constructible struct adds field ---
Description:
A pub struct constructible with a struct literal has a new pub field. Existing struct literals must be updated to include the new field.
ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/constructible_struct_adds_field.ron
Failed in:
field CliSignPrivateKey.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_private_key/mod.rs:5
field InteractiveClapContextScopeForGenerateKeypair.signature_scheme in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/create_account/sponsor_by_faucet_service/add_key/autogenerate_new_keypair/mod.rs:3
field CliSignLedger.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_ledger/mod.rs:18
field CliSignLegacyKeychain.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_legacy_keychain/mod.rs:12
field CliSignKeychain.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_keychain/mod.rs:9
field SignLedgerContext.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_ledger/mod.rs:51
field InteractiveClapContextScopeForSignSeedPhrase.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_seed_phrase/mod.rs:7
field CliSignAccessKeyFile.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_access_key_file/mod.rs:5
field CliGenerateKeypair.signature_scheme in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/create_account/sponsor_by_faucet_service/add_key/autogenerate_new_keypair/mod.rs:3
field InteractiveClapContextScopeForSignPrivateKey.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_private_key/mod.rs:5
field InteractiveClapContextScopeForSignLedger.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_ledger/mod.rs:18
field InteractiveClapContextScopeForSignLegacyKeychain.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_legacy_keychain/mod.rs:12
field InteractiveClapContextScopeForSignKeychain.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_keychain/mod.rs:9
field InteractiveClapContextScopeForSignAccessKeyFile.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_access_key_file/mod.rs:5
field CliSignSeedPhrase.nonce_index in /tmp/.tmpat9wFR/near-cli-rs/src/transaction_signature_options/sign_with_seed_phrase/mod.rs:7
--- failure enum_no_repr_variant_discriminant_changed: enum variant had its discriminant change value ---
Description:
The enum's variant had its discriminant value change. This breaks downstream code that used its value via a numeric cast like `as isize`.
ref: https://doc.rust-lang.org/reference/items/enumerations.html#assigning-discriminant-values
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/enum_no_repr_variant_discriminant_changed.ron
Failed in:
variant TopLevelCommandDiscriminants::Extensions 7 -> 8 in /tmp/.tmpat9wFR/near-cli-rs/src/commands/mod.rs:60
variant TopLevelCommandDiscriminants::Extensions 7 -> 8 in /tmp/.tmpat9wFR/near-cli-rs/src/commands/mod.rs:60
variant AccountActionsDiscriminants::GetPublicKey 7 -> 8 in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:69
variant AccountActionsDiscriminants::AddKey 8 -> 9 in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:74
variant AccountActionsDiscriminants::DeleteKeys 9 -> 12 in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:89
variant AccountActionsDiscriminants::ManageStorageDeposit 10 -> 13 in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:94
variant AccountActionsDiscriminants::GetPublicKey 7 -> 8 in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:69
variant AccountActionsDiscriminants::AddKey 8 -> 9 in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:74
variant AccountActionsDiscriminants::DeleteKeys 9 -> 12 in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:89
variant AccountActionsDiscriminants::ManageStorageDeposit 10 -> 13 in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:94
--- failure enum_variant_added: enum variant added on exhaustive enum ---
Description:
A publicly-visible enum without #[non_exhaustive] has a new variant.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#enum-variant-new
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/enum_variant_added.ron
Failed in:
variant AccountActionsDiscriminants:ViewGasKeyNonces in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:64
variant AccountActionsDiscriminants:FundGasKey in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:79
variant AccountActionsDiscriminants:WithdrawFromGasKey in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:84
variant AccountActionsDiscriminants:ViewGasKeyNonces in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:64
variant AccountActionsDiscriminants:FundGasKey in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:79
variant AccountActionsDiscriminants:WithdrawFromGasKey in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:84
variant CliTopLevelCommand:GenerateKeypair in /tmp/.tmpat9wFR/near-cli-rs/src/commands/mod.rs:16
variant CliAccountActions:ViewGasKeyNonces in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:27
variant CliAccountActions:FundGasKey in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:27
variant CliAccountActions:WithdrawFromGasKey in /tmp/.tmpat9wFR/near-cli-rs/src/commands/account/mod.rs:27
variant TopLevelCommandDiscriminants:GenerateKeypair in /tmp/.tmpat9wFR/near-cli-rs/src/commands/mod.rs:56
variant TopLevelCommandDiscriminants:GenerateKeypair in /tmp/.tmpat9wFR/near-cli-rs/src/commands/mod.rs:56
--- failure function_missing: pub fn removed or renamed ---
Description:
A publicly-visible function cannot be imported by its prior path. A `pub use` may have been removed, or the function itself may have been renamed or removed entirely.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/function_missing.ron
Failed in:
function near_cli_rs::transaction_signature_options::send::sleep_after_error, previously in file /tmp/.tmppWZUdR/near-cli-rs/src/transaction_signature_options/send/mod.rs:193
--- failure trait_method_added: pub trait method added ---
Description:
A non-sealed public trait added a new method without a default implementation, which breaks downstream implementations of the trait
ref: https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-item-no-default
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/trait_method_added.ron
Failed in:
trait method near_cli_rs::common::RpcQueryResponseExt::gas_key_nonces_view in file /tmp/.tmpat9wFR/near-cli-rs/src/common.rs:3499
```
<details><summary><i><b>Changelog</b></i></summary><p>
<blockquote>
##
[0.28.0](v0.27.0...v0.28.0)
- 2026-07-02
### Added
- sign transactions with a gas key (nonce_index)
([#608](#608))
- gas-key support (nearcore 2.13)
([#607](#607))
- post-quantum (ML-DSA-65) key generation (draft)
([#605](#605))
- Validate beneficiary account before deleting account
([#596](#596))
### Fixed
- Fixed output information for transaction status "Started"
([#594](#594))
### Other
- Fixed NPM Trusted Publishing
([#603](#603))
</blockquote>
</p></details>
---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
---------
Co-authored-by: r-near <[email protected]>
What
Adds full CLI support for the post-quantum ML-DSA-65 (FIPS 204) signature scheme that NEAR protocol 2.13 introduces as a third access-key type, alongside the classic Ed25519.
1. A new offline
near generate-keypaircommand — create a key pair for either scheme and print it or save it to a JSON file (no account, no network):2. A
--signature-schemeoption on the on-chain autogenerate flows —account add-key,account create-account fund-myself, andaccount create-account sponsor-by-faucet-servicecan now autogenerate and submit an ML-DSA-65 access key:The flag defaults to
ed25519when omitted (and only prompts in an interactive terminal), so every existing non-interactive invocation behaves exactly as before. ML-DSA-65 keys are also emitted in the standardml-dsa-65:<base58>string form, so an externally generated key can be added via the existingaccount add-key … use-manually-provided-public-keyflow too.Why
NEAR protocol 2.13 introduces post-quantum ML-DSA-65 access keys (see near/nearcore#15731). This lands the CLI side: generating PQ key pairs. Related ecosystem work: near/docs#3061, near/near-sdk-rs#1557.
2.13 API adaptations
Bumping to 2.13 required adapting to a few
near-primitives/near-cryptochanges (unrelated to keygen, but needed to compile):Action::DelegateV2/ActionView::DelegateV2(v2 meta-transactions) — handled in the transaction display paths.ActionErrorKind::DelegateActionInvalidNonceIndexandInvalidAccessKeyError::DelegateActionRequiresNonGasKey/DelegateActionRequiresGasKey(gas keys) — mapped to CLI error messages.QueryRequest::ViewStategainedafter_key/limitfields.PublicKeyHandle; the delete-key picker usesfull_pubkey()and skips ML-DSA-65 entries (which are stored on-chain only as a hash, so the full key isn't recoverable for aDeleteKeyaction).KeyTypeno longer derivesPartialEq— comparisons switched to pattern matching.Scope / limitations
create-accountnamed-account flows support them but theed25519-only implicit-account paths are unchanged.PostQuantumSignaturesprotocol feature, stabilized at protocol version 85 (the 2.13 stable version) — so theAddKeytransactions these flows build become valid on a network once it upgrades to protocol 85 with the 2.13 rollout (it is not nightly-gated).Testing
near-crypto, and verify a signature round-trips.cargo test --all-features,cargo clippy --all-features --all-targets -- -D warnings,cargo fmt --check, and the--no-default-features(+ledger) checks.