Skip to content

Commit a24bb60

Browse files
committed
primitives: minimiseSignedTopology
1 parent e3541f2 commit a24bb60

4 files changed

Lines changed: 181 additions & 2 deletions

File tree

packages/wallet/core/src/state/local/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ export class Provider implements ProviderInterface {
313313
continue
314314
}
315315

316+
const minimalTopology = Config.minimiseSignedTopology(encoded, fromConfig.threshold)
317+
316318
best = {
317319
nextImageHash: candidate.nextImageHash,
318320
checkpoint: candidate.config!.checkpoint,
@@ -321,7 +323,7 @@ export class Provider implements ProviderInterface {
321323
configuration: {
322324
threshold: fromConfig.threshold,
323325
checkpoint: fromConfig.checkpoint,
324-
topology: encoded,
326+
topology: minimalTopology,
325327
},
326328
},
327329
}

packages/wallet/core/src/state/sequence/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,9 @@ export class Provider implements ProviderInterface {
249249
const { configuration } = await Signature.recover(decoded, wallet, 0, Payload.fromConfigUpdate(toImageHash), {
250250
provider: passkeySignatureValidator,
251251
})
252+
const topology = Config.minimiseSignedTopology(configuration.topology, configuration.threshold)
252253

253-
return { imageHash: toImageHash, signature: { ...decoded, configuration } }
254+
return { imageHash: toImageHash, signature: { ...decoded, configuration: { ...configuration, topology } } }
254255
}),
255256
)
256257
}

packages/wallet/primitives/src/config.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,89 @@ export function getWeight(
240240
}
241241
}
242242

