Fix simulation gas issue#67
Conversation
There was a problem hiding this comment.
29 issues found across 52 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/allora_sdk/logging_config.py">
<violation number="1" location="src/allora_sdk/logging_config.py:76">
P2: `basicConfig(..., force=force)` becomes a no-op when the host app already set up logging, so the SDK can mark itself configured without ever installing the new formatter/handler.</violation>
</file>
<file name="src/allora_sdk/loss_methods/binary_cross_entropy.py">
<violation number="1" location="src/allora_sdk/loss_methods/binary_cross_entropy.py:38">
P2: Reject non-finite inputs explicitly; `NaN` currently bypasses the range checks and propagates a silent `nan` loss.</violation>
<violation number="2" location="src/allora_sdk/loss_methods/binary_cross_entropy.py:43">
P2: Validate `epsilon` before clipping; values outside `(0, 0.5)` can make the loss ignore `predicted` entirely or fail with a math-domain error.</violation>
</file>
<file name="src/allora_sdk/loss_methods/ztae.py">
<violation number="1" location="src/allora_sdk/loss_methods/ztae.py:61">
P2: Reject non-finite `std` values here; `nan`/`inf` currently pass validation and make the loss return invalid results.</violation>
</file>
<file name="src/allora_sdk/rpc_client/tx_manager.py">
<violation number="1" location="src/allora_sdk/rpc_client/tx_manager.py:194">
P1: `InsufficientFeesError` is still treated as a fatal simulation failure, so nodes that enforce min fees during simulation can block otherwise valid broadcasts.</violation>
<violation number="2" location="src/allora_sdk/rpc_client/tx_manager.py:317">
P2: The simulation fallback is incomplete: non-`grpc.RpcError` failures still escape and abort submission instead of using the default gas path.</violation>
</file>
<file name="scripts/templates/rest_client.py.jinja">
<violation number="1" location="scripts/templates/rest_client.py.jinja:46">
P1: Convert `height` to a string before sending it as an HTTP header; passing the raw `int` here can make every `height=` query fail before the request is sent.</violation>
</file>
<file name="src/allora_sdk/worker/inferer.py">
<violation number="1" location="src/allora_sdk/worker/inferer.py:127">
P2: This registration call ignores the configured `fee_tier` and always pays `PRIORITY` fees.</violation>
</file>
<file name="src/allora_sdk/worker/__init__.py">
<violation number="1" location="src/allora_sdk/worker/__init__.py:9">
P2: `PredictFnResultType` was dropped from the module’s public exports, introducing a backward-incompatible API break for existing callers.</violation>
</file>
<file name="README.md">
<violation number="1" location="README.md:130">
P2: The advanced configuration example references an undefined callback (`my_model`) after the quick-start example was renamed to `run_model`.</violation>
</file>
<file name="example_forecaster.py">
<violation number="1" location="example_forecaster.py:11">
P2: Guard the previous-epoch nonce from going negative before querying network inference.</violation>
</file>
<file name="tests/test_worker_review_fixes.py">
<violation number="1" location="tests/test_worker_review_fixes.py:68">
P2: Assert that the API key header is omitted when `api_key=None`; checking for the string "None" makes this test validate the wrong behavior.</violation>
</file>
<file name="src/allora_sdk/loss_methods/huber.py">
<violation number="1" location="src/allora_sdk/loss_methods/huber.py:41">
P2: Reject `NaN` for `delta` explicitly; it bypasses this guard and turns every Huber loss result into `nan`.</violation>
</file>
<file name="src/allora_sdk/utils/format.py">
<violation number="1" location="src/allora_sdk/utils/format.py:7">
P2: Renaming this exported helper without a compatibility alias introduces a breaking API change for existing SDK users.</violation>
</file>
<file name="src/allora_sdk/rpc_client/config.py">
<violation number="1" location="src/allora_sdk/rpc_client/config.py:126">
P2: This new local gRPC default is shadowed by `AlloraRPCClient.local()`, which still passes `26657`, so the two local helpers now resolve to different ports.</violation>
</file>
<file name="src/allora_sdk/loss_methods/zptae.py">
<violation number="1" location="src/allora_sdk/loss_methods/zptae.py:99">
P2: Validate `mse_norm` and `gamma` here. Without those checks, valid caller input can make `mse_term` raise on exact matches or return a complex value instead of a float.</violation>
</file>
<file name="src/allora_sdk/rpc_client/client_staking.py">
<violation number="1" location="src/allora_sdk/rpc_client/client_staking.py:62">
P1: Keep `staking.query` as `None` when no gRPC staking client is available. Wrapping `None` in `StakingQueries` breaks the existing REST-mode feature check and makes validator autostake configuration fail instead of skipping validation.</violation>
</file>
<file name="src/allora_sdk/loss_methods/defaults.py">
<violation number="1" location="src/allora_sdk/loss_methods/defaults.py:25">
P2: This error message points users to a nonexistent `loss_fn` argument on `AlloraWorker.reputer()`; tell them to provide a custom `reputer_fn` instead.</violation>
<violation number="2" location="src/allora_sdk/loss_methods/defaults.py:37">
P1: Don't auto-register `ztae`/`zptae` as default loss methods; their `std=1.0` placeholders will silently compute the wrong loss for topics that require caller-supplied standardization.</violation>
</file>
<file name="src/allora_sdk/worker/reputer.py">
<violation number="1" location="src/allora_sdk/worker/reputer.py:121">
P2: Returning an empty set here removes the polling catch-up path, so reputer windows are missed whenever the open event was not observed live.</violation>
<violation number="2" location="src/allora_sdk/worker/reputer.py:194">
P1: Post-submit staking here can consume the next account sequence and break the following nonce submission in the same polling cycle.</violation>
</file>
<file name="src/allora_sdk/rpc_client/client_websocket_events.py">
<violation number="1" location="src/allora_sdk/rpc_client/client_websocket_events.py:169">
P2: The new empty `TBetterproto2Message` bound removes the protobuf-type constraint from the typed subscription API, so unrelated classes can now slip through the name-based event lookup.</violation>
</file>
<file name="src/allora_sdk/worker/worker.py">
<violation number="1" location="src/allora_sdk/worker/worker.py:760">
P1: Don't advance `account_seq` by loop index here; some submit paths return before broadcasting, so later nonces can use a future sequence and fail with account-sequence mismatch.</violation>
</file>
<file name="src/allora_sdk/worker/autostake.py">
<violation number="1" location="src/allora_sdk/worker/autostake.py:315">
P2: The balance precheck ignores `fee_reserve_uallo`, so autostake can consume the configured fee reserve and still leave the wallet without gas.</violation>
</file>
<file name="src/allora_sdk/worker/utils.py">
<violation number="1" location="src/allora_sdk/worker/utils.py:22">
P1: Return `wallet.wallet` here; otherwise `AlloraWalletConfig(wallet=...)` is ignored and the worker may use a different account.</violation>
<violation number="2" location="src/allora_sdk/worker/utils.py:29">
P1: Fail when an explicit `wallet.mnemonic_file` is missing; silently generating a new wallet here can switch the worker to an unintended account.</violation>
<violation number="3" location="src/allora_sdk/worker/utils.py:127">
P2: Include the client/network in this cache key; topic ID and nonce alone are not enough for a `gt_fn` that depends on `RunContext.client`.</violation>
</file>
<file name="src/allora_sdk/rpc_client/client_emissions.py">
<violation number="1" location="src/allora_sdk/rpc_client/client_emissions.py:239">
P2: Add default gas fallbacks for the new tx types. Without entries in `TxManager._default_gas_limits`, simulation-unavailable paths drop to the generic 240k estimate for `AddStakeRequest` and `InsertReputerPayloadRequest`.</violation>
<violation number="2" location="src/allora_sdk/rpc_client/client_emissions.py:272">
P2: Populate/validate the signed bundle's `topic_id` and `reputer` before hashing. Right now this helper signs whatever is already inside `value_bundle`, so a stale bundle can be submitted for the wrong topic or actor.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as Client Code (Inferer/Forecaster)
participant Worker as AlloraWorker
participant TxMgr as TxManager.submit_transaction()
participant Sim as simulate_transaction()
participant Fee as _calculate_optimal_fee()
participant RPC as gRPC / REST (Chain)
participant Broadcast as _attempt_submissions()
Note over Client,Broadcast: Transaction Submission Flow
Client->>Worker: submit_transaction() call
Worker->>TxMgr: submit_transaction(type_url, msgs, account_seq)
TxMgr->>Sim: simulate_transaction(type_url, msgs, account_seq)
Sim->>RPC: SimulateRequest (tx_bytes)
alt Simulation succeeds
RPC-->>Sim: GasInfo (gas_used)
Sim-->>TxMgr: estimated_gas (int)
TxMgr->>Fee: _calculate_optimal_fee(gas, multiplier)
Fee-->>TxMgr: fee_preview (for logging only)
alt Fee preview fails
TxMgr->>TxMgr: Log debug, keep estimated_gas
end
TxMgr->>Broadcast: _attempt_submissions(pending, gas_limit=estimated_gas, account_seq)
else Deterministic rejection (TxError, OutOfGasError, AccountSequenceMismatchError)
RPC-->>Sim: grpc.RpcError (classified)
Sim-->>TxMgr: Raise classified error
Note over TxMgr: NEW: Fail-fast, do not broadcast
TxMgr-->>Worker: Raise exception (TxError / OutOfGasError / AccountSequenceMismatchError)
else Simulation infrastructure unavailable (GasSimulationUnavailableError)
RPC-->>Sim: grpc.RpcError (UNAVAILABLE)
Sim-->>TxMgr: Raise GasSimulationUnavailableError
Note over TxMgr: NEW: Only fallback for infrastructure issues
TxMgr->>Broadcast: _attempt_submissions(pending, gas_limit=None, account_seq)
else Fee-specific simulation failure (InsufficientFeesError)
RPC-->>Sim: grpc.RpcError (classified)
Sim-->>TxMgr: Raise InsufficientFeesError
Note over TxMgr: NEW: Fallback with warning for fee issues
TxMgr->>TxMgr: Log warning
TxMgr->>Broadcast: _attempt_submissions(pending, gas_limit=None, account_seq)
end
Broadcast->>Broadcast: Build & sign tx with estimated_gas or default
Broadcast->>RPC: BroadcastTx (signed)
RPC-->>Broadcast: TxResponse
Broadcast-->>Worker: PendingTx result
Note over Client,Broadcast: Key Change: Deterministic simulation failures now raise before broadcast
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| except (TxError, OutOfGasError, AccountSequenceMismatchError, InsufficientFeesError): | ||
| logger.error(f"Transaction simulation failed for {type_url}; not broadcasting", exc_info=True) | ||
| raise |
There was a problem hiding this comment.
P1: InsufficientFeesError is still treated as a fatal simulation failure, so nodes that enforce min fees during simulation can block otherwise valid broadcasts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/allora_sdk/rpc_client/tx_manager.py, line 194:
<comment>`InsufficientFeesError` is still treated as a fatal simulation failure, so nodes that enforce min fees during simulation can block otherwise valid broadcasts.</comment>
<file context>
@@ -156,34 +171,67 @@ async def submit_transaction(
+ f"Unable to simulate transaction for {type_url}; "
+ f"falling back to default gas estimate: {err}"
+ )
+ except (TxError, OutOfGasError, AccountSequenceMismatchError, InsufficientFeesError):
+ logger.error(f"Transaction simulation failed for {type_url}; not broadcasting", exc_info=True)
+ raise
</file context>
| except (TxError, OutOfGasError, AccountSequenceMismatchError, InsufficientFeesError): | |
| logger.error(f"Transaction simulation failed for {type_url}; not broadcasting", exc_info=True) | |
| raise | |
| except InsufficientFeesError as err: | |
| logger.warning( | |
| f"Simulation reported insufficient fees for {type_url}; " | |
| f"falling back to default gas estimate: {err}" | |
| ) | |
| except (TxError, OutOfGasError, AccountSequenceMismatchError): | |
| logger.error(f"Transaction simulation failed for {type_url}; not broadcasting", exc_info=True) | |
| raise |
| url = self.base_url + f"{{ method.url_path }}" | ||
| headers = {"x-cosmos-block-height": height} if height else None | ||
| response = await self.session.get(url, params=params) | ||
| response = await self.session.get(url, params=params, headers=headers) |
There was a problem hiding this comment.
P1: Convert height to a string before sending it as an HTTP header; passing the raw int here can make every height= query fail before the request is sent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/templates/rest_client.py.jinja, line 46:
<comment>Convert `height` to a string before sending it as an HTTP header; passing the raw `int` here can make every `height=` query fail before the request is sent.</comment>
<file context>
@@ -47,9 +43,9 @@ class {{ service.class_name }}({{ service.protocol_name }}):
url = self.base_url + f"{{ method.url_path }}"
headers = {"x-cosmos-block-height": height} if height else None
- response = await self.session.get(url, params=params)
+ response = await self.session.get(url, params=params, headers=headers)
response.raise_for_status()
- return {{ method.response_type_name }}().from_json(response.text)
</file context>
| response = await self.session.get(url, params=params, headers=headers) | |
| if headers is not None: | |
| headers["x-cosmos-block-height"] = str(headers["x-cosmos-block-height"]) | |
| response = await self.session.get(url, params=params, headers=headers) |
| """ | ||
|
|
||
| def __init__(self, query_client: StakingQueryClientLike | None = None, tx_manager: TxManager | None = None): | ||
| self.query = StakingQueries(query_client) |
There was a problem hiding this comment.
P1: Keep staking.query as None when no gRPC staking client is available. Wrapping None in StakingQueries breaks the existing REST-mode feature check and makes validator autostake configuration fail instead of skipping validation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/allora_sdk/rpc_client/client_staking.py, line 62:
<comment>Keep `staking.query` as `None` when no gRPC staking client is available. Wrapping `None` in `StakingQueries` breaks the existing REST-mode feature check and makes validator autostake configuration fail instead of skipping validation.</comment>
<file context>
@@ -0,0 +1,112 @@
+ """
+
+ def __init__(self, query_client: StakingQueryClientLike | None = None, tx_manager: TxManager | None = None):
+ self.query = StakingQueries(query_client)
+ self.tx = StakingTxs(txs=tx_manager)
+
</file context>
| self.query = StakingQueries(query_client) | |
| self.query = StakingQueries(query_client) if query_client is not None else None |
| "logcosh": log_cosh_loss, | ||
| "bce": binary_cross_entropy_loss, | ||
| "poisson": poisson_loss, | ||
| "ztae": ztae_loss, |
There was a problem hiding this comment.
P1: Don't auto-register ztae/zptae as default loss methods; their std=1.0 placeholders will silently compute the wrong loss for topics that require caller-supplied standardization.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/allora_sdk/loss_methods/defaults.py, line 37:
<comment>Don't auto-register `ztae`/`zptae` as default loss methods; their `std=1.0` placeholders will silently compute the wrong loss for topics that require caller-supplied standardization.</comment>
<file context>
@@ -0,0 +1,144 @@
+ "logcosh": log_cosh_loss,
+ "bce": binary_cross_entropy_loss,
+ "poisson": poisson_loss,
+ "ztae": ztae_loss,
+ "zptae": zptae_loss,
+}
</file context>
| logger.error(f"❌ Unexpected error submitting reputer payload: {err.__class__.__name__} {err}") | ||
| return err | ||
|
|
||
| # Optional stake top-up after successful payload; runs with fresh context |
There was a problem hiding this comment.
P1: Post-submit staking here can consume the next account sequence and break the following nonce submission in the same polling cycle.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/allora_sdk/worker/reputer.py, line 194:
<comment>Post-submit staking here can consume the next account sequence and break the following nonce submission in the same polling cycle.</comment>
<file context>
@@ -0,0 +1,373 @@
+ logger.error(f"❌ Unexpected error submitting reputer payload: {err.__class__.__name__} {err}")
+ return err
+
+ # Optional stake top-up after successful payload; runs with fresh context
+ # so it does not consume sequence before core submission.
+ try:
</file context>
|
|
||
|
|
||
| GenericSyncCallbackFn = Callable[[Dict[str, Any], int], None] | ||
| TEvent = TypeVar("TEvent", bound="TBetterproto2Message") |
There was a problem hiding this comment.
P2: The new empty TBetterproto2Message bound removes the protobuf-type constraint from the typed subscription API, so unrelated classes can now slip through the name-based event lookup.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/allora_sdk/rpc_client/client_websocket_events.py, line 169:
<comment>The new empty `TBetterproto2Message` bound removes the protobuf-type constraint from the typed subscription API, so unrelated classes can now slip through the name-based event lookup.</comment>
<file context>
@@ -167,13 +166,13 @@ def transactions():
-GenericSyncCallbackFn = Callable[[Dict[str, Any], int], None]
+TEvent = TypeVar("TEvent", bound="TBetterproto2Message")
+GenericSyncCallbackFn = Callable[[Dict[str, Any], int], None]
GenericAsyncCallbackFn = Callable[[Dict[str, Any], int], Awaitable[None]]
</file context>
| TEvent = TypeVar("TEvent", bound="TBetterproto2Message") | |
| TEvent = TypeVar("TEvent", bound=betterproto2.Message) |
| QueryBalanceRequest(address=wallet_addr, denom=client.network.fee_denom) | ||
| ) | ||
| available = int(bal_resp.balance.amount) if bal_resp.balance else 0 | ||
| if available < stake_amount_uallo: |
There was a problem hiding this comment.
P2: The balance precheck ignores fee_reserve_uallo, so autostake can consume the configured fee reserve and still leave the wallet without gas.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/allora_sdk/worker/autostake.py, line 315:
<comment>The balance precheck ignores `fee_reserve_uallo`, so autostake can consume the configured fee reserve and still leave the wallet without gas.</comment>
<file context>
@@ -0,0 +1,386 @@
+ QueryBalanceRequest(address=wallet_addr, denom=client.network.fee_denom)
+ )
+ available = int(bal_resp.balance.amount) if bal_resp.balance else 0
+ if available < stake_amount_uallo:
+ logger.warning(
+ "[AUTO-STAKE] Insufficient balance: have %d uallo, need %d uallo. Skipping.",
</file context>
| async def rep_func(context: RunContext, inference: float) -> float: | ||
| nonlocal gt_cache | ||
|
|
||
| cache_key = (context.topic_id, context.nonce) |
There was a problem hiding this comment.
P2: Include the client/network in this cache key; topic ID and nonce alone are not enough for a gt_fn that depends on RunContext.client.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/allora_sdk/worker/utils.py, line 127:
<comment>Include the client/network in this cache key; topic ID and nonce alone are not enough for a `gt_fn` that depends on `RunContext.client`.</comment>
<file context>
@@ -0,0 +1,143 @@
+ async def rep_func(context: RunContext, inference: float) -> float:
+ nonlocal gt_cache
+
+ cache_key = (context.topic_id, context.nonce)
+ if gt_cache is not None and gt_cache[0] == cache_key:
+ gt = gt_cache[1]
</file context>
| @@ -1,7 +1,9 @@ | |||
| import hashlib | |||
There was a problem hiding this comment.
P2: Add default gas fallbacks for the new tx types. Without entries in TxManager._default_gas_limits, simulation-unavailable paths drop to the generic 240k estimate for AddStakeRequest and InsertReputerPayloadRequest.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/allora_sdk/rpc_client/client_emissions.py, line 239:
<comment>Add default gas fallbacks for the new tx types. Without entries in `TxManager._default_gas_limits`, simulation-unavailable paths drop to the generic 240k estimate for `AddStakeRequest` and `InsertReputerPayloadRequest`.</comment>
<file context>
@@ -193,29 +178,123 @@ async def delegate_stake(
+ logger.debug(f"Adding stake of {amount} uallo to topic {topic_id}")
+
+ return await self._txs.submit_transaction(
+ type_url="/emissions.v9.AddStakeRequest",
+ msgs=[msg],
+ fee_tier=fee_tier,
</file context>
|
|
||
| reputer_address = str(self._txs.wallet.address()) | ||
|
|
||
| value_bundle.reputer_request_nonce = reputer_request_nonce |
There was a problem hiding this comment.
P2: Populate/validate the signed bundle's topic_id and reputer before hashing. Right now this helper signs whatever is already inside value_bundle, so a stale bundle can be submitted for the wrong topic or actor.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/allora_sdk/rpc_client/client_emissions.py, line 272:
<comment>Populate/validate the signed bundle's `topic_id` and `reputer` before hashing. Right now this helper signs whatever is already inside `value_bundle`, so a stale bundle can be submitted for the wrong topic or actor.</comment>
<file context>
@@ -193,29 +178,123 @@ async def delegate_stake(
+
+ reputer_address = str(self._txs.wallet.address())
+
+ value_bundle.reputer_request_nonce = reputer_request_nonce
+
+ # Sign the value bundle
</file context>
| value_bundle.reputer_request_nonce = reputer_request_nonce | |
| if value_bundle.topic_id and value_bundle.topic_id != topic_id: | |
| raise ValueError(f"value_bundle.topic_id ({value_bundle.topic_id}) must match topic_id ({topic_id})") | |
| value_bundle.topic_id = topic_id | |
| value_bundle.reputer = reputer_address | |
| value_bundle.reputer_request_nonce = reputer_request_nonce |
51afeb1 to
7b8f197
Compare
Clement Issue 3: TX Simulation Catch-All Hides Guaranteed Failures
Verdict
Clement's issue 3 is still valid in the current tree.
The current
TxManager.submit_transaction()treats every simulation-stage exception as if it were merely a gas-estimation failure. That is too broad. Some simulation failures mean "we could not estimate gas", but others mean "the transaction message is invalid and should not be broadcast." The current code does not distinguish those cases.There is also a second, smaller bug in the same block: fee preview is wrapped in the same
tryas simulation, so a fee-preview failure can discard a successful gas simulation and force a fallback to default gas.Current Problem Code
Current code in
src/allora_sdk/rpc_client/tx_manager.py:The problematic part is:
This catches all of the following as equivalent:
TxError.AccountSequenceMismatchError.OutOfGasError.InsufficientFeesError._calculate_optimal_fee(), even after simulation succeeded.Those cases should not have the same behavior.
Why This Is A Real Bug
simulate_transaction()already classifies errors into meaningful exception types:But
submit_transaction()immediately flattens those classified errors back intoestimated_gas_limit = None.That defeats the purpose of simulation classification. A simulation
TxErrorsuch as "unauthorized", "already submitted", "account not found", or a malformed message is not a gas estimation problem. Broadcasting after that is at best noisy and at worst fee-wasting.Current Test Codifies The Bad Behavior
The current test suite explicitly asserts the problematic fallback:
That test should be inverted. A deterministic simulation
TxErrorshould stop submission before_attempt_submissions()is scheduled.Concrete Failure Mode
Example flow with current code:
TxManager.submit_transaction()callssimulate_transaction().simulate_transaction()raisesTxError(codespace="simulation", code=..., message="unauthorized").submit_transaction()catches it withexcept Exception.PendingTx._attempt_submissions(..., gas_limit=None)._build_and_broadcast()falls back to_estimate_gas(type_url).The user now sees a later broadcast-time or inclusion-time failure, not the original simulation failure. In production, this makes debugging harder and can waste fees.
Extra Problem: Fee Preview Can Discard A Good Simulation
This block also wraps
_calculate_optimal_fee():If simulation succeeds but fee preview raises, the catch-all sets:
That throws away a valid simulated gas estimate. Fee preview is only used for logging. It should not be able to force a default-gas fallback after simulation succeeded.
Integration Perspective
This differs from
allora-offchain-nodesemantics.In
allora-offchain-node, transaction construction calls simulation duringBuildAndSignTransaction(). If simulation fails, transaction construction returns an error. Retry handling then happens in the transaction send path, where sequence, fees, and gas are handled as classified cases.The current SDK behavior is weaker:
For a tool intended to replace
allora-offchain-node, this should be tightened.Suggested Fix
Design Goal
Do not treat every simulation failure as recoverable.
Recommended behavior:
TxError, fail fast and do not broadcast.Minimal Code Change
This is the smallest useful change:
This preserves fallback for truly unclassified simulation infrastructure failures, but prevents known deterministic transaction failures from being broadcast.
Better Long-Term Fix
The cleaner version is to introduce a dedicated exception for simulation availability failures and keep classified chain rejections separate:
Then
simulate_transaction()should only raiseGasSimulationUnavailableErrorfor cases where the SDK genuinely could not ask the chain for an estimate. It should continue raisingTxError,OutOfGasError,AccountSequenceMismatchError, andInsufficientFeesErrorfor chain-evaluated transaction failures.Then
submit_transaction()becomes simpler:That makes the policy explicit:
Suggested Test Changes
Replace The Current TxError Fallback Test
Current test name:
test_submit_transaction_falls_back_when_simulation_raises_tx_errorSuggested replacement:
Add A Test For Fee Preview Failure
This captures the second bug:
Optional Fallback Test
If the SDK intentionally keeps fallback for simulation infrastructure outages, add a narrow test for only that class of error. If no dedicated exception exists yet, this test should wait until the exception is introduced; otherwise it risks keeping the current broad fallback alive.
Risk Of The Suggested Change
This is a behavior change. Some callers may currently rely on
submit_transaction()returning aPendingTxeven when simulation fails. After the minimal fix, deterministic simulation failures would raise before aPendingTxis created.That behavior is still preferable for production transaction safety, but if API compatibility is important, there is an alternative:
PendingTxbefore simulation.pending._final_future.set_exception(err).PendingTxwithout scheduling_attempt_submissions().That preserves the "caller awaits
pending.wait()" shape while still preventing broadcast.Suggested compatibility variant:
I would only choose this variant if existing SDK users strongly depend on
submit_transaction()never raising simulation errors directly.Final Recommendation
Fix this before positioning the SDK as the replacement runtime for production offchain-node users.
The minimum acceptable fix is:
TxError,OutOfGasError, andAccountSequenceMismatchErrorfrom simulation._attempt_submissions().Summary by cubic
Hardened transaction simulation, gas, and fee handling to stop broadcasting guaranteed failures, keep valid gas estimates, and make fallbacks explicit. Also added first‑class worker modules (Inferer, Forecaster, Reputer), default loss methods, autostake, staking queries, examples, and better logging.
Bug Fixes
TxManager.submit_transaction: fail fast onTxError,OutOfGasError,AccountSequenceMismatchError, andInsufficientFeesError; fallback only when RPC is unavailable via newGasSimulationUnavailableError; keep simulated gas when fee preview fails.account_seqso the same sequence is used for simulation and broadcast; keeps parent tx ID safety and serialized submissions.UNAVAILABLE/DEADLINE_EXCEEDEDtoGasSimulationUnavailableError; raiseWalletNotConfiguredErrorinstead of a generic exception.x-cosmos-block-height, ignore unknown fields, and avoid per‑request session churn.Migration
PendingTxon guaranteed failures. Fallback to default gas happens only when simulation is unavailable.RunContext; update run functions toasync def run(ctx: RunContext) -> .... New helpers:make_reputer_function,get_network_inference,get_block_time.ztae/zptaerequire explicit loss configuration. Default loss selection available viaallora_sdk.loss_methods.grpcioandprotobuf.AlloraNetworkConfigaddsdynamic_gas_price_default_multiplierandquery_timeout_secs; testnet faucet URL updated.Written for commit 7b8f197. Summary will update on new commits. Review in cubic