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
22 changes: 18 additions & 4 deletions .github/workflows/publish-worker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ jobs:

const PRIVATE_PROGRAM_ROLE = "private-program-consumer";
const PRIVATE_PROGRAM_METADATA_PATH = "private-program/build-metadata.json";
const PRIVATE_PROGRAM_ABI_PATH = "private-program/abi-v2.js";
const PRIVATE_PROGRAM_CONSUMER_PATH = "private-program/consumer.js";
const PRIVATE_PROGRAM_WASM_PATH =
"private-program/labcolors_private_program.wasm";
Expand Down Expand Up @@ -357,6 +358,7 @@ jobs:
"evidence/wcag22-srgb8-q55-proof-v1.json",
"evidence/point-support-reference-surplus-q55-bps-proof-v1.json",
PRIVATE_PROGRAM_CONSUMER_PATH,
PRIVATE_PROGRAM_ABI_PATH,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
PRIVATE_PROGRAM_WASM_PATH,
PRIVATE_PROGRAM_METADATA_PATH,
"pkg/labcolors.js",
Expand Down Expand Up @@ -395,11 +397,11 @@ jobs:
}

if (
EXPECTED_PACKAGE_FILES.length !== 28 ||
new Set(EXPECTED_PACKAGE_FILES).size !== 28 ||
EXPECTED_PACKAGE_FILES.length !== 29 ||
new Set(EXPECTED_PACKAGE_FILES).size !== 29 ||
EXPECTED_PACKAGE_FILES.some((path) => path.includes("*")) ||
EXPECTED_TAR_INVENTORY.length !== 30 ||
new Set(EXPECTED_TAR_INVENTORY).size !== 30
EXPECTED_TAR_INVENTORY.length !== 31 ||
new Set(EXPECTED_TAR_INVENTORY).size !== 31
) {
fail("worker package inventory allowlist is not canonical");
}
Expand Down Expand Up @@ -1372,6 +1374,18 @@ jobs:
) {
fail("packed package files declaration differs from the admitted inventory");
}
const packedPrivateAbi = packedMember(
tarball,
tarMembers,
PRIVATE_PROGRAM_ABI_PATH,
"packed private Program ABI",
);
const taggedPrivateAbi = readFileSync(
join(workspace, "packages/colors", PRIVATE_PROGRAM_ABI_PATH),
);
if (!packedPrivateAbi.equals(taggedPrivateAbi)) {
fail("packed private Program ABI differs from the exact tag source");
}
if (JSON.stringify(packedPackage.exports) !== JSON.stringify(EXPECTED_PACKAGE_EXPORTS)) {
fail("packed package exports differ from the admitted public surface");
}
Expand Down
66 changes: 49 additions & 17 deletions fixtures/private-program-browser/proof.mjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
const OUTPUT_BINDING = "--lab-private-program-output";
import {
CHANGED_UPDATE_HEX,
exactWireBytes,
EXPECTED_CONTENT_IDENTITY,
EXPECTED_OUTPUT,
EXPECTED_SINK_OUTPUT,
OUTPUT_BINDING,
receiptFingerprint,
REQUEST_HEX,
UPDATE_HEX,
} from "./vectors.mjs";

const EXPECTED_REQUEST_LENGTH = 70;
const EXPECTED_UPDATE_LENGTH = 40;
const EXPECTED_OUTPUT = 17;
const EXPECTED_SINK_OUTPUT = 501;
const EXPECTED_PAINT_SOURCE = Object.freeze([64, 64, 64]);
const EXPECTED_PAINT_OPACITY_BITS_HEX = "3fe0000000000000";
const EXPECTED_COMPUTED_CSS = "rgba(64, 64, 64, 0.5)";
const WORKER_RESPONSE_TIMEOUT_MS = 10_000;

const REQUEST_HEX = "4c43465102004600404040000000000000e03f00000000000050409a9999999999c93f01606060f50100001f0000000100000000000000010100000080808000000000000000";
const UPDATE_HEX = "4c434655020028001f00000002000000000000000101020000008080800000000000000000000000";
const CHANGED_UPDATE_HEX =
"4c434655020028001f0000000300000000000000010103000000ffffff0000000000000000000000";
const EXPECTED_CONTENT_IDENTITY =
"4daf4731d823ba228f4189d08c76183dd8a81e8b593ff709828b7329e075a914";