243+
type MinimisedTopologyPlan = {
244+
weight: bigint
245+
topology: Topology
246+
}
247+
248+
function stripSignedState(leaf: SignerLeaf | SapientSignerLeaf): SignerLeaf | SapientSignerLeaf {
249+
const { signed: _signed, signature: _signature, ...rest } = leaf
250+
return rest
251+
}
252+
253+
function buildMinimisedTopologyPlans(topology: Topology): Array<MinimisedTopologyPlan | undefined> {
254+
if (isSignedSignerLeaf(topology) || isSignedSapientSignerLeaf(topology)) {
255+
return [
256+
{ weight: 0n, topology: stripSignedState(topology) },
257+
{ weight: topology.weight, topology },
258+
]
259+
}
260+
261+
if (
262+
isSignerLeaf(topology) ||
263+
isSapientSignerLeaf(topology) ||
264+
isSubdigestLeaf(topology) ||
265+
isAnyAddressSubdigestLeaf(topology) ||
266+
isNodeLeaf(topology)
267+
) {
268+
return [{ weight: 0n, topology }]
269+
}
270+
271+
if (isNestedLeaf(topology)) {
272+
return buildMinimisedTopologyPlans(topology.tree).map((plan) => {
273+
if (!plan) {
274+
return undefined
275+
}
276+
277+
return {
278+
weight: plan.weight >= topology.threshold ? topology.weight : 0n,
279+
topology: {
280+
...topology,
281+
tree: plan.topology,
282+
},
283+
}
284+
})
285+
}
286+
287+
if (isNode(topology)) {
288+
const leftPlans = buildMinimisedTopologyPlans(topology[0])
289+
const rightPlans = buildMinimisedTopologyPlans(topology[1])
290+
const plans = new Array<MinimisedTopologyPlan | undefined>(leftPlans.length + rightPlans.length - 1)
291+
292+
for (let total = 0; total < plans.length; total++) {
293+
const maxLeft = Math.min(total, leftPlans.length - 1)
294+
const minLeft = Math.max(0, total - (rightPlans.length - 1))
295+
296+
// Iterate from right to left so earlier topology positions win ties.
297+
for (let leftCount = maxLeft; leftCount >= minLeft; leftCount--) {
298+
const leftPlan = leftPlans[leftCount]
299+
const rightPlan = rightPlans[total - leftCount]
300+
301+
if (!leftPlan || !rightPlan) {
302+
continue
303+
}
304+
305+
const weight = leftPlan.weight + rightPlan.weight
306+
if (!plans[total] || weight > plans[total]!.weight) {
307+
plans[total] = {
308+
weight,
309+
topology: [leftPlan.topology, rightPlan.topology],
310+
}
311+
}
312+
}
313+
}
314+
315+
return plans
316+
}
317+
318+
throw new Error('Invalid topology')
319+
}
320+
321+
export function minimiseSignedTopology(topology: Topology, threshold: bigint): Topology {
322+
const plans = buildMinimisedTopologyPlans(topology)
323+
return plans.find((plan) => plan && plan.weight >= threshold)?.topology ?? topology
324+
}
325+
243326
export function hashConfiguration(topology: Topology | Config): Bytes.Bytes {
244327
if (isConfig(topology)) {
245328
let root = hashConfiguration(topology.topology)

packages/wallet/primitives/test/config.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
getSigners,
2525
findSignerLeaf,
2626
getWeight,
27+
minimiseSignedTopology,
2728
hashConfiguration,
2829
flatLeavesToTopology,
2930
configToJson,
@@ -91,6 +92,37 @@ describe('Config', () => {
9192
checkpointer: testAddress1,
9293
}
9394

95+
function signedAddresses(topology: Topology): string[] {
96+
if (isNode(topology)) {
97+
return [...signedAddresses(topology[0]), ...signedAddresses(topology[1])]
98+
}
99+
100+
if (isNestedLeaf(topology)) {
101+
return signedAddresses(topology.tree)
102+
}
103+
104+
if ((isSignerLeaf(topology) || isSapientSignerLeaf(topology)) && topology.signature) {
105+
return [topology.address]
106+
}
107+
108+
return []
109+
}
110+
111+
function signSigner(address: string, weight: bigint, nonce: bigint): SignerLeaf {
112+
return {
113+
type: 'signer',
114+
address,
115+
weight,
116+
signed: true,
117+
signature: {
118+
type: 'hash',
119+
r: nonce,
120+
s: nonce + 1n,
121+
yParity: 0,
122+
},
123+
}
124+
}
125+
94126
describe('Type Guards', () => {
95127
describe('isSignerLeaf', () => {
96128
it('should return true for valid signer leaf', () => {
@@ -417,6 +449,67 @@ describe('Config', () => {
417449
})
418450
})
419451

452+
describe('minimiseSignedTopology', () => {
453+
it('should prefer the smallest signature count that still meets threshold', () => {
454+
const topology = flatLeavesToTopology([
455+
signSigner('0x1000000000000000000000000000000000000001', 4n, 1n),
456+
signSigner('0x1000000000000000000000000000000000000002', 4n, 3n),
457+
signSigner('0x1000000000000000000000000000000000000003', 4n, 5n),
458+
signSigner('0x1000000000000000000000000000000000000004', 6n, 7n),
459+
signSigner('0x1000000000000000000000000000000000000005', 6n, 9n),
460+
])
461+
462+
const result = minimiseSignedTopology(topology, 12n)
463+
464+
expect(signedAddresses(result)).toEqual([
465+
'0x1000000000000000000000000000000000000004',
466+
'0x1000000000000000000000000000000000000005',
467+
])
468+
expect(getWeight(result, () => false).weight).toBe(12n)
469+
})
470+
471+
it('should keep earlier signers when equal-count solutions tie', () => {
472+
const topology = flatLeavesToTopology([
473+
signSigner('0x2000000000000000000000000000000000000001', 1n, 11n),
474+
signSigner('0x2000000000000000000000000000000000000002', 1n, 13n),
475+
signSigner('0x2000000000000000000000000000000000000003', 1n, 15n),
476+
])
477+
478+
const result = minimiseSignedTopology(topology, 2n)
479+
480+
expect(signedAddresses(result)).toEqual([
481+
'0x2000000000000000000000000000000000000001',
482+
'0x2000000000000000000000000000000000000002',
483+
])
484+
})
485+
486+
it('should minimise nested signers while preserving nested thresholds', () => {
487+
const nested: NestedLeaf = {
488+
type: 'nested',
489+
weight: 4n,
490+
threshold: 2n,
491+
tree: flatLeavesToTopology([
492+
signSigner('0x3000000000000000000000000000000000000001', 1n, 21n),
493+
signSigner('0x3000000000000000000000000000000000000002', 1n, 23n),
494+
signSigner('0x3000000000000000000000000000000000000003', 1n, 25n),
495+
]),
496+
}
497+
const topology: Topology = [
498+
nested,
499+
signSigner('0x3000000000000000000000000000000000000004', 3n, 27n),
500+
]
501+
502+
const result = minimiseSignedTopology(topology, 5n)
503+
504+
expect(signedAddresses(result)).toEqual([
505+
'0x3000000000000000000000000000000000000001',
506+
'0x3000000000000000000000000000000000000002',
507+
'0x3000000000000000000000000000000000000004',
508+
])
509+
expect(getWeight(result, () => false).weight).toBe(7n)
510+
})
511+
})
512+
420513
describe('hashConfiguration', () => {
421514
it('should hash signer leaf correctly', () => {
422515
const hash = hashConfiguration(sampleSignerLeaf)

0 commit comments

Comments
 (0)