Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions __tests__/generator/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ describe('generateCompact', () => {

it('declares the commitmentsToIds ledger', () => {
const result = generateCompact(minimalScript);
expect(result).toContain('export ledger commitmentsToIds');
expect(result).toContain('ledger commitmentsToIds');
});

it('declares the idsToCommitments ledger', () => {
const result = generateCompact(minimalScript);
expect(result).toContain('export ledger idsToCommitments');
expect(result).toContain('ledger idsToCommitments');
});

it('declares the localSecret witness', () => {
Expand Down
110 changes: 110 additions & 0 deletions __tests__/generator/verify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ const cmtHexSet = new Set(
cmtLeaves.flatMap((l) => l.hashes).map((h) => Buffer.from(h).toString('hex'))
);
const matchingPairs = secretPairs.filter((p: any) => cmtHexSet.has(p.commitment));
const isAtLeast = input.type === 'atLeast';
const atLeastRequired = isAtLeast ? input.required : 0;
const isFlatAtLeast =
isAtLeast &&
(input.scripts as NativeScriptSchema[]).every(
(c) => c.type === 'cmt' || c.type === 'after' || c.type === 'before'
);

function hasTimeLocks(script: NativeScriptSchema): boolean {
if (script.type === 'after' || script.type === 'before') return true;
Expand Down Expand Up @@ -136,4 +143,107 @@ describe('Verify circuit', () => {
}
}
);

it.runIf(isFlatAtLeast && matchingPairs.length >= atLeastRequired)(
'atLeast: passes when exactly required number of commitments are committed',
() => {
const sim = new WardenSimulator();
sim.init();
for (const { secret, randomness } of matchingPairs.slice(0, atLeastRequired)) {
sim.commitWith(
new Uint8Array(Buffer.from(secret, 'hex')),
new Uint8Array(Buffer.from(randomness, 'hex'))
);
}
expect(() => sim.verify()).not.toThrow();
}
);

it.runIf(isFlatAtLeast && matchingPairs.length > atLeastRequired)(
'atLeast: passes when more than required number are committed',
() => {
const sim = new WardenSimulator();
sim.init();
for (const { secret, randomness } of matchingPairs) {
sim.commitWith(
new Uint8Array(Buffer.from(secret, 'hex')),
new Uint8Array(Buffer.from(randomness, 'hex'))
);
}
expect(() => sim.verify()).not.toThrow();
}
);

it.runIf(isFlatAtLeast && atLeastRequired > 1 && matchingPairs.length >= atLeastRequired)(
'atLeast: fails when fewer than required are committed',
() => {
const sim = new WardenSimulator();
sim.init();
for (const { secret, randomness } of matchingPairs.slice(0, atLeastRequired - 1)) {
sim.commitWith(
new Uint8Array(Buffer.from(secret, 'hex')),
new Uint8Array(Buffer.from(randomness, 'hex'))
);
}
expect(() => sim.verify()).toThrow();
}
);

it.runIf(isAtLeast && hasCmt)('atLeast: fails when no commitments are committed', () => {
const sim = new WardenSimulator();
sim.init();
expect(() => sim.verify()).toThrow('Commitments or time-lock conditions not satisfied');
});

it.runIf(isAtLeast)('atLeast: fails on uninitialized contract', () => {
const sim = new WardenSimulator();
expect(() => sim.verify()).toThrow();
});

it.runIf(isFlatAtLeast && matchingPairs.length >= atLeastRequired)(
'atLeast: cleans up idsToCommitments after successful verify',
() => {
const sim = new WardenSimulator();
sim.init();
for (const { secret, randomness } of matchingPairs.slice(0, atLeastRequired)) {
sim.commitWith(
new Uint8Array(Buffer.from(secret, 'hex')),
new Uint8Array(Buffer.from(randomness, 'hex'))
);
}

const paths = [...new Set(cmtLeaves.map((l) => l.path))];
for (const p of paths) {
expect(
(sim.getLedger() as any).idsToCommitments.lookup(Buffer.from(p.padEnd(8))).isEmpty()
).toBe(false);
}

expect(() => sim.verify()).not.toThrow();

for (const p of paths) {
expect(
(sim.getLedger() as any).idsToCommitments.lookup(Buffer.from(p.padEnd(8))).isEmpty()
).toBe(true);
}
}
);

it.runIf(isAtLeast && hasTimeLocks(input))(
'atLeast: passes when time-lock conditions are met',
() => {
const sim = new WardenSimulator();
if (hasCmt) {
sim.init();
for (const { secret, randomness } of matchingPairs) {
sim.commitWith(
new Uint8Array(Buffer.from(secret, 'hex')),
new Uint8Array(Buffer.from(randomness, 'hex'))
);
}
}
sim.setBlockTime(400);
expect(() => sim.verify()).not.toThrow();
}
);
});
26 changes: 26 additions & 0 deletions examples/inputs/atLeast-10-of-20.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"type": "atLeast",
"required": 10,
"scripts": [
{ "type": "cmt", "hash": "21f314e3679165f234fe8a986f60d2a0308e4b846415fe68f4bf9d3d194ea74c" },
{ "type": "cmt", "hash": "9517dcb986da18099688e88b6fe1fce568fd0eeb7ddbba86a8263dc489f85783" },
{ "type": "cmt", "hash": "da6d42c11f5ca536f413497d8e8ebd3e64d78bdd80e21f5879537d7ea94b1583" },
{ "type": "cmt", "hash": "2e9d019799b4d93bf1cc68c8119c154cc4e7f814743f2201b6b7f8a8897b4fe1" },
{ "type": "cmt", "hash": "725bb974e724df4497f9ec262f5bf3859ac6a81949a7e29a6faeb2c72aad8a86" },
{ "type": "cmt", "hash": "ba660000921e9e4f1d6e178440fc187e28a4eefc7a2a20e90bb75c3d1fa66252" },
{ "type": "cmt", "hash": "78d6b87a286cea8de00926528df0fb9dd2d0e8de86a4838b303a82ecf19e6a91" },
{ "type": "cmt", "hash": "25b75103abd3122985dc69e586aea16ca25a90831b69f15d4a38a7559acbd82a" },
{ "type": "cmt", "hash": "13e29897dcb463d2a6c471694c4643593b7e8b5783631d14fd5ef1e0422ed565" },
{ "type": "cmt", "hash": "c64cc99262310c372db285c5d173a1ccf8d81ee571cf6aaa21a55e6e2e35c352" },
{ "type": "cmt", "hash": "74e084e4af59297a23511ff1d1e4f51665cec78ae26428338a67eb9278d78715" },
{ "type": "cmt", "hash": "23531f33c3b9fb790a40cc2dd31a1bc562a3e214ba843d03e0e613f1f3b07fd9" },
{ "type": "cmt", "hash": "7af0d79f2936593051b77ac6fd8026f2923dbaecf8ae25cfa245db1cf8a3a5ce" },
{ "type": "cmt", "hash": "926587b6cdbf1c50d6a45be911989b14b2ec7cf4be14dacbce89c2bc4ccfcf6c" },
{ "type": "cmt", "hash": "bee8dee9fb2da87ae60b650a4582d6b80eebd41dc62bd31446bee9185f991c8a" },
{ "type": "cmt", "hash": "04c8b7c3ba1b9423c3c90efd9138690f7265d053013f6c17ecd4bb8d4420ea9a" },
{ "type": "cmt", "hash": "12f5821d63b30642f8f75dc9694cc33620a20104c62a886b89ecd29b4c8343af" },
{ "type": "cmt", "hash": "6fdb14f712f93813edf7fece303d60d9239f70b21d594dd791d4c776d31c084d" },
{ "type": "cmt", "hash": "d5e7184b811f179e8b5662511bef2be2f22584870c1f34e8aac90f94dfee52d4" },
{ "type": "cmt", "hash": "8954876f9030dba9110149dfac22b5333dd2b9abb0316dcee20ca0af4dccadd7" }
]
}
102 changes: 102 additions & 0 deletions examples/inputs/atLeast-nested-big.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{
"type": "atLeast",
"required": 2,
"scripts": [
{
"type": "atLeast",
"required": 2,
"scripts": [
{
"type": "cmt",
"hash": "21f314e3679165f234fe8a986f60d2a0308e4b846415fe68f4bf9d3d194ea74c"
},
{
"type": "cmt",
"hash": "9517dcb986da18099688e88b6fe1fce568fd0eeb7ddbba86a8263dc489f85783"
},
{
"type": "cmt",
"hash": "da6d42c11f5ca536f413497d8e8ebd3e64d78bdd80e21f5879537d7ea94b1583"
},
{
"type": "cmt",
"hash": "2e9d019799b4d93bf1cc68c8119c154cc4e7f814743f2201b6b7f8a8897b4fe1"
}
]
},
{
"type": "atLeast",
"required": 3,
"scripts": [
{
"type": "cmt",
"hash": "725bb974e724df4497f9ec262f5bf3859ac6a81949a7e29a6faeb2c72aad8a86"
},
{
"type": "cmt",
"hash": "ba660000921e9e4f1d6e178440fc187e28a4eefc7a2a20e90bb75c3d1fa66252"
},
{
"type": "cmt",
"hash": "78d6b87a286cea8de00926528df0fb9dd2d0e8de86a4838b303a82ecf19e6a91"
},
{
"type": "cmt",
"hash": "25b75103abd3122985dc69e586aea16ca25a90831b69f15d4a38a7559acbd82a"
},
{
"type": "cmt",
"hash": "13e29897dcb463d2a6c471694c4643593b7e8b5783631d14fd5ef1e0422ed565"
}
]
},
{
"type": "atLeast",
"required": 2,
"scripts": [
{
"type": "cmt",
"hash": "c64cc99262310c372db285c5d173a1ccf8d81ee571cf6aaa21a55e6e2e35c352"
},
{
"type": "cmt",
"hash": "74e084e4af59297a23511ff1d1e4f51665cec78ae26428338a67eb9278d78715"
},
{
"type": "cmt",
"hash": "23531f33c3b9fb790a40cc2dd31a1bc562a3e214ba843d03e0e613f1f3b07fd9"
},
{
"type": "cmt",
"hash": "7af0d79f2936593051b77ac6fd8026f2923dbaecf8ae25cfa245db1cf8a3a5ce"
}
]
},
{
"type": "atLeast",
"required": 3,
"scripts": [
{
"type": "cmt",
"hash": "926587b6cdbf1c50d6a45be911989b14b2ec7cf4be14dacbce89c2bc4ccfcf6c"
},
{
"type": "cmt",
"hash": "bee8dee9fb2da87ae60b650a4582d6b80eebd41dc62bd31446bee9185f991c8a"
},
{
"type": "cmt",
"hash": "04c8b7c3ba1b9423c3c90efd9138690f7265d053013f6c17ecd4bb8d4420ea9a"
},
{
"type": "cmt",
"hash": "12f5821d63b30642f8f75dc9694cc33620a20104c62a886b89ecd29b4c8343af"
},
{
"type": "cmt",
"hash": "6fdb14f712f93813edf7fece303d60d9239f70b21d594dd791d4c776d31c084d"
}
]
}
]
}
6 changes: 3 additions & 3 deletions src/generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ export function generateCompact(
* @key commitment hash
* @value set of IDs
*/
export ledger commitmentsToIds: Map<Bytes<32>, Set<Bytes<8>>>;
${testMode ? 'export ' : ''}ledger commitmentsToIds: Map<Bytes<32>, Set<Bytes<8>>>;

/**
* @description Store commitments.
* @key hash id of the set
* @value set of commitments
*/
export ledger idsToCommitments: Map<Bytes<8>, Set<Bytes<32>>>;
${testMode ? 'export ' : ''}ledger idsToCommitments: Map<Bytes<8>, Set<Bytes<32>>>;
`
: '';

Expand All @@ -80,7 +80,7 @@ export function generateCompact(
`
: '';

const getCommitmentCircuit = testMode
const getCommitmentCircuit = hasCmtLeaves
? `
/**
* @description Generates the commitment that will be added into the idsToCommitments ledger.
Expand Down
66 changes: 40 additions & 26 deletions src/generator/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,6 @@ import type { NativeScriptSchema } from '../index.js';
import { formatBytes, collectUniquePaths } from './utils.js';
import type { CmtLeaf } from './utils.js';

/**
* Generates all k-sized combinations from an array.
*
* @param arr - Source elements
* @param k - Size of each combination
* @returns Array of combinations
*/
function combinations<T>(arr: T[], k: number): T[][] {
if (k === 0) return [[]];
if (arr.length < k) return [];
const [first, ...rest] = arr;
return [...combinations(rest, k - 1).map((c) => [first, ...c]), ...combinations(rest, k)];
}

/**
* Result of recursively generating a Compact expression from a script subtree.
*
Expand All @@ -31,7 +17,7 @@ type GenResult = {
* Recursively generates Compact expressions for a native script subtree.
*
* Walks the tree and emits constraint expressions for commitment lookups,
* time-lock checks, and combinators (`any`/`all`/`atLeast`).
* time-lock checks, and combinators (`any`, `all`, `atLeast`).
*
* @param script - Script subtree to generate expressions for
* @param path - Dot-separated tree path identifying this node's position
Expand All @@ -57,8 +43,14 @@ function genExpr(script: NativeScriptSchema, path: string): GenResult {
lines: [`const id_${path} = blockTimeLt(${script.block});`],
};
}
case 'any':
case 'all':
/**
* Generates a linear counter sum for atLeast N-of-M conditions.
*
* Each child expression is converted to a boolean via ternary (`? 1 : 0`),
* summed with `+`, and compared against the required threshold.
* This is O(n) in the number of children, avoiding the combinatorial
* explosion of generating all k-combinations.
*/
case 'atLeast': {
const children: GenResult[] = [];
const inlineExprs: string[] = [];
Expand Down Expand Up @@ -86,18 +78,40 @@ function genExpr(script: NativeScriptSchema, path: string): GenResult {
const parts = [...inlineExprs, ...children.map((c) => c.expr)];
if (parts.length === 0) return { expr: '', lines };

let combined: string;
const sum = parts.map((p) => `(${p} ? 1 : 0)`).join(' + ');
lines.push(`const id_${path} = (${sum}) >= ${script.required};`);
return { expr: `id_${path}`, lines };
}
case 'any':
case 'all': {
const children: GenResult[] = [];
const inlineExprs: string[] = [];
const lines: string[] = [];

if (script.type === 'atLeast') {
combined = combinations(parts, script.required)
.map((c) => `(${c.join(' && ')})`)
.join(' || ');
} else {
const op = script.type === 'all' ? ' && ' : ' || ';
combined = parts.join(op);
for (let i = 0; i < script.scripts.length; i++) {
const child = script.scripts[i];
if (child.type === 'cmt') {
const hash = formatBytes(child.hash);
const id = formatBytes(path.padEnd(8));
inlineExprs.push(`idsToCommitments.lookup(${id}).member(${hash})`);
} else if (child.type === 'after') {
inlineExprs.push(`blockTimeGte(${child.block})`);
} else if (child.type === 'before') {
inlineExprs.push(`blockTimeLt(${child.block})`);
} else if (child.type === 'any' || child.type === 'all' || child.type === 'atLeast') {
const childPath = path ? `${path}_${i}` : `${i}`;
const childResult = genExpr(child, childPath);
if (childResult.expr === '') continue;
children.push(childResult);
lines.push(...childResult.lines);
}
}

lines.push(`const id_${path} = ${combined};`);
const parts = [...inlineExprs, ...children.map((c) => c.expr)];
if (parts.length === 0) return { expr: '', lines };

const op = script.type === 'all' ? ' && ' : ' || ';
lines.push(`const id_${path} = ${parts.join(op)};`);
return { expr: `id_${path}`, lines };
}
}
Expand Down
Loading