Skip to content

Commit fd3dc46

Browse files
authored
add comments, fix for blindDeserialize (#504)
1 parent 7a7fbcc commit fd3dc46

File tree

6 files changed

+177
-11
lines changed

6 files changed

+177
-11
lines changed

core/base/src/utils/amount.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,11 @@ export function display(amount: Amount, precision?: number): string {
170170
export function whole(amount: Amount): number {
171171
return Number(display(amount));
172172
}
173+
173174
/**
174-
*
175-
* @param amount
176-
* @param decimals
175+
* fmt formats a bigint amount to a string with the given number of decimals
176+
* @param amount bigint amount
177+
* @param decimals number of decimals
177178
*/
178179
export function fmt(amount: bigint, decimals: number): string {
179180
return display(fromBaseUnits(amount, decimals));

core/base/src/utils/encoding.ts

+19
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ const isHexRegex = /^(?:0x)?[0-9a-fA-F]+$/;
1010

1111
/** Base16/Hex encoding and decoding utilities */
1212
export const hex = {
13+
/** check if a string is valid hex */
1314
valid: (input: string) => isHexRegex.test(input),
15+
/** decode a hex string to Uint8Array */
1416
decode: (input: string) => base16.decode(stripPrefix("0x", input).toUpperCase()),
17+
/** encode a string or Uint8Array to hex */
1518
encode: (input: string | Uint8Array, prefix: boolean = false) => {
1619
input = typeof input === "string" ? bytes.encode(input) : input;
1720
return (prefix ? "0x" : "") + base16.encode(input).toLowerCase();
@@ -24,33 +27,44 @@ const isB64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}
2427

2528
/** Base64 encoding and decoding utilities */
2629
export const b64 = {
30+
/** check if a string is valid base64 */
2731
valid: (input: string) => isB64Regex.test(input),
32+
/** decode a base64 string to Uint8Array */
2833
decode: base64.decode,
34+
/** encode a string or Uint8Array to base64 */
2935
encode: (input: string | Uint8Array) =>
3036
base64.encode(typeof input === "string" ? bytes.encode(input) : input),
3137
};
3238

3339
/** Base58 encoding and decoding utilities */
3440
export const b58 = {
41+
/** decode a base58 string to Uint8Array */
3542
decode: base58.decode,
43+
/** encode a string or Uint8Array to base58 */
3644
encode: (input: string | Uint8Array) =>
3745
base58.encode(typeof input === "string" ? bytes.encode(input) : input),
3846
};
3947

4048
/** BigInt encoding and decoding utilities */
4149
export const bignum = {
50+
/** decode a hex string or bytes to a bigint */
4251
decode: (input: string | Uint8Array) => {
4352
if (typeof input !== "string") input = hex.encode(input, true);
4453
if (input === "" || input === "0x") return 0n;
4554
return BigInt(input);
4655
},
56+
/** encode a bigint as a hex string */
4757
encode: (input: bigint, prefix: boolean = false) => bignum.toString(input, prefix),
58+
/** convert a bigint to a hexstring */
4859
toString: (input: bigint, prefix: boolean = false) => {
4960
let str = input.toString(16);
5061
str = str.length % 2 === 1 ? (str = "0" + str) : str;
5162
if (prefix) return "0x" + str;
5263
return str;
5364
},
65+
/** convert a bigint or number to bytes,
66+
* optionally specify length, left padded with 0s to length
67+
*/
5468
toBytes: (input: bigint | number, length?: number) => {
5569
const b = hex.decode(bignum.toString(typeof input === "number" ? BigInt(input) : input));
5670
if (!length) return b;
@@ -60,14 +74,19 @@ export const bignum = {
6074

6175
/** Uint8Array encoding and decoding utilities */
6276
export const bytes = {
77+
/** encode a string to Uint8Array */
6378
encode: (value: string): Uint8Array => new TextEncoder().encode(value),
79+
/** decode a Uint8Array to string */
6480
decode: (value: Uint8Array): string => new TextDecoder().decode(value),
81+
/** compare two Uint8Arrays for equality */
6582
equals: (lhs: Uint8Array, rhs: Uint8Array): boolean =>
6683
lhs.length === rhs.length && lhs.every((v, i) => v === rhs[i]),
84+
/** pad a Uint8Array to a given length, optionally specifying padding direction */
6785
zpad: (arr: Uint8Array, length: number, padStart: boolean = true): Uint8Array =>
6886
padStart
6987
? bytes.concat(new Uint8Array(length - arr.length), arr)
7088
: bytes.concat(arr, new Uint8Array(length - arr.length)),
89+
/** concatenate multiple Uint8Arrays into a single Uint8Array */
7190
concat: (...args: Uint8Array[]): Uint8Array => {
7291
const length = args.reduce((acc, curr) => acc + curr.length, 0);
7392
const result = new Uint8Array(length);

core/base/src/utils/layout/discriminate.ts

+46-7
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,8 @@ function buildAscendingBounds(sortedBounds: readonly (readonly [Bounds, LayoutIn
298298
function generateLayoutDiscriminator(
299299
layouts: readonly Layout[]
300300
): [boolean, (encoded: BytesType) => readonly LayoutIndex[]] {
301+
//for debug output:
302+
// const candStr = (candidate: Bitset) => candidate.toString(2).padStart(layouts.length, '0');
301303

302304
if (layouts.length === 0)
303305
throw new Error("Cannot discriminate empty set of layouts");
@@ -357,6 +359,11 @@ function generateLayoutDiscriminator(
357359
for (let j = 0; j < serialized.length; ++j)
358360
fixedKnownBytes[offset + j]!.push([serialized[j]!, i]);
359361

362+
//debug output:
363+
// console.log("fixedKnownBytes:",
364+
// fixedKnownBytes.map((v, i) => v.length > 0 ? [i, v] : undefined).filter(v => v !== undefined)
365+
// );
366+
360367
let bestBytes = [];
361368
for (const [bytePos, fixedKnownByte] of fixedKnownBytes.entries()) {
362369
//the number of layouts with a given size is an upper bound on the discriminatory power of
@@ -376,7 +383,7 @@ function generateLayoutDiscriminator(
376383
distinctValues.set(byteVal, distinctValues.get(byteVal)! | 1n << BigInt(candidate));
377384
}
378385

379-
let power = count(lwba);
386+
let power = layouts.length - Math.max(count(anyValueLayouts), count(outOfBoundsLayouts));
380387
for (const layoutsWithValue of distinctValues.values()) {
381388
//if we find the byte value associated with this set of layouts, we can eliminate
382389
// all other layouts that don't have this value at this position and all layouts
@@ -385,6 +392,17 @@ function generateLayoutDiscriminator(
385392
power = Math.min(power, curPower);
386393
}
387394

395+
//debug output:
396+
// console.log(
397+
// "bytePos:", bytePos,
398+
// "\npower:", power,
399+
// "\nfixedKnownByte:", fixedKnownByte,
400+
// "\nlwba:", candStr(lwba),
401+
// "\nanyValueLayouts:", candStr(anyValueLayouts),
402+
// "\noutOfBoundsLayouts:", candStr(outOfBoundsLayouts),
403+
// "\ndistinctValues:", new Map([...distinctValues].map(([k, v]) => [k, candStr(v)]))
404+
// );
405+
388406
if (power === 0)
389407
continue;
390408

@@ -521,14 +539,31 @@ function generateLayoutDiscriminator(
521539
throw new Error("Implementation error in layout discrimination algorithm");
522540
};
523541

542+
//debug output:
543+
// console.log("strategies:", JSON.stringify(
544+
// new Map([...strategies].map(([cands, strat]) => [
545+
// candStr(cands),
546+
// typeof strat === "string"
547+
// ? strat
548+
// : [
549+
// strat[0], //bytePos
550+
// candStr(strat[1]), //outOfBoundsLayouts
551+
// new Map([...strat[2]].map(([value, cands]) => [value, candStr(cands)]))
552+
// ]
553+
// ]
554+
// ))
555+
// ));
556+
524557
return [distinguishable, (encoded: BytesType) => {
525558
let candidates = allLayouts;
526559

527-
for (
528-
let strategy = strategies.get(candidates)!;
529-
strategy !== "indistinguishable";
530-
strategy = strategies.get(candidates) ?? findSmallestSuperSetStrategy(candidates)
531-
) {
560+
let strategy = strategies.get(candidates)!;
561+
while (strategy !== "indistinguishable") {
562+
//debug output:
563+
// console.log(
564+
// "applying strategy", strategy,
565+
// "\nfor remaining candidates:", candStr(candidates)
566+
// );
532567
if (strategy === "size")
533568
candidates &= layoutsWithSize(encoded.length);
534569
else {
@@ -546,9 +581,13 @@ function generateLayoutDiscriminator(
546581
}
547582

548583
if (count(candidates) <= 1)
549-
return bitsetToArray(candidates);
584+
break;
585+
586+
strategy = strategies.get(candidates) ?? findSmallestSuperSetStrategy(candidates)
550587
}
551588

589+
//debug output:
590+
// console.log("final candidates", candStr(candidates));
552591
return bitsetToArray(candidates);
553592
}];
554593
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { encoding } from "@wormhole-foundation/sdk-base";
2+
import {
3+
blindDeserializePayload,
4+
deserialize,
5+
deserializePayload,
6+
exhaustiveDeserialize,
7+
} from "../src/index.js";
8+
import "../src/protocols/index.js";
9+
10+
const cases = [
11+
"AQAAAAQNAEI6Ol1ax5h7zmg2cv9pxdznRm1grHS9RL1JLj/I8HJ4Akb1nwXc+zMcOFdAiMASnEwUYpxyBwgJF5QHMS0hXwQBAvTwT11h4/z9dsCEW1pDNaDon0B9aQ6o9/S3zjUnCcuEEgxSwp5hXRjUJ4lds48rpDSfAlMzF/RB3x5+p1NxBDcBA5CMs0VxzRxgAxB+zy9Hn263MEEn5c98Lky8604/RI56C/O/mtoZrvOHtM3ln0yEapeqBcNMvl5L0CpuJ4xtWFsABCVTI/T6ou8+EHP4LC6PBCt/yjEr/QEVJsMx21eFT0y2Z86cBQCw6LmA1ER179Z9WO69FyGPmtHxxHovLi3+sZUABrnxQ0eXTkt7OjiHx/yj3rE6xqzwHqdGQzUBN5SgWCFhTxxfA9Kmt3hlJ6bRnfpR9QMeLxc5tlZCfQGODIQrra8BBwa71PjV61JJaipwAjA/pUsG9fI1qeX5CQknohKbpGUMUb1MZixB8YUMGOsQbBidNh67BwHe0kX7ofh2Y1hYxp0ACEsz46+CAuELDC3Q8jRarQLW20cAWWsRmjjXxSyqOrEDUdSHdJHe1dvzRL1LgD7gsOX7cGuuY/6USFFhPl7cCUAACXm7gAjTn194YTNUdWnZtgNyP+V02tr9a5kcM1xb6D7AcCMW6NbUnjby66L1RycPkyXGoITkGjXvsVxJFxcotzcBCrpiwyBJNx+XM1GEGAmbYYd5Fw6y69L0q9RTk5oNtOmIE2ssvW+/ZMbaLPB3fWf37fYzulNL0YU/u7+JpE+eSuIADSYc1vb3lV+P1rFBgVMlWnnpgpE3UJPq5ZbRHsiP4aLCQUAGkQMl/rtyAyh64fZGA9eRhRRWV76KiwY9CMaWMEYADmhp08yb3cBPvzpDlc1MQ52UKIgHU94cd1dmP27xJbrSJZiwSxL2HiJZdtHZ1EnEp/LBnIrzrTT5w/qwLa7T/0UBD9BzsGb9ekj0TrRLdYdN4PgAzBPV/M8XfFzW2Ex4hhg/BAnd4pH32FN8dGwueGciKr6/z5/ORV8UTCgUDcgTziEBEu7MNJ5xy2J5OWK9ZsKX4UnC29zaTCGIwfx/9bKctOAhIDLn+IIDEwPWQszd2mx4z1IeT0AhHs9Jpuf80uJGuu4BZi0+vA2QAAAADgAAAAAAAAAAAAAAAHlt/2108+JwYLcSVf5Re/sjyT7tAAAAAAACudkBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAX14QAAAAAAAAAAAAAAAAAwCqqObIj/o0KDlxPJ+rZCDx1bMIAAg9A62uZJ8TprduI7DEsB7ndr+gKgbGMF8Hnc05bod8YAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
12+
"AQAAAAQNAOk1OzNZ9DfkOsrjn2jpzz01k8MMqxEeS+NtyMPySJw6OhFAvz76Buk+O/hec/ifJ3m1y0joY1DUeru6sJ+PMF0BAunCoXhMBfluALeB3duNBsgwG12ZfuxOH33FPgWq98qnTh9xFUd6z3DPXb7CJA4oFYm9nYEZtfVTkYs6WEjYCc4BA+ZpLzYmZ46XLxRtHdggukDaK+gHiVyMxoL1BOb0OmCGIwV07bMMk1pG5B0IQwRDf68WDhe3PYQDjuvze9Gkw0gABANQu3VOyPj9tWTbC5jWglfzwgQ98JOau0liKfwF/GYiRV3r+7JPiTXhmmk6J+j7b8WGNidGqrRsomcTD2V1/TsABtmKBYXbmuSds3OWE9ZxubU8cw2h9gv6SHxeTpBWIwHPaiHggfWEznJcUkWByjWZ+nOA+kAAFjhoAlyjsBZkgRMBB7FezvEAnCYCneStFT3AolKVifA8mIBfiKxQpiX9mJkTGatmhMpi8Tp+YXgXYMIk3wCxALg/ZwMu3x76KkIEiIwBCCrfe4hZexgLJMZcReLKvvVfJmcThyEEk9aF/sM5pqaRBuAzG0jKvyN9oHYdq7p9qSzOYuJMbKneJ1ERAOdiWrEACVfoGUdlV4p3etUt32Lr8zUf1NtEuE47UtIUBbMRTZxgNqdP3wZIkrOFoOj5k6+2XTVNIRqTxiEMJohjvVtVms4ADSkByD3p1QM1A8GeGslv+wfsTDhq6cI0MRiGouS277uiXMTZ9MHPZ6VVyIb4gppeZm+A7xzJqp6584oPV+LtqsYAD9yLQKItaBTEWXpPzlExi38ztxCe2Soio+udkOIcbq7mdMx/UWvSpQf04/jhNInBpUEgI4GbSlxrqke9ue61p6cAECaPI5R1x7avvXlWP3hx0V08Jz40kqtf6x7M8ZSb+2gRGQTYdRaY5ZtVzOyq0Nz7+F2XYVZ0tGskcTIABikvWIwBEZJGZDxtl0iB1oyM9H3W0ErJ8Sjdjf7pKbrorQ8JnyqmBWws0zWI2Y4wXdadA5kg8kH6vpvsk8JaJ9vvdsbaEGkBEtVhl/Dg4KMluxuPch+EOwEJ8eY+FFm0jxZeHW65zwX6G3QvqlBsGsFbWeXsWrOXdIhhfSI3uUO3WrU3hb08RCIBZi0/JAABaM8AFczO6yk0j3G90i/+9DoqGcH1teF8XMpUEVKRIBgmcq3lAAAAAAAB50YAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv68CkAAAAAAAAAAAAAAAAisdqUcyVDZgi1ouD/hrZezLNWA0ABAAAAAAAAAAAAAAAAP5/vLIafffsHZUHsJ1mYOEqD6EWAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
13+
];
14+
15+
describe("Blind Deserialize", function () {
16+
it("Should deserialize a blind message", function () {
17+
for (const c of cases) {
18+
const decoded = encoding.b64.decode(c);
19+
const vaa = deserialize("Uint8Array", decoded);
20+
const actual = deserializePayload("TokenBridge:Transfer", vaa.payload);
21+
expect(actual).toBeDefined();
22+
23+
console.time("exhaustive");
24+
const result = exhaustiveDeserialize(vaa.payload);
25+
expect(result).toHaveLength(1);
26+
console.timeEnd("exhaustive");
27+
28+
console.time("blind");
29+
const blind = blindDeserializePayload(vaa.payload);
30+
expect(blind).toHaveLength(1);
31+
console.timeEnd("blind");
32+
}
33+
});
34+
});

core/definitions/src/vaa/create.ts

+7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ type DynamicProperties<PL extends PayloadLiteral> = LayoutToType<
2525
DynamicItemsOfLayout<[...typeof baseLayout, PayloadLiteralToPayloadItemLayout<PL>]>
2626
>;
2727

28+
/**
29+
* Create a VAA from a payload literal and a set of dynamic properties.
30+
* @param payloadLiteral The payload literal to create a VAA for.
31+
* @param vaaData The dynamic properties to include in the VAA.
32+
* @returns A VAA with the given payload literal and dynamic properties.
33+
* @throws If the dynamic properties do not match the payload literal.
34+
*/
2835
export function createVAA<PL extends PayloadLiteral>(
2936
payloadLiteral: PL,
3037
vaaData: DynamicProperties<PL>,

core/definitions/src/vaa/functions.ts

+67-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export function payloadLiteralToPayloadItemLayout<PL extends PayloadLiteral>(pay
3838
} as PayloadLiteralToPayloadItemLayout<PL>;
3939
}
4040

41+
/**
42+
* serialize a VAA to a Uint8Array
43+
* @param vaa the VAA to serialize
44+
* @returns a Uint8Array representation of the VAA
45+
* @throws if the VAA is not valid
46+
*/
4147
export function serialize<PL extends PayloadLiteral>(vaa: VAA<PL>): Uint8Array {
4248
const layout = [
4349
...baseLayout,
@@ -46,6 +52,13 @@ export function serialize<PL extends PayloadLiteral>(vaa: VAA<PL>): Uint8Array {
4652
return serializeLayout(layout, vaa as unknown as LayoutToType<typeof layout>);
4753
}
4854

55+
/**
56+
* serialize a VAA payload to a Uint8Array
57+
*
58+
* @param payloadLiteral The payload literal to use for serialization
59+
* @param payload The dynamic properties to include in the payload
60+
* @returns a Uint8Array representation of the VAA Payload
61+
*/
4962
export function serializePayload<PL extends PayloadLiteral>(
5063
payloadLiteral: PL,
5164
payload: Payload<PL>,
@@ -136,6 +149,15 @@ export function payloadDiscriminator<
136149

137150
type ExtractLiteral<T> = T extends PayloadDiscriminator<infer LL> ? LL : T;
138151

152+
/**
153+
* deserialize a VAA from a Uint8Array
154+
*
155+
* @param payloadDet The payload literal or discriminator to use for deserialization
156+
* @param data the data to deserialize
157+
* @returns a VAA object with the given payload literal or discriminator
158+
* @throws if the data is not a valid VAA
159+
*/
160+
139161
export function deserialize<T extends PayloadLiteral | PayloadDiscriminator>(
140162
payloadDet: T,
141163
data: Byteish,
@@ -188,6 +210,15 @@ type DeserializePayloadReturn<T> = T extends infer PL extends PayloadLiteral
188210
? DeserializedPair<LL>
189211
: never;
190212

213+
/**
214+
* deserialize a payload from a Uint8Array
215+
*
216+
* @param payloadDet the payload literal or discriminator to use for deserialization
217+
* @param data the data to deserialize
218+
* @param offset the offset to start deserializing from
219+
* @returns the deserialized payload
220+
* @throws if the data is not a valid payload
221+
*/
191222
export function deserializePayload<T extends PayloadLiteral | PayloadDiscriminator>(
192223
payloadDet: T,
193224
data: Byteish,
@@ -211,6 +242,42 @@ export function deserializePayload<T extends PayloadLiteral | PayloadDiscriminat
211242
})() as DeserializePayloadReturn<T>;
212243
}
213244

245+
/**
246+
* Attempt to deserialize a payload from a Uint8Array using all registered layouts
247+
*
248+
* @param data the data to deserialize
249+
* @returns an array of all possible deserialized payloads
250+
* @throws if the data is not a valid payload
251+
*/
252+
export const exhaustiveDeserialize = (() => {
253+
const rebuildDiscrimininator = () => {
254+
const layoutLiterals = Array.from(payloadFactory.keys());
255+
const layouts = layoutLiterals.map((l) => payloadFactory.get(l)!);
256+
return [layoutLiterals, layoutDiscriminator(layouts, true)] as const;
257+
};
258+
259+
let layoutLiterals = [] as LayoutLiteral[];
260+
261+
return (data: Byteish): readonly DeserializedPair[] => {
262+
if (payloadFactory.size !== layoutLiterals.length) [layoutLiterals] = rebuildDiscrimininator();
263+
264+
const candidates = layoutLiterals;
265+
return candidates.reduce((acc, literal) => {
266+
try {
267+
acc.push([literal, deserializePayload(literal!, data)] as DeserializedPair);
268+
} catch {}
269+
return acc;
270+
}, [] as DeserializedPair[]);
271+
};
272+
})();
273+
274+
/**
275+
* Blindly deserialize a payload from a Uint8Array
276+
*
277+
* @param data the data to deserialize
278+
* @returns an array of all possible deserialized payloads
279+
* @throws if the data is not a valid payload
280+
*/
214281
export const blindDeserializePayload = (() => {
215282
const rebuildDiscrimininator = () => {
216283
const layoutLiterals = Array.from(payloadFactory.keys());
@@ -226,7 +293,6 @@ export const blindDeserializePayload = (() => {
226293
[layoutLiterals, discriminator] = rebuildDiscrimininator();
227294

228295
if (typeof data === "string") data = encoding.hex.decode(data);
229-
230296
const candidates = discriminator(data).map((c) => layoutLiterals[c]);
231297
return candidates.reduce((acc, literal) => {
232298
try {

0 commit comments

Comments
 (0)