Rand Protocol
Documentation
Guides

Pont

Le pont à gardiens tel qu'il est construit : un verrouillage sur la chaîne source, une attestation de gardien avec une cosignature Dilithium2, un BridgeAttest qui dépose une note, un BridgeBurn en un seul bundle qui retire, avec le format de transmission, les sept garanties de zUSD, les frais zUSD à la création et à la destruction, les plafonds de création et la mise en pause, les rotations rules-v2 avec leur preuve de possession et leur délai, et ce qui est en production sur la chain 20.

Utilisez cette page pour intégrer le bridge : construire un dépôt sur une chaîne source, soumettre une attestation sur Rand, brûler un actif bridgé pour le ressortir, et lire l’état public du bridge. La chaîne 20, le testnet actuel (en service depuis le 2026-10-01 06:17 UTC, fullnode v0.6.8), a été créée avec une section bridge : huit gardiens à partir de l’indice d’ensemble de gardiens 1, huit gardiens post-quantiques, une clé de pause, des émetteurs pour Ethereum, BSC, Tron et Solana (les trois premiers étant les endpoints redéployés le 2026-09-30), le groupe rules-v2, dont le plafond global d’émission est de 4 000 zUSD par fenêtre glissante de 24 heures, un plancher anti-rejeu entrant, et deux groupes nouveaux dans la v0.6.8 : des frais de 10 bps sur chaque émission et chaque destruction, prélevés en zUSD, et des règles de rotation qui obligent toute rotation de l’ensemble post-quantique ou de la clé de pause à prouver la possession des nouvelles clés et à attendre un jour. Contrairement à celui de la chaîne 14, son genesis liste zUSD avec les sept collatéraux et reprend la garde transmise depuis la chaîne 14 à chaque création de chaîne depuis (Reprise depuis la chaîne 14). rand_getAssets et le /bridge/assets de l’explorateur donnent le locked courant de chaque collatéral.

Le flux

source chain (Ethereum / BSC / Tron / Solana)                 Rand
┌────────────────────────────────┐                           ┌────────────────────────────────┐
│ lock tokens, emit              │  guardians observe,       │ BridgeAttest transaction        │
│ (amount, token, to, fee)       │  sign mu = keccak²(body)  │   admission → one deposit note  │
└────────────────────────────────┘  threshold n·2/3 + 1      │   computed by the chain          │
              ▲                     + Dilithium2 co-signers  │                                  │
   release after guardian         guardian committee         │ BridgeBurn transaction           │
   signatures on the burn  ◄───── secp256k1 keys ◄─────────  │   one bundle: slots 0–1 burn the │
   message                        read the burn log          │   asset, 2–3 pay the RAND fee;   │
                                                             │   emits a sequenced burn message │
                                                             └────────────────────────────────┘

Entrant : la valeur est verrouillée sur la chaîne source, les gardiens attestent, n’importe qui soumet l’attestation à Rand, et le registre ajoute une note de dépôt pour le destinataire — une note dont le mot asset est l’indice de l’actif bridgé dans le registre. Sortant : une transaction Rand brûle des notes de cet actif et émet un message ; les gardiens attestent ce message ; le contrat de la chaîne source libère les fonds. Rand ne vérifie jamais d’en-tête de bloc ni de preuve d’état d’une chaîne source : les signatures du comité de gardiens constituent l’intégralité du modèle de confiance.

Modèle de confiance et identifiants de chaîne

Identifiants de chaîne propres au bridge, partagés avec les contrats (bridge-codec) :

chaîneid
Rand1
Ethereum2
BSC3
Tron4
Solana5

Les gardiens forment un comité de clés secp256k1 ; l’identité d’un gardien correspond aux 20 derniers octets de keccak256(uncompressed_pubkey[1..]), la dérivation d’adresse d’Ethereum, de sorte qu’une même clé fonctionne sur les contrats EVM, le programme Solana et Rand. Le quorum pour un ensemble de taille n est n·2/3 + 1. Toute signature doit être low-s (s ≤ n/2), ce qui exclut la malléabilité. Le condensat que signent les gardiens est mu = keccak256(keccak256(body_bytes)). Une rotation de l’ensemble de gardiens est une attestation de l’émetteur de gouvernance (chain 1, keccak256("rand-bridge-governance")), doit porter new_index == current + 1, et doit être signée par l’ensemble courant, non par un ensemble remplacé ; l’ensemble sortant conserve une fenêtre de grâce de 86 400 secondes pour les transferts en cours. (Il s’agit de l’ensemble ECDSA. L’ensemble post-quantique et la clé de pause font l’objet d’actions de rotation distinctes, qui sur la chaîne 20 attendent un délai d’un jour : ci-dessous.) Si n·2/3 + 1 gardiens s’entendent, ils peuvent émettre une valeur arbitraire ; le renforcement du bridge borne le montant et la vitesse, sans supprimer ce risque.

Liaison de la transaction. Une transaction Rand ne porte aucune signature : une destruction est autorisée par la preuve STARK de son bundle. Depuis la chaîne 14, chaque preuve de bundle est produite sur Transaction::binding, et vérifiée par rapport à lui — un condensat de la transaction entière (identifiant de chaîne, l’unique bundle avec ses quatre enveloppes, l’action) dont seuls les octets de preuve sont mis à blanc. Depuis la chaîne 17, un bundle porte deux preuves, la preuve du bundle et la preuve d’autorisation de dépense : la liaison met à blanc les deux, conserve le auth_commit du bundle, et les deux sont vérifiées par rapport à elle. Une copie d’une destruction en attente dont to, relayer_fee ou (to_chain, token) ont été modifiés, ou d’une attestation dont le time ou l’enveloppe ont été modifiés, échoue à la preuve (TxError::InvalidBundleProof). Sur la chaîne 20, la liaison porte aussi le condensat du genesis (genesis binding_domain: 1 : rand-tx-bind-2 sur le condensat du genesis, l’identifiant de chaîne, le bundle et l’action), de sorte qu’une preuve produite pour une autre chaîne partageant le même identifiant de chaîne n’est pas non plus vérifiée. Les portefeuilles et les relayeurs doivent exécuter un binaire compilé pour la chaîne à laquelle ils soumettent : v0.6.8 sur la chaîne 20 (v0.6.7 sur les chaînes 18 et 19) ; un rand plus ancien produit des preuves et des signatures que la chaîne 20 refuse.

Les démons gardien et relayeur ne font pas partie du full node ; le full node embarque le codec partagé et la moitié réceptrice.

Format de transmission

Tous les entiers sont en big-endian ; la version du format est 1.

Enveloppe d’attestation

décalagelong.champ
01version = 1
14guardian_set_index
51n_sigs
666·n_sigssignatures : index (1), r (32), s (32), v (1)
6 + 66·n_sigs…body

En-tête du corps — 51 octets, puis la charge utile

décalagelong.champ
04timestamp
44nonce
82emitter_chain
1032emitter_address
428sequence
501consistency_level
51…payload

Charge utile Transfer — id 1, 133 octets

