Saltar al contenido principal

Cuentas y firma PQC

Las cuentas de QoreChain se derivan de un único mnemónico BIP-39. Existen dos modelos de cuentas, ambos totalmente compatibles:

  • Derivación HD por carril (heredado/predeterminado) — el mismo mnemónico genera una cuenta nativa (coin type 118), una cuenta EVM (coin type 60) y una cuenta SVM (coin type 501) mediante rutas de derivación independientes. Tres claves, tres direcciones.
  • Cuentas unificadas eth-native (SDK 0.6.0, cadena v3.1.83) — UNA clave eth_secp256k1 es UNA identidad de 20 bytes representada en las tres codificaciones de dirección, con un único saldo compartido. Consulta Cuentas unificadas.

Derivación HD (heredado/predeterminado, 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

El mnemónico se valida (palabras y checksum) antes de derivar cualquier clave, de modo que un error tipográfico lanza una excepción en lugar de producir silenciosamente una cuenta incorrecta. Puedes validar explícitamente con validateMnemonic(mnemonic).

Esquemas de derivación​

TipoCurvaRutaDirección
nativesecp256k1m/44'/118'/0'/0/{i}bech32 qor de ripemd160(sha256(pubkey))
evmsecp256k1m/44'/60'/0'/0/{i}0x + keccak256(pubkey)[-20:], EIP-55
svmed25519m/44'/501'/{i}'/0'base58 de la clave pública de 32 bytes

Pasa un índice de cuenta para derivar cuentas adicionales. En TypeScript:

const second = await deriveNativeAccount(mnemonic, { accountIndex: 1 });

En Python/Go/Rust el índice es un argumento posicional (derive_native_account(mnemonic, 1) / DeriveNativeAccount(mnemonic, 1) / derive_native_account(&mnemonic, 1)).

Nota sobre vectores de prueba conocidos​

Los esquemas de derivación son deterministas y están cubiertos por pruebas de vectores conocidos (known-answer tests) en los cuatro SDKs, de modo que el mismo mnemónico produce direcciones idénticas en TypeScript, Python, Go y Rust. Esto te permite derivar en un lenguaje y verificar en otro.

Esta derivación por carril (deriveNativeAccount con coin type 118, más deriveEvmAccount / deriveSvmAccount) es el modelo heredado/predeterminado y sigue siendo compatible y sin cambios. Las cuentas unificadas descritas más abajo son un modelo de identidad adicional y opcional (opt-in).

Cuentas unificadas (eth-native)​

Desde el SDK 0.6.0 (cadena v3.1.83), deriveUnifiedAccount(mnemonic, index = 0) deriva UNA clave eth_secp256k1 en la ruta HD de Ethereum m/44'/60'/0'/0/{index}, cuyos 20 bytes de dirección (keccak256(pubkey)[12:]) son la MISMA identidad representada de tres formas:

CarrilCodificación
Nativebech32 con el prefijo qor (qor1…)
EVM0x + hexadecimal con checksum de mayúsculas/minúsculas mixtas EIP-55
SVMbase58 de los 20 bytes rellenados a la derecha con 12 bytes cero (32 bytes)

Un depósito en cualquiera de los tres carriles termina en un solo saldo, y la única clave es la firmante en todos los carriles (en el carril EVM dentro de una ventana de autorización; el carril de transacciones SVM está actualmente desactivado en toda la red, por lo que SVM es hoy de solo lectura):

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) hace lo mismo a partir de una clave privada secp256k1 sin procesar de 32 bytes.

La derivación de la semilla PQC​

El par de claves ML-DSA-87 de la cuenta se deriva de forma determinista y vinculada a la dirección:

pqcSeed = shake256("qorechain:pqc:v1|" + cosmosAddress + "|" + mnemonic, 32)

por lo que es recuperable a partir de { address, mnemonic } e idéntica en todos los SDKs de lenguaje de QoreChain. (Para unifiedAccountFromSeed, el campo del mnemónico es "seed:" + hex(seed32).) Desde el SDK 0.8.3, derivePqcCanonical(account, mnemonic) resuelve primero account a su forma qor1… en minúsculas (canonicalAccount), de modo que un bech32 en mayúsculas o la forma 0x de la misma cuenta produce la misma clave.

Envío en el carril Native con la clave eth​

Una cuenta unificada firma las transacciones de la ruta Native con el esquema eth_secp256k1: la firma clásica es secp256k1 sobre keccak256 de los bytes del SignDoc (no sha256), y la clave pública de SignerInfo usa la URL de tipo /cosmos.evm.crypto.v1.ethsecp256k1.PubKey. La ruta híbrida (signHybridEth, o EthNativeSigner con su signMode: "hybrid" predeterminado) además adjunta la extensión PQCHybridSignature de ML-DSA-87, obligatoria en las redes activas.

