ox 0.14.34 → 0.14.35

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/_cjs/core/AesGcm.js +47 -1
  3. package/_cjs/core/AesGcm.js.map +1 -1
  4. package/_cjs/core/Ed25519.js +42 -1
  5. package/_cjs/core/Ed25519.js.map +1 -1
  6. package/_cjs/core/P256.js +44 -1
  7. package/_cjs/core/P256.js.map +1 -1
  8. package/_cjs/core/Secp256k1.js +38 -1
  9. package/_cjs/core/Secp256k1.js.map +1 -1
  10. package/_cjs/core/internal/keyDerivation.js +15 -0
  11. package/_cjs/core/internal/keyDerivation.js.map +1 -0
  12. package/_cjs/tempo/MultisigConfig.js +15 -4
  13. package/_cjs/tempo/MultisigConfig.js.map +1 -1
  14. package/_cjs/version.js +1 -1
  15. package/_esm/core/AesGcm.js +85 -0
  16. package/_esm/core/AesGcm.js.map +1 -1
  17. package/_esm/core/Ed25519.js +81 -0
  18. package/_esm/core/Ed25519.js.map +1 -1
  19. package/_esm/core/P256.js +83 -0
  20. package/_esm/core/P256.js.map +1 -1
  21. package/_esm/core/Secp256k1.js +77 -0
  22. package/_esm/core/Secp256k1.js.map +1 -1
  23. package/_esm/core/internal/keyDerivation.js +13 -0
  24. package/_esm/core/internal/keyDerivation.js.map +1 -0
  25. package/_esm/tempo/MultisigConfig.js +17 -5
  26. package/_esm/tempo/MultisigConfig.js.map +1 -1
  27. package/_esm/version.js +1 -1
  28. package/_types/core/AesGcm.d.ts +66 -1
  29. package/_types/core/AesGcm.d.ts.map +1 -1
  30. package/_types/core/Ed25519.d.ts +82 -1
  31. package/_types/core/Ed25519.d.ts.map +1 -1
  32. package/_types/core/P256.d.ts +82 -1
  33. package/_types/core/P256.d.ts.map +1 -1
  34. package/_types/core/Secp256k1.d.ts +85 -1
  35. package/_types/core/Secp256k1.d.ts.map +1 -1
  36. package/_types/core/internal/keyDerivation.d.ts +12 -0
  37. package/_types/core/internal/keyDerivation.d.ts.map +1 -0
  38. package/_types/tempo/MultisigConfig.d.ts +4 -3
  39. package/_types/tempo/MultisigConfig.d.ts.map +1 -1
  40. package/_types/version.d.ts +1 -1
  41. package/core/AesGcm.ts +137 -1
  42. package/core/Ed25519.ts +133 -1
  43. package/core/P256.ts +135 -1
  44. package/core/Secp256k1.ts +133 -1
  45. package/core/internal/keyDerivation.ts +33 -0
  46. package/package.json +1 -1
  47. package/tempo/MultisigConfig.test.ts +40 -6
  48. package/tempo/MultisigConfig.ts +18 -5
  49. package/version.ts +1 -1
package/core/Ed25519.ts CHANGED
@@ -4,8 +4,10 @@ import {
4
4
  edwardsToMontgomeryPub,
5
5
  } from '@noble/curves/ed25519'
6
6
  import * as Bytes from './Bytes.js'
7
- import type * as Errors from './Errors.js'
7
+ import * as Errors from './Errors.js'
8
8
  import * as Hex from './Hex.js'
9
+ import * as keyDerivation from './internal/keyDerivation.js'
10
+ import * as Mnemonic from './Mnemonic.js'
9
11
 
10
12
  /** Re-export of noble/curves Ed25519 utilities. */
11
13
  export const noble = ed25519
@@ -61,6 +63,115 @@ export declare namespace createKeyPair {
61
63
  | Errors.GlobalErrorType
62
64
  }
63
65
 