décalagelong.champ
01id = 1
132amount (u256 ; Rand utilise les 128 bits de poids faible, et un dépôt doit aussi tenir dans le u64 d’une note)
3332token_address
652token_chain
6732to — le condensat du destinataire, voir ci-dessous
992to_chain
10132fee (u256) : la part de amount revenant au relayeur

Charge utile GuardianSetUpgrade — id 2 : id (1), new_index (4), n (1), puis n clés de 20 octets.

Les adresses Ethereum, BSC et Tron font 20 octets, complétés à gauche par 12 octets nuls dans les emplacements de 32 octets ; une adresse Solana remplit l’emplacement. Un Transfer doit provenir de l’émetteur enregistré de la chaîne pour emitter_chain, et token_chain doit être égal à la chaîne émettrice. Une attestation fait au plus 16 KiB. Aucun des volets du renforcement du bridge n’a modifié le format de transmission, le codec ni les endpoints déployés sur les chaînes sources : les co-signatures post-quantiques et les messages de gouvernance circulent dans la transaction Rand, non dans l’attestation.

Le destinataire sur 32 octets

Une adresse blindée fait 1 216 octets et le format de transmission n’en prévoit que 32. Le champ to porte un condensat de cette adresse :

to = blake3("rand-shielded-recipient" || pk_bytes || kem_ek)     // ShieldedAddress::recipient_hash

Le déposant indique le condensat sur la chaîne source, les gardiens le signent, le soumissionnaire place l’adresse complète dans l’action BridgeAttest, et le registre recalcule le condensat et refuse toute différence — faute de quoi le soumissionnaire pourrait choisir qui reçoit le dépôt d’autrui. Les interfaces des chaînes sources doivent accepter cette forme ; le portefeuille l’affiche. La chaîne 11 a brièvement fait de cet emplacement directement l’identifiant du récepteur ; cette forme d’adresse a été annulée avec la chaîne 12 (Comptes et adresses).

Un jeton, plusieurs collatéraux : zUSD

Le registre des actifs n’appartient plus au bridge. Il a été déplacé dans le registre de jetons RPL au niveau du registre, partagé par les jetons natifs et bridgés, et la racine d’état de la chaîne s’y engage sous le nom tokens_root. Un jeton bridgé est une entrée du registre dont l’autorité d’émission est Bridge { backings }, et il ne correspond plus à une seule paire (chain, token). Le premier, zUSD, est adossé à des USDT et des USDC verrouillés sur quatre chaînes — Ethereum, Binance Smart Chain et Solana portent les deux pièces, Tron seulement l’USDT (Circle a abandonné l’USDC sur Tron) :

chain  token (32-byte wire form)                                                  coin  decimals
2      000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7           USDT  6
2      000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48           USDC  6
3      00000000000000000000000055d398326f99059ff775485246999027b3197955           USDT  18
3      0000000000000000000000008ac76a51cc950d9822d68b83fe1ad97b32cd580d           USDC  18
4      000000000000000000000000a614f803b6fd780986a42c78ec9c7f77e6ded13c           USDT  6
5      ce010e60afedb22717bd63192f54145a3f965a33bb82d2c7029eb2ce1e208264           USDT  6
5      c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d61           USDC  6

Sur la chaîne 20, zUSD porte l’indice 1, rpl1xtj6k2x8strx8c2d5fjs50ltztck5ykms4ve7nmzmstf6fhn0v0spelqtx, le nom « Shielded USD », 8 décimales, et figure dans le fichier genesis, comme sur les chaînes 15 à 19, avec le nom, le symbole et le sel qu’il avait sur la chaîne 14 ; son identifiant d’actif (32e5ab28c782c663e14da2650a3feb12f16a12db85599f4f62dc169d26f37b1f) et son identifiant rpl1… ont donc été repris à l’identique (explorateur : GET /tokens/1). Sur la chaîne 14, il avait été enregistré à la hauteur 256 par une transaction register_bridged_token.

Backing { chain: u16, token: [u8; 32], decimals: u8, locked: u64, minted_today: u64, mint_day: u32 }

Une paire (chain, token) ne garantit qu’un seul jeton dans tout le registre (TokenError::BackingTaken) ; un jeton a de 1 à 32 collatéraux ; decimals est le nombre de décimales propre à la pièce source, de 0 à 18, jamais les 8 du jeton bridgé. Le collatéral mutualisé est un risque assumé : les sept pièces sont fongibles à parité en entrée comme en sortie de zUSD, de sorte que la perte de parité d’une seule d’entre elles dilue chaque détenteur de zUSD, et il n’existe aucun plafond côté Rand sur la part d’un collatéral.

zUSD est un panier. Un détenteur supporte le risque de chaque collatéral listé, et non celui de la pièce qu’il a déposée : un zUSD est une créance sur le pool, sur tout collatéral dont le locked est suffisant. Rien ne fixe le prix d’un collatéral. Un verrouillage de 1 unité de n’importe quelle pièce listée émet 1 zUSD, et une destruction peut désigner tout collatéral dont le locked la couvre ; une pièce qui a perdu sa parité peut donc être déposée au pair et le zUSD détruit contre une pièce qui a conservé la sienne, jusqu’à épuisement des collatéraux sains. La perte est bornée par les fenêtres d’émission glissantes et par la garde effectivement verrouillée : sur la chaîne 20, au plus 4,000 zUSD tous collatéraux confondus, et 100,000 par collatéral, peuvent être émis en 24 heures (rules v2), et il ne peut sortir par un collatéral sain plus que ce qu’il détient. La fenêtre limite le débit, non le total : elle achète le temps de suspendre les émissions, ce qui interrompt les nouveaux dépôts de la pièce faible tandis que les destructions restent ouvertes. Il n’existe ni limite de rachat par collatéral, ni pause par collatéral. Sur la chaîne 14, avant rules v2, le pauser propre aux endpoints source et les plafonds de débit par token étaient la seule limite.

L’invariant : total_supply == Σ backings.locked. TokenRegistry::lock (un BridgeAttest) ajoute le montant attesté au locked du collatéral et au total_supply du token en un seul appel ; TokenRegistry::release (un BridgeBurn) soustrait le montant libéré des deux. Rien d’autre n’écrit l’un ou l’autre champ, et la feuille du token engage le locked de chaque collatéral dans tokens_root, de sorte que deux nœuds en désaccord sur l’un divergent à la racine d’état. Les endpoints source ne prélèvent aucun frais au verrouillage et déduisent la totalité du montant attesté à la libération ; le locked de chaque collatéral est donc égal à la garde de cet endpoint, aux messages en transit près. Les frais sur la chaîne 20 n’y contreviennent pas : un frais est une note zUSD qui reste émise et adossée, de sorte que le dépôt verrouille le montant brut et que la destruction ne libère que amount − fee. Sur la chaîne 20, le prélèvement de 10 bps de USDT que les endpoints appliquaient eux-mêmes à la libération a été ramené à 0 lors de la coupure ; le frais est désormais prélevé sur Rand.

