mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
refactor: switch logic to Docker file
still requires SSH for saving to trees.self.xyz, needs refactoring
This commit is contained in:
21
.dockerignore
Normal file
21
.dockerignore
Normal 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
|
||||
45
.github/workflows/ofac-updater-image.yml
vendored
Normal file
45
.github/workflows/ofac-updater-image.yml
vendored
Normal 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
|
||||
20
common/scripts/ofac/Dockerfile
Normal file
20
common/scripts/ofac/Dockerfile
Normal 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"]
|
||||
@@ -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 isn’t 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`.
|
||||
|
||||
4
common/scripts/ofac/entrypoint.sh
Executable file
4
common/scripts/ofac/entrypoint.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
exec yarn tsx common/scripts/ofac/runOfacAutoUpdate.ts "$@"
|
||||
453
common/scripts/ofac/runOfacAutoUpdate.ts
Normal file
453
common/scripts/ofac/runOfacAutoUpdate.ts
Normal 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);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
11
ofac-test-data/ofac/inputs/names.json
Normal file
11
ofac-test-data/ofac/inputs/names.json
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"First_Name": "TEST",
|
||||
"Last_Name": "PERSON",
|
||||
"day": "01",
|
||||
"month": "jan",
|
||||
"year": "1980",
|
||||
"Pass_No": "ABC123",
|
||||
"Pass_Country": "US"
|
||||
}
|
||||
]
|
||||
11
ofac-test-data/ofac/inputs/passports.json
Normal file
11
ofac-test-data/ofac/inputs/passports.json
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"First_Name": "TEST",
|
||||
"Last_Name": "PERSON",
|
||||
"day": "01",
|
||||
"month": "jan",
|
||||
"year": "1980",
|
||||
"Pass_No": "ABC123",
|
||||
"Pass_Country": "US"
|
||||
}
|
||||
]
|
||||
12
ofac-test-data/ofac/outputs/latest-roots.json
Normal file
12
ofac-test-data/ofac/outputs/latest-roots.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
ofac-test-data/ofac/outputs/nameAndDobSMT.json
Normal file
1
ofac-test-data/ofac/outputs/nameAndDobSMT.json
Normal file
@@ -0,0 +1 @@
|
||||
"{\n \"root\": [\n \"5544951091249200846806871614012676394299437218418939793725351601106909872296\"\n ],\n \"5544951091249200846806871614012676394299437218418939793725351601106909872296\": [\n \"1077031007873547785\",\n \"1\",\n \"1\"\n ]\n}"
|
||||
1
ofac-test-data/ofac/outputs/nameAndDobSMT_AADHAAR.json
Normal file
1
ofac-test-data/ofac/outputs/nameAndDobSMT_AADHAAR.json
Normal 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}"
|
||||
1
ofac-test-data/ofac/outputs/nameAndDobSMT_ID.json
Normal file
1
ofac-test-data/ofac/outputs/nameAndDobSMT_ID.json
Normal file
@@ -0,0 +1 @@
|
||||
"{\n \"root\": [\n \"6573444439983659365720448932456048798145631688171435793897416156154787373268\"\n ],\n \"6573444439983659365720448932456048798145631688171435793897416156154787373268\": [\n \"4607095122680900159\",\n \"1\",\n \"1\"\n ]\n}"
|
||||
1
ofac-test-data/ofac/outputs/nameAndYobSMT.json
Normal file
1
ofac-test-data/ofac/outputs/nameAndYobSMT.json
Normal file
@@ -0,0 +1 @@
|
||||
"{\n \"root\": [\n \"1314608990645991677552549543922168545898839633191445680307066312363599542242\"\n ],\n \"1314608990645991677552549543922168545898839633191445680307066312363599542242\": [\n \"12461007423780980778\",\n \"1\",\n \"1\"\n ]\n}"
|
||||
1
ofac-test-data/ofac/outputs/nameAndYobSMT_AADHAAR.json
Normal file
1
ofac-test-data/ofac/outputs/nameAndYobSMT_AADHAAR.json
Normal 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}"
|
||||
1
ofac-test-data/ofac/outputs/nameAndYobSMT_ID.json
Normal file
1
ofac-test-data/ofac/outputs/nameAndYobSMT_ID.json
Normal file
@@ -0,0 +1 @@
|
||||
"{\n \"root\": [\n \"6987055835518751645787371857318768906995607783193077520937703046696298194030\"\n ],\n \"6987055835518751645787371857318768906995607783193077520937703046696298194030\": [\n \"10431265238245082537\",\n \"1\",\n \"1\"\n ]\n}"
|
||||
@@ -0,0 +1 @@
|
||||
"{\n \"root\": [\n \"19093086671255120139524014820177311648176792519166522478620636154509728158243\"\n ],\n \"19093086671255120139524014820177311648176792519166522478620636154509728158243\": [\n \"791301478567090101\",\n \"1\",\n \"1\"\n ]\n}"
|
||||
9
ofac-test-data/ofac/outputs/roots.json
Normal file
9
ofac-test-data/ofac/outputs/roots.json
Normal 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"
|
||||
}
|
||||
15
ofac-test-data/ofac/raw/sdn-latest.xml
Normal file
15
ofac-test-data/ofac/raw/sdn-latest.xml
Normal 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>
|
||||
Reference in New Issue
Block a user