[0m
build · gpt-5.3-codex [0m
Spécification technique — PD-53¶
Smart Contract d’ancrage Merkle roots (ProbatioVaultAnchor)¶
1. Vue d’ensemble¶
ProbatioVaultAnchor est un registre on-chain minimaliste, append-only, destiné à ancrer des merkleRoot (bytes32) produits hors chaîne par ProbatioVault. Le contrat ne construit pas l’arbre de Merkle, ne valide pas la cryptographie interne, et ne gère ni TSA ni HSM ; il fournit uniquement une preuve publique d’existence à une date/bloc donnés, via stockage immuable + événement.
Le contrat est non-upgradable, gouverné par Ownable, et déployé sur réseaux EVM (cibles: Polygon/Arbitrum). Seul owner peut ancrer une racine, chaque racine est unique, et un tiers peut vérifier indépendamment l’existence et les métadonnées d’ancrage sans dépendre d’une base centralisée.
flowchart LR
A[Backend ProbatioVault\nWORM events] --> B[Merkle Root bytes32]
B --> C[anchor(bytes32)]
C --> D[ProbatioVaultAnchor (EVM)]
D --> E[(Storage append-only)]
D --> F[[Event MerkleRootAnchored]]
E --> G[isAnchored(root)]
E --> H[getAnchor(root)]
F --> I[Explorateurs\nPolygonscan/Arbiscan]
G --> J[Tiers vérificateur]
H --> J
I --> J Acteurs et rôles
- Owner (wallet sécurisé / multisig / HSM-backed): unique autorité d’appel
anchor. - Tiers vérificateur (auditeur, juge, partie adverse): consulte événements + appelle vues
isAnchored/getAnchor. - Explorateur blockchain: source publique pour logs, tx hash, block timestamp, block number.
- Backend ProbatioVault (hors périmètre PD-53): calcule la racine, orchestre la soumission on-chain (via PD-52).
2. Interfaces Solidity¶
2.1 Interface contractuelle (signatures exactes + NatSpec)¶
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title ProbatioVaultAnchor
/// @notice Registre append-only d'ancrage de Merkle roots pour preuve publique.
/// @dev Non-upgradable. Seul owner peut ancrer. Une racine ne peut être ancrée qu'une fois.
contract ProbatioVaultAnchor is Ownable {
/// @notice Version sémantique du contrat.
string public constant VERSION = "1.0.0";
/// @notice Émis à chaque ancrage réussi.
/// @param merkleRoot Racine Merkle ancrée.
/// @param anchor Adresse appelante (owner).
/// @param timestamp Horodatage bloc au moment de l'ancrage.
/// @param blockNumber Numéro du bloc d'ancrage.
event MerkleRootAnchored(
bytes32 indexed merkleRoot,
address indexed anchor,
uint256 timestamp,
uint256 blockNumber
);
/// @notice Erreur si la racine est nulle.
error ZeroMerkleRoot();
/// @notice Erreur si la racine est déjà ancrée.
/// @param merkleRoot Racine déjà présente.
error MerkleRootAlreadyAnchored(bytes32 merkleRoot);
/// @notice Erreur si la racine n'existe pas dans le registre.
/// @param merkleRoot Racine absente.
error MerkleRootNotAnchored(bytes32 merkleRoot);
/// @notice Erreur si tentative de renonciation de ownership désactivée.
error RenounceOwnershipDisabled();
/// @notice Crée le contrat avec owner initial.
/// @param initialOwner Adresse owner initiale (multisig recommandée).
constructor(address initialOwner) Ownable(initialOwner) {}
/// @notice Ancre une nouvelle racine Merkle.
/// @dev Revert si root nulle ou déjà ancrée.
/// @param merkleRoot Racine Merkle à ancrer.
function anchor(bytes32 merkleRoot) external onlyOwner;
/// @notice Vérifie si une racine a déjà été ancrée.
/// @param merkleRoot Racine à vérifier.
/// @return anchored true si la racine est présente.
function isAnchored(bytes32 merkleRoot) external view returns (bool anchored);
/// @notice Retourne les métadonnées d'ancrage d'une racine.
/// @dev Revert si la racine n'est pas ancrée.
/// @param merkleRoot Racine recherchée.
/// @return anchorAddress Adresse ayant ancré (owner au moment T).
/// @return timestamp Horodatage bloc lors de l'ancrage.
/// @return blockNumber Numéro du bloc d'ancrage.
function getAnchor(bytes32 merkleRoot)
external
view
returns (address anchorAddress, uint256 timestamp, uint256 blockNumber);
/// @notice Désactivée pour éviter un contrat ownerless irréversible.
function renounceOwnership() public view override {
revert RenounceOwnershipDisabled();
}
// Inherited Ownable public/external interface:
// function owner() public view returns (address);
// function transferOwnership(address newOwner) public onlyOwner;
}
2.2 Événements¶
MerkleRootAnchored(bytes32 indexed merkleRoot, address indexed anchor, uint256 timestamp, uint256 blockNumber)
2.3 Erreurs custom (revert reasons)¶
ZeroMerkleRoot()MerkleRootAlreadyAnchored(bytes32 merkleRoot)MerkleRootNotAnchored(bytes32 merkleRoot)RenounceOwnershipDisabled()- Hérité OpenZeppelin
Ownable: OwnableUnauthorizedAccount(address account)OwnableInvalidOwner(address owner)
3. Modèle de données¶
3.1 Variables d’état¶
string public constant VERSION = "1.0.0";mapping(bytes32 => AnchorRecord) private _anchors;
3.2 Structure de données¶
struct AnchorRecord {
address anchorAddress; // 20 bytes
uint64 timestamp; // 8 bytes
uint64 blockNumber; // 8 bytes
}
// Packing en 1 slot (32 bytes) pour optimiser le gas
3.3 Mapping (clé/valeur)¶
- Clé:
bytes32 merkleRoot - Valeur:
AnchorRecord - Sentinelle d’existence:
timestamp != 0(possible carblock.timestampne vaut jamais 0 sur réseaux cibles)
3.4 Constantes / métadonnées¶
VERSIONexposée on-chain.- Nom contractuel logique:
ProbatioVaultAnchor(identité publique). - Pas de stockage mutable pour suppression/modification d’entrées.
4. Invariants de sécurité¶
- INV-53-01: seul
ownerpeut exécuteranchor. - INV-53-02:
merkleRoot == bytes32(0)est toujours rejetée. - INV-53-03: une racine ancrée ne peut jamais être ré-ancrée.
- INV-53-04: aucune fonction ne permet la modification d’un enregistrement existant.
- INV-53-05: aucune fonction ne permet la suppression d’un enregistrement existant.
- INV-53-06: chaque ancrage réussi émet exactement un
MerkleRootAnchored. - INV-53-07: les champs événementiels
timestampetblockNumberreflètent le bloc courant de la tx. - INV-53-08:
isAnchored(root)retournetruesi et seulement si_anchors[root].timestamp != 0. - INV-53-09:
getAnchor(root)retourne des métadonnées cohérentes avec l’événement émis. - INV-53-10: le contrat reste non-upgradable (pas de proxy, pas de delegate upgrade path).
- INV-53-11: l’historique des ancrages est append-only et indéfini en taille (limité uniquement par la chaîne).
- INV-53-12:
renounceOwnershipest interdite pour éviter un état ownerless irréversible (continuité opérationnelle).
Conditions interdites (NE JAMAIS se produire)
- Ancrage d’une racine nulle.
- Double ancrage d’une même racine.
- Altération/suppression post-ancrage.
- Validation dépendante d’une source off-chain propriétaire.
5. Critères d’acceptation (CA)¶
- CA-53-01: QUAND
ownerappelleanchor(root)avecroot != 0x0non ancrée ALORS la tx réussit. - CA-53-02: QUAND
anchor(root)réussit ALORSMerkleRootAnchoredest émis avecmerkleRoot=root. - CA-53-03: QUAND
anchor(root)réussit ALORSevent.anchor == msg.sender(owner). - CA-53-04: QUAND
anchor(root)réussit ALORSevent.timestamp == block.timestampde la tx. - CA-53-05: QUAND
anchor(root)réussit ALORSevent.blockNumber == block.numberde la tx. - CA-53-06: QUAND
rootvient d’être ancrée ALORSisAnchored(root) == true. - CA-53-07: QUAND
rootn’a jamais été ancrée ALORSisAnchored(root) == false. - CA-53-08: QUAND
getAnchor(root)est appelée pour une racine ancrée ALORS retourne(anchorAddress,timestamp,blockNumber)non nuls et cohérents. - CA-53-09: QUAND
getAnchor(root)est appelée pour une racine absente ALORS revertMerkleRootNotAnchored(root). - CA-53-10: QUAND un non-owner appelle
anchor(root)ALORS revertOwnableUnauthorizedAccount. - CA-53-11: QUAND
ownerappelleanchor(bytes32(0))ALORS revertZeroMerkleRoot(). - CA-53-12: QUAND
ownerappelleanchor(root)déjà ancrée ALORS revertMerkleRootAlreadyAnchored(root). - CA-53-13: QUAND
owner()est consultée ALORS retourne l’adresse owner courante. - CA-53-14: QUAND
ownerappelletransferOwnership(newOwner!=0)ALORSowner()devientnewOwner. - CA-53-15: QUAND
renounceOwnership()est appelée ALORS revertRenounceOwnershipDisabled(). - CA-53-16: QUAND
VERSION()est consultée ALORS retourne strictement"1.0.0". - CA-53-17: QUAND benchmark gas est exécuté sur
anchor(nouvelle root) ALORS gas consommé< 50_000. - CA-53-18: QUAND le contrat est publié sur Amoy/Sepolia ALORS le source code est vérifié sur explorateurs.
- CA-53-19: QUAND un tiers lit logs + vues ALORS il peut établir qu’un hash était ancré à une date/bloc donné sans backend ProbatioVault.
- CA-53-20: QUAND des ancrages successifs sont réalisés ALORS les entrées précédentes restent accessibles et inchangées.
6. Flux nominaux¶
6.1 constructor(address initialOwner)¶
- Préconditions:
initialOwner != address(0). - Étapes:
- Initialiser
Ownable(initialOwner). - Exposer
VERSION. - Postconditions: owner défini, contrat prêt.
- Événement:
OwnershipTransferred(address(0), initialOwner)(Ownable). - Gas estimé: déploiement dépend bytecode (hors cible ENF-1).
6.2 anchor(bytes32 merkleRoot) external onlyOwner¶
- Préconditions:
- caller = owner
merkleRoot != bytes32(0)!isAnchored(merkleRoot)- Étapes:
- Vérifier contrôle d’accès
onlyOwner. - Vérifier non-nullité.
- Vérifier unicité.
- Stocker
AnchorRecord(msg.sender,uint64(block.timestamp),uint64(block.number)). - Émettre
MerkleRootAnchored(merkleRoot,msg.sender,block.timestamp,block.number). - Postconditions:
isAnchored(merkleRoot)==truegetAnchor(merkleRoot)retourne les métadonnées stockées- Événement émis:
MerkleRootAnchored. - Gas estimé: ~42k–48k (objectif
< 50k).
6.3 isAnchored(bytes32 merkleRoot) external view returns (bool)¶
- Préconditions: aucune.
- Étapes:
- Lire
_anchors[merkleRoot].timestamp. - Retourner
timestamp != 0. - Postconditions: aucun état modifié.
- Événement: aucun.
- Gas estimé: lecture
view(RPC call off-chain, coût on-chain marginal si appelé par contrat).
6.4 getAnchor(bytes32 merkleRoot) external view returns (...)¶
- Préconditions: racine existante.
- Étapes:
- Lire record.
- Si absent (
timestamp==0), revertMerkleRootNotAnchored(merkleRoot). - Retourner
(anchorAddress,timestamp,blockNumber). - Postconditions: aucun état modifié.
- Événement: aucun.
- Gas estimé: lecture
view.
6.5 transferOwnership(address newOwner) public onlyOwner (hérité)¶
- Préconditions: caller owner,
newOwner != address(0). - Étapes: changement owner.
- Postconditions: nouveau owner actif pour futurs ancrages.
- Événement:
OwnershipTransferred(oldOwner,newOwner).
6.6 renounceOwnership() (override)¶
- Préconditions: aucune utile.
- Étapes: revert immédiat
RenounceOwnershipDisabled(). - Postconditions: owner inchangé.
- Événement: aucun.
7. Flux alternatifs et d’erreur¶
| ID | Condition déclenchante | Fonction | Erreur / revert | Code |
|---|---|---|---|---|
| ERR-53-01 | caller non-owner | anchor | OwnableUnauthorizedAccount(caller) | OZ-OWN-401 |
| ERR-53-02 | merkleRoot == bytes32(0) | anchor | ZeroMerkleRoot() | PV-53-001 |
| ERR-53-03 | merkleRoot déjà présente | anchor | MerkleRootAlreadyAnchored(merkleRoot) | PV-53-002 |
| ERR-53-04 | racine absente | getAnchor | MerkleRootNotAnchored(merkleRoot) | PV-53-003 |
| ERR-53-05 | newOwner == address(0) | transferOwnership | OwnableInvalidOwner(address(0)) | OZ-OWN-400 |
| ERR-53-06 | appel renounceOwnership | renounceOwnership | RenounceOwnershipDisabled() | PV-53-004 |
8. Matrice de traçabilité (exigences → critères)¶
| Exigence | Description | CA couverts |
|---|---|---|
| EF-1 | Enregistrement root + rejet null + unicité + event | CA-53-01, 02, 03, 04, 05, 11, 12 |
| EF-2 | Traçabilité publique événementielle | CA-53-02, 03, 04, 05, 19 |
| EF-3 | Non-altérabilité / non-suppression | CA-53-12, 20 |
| EF-4 | Vérifiabilité indépendante (isAnchored, getAnchor) | CA-53-06, 07, 08, 09, 19 |
| EF-5 | Identité stable (nom/version/adresse non-upgradable/source vérifié) | CA-53-16, 18 |
| ENF-1 | Gas maîtrisé <50k | CA-53-17 |
| ENF-2 | Scalabilité append-only, durée longue | CA-53-20 |
| ENF-3 | Pérennité, indépendance services externes | CA-53-18, 19 |
| ENF-4 | Simplicité, surface minimale, non-upgradable | CA-53-15, 16, 17, 20 |
Couverture: 100% des EF/ENF mappées à au moins un CA testable.
9. Considérations de sécurité¶
- Access control:
onlyOwnersuranchor,OwnableOpenZeppelin v5 audité; owner recommandé multisig. - Reentrancy: aucune interaction externe, aucun
call; risque négligeable. - Overflow/underflow: Solidity
^0.8.20protège nativement; castuint64sûr pour horizons temporels/pratiques (> 500 milliards d’années pour timestamp, > 500 milliards de blocs à cadence actuelle). - DoS gas: pas de boucles non bornées; complexité O(1) par ancrage.
- Input validation: rejet explicite root nulle + duplication.
- Immutabilité probatoire: pas de méthode update/delete; non-upgradable.
- Key management risk (R-05): utiliser owner multisig;
transferOwnershippour rotation contrôlée;renounceOwnershipdésactivé. - Front-running / ordering: non critique (owner unique); l’ordre de blocs est déjà la vérité probatoire.
- Audit trail juridique:
- preuve primaire:
MerkleRootAnchored(log immuable) - preuve contextuelle: tx hash, block hash, block timestamp, block number
- preuve d’intégrité: lecture croisée explorateur + RPC +
getAnchor/isAnchored - opposabilité: tiers autonome sans composant propriétaire ProbatioVault.
10. Déploiement¶
10.1 Configuration Foundry (référence)¶
solc = "0.8.20"(ou^0.8.20verrouillé parfoundry.toml)- Optimizer activé (ex:
optimizer = true,optimizer_runs = 200) evm_version = "paris"(ou réseau-compatible)- Tests:
forge test -vvv - Gas snapshot:
forge snapshot
10.2 Structure des scripts¶
script/deploy-polygon.s.sol- réseau: Polygon Amoy (
chainId 80002) - lit
PRIVATE_KEY+INITIAL_OWNER - déploie
new ProbatioVaultAnchor(initialOwner) - log adresse + tx hash + block
script/deploy-arbitrum.s.sol- réseau: Arbitrum Sepolia (
chainId 421614) - même logique
10.3 Vérification explorateurs¶
- Polygon Amoy: vérification source (API Polygonscan Amoy)
- Arbitrum Sepolia: vérification source (API Arbiscan Sepolia)
- Commandes type:
forge script ... --broadcast --verify --verifier etherscan --etherscan-api-key $KEY- Exiger publication ABI + metadata compilateur.
10.4 Adresses attendues / déterminisme¶
- Par défaut:
CREATEclassique, adresse différente par réseau. - Optionnel (si exigence d’adresse prédictible):
CREATE2via factory déterministe (même deployer + même salt + même bytecode => même adresse cross-chain). - Recommandation PD-53:
CREATEsuffit (EF-5 = stabilité post-déploiement, pas nécessité d’égalité d’adresse inter-chaînes).
10.5 Validation post-déploiement¶
- Appeler
VERSION()et vérifier"1.0.0". - Appeler
owner()et vérifier adresse attendue. - Exécuter un
anchor(testRoot)sur chaque testnet. - Vérifier présence de
MerkleRootAnchoreddans explorateur. - Vérifier
isAnchored(testRoot)==true. - Vérifier
getAnchor(testRoot)cohérent avec bloc/tx. - Archiver adresses et tx de déploiement dans la documentation technique.
Si tu veux, je peux enchaîner directement avec la section “tests Foundry dérivés des CA-53-XX” (matrice test unitaire/integration + squelette ProbatioVaultAnchor.t.sol).