Account e firma PQC
Gli account QoreChain derivano da un singolo mnemonico BIP-39. Esistono due modelli di account, entrambi pienamente supportati:
- Derivazione HD per lane (legacy/predefinito) — lo stesso mnemonico produce un account nativo (coin type 118), un account EVM (coin type 60) e un account SVM (coin type 501) tramite percorsi di derivazione indipendenti. Tre chiavi, tre indirizzi.
- Account unificati eth-native (SDK 0.6.0, chain v3.1.83) — UNA sola
chiave
eth_secp256k1è UN'unica identità a 20 byte rappresentata come tutte e tre le codifiche di indirizzo, con un unico saldo condiviso. Vedi Account unificati.
Derivazione HD (legacy/predefinito, coin type 118)
import {
generateMnemonic,
validateMnemonic,
deriveNativeAccount,
deriveEvmAccount,
deriveSvmAccount,
} from "@qorechain/sdk";
const mnemonic = generateMnemonic(); // 12 words; pass 256 for 24 words
const native = await deriveNativeAccount(mnemonic);
console.log(native.address); // "qor1..." (secp256k1, bech32)
const evm = await deriveEvmAccount(mnemonic);
console.log(evm.address); // "0x..." (EIP-55 checksummed)
const svm = await deriveSvmAccount(mnemonic);
console.log(svm.address); // base58 ed25519 public key
Il mnemonico viene validato (parole e checksum) prima che venga derivata
qualsiasi chiave, così un errore di battitura genera un'eccezione invece di
produrre silenziosamente un account errato. Puoi validarlo esplicitamente con
validateMnemonic(mnemonic).
Schemi di derivazione
| Tipo | Curva | Percorso | Indirizzo |
|---|---|---|---|
| native | secp256k1 | m/44'/118'/0'/0/{i} | bech32 qor di ripemd160(sha256(pubkey)) |
| evm | secp256k1 | m/44'/60'/0'/0/{i} | 0x + keccak256(pubkey)[-20:], EIP-55 |
| svm | ed25519 | m/44'/501'/{i}'/0' | base58 della chiave pubblica a 32 byte |
Passa un indice di account per derivare account aggiuntivi. In TypeScript:
const second = await deriveNativeAccount(mnemonic, { accountIndex: 1 });
In Python/Go/Rust l'indice è un argomento posizionale
(derive_native_account(mnemonic, 1) / DeriveNativeAccount(mnemonic, 1) /
derive_native_account(&mnemonic, 1)).
Nota sui known-answer test
Gli schemi di derivazione sono deterministici e coperti da known-answer test in tutti e quattro gli SDK, quindi lo stesso mnemonico produce indirizzi identici in TypeScript, Python, Go e Rust. Questo ti permette di derivare in un linguaggio e verificare in un altro.
Questa derivazione per lane (
deriveNativeAccountal coin type 118, piùderiveEvmAccount/deriveSvmAccount) è il modello legacy/predefinito e rimane supportata e invariata. Gli account unificati qui sotto sono un modello di identità aggiuntivo, opzionale.
Account unificati (eth-native)
Dalla SDK 0.6.0 (chain v3.1.83), deriveUnifiedAccount(mnemonic, index = 0)
deriva UNA sola chiave eth_secp256k1 sul percorso HD Ethereum
m/44'/60'/0'/0/{index} i cui 20 byte di indirizzo
(keccak256(pubkey)[12:]) sono la STESSA identità rappresentata in tre modi:
| Lane | Codifica |
|---|---|
| Native | bech32 con prefisso qor (qor1…) |
| EVM | 0x + checksum esadecimale misto EIP-55 |
| SVM | base58 dei 20 byte con padding a destra di 12 byte zero (32 byte) |
Un deposito su una qualsiasi delle tre lane confluisce in un unico saldo, e l'unica chiave è il firmatario su ogni lane (sulla lane EVM entro una finestra di autorizzazione; la lane delle transazioni SVM è attualmente disattivata su tutta la rete, quindi SVM oggi è di sola lettura):
import {
deriveUnifiedAccount,
qoreAddresses,
addressesFrom20,
} from "@qorechain/sdk";
const account = await deriveUnifiedAccount(mnemonic);
account.cosmos; // "qor1…" bech32, Native lane
account.evm; // "0x…" EIP-55 hex, EVM lane
account.svm; // "<base58>" 32-byte SVM address (addr20 + 12 zero bytes)
account.addressBytes; // the raw 20 bytes shared by all three
account.publicKey; // "0x…" hex string of the 33-byte compressed secp256k1 key
account.privateKey; // "0x…" hex string (a secret: never log or commit it)
account.pqc; // { publicKey, secretKey } — ML-DSA-87, derived below
// Decode any ONE encoding into all three:
const all = qoreAddresses({ evm: account.evm });
all.cosmos; // qor1…
all.svm; // base58
// or straight from the raw 20 bytes:
const same = addressesFrom20(account.addressBytes);
unifiedAccountFromSeed(seed32) fa lo stesso a partire da una chiave privata
secp256k1 grezza a 32 byte.
La derivazione del seed PQC
La coppia di chiavi ML-DSA-87 dell'account è derivata in modo deterministico e legata all'indirizzo:
pqcSeed = shake256("qorechain:pqc:v1|" + cosmosAddress + "|" + mnemonic, 32)
quindi è recuperabile da { address, mnemonic } ed è identica in tutti gli
SDK di linguaggio di QoreChain. (Per unifiedAccountFromSeed, lo slot del
mnemonico è "seed:" + hex(seed32).) Dalla SDK 0.8.3
derivePqcCanonical(account, mnemonic) riconduce prima account alla sua
forma qor1… in minuscolo (canonicalAccount), quindi un bech32 in maiuscolo
o la forma 0x dello stesso account producono la stessa chiave.
Invio sulla Native lane con la chiave eth
Un account unificato firma le transazioni sul percorso Native con lo schema
eth_secp256k1: la firma classica è secp256k1 su keccak256 dei byte del
SignDoc (non sha256), e la chiave pubblica in SignerInfo usa il type URL
/cosmos.evm.crypto.v1.ethsecp256k1.PubKey. Il percorso ibrido
(signHybridEth, oppure EthNativeSigner con il suo signMode: "hybrid"
predefinito) allega inoltre l'estensione PQCHybridSignature ML-DSA-87, che è
obbligatoria sulle reti live.
Prima del primo invio ibrido servono due cose. Primo, la chiave ML-DSA-87
dell'account deve essere registrata (una sola volta). La registrazione è
l'unica transazione firmata in modo classico ed esente dalla regola ibrida:
firmala con signMode: "classical". La publicKey dell'account è una stringa
esadecimale 0x, quindi decodificala in byte per ecdsaPubkey. Secondo,
passa rest al firmatario in modo che possa chiedere alla rete quale
forma dei sign-bytes verifica (su qorechain-vladi e
qorechain-diana genera un errore invece di tirare a indovinare). Richiede
SDK 0.8.1 o successivo e npm i @cosmjs/stargate:
import { StargateClient } from "@cosmjs/stargate";
import {
createClient,
deriveUnifiedAccount,
EthNativeSigner,
buildRegisterPqcKeyMsg,
toBase,
} from "@qorechain/sdk";
const REST = "https://api.qore.host";
const RPC = "https://rpc.qore.host";
const CHAIN_ID = "qorechain-vladi";
const client = createClient({ network: "mainnet", endpoints: { rest: REST, rpc: RPC } });
const transport = await StargateClient.connect(RPC);
const account = await deriveUnifiedAccount(mnemonic);
// The REST account record carries the number and sequence (the account must
// exist, that is, have received funds).
async function authInfo() {
const { account: base } = await client.rest.get<{
account: { account_number: string; sequence: string };
}>(`/cosmos/auth/v1beta1/accounts/${account.cosmos}`);
return { accountNumber: BigInt(base.account_number), sequence: BigInt(base.sequence) };
}
// 1) Register the ML-DSA-87 key once (classical-signed, exempt from the hybrid rule).
const classical = new EthNativeSigner(account, { signMode: "classical" });
await classical.signAndBroadcast(transport, {
chainId: CHAIN_ID,
...(await authInfo()),
messages: [
buildRegisterPqcKeyMsg(account.cosmos, {
pqcKeypair: account.pqc,
ecdsaPubkey: Uint8Array.from(Buffer.from(account.publicKey.slice(2), "hex")),
}),
],
fee: { amount: [{ denom: "uqor", amount: "60000" }], gas: "600000" }, // the 2,592-byte key needs about 600k gas
});
// 2) Send with the hybrid signature (the default signMode).
const signer = new EthNativeSigner(account, { rest: REST });
const res = await signer.bankSend(
transport,
"qor1recipient…",
[{ denom: "uqor", amount: toBase("1.5") }],
{
chainId: CHAIN_ID,
...(await authInfo()),
fee: { amount: [{ denom: "uqor", amount: "40000" }], gas: "400000" }, // 0.1 uqor per gas is the floor
},
);
console.log(res.transactionHash);
Per un controllo di livello più basso, signHybridEth(params) /
signClassicalEth(params) restituiscono i byte TxRaw assemblati e gli
artefatti di firma, e accountAuthInfo(baseAccount) legge
account_number / sequence da un BaseAccount protobuf il cui pubkey
on-chain usa il type URL eth_secp256k1 (un account in JSON REST richiede
prima la mappatura dei campi, come fa authInfo qui sopra).
SDK 0.6.1 ha corretto un bug di codifica critico per il consenso:
l'estensione tx-body /qorechain.pqc.v1.PQCHybridSignature veniva
serializzata in JSON dentro Any.value, e la chain rifiutava quelle
transazioni già a CheckTx (un errore di parsing della tx). Ora è codificata
in protobuf (il valore dell'estensione inizia con 0x08) in tutti e cinque i
linguaggi. Qualsiasi transazione ibrida — inclusa la lane eth-native —
costruita con SDK ≤ 0.6.0 viene rifiutata on-chain: aggiorna a 0.6.1 o
successivo.
Phantom e altri wallet esterni
Gli SDK dalla 0.6.0 alla 0.7.0 includevano connectPhantomUnified() e unifiedAccountFromPhantomSignature() (rimosse nella 0.8.0; Python e Rust: unified_account_from_phantom_signature, Go: unified.UnifiedAccountFromPhantomSignature, Java: UnifiedAccounts.unifiedAccountFromPhantomSignature), che derivavano la chiave di spesa di un account da una firma Phantom sul messaggio fisso QoreChain unified account derivation v1 seguito dalla chiave pubblica base58 del wallet.
Una firma ed25519 è deterministica e qualsiasi sito web può chiedere al wallet di firmare lo stesso messaggio, quindi chiunque abbia mai ottenuto la firma può ricostruire le chiavi secp256k1 e ML-DSA dell'account. Entrambe le funzioni ora lanciano un errore.
Se hai creato un account in questo modo, consideralo compromesso da subito: crea un nuovo account da un mnemonic, sposta i fondi lì e poi collega Phantom al nuovo account come authenticator (vedi sotto). Il vecchio account non si può riparare, perché il suo indirizzo è legato alla chiave esposta e una rotazione della chiave PQC sostituisce solo la metà post-quantistica. Usa l'SDK 0.8.2 o successivo.
Per far spendere una chiave Phantom (o MetaMask) da un account, usa le corsie authenticator: registra la chiave del wallet con MsgRegisterAuthenticator, poi spendi dall'account PQC canonico tramite MsgExecuteCosmos / MsgExecuteEVM entro un permesso e una SpendingRule. L'account mantiene le proprie chiavi derivate dal mnemonic e la chiave del wallet può essere revocata in qualsiasi momento. Vedi Authenticator e spesa delegata. Nota che il modulo abstractaccount che fornisce queste corsie è attualmente disattivato su entrambe le reti (GET /qorechain/abstractaccount/v1/config mostra enabled: false), quindi una chiave del wallet non può ancora essere collegata; fino ad allora usa direttamente il nuovo account da mnemonic.
Crittografia post-quantistica (PQC)
QoreChain supporta firme ML-DSA-87 (Dilithium-5, FIPS 204). L'SDK espone direttamente le primitive.
import {
generatePqcKeypair,
pqcSign,
pqcVerify,
ML_DSA_87_PUBLIC_KEY_LENGTH,
ML_DSA_87_SIGNATURE_LENGTH,
} from "@qorechain/sdk";
const keypair = generatePqcKeypair();
const message = new TextEncoder().encode("hello");
const signature = pqcSign(keypair.secretKey, message);
const ok = pqcVerify(keypair.publicKey, message, signature);
Le costanti di lunghezza esportate (ML_DSA_87_PUBLIC_KEY_LENGTH,
ML_DSA_87_SECRET_KEY_LENGTH, ML_DSA_87_SIGNATURE_LENGTH,
ML_DSA_87_SEED_LENGTH) ti permettono di validare le dimensioni dei buffer.
Alla base, le primitive PQC provengono da qorechain-pqc — la libreria open-source basata solo su standard che avvolge implementazioni FIPS-204/203/202 sottoposte ad audit dietro un'unica API coerente in sei linguaggi (JavaScript/TypeScript, Rust, Go, C, Python, Java). Usala direttamente quando ti servono le primitive grezze o il framing
hybridSignBytesal di fuori dell'SDK.
Firmatari collegabili (pluggable)
Per la composizione, l'SDK fornisce un'astrazione Signer più le
implementazioni PqcSigner e HybridSigner, e un tipo SignatureMode
("classical" | "pqc" | "hybrid"). Usali quando vuoi inserire la firma PQC nel
tuo flusso invece di chiamare direttamente le primitive.
Firma ibrida
Una transazione ibrida porta sia una firma classica secp256k1 sia una
firma ML-DSA-87, così rimane valida sotto verifica classica pur ottenendo
protezione post-quantistica. La parte post-quantistica viaggia come
estensione PQCHybridSignature sulla transazione.
A partire dalla versione v3.2.0 della chain (quella su cui girano
entrambe le reti), il valore predefinito di rete è
hybrid_signature_mode = required con allow_classical_fallback = false. La
firma ibrida tramite buildHybridTx / signAndBroadcastHybrid (passa rest),
oppure signHybridEth / EthNativeSigner per gli account unificati
eth-native, è obbligatoria per le transazioni sul percorso Native; le
transazioni Native solo-classiche vengono rifiutate on-chain. Le transazioni
EVM sono una lane separata, ma dalla v3.2.0 sono ammesse solo da un account con
una chiave ML-DSA-87 registrata e una
finestra di autorizzazione aperta.
La release 0.6.1 ha corretto la codifica dell'estensione
PQCHybridSignature (JSON → protobuf, critico per il consenso). Le
transazioni ibride costruite con SDK 0.6.0 o precedente falliscono a CheckTx
con un errore di parsing della tx — aggiorna a 0.6.1+.
Il costruttore end-to-end per un account con coin type 118 è
signAndBroadcastHybrid (oppure buildHybridTx per costruire senza
trasmettere). Richiede il firmatario classico, la coppia di chiavi ML-DSA-87
dell'account, il numero di account e la sequence, e rest. SDK 0.8.3 o
successivo, npm i @cosmjs/stargate:
import { StargateClient } from "@cosmjs/stargate";
import {
deriveNativeAccount,
directSignerFromPrivateKey,
derivePqcCanonical,
signAndBroadcastHybrid,
qorechainRegistry,
MSG_SEND_TYPE_URL,
toBase,
} from "@qorechain/sdk";
const REST = "https://api.qore.host";
const RPC = "https://rpc.qore.host";
const account = await deriveNativeAccount(mnemonic); // coin type 118
const signer = await directSignerFromPrivateKey(account.privateKey, "qor");
const pqcKeypair = derivePqcCanonical(account.address, mnemonic); // the key registered on-chain
const transport = await StargateClient.connect(RPC);
const info = await transport.getSequence(account.address); // { accountNumber, sequence }
const result = await signAndBroadcastHybrid({
registry: qorechainRegistry(),
signer,
pqcKeypair,
messages: [
{
typeUrl: MSG_SEND_TYPE_URL,
value: {
fromAddress: account.address,
toAddress: "qor1recipient…",
amount: [{ denom: "uqor", amount: toBase("1.5") }],
},
},
],
fee: { amount: [{ denom: "uqor", amount: "40000" }], gas: "400000" }, // 0.1 uqor per gas is the floor
chainId: "qorechain-vladi",
accountNumber: info.accountNumber,
sequence: info.sequence,
rest: REST, // lets the SDK pick the v1/v2 PQC sign-bytes form
transport,
});
Prerequisito on-chain
Prima che una transazione ibrida possa essere verificata PQC, la chiave
pubblica PQC del firmatario deve essere registrata con
MsgRegisterPQCKeyV2 (firmata in modo classico, esente dalla regola ibrida).
Se ne occupano ensurePqcRegistered / buildRegisterPqcKeyMsg dell'SDK,
qorechaind tx pqc register-key o un wallet QoreChain. includePqcPublicKey: true si limita a incorporare la chiave nell'estensione; non la registra, e
la chain non registra una chiave al primo utilizzo.
Quale forma dei sign-bytes?
La metà ML-DSA-87 di una transazione ibrida è firmata su una di due forme di byte, e a ogni altezza una rete ne verifica esattamente una:
v1: BE32(len B0) ‖ B0 ‖ BE32(len A) ‖ A
v2: "qorechain-pqc-hybrid-v2" ‖ BE64(len chainId) ‖ chainId ‖ BE32(len B0) ‖ B0 ‖ BE32(len A) ‖ A
B0 è il corpo della transazione senza l'estensione PQC e A i byte di
AuthInfo. qorechain-vladi e qorechain-diana verificano entrambe la forma
v2. Il passaggio è stato registrato con il nome di upgrade v3.2.0 sulla
mainnet e con v3.1.98 (poi v3.2.0) sulla testnet, quindi un client deve
interrogare GET /cosmos/upgrade/v1beta1/applied_plan/{name} per entrambi
i nomi e usare v2 se una delle due altezze è maggiore di zero. L'SDK lo fa per
te: ogni builder ibrido accetta signBytesVersion ("auto" per impostazione
predefinita, oppure "v1" / "v2"), e "auto" richiede l'URL REST della rete
(rest) su queste due chain: genera un errore invece di tirare a indovinare. I
percorsi di alto livello rieseguono la risoluzione e ritentano una volta
quando la chain rifiuta la firma con il codice pqc 21. Nei test unitari passa
signBytesVersion esplicitamente, così il test non dipende da un nodo live.
Vedi anche Risoluzione dei problemi del codice 21.
Contratto della tx ibrida (panoramica)
La transazione viene firmata classicamente sui byte di firma standard (che
escludono l'estensione PQC), e la firma ML-DSA-87 viene calcolata e
allegata come estensione PQCHybridSignature. Poiché i byte di firma
classici escludono l'estensione, la firma classica resta valida sia che un
verificatore comprenda la parte PQC sia che non la comprenda. Gli helper di
livello più basso (encodeHybridExtension, attachHybridExtension,
buildHybridSignatureExtension, HYBRID_SIG_TYPE_URL) e i costruttori
end-to-end (buildHybridTx, signAndBroadcastHybrid) sono esportati per usi
avanzati.
L'invio di transazioni ibride è il percorso obbligatorio sulla rete live per le transazioni cosmos. Le primitive locali di firma/verifica e gli helper di costruzione tx sono disponibili già oggi.
Rotazione della chiave PQC
Dalla SDK 0.7.0 un account può ruotare la propria chiave ML-DSA-87 verso una
nuova chiave dello stesso algoritmo — migrando canonicamente una chiave
legacy shake256(mnemonic) verso la chiave legata all'indirizzo
shake256("qorechain:pqc:v1|addr|mnemonic") — tramite
rotatePqcKeyMsgFromMnemonic (entrambe le chiavi firmano congiuntamente i
byte della rotazione). Vedi
Rotazione della chiave nella guida
Autenticatori per un esempio completo.
Identificatori di algoritmo
L'SDK esporta ID di algoritmo e helper per lavoro a livello di protocollo:
AlgorithmUnspecified, AlgorithmDilithium5, AlgorithmMLKEM1024,
algorithmName(id), e isSignatureAlgorithm(id).