Les montants sont des unités à 8 décimales sur le réseau, quelles que soient les décimales propres de la pièce source : un dépôt de 1 USDT depuis Ethereum (6 décimales) se verrouille sous la forme de 100_000_000 unités. Seul l’indice 0, RAND, a les neuf décimales de cette chaîne.

Un token doit être listé avant son premier dépôt. L’enregistrement à la première occurrence n’existe plus : une attestation désignant un (chain, token) que personne n’a listé est refusée avec BridgeError::UnlistedToken (token … of chain … is not listed on this chain). Les fonds restent sous la garde de l’endpoint source jusqu’à ce qu’un quorum de gardiens post-quantique liste la pièce (Listing after genesis).

asset_id = blake3("rand-bridge-asset" || token_chain (2 bytes BE) || token_address (32 bytes))

rand_bridgeAssetId(token_chain, token_address) calcule toujours cet identifiant sur n’importe quelle chaîne, et c’est lui que résout le (token_chain, token_address) de la charge utile d’un Transfer, mais ce n’est plus une clé du registre. Ce que portent une note et un wallet est l’indice du token, un u32 dense : l’indice 0 est RAND et ne figure jamais dans le registre. rand_getAssets renvoie une ligne par collatéral, toutes les lignes d’un même token sous le même indice :

[{ "index": 1, "chain": 2, "token": "000000000000000000000000dac17f…", "asset_id": "…", "decimals": 6,
   "locked": "0", "mint_cap_per_day": "10000000000000", "minted_today": "0", "mint_day": 20716 }]

locked, mint_cap_per_day et minted_today sont des chaînes décimales en unités à 8 décimales, comme tout montant u64 que le RPC sert depuis la chaîne 14 ; index, chain, decimals et mint_day restent des nombres. Les méthodes au niveau du token — rand_getTokens, rand_getToken, rand_getTokenSupply — renvoient un token émis par le pont sous la forme d’une seule ligne portant tous ses collatéraux (RPL tokens). Le GET /bridge/tokens de l’explorateur est la liste d’autorisation statique des pièces que le pont accepte.

Entrant : BridgeAttest

Action::BridgeAttest { attestation: Vec<u8>, recipient: ShieldedAddress, r: Word8, time: u32,
                       asset: u32, envelope: Envelope, pq_signatures: Vec<PqSignature> }

PqSignature { index: u8, signature: Vec<u8> }        // Dilithium2, 2,420 bytes

Porté par une transaction blindée ordinaire dont l’unique bundle d’actif masqué paie la base de 0.001 RAND et rien de plus : son relayeur paie pour un déposant qui ne détient pas encore de RAND. C’est la chaîne, et non l’émetteur de la transaction, qui calcule la note de dépôt { pk: recipient.pk, from: 0, amount, asset: <index>, time, r } à partir du montant signé par les gardiens ; un relayeur ne peut donc ni gonfler ni rediriger une émission ; son engagement est la seule feuille qu’une transaction ne transporte pas sur le réseau. Sur la chaîne 20, l’amount de la note est le brut moins les frais d’émission, et la chaîne ajoute une seconde note, les frais, à destination de la trésorerie (Fees on mint and burn).

Le blinding est dérivé, non choisi. r doit être

r = blake3("rand-deposit-r-1" || mu)        // read as eight little-endian words; mu = keccak256(keccak256(body))

le condensat que le quorum de gardiens a signé ; toute autre valeur donne BridgeError::WrongDepositBlinding, refusée avant tout travail de signature. Le champ reste sur le réseau — la reconstruction du wallet à partir des champs publics l’y lit — il est seulement contraint, et la règle couvre aussi l’attest d’une rotation. Auparavant, quiconque avait vu la transaction en attente d’un relayeur pouvait soumettre la même attestation avec un blinding de son choix : le destinataire recevait quand même une note, mais la transaction du relayeur mourait après qu’il avait payé une preuve. Avec r fixé, deux émetteurs d’une même attestation au même time désignent la même note et le pool les met en conflit sur l’engagement.

time et asset sont les deux prédictions du déposant, faites pour que l’enveloppe puisse être scellée avant que la transaction n’existe. time est tenu à la même fenêtre que celui d’un bundle (1,024 blocs sur la chaîne 20, d’après le proof_window_blocks de la genèse ; 256 auparavant), et n’est volontairement pas dérivé : aucune règle ne donne un time sur lequel tous les émetteurs s’accorderaient, de sorte qu’un front-runner peut encore choisir un autre time dans la fenêtre — une note différente du même destinataire, du même montant et du même actif, que le wallet du destinataire retrouve de la même façon — et les deux soumissions entrent alors en collision sur le condensat de l’attestation. asset doit être égal à l’indice que le registre a déjà attribué à (chain, token), sinon TxError::AttestAssetMismatch.

Le fee de la charge utile du Transfer est porté et ne revient à personne : il n’y a aucune identité d’émetteur à payer, de sorte que le dépôt est le montant brut, diminué seulement des frais d’émission propres à la chaîne lorsque la genèse en fixe (chaîne 20). La protection contre le rejeu se fait par condensat : chaque mu consommé entre dans l’ensemble spent du pont, et le mempool refuse un second relayeur qui concourt pour la même attestation. Un dépôt est aussi soumis au plafond d’émission du collatéral, sur la chaîne 20 au plafond global également, et refusé tant que les émissions sont suspendues (B1, rules v2). Les deux plafonds comptent le brut. Sur une chaîne dont la genèse fixe un plancher de rejeu, dont la chaîne 20, un transfert dont la sequence est inférieure au plancher de sa chaîne source est refusé avec BelowReplayFloor avant toute vérification de signature (Carried over from chain 14).

rand bridge-mint <attestation hex or @file> --pq <JSON or @file> [--to rand1…] [--fee <RAND>] [--no-wait]
rand bridge-rotate <rotation hex or @file> --pq <JSON or @file> [--fee <RAND>] [--no-wait]

--pq est obligatoire : le quorum de cosignatures Dilithium2 des gardiens, sous la forme [{"index":0,"signature":"<4840 hex>"},…]. Avant toute preuve, le wallet décode l’attestation, vérifie le quorum au regard des pq_guardians du nœud et de l’identifiant de chaîne, vérifie que le destinataire a pour condensat celui qui est attesté, et lit l’indice listé ; puis il dérive r, scelle l’enveloppe du destinataire (pour le montant net sur une chaîne à frais) et paie avec un bundle de son propre RAND. bridge-rotate soumet une rotation de l’ensemble des gardiens (charge utile 2), qui ne dépose rien, et refuse tout ce qui ne fait pas avancer d’un cran l’ensemble de Rand. Comme chaque champ d’une note de dépôt est public dans cette unique transaction, le wallet d’un destinataire reconstruit les notes de dépôt qui lui sont adressées en parcourant les transactions bridge_attest validées, sans ouvrir aucune enveloppe — un relayeur hostile peut sceller du bruit, mais il ne peut pas cacher la note.

Sortant : BridgeBurn

