Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simulation: Lift log limits option in SimulateRequest #469

Merged
merged 23 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 56 additions & 6 deletions algosdk/atomic_transaction_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from algosdk import abi, error, transaction
from algosdk.transaction import GenericSignedTransaction
from algosdk.abi.address_type import AddressType
from algosdk.v2client import algod
from algosdk.v2client import algod, models


# The first four bytes of an ABI method call return must have this hash
ABI_RETURN_HASH = b"\x15\x1f\x7c\x75"
Expand Down Expand Up @@ -255,15 +256,47 @@ def __init__(
decode_error: Optional[Exception],
tx_info: dict,
method: abi.Method,
missing_signature: bool,
) -> None:
self.tx_id = tx_id
self.raw_value = raw_value
self.return_value = return_value
self.decode_error = decode_error
self.tx_info = tx_info
self.method = method
self.missing_signature = missing_signature


class SimulateEvalOverrides:
def __init__(
self,
*,
max_log_calls: Optional[int] = None,
ahangsu marked this conversation as resolved.
Show resolved Hide resolved
max_log_size: Optional[int] = None,
allow_empty_signatures: Optional[bool] = None,
bbroder-algo marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
self.max_log_calls = max_log_calls
self.max_log_size = max_log_size
self.allow_empty_signatures = allow_empty_signatures

@staticmethod
def from_simulation_result(
simulation_result: Dict[str, Any]
) -> Optional["SimulateEvalOverrides"]:
if "eval-overrides" not in simulation_result:
return None

eval_override_dict = simulation_result.get("eval-overrides", dict())
eval_override = SimulateEvalOverrides()

if "max-log-calls" in eval_override_dict:
eval_override.max_log_calls = eval_override_dict["max-log-calls"]
if "max-log-size" in eval_override_dict:
eval_override.max_log_size = eval_override_dict["max-log-size"]
if "allow-empty-signatures" in eval_override_dict:
eval_override.allow_empty_signatures = eval_override_dict[
"allow-empty-signatures"
]

return eval_override


class SimulateAtomicTransactionResponse:
Expand All @@ -275,13 +308,15 @@ def __init__(
simulate_response: Dict[str, Any],
tx_ids: List[str],
results: List[SimulateABIResult],
eval_overrides: Optional[SimulateEvalOverrides] = None,
) -> None:
self.version = version
self.failure_message = failure_message
self.failed_at = failed_at
self.simulate_response = simulate_response
self.tx_ids = tx_ids
self.abi_results = results
self.eval_overrides = eval_overrides


class AtomicTransactionComposer:
Expand Down Expand Up @@ -684,7 +719,9 @@ def submit(self, client: algod.AlgodClient) -> List[str]:
return self.tx_ids

def simulate(
self, client: algod.AlgodClient
self,
client: algod.AlgodClient,
request: Optional[models.SimulateRequest] = None,
) -> SimulateAtomicTransactionResponse:
"""
Send the transaction group to the `simulate` endpoint and wait for results.
Expand All @@ -694,6 +731,8 @@ def simulate(

Args:
client (AlgodClient): Algod V2 client
request (models.SimulateRequest): SimulateRequest with options in simulation.
The request's transaction group will be overrwritten by the composer's group, only simulation related options will be used.

Returns:
SimulateAtomicTransactionResponse: Object with simulation results for this
Expand All @@ -711,9 +750,18 @@ def simulate(
"lower to simulate a group"
)

current_simulation_request = (
request if request else models.SimulateRequest(txn_groups=list())
)
current_simulation_request.txn_groups = [
models.SimulateRequestTransactionGroup(txns=self.signed_txns)
]

simulation_result = cast(
Dict[str, Any], client.simulate_raw_transactions(self.signed_txns)
Dict[str, Any],
client.simulate_transactions(current_simulation_request),
)

# Only take the first group in the simulate response
txn_group: Dict[str, Any] = simulation_result["txn-groups"][0]

Expand Down Expand Up @@ -752,7 +800,6 @@ def simulate(
decode_error=result.decode_error,
tx_info=result.tx_info,
method=result.method,
missing_signature=sim_txn.get("missing-signature", False),
)
)

Expand All @@ -763,6 +810,9 @@ def simulate(
simulate_response=simulation_result,
tx_ids=self.tx_ids,
results=sim_results,
eval_overrides=SimulateEvalOverrides.from_simulation_result(
simulation_result
),
)

def execute(
Expand Down
14 changes: 12 additions & 2 deletions algosdk/v2client/models/simulate_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,25 @@ def dictify(self) -> Dict[str, Any]:

class SimulateRequest(object):
txn_groups: List[SimulateRequestTransactionGroup]
allow_more_logs: bool
allow_empty_signatures: bool

def __init__(
self, *, txn_groups: List[SimulateRequestTransactionGroup]
self,
*,
txn_groups: List[SimulateRequestTransactionGroup],
allow_more_logs: bool = False,
allow_empty_signatures: bool = False,
) -> None:
self.txn_groups = txn_groups
self.allow_more_logs = allow_more_logs
self.allow_empty_signatures = allow_empty_signatures

def dictify(self) -> Dict[str, Any]:
return {
"txn-groups": [
txn_group.dictify() for txn_group in self.txn_groups
]
],
"allow-more-logging": self.allow_more_logs,
"allow-empty-signatures": self.allow_empty_signatures,
}
2 changes: 2 additions & 0 deletions tests/integration.tags
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@
@rekey_v1
@send
@send.keyregtxn
@simulate
@simulate.lift_log_limits
77 changes: 43 additions & 34 deletions tests/steps/other_v2_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
ApplicationLocalState,
DryrunRequest,
DryrunSource,
SimulateRequest,
)
from tests.steps.steps import algod_port, indexer_port
from tests.steps.steps import token as daemon_token
Expand Down Expand Up @@ -1445,10 +1446,14 @@ def simulate_transaction(context):

@then("the simulation should succeed without any failure message")
def simulate_transaction_succeed(context):
if hasattr(context, "simulate_response"):
assert context.simulate_response["would-succeed"] is True
else:
assert context.atomic_transaction_composer_return.would_succeed is True
resp = (
context.simulate_response
if hasattr(context, "simulate_response")
else context.atomic_transaction_composer_return.simulate_response
)

for group in resp["txn-groups"]:
assert "failure-message" not in group


@then("I simulate the current transaction group with the composer")
Expand All @@ -1462,47 +1467,51 @@ def simulate_atc(context):
'the simulation should report a failure at group "{group}", path "{path}" with message "{message}"'
)
def simulate_atc_failure(context, group, path, message):
resp: SimulateAtomicTransactionResponse = (
context.atomic_transaction_composer_return
)
if hasattr(context, "simulate_response"):
resp = context.simulate_response
else:
resp = context.atomic_transaction_composer_return.simulate_response
group_idx: int = int(group)
fail_path = ",".join(
[
str(pe)
for pe in resp.simulate_response["txn-groups"][group_idx][
"failed-at"
]
]
[str(pe) for pe in resp["txn-groups"][group_idx]["failed-at"]]
)
assert resp.would_succeed is False
assert fail_path == path
assert message in resp.failure_message
assert message in resp["txn-groups"][group_idx]["failure-message"]


@when("I prepare the transaction without signatures for simulation")
def step_impl(context):
context.stx = transaction.SignedTransaction(context.txn, None)
@when("I make a new simulate request.")
def make_simulate_request(context):
context.simulate_request = SimulateRequest(txn_groups=[])


@then(
'the simulation should report missing signatures at group "{group}", transactions "{path}"'
)
def check_missing_signatures(context, group, path):
if hasattr(context, "simulate_response"):
resp = context.simulate_response
else:
resp = context.atomic_transaction_composer_return.simulate_response
@then("I allow more logs on that simulate request.")
def allow_more_logs_in_request(context):
context.simulate_request.allow_more_logs = True


@then("I simulate the transaction group with the simulate request.")
def simulate_group_with_request(context):
context.atomic_transaction_composer_return = (
context.atomic_transaction_composer.simulate(
context.app_acl, context.simulate_request
)
)

group_idx: int = int(group)
tx_idxs: list[int] = [int(pe) for pe in path.split(",")]

assert resp["would-succeed"] is False
@then("I check the simulation result has power packs allow-more-logging.")
def power_pack_simulation_should_pass(context):
assert context.atomic_transaction_composer_return.eval_overrides
assert (
context.atomic_transaction_composer_return.eval_overrides.max_log_calls
)
assert (
context.atomic_transaction_composer_return.eval_overrides.max_log_size
)

for tx_idx in tx_idxs:
missing_sig = resp["txn-groups"][group_idx]["txn-results"][tx_idx][
"missing-signature"
]
assert missing_sig is True

@when("I prepare the transaction without signatures for simulation")
def step_impl(context):
context.stx = transaction.SignedTransaction(context.txn, None)


@when("we make a GetLedgerStateDelta call against round {round}")
Expand Down