Skip to content

Commit a350ce4

Browse files
committed
feat!: lsig delegation refactor
1 parent aaef7b2 commit a350ce4

File tree

8 files changed

+253
-121
lines changed

8 files changed

+253
-121
lines changed

packages/transact/src/logicsig.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { encodeMsgpack } from '@algorandfoundation/algokit-common'
1+
import { Address, encodeMsgpack } from '@algorandfoundation/algokit-common'
22
import { describe, expect, test } from 'vitest'
33
import { LogicSigAccount } from './logicsig'
44
import { LogicSignature } from './transactions/signed-transaction'
@@ -15,7 +15,7 @@ describe('logicsig', () => {
1515
} satisfies LogicSignature
1616
const encoded = encodeMsgpack(logicSignatureCodec.encode(logicSignature, 'msgpack'))
1717

18-
const decoded = LogicSigAccount.fromBytes(encoded)
18+
const decoded = LogicSigAccount.fromBytes(encoded, Address.zeroAddress())
1919

2020
expect(decoded.logic).toEqual(logicSignature.logic)
2121
expect(decoded.sig).toEqual(signature)

packages/transact/src/logicsig.ts

Lines changed: 113 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Address, concatArrays, decodeMsgpack, hash } from '@algorandfoundation/algokit-common'
1+
import { Address, Addressable, concatArrays, decodeMsgpack, hash } from '@algorandfoundation/algokit-common'
22
import { MultisigAccount } from './multisig'
3-
import { TransactionSigner } from './signer'
3+
import { AddressWithDelegatedLsigSigner, TransactionSigner } from './signer'
44
import { LogicSignature, MultisigSignature, SignedTransaction, encodeSignedTransaction } from './transactions/signed-transaction'
55
import { Transaction } from './transactions/transaction'
66
import { logicSignatureCodec } from './transactions/signed-transaction-meta'
@@ -9,43 +9,118 @@ const PROGRAM_TAG = new TextEncoder().encode('Program')
99
const MSIG_PROGRAM_TAG = new TextEncoder().encode('MsigProgram')
1010
const SIGN_PROGRAM_DATA_PREFIX = new TextEncoder().encode('ProgData')
1111

12-
/** Function for signing logic signatures for delegation */
13-
export type DelegatedLsigSigner = (lsig: LogicSigAccount, msig?: MultisigAccount) => Promise<Uint8Array>
12+
/** Function for signing logic signatures for delegation
13+
* @param lsig - The logic signature that is being signed for delegation
14+
* @param msig - Optional multisig account that should be set when a public key is signing as a subsigner of a multisig
15+
* @returns The address of the delegator
16+
* */
17+
export type DelegatedLsigSigner = (
18+
lsig: LogicSigAccount,
19+
msig?: MultisigAccount,
20+
) => Promise<{ addr: Address } & ({ sig?: Uint8Array } | { lmsig?: MultisigSignature })>
1421

1522
/** Function for signing program data for a logic signature */
16-
export type ProgramDataSigner = (data: Uint8Array, lsig: LogicSigAccount) => Promise<Uint8Array>
23+
export type ProgramDataSigner = (data: Uint8Array, lsig: LogicSig) => Promise<Uint8Array>
1724

