Skip to content

Commit 0cabc1e

Browse files
authored
Merge pull request #257 from algorandfoundation/feat/gather-signatures
feat: return bytes from gatherSignatures to prevent double encoding
2 parents 9925ae1 + 8a3420e commit 0cabc1e

File tree

8 files changed

+34
-67
lines changed

8 files changed

+34
-67
lines changed

docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ Build queued transactions without resource population or grouping.
174174
Returns raw transactions, method call metadata, and any explicit signers. This does not
175175
populate unnamed resources or adjust fees, and it leaves grouping unchanged.
176176

177-
#### gather_signatures() → list[algokit_transact.models.signed_transaction.SignedTransaction]
177+
#### gather_signatures() → list[bytes]
178178

179179
#### send(params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None) → [SendTransactionComposerResults](#algokit_utils.transactions.transaction_composer.SendTransactionComposerResults)
180180

src/algokit_transact/logicsig.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from algokit_transact.codec.signed import encode_signed_transaction
99
from algokit_transact.models.signed_transaction import SignedTransaction
1010
from algokit_transact.models.transaction import Transaction
11+
from algokit_transact.ops.validate import validate_signed_transaction
1112
from algokit_transact.signing.logic_signature import LogicSigSignature
1213
from algokit_transact.signing.types import MultisigSignature
1314
from algokit_transact.signing.validation import sanity_check_program
@@ -155,6 +156,7 @@ def signer(txn_group: Sequence[Transaction], indexes_to_sign: Sequence[int]) ->
155156
lsig=logic_sig,
156157
auth_address=auth_addr,
157158
)
159+
validate_signed_transaction(signed)
158160
blobs.append(encode_signed_transaction(signed))
159161
return blobs
160162

src/algokit_transact/models/state_proof.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class StateProofMessage:
8383

8484
@dataclass(slots=True, frozen=True)
8585
class StateProofTransactionFields:
86-
state_proof_type: int | None = field(default=None, metadata=wire("sptype"))
86+
state_proof_type: int = field(default=0, metadata=wire("sptype"))
8787
# Flatten state proof and message at the transaction level (aliases under root: sp and spmsg)
8888
state_proof: StateProof | None = field(default=None, metadata=nested("sp", StateProof))
8989
message: StateProofMessage | None = field(default=None, metadata=nested("spmsg", StateProofMessage))

