diff --git a/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx b/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx
index 42fa881bf3..d4e825a2f2 100644
--- a/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx
+++ b/applications/tari_indexer/web_ui/src/routes/Substates/Substates.tsx
@@ -242,12 +242,6 @@ function TransactionReceiptView({ data }: { data: any }) {
{renderJson(data.events)}
>
)}
- {data.logs?.length > 0 && (
- <>
- Logs ({data.logs.length})
- {renderJson(data.logs)}
- >
- )}
Diff Summary{renderJson(data.diff_summary)}
diff --git a/applications/tari_ootle_app_utilities/src/fee_tables.rs b/applications/tari_ootle_app_utilities/src/fee_tables.rs
index 1d585c9db8..699936c71c 100644
--- a/applications/tari_ootle_app_utilities/src/fee_tables.rs
+++ b/applications/tari_ootle_app_utilities/src/fee_tables.rs
@@ -168,3 +168,31 @@ pub const fn get_fee_table_by_network(network: Network) -> &'static FeeTable {
Network::MainNet => &MAINNET_FEE_TABLE,
}
}
+
+#[cfg(test)]
+mod tests {
+ use tari_engine_types::fees::{FEE_ESTIMATE_ALLOWANCE, MAX_EXHAUST_BURN_RATE_BPS};
+
+ use super::*;
+
+ /// `FEE_ESTIMATE_ALLOWANCE` is restated in `tari_engine_types`, which cannot see a `FeeTable`.
+ /// Every shipped table must come in under it at the highest burn the estimate is derived
+ /// against, or a dry run under-states what a real submission costs.
+ #[test]
+ fn fee_estimate_allowance_covers_every_shipped_network() {
+ for network in [
+ Network::MainNet,
+ Network::StageNet,
+ Network::NextNet,
+ Network::Igor,
+ Network::Esmeralda,
+ Network::LocalNet,
+ ] {
+ let derived = get_fee_table_by_network(network).fee_estimate_allowance(MAX_EXHAUST_BURN_RATE_BPS);
+ assert!(
+ derived <= FEE_ESTIMATE_ALLOWANCE,
+ "{network} needs an allowance of {derived}, above the restated {FEE_ESTIMATE_ALLOWANCE}"
+ );
+ }
+ }
+}
diff --git a/applications/tari_wallet_cli/src/command/account.rs b/applications/tari_wallet_cli/src/command/account.rs
index a34186ba22..2ae975a728 100644
--- a/applications/tari_wallet_cli/src/command/account.rs
+++ b/applications/tari_wallet_cli/src/command/account.rs
@@ -187,7 +187,9 @@ async fn handle_create_free_test_coins(
let resp = client
.create_free_test_coins(AccountsCreateFreeTestCoinsRequest {
account: account.component_address.into(),
- max_fee: args.fee.unwrap_or(1500),
+ // The claim creates the account and funds it, so it pays for a component, a vault, a
+ // badge and the receipt. Defaulted well clear of that; the unspent remainder is refunded.
+ max_fee: args.fee.unwrap_or(50_000),
})
.await?;
diff --git a/applications/tari_wallet_cli/src/command/validator.rs b/applications/tari_wallet_cli/src/command/validator.rs
index c9b6c88db9..bfed18387b 100644
--- a/applications/tari_wallet_cli/src/command/validator.rs
+++ b/applications/tari_wallet_cli/src/command/validator.rs
@@ -99,7 +99,9 @@ pub async fn handle_claim_validator_fees(
.map(|name| ComponentAddressOrName::from_str(&name))
.transpose()?,
claim_key_index: None,
- max_fee: max_fee.map(Into::into).unwrap_or(1500),
+ // A claim writes the fee pool, the destination vault and the receipt. Defaulted well
+ // clear of that; the unspent remainder is refunded.
+ max_fee: max_fee.map(Into::into).unwrap_or(50_000),
shards: vec![shard],
dry_run,
output_to_revealed: revealed,
diff --git a/bindings/package.json b/bindings/package.json
index 2bfd9ab31a..c790bd6820 100644
--- a/bindings/package.json
+++ b/bindings/package.json
@@ -1,6 +1,6 @@
{
"name": "@tari-project/ootle-ts-bindings",
- "version": "1.49.0",
+ "version": "1.49.1",
"private": false,
"publishConfig": {
"access": "public"
diff --git a/bindings/src/index.ts b/bindings/src/index.ts
index 924e0906c4..d634161883 100644
--- a/bindings/src/index.ts
+++ b/bindings/src/index.ts
@@ -12,39 +12,40 @@ export * from "./types/ArgDef";
export * from "./types/Assertion";
export * from "./types/AtomicCondition";
export * from "./types/AuthHook";
-export * from "./types/Blob";
export * from "./types/BlobHashes";
export * from "./types/Blobs";
-export * from "./types/Block";
+export * from "./types/Blob";
export * from "./types/BlockHeader";
export * from "./types/BlockId";
+export * from "./types/Block";
export * from "./types/BucketId";
export * from "./types/BuiltinPredicate";
export * from "./types/Bytes";
export * from "./types/CheckOrd";
export * from "./types/ClaimBurnOutputData";
-export * from "./types/ClaimedOutputTombstone";
export * from "./types/ClaimedOutputTombstoneAddress";
+export * from "./types/ClaimedOutputTombstone";
export * from "./types/Claims";
export * from "./types/Command";
export * from "./types/CommitmentSignatureBytes";
-export * from "./types/Committee";
+export * from "./types/CommitmentValueProof";
export * from "./types/CommitteeInfo";
export * from "./types/CommitteeMember";
-export * from "./types/Component";
+export * from "./types/Committee";
export * from "./types/ComponentAccessRules";
export * from "./types/ComponentAddress";
export * from "./types/ComponentBody";
export * from "./types/ComponentHeader";
export * from "./types/ComponentKey";
export * from "./types/ComponentReference";
-export * from "./types/ConfidentialOutput";
-export * from "./types/ConfidentialOutputAddress";
+export * from "./types/Component";
export * from "./types/ConfidentialOutputAddressContents";
+export * from "./types/ConfidentialOutputAddress";
export * from "./types/ConfidentialOutputStatement";
+export * from "./types/ConfidentialOutput";
export * from "./types/ConfidentialWithdrawProof";
-export * from "./types/Covenant";
export * from "./types/CovenantBalanceClaim";
+export * from "./types/Covenant";
export * from "./types/Crud";
export * from "./types/CurrentInputView";
export * from "./types/Decision";
@@ -58,8 +59,8 @@ export * from "./types/Epoch";
export * from "./types/Era";
export * from "./types/Event";
export * from "./types/EvictNodeAtom";
-export * from "./types/Evidence";
export * from "./types/EvidenceInputLockData";
+export * from "./types/Evidence";
export * from "./types/ExecuteResult";
export * from "./types/ExtraData";
export * from "./types/FeeBreakdown";
@@ -76,12 +77,12 @@ export * from "./types/Hash64";
export * from "./types/HashAlg";
export * from "./types/IndexedValue";
export * from "./types/IndexedWellKnownTypes";
-export * from "./types/Instruction";
export * from "./types/InstructionArg";
export * from "./types/InstructionResult";
+export * from "./types/Instruction";
export * from "./types/LeaderFee";
-export * from "./types/LockFlag";
export * from "./types/LockedEpoch";
+export * from "./types/LockFlag";
export * from "./types/LogEntry";
export * from "./types/LogLevel";
export * from "./types/Memo";
@@ -92,12 +93,12 @@ export * from "./types/MinotariBurnClaimProof";
export * from "./types/Network";
export * from "./types/NftCheck";
export * from "./types/NodeHeight";
-export * from "./types/NonFungible";
-export * from "./types/NonFungibleAddress";
export * from "./types/NonFungibleAddressContents";
+export * from "./types/NonFungibleAddress";
export * from "./types/NonFungibleContainer";
export * from "./types/NonFungibleId";
export * from "./types/NonFungibleToken";
+export * from "./types/NonFungible";
export * from "./types/NumPreshards";
export * from "./types/OotleAddress";
export * from "./types/Ordering";
@@ -107,8 +108,8 @@ export * from "./types/OwnerRule";
export * from "./types/PayRef";
export * from "./types/PedersenCommitmentBytes";
export * from "./types/PeerAddress";
-export * from "./types/Permission";
export * from "./types/Permissions";
+export * from "./types/Permission";
export * from "./types/PrecisionAmount";
export * from "./types/ProofId";
export * from "./types/ProposalCertificate";
@@ -116,42 +117,40 @@ export * from "./types/PrunedTransaction";
export * from "./types/PrunedTransactionV1";
export * from "./types/PrunedUnsealedTransactionV1";
export * from "./types/PrunedUnsignedTransactionV1";
-export * from "./types/PublishedTemplate";
export * from "./types/PublishedTemplateAddress";
export * from "./types/PublishedTemplateMetadata";
+export * from "./types/PublishedTemplate";
export * from "./types/RangeProofBytes";
export * from "./types/ReadOnly";
export * from "./types/RejectReason";
export * from "./types/RequireRule";
-export * from "./types/Resource";
export * from "./types/ResourceAccessRules";
-export * from "./types/ResourceAddress";
export * from "./types/ResourceAddressRef";
+export * from "./types/ResourceAddress";
export * from "./types/ResourceContainer";
+export * from "./types/Resource";
export * from "./types/ResourceType";
export * from "./types/RestrictedAccessRule";
export * from "./types/RistrettoPublicKeyBytes";
export * from "./types/RuleRequirement";
export * from "./types/Scalar32Bytes";
export * from "./types/SchnorrSignatureBytes";
-export * from "./types/Shard";
-export * from "./types/ShardGroup";
export * from "./types/ShardGroupAccumulatedData";
export * from "./types/ShardGroupEvidence";
+export * from "./types/ShardGroup";
export * from "./types/ShardStateVersions";
+export * from "./types/Shard";
export * from "./types/SpendAuthorization";
export * from "./types/SpendCondition";
export * from "./types/SpendWitness";
export * from "./types/StateVersion";
+export * from "./types/StealthInputsStatement";
export * from "./types/StealthInput";
export * from "./types/StealthInputView";
-export * from "./types/StealthInputsStatement";
-export * from "./types/StealthOutputView";
export * from "./types/StealthOutputsStatement";
+export * from "./types/StealthOutputView";
export * from "./types/StealthTransferStatement";
export * from "./types/StealthUnspentOutput";
-export * from "./types/CommitmentValueProof";
-export * from "./types/Substate";
export * from "./types/SubstateAddress";
export * from "./types/SubstateCreated";
export * from "./types/SubstateDestroyed";
@@ -161,6 +160,7 @@ export * from "./types/SubstateLockType";
export * from "./types/SubstateOwnerRule";
export * from "./types/SubstateRecord";
export * from "./types/SubstateRequirement";
+export * from "./types/Substate";
export * from "./types/SubstateType";
export * from "./types/SubstateValue";
export * from "./types/TemplateDef";
@@ -168,19 +168,19 @@ export * from "./types/TemplateDefV1";
export * from "./types/TemplateFunction";
export * from "./types/TemplateMetadata";
export * from "./types/TimeoutCertificate";
-export * from "./types/Transaction";
export * from "./types/TransactionAtom";
export * from "./types/TransactionEnvelope";
export * from "./types/TransactionExecution";
export * from "./types/TransactionId";
export * from "./types/TransactionPoolRecord";
export * from "./types/TransactionPoolStage";
-export * from "./types/TransactionReceipt";
export * from "./types/TransactionReceiptAddress";
+export * from "./types/TransactionReceipt";
export * from "./types/TransactionResult";
export * from "./types/TransactionSealSignature";
export * from "./types/TransactionSignature";
export * from "./types/TransactionStatus";
+export * from "./types/Transaction";
export * from "./types/TransactionV1";
export * from "./types/TxRequestAction";
export * from "./types/Type";
@@ -189,11 +189,10 @@ export * from "./types/UnsealedTransactionV1";
export * from "./types/UnsignedTransaction";
export * from "./types/UnsignedTransactionV1";
export * from "./types/UnspentOutput";
-export * from "./types/UpSubstate";
export * from "./types/UpdateRule";
-export * from "./types/Utxo";
-export * from "./types/UtxoAddress";
+export * from "./types/UpSubstate";
export * from "./types/UtxoAddressContents";
+export * from "./types/UtxoAddress";
export * from "./types/UtxoBurnt";
export * from "./types/UtxoId";
export * from "./types/UtxoInputSelection";
@@ -201,18 +200,19 @@ export * from "./types/UtxoOutput";
export * from "./types/UtxoSpent";
export * from "./types/UtxoStateUpdateSet";
export * from "./types/UtxoTag";
+export * from "./types/Utxo";
export * from "./types/UtxoUnspent";
export * from "./types/UtxoUpdateSet";
-export * from "./types/ValidatorFeePool";
export * from "./types/ValidatorFeePoolAddress";
+export * from "./types/ValidatorFeePool";
export * from "./types/ValidatorFeeWithdrawal";
export * from "./types/ValidatorSignatureBytes";
export * from "./types/ValueKnowledgeProof";
-export * from "./types/Vault";
export * from "./types/VaultFreezeFlags";
export * from "./types/VaultId";
-export * from "./types/VersionedSubstateId";
+export * from "./types/Vault";
export * from "./types/VersionedSubstateIdLockIntent";
+export * from "./types/VersionedSubstateId";
export * from "./types/ViewableBalanceProof";
export * from "./types/VotePower";
export * from "./types/WalletTransaction";
@@ -223,10 +223,10 @@ export * from "./tari-indexer-client";
export * from "./validator-node-client";
export * from "./wallet-types";
export * from "./helpers/BigAmount";
-export * from "./helpers/NetworkByte";
export * from "./helpers/cbor";
export * from "./helpers/consts";
export * from "./helpers/enum";
export * from "./helpers/helpers";
+export * from "./helpers/NetworkByte";
export * from "./helpers/ootleAddress";
export * from "./helpers/tariTypeTag";
diff --git a/bindings/src/tari-indexer-client.ts b/bindings/src/tari-indexer-client.ts
index 74696b6255..ac87b9f223 100644
--- a/bindings/src/tari-indexer-client.ts
+++ b/bindings/src/tari-indexer-client.ts
@@ -1,68 +1,68 @@
// Copyright 2026 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause
-export * from "./types/tari-indexer-client/NetworkDescription";
-export * from "./types/tari-indexer-client/TemplateCatalogueItem";
-export * from "./types/tari-indexer-client/IndexerGetEpochManagerStatsResponse";
-export * from "./types/tari-indexer-client/GetLatestEpochCheckpointResponse";
-export * from "./types/tari-indexer-client/ListTransactionReceiptsResponse";
-export * from "./types/tari-indexer-client/NonFungibleSubstate";
-export * from "./types/tari-indexer-client/QueryTransactionEventsResponse";
-export * from "./types/tari-indexer-client/IndexerGetIdentityResponse";
-export * from "./types/tari-indexer-client/TemplateMeta";
-export * from "./types/tari-indexer-client/ListTemplateCatalogueResponse";
-export * from "./types/tari-indexer-client/IndexerGetSubstateRequest";
+export * from "./types/tari-indexer-client/IndexerSubmitTransactionResponse";
+export * from "./types/tari-indexer-client/GetTransactionReceiptResponse";
export * from "./types/tari-indexer-client/GetNetworkInfoResponse";
-export * from "./types/tari-indexer-client/GetTemplateDefinitionResponse";
-export * from "./types/tari-indexer-client/ListTemplatesRequest";
-export * from "./types/tari-indexer-client/GetNonFungiblesRequest";
+export * from "./types/tari-indexer-client/WatchedSubstateItem";
+export * from "./types/tari-indexer-client/GetUtxoUpdatesRequest";
+export * from "./types/tari-indexer-client/WatchedTemplateItem";
+export * from "./types/tari-indexer-client/ValidatorConsensusState";
+export * from "./types/tari-indexer-client/StreamTransactionEventsRequest";
+export * from "./types/tari-indexer-client/QueryTransactionEventsRequest";
+export * from "./types/tari-indexer-client/TransactionEntry";
+export * from "./types/tari-indexer-client/InspectSubstateRequest";
+export * from "./types/tari-indexer-client/ListSubstateItem";
+export * from "./types/tari-indexer-client/IndexerTransactionFinalizedResult";
+export * from "./types/tari-indexer-client/NetworkDescription";
+export * from "./types/tari-indexer-client/GetUtxoUpdatesResponse";
+export * from "./types/tari-indexer-client/TransactionResultSummary";
export * from "./types/tari-indexer-client/IndexerGetSubstateResponse";
+export * from "./types/tari-indexer-client/IndexerGetCommsStatsResponse";
export * from "./types/tari-indexer-client/ListWatchedTemplatesResponse";
-export * from "./types/tari-indexer-client/ListUtxosRequest";
-export * from "./types/tari-indexer-client/GetSubstatesRequest";
-export * from "./types/tari-indexer-client/GetUtxoUpdatesResponse";
-export * from "./types/tari-indexer-client/GetNetworkSyncStateResponse";
-export * from "./types/tari-indexer-client/IndexerSubmitTransactionRequest";
-export * from "./types/tari-indexer-client/ListWatchedSubstatesResponse";
-export * from "./types/tari-indexer-client/IndexerConnection";
export * from "./types/tari-indexer-client/ListValidatorsResponse";
-export * from "./types/tari-indexer-client/InspectSubstateResponse";
-export * from "./types/tari-indexer-client/InspectSubstateRequest";
-export * from "./types/tari-indexer-client/ValidatorConsensusState";
-export * from "./types/tari-indexer-client/GetUtxoUpdatesRequest";
+export * from "./types/tari-indexer-client/IndexerConnection";
+export * from "./types/tari-indexer-client/GetNonFungiblesRequest";
export * from "./types/tari-indexer-client/ListValidatorsRequest";
-export * from "./types/tari-indexer-client/IndexerGetCommsStatsResponse";
-export * from "./types/tari-indexer-client/ListUtxosResponse";
-export * from "./types/tari-indexer-client/IndexerGetConnectionsResponse";
-export * from "./types/tari-indexer-client/GetNonFungiblesResponse";
-export * from "./types/tari-indexer-client/ValidatorInfo";
-export * from "./types/tari-indexer-client/ValidatorStatus";
-export * from "./types/tari-indexer-client/SyncProgress";
-export * from "./types/tari-indexer-client/ListRecentTransactionsRequest";
-export * from "./types/tari-indexer-client/GetTransactionReceiptResponse";
-export * from "./types/tari-indexer-client/GetUtxosRequest";
export * from "./types/tari-indexer-client/GetResourceResponse";
-export * from "./types/tari-indexer-client/GetNetworkEconomicsResponse";
export * from "./types/tari-indexer-client/ListEpochCheckpointsResponse";
-export * from "./types/tari-indexer-client/IndexerGetTransactionResultResponse";
-export * from "./types/tari-indexer-client/TransactionEntry";
-export * from "./types/tari-indexer-client/ListTemplateCatalogueRequest";
+export * from "./types/tari-indexer-client/ListTransactionReceiptsRequest";
+export * from "./types/tari-indexer-client/GetUtxosRequest";
+export * from "./types/tari-indexer-client/ListRecentTransactionsResponse";
+export * from "./types/tari-indexer-client/ListWatchedSubstatesRequest";
+export * from "./types/tari-indexer-client/ValidatorInfo";
+export * from "./types/tari-indexer-client/GetLatestEpochCheckpointResponse";
+export * from "./types/tari-indexer-client/GetNetworkSyncStateResponse";
export * from "./types/tari-indexer-client/GetUtxosResponse";
-export * from "./types/tari-indexer-client/ListTemplatesResponse";
-export * from "./types/tari-indexer-client/TransactionResultSummary";
+export * from "./types/tari-indexer-client/ListUtxosResponse";
+export * from "./types/tari-indexer-client/GetNonFungiblesResponse";
+export * from "./types/tari-indexer-client/GetTemplateDefinitionResponse";
+export * from "./types/tari-indexer-client/ListTransactionReceiptsResponse";
+export * from "./types/tari-indexer-client/IndexerGetConnectionsResponse";
export * from "./types/tari-indexer-client/IndexerGetTransactionResponse";
+export * from "./types/tari-indexer-client/ListTemplateCatalogueRequest";
export * from "./types/tari-indexer-client/GetSubstatesResponse";
-export * from "./types/tari-indexer-client/IndexerGetTransactionResultRequest";
-export * from "./types/tari-indexer-client/WatchedSubstateItem";
-export * from "./types/tari-indexer-client/ListRecentTransactionsResponse";
-export * from "./types/tari-indexer-client/WatchedTemplateItem";
-export * from "./types/tari-indexer-client/QueryTransactionEventsRequest";
-export * from "./types/tari-indexer-client/IndexerTransactionFinalizedResult";
-export * from "./types/tari-indexer-client/IndexerReadyResponse";
-export * from "./types/tari-indexer-client/IndexerSubmitTransactionResponse";
-export * from "./types/tari-indexer-client/ListTransactionReceiptsRequest";
+export * from "./types/tari-indexer-client/TemplateCatalogueItem";
+export * from "./types/tari-indexer-client/ValidatorStatus";
+export * from "./types/tari-indexer-client/ListWatchedSubstatesResponse";
+export * from "./types/tari-indexer-client/IndexerSubmitTransactionRequest";
+export * from "./types/tari-indexer-client/ListTemplatesResponse";
+export * from "./types/tari-indexer-client/TemplateMeta";
+export * from "./types/tari-indexer-client/InspectSubstateResponse";
+export * from "./types/tari-indexer-client/ListRecentTransactionsRequest";
+export * from "./types/tari-indexer-client/GetSubstatesRequest";
+export * from "./types/tari-indexer-client/IndexerGetSubstateRequest";
+export * from "./types/tari-indexer-client/IndexerGetEpochManagerStatsResponse";
+export * from "./types/tari-indexer-client/NonFungibleSubstate";
+export * from "./types/tari-indexer-client/ListTemplateCatalogueResponse";
+export * from "./types/tari-indexer-client/ListTemplatesRequest";
+export * from "./types/tari-indexer-client/IndexerGetTransactionResultResponse";
export * from "./types/tari-indexer-client/IndexerConnectionDirection";
-export * from "./types/tari-indexer-client/ListSubstateItem";
-export * from "./types/tari-indexer-client/StreamTransactionEventsRequest";
-export * from "./types/tari-indexer-client/ListWatchedSubstatesRequest";
+export * from "./types/tari-indexer-client/IndexerGetIdentityResponse";
export * from "./types/tari-indexer-client/ListEpochCheckpointsRequest";
+export * from "./types/tari-indexer-client/ListUtxosRequest";
+export * from "./types/tari-indexer-client/GetNetworkEconomicsResponse";
+export * from "./types/tari-indexer-client/SyncProgress";
+export * from "./types/tari-indexer-client/IndexerGetTransactionResultRequest";
+export * from "./types/tari-indexer-client/QueryTransactionEventsResponse";
+export * from "./types/tari-indexer-client/IndexerReadyResponse";
diff --git a/bindings/src/types/AbortReason.ts b/bindings/src/types/AbortReason.ts
index 96cdf1ee76..068152079e 100644
--- a/bindings/src/types/AbortReason.ts
+++ b/bindings/src/types/AbortReason.ts
@@ -9,4 +9,5 @@ export type AbortReason =
| "OneOrMoreInputsNotFound"
| "InsufficientFeesPaid"
| "FeePaymentInMainIntent"
- | "EpochExpired";
+ | "EpochExpired"
+ | "ValidityWindowTooLong";
diff --git a/bindings/src/types/Amount.ts b/bindings/src/types/Amount.ts
index 86e36c8431..4a1abffb18 100644
--- a/bindings/src/types/Amount.ts
+++ b/bindings/src/types/Amount.ts
@@ -1,9 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
- * A 128-bit signed amount.
+ * A 128-bit unsigned amount.
*
- * This is a general purpose signed integer, but is primarily used to represent the smallest unit of value in
+ * This is a general purpose unsigned integer, but is primarily used to represent the smallest unit of value in
* resources/vaults etc.
*
* This allows Tari to support a massive number tokens within resources.
diff --git a/bindings/src/types/CommitmentValueProof.ts b/bindings/src/types/CommitmentValueProof.ts
index 4b3532c17c..bdcac14c05 100644
--- a/bindings/src/types/CommitmentValueProof.ts
+++ b/bindings/src/types/CommitmentValueProof.ts
@@ -4,7 +4,9 @@ import type { ValueKnowledgeProof } from "./ValueKnowledgeProof";
/**
* Proof of knowledge of the opening to a commitment and that the commitment commits to a specific value.
- * Currently used when burning UTXOs to allow the total supply to be adjusted.
+ *
+ * Used wherever a resource's total supply must account for a value that is otherwise hidden in a commitment:
+ * burning a stealth UTXO, and minting a confidential commitment.
*/
export type CommitmentValueProof = {
/**
diff --git a/bindings/src/types/ConfidentialOutput.ts b/bindings/src/types/ConfidentialOutput.ts
index d5be8927fb..57700c99c5 100644
--- a/bindings/src/types/ConfidentialOutput.ts
+++ b/bindings/src/types/ConfidentialOutput.ts
@@ -5,7 +5,7 @@ import type { OutputBody } from "./OutputBody";
* A confidential output stored as its own substate, referenced by a vault's commitment list.
*
* Spend authorization is entirely controlled by the owning vault's access rules (unlike a stealth
- * `Utxo`, which carries its own spend authorization), so no per-output spend key is held here.
+ * [`crate::utxo::Utxo`], which carries its own spend authorization), so no per-output spend key is held here.
* Spending or burning downs this substate; the only in-place mutation is freezing.
*/
export type ConfidentialOutput = { output: OutputBody; is_frozen: boolean };
diff --git a/bindings/src/types/ExecuteResult.ts b/bindings/src/types/ExecuteResult.ts
index 0621fdfeab..02d3146a36 100644
--- a/bindings/src/types/ExecuteResult.ts
+++ b/bindings/src/types/ExecuteResult.ts
@@ -19,8 +19,17 @@ export type ExecuteResult = {
/**
* Total WASM metering points consumed by the transaction across all calls, including failed/aborted execution.
* Metering is deterministic, so every validator computes the identical value for the same transaction and
- * pledged state. Used to enforce the per-block WASM points budget. Defaults to 0 when decoding executions
- * persisted before this field existed.
+ * pledged state. Defaults to 0 when decoding executions persisted before this field existed.
*/
wasm_execution_points: bigint;
+ /**
+ * Total native-verification points (stealth transfers, confidential withdraws, burn claims) charged to the
+ * transaction, priced in WASM-point equivalents by [`NativeExecutionPoints`]. Deterministic for the same
+ * reason as [`Self::wasm_execution_points`] — the price is a pure function of the declared statement — so
+ * every validator computes the identical value. Defaults to 0 when decoding executions persisted before this
+ * field existed.
+ *
+ * [`NativeExecutionPoints`]: crate::limits::NativeExecutionPoints
+ */
+ native_execution_points: bigint;
};
diff --git a/bindings/src/types/FinalizeResult.ts b/bindings/src/types/FinalizeResult.ts
index 2683060cf5..a8f592040f 100644
--- a/bindings/src/types/FinalizeResult.ts
+++ b/bindings/src/types/FinalizeResult.ts
@@ -13,4 +13,15 @@ export type FinalizeResult = {
execution_results: Array;
result: TransactionResult;
fee_receipt: FeeReceipt;
+ /**
+ * What committing the whole transaction was priced at, which is what the commit-or-reject
+ * decision was made against.
+ *
+ * Equal to `fee_receipt.total_fees_charged()` on a full commit. When only the fee intent
+ * commits, the charges are re-derived over the fee checkpoint alone and so fall below what was
+ * paid; this keeps the figure the main intent was rejected for — the one a resubmission has to
+ * clear. Execution metadata rather than settled state, so it stays out of the persisted
+ * receipt, whose every field is priced into the storage charge of each transaction.
+ */
+ total_fees_required: bigint;
};
diff --git a/bindings/src/types/IndexedWellKnownTypes.ts b/bindings/src/types/IndexedWellKnownTypes.ts
index 83cb939181..f0b773a174 100644
--- a/bindings/src/types/IndexedWellKnownTypes.ts
+++ b/bindings/src/types/IndexedWellKnownTypes.ts
@@ -2,6 +2,7 @@
import type { BucketId } from "./BucketId";
import type { ClaimedOutputTombstoneAddress } from "./ClaimedOutputTombstoneAddress";
import type { ComponentAddress } from "./ComponentAddress";
+import type { ConfidentialOutputAddress } from "./ConfidentialOutputAddress";
import type { Metadata } from "./Metadata";
import type { NonFungibleAddress } from "./NonFungibleAddress";
import type { ProofId } from "./ProofId";
@@ -27,4 +28,5 @@ export type IndexedWellKnownTypes = {
utxos: Array;
component_address_allocations: number[];
resource_address_allocations: number[];
+ confidential_outputs: Array;
};
diff --git a/bindings/src/types/LeaderFee.ts b/bindings/src/types/LeaderFee.ts
index b8969f3925..990331c6bc 100644
--- a/bindings/src/types/LeaderFee.ts
+++ b/bindings/src/types/LeaderFee.ts
@@ -1,3 +1,19 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-export type LeaderFee = { fee: bigint; exhaust_burn: bigint };
+export type LeaderFee = {
+ /**
+ * The fee payable to the leader of each involved shard group.
+ */
+ fee: bigint;
+ /**
+ * The amount burned for the whole transaction across all involved shard groups: the exhaust burn collected by
+ * the executor plus the indivisible remainder from dividing the transaction fee between the leaders
+ * (`fee * num_involved_shard_groups + exhaust_burn == transaction_fee + executor_exhaust_burn`).
+ *
+ * CONSENSUS RULE: this must equal the amount actually withheld from validators, exactly — the accumulated burn
+ * in block headers determines the total supply, so it must not be re-derived with lossy arithmetic. Each shard
+ * group accumulates only its portion of this into its block header burn total — see
+ * `Evidence::exhaust_burn_portion`.
+ */
+ exhaust_burn: bigint;
+};
diff --git a/bindings/src/types/ResourceContainer.ts b/bindings/src/types/ResourceContainer.ts
index fea5a1593d..1cf10cf370 100644
--- a/bindings/src/types/ResourceContainer.ts
+++ b/bindings/src/types/ResourceContainer.ts
@@ -1,7 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Amount } from "./Amount";
import type { NonFungibleId } from "./NonFungibleId";
-import type { OutputBody } from "./OutputBody";
import type { PedersenCommitmentBytes } from "./PedersenCommitmentBytes";
import type { ResourceAddress } from "./ResourceAddress";
@@ -20,9 +19,9 @@ export type ResourceContainer =
| {
Confidential: {
address: ResourceAddress;
- commitments: { [key in PedersenCommitmentBytes]?: OutputBody };
+ commitments: Array;
revealed_amount: Amount;
- locked_commitments: { [key in PedersenCommitmentBytes]?: OutputBody };
+ locked_commitments: Array;
locked_revealed_amount: Amount;
};
}
diff --git a/bindings/src/types/TransactionPoolRecord.ts b/bindings/src/types/TransactionPoolRecord.ts
index 611e1c85b5..ec5adcb384 100644
--- a/bindings/src/types/TransactionPoolRecord.ts
+++ b/bindings/src/types/TransactionPoolRecord.ts
@@ -37,7 +37,7 @@ export type TransactionPoolRecord = {
*/
transaction_weight: bigint;
/**
- * The exhaust burn surcharge collected by the executor (`FeeReceipt::exhaust_burn_charged`), set from local
+ * The exhaust burn collected by the executor (`FeeReceipt::exhaust_burn_charged`), set from local
* execution alongside `transaction_fee`.
*/
exhaust_burn: bigint;
diff --git a/bindings/src/types/TransactionReceipt.ts b/bindings/src/types/TransactionReceipt.ts
index d92e7d3f37..bce3d7980f 100644
--- a/bindings/src/types/TransactionReceipt.ts
+++ b/bindings/src/types/TransactionReceipt.ts
@@ -14,5 +14,17 @@ export type TransactionReceipt = {
events: Array;
fee_receipt: FeeReceipt;
epoch: Epoch;
+ /**
+ * Commitment to the transaction's intent: every field the signers authorized (network, fee
+ * instructions, instructions, inputs, epoch bounds, flags and blob commitments), excluding the
+ * signatures themselves.
+ *
+ * The receipt is already bound to the transaction transitively — it is addressed by the
+ * transaction id — but reproducing that id requires the signatures and the seal signature, so
+ * establishing the link that way reveals the signers. This commitment is over the same
+ * projection minus the signatures, so whoever holds the transaction can link it to this
+ * receipt without revealing who authorized it. It identifies the intent, not the signers:
+ * transactions differing only in who signed them share a commitment.
+ */
intent_commitment: Hash32;
};
diff --git a/bindings/src/types/UnsignedTransactionV1.ts b/bindings/src/types/UnsignedTransactionV1.ts
index c14553c11d..c9e68bc3f2 100644
--- a/bindings/src/types/UnsignedTransactionV1.ts
+++ b/bindings/src/types/UnsignedTransactionV1.ts
@@ -13,6 +13,12 @@ export type UnsignedTransactionV1 = {
*/
inputs: Array;
min_epoch: Epoch | null;
+ /**
+ * The last epoch in which this transaction may be sequenced. Mandatory: every transaction has a
+ * bounded lifetime, capped at `ConsensusConstants::max_transaction_validity_epochs` past the
+ * current epoch, so a transaction's death is deterministic and an aborted attempt cannot be
+ * retried indefinitely.
+ */
max_epoch: Epoch;
is_seal_signer_authorized: boolean;
dry_run: boolean;
diff --git a/bindings/src/types/tari-indexer-client/GetNetworkEconomicsResponse.ts b/bindings/src/types/tari-indexer-client/GetNetworkEconomicsResponse.ts
index 3befc72b85..07f48caa93 100644
--- a/bindings/src/types/tari-indexer-client/GetNetworkEconomicsResponse.ts
+++ b/bindings/src/types/tari-indexer-client/GetNetworkEconomicsResponse.ts
@@ -4,11 +4,36 @@ import type { Epoch } from "../Epoch";
export type GetNetworkEconomicsResponse = {
current_epoch: Epoch;
+ /**
+ * Total XTR claimed (peg-in).
+ */
total_claimed: Amount;
+ /**
+ * Total exhaust burned, sourced from checkpoint headers (consensus-backed, complete since genesis).
+ * Kept as a cross-check against `receipt_exhaust_burned`.
+ */
total_exhaust_burned: Amount;
+ /**
+ * Total pre-burn execution fees `F`, summed from transaction receipts.
+ */
fee_volume: Amount;
+ /**
+ * Total exhaust burned summed from the same receipts as `fee_volume`; `receipt_exhaust_burned /
+ * fee_volume` is the exact realized burn rate, and this is the burn netted from `total_supply`. May
+ * transiently trail `total_exhaust_burned` while the receipt sync frontier catches up to the checkpoint
+ * frontier.
+ */
receipt_exhaust_burned: Amount;
+ /**
+ * Circulating L2 supply: `total_claimed - receipt_exhaust_burned`.
+ */
total_supply: Amount;
+ /**
+ * Number of transaction receipts the indexer has stored.
+ */
transaction_receipt_count: bigint;
+ /**
+ * The target exhaust burn rate in basis points in effect at `current_epoch`.
+ */
target_burn_rate_bps: number;
};
diff --git a/bindings/src/types/tari-indexer-client/IndexerSubmitTransactionResponse.ts b/bindings/src/types/tari-indexer-client/IndexerSubmitTransactionResponse.ts
index 2c03b6c88a..ca5b04822b 100644
--- a/bindings/src/types/tari-indexer-client/IndexerSubmitTransactionResponse.ts
+++ b/bindings/src/types/tari-indexer-client/IndexerSubmitTransactionResponse.ts
@@ -1,15 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-import type { ExecuteResult } from "../ExecuteResult";
import type { TransactionId } from "../TransactionId";
export type IndexerSubmitTransactionResponse = {
/**
- * The ID of the transaction that was dry-run
+ * The ID of the submitted transaction
*/
transaction_id: TransactionId;
- /**
- * The result of the dry-run execution, including any emitted events and state changes, but without a final
- * decision or commitment to the ledger
- */
- result: ExecuteResult;
};
diff --git a/bindings/src/types/wallet-types/CallInstructionRequest.ts b/bindings/src/types/wallet-types/CallInstructionRequest.ts
index d924575b59..489492d54e 100644
--- a/bindings/src/types/wallet-types/CallInstructionRequest.ts
+++ b/bindings/src/types/wallet-types/CallInstructionRequest.ts
@@ -7,7 +7,14 @@ export type CallInstructionRequest = {
instructions: Array;
fee_account: ComponentAddressOrName;
max_fee: number | bigint | string;
+ /**
+ * Substates the instructions require as transaction inputs. Needed for any input that cannot be inferred
+ * from the instructions, e.g. a `ConfidentialOutput` named only by a commitment inside an opaque proof.
+ */
inputs: Array;
+ /**
+ * If true, inputs inferred from the instructions are added to `inputs`.
+ */
override_inputs: boolean | null;
new_outputs: number | null;
proof_ids: Array;
diff --git a/bindings/src/types/wallet-types/SettingsGetResponse.ts b/bindings/src/types/wallet-types/SettingsGetResponse.ts
index a1ec6a107e..c8d899a0d2 100644
--- a/bindings/src/types/wallet-types/SettingsGetResponse.ts
+++ b/bindings/src/types/wallet-types/SettingsGetResponse.ts
@@ -7,6 +7,16 @@ export type SettingsGetResponse = {
network: NetworkInfo;
advanced_ui_features: AdvancedUiFeatures;
claimed_accounts: Array;
+ /**
+ * The network's current epoch, as the wallet daemon sees it. Callers that build transactions
+ * themselves need it to choose a `max_epoch` the network will accept. `None` when the indexer
+ * is unreachable — settings must stay readable while the network is down, not least so the
+ * caller can see and correct the indexer URL.
+ */
current_epoch: number | null;
+ /**
+ * How many epochs past `current_epoch` the wallet daemon stamps `max_epoch` when a caller does
+ * not choose one.
+ */
default_transaction_validity_epochs: number;
};
diff --git a/bindings/src/types/wallet-types/StealthUtxosGetValueLookupInfoResponse.ts b/bindings/src/types/wallet-types/StealthUtxosGetValueLookupInfoResponse.ts
index 812d3d42f8..f222df0e27 100644
--- a/bindings/src/types/wallet-types/StealthUtxosGetValueLookupInfoResponse.ts
+++ b/bindings/src/types/wallet-types/StealthUtxosGetValueLookupInfoResponse.ts
@@ -1,5 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+/**
+ * Describes the value lookup table configured for confidential/stealth balance decryption. When no file is
+ * configured, `configured` is `false` and the remaining fields are `None`.
+ */
export type StealthUtxosGetValueLookupInfoResponse = {
configured: boolean;
path: string | null;
diff --git a/bindings/src/types/wallet-types/TransactionDetectInputsRequest.ts b/bindings/src/types/wallet-types/TransactionDetectInputsRequest.ts
index 1bc6a81492..fe2f32f0ca 100644
--- a/bindings/src/types/wallet-types/TransactionDetectInputsRequest.ts
+++ b/bindings/src/types/wallet-types/TransactionDetectInputsRequest.ts
@@ -1,6 +1,16 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UnsignedTransaction } from "../UnsignedTransaction";
+/**
+ * Resolve a transaction's inputs without submitting it.
+ *
+ * Detection returns the **dependency closure** of everything the instructions
+ * reference (e.g. an account component pulls in every vault it holds), which
+ * is a superset of what execution will touch: the wallet cannot know intent
+ * without executing, so it over-approximates. Narrowing the set is the
+ * caller's job — every declared input adds transaction weight (fees) and must
+ * be locked by consensus, so the caller pays for anything left in.
+ */
export type TransactionDetectInputsRequest = {
transaction: UnsignedTransaction;
/**
diff --git a/bindings/src/validator-node-client.ts b/bindings/src/validator-node-client.ts
index c93f7b31bd..99f0277fb3 100644
--- a/bindings/src/validator-node-client.ts
+++ b/bindings/src/validator-node-client.ts
@@ -1,56 +1,56 @@
// Copyright 2026 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause
-export * from "./types/validator-node-client/GetCommitteeRequest";
-export * from "./types/validator-node-client/VNTemplateMetadata";
-export * from "./types/validator-node-client/GetBlocksCountResponse";
-export * from "./types/validator-node-client/VNAddPeerRequest";
-export * from "./types/validator-node-client/GetCommitteeResponse";
-export * from "./types/validator-node-client/VNSubmitTransactionRequest";
-export * from "./types/validator-node-client/VNConnectionDirection";
-export * from "./types/validator-node-client/VNAddPeerResponse";
-export * from "./types/validator-node-client/VNCommitteeShardInfo";
-export * from "./types/validator-node-client/VNFunctionDef";
-export * from "./types/validator-node-client/LayerOneTransactionParams";
export * from "./types/validator-node-client/ValidatorNodeChange";
-export * from "./types/validator-node-client/PrepareLayerOneTransactionResponse";
-export * from "./types/validator-node-client/VNGetAllVnsResponse";
-export * from "./types/validator-node-client/TemplateAbi";
-export * from "./types/validator-node-client/GetMempoolStatsResponse";
-export * from "./types/validator-node-client/GetBlockResponse";
-export * from "./types/validator-node-client/VNLogLevel";
-export * from "./types/validator-node-client/VNGetAllVnsRequest";
-export * from "./types/validator-node-client/GetBaseLayerEpochChangesRequest";
-export * from "./types/validator-node-client/GetBaseLayerEpochChangesResponse";
-export * from "./types/validator-node-client/PrepareLayerOneTransactionRequest";
+export * from "./types/validator-node-client/VNSubmitTransactionRequest";
+export * from "./types/validator-node-client/GetFilteredBlocksCountRequest";
export * from "./types/validator-node-client/VNLogEntry";
export * from "./types/validator-node-client/GetShardKeyRequest";
-export * from "./types/validator-node-client/GetBlockRequest";
+export * from "./types/validator-node-client/VNGetTransactionResultResponse";
export * from "./types/validator-node-client/VNSubmitTransactionResponse";
-export * from "./types/validator-node-client/DryRunTransactionFinalizeResult";
-export * from "./types/validator-node-client/GetTransactionResponse";
-export * from "./types/validator-node-client/GetStateResponse";
-export * from "./types/validator-node-client/VNGetSubstateRequest";
-export * from "./types/validator-node-client/VNGetSubstateResponse";
-export * from "./types/validator-node-client/ListBlocksResponse";
-export * from "./types/validator-node-client/SubstateStatus";
-export * from "./types/validator-node-client/ValidatorNode";
-export * from "./types/validator-node-client/GetFilteredBlocksCountRequest";
-export * from "./types/validator-node-client/ListBlocksRequest";
-export * from "./types/validator-node-client/GetEpochManagerStatsResponse";
export * from "./types/validator-node-client/GetTxPoolResponse";
-export * from "./types/validator-node-client/VNConnection";
-export * from "./types/validator-node-client/GetConsensusStatusResponse";
export * from "./types/validator-node-client/GetTemplateResponse";
-export * from "./types/validator-node-client/VNGetConnectionsResponse";
-export * from "./types/validator-node-client/VNGetCommsStatsResponse";
-export * from "./types/validator-node-client/GetTemplateRequest";
+export * from "./types/validator-node-client/GetBlocksCountResponse";
+export * from "./types/validator-node-client/VNConnectionDirection";
+export * from "./types/validator-node-client/VNAddPeerRequest";
+export * from "./types/validator-node-client/PrepareLayerOneTransactionResponse";
+export * from "./types/validator-node-client/GetTransactionRequest";
export * from "./types/validator-node-client/GetStateRequest";
+export * from "./types/validator-node-client/VNGetIdentityResponse";
+export * from "./types/validator-node-client/VNFunctionDef";
+export * from "./types/validator-node-client/LayerOneTransactionParams";
export * from "./types/validator-node-client/GetBlocksRequest";
-export * from "./types/validator-node-client/VNGetTransactionResultRequest";
-export * from "./types/validator-node-client/GetTransactionRequest";
-export * from "./types/validator-node-client/VNGetTransactionResultResponse";
+export * from "./types/validator-node-client/GetTemplateRequest";
export * from "./types/validator-node-client/GetShardKeyResponse";
+export * from "./types/validator-node-client/GetBaseLayerEpochChangesResponse";
+export * from "./types/validator-node-client/VNGetCommsStatsResponse";
+export * from "./types/validator-node-client/GetTransactionResponse";
+export * from "./types/validator-node-client/GetCommitteeResponse";
+export * from "./types/validator-node-client/VNGetAllVnsRequest";
+export * from "./types/validator-node-client/GetCommitteeRequest";
+export * from "./types/validator-node-client/DryRunTransactionFinalizeResult";
+export * from "./types/validator-node-client/GetStateResponse";
+export * from "./types/validator-node-client/GetBaseLayerEpochChangesRequest";
+export * from "./types/validator-node-client/SubstateStatus";
+export * from "./types/validator-node-client/GetConsensusStatusResponse";
export * from "./types/validator-node-client/GetBlocksResponse";
-export * from "./types/validator-node-client/VNGetIdentityResponse";
+export * from "./types/validator-node-client/ListBlocksResponse";
+export * from "./types/validator-node-client/GetBlockRequest";
+export * from "./types/validator-node-client/VNGetTransactionResultRequest";
+export * from "./types/validator-node-client/VNTemplateMetadata";
+export * from "./types/validator-node-client/VNLogLevel";
+export * from "./types/validator-node-client/VNAddPeerResponse";
+export * from "./types/validator-node-client/VNConnection";
+export * from "./types/validator-node-client/ValidatorNode";
+export * from "./types/validator-node-client/PrepareLayerOneTransactionRequest";
+export * from "./types/validator-node-client/VNGetConnectionsResponse";
+export * from "./types/validator-node-client/ListBlocksRequest";
export * from "./types/validator-node-client/GetNetworkCommitteeResponse";
+export * from "./types/validator-node-client/VNGetSubstateRequest";
+export * from "./types/validator-node-client/GetMempoolStatsResponse";
+export * from "./types/validator-node-client/TemplateAbi";
+export * from "./types/validator-node-client/VNGetSubstateResponse";
+export * from "./types/validator-node-client/VNGetAllVnsResponse";
+export * from "./types/validator-node-client/GetEpochManagerStatsResponse";
+export * from "./types/validator-node-client/GetBlockResponse";
+export * from "./types/validator-node-client/VNCommitteeShardInfo";
diff --git a/bindings/src/wallet-types.ts b/bindings/src/wallet-types.ts
index dabd0e4915..cc2a175728 100644
--- a/bindings/src/wallet-types.ts
+++ b/bindings/src/wallet-types.ts
@@ -1,201 +1,201 @@
// Copyright 2026 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause
-export * from "./types/wallet-types/AccountsListResponse";
-export * from "./types/wallet-types/PublishTemplateMetadata";
-export * from "./types/wallet-types/AddressBookUpdateRequest";
-export * from "./types/wallet-types/SignTemplateMetadataResponse";
-export * from "./types/wallet-types/WalletGetInfoRequest";
-export * from "./types/wallet-types/AuthListSessionsResponse";
-export * from "./types/wallet-types/ClaimBurnResponse";
-export * from "./types/wallet-types/KeysSetActiveRequest";
-export * from "./types/wallet-types/AccountsGetBalancesResponse";
-export * from "./types/wallet-types/SubstatesGetResponse";
-export * from "./types/wallet-types/AccountsRenameResponse";
-export * from "./types/wallet-types/AddressBookGetRequest";
-export * from "./types/wallet-types/StealthTransferRequest";
-export * from "./types/wallet-types/BurnProofsListResponse";
-export * from "./types/wallet-types/AccountsCreateStealthTransferStatementRequest";
-export * from "./types/wallet-types/AddressBookListResponse";
-export * from "./types/wallet-types/AddressBookAddRequest";
-export * from "./types/wallet-types/AccountSetDefaultRequest";
-export * from "./types/wallet-types/TransactionGetAllResponse";
-export * from "./types/wallet-types/TransactionGetRequest";
-export * from "./types/wallet-types/KeyBranch";
-export * from "./types/wallet-types/KeyId";
-export * from "./types/wallet-types/AddressBookListRequest";
-export * from "./types/wallet-types/PayTo";
-export * from "./types/wallet-types/AccountSetDefaultResponse";
-export * from "./types/wallet-types/SubstatesListResponse";
-export * from "./types/wallet-types/AccountGetDefaultRequest";
-export * from "./types/wallet-types/StealthUtxosListResponse";
-export * from "./types/wallet-types/WebauthnAlreadyRegisteredResponse";
-export * from "./types/wallet-types/KeysListResponse";
-export * from "./types/wallet-types/ProofsGenerateResponse";
-export * from "./types/wallet-types/AccountsAssociateStealthResourceRequest";
-export * from "./types/wallet-types/TransactionGetAllRequest";
-export * from "./types/wallet-types/AccountsTransferResponse";
-export * from "./types/wallet-types/AuthLoginResponse";
-export * from "./types/wallet-types/WebauthnStartAuthResponse";
-export * from "./types/wallet-types/ClaimBurnRequest";
-export * from "./types/wallet-types/AccountsRenameRequest";
-export * from "./types/wallet-types/BalanceEntry";
-export * from "./types/wallet-types/KeysListRequest";
-export * from "./types/wallet-types/SubstatesGetRequest";
+export * from "./types/wallet-types/AddressBookUpdateResponse";
+export * from "./types/wallet-types/BalanceChange";
+export * from "./types/wallet-types/ConfidentialOutputsListRequest";
export * from "./types/wallet-types/ProofsFinalizeResponse";
-export * from "./types/wallet-types/ClaimBurnProof";
-export * from "./types/wallet-types/AuthRevokeTokenRequest";
-export * from "./types/wallet-types/TransactionWaitResultRequest";
-export * from "./types/wallet-types/PublishTemplateResponse";
-export * from "./types/wallet-types/TransactionSubmitResponse";
-export * from "./types/wallet-types/TemplatesListAuthoredRequest";
-export * from "./types/wallet-types/AccountsCreateRequest";
+export * from "./types/wallet-types/WalletGetInfoResponse";
+export * from "./types/wallet-types/SwapPoolGetExchangeRateResponse";
export * from "./types/wallet-types/CallInstructionRequest";
-export * from "./types/wallet-types/AuthGetMethodRequest";
-export * from "./types/wallet-types/AccountsGetBalanceChangesRequest";
-export * from "./types/wallet-types/WebauthnStartRegisterRequest";
-export * from "./types/wallet-types/AccountGetByKeyIndexRequest";
export * from "./types/wallet-types/AddressBookDeleteResponse";
-export * from "./types/wallet-types/WebRtcStartRequest";
-export * from "./types/wallet-types/WebRtcStartResponse";
-export * from "./types/wallet-types/AuthSessionInfo";
-export * from "./types/wallet-types/StealthUtxosListRequest";
+export * from "./types/wallet-types/AccountsListRequest";
+export * from "./types/wallet-types/AccountsGetBalanceChangesRequest";
+export * from "./types/wallet-types/AuthLoginResponse";
+export * from "./types/wallet-types/GetValidatorFeesRequest";
+export * from "./types/wallet-types/StealthUtxosListResponse";
+export * from "./types/wallet-types/AccountGetDefaultRequest";
+export * from "./types/wallet-types/AddressBookListResponse";
+export * from "./types/wallet-types/KeyBranch";
+export * from "./types/wallet-types/PublishTemplateResponse";
+export * from "./types/wallet-types/AccountsAssociateStealthResourceResponse";
+export * from "./types/wallet-types/WalletSubstateInfo";
+export * from "./types/wallet-types/WebauthnAlreadyRegisteredResponse";
+export * from "./types/wallet-types/AccountGetResponse";
+export * from "./types/wallet-types/SettingsSetRequest";
export * from "./types/wallet-types/StealthTransferResponse";
-export * from "./types/wallet-types/AdvancedUiFeatures";
+export * from "./types/wallet-types/ConfidentialOutputsListResponse";
+export * from "./types/wallet-types/StealthTransferRequest";
+export * from "./types/wallet-types/GetValidatorFeesResponse";
+export * from "./types/wallet-types/StealthUtxoSpendKeyId";
+export * from "./types/wallet-types/AccountsCreateStealthTransferStatementResponse";
+export * from "./types/wallet-types/ClaimBurnProof";
+export * from "./types/wallet-types/TransactionGetAllRequest";
+export * from "./types/wallet-types/WebauthnAlreadyRegisteredRequest";
+export * from "./types/wallet-types/BurnProofsListRequest";
+export * from "./types/wallet-types/AuthMethod";
export * from "./types/wallet-types/SwapPoolsListRequest";
-export * from "./types/wallet-types/SwapPoolInfo";
-export * from "./types/wallet-types/BurnProofsGetResponse";
-export * from "./types/wallet-types/WalletGetInfoResponse";
-export * from "./types/wallet-types/AuthoredTemplate";
+export * from "./types/wallet-types/StealthUtxosDecryptValueResponse";
+export * from "./types/wallet-types/MintFaucetNftRequest";
+export * from "./types/wallet-types/PublishTemplateRequest";
+export * from "./types/wallet-types/TransactionSubmitResponse";
+export * from "./types/wallet-types/StealthTransfer";
+export * from "./types/wallet-types/ProofsGenerateResponse";
export * from "./types/wallet-types/TransactionSubmitManifestResponse";
-export * from "./types/wallet-types/ProofsCancelResponse";
-export * from "./types/wallet-types/AccountGetResponse";
-export * from "./types/wallet-types/AddressBookUpdateResponse";
+export * from "./types/wallet-types/ListNftsResponse";
+export * from "./types/wallet-types/StealthUtxosGetValueLookupInfoRequest";
export * from "./types/wallet-types/ClaimValidatorFeesRequest";
-export * from "./types/wallet-types/WebauthnStartAuthRequest";
-export * from "./types/wallet-types/StealthTransfer";
+export * from "./types/wallet-types/PublishTemplateMetadata";
+export * from "./types/wallet-types/AuthLoginRequest";
+export * from "./types/wallet-types/EffectiveStatus";
+export * from "./types/wallet-types/AuthGetMethodResponse";
+export * from "./types/wallet-types/TransactionGetRequest";
export * from "./types/wallet-types/AuthRevokeTokenResponse";
-export * from "./types/wallet-types/WebauthnFinishRegisterResponse";
-export * from "./types/wallet-types/TransferFeeParams";
-export * from "./types/wallet-types/WebRtcStart";
-export * from "./types/wallet-types/TransactionWaitResultResponse";
-export * from "./types/wallet-types/AccountsAssociateStealthResourceResponse";
-export * from "./types/wallet-types/GetValidatorFeesRequest";
-export * from "./types/wallet-types/ConfidentialCreateOutputProofRequest";
-export * from "./types/wallet-types/KeysCreateResponse";
-export * from "./types/wallet-types/WebauthnFinishAuthRequest";
-export * from "./types/wallet-types/AccountsGetBalanceChangesResponse";
-export * from "./types/wallet-types/SwapPoolGetExchangeRateResponse";
-export * from "./types/wallet-types/MintFaucetNftResponse";
-export * from "./types/wallet-types/AuthCreateApiKeyResponse";
-export * from "./types/wallet-types/AddressBookEntry";
export * from "./types/wallet-types/ProofsFinalizeRequest";
-export * from "./types/wallet-types/BurnProofsGetRequest";
-export * from "./types/wallet-types/SwapPoolsListResponse";
-export * from "./types/wallet-types/BalanceChange";
-export * from "./types/wallet-types/ProofsCancelRequest";
-export * from "./types/wallet-types/StealthUtxosDecryptValueRequest";
+export * from "./types/wallet-types/TransactionRequestDecisionResponse";
+export * from "./types/wallet-types/PayFeeWithSwapParams";
+export * from "./types/wallet-types/WebauthnStartRegisterRequest";
export * from "./types/wallet-types/FeePoolDetails";
-export * from "./types/wallet-types/SettingsGetResponse";
-export * from "./types/wallet-types/NetworkInfo";
-export * from "./types/wallet-types/ClaimBurnProofContents";
-export * from "./types/wallet-types/ConfidentialViewVaultBalanceRequest";
-export * from "./types/wallet-types/ConfidentialOutputsListRequest";
-export * from "./types/wallet-types/ConfidentialOutputsListResponse";
-export * from "./types/wallet-types/ConfidentialOutputInfo";
-export * from "./types/wallet-types/GetValidatorFeesResponse";
-export * from "./types/wallet-types/TransferNftRequest";
-export * from "./types/wallet-types/AuthCredentials";
-export * from "./types/wallet-types/StealthUtxoSpendKeyId";
-export * from "./types/wallet-types/InputSelection";
-export * from "./types/wallet-types/TransactionGetResponse";
-export * from "./types/wallet-types/RefreshTokenHash";
-export * from "./types/wallet-types/AddressBookAddResponse";
-export * from "./types/wallet-types/ListNftsRequest";
-export * from "./types/wallet-types/TransactionSubmitRequest";
-export * from "./types/wallet-types/BurnProofsListRequest";
-export * from "./types/wallet-types/BalanceChangeSourceType";
-export * from "./types/wallet-types/AccountsCreateStealthTransferStatementResponse";
-export * from "./types/wallet-types/IssuedApiKey";
-export * from "./types/wallet-types/BadgeUsage";
-export * from "./types/wallet-types/AuthRefreshRequest";
-export * from "./types/wallet-types/AddressBookDeleteRequest";
-export * from "./types/wallet-types/AccountsListRequest";
-export * from "./types/wallet-types/TransactionSubmitDryRunResponse";
-export * from "./types/wallet-types/ConfidentialCreateOutputProofResponse";
+export * from "./types/wallet-types/AccountsTransferRequest";
export * from "./types/wallet-types/AccountOrKeyId";
-export * from "./types/wallet-types/TemplatesGetRequest";
-export * from "./types/wallet-types/AuthRevokeApiKeyResponse";
-export * from "./types/wallet-types/WebauthnFinishRegisterRequest";
-export * from "./types/wallet-types/ConfidentialViewVaultBalanceResponse";
-export * from "./types/wallet-types/WalletSubstateInfo";
-export * from "./types/wallet-types/SettingsSetRequest";
-export * from "./types/wallet-types/ListNftsResponse";
+export * from "./types/wallet-types/TransactionRequestValueSummary";
+export * from "./types/wallet-types/AddressBookAddRequest";
export * from "./types/wallet-types/DerivedKeyId";
-export * from "./types/wallet-types/TransactionSubmitManifestRequest";
-export * from "./types/wallet-types/TransactionClaimBurnResponse";
-export * from "./types/wallet-types/ClaimValidatorFeesResponse";
-export * from "./types/wallet-types/ConfidentialTransferResponse";
-export * from "./types/wallet-types/TemplatesGetResponse";
-export * from "./types/wallet-types/NewAccountData";
-export * from "./types/wallet-types/AccountsTransferRequest";
-export * from "./types/wallet-types/AccountsCreateOrGetResponse";
-export * from "./types/wallet-types/SignTemplateMetadataRequest";
-export * from "./types/wallet-types/AuthLoginRequest";
-export * from "./types/wallet-types/UtxoInfo";
-export * from "./types/wallet-types/AuthListApiKeysResponse";
-export * from "./types/wallet-types/AccountsCreateFreeTestCoinsRequest";
-export * from "./types/wallet-types/ConfidentialTransferRequest";
export * from "./types/wallet-types/SettingsSetResponse";
-export * from "./types/wallet-types/AuthListSessionsRequest";
-export * from "./types/wallet-types/BalanceChangeSource";
-export * from "./types/wallet-types/AuthRefreshResponse";
-export * from "./types/wallet-types/WebauthnAlreadyRegisteredRequest";
-export * from "./types/wallet-types/TransferStatementRequest";
-export * from "./types/wallet-types/WebauthnStartRegisterResponse";
-export * from "./types/wallet-types/PublishTemplateRequest";
-export * from "./types/wallet-types/GetNftRequest";
+export * from "./types/wallet-types/WebauthnFinishAuthRequest";
+export * from "./types/wallet-types/AdvancedUiFeatures";
+export * from "./types/wallet-types/KeysListRequest";
+export * from "./types/wallet-types/AccountsTransferResponse";
+export * from "./types/wallet-types/TransactionGetResultResponse";
+export * from "./types/wallet-types/StealthUtxosGetValueLookupInfoResponse";
+export * from "./types/wallet-types/AccountsAssociateStealthResourceRequest";
+export * from "./types/wallet-types/ClaimBurnRequest";
+export * from "./types/wallet-types/BurnProofsListResponse";
+export * from "./types/wallet-types/SignTemplateMetadataRequest";
export * from "./types/wallet-types/BurnProofFileInfo";
-export * from "./types/wallet-types/TransferOutput";
+export * from "./types/wallet-types/TransactionRequestCreateRequest";
+export * from "./types/wallet-types/KeysListResponse";
+export * from "./types/wallet-types/KeyId";
+export * from "./types/wallet-types/SubstatesGetRequest";
+export * from "./types/wallet-types/TransferFeeParams";
export * from "./types/wallet-types/SwapPoolGetExchangeRateRequest";
-export * from "./types/wallet-types/MintFaucetNftRequest";
-export * from "./types/wallet-types/AuthMethod";
+export * from "./types/wallet-types/TransactionSubmitRequest";
+export * from "./types/wallet-types/AccountsGetBalancesResponse";
+export * from "./types/wallet-types/TransactionGetAllResponse";
+export * from "./types/wallet-types/TransactionRequestGetRequest";
export * from "./types/wallet-types/AccountsCreateFreeTestCoinsResponse";
+export * from "./types/wallet-types/ListNftsRequest";
+export * from "./types/wallet-types/TransactionSubmitManifestRequest";
+export * from "./types/wallet-types/AuthListApiKeysResponse";
+export * from "./types/wallet-types/ConfidentialTransferResponse";
export * from "./types/wallet-types/AuthRevokeApiKeyRequest";
-export * from "./types/wallet-types/TransferNftResponse";
+export * from "./types/wallet-types/BurnProofsGetResponse";
+export * from "./types/wallet-types/TransactionGetResponse";
+export * from "./types/wallet-types/SubstatesGetResponse";
+export * from "./types/wallet-types/AccountsRenameRequest";
+export * from "./types/wallet-types/BurnProofsGetRequest";
+export * from "./types/wallet-types/AuthRevokeTokenRequest";
export * from "./types/wallet-types/AuthCreateApiKeyRequest";
-export * from "./types/wallet-types/AccountGetRequest";
-export * from "./types/wallet-types/KeysCreateRequest";
export * from "./types/wallet-types/ProofsGenerateRequest";
-export * from "./types/wallet-types/PayFeeWithSwapParams";
-export * from "./types/wallet-types/AddressBookGetResponse";
-export * from "./types/wallet-types/TransactionGetResultResponse";
-export * from "./types/wallet-types/KeysSetActiveResponse";
-export * from "./types/wallet-types/AuthGetMethodResponse";
+export * from "./types/wallet-types/MintFaucetNftResponse";
+export * from "./types/wallet-types/TransactionRequestInfo";
+export * from "./types/wallet-types/AuthListSessionsRequest";
+export * from "./types/wallet-types/TransactionRequestGetResponse";
+export * from "./types/wallet-types/TemplatesListAuthoredRequest";
+export * from "./types/wallet-types/AuthRefreshResponse";
+export * from "./types/wallet-types/AddressBookDeleteRequest";
+export * from "./types/wallet-types/AccountsListResponse";
+export * from "./types/wallet-types/WebRtcStartResponse";
+export * from "./types/wallet-types/AuthCreateApiKeyResponse";
export * from "./types/wallet-types/TransactionGetResultRequest";
+export * from "./types/wallet-types/WebauthnFinishRegisterRequest";
+export * from "./types/wallet-types/ProofsCancelResponse";
+export * from "./types/wallet-types/ClaimBurnProofContents";
+export * from "./types/wallet-types/AccountsCreateRequest";
+export * from "./types/wallet-types/TransactionClaimBurnResponse";
+export * from "./types/wallet-types/TransactionWaitResultRequest";
+export * from "./types/wallet-types/ClaimValidatorFeesResponse";
+export * from "./types/wallet-types/ConfidentialCreateOutputProofRequest";
+export * from "./types/wallet-types/KeysCreateRequest";
+export * from "./types/wallet-types/WebauthnStartAuthRequest";
+export * from "./types/wallet-types/TransactionRequestDecisionRequest";
+export * from "./types/wallet-types/NetworkInfo";
+export * from "./types/wallet-types/TransferStatementRequest";
+export * from "./types/wallet-types/AuthSessionInfo";
export * from "./types/wallet-types/AccountsCreateOrGetRequest";
-export * from "./types/wallet-types/AccountsCreateResponse";
+export * from "./types/wallet-types/SettingsGetResponse";
+export * from "./types/wallet-types/WebauthnFinishRegisterResponse";
+export * from "./types/wallet-types/TransactionSubmitDryRunResponse";
+export * from "./types/wallet-types/WalletGetInfoRequest";
+export * from "./types/wallet-types/StealthUtxosListRequest";
+export * from "./types/wallet-types/ClaimBurnResponse";
+export * from "./types/wallet-types/AccountsGetBalanceChangesResponse";
+export * from "./types/wallet-types/TransactionRequestListResponse";
+export * from "./types/wallet-types/WebRtcStartRequest";
+export * from "./types/wallet-types/TransactionRequestCreateResponse";
+export * from "./types/wallet-types/AuthRefreshRequest";
+export * from "./types/wallet-types/InputSelection";
+export * from "./types/wallet-types/ConfidentialViewVaultBalanceResponse";
+export * from "./types/wallet-types/AddressBookGetRequest";
+export * from "./types/wallet-types/WebRtcStart";
+export * from "./types/wallet-types/ConfidentialOutputInfo";
+export * from "./types/wallet-types/AccountSetDefaultRequest";
+export * from "./types/wallet-types/WebauthnStartRegisterResponse";
+export * from "./types/wallet-types/AddressBookGetResponse";
+export * from "./types/wallet-types/SignTemplateMetadataResponse";
+export * from "./types/wallet-types/ConfidentialCreateOutputProofResponse";
+export * from "./types/wallet-types/AccountsCreateStealthTransferStatementRequest";
+export * from "./types/wallet-types/SwapPoolInfo";
+export * from "./types/wallet-types/RefreshTokenHash";
+export * from "./types/wallet-types/TemplatesGetResponse";
export * from "./types/wallet-types/AccountsGetBalancesRequest";
+export * from "./types/wallet-types/AuthRevokeApiKeyResponse";
+export * from "./types/wallet-types/TransactionRequestListRequest";
+export * from "./types/wallet-types/AccountsCreateFreeTestCoinsRequest";
+export * from "./types/wallet-types/AccountGetByKeyIndexRequest";
+export * from "./types/wallet-types/TransactionDetectInputsRequest";
+export * from "./types/wallet-types/AddressBookListRequest";
+export * from "./types/wallet-types/AccountsRenameResponse";
+export * from "./types/wallet-types/KeysCreateResponse";
+export * from "./types/wallet-types/AccountsCreateOrGetResponse";
+export * from "./types/wallet-types/PayTo";
export * from "./types/wallet-types/AccountInfo";
-export * from "./types/wallet-types/AuthListApiKeysRequest";
+export * from "./types/wallet-types/AddressBookAddResponse";
+export * from "./types/wallet-types/TransferNftResponse";
export * from "./types/wallet-types/SubstatesListRequest";
-export * from "./types/wallet-types/ComponentAddressOrName";
-export * from "./types/wallet-types/TemplatesListAuthoredResponse";
-export * from "./types/wallet-types/StealthUtxosDecryptValueResponse";
-export * from "./types/wallet-types/StealthUtxosGetValueLookupInfoRequest";
-export * from "./types/wallet-types/StealthUtxosGetValueLookupInfoResponse";
-export * from "./types/wallet-types/EffectiveStatus";
-export * from "./types/wallet-types/TransactionRequestCreateRequest";
-export * from "./types/wallet-types/TransactionRequestCreateResponse";
-export * from "./types/wallet-types/TransactionRequestDecisionRequest";
-export * from "./types/wallet-types/TransactionRequestDecisionResponse";
-export * from "./types/wallet-types/TransactionRequestGetRequest";
-export * from "./types/wallet-types/TransactionRequestGetResponse";
-export * from "./types/wallet-types/TransactionRequestInfo";
-export * from "./types/wallet-types/TransactionRequestListRequest";
-export * from "./types/wallet-types/TransactionRequestListResponse";
-export * from "./types/wallet-types/TransactionRequestSubmitRequest";
+export * from "./types/wallet-types/BalanceEntry";
+export * from "./types/wallet-types/TemplatesGetRequest";
+export * from "./types/wallet-types/AddressBookUpdateRequest";
+export * from "./types/wallet-types/SwapPoolsListResponse";
+export * from "./types/wallet-types/AuthCredentials";
+export * from "./types/wallet-types/BalanceChangeSourceType";
+export * from "./types/wallet-types/ProofsCancelRequest";
+export * from "./types/wallet-types/BadgeUsage";
+export * from "./types/wallet-types/AccountSetDefaultResponse";
+export * from "./types/wallet-types/GetNftRequest";
+export * from "./types/wallet-types/SubstatesListResponse";
+export * from "./types/wallet-types/AuthListSessionsResponse";
+export * from "./types/wallet-types/StealthUtxosDecryptValueRequest";
+export * from "./types/wallet-types/WebauthnStartAuthResponse";
export * from "./types/wallet-types/TransactionRequestSubmitResponse";
-export * from "./types/wallet-types/TransactionRequestValueSummary";
-export * from "./types/wallet-types/TransactionDetectInputsRequest";
+export * from "./types/wallet-types/TransferNftRequest";
+export * from "./types/wallet-types/AddressBookEntry";
+export * from "./types/wallet-types/KeysSetActiveResponse";
+export * from "./types/wallet-types/ComponentAddressOrName";
+export * from "./types/wallet-types/AuthGetMethodRequest";
+export * from "./types/wallet-types/NewAccountData";
+export * from "./types/wallet-types/AuthListApiKeysRequest";
export * from "./types/wallet-types/TransactionDetectInputsResponse";
+export * from "./types/wallet-types/KeysSetActiveRequest";
+export * from "./types/wallet-types/TransactionRequestSubmitRequest";
+export * from "./types/wallet-types/ConfidentialViewVaultBalanceRequest";
+export * from "./types/wallet-types/UtxoInfo";
+export * from "./types/wallet-types/TemplatesListAuthoredResponse";
+export * from "./types/wallet-types/BalanceChangeSource";
+export * from "./types/wallet-types/TransferOutput";
+export * from "./types/wallet-types/ConfidentialTransferRequest";
+export * from "./types/wallet-types/AuthoredTemplate";
+export * from "./types/wallet-types/AccountsCreateResponse";
+export * from "./types/wallet-types/IssuedApiKey";
+export * from "./types/wallet-types/TransactionWaitResultResponse";
+export * from "./types/wallet-types/AccountGetRequest";
diff --git a/clients/javascript/indexer_client/package.json b/clients/javascript/indexer_client/package.json
index b627e73107..68285cbde4 100644
--- a/clients/javascript/indexer_client/package.json
+++ b/clients/javascript/indexer_client/package.json
@@ -1,6 +1,6 @@
{
"name": "@tari-project/indexer-client",
- "version": "1.5.0",
+ "version": "1.6.0",
"description": "Tari indexer REST API client library",
"homepage": "https://github.com/tari-project/tari-ootle#readme",
"bugs": {
diff --git a/clients/javascript/wallet_daemon_client/package.json b/clients/javascript/wallet_daemon_client/package.json
index 97f8a45267..7c046c29f7 100644
--- a/clients/javascript/wallet_daemon_client/package.json
+++ b/clients/javascript/wallet_daemon_client/package.json
@@ -1,6 +1,6 @@
{
"name": "@tari-project/wallet_jrpc_client",
- "version": "1.21.0",
+ "version": "1.22.0",
"description": "Tari wallet JSON-RPC client library",
"homepage": "https://github.com/tari-project/tari-ootle#readme",
"bugs": {
diff --git a/crates/consensus/src/consensus_constants.rs b/crates/consensus/src/consensus_constants.rs
index 7821cd77fa..a00128d926 100644
--- a/crates/consensus/src/consensus_constants.rs
+++ b/crates/consensus/src/consensus_constants.rs
@@ -308,6 +308,29 @@ impl From for ConsensusConstants {
#[cfg(test)]
mod tests {
+ use tari_engine_types::fees::MAX_EXHAUST_BURN_RATE_BPS;
+
+ /// `FeeReceipt::required_fees` is derived against `MAX_EXHAUST_BURN_RATE_BPS`. A network that
+ /// burns faster than that makes every dry-run estimate too low to submit with.
+ #[test]
+ fn every_shipped_network_stays_within_the_burn_rate_ceiling() {
+ for network in [
+ Network::MainNet,
+ Network::StageNet,
+ Network::NextNet,
+ Network::Igor,
+ Network::Esmeralda,
+ Network::LocalNet,
+ ] {
+ let constants = super::ConsensusConstants::from(network);
+ assert!(
+ constants.exhaust_burn_rate_bps <= MAX_EXHAUST_BURN_RATE_BPS,
+ "{network} burns at {} bps, above the {MAX_EXHAUST_BURN_RATE_BPS} bps the fee estimate assumes",
+ constants.exhaust_burn_rate_bps
+ );
+ }
+ }
+
use tari_engine_types::limits::{
ENGINE_LIMITS,
MAX_NATIVE_POINTS_PER_TRANSACTION,
diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml
index 89d41173a7..494847e04a 100644
--- a/crates/engine/Cargo.toml
+++ b/crates/engine/Cargo.toml
@@ -45,6 +45,7 @@ wasm-cache = ["dep:bytes", "dep:memmap2"]
# depends on tari_engine, so we can't list it with a published version here or
# `cargo publish` can't resolve the manifest.
tari_template_test_tooling = { path = "../template_test_tooling" }
+tari_bor = { workspace = true, default-features = true, features = ["std"] }
tari_transaction_manifest = { workspace = true }
tari_ootle_transaction = { workspace = true, features = ["serde"] }
tari_template_builtin = { workspace = true, features = ["templates", "state"] }
diff --git a/crates/engine/src/fees/fee_module.rs b/crates/engine/src/fees/fee_module.rs
index 9250e9322c..4ece67fc2d 100644
--- a/crates/engine/src/fees/fee_module.rs
+++ b/crates/engine/src/fees/fee_module.rs
@@ -7,7 +7,7 @@ use tari_template_lib::types::TemplateAddress;
use super::FeeTable;
use crate::{
- runtime::{RuntimeEvent, RuntimeModule, RuntimeModuleError, StateTracker},
+ runtime::{ChargeableState, RuntimeEvent, RuntimeModule, RuntimeModuleError, StateTracker},
state_store::StateReader,
};
@@ -47,6 +47,113 @@ impl FeeModule {
Ok((base_bytes, charge))
}
+ /// Charges everything that is a function of the state being persisted, rather than of the work
+ /// done to produce it: storage, the template-publish premium, substate slots, the metering of
+ /// WASM and native execution, and the exhaust burn over the resulting total.
+ ///
+ /// Every charge here is *assigned*, not accumulated, so that running this again against a
+ /// different state replaces the result rather than doubling it. That is what lets a transaction
+ /// be gated on the cost of the state it asked to commit and then billed for the state that is
+ /// actually persisted — see [`RuntimeModule::on_before_persist`].
+ fn charge_finalization_fees(
+ &self,
+ state: &mut ChargeableState<'_, TStore>,
+ ) -> Result<(), RuntimeModuleError> {
+ let mut counter = ByteCounter::new();
+ let mut template_base_bytes = 0u64;
+ let mut template_charge = 0u64;
+ for substate in state.substates_to_persist().values() {
+ // A published template's binary is priced by the dedicated base + quadratic publish
+ // model, so keep it out of the flat per-byte storage tally. Accumulate the raw
+ // metrics here and apply the storage divisor once below.
+ if let SubstateValue::Template(template) = substate {
+ let (tpl_base_bytes, tpl_charge) = self.template_publish_metrics(template.binary.len())?;
+ template_base_bytes = template_base_bytes.checked_add(tpl_base_bytes).ok_or_else(|| {
+ RuntimeModuleError::Overflow("Overflow accumulating template base bytes".to_string())
+ })?;
+ template_charge = template_charge.checked_add(tpl_charge).ok_or_else(|| {
+ RuntimeModuleError::Overflow("Overflow accumulating template publish premium".to_string())
+ })?;
+ continue;
+ }
+ encode_into_writer(substate, &mut counter)?;
+ }
+
+ // Finalization persists the transaction receipt on top of the mutated substates. It carries
+ // the transaction's events, so leaving it out of the tally would make that payload — the
+ // largest caller-controlled contribution to permanent state after the substates themselves —
+ // free.
+ let receipt_bytes = state
+ .transaction_receipt_size()
+ .map_err(|e| RuntimeModuleError::Runtime(e.to_string()))?;
+ let total_storage = counter
+ .get()
+ .checked_add(receipt_bytes)
+ .ok_or_else(|| RuntimeModuleError::Overflow("Overflow accumulating storage bytes".to_string()))?;
+
+ let cost = self
+ .fee_table
+ .per_byte_storage_cost()
+ .checked_mul(total_storage as u64)
+ .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating storage cost".to_string()))?;
+ let storage_cost = cost / self.fee_table.storage_cost_divisor();
+
+ let template_base_cost = self
+ .fee_table
+ .per_byte_storage_cost()
+ .checked_mul(template_base_bytes)
+ .ok_or_else(|| {
+ RuntimeModuleError::Overflow("Overflow calculating template base storage cost".to_string())
+ })? /
+ self.fee_table.storage_cost_divisor();
+ let template_publish_cost = template_base_cost
+ .checked_add(template_charge)
+ .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating template publish cost".to_string()))?;
+
+ // The receipt occupies a slot of its own — it is always newly created, since it is addressed
+ // by a transaction id that can only be finalized once.
+ let new_substate_count = state
+ .count_newly_created_substates()
+ .map_err(|e| RuntimeModuleError::Runtime(e.to_string()))?
+ .saturating_add(1);
+ let create_cost = (new_substate_count as u64)
+ .checked_mul(self.fee_table.per_substate_create_cost())
+ .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating substate create cost".to_string()))?;
+
+ // WASM execution: charge once against the transaction's accumulated points so the divisor
+ // rounds against the total. Per-call rounding would let a transaction split work into
+ // sub-divisor chunks and pay zero for any single one (each `points/divisor` is `0`), even
+ // though the summed work is non-trivial.
+ let units = state.fee_state().accumulated_wasm_points() / self.fee_table.wasm_points_cost_divisor();
+ let wasm_cost = units
+ .checked_mul(self.fee_table.per_wasm_point_cost())
+ .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating WASM execution cost".to_string()))?;
+
+ // Native verification is priced in the same points and charged at the same rate, under its
+ // own source so the breakdown distinguishes crypto verification from template execution.
+ let native_units = state.fee_state().accumulated_native_points() / self.fee_table.wasm_points_cost_divisor();
+ let native_cost = native_units
+ .checked_mul(self.fee_table.per_wasm_point_cost())
+ .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating native execution cost".to_string()))?;
+
+ let fee_state = state.fee_state_mut();
+ fee_state.set_charge(FeeSource::Storage, storage_cost);
+ fee_state.set_charge(FeeSource::TemplatePublish, template_publish_cost);
+ fee_state.set_charge(FeeSource::SubstateCreate, create_cost);
+ fee_state.set_charge(FeeSource::WasmExecution, wasm_cost);
+ fee_state.set_charge(FeeSource::NativeExecution, native_cost);
+
+ // Exhaust burn: charged on top of the execution fee accrued so far, so leaders receive the execution fee in
+ // full and the burn amount is destroyed separately. The rate is seeded onto the fee state at execution time
+ // for the execution epoch. Zeroed first so that the total it is taken over never includes a burn from an
+ // earlier pass over a different state.
+ fee_state.set_charge(FeeSource::ExhaustBurn, 0);
+ let burn = calculate_burn_amount(fee_state.total_charges(), fee_state.burn_rate_bps())?;
+ fee_state.set_charge(FeeSource::ExhaustBurn, burn);
+
+ Ok(())
+ }
+
#[cfg(test)]
fn template_publish_cost(&self, binary_len: usize) -> Result {
let (base_bytes, charge) = self.template_publish_metrics(binary_len)?;
@@ -110,98 +217,11 @@ impl RuntimeModule for FeeModule {
}
fn on_before_finalize(&self, track: &mut StateTracker) -> Result<(), RuntimeModuleError> {
- let (total_storage, template_base_bytes, template_charge) = track.with_substates_to_persist(|changes| {
- let mut counter = ByteCounter::new();
- let mut base_bytes = 0u64;
- let mut charge = 0u64;
- for substate in changes.values() {
- // A published template's binary is priced by the dedicated base + quadratic publish
- // model, so keep it out of the flat per-byte storage tally. Accumulate the raw
- // metrics here and apply the storage divisor once below.
- if let SubstateValue::Template(template) = substate {
- let (tpl_base_bytes, tpl_charge) = self.template_publish_metrics(template.binary.len())?;
- base_bytes = base_bytes.checked_add(tpl_base_bytes).ok_or_else(|| {
- RuntimeModuleError::Overflow("Overflow accumulating template base bytes".to_string())
- })?;
- charge = charge.checked_add(tpl_charge).ok_or_else(|| {
- RuntimeModuleError::Overflow("Overflow accumulating template publish premium".to_string())
- })?;
- continue;
- }
- encode_into_writer(substate, &mut counter)?;
- }
- Ok::<_, RuntimeModuleError>((counter.get(), base_bytes, charge))
- })?;
-
- // Finalization persists the transaction receipt on top of the mutated substates. It carries
- // the transaction's events, so leaving it out of the tally would make that payload — the
- // largest caller-controlled contribution to permanent state after the substates themselves —
- // free.
- let receipt_bytes = track
- .transaction_receipt_size()
- .map_err(|e| RuntimeModuleError::Runtime(e.to_string()))?;
- let total_storage = total_storage
- .checked_add(receipt_bytes)
- .ok_or_else(|| RuntimeModuleError::Overflow("Overflow accumulating storage bytes".to_string()))?;
-
- let cost = self
- .fee_table
- .per_byte_storage_cost()
- .checked_mul(total_storage as u64)
- .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating storage cost".to_string()))?;
- track.add_fee_charge(FeeSource::Storage, cost / self.fee_table.storage_cost_divisor());
-
- let template_base_cost = self
- .fee_table
- .per_byte_storage_cost()
- .checked_mul(template_base_bytes)
- .ok_or_else(|| {
- RuntimeModuleError::Overflow("Overflow calculating template base storage cost".to_string())
- })? /
- self.fee_table.storage_cost_divisor();
- let template_publish_cost = template_base_cost
- .checked_add(template_charge)
- .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating template publish cost".to_string()))?;
- if template_publish_cost > 0 {
- track.add_fee_charge(FeeSource::TemplatePublish, template_publish_cost);
- }
-
- // The receipt occupies a slot of its own — it is always newly created, since it is addressed
- // by a transaction id that can only be finalized once.
- let new_substate_count = track
- .count_newly_created_substates()
- .map_err(|e| RuntimeModuleError::Runtime(e.to_string()))?
- .saturating_add(1);
- let create_cost = (new_substate_count as u64)
- .checked_mul(self.fee_table.per_substate_create_cost())
- .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating substate create cost".to_string()))?;
- track.add_fee_charge(FeeSource::SubstateCreate, create_cost);
-
- // WASM execution: charge once against the transaction's accumulated points so the divisor
- // rounds against the total. Per-call rounding would let a transaction split work into
- // sub-divisor chunks and pay zero for any single one (each `points/divisor` is `0`), even
- // though the summed work is non-trivial.
- let units = track.accumulated_wasm_points() / self.fee_table.wasm_points_cost_divisor();
- let wasm_cost = units
- .checked_mul(self.fee_table.per_wasm_point_cost())
- .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating WASM execution cost".to_string()))?;
- track.add_fee_charge(FeeSource::WasmExecution, wasm_cost);
-
- // Native verification is priced in the same points and charged at the same rate, under its
- // own source so the breakdown distinguishes crypto verification from template execution.
- let native_units = track.accumulated_native_points() / self.fee_table.wasm_points_cost_divisor();
- let native_cost = native_units
- .checked_mul(self.fee_table.per_wasm_point_cost())
- .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating native execution cost".to_string()))?;
- track.add_fee_charge(FeeSource::NativeExecution, native_cost);
-
- // Exhaust burn: charged on top of the execution fee accrued so far, so leaders receive the execution fee in
- // full and the burn amount is destroyed separately. The rate is seeded onto the fee state at execution time
- // for the execution epoch.
- let burn = calculate_burn_amount(track.total_fee_charges(), track.fee_burn_rate_bps())?;
- track.add_fee_charge(FeeSource::ExhaustBurn, burn);
+ self.charge_finalization_fees(&mut track.chargeable_state())
+ }
- Ok(())
+ fn on_before_persist(&self, state: &mut ChargeableState<'_, TStore>) -> Result<(), RuntimeModuleError> {
+ self.charge_finalization_fees(state)
}
fn on_runtime_event(
diff --git a/crates/engine/src/fees/fee_table.rs b/crates/engine/src/fees/fee_table.rs
index 0e74ef3316..4a41a667cc 100644
--- a/crates/engine/src/fees/fee_table.rs
+++ b/crates/engine/src/fees/fee_table.rs
@@ -1,6 +1,35 @@
// Copyright 2023 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause
+use tari_ootle_transaction::LITERAL_BYTE_DIVISOR;
+
+/// The narrowest `max_fee` a run can meter at is `1`, which a dry run clamps to. `Amount` encodes as
+/// a native CBOR integer, so that is a single byte.
+const NARROWEST_FEE_LITERAL_BYTES: u64 = 1;
+/// The widest `max_fee` a submission can carry is `u64::MAX` — `FeeState::add_fee_payment_checked`
+/// rejects a payment above it — which encodes as a leading byte over eight payload bytes.
+const WIDEST_FEE_LITERAL_BYTES: u64 = 9;
+
+/// The span of encoded widths a `max_fee` can take.
+const FEE_LITERAL_BYTE_SPAN: u64 = WIDEST_FEE_LITERAL_BYTES - NARROWEST_FEE_LITERAL_BYTES;
+
+/// The most transaction-weight units the `max_fee` literal's encoded width can move a transaction.
+///
+/// `calc_args_weight` prices an instruction's literals as `literal_bytes / LITERAL_BYTE_DIVISOR`, so
+/// widening the amount by `d` bytes lifts that quotient by at most `ceil(d / LITERAL_BYTE_DIVISOR)`,
+/// attained when the narrow encoding sits just above a multiple of the divisor.
+const MAX_FEE_LITERAL_WEIGHT_DRIFT: u64 = FEE_LITERAL_BYTE_SPAN.div_ceil(LITERAL_BYTE_DIVISOR);
+
+/// The most bytes `max_fee` can move the storage tally.
+///
+/// The tally byte-counts the fee vault as it stands when finalization charges, which is before the
+/// unspent payment is returned — so it counts `balance - max_fee`, whose width moves with `max_fee`
+/// as surely as the literal's does. A payment must fit in `u64`
+/// (`FeeState::add_fee_payment_checked`), so `max_fee` can only shift that residual across encoding
+/// widths while the balance is itself in `u64` range; above it, every payment leaves a residual of
+/// the same width. The span is therefore the literal's.
+const MAX_FEE_RESIDUAL_BYTE_DRIFT: u64 = FEE_LITERAL_BYTE_SPAN;
+
#[derive(Debug, Clone)]
pub struct FeeTable {
pub per_transaction_weight_cost: u64,
@@ -67,6 +96,28 @@ impl FeeTable {
}
}
+ /// The allowance a dry-run estimate must carry so that a real run of the same transaction at a
+ /// different `max_fee` cannot cost more than the estimate.
+ ///
+ /// Two charges read `max_fee` back, and they move in opposite directions, so both are counted.
+ /// The weight term is the fee literal's width drift at this table's weight cost. The storage
+ /// term is the fee vault's residual width drift at this table's byte cost, plus the rounding
+ /// boundary the storage divisor can land on. The burn re-multiplies their sum, being taken over
+ /// the running total, and the trailing `1` covers its own rounding boundary.
+ ///
+ /// `tari_engine_types::fees::FEE_ESTIMATE_ALLOWANCE` is the value this yields, restated where
+ /// `FeeReceipt::required_fees` can reach it.
+ pub const fn fee_estimate_allowance(&self, burn_rate_bps: u16) -> u64 {
+ let weight_drift = MAX_FEE_LITERAL_WEIGHT_DRIFT.saturating_mul(self.per_transaction_weight_cost);
+ let storage_drift = MAX_FEE_RESIDUAL_BYTE_DRIFT.saturating_mul(self.per_byte_storage_cost) /
+ non_zero(self.storage_cost_divisor) +
+ 1;
+ let drift = weight_drift.saturating_add(storage_drift);
+ drift
+ .saturating_add(drift.saturating_mul(burn_rate_bps as u64) / 10_000)
+ .saturating_add(1)
+ }
+
pub fn per_transaction_weight_cost(&self) -> u64 {
self.per_transaction_weight_cost
}
@@ -124,8 +175,8 @@ impl FeeTable {
}
}
-fn non_zero(divisor: u64) -> u64 {
- divisor.max(1)
+const fn non_zero(divisor: u64) -> u64 {
+ if divisor == 0 { 1 } else { divisor }
}
/// The WASM-execution fee rate extracted from a [`FeeTable`], plus the conversion from fees paid
@@ -168,3 +219,65 @@ impl WasmMeteringRate {
Some(u64::try_from(funded).unwrap_or(u64::MAX))
}
}
+
+#[cfg(test)]
+mod tests {
+ use tari_engine_types::fees::{FEE_ESTIMATE_ALLOWANCE, MAX_EXHAUST_BURN_RATE_BPS};
+ use tari_template_lib::types::Amount;
+
+ use super::*;
+
+ /// The drift is computed from constant widths, so this holds the encoder to them.
+ #[test]
+ fn the_fee_literal_widths_match_the_encoder() {
+ assert_eq!(minicbor::len(Amount::from(1u64)) as u64, NARROWEST_FEE_LITERAL_BYTES);
+ assert_eq!(minicbor::len(Amount::from(u64::MAX)) as u64, WIDEST_FEE_LITERAL_BYTES);
+ // The width the instruction actually carries, which is what `calc_args_weight` prices.
+ assert_eq!(
+ tari_bor::encoded_len_via_writer(&Amount::from(u64::MAX)).unwrap() as u64,
+ WIDEST_FEE_LITERAL_BYTES
+ );
+ assert_eq!(MAX_FEE_LITERAL_WEIGHT_DRIFT, 3);
+ }
+
+ /// A table priced like the shipped ones: one microtari per weight unit and per stored byte.
+ fn shipped_like() -> FeeTable {
+ let mut table = FeeTable::zero_rated();
+ table.per_transaction_weight_cost = 1;
+ table.per_byte_storage_cost = 1;
+ table.storage_cost_divisor = 1;
+ table
+ }
+
+ #[test]
+ fn the_allowance_covers_both_directions_max_fee_moves() {
+ let mut table = shipped_like();
+
+ // Weight drift (3 units) + storage drift (8 bytes) + the storage rounding boundary.
+ assert_eq!(table.fee_estimate_allowance(0), 3 + 8 + 1 + 1);
+ // A full burn doubles all of it.
+ assert_eq!(table.fee_estimate_allowance(MAX_EXHAUST_BURN_RATE_BPS), 12 * 2 + 1);
+
+ // The storage divisor scales the residual term down.
+ table.storage_cost_divisor = 4;
+ assert_eq!(table.fee_estimate_allowance(0), 3 + 8 / 4 + 1 + 1);
+
+ // Each rate multiplies the whole drift.
+ table.storage_cost_divisor = 1;
+ table.per_transaction_weight_cost = 2;
+ assert_eq!(table.fee_estimate_allowance(0), 6 + 8 + 1 + 1);
+ table.per_byte_storage_cost = 2;
+ assert_eq!(table.fee_estimate_allowance(0), 6 + 16 + 1 + 1);
+ }
+
+ /// `FEE_ESTIMATE_ALLOWANCE` is stated in `tari_engine_types`, which cannot see a `FeeTable`. It
+ /// holds for a table priced like the shipped ones at the highest burn the estimate is derived
+ /// against.
+ #[test]
+ fn the_restated_allowance_matches_the_derivation() {
+ assert_eq!(
+ FEE_ESTIMATE_ALLOWANCE,
+ shipped_like().fee_estimate_allowance(MAX_EXHAUST_BURN_RATE_BPS)
+ );
+ }
+}
diff --git a/crates/engine/src/runtime/fee_state.rs b/crates/engine/src/runtime/fee_state.rs
index b310d7d4bc..23d79fe717 100644
--- a/crates/engine/src/runtime/fee_state.rs
+++ b/crates/engine/src/runtime/fee_state.rs
@@ -93,6 +93,12 @@ impl FeeState {
self.fee_charges.add(source, amount)
}
+ /// Replaces the charge for `source`. Used by the fee module to recompute the finalization
+ /// charges once the state that will actually be persisted is known.
+ pub fn set_charge(&mut self, source: FeeSource, amount: u64) {
+ self.fee_charges.set(source, amount)
+ }
+
pub fn accumulate_wasm_points(&mut self, points: u64) {
self.accumulated_wasm_points = self.accumulated_wasm_points.saturating_add(points);
}
diff --git a/crates/engine/src/runtime/impl.rs b/crates/engine/src/runtime/impl.rs
index dea4bf9512..ac36a520b6 100644
--- a/crates/engine/src/runtime/impl.rs
+++ b/crates/engine/src/runtime/impl.rs
@@ -168,7 +168,7 @@ use crate::{
locking::{LockError, LockedSubstate},
pay_fee::PayFee,
scope::PushCallFrame,
- tracker::StateTracker,
+ tracker::{FinalizedState, StateTracker},
},
state_store::StateReader,
template::LoadedTemplate,
@@ -279,6 +279,34 @@ impl) -> Result {
+ self.invoke_modules_on_before_finalize()?;
+ let mut finalized = self.tracker.select_finalized_state(failure)?;
+ // A commit persists the very state the first pass charged against, so charging it again
+ // would recompute the same numbers from the same inputs. Only a fee-intent commit swaps the
+ // state out from under those charges, and only it needs them redone.
+ if !finalized.outcome().is_commit() {
+ self.invoke_modules_on_before_persist(&mut finalized)?;
+ }
+ self.tracker.finalize(finalized)
+ }
+
+ fn invoke_modules_on_before_persist(&mut self, finalized: &mut FinalizedState) -> Result<(), RuntimeError> {
+ for module in self.modules.iter() {
+ module.on_before_persist(&mut finalized.chargeable_state())?;
+ }
+ Ok(())
+ }
+
fn invoke_modules_on_runtime_event(&mut self, event: RuntimeEvent) -> Result<(), RuntimeError> {
for module in self.modules.iter() {
module.on_runtime_event(&mut self.tracker, &event)?;
@@ -3454,16 +3482,12 @@ where
fn finalize(&mut self) -> Result {
self.invoke_modules_on_runtime_call("finalize")?;
- // If the fee module is present, this will add substate storage fees
- self.invoke_modules_on_before_finalize()?;
- self.tracker.finalize(None)
+ self.finalize_with(None)
}
fn finalize_failure(&mut self, reason: RejectReason) -> Result {
self.invoke_modules_on_runtime_call("finalize_failure")?;
- // If the fee module is present, this will add substate storage fees
- self.invoke_modules_on_before_finalize()?;
- self.tracker.finalize(Some(reason))
+ self.finalize_with(Some(reason))
}
fn validate_finalized(&self) -> Result<(), RuntimeError> {
diff --git a/crates/engine/src/runtime/mod.rs b/crates/engine/src/runtime/mod.rs
index 91179afd3d..4df2229414 100644
--- a/crates/engine/src/runtime/mod.rs
+++ b/crates/engine/src/runtime/mod.rs
@@ -109,7 +109,8 @@ use tari_template_lib::{
stealth::StealthTransferStatement,
},
};
-pub use tracker::StateTracker;
+pub use tracker::{FinalizedState, StateTracker};
+pub use working_state::ChargeableState;
use crate::runtime::{locking::LockedSubstate, scope::PushCallFrame};
diff --git a/crates/engine/src/runtime/module.rs b/crates/engine/src/runtime/module.rs
index 678074e99f..575dfde374 100644
--- a/crates/engine/src/runtime/module.rs
+++ b/crates/engine/src/runtime/module.rs
@@ -3,7 +3,7 @@
use tari_template_lib::types::TemplateAddress;
-use crate::runtime::StateTracker;
+use crate::runtime::{ChargeableState, StateTracker};
pub trait RuntimeModule: Send + Sync {
fn on_initialize(&self, _track: &mut StateTracker) -> Result<(), RuntimeModuleError> {
@@ -32,10 +32,21 @@ pub trait RuntimeModule: Send + Sync {
Ok(())
}
+ /// Invoked at the start of finalization, against the working state — before it is known
+ /// whether the transaction commits or falls back to a fee-intent commit. Charges added here
+ /// decide that outcome: they are what the paid-in-full check sees.
fn on_before_finalize(&self, _track: &mut StateTracker) -> Result<(), RuntimeModuleError> {
Ok(())
}
+ /// Invoked once the state that finalization will persist has been chosen, and before its fees
+ /// are settled. On a fee-intent commit that state is the fee checkpoint, not the working state
+ /// [`Self::on_before_finalize`] saw, so any charge that is a function of what gets persisted
+ /// must be recomputed here against `state`.
+ fn on_before_persist(&self, _state: &mut ChargeableState<'_, TStore>) -> Result<(), RuntimeModuleError> {
+ Ok(())
+ }
+
fn on_runtime_event(
&self,
_track: &mut StateTracker,
diff --git a/crates/engine/src/runtime/tracker.rs b/crates/engine/src/runtime/tracker.rs
index 5c2c18cd7f..745680b1b4 100644
--- a/crates/engine/src/runtime/tracker.rs
+++ b/crates/engine/src/runtime/tracker.rs
@@ -20,13 +20,12 @@
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-use indexmap::IndexMap;
use log::*;
use tari_engine_types::{
commit_result::{FinalizeResult, RejectReason, TransactionResult},
component::{Component, ComponentBody, ComponentHeader},
events::Event,
- fees::FeeSource,
+ fees::{FeeReceipt, FeeSource},
indexed_value::{IndexedValue, IndexedWellKnownTypes},
limits,
lock::LockFlag,
@@ -56,7 +55,7 @@ use crate::{
error::ArgumentValidationError,
locking::LockedSubstate,
scope::{CallScope, PushCallFrame},
- working_state::WorkingState,
+ working_state::{ChargeableState, WorkingState},
workspace::Workspace,
},
state_store::StateReader,
@@ -64,6 +63,28 @@ use crate::{
const LOG_TARGET: &str = "tari::ootle::engine::runtime::state_tracker";
+/// The state finalization will persist, detached from the tracker and awaiting fee settlement.
+#[derive(Debug)]
+pub struct FinalizedState {
+ state: WorkingState,
+ outcome: FinalizeOutcome,
+ /// Why the main intent was rejected, when this is a fee-intent commit.
+ reason: Option,
+ /// What committing the whole transaction was priced at, captured before the second charging
+ /// pass re-derives the charges over whichever state was chosen.
+ total_fees_required: u64,
+}
+
+impl FinalizedState {
+ pub fn chargeable_state(&mut self) -> ChargeableState<'_, TStore> {
+ ChargeableState::new(&mut self.state)
+ }
+
+ pub fn outcome(&self) -> FinalizeOutcome {
+ self.outcome
+ }
+}
+
#[derive(Debug)]
pub struct StateTracker {
working_state: Option>,
@@ -349,7 +370,20 @@ impl StateTracker {
Ok(())
}
- pub fn finalize(&mut self, failure: Option) -> Result {
+ /// Chooses the state that finalization will persist.
+ ///
+ /// A transaction that failed — either explicitly, or by not covering the fees charged against
+ /// the working state — commits only its fee intent, so the state to persist is the fee
+ /// checkpoint rather than the state execution ended on. The fee state carries over either way,
+ /// so the work done before the failure is still charged for.
+ ///
+ /// Split out from [`Self::finalize`] so the runtime can run its modules against the chosen state
+ /// before its fees are settled.
+ pub fn select_finalized_state(
+ &mut self,
+ failure: Option,
+ ) -> Result, RuntimeError> {
+ let total_fees_required = self.read_with(|state| state.fee_state().total_charges());
let failure = failure.or_else(|| {
self.read_with(|state| {
let fee_state = state.fee_state();
@@ -365,75 +399,112 @@ impl StateTracker {
})
});
- if let Some(reason) = failure {
- let mut checkpoint_state = self.take_fee_checkpoint().ok_or(RuntimeError::NoFeeCheckpoint)?;
- // Preserve fee state across resets so that we can charge for fees incurred during execution before the
- // failure
- self.read_with(|state| {
- // Fee state in `state` includes the payments and charges from the fee transaction
- *checkpoint_state.fee_state_mut() = state.fee_state().clone();
+ let Some(reason) = failure else {
+ // Finalise will always reset the state
+ return Ok(FinalizedState {
+ state: self.take_working_state()?,
+ outcome: FinalizeOutcome::Commit,
+ reason: None,
+ total_fees_required,
});
- let mut substates_to_persist = checkpoint_state.take_mutated_substates();
- // Process fees and refunds based on the fee checkpoint state
- let fee_receipt = checkpoint_state.finalize_fees_and_refunds(&mut substates_to_persist)?;
-
- let downed_utxos = checkpoint_state.take_downed_utxos();
- let downed_confidential_outputs = checkpoint_state.take_downed_confidential_outputs();
- let fee_withdrawals = checkpoint_state.take_validator_fee_withdrawals();
-
- let mut diff = checkpoint_state.generate_substate_diff(
- substates_to_persist,
- downed_utxos,
- downed_confidential_outputs,
- fee_withdrawals,
- )?;
- let transaction_receipt = checkpoint_state.finalize_transaction_receipt(
- FinalizeOutcome::FeeIntentCommit,
- &diff,
- fee_receipt.clone(),
- )?;
- diff.up(
- SubstateId::TransactionReceipt(checkpoint_state.transaction_hash().into()),
- Substate::new(0, transaction_receipt),
- );
+ };
+
+ let mut checkpoint_state = self.take_fee_checkpoint().ok_or(RuntimeError::NoFeeCheckpoint)?;
+ // Preserve fee state across resets so that we can charge for fees incurred during execution before the
+ // failure
+ self.read_with(|state| {
+ // Fee state in `state` includes the payments and charges from the fee transaction
+ *checkpoint_state.fee_state_mut() = state.fee_state().clone();
+ });
+
+ Ok(FinalizedState {
+ state: checkpoint_state,
+ outcome: FinalizeOutcome::FeeIntentCommit,
+ reason: Some(reason),
+ total_fees_required,
+ })
+ }
+
+ pub fn finalize(&mut self, finalized: FinalizedState) -> Result {
+ let FinalizedState {
+ mut state,
+ outcome,
+ reason,
+ total_fees_required,
+ } = finalized;
+
+ // Committing costs whatever the charges now say, and they have just been recomputed over
+ // this exact state. A payment that cannot cover that commits nothing: a fee-intent commit
+ // persists real state — substates, and a receipt carrying every event the intent emitted —
+ // and there is no shallower checkpoint left to fall back to. Letting it through is what
+ // would make an underfunded fee intent a way to write state for free.
+ //
+ // The commit path cannot fail here: its charges were tested against payments to reach it,
+ // and recomputing them over the same state does not change them.
+ let fee_state = state.fee_state();
+ if !fee_state.is_dry_run() && !fee_state.is_paid_in_full() {
+ // Whatever rejected the main intent is why the transaction failed and stays the reason
+ // reported; the shortfall below only decides that not even the fee intent survives it.
+ let reason = reason.unwrap_or_else(|| {
+ RejectReason::InsufficientFeesPaid(format!(
+ "Committing requires {} but {} paid",
+ fee_state.total_charges(),
+ fee_state.total_payments()
+ ))
+ });
+
+ // Nothing is persisted and nothing is taken, but what committing *would* have cost is
+ // the number the payer needs to retry with — and when the main intent failed for its own
+ // reason, the breakdown is the only signal that fees were the second problem. Report the
+ // charges as metered, against no payment.
+ let fee_receipt = FeeReceipt::builder()
+ .with_cost_breakdown(state.fee_state_mut().take_fee_charges())
+ .build();
return Ok(FinalizeResult::new(
- checkpoint_state.transaction_hash(),
- checkpoint_state.take_logs(),
- checkpoint_state.take_events(),
- TransactionResult::AcceptFeeRejectRest(diff, reason),
+ state.transaction_hash(),
+ state.take_logs(),
+ // Events describe state changes, and none of them happened.
+ Vec::new(),
+ TransactionResult::Reject(reason),
fee_receipt,
- ));
+ )
+ .with_total_fees_required(total_fees_required));
}
- // Finalise will always reset the state
- let mut state = self.take_working_state()?;
// Resolve the transfers to the fee pool resource and vault refunds
let mut substates_to_persist = state.take_mutated_substates();
let fee_receipt = state.finalize_fees_and_refunds(&mut substates_to_persist)?;
+
let downed_utxos = state.take_downed_utxos();
let downed_confidential_outputs = state.take_downed_confidential_outputs();
let fee_withdrawals = state.take_validator_fee_withdrawals();
+
let mut diff = state.generate_substate_diff(
substates_to_persist,
downed_utxos,
downed_confidential_outputs,
fee_withdrawals,
)?;
- let transaction_receipt =
- state.finalize_transaction_receipt(FinalizeOutcome::Commit, &diff, fee_receipt.clone())?;
+ let transaction_receipt = state.finalize_transaction_receipt(outcome, &diff, fee_receipt.clone())?;
diff.up(
SubstateId::TransactionReceipt(state.transaction_hash().into()),
Substate::new(0, transaction_receipt),
);
+ let result = match reason {
+ Some(reason) => TransactionResult::AcceptFeeRejectRest(diff, reason),
+ None => TransactionResult::Accept(diff),
+ };
+
Ok(FinalizeResult::new(
state.transaction_hash(),
state.take_logs(),
state.take_events(),
- TransactionResult::Accept(diff),
+ result,
fee_receipt,
- ))
+ )
+ .with_total_fees_required(total_fees_required))
}
fn take_fee_checkpoint(&mut self) -> Option> {
@@ -447,31 +518,16 @@ impl StateTracker {
})
}
- pub fn with_substates_to_persist) -> R, R>(&mut self, mut f: F) -> R {
- self.write_with(|state| f(state.mutated_substates()))
- }
-
- /// The storage footprint of the transaction receipt, which finalization persists in addition to
- /// the substates seen by [`Self::with_substates_to_persist`].
- pub fn transaction_receipt_size(&mut self) -> Result {
- self.write_with(|state| state.transaction_receipt_size())
- }
-
- /// Counts substates in the to-persist set that did not previously exist in the state store.
- /// Used by the fee module to charge a slot-allocation premium on top of per-byte storage.
- pub fn count_newly_created_substates(&self) -> Result {
- self.read_with(|state| {
- let store = state.store();
- let mut count = 0;
- for id in store.mutated_substates().keys() {
- match store.get_unmodified_substate(id) {
- Ok(_) => {},
- Err(RuntimeError::SubstateNotFound { .. }) => count += 1,
- Err(e) => return Err(e),
- }
- }
- Ok(count)
- })
+ /// The working state, as a module may charge against it.
+ ///
+ /// The fee module uses this to compute its finalization charges before the state to persist has
+ /// been chosen; once it has been, the same computation runs against that state directly.
+ pub fn chargeable_state(&mut self) -> ChargeableState<'_, TStore> {
+ ChargeableState::new(
+ self.working_state
+ .as_mut()
+ .expect("BUG: chargeable_state called after finalize consumed working state"),
+ )
}
pub fn are_fees_paid_in_full(&self) -> bool {
diff --git a/crates/engine/src/runtime/working_state.rs b/crates/engine/src/runtime/working_state.rs
index 3cec888c8b..4e88cc68a2 100644
--- a/crates/engine/src/runtime/working_state.rs
+++ b/crates/engine/src/runtime/working_state.rs
@@ -93,6 +93,47 @@ use crate::{
const LOG_TARGET: &str = "dan::engine::runtime::working_state";
+/// The view of a transaction's state that a runtime module is given when charging for it.
+///
+/// Exposes what a charge computed from the state needs to read — what will be persisted, and the fee
+/// state to record against — and nothing that would let a module alter the state it is pricing. It
+/// also keeps [`WorkingState`] itself, which is a large internal surface, out of the public module
+/// API that [`super::RuntimeModule`] defines.
+pub struct ChargeableState<'a, TStore> {
+ state: &'a mut WorkingState,
+}
+
+impl<'a, TStore> ChargeableState<'a, TStore> {
+ pub(super) fn new(state: &'a mut WorkingState) -> Self {
+ Self { state }
+ }
+}
+
+impl ChargeableState<'_, TStore> {
+ /// The substates this state will persist, keyed by id.
+ pub fn substates_to_persist(&mut self) -> &IndexMap {
+ self.state.mutated_substates()
+ }
+
+ /// How many of those substates did not previously exist in the state store.
+ pub fn count_newly_created_substates(&self) -> Result {
+ self.state.count_newly_created_substates()
+ }
+
+ /// The storage footprint of the transaction receipt this state will finalize into.
+ pub fn transaction_receipt_size(&mut self) -> Result {
+ self.state.transaction_receipt_size()
+ }
+
+ pub fn fee_state(&self) -> &FeeState {
+ self.state.fee_state()
+ }
+
+ pub fn fee_state_mut(&mut self) -> &mut FeeState {
+ self.state.fee_state_mut()
+ }
+}
+
#[derive(Debug, Clone)]
pub(super) struct WorkingState {
transaction_hash: Hash32,
@@ -1316,6 +1357,20 @@ impl WorkingState {
self.last_instruction_output = Some(output);
}
+ /// Counts substates in the to-persist set that did not previously exist in the state store.
+ /// Used by the fee module to charge a slot-allocation premium on top of per-byte storage.
+ pub fn count_newly_created_substates(&self) -> Result {
+ let mut count = 0;
+ for id in self.store.mutated_substates().keys() {
+ match self.store.get_unmodified_substate(id) {
+ Ok(_) => {},
+ Err(RuntimeError::SubstateNotFound { .. }) => count += 1,
+ Err(e) => return Err(e),
+ }
+ }
+ Ok(count)
+ }
+
/// The storage footprint of the transaction receipt this state will finalize into.
///
/// The receipt is persisted like any other substate but is only built once fees are settled, so
diff --git a/crates/engine/tests/fees.rs b/crates/engine/tests/fees.rs
index 44d4ea687e..e9c3be16c4 100644
--- a/crates/engine/tests/fees.rs
+++ b/crates/engine/tests/fees.rs
@@ -1,10 +1,24 @@
// Copyright 2023 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause
-use tari_engine_types::{commit_result::RejectReason, fees::FeeSource};
+use tari_crypto::ristretto::RistrettoSecretKey;
+use tari_engine_types::{
+ commit_result::{RejectReason, TransactionResult},
+ fees::{FeeReceipt, FeeSource},
+};
use tari_ootle_transaction::{Epoch, Transaction, args};
-use tari_template_lib::types::{Amount, ComponentAddress, constants::STEALTH_TARI_RESOURCE_ADDRESS};
-use tari_template_test_tooling::{TemplateTest, support::assert_error::assert_reject_reason, xtr_faucet_component};
+use tari_template_lib::types::{
+ Amount,
+ ComponentAddress,
+ NonFungibleAddress,
+ constants::STEALTH_TARI_RESOURCE_ADDRESS,
+};
+use tari_template_test_tooling::{
+ TemplateTest,
+ compile::compile_template,
+ support::assert_error::assert_reject_reason,
+ xtr_faucet_component,
+};
const CRATE_PATH: &str = env!("CARGO_MANIFEST_DIR");
const TEMPLATE_PATHS: [&str; 1] = ["tests/templates/state"];
@@ -29,7 +43,7 @@ fn deducts_fees_from_payments_and_refunds_the_rest() {
test.disable_fees();
// Check difference was refunded
- let payment = result.finalize.fee_receipt;
+ let payment = result.finalize.fee_receipt.clone();
let new_balance = test
.read_only_state_store()
.get_vaults_for_account(account)
@@ -196,6 +210,103 @@ fn failed_fee_transaction() {
assert_eq!(new_balance, initial_balance);
}
+/// Runs a transaction that creates several components, paying `fee` towards it, and returns the
+/// storage fee it was charged along with whether the main intent committed.
+fn storage_charged_for_creating_components(fee: u64) -> (u64, bool) {
+ let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
+ let (account, owner_token, private_key) = test.create_funded_account();
+ test.enable_fees();
+
+ let state_template = test.get_template_address("State");
+ let mut builder = test.transaction().pay_fee_from_component(account, Amount::from(fee));
+ for _ in 0..5 {
+ builder = builder.call_function(state_template, "new", args![]);
+ }
+
+ let result = test.execute_expect_commit(builder.build_and_seal(&private_key), vec![owner_token]);
+ let committed = result.finalize.result.is_accept();
+ (
+ result.finalize.fee_receipt.fee_breakdown().get(FeeSource::Storage),
+ committed,
+ )
+}
+
+/// A transaction that cannot pay for its main intent commits only its fee intent, so the storage it
+/// is charged for is the fee checkpoint's — not that of the state it built and abandoned.
+#[test]
+fn a_fee_intent_commit_is_not_charged_for_the_state_it_abandons() {
+ let (storage_rejected, committed) = storage_charged_for_creating_components(1_000);
+ assert!(!committed, "expected the main intent to be rejected");
+
+ let (storage_committed, committed) = storage_charged_for_creating_components(100_000);
+ assert!(committed, "expected the main intent to commit");
+
+ // The components only exist in the committed run, so only it pays for them. Charging the
+ // rejected run over the working state instead of the checkpoint would put the two within a few
+ // bytes of each other.
+ assert!(
+ storage_rejected * 2 < storage_committed,
+ "rejected run charged {storage_rejected} storage against the committed run's {storage_committed}"
+ );
+}
+
+/// A fee-intent commit persists real state, so it happens only when the payment covers what that
+/// state costs. When it does not, nothing commits — there is no shallower checkpoint to fall back
+/// to, and committing anyway would be a way to write state for free.
+#[test]
+fn an_unaffordable_fee_intent_commits_nothing() {
+ let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
+ let (account, owner_token, private_key) = test.create_empty_account();
+ test.enable_fees();
+
+ // Enough for the fee intent's execution, nowhere near the state it writes.
+ const FEE: u64 = 100;
+
+ let result = test
+ .try_execute(
+ test.transaction()
+ .with_fee_instructions_builder(|builder| {
+ builder
+ // Writes the account's vault and the faucet's, then pays a fraction of what
+ // storing them costs.
+ .call_method(xtr_faucet_component(), "take", args![account])
+ .call_method(account, "pay_fee", args![FEE])
+ })
+ .build_and_seal(&private_key),
+ vec![owner_token],
+ )
+ .unwrap();
+
+ assert!(
+ matches!(
+ result.finalize.result,
+ TransactionResult::Reject(RejectReason::InsufficientFeesPaid(_))
+ ),
+ "actual result: {:?}",
+ result.finalize.result
+ );
+
+ // Nothing was written, so nothing was taken.
+ assert_eq!(result.finalize.fee_receipt.total_fees_paid(), 0);
+
+ // What committing would have cost is still reported: it is the number the payer has to raise
+ // their fee to, and on a rejection there is nowhere else to read it from.
+ let charged = result.finalize.fee_receipt.total_fees_charged();
+ assert!(charged > FEE, "expected the metered charges, got {charged}");
+ assert!(result.finalize.fee_receipt.required_fees() > FEE);
+
+ let balance = test
+ .read_only_state_store()
+ .get_vaults_for_account(account)
+ .unwrap()
+ .get(&STEALTH_TARI_RESOURCE_ADDRESS)
+ .map(|v| v.balance());
+ assert!(
+ balance.is_none() || balance == Some(Amount::zero()),
+ "account was funded: {balance:?}"
+ );
+}
+
#[test]
fn fail_partial_paid_fees() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
@@ -205,10 +316,10 @@ fn fail_partial_paid_fees() {
let orig_balance: Amount = test.call_method(account, "balance", args![STEALTH_TARI_RESOURCE_ADDRESS], vec![]);
test.enable_fees();
- // Must cover the fee section's own cost (so the fee instructions succeed) yet stay smaller
- // than the full transaction's fee, so the main instructions exhaust the compute the payment
- // funds and trap.
- const FEE_PAID: u64 = 100;
+ // Must cover what committing the fee intent costs — otherwise nothing commits at all — yet stay
+ // smaller than the full transaction's fee, so the main instructions exhaust the compute the
+ // payment funds and trap.
+ const FEE_PAID: u64 = 1000;
let result = test.execute_expect_commit(
Transaction::builder_localnet(Epoch(1))
@@ -227,25 +338,33 @@ fn fail_partial_paid_fees() {
vec![owner_token, owner_token2],
);
- let total_fees = result.finalize.fee_receipt.fee_breakdown().get_total();
- // The fee charged exceeds the fee paid, so the transaction is rejected. Execution stops as soon
- // as the fees are determined insufficient, so the *execution* charges stay close to what was
- // paid. Storage is excluded: it is charged at finalization for the substates and the receipt
- // that a fee-intent commit still persists, whenever execution gave up.
- let storage = result.finalize.fee_receipt.fee_breakdown().get(FeeSource::Storage);
- assert!(
- total_fees > FEE_PAID && total_fees - storage < FEE_PAID * 3,
- "total fees: {total_fees}, of which storage: {storage}"
- );
+ // The main instructions cost more than was paid, so they are rejected and only the fee intent
+ // commits.
let reason = result.expect_failure();
assert!(
- matches!(reason, RejectReason::ExecutionFailure(msg) if msg.contains("Insufficient fees")),
+ matches!(reason, RejectReason::InsufficientFeesPaid(_)),
"actual reason: {reason}"
);
- // Check that the fee paid was deducted
- let payment = result.finalize.fee_receipt;
- assert!(!payment.is_paid_in_full());
+ // What is charged is what the fee intent persisted, not what the abandoned instructions would
+ // have. That is below the payment, so the commit is affordable and the rest is refunded.
+ let payment = &result.finalize.fee_receipt;
+ let total_fees = payment.fee_breakdown().get_total();
+ assert!(
+ total_fees < FEE_PAID,
+ "total fees {total_fees} exceeds the {FEE_PAID} paid"
+ );
+ assert!(payment.is_paid_in_full());
+ assert_eq!(payment.total_refunded(), FEE_PAID - total_fees);
+
+ // The fee intent's own cost is below what was paid, so it cannot tell a resubmission what to
+ // clear. The result keeps the figure the main intent was rejected for.
+ assert!(
+ result.finalize.total_fees_required > FEE_PAID,
+ "the transaction was rejected for underpayment, so what it required must exceed the {FEE_PAID} paid"
+ );
+ assert!(result.finalize.required_fees() > result.finalize.total_fees_required);
+
let new_balance = test
.read_only_state_store()
.get_vaults_for_account(account)
@@ -253,7 +372,7 @@ fn fail_partial_paid_fees() {
.get(&STEALTH_TARI_RESOURCE_ADDRESS)
.unwrap()
.balance();
- assert_eq!(new_balance, orig_balance - Amount::from(FEE_PAID));
+ assert_eq!(new_balance, orig_balance - Amount::from(total_fees));
}
#[test]
@@ -301,9 +420,8 @@ fn fail_pay_less_fees_than_fee_transaction() {
.call_method(
account,
"pay_fee".to_string(),
- // Lands between the fee-instructions cost and the full transaction
- // cost, so the fee section is accepted while the rest fails for
- // InsufficientFeesPaid.
+ // Less than the fee instructions themselves cost to commit, so not
+ // even the fee intent survives.
args![150],
)
@@ -322,13 +440,15 @@ fn fail_pay_less_fees_than_fee_transaction() {
test.disable_fees();
- let (diff, reason) = result.expect_fee_accept_transaction_reject();
- assert_reject_reason(reason, RejectReason::InsufficientFeesPaid(String::new()));
-
- let (_, s) = diff.up_iter().find(|(id, _)| id.is_vault()).expect("Account not found");
- assert_eq!(
- s.substate_value().as_vault().unwrap().balance(),
- orig_balance - Amount::from(150u64)
+ // The payment does not cover the state the fee intent writes, so nothing commits at all.
+ assert!(
+ matches!(result.finalize.result, TransactionResult::Reject(_)),
+ "actual result: {:?}",
+ result.finalize.result
+ );
+ assert_reject_reason(
+ result.expect_failure(),
+ RejectReason::InsufficientFeesPaid(String::new()),
);
// Fee was not deducted
@@ -434,8 +554,8 @@ fn dangling_bucket_pay_fees() {
test.enable_fees();
let result = test.execute_and_commit_on_success(
- Transaction::builder_localnet(Epoch(1))
- .pay_fee_from_component(account, Amount::from(500u64))
+ test.transaction()
+ .pay_fee_from_component(account, Amount::from(3000u64))
.call_method(account, "withdraw", args![STEALTH_TARI_RESOURCE_ADDRESS, 10])
.put_last_instruction_output_on_workspace("dangling_bucket")
.build_and_seal(&private_key),
@@ -463,6 +583,368 @@ fn dangling_bucket_pay_fees() {
assert_eq!(orig_balance - new_balance, payment.total_fees_paid());
}
+// A submitted `max_fee` is itself an input to what the transaction costs, so a dry run metered at
+// one `max_fee` does not perfectly predict a real run at another. There are three quantities the
+// cost could read `max_fee` through, and what a caller can rely on is which of them it does:
+//
+// - the encoded *width* of the fee literal, through `calc_args_weight`. Live, and the only term that can make a real
+// run cost more than the dry run said.
+// - the fee amount's *digit count*, through the `std.vault.pay_fee` event the persisted receipt carries. Neutralized —
+// priced at its widest, so it buys nothing.
+// - the *residual vault balance*'s width, since the finalization charges run before refunds. Live, and opposed to the
+// first: a wider `max_fee` leaves a narrower residual. Nothing narrows the `max_fee` a dry run meters at, so a
+// submission can move either term in either direction and the allowance has to cover both.
+//
+// `try_execute` never commits, so every run below starts from identical state and the whole delta
+// is attributable to `max_fee`. Each test varies one of the three quantities and holds the other
+// two fixed, so a failure names its mechanism rather than a total.
+
+/// The balance `create_funded_account` starts an account with, so the residual vault balance the
+/// byte counter sees (`FUNDED - max_fee`) is predictable.
+const FUNDED: u64 = 1_000_000_000;
+
+/// `calc_args_weight` prices an instruction's literal args by their encoded bytes, so the weight
+/// charge reads `max_fee` through the width of its encoding.
+fn amount_len(value: u64) -> u64 {
+ tari_bor::encode(&Amount::from(value)).unwrap().len() as u64
+}
+
+/// The `std.vault.pay_fee` event records the amount as a decimal string, so the receipt's size — and
+/// with it the storage charge — would read `max_fee` through its digit count if the amount were not
+/// priced at its widest.
+fn digit_count(value: u64) -> u64 {
+ value.to_string().len() as u64
+}
+
+/// Meters `build(max_fee)` once per `max_fee`, from identical state each time.
+fn meter_across_max_fees(
+ test: &mut TemplateTest,
+ max_fees: &[u64],
+ proofs: &[NonFungibleAddress],
+ build: impl Fn(u64) -> Transaction,
+) -> Vec {
+ max_fees
+ .iter()
+ .map(|&max_fee| {
+ let result = test.try_execute(build(max_fee), proofs.to_vec()).unwrap();
+ // A run that does not commit meters a different execution — only the fee intent — and
+ // would not be comparable with the others.
+ assert!(
+ matches!(result.finalize.result, TransactionResult::Accept(_)),
+ "max_fee {max_fee} did not commit, so this run is not comparable: {:?}",
+ result.finalize.result
+ );
+ result.finalize.fee_receipt
+ })
+ .collect()
+}
+
+fn state_transaction<'a>(
+ test: &TemplateTest,
+ account: ComponentAddress,
+ key: &'a RistrettoSecretKey,
+) -> impl Fn(u64) -> Transaction + use<'a> {
+ let template = test.get_template_address("State");
+ let tx = test.transaction();
+ move |max_fee| {
+ tx.clone()
+ .pay_fee_from_component(account, max_fee)
+ .call_function(template, "new", args![])
+ .build_and_seal(key)
+ }
+}
+
+/// `max_fee` is the only literal arg of the `pay_fee` instruction, and `calc_args_weight` prices an
+/// instruction's literals at `total_bytes / LITERAL_BYTE_DIVISOR`. The whole weight of this
+/// transaction is that one term, so it steps whenever the encoded width crosses the divisor.
+#[test]
+fn transaction_weight_follows_the_max_fee_literal_width() {
+ const LITERAL_BYTE_DIVISOR: u64 = 3;
+ // Straddles an encoding-width boundary while keeping the digit count and the residual balance's
+ // width fixed, so the weight charge is the only thing that can move.
+ const MAX_FEES: [u64; 2] = [65_535, 65_536];
+
+ let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
+ let (account, owner_token, key) = test.create_funded_account();
+ test.enable_fees();
+ let build = state_transaction(&test, account, &key);
+ let receipts = meter_across_max_fees(&mut test, &MAX_FEES, &[owner_token], build);
+
+ assert_ne!(amount_len(MAX_FEES[0]), amount_len(MAX_FEES[1]));
+ assert_eq!(digit_count(MAX_FEES[0]), digit_count(MAX_FEES[1]));
+
+ let per_weight = test.fee_table().per_transaction_weight_cost();
+ for (max_fee, receipt) in MAX_FEES.iter().zip(&receipts) {
+ assert_eq!(
+ receipt.fee_breakdown().get(FeeSource::TransactionWeight),
+ (amount_len(*max_fee) / LITERAL_BYTE_DIVISOR) * per_weight,
+ "TransactionWeight at max_fee {max_fee}"
+ );
+ assert_eq!(
+ receipt.fee_breakdown().get(FeeSource::Storage),
+ receipts[0].fee_breakdown().get(FeeSource::Storage),
+ "Storage must not move while the digit count and residual width hold"
+ );
+ }
+}
+
+/// The transaction receipt is part of the state a transaction pays to persist, and it carries the
+/// `std.vault.pay_fee` event, whose payload records the amount as a decimal string. Charging that
+/// verbatim would price permanent state by the digit count of `max_fee`, so the amount is priced at
+/// its widest instead and the digit count buys nothing.
+#[test]
+fn storage_does_not_follow_the_max_fee_digit_count() {
+ // One encoding width and one residual width throughout, so the digit count is the only thing
+ // that varies.
+ const MAX_FEES: [u64; 4] = [65_536, 1_000_000, 100_000_000, 999_000_000];
+
+ let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
+ let (account, owner_token, key) = test.create_funded_account();
+ test.enable_fees();
+ let build = state_transaction(&test, account, &key);
+ let receipts = meter_across_max_fees(&mut test, &MAX_FEES, &[owner_token], build);
+
+ assert_ne!(
+ digit_count(MAX_FEES[0]),
+ digit_count(MAX_FEES[MAX_FEES.len() - 1]),
+ "the digit count must actually vary for this to prove anything"
+ );
+ for (max_fee, receipt) in MAX_FEES.iter().zip(&receipts) {
+ assert_eq!(
+ amount_len(*max_fee),
+ amount_len(MAX_FEES[0]),
+ "encoding width must hold"
+ );
+ assert_eq!(
+ amount_len(FUNDED - max_fee),
+ amount_len(FUNDED - MAX_FEES[0]),
+ "residual width must hold"
+ );
+ assert_eq!(
+ receipt.fee_breakdown().get(FeeSource::Storage),
+ receipts[0].fee_breakdown().get(FeeSource::Storage),
+ "Storage at max_fee {max_fee}"
+ );
+ }
+}
+
+/// The finalization charges run before `finalize_fees_and_refunds` returns the unspent payment, so
+/// the fee vault is byte-counted holding `balance - max_fee`. A larger `max_fee` narrows that
+/// residual and makes storage *cheaper* — the opposite direction to the digit-count term above.
+#[test]
+fn storage_follows_the_residual_vault_balance_width() {
+ // One encoding width and one digit count throughout, so the residual width is the only thing
+ // that varies.
+ const MAX_FEES: [u64; 3] = [999_940_000, 999_999_800, 999_999_990];
+
+ let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
+ let (account, owner_token, key) = test.create_funded_account();
+ test.enable_fees();
+ let build = state_transaction(&test, account, &key);
+ let receipts = meter_across_max_fees(&mut test, &MAX_FEES, &[owner_token], build);
+
+ let per_byte = test.fee_table().per_byte_storage_cost();
+ let divisor = test.fee_table().storage_cost_divisor();
+ for (max_fee, receipt) in MAX_FEES.iter().zip(&receipts) {
+ assert_eq!(
+ amount_len(*max_fee),
+ amount_len(MAX_FEES[0]),
+ "encoding width must hold"
+ );
+ assert_eq!(digit_count(*max_fee), digit_count(MAX_FEES[0]), "digit count must hold");
+ let bytes_saved = amount_len(FUNDED - MAX_FEES[0]) - amount_len(FUNDED - max_fee);
+ assert_eq!(
+ receipts[0].fee_breakdown().get(FeeSource::Storage) - receipt.fee_breakdown().get(FeeSource::Storage),
+ bytes_saved * per_byte / divisor,
+ "Storage at max_fee {max_fee}"
+ );
+ }
+}
+
+/// Only `TransactionWeight` and `Storage` are `max_fee`-sensitive. Anything else moving would mean
+/// the three mechanisms above do not account for the whole drift.
+#[test]
+fn no_charge_other_than_weight_and_storage_moves_with_max_fee() {
+ const MAX_FEES: [u64; 6] = [1_000, 65_535, 65_536, 100_000_000, FUNDED - 60_000, FUNDED - 10];
+
+ let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
+ let (account, owner_token, key) = test.create_funded_account();
+ test.enable_fees();
+ let build = state_transaction(&test, account, &key);
+ let receipts = meter_across_max_fees(&mut test, &MAX_FEES, &[owner_token], build);
+
+ assert_only_weight_and_storage_move(&MAX_FEES, &receipts);
+}
+
+/// A publish carries a blob and takes the dedicated `TemplatePublish` pricing path, so it is the
+/// shape most likely to hide a fourth mechanism. It does not.
+#[test]
+fn a_template_publish_introduces_no_further_max_fee_sensitivity() {
+ // Every entry clears the publish cost; between them they move all three quantities.
+ const MAX_FEES: [u64; 4] = [400_000, 100_000_000, FUNDED - 60_000, FUNDED - 10];
+
+ let mut test = TemplateTest::new(CRATE_PATH, &[] as &[&str]);
+ let (account, owner_proof, key, _) = test.create_funded_account_with_keypair();
+ let template = compile_template("tests/templates/hello_world", &[]).unwrap();
+ test.enable_fees();
+
+ let tx = test.transaction();
+ let receipts = meter_across_max_fees(&mut test, &MAX_FEES, &[owner_proof], |max_fee| {
+ tx.clone()
+ .pay_fee_from_component(account, max_fee)
+ .publish_template(template.clone().into_code())
+ .build_and_seal(&key)
+ });
+
+ assert_only_weight_and_storage_move(&MAX_FEES, &receipts);
+}
+
+/// The property a caller actually depends on, asserted as the promise itself rather than as a
+/// number: what `required_fees` returns for *any* run must cover *every* other run.
+///
+/// A dry run meters at whatever `max_fee` the caller submitted, and the submission built from it
+/// uses a smaller one, so the estimate has to hold in both directions — asserting it only from the
+/// cheapest run would assume the very thing the allowance exists to cover. The burn rate is varied
+/// because the burn is taken over the running total and so re-multiplies both terms; a bound
+/// established with the burn disabled would not hold on a live network.
+#[test]
+fn required_fees_covers_a_real_run_at_any_max_fee() {
+ // Spans every encoding width a fee above this transaction's cost can take, every residual
+ // width, and digit counts from four to nine.
+ // The smallest entry must still cover the transaction at a 100% burn, which roughly doubles it.
+ const MAX_FEES: [u64; 8] = [
+ 2_000,
+ 65_535,
+ 65_536,
+ 1_000_000,
+ 100_000_000,
+ FUNDED - 60_000,
+ FUNDED - 200,
+ FUNDED - 10,
+ ];
+
+ for rate in [0u16, 500, 2_000, 10_000] {
+ let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
+ let (account, owner_token, key) = test.create_funded_account();
+ test.enable_fees();
+ test.set_burn_rate_bps(rate);
+ let build = state_transaction(&test, account, &key);
+ let receipts = meter_across_max_fees(&mut test, &MAX_FEES, &[owner_token], build);
+
+ let dearest = receipts
+ .iter()
+ .map(|r| r.total_fees_charged())
+ .max()
+ .expect("max_fees is not empty");
+ for (max_fee, receipt) in MAX_FEES.iter().zip(&receipts) {
+ let estimate = receipt.required_fees();
+ assert!(
+ dearest <= estimate,
+ "at {rate} bps, a dry run at max_fee {max_fee} estimates {estimate}, under the {dearest} that some \
+ other max_fee costs"
+ );
+ }
+ }
+}
+
+/// The residual term's direction, which is what makes it harmless. Raising `max_fee` narrows the
+/// balance left in the fee vault when the byte counter runs, so it can only ever take storage down.
+#[test]
+fn a_wider_max_fee_never_raises_the_storage_charge() {
+ // Ascending, at one encoding width, so only the residual moves and it only narrows.
+ const MAX_FEES: [u64; 4] = [65_536, FUNDED - 60_000, FUNDED - 200, FUNDED - 10];
+
+ let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
+ let (account, owner_token, key) = test.create_funded_account();
+ test.enable_fees();
+ let build = state_transaction(&test, account, &key);
+ let receipts = meter_across_max_fees(&mut test, &MAX_FEES, &[owner_token], build);
+
+ for (pair, fees) in receipts.windows(2).zip(MAX_FEES.windows(2)) {
+ assert!(
+ pair[1].fee_breakdown().get(FeeSource::Storage) <= pair[0].fee_breakdown().get(FeeSource::Storage),
+ "storage rose between max_fee {} and {}",
+ fees[0],
+ fees[1]
+ );
+ }
+ // The mechanism is live, not merely one-directional by accident.
+ assert!(
+ receipts[receipts.len() - 1].fee_breakdown().get(FeeSource::Storage) <
+ receipts[0].fee_breakdown().get(FeeSource::Storage)
+ );
+}
+
+fn assert_only_weight_and_storage_move(max_fees: &[u64], receipts: &[FeeReceipt]) {
+ let base = &receipts[0];
+ for (max_fee, receipt) in max_fees.iter().zip(receipts) {
+ for (source, amount) in receipt.fee_breakdown().iter() {
+ if matches!(source, FeeSource::TransactionWeight | FeeSource::Storage) {
+ continue;
+ }
+ assert_eq!(
+ *amount,
+ base.fee_breakdown().get(*source),
+ "{source:?} moved between max_fee {} and {max_fee}",
+ max_fees[0]
+ );
+ }
+ }
+}
+
+/// The digit-count mechanism traced to its source: the event payload the receipt carries holds
+/// `max_fee` itself, rendered in decimal.
+#[test]
+fn the_pay_fee_event_records_max_fee_in_decimal() {
+ const MAX_FEE: u64 = 123_456_789;
+
+ let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
+ let (account, owner_token, key) = test.create_funded_account();
+ test.enable_fees();
+ let build = state_transaction(&test, account, &key);
+ let result = test.try_execute(build(MAX_FEE), vec![owner_token]).unwrap();
+
+ let pay_fee = result
+ .finalize
+ .events
+ .iter()
+ .find(|e| e.topic() == "std.vault.pay_fee")
+ .expect("pay_fee event");
+ assert_eq!(
+ pay_fee.get_payload("amount"),
+ Some(MAX_FEE.to_string().as_str()),
+ "the event records the payment cap, not the fee actually charged"
+ );
+}
+
+/// The exhaust burn adds no mechanism of its own but re-multiplies the others, being taken over the
+/// running total. At a 100% rate the compounding is exact: the total moves by twice the movement of
+/// the charges beneath it.
+#[test]
+fn the_exhaust_burn_compounds_the_drift() {
+ const FULL_RATE_BPS: u16 = 10_000;
+ // Four bytes of residual width apart, so the drift beneath the burn is unambiguously non-zero.
+ const MAX_FEES: [u64; 2] = [65_536, FUNDED - 10];
+
+ let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
+ let (account, owner_token, key) = test.create_funded_account();
+ test.enable_fees();
+ test.set_burn_rate_bps(FULL_RATE_BPS);
+ let build = state_transaction(&test, account, &key);
+ let receipts = meter_across_max_fees(&mut test, &MAX_FEES, &[owner_token], build);
+
+ let pre_burn = |r: &FeeReceipt| r.total_fees_charged() - r.fee_breakdown().get(FeeSource::ExhaustBurn);
+ let pre_burn_delta = pre_burn(&receipts[1]).abs_diff(pre_burn(&receipts[0]));
+ assert!(pre_burn_delta > 0, "the chosen max_fees must move the pre-burn charges");
+ assert_eq!(
+ receipts[1]
+ .total_fees_charged()
+ .abs_diff(receipts[0].total_fees_charged()),
+ pre_burn_delta * 2,
+ "a 100% burn doubles whatever the max_fee-sensitive charges contribute"
+ );
+}
+
#[test]
fn template_load_fee_charged_once_per_template_per_transaction() {
let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS);
@@ -484,7 +966,7 @@ fn template_load_fee_charged_once_per_template_per_transaction() {
// Five State calls — same template touched five extra times. Without dedup, TemplateLoad
// would scale with call count; with dedup it must match the single-call baseline.
let many = test.execute_expect_success(
- Transaction::builder_localnet(Epoch(1))
+ test.transaction()
.pay_fee_from_component(account, 1000u64)
.call_method(state, "set", args![1u32])
.call_method(state, "set", args![2u32])
diff --git a/crates/engine_types/src/commit_result.rs b/crates/engine_types/src/commit_result.rs
index ef368954d2..80dc7842f7 100644
--- a/crates/engine_types/src/commit_result.rs
+++ b/crates/engine_types/src/commit_result.rs
@@ -31,7 +31,7 @@ use tari_template_lib::types::{ComponentAddress, Hash32, ResourceAddress, Templa
use crate::{
Epoch,
events::Event,
- fees::FeeReceipt,
+ fees::{FEE_ESTIMATE_ALLOWANCE, FeeReceipt},
instruction_result::InstructionResult,
logs::LogEntry,
resource::Resource,
@@ -182,6 +182,17 @@ pub struct FinalizeResult {
pub result: TransactionResult,
#[n(5)]
pub fee_receipt: FeeReceipt,
+ /// What committing the whole transaction was priced at, which is what the commit-or-reject
+ /// decision was made against.
+ ///
+ /// Equal to `fee_receipt.total_fees_charged()` on a full commit. When only the fee intent
+ /// commits, the charges are re-derived over the fee checkpoint alone and so fall below what was
+ /// paid; this keeps the figure the main intent was rejected for — the one a resubmission has to
+ /// clear. Execution metadata rather than settled state, so it stays out of the persisted
+ /// receipt, whose every field is priced into the storage charge of each transaction.
+ #[n(6)]
+ #[serde(default)]
+ pub total_fees_required: u64,
}
impl FinalizeResult {
@@ -193,6 +204,7 @@ impl FinalizeResult {
fee_receipt: FeeReceipt,
) -> Self {
Self {
+ total_fees_required: fee_receipt.total_fees_charged(),
transaction_hash,
logs,
events,
@@ -202,6 +214,18 @@ impl FinalizeResult {
}
}
+ /// Records what committing the whole transaction was priced at, when that differs from what the
+ /// receipt was ultimately charged.
+ pub fn with_total_fees_required(mut self, amount: u64) -> Self {
+ self.total_fees_required = amount.max(self.fee_receipt.total_fees_charged());
+ self
+ }
+
+ /// The minimum `max_fee` a resubmission of this transaction has to carry.
+ pub fn required_fees(&self) -> u64 {
+ self.total_fees_required.saturating_add(FEE_ESTIMATE_ALLOWANCE)
+ }
+
pub fn new_rejected(transaction_hash: Hash32, reason: RejectReason) -> Self {
Self {
transaction_hash,
@@ -210,6 +234,7 @@ impl FinalizeResult {
execution_results: Vec::new(),
result: TransactionResult::Reject(reason),
fee_receipt: FeeReceipt::default(),
+ total_fees_required: 0,
}
}
diff --git a/crates/engine_types/src/events.rs b/crates/engine_types/src/events.rs
index dc1892a995..d70a94b364 100644
--- a/crates/engine_types/src/events.rs
+++ b/crates/engine_types/src/events.rs
@@ -30,6 +30,9 @@ use crate::substate::SubstateId;
// Topics for builtin events emitted by the engine
const STANDARD_TOPIC_PREFIX: &str = "std.";
+/// The widest decimal an amount renders to, amounts being `u64` microtari.
+const WIDEST_AMOUNT: &str = "18446744073709551615";
+
fn std_event(object_name: &str, action_name: &str) -> String {
format!("{}{}.{}", STANDARD_TOPIC_PREFIX, object_name, action_name)
}
@@ -96,6 +99,38 @@ impl Event {
)
}
+ /// Whether this is the standard event `object_name` emits for `action_name`. Matches the topic
+ /// [`std_event`] builds, without building it.
+ pub fn is_std(&self, object_name: &str, action_name: &str) -> bool {
+ self.topic
+ .strip_prefix(STANDARD_TOPIC_PREFIX)
+ .and_then(|rest| rest.strip_prefix(object_name))
+ .and_then(|rest| rest.strip_prefix('.'))
+ .is_some_and(|rest| rest == action_name)
+ }
+
+ /// The bytes to add to this event's encoded length when pricing the permanent state a
+ /// transaction pays for, so that the price cannot depend on the fee being priced.
+ ///
+ /// A fee-payment event records the payment as a decimal string, so its length tracks `max_fee`
+ /// digit for digit. Charging that verbatim lets the storage price read back the very amount it
+ /// is pricing: a submission whose `max_fee` is wider than the one a dry run measured costs more
+ /// than that dry run reported, and the transaction is rejected for underpayment. Pricing it at
+ /// its widest breaks the loop — the same stand-in [`crate::fees::FeeReceipt::widest`] gets, for
+ /// the same reason.
+ ///
+ /// Only the engine's own fee event is neutralized here. A template that echoes an amount its
+ /// caller passed it is priced as written: that coupling is visible to whoever chose both the
+ /// `max_fee` and the template, and is theirs to account for.
+ pub fn charged_size_padding(&self) -> usize {
+ if !self.is_std("vault", "pay_fee") {
+ return 0;
+ }
+ self.get_payload("amount").map_or(0, |amount| {
+ minicbor::len(WIDEST_AMOUNT).saturating_sub(minicbor::len(amount))
+ })
+ }
+
pub fn validate_custom_topic>(topic: T) -> Result<(), String> {
let s = topic.as_ref();
if topic.as_ref().starts_with(STANDARD_TOPIC_PREFIX) {
@@ -154,3 +189,62 @@ impl Display for Event {
)
}
}
+
+#[cfg(test)]
+mod tests {
+ use tari_template_lib::types::Hash32;
+
+ use super::*;
+
+ fn pay_fee_event(amount: &str) -> Event {
+ Event::std(
+ None,
+ Hash32::from_array([0u8; Hash32::LENGTH]),
+ "vault",
+ "pay_fee",
+ Metadata::from_iter([("amount", amount.to_string())]),
+ )
+ }
+
+ #[test]
+ fn is_std_recognises_the_topics_std_event_builds() {
+ let event = pay_fee_event("1000");
+ assert_eq!(event.topic(), std_event("vault", "pay_fee"));
+ assert!(event.is_std("vault", "pay_fee"));
+ assert!(!event.is_std("vault", "deposit"));
+ assert!(!event.is_std("component", "pay_fee"));
+ // A prefix of the object name must not match.
+ assert!(!event.is_std("vau", "lt.pay_fee"));
+ }
+
+ #[test]
+ fn a_template_cannot_reach_the_fee_topic() {
+ // What makes matching on the topic safe: only the engine can emit it.
+ assert!(Event::validate_custom_topic(std_event("vault", "pay_fee")).is_err());
+ }
+
+ #[test]
+ fn a_fee_payment_amount_is_priced_at_one_width_whatever_its_value() {
+ let widest = pay_fee_event(WIDEST_AMOUNT);
+ let expected = minicbor::len(&widest) + widest.charged_size_padding();
+ for amount in ["1", "1000", "398287", "18446744073709551614"] {
+ let event = pay_fee_event(amount);
+ assert_eq!(
+ minicbor::len(&event) + event.charged_size_padding(),
+ expected,
+ "amount {amount} priced differently to the widest amount"
+ );
+ }
+ }
+
+ #[test]
+ fn an_amount_a_template_records_is_priced_as_written() {
+ let event = Event::custom(
+ None,
+ Hash32::from_array([0u8; Hash32::LENGTH]),
+ "mytemplate.paid".to_string(),
+ Metadata::from_iter([("amount", "398287".to_string())]),
+ );
+ assert_eq!(event.charged_size_padding(), 0);
+ }
+}
diff --git a/crates/engine_types/src/fees.rs b/crates/engine_types/src/fees.rs
index 379b7dc0bf..fc6adc7ec1 100644
--- a/crates/engine_types/src/fees.rs
+++ b/crates/engine_types/src/fees.rs
@@ -4,6 +4,35 @@
use indexmap::{IndexMap, map::Entry};
use serde::{Deserialize, Serialize};
+/// The exhaust burn rate that [`FEE_ESTIMATE_ALLOWANCE`] is derived against, in basis points.
+///
+/// The burn is taken over the running fee total, so it re-multiplies every term that can make a
+/// real run cost more than the dry run that estimated it. A network configured above this rate
+/// under-states `FeeReceipt::required_fees`, and every submission built from a dry run is then
+/// rejected as underpaid — `every_shipped_network_stays_within_the_burn_rate_ceiling` holds the
+/// shipped networks to it.
+pub const MAX_EXHAUST_BURN_RATE_BPS: u16 = 10_000;
+
+/// The allowance a dry-run estimate carries on top of what it metered, so that a real run of the
+/// same transaction at a different `max_fee` can never cost more than the estimate.
+///
+/// `max_fee` is itself an input to the cost, so the two runs meter differently. Two charges read it
+/// back, and they move in opposite directions, so the allowance covers both. Transaction weight
+/// prices the fee instruction's literal args by their encoded bytes, so a wider `max_fee` pushes
+/// `literal_bytes / LITERAL_BYTE_DIVISOR` up by a bounded number of steps. The storage tally
+/// byte-counts the fee vault before the unspent payment is returned, so it counts
+/// `balance - max_fee`, and a wider `max_fee` narrows that. A dry run meters at whatever `max_fee`
+/// the caller submitted — nothing narrows it — so either direction is reachable between a dry run
+/// and the submission built from it. The exhaust burn reads nothing of its own but re-multiplies
+/// both, being taken over the running total.
+///
+/// The value is derived rather than chosen: `FeeTable::fee_estimate_allowance` computes it from
+/// `per_transaction_weight_cost`, `per_byte_storage_cost`, `storage_cost_divisor`,
+/// `LITERAL_BYTE_DIVISOR` and the burn rate, all of which live in crates downstream of this one.
+/// `fee_estimate_allowance_covers_every_shipped_network` asserts this value covers every shipped
+/// network at `MAX_EXHAUST_BURN_RATE_BPS`.
+pub const FEE_ESTIMATE_ALLOWANCE: u64 = 25;
+
#[derive(Debug, Clone, Default)]
pub struct FeeReceiptBuilder {
/// The total amount of the fee payment(s)
@@ -106,12 +135,24 @@ impl FeeReceipt {
self.cost_breakdown.get_total()
}
- /// The minimum fee required to submit a transaction based on a dry run result.
- /// This is `total_fees_charged + 1` to account for potential rounding differences in the storage cost calculation.
- /// The storage fee depends on the vault balance at calculation time, which changes when a different max_fee is used
- /// in the actual submission vs the dry run — this can shift `floor(total_bytes / 4)` by 1 at a rounding boundary.
+ /// The minimum fee to submit with, given what a dry run metered.
+ ///
+ /// A submission cannot simply use `total_fees_charged`: the `max_fee` it carries is itself an
+ /// input to the cost, so a real run meters slightly differently from the dry run that produced
+ /// the estimate. The allowance covers the whole of that difference.
+ ///
+ /// Two charges read `max_fee` back, in opposite directions. The transaction weight prices the fee
+ /// instruction's literal args by their encoded bytes, so it steps whenever the amount's width
+ /// crosses a multiple of the literal divisor. The storage tally byte-counts the fee vault before
+ /// the unspent payment is returned, so a wider `max_fee` leaves a narrower residual there. The
+ /// exhaust burn adds no reading of its own but re-multiplies both, being taken over the running
+ /// total. [`FEE_ESTIMATE_ALLOWANCE`] bounds the pair.
+ ///
+ /// This is a floor, not a recommendation. Overpayment is returned to the paying vault, so a
+ /// caller with a vault to refund to loses nothing by submitting above it — and one paying purely
+ /// by stealth reveal, where the overpayment is not refundable, has reason to sit on it.
pub fn required_fees(&self) -> u64 {
- self.total_fees_charged().saturating_add(1)
+ self.total_fees_charged().saturating_add(FEE_ESTIMATE_ALLOWANCE)
}
/// The total amount of fees refunded to the respective vaults
@@ -274,13 +315,44 @@ impl FeeBreakdown {
}
}
+ /// Replaces whatever `source` has been charged so far.
+ ///
+ /// Charges accrued during execution accumulate with [`Self::add`], but the charges computed at
+ /// finalization are absolute functions of the state being persisted. They are recomputed once
+ /// that state is known, so they must be assignable rather than additive.
+ ///
+ /// A charge of zero leaves the source absent rather than recording a zero against it. The
+ /// breakdown is persisted inside every transaction receipt and rendered by every wallet, so a
+ /// source that never charged anything should not occupy a row. [`Self::get`] reads an absent
+ /// source as zero, so the two are indistinguishable to a reader.
+ pub fn set(&mut self, source: FeeSource, amount: u64) {
+ if amount == 0 {
+ self.breakdown.shift_remove(&source);
+ return;
+ }
+ match self.breakdown.entry(source) {
+ Entry::Occupied(entry) => {
+ *entry.into_mut() = amount;
+ },
+ Entry::Vacant(entry) => {
+ entry.insert(amount);
+ self.breakdown.sort_keys();
+ },
+ }
+ }
+
/// Returns an iterator over the fee breakdown in a canonical order.
pub fn iter(&self) -> impl Iterator {
self.breakdown.iter()
}
+ /// Saturating, so a breakdown that somehow exceeds `u64` reports the ceiling rather than
+ /// wrapping to a total below the charges it is made of. Individual charges are checked as they
+ /// are computed, so reaching it means something upstream already went wrong.
pub fn get_total(&self) -> u64 {
- self.breakdown.values().sum()
+ self.breakdown
+ .values()
+ .fold(0u64, |acc, amount| acc.saturating_add(*amount))
}
pub fn get(&self, source: FeeSource) -> u64 {
diff --git a/crates/engine_types/src/indexed_value.rs b/crates/engine_types/src/indexed_value.rs
index 4a714a11dd..8153cc5a3b 100644
--- a/crates/engine_types/src/indexed_value.rs
+++ b/crates/engine_types/src/indexed_value.rs
@@ -433,69 +433,78 @@ pub enum WellKnownTariValue {
impl FromTagAndValue for WellKnownTariValue {
type Error = IndexedValueError;
- fn try_from_tag_and_value(tag: u64, value: &tari_bor::Value) -> Result
+ fn try_from_tag_and_value(tag: u64, value: &tari_bor::Value) -> Result