From ca3b7faca125f25864fa4a8b59978e459209374b Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:45:10 +1000 Subject: [PATCH] fix(tempo): align multisig config limits --- .changeset/wise-owners-reach.md | 5 ++++ src/tempo/MultisigConfig.test.ts | 46 +++++++++++++++++++++++++++----- src/tempo/MultisigConfig.ts | 23 ++++++++++++---- 3 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 .changeset/wise-owners-reach.md diff --git a/.changeset/wise-owners-reach.md b/.changeset/wise-owners-reach.md new file mode 100644 index 00000000..bc117370 --- /dev/null +++ b/.changeset/wise-owners-reach.md @@ -0,0 +1,5 @@ +--- +'ox': patch +--- + +Aligned configurable account owner and threshold validation with TIP-1061. diff --git a/src/tempo/MultisigConfig.test.ts b/src/tempo/MultisigConfig.test.ts index 988b701c..cf7d24ba 100644 --- a/src/tempo/MultisigConfig.test.ts +++ b/src/tempo/MultisigConfig.test.ts @@ -169,21 +169,21 @@ describe('assert / validate', () => { expect(MultisigConfig.validate({ threshold: 1, owners: [] })).toBe(false) }) - test('accepts 50 owners', () => { - const owners = Array.from({ length: 50 }, (_, i) => ({ + test('accepts 48 owners', () => { + const owners = Array.from({ length: 48 }, (_, i) => ({ owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`, weight: 1, })) expect( MultisigConfig.validate({ - threshold: MultisigConfig.maxThreshold, + threshold: MultisigConfig.maxSignatures, owners, }), ).toBe(true) }) test('too many owners', () => { - const owners = Array.from({ length: 51 }, (_, i) => ({ + const owners = Array.from({ length: 49 }, (_, i) => ({ owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`, weight: 1, })) @@ -199,11 +199,29 @@ describe('assert / validate', () => { ).toBe(false) }) - test('threshold exceeds protocol maximum', () => { + test('accepts a uint8 threshold above the signature limit', () => { + expect( + MultisigConfig.validate({ + threshold: 9, + owners: [{ owner: owner1, weight: 9 }], + }), + ).toBe(true) + }) + + test('accepts the maximum uint8 threshold', () => { + expect( + MultisigConfig.validate({ + threshold: MultisigConfig.maxThreshold, + owners: [{ owner: owner1, weight: MultisigConfig.maxThreshold }], + }), + ).toBe(true) + }) + + test('threshold exceeds uint8 maximum', () => { expect( MultisigConfig.validate({ threshold: MultisigConfig.maxThreshold + 1, - owners: [{ owner: owner1, weight: MultisigConfig.maxThreshold + 1 }], + owners: [{ owner: owner1, weight: MultisigConfig.maxThreshold }], }), ).toBe(false) }) @@ -226,6 +244,22 @@ describe('assert / validate', () => { ).toBe(false) }) + test('threshold cannot require more than eight owners', () => { + const owners = Array.from({ length: 9 }, (_, i) => ({ + owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`, + weight: 1, + })) + expect(MultisigConfig.validate({ threshold: 9, owners })).toBe(false) + }) + + test('threshold can be reached by eight of more than eight owners', () => { + const owners = Array.from({ length: 9 }, (_, i) => ({ + owner: `0x${(i + 1).toString(16).padStart(40, '0')}` as `0x${string}`, + weight: i === 0 ? 2 : 1, + })) + expect(MultisigConfig.validate({ threshold: 9, owners })).toBe(true) + }) + test('total weight exceeds u8 max', () => { expect( MultisigConfig.validate({ diff --git a/src/tempo/MultisigConfig.ts b/src/tempo/MultisigConfig.ts index af4ac3d9..9475ed40 100644 --- a/src/tempo/MultisigConfig.ts +++ b/src/tempo/MultisigConfig.ts @@ -6,13 +6,13 @@ import * as Hex from '../core/Hex.js' import type { Compute, OneOf } from '../core/internal/types.js' /** Maximum number of owners allowed in a native multisig config. */ -export const maxOwners = 50 +export const maxOwners = 48 /** Maximum threshold accepted by a native multisig config. */ -export const maxThreshold = 8 +export const maxThreshold = 0xff /** Maximum number of owner approvals in a native multisig signature. */ -export const maxSignatures = maxThreshold +export const maxSignatures = 8 /** * Maximum number of native multisig signatures in one nested authorization @@ -72,7 +72,8 @@ export type Tuple = readonly [ * Mirrors the Tempo `InitMultisig::validate` rules: owners non-empty and * `<= maxOwners`, strictly ascending unique nonzero owner addresses, nonzero * integer owner weights, integer `threshold` between `1` and `maxThreshold`, - * total weight `<= 255` (u8 max), and `threshold <= total weight`. + * total weight `<= 255` (u8 max), and a threshold reachable by at most + * `maxSignatures` owners. * * @example * ```ts twoslash @@ -105,6 +106,7 @@ export function assert(config: Config): void { throw new InvalidConfigError({ reason: 'threshold exceeds max threshold' }) let totalWeight = 0 + const weights: number[] = [] let previous: bigint | undefined for (const owner of owners) { if (!Address.validate(owner.owner) || Hex.toBigInt(owner.owner) === 0n) @@ -123,7 +125,9 @@ export function assert(config: Config): void { }) previous = current - totalWeight += Number(owner.weight) + const weight = Number(owner.weight) + totalWeight += weight + weights.push(weight) } if (totalWeight > 0xff) @@ -134,6 +138,15 @@ export function assert(config: Config): void { throw new InvalidConfigError({ reason: 'threshold exceeds total owner weight', }) + + const reachableWeight = weights + .sort((a, b) => b - a) + .slice(0, maxSignatures) + .reduce((sum, weight) => sum + weight, 0) + if (Number(threshold) > reachableWeight) + throw new InvalidConfigError({ + reason: `threshold exceeds weight reachable by ${maxSignatures} owner signatures`, + }) } export declare namespace assert {