Protocol · privacy-preserving authorization
zk-multi-signers
A privacy-preserving multisignature smart-account protocol. It proves that an M-of-N approval policy was satisfied while keeping the signer set, threshold, and signing pattern private.
EthereumNoirERC-1271ERC-8039
Overview
The account stores one public commitment, the stateRoot. Authorized signers approve a transaction off-chain, then a zero-knowledge proof demonstrates that enough valid signatures came from members of the committed signer set. On-chain contracts learn that the policy passed and nothing about the identities behind it.
Core architecture
Private state commitmentA Poseidon-based Merkle commitment binds the authorized signer set and hidden M-of-N threshold into one public stateRoot.
State validation circuitA deployment-time Noir circuit proves that the public state root represents a valid private signer configuration and threshold.
Transaction validation circuitAn execution-time Noir circuit verifies ECDSA signatures, signer membership, signature uniqueness, and threshold satisfaction without revealing the signers.
ERC-8039 verifier interfaceA proof-system-agnostic contract interface verifies the circuit proof on-chain and lets Safe, Nexus, or another account adapter consume the same result.
How it works
The protocol has a private policy layer, a proof layer, and an on-chain verification layer. Signers and thresholds stay off-chain. Their configuration is represented by a public state root. Noir circuits prove that the configuration is valid and that a transaction received enough authorized approvals. ERC-8039 provides the common verification boundary used by each smart-account integration.
- Define the private policy. The group chooses its authorized signers and M-of-N approval threshold off-chain.
- Create the commitment. Signer identifiers are placed in a Poseidon Merkle tree. Its root and a hash of the threshold are combined into the public
stateRoot. - Prove valid state. The state-validation circuit proves that the commitment was constructed from a valid signer set and threshold while keeping both private.
- Approve a transaction privately. At least M authorized signers sign the transaction hash. Their public keys, signatures, Merkle paths, and threshold become private circuit inputs.
- Generate the authorization proof. The transaction circuit verifies the ECDSA signatures, derives signer addresses, checks Merkle membership, prevents duplicate approvals, and confirms that the hidden threshold was met.
- Verify through the account integration. The integration supplies the transaction hash, stored state root, and proof to an ERC-8039 verifier. A valid result authorizes the Safe owner flow, Nexus validator flow, or another compatible smart-account flow.
Private policy
signers + threshold → stateRoot → state-validation proof
Transaction authorization
M signatures + transaction hash → transaction proof
→ ERC-8039 verifier → smart-account integration → execution
Publicly visible: the state root, verifier, proof result, and transaction outcome. Private: signer identities, signer count, threshold, individual signatures, and historical approval patterns.
Transaction flow
- Commit the policy. Build a Merkle tree from the private signer set and threshold, then derive the public state root.
- Register the commitment. The chosen smart-account integration stores or references the state root and ERC-8039 verifier.
- Collect approvals. The required signers sign the transaction hash off-chain.
- Prove authorization. The Noir circuit verifies ECDSA signatures, membership, uniqueness, and the threshold.
- Execute. Submit the transaction and proof through the relevant Safe, Nexus, or compatible account flow. Execution continues after ERC-8039 verification succeeds.
State commitment
stateRoot = MerkleRoot([
signersMerkleRoot,
Poseidon(threshold)
])
The transaction circuit exposes only the transaction hash and state root as public inputs. Signers, signatures, membership paths, and threshold remain private witnesses.
Install and test
git clone https://github.com/MicrochainLabs/microchain-zk-signers.git
cd microchain-zk-signers
forge install
pnpm install
cd noir && nargo compile && cd ..
npx hardhat compile
npx hardhat test
Prerequisites: Node.js 18 or later, pnpm, Foundry, Nargo, and the Barretenberg bb CLI.
Read the source repository ↗
Circuit toolkit · composable account logic
zk-smart-account-kit
Composable Noir primitives for privacy-preserving identity, threshold signatures, and programmable transaction policies inside smart accounts.
Noir workspaceSafeNexus / ERC-7579ERC-8039
Why it exists
Conventional smart-account authorization publishes signer addresses, approval thresholds, and policy rules. The kit moves that logic into zero-knowledge circuits. The account stores a cryptographic commitment and verifies that a private authorization rule passed.
Primitive library
zk_signerECDSA address recovery and single-signer authorization inside a zero-knowledge proof.
zk_labelsPrivate identity and role labels committed to a Poseidon Merkle tree.
zk_multisigM-of-N threshold validation with hidden signer identities and private policy parameters.
zk_scopeProgrammable access control for targets, function selectors, value limits, and calldata constraints.
Repository structure
zk-smart-account-kit/
├── Nargo.toml
├── lib/
│ ├── zk_signer/
│ ├── zk_labels/
│ ├── zk_multisig/
│ └── zk_scope/
└── noir/
Integration model
- Choose a primitive and construct its private state tree.
- Commit the resulting root to the smart account or module.
- Build circuit inputs and generate the proof off-chain.
- Encode public signals and proof bytes for the account call.
- Verify through an ERC-8039-compatible verifier before execution.
Explore the circuit toolkit ↗
Ethereum standard · proof verification
ERC-8039
A proof-system-agnostic interface for smart accounts to verify zero-knowledge proofs on-chain and program account logic with ZK circuits.
Standards TrackAccount AbstractionZK proofsUnder review
Why a standard is needed
Every ZK verifier contract on Ethereum currently exposes its own ABI. Groth16 expects one arrangement of curve points and public inputs. PLONK wrappers may accept receipts. UltraHonk has another layout. Proof encoding, failure behavior, and domain separation vary across implementations. Wallets and protocols must write custom integration logic for each verifier.
ERC-8039 creates one contract boundary across those systems. The parallel is ERC-1271: before its standard signature interface, each smart contract verified signatures differently. isValidSignature(bytes32,bytes) gave wallets and protocols one method they could rely on. ERC-8039 applies the same idea to zero-knowledge proofs.
The interface
Each ERC-8039 verifier is specialized for one proof system and one circuit. The surrounding smart account, protocol, or tool calls the same three functions regardless of the verifier behind them.
interface IERC8039 {
// Verify a ZK proof. Must not revert on failure.
function verifyProof(
bytes calldata publicInputs,
bytes calldata proof
) external view returns (bytes4 magicValue);
// Machine-readable proof system identifier.
function getProofType() external view returns (bytes32);
// Human-readable description of the statement.
function metadata() external view returns (string memory);
}
Magic-value verification
The return-value pattern mirrors ERC-1271. Successful verification returns the selector of verifyProof(bytes,bytes). An invalid proof returns zero. Verification failure is expressed as a return value, allowing callers to check proofs inline without exception-handling overhead.
Valid proof0x534f5876
bytes4(keccak256("verifyProof(bytes,bytes)"))
Invalid proof0x00000000
The verifier must not revert because the proof is invalid.
Integrations should compare the returned bytes directly with the ERC-8039 magic value. Any other value is a failed verification.
Proof type and statement metadata
getProofType() describes how a proof is verified. It returns a machine-readable hash that routers, wallets, registries, and developer tooling can use to identify the proving stack.
keccak256("honk-barretenberg")
keccak256("groth16-circom")
keccak256("groth16-gnark")
keccak256("plonk-circom")
keccak256("plonk-halo2_pse")
keccak256("ultraplonk-barretenberg")
keccak256("sp1")
keccak256("risc0")
The standard defines nine proof-system identifiers and can accommodate additional identifiers as implementations evolve.
metadata() describes what the circuit proves. It gives auditors, interfaces, and operators readable context about the statement and its version.
"Private signers v1.3.0 - Private multisig wallet"
"Age verification v2.1.0 - Prove age > 18"
"DAO voting v1.0.0 - Token holder eligibility"
"Private transfer v2.5.0 - Shielded USDC transactions"
getProofType()The technical verification system.
metadata()The semantic meaning of the statement.
Generic public inputs
publicInputs is deliberately typed as bytes. Proof systems and circuits use different encodings, including arrays of field elements, integers, fixed-width values, and structured data. Each verifier adapter decodes the bytes into the format required by its underlying verifier.
This keeps the ERC interface independent of any proof system. An account can replace a Honk verifier with a Groth16, SP1, or RISC Zero verifier while preserving the account-facing call pattern. The verifier contract remains responsible for decoding its public inputs and invoking the underlying proof implementation.
Replay protection
A valid ZK proof can be replayed whenever its statement is not bound to a specific context. ERC-8039 leaves replay prevention to the circuit and its public signals because the verifier interface cannot infer the application’s security domain.
Context values should be included in publicInputs and consumed by the circuit itself:
Chain IDPrevents a proof generated for one network from being reused on another.
Account addressBinds authorization to the intended smart account or module installation.
NonceMakes a proof valid for one operation and prevents repeat execution.
ExpiryRestricts authorization to an explicit time window when the application requires it.
Replay protection belongs inside the proven statement. The circuit must cryptographically enforce the context, rather than relying on metadata or off-chain conventions.
Inside the Microchain stack
zk-smart-account-kit targets ERC-8039 across its circuits. Verifiers for zk_multi_sig_ecdsa, zk_label_binding, and zk_scope_validation implement the same interface. Safe contract-owner adapters and ERC-7579 validator modules delegate proof checks to those verifier contracts.
- A circuit defines the private witnesses and public statement.
- Its ERC-8039 verifier decodes the public inputs and verifies the proof.
- The Safe or ERC-7579 adapter checks the returned magic value.
- The account’s authorization logic continues without depending on the underlying proof system.
smart account logic
↓ same IERC8039 call
ERC-8039 verifier adapter
↓ system-specific decoding
Honk / Groth16 / PLONK / SP1 / RISC Zero
Changing the proof system means changing the verifier contract. The smart-account integration and its authorization flow can remain unchanged.
Status and authorship
ERC-8039 is under review in the Ethereum ERCs repository as PR #1238. It is authored by Walid Khemiri and Ismail Amara, and emerged from the verification requirements of Microchain Labs’ private smart-account work.
Read the full ERC-8039 proposal ↗
Guide · application architecture
Smart Account Integration
Connect a private authorization circuit to a Safe or ERC-7579-compatible account through a small, auditable verification boundary.
ClientProverModuleVerifier
Integration path
- Define private state. Build the signer, label, multisig, or scope tree locally and retain the witnesses off-chain.
- Install the commitment. Configure the account module with the root and its ERC-8039 verifier address.
- Prepare the operation. Bind the proof to the account, chain, nonce, and transaction or user-operation hash.
- Generate the proof. Run Noir and Barretenberg in the client or a trusted proving environment.
- Encode validation data. Package the public signals and proof bytes in the format expected by the module.
- Verify and execute. The account calls
verifyProof, checks the magic value, and continues after validation.
Trust boundary
private witnesses
↓
client / prover ── proof + signals ──▶ smart account
↓
ERC-8039 verifier
↓
execution
The verifier should bind every proof to its intended account context. Domain separation, chain ID, nonce, and operation hash prevent replay in another account or transaction.
Safe integration
Safe integrates the protocol as an ERC-1271 contract owner. Each private signer group receives a ZKMultiSigEcdsaProxy. The proxy holds that group’s stateRoot and ERC-8039 verifier address, while the shared ZKMultiSigEcdsaSingleton contains the signature-validation logic. This keeps deployments small and allows many private signer groups to reuse one implementation.
01Proxy becomes a Safe ownerDeploy the proxy through the factory after proving the initial state, then add its address to the Safe owner set. The proxy can sit beside EOAs or other ZK proxies.
02Signers approve off-chainPrivate signers sign the Safe transaction hash and generate a threshold proof. The proof is supplied to Safe as the contract owner’s signature.
03Safe calls ERC-1271Safe calls isValidSignature(hash, proof) on the proxy. The proxy appends its configuration and delegates the call to the singleton.
04Singleton verifiesThe singleton sends the state root, transaction hash, and proof to the ERC-8039 verifier. Safe counts the proxy’s approval when the expected magic value is returned.
Safe wallet
└─ owner: ZKMultiSigEcdsaProxy
├─ stateRoot
├─ ERC-8039 verifier address
└─ delegatecall → ZKMultiSigEcdsaSingleton
└─ verifyProof(stateRoot, safeTxHash, proof)
Nested thresholds
A Safe can combine public and private authorization layers. For example, a 2-of-3 Safe may have one EOA owner, one proxy representing a private 3-of-5 group, and another proxy representing a private 2-of-3 group. Safe sees three owner addresses; the members and internal thresholds behind the two proxies stay hidden.
Deployment flow
- Deploy the factory, shared singleton, transaction verifier, and private-state verifier.
- Compute the private signer commitment and generate the state-validation proof.
- Call the factory to create a proxy containing the state root and verifier address.
- Add the proxy address as a Safe owner and set the Safe-level threshold.
- For each transaction, generate the ZK authorization proof and attach it as an ERC-1271 contract signature.
The proxy is public. The people represented by it, their number, their internal threshold, and the individual approvals remain private.
Nexus integration
Nexus uses the same private M-of-N circuit through a native ERC-7579 validator module. The ZKMultiSigValidator is installed on the Nexus smart account with an account-specific state root and ERC-8039 verifier. It validates ERC-4337 UserOperations and ERC-1271 signatures while working alongside other Nexus validators, executors, and hooks.
01Install the validatorInstall ZKMultiSigValidator as module type 1 and initialize it with abi.encode(stateRoot, verifierAddress), or create an account with the module pre-installed through ZKMultiSigValidatorFactory.
02Select it for the UserOpEncode the validator in the ERC-4337 nonce so Nexus routes validation to the ZK module. Put the generated proof in the UserOperation signature field.
03Validate the operationvalidateUserOp() binds the proof to the UserOperation hash and account configuration. isValidSignatureWithSender() supports ERC-1271-style message validation.
04Verify through ERC-8039The module passes the public inputs and proof to the configured verifier, which confirms that enough authorized private signers approved the operation.
Nexus smart account · ERC-7579 / ERC-4337
├─ default validator
└─ ZKMultiSigValidator
├─ stateRoot per account
├─ ERC-8039 verifier address
├─ validateUserOp()
└─ isValidSignatureWithSender()
Deployment options
Factory deploymentZKMultiSigValidatorFactory creates a Nexus account with the validator already installed. Use this route for new accounts and deterministic setup.
Existing accountCall installModule(MODULE_TYPE_VALIDATOR, validator, initData) through a UserOperation to add private multisignature validation to an existing Nexus account.
Execution flow
- Define private signers and threshold, then compute the state root off-chain.
- Install the validator or deploy a Nexus account through the factory.
- Encode the ERC-7579 execution and build the UserOperation.
- Collect the private approvals and generate a proof over the UserOperation hash.
- Select the ZK validator in the nonce, place the proof in the signature field, and submit through the ERC-4337 flow.
Signer rotation changes the committed state used by the validator. The Nexus account address and the reusable verification module remain in place.
Read the integration source ↗