Skip to content

Commit d2329ab

Browse files
Merge pull request #886 from 0xsequence/decodeSessionSignature
Add session signature decoding
2 parents c30c3ea + 11c060b commit d2329ab

File tree

6 files changed

+471
-103
lines changed

6 files changed

+471
-103
lines changed

packages/wallet/core/src/signers/session-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ export class SessionManager implements SapientSigner {
325325
}
326326

327327
// Perform encoding
328-
const encodedSignature = SessionSignature.encodeSessionCallSignatures(
328+
const encodedSignature = SessionSignature.encodeSessionSignature(
329329
signatures,
330330
await this.topology,
331331
identitySigner,

packages/wallet/primitives-cli/src/subcommands/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export async function doEncodeSessionCallSignatures(
3333
}
3434
identitySigner = identitySigners[0]!
3535
}
36-
const encoded = SessionSignature.encodeSessionCallSignatures(
36+
const encoded = SessionSignature.encodeSessionSignature(
3737
callSignatures,
3838
sessionTopology,
3939
identitySigner as `0x${string}`,

packages/wallet/primitives/src/session-config.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ export type SessionLeaf = SessionPermissionsLeaf | ImplicitBlacklistLeaf | Ident
3535
export type SessionBranch = [SessionsTopology, SessionsTopology, ...SessionsTopology[]]
3636
export type SessionsTopology = SessionBranch | SessionLeaf | SessionNode
3737

38+
const SESSIONS_NODE_SIZE_BYTES = 32
39+
3840
function isSessionsNode(topology: any): topology is SessionNode {
39-
return Hex.validate(topology) && Hex.size(topology) === 32
41+
return Hex.validate(topology) && Hex.size(topology) === SESSIONS_NODE_SIZE_BYTES
4042
}
4143

4244
function isImplicitBlacklist(topology: any): topology is ImplicitBlacklistLeaf {
@@ -342,6 +344,90 @@ export function encodeSessionsTopology(topology: SessionsTopology): Bytes.Bytes
342344
throw new Error('Invalid topology')
343345
}
344346

347+
export function decodeSessionsTopology(bytes: Bytes.Bytes): SessionsTopology {
348+
const { topology } = decodeSessionTopologyPointer(bytes)
349+
return topology
350+
}
351+
352+
function decodeSessionTopologyPointer(bytes: Bytes.Bytes): {
353+
topology: SessionsTopology
354+
pointer: number
355+
} {
356+
if (bytes.length === 0) {
357+
throw new Error('Empty topology bytes')
358+
}
359+
360+
const flagByte = bytes[0]!
361+
const flag = (flagByte & 0xf0) >> 4
362+
const sizeSize = flagByte & 0x0f
363+
364+
if (flag === SESSIONS_FLAG_BRANCH) {
365+
// Branch
366+
if (sizeSize === 0 || sizeSize > 15) {
367+
throw new Error('Invalid branch size')
368+
}
369+
370+
let offset = 1
371+
const encodedLength = Bytes.toNumber(bytes.slice(offset, offset + sizeSize))
372+
offset += sizeSize
373+
374+
const encodedBranches = bytes.slice(offset, offset + encodedLength)
375+
const branches: SessionsTopology[] = []
376+
377+
let branchOffset = 0
378+
while (branchOffset < encodedBranches.length) {
379+
const { topology: branchTopology, pointer: branchPointer } = decodeSessionTopologyPointer(
380+
encodedBranches.slice(branchOffset),
381+
)
382+
branches.push(branchTopology)
383+
branchOffset += branchPointer
384+
}
385+
386+
return { topology: branches as SessionsTopology, pointer: offset + encodedLength }
387+
} else if (flag === SESSIONS_FLAG_PERMISSIONS) {
388+
// Permissions
389+
const sessionPermissions = decodeSessionPermissions(bytes.slice(1))
390+
const nodeLength = 1 + encodeSessionPermissions(sessionPermissions).length
391+
return { topology: { type: 'session-permissions', ...sessionPermissions }, pointer: nodeLength }
392+
} else if (flag === SESSIONS_FLAG_NODE) {
393+
// Node
394+
const nodeLength = SESSIONS_NODE_SIZE_BYTES + 1
395+
if (bytes.length < nodeLength) {
396+
throw new Error('Invalid node length')
397+
}
398+
return { topology: Hex.fromBytes(bytes.slice(1, nodeLength)), pointer: nodeLength }
399+
} else if (flag === SESSIONS_FLAG_BLACKLIST) {
400+
// Blacklist
401+
let offset = 1
402+
let blacklistLength = sizeSize
403+
if (sizeSize === 0x0f) {
404+
// Size is encoded in the next 2 bytes
405+
blacklistLength = Bytes.toNumber(bytes.slice(offset, offset + 2))
406+
offset += 2
407+
}
408+
409+
const blacklist: Address.Address[] = []
410+
for (let i = 0; i < blacklistLength; i++) {
411+
const addressBytes = bytes.slice(offset + i * 20, offset + (i + 1) * 20)
412+
blacklist.push(Address.from(Hex.fromBytes(addressBytes)))
413+
}
414+
415+
return { topology: { type: 'implicit-blacklist', blacklist }, pointer: offset + blacklistLength * 20 }
416+
} else if (flag === SESSIONS_FLAG_IDENTITY_SIGNER) {
417+
// Identity signer
418+
const nodeLength = 21 // Flag + address
419+
if (bytes.length < nodeLength) {
420+
throw new Error('Invalid identity signer length')
421+
}
422+
return {
423+
topology: { type: 'identity-signer', identitySigner: Address.from(Hex.fromBytes(bytes.slice(1, nodeLength))) },
424+
pointer: nodeLength,
425+
}
426+
} else {
427+
throw new Error(`Invalid topology flag: ${flag}`)
428+
}
429+
}
430+
345431
// JSON
346432

347433
export function sessionsTopologyToJson(topology: SessionsTopology): string {

packages/wallet/primitives/src/session-signature.ts

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { Address, Bytes, Hash, Hex } from 'ox'
2-
import { Attestation, encode, encodeForJson, fromParsed, toJson } from './attestation.js'
32
import { MAX_PERMISSIONS_COUNT } from './permission.js'
43
import {
4+
decodeSessionsTopology,
55
encodeSessionsTopology,
66
getIdentitySigners,
77
isCompleteSessionsTopology,
88
minimiseSessionsTopology,
99
SessionsTopology,
1010
} from './session-config.js'
1111
import { RSY } from './signature.js'
12-
import { minBytesFor, packRSY } from './utils.js'
13-
import { Payload } from './index.js'
12+
import { minBytesFor, packRSY, unpackRSY } from './utils.js'
13+
import { Attestation, Payload } from './index.js'
1414

1515
export type ImplicitSessionCallSignature = {
16-
attestation: Attestation
16+
attestation: Attestation.Attestation
1717
identitySignature: RSY
1818
sessionSignature: RSY
1919
}
@@ -46,7 +46,7 @@ export function sessionCallSignatureToJson(callSignature: SessionCallSignature):
4646
export function encodeSessionCallSignatureForJson(callSignature: SessionCallSignature): any {
4747
if (isImplicitSessionCallSignature(callSignature)) {
4848
return {
49-
attestation: encodeForJson(callSignature.attestation),
49+
attestation: Attestation.encodeForJson(callSignature.attestation),
5050
identitySignature: rsyToRsvStr(callSignature.identitySignature),
5151
sessionSignature: rsyToRsvStr(callSignature.sessionSignature),
5252
}
@@ -68,7 +68,7 @@ export function sessionCallSignatureFromJson(json: string): SessionCallSignature
6868
export function sessionCallSignatureFromParsed(decoded: any): SessionCallSignature {
6969
if (decoded.attestation) {
7070
return {
71-
attestation: fromParsed(decoded.attestation),
71+
attestation: Attestation.fromParsed(decoded.attestation),
7272
identitySignature: rsyFromRsvStr(decoded.identitySignature),
7373
sessionSignature: rsyFromRsvStr(decoded.sessionSignature),
7474
}
@@ -113,7 +113,7 @@ function rsyFromRsvStr(sigStr: string): RSY {
113113
* @param identitySigner The identity signer to encode. Others will be hashed into nodes.
114114
* @returns The encoded session call signatures.
115115
*/
116-
export function encodeSessionCallSignatures(
116+
export function encodeSessionSignature(
117117
callSignatures: SessionCallSignature[],
118118
topology: SessionsTopology,
119119
identitySigner: Address.Address,
@@ -151,10 +151,12 @@ export function encodeSessionCallSignatures(
151151
// Map each call signature to its attestation index
152152
callSignatures.filter(isImplicitSessionCallSignature).forEach((callSig) => {
153153
if (callSig.attestation) {
154-
const attestationStr = toJson(callSig.attestation)
154+
const attestationStr = Attestation.toJson(callSig.attestation)
155155
if (!attestationMap.has(attestationStr)) {
156156
attestationMap.set(attestationStr, encodedAttestations.length)
157-
encodedAttestations.push(Bytes.concat(encode(callSig.attestation), packRSY(callSig.identitySignature)))
157+
encodedAttestations.push(
158+
Bytes.concat(Attestation.encode(callSig.attestation), packRSY(callSig.identitySignature)),
159+
)
158160
}
159161
}
160162
})
@@ -169,7 +171,7 @@ export function encodeSessionCallSignatures(
169171
for (const callSignature of callSignatures) {
170172
if (isImplicitSessionCallSignature(callSignature)) {
171173
// Implicit
172-
const attestationStr = toJson(callSignature.attestation)
174+
const attestationStr = Attestation.toJson(callSignature.attestation)
173175
const attestationIndex = attestationMap.get(attestationStr)
174176
if (attestationIndex === undefined) {
175177
// Unreachable
@@ -193,7 +195,83 @@ export function encodeSessionCallSignatures(
193195
return Bytes.concat(...parts)
194196
}
195197

196-
// Helper
198+
export function decodeSessionSignature(encodedSignatures: Bytes.Bytes): {
199+
topology: SessionsTopology
200+
callSignatures: SessionCallSignature[]
201+
} {
202+
let offset = 0
203+
204+
// Parse session topology length (3 bytes)
205+
const topologyLength = Bytes.toNumber(encodedSignatures.slice(offset, offset + 3))
206+
offset += 3
207+
208+
// Parse session topology
209+
const topologyBytes = encodedSignatures.slice(offset, offset + topologyLength)
210+
offset += topologyLength
211+
const topology = decodeSessionsTopology(topologyBytes)
212+
213+
// Parse attestations count (1 byte)
214+
const attestationsCount = Bytes.toNumber(encodedSignatures.slice(offset, offset + 1))
215+
offset += 1
216+
217+
// Parse attestations and identity signatures
218+
const attestations: Attestation.Attestation[] = []
219+
const identitySignatures: RSY[] = []
220+
221+
for (let i = 0; i < attestationsCount; i++) {
222+
// Parse attestation
223+
const attestation = Attestation.decode(encodedSignatures.slice(offset))
224+
offset += Attestation.encode(attestation).length
225+
attestations.push(attestation)
226+
227+
// Parse identity signature (64 bytes)
228+
const identitySignature = unpackRSY(encodedSignatures.slice(offset, offset + 64))
229+
offset += 64
230+
identitySignatures.push(identitySignature)
231+
}
232+
233+
// Parse call signatures
234+
const callSignatures: SessionCallSignature[] = []
235+
236+
while (offset < encodedSignatures.length) {
237+
// Parse flag byte
238+
const flagByte = encodedSignatures[offset]!
239+
offset += 1
240+
241+
// Parse session signature (64 bytes)
242+
const sessionSignature = unpackRSY(encodedSignatures.slice(offset, offset + 64))
243+
offset += 64
244+
245+
// Check if implicit (MSB set) or explicit
246+
if ((flagByte & 0x80) !== 0) {
247+
// Implicit call signature
248+
const attestationIndex = flagByte & 0x7f
249+
if (attestationIndex >= attestations.length) {
250+
throw new Error('Invalid attestation index')
251+
}
252+
253+
callSignatures.push({
254+
attestation: attestations[attestationIndex]!,
255+
identitySignature: identitySignatures[attestationIndex]!,
256+
sessionSignature,
257+
})
258+
} else {
259+
// Explicit call signature
260+
const permissionIndex = flagByte
261+
callSignatures.push({
262+
permissionIndex: BigInt(permissionIndex),
263+
sessionSignature,
264+
})
265+
}
266+
}
267+
268+
return {
269+
topology,
270+
callSignatures,
271+
}
272+
}
273+
274+
// Call encoding
197275

198276
export function hashCallWithReplayProtection(
199277
payload: Payload.Calls,

0 commit comments

Comments
 (0)