Transaction {
  chain_id,
  bundle: <hidden-asset bundle: slots 0–1 spend the token, slots 2–3 pay the RAND fee;
           burn_asset = asset, burn_a = amount, burn_r = 0, fee >= BRIDGE_BURN_FEE>,
  action: BridgeBurn { asset, amount, relayer_fee, to_chain, token, to }
}

Depuis le bundle d’actif masqué de la chaîne 14, une destruction est un seul bundle (il y en avait deux auparavant) : les emplacements 0–1 prouvent que l’émetteur de la destruction possédait des notes de l’actif dont la somme atteint au moins amount, burn_a étant la valeur qui quitte le pool, et les emplacements 2–3 paient les frais en RAND. Les frais sont BRIDGE_BURN_FEE, 0.01 RAND, ce qui couvre la base du bundle. relayer_fee est une part de amount (fee ≤ amount) prélevée sur la chaîne de destination par le contrat de libération, et non une charge supplémentaire ; burn_a vaut donc exactement amount. Sur la chaîne 20, la chaîne conserve en outre des frais de destruction de 10 bps de amount, sous la forme d’une note zUSD versée à la trésorerie, et la libération est de amount − fee ; la part du relayeur doit tenir dans cette libération (Fees on mint and burn).

Une destruction désigne sa pièce. (to_chain, token) doit être un collatéral de l’actif détruit (sinon TokenError::NotABacking), et deux règles sont vérifiées à l’étape de l’action, avant la preuve du bundle :

  • L’unité de libération. Un collatéral dont la pièce source a d < 8 décimales se libère par unités de 10^(8−d) (100 pour une pièce à 6 décimales ; 1 pour une pièce à 18). Le montant libéré et relayer_fee doivent chacun en être un multiple entier, sinon TokenError::NotReleasable (… is not a multiple of the release unit …) : une fraction inférieure à la plus petite unité de la pièce source resterait bloquée en garde.
  • La garde propre du collatéral. Le montant libéré doit être ≤ locked pour ce collatéral, sinon TokenError::InsufficientBacking (only … is locked in that backing, not the … asked for). L’offre totale du token peut couvrir la libération ; c’est la garde de ce seul collatéral qui doit le faire.

Le montant libéré est amount sur une chaîne sans frais et amount − fee sur la chaîne 20.

to doit avoir la bonne forme (20 octets complétés par des zéros sur les chaînes 2, 3 et 4 ; jamais entièrement nul). Le registre consigne un BridgeBurnRecord sous un burn_sequence strictement croissant, avec le hash de la transaction dans l’emplacement de l’expéditeur, et les gardiens lisent le journal des destructions :

→ {"method":"rand_getBridgeBurn","params":[13]}
← { "sequence": 13, "amount": "99900000", "fee": "99900", "release_amount": "99800100",
    "body_hex": "…", "digest": "a4bc1c7d…63f4", "tx": "ceb9e3c2…0be7", "height": 2899 }

body_hex est le message sortant exactement tel que les gardiens doivent le hacher et le signer ; son montant Transfer est release_amount. L’enregistrement ci-dessus est le premier burn de la chain 20 : 0,999 zUSD brûlés vers du BSC USDT à la hauteur 2 899, dont 0,000999 sont allés à la trésorerie et 0,998001 ont été libérés.

rand bridge-burn <ASSET> <AMOUNT> <TO_CHAIN> <TOKEN> <TO> [--relayer-fee N] [--fee <RAND>] [--no-wait] [--yes]
rand bridge-message <SEQUENCE>       # one outbound message, verbatim, for a guardian to sign

Le wallet vérifie d’abord — à partir d’une seule lecture de l’état du bridge et du registre des tokens, avant toute preuve — que la chain dispose d’un bridge, que ASSET figure dans le registre, que TOKEN sur TO_CHAIN le garantit et en a au moins AMOUNT verrouillés, et que AMOUNT et les frais du relayer sont des unités de libération entières. Sur une chain avec frais, il affiche ensuite les frais du bridge et le montant qui sera libéré, puis demande confirmation avant de prouver (--yes passe la question). Un burn prouve ensuite un seul bundle : environ 100 s sur un ordinateur portable et environ 1,5 Mo de preuve, et sur la chain 20, comme pour tout bundle depuis la chain 17, une preuve d’autorisation de dépense à côté (quelques secondes, environ 1,37 Mo).

Frais sur le mint et le burn

Nouveau dans fullnode v0.6.8 et fixé sur la chain 20 par le groupe de genèse

"bridge": { …, "fees": { "mint_bps": 10, "burn_bps": 10, "recipient": "rand1…" } }

la chain prélève des frais sur chaque mint ponté et chaque burn ponté, dans le token ponté, sur Rand, sous forme de note blindée versée à l’adresse de trésorerie recipient. Chaque taux est au plus de 100 bps (1 %) ; sur la chain 20, les deux sont de 10 bps, soit 0,1 %. Les frais valent ⌊amount · bps / 10⁴⌋ arrondi à l’inférieur à une unité de libération entière du collatéral (10^(8 − source decimals) : 100 unités pour un USDT à 6 décimales, 1 pour une cryptomonnaie à 18 décimales), de sorte que les frais et le reste sont tous deux des unités entières, et des frais qui arrondissent à zéro ne créent aucune note (ledger/bridge_notes.rs, bridge_fee).

dépôt (BridgeAttest) d’un montant brut gburn (BridgeBurn) de amount
frais⌊g · mint_bps / 10⁴⌋, arrondi à l’inférieur à une unité de libération⌊amount · burn_bps / 10⁴⌋, arrondi à l’inférieur à une unité de libération
côté utilisateurune note de dépôt de g − feeamount brûlé (burn_a == amount, pas de monnaie rendue par le guest) ; le corps sortant atteste release_amount = amount − fee, et le endpoint libère exactement ce montant
la note de la trésoreriefee, blinding blake3("rand-bridge-fee-r-1" ‖ mu), au time et sur l’asset du dépôtfee, blinding blake3("rand-bridge-burn-fee-r-1" ‖ txid), au time du bundle
locked et total_supplyaugmentent de gdiminuent de release_amount
contrôlé par rapport àles plafonds de mint par collatéral et global, sur gInsufficientBacking et NotReleasable sur release_amount ; la part du relayer doit y tenir (FeeExceedsAmount)

Les deux notes sont émises et garanties, de sorte que total_supply == Σ locked == custody reste vrai : la note de frais est du zUSD comme un autre, et sa valeur reste verrouillée sur la chain source jusqu’à ce que quelqu’un la burn. BRIDGE_BURN_FEE, les 0,01 RAND qu’un burn paie dans ses slots RAND, est inchangé et distinct.

Premier aller-retour de la chain 20, lu sur le nœud : un dépôt de 1 zUSD depuis BSC à la hauteur 1 664 (4f6e6c54…1a64) a donné 0,999 zUSD au déposant et 0,001 à la trésorerie ; un burn de 0,999 zUSD vers BSC USDT à la hauteur 2 899 (séquence 13) a retenu 0,000999 zUSD et libéré 0,998001. Après lui (lecture du 2026-10-01), le collatéral BSC USDT contenait 22,001999 zUSD, et la masse en circulation du token, 99,001999 zUSD, égalait la somme de ses collatéraux.

