refactor: switch logic to Docker file

still requires SSH for saving to trees.self.xyz, needs refactoring
This commit is contained in:
Evi Nova
2026-01-09 17:27:29 +10:00
parent 6135f6fa2b
commit ce49ca682c
22 changed files with 680 additions and 83 deletions

21
.dockerignore Normal file
View File

@@ -0,0 +1,21 @@
.git
.github
.cursor
.vscode
.idea
.DS_Store
**/node_modules
**/dist
**/build
**/.turbo
**/.next
**/.cache
**/coverage
**/Pods
**/DerivedData
**/artifacts
**/cache
**/*.log
**/.env
**/.env.*
.yarn/cache

View File

@@ -0,0 +1,45 @@
name: OFAC Auto Updater Image
on:
push:
branches: [ feat/ofac-auto-updater ]
paths:
- common/scripts/ofac/**
- contracts/contracts/registry/**
- contracts/contracts/upgradeable/ImplRoot.sol
- .github/workflows/ofac-updater-image.yml
schedule:
- cron: "0 5 * * *"
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
file: common/scripts/ofac/Dockerfile
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/ofac-auto-updater:latest
ghcr.io/${{ github.repository_owner }}/ofac-auto-updater:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -0,0 +1,20 @@
FROM node:22-slim
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends git openssh-client ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare yarn@stable --activate
COPY . .
RUN yarn install --immutable
ENV OFAC_DATA_DIR=/data/ofac
VOLUME ["/data"]
RUN chmod +x common/scripts/ofac/entrypoint.sh
ENTRYPOINT ["/app/common/scripts/ofac/entrypoint.sh"]

View File

@@ -26,97 +26,61 @@ Connect to NordLayer VPN before running any commands.
---
## Production Deployment
## Single-Shot Auto Update (Docker/TEE)
### First Signer
This is the unified flow that runs the pipeline, updates on-chain roots directly,
and performs the prestaged upload + atomic move in one run.
### Docker
Build the image:
```bash
cd /path/to/self
# Download OFAC list and build trees
yarn dlx tsx common/scripts/ofac/index.ts
# Propose and sign
cd contracts
PRIVATE_KEY=0x... \
NETWORK=celo \
yarn dlx tsx scripts/ofac/prepareMultisigUpdate.ts
docker build -f common/scripts/ofac/Dockerfile -t ofac-auto-updater .
```
### Final Signer (2nd of 2/5)
Run with a single mount (all inputs/outputs live under `/data/ofac`):
```bash
cd /path/to/self/contracts
PRIVATE_KEY=0x... \
NETWORK=celo \
SSH_HOST=self-infra-prod \
yarn dlx tsx scripts/ofac/signExecuteAndUpload.ts
docker run --rm \\
-e PRIVATE_KEY=0x... \\
-e NETWORK=celo \\
-e SSH_HOST=self-infra-prod \\
-e UPLOAD_PATH=/home/ec2-user/self-infra/merkle-tree-reader/common/constants/ofac \\
-v /local/ofac:/data \\
-v ~/.ssh:/root/.ssh:ro \\
ofac-auto-updater
```
This script:
1. Pre-stages trees to server `/tmp/`
2. Signs the pending transaction
3. Executes on-chain
4. Moves trees to production (~6-7s mismatch)
Environment variables:
- `PRIVATE_KEY` (required): signer key used for on-chain updates
- Signer must have `TEE_ROLE` on the registry contracts
- `NETWORK`: `celo`, `celo-sepolia`, or `sepolia`
- `RPC_URL` or network-specific RPC envs (`CELO_RPC_URL`, `CELO_SEPOLIA_RPC_URL`, `SEPOLIA_RPC_URL`)
- `OFAC_DATA_DIR` (default: `/data/ofac`)
- `SSH_HOST` (default: `self-infra-staging`)
- `UPLOAD_PATH` (default: production path for the chosen network)
- `DRY_RUN=true` to skip on-chain update and upload
- `SKIP_PRESTAGE=true` to skip pre-staging (not recommended)
---
If deploying this change to existing registries, call `initializeTeeRole(TEE_ADDRESS)` after upgrade to set role admin and grant `TEE_ROLE`.
## E2E Testing (Sepolia)
Test Safe: `0x4264a631c5E685a622b5C8171b5f17BeD7FB30c6` (2/2 threshold)
### First Signer
### Local (no Docker)
```bash
cd /path/to/self
# Download and build trees (if not already done)
yarn dlx tsx common/scripts/ofac/index.ts
# Propose test transaction
cd contracts
PRIVATE_KEY=0x_SIGNER_1_KEY_ \
yarn dlx tsx scripts/ofac/test-e2e/testSafeProposal.ts
```
### Second Signer
```bash
cd /path/to/self/contracts
PRIVATE_KEY=0x_SIGNER_2_KEY_ \
NETWORK=sepolia \
SSH_HOST=self-infra-staging \
UPLOAD_PATH=/home/ec2-user/ofac-e2e-test \
yarn dlx tsx scripts/ofac/signExecuteAndUpload.ts
```
### Cleanup
```bash
ssh self-infra-staging "rm -rf /home/ec2-user/ofac-e2e-test /tmp/ofac-prestage-*"
PRIVATE_KEY=0x... \\
NETWORK=celo \\
SSH_HOST=self-infra-prod \\
yarn tsx common/scripts/ofac/runOfacAutoUpdate.ts
```
---
## How SSH Is Used
## Configuration
The updater uses SSH only for the file staging + atomic move:
1. Pre-stage generated tree files to a temp directory on the server.
2. After on-chain updates complete, atomically move the files into production.
3. Optionally verify the production directory contents.
| Environment | Safe Address | SSH Host |
|-------------|--------------|----------|
| Production (Celo) | `0x067b18e09A10Fa03d027c1D60A098CEbbE5637f0` | `self-infra-prod` |
| Staging (Celo Sepolia) | `0x067b18e09A10Fa03d027c1D60A098CEbbE5637f0` | `self-infra-staging` |
| E2E Test (Eth Sepolia) | `0x4264a631c5E685a622b5C8171b5f17BeD7FB30c6` | `self-infra-staging` |
Default RPC URLs: Celo (`forno.celo.org`), Eth Sepolia (`rpc.sepolia.org`).
Celo Sepolia requires `CELO_SEPOLIA_RPC_URL` env var.
---
## Troubleshooting
| Issue | Solution |
|-------|----------|
| `tsx: command not found` | Use `yarn dlx tsx` |
| SSH timeout | Connect to NordLayer VPN |
| Orphaned pre-staged files | `ssh <host> "rm -rf /tmp/ofac-prestage-*"` |
If SSH isnt available (e.g., missing VPN/host config), the on-chain updates can still run,
but the tree deployment step will fail unless `DRY_RUN=true`.

View File

@@ -0,0 +1,4 @@
#!/bin/sh
set -eu
exec yarn tsx common/scripts/ofac/runOfacAutoUpdate.ts "$@"

View File

@@ -0,0 +1,453 @@
/**
* OFAC Auto Updater (Single-Shot)
*
* Pipeline + on-chain update + prestaged upload in one run:
* 1. Download + parse OFAC SDN list
* 2. Build all OFAC Merkle trees
* 3. Pre-stage trees to server
* 4. Update on-chain OFAC roots (direct signer, no multisig)
* 5. Atomically move trees into production
*/
import { ethers } from 'ethers';
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
import { runOfacPipeline } from './index.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Default RPC URLs (public endpoints)
const DEFAULT_RPC_URLS: Record<string, string> = {
celo: 'https://forno.celo.org',
'celo-sepolia': 'https://celo-sepolia.drpc.org',
sepolia: 'https://rpc.sepolia.org',
};
const DEFAULT_UPLOAD_PATHS: Record<string, string> = {
celo: '/home/ec2-user/self-infra/merkle-tree-reader/common/constants/ofac',
'celo-sepolia': '/home/ec2-user/self-infra-staging/merkle-tree-reader/common/constants/ofac',
sepolia: '/home/ec2-user/ofac-e2e-test',
};
// Hardcoded registry addresses (Celo Mainnet)
const CELO_REGISTRY_ADDRESSES: Record<string, string> = {
IdentityRegistry: '0x37F5CB8cB1f6B00aa768D8aA99F1A9289802A968',
IdentityRegistryIdCard: '0xeAD1E6Ec29c1f3D33a0662f253a3a94D189566E1',
IdentityRegistryAadhaar: '0xd603Fa8C8f4694E8DD1DcE1f27C0C3fc91e32Ac4',
};
// Registry configuration
interface RegistryConfig {
name: string;
registryKey: string;
hasPassportNo: boolean;
hasNameAndDob: boolean;
hasNameAndYob: boolean;
rootTreePrefix: string;
}
const REGISTRY_CONFIGS: RegistryConfig[] = [
{
name: 'Passport Registry',
registryKey: 'IdentityRegistry',
hasPassportNo: true,
hasNameAndDob: true,
hasNameAndYob: true,
rootTreePrefix: '',
},
{
name: 'ID Card Registry',
registryKey: 'IdentityRegistryIdCard',
hasPassportNo: false,
hasNameAndDob: true,
hasNameAndYob: true,
rootTreePrefix: '_id_card',
},
{
name: 'Aadhaar Registry',
registryKey: 'IdentityRegistryAadhaar',
hasPassportNo: false,
hasNameAndDob: true,
hasNameAndYob: true,
rootTreePrefix: '_aadhaar',
},
];
// Minimal ABI for OFAC root functions
const REGISTRY_ABI = [
'function getPassportNoOfacRoot() view returns (uint256)',
'function getNameAndDobOfacRoot() view returns (uint256)',
'function getNameAndYobOfacRoot() view returns (uint256)',
'function updatePassportNoOfacRoot(uint256 root)',
'function updateNameAndDobOfacRoot(uint256 root)',
'function updateNameAndYobOfacRoot(uint256 root)',
];
// Tree files to upload
const TREE_FILES = [
'passportNoAndNationalitySMT.json',
'nameAndDobSMT.json',
'nameAndYobSMT.json',
'nameAndDobSMT_ID.json',
'nameAndYobSMT_ID.json',
'nameAndDobSMT_AADHAAR.json',
'nameAndYobSMT_AADHAAR.json',
'roots.json',
'latest-roots.json',
];
function log(msg: string) {
const timestamp = new Date().toISOString().slice(11, 23);
console.log(`[${timestamp}] ${msg}`);
}
function getRpcUrl(network: string): string | undefined {
switch (network) {
case 'celo':
return process.env.CELO_RPC_URL || DEFAULT_RPC_URLS.celo;
case 'celo-sepolia':
return process.env.CELO_SEPOLIA_RPC_URL || DEFAULT_RPC_URLS['celo-sepolia'];
case 'sepolia':
return process.env.SEPOLIA_RPC_URL || DEFAULT_RPC_URLS.sepolia;
default:
return process.env.RPC_URL;
}
}
function getRegistryAddress(registryKey: string, network: string): string | null {
if (network === 'celo') {
return CELO_REGISTRY_ADDRESSES[registryKey] || null;
}
return null;
}
function loadRoots(rootsPath: string): Record<string, string> {
if (!fs.existsSync(rootsPath)) {
throw new Error('Roots file not found: ' + rootsPath);
}
const data = JSON.parse(fs.readFileSync(rootsPath, 'utf-8'));
return data.roots || data;
}
function getRootForRegistry(
roots: Record<string, string>,
config: RegistryConfig,
rootType: 'passportNo' | 'nameAndDob' | 'nameAndYob'
): string | null {
let key: string;
switch (rootType) {
case 'passportNo':
key = 'passport_no_and_nationality';
break;
case 'nameAndDob':
if (config.rootTreePrefix === '_aadhaar') key = 'aadhaar_name_and_dob';
else if (config.rootTreePrefix === '_kyc') key = 'kyc_name_and_dob';
else if (config.rootTreePrefix === '_id_card') key = 'name_and_dob_id_card';
else key = 'name_and_dob';
break;
case 'nameAndYob':
if (config.rootTreePrefix === '_aadhaar') key = 'aadhaar_name_and_yob';
else if (config.rootTreePrefix === '_kyc') key = 'kyc_name_and_yob';
else if (config.rootTreePrefix === '_id_card') key = 'name_and_yob_id_card';
else key = 'name_and_yob';
break;
}
return roots[key] || null;
}
async function getCurrentRoot(
contract: ethers.Contract,
rootType: 'passportNo' | 'nameAndDob' | 'nameAndYob'
): Promise<string> {
try {
switch (rootType) {
case 'passportNo':
return (await contract.getPassportNoOfacRoot()).toString();
case 'nameAndDob':
return (await contract.getNameAndDobOfacRoot()).toString();
case 'nameAndYob':
return (await contract.getNameAndYobOfacRoot()).toString();
}
} catch {
return '0';
}
}
async function updateRegistryRoots(
config: RegistryConfig,
registryAddress: string,
signer: ethers.Wallet,
roots: Record<string, string>,
dryRun: boolean
): Promise<number> {
const contract = new ethers.Contract(registryAddress, REGISTRY_ABI, signer);
let updates = 0;
async function maybeUpdate(
rootType: 'passportNo' | 'nameAndDob' | 'nameAndYob',
updateFn: keyof ethers.Contract
) {
const newRoot = getRootForRegistry(roots, config, rootType);
if (!newRoot) return;
const oldRoot = await getCurrentRoot(contract, rootType);
if (oldRoot === newRoot) {
log(`No change for ${config.name} ${rootType}`);
return;
}
log(`Updating ${config.name} ${rootType}`);
if (dryRun) {
log('[DRY RUN] Skipping on-chain update');
updates += 1;
return;
}
const tx = await (contract[updateFn] as any)(newRoot);
log(`TX submitted: ${tx.hash}`);
const receipt = await tx.wait(1);
if (receipt?.status !== 1) {
throw new Error(`Update failed for ${config.name} ${rootType}`);
}
log(`Confirmed in block ${receipt.blockNumber}`);
updates += 1;
}
if (config.hasPassportNo) {
await maybeUpdate('passportNo', 'updatePassportNoOfacRoot');
}
if (config.hasNameAndDob) {
await maybeUpdate('nameAndDob', 'updateNameAndDobOfacRoot');
}
if (config.hasNameAndYob) {
await maybeUpdate('nameAndYob', 'updateNameAndYobOfacRoot');
}
return updates;
}
function prestageFiles(
treesDir: string,
sshHost: string,
stagingPath: string,
dryRun: boolean
): boolean {
log(`PRE-STAGING: Uploading trees to ${sshHost}:${stagingPath}`);
const filesToUpload = TREE_FILES
.map((f) => path.join(treesDir, f))
.filter((f) => fs.existsSync(f));
if (filesToUpload.length === 0) {
log('ERROR: No tree files found to upload!');
return false;
}
log(` Found ${filesToUpload.length} files`);
if (dryRun) {
log(' [DRY RUN] Would upload:');
filesToUpload.forEach((f) => log(` - ${path.basename(f)}`));
return true;
}
try {
execSync(`ssh ${sshHost} "mkdir -p ${stagingPath}"`, { stdio: 'pipe' });
for (const file of filesToUpload) {
const basename = path.basename(file);
process.stdout.write(` Uploading ${basename}...`);
execSync(`scp "${file}" "${sshHost}:${stagingPath}/"`, { stdio: 'pipe' });
console.log(' ok');
}
log(`Pre-staged ${filesToUpload.length} files`);
return true;
} catch (error) {
log(`ERROR: Pre-staging failed: ${error}`);
return false;
}
}
function atomicMove(
sshHost: string,
stagingPath: string,
productionPath: string,
dryRun: boolean
): { success: boolean; durationMs: number } {
log(`ATOMIC MOVE: ${stagingPath} -> ${productionPath}`);
if (dryRun) {
log(' [DRY RUN] Would move files');
return { success: true, durationMs: 0 };
}
const startTime = Date.now();
try {
execSync(`ssh ${sshHost} "mkdir -p ${productionPath}"`, { stdio: 'pipe' });
const moveCmd = `ssh ${sshHost} "mv ${stagingPath}/*.json ${productionPath}/ && rm -rf ${stagingPath}"`;
execSync(moveCmd, { stdio: 'pipe' });
const durationMs = Date.now() - startTime;
log(`Atomic move completed in ${durationMs}ms`);
return { success: true, durationMs };
} catch (error) {
const durationMs = Date.now() - startTime;
log(`ERROR: Atomic move failed after ${durationMs}ms: ${error}`);
return { success: false, durationMs };
}
}
function verifyProduction(sshHost: string, productionPath: string): void {
log('Verifying production files...');
try {
const result = execSync(
`ssh ${sshHost} "ls -la ${productionPath}/*.json 2>/dev/null | tail -10"`,
{ encoding: 'utf-8' }
);
console.log(result);
} catch {
log('Could not verify (may still be successful)');
}
}
async function main() {
console.log('');
console.log('='.repeat(70));
console.log(' OFAC AUTO UPDATE (PIPELINE + ON-CHAIN + UPLOAD)');
console.log('='.repeat(70));
console.log('');
const network = process.env.NETWORK || 'celo';
const privateKey = process.env.PRIVATE_KEY;
const rpcUrl = process.env.RPC_URL || getRpcUrl(network);
const dryRun = process.env.DRY_RUN === 'true';
if (!privateKey) {
console.error('ERROR: PRIVATE_KEY environment variable required');
process.exit(1);
}
if (!rpcUrl) {
console.error('ERROR: RPC URL required (set RPC_URL or network-specific env var)');
process.exit(1);
}
const dataDir = process.env.OFAC_DATA_DIR || '/data/ofac';
const rawDir = path.join(dataDir, 'raw');
const inputDir = path.join(dataDir, 'inputs');
const outputDir = path.join(dataDir, 'outputs');
const rootsPath = process.env.ROOTS_PATH || path.join(outputDir, 'latest-roots.json');
const treesDir = process.env.TREES_DIR || outputDir;
const sshHost = process.env.SSH_HOST || 'self-infra-staging';
const productionPath =
process.env.UPLOAD_PATH || DEFAULT_UPLOAD_PATHS[network] || DEFAULT_UPLOAD_PATHS.celo;
const skipPrestage = process.env.SKIP_PRESTAGE === 'true';
const timestamp = Date.now();
const stagingPath = process.env.STAGING_PATH || `/tmp/ofac-prestage-${timestamp}`;
log(`Network: ${network}`);
log(`RPC: ${rpcUrl}`);
log(`Data dir: ${dataDir}`);
log(`Trees dir: ${treesDir}`);
log(`SSH host: ${sshHost}`);
log(`Staging: ${stagingPath}`);
log(`Production: ${productionPath}`);
log(`Dry Run: ${dryRun}`);
console.log('');
// Step 1-3: Pipeline
log('Running OFAC pipeline...');
const pipeline = await runOfacPipeline({
rawDir,
inputDir,
outputDir,
});
if (!pipeline.success) {
console.error('ERROR: Pipeline failed:', pipeline.error);
process.exit(1);
}
// Step 4: Pre-stage files
console.log('');
console.log('-'.repeat(70));
console.log(' PHASE: PRE-STAGE FILES');
console.log('-'.repeat(70));
console.log('');
if (!skipPrestage) {
const prestageSuccess = prestageFiles(treesDir, sshHost, stagingPath, dryRun);
if (!prestageSuccess && !dryRun) {
console.error('ERROR: Pre-staging failed. Aborting before on-chain update.');
process.exit(1);
}
} else {
log('Skipping pre-stage (SKIP_PRESTAGE=true)');
}
// Step 5: On-chain updates
console.log('');
console.log('-'.repeat(70));
console.log(' PHASE: ON-CHAIN UPDATES');
console.log('-'.repeat(70));
console.log('');
const roots = fs.existsSync(rootsPath)
? loadRoots(rootsPath)
: loadRoots(path.join(outputDir, 'roots.json'));
const provider = new ethers.JsonRpcProvider(rpcUrl);
const signer = new ethers.Wallet(privateKey, provider);
log(`Signer: ${signer.address}`);
let totalUpdates = 0;
for (const config of REGISTRY_CONFIGS) {
const address = getRegistryAddress(config.registryKey, network);
if (!address) {
log(`Registry not configured for network: ${config.name}`);
continue;
}
log(`Updating ${config.name} at ${address}`);
totalUpdates += await updateRegistryRoots(config, address, signer, roots, dryRun);
}
log(`Total updates submitted: ${totalUpdates}`);
// Step 6: Atomic move to production
console.log('');
console.log('-'.repeat(70));
console.log(' PHASE: ATOMIC MOVE');
console.log('-'.repeat(70));
console.log('');
const moveResult = atomicMove(sshHost, stagingPath, productionPath, dryRun);
if (moveResult.success) {
verifyProduction(sshHost, productionPath);
log(`Mismatch window: ${moveResult.durationMs}ms (~${(moveResult.durationMs / 1000).toFixed(1)}s)`);
} else {
console.error('WARNING: On-chain updates succeeded but move failed. Manual move required.');
console.error(` ssh ${sshHost} "mv ${stagingPath}/*.json ${productionPath}/"`);
process.exit(1);
}
console.log('');
console.log('='.repeat(70));
console.log(' OFAC AUTO UPDATE COMPLETE');
console.log('='.repeat(70));
}
main().catch((error) => {
console.error('ERROR: Fatal error:', error);
process.exit(1);
});

