Skip to content

Commit dce49c9

Browse files
committed
Support new lmsig for multisig delegated logicsigs
1 parent 0bb85fd commit dce49c9

File tree

7 files changed

+243
-115
lines changed

7 files changed

+243
-115
lines changed

algosdk/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
"""str: prefix for multisig addresses"""
5757
LOGIC_PREFIX = b"Program"
5858
"""bytes: program (logic) prefix when signing"""
59+
MULTISIG_LOGIC_PREFIX = b"MsigProgram"
60+
"""bytes: program (logic) prefix when signing"""
5961
LOGIC_DATA_PREFIX = b"ProgData"
6062
"""bytes: program (logic) data prefix when signing"""
6163
APPID_PREFIX = b"appID"
@@ -123,6 +125,7 @@
123125
bytes_prefix = BYTES_PREFIX
124126
msig_addr_prefix = MSIG_ADDR_PREFIX
125127
logic_prefix = LOGIC_PREFIX
128+
multisig_logic_prefix = MULTISIG_LOGIC_PREFIX
126129
logic_data_prefix = LOGIC_DATA_PREFIX
127130
hash_len = HASH_LEN
128131
check_sum_len_bytes = CHECK_SUM_LEN_BYTES

algosdk/transaction.py

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2364,17 +2364,20 @@ def validate(self):
23642364
if len(self.subsigs) > constants.multisig_account_limit:
23652365
raise error.MultisigAccountSizeError
23662366

2367-
def address(self):
2368-
"""Return the multisig account address."""
2367+
def address_bytes(self):
2368+
"""Return the raw bytes of the multisig account address."""
23692369
msig_bytes = (
23702370
bytes(constants.msig_addr_prefix, "utf-8")
23712371
+ bytes([self.version])
23722372
+ bytes([self.threshold])
23732373
)
23742374
for s in self.subsigs:
23752375
msig_bytes += s.public_key
2376-
addr = encoding.checksum(msig_bytes)
2377-
return encoding.encode_address(addr)
2376+
return encoding.checksum(msig_bytes)
2377+
2378+
def address(self):
2379+
"""Return the multisig account address."""
2380+
return encoding.encode_address(self.address_bytes())
23782381

23792382
def verify(self, message):
23802383
"""Verify that the multisig is valid for the message."""
@@ -2509,6 +2512,7 @@ def __init__(self, program, args=None):
25092512
self.args = args
25102513
self.sig = None
25112514
self.msig = None
2515+
self.lmsig = None
25122516

25132517
@staticmethod
25142518
def _sanity_check_program(program):
@@ -2561,6 +2565,8 @@ def dictify(self):
25612565
od["sig"] = base64.b64decode(self.sig)
25622566
elif self.msig:
25632567
od["msig"] = self.msig.dictify()
2568+
elif self.lmsig:
2569+
od["lmsig"] = self.lmsig.dictify()
25642570
return od
25652571

25662572
@staticmethod
@@ -2570,6 +2576,8 @@ def undictify(d):
25702576
lsig.sig = base64.b64encode(d["sig"]).decode()
25712577
elif "msig" in d:
25722578
lsig.msig = Multisig.undictify(d["msig"])
2579+
elif "lmsig" in d:
2580+
lsig.lmsig = Multisig.undictify(d["lmsig"])
25732581
return lsig
25742582

25752583
def verify(self, public_key):
@@ -2584,29 +2592,41 @@ def verify(self, public_key):
25842592
the logic hash or the signature is valid against the sender\
25852593
address), false otherwise
25862594
"""
2587-
if self.sig and self.msig:
2588-
return False
2589-
25902595
try:
25912596
self._sanity_check_program(self.logic)
25922597
except error.InvalidProgram:
25932598
return False
25942599

2595-
to_sign = constants.logic_prefix + self.logic
2596-
2597-
if not self.sig and not self.msig:
2598-
checksum = encoding.checksum(to_sign)
2599-
return checksum == public_key
2600-
26012600
if self.sig:
2601+
if self.msig or self.lmsig:
2602+
return False
26022603
verify_key = VerifyKey(public_key)
26032604
try:
2605+
to_sign = constants.logic_prefix + self.logic
26042606
verify_key.verify(to_sign, base64.b64decode(self.sig))
26052607
return True
26062608
except (BadSignatureError, ValueError, TypeError):
26072609
return False
26082610

2609-
return self.msig.verify(to_sign)
2611+
if self.msig:
2612+
if self.sig or self.lmsig:
2613+
return False
2614+
to_sign = constants.logic_prefix + self.logic
2615+
return self.msig.verify(to_sign)
2616+
2617+
if self.lmsig:
2618+
if self.sig or self.msig:
2619+
return False
2620+
to_sign = (
2621+
constants.multisig_logic_prefix
2622+
+ self.lmsig.address_bytes()
2623+
+ self.logic
2624+
)
2625+
return self.lmsig.verify(to_sign)
2626+
2627+
# Non-delegated
2628+
to_sign = constants.logic_prefix + self.logic
2629+
return public_key == encoding.checksum(to_sign)
26102630

26112631
def address(self):
26122632
"""
@@ -2626,6 +2646,18 @@ def sign_program(program, private_key):
26262646
signed = signing_key.sign(to_sign)
26272647
return base64.b64encode(signed.signature).decode()
26282648

