Custodial markets
This commit is contained in:
+14
-3
@@ -6,11 +6,14 @@ import { DatabaseSync } from 'node:sqlite';
|
||||
import { z } from 'zod';
|
||||
import { NETWORK, PROFILE } from '../shared/network.ts';
|
||||
import { CHAIN_ID } from './testnet/consensus.ts';
|
||||
import { CUSTODY_POLICY } from '../shared/custody.ts';
|
||||
|
||||
const files=['ledger.sqlite','market.sqlite'] as const;
|
||||
const files=['ledger.sqlite','market.sqlite','custody-sandbox.sqlite'] as const;
|
||||
const manifestSchema=z.object({format:z.literal('wikideck-stopped-backup'),version:z.literal(1),
|
||||
network:z.literal(NETWORK),chainId:z.literal(CHAIN_ID),profile:z.literal(PROFILE),createdAt:z.string().datetime(),
|
||||
files:z.array(z.object({name:z.enum(files),bytes:z.number().int().positive().max(64*1024**3),sha256:z.string().regex(/^[a-f0-9]{64}$/)}).strict()).length(2)}).strict();
|
||||
files:z.array(z.object({name:z.enum(files),bytes:z.number().int().positive().max(64*1024**3),sha256:z.string().regex(/^[a-f0-9]{64}$/)}).strict()).min(2).max(3)}).strict()
|
||||
.refine(m => new Set(m.files.map(f=>f.name)).size === m.files.length &&
|
||||
m.files.some(f=>f.name==='ledger.sqlite') && m.files.some(f=>f.name==='market.sqlite'), 'Missing or duplicate backup database');
|
||||
async function regular(path:string){const s=await lstat(path);if(!s.isFile()||s.isSymbolicLink())throw new Error('Expected regular non-symlink database file');return s;}
|
||||
async function digest(path:string){const hash=createHash('sha256');for await(const chunk of createReadStream(path))hash.update(chunk);return hash.digest('hex');}
|
||||
async function flush(path:string){const handle=await open(path,'r+');try{await handle.sync();}finally{await handle.close();}}
|
||||
@@ -26,6 +29,11 @@ function inspect(file:string,name:typeof files[number]) {
|
||||
if(name==='ledger.sqlite'){
|
||||
const meta=(key:string)=>(db.prepare('SELECT value FROM metadata WHERE key=?').get(key) as {value:string}|undefined)?.value;
|
||||
if(meta('schema_version')!=='1'||meta('network')!==NETWORK||meta('chain_id')!==CHAIN_ID)throw new Error('Ledger network/schema mismatch');
|
||||
}else if(name==='custody-sandbox.sqlite'){
|
||||
const row=db.prepare("SELECT value FROM custody_meta WHERE key='identity'").get() as {value:string}|undefined;
|
||||
const identity=row?JSON.parse(row.value):undefined;
|
||||
if(identity?.version!==1||identity.network!==NETWORK||identity.chainId!==CHAIN_ID||identity.mode!=='sandbox'||
|
||||
identity.policy!==CUSTODY_POLICY||!z.string().uuid().safeParse(identity.id).success)throw new Error('Custody identity mismatch');
|
||||
}else{
|
||||
const row=db.prepare("SELECT value FROM market_meta WHERE key='identity'").get() as {value:string}|undefined;
|
||||
const identity=row?JSON.parse(row.value):undefined;
|
||||
@@ -50,6 +58,9 @@ export async function createBackup(dataDir:string,destination:string){
|
||||
const entries=[];
|
||||
for(const name of files){
|
||||
const src=join(source,PROFILE,name),target=join(dest,name);
|
||||
if(name==='custody-sandbox.sqlite'){
|
||||
try{await lstat(src);}catch(e){if((e as NodeJS.ErrnoException).code==='ENOENT')continue;throw e;}
|
||||
}
|
||||
await regular(src);inspect(src,name);
|
||||
const db=new DatabaseSync(src,{readOnly:true});
|
||||
try{db.prepare('VACUUM INTO ?').run(target);}finally{db.close();}
|
||||
@@ -71,7 +82,7 @@ export async function verifyBackup(directory:string){
|
||||
const root=await realpath(directory),manifestFile=join(root,'manifest.json');
|
||||
if((await regular(manifestFile)).size>16384)throw new Error('Backup manifest too large');
|
||||
const manifest=manifestSchema.parse(JSON.parse(await readFile(manifestFile,'utf8')));
|
||||
if(new Set(manifest.files.map(f=>f.name)).size!==2)throw new Error('Duplicate backup database');
|
||||
if(new Set(manifest.files.map(f=>f.name)).size!==manifest.files.length)throw new Error('Duplicate backup database');
|
||||
for(const file of manifest.files){
|
||||
const path=join(root,file.name),info=await regular(path);
|
||||
if(info.size!==file.bytes||await digest(path)!==file.sha256)throw new Error('Backup database hash/size mismatch');
|
||||
|
||||
Reference in New Issue
Block a user