src/algokit_transact/multisig.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from algokit_transact.codec.transaction import encode_transaction
99
from algokit_transact.models.signed_transaction import SignedTransaction
1010
from algokit_transact.models.transaction import Transaction as AlgokitTransaction
11+
from algokit_transact.ops.validate import validate_signed_transaction
1112
from algokit_transact.signer import AddressWithSigners
1213
from algokit_transact.signing.multisig import (
1314
address_from_multisig_signature,
@@ -87,6 +88,7 @@ def signer(txn_group: Sequence[AlgokitTransaction], indexes_to_sign: Sequence[in
8788
lsig=None,
8889
auth_address=msig_address if txn.sender != msig_address else None,
8990
)
91+
validate_signed_transaction(signed)
9092
blobs.append(encode_signed_transaction(signed))
9193
return blobs
9294

src/algokit_transact/ops/validate.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@
2020
PROGRAM_PAGE_SIZE,
2121
SIGNATURE_BYTE_LENGTH,
2222
)
23-
from algokit_transact import SignedTransaction
2423
from algokit_transact.exceptions import TransactionValidationError
2524
from algokit_transact.models.app_call import AppCallTransactionFields
2625
from algokit_transact.models.asset_config import AssetConfigTransactionFields
2726
from algokit_transact.models.asset_freeze import AssetFreezeTransactionFields
2827
from algokit_transact.models.asset_transfer import AssetTransferTransactionFields
2928
from algokit_transact.models.common import OnApplicationComplete
3029
from algokit_transact.models.key_registration import KeyRegistrationTransactionFields
30+
from algokit_transact.models.signed_transaction import SignedTransaction
3131
from algokit_transact.models.transaction import Transaction
3232

3333

@@ -60,9 +60,8 @@ def _issue(
6060
def validate_signed_transaction(stx: SignedTransaction) -> None:
6161
validate_transaction(stx.txn)
6262

63-
signatures = {stx.sig, stx.msig, stx.lsig} - {None}
64-
if not signatures:
65-
raise ValueError("At least one signature type must be set")
63+
signatures = [s for s in (stx.sig, stx.msig, stx.lsig) if s is not None]
64+
6665
if len(signatures) > 1:
6766
raise ValueError("Only one signature type can be set")
6867

src/algokit_transact/signer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from algokit_transact.codec.transaction import encode_transaction
1414
from algokit_transact.models.signed_transaction import SignedTransaction
1515
from algokit_transact.models.transaction import Transaction
16+
from algokit_transact.ops.validate import validate_signed_transaction
1617

1718
if TYPE_CHECKING:
1819
from algokit_transact.logicsig import (
@@ -115,6 +116,7 @@ def transaction_signer(txn_group: Sequence[Transaction], indexes_to_sign: Sequen
115116
sig=signature,
116117
auth_address=auth_addr if txn.sender != auth_addr else None,
117118
)
119+
validate_signed_transaction(stxn)
118120
result.append(encode_signed_transaction(stxn))
119121
return result
120122

@@ -153,6 +155,7 @@ def empty_signer(txn_group: Sequence[Transaction], indexes_to_sign: Sequence[int
153155
txn=txn_group[index],
154156
sig=EMPTY_SIGNATURE,
155157
)
158+
validate_signed_transaction(stxn)
156159
result.append(encode_signed_transaction(stxn))
157160
return result
158161

src/algokit_utils/transactions/transaction_composer.py

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
from algokit_algod_client.exceptions import UnexpectedStatusError
1212
from algokit_algod_client.models import SimulateTransactionResult
1313
from algokit_common.constants import MAX_TRANSACTION_GROUP_SIZE
14-
from algokit_transact import decode_signed_transaction, encode_signed_transactions, make_empty_transaction_signer
15-
from algokit_transact.models.signed_transaction import SignedTransaction
14+
from algokit_transact import make_empty_transaction_signer
15+
from algokit_transact.codec.signed import decode_signed_transactions
1616
from algokit_transact.models.transaction import Transaction, TransactionType
1717
from algokit_transact.ops.fees import calculate_fee
1818
from algokit_transact.ops.group import group_transactions
1919
from algokit_transact.ops.ids import get_transaction_id
20-
from algokit_transact.ops.validate import validate_signed_transaction, validate_transaction
20+
from algokit_transact.ops.validate import validate_transaction
2121
from algokit_transact.signer import AddressWithTransactionSigner, TransactionSigner
2222
from algokit_utils.applications.abi import ABIReturn
2323
from algokit_utils.applications.app_manager import AppManager
@@ -262,7 +262,7 @@ def __init__(self, params: TransactionComposerParams) -> None:
262262

263263
self._queued: list[_QueuedTransaction] = []
264264
self._transactions_with_signers: list[TransactionWithSigner] | None = None
265-
self._signed_transactions: list[SignedTransaction] | None = None
265+
self._signed_transactions: list[bytes] | None = None
266266
self._raw_built_transactions: list[Transaction] | None = None
267267

268268
def clone(self, composer_config: TransactionComposerConfig | None = None) -> "TransactionComposer":
@@ -459,7 +459,7 @@ def build_transactions(self) -> BuiltTransactions:
459459
signers = {index: entry.signer for index, entry in enumerate(built_entries) if entry.signer is not None}
460460
return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers)
461461

462-
def gather_signatures(self) -> list[SignedTransaction]:
462+
def gather_signatures(self) -> list[bytes]:
463463
self._ensure_built()
464464
if self._signed_transactions is None:
465465
self._signed_transactions = self._sign_transactions(self._transactions_with_signers or [])
@@ -503,8 +503,7 @@ def send(self, params: SendParams | None = None) -> SendTransactionComposerResul
503503

504504
# Send transactions and handle network errors
505505
try:
506-
blobs = encode_signed_transactions(signed_transactions)
507-
self._algod.send_raw_transaction(blobs)
506+
self._algod.send_raw_transaction(signed_transactions)
508507

509508
tx_ids = [get_transaction_id(entry.txn) for entry in self._transactions_with_signers or []]
510509
group_id = self._group_id()
@@ -627,7 +626,8 @@ def simulate(
627626
)
628627
for entry in txns_with_signers
629628
]
630-
signed_transactions = self._sign_transactions(signing_entries)
629+
encoded_signed_transactions = self._sign_transactions(signing_entries)
630+
signed_transactions = decode_signed_transactions(encoded_signed_transactions)
631631

632632
request = algod_models.SimulateRequest(
633633
txn_groups=[algod_models.SimulateRequestTransactionGroup(txns=signed_transactions)],
@@ -813,9 +813,10 @@ def _analyze_group_requirements( # noqa: C901, PLR0912
813813
transactions_to_simulate = group_transactions(transactions_to_simulate)
814814

815815
empty_signer: TransactionSigner = make_empty_transaction_signer()
816-
signed_transactions = self._sign_transactions(
816+
encoded_signed_transactions = self._sign_transactions(
817817
[TransactionWithSigner(txn=txn, signer=empty_signer) for txn in transactions_to_simulate]
818818
)
819+
signed_transactions = decode_signed_transactions(encoded_signed_transactions)
819820

820821
simulate_request = algod_models.SimulateRequest(
821822
txn_groups=[algod_models.SimulateRequestTransactionGroup(txns=signed_transactions)],
@@ -1348,7 +1349,7 @@ def _resolve_param_signer(
13481349
return resolved
13491350
return signer
13501351

1351-
def _sign_transactions(self, txns_with_signers: Sequence[TransactionWithSigner]) -> list[SignedTransaction]: # noqa: C901
1352+
def _sign_transactions(self, txns_with_signers: Sequence[TransactionWithSigner]) -> list[bytes]:
13521353
if not txns_with_signers:
13531354
raise ValueError("No transactions available to sign")
13541355

@@ -1365,33 +1366,19 @@ def _sign_transactions(self, txns_with_signers: Sequence[TransactionWithSigner])
13651366
blobs = signer(transactions, indexes)
13661367
signed_blobs[key] = list(blobs)
13671368

1368-
raw_signed_transactions: list[bytes | None] = [None] * len(transactions)
1369+
encoded_signed_transactions: list[bytes | None] = [None] * len(transactions)
13691370

13701371
for key, (_, indexes) in signer_groups.items():
13711372
blobs = signed_blobs[key]
13721373
for blob_index, txn_index in enumerate(indexes):
13731374
if blob_index < len(blobs):
1374-
raw_signed_transactions[txn_index] = blobs[blob_index]
1375+
encoded_signed_transactions[txn_index] = blobs[blob_index]
13751376

1376-
unsigned_indexes = [i for i, item in enumerate(raw_signed_transactions) if item is None]
1377+
unsigned_indexes = [i for i, item in enumerate(encoded_signed_transactions) if item is None]
13771378
if unsigned_indexes:
13781379
raise ValueError(f"Transactions at indexes [{', '.join(map(str, unsigned_indexes))}] were not signed")
13791380

1380-
# Decode and validate all signed transactions
1381-
signed_transactions: list[SignedTransaction] = []
1382-
for index, stxn in enumerate(raw_signed_transactions):
1383-
if stxn is None:
1384-
# This shouldn't happen due to the check above, but ensures type safety
1385-
raise ValueError(f"Transaction at index {index} was not signed")
1386-
1387-
try:
1388-
signed_transaction = decode_signed_transaction(stxn)
1389-
validate_signed_transaction(signed_transaction)
1390-
signed_transactions.append(signed_transaction)
1391-
except Exception as err:
1392-
raise ValueError(f"Invalid signed transaction at index {index}. {err}") from err
1393-
1394-
return signed_transactions
1381+
return cast(list[bytes], encoded_signed_transactions) # The guard above ensures no None values
13951382

13961383
def _group_id(self) -> str | None:
13971384
txns = self._transactions_with_signers or []
@@ -1468,9 +1455,10 @@ def _simulate_error_context(
14681455
"""
14691456
try:
14701457
empty_signer: TransactionSigner = make_empty_transaction_signer()
1471-
signed_transactions = self._sign_transactions(
1458+
encoded_signed_transactions = self._sign_transactions(
14721459
[TransactionWithSigner(txn=txn, signer=empty_signer) for txn in sent_transactions]
14731460
)
1461+
signed_transactions = decode_signed_transactions(encoded_signed_transactions)
14741462
request = algod_models.SimulateRequest(
14751463
txn_groups=[algod_models.SimulateRequestTransactionGroup(txns=signed_transactions)],
14761464
allow_empty_signatures=True,

tests/transactions/test_transaction_composer.py

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -595,8 +595,7 @@ def test_should_successfully_sign_a_single_transaction(
595595
signed_txns = composer.gather_signatures()
596596

597597
assert len(signed_txns) == 1
598-
assert signed_txns[0] is not None
599-
assert signed_txns[0].sig is not None
598+
assert len(signed_txns[0]) > 0
600599

601600
def test_should_successfully_sign_multiple_transactions_with_same_signer(
602601
self, algorand: AlgorandClient, funded_account: AddressWithSigners
@@ -621,8 +620,8 @@ def test_should_successfully_sign_multiple_transactions_with_same_signer(
621620
signed_txns = composer.gather_signatures()
622621

623622
assert len(signed_txns) == 2
624-
assert signed_txns[0].sig is not None
625-
assert signed_txns[1].sig is not None
623+
assert len(signed_txns[0]) > 0
624+
assert len(signed_txns[1]) > 0
626625

627626
def test_should_successfully_sign_transactions_with_multiple_different_signers(
628627
self, algorand: AlgorandClient, funded_account: AddressWithSigners
@@ -658,8 +657,8 @@ def test_should_successfully_sign_transactions_with_multiple_different_signers(
658657
signed_txns = composer.gather_signatures()
659658

660659
assert len(signed_txns) == 2
661-
assert signed_txns[0].sig is not None
662-
assert signed_txns[1].sig is not None
660+
assert len(signed_txns[0]) > 0
661+
assert len(signed_txns[1]) > 0
663662

664663
def test_should_throw_error_when_no_transactions_to_sign(self, algorand: AlgorandClient) -> None:
665664
"""Test that an error is thrown when there are no transactions to sign."""
@@ -754,29 +753,3 @@ def faulty_signer(_txns: Sequence[Transaction], _indexes: Sequence[int]) -> list
754753

755754
with pytest.raises(ValueError, match=r"Transactions at indexes \[0\] were not signed"):
756755
composer.gather_signatures()
757-
758-
def test_should_throw_error_when_signer_returns_invalid_signed_transaction_data(
759-
self, algorand: AlgorandClient, funded_account: AddressWithSigners
760-
) -> None:
761-
"""Test error handling when a signer returns malformed signed transaction data."""
762-
from collections.abc import Sequence
763-
764-
from algokit_transact.models.transaction import Transaction
765-
766-
# Create a faulty signer that returns invalid data
767-
def faulty_signer(_txns: Sequence[Transaction], indexes: Sequence[int]) -> list[bytes]:
768-
return [bytes([1, 2, 3]) for _ in indexes]
769-
770-
composer = algorand.new_group()
771-
composer.add_payment(
772-
PaymentParams(
773-
sender=funded_account.addr,
774-
receiver=funded_account.addr,
775-
amount=AlgoAmount.from_micro_algo(1000),
776-
signer=faulty_signer,
777-
)
778-
)
779-
780-
# Should provide a clear error message indicating which transaction had invalid data
781-
with pytest.raises(ValueError, match="Invalid signed transaction at index 0"):
782-
composer.gather_signatures()

0 commit comments

Comments
 (0)