Skip to main content

Threshold Witness

The ThresholdWitness contract is a reference policy that requires M-of-N witness signatures before a reveal is permitted. A designated set of witnesses must cryptographically approve the reveal by signing the reveal parameters with their Ethereum private keys. The reveal proceeds only if the required threshold of valid signatures is met.

Contract Overview

contract ThresholdWitness is IRevealPolicy {
function validate(
bytes32 commitment,
bytes32 nullifier,
address recipient,
uint256 amount,
address token,
bytes calldata policyParams
) external pure returns (bool valid) {
(address[] memory witnesses, bytes[] memory signatures, uint256 threshold) =
abi.decode(policyParams, (address[], bytes[], uint256));

if (threshold == 0 || threshold > witnesses.length) return false;

bytes32 messageHash = keccak256(
abi.encodePacked(commitment, nullifier, recipient, amount, token)
);
bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash);

// Count valid, unique witness signatures
bool[] memory counted = new bool[](witnesses.length);
uint256 validCount = 0;

for (uint256 i = 0; i < signatures.length; i++) {
(address recovered, ECDSA.RecoverError err,) =
ECDSA.tryRecover(ethSignedHash, signatures[i]);
if (err != ECDSA.RecoverError.NoError) continue;

for (uint256 j = 0; j < witnesses.length; j++) {
if (recovered == witnesses[j] && !counted[j]) {
counted[j] = true;
validCount++;
break;
}
}

if (validCount >= threshold) return true;
}

return false;
}
}

The contract is stateless and pure -- it performs all validation using only the submitted parameters. It uses OpenZeppelin's ECDSA.tryRecover for safe signature recovery (no reverts on malformed signatures) and MessageHashUtils.toEthSignedMessageHash for EIP-191 prefix compliance.

Parameters

ParameterTypeDescription
witnessesaddress[]The set of N authorized witness addresses
signaturesbytes[]The M (or more) ECDSA signatures from witnesses
thresholduint256Minimum number of valid, unique witness signatures required

The parameters are ABI-encoded as abi.encode(witnesses, signatures, threshold). The policyParamsHash bound to the commitment is:

policyParamsHash = keccak256(abi.encode(witnesses, signatures, threshold)) % BN254_SCALAR_FIELD

Important: Because signatures are part of the policyParams, they are included in the policyParamsHash computation. This means the exact set of signatures must be known at the time the policyParamsHash is committed. In practice, this means the witness signatures are collected before the reveal transaction is submitted, and their hash is verified against what was bound in the commitment.

Signature Verification Flow

Message Format

Each witness signs the following message:

messageHash = keccak256(abi.encodePacked(commitment, nullifier, recipient, amount, token))

The hash is then prefixed with the standard Ethereum signed message prefix:

ethSignedHash = "\x19Ethereum Signed Message:\n32" + messageHash

This ensures compatibility with standard Ethereum wallets (MetaMask, hardware wallets) that automatically apply the EIP-191 prefix when signing arbitrary data.

The message includes all five reveal parameters, meaning each witness explicitly approves the specific commitment, nullifier, recipient, amount, and token of the reveal. A signature for one reveal cannot be reused for a different reveal.

Deduplication and Safety

The contract includes several safety mechanisms:

MechanismPurpose
Duplicate signer detectionEach witness address can only be counted once, even if multiple signatures from the same key are provided
Invalid signature handlingECDSA.tryRecover returns an error code instead of reverting on malformed signatures -- invalid signatures are silently skipped
Non-witness signatures ignoredIf a valid signature is recovered but the signer is not in the witnesses array, it is not counted
Early exitThe loop terminates as soon as validCount >= threshold, saving gas
Zero threshold rejectionthreshold == 0 immediately returns false (at least 1 signature is always required)
Threshold > N rejectionthreshold > witnesses.length immediately returns false (impossible to meet)

Use Cases

Multi-Sig Escrow

A buyer commits payment with a 2-of-3 ThresholdWitness policy where the witnesses are: the buyer, the seller, and a neutral arbitrator. The payment can be released only when 2 of the 3 parties sign off. This creates a trustless escrow where disputes are resolved by the arbitrator's vote.

Corporate Treasury

A company commits treasury funds with a 3-of-5 ThresholdWitness policy where the witnesses are board members. No single board member can unilaterally access the funds. Withdrawals require majority approval, enforced cryptographically rather than by corporate governance convention.

Compliance Approval Gates

A financial institution commits tokens with a ThresholdWitness policy requiring signatures from compliance officers. The KYC/AML team must sign off on each reveal, ensuring regulatory compliance is enforced at the protocol level. The institution cannot bypass its own compliance process.

Multi-Party Credential Issuance

A professional credential (e.g., medical license) requires approval from multiple authorities: the examining board, the state licensing agency, and the educational institution. The credential commitment uses a 3-of-3 ThresholdWitness policy. The credential can only be issued (revealed) when all three authorities have signed.

Dead Man's Switch

A user commits sensitive data with a ThresholdWitness policy where the witnesses are trusted family members or attorneys. The data can only be revealed with M-of-N family member signatures, creating a decentralized dead man's switch for estate planning or emergency key recovery.

DAO Governance Execution

A DAO commits funds for a proposal with a ThresholdWitness policy requiring signatures from elected council members. Even after a governance vote passes, the funds are not released until the council members cryptographically confirm execution. This adds an execution layer on top of the voting layer.

Gas Considerations

ThresholdWitness is the most gas-intensive of the three reference policies because it performs ECDSA signature recovery in a loop. The gas cost scales with the number of signatures:

SignaturesApproximate Gas
1~8,000
2~14,000
3~20,000
5~32,000
10~62,000

The policy executes within a staticcall with a 100,000 gas limit. This practically limits the threshold to approximately 15 signatures before the gas cap is reached. For larger witness sets, a custom policy contract could use more gas-efficient signature aggregation (e.g., BLS signatures).