ox 0.14.29 → 0.14.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/_cjs/tempo/MultisigConfig.js +16 -26
- package/_cjs/tempo/MultisigConfig.js.map +1 -1
- package/_cjs/tempo/SignatureEnvelope.js +46 -33
- package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/tempo/MultisigConfig.js +39 -79
- package/_esm/tempo/MultisigConfig.js.map +1 -1
- package/_esm/tempo/SignatureEnvelope.js +61 -51
- package/_esm/tempo/SignatureEnvelope.js.map +1 -1
- package/_esm/tempo/index.js +2 -2
- package/_esm/version.js +1 -1
- package/_types/tempo/MultisigConfig.d.ts +37 -81
- package/_types/tempo/MultisigConfig.d.ts.map +1 -1
- package/_types/tempo/SignatureEnvelope.d.ts +32 -30
- package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
- package/_types/tempo/index.d.ts +2 -2
- package/_types/version.d.ts +1 -1
- package/package.json +1 -1
- package/tempo/MultisigConfig.test.ts +54 -37
- package/tempo/MultisigConfig.ts +59 -130
- package/tempo/SignatureEnvelope.test.ts +118 -17
- package/tempo/SignatureEnvelope.ts +77 -72
- package/tempo/e2e.test.ts +243 -12
- package/tempo/index.ts +2 -2
- package/version.ts +1 -1
package/tempo/MultisigConfig.ts
CHANGED
|
@@ -6,7 +6,13 @@ import * as Hex from '../core/Hex.js'
|
|
|
6
6
|
import type { Compute, OneOf } from '../core/internal/types.js'
|
|
7
7
|
|
|
8
8
|
/** Maximum number of owners allowed in a native multisig config. */
|
|
9
|
-
export const maxOwners =
|
|
9
|
+
export const maxOwners = 255
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Maximum number of native multisig signatures in one nested authorization
|
|
13
|
+
* path, including the top-level transaction signature.
|
|
14
|
+
*/
|
|
15
|
+
export const maxNestingDepth = 3
|
|
10
16
|
|
|
11
17
|
/** Maximum encoded byte length for one primitive owner approval. */
|
|
12
18
|
export const maxOwnerSignatureBytes = 2049
|
|
@@ -20,20 +26,17 @@ export const zeroSalt = `0x${'00'.repeat(32)}` as const
|
|
|
20
26
|
/** Domain prefix for the native multisig account address derivation. */
|
|
21
27
|
const accountDomain = 'tempo:multisig:account'
|
|
22
28
|
|
|
23
|
-
/** Domain prefix for the native multisig config ID derivation. */
|
|
24
|
-
const configDomain = 'tempo:multisig:config'
|
|
25
|
-
|
|
26
29
|
/** Domain prefix for native multisig owner approvals. */
|
|
27
30
|
const signatureDomain = 'tempo:multisig:signature'
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
|
-
* Native multisig configuration. Determines the
|
|
31
|
-
*
|
|
33
|
+
* Native multisig configuration. Determines the stable multisig account
|
|
34
|
+
* address.
|
|
32
35
|
*/
|
|
33
36
|
export type Config<numberType = number> = Compute<{
|
|
34
37
|
/**
|
|
35
|
-
* Caller-chosen 32-byte salt mixed into the
|
|
36
|
-
* the zero salt (`MultisigConfig.zeroSalt`) when omitted.
|
|
38
|
+
* Caller-chosen 32-byte salt mixed into the derived account address.
|
|
39
|
+
* Defaults to the zero salt (`MultisigConfig.zeroSalt`) when omitted.
|
|
37
40
|
*/
|
|
38
41
|
salt?: Hex.Hex | undefined
|
|
39
42
|
/** Minimum total owner weight required to authorize a transaction. */
|
|
@@ -44,7 +47,7 @@ export type Config<numberType = number> = Compute<{
|
|
|
44
47
|
|
|
45
48
|
/** Native multisig owner entry. */
|
|
46
49
|
export type Owner<numberType = number> = {
|
|
47
|
-
/** Owner address (recovered from the owner's
|
|
50
|
+
/** Owner address (recovered from the owner's approval). */
|
|
48
51
|
owner: Address.Address
|
|
49
52
|
/** Nonzero owner weight. */
|
|
50
53
|
weight: numberType
|
|
@@ -60,9 +63,9 @@ export type Tuple = readonly [
|
|
|
60
63
|
/**
|
|
61
64
|
* Asserts that a native multisig {@link ox#MultisigConfig.Config} is valid.
|
|
62
65
|
*
|
|
63
|
-
* Mirrors the Tempo `
|
|
66
|
+
* Mirrors the Tempo `InitMultisig::validate` rules: owners non-empty and
|
|
64
67
|
* `<= maxOwners`, strictly ascending unique nonzero owner addresses, nonzero
|
|
65
|
-
* owner weights, `threshold >= 1`, total weight `<=
|
|
68
|
+
* owner weights, `threshold >= 1`, total weight `<= 255` (u8 max), and
|
|
66
69
|
* `threshold <= total weight`.
|
|
67
70
|
*
|
|
68
71
|
* @example
|
|
@@ -109,9 +112,9 @@ export function assert<numberType = number>(config: Config<numberType>): void {
|
|
|
109
112
|
totalWeight += Number(owner.weight)
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
if (totalWeight >
|
|
115
|
+
if (totalWeight > 0xff)
|
|
113
116
|
throw new InvalidConfigError({
|
|
114
|
-
reason: 'total owner weight exceeds
|
|
117
|
+
reason: 'total owner weight exceeds u8 max',
|
|
115
118
|
})
|
|
116
119
|
if (Number(threshold) > totalWeight)
|
|
117
120
|
throw new InvalidConfigError({
|
|
@@ -127,7 +130,7 @@ export declare namespace assert {
|
|
|
127
130
|
* Normalizes a native multisig {@link ox#MultisigConfig.Config}.
|
|
128
131
|
*
|
|
129
132
|
* Sorts owners into strictly ascending `owner` address order (the canonical
|
|
130
|
-
* form required for
|
|
133
|
+
* form required for account derivation) and asserts the config is valid.
|
|
131
134
|
*
|
|
132
135
|
* @example
|
|
133
136
|
* ```ts twoslash
|
|
@@ -197,7 +200,11 @@ export function fromTuple(tuple: Tuple): Config {
|
|
|
197
200
|
/**
|
|
198
201
|
* Derives the stable native multisig account address.
|
|
199
202
|
*
|
|
200
|
-
*
|
|
203
|
+
* Preimage (fixed-width big-endian, **not** RLP):
|
|
204
|
+
* `keccak256("tempo:multisig:account" || salt || u8(threshold) || u8(owners.length) || (owner || u8(weight)) for each owner)[12:32]`.
|
|
205
|
+
*
|
|
206
|
+
* The address is derived once from the initial (bootstrap) config and never
|
|
207
|
+
* changes — config updates do not affect it.
|
|
201
208
|
*
|
|
202
209
|
* @example
|
|
203
210
|
* ```ts twoslash
|
|
@@ -213,49 +220,37 @@ export function fromTuple(tuple: Tuple): Config {
|
|
|
213
220
|
* const address = MultisigConfig.getAddress(genesisConfig)
|
|
214
221
|
* ```
|
|
215
222
|
*
|
|
216
|
-
* @
|
|
217
|
-
* ### From an genesis config ID
|
|
218
|
-
*
|
|
219
|
-
* If you already have the permanent `genesisConfigId`, pass it directly:
|
|
220
|
-
*
|
|
221
|
-
* ```ts twoslash
|
|
222
|
-
* import { MultisigConfig } from 'ox/tempo'
|
|
223
|
-
*
|
|
224
|
-
* const genesisConfigId =
|
|
225
|
-
* `0x${'00'.repeat(32)}` as const
|
|
226
|
-
*
|
|
227
|
-
* const address = MultisigConfig.getAddress({ genesisConfigId })
|
|
228
|
-
* ```
|
|
229
|
-
*
|
|
230
|
-
* @param value - The genesis config (positional) or `{ genesisConfigId }`.
|
|
223
|
+
* @param config - The initial (bootstrap) multisig config.
|
|
231
224
|
* @returns The multisig account address.
|
|
232
225
|
*/
|
|
233
|
-
export function getAddress(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
226
|
+
export function getAddress(config: Config): Address.Address {
|
|
227
|
+
assert(config)
|
|
228
|
+
const hash = Hash.keccak256(
|
|
229
|
+
Hex.concat(
|
|
230
|
+
Hex.fromString(accountDomain),
|
|
231
|
+
Hex.padLeft(config.salt ?? zeroSalt, 32),
|
|
232
|
+
Hex.fromNumber(config.threshold, { size: 1 }),
|
|
233
|
+
Hex.fromNumber(config.owners.length, { size: 1 }),
|
|
234
|
+
...config.owners.flatMap((owner) => [
|
|
235
|
+
owner.owner,
|
|
236
|
+
Hex.fromNumber(owner.weight, { size: 1 }),
|
|
237
|
+
]),
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
const account = Address.from(Hex.slice(hash, 12, 32))
|
|
241
|
+
if (Hex.toBigInt(account) === 0n)
|
|
242
|
+
throw new InvalidConfigError({ reason: 'derived account cannot be zero' })
|
|
243
|
+
return account
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
export declare namespace getAddress {
|
|
243
|
-
type Value =
|
|
244
|
-
| Config
|
|
245
|
-
| {
|
|
246
|
-
/**
|
|
247
|
-
* The permanent genesis config ID (`MultisigConfig.toId(genesisConfig)`).
|
|
248
|
-
* Config updates never change this value, so it identifies the
|
|
249
|
-
* account permanently.
|
|
250
|
-
*/
|
|
251
|
-
genesisConfigId: Hex.Hex
|
|
252
|
-
}
|
|
253
|
-
|
|
254
247
|
type ErrorType =
|
|
255
|
-
|
|
|
248
|
+
| assert.ErrorType
|
|
256
249
|
| Address.from.ErrorType
|
|
257
250
|
| Hash.keccak256.ErrorType
|
|
258
251
|
| Hex.concat.ErrorType
|
|
252
|
+
| Hex.fromNumber.ErrorType
|
|
253
|
+
| Hex.fromString.ErrorType
|
|
259
254
|
| Hex.slice.ErrorType
|
|
260
255
|
| Errors.GlobalErrorType
|
|
261
256
|
}
|
|
@@ -263,14 +258,16 @@ export declare namespace getAddress {
|
|
|
263
258
|
/**
|
|
264
259
|
* Computes the digest a native multisig owner approves (signs).
|
|
265
260
|
*
|
|
266
|
-
* `keccak256("tempo:multisig:signature" || inner_digest || account
|
|
261
|
+
* `keccak256("tempo:multisig:signature" || inner_digest || account)`,
|
|
267
262
|
* where `inner_digest` is the transaction sign payload
|
|
268
263
|
* ({@link ox#TxEnvelopeTempo.(getSignPayload:function)}).
|
|
269
264
|
*
|
|
270
|
-
* The digest is
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
265
|
+
* The digest is keyed on the permanent `account` derived from the genesis
|
|
266
|
+
* (bootstrap) config — config updates never change it, so the genesis config
|
|
267
|
+
* is the correct input even for post-update transactions.
|
|
268
|
+
*
|
|
269
|
+
* For a nested multisig owner approval, the parent digest becomes the nested
|
|
270
|
+
* approval's `payload`, with the nested multisig `account`.
|
|
274
271
|
*
|
|
275
272
|
* @example
|
|
276
273
|
* ```ts twoslash
|
|
@@ -295,10 +292,10 @@ export declare namespace getAddress {
|
|
|
295
292
|
* ```
|
|
296
293
|
*
|
|
297
294
|
* @example
|
|
298
|
-
* ### From `account`
|
|
295
|
+
* ### From `account`
|
|
299
296
|
*
|
|
300
|
-
* If you already have the permanent `account`
|
|
301
|
-
*
|
|
297
|
+
* If you already have the permanent `account` (for example, recovered from a
|
|
298
|
+
* stored envelope), pass it directly:
|
|
302
299
|
*
|
|
303
300
|
* ```ts twoslash
|
|
304
301
|
* import { MultisigConfig, TxEnvelopeTempo } from 'ox/tempo'
|
|
@@ -309,15 +306,13 @@ export declare namespace getAddress {
|
|
|
309
306
|
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
310
307
|
* ],
|
|
311
308
|
* })
|
|
312
|
-
* const genesisConfigId = MultisigConfig.toId(genesisConfig)
|
|
313
309
|
* const account = MultisigConfig.getAddress(genesisConfig)
|
|
314
310
|
*
|
|
315
311
|
* const envelope = TxEnvelopeTempo.from({ chainId: 1, calls: [] })
|
|
316
312
|
*
|
|
317
313
|
* const digest = MultisigConfig.getSignPayload({
|
|
318
314
|
* payload: TxEnvelopeTempo.getSignPayload(envelope),
|
|
319
|
-
* account
|
|
320
|
-
* genesisConfigId,
|
|
315
|
+
* account
|
|
321
316
|
* })
|
|
322
317
|
* ```
|
|
323
318
|
*
|
|
@@ -330,17 +325,8 @@ export function getSignPayload(value: getSignPayload.Value): Hex.Hex {
|
|
|
330
325
|
'account' in value && value.account
|
|
331
326
|
? value.account
|
|
332
327
|
: getAddress((value as { genesisConfig: Config }).genesisConfig)
|
|
333
|
-
const genesisConfigId =
|
|
334
|
-
'genesisConfigId' in value && value.genesisConfigId
|
|
335
|
-
? value.genesisConfigId
|
|
336
|
-
: toId((value as { genesisConfig: Config }).genesisConfig)
|
|
337
328
|
return Hash.keccak256(
|
|
338
|
-
Hex.concat(
|
|
339
|
-
Hex.fromString(signatureDomain),
|
|
340
|
-
Hex.from(payload),
|
|
341
|
-
account,
|
|
342
|
-
genesisConfigId,
|
|
343
|
-
),
|
|
329
|
+
Hex.concat(Hex.fromString(signatureDomain), Hex.from(payload), account),
|
|
344
330
|
)
|
|
345
331
|
}
|
|
346
332
|
|
|
@@ -352,16 +338,13 @@ export declare namespace getSignPayload {
|
|
|
352
338
|
| {
|
|
353
339
|
/** The native multisig account address. */
|
|
354
340
|
account: Address.Address
|
|
355
|
-
/** The permanent config ID. */
|
|
356
|
-
genesisConfigId: Hex.Hex
|
|
357
341
|
}
|
|
358
342
|
| {
|
|
359
343
|
/**
|
|
360
344
|
* The initial multisig config (the bootstrap config that derived the
|
|
361
|
-
* permanent `account`
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* transactions.
|
|
345
|
+
* permanent `account`). Used to derive the account automatically.
|
|
346
|
+
* Config updates never change `account`, so the genesis config is
|
|
347
|
+
* also the correct input for post-update transactions.
|
|
365
348
|
*/
|
|
366
349
|
genesisConfig: Config
|
|
367
350
|
}
|
|
@@ -369,66 +352,12 @@ export declare namespace getSignPayload {
|
|
|
369
352
|
|
|
370
353
|
type ErrorType =
|
|
371
354
|
| getAddress.ErrorType
|
|
372
|
-
| toId.ErrorType
|
|
373
355
|
| Hash.keccak256.ErrorType
|
|
374
356
|
| Hex.concat.ErrorType
|
|
375
357
|
| Hex.from.ErrorType
|
|
376
358
|
| Errors.GlobalErrorType
|
|
377
359
|
}
|
|
378
360
|
|
|
379
|
-
/**
|
|
380
|
-
* Derives the permanent config ID for a native multisig
|
|
381
|
-
* {@link ox#MultisigConfig.Config}.
|
|
382
|
-
*
|
|
383
|
-
* Preimage (fixed-width big-endian, **not** RLP):
|
|
384
|
-
* `keccak256("tempo:multisig:config" || salt || be_u32(threshold) || be_u32(owners.length) || (owner || be_u32(weight)) for each owner)`.
|
|
385
|
-
*
|
|
386
|
-
* @example
|
|
387
|
-
* ```ts twoslash
|
|
388
|
-
* import { MultisigConfig } from 'ox/tempo'
|
|
389
|
-
*
|
|
390
|
-
* const config = MultisigConfig.from({
|
|
391
|
-
* threshold: 1,
|
|
392
|
-
* owners: [
|
|
393
|
-
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
394
|
-
* ],
|
|
395
|
-
* })
|
|
396
|
-
*
|
|
397
|
-
* const genesisConfigId = MultisigConfig.toId(config)
|
|
398
|
-
* ```
|
|
399
|
-
*
|
|
400
|
-
* @param config - The multisig config.
|
|
401
|
-
* @returns The 32-byte config ID.
|
|
402
|
-
*/
|
|
403
|
-
export function toId(config: Config): Hex.Hex {
|
|
404
|
-
assert(config)
|
|
405
|
-
const id = Hash.keccak256(
|
|
406
|
-
Hex.concat(
|
|
407
|
-
Hex.fromString(configDomain),
|
|
408
|
-
Hex.padLeft(config.salt ?? zeroSalt, 32),
|
|
409
|
-
Hex.fromNumber(config.threshold, { size: 4 }),
|
|
410
|
-
Hex.fromNumber(config.owners.length, { size: 4 }),
|
|
411
|
-
...config.owners.flatMap((owner) => [
|
|
412
|
-
owner.owner,
|
|
413
|
-
Hex.fromNumber(owner.weight, { size: 4 }),
|
|
414
|
-
]),
|
|
415
|
-
),
|
|
416
|
-
)
|
|
417
|
-
if (Hex.toBigInt(id) === 0n)
|
|
418
|
-
throw new InvalidConfigError({ reason: 'config ID cannot be zero' })
|
|
419
|
-
return id
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
export declare namespace toId {
|
|
423
|
-
type ErrorType =
|
|
424
|
-
| assert.ErrorType
|
|
425
|
-
| Hash.keccak256.ErrorType
|
|
426
|
-
| Hex.concat.ErrorType
|
|
427
|
-
| Hex.fromNumber.ErrorType
|
|
428
|
-
| Hex.fromString.ErrorType
|
|
429
|
-
| Errors.GlobalErrorType
|
|
430
|
-
}
|
|
431
|
-
|
|
432
361
|
/**
|
|
433
362
|
* Converts a {@link ox#MultisigConfig.Config} to its RLP tuple form (carried
|
|
434
363
|
* by the multisig signature `init`).
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
Hex,
|
|
4
4
|
P256,
|
|
5
5
|
PublicKey,
|
|
6
|
+
Rlp,
|
|
6
7
|
Secp256k1,
|
|
7
8
|
Signature,
|
|
8
9
|
WebAuthnP256,
|
|
@@ -1109,7 +1110,7 @@ describe('from', () => {
|
|
|
1109
1110
|
],
|
|
1110
1111
|
})
|
|
1111
1112
|
|
|
1112
|
-
test('behavior: derives `account
|
|
1113
|
+
test('behavior: derives `account` from `genesisConfig`', () => {
|
|
1113
1114
|
const envelope = SignatureEnvelope.from({
|
|
1114
1115
|
genesisConfig,
|
|
1115
1116
|
signatures: [SignatureEnvelope.from(signature_secp256k1)],
|
|
@@ -1118,7 +1119,6 @@ describe('from', () => {
|
|
|
1118
1119
|
expect(envelope).toMatchObject({
|
|
1119
1120
|
type: 'multisig',
|
|
1120
1121
|
account: MultisigConfig.getAddress(genesisConfig),
|
|
1121
|
-
genesisConfigId: MultisigConfig.toId(genesisConfig),
|
|
1122
1122
|
})
|
|
1123
1123
|
expect('genesisConfig' in envelope).toBe(false)
|
|
1124
1124
|
expect((envelope as SignatureEnvelope.Multisig).init).toBeUndefined()
|
|
@@ -1155,19 +1155,16 @@ describe('from', () => {
|
|
|
1155
1155
|
expect((envelope as SignatureEnvelope.Multisig).init).toEqual(otherConfig)
|
|
1156
1156
|
})
|
|
1157
1157
|
|
|
1158
|
-
test('behavior: `{account
|
|
1158
|
+
test('behavior: `{ account }` form still works', () => {
|
|
1159
1159
|
const account = MultisigConfig.getAddress(genesisConfig)
|
|
1160
|
-
const genesisConfigId = MultisigConfig.toId(genesisConfig)
|
|
1161
1160
|
const envelope = SignatureEnvelope.from({
|
|
1162
1161
|
account,
|
|
1163
|
-
genesisConfigId,
|
|
1164
1162
|
signatures: [SignatureEnvelope.from(signature_secp256k1)],
|
|
1165
1163
|
})
|
|
1166
1164
|
|
|
1167
1165
|
expect(envelope).toMatchObject({
|
|
1168
1166
|
type: 'multisig',
|
|
1169
1167
|
account,
|
|
1170
|
-
genesisConfigId,
|
|
1171
1168
|
})
|
|
1172
1169
|
})
|
|
1173
1170
|
})
|
|
@@ -1836,9 +1833,8 @@ describe('sortMultisigApprovals', () => {
|
|
|
1836
1833
|
expect(recovered).toEqual(ascending.map((owner) => owner.address))
|
|
1837
1834
|
})
|
|
1838
1835
|
|
|
1839
|
-
test('behavior: `genesisConfig` and `{account
|
|
1836
|
+
test('behavior: `genesisConfig` and `{ account }` produce identical ordering', () => {
|
|
1840
1837
|
const account = MultisigConfig.getAddress(genesisConfig)
|
|
1841
|
-
const genesisConfigId = MultisigConfig.toId(genesisConfig)
|
|
1842
1838
|
const signatures = owners.map((owner) => owner.signature)
|
|
1843
1839
|
|
|
1844
1840
|
const fromConfig = SignatureEnvelope.sortMultisigApprovals({
|
|
@@ -1846,13 +1842,12 @@ describe('sortMultisigApprovals', () => {
|
|
|
1846
1842
|
payload,
|
|
1847
1843
|
signatures,
|
|
1848
1844
|
})
|
|
1849
|
-
const
|
|
1845
|
+
const fromAccount = SignatureEnvelope.sortMultisigApprovals({
|
|
1850
1846
|
account,
|
|
1851
|
-
genesisConfigId,
|
|
1852
1847
|
payload,
|
|
1853
1848
|
signatures,
|
|
1854
1849
|
})
|
|
1855
|
-
expect(fromConfig).toEqual(
|
|
1850
|
+
expect(fromConfig).toEqual(fromAccount)
|
|
1856
1851
|
})
|
|
1857
1852
|
})
|
|
1858
1853
|
|
|
@@ -2855,8 +2850,6 @@ describe('CoercionError', () => {
|
|
|
2855
2850
|
|
|
2856
2851
|
describe('multisig', () => {
|
|
2857
2852
|
const account = '0x8ba6d26ff5c4e82ba0c8caf8c8ca794e1489a7ae'
|
|
2858
|
-
const genesisConfigId =
|
|
2859
|
-
'0x01781fe551182476f2422c759e82d81c92e3263737afbbad57def6e8b69d21f5'
|
|
2860
2853
|
|
|
2861
2854
|
// P256 signatures do not carry `yParity` in the wire format, so use a clean
|
|
2862
2855
|
// inner signature for round-trip equality checks.
|
|
@@ -2869,7 +2862,6 @@ describe('multisig', () => {
|
|
|
2869
2862
|
const envelope = SignatureEnvelope.from({
|
|
2870
2863
|
type: 'multisig',
|
|
2871
2864
|
account,
|
|
2872
|
-
genesisConfigId,
|
|
2873
2865
|
signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
|
|
2874
2866
|
})
|
|
2875
2867
|
|
|
@@ -2878,11 +2870,32 @@ describe('multisig', () => {
|
|
|
2878
2870
|
expect(serialized.startsWith('0x05')).toBe(true)
|
|
2879
2871
|
})
|
|
2880
2872
|
|
|
2873
|
+
test('serialize: wire shape is `0x05 || rlp([account, signatures])`', () => {
|
|
2874
|
+
const deterministic = SignatureEnvelope.from({
|
|
2875
|
+
type: 'multisig',
|
|
2876
|
+
account,
|
|
2877
|
+
signatures: [innerP256],
|
|
2878
|
+
})
|
|
2879
|
+
expect(SignatureEnvelope.serialize(deterministic)).toMatchInlineSnapshot(
|
|
2880
|
+
`"0x05f89b948ba6d26ff5c4e82ba0c8caf8c8ca794e1489a7aef884b88201ccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35ad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f511edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba001"`,
|
|
2881
|
+
)
|
|
2882
|
+
})
|
|
2883
|
+
|
|
2881
2884
|
test('serialize/deserialize round-trip', () => {
|
|
2882
2885
|
const serialized = SignatureEnvelope.serialize(envelope)
|
|
2883
2886
|
expect(SignatureEnvelope.deserialize(serialized)).toEqual(envelope)
|
|
2884
2887
|
})
|
|
2885
2888
|
|
|
2889
|
+
test('deserialize: rejects the empty `init` placeholder (`0x80`)', () => {
|
|
2890
|
+
const withPlaceholder = Hex.concat(
|
|
2891
|
+
'0x05',
|
|
2892
|
+
Rlp.fromHex([account, [SignatureEnvelope.serialize(innerP256)], '0x']),
|
|
2893
|
+
)
|
|
2894
|
+
expect(() => SignatureEnvelope.deserialize(withPlaceholder)).toThrowError(
|
|
2895
|
+
SignatureEnvelope.InvalidSerializedError,
|
|
2896
|
+
)
|
|
2897
|
+
})
|
|
2898
|
+
|
|
2886
2899
|
test('getType', () => {
|
|
2887
2900
|
expect(SignatureEnvelope.getType(envelope)).toBe('multisig')
|
|
2888
2901
|
})
|
|
@@ -2908,6 +2921,97 @@ describe('multisig', () => {
|
|
|
2908
2921
|
).toThrowError()
|
|
2909
2922
|
})
|
|
2910
2923
|
|
|
2924
|
+
test('assert: rejects keychain owner approvals', () => {
|
|
2925
|
+
expect(() =>
|
|
2926
|
+
SignatureEnvelope.assert({
|
|
2927
|
+
type: 'multisig',
|
|
2928
|
+
account,
|
|
2929
|
+
signatures: [signature_keychain_secp256k1],
|
|
2930
|
+
} as never),
|
|
2931
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
2932
|
+
`[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: keychain owner approvals are not allowed.]`,
|
|
2933
|
+
)
|
|
2934
|
+
})
|
|
2935
|
+
|
|
2936
|
+
describe('nested approvals', () => {
|
|
2937
|
+
const nestedAccount = '0x1111111111111111111111111111111111111111'
|
|
2938
|
+
const nested = SignatureEnvelope.from({
|
|
2939
|
+
type: 'multisig',
|
|
2940
|
+
account: nestedAccount,
|
|
2941
|
+
signatures: [innerP256],
|
|
2942
|
+
})
|
|
2943
|
+
|
|
2944
|
+
test('serialize/deserialize round-trip with a nested multisig approval', () => {
|
|
2945
|
+
const parent = SignatureEnvelope.from({
|
|
2946
|
+
type: 'multisig',
|
|
2947
|
+
account,
|
|
2948
|
+
signatures: [innerP256, nested],
|
|
2949
|
+
})
|
|
2950
|
+
const serialized = SignatureEnvelope.serialize(parent)
|
|
2951
|
+
expect(SignatureEnvelope.deserialize(serialized)).toEqual(parent)
|
|
2952
|
+
})
|
|
2953
|
+
|
|
2954
|
+
test('assert: accepts the maximum nesting depth', () => {
|
|
2955
|
+
const depth3 = SignatureEnvelope.from({
|
|
2956
|
+
type: 'multisig',
|
|
2957
|
+
account,
|
|
2958
|
+
signatures: [
|
|
2959
|
+
SignatureEnvelope.from({
|
|
2960
|
+
type: 'multisig',
|
|
2961
|
+
account: nestedAccount,
|
|
2962
|
+
signatures: [nested],
|
|
2963
|
+
}),
|
|
2964
|
+
],
|
|
2965
|
+
})
|
|
2966
|
+
expect(() => SignatureEnvelope.assert(depth3)).not.toThrowError()
|
|
2967
|
+
})
|
|
2968
|
+
|
|
2969
|
+
test('assert: rejects nesting deeper than the maximum', () => {
|
|
2970
|
+
const depth4 = SignatureEnvelope.from({
|
|
2971
|
+
type: 'multisig',
|
|
2972
|
+
account,
|
|
2973
|
+
signatures: [
|
|
2974
|
+
SignatureEnvelope.from({
|
|
2975
|
+
type: 'multisig',
|
|
2976
|
+
account: nestedAccount,
|
|
2977
|
+
signatures: [
|
|
2978
|
+
SignatureEnvelope.from({
|
|
2979
|
+
type: 'multisig',
|
|
2980
|
+
account: nestedAccount,
|
|
2981
|
+
signatures: [nested],
|
|
2982
|
+
}),
|
|
2983
|
+
],
|
|
2984
|
+
}),
|
|
2985
|
+
],
|
|
2986
|
+
})
|
|
2987
|
+
expect(() =>
|
|
2988
|
+
SignatureEnvelope.assert(depth4),
|
|
2989
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
2990
|
+
`[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig nesting depth exceeds 3.]`,
|
|
2991
|
+
)
|
|
2992
|
+
})
|
|
2993
|
+
|
|
2994
|
+
test('assert: rejects nested approvals carrying `init`', () => {
|
|
2995
|
+
expect(() =>
|
|
2996
|
+
SignatureEnvelope.assert({
|
|
2997
|
+
type: 'multisig',
|
|
2998
|
+
account,
|
|
2999
|
+
signatures: [
|
|
3000
|
+
{
|
|
3001
|
+
...nested,
|
|
3002
|
+
init: {
|
|
3003
|
+
threshold: 1,
|
|
3004
|
+
owners: [{ owner: nestedAccount, weight: 1 }],
|
|
3005
|
+
},
|
|
3006
|
+
},
|
|
3007
|
+
],
|
|
3008
|
+
} as never),
|
|
3009
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
3010
|
+
`[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: nested multisig owner approvals cannot carry \`init\`.]`,
|
|
3011
|
+
)
|
|
3012
|
+
})
|
|
3013
|
+
})
|
|
3014
|
+
|
|
2911
3015
|
describe('init (bootstrap)', () => {
|
|
2912
3016
|
const init = {
|
|
2913
3017
|
salt: `0x${'00'.repeat(32)}` as const,
|
|
@@ -2923,7 +3027,6 @@ describe('multisig', () => {
|
|
|
2923
3027
|
const bootstrapEnvelope = SignatureEnvelope.from({
|
|
2924
3028
|
type: 'multisig',
|
|
2925
3029
|
account,
|
|
2926
|
-
genesisConfigId,
|
|
2927
3030
|
signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
|
|
2928
3031
|
init,
|
|
2929
3032
|
})
|
|
@@ -2939,7 +3042,6 @@ describe('multisig', () => {
|
|
|
2939
3042
|
const salted = SignatureEnvelope.from({
|
|
2940
3043
|
type: 'multisig',
|
|
2941
3044
|
account,
|
|
2942
|
-
genesisConfigId,
|
|
2943
3045
|
signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
|
|
2944
3046
|
init: { ...init, salt: `0x${'42'.repeat(32)}` },
|
|
2945
3047
|
})
|
|
@@ -2993,7 +3095,6 @@ describe('multisig', () => {
|
|
|
2993
3095
|
SignatureEnvelope.assert({
|
|
2994
3096
|
type: 'multisig',
|
|
2995
3097
|
account,
|
|
2996
|
-
genesisConfigId,
|
|
2997
3098
|
signatures: [],
|
|
2998
3099
|
init: { threshold: 1, owners: [] },
|
|
2999
3100
|
} as never),
|