View File

@@ -148,6 +148,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
_;
}
// ====================================================
// Constructor
// ====================================================
@@ -305,7 +306,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
/// @notice Updates the name and date of birth OFAC root.
/// @dev Callable only via a proxy and restricted to the contract owner.
/// @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyTEE {
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
}
@@ -313,7 +314,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
/// @notice Updates the name and year of birth OFAC root.
/// @dev Callable only via a proxy and restricted to the contract owner.
/// @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyTEE {
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
}

View File

@@ -157,6 +157,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
_;
}
// ====================================================
// Constructor
// ====================================================
@@ -406,7 +407,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
* @dev Callable only via a proxy and restricted to the contract owner.
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
*/
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyTEE {
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
}
@@ -416,7 +417,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
* @dev Callable only via a proxy and restricted to the contract owner.
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
*/
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyTEE {
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
}

View File

@@ -164,6 +164,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
_;
}
// ====================================================
// Constructor
// ====================================================
@@ -429,7 +430,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
* @dev Callable only via a proxy and restricted to the contract owner.
* @param newPassportNoOfacRoot The new passport number OFAC root value.
*/
function updatePassportNoOfacRoot(uint256 newPassportNoOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
function updatePassportNoOfacRoot(uint256 newPassportNoOfacRoot) external onlyProxy onlyTEE {
_passportNoOfacRoot = newPassportNoOfacRoot;
emit PassportNoOfacRootUpdated(newPassportNoOfacRoot);
}
@@ -439,7 +440,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
* @dev Callable only via a proxy and restricted to the contract owner.
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
*/
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyTEE {
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
}
@@ -449,7 +450,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
* @dev Callable only via a proxy and restricted to the contract owner.
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
*/
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyTEE {
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
}

View File

@@ -21,6 +21,16 @@ abstract contract ImplRoot is UUPSUpgradeable, AccessControlUpgradeable {
/// @notice Routine operations requiring 2/5 multisig consensus
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
/// @notice TEE-operated routines (attested off-chain)
bytes32 public constant TEE_ROLE = keccak256("TEE_ROLE");
modifier onlyTEE() {
if (!hasRole(TEE_ROLE, msg.sender)) {
revert AccessControlUnauthorizedAccount(msg.sender, TEE_ROLE);
}
_;
}
// Reserved storage space to allow for layout changes in the future.
uint256[50] private __gap;
@@ -35,10 +45,22 @@ abstract contract ImplRoot is UUPSUpgradeable, AccessControlUpgradeable {
_grantRole(SECURITY_ROLE, msg.sender);
_grantRole(OPERATIONS_ROLE, msg.sender);
_grantRole(TEE_ROLE, msg.sender);
// Set role admins - SECURITY_ROLE manages all roles
_setRoleAdmin(SECURITY_ROLE, SECURITY_ROLE);
_setRoleAdmin(OPERATIONS_ROLE, SECURITY_ROLE);
_setRoleAdmin(TEE_ROLE, SECURITY_ROLE);
}
/**
* @notice Initializes TEE role administration for existing deployments.
* @dev Call once after upgrade to set admin and optionally grant the role.
*/
function initializeTeeRole(address tee) external reinitializer(3) onlyRole(SECURITY_ROLE) {
require(tee != address(0), "TEE address required");
_setRoleAdmin(TEE_ROLE, SECURITY_ROLE);
_grantRole(TEE_ROLE, tee);
}
/**

View File

@@ -0,0 +1,11 @@
[
{
"First_Name": "TEST",
"Last_Name": "PERSON",
"day": "01",
"month": "jan",
"year": "1980",
"Pass_No": "ABC123",
"Pass_Country": "US"
}
]

View File

@@ -0,0 +1,11 @@
[
{
"First_Name": "TEST",
"Last_Name": "PERSON",
"day": "01",
"month": "jan",
"year": "1980",
"Pass_No": "ABC123",
"Pass_Country": "US"
}
]

View File

@@ -0,0 +1,12 @@
{
"timestamp": "2026-01-09T05:21:27.516Z",
"roots": {
"passport_no_and_nationality": "19093086671255120139524014820177311648176792519166522478620636154509728158243",
"name_and_dob": "5544951091249200846806871614012676394299437218418939793725351601106909872296",
"name_and_yob": "1314608990645991677552549543922168545898839633191445680307066312363599542242",
"name_and_dob_id_card": "6573444439983659365720448932456048798145631688171435793897416156154787373268",
"name_and_yob_id_card": "6987055835518751645787371857318768906995607783193077520937703046696298194030",
"aadhaar_name_and_dob": "8317919717615673742680048915614906976914861173746663189741717324783017749380",
"aadhaar_name_and_yob": "13879509981770598190092727230354697256949761324818440181339029157555278641216"
}
}

View File

@@ -0,0 +1 @@
"{\n \"root\": [\n \"5544951091249200846806871614012676394299437218418939793725351601106909872296\"\n ],\n \"5544951091249200846806871614012676394299437218418939793725351601106909872296\": [\n \"1077031007873547785\",\n \"1\",\n \"1\"\n ]\n}"

View File

@@ -0,0 +1 @@
"{\n \"root\": [\n \"8317919717615673742680048915614906976914861173746663189741717324783017749380\"\n ],\n \"21738374741915498099388398664106567988787243035569819211569773230971285233685\": [\n \"5679042124934938444\",\n \"1\",\n \"1\"\n ],\n \"17688522252779207538596282116222828472134789533100371398991620091447468692838\": [\n \"464063102694161803\",\n \"1\",\n \"1\"\n ],\n \"8317919717615673742680048915614906976914861173746663189741717324783017749380\": [\n \"21738374741915498099388398664106567988787243035569819211569773230971285233685\",\n \"17688522252779207538596282116222828472134789533100371398991620091447468692838\"\n ]\n}"

View File

@@ -0,0 +1 @@
"{\n \"root\": [\n \"6573444439983659365720448932456048798145631688171435793897416156154787373268\"\n ],\n \"6573444439983659365720448932456048798145631688171435793897416156154787373268\": [\n \"4607095122680900159\",\n \"1\",\n \"1\"\n ]\n}"

View File

@@ -0,0 +1 @@
"{\n \"root\": [\n \"1314608990645991677552549543922168545898839633191445680307066312363599542242\"\n ],\n \"1314608990645991677552549543922168545898839633191445680307066312363599542242\": [\n \"12461007423780980778\",\n \"1\",\n \"1\"\n ]\n}"

View File

@@ -0,0 +1 @@
"{\n \"root\": [\n \"13879509981770598190092727230354697256949761324818440181339029157555278641216\"\n ],\n \"19273301844675294683265392003043449732265389717311171700469302150079093649597\": [\n \"1570500630637333439\",\n \"1\",\n \"1\"\n ],\n \"8584942034696929786561540592309592954167387838313827136318214772859157338743\": [\n \"15598640882554133241\",\n \"1\",\n \"1\"\n ],\n \"1113986819524813723721246963763456660382877693197274811581537794979870962711\": [\n \"8584942034696929786561540592309592954167387838313827136318214772859157338743\",\n \"19273301844675294683265392003043449732265389717311171700469302150079093649597\"\n ],\n \"13879509981770598190092727230354697256949761324818440181339029157555278641216\": [\n \"0\",\n \"1113986819524813723721246963763456660382877693197274811581537794979870962711\"\n ]\n}"

View File

@@ -0,0 +1 @@
"{\n \"root\": [\n \"6987055835518751645787371857318768906995607783193077520937703046696298194030\"\n ],\n \"6987055835518751645787371857318768906995607783193077520937703046696298194030\": [\n \"10431265238245082537\",\n \"1\",\n \"1\"\n ]\n}"

View File

@@ -0,0 +1 @@
"{\n \"root\": [\n \"19093086671255120139524014820177311648176792519166522478620636154509728158243\"\n ],\n \"19093086671255120139524014820177311648176792519166522478620636154509728158243\": [\n \"791301478567090101\",\n \"1\",\n \"1\"\n ]\n}"

View File

@@ -0,0 +1,9 @@
{
"passport_no_and_nationality": "19093086671255120139524014820177311648176792519166522478620636154509728158243",
"name_and_dob": "5544951091249200846806871614012676394299437218418939793725351601106909872296",
"name_and_yob": "1314608990645991677552549543922168545898839633191445680307066312363599542242",
"name_and_dob_id_card": "6573444439983659365720448932456048798145631688171435793897416156154787373268",
"name_and_yob_id_card": "6987055835518751645787371857318768906995607783193077520937703046696298194030",
"aadhaar_name_and_dob": "8317919717615673742680048915614906976914861173746663189741717324783017749380",
"aadhaar_name_and_yob": "13879509981770598190092727230354697256949761324818440181339029157555278641216"
}

View File

@@ -0,0 +1,15 @@
<sdnList>
<sdnEntry>
<sdnType>Individual</sdnType>
<firstName>Test</firstName>
<lastName>Person</lastName>
<dateOfBirthItem>
<dateOfBirth>1980-01-01</dateOfBirth>
</dateOfBirthItem>
<id>
<idType>Passport</idType>
<idNumber>ABC123</idNumber>
<idCountry>US</idCountry>
</id>
</sdnEntry>
</sdnList>