-
Notifications
You must be signed in to change notification settings - Fork 292
/
Copy pathserialize.ts
319 lines (301 loc) · 10.5 KB
/
serialize.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
import { toBigIntBE, toBufferBE } from '../bigint-buffer/index.js';
import { Fr } from '../fields/fields.js';
import { numToUInt32BE } from './free_funcs.js';
/**
* For serializing an array of fixed length buffers.
* @param arr - Array of bufferable.
* @param prefixLength - The length of the prefix (denominated in bytes).
* @returns The serialized buffers.
*/
export function serializeArrayOfBufferableToVector(objs: Bufferable[], prefixLength = 4): Buffer {
const arr = serializeToBufferArray(objs);
let lengthBuf: Buffer;
if (prefixLength === 1) {
lengthBuf = Buffer.alloc(1);
lengthBuf.writeUInt8(arr.length, 0);
} else if (prefixLength === 4) {
lengthBuf = Buffer.alloc(4);
lengthBuf.writeUInt32BE(arr.length, 0);
} else {
throw new Error(`Unsupported prefix length. Got ${prefixLength}, expected 1 or 4`);
}
return Buffer.concat([lengthBuf, ...arr]);
}
/**
* Helper function for deserializeArrayFromVector.
*/
type DeserializeFn<T> = (
buf: Buffer,
offset: number,
) => {
/**
* The deserialized type.
*/
elem: T;
/**
* How many bytes to advance by.
*/
adv: number;
};
/**
* Deserializes an array from a vector on an element-by-element basis.
* @param deserialize - A function used to deserialize each element of the vector.
* @param vector - The vector to deserialize.
* @param offset - The position in the vector to start deserializing from.
* @returns Deserialized array and how many bytes we advanced by.
*/
export function deserializeArrayFromVector<T>(
deserialize: DeserializeFn<T>,
vector: Buffer,
offset = 0,
): {
/**
* The deserialized array.
*/
elem: T[];
/**
* How many bytes we advanced by.
*/
adv: number;
} {
let pos = offset;
const size = vector.readUInt32BE(pos);
pos += 4;
const arr = new Array<T>(size);
for (let i = 0; i < size; ++i) {
const { elem, adv } = deserialize(vector, pos);
pos += adv;
arr[i] = elem;
}
return { elem: arr, adv: pos - offset };
}
/**
* Cast a uint8 array to a number.
* @param array - The uint8 array.
* @returns The number.
*/
export function uint8ArrayToNum(array: Uint8Array): number {
const buf = Buffer.from(array);
return buf.readUint32LE();
}
/**
* Serializes a boolean to a buffer.
* @param value - Value to serialize.
* @returns The serialized boolean.
*/
export function boolToBuffer(value: boolean, bufferSize = 1): Buffer {
const buf = Buffer.alloc(bufferSize);
buf.writeUInt8(value ? 1 : 0, bufferSize - 1);
return buf;
}
/**
* Deserialize the 256-bit number at address `offset`.
* @param buf - The buffer.
* @param offset - The address.
* @returns The deserialized 256-bit field.
*/
export function deserializeField(buf: Buffer, offset = 0) {
const adv = 32;
return { elem: buf.slice(offset, offset + adv), adv };
}
/** A type that can be written to a buffer. */
export type Bufferable =
| boolean
| Buffer
| number
| bigint
| string
| {
/**
* Serialize to a buffer.
*/
toBuffer: () => Buffer;
}
| Bufferable[];
/** A type that can be converted to a Field or a Field array. */
export type Fieldeable =
| Fr
| boolean
| number
| bigint
| {
/** Serialize to a field. */
toField: () => Fr;
}
| {
/** Serialize to an array of fields. */
toFields: () => Fr[];
}
| Fieldeable[];
/**
* Serializes a list of objects contiguously.
* @param objs - Objects to serialize.
* @returns A buffer list with the concatenation of all fields.
*/
export function serializeToBufferArray(...objs: Bufferable[]): Buffer[] {
let ret: Buffer[] = [];
for (const obj of objs) {
if (Array.isArray(obj)) {
ret = [...ret, ...serializeToBufferArray(...obj)];
} else if (Buffer.isBuffer(obj)) {
ret.push(obj);
} else if (typeof obj === 'boolean') {
ret.push(boolToBuffer(obj));
} else if (typeof obj === 'bigint') {
// Throw if bigint does not fit into 32 bytes
if (obj > BigInt('0xffffffffffffffffffffffffffffffff')) {
throw new Error(`BigInt ${obj} does not fit into 32 bytes`);
}
ret.push(serializeBigInt(obj));
} else if (typeof obj === 'number') {
// Note: barretenberg assumes everything is big-endian
ret.push(numToUInt32BE(obj)); // TODO: Are we always passing numbers as UInt32?
} else if (typeof obj === 'string') {
ret.push(numToUInt32BE(obj.length));
ret.push(Buffer.from(obj));
} else if ('toBuffer' in obj) {
ret.push(obj.toBuffer());
} else {
throw new Error(`Cannot serialize input to buffer: ${typeof obj} ${(obj as any).constructor?.name}`);
}
}
return ret;
}
/**
* Serializes a list of objects contiguously.
* @param objs - Objects to serialize.
* @returns An array of fields with the concatenation of all fields.
*/
export function serializeToFields(...objs: Fieldeable[]): Fr[] {
let ret: Fr[] = [];
for (const obj of objs) {
if (Array.isArray(obj)) {
ret = [...ret, ...serializeToFields(...obj)];
} else if (obj instanceof Fr) {
ret.push(obj);
} else if (typeof obj === 'boolean' || typeof obj === 'number' || typeof obj === 'bigint') {
ret.push(new Fr(obj));
} else if ('toFields' in obj) {
ret = [...ret, ...obj.toFields()];
} else {
ret.push(obj.toField());
}
}
return ret;
}
/**
* Serializes a list of objects contiguously.
* @param objs - Objects to serialize.
* @returns A single buffer with the concatenation of all fields.
*/
export function serializeToBuffer(...objs: Bufferable[]): Buffer {
return Buffer.concat(serializeToBufferArray(...objs));
}
/**
* Returns a user-friendly JSON representation of an object, showing buffers as hex strings.
* @param obj - Object to json-stringify.
* @returns A JSON string.
*/
export function toFriendlyJSON(obj: object): string {
return JSON.stringify(
obj,
(key, value) => {
if (value !== null && typeof value === 'object' && value.type === 'Buffer' && Array.isArray(value.data)) {
return '0x' + Buffer.from(value.data).toString('hex');
} else if (typeof value === 'bigint') {
return value.toString();
} else if (
value &&
(
value as {
/**
* Signature of the target serialization function.
*/
toFriendlyJSON: () => string;
}
).toFriendlyJSON
) {
return value.toFriendlyJSON();
} else {
return value;
}
},
2,
);
}
/**
* Serialize a BigInt value into a Buffer of specified width.
* The function converts the input BigInt into its big-endian representation and stores it in a Buffer of the given width.
* If the width is not provided, a default value of 32 bytes will be used. It is important to provide an appropriate width
* to avoid truncation or incorrect serialization of large BigInt values.
*
* @param n - The BigInt value to be serialized.
* @param width - The width (in bytes) of the output Buffer, optional with default value 32.
* @returns A Buffer containing the serialized BigInt value in big-endian format.
*/
export function serializeBigInt(n: bigint, width = 32) {
return toBufferBE(n, width);
}
/**
* Deserialize a big integer from a buffer, given an offset and width.
* Reads the specified number of bytes from the buffer starting at the offset, converts it to a big integer, and returns the deserialized result along with the number of bytes read (advanced).
*
* @param buf - The buffer containing the big integer to be deserialized.
* @param offset - The position in the buffer where the big integer starts. Defaults to 0.
* @param width - The number of bytes to read from the buffer for the big integer. Defaults to 32.
* @returns An object containing the deserialized big integer value ('elem') and the number of bytes advanced ('adv').
*/
export function deserializeBigInt(buf: Buffer, offset = 0, width = 32) {
return { elem: toBigIntBE(buf.subarray(offset, offset + width)), adv: width };
}
/**
* Serializes a Date object into a Buffer containing its timestamp as a big integer value.
* The resulting Buffer has a fixed width of 8 bytes, representing a 64-bit big-endian integer.
* This function is useful for converting date values into a binary format that can be stored or transmitted easily.
*
* @param date - The Date object to be serialized.
* @returns A Buffer containing the serialized timestamp of the input Date object.
*/
export function serializeDate(date: Date) {
return serializeBigInt(BigInt(date.getTime()), 8);
}
/**
* Deserialize a boolean value from a given buffer at the specified offset.
* Reads a single byte at the provided offset in the buffer and returns
* the deserialized boolean value along with the number of bytes read (adv).
*
* @param buf - The buffer containing the serialized boolean value.
* @param offset - The position in the buffer to start reading the boolean value.
* @returns An object containing the deserialized boolean value (elem) and the number of bytes read (adv).
*/
export function deserializeBool(buf: Buffer, offset = 0) {
const adv = 1;
return { elem: buf.readUInt8(offset), adv };
}
/**
* Deserialize a 4-byte unsigned integer from a buffer, starting at the specified offset.
* The deserialization reads 4 bytes from the given buffer and converts it into a number.
* Returns an object containing the deserialized unsigned integer and the number of bytes advanced (4).
*
* @param buf - The buffer containing the serialized unsigned integer.
* @param offset - The starting position in the buffer to deserialize from (default is 0).
* @returns An object with the deserialized unsigned integer as 'elem' and the number of bytes advanced ('adv') as 4.
*/
export function deserializeUInt32(buf: Buffer, offset = 0) {
const adv = 4;
return { elem: buf.readUInt32BE(offset), adv };
}
/**
* Deserialize a signed 32-bit integer from a buffer at the given offset.
* The input 'buf' should be a Buffer containing binary data, and 'offset' should be the position in the buffer
* where the signed 32-bit integer starts. Returns an object with both the deserialized integer (elem) and the
* number of bytes advanced in the buffer (adv, always equal to 4).
*
* @param buf - The buffer containing the binary data.
* @param offset - Optional, the position in the buffer where the signed 32-bit integer starts (default is 0).
* @returns An object with the deserialized integer as 'elem' and the number of bytes advanced as 'adv'.
*/
export function deserializeInt32(buf: Buffer, offset = 0) {
const adv = 4;
return { elem: buf.readInt32BE(offset), adv };
}