Comment la trésorerie retrouve ses notes. Une note de frais n’a pas d’enveloppe : chacun de ses éléments est public (le montant, issu de la transaction et du taux de genèse ; l’asset et time, issus de la transaction ; le blinding, dérivé ; le propriétaire, le recipient de la genèse). Le wallet dont l’adresse est recipient reconstruit les notes de frais dans la même passe publique que celle qui reconstruit les notes de dépôt, en lisant rand_getBridgeState.fees et les décimales des collatéraux une fois par synchronisation. Un nœud qui ment sur les frais ne peut que faire manquer une note, jamais en créditer une. Le wallet d’un déposant évalue de la même façon son propre dépôt, brut moins frais. Seul un wallet v0.6.8 le fait ; un wallet plus ancien ne peut de toute façon pas envoyer sur la chain 20.

Sans le groupe, chains 14 à 19, il n’y a pas de frais : un dépôt crédite le brut et un burn libère amount. Jusqu’à la chain 20, les seuls frais du bridge étaient le prélèvement de 10 bps de USDT par les endpoints source à la libération, sur la chain hôte ; il a été fixé à 0 lors de la coupure de la chain 20. Le groupe est inscrit dans le hash de genèse et intégré à la racine du bridge sous rand-bridge-fees-1. Le docs/bridge.md §25 du fullnode donne l’encodage.

Durcissement du bridge

Le locked par collatéral et la garde InsufficientBacking encadrent le rachat. Ils ne bornent pas le mint : une attestation portant un quorum de gardiens valide mint ce qu’elle indique. Trois couches comblent cette lacune et une quatrième permet de lister un token sans coupure de chain. Aucune ne modifie les endpoints de la chain source, le format de transmission des attestations ni bridge-codec.

B1 : un plafond de mint par collatéral, et une pause du mint

Le tokens.mint_cap_per_day de la genèse s’applique par collatéral : la chain 20 fixe 10000000000000, soit 100 000 zUSD par collatéral et par jour, comme toute chain depuis la 14. Sans les règles v2 (chain 14), un jour est timestamp_ms / 86_400_000 de l’horodatage du bloc ; sous les règles v2 (chains 15 à 20), le même plafond est mesuré sur une fenêtre glissante de cap_window_secs de temps de bloc, et le plafond global du registre — 4 000 zUSD par 86 400 s sur la chain 20 — est la borne la plus stricte (règles v2). Un dépôt au-delà du plafond est refusé avec TokenError::MintCapExceeded avant le travail de signature des gardiens, et redevient admissible dès que la fenêtre a de nouveau de la place.

Le bridge.pause_key de la genèse est une unique clé Dilithium2, distincte de tout gardien post-quantique. Deux actions sans bundle et sans frais :

  • PauseMints { nonce, signature } — signée par la seule clé de pause. Elle ne peut que mettre en pause.
  • UnpauseMints { nonce, pq_signatures } — exige un quorum de gardiens post-quantiques, jamais la clé de pause.

Toutes deux portent et incrémentent pause_nonce, si bien qu’aucune ne peut être rejouée. Tant que le mint est en pause, tout BridgeAttest de transfert est refusé avec BridgeError::MintsPaused ; les burns et les rotations de l’ensemble de gardiens restent ouverts, de sorte qu’une pause ne piège jamais le rachat. Sous les règles v2 (chains 15 à 20), le listing est aussi refusé pendant la pause. La clé de pause est destinée à être conservée sur une autre machine que les clés des gardiens.

rand bridge-pause --sig <HEX or @file> [--no-wait]        # no key file and no RAND needed
rand bridge-unpause --pq <JSON or @file> [--no-wait]

Ce que couvre la pause : des daemons de gardiens trompés ou compromis qui cosignent un mint forgé, alors que les détenteurs du quorum post-quantique ne le sont pas. Elle ne protège pas contre le vol de ce quorum, qui peut lever la pause, lister jusqu’à 32 collatéraux sur un même token, chacun avec son propre plafond quotidien, et, sous les règles v2, remplacer l’ensemble post-quantique et la clé de pause.

B2 : des horodatages de bloc bornés

L’horodatage d’un bloc d’une chain pontée détermine l’expiration de l’ensemble de gardiens et le jour du plafond de mint ; un leader ne doit donc pas pouvoir le faire sauter. Sur une chain dotée d’un bridge :

  • Une règle de validité, vérifiée aussi au rejeu : timestamp_ms ≤ parent.timestamp_ms + 60_000, sinon BlockError::TimestampLeap. Avec la règle de recul existante (BlockError::TimestampRewind), le temps de bloc ne fait qu’avancer, d’une minute au plus par bloc.
  • Une règle de vote, jamais une règle de rejeu : un validateur retient son vote pour un bloc en avance de plus de 15 000 ms sur sa propre horloge.

Tout validateur d’une chain pontée a donc besoin de NTP : un validateur décalé de plus de 15 secondes est à la fois un leader fautif et un votant fautif. Pendant qu’une chain rattrape son retard après un arrêt, le temps de bloc peut avancer jusqu’à 60 s par bloc, si bien qu’un jour de plafond de mint peut s’écouler plus vite qu’un vrai jour — au plus un jour de plafond supplémentaire par jour entier de retard accumulé.

B3 : une cosignature Dilithium2 sur chaque mint

Chaque BridgeAttest, dépôt comme rotation, doit porter, à côté de son quorum ECDSA, un quorum de signatures Dilithium2 sur

M = "rand-bridge-pq-cosign-1" || rand_chain_id (u64 BE) || mu        // 63 bytes

mu est le condensé que signe le quorum ECDSA ; l’identifiant de chain empêche une cosignature de testnet d’être valide sur une autre chain Rand. Les signatures voyagent dans les pq_signatures de l’action, à l’intérieur de la liaison de la transaction, de sorte qu’on ne peut ni les retirer ni les échanger sur une copie. Les contrôles vont du moins coûteux au plus coûteux : quorum(n) ≤ len ≤ n (PqNoQuorum), indices strictement croissants (PqIndexOrder), chaque indice < n (PqIndexOutOfRange), chaque signature d’exactement 2 420 octets (PqBadSignatureLength), et enfin la vérification de chacune sous pq_guardians[index] (PqBadSignature). Les deux quorums sont comptés indépendamment. Le bridge.pq_guardians de la genèse est aligné par indice sur l’ensemble ECDSA de la genèse (l’ensemble 1 sur la chain 20). Une rotation de payload 2 ne déplace que l’ensemble ECDSA ; sans les règles v2, l’ensemble post-quantique est fixe pour toute la vie de la chain, et sous les règles v2 seule RotatePqGuardians le remplace (sur la chain 20, sa forme V2, avec une preuve de possession et un délai d’un jour). La chain 20 en compte huit, les mêmes huit clés que les chains 15 à 19 ; un mint en exige six (la chain 14 en comptait six et en exigeait cinq).