const EXPECTED_CHECKS = Object.freeze([
"installed-physical-private-program",
Expand All @@ -22,6 +26,7 @@ const EXPECTED_CHECKS = Object.freeze([
"exact-certified-receipt",
"explicit-observation-update",
"changed-observation-invalidates-certified-result",
"independent-worker-client-parity",
"dispose",
"post-run-dispose-idempotence",
]);
Expand Down Expand Up @@ -51,14 +56,6 @@ function assertStableLiterals() {
);
}

function exactWireBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let index = 0; index < bytes.length; index += 1) {
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
}
return bytes;
}

async function runProof() {
assertStableLiterals();
const checks = [];
Expand Down Expand Up @@ -100,6 +97,9 @@ async function runProof() {
checks.push("pre-run-dispose-idempotence");

let disposeResult;
let initialFingerprint;
let updatedFingerprint;
let changedFingerprint;
try {
const receipt = await consumer.run(requestBytes);
assert(receipt && typeof receipt === "object", "run returns a receipt object");
Expand Down Expand Up @@ -129,6 +129,7 @@ async function runProof() {
"certified paint opacity bits",
);
equal(receipt.contentIdentity, EXPECTED_CONTENT_IDENTITY, "certified content identity");
initialFingerprint = receiptFingerprint(receipt);
checks.push("exact-certified-receipt");

const updateBytes = exactWireBytes(UPDATE_HEX);
Expand All @@ -138,6 +139,7 @@ async function runProof() {
equal(updated.stream, 31, "updated state retains stream identity");
equal(updated.revision, 2n, "updated state advances revision");
equal(updated.contentIdentity, EXPECTED_CONTENT_IDENTITY, "update reuses compiled identity");
updatedFingerprint = receiptFingerprint(updated);
checks.push("explicit-observation-update");

const changedUpdateBytes = exactWireBytes(CHANGED_UPDATE_HEX);
Expand All @@ -148,13 +150,43 @@ async function runProof() {
equal(changed.revision, 3n, "failed state advances revision");
equal(changed.output, 0, "failed state has no certified output");
equal(changed.contentIdentity, "0".repeat(64), "failed state has no certified identity");
changedFingerprint = receiptFingerprint(changed);
probe.getBoundingClientRect();
equal(
getComputedStyle(probe).backgroundColor,
"rgba(0, 0, 0, 0)",
"changed observation revokes the previous certified output",
);
checks.push("changed-observation-invalidates-certified-result");

const workerResult = await new Promise((resolve, reject) => {
const worker = new Worker("./worker-client.mjs", { type: "module" });
const finish = (action) => {
clearTimeout(timeout);
worker.terminate();
action();
};
const timeout = setTimeout(() => {
finish(() => reject(new Error("worker client response timed out")));
}, WORKER_RESPONSE_TIMEOUT_MS);
worker.addEventListener("message", (event) => {
finish(() => {
if (event.data?.ok === true) resolve(event.data.value);
else reject(new Error(event.data?.error ?? "worker client failed without detail"));
});
}, { once: true });
worker.addEventListener("error", (event) => {
finish(() => reject(event.error ?? new Error(event.message)));
}, { once: true });
worker.addEventListener("messageerror", () => {
finish(() => reject(new Error("worker client returned an uncloneable message")));
}, { once: true });
worker.postMessage("run");
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
equal(workerResult.initial, initialFingerprint, "worker initial certified outcome matches");
equal(workerResult.updated, updatedFingerprint, "worker updated certified outcome matches");
equal(workerResult.changed, changedFingerprint, "worker failed outcome matches");
checks.push("independent-worker-client-parity");
} finally {
disposeResult = await consumer.dispose();
}
Expand Down
29 changes: 29 additions & 0 deletions fixtures/private-program-browser/vectors.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const OUTPUT_BINDING = "--lab-private-program-output";
export const EXPECTED_OUTPUT = 17;
export const EXPECTED_SINK_OUTPUT = 501;
export const REQUEST_HEX = "4c43465102004600404040000000000000e03f00000000000050409a9999999999c93f01606060f50100001f0000000100000000000000010100000080808000000000000000";
export const UPDATE_HEX = "4c434655020028001f00000002000000000000000101020000008080800000000000000000000000";
export const CHANGED_UPDATE_HEX = "4c434655020028001f0000000300000000000000010103000000ffffff0000000000000000000000";
export const EXPECTED_CONTENT_IDENTITY = "4daf4731d823ba228f4189d08c76183dd8a81e8b593ff709828b7329e075a914";

export function exactWireBytes(hex) {
if (!/^(?:[0-9a-f]{2})+$/u.test(hex)) throw new TypeError("wire vector must be lowercase hex");
const bytes = new Uint8Array(hex.length / 2);
for (let index = 0; index < bytes.length; index += 1) {
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
}
return bytes;
}

export function receiptFingerprint(receipt) {
return JSON.stringify({
output: receipt.output,
sinkOutput: receipt.sinkOutput,
state: receipt.state,
stream: receipt.stream,
revision: receipt.revision.toString(),
paintSource: receipt.paintSource,
paintOpacityBits: receipt.paintOpacityBits.toString(16).padStart(16, "0"),
contentIdentity: receipt.contentIdentity,
});
}
171 changes: 171 additions & 0 deletions fixtures/private-program-browser/worker-client.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import {
decodeCertifiedReceipt,
DISPOSE_TOKEN_BASE_V1,
EXPORTS_V1,
HOST_CONFIRM_DISPOSED_V1,
HOST_DISPOSE_CONFIRMED_V1,
HOST_INSTALL_SUCCESS_V1,
HOST_INSTALL_V1,
HOST_MODULE_V1,
OPERATION_CONFIRM_EXACT_V1,
OPERATION_REVOKE_ALL_V1,
OPERATION_SET_ALL_V1,
REQUEST_REVISION_OFFSET,
REQUEST_SINK_OUTPUT_OFFSET,
REQUEST_STREAM_OFFSET,
REQUEST_V2_LENGTH,
RESULT_V2_LENGTH,
UPDATE_REVISION_OFFSET,
UPDATE_STREAM_OFFSET,
UPDATE_V2_LENGTH,
} from "/installed/private-program/abi-v2.js";
import {
CHANGED_UPDATE_HEX,
exactWireBytes,
receiptFingerprint,
REQUEST_HEX,
UPDATE_HEX,
} from "./vectors.mjs";

const WASM_URL = "/installed/private-program/labcolors_private_program.wasm";

function u32(value) {
return value >>> 0;
}

function checkedView(memory, pointer, length) {
const offset = u32(pointer);
if (offset > memory.buffer.byteLength || length > memory.buffer.byteLength - offset) {
throw new RangeError("private Program worker memory range is invalid");
}
return new Uint8Array(memory.buffer, offset, length);
}

async function runLifecycle() {
let exports;
let installedOutput = 0;
let installedSinkOutput = 0;
let expectedSinkOutput = 0;
let sequence = 0n;
let generation = 0;
let disposeConfirmed = false;

function install(
rawGeneration,
rawOperation,
_revisionLow,
_revisionHigh,
expectedLow,
expectedHigh,
desiredLow,
desiredHigh,
rawOutput,
rawSinkOutput,
) {
generation = u32(rawGeneration);
const operation = u32(rawOperation);
const expected = BigInt(u32(expectedLow)) | (BigInt(u32(expectedHigh)) << 32n);
const desired = BigInt(u32(desiredLow)) | (BigInt(u32(desiredHigh)) << 32n);
const output = u32(rawOutput);
const sinkOutput = u32(rawSinkOutput);
if (operation === OPERATION_CONFIRM_EXACT_V1) {
return expected === sequence && desired === sequence && output === installedOutput && sinkOutput === installedSinkOutput
? HOST_INSTALL_SUCCESS_V1
: 0;
}
if (expected !== sequence || desired !== sequence + 1n) return 0;
if (operation === OPERATION_SET_ALL_V1) {
if (output === 0 || sinkOutput !== expectedSinkOutput) return 0;
installedOutput = output;
installedSinkOutput = sinkOutput;
} else if (operation === OPERATION_REVOKE_ALL_V1) {
if (output !== 0 || sinkOutput !== 0) return 0;
installedOutput = 0;
installedSinkOutput = 0;
} else {
return 0;
}
sequence = desired;
return HOST_INSTALL_SUCCESS_V1;
}

function confirmDisposed(rawGeneration, rawToken) {
disposeConfirmed = u32(rawGeneration) === generation && u32(rawToken) === generation;
return disposeConfirmed ? HOST_DISPOSE_CONFIRMED_V1 : 0;
}

const response = await fetch(WASM_URL);
if (response.ok !== true) throw new Error(`private Program WASM fetch failed: ${response.status}`);
const source = await response.arrayBuffer();
const instance = await WebAssembly.instantiate(source, {
[HOST_MODULE_V1]: {
[HOST_INSTALL_V1]: install,
[HOST_CONFIRM_DISPOSED_V1]: confirmDisposed,
},
});
exports = instance.instance.exports;
const memory = exports.memory;

function invoke(bytes, pointerName, length, operationName, minimumRevision) {
checkedView(memory, exports[pointerName](), length).set(bytes);
const status = u32(exports[operationName]());
if (status !== 0) throw new Error(`${operationName} failed with status ${status}`);
return decodeCertifiedReceipt({
bytes: new Uint8Array(checkedView(memory, exports[EXPORTS_V1.resultPointer](), RESULT_V2_LENGTH)),
expectedSinkOutput,
installedOutput,
installedSinkOutput,
expectedStream: new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(
operationName === EXPORTS_V1.run ? REQUEST_STREAM_OFFSET : UPDATE_STREAM_OFFSET,
true,
),
minimumRevision,
});
}

const request = exactWireBytes(REQUEST_HEX);
expectedSinkOutput = new DataView(request.buffer).getUint32(REQUEST_SINK_OUTPUT_OFFSET, true);
const initial = invoke(
request,
EXPORTS_V1.requestPointer,
REQUEST_V2_LENGTH,
EXPORTS_V1.run,
new DataView(request.buffer).getBigUint64(REQUEST_REVISION_OFFSET, true),
);
const update = exactWireBytes(UPDATE_HEX);
const updated = invoke(
update,
EXPORTS_V1.updatePointer,
UPDATE_V2_LENGTH,
EXPORTS_V1.update,
new DataView(update.buffer).getBigUint64(UPDATE_REVISION_OFFSET, true),
);
const changedUpdate = exactWireBytes(CHANGED_UPDATE_HEX);
const changed = invoke(
changedUpdate,
EXPORTS_V1.updatePointer,
UPDATE_V2_LENGTH,
EXPORTS_V1.update,
new DataView(changedUpdate.buffer).getBigUint64(UPDATE_REVISION_OFFSET, true),
);

const token = u32(exports[EXPORTS_V1.beginDispose]());
if (token !== generation + DISPOSE_TOKEN_BASE_V1) throw new Error("dispose token mismatch");
if (u32(exports[EXPORTS_V1.commitDispose](token)) !== 0 || !disposeConfirmed) {
throw new Error("dispose confirmation mismatch");
}
return Object.freeze({
initial: receiptFingerprint(initial),
updated: receiptFingerprint(updated),
changed: receiptFingerprint(changed),
});
}

self.addEventListener("message", async (event) => {
if (event.data !== "run") return;
try {
self.postMessage({ ok: true, value: await runLifecycle() });
} catch (error) {
self.postMessage({ ok: false, error: error instanceof Error ? error.message : String(error) });
}
});
1 change: 1 addition & 0 deletions packages/colors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"evidence/wcag22-srgb8-q55-proof-v1.json",
"evidence/point-support-reference-surplus-q55-bps-proof-v1.json",
"private-program/consumer.js",
"private-program/abi-v2.js",
"private-program/labcolors_private_program.wasm",
"private-program/build-metadata.json",
"pkg/labcolors.js",
Expand Down
Loading
Loading