Utils
The Utils directory contains two modules. The Utils module provides miscellaneous circuits and common utilities for Compact contract development. Most of them are pure circuits with no ledger state, so they can be called from a circuit, from a constructor, or off-chain through the compiled contract. The EvmAbi module encodes values as Ethereum ABI words, for messages that must hash identically in Compact and on the EVM.
Account identifiers
The Ownable and AccessControl modules, and the token modules, identify a caller by a Bytes<32> account identifier
rather than by a public key.
The caller proves knowledge of a 32-byte secret key through a witness, and the module derives the identifier as:
accountId = H(secretKey)where H is persistentHash.
computeAccountId is the shared derivation: it is a pure circuit, so the same function that the modules
evaluate inside a proof can be evaluated ahead of time to produce the identifier a contract expects as input,
for example the initialOwner of an Ownable contract, the defaultAdmin of an AccessControl contract,
or the recipient of a token mint.
The witnesses that supply the secret key are authored by the implementing contract's TypeScript side. The witnesses in the library repository are test-only and are not published with the package.
Deriving an identifier off-chain
Any of the following produce the same identifier for the same key:
- Call the pure circuit through the compiled contract:
pureCircuits.computeAccountId(secretKey)(exposed by any contract that re-exports it as apure circuit). - Compute it directly with
@midnight-ntwrk/compact-runtime:
import {
CompactTypeBytes,
CompactTypeVector,
persistentHash,
} from '@midnight-ntwrk/compact-runtime';
const accountIdOf = (secretKey: Uint8Array): Uint8Array =>
persistentHash(new CompactTypeVector(1, new CompactTypeBytes(32)), [secretKey]);The secret key must be a 32-byte cryptographically secure random value (crypto.getRandomValues() or equivalent).
The secret key is the identity. Never log or persist it in plaintext, keep it in offline or enclave storage where possible, and avoid evaluating the derivation in an untrusted runtime. A lost key cannot be recovered, and an exposed key allows impersonation.
Scope of the identifier
The derivation is deliberately untagged and global: the same secret key yields the same identifier in every module and every contract that derives one, so a user who wants to carry a single identity across contracts can. Users who want unlinkable identities across contracts should use a different secret key per contract. A module that needs a per-deployment, unlinkable identity should not use this circuit.
The output is an identifier, not a secret. Because the construction carries no domain-separation tag, it must not be reused to derive private values: a circuit that derives a private value from a secret must use its own domain-separation tag.
Zero accounts
zeroAccount returns the canonical zero Either<Bytes<32>, ContractAddress> (the left variant set to zero),
which the token modules use as the mint and burn sentinel and for cleared approvals.
isTargetZero checks whether the active side of such a value is zero.
Contract as recipient
selfAsRecipient wraps the executing contract's own address as an Either<ZswapCoinPublicKey, ContractAddress>,
the recipient shape expected by shielded send operations, for deposits into the contract and for receiving change.
EvmAbi
The EvmAbi API supplies the encoders a contract needs to hash the exact bytes an EVM signer hashed.
keccak256<T>(value) hashes Compact's binary representation of T, which coincides with abi.encode for some types
and diverges for others:
Bytes<n>is emitted as exactlynbytes. ABytes<32>already is an ABI word, andkeccak256<Vector<n, Bytes<32>>>(words)is byte-identical to Solidity'skeccak256(abi.encode(word0, ..., wordN)).- Integers cast with
as Bytes<n>are emitted little-endian at their own width (aUint<64>occupies 8 bytes), whileabi.encode(uint256)is a big-endian 32-byte word. uint8Word, uint64Word, and uint128Word produce that word. Booleanis emitted as a single byte, whileabi.encode(bool)is a full word holding 0 or 1 in its least significant byte. boolWord produces that word.
A message is assembled as a Vector<n, Bytes<32>> of ABI words and hashed with keccak256 to reproduce an EVM digest exactly.
A Compact enum reaches a digest as uint8Word(kind as Uint<8>), matching Solidity, where an enum ABI-encodes as its uint8 ordinal.
Pick the narrowest encoder that fits the value: uint128Word costs roughly twice uint64Word for the same 32-byte word.
These are encoding primitives only. The signing envelope (a raw digest, EIP-191, or EIP-712 typed data) belongs to the contract that owns the message; the Crypto guide composes EvmAbi with the Eip712 and Ecdsa modules.
EvmAbi needs no keccak256 of its own and compiles on either ZKIR backend.
Hashing its words with keccak256 requires ZKIR v3 (--feature-zkir-v3).
Usage
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
import "./node_modules/@openzeppelin/compact-contracts/utils/Utils"
prefix Utils_;
export circuit performActionWhenEqual(
a: Either<ZswapCoinPublicKey, ContractAddress>,
b: Either<ZswapCoinPublicKey, ContractAddress>,
): [] {
const isEqual = Utils_isKeyOrAddressEqual(a, b);
if (isEqual) {
// Do something
} else {
// Do something else
}
}
// Re-export the derivation so the identifier can be computed off-chain
// through the compiled contract.
export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
return Utils_computeAccountId(secretKey);
}See the Utils API for the full list of circuits.