Se necesitan dos cosas antes del primer envío híbrido. Primero, la clave ML-DSA-87 de la cuenta debe estar registrada (una sola vez). El registro es la única transacción firmada de forma clásica y exenta de la regla híbrida: fírmala con signMode: "classical". El publicKey de la cuenta es una cadena hexadecimal 0x, así que decodifícala a bytes para ecdsaPubkey. Segundo, pasa rest al firmante para que pueda preguntar a la red qué forma de sign-bytes verifica (en qorechain-vladi y qorechain-diana lanza un error en lugar de adivinar). Requiere el SDK 0.8.1 o posterior y 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);

Para un control de más bajo nivel, signHybridEth(params) / signClassicalEth(params) devuelven los bytes ensamblados de TxRaw y los artefactos de firma, y accountAuthInfo(baseAccount) lee account_number / sequence de un BaseAccount protobuf cuya clave pública on-chain usa la URL de tipo eth_secp256k1 (una cuenta JSON de REST necesita que sus campos se mapeen primero, como hace authInfo arriba).

Actualiza al SDK 0.6.1+ para transacciones híbridas

El SDK 0.6.1 corrigió un error de codificación crítico para el consenso: la extensión del cuerpo de la transacción /qorechain.pqc.v1.PQCHybridSignature se serializaba en JSON dentro de Any.value, y la cadena rechazaba esas transacciones en CheckTx (un error de análisis de la transacción). Ahora se codifica en protobuf (el valor de la extensión comienza con 0x08) en los cinco lenguajes. Cualquier transacción híbrida — incluido el carril eth-native — construida con SDK ≤ 0.6.0 es rechazada on-chain: actualiza a la versión 0.6.1 o posterior.

Phantom y otros wallets externos​

Eliminado en el SDK 0.8.0

Los SDK 0.6.0 a 0.7.0 incluían connectPhantomUnified() y unifiedAccountFromPhantomSignature() (eliminadas en 0.8.0; en Python y Rust, unified_account_from_phantom_signature; en Go, unified.UnifiedAccountFromPhantomSignature; en Java, UnifiedAccounts.unifiedAccountFromPhantomSignature), que derivaban la clave de gasto de una cuenta a partir de una firma de Phantom sobre el mensaje fijo QoreChain unified account derivation v1 seguido de la clave pública base58 del wallet. Una firma ed25519 es determinista y cualquier sitio web puede pedir al wallet que firme ese mismo mensaje, así que quien haya obtenido alguna vez la firma puede reconstruir las claves secp256k1 y ML-DSA de la cuenta. Ambas funciones lanzan ahora un error.

Si creaste una cuenta de esta forma, considérala comprometida desde ya: crea una cuenta nueva a partir de un mnemónico, mueve los fondos a ella y después vincula Phantom a la nueva cuenta como autenticador (ver abajo). La cuenta antigua no se puede reparar, porque su dirección está ligada a la clave expuesta y una rotación de la clave PQC solo sustituye la mitad poscuántica. Usa el SDK 0.8.2 o posterior.

Para que una clave de Phantom (o MetaMask) gaste desde una cuenta, usa los carriles de autenticador: registra la clave del wallet con MsgRegisterAuthenticator y luego gasta desde la cuenta PQC canónica mediante MsgExecuteCosmos / MsgExecuteEVM bajo un permiso y una SpendingRule. La cuenta conserva sus propias claves derivadas del mnemónico y la clave del wallet se puede revocar en cualquier momento. Consulta Autenticadores y gasto delegado. Ten en cuenta que el módulo abstractaccount que ofrece estos carriles está desactivado en ambas redes (GET /qorechain/abstractaccount/v1/config muestra enabled: false), así que todavía no se puede vincular una clave de wallet; hasta entonces, usa directamente la nueva cuenta del mnemónico.

Criptografía poscuántica (PQC)​

QoreChain admite firmas ML-DSA-87 (Dilithium-5, FIPS 204). El SDK expone las primitivas directamente.

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);

Las constantes de longitud exportadas (ML_DSA_87_PUBLIC_KEY_LENGTH, ML_DSA_87_SECRET_KEY_LENGTH, ML_DSA_87_SIGNATURE_LENGTH, ML_DSA_87_SEED_LENGTH) te permiten validar el tamaño de los búferes.

Por debajo, las primitivas PQC provienen de qorechain-pqc — la biblioteca de código abierto y basada únicamente en estándares que envuelve implementaciones auditadas de FIPS-204/203/202 detrás de una API coherente en seis lenguajes (JavaScript/TypeScript, Rust, Go, C, Python, Java). Recurre a ella directamente cuando necesites las primitivas sin procesar o el formato hybridSignBytes fuera del SDK.

Firmantes conectables​

Para la composición, el SDK ofrece una abstracción Signer más las implementaciones PqcSigner y HybridSigner, y un tipo SignatureMode ("classical" | "pqc" | "hybrid"). Úsalos cuando quieras integrar la firma PQC en tu propio flujo en lugar de llamar directamente a las primitivas.

Firma híbrida​