Cette co-signature ne lie toujours que l’identifiant de chaîne, y compris sur la chaîne 20 : contrairement aux messages de gouvernance ci-dessous, elle n’a pas été placée derrière le hash de genesis, car les démons des guardians qui la produisent vivent dans un autre dépôt et n’ont pas changé. Une copie à identifiant identique d’une chaîne qui aurait aussi copié l’ensemble de guardians accepterait les co-signatures d’un dépôt établies pour l’original ; le lot de frais de la transaction de dépôt est, lui, lié au genesis sur la chaîne 20, mais l’attestation qu’il contient ne l’est pas.

B4 : lister un jeton ponté après le genesis

Une adresse ordinaire ne peut jamais créer un jeton d’autorité Bridge. Deux actions, chacune sur un lot de frais RAND ordinaire et chacune sous le quorum post-quantique :

  • RegisterBridgedToken { name, symbol, salt, chain, token, decimals, nonce, pq_signatures } — un nouveau jeton ponté à l’indice suivant, avec 8 décimales sur Rand, sous le plafond de mint du genesis, avec son premier collatéral. Frais : la base du lot plus le registration_fee du registre (1 RAND sur la chaîne 20, soit 1,001 RAND ; la chaîne 20 brûle les frais d’enregistrement au lieu de les verser au proposer).
  • ListBacking { token_index, chain, token, decimals, nonce, pq_signatures } — un collatéral supplémentaire sur un jeton ponté enregistré, à partir de locked = 0. Frais : la base du lot.

Les deux portent et incrémentent list_nonce (sinon BadListNonce { expected, got }) et exécutent les mêmes contrôles : la chaîne a un émetteur enregistré (BridgeError::NoEmitter), (chain, token) ne garantit encore rien, moins de 32 collatéraux, decimals ≤ 18.

rand token register-bridged --name … --symbol … --salt <hex32> --chain N --token <hex32> --decimals N --pq <JSON or @file>
rand token list-backing --asset <index> --chain N --token <hex32> --decimals N --pq <JSON or @file>

Deux règles d’ordonnancement. Les guardians signent d’abord l’enregistrement, attendent qu’il soit validé, relisent l’indice attribué, et ne signent qu’ensuite les listages qui le nomment : l’enregistrement d’un jeton natif est sans permission, de sorte qu’un listage signé en avance pourrait trouver son indice déjà pris. Et une pièce est listée sur Rand avant que setToken l’active sur le point d’accès source : dans l’ordre inverse, un dépôt pourrait verrouiller des fonds avant que Rand ait un collatéral à créditer, et son attestation serait refusée avec UnlistedToken jusqu’à ce que Rand rattrape son retard.

Messages de gouvernance

Octets fixes, jamais bincode ; tous les entiers en big-endian :

M_pause    = "rand-bridge-pause-1"        || chain_id u64 || nonce u64
M_unpause  = "rand-bridge-pq-unpause-1"   || chain_id u64 || nonce u64
M_list     = "rand-bridge-pq-list-1"      || chain_id u64 || nonce u64 || token_index u32 || chain u16 || token [32] || decimals u8
M_register = "rand-bridge-pq-register-1"  || chain_id u64 || nonce u64 || u8 len || name || u8 len || symbol || salt [32] || chain u16 || token [32] || decimals u8
M_rotate_pq    = "rand-bridge-pq-rotate-pq-1"    || chain_id u64 || rotation_nonce u64 || count u32 || key [1312] …   // rules v2
M_rotate_pause = "rand-bridge-pq-rotate-pause-1" || chain_id u64 || rotation_nonce u64 || key [1312]            // rules v2

M_pause/M_unpause portent pause_nonce, M_list/M_register portent list_nonce, et M_rotate_pq/M_rotate_pause portent rotation_nonce. count est le nombre de clés, et non une longueur en octets ; chaque clé est une clé publique Dilithium2 de 1 312 octets bruts, dans l’ordre que aura le nouvel ensemble.

Sur la chaîne 20, chaque message est lié au hash de genesis (genesis binding_domain: 1) : le chiffre de version de la balise passe à 2 et le hash de genesis de 32 octets se place entre la balise et l’identifiant de chaîne, le reste étant inchangé — M'_pause = "rand-bridge-pause-2" || genesis [32] || chain_id u64 || nonce u64, et de même pour rand-bridge-pq-unpause-2, rand-bridge-pq-list-2, rand-bridge-pq-register-2, rand-bridge-pq-rotate-pq-2 et rand-bridge-pq-rotate-pause-2 (bridge/gov.rs). Une clé de pause ou un quorum qui signe pour une chaîne ne signe alors pour aucune autre chaîne qui partage son identifiant de chaîne et ses clés. Un fichier signé sous la forme …-1 est refusé sur la chaîne 20 (BadPauseSignature, PqBadSignature). Les chaînes 14 à 19 utilisent les structures …-1 ci-dessus, octet pour octet.

Règles v2 : rotations, plafonds glissants, pas de listage en pause

Un groupe optionnel dans la section bridge du genesis. La chaîne 14 ne l’avait pas ; toutes les chaînes depuis la 15 l’ont, la chaîne 20 avec les mêmes valeurs :

"bridge": { …, "rules_v2": { "global_mint_cap_per_window": 400000000000, "cap_window_secs": 86400 } }
  • Deux rotations sous le quorum post-quantique. RotatePqGuardians { new_pq_guardians, nonce, pq_signatures } remplace l’ensemble post-quantique entier ; RotatePauseKey { new_pause_key, nonce, pq_signatures } remplace la clé de pause. Les deux sont sans lot et sans frais, comme PauseMints, les deux sont signées par l’ensemble post-quantique actuel, et les deux portent et incrémentent rotation_nonce, de sorte qu’un quorum signé n’autorise qu’une seule rotation. Le nouvel ensemble doit avoir la même longueur que l’ensemble ECDSA actuel, des clés de 1 312 octets, aucun doublon et ne pas contenir la clé de pause. Il n’y a pas de période de grâce : l’ensemble ou la clé remplacés ne signent plus rien. Sur la chaîne 20, ces actions v1 sont refusées, et une rotation doit prouver la possession des nouvelles clés et attendre un jour (ci-dessous).
  • Plafonds sur fenêtre glissante. Le mint_cap_per_day de chaque collatéral est mesuré sur une fenêtre glissante de cap_window_secs au lieu du jour calendaire, et un second plafond, global au registre, global_mint_cap_per_window, couvre ensemble tous les collatéraux de tous les jetons (TokenError::GlobalMintCapExceeded). Sur la chaîne 20, il s’élève à 400 000 000 000 unités, soit 4 000 zUSD par 86 400 s. Avec la règle du jour, le plafond à 23:59:59 puis à 00:00:01 valait le double du plafond en deux secondes ; avec la fenêtre, la seconde opération est refusée.
  • Pas de listage en pause. RegisterBridgedToken et ListBacking sont refusés avec MintsPaused tant que mint_paused est actif. Les burns et les deux rotations restent ouverts.

