CryptoRegistry¶
Source: fourier/stdlib/crypto_registry.fou.
The on-chain mapping from scheme_id to PQC precompile address. Deployed once per chain and held by a Timelock for governance.
Storage¶
| Slot | Name | Type | Purpose |
|---|---|---|---|
0 | owner | address | Initial deployer; transferred to the Timelock |
1 | scheme_to_addr | map[uint, uint] | scheme_id → precompile_addr |
Slots 0–1 are reserved.
Source¶
contract CryptoRegistry {
storage owner: address @ 0;
storage scheme_to_addr: map[uint, uint] @ 1;
event SchemeRegistered(scheme_id: uint, precompile: uint);
event SchemeRetired(scheme_id: uint);
event OwnerTransferred(new_owner: address);
fn init() {
owner = caller();
scheme_to_addr[1] = 2; // ML-DSA-87 at 0x02
scheme_to_addr[2] = 3; // SLH-DSA at 0x03
}
pub fn set_scheme(scheme_id: uint, precompile: uint) {
require(caller() == owner);
scheme_to_addr[scheme_id] = precompile;
emit SchemeRegistered(scheme_id, precompile);
}
pub fn retire_scheme(scheme_id: uint) {
require(caller() == owner);
scheme_to_addr[scheme_id] = 0;
emit SchemeRetired(scheme_id);
}
pub fn get_scheme(scheme_id: uint) -> uint {
return scheme_to_addr[scheme_id];
}
pub fn transfer_ownership(new_owner: address) {
require(caller() == owner);
owner = new_owner;
emit OwnerTransferred(new_owner);
}
pub fn get_owner() -> address {
return owner;
}
}
Selectors¶
| Selector | Function |
|---|---|
0x01 | set_scheme(uint, uint) |
0x02 | retire_scheme(uint) |
0x03 | get_scheme(uint) -> uint |
0x04 | transfer_ownership(address) |
0x05 | get_owner() -> address |
Pre-registered schemes¶
At deploy time, init populates:
scheme_id | precompile_addr | Algorithm |
|---|---|---|
1 | 2 (0x02) | ML-DSA-87 (FIPS 204) |
2 | 3 (0x03) | SLH-DSA-SHA2-128s (FIPS 205) |
These match the constants in fourier/codegen.py::CRYPTO_SCHEMES and the precompile addresses in vm/precompiles.py.
Role¶
The compiler hard-codes CRYPTO_SCHEMES because verify_sig(scheme_id, ...) requires a compile-time literal scheme_id to select the precompile address. The CryptoRegistry serves a complementary role: it is the public, on-chain record of which scheme identifiers are valid, so:
- Clients and indexers can resolve
scheme_id → precompile_addrwithout trusting compiler internals. - Governance can add new schemes — after node operators upgrade their VMs to recognize the precompile — through Timelock proposals.
- Retired schemes can be removed so client SDKs stop emitting signatures with them.
The registry does not prevent contracts from referencing an unknown or retired scheme at compile time; that is a compiler-level decision. Clients querying the registry observe what the chain considers authoritative.
Governance flow¶
1. Foundation operator deploys CryptoRegistry.
init() sets owner = operator and registers schemes 1 + 2.
2. Foundation operator deploys Timelock.
init() sets owner = operator, delay = 14 days, grace = 14 days.
3. Foundation operator calls registry.transfer_ownership(timelock).
The registry is now governed by the Timelock.
4. Foundation operator queues a registry update through the Timelock:
timelock.queue(
target = registry_addr,
value = 0,
selector = 0x01, // set_scheme
arg = packed_args, // see "Packing two arguments" below
eta = now + 14 days
)
5. After the delay window, anyone calls timelock.execute(id).
The Timelock invokes registry.set_scheme(scheme_id, precompile)
with the Timelock as caller, satisfying the owner check.
Packing two arguments¶
set_scheme takes two arguments; the shipped Timelock stores one argument per proposal (p_arg: map[uint, uint]). Two compatible patterns:
- Argument packing. Pack both values into a single 32-byte word — for example,
(scheme_id << 160) | precompile_addr— and unpack inside a wrapper function called by the Timelock. - Wrapper contract. Deploy a small contract whose single
set(scheme_id, precompile)function the Timelock calls; the wrapper unpacks and re-callsset_schemeon the registry.
A multi-argument Timelock variant is a planned addition.
Adding a new scheme¶
The compiler must recognize the new scheme before contracts can invoke verify_sig(N, ...). The end-to-end procedure:
- Implement the algorithm in
vm/precompiles.py. Reserve a precompile address and a gas cost. - Add the
(scheme_id, precompile_addr)pair tofourier/codegen.py::CRYPTO_SCHEMES. - Node operators upgrade to the new VM release. Operators are the final veto — nodes that decline the upgrade do not honor the new precompile.
- Governance queues
registry.set_scheme(N, addr)through the Timelock. - After the delay window, the registry reflects the new scheme and clients reading the registry begin trusting
scheme_id = N.
The registry is declarative: on-chain verification still requires nodes to ship the precompile in their VM binary. The dual veto between node operators and on-chain governance is intentional.
Composition notes¶
CryptoRegistry is intentionally small — two storage slots and five public functions — and is not designed to be inherited or copied. Its purpose is to serve as a single canonical record per chain. Deploy it once and reference its address from contracts that need to resolve scheme metadata.