Zum Hauptinhalt springen

Authenticatoren & delegiertes Ausgeben

Authenticator-Lanes (SDK 0.7.0, Chain v3.1.85) erlauben es einem verknüpften externen Schlüssel — einem Phantom-ed25519-Schlüssel oder einem MetaMask-/EVM-secp256k1-Schlüssel — vom EINEN kanonischen PQC-pflichtigen Konto auszugeben: nach dem Prinzip der geringsten Berechtigung, mit Ausgabenlimits und jederzeit widerrufbar, ohne dass der externe Schlüssel jemals eine ML-DSA-Co-Signatur erzeugt.

Dies ist das SDK-Gegenstück zum Account-Abstraction-Modul der Chain.

Auf den Live-Netzwerken nicht verfügbar

Das Modul x/abstractaccount ist auf qorechain-vladi und qorechain-diana deaktiviert (GET /qorechain/abstractaccount/v1/config liefert "enabled": false). Ab Chain v3.2.0 wird dieses Flag auf jedem Pfad durchgesetzt (Erstellen eines Abstract Accounts, Verknüpfen eines Authenticators sowie MsgExecuteEVM / MsgExecuteCosmos), und die Chain weist sie mit abstract account module is disabled (Code 9) ab. Keine Transaktion kann es einschalten. Alles Folgende beschreibt die Schnittstellen des Moduls nur zur Referenz: Bauen Sie auf den Live-Netzwerken keinen Ablauf mit verknüpften Schlüsseln oder delegiertem Ausgeben, solange das Modul nicht aktiviert ist, und prüfen Sie das Flag, bevor Sie einen solchen Ablauf anbieten.

Das Relayer-Modell​

Ein Relayer reicht die Transaktion ein und bezahlt die Gebühren. Die eigene hybride Signatur des Relayers (klassisch + ML-DSA-87) erfüllt den Ante-Handler auf dem Envelope, sodass die PQC-Signatur des kanonischen Kontos on-chain nicht benötigt wird. Die Autorisierung ist stattdessen die Signatur des verknüpften Schlüssels über einen domänen-separierten, replay-gebundenen Sign-Bytes-Digest.

Phantom / MetaMask key Relayer (pays fees) Chain
───────────────────── ─────────────────── ─────
sign(authSignBytes) ──────────▶ wrap in Msg, sign envelope ──▶ verify authenticator sig
check permission + rule
spend FROM canonical account

Der Relayer ist ein anderes Konto als der Eigentümer und erhöht daher die EVM-Nonce des Kontos nicht.

Die drei Lanes​

LaneMessageSign-BytesGibt aus
EVMMsgExecuteEVMevmAuthSignBytesnatives QOR / EVM-Call von der 0x-Adresse des Kontos
NativeMsgExecuteCosmoscosmosAuthSignBytesnatives QOR über x/bank vom Konto
SchlüsselrotationMsgRotatePQCKeyrotationSignBytes(rotiert den PQC-Schlüssel des Kontos)

Die Message-Typ-URLs sind /qorechain.abstractaccount.v1.MsgExecuteEVM, /qorechain.abstractaccount.v1.MsgExecuteCosmos und /qorechain.pqc.v1.MsgRotatePQCKey.

Einen Phantom-Authenticator registrieren​

Das Verknüpfen eines Schlüssels ist owner-signiert (eine normale hybride Transaktion des kanonischen Kontos): MsgRegisterAuthenticator benennt den Schlüssel (Schema + rohe Pubkey-Bytes), die gewährten permissions und eine expiryUnix-Sitzungsfrist. Ausgabenlimits werden mit einer SpendingRule über MsgUpdateSpendingRules angehängt:

import { msg } from "@qorechain/sdk";

// The Phantom wallet in the browser:
const phantomPubkey = window.solana.publicKey.toBytes(); // 32-byte ed25519

// 1) Link the key: least privilege ("send" only) + a session expiry.
const register = msg.abstractaccount.registerAuthenticator({
owner: canonicalAccount, // the PQC-required account ("qor1…")
accountAddress: canonicalAccount, // the account the key may act for
scheme: "ed25519", // Phantom keys are ed25519
pubkey: phantomPubkey,
permissions: ["send"], // e.g. "send", "evm", "svm" — never "all" for a hot key
expiryUnix: String(Math.floor(Date.now() / 1000) + 7 * 24 * 3600), // 7 days
label: "phantom",
});

