diff --git a/packages/tlock/src/commitment.test.ts b/packages/tlock/src/commitment.test.ts index fc11cb3..73d3b4c 100644 --- a/packages/tlock/src/commitment.test.ts +++ b/packages/tlock/src/commitment.test.ts @@ -8,6 +8,7 @@ import { encodeBidPreimage, fromHex, i128ToBeBytes, + isValidHex, toHex, } from "./commitment.js"; @@ -72,3 +73,14 @@ test("fromHex rejects odd-length and non-hex input", () => { assert.throws(() => fromHex("12 3"), /invalid hex characters/); assert.throws(() => fromHex("0x12gg"), /invalid hex characters/); }); + +test("isValidHex agrees with fromHex's accept/reject decisions", () => { + for (const hex of ["abcdef", "ABCDEF", "0xAbCdEf", "0XABCDEF", "", "0x"]) { + assert.equal(isValidHex(hex), true, hex); + assert.doesNotThrow(() => fromHex(hex)); + } + for (const hex of ["abc", "zz", "12 3", "0x12gg"]) { + assert.equal(isValidHex(hex), false, hex); + assert.throws(() => fromHex(hex)); + } +}); diff --git a/packages/tlock/src/commitment.ts b/packages/tlock/src/commitment.ts index 9994867..bc1414c 100644 --- a/packages/tlock/src/commitment.ts +++ b/packages/tlock/src/commitment.ts @@ -77,6 +77,13 @@ export function toHex(bytes: Uint8Array): string { .join(""); } +/// True iff `hex` is a valid hex string (optionally 0x-prefixed) that fromHex +/// would accept: even length, and every remaining character is a hex digit. +export function isValidHex(hex: string): boolean { + const clean = /^0x/i.test(hex) ? hex.slice(2) : hex; + return clean.length % 2 === 0 && /^[0-9a-fA-F]*$/.test(clean); +} + export function fromHex(hex: string): Uint8Array { const clean = /^0x/i.test(hex) ? hex.slice(2) : hex; if (clean.length % 2 !== 0) throw new Error("odd hex length"); diff --git a/packages/tlock/src/index.ts b/packages/tlock/src/index.ts index 0cdd65f..36d1a49 100644 --- a/packages/tlock/src/index.ts +++ b/packages/tlock/src/index.ts @@ -6,6 +6,7 @@ export { beBytesToI128, toHex, fromHex, + isValidHex, VALUE_BYTES, NONCE_BYTES, PREIMAGE_BYTES, diff --git a/packages/tlock/src/seal.test.ts b/packages/tlock/src/seal.test.ts index 66543ca..224c302 100644 --- a/packages/tlock/src/seal.test.ts +++ b/packages/tlock/src/seal.test.ts @@ -99,6 +99,24 @@ test( }, ); +test("sealBid rejects a non-positive Drand round", async () => { + const client = quicknet(); + for (const round of [0, -1, -100]) { + await assert.rejects( + sealBid({ value: 1n, nonce: generateNonce(), round, client }), + /round must be a positive integer/, + ); + } +}); + +test("openBid rejects an empty ciphertext", async () => { + const client = quicknet(); + await assert.rejects( + openBid(new Uint8Array(0), client), + /ciphertext must not be empty/, + ); +}); + test( "a bid sealed to a future round cannot be opened — the seal holds", { timeout: NET_TIMEOUT }, diff --git a/packages/tlock/src/seal.ts b/packages/tlock/src/seal.ts index 0cfdf83..b118434 100644 --- a/packages/tlock/src/seal.ts +++ b/packages/tlock/src/seal.ts @@ -42,6 +42,10 @@ export function generateNonce(): Uint8Array { export async function sealBid(params: SealBidParams): Promise { const { value, nonce, round, client, identity, auditorPublicKey } = params; + if (!Number.isInteger(round) || round <= 0) { + throw new Error(`round must be a positive integer, got ${round}`); + } + const preimage = encodeBidPreimage(value, nonce); const h = commitment(value, nonce); const armored = await timelockEncrypt(round, TlockBuffer.from(preimage), client); @@ -68,6 +72,10 @@ export async function openBid( ciphertext: Uint8Array, client: DrandClient, ): Promise { + if (ciphertext.length === 0) { + throw new Error("ciphertext must not be empty"); + } + const armored = utf8Decode.decode(ciphertext); const plaintext = await timelockDecrypt(armored, client); return decodeBidPreimage(Uint8Array.from(plaintext));