2649+
@staticmethod
2650+
def multisig_sign_program(program, private_key, multisig):
2651+
private_key = base64.b64decode(private_key)
2652+
signing_key = SigningKey(private_key[: constants.key_len_bytes])
2653+
to_sign = (
2654+
constants.multisig_logic_prefix
2655+
+ multisig.address_bytes()
2656+
+ program
2657+
)
2658+
signed = signing_key.sign(to_sign)
2659+
return base64.b64encode(signed.signature).decode()
2660+
26292661
@staticmethod
26302662
def single_sig_multisig(program, private_key, multisig):
26312663
index = -1
@@ -2637,7 +2669,7 @@ def single_sig_multisig(program, private_key, multisig):
26372669
break
26382670
if index == -1:
26392671
raise error.InvalidSecretKeyError
2640-
sig = LogicSig.sign_program(program, private_key)
2672+
sig = LogicSig.multisig_sign_program(program, private_key, multisig)
26412673

26422674
return sig, index
26432675

@@ -2657,7 +2689,7 @@ def sign(self, private_key, multisig=None):
26572689
already been provided
26582690
"""
26592691
if not multisig:
2660-
if self.msig:
2692+
if self.msig or self.lmsig:
26612693
raise error.LogicSigOverspecifiedSignature
26622694
self.sig = LogicSig.sign_program(self.logic, private_key)
26632695
else:
@@ -2667,7 +2699,7 @@ def sign(self, private_key, multisig=None):
26672699
self.logic, private_key, multisig
26682700
)
26692701
multisig.subsigs[index].signature = base64.b64decode(sig)
2670-
self.msig = multisig
2702+
self.lmsig = multisig
26712703

26722704
def append_to_multisig(self, private_key):
26732705
"""
@@ -2680,12 +2712,12 @@ def append_to_multisig(self, private_key):
26802712
InvalidSecretKeyError: if no matching private key in multisig\
26812713
object
26822714
"""
2683-
if self.msig is None:
2715+
if self.lmsig is None:
26842716
raise error.InvalidSecretKeyError
26852717
sig, index = LogicSig.single_sig_multisig(
2686-
self.logic, private_key, self.msig
2718+
self.logic, private_key, self.lmsig
26872719
)
2688-
self.msig.subsigs[index].signature = base64.b64decode(sig)
2720+
self.lmsig.subsigs[index].signature = base64.b64decode(sig)
26892721

26902722
def __eq__(self, other):
26912723
if not isinstance(other, LogicSig):
@@ -2695,6 +2727,7 @@ def __eq__(self, other):
26952727
and self.args == other.args
26962728
and self.sig == other.sig
26972729
and self.msig == other.msig
2730+
and self.lmsig == other.lmsig
26982731
)
26992732

27002733

@@ -2744,7 +2777,7 @@ def is_delegated(self) -> bool:
27442777
Returns:
27452778
bool: True if and only if this is a delegated LogicSigAccount.
27462779
"""
2747-
return bool(self.lsig.sig or self.lsig.msig)
2780+
return bool(self.lsig.sig or self.lsig.msig or self.lsig.lmsig)
27482781

27492782
def verify(self) -> bool:
27502783
"""
@@ -2767,9 +2800,6 @@ def address(self) -> str:
27672800
If the LogicSig is not delegated to another account, this will return an
27682801
escrow address that is the hash of the LogicSig's program code.
27692802
"""
2770-
if self.lsig.sig and self.lsig.msig:
2771-
raise error.LogicSigOverspecifiedSignature
2772-
27732803
if self.lsig.sig:
27742804
if not self.sigkey:
27752805
raise error.LogicSigSigningKeyMissing
@@ -2778,6 +2808,9 @@ def address(self) -> str:
27782808
if self.lsig.msig:
27792809
return self.lsig.msig.address()
27802810

2811+
if self.lsig.lmsig:
2812+
return self.lsig.lmsig.address()
2813+
27812814
return self.lsig.address()
27822815