// 2) Bound what it can move: per-tx and daily limits, uqor only.
const limits = msg.abstractaccount.updateSpendingRules({
owner: canonicalAccount,
accountAddress: canonicalAccount,
rules: [
{
id: "phantom-hot",
perTxLimit: "1000000", // ≤ 1 QOR per spend
dailyLimit: "10000000", // ≤ 10 QOR per day
allowedDenoms: ["uqor"],
enabled: true,
},
],
});

// Broadcast BOTH owner-signed (hybrid) — e.g. via the hybrid tx path:
// await signAndBroadcastHybrid({ ..., messages: [register, limits] });
Eine Regel begrenzt einen Schlüssel nur, wenn eine existiert

Die Chain ermittelt das Limit in dieser Reihenfolge: die eigene Regel des Authenticators, wenn sie gesetzt und enabled ist; andernfalls die erste enabled-Regel des Kontos; andernfalls keine, und dann gibt es kein Limit. Eine mit enabled: false gespeicherte Regel bedeutet kein Limit, nicht ein Limit von null. MsgRegisterAuthenticator trägt keine Regel, ein allein mit dieser Message verknüpfter Schlüssel ist also nur durch eine Regel auf Kontoebene begrenzt. Setzen Sie bewusst eine Regel mit MsgUpdateSpendingRules und registrieren Sie nur die Berechtigungen, die der Schlüssel braucht. Ein Limit begrenzt den Schaden, den ein verknüpfter Schlüssel anrichten kann; es ersetzt nicht den Widerruf des Schlüssels.

Um einen Schlüssel sofort zu deaktivieren, broadcastet der Eigentümer msg.abstractaccount.revokeAuthenticator({ owner, accountAddress, scheme, pubkey }).

Von Phantom ausgeben (Native-Lane, über einen Relayer)​

Sobald der Schlüssel verknüpft ist, baut der Browser eine relayer-fertige MsgExecuteCosmos: buildPhantomExecuteCosmos rekonstruiert den domänen-separierten Digest, lässt ihn von Phantom signieren (signMessage) und liefert die { typeUrl, value }-Message zurück.

Browser (der Phantom-Nutzer):

import { buildPhantomExecuteCosmos } from "@qorechain/sdk";

// window.solana is a Phantom-style wallet: { publicKey, signMessage }.
const msgExecute = await buildPhantomExecuteCosmos({
wallet: window.solana,
relayer: relayerAddress, // who will submit + pay fees
chainId: "qorechain-vladi",
account: canonicalAccount, // the PQC-required owner
to: recipient, // "qor1…"
amount: "100uqor", // single-coin amount string
nonce, // the per-authenticator sequence for (account, pubkey)
});

// Ship `msgExecute` to your relayer service (it is already signed by Phantom):
await fetch("/api/relay", {
method: "POST",
body: JSON.stringify({
typeUrl: msgExecute.typeUrl,
value: {
...msgExecute.value,
pubkey: Buffer.from(msgExecute.value.pubkey).toString("base64"),
signature: Buffer.from(msgExecute.value.signature).toString("base64"),
nonce: msgExecute.value.nonce.toString(),
},
}),
});

Server (der Relayer): signiert den Envelope mit seinem eigenen Konto über den Hybrid-Pfad (signAndBroadcastHybrid, mit übergebenem rest) und bezahlt die Gebühren. Der ML-DSA-87-Schlüssel des Relayers muss wie bei jedem Konto registriert sein. Die Signatur des Authenticators innerhalb der Message ist die Autorisierung, vom Konto des Eigentümers auszugeben.

import { StargateClient } from "@cosmjs/stargate";
import {
deriveNativeAccount,
directSignerFromPrivateKey,
derivePqcCanonical,
signAndBroadcastHybrid,
qorechainRegistry,
} from "@qorechain/sdk";

const REST = "https://api.qore.host";
const RPC = "https://rpc.qore.host";

// The relayer's OWN account: a different account than the owner.
const mnemonic = process.env.RELAYER_MNEMONIC!;
const relayer = await deriveNativeAccount(mnemonic);
const signer = await directSignerFromPrivateKey(relayer.privateKey, "qor");
const pqcKeypair = derivePqcCanonical(relayer.address, mnemonic);

const transport = await StargateClient.connect(RPC);
const info = await transport.getSequence(relayer.address);