Una transacción híbrida lleva tanto una firma clásica secp256k1 como una firma ML-DSA-87, de modo que sigue siendo válida bajo verificación clásica mientras gana protección poscuántica. La parte poscuántica viaja como una extensión PQCHybridSignature en la transacción.

La firma híbrida es obligatoria en la ruta Native

A partir de la versión de cadena v3.2.0 (la que ejecutan ambas redes), el valor predeterminado de la red es hybrid_signature_mode = required con allow_classical_fallback = false. La firma híbrida mediante buildHybridTx / signAndBroadcastHybrid (pasa rest), o signHybridEth / EthNativeSigner para cuentas unificadas eth-native, es obligatoria para las transacciones de la ruta Native; las transacciones Native solo-clásicas son rechazadas on-chain. Las transacciones EVM son un carril independiente, pero desde v3.2.0 solo se admiten desde una cuenta con una clave ML-DSA-87 registrada y una ventana de autorización abierta.

Las transacciones híbridas del SDK ≤ 0.6.0 son rechazadas

La versión 0.6.1 corrigió la codificación de la extensión PQCHybridSignature (JSON → protobuf, crítico para el consenso). Las transacciones híbridas construidas con SDK 0.6.0 o anterior fallan en CheckTx con un error de análisis de la transacción — actualiza a 0.6.1+.

El constructor de extremo a extremo para una cuenta con coin type 118 es signAndBroadcastHybrid (o buildHybridTx para construir sin difundir). Necesita el firmante clásico, el par de claves ML-DSA-87 de la cuenta, el número de cuenta y la secuencia, y rest. SDK 0.8.3 o posterior, 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,
});

Requisito previo on-chain​

Antes de que una transacción híbrida pueda verificarse mediante PQC, la clave pública PQC del firmante debe estar registrada con MsgRegisterPQCKeyV2 (firmada de forma clásica, exenta de la regla híbrida). Lo hacen ensurePqcRegistered / buildRegisterPqcKeyMsg del SDK, qorechaind tx pqc register-key o un wallet de QoreChain. includePqcPublicKey: true solo incrusta la clave en la extensión; no la registra, y la cadena no registra una clave en el primer uso.

¿Qué forma de sign-bytes?​

La mitad ML-DSA-87 de una transacción híbrida se firma sobre una de dos formas de bytes, y una red verifica exactamente una en cada altura:

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 es el cuerpo de la transacción sin la extensión PQC y A los bytes de AuthInfo. qorechain-vladi y qorechain-diana verifican ambas v2. El cambio quedó registrado con el nombre de actualización v3.2.0 en la mainnet y con v3.1.98 (después v3.2.0) en la testnet, así que un cliente debe consultar GET /cosmos/upgrade/v1beta1/applied_plan/{name} para ambos nombres y usar v2 si alguna de las alturas es mayor que cero. El SDK lo hace por ti: cada constructor híbrido acepta signBytesVersion ("auto" por defecto, o "v1" / "v2"), y "auto" necesita la URL REST de la red (rest) en estas dos cadenas: lanza un error en lugar de adivinar. Las rutas de alto nivel vuelven a resolver y reintentan una vez cuando la cadena rechaza la firma con el código pqc 21. En las pruebas unitarias pasa signBytesVersion de forma explícita para que la prueba no dependa de un nodo activo. Consulta también Solución de problemas del código 21.

Contrato de la tx híbrida (nivel alto)​

La transacción se firma clásicamente sobre los bytes de firma estándar (que excluyen la extensión PQC), y la firma ML-DSA-87 se calcula y se adjunta como la extensión PQCHybridSignature. Dado que los bytes de firma clásicos excluyen la extensión, la firma clásica sigue siendo válida sea o no que un verificador entienda la parte PQC. Los ayudantes de bajo nivel (encodeHybridExtension, attachHybridExtension, buildHybridSignatureExtension, HYBRID_SIG_TYPE_URL) y los constructores de extremo a extremo (buildHybridTx, signAndBroadcastHybrid) se exportan para uso avanzado.

El envío de transacciones híbridas es la ruta obligatoria en la red activa para las transacciones cosmos. Las primitivas locales de firma/verificación y los ayudantes de construcción de transacciones están disponibles hoy.

Rotación de claves PQC​

Desde el SDK 0.7.0, una cuenta puede rotar su clave ML-DSA-87 a una nueva clave del mismo algoritmo — migrando de forma canónica una clave heredada shake256(mnemonic) a la clave vinculada a la dirección shake256("qorechain:pqc:v1|addr|mnemonic") — mediante rotatePqcKeyMsgFromMnemonic (ambas claves firman conjuntamente los bytes de la rotación). Consulta Rotación de claves en la guía de Autenticadores para ver un ejemplo completo.

Identificadores de algoritmo​

El SDK exporta identificadores de algoritmo y funciones auxiliares para trabajo a nivel de protocolo: AlgorithmUnspecified, AlgorithmDilithium5, AlgorithmMLKEM1024, algorithmName(id), y isSignatureAlgorithm(id).