66
+ /**
67
+ * Derives an Ed25519 private key from a BIP-39 mnemonic.
68
+ *
69
+ * This is equivalent to passing `Mnemonic.toSeed(mnemonic, { passphrase })`
70
+ * to `Ed25519.fromSeed`.
71
+ *
72
+ * @example
73
+ * ```ts twoslash
74
+ * import { Ed25519 } from 'ox'
75
+ *
76
+ * const privateKey = Ed25519.fromMnemonic(
77
+ * 'test test test test test test test test test test test junk'
78
+ * )
79
+ * ```
80
+ *
81
+ * @param mnemonic - BIP-39 mnemonic phrase.
82
+ * @param options - Options.
83
+ * @returns An Ed25519 private key.
84
+ */
85
+ export function fromMnemonic<as extends 'Hex' | 'Bytes' = 'Hex'>(
86
+ mnemonic: string,
87
+ options: fromMnemonic.Options<as> = {},
88
+ ): fromMnemonic.ReturnType<as> {
89
+ const { passphrase } = options
90
+ const seed = Mnemonic.toSeed(mnemonic, { passphrase })
91
+ try {
92
+ return fromSeed(seed, options)
93
+ } finally {
94
+ seed.fill(0)
95
+ }
96
+ }
97
+
98
+ export declare namespace fromMnemonic {
99
+ type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
100
+ /**
101
+ * Format of the returned private key.
102
+ * @default 'Hex'
103
+ */
104
+ as?: as | 'Hex' | 'Bytes' | undefined
105
+ /** Optional BIP-39 passphrase. */
106
+ passphrase?: string | undefined
107
+ }
108
+
109
+ type ReturnType<as extends 'Hex' | 'Bytes'> = fromSeed.ReturnType<as>
110
+
111
+ type ErrorType = fromSeed.ErrorType
112
+ }
113
+
114
+ /**
115
+ * Derives an Ed25519 private key from a seed.
116
+ *
117
+ * The seed must contain at least 32 bytes of cryptographically strong key
118
+ * material. Do not pass a password directly; use a password KDF first.
119
+ *
120
+ * The permanent derivation contract uses the seed as the HMAC-SHA256
121
+ * key. The HMAC message uses the `ox.ed25519.fromSeed.v1` domain followed by a
122
+ * 32-bit big-endian counter set to zero.
123
+ *
124
+ * @example
125
+ * ```ts twoslash
126
+ * import { Ed25519 } from 'ox'
127
+ *
128
+ * const privateKey = Ed25519.fromSeed(
129
+ * '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
130
+ * )
131
+ * ```
132
+ *
133
+ * @param seed - Seed containing at least 32 bytes of cryptographically strong key material.
134
+ * @param options - Options.
135
+ * @returns An Ed25519 private key.
136
+ */
137
+ export function fromSeed<as extends 'Hex' | 'Bytes' = 'Hex'>(
138
+ seed: Hex.Hex | Bytes.Bytes,
139
+ options: fromSeed.Options<as> = {},
140
+ ): fromSeed.ReturnType<as> {
141
+ const { as = 'Hex' } = options
142
+ const bytes = Bytes.from(seed)
143
+ if (bytes.length < 32) throw new InvalidSeedSizeError({ size: bytes.length })
144
+
145
+ const privateKey = keyDerivation.derive(bytes, fromSeedDomain)
146
+ if (as === 'Hex') {
147
+ const value = Hex.fromBytes(privateKey)
148
+ privateKey.fill(0)
149
+ return value as never
150
+ }
151
+ return privateKey as never
152
+ }
153
+
154
+ export declare namespace fromSeed {
155
+ type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
156
+ /**
157
+ * Format of the returned private key.
158
+ * @default 'Hex'
159
+ */
160
+ as?: as | 'Hex' | 'Bytes' | undefined
161
+ }
162
+
163
+ type ReturnType<as extends 'Hex' | 'Bytes'> =
164
+ | (as extends 'Bytes' ? Bytes.Bytes : never)
165
+ | (as extends 'Hex' ? Hex.Hex : never)
166
+
167
+ type ErrorType =
168
+ | Bytes.from.ErrorType
169
+ | Hex.fromBytes.ErrorType
170
+ | keyDerivation.derive.ErrorType
171
+ | InvalidSeedSizeError
172
+ | Errors.GlobalErrorType
173
+ }
174
+
64
175
  /**
65
176
  * Computes the Ed25519 public key from a provided private key.
66
177
  *
@@ -337,3 +448,24 @@ export declare namespace toX25519PrivateKey {
337
448
  | Hex.fromBytes.ErrorType
338
449
  | Errors.GlobalErrorType
339
450
  }
451
+
452
+ /** Thrown when a seed contains fewer than 32 bytes. */
453
+ export class InvalidSeedSizeError extends Errors.BaseError {
454
+ override readonly name = 'Ed25519.InvalidSeedSizeError'
455
+
456
+ constructor(options: InvalidSeedSizeError.Options) {
457
+ super(
458
+ `Seed must contain at least 32 bytes. Received ${options.size} bytes.`,
459
+ )
460
+ }
461
+ }
462
+
463
+ export declare namespace InvalidSeedSizeError {
464
+ /** Options for `Ed25519.InvalidSeedSizeError`. */
465
+ type Options = {
466
+ /** Received seed size. */
467
+ size: number
468
+ }
469
+ }
470
+
471
+ const fromSeedDomain = Bytes.fromString('ox.ed25519.fromSeed.v1')
package/core/P256.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { secp256r1 } from '@noble/curves/p256'
2
2
  import * as Bytes from './Bytes.js'
