Programmable Policies
Policies are programmable constraints on data reveals. They answer three questions for every commitment in the Specter protocol:
- Who can reveal the committed data?
- When can the data be revealed?
- Under what conditions is the reveal permitted?
Policies apply to any data type in the protocol -- not just tokens. A timelock can restrict when a credential becomes valid. A destination restriction can ensure an API key is only delivered to a specific recipient. A threshold witness gate can require board approval before corporate secrets are unsealed. The policy system is data-agnostic by design.
How Policies Work
A policy is a smart contract that implements the IRevealPolicy interface. When a user commits data with a policy attached, two values are embedded in the commitment hash:
policyId-- the Ethereum address of the policy contractpolicyParamsHash--keccak256(abi.encode(policyParams))reduced modulo the BN254 scalar field prime
Because these values are inputs to the Poseidon commitment hash, they are cryptographically bound to the commitment. They cannot be stripped, modified, or bypassed after the commitment is created. A commitment made with a timelock policy will always require the timelock policy -- there is no administrative override, no escape hatch, and no way to remove it.
Dual Enforcement
Specter enforces policies at two independent layers. Both must pass for a reveal to succeed.
1. ZK Circuit Enforcement (Prover Side)
The policyId and policyParamsHash are public inputs to the ZK proof. The prover must know the correct policy address and parameters to generate a valid proof. Dummy quadratic constraints in the circuit (policyIdSquare === policyId * policyId) ensure the prover cannot omit these values -- attempting to do so produces an invalid proof.
2. On-Chain Enforcement (Verifier Side)
After the proof is verified, the vault contract executes the policy's validate() function via staticcall with a 100,000 gas limit. The policy contract receives the full reveal context and returns true or false. If the policy returns false or reverts, the entire reveal transaction is rejected.
This dual enforcement means that even a malicious prover cannot bypass a policy. The ZK proof binds the policy to the commitment (the prover must know it), and the on-chain execution verifies the policy logic (the chain must approve it). Neither layer alone is sufficient -- both are required.
The IRevealPolicy Interface
Every policy contract implements the following interface:
interface IRevealPolicy {
function validate(
bytes32 commitment, // The commitment being revealed
bytes32 nullifier, // The nullifier for this reveal
address recipient, // The address receiving the data/tokens
uint256 amount, // The amount being revealed (0 for non-token data)
address token, // The token address (address(0) for native GHOST)
bytes calldata policyParams // ABI-encoded parameters specific to this policy
) external view returns (bool valid);
}
Key constraints on policy contracts:
| Constraint | Reason |
|---|---|
Called via staticcall | No state writes permitted -- policies are pure validation logic |
| 100,000 gas limit | Prevents policies from performing expensive computation or griefing |
Must return bool | true to allow the reveal, false to reject |
| No external calls | Policy contracts should not depend on other contract state (reliability) |
| No constructor / no admin | Reference policies are stateless -- anyone can verify their behavior |
PolicyRegistry
The PolicyRegistry is a purely informational contract for discoverability. It does not enforce anything at the protocol level -- unregistered policies work exactly the same as registered ones. The registry exists so that users and frontends can discover available policies.
struct PolicyInfo {
string name; // Human-readable name
string description; // Brief description of behavior
address registrant; // Who registered it
uint256 timestamp; // When it was registered
}
Anyone can register a policy by calling register(address policy, string name, string description). The only requirement is that the policy address must be a deployed contract (not an EOA). The registry provides paginated listing via getPolicies(offset, limit) and lookup via getPolicy(address).
Data-Agnostic Policy Enforcement
Policies are not limited to token reveals. The same policy system applies to every data type in the protocol:
| Data Type | Policy Example | Effect |
|---|---|---|
| Tokens (GHOST) | TimelockExpiry | Tokens cannot be revealed before unlock time or after expiry |
| Credentials | DestinationRestriction | Degree attestation can only be delivered to a specific employer |
| API Keys | TimelockExpiry | Key is only valid during a specific time window |
| Encrypted Documents | ThresholdWitness | 3-of-5 board members must sign before document is unsealed |
| Images | DestinationRestriction | Provenance proof delivered only to the authenticated buyer |
| Bearer Instruments | TimelockExpiry + DestinationRestriction | Ticket redeemable only at event time, only by ticket holder |
The amount parameter is 0 for non-token data types, and the token parameter is address(0). Policy contracts can inspect these fields to implement data-type-specific logic, or they can ignore them and enforce purely time-based or recipient-based constraints.
Built-In Policies
Specter ships with three reference policy contracts:
| Policy | Parameters | Constraint |
|---|---|---|
| TimelockExpiry | lockUntil, expiresAt | Reveal only within a time window |
| DestinationRestriction | allowedRecipient or Merkle allowlist | Reveal only to specific address(es) |
| ThresholdWitness | Witness set, signatures, threshold | M-of-N signatures required |
These are reference implementations. Any developer can deploy custom policy contracts implementing IRevealPolicy -- the protocol imposes no restrictions on policy logic beyond the interface, gas limit, and staticcall constraint.
No Policy (Default)
When policyId == address(0), no policy is enforced. In this case, policyParamsHash must also be 0. The commitment can be revealed at any time, by anyone who knows the phantom key, to any recipient. This is the default behavior for commitments created without a policy.