Rotation : possession et délai d’un jour

Les règles v2 laissaient trois points ouverts : rien ne prouvait que les nouvelles clés étaient détenues par quelqu’un (une faute de frappe, ou une clé que personne ne détient, aurait bloqué définitivement le quorum post-quantique), une rotation prenait effet dans le bloc où elle était incluse, et rien ne pouvait en arrêter une hostile. Un second groupe optionnel, placé à côté de rules_v2 (dont il dépend), nouveau dans la v0.6.8 (audit v6, BRG-14), les referme. La chaîne 20 l’active :

"bridge": { …, "rotation": { "delay_secs": 86400, "needs_possession": true } }
  • Possession. Une rotation doit être RotatePqGuardiansV2 { new_pq_guardians, possession, nonce, pq_signatures } ou RotatePauseKeyV2 { new_pause_key, possession, nonce, pq_signatures } : possession[i] est la propre signature Dilithium2 de la nouvelle clé i sur le message exact que signe le quorum (M_rotate_pq ou M_rotate_pause, sous leur forme …-2 sur la chaîne 20), une par clé dans l’ordre des clés, de 2 420 octets chacune. Les actions v1 sont refusées avec PossessionRequired. Un nombre ou une longueur erronés sont refusés avant la vérification du quorum ; une mauvaise signature de possession (BadPossession { index }) l’est après.
  • Un délai. Une rotation acceptée est enregistrée comme en attente, avec effective_at_secs = block time + delay_secs, et prend effet à la fin du premier bloc dont l’horodatage l’atteint. Sur la chaîne 20, cela fait 86 400 s, soit un jour. Jusque-là, l’ancien ensemble co-signe chaque mint et l’ancienne clé de pause met en pause. Accepter une rotation consomme un rotation_nonce et l’activer en consomme un autre, de sorte qu’une attestation de mint mise en pool sous l’ancien ensemble quitte le pool quand l’ensemble change. Une rotation de chaque type à la fois peut être en attente (RotationPending { effective_at_secs }).
  • L’annulation. CancelRotation { kind, nonce, signature } (sans lot, sans frais) supprime la rotation en attente de type kind (0 = l’ensemble post-quantique, 1 = la clé de pause). Elle est signée par la clé de pause actuelle, la seule clé détenue séparément du quorum, sur "rand-bridge-cancel-rotation-2" || genesis [32] || chain_id u64 || rotation_nonce u64 || kind u8 sur la chaîne 20, et consomme le nonce. Un quorum capturé ne peut donc pas faire pivoter l’ensemble en moins d’un jour, et le détenteur de la clé de pause peut l’arrêter pendant ce jour ; la clé de pause entrante ne peut pas annuler sa propre arrivée.

Le signataire est rand-node bridge-gov (rotate-pq, rotate-pause, cancel-rotation, chacun avec les étapes message, sign et submit ; submit revérifie chaque ligne de possession avant l’envoi). rand_getBridgeState fournit rotation_rules et pending_rotations ; les deux valent null sur une chaîne sans ce groupe (chaînes 14 à 19). La rotation de l’ensemble de guardians ECDSA (payload 2) n’est pas concernée : elle garde ses propres règles et sa période de grâce. Aucune rotation n’a été exécutée sur la chaîne 20.

Reporté depuis la chaîne 14

La chaîne 15 a été créée à partir de la position de pont de la chaîne 14 (mêmes émetteurs et mêmes points d’accès source), et les chaînes 16, 17 et 18 l’ont chacune été de la même façon à partir de leur prédécesseur. La chaîne 19 a été créée à partir de la position de la chaîne 18 avec un seul changement : ses émetteurs Ethereum, BSC et Tron sont les points d’accès redéployés le 2026-09-30 (0x7af6b17047c1db6cb54347fdea45cf9179075bfa pour les chaînes 2 et 3, 0x6410797df959987a5baf65b5fab97edeb34d5163 pour la chaîne 4). La chaîne 18 ne les a jamais reconnus, donc rien n’y a été minté à partir d’eux. La chaîne 20 a été créée à partir d’un instantané de la chaîne 19 à la hauteur 7 258, après arrêt préalable du pont, et conserve inchangés les émetteurs, les guardians, les clés post-quantiques et la clé de pause de la chaîne 19 ; elle y ajoute les groupes frais et rotation. L’émetteur de Solana et l’émetteur côté Rand n’ont pas changé depuis la chaîne 14. Ce que contient le genesis de la chaîne 20 :

  • Garde. 99 zUSD, sous la forme de deux notes zUSD de genèse (10 et 89 zUSD), contre 22 USDT verrouillés sur Ethereum, 22 USDT sur BSC, 30 USDT sur Tron et 25 USDT sur Solana, et 0 sur chaque garantie en USDC : le locked de la chaîne 19 au moment de l’instantané, qui correspondait à la garde des endpoints. total_supply == Σ locked est égal à la garde des endpoints au bloc 0. Historique : les 10 zUSD restés sur la chaîne 14 formaient une seule note de genèse sur les chaînes 15 à 19 ; la genèse de la chaîne 18 les détenait sous la forme de 9 USDT verrouillés sur Tron et 1 sur Solana, et celle de la chaîne 19 sous la forme de 10 sur Solana après un rééquilibrage sur la chaîne 18. Les 89 autres correspondent aux dépôts nets de la chaîne 19, moins ses burns.
  • Indice d’ensemble de guardians 1, là où se trouvent déjà les endpoints sources ; la prochaine rotation ECDSA doit porter l’indice 2. Les huit clés post-quantiques et la clé de pause sont celles de la chaîne 15, inchangées.
  • burn_sequence 13 : les burns de la chaîne 14 se sont arrêtés à 6, la genèse des chaînes 15 à 18 a commencé à 7, la chaîne 18 a effectué un burn (7) et la chaîne 19 a commencé à 8. La chaîne 19 a effectué cinq burns, de séquences 8 à 12, de sorte que la chaîne 20 commence à 13 et qu’aucun endpoint ni daemon indexé par séquence n’en voit une deux fois. Le premier burn de la chaîne 20 a pris la séquence 13.
  • Rejeu. Un mint d’une chaîne antérieure passerait encore le quorum ECDSA, mais sa co-signature post-quantique lie l’identifiant de cette chaîne ; il est donc refusé sur la chaîne 20. La genèse de la chaîne 15 n’avait pas de plancher de rejeu entrant (min_inbound_sequence valait null) ; la protection reposait donc sur la co-signature et sur le fait que les guardians ne co-signent jamais un ancien verrouillage pour le nouvel identifiant de chaîne. Depuis la chaîne 16, la genèse en fixe un : pour chaque chaîne source, la plus petite sequence que peut porter une attestation de transfert issue de l’émetteur de cette chaîne. Celui de la chaîne 20 est { "2": 3, "3": 3, "4": 3, "5": 7 } : la prochaine séquence sortante de chaque endpoint lors de l’instantané de la chaîne 19, chaque verrouillage antérieur ayant déjà été minté sur la chaîne 19. (Celui de la chaîne 18 était { "2": 2, "3": 2, "4": 2, "5": 2 } ; celui de la chaîne 19 était { "2": 1, "3": 1, "4": 1, "5": 4 }, où la séquence 0 de chaque endpoint redéployé était le propre verrouillage de l’opérateur, qui ne doit jamais minter.) Un transfert inférieur au plancher est refusé avec BelowReplayFloor, quels que soient le quorum et la co-signature qu’il porte. Les rotations d’ensemble de guardians ne sont pas jugées selon ce plancher. Le plancher est fixé à la genèse, engagé dans le hash de genèse et intégré à la racine du bridge.

