Nullifier System Documentation¶
Overview¶
The Nullifier System provides a cryptographically secure way to manage sets of blocked (nullified) markets and addresses in the FairWins prediction market platform. It uses an RSA Accumulator to efficiently prove that a market or address is NOT in the blocked set, enabling the frontend to filter out malicious or problematic content without revealing the full blocklist.
Why RSA Accumulators?¶
Traditional blocklists have significant drawbacks:
| Approach | Storage | Lookup | Privacy | Scalability |
|---|---|---|---|---|
| On-chain mapping | O(n) | O(1) | None - list is public | Poor - gas grows with size |
| Merkle Tree | O(log n) proofs | O(log n) | Partial | Good for membership |
| RSA Accumulator | O(1) | O(1) | Full | Excellent |
RSA Accumulators provide: - Constant storage: Only a single 256-byte value stored on-chain - Non-membership proofs: Efficiently prove something is NOT in the set - Privacy: The accumulator value reveals nothing about set contents - Scalability: Can handle millions of entries with the same storage cost
Architecture¶
┌─────────────────────────────────────────────────────────────────────┐
│ ADMIN OPERATIONS │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Nullify Market│ │Nullify Address│ │ Reinstate │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ NullifierRegistry │ │
│ │ (On-Chain) │ │
│ ├─────────────────────┤ │
│ │ • RSA Accumulator │ │
│ │ • Nullified Markets │ │
│ │ • Nullified Addrs │ │
│ │ • Statistics │ │
│ └──────────┬──────────┘ │
└───────────────────────────────┼─────────────────────────────────────┘
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ TreasuryVault │ │Conditional │ │ Frontend │
│ │ │MarketFactory │ │ │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ Block nullified │ Block nullified │ Filter nullified
│ withdrawal │ │ trading │ │ markets from │
│ recipients │ │ for addresses │ │ display │
└───────────────┘ └───────────────┘ └───────────────┘
Core Concepts¶
1. Prime Number Mapping¶
Every market and address is mapped to a unique prime number using deterministic hashing:
Market Hash → keccak256(proposalId, collateralToken, conditionId, passPositionId, failPositionId)
Address Hash → keccak256(address)
Hash → Prime Number (using Miller-Rabin primality test with incremental search)
This ensures: - Same input always produces the same prime - Different inputs produce different primes (with overwhelming probability) - Primes are suitable for RSA accumulator operations
2. RSA Accumulator Operations¶
The accumulator value A represents the product of all nullified primes:
A = g^(p1 × p2 × p3 × ... × pn) mod n
Where:
n = RSA modulus (product of two secret safe primes)
g = Generator (coprime to n)
p1...pn = Prime numbers of nullified elements
Adding an element (nullification):
Non-membership proof (for frontend validation):
Given element x that is NOT in the set, prove it using Bezout's identity:
If gcd(x, product_of_all_primes) = 1
Then there exist integers a, b such that:
a*x + b*product = 1
The witness (d, b) satisfies:
A^b * g^a ≡ A mod n (only if x is NOT in the set)
3. Market Hash Computation¶
Markets are identified by a deterministic hash of their core parameters:
bytes32 marketHash = keccak256(abi.encodePacked(
"MARKET_V1", // Version prefix for future compatibility
proposalId, // Unique proposal identifier
collateralToken, // Token used for collateral
conditionId, // Gnosis CTF condition ID
passPositionId, // Position ID for PASS outcome
failPositionId // Position ID for FAIL outcome
));
Smart Contracts¶
NullifierRegistry.sol¶
The central contract managing all nullification state.
Roles¶
| Role | Permission | Managed By |
|---|---|---|
DEFAULT_ADMIN_ROLE |
Grant/revoke all roles | Contract deployer |
NULLIFIER_ADMIN_ROLE |
Add/remove nullifications | Operations Admin |
Key Functions¶
Initialization:
// Set up RSA parameters (owner only, once)
function initializeRSAParams(
bytes memory _n, // RSA modulus (256 bytes)
bytes memory _g, // Generator (256 bytes)
bytes memory _initialAcc // Initial accumulator = g
) external;
Nullification Operations:
// Nullify a single market
function nullifyMarket(bytes32 marketHash) external;
// Nullify a single address
function nullifyAddress(address addr) external;
// Batch nullify markets (max 50 per call)
function batchNullifyMarkets(bytes32[] memory marketHashes) external;
// Batch nullify addresses (max 50 per call)
function batchNullifyAddresses(address[] memory addrs) external;
// Reinstate previously nullified market
function reinstateMarket(bytes32 marketHash) external;
// Reinstate previously nullified address
function reinstateAddress(address addr) external;
Query Functions:
// Check if a market is nullified
function isMarketNullified(bytes32 marketHash) external view returns (bool);
// Check if an address is nullified
function isAddressNullified(address addr) external view returns (bool);
// Get current accumulator value
function getAccumulator() external view returns (bytes memory);
// Get nullification statistics
function getStatistics() external view returns (
uint256 nullifiedMarketCount,
uint256 nullifiedAddressCount,
uint256 totalNullifications,
uint256 totalReinstatements,
uint256 lastAccumulatorUpdate
);
// Paginated list of nullified markets
function getNullifiedMarkets(uint256 offset, uint256 limit)
external view returns (bytes32[] memory, uint256 total);
// Paginated list of nullified addresses
function getNullifiedAddresses(uint256 offset, uint256 limit)
external view returns (address[] memory, uint256 total);
Events¶
event RSAParamsInitialized(bytes n, bytes g);
event MarketNullified(bytes32 indexed marketHash, address indexed nullifiedBy);
event MarketReinstated(bytes32 indexed marketHash, address indexed reinstatedBy);
event AddressNullified(address indexed addr, address indexed nullifiedBy);
event AddressReinstated(address indexed addr, address indexed reinstatedBy);
event AccumulatorUpdated(bytes newAccumulator);
TreasuryVault Integration¶
The TreasuryVault can optionally block withdrawals to nullified addresses.
Configuration¶
// Set the NullifierRegistry address (owner only)
function setNullifierRegistry(address _nullifierRegistry) external;
// Enable/disable enforcement (owner only)
function setNullificationEnforcement(bool _enforce) external;
// Check if an address would be blocked
function isRecipientNullified(address recipient) external view returns (bool);
Behavior¶
When enforcement is enabled:
- withdrawETH() reverts if recipient is nullified
- withdrawERC20() reverts if recipient is nullified
- WithdrawalBlockedByNullification event is emitted before revert
When enforcement is disabled (default): - Withdrawals proceed normally regardless of nullification status
ConditionalMarketFactory Integration¶
The market factory can optionally block trading for nullified markets and addresses.
Configuration¶
// Set the NullifierRegistry address (owner only)
function setNullifierRegistry(address _nullifierRegistry) external;
// Enable/disable on-chain enforcement (owner only)
function setNullificationEnforcement(bool _enforce) external;
// Check if a market is nullified
function isMarketNullified(uint256 marketId) external view returns (bool);
Behavior¶
When enforcement is enabled:
- buyTokens() reverts if market or caller is nullified
- sellTokens() reverts if market or caller is nullified
- Trading is blocked for all nullified entities
When enforcement is disabled: - Trading proceeds normally - Frontend is expected to filter markets
Frontend Integration¶
React Hooks¶
useNullifierContracts¶
Provides contract interactions for the NullifierRegistry:
import { useNullifierContracts } from '../hooks/useNullifierContracts';
function AdminPanel() {
const {
// State
isLoading,
error,
statistics,
nullifiedMarkets,
nullifiedAddresses,
// Actions
nullifyMarket,
nullifyAddress,
reinstateMarket,
reinstateAddress,
batchNullifyMarkets,
batchNullifyAddresses,
// Queries
isMarketNullified,
isAddressNullified,
refreshStatistics,
} = useNullifierContracts();
// Use in component...
}
useMarketNullification¶
Provides market filtering for display components:
import { useMarketNullification } from '../hooks/useMarketNullification';
function MarketGrid({ markets }) {
const {
filterMarkets,
isLoading,
isMarketNullified
} = useMarketNullification();
// Filter out nullified markets before display
const visibleMarkets = useMemo(() => {
if (isLoading) return markets;
return filterMarkets(markets);
}, [markets, filterMarkets, isLoading]);
return (
<div>
{visibleMarkets.map(market => (
<MarketCard key={market.id} market={market} />
))}
</div>
);
}
JavaScript RSA Accumulator Library¶
Located in frontend/src/utils/rsaAccumulator.js:
import {
RSAAccumulator,
hashToPrime,
computeMarketHash
} from '../utils/rsaAccumulator';
// Create accumulator instance
const accumulator = new RSAAccumulator(nHex, gHex);
// Compute market hash
const marketHash = computeMarketHash({
proposalId: 123,
collateralToken: '0x...',
conditionId: '0x...',
passPositionId: 1,
failPositionId: 2
});
// Convert to prime
const prime = hashToPrime(marketHash);
// Check membership (for debugging)
const isMember = accumulator.contains(prime);
// Generate non-membership witness
const witness = accumulator.generateNonMembershipWitness(prime);
// Verify non-membership
const isNotMember = accumulator.verifyNonMembership(prime, witness);
Admin Panel¶
The NullifierTab component provides a UI for managing nullifications:
Features: - View statistics (counts, last update time) - Nullify markets by hash or market data - Nullify addresses - View paginated lists of nullified items - Reinstate markets and addresses - Batch operations
Access Control:
- Requires NULLIFIER_ADMIN_ROLE or OPERATIONS_ADMIN_ROLE
- Read-only mode for users without admin permissions
Security Considerations¶
RSA Modulus Generation¶
The security of the RSA accumulator depends on the RSA modulus n being the product of two secret safe primes.
Trusted Setup Requirements:
1. Generate two large safe primes p and q (each ~1024 bits)
2. Compute n = p × q
3. Securely destroy p and q after computing n
4. Use a verifiable ceremony or secure multi-party computation
If the factorization of n is known, an attacker could:
- Forge membership/non-membership proofs
- Add elements without detection
On-Chain vs Off-Chain Enforcement¶
| Mode | Frontend | On-Chain | Use Case |
|---|---|---|---|
| Off-chain only | Filters markets | No checks | Low gas, trust frontend |
| On-chain enabled | Filters markets | Blocks trades | High security, higher gas |
| Hybrid | Filters markets | Blocks critical ops | Balance security/cost |
Recommendation: Enable on-chain enforcement for TreasuryVault (critical funds) and optionally for ConditionalMarketFactory based on threat model.
Role Management¶
- Only
OPERATIONS_ADMIN_ROLEholders can grantNULLIFIER_ADMIN_ROLE - Consider multi-sig or timelock for admin operations
- Monitor
MarketNullifiedandAddressNullifiedevents
Attack Vectors & Mitigations¶
| Attack | Mitigation |
|---|---|
| Admin key compromise | Multi-sig, timelock, role separation |
| Malicious nullification | Governance review, reinstatement capability |
| RSA modulus factorization | Proper trusted setup, large primes |
| Frontend bypass | On-chain enforcement for critical operations |
| DoS via large batch | MAX_BATCH_SIZE = 50 |
Deployment & Configuration¶
1. Deploy NullifierRegistry¶
const NullifierRegistry = await ethers.getContractFactory("NullifierRegistry");
const nullifierRegistry = await NullifierRegistry.deploy();
await nullifierRegistry.waitForDeployment();
2. Initialize RSA Parameters¶
// Generate or use pre-generated RSA parameters
const n = "0x..."; // 256 bytes (2048-bit modulus)
const g = "0x03"; // Generator (typically 3)
const initialAcc = g; // Initial accumulator = generator
await nullifierRegistry.initializeRSAParams(n, g, initialAcc);
3. Grant Admin Roles¶
const NULLIFIER_ADMIN_ROLE = ethers.keccak256(
ethers.toUtf8Bytes("NULLIFIER_ADMIN_ROLE")
);
await nullifierRegistry.grantRole(NULLIFIER_ADMIN_ROLE, adminAddress);
4. Integrate with TreasuryVault¶
// Set the registry
await treasuryVault.setNullifierRegistry(nullifierRegistry.address);
// Enable enforcement
await treasuryVault.setNullificationEnforcement(true);
5. Integrate with ConditionalMarketFactory¶
// Set the registry
await marketFactory.setNullifierRegistry(nullifierRegistry.address);
// Optionally enable on-chain enforcement
await marketFactory.setNullificationEnforcement(true);
6. Frontend Configuration¶
Update the contract addresses in frontend configuration:
// src/config/contracts.js
export const CONTRACTS = {
// ... other contracts
NULLIFIER_REGISTRY: '0x...',
};
Testing¶
Unit Tests¶
# Run NullifierRegistry tests
npx hardhat test test/NullifierRegistry.test.js
# Run TreasuryVault tests (includes nullification tests)
npx hardhat test test/TreasuryVault.test.js
Integration Tests¶
# Run nullifier integration tests
npx hardhat test test/integration/nullifier/nullifier-integration.test.js
E2E Tests¶
# Run Cypress E2E tests for admin panel
npx cypress run --spec "cypress/e2e/08-nullifier-management.cy.js"
Troubleshooting¶
Common Issues¶
"RSA params already initialized" - RSA parameters can only be set once - Deploy a new contract if you need different parameters
"Not authorized" on nullification
- Caller needs NULLIFIER_ADMIN_ROLE
- Check role with hasRole(NULLIFIER_ADMIN_ROLE, address)
"Recipient address is nullified" - Address is in the nullified set - Either reinstate the address or disable enforcement
Frontend not filtering markets - Check that NullifierRegistry address is configured - Verify the hook is properly initialized - Check browser console for errors
High gas costs - Use batch operations for multiple nullifications - Consider off-chain enforcement for lower priority cases
API Reference¶
NullifierRegistry ABI¶
See frontend/src/abis/NullifierRegistry.js for the complete ABI.
Key TypeScript Types¶
interface NullifierStatistics {
nullifiedMarketCount: bigint;
nullifiedAddressCount: bigint;
totalNullifications: bigint;
totalReinstatements: bigint;
lastAccumulatorUpdate: bigint;
}
interface MarketData {
proposalId: number | bigint;
collateralToken: string;
conditionId: string;
passPositionId: number | bigint;
failPositionId: number | bigint;
}
Changelog¶
v1.0.0 (Initial Release)¶
- RSA Accumulator-based nullifier system
- NullifierRegistry contract
- TreasuryVault integration
- ConditionalMarketFactory integration
- Frontend hooks and admin panel
- Comprehensive test coverage