27832816
def sign_multisig(self, multisig: Multisig, private_key: str) -> None:
@@ -2874,6 +2907,8 @@ def __init__(
28742907
lsigAddr = transaction.sender
28752908
elif lsig.msig:
28762909
lsigAddr = lsig.msig.address()
2910+
elif lsig.lmsig:
2911+
lsigAddr = lsig.lmsig.address()
28772912
else:
28782913
lsigAddr = lsig.address()
28792914
self.lsig = lsig
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"lsig": {
3+
"arg:b64": [
4+
"AQ==",
5+
"AgM="
6+
],
7+
"l:b64": "ASABASI=",
8+
"lmsig": {
9+
"subsig": [
10+
{
11+
"pk:b64": "G37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXg=",
12+
"s:b64": "jDNlxLHRE3DyP3DXd0af4dlHeb9NjWSBdkk173hMzHZXbMDg7+nBRvpgdtr6zlXumvMbdo68MrTsGiyMPcBnBg=="
13+
},
14+
{
15+
"pk:b64": "CWMyCVNzifB1ZxF3OZHH0D4bc8jE9Sv2r/Aaolz5wnE=",
16+
"s:b64": "T1bUfdnAsWoV9Yhk6edD1TEJH/evbLKB3zQNFTojbqXZF3PZ/5dljvfqY/A3aM3+KL6KxoJ683Gt2YSnOOrlDQ=="
17+
},
18+
{
19+
"pk:b64": "5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKE="
20+
}
21+
],
22+
"thr": 2,
23+
"v": 1
24+
}
25+
},
26+
"txn": {
27+
"amt": 5000,
28+
"fee": 217000,
29+
"fv": 972508,
30+
"gen": "testnet-v31.0",
31+
"gh:b64": "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=",
32+
"lv": 973508,
33+
"note:b64": "tFF5Ofz60nE=",
34+
"rcv:b64": "tMYiaKTDNVD1im3UuMojnJ8dELNBqn4aNuPOYfv8+Yo=",
35+
"snd:b64": "jZK0iZABc6BN+kNZo2ZqavzqLEKgXdnB9z7rpUeAN+k=",
36+
"type": "pay"
37+
}
38+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"lsig": {
3+
"arg:b64": [
4+
"AQ==",
5+
"AgM="
6+
],
7+
"l:b64": "ASABASI=",
8+
"lmsig": {
9+
"subsig": [
10+
{
11+
"pk:b64": "G37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXg=",
12+
"s:b64": "jDNlxLHRE3DyP3DXd0af4dlHeb9NjWSBdkk173hMzHZXbMDg7+nBRvpgdtr6zlXumvMbdo68MrTsGiyMPcBnBg=="
13+
},
14+
{
15+
"pk:b64": "CWMyCVNzifB1ZxF3OZHH0D4bc8jE9Sv2r/Aaolz5wnE=",
16+
"s:b64": "T1bUfdnAsWoV9Yhk6edD1TEJH/evbLKB3zQNFTojbqXZF3PZ/5dljvfqY/A3aM3+KL6KxoJ683Gt2YSnOOrlDQ=="
17+
},
18+
{
19+
"pk:b64": "5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKE="
20+
}
21+
],
22+
"thr": 2,
23+
"v": 1
24+
}
25+
},
26+
"sgnr:b64": "jZK0iZABc6BN+kNZo2ZqavzqLEKgXdnB9z7rpUeAN+k=",
27+
"txn": {
28+
"amt": 5000,
29+
"fee": 217000,
30+
"fv": 972508,
31+
"gen": "testnet-v31.0",
32+
"gh:b64": "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=",
33+
"lv": 973508,
34+
"note:b64": "tFF5Ofz60nE=",
35+
"rcv:b64": "tMYiaKTDNVD1im3UuMojnJ8dELNBqn4aNuPOYfv8+Yo=",
36+
"snd:b64": "tMYiaKTDNVD1im3UuMojnJ8dELNBqn4aNuPOYfv8+Yo=",
37+
"type": "pay"
38+
}
39+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"lsig": {
3+
"l:b64": "ASABASI=",
4+
"lmsig": {
5+
"subsig": [
6+
{
7+
"pk:b64": "eUdQSBmJmLH5xdIDnkf+V3AQH6usPifhfJVwnJ7d7nM=",
8+
"s:b64": "E5HLGOcgWODZRLIlpQu2iAf+NLucRSNz1xfgomJdgggUrNljWEsrkLGXBjFMqHHKD02V4gkl4pgqLX96lpU3AA=="
9+
},
10+
{
11+
"pk:b64": "vEhvB7iC7lgJHhMsBIQGlMnblx0lmrEyqGSAwdCqvrU=",
12+
"s:b64": "4IOlC8zhjjy7j5yKTODB7So5qfdlhDe89/BdCQw7mL7ULX0LFdr+HyOh39mdnPdgM7+lfjbOhsrY1RO7AwRyAw=="
13+
}
14+
],
15+
"thr": 2,
16+
"v": 1
17+
}
18+
},
19+
"txn": {
20+
"amt": 1000000,
21+
"fee": 1000,
22+
"fv": 447,
23+
"gen": "network-v1",
24+
"gh:b64": "zNQES/4IqimxRif40xYvzBBIYCZSbYvNSRIzVIh4swo=",
25+
"lv": 1447,
26+
"rcv:b64": "G5lBeqbJ/yljtd70mPTxXmTkjs14UccjSEBp4G3lW4I=",
27+
"snd:b64": "jK0vte/Ze647qZL7ch63CUpU8zSp0oEiIxV2HMA4w8o=",
28+
"type": "pay"
29+
}
30+
}

0 commit comments

Comments
 (0)