3
- import type * as Errors from './Errors.js'
3
+ import * as Errors from './Errors.js'
4
4
  import * as Hex from './Hex.js'
5
5
  import * as Entropy from './internal/entropy.js'
6
+ import * as keyDerivation from './internal/keyDerivation.js'
7
+ import * as Mnemonic from './Mnemonic.js'
6
8
  import * as PublicKey from './PublicKey.js'
7
9
  import type * as Signature from './Signature.js'
8
10
 
@@ -57,6 +59,117 @@ export declare namespace createKeyPair {
57
59
  | Errors.GlobalErrorType
58
60
  }
59
61
 
62
+ /**
63
+ * Derives a valid P256 private key from a BIP-39 mnemonic.
64
+ *
65
+ * This is equivalent to passing `Mnemonic.toSeed(mnemonic, { passphrase })`
66
+ * to `P256.fromSeed`.
67
+ *
68
+ * @example
69
+ * ```ts twoslash
70
+ * import { P256 } from 'ox'
71
+ *
72
+ * const privateKey = P256.fromMnemonic(
73
+ * 'test test test test test test test test test test test junk'
74
+ * )
75
+ * ```
76
+ *
77
+ * @param mnemonic - BIP-39 mnemonic phrase.
78
+ * @param options - Options.
79
+ * @returns A valid P256 private key.
80
+ */
81
+ export function fromMnemonic<as extends 'Hex' | 'Bytes' = 'Hex'>(
82
+ mnemonic: string,
83
+ options: fromMnemonic.Options<as> = {},
84
+ ): fromMnemonic.ReturnType<as> {
85
+ const { passphrase } = options
86
+ const seed = Mnemonic.toSeed(mnemonic, { passphrase })
87
+ try {
88
+ return fromSeed(seed, options)
89
+ } finally {
90
+ seed.fill(0)
91
+ }
92
+ }
93
+
94
+ export declare namespace fromMnemonic {
95
+ type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
96
+ /**
97
+ * Format of the returned private key.
98
+ * @default 'Hex'
99
+ */
100
+ as?: as | 'Hex' | 'Bytes' | undefined
101
+ /** Optional BIP-39 passphrase. */
102
+ passphrase?: string | undefined
103
+ }
104
+
105
+ type ReturnType<as extends 'Hex' | 'Bytes'> = fromSeed.ReturnType<as>
106
+
107
+ type ErrorType = fromSeed.ErrorType
108
+ }
109
+
110
+ /**
111
+ * Derives a valid P256 private key from a seed.
112
+ *
113
+ * The seed must contain at least 32 bytes of cryptographically strong key
114
+ * material. Do not pass a password directly; use a password KDF first.
115
+ *
116
+ * The permanent derivation contract uses the seed as the HMAC-SHA256
117
+ * key. The HMAC message uses the `ox.p256.fromSeed.v1` domain followed by a
118
+ * 32-bit big-endian counter starting at zero. Invalid scalars are skipped.
119
+ *
120
+ * @example
121
+ * ```ts twoslash
122
+ * import { P256 } from 'ox'
123
+ *
124
+ * const privateKey = P256.fromSeed(
125
+ * '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
126
+ * )
127
+ * ```
128
+ *
129
+ * @param seed - Seed containing at least 32 bytes of cryptographically strong key material.
130
+ * @param options - Options.
131
+ * @returns A valid P256 private key.
132
+ */
133
+ export function fromSeed<as extends 'Hex' | 'Bytes' = 'Hex'>(
134
+ seed: Hex.Hex | Bytes.Bytes,
135
+ options: fromSeed.Options<as> = {},
136
+ ): fromSeed.ReturnType<as> {
137
+ const { as = 'Hex' } = options
138
+ const bytes = Bytes.from(seed)
139
+ if (bytes.length < 32) throw new InvalidSeedSizeError({ size: bytes.length })
140
+
141
+ const privateKey = keyDerivation.derive(bytes, fromSeedDomain, {
142
+ validate: noble.utils.isValidPrivateKey,
143
+ })
144
+ if (as === 'Hex') {
145
+ const value = Hex.fromBytes(privateKey)
146
+ privateKey.fill(0)
147
+ return value as never
148
+ }
149
+ return privateKey as never
150
+ }
151
+
152
+ export declare namespace fromSeed {
153
+ type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
154
+ /**
155
+ * Format of the returned private key.
156
+ * @default 'Hex'
157
+ */
158
+ as?: as | 'Hex' | 'Bytes' | undefined
159
+ }
160
+
161
+ type ReturnType<as extends 'Hex' | 'Bytes'> =
162
+ | (as extends 'Bytes' ? Bytes.Bytes : never)
163
+ | (as extends 'Hex' ? Hex.Hex : never)
164
+
165
+ type ErrorType =
166
+ | Bytes.from.ErrorType
167
+ | Hex.fromBytes.ErrorType
168
+ | keyDerivation.derive.ErrorType
169
+ | InvalidSeedSizeError
170
+ | Errors.GlobalErrorType
171
+ }
172
+
60
173
  /**
61
174
  * Computes the P256 ECDSA public key from a provided private key.
62
175
  *
@@ -354,3 +467,24 @@ export declare namespace verify {
354
467
 
355
468
  type ErrorType = Errors.GlobalErrorType
356
469
  }
470
+
471
+ /** Thrown when a seed contains fewer than 32 bytes. */
472
+ export class InvalidSeedSizeError extends Errors.BaseError {
473
+ override readonly name = 'P256.InvalidSeedSizeError'
474
+
475
+ constructor(options: InvalidSeedSizeError.Options) {
476
+ super(
477
+ `Seed must contain at least 32 bytes. Received ${options.size} bytes.`,
478
+ )
479
+ }
480
+ }
481
+
482
+ export declare namespace InvalidSeedSizeError {
483
+ /** Options for `P256.InvalidSeedSizeError`. */
484
+ type Options = {
485
+ /** Received seed size. */
486
+ size: number
487
+ }
488
+ }
489
+
490
+ const fromSeedDomain = Bytes.fromString('ox.p256.fromSeed.v1')
package/core/Secp256k1.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { secp256k1 } from '@noble/curves/secp256k1'
2
2
  import * as Address from './Address.js'