// `msgExecute` is the message decoded from the request. The relayer pays fees.
const result = await signAndBroadcastHybrid({
registry: qorechainRegistry(),
signer,
pqcKeypair,
messages: [msgExecute],
fee: { amount: [{ denom: "uqor", amount: "40000" }], gas: "400000" },
chainId: "qorechain-vladi",
accountNumber: info.accountNumber,
sequence: info.sequence,
rest: REST,
transport,
});
console.log(result.transactionHash);

Eine lauffähige End-to-End-Version (mit einem lokalen ed25519-Schlüssel als Stellvertreter für Phantom) ist das Beispiel authenticator-spend.

Von MetaMask ausgeben (EVM-Lane)​

Ein MetaMask-Schlüssel wird über seine 20-Byte-ETH-Adresse (Schema secp256k1) mit registerEthAuthenticatorMsg verknüpft und autorisiert Ausgaben mit einer 65-Byte-EIP-191-personal_sign-Signatur über dieselbe Art von Digest.

1) Der Eigentümer verknüpft die MetaMask-Adresse (owner-signiert, hybrid):

import { registerEthAuthenticatorMsg } from "@qorechain/sdk";

const [ethAddress] = await window.ethereum.request({
method: "eth_requestAccounts",
params: [],
});

const register = registerEthAuthenticatorMsg({
owner: canonicalAccount,
ethAddress, // 0x-hex 20-byte address = the authenticator pubkey
permissions: ["evm"], // EVM lane only
expiryUnix: Math.floor(Date.now() / 1000) + 24 * 3600, // 24 h session
label: "metamask",
});
// broadcast owner-signed (hybrid), like any other message

2) MetaMask autorisiert einen EVM-Transfer — buildMetaMaskExecuteEvm baut den Digest, fordert personal_sign (EIP-191) vom Provider an und liefert eine relayer-fertige MsgExecuteEVM zurück:

import { buildMetaMaskExecuteEvm } from "@qorechain/sdk";

const msgExecute = await buildMetaMaskExecuteEvm({
provider: window.ethereum, // any EIP-1193 provider
address: ethAddress, // the linked 20-byte address (0x-hex)
relayer: relayerAddress,
chainId: "qorechain-vladi",
account: canonicalAccount, // the PQC-required owner
to: "0xRecipient…", // 0x-hex recipient
value: "1000000000000000000",// decimal wei string (EVM lane: 18 decimals)
gasLimit: 100000,
nonce: evmNonce, // the account's CURRENT EVM nonce — do NOT +1
});
// hand `msgExecute` to the relayer, exactly as in the Phantom flow

buildMetaMaskExecuteCosmos funktioniert auf dieselbe Weise für die Native-Lane (to: "qor1…", amount: "100uqor", nonce = die Sequenz pro Authenticator). Es gibt passende Low-Level-Composer — executeEvmMsg, executeCosmosMsg, registerEthAuthenticatorMsg, revokeAuthenticatorMsg, rotatePqcKeyMsg — falls Sie Schlüssel und Signaturen selbst verwalten.

Sign-Bytes (byte-exakt)​

Zwei Byte-Helfer: BE64(n) ist eine 8-Byte-Big-Endian-Ganzzahl; LP(bytes) ist BE64(len) ‖ bytes (längenpräfixiert).

EVM-Lane — evmAuthSignBytes({ chainId, account, pubkey, to, value, data, nonce }) liefert einen 32-Byte-Digest zurück:

sha256( "qorechain-evm-auth-v1"
‖ LP(chainId) ‖ LP(account) ‖ LP(pubkey)
‖ LP(to) ‖ LP(value) ‖ LP(data) ‖ BE64(nonce) )

to ist der 0x-Hex-Empfänger, value der dezimale wei-String, data die rohen Calldata.

Native-Lane — cosmosAuthSignBytes({ chainId, account, pubkey, to, amount, nonce }) liefert einen 32-Byte-Digest zurück:

sha256( "qorechain-cosmos-auth-v1"
‖ LP(chainId) ‖ LP(account) ‖ LP(pubkey)
‖ LP(to) ‖ LP(amount) ‖ BE64(nonce) )

amount ist der kanonische Single-Coin-String (z. B. 100uqor).

Rotation — rotationSignBytes(chainId, algorithmId, account, oldPub, newPub) liefert den String zurück, den beide Schlüssel signieren (dessen UTF-8):

