Skip to content
Open
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
12 changes: 12 additions & 0 deletions packages/tlock/src/commitment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
encodeBidPreimage,
fromHex,
i128ToBeBytes,
isValidHex,
toHex,
} from "./commitment.js";

Expand Down Expand Up @@ -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));
}
});
7 changes: 7 additions & 0 deletions packages/tlock/src/commitment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions packages/tlock/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
beBytesToI128,
toHex,
fromHex,
isValidHex,
VALUE_BYTES,
NONCE_BYTES,
PREIMAGE_BYTES,
Expand Down
18 changes: 18 additions & 0 deletions packages/tlock/src/seal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
8 changes: 8 additions & 0 deletions packages/tlock/src/seal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export function generateNonce(): Uint8Array {
export async function sealBid(params: SealBidParams): Promise<SealedBid> {
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);
Expand All @@ -68,6 +72,10 @@ export async function openBid(
ciphertext: Uint8Array,
client: DrandClient,
): Promise<OpenedBid> {
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));
Expand Down