3
3
  import * as Bytes from './Bytes.js'
4
- import type * as Errors from './Errors.js'
4
+ import * as Errors from './Errors.js'
5
5
  import * as Hex from './Hex.js'
6
6
  import * as Entropy from './internal/entropy.js'
7
+ import * as keyDerivation from './internal/keyDerivation.js'
7
8
  import type { OneOf } from './internal/types.js'
9
+ import * as Mnemonic from './Mnemonic.js'
8
10
  import * as PublicKey from './PublicKey.js'
9
11
  import type * as Signature from './Signature.js'
10
12
 
@@ -59,6 +61,115 @@ export declare namespace createKeyPair {
59
61
  | Errors.GlobalErrorType
60
62
  }
61
63
 
64
+ /**
65
+ * Derives a valid secp256k1 private key from a BIP-39 mnemonic.
66
+ *
67
+ * This is equivalent to `Mnemonic.toPrivateKey`, and derives the
68
+ * private key at `m/44'/60'/0'/0/0` by default.
69
+ *
70
+ * @example
71
+ * ```ts twoslash
72
+ * import { Secp256k1 } from 'ox'
73
+ *
74
+ * const privateKey = Secp256k1.fromMnemonic(
75
+ * 'test test test test test test test test test test test junk'
76
+ * )
77
+ * ```
78
+ *
79
+ * @param mnemonic - BIP-39 mnemonic phrase.
80
+ * @param options - Options.
81
+ * @returns A valid secp256k1 private key.
82
+ */
83
+ export function fromMnemonic<as extends 'Hex' | 'Bytes' = 'Hex'>(
84
+ mnemonic: string,
85
+ options: fromMnemonic.Options<as> = {},
86
+ ): fromMnemonic.ReturnType<as> {
87
+ const { as = 'Hex', passphrase, path } = options
88
+ return Mnemonic.toPrivateKey(mnemonic, { as, passphrase, path }) as never
89
+ }
90
+
91
+ export declare namespace fromMnemonic {
92
+ type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
93
+ /**
94
+ * Format of the returned private key.
95
+ * @default 'Hex'
96
+ */
97
+ as?: as | 'Hex' | 'Bytes' | undefined
98
+ /** Derivation path. @default `m/44'/60'/0'/0/0` */
99
+ path?: string | undefined
100
+ /** Optional BIP-39 passphrase. */
101
+ passphrase?: string | undefined
102
+ }
103
+
104
+ type ReturnType<as extends 'Hex' | 'Bytes'> =
105
+ Mnemonic.toPrivateKey.ReturnType<as>
106
+
107
+ type ErrorType = Mnemonic.toPrivateKey.ErrorType
108
+ }
109
+
110
+ /**
111
+ * Derives a valid secp256k1 private key from a seed.
112
+ *
113
+ * The seed must contain at least 32 bytes of cryptographically strong key
114
+ * material. Do not pass a password directly; use a password KDF first.
115
+ *
116
+ * The permanent derivation contract uses the seed as the HMAC-SHA256
117
+ * key. The HMAC message uses the `ox.secp256k1.fromSeed.v1` domain followed by
118
+ * a 32-bit big-endian counter starting at zero. Invalid scalars are skipped.
119
+ *
120
+ * @example
121
+ * ```ts twoslash
122
+ * import { Secp256k1 } from 'ox'
123
+ *
124
+ * const privateKey = Secp256k1.fromSeed(
125
+ * '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'
126
+ * )
127
+ * ```
128
+ *
129
+ * @param seed - Seed containing at least 32 bytes of cryptographically strong key material.
130
+ * @param options - Options.
131
+ * @returns A valid secp256k1 private key.
132
+ */
133
+ export function fromSeed<as extends 'Hex' | 'Bytes' = 'Hex'>(
134
+ seed: Hex.Hex | Bytes.Bytes,
135
+ options: fromSeed.Options<as> = {},
136
+ ): fromSeed.ReturnType<as> {
137
+ const { as = 'Hex' } = options
138
+ const bytes = Bytes.from(seed)
139
+ if (bytes.length < 32) throw new InvalidSeedSizeError({ size: bytes.length })
140
+
141
+ const privateKey = keyDerivation.derive(bytes, fromSeedDomain, {
142
+ validate: noble.utils.isValidPrivateKey,
143
+ })
144
+ if (as === 'Hex') {
145
+ const value = Hex.fromBytes(privateKey)
146
+ privateKey.fill(0)
147
+ return value as never
148
+ }
149
+ return privateKey as never
150
+ }
151
+
152
+ export declare namespace fromSeed {
153
+ type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
154
+ /**
155
+ * Format of the returned private key.
156
+ * @default 'Hex'
157
+ */
158
+ as?: as | 'Hex' | 'Bytes' | undefined
159
+ }
160
+
161
+ type ReturnType<as extends 'Hex' | 'Bytes'> =
162
+ | (as extends 'Bytes' ? Bytes.Bytes : never)
163
+ | (as extends 'Hex' ? Hex.Hex : never)
164
+
165
+ type ErrorType =
166
+ | Bytes.from.ErrorType
167
+ | Hex.fromBytes.ErrorType
168
+ | keyDerivation.derive.ErrorType
169
+ | InvalidSeedSizeError
170
+ | Errors.GlobalErrorType
171
+ }
172
+
62
173
  /**
63
174
  * Computes the secp256k1 ECDSA public key from a provided private key.
64
175
  *
@@ -423,3 +534,24 @@ export declare namespace verify {
423
534
 
424
535
  type ErrorType = Errors.GlobalErrorType
425
536
  }
537
+
538
+ /** Thrown when a seed contains fewer than 32 bytes. */
539
+ export class InvalidSeedSizeError extends Errors.BaseError {
540
+ override readonly name = 'Secp256k1.InvalidSeedSizeError'
541
+
542
+ constructor(options: InvalidSeedSizeError.Options) {
543
+ super(
544
+ `Seed must contain at least 32 bytes. Received ${options.size} bytes.`,
545
+ )
546
+ }
547
+ }
548
+
549
+ export declare namespace InvalidSeedSizeError {
550
+ /** Options for `Secp256k1.InvalidSeedSizeError`. */
551
+ type Options = {
552
+ /** Received seed size. */
553
+ size: number
554
+ }
555
+ }
556
+
557
+ const fromSeedDomain = Bytes.fromString('ox.secp256k1.fromSeed.v1')
@@ -0,0 +1,33 @@
1
+ import * as Bytes from '../Bytes.js'
2
+ import type * as Errors from '../Errors.js'
3
+ import * as Hash from '../Hash.js'
4
+
5
+ /** @internal */
6
+ export function derive(
7
+ seed: Bytes.Bytes,
8
+ domain: Bytes.Bytes,
9
+ options: derive.Options = {},
10
+ ): Bytes.Bytes {
11
+ const { validate } = options
12
+ for (let counter = 0; ; counter++) {
13
+ const key = Hash.hmac256(
14
+ seed,
15
+ Bytes.concat(domain, Bytes.fromNumber(counter, { size: 4 })),
16
+ { as: 'Bytes' },
17
+ )
18
+ if (!validate || validate(key)) return key
19
+ key.fill(0)
20
+ }
21
+ }
22
+
23
+ export declare namespace derive {
24
+ type Options = {
25
+ validate?: ((key: Bytes.Bytes) => boolean) | undefined
26
+ }
27
+
28
+ type ErrorType =
29
+ | Bytes.concat.ErrorType
30
+ | Bytes.fromNumber.ErrorType
31
+ | Hash.hmac256.ErrorType
32
+ | Errors.GlobalErrorType
33
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ox",
3
3
  "description": "Ethereum Standard Library",
4
- "version": "0.14.34",
4
+ "version": "0.14.35",
5
5
  "main": "./_cjs/index.js",
6
6
  "module": "./_esm/index.js",
7
7
  "types": "./_types/index.d.ts",
@@ -169,21 +169,21 @@ describe('assert / validate', () => {
169
169
  expect(MultisigConfig.validate({ threshold: 1, owners: [] })).toBe(false)
170
170
  })
171
171
 
172
- test('accepts 50 owners', () => {
173
- const owners = Array.from({ length: 50 }, (_, i) => ({
172
+ test('accepts 48 owners', () => {
173
+ const owners = Array.from({ length: 48 }, (_, i) => ({
174
174
  owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`,
175
175
  weight: 1,
176
176
  }))
177
177
  expect(
178
178
  MultisigConfig.validate({
179
- threshold: MultisigConfig.maxThreshold,
179
+ threshold: MultisigConfig.maxSignatures,
180
180
  owners,
181
181
  }),
182
182
  ).toBe(true)
183
183
  })
184
184
 
185
185
  test('too many owners', () => {
186
- const owners = Array.from({ length: 51 }, (_, i) => ({
186
+ const owners = Array.from({ length: 49 }, (_, i) => ({
187
187
  owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`,
188
188
  weight: 1,
189
189
  }))
@@ -199,11 +199,29 @@ describe('assert / validate', () => {
199
199
  ).toBe(false)
200
200
  })
201
201
 
202
- test('threshold exceeds protocol maximum', () => {
202
+ test('accepts a uint8 threshold above the signature limit', () => {
203
+ expect(
204
+ MultisigConfig.validate({
205
+ threshold: 9,
206
+ owners: [{ owner: owner1, weight: 9 }],
207
+ }),
208
+ ).toBe(true)
209
+ })
210
+
211
+ test('accepts the maximum uint8 threshold', () => {
212
+ expect(
213
+ MultisigConfig.validate({
214
+ threshold: MultisigConfig.maxThreshold,
215
+ owners: [{ owner: owner1, weight: MultisigConfig.maxThreshold }],
216
+ }),
217
+ ).toBe(true)
218
+ })
219
+
220
+ test('threshold exceeds uint8 maximum', () => {
203
221
  expect(
204
222
  MultisigConfig.validate({
205
223
  threshold: MultisigConfig.maxThreshold + 1,
206
- owners: [{ owner: owner1, weight: MultisigConfig.maxThreshold + 1 }],
224
+ owners: [{ owner: owner1, weight: MultisigConfig.maxThreshold }],
207
225
  }),
208
226
  ).toBe(false)
209
227
  })
@@ -226,6 +244,22 @@ describe('assert / validate', () => {
226
244
  ).toBe(false)
227
245
  })
228
246
 
247
+ test('threshold cannot require more than eight owners', () => {
248
+ const owners = Array.from({ length: 9 }, (_, i) => ({
249
+ owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`,
250
+ weight: 1,
251
+ }))
252
+ expect(MultisigConfig.validate({ threshold: 9, owners })).toBe(false)
253
+ })
254
+
255
+ test('threshold can be reached by eight of more than eight owners', () => {
256
+ const owners = Array.from({ length: 9 }, (_, i) => ({
257
+ owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`,
258
+ weight: i === 0 ? 2 : 1,
259
+ }))
260
+ expect(MultisigConfig.validate({ threshold: 9, owners })).toBe(true)
261
+ })
262
+
229
263
  test('total weight exceeds u8 max', () => {
230
264
  expect(
231
265
  MultisigConfig.validate({
@@ -6,13 +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 = 50
9
+ export const maxOwners = 48
10
10
 
11
11
  /** Maximum threshold accepted by a native multisig config. */
12
- export const maxThreshold = 8
12
+ export const maxThreshold = 0xff
13
13
 
14
14
  /** Maximum number of owner approvals in a native multisig signature. */
15
- export const maxSignatures = maxThreshold
15
+ export const maxSignatures = 8
16
16
 
17
17
  /**
18
18
  * Maximum number of native multisig signatures in one nested authorization
@@ -72,7 +72,8 @@ export type Tuple = readonly [
72
72
  * Mirrors the Tempo `InitMultisig::validate` rules: owners non-empty and
73
73
  * `<= maxOwners`, strictly ascending unique nonzero owner addresses, nonzero
74
74
  * integer owner weights, integer `threshold` between `1` and `maxThreshold`,
75
- * total weight `<= 255` (u8 max), and `threshold <= total weight`.
75
+ * total weight `<= 255` (u8 max), and a threshold reachable by at most
76
+ * `maxSignatures` owners.
76
77
  *
77
78
  * @example
78
79
  * ```ts twoslash
@@ -105,6 +106,7 @@ export function assert<numberType = number>(config: Config<numberType>): void {
105
106
  throw new InvalidConfigError({ reason: 'threshold exceeds max threshold' })
106
107
 
107
108
  let totalWeight = 0
109
+ const weights: number[] = []
108
110
  let previous: bigint | undefined
109
111
  for (const owner of owners) {
110
112
  if (!Address.validate(owner.owner) || Hex.toBigInt(owner.owner) === 0n)
@@ -123,7 +125,9 @@ export function assert<numberType = number>(config: Config<numberType>): void {
123
125
  })
124
126
  previous = current
125
127
 
126
- totalWeight += Number(owner.weight)
128
+ const weight = Number(owner.weight)
129
+ totalWeight += weight
130
+ weights.push(weight)
127
131
  }
128
132
 
129
133
  if (totalWeight > 0xff)
@@ -134,6 +138,15 @@ export function assert<numberType = number>(config: Config<numberType>): void {
134
138
  throw new InvalidConfigError({
135
139
  reason: 'threshold exceeds total owner weight',
136
140
  })
141
+
142
+ const reachableWeight = weights
143
+ .sort((a, b) => b - a)
144
+ .slice(0, maxSignatures)
145
+ .reduce((sum, weight) => sum + weight, 0)
146
+ if (Number(threshold) > reachableWeight)
147
+ throw new InvalidConfigError({
148
+ reason: `threshold exceeds weight reachable by ${maxSignatures} owner signatures`,
149
+ })
137
150
  }
138
151
 
139
152
  export declare namespace assert {
package/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** @internal */
2
- export const version = '0.14.34'
2
+ export const version = '0.14.35'