Ordre de lancement sur la chaîne 14 (historique)

Comment le bridge a démarré sur la chaîne 14, un testnet antérieur, en septembre 2026 :

  1. Créer la chaîne avec la section bridge (pq_guardians et pause_key inclus) et une section tokens qui fixe registration_fee et mint_cap_per_day, sans lister aucun token. Fait le 2026-09-20.
  2. Activer le faucet pour qu’un portefeuille de déploiement puisse obtenir des RAND. Fait.
  3. Le déployeur enregistre zUSD avec sa première garantie, sous la signature du quorum sur M_register à list_nonce = 0. Fait à la hauteur 256.
  4. Les guardians relisent l’indice et signent les six messages ListBacking restants, soumis un à un. Fait : list_nonce vaut 7.
  5. Pour chaque garantie : lister sur Rand avant setToken sur l’endpoint source.
  6. La rotation de l’ensemble de guardians (ensemble 0 → ensemble 1, signée par 5 membres sur 6 de l’ensemble courant) sur les quatre endpoints sources, puis la même attestation sur Rand. Rand a lu l’ensemble 0 pendant toute la vie de la chaîne 14 ; la chaîne 15 démarre à l’ensemble 1.
  7. Un aller-retour réel par chaîne source avec de petits montants — bridge entrant, mint de zUSD, transfert, burn de retour — et un rapprochement du locked de chaque garantie (rand_getTokenSupply) avec la garde de l’endpoint.

État public

rand_getBridgeState :

{
  "enabled": true,
  "emitter": "01…",  "emitters": { "2": "02…" },
  "guardian_set_index": 0, "guardians": ["aabb…"],
  "pq_guardians": ["…"],
  "mint_paused": false, "pause_nonce": 0, "list_nonce": 0, "pause_key": "…",
  "registration_fee": "1000000000",
  "burn_sequence": 1,
  "rotation_nonce": 0,
  "rules_v2": { "global_mint_cap_per_window": "400000000000", "cap_window_secs": 86400,
                "global_minted_in_window": "…", "global_mint_headroom": "…" },
  "min_inbound_sequence": null,
  "rotation_rules": null, "pending_rotations": null,
  "fees": null,
  "assets": [ … ]
}

Les pq_guardians sont des clés publiques Dilithium2, de 1 312 octets chacune en hexadécimal. Il n’y a plus de next_index : un token ponté est listé, sous un indice fixé par son enregistrement, avant de pouvoir être déposé ; il ne reste donc aucun indice à prédire. rotation_nonce est ce que doit porter le prochain M_rotate_pq/M_rotate_pause/M_cancel ; rules_v2 vaut null sur une chaîne sans ce groupe (chaîne 14), et min_inbound_sequence vaut null sur une chaîne sans plancher de rejeu (chaîne 15 et antérieures) ; sur la chaîne 20, il vaut { "2": 3, "3": 3, "4": 3, "5": 7 }. rotation_rules, pending_rotations et fees valent null sur une chaîne sans ces groupes (chaînes 14 à 19). L’exemple ci-dessus montre la forme, pas les valeurs de la chaîne 20 : la chaîne 20 a démarré à guardian_set_index 1 et burn_sequence 13, et sert "rotation_rules": { "delay_secs": 86400, "needs_possession": true }, "pending_rotations": [] et "fees": { "mint_bps": 10, "burn_bps": 10, "recipient": "rand1…" }. Une chaîne sans section bridge répond {"enabled": false}.

Aucun solde : la valeur pontée est faite de notes, et un portefeuille lit ce qu’il détient avec rand asset-balance [INDEX] à partir de ses propres notes. La racine du bridge engage les émetteurs, les ensembles de guardians, les digests dépensés, la séquence de burn, l’ensemble de guardians post-quantique, ainsi que la clé de pause, l’indicateur de pause et les nonces de gouvernance (deux, et rotation_nonce sous les règles v2), le plancher de rejeu lorsque la genèse en fixe un, et, sur la chaîne 20, les règles de rotation avec toute rotation en attente ainsi que le groupe de frais ; elle n’est intégrée à la racine d’état que sur une chaîne dotée d’un bridge. Le registre des tokens, avec le locked de chaque garantie, est engagé séparément sous la forme de tokens_root. Les routes GET /bridge et GET /bridge/assets de l’explorateur servent le même état, la seconde avec les totaux de dépôts et de burns par garantie sous forme de chaînes à 8 décimales ; GET /tokens et GET /tokens/:id servent le registre.

Ce qui est public et ce qui est caché

ce que c’estqui le voit
le montant d’un dépôt, l’indice de l’actif et l’adresse du destinatairetout le monde, dans cette seule transaction
quelle note un destinataire dépense ensuitepersonne
le montant d’un burn, l’actif, la pièce, la destination et les frais du relayeurtout le monde — les guardians en ont besoin
le montant, l’actif et le propriétaire d’une note de frais de bridge, la trésorerie (chaîne 20)tout le monde : elle est reconstruite à partir de champs publics
quelles notes ont financé un burnpersonne
les soldes pontésle seul détenteur, ou un détenteur de clé de visualisation
les ensembles de guardians, les émetteurs, le registre avec le locked de chaque garantie, le journal des burnstout le monde

Lacunes connues

Tirées de la liste du fullnode lui-même : aucun relayeur n’est rémunéré sur Rand ; l’enveloppe d’un dépôt n’est liée à rien, de sorte qu’un relayeur hostile peut sceller du contenu arbitraire, ce qui explique que le portefeuille reconstruise les notes de dépôt à partir de champs publics ; les vecteurs de test partagés au niveau du ledger n’ont pas été reconstruits sur le pool de notes ; bridge-burn n’apprend encore la politique propre de l’endpoint de destination qu’après avoir payé une preuve ; les horodatages de blocs égaux à celui du parent sont autorisés sur une chaîne pontée, de sorte que des leaders qui s’entendent pourraient maintenir un ensemble de guardians remplacé dans sa fenêtre de grâce ; le journal des burns en mémoire n’est pas borné ; il n’existe, ni n’est prévu, de client léger ni de vérification on-chain de l’état des chaînes sources.

Dernière vérification par rapport au commit c0878c3f de fullnode (chaîne 21) le 2026-10-12. Lorsque le code et cette page divergent, c'est le code qui a raison ; signalez-le-nous.