qorechain-pqc-rotate-v1|<chainId>|<algorithmId>|<account>|<oldHex>|<newHex>

Nonces​

  • MsgExecuteEVM.nonce = die aktuelle EVM-Nonce des Kontos (der Relayer ist ein anderes Konto und erhöht die Nonce des Eigentümers nicht — also nicht 1 addieren).
  • MsgExecuteCosmos.nonce = die Sequenz pro Authenticator für (account, pubkey) — ein Store-Zähler, getrennt von der eigenen Sequenz des Kontos, der bei jeder erfolgreichen Native-Lane-Ausgabe inkrementiert wird.

Eine falsche Nonce führt zu einer Replay-Ablehnung (abstractaccount Code 11, siehe unten).

// EVM lane: the account's current nonce, straight from the EVM JSON-RPC.
const evmNonce = await client.evm.call<string>("eth_getTransactionCount", [
account0x,
"latest",
]);

Das Permission-Schema​

Die Chain veröffentlicht die kanonische Authenticator-Permission-Taxonomie, damit Clients Scopes validieren können, ohne Strings zu hartkodieren, und Abweichungen über schema_version erkennen:

// REST (LCD):
const schema = await client.rest.getPermissionSchema();

schema.schema_version; // bumps on any taxonomy/mapping change
schema.permissions; // ["send", "evm", "svm", "all", ...]
schema.msg_permissions; // { "/qorechain.abstractaccount.v1.MsgExecuteEVM": "evm", ... }
schema.key_management_msgs; // typeURLs NEVER delegable to a linked key

Die REST-Route ist GET /qorechain/abstractaccount/v1/permission_schema; der typisierte gRPC-Query-Client stellt dieselben Daten als clients.abstractaccount.permissionSchema() bereit. Das Modul bedient außerdem /config, /accounts und /accounts/{address}.

Fehlercodes​

Fehlschläge werden über decodeTxError mit einem verständlichen kind dekodiert:

CodespaceCodeKind
abstractaccount5spending_limit_exceeded
abstractaccount6session_key_expired
abstractaccount10permission_denied
abstractaccount11authenticator_replay
pqc21hybrid_verify_failed
import { decodeTxError } from "@qorechain/sdk";

const decoded = decodeTxError({
code: result.code,
codespace: result.codespace,
rawLog: result.rawLog,
});

switch (decoded.kind) {
case "spending_limit_exceeded": // over the per-tx or daily SpendingRule
break;
case "session_key_expired": // expiryUnix passed — re-register the key
break;
case "permission_denied": // scope missing — check the permission_schema
break;
case "authenticator_replay": // wrong nonce — refetch and re-sign
break;
case "hybrid_verify_failed": // ML-DSA sig did not verify (see note below)
break;
}

hybrid_verify_failed bedeutet, dass die ML-DSA-87-Signatur nicht verifiziert wurde. Hedged Signierung ist nicht die Ursache (die Chain akzeptiert hedged und deterministische ML-DSA-87-Signaturen gleichermaßen); siehe Fehlerbehebung für Code 21. Derselbe Fehler tritt auch auf, wenn ein SDK vor 0.6.1 die Hybrid-Extension JSON-kodiert hat (bitte upgraden - siehe Accounts & PQC-Signierung).

Schlüsselrotation​

Rotieren Sie den ML-DSA-87-Schlüssel eines Kontos zu einem neuen Schlüssel desselben Algorithmus — zum Beispiel bei der Migration eines Legacy-Schlüssels aus der Chain-Bridge-Ableitung (shake256(mnemonic)) zum kanonischen adressgebundenen Schlüssel (shake256("qorechain:pqc:v1|addr|mnemonic")):

import { rotatePqcKeyMsgFromMnemonic, derivePqcLegacy } from "@qorechain/sdk";

const { msg, oldKeypair, newKeypair } = rotatePqcKeyMsgFromMnemonic({
account,
mnemonic,
chainId: "qorechain-vladi",
// oldDerivation: "bridge" (legacy), newDerivation: "adapter" (canonical) by default
});
// broadcast `msg` BY the account, cosigned (hybrid) with the OLD key —
// both keys dual-sign the rotation bytes (old proves ownership, new proves control).

derivePqcLegacy(mnemonic) reproduziert das Legacy-Schlüsselpaar eigenständig, wenn Sie es benötigen (z. B. um bis zum Abschluss der Rotation weiter zu signieren).

Weiter​