Skip to content

Commit d8e5b96

Browse files
committed
fix(wallet): minimize config update topologies with subdigest weight
1 parent a24bb60 commit d8e5b96

5 files changed

Lines changed: 146 additions & 29 deletions

File tree

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,6 @@ export class Provider implements ProviderInterface {
285285
}),
286286
])
287287

288-
let totalWeight = 0n
289288
const encoded = Signature.fillLeaves(fromConfig.topology, (leaf) => {
290289
if (Config.isSapientSignerLeaf(leaf)) {
291290
const sapientSignature = signaturesOfSigners.find(
@@ -295,7 +294,6 @@ export class Provider implements ProviderInterface {
295294
)?.signature
296295

297296
if (sapientSignature) {
298-
totalWeight += leaf.weight
299297
return sapientSignature
300298
}
301299
}
@@ -305,15 +303,21 @@ export class Provider implements ProviderInterface {
305303
return undefined
306304
}
307305

308-
totalWeight += leaf.weight
309306
return signature
310307
})
311308

309+
const topologyContext: Config.TopologyWeightContext = {
310+
wallet,
311+
chainId: candidate.payload.chainId,
312+
payload: candidate.payload.content,
313+
}
314+
const { weight: totalWeight } = Config.getWeight(encoded, () => false, topologyContext)
315+
312316
if (totalWeight < fromConfig.threshold) {
313317
continue
314318
}
315319

316-
const minimalTopology = Config.minimiseSignedTopology(encoded, fromConfig.threshold)
320+
const minimalTopology = Config.minimiseSignedTopology(encoded, fromConfig.threshold, topologyContext)
317321

318322
best = {
319323
nextImageHash: candidate.nextImageHash,

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,12 +244,17 @@ export class Provider implements ProviderInterface {
244244
Hex.assert(toImageHash)
245245
Hex.assert(signature)
246246

247+
const payload = Payload.fromConfigUpdate(toImageHash)
247248
const decoded = Signature.decodeSignature(Hex.toBytes(signature))
248249

249-
const { configuration } = await Signature.recover(decoded, wallet, 0, Payload.fromConfigUpdate(toImageHash), {
250+
const { configuration } = await Signature.recover(decoded, wallet, 0, payload, {
250251
provider: passkeySignatureValidator,
251252
})
252-
const topology = Config.minimiseSignedTopology(configuration.topology, configuration.threshold)
253+
const topology = Config.minimiseSignedTopology(configuration.topology, configuration.threshold, {
254+
wallet,
255+
chainId: 0,
256+
payload,
257+
})
253258

254259
return { imageHash: toImageHash, signature: { ...decoded, configuration: { ...configuration, topology } } }
255260
}),

packages/wallet/primitives/src/config.ts

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
SignatureOfSapientSignerLeaf,
1111
SignatureOfSignerLeaf,
1212
} from './signature.js'
13+
import { hash as hashPayload } from './payload.js'
14+
import type { Parented } from './payload.js'
1315
import { Constants } from './index.js'
1416

1517
export type SignerLeaf = {
@@ -104,6 +106,19 @@ export type Config = {
104106
checkpointer?: Address.Address
105107
}
106108

109+
export type TopologyWeightContext = {
110+
wallet: Address.Address
111+
chainId: number
112+
payload: Parented
113+
}
114+
115+
export const MATCHING_SUBDIGEST_WEIGHT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn
116+
117+
type ResolvedTopologyWeightContext = {
118+
digest: Bytes.Bytes
119+
anyAddressDigest: Bytes.Bytes
120+
}
121+
107122
export function isSignerLeaf(cand: unknown): cand is SignerLeaf {
108123
return typeof cand === 'object' && cand !== null && 'type' in cand && cand.type === 'signer'
109124
}
@@ -209,6 +224,41 @@ export function findSignerLeaf(
209224
export function getWeight(
210225
topology: RawTopology | RawConfig | Config,
211226
canSign: (signer: SignerLeaf | SapientSignerLeaf) => boolean,
227+
context?: TopologyWeightContext,
228+
): { weight: bigint; maxWeight: bigint } {
229+
return getWeightWithContext(topology, canSign, resolveTopologyWeightContext(context))
230+
}
231+
232+
function resolveTopologyWeightContext(context?: TopologyWeightContext): ResolvedTopologyWeightContext | undefined {
233+
if (!context) {
234+
return undefined
235+
}
236+
237+
return {
238+
digest: hashPayload(context.wallet, context.chainId, context.payload),
239+
anyAddressDigest: hashPayload(Constants.ZeroAddress, context.chainId, context.payload),
240+
}
241+
}
242+
243+
function getSubdigestWeight(
244+
topology: SubdigestLeaf | AnyAddressSubdigestLeaf,
245+
context?: ResolvedTopologyWeightContext,
246+
): bigint {
247+
if (!context) {
248+
return 0n
249+
}
250+
251+
if (isSubdigestLeaf(topology)) {
252+
return Bytes.isEqual(Bytes.fromHex(topology.digest), context.digest) ? MATCHING_SUBDIGEST_WEIGHT : 0n
253+
}
254+
255+
return Bytes.isEqual(Bytes.fromHex(topology.digest), context.anyAddressDigest) ? MATCHING_SUBDIGEST_WEIGHT : 0n
256+
}
257+
258+
function getWeightWithContext(
259+
topology: RawTopology | RawConfig | Config,
260+
canSign: (signer: SignerLeaf | SapientSignerLeaf) => boolean,
261+
context?: ResolvedTopologyWeightContext,
212262
): { weight: bigint; maxWeight: bigint } {
213263
topology = isRawConfig(topology) || isConfig(topology) ? topology.topology : topology
214264

@@ -223,19 +273,24 @@ export function getWeight(
223273
} else if (isSapientSignerLeaf(topology)) {
224274
return { weight: 0n, maxWeight: canSign(topology) ? topology.weight : 0n }
225275
} else if (isSubdigestLeaf(topology)) {
226-
return { weight: 0n, maxWeight: 0n }
276+
const weight = getSubdigestWeight(topology, context)
277+
return { weight, maxWeight: weight }
227278
} else if (isAnyAddressSubdigestLeaf(topology)) {
228-
return { weight: 0n, maxWeight: 0n }
279+
const weight = getSubdigestWeight(topology, context)
280+
return { weight, maxWeight: weight }
229281
} else if (isRawNestedLeaf(topology)) {
230-
const { weight, maxWeight } = getWeight(topology.tree, canSign)
282+
const { weight, maxWeight } = getWeightWithContext(topology.tree, canSign, context)
231283
return {
232284
weight: weight >= topology.threshold ? topology.weight : 0n,
233285
maxWeight: maxWeight >= topology.threshold ? topology.weight : 0n,
234286
}
235287
} else if (isNodeLeaf(topology)) {
236288
return { weight: 0n, maxWeight: 0n }
237289
} else {
238-
const [left, right] = [getWeight(topology[0], canSign), getWeight(topology[1], canSign)]
290+
const [left, right] = [
291+
getWeightWithContext(topology[0], canSign, context),
292+
getWeightWithContext(topology[1], canSign, context),
293+
]
239294
return { weight: left.weight + right.weight, maxWeight: left.maxWeight + right.maxWeight }
240295
}
241296
}
@@ -250,26 +305,27 @@ function stripSignedState(leaf: SignerLeaf | SapientSignerLeaf): SignerLeaf | Sa
250305
return rest
251306
}
252307

253-
function buildMinimisedTopologyPlans(topology: Topology): Array<MinimisedTopologyPlan | undefined> {
308+
function buildMinimisedTopologyPlans(
309+
topology: Topology,
310+
context?: ResolvedTopologyWeightContext,
311+
): Array<MinimisedTopologyPlan | undefined> {
254312
if (isSignedSignerLeaf(topology) || isSignedSapientSignerLeaf(topology)) {
255313
return [
256314
{ weight: 0n, topology: stripSignedState(topology) },
257315
{ weight: topology.weight, topology },
258316
]
259317
}
260318

261-
if (
262-
isSignerLeaf(topology) ||
263-
isSapientSignerLeaf(topology) ||
264-
isSubdigestLeaf(topology) ||
265-
isAnyAddressSubdigestLeaf(topology) ||
266-
isNodeLeaf(topology)
267-
) {
319+
if (isSubdigestLeaf(topology) || isAnyAddressSubdigestLeaf(topology)) {
320+
return [{ weight: getSubdigestWeight(topology, context), topology }]
321+
}
322+
323+
if (isSignerLeaf(topology) || isSapientSignerLeaf(topology) || isNodeLeaf(topology)) {
268324
return [{ weight: 0n, topology }]
269325
}
270326

271327
if (isNestedLeaf(topology)) {
272-
return buildMinimisedTopologyPlans(topology.tree).map((plan) => {
328+
return buildMinimisedTopologyPlans(topology.tree, context).map((plan) => {
273329
if (!plan) {
274330
return undefined
275331
}
@@ -285,8 +341,8 @@ function buildMinimisedTopologyPlans(topology: Topology): Array<MinimisedTopolog
285341
}
286342

287343
if (isNode(topology)) {
288-
const leftPlans = buildMinimisedTopologyPlans(topology[0])
289-
const rightPlans = buildMinimisedTopologyPlans(topology[1])
344+
const leftPlans = buildMinimisedTopologyPlans(topology[0], context)
345+
const rightPlans = buildMinimisedTopologyPlans(topology[1], context)
290346
const plans = new Array<MinimisedTopologyPlan | undefined>(leftPlans.length + rightPlans.length - 1)
291347

292348
for (let total = 0; total < plans.length; total++) {
@@ -318,8 +374,8 @@ function buildMinimisedTopologyPlans(topology: Topology): Array<MinimisedTopolog
318374
throw new Error('Invalid topology')
319375
}
320376

321-
export function minimiseSignedTopology(topology: Topology, threshold: bigint): Topology {
322-
const plans = buildMinimisedTopologyPlans(topology)
377+
export function minimiseSignedTopology(topology: Topology, threshold: bigint, context?: TopologyWeightContext): Topology {
378+
const plans = buildMinimisedTopologyPlans(topology, resolveTopologyWeightContext(context))
323379
return plans.find((plan) => plan && plan.weight >= threshold)?.topology ?? topology
324380
}
325381

packages/wallet/primitives/src/signature.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SignerLeaf,
88
SubdigestLeaf,
99
AnyAddressSubdigestLeaf,
10+
MATCHING_SUBDIGEST_WEIGHT,
1011
Topology,
1112
hashConfiguration,
1213
isNestedLeaf,
@@ -1312,17 +1313,13 @@ async function recoverTopology(
13121313
} else if (isSubdigestLeaf(topology)) {
13131314
return {
13141315
topology,
1315-
weight: Bytes.isEqual(Bytes.fromHex(topology.digest), digest)
1316-
? 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn
1317-
: 0n,
1316+
weight: Bytes.isEqual(Bytes.fromHex(topology.digest), digest) ? MATCHING_SUBDIGEST_WEIGHT : 0n,
13181317
}
13191318
} else if (isAnyAddressSubdigestLeaf(topology)) {
13201319
const anyAddressOpHash = hash(Constants.ZeroAddress, chainId, payload)
13211320
return {
13221321
topology,
1323-
weight: Bytes.isEqual(Bytes.fromHex(topology.digest), anyAddressOpHash)
1324-
? 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn
1325-
: 0n,
1322+
weight: Bytes.isEqual(Bytes.fromHex(topology.digest), anyAddressOpHash) ? MATCHING_SUBDIGEST_WEIGHT : 0n,
13261323
}
13271324
} else if (isNodeLeaf(topology)) {
13281325
return { topology, weight: 0n }

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
getSigners,
2525
findSignerLeaf,
2626
getWeight,
27+
MATCHING_SUBDIGEST_WEIGHT,
2728
minimiseSignedTopology,
2829
hashConfiguration,
2930
flatLeavesToTopology,
@@ -36,6 +37,7 @@ import {
3637
normalizeSignerSignature,
3738
replaceAddress,
3839
} from '../src/config.js'
40+
import { fromConfigUpdate, hash as hashPayload } from '../src/payload.js'
3941

4042
describe('Config', () => {
4143
const testAddress1 = '0x742d35cc6635c0532925a3b8d563a6b35b7f05f1'
@@ -508,6 +510,59 @@ describe('Config', () => {
508510
])
509511
expect(getWeight(result, () => false).weight).toBe(7n)
510512
})
513+
514+
it('should strip signer signatures when a matching subdigest satisfies the threshold', () => {
515+
const payload = fromConfigUpdate(
516+
'0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as `0x${string}`,
517+
)
518+
const topology = flatLeavesToTopology([
519+
{
520+
type: 'subdigest',
521+
digest: Bytes.toHex(hashPayload(testAddress1, 0, payload)) as `0x${string}`,
522+
},
523+
signSigner('0x4000000000000000000000000000000000000001', 1n, 31n),
524+
signSigner('0x4000000000000000000000000000000000000002', 1n, 33n),
525+
])
526+
527+
expect(signedAddresses(minimiseSignedTopology(topology, 1n))).toEqual([
528+
'0x4000000000000000000000000000000000000001',
529+
])
530+
531+
const result = minimiseSignedTopology(topology, 1n, {
532+
wallet: testAddress1,
533+
chainId: 0,
534+
payload,
535+
})
536+
537+
expect(signedAddresses(result)).toEqual([])
538+
expect(getWeight(result, () => false, { wallet: testAddress1, chainId: 0, payload }).weight).toBe(
539+
MATCHING_SUBDIGEST_WEIGHT,
540+
)
541+
})
542+
543+
it('should strip signer signatures when a matching any-address subdigest satisfies the threshold', () => {
544+
const payload = fromConfigUpdate(
545+
'0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' as `0x${string}`,
546+
)
547+
const topology = flatLeavesToTopology([
548+
{
549+
type: 'any-address-subdigest',
550+
digest: Bytes.toHex(
551+
hashPayload('0x0000000000000000000000000000000000000000', 0, payload),
552+
) as `0x${string}`,
553+
},
554+
signSigner('0x5000000000000000000000000000000000000001', 1n, 41n),
555+
signSigner('0x5000000000000000000000000000000000000002', 1n, 43n),
556+
])
557+
558+
const result = minimiseSignedTopology(topology, 1n, {
559+
wallet: testAddress1,
560+
chainId: 0,
561+
payload,
562+
})
563+
564+
expect(signedAddresses(result)).toEqual([])
565+
})
511566
})
512567

513568
describe('hashConfiguration', () => {

0 commit comments

Comments
 (0)