Skip to content

Commit 720681d

Browse files
authored
Signatures: Support new lmsig for multisig delegated logicsigs (#579)
* Support new lmsig for multisig delegated logicsigs
1 parent 0bb85fd commit 720681d

File tree

7 files changed

+256
-114
lines changed

7 files changed

+256
-114
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: 72 additions & 21 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,8 +2576,17 @@ 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

2583+
def sig_count(self):
2584+
return (
2585+
int(self.sig is not None)
2586+
+ int(self.msig is not None)
2587+
+ int(self.lmsig is not None)
2588+
)
2589+
25752590
def verify(self, public_key):
25762591
"""
25772592
Verifies LogicSig against the transaction's sender address
@@ -2584,29 +2599,38 @@ def verify(self, public_key):
25842599
the logic hash or the signature is valid against the sender\
25852600
address), false otherwise
25862601
"""
2587-
if self.sig and self.msig:
2588-
return False
2589-
25902602
try:
25912603
self._sanity_check_program(self.logic)
25922604
except error.InvalidProgram:
25932605
return False
25942606

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
2607+
if self.sig_count() > 1:
2608+
return False
26002609

26012610
if self.sig:
26022611
verify_key = VerifyKey(public_key)
26032612
try:
2613+
to_sign = constants.logic_prefix + self.logic
26042614
verify_key.verify(to_sign, base64.b64decode(self.sig))
26052615
return True
26062616
except (BadSignatureError, ValueError, TypeError):
26072617
return False
26082618

2609-
return self.msig.verify(to_sign)
2619+
if self.msig:
2620+
to_sign = constants.logic_prefix + self.logic
2621+
return self.msig.verify(to_sign)
2622+
2623+
if self.lmsig:
2624+
to_sign = (
2625+
constants.multisig_logic_prefix
2626+
+ self.lmsig.address_bytes()
2627+
+ self.logic
2628+
)
2629+
return self.lmsig.verify(to_sign)
2630+
2631+
# Non-delegated
2632+
to_sign = constants.logic_prefix + self.logic
2633+
return public_key == encoding.checksum(to_sign)
26102634

26112635
def address(self):
26122636
"""
@@ -2626,6 +2650,18 @@ def sign_program(program, private_key):
26262650
signed = signing_key.sign(to_sign)
26272651
return base64.b64encode(signed.signature).decode()
26282652

2653+
@staticmethod
2654+
def multisig_sign_program(program, private_key, multisig):
2655+
private_key = base64.b64decode(private_key)
2656+
signing_key = SigningKey(private_key[: constants.key_len_bytes])
2657+
to_sign = (
2658+
constants.multisig_logic_prefix
2659+
+ multisig.address_bytes()
2660+
+ program
2661+
)
2662+
signed = signing_key.sign(to_sign)
2663+
return base64.b64encode(signed.signature).decode()
2664+
26292665
@staticmethod
26302666
def single_sig_multisig(program, private_key, multisig):
26312667
index = -1
@@ -2637,7 +2673,7 @@ def single_sig_multisig(program, private_key, multisig):
26372673
break
26382674
if index == -1:
26392675
raise error.InvalidSecretKeyError
2640-
sig = LogicSig.sign_program(program, private_key)
2676+
sig = LogicSig.multisig_sign_program(program, private_key, multisig)
26412677

26422678
return sig, index
26432679

@@ -2657,7 +2693,7 @@ def sign(self, private_key, multisig=None):
26572693
already been provided
26582694
"""
26592695
if not multisig:
2660-
if self.msig:
2696+
if self.msig or self.lmsig:
26612697
raise error.LogicSigOverspecifiedSignature
26622698
self.sig = LogicSig.sign_program(self.logic, private_key)
26632699
else:
@@ -2667,7 +2703,7 @@ def sign(self, private_key, multisig=None):
26672703
self.logic, private_key, multisig
26682704
)
26692705
multisig.subsigs[index].signature = base64.b64decode(sig)
2670-
self.msig = multisig
2706+
self.lmsig = multisig
26712707

26722708
def append_to_multisig(self, private_key):
26732709
"""
@@ -2680,12 +2716,12 @@ def append_to_multisig(self, private_key):
26802716
InvalidSecretKeyError: if no matching private key in multisig\
26812717
object
26822718
"""
2683-
if self.msig is None:
2719+
if self.lmsig is None:
26842720
raise error.InvalidSecretKeyError
26852721
sig, index = LogicSig.single_sig_multisig(
2686-
self.logic, private_key, self.msig
2722+
self.logic, private_key, self.lmsig
26872723
)
2688-
self.msig.subsigs[index].signature = base64.b64decode(sig)
2724+
self.lmsig.subsigs[index].signature = base64.b64decode(sig)
26892725

26902726
def __eq__(self, other):
26912727
if not isinstance(other, LogicSig):
@@ -2695,6 +2731,7 @@ def __eq__(self, other):
26952731
and self.args == other.args
26962732
and self.sig == other.sig
26972733
and self.msig == other.msig
2734+
and self.lmsig == other.lmsig
26982735
)
26992736

27002737

@@ -2744,7 +2781,7 @@ def is_delegated(self) -> bool:
27442781
Returns:
27452782
bool: True if and only if this is a delegated LogicSigAccount.
27462783
"""
2747-
return bool(self.lsig.sig or self.lsig.msig)
2784+
return bool(self.lsig.sig or self.lsig.msig or self.lsig.lmsig)
27482785

27492786
def verify(self) -> bool:
27502787
"""
@@ -2757,6 +2794,15 @@ def verify(self) -> bool:
27572794
addr = self.address()
27582795
return self.lsig.verify(encoding.decode_address(addr))
27592796

2797+
def sig_count(self) -> int:
2798+
"""
2799+
Returns the number of cryptographic signatures on the LogicSig
2800+
2801+
Returns:
2802+
int: The number of signatures. Should never exceed 1.
2803+
"""
2804+
return self.lsig.sig_count()
2805+
27602806
def address(self) -> str:
27612807
"""
27622808
Get the address of this LogicSigAccount.
@@ -2767,7 +2813,7 @@ def address(self) -> str:
27672813
If the LogicSig is not delegated to another account, this will return an
27682814
escrow address that is the hash of the LogicSig's program code.
27692815
"""
2770-
if self.lsig.sig and self.lsig.msig:
2816+
if self.sig_count() > 1:
27712817
raise error.LogicSigOverspecifiedSignature
27722818

27732819
if self.lsig.sig:
@@ -2778,6 +2824,9 @@ def address(self) -> str:
27782824
if self.lsig.msig:
27792825
return self.lsig.msig.address()
27802826

2827+
if self.lsig.lmsig:
2828+
return self.lsig.lmsig.address()
2829+
27812830
return self.lsig.address()
27822831

27832832
def sign_multisig(self, multisig: Multisig, private_key: str) -> None:
@@ -2874,6 +2923,8 @@ def __init__(
28742923
lsigAddr = transaction.sender
28752924
elif lsig.msig:
28762925
lsigAddr = lsig.msig.address()
2926+
elif lsig.lmsig:
2927+
lsigAddr = lsig.lmsig.address()
28772928
else:
28782929
lsigAddr = lsig.address()
28792930
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)