18-
export class LogicSigAccount {
25+
export class LogicSig implements Addressable {
1926
logic: Uint8Array
2027
args: Uint8Array[]
28+
protected _addr: Address
29+
30+
constructor(program: Uint8Array, programArgs?: Array<Uint8Array>) {
31+
this.logic = program
32+
this.args = programArgs?.map((arg) => new Uint8Array(arg)) ?? []
33+
this._addr = this.address()
34+
const toBeSigned = concatArrays(PROGRAM_TAG, this.logic)
35+
const h = hash(toBeSigned)
36+
this._addr = new Address(h)
37+
}
38+
39+
static fromSignature(signature: LogicSignature): LogicSig {
40+
return new LogicSig(signature.logic, signature.args || [])
41+
}
42+
43+
static fromBytes(encodedLsig: Uint8Array): LogicSig {
44+
const decoded = decodeMsgpack(encodedLsig)
45+
const lsigSignature = logicSignatureCodec.decode(decoded, 'msgpack')
46+
return LogicSig.fromSignature(lsigSignature)
47+
}
48+
49+
address(): Address {
50+
return this._addr
51+
}
52+
53+
get addr(): Address {
54+
return this._addr
55+
}
56+
57+
bytesToSignForDelegation(msig?: MultisigAccount): Uint8Array {
58+
if (msig) {
59+
return concatArrays(MSIG_PROGRAM_TAG, msig.addr.publicKey, this.logic)
60+
} else {
61+
return concatArrays(PROGRAM_TAG, this.logic)
62+
}
63+
}
64+
65+
signProgramData(data: Uint8Array, signer: ProgramDataSigner): Promise<Uint8Array> {
66+
return signer(data, this)
67+
}
68+
69+
programDataToSign(data: Uint8Array): Uint8Array {
70+
return concatArrays(SIGN_PROGRAM_DATA_PREFIX, this.address().publicKey, data)
71+
}
72+
73+
account(): LogicSigAccount {
74+
return new LogicSigAccount(this.logic, this.args)
75+
}
76+
77+
delegatedAccount(delegator: Address): LogicSigAccount {
78+
return new LogicSigAccount(this.logic, this.args, delegator)
79+
}
80+
}
81+
82+
export class LogicSigAccount extends LogicSig {
2183
sig?: Uint8Array
2284
msig?: MultisigSignature
2385
lmsig?: MultisigSignature
2486

25-
static fromSignature(signature: LogicSignature): LogicSigAccount {
26-
const lsigAccount = new LogicSigAccount(signature.logic, signature.args || [])
27-
lsigAccount.sig = signature.sig
28-
lsigAccount.msig = signature.msig
29-
lsigAccount.lmsig = signature.lmsig
87+
static fromSignature(signature: LogicSignature, delegator?: Address): LogicSigAccount {
88+
if (signature.lmsig || signature.msig) {
89+
const msigAddr = MultisigAccount.fromSignature((signature.lmsig || signature.msig)!).addr
90+
91+
if (delegator && !msigAddr.equals(delegator)) {
92+
throw new Error('Provided delegator address does not match multisig address')
93+
}
94+
95+
const lsig = new LogicSigAccount(signature.logic, signature.args || [], msigAddr)
96+
lsig.lmsig = signature.lmsig
97+
lsig.msig = signature.msig
98+
return lsig
99+
}
100+
101+
const lsigAccount = new LogicSigAccount(signature.logic, signature.args || [], delegator)
102+
103+
if (signature.sig && delegator === undefined) {
104+
throw new Error('Delegated address must be provided when logic sig has a signature')
105+
}
106+
107+
if (signature.sig) {
108+
lsigAccount.sig = signature.sig
109+
return lsigAccount
110+
}
111+
30112
return lsigAccount
31113
}
32114

33-
static fromBytes(encodedLsig: Uint8Array): LogicSigAccount {
115+
static fromBytes(encodedLsig: Uint8Array, delegator?: Address): LogicSigAccount {
34116
const decoded = decodeMsgpack(encodedLsig)
35117
const lsigSignature = logicSignatureCodec.decode(decoded, 'msgpack')
36-
return LogicSigAccount.fromSignature(lsigSignature)
118+
return LogicSigAccount.fromSignature(lsigSignature, delegator)
37119
}
38120

39-
constructor(program: Uint8Array, programArgs?: Array<Uint8Array> | null) {
40-
if (programArgs && (!Array.isArray(programArgs) || !programArgs.every((arg) => arg.constructor === Uint8Array))) {
41-
throw new TypeError('Invalid arguments')
42-
}
43-
44-
let args: Uint8Array[] = []
45-
if (programArgs != null) args = programArgs.map((arg) => new Uint8Array(arg))
46-
47-
this.logic = program
48-
this.args = args
121+
constructor(program: Uint8Array, programArgs?: Array<Uint8Array> | null, delegator?: Address) {
122+
super(program, programArgs ?? undefined)
123+
this._addr = delegator ?? this._addr
49124
}
50125

51126
get signer(): TransactionSigner {
@@ -59,60 +134,32 @@ export class LogicSigAccount {
59134
lsig: { logic: this.logic, args: this.args, msig: this.msig, lmsig: this.lmsig, sig: this.sig },
60135
}
61136

137+
if (!stxn.txn.sender.equals(this.addr)) {
138+
stxn.authAddress = this.addr
139+
}
140+
62141
signedTxns.push(encodeSignedTransaction(stxn))
63142
}
64143

65144
return signedTxns
66145
}
67146
}
68147

69-
get addr(): Address {
70-
return this.address()
71-
}
72-
73-
/**
74-
* Compute hash of the logic sig program (that is the same as escrow account address) as string address
75-
* @returns String representation of the address
76-
*/
77-
address(): Address {
78-
const toBeSigned = concatArrays(PROGRAM_TAG, this.logic)
79-
const h = hash(toBeSigned)
80-
return new Address(h)
81-
}
82-
83-
async delegate(signer: DelegatedLsigSigner) {
84-
this.sig = await signer(this)
85-
}
86-
87-
async delegateMultisig(msig: MultisigAccount) {
88-
if (this.lmsig == undefined) {
89-
this.lmsig = {
90-
subsigs: [],
91-
version: msig.params.version,
92-
threshold: msig.params.threshold,
93-
}
94-
}
95-
for (const addrWithSigner of msig.subSigners) {
96-
const { lsigSigner, addr } = addrWithSigner
97-
const signature = await lsigSigner(this, msig)
148+
async signForDelegation(delegator: AddressWithDelegatedLsigSigner) {
149+
const result = await delegator.lsigSigner(this)
98150

99-
this.lmsig.subsigs.push({ publicKey: addr.publicKey, sig: signature })
151+
if (!result.addr.equals(this._addr)) {
152+
throw new Error(
153+
`Delagator address from signer does not match expected delegator address. Expected: ${this._addr.toString()}, got: ${result.addr.toString()}`,
154+
)
100155
}
101-
}
102156

103-
bytesToSignForDelegation(msig?: MultisigAccount): Uint8Array {
104-
if (msig) {
105-
return concatArrays(MSIG_PROGRAM_TAG, msig.addr.publicKey, this.logic)
157+
if ('sig' in result && result.sig) {
158+
this.sig = result.sig
159+
} else if ('lmsig' in result && result.lmsig) {
160+
this.lmsig = result.lmsig
106161
} else {
107-
return concatArrays(PROGRAM_TAG, this.logic)
162+
throw new Error('Delegated lsig signer must return either a sig or lmsig')
108163
}
109164
}
110-
111-
signProgramData(data: Uint8Array, signer: ProgramDataSigner): Promise<Uint8Array> {
112-
return signer(data, this)
113-
}
114-
115-
programDataToSign(data: Uint8Array): Uint8Array {
116-
return concatArrays(SIGN_PROGRAM_DATA_PREFIX, this.address().publicKey, data)
117-
}
118165
}

packages/transact/src/multisig.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
SignedTransaction,
1818
} from './transactions/signed-transaction'
1919
import { Transaction } from './transactions/transaction'
20+
import { DelegatedLsigSigner } from './logicsig'
2021

2122
const toPublicKeys = (addrs: Array<string | Address>): Uint8Array[] => addrs.map((addr) => getAddress(addr).publicKey)
2223

@@ -382,11 +383,12 @@ export interface MultisigMetadata {
382383
}
383384

384385
/** Account wrapper that supports partial or full multisig signing. */
385-
export class MultisigAccount implements AddressWithTransactionSigner {
386+
export class MultisigAccount implements AddressWithTransactionSigner, AddressWithDelegatedLsigSigner {
386387
_params: MultisigMetadata
387388
_subSigners: (AddressWithTransactionSigner & AddressWithDelegatedLsigSigner)[]
388389
_addr: Address
389390
_signer: TransactionSigner
391+
_lsigSigner: DelegatedLsigSigner
390392

391393
/** The parameters for the multisig account */
392394
get params(): Readonly<MultisigMetadata> {
@@ -408,6 +410,10 @@ export class MultisigAccount implements AddressWithTransactionSigner {
408410
return this._signer
409411
}
410412

413+
get lsigSigner(): DelegatedLsigSigner {
414+
return this._lsigSigner
415+
}
416+
411417
static fromSignature(signature: MultisigSignature): MultisigAccount {
412418
const params: MultisigMetadata = {
413419
version: signature.version,
@@ -447,6 +453,25 @@ export class MultisigAccount implements AddressWithTransactionSigner {
447453

448454
return signedMsigTxns.map(encodeSignedTransaction)
449455
}
456+
457+
this._lsigSigner = async (lsig, _) => {
458+
let lmsig = lsig.lmsig ?? this.createMultisigSignature()
459+
460+
for (const addrWithSigner of this.subSigners) {
461+
const { lsigSigner, addr } = addrWithSigner
462+
const result = await lsigSigner(lsig, this)
463+
if (!('sig' in result) || !result.sig) {
464+
console.debug(result)
465+
throw new Error(
466+
`Signer for address ${addr.toString()} did not produce a valid signature when signing logic sig for multisig account ${this._addr.toString()}`,
467+
)
468+
}
469+
470+
lmsig = this.applySignature(lmsig, addr.publicKey, result.sig)
471+
}
472+
473+
return { addr: this.addr, lmsig }
474+
}
450475
}
451476

452477
createMultisigTransaction(txn: Transaction): SignedTransaction {

packages/transact/src/signer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export function generateAddressWithSigners(args: {
7575

7676
const lsigSigner: DelegatedLsigSigner = async (lsig, msig) => {
7777
const bytesToSign = lsig.bytesToSignForDelegation(msig)
78-
return await rawEd25519Signer(bytesToSign)
78+
const sig = await rawEd25519Signer(bytesToSign)
79+
return { sig, addr: sendingAddress }
7980
}
8081

8182
const programDataSigner: ProgramDataSigner = async (data, lsig) => {

src/testing/account.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { AlgodClient } from '@algorandfoundation/algokit-algod-client'
22
import { Address } from '@algorandfoundation/algokit-common'
3-
import { AddressWithSigners, AddressWithTransactionSigner } from '@algorandfoundation/algokit-transact'
3+
import {
4+
AddressWithSigners,
5+
AddressWithTransactionSigner,
6+
DelegatedLsigSigner,
7+
MxBytesSigner,
8+
ProgramDataSigner,
9+
TransactionSigner,
10+
} from '@algorandfoundation/algokit-transact'
411
import { KmdClient } from '@algorandfoundation/algokit-kmd-client'
512
import { AlgorandClient, Config } from '../'
613
import { GetTestAccountParams } from '../types/testing'
@@ -36,7 +43,7 @@ export async function getTestAccount(
3643
{ suppressLog, initialFunds, accountGetter }: GetTestAccountParams,
3744
algodOrAlgorandClient: AlgodClient | AlgorandClient,
3845
kmd?: KmdClient,
39-
): Promise<Address & AddressWithTransactionSigner> {
46+
): Promise<Address & AddressWithSigners> {
4047
const algorand =
4148
algodOrAlgorandClient instanceof AlgorandClient
4249
? algodOrAlgorandClient
@@ -65,10 +72,13 @@ export async function getTestAccount(
6572

6673
algorand.setSignerFromAccount(account)
6774

68-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
69-
const address = Address.fromString(account.addr.toString()) as any
70-
address.addr = account.addr
71-
address.signer = algorand.account.getSigner(address)
75+
const address = Address.fromString(account.addr.toString()) as Address & AddressWithSigners
76+
for (const key of Object.keys(account as AddressWithSigners)) {
77+
if (!(key in address)) {
78+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
79+
;(address as any)[key] = (account as any)[key]
80+
}
81+
}
7282

7383
return address
7484
}

0 commit comments

Comments
 (0)