Skip to content

Transactional Stake Holders (TSH) ​

A Transactional Stake Holder (TSH) is an account — other than the originating (sfAccount) account — that a transaction affects closely enough that the account's own installed hooks are also given a chance to run for that transaction. The canonical example is a Payment: the sender's hooks always fire (they are the originator, not a TSH), and the destination account is added as a TSH so its hooks fire too, even though the destination did not sign or submit the transaction.

Which accounts become a transaction's TSHs is fixed and deterministic: for each transaction type, Xahau derives the TSH set from the transaction's own fields (for a Payment, for instance, the sfDestination). Because every node applies the same rules, you can tell from a transaction alone whose hooks will be offered a chance to run. If hooks are not enabled on the network, or the transaction has no originating sfAccount, it has no TSHs. Every transaction type has a defined rule — see TSH by transaction type below for the full list.

Two rules govern how that set is built, and both matter when reading the table below:

  • The originating account is never its own TSH. The account that signs and submits the transaction is the originator, not a TSH; its own hooks run as the "outgoing" chain instead (see Relationship to HookOn below). If a rule below would nominate the originator, that nomination is dropped.
  • Strong wins over weak. If the same account qualifies as a TSH more than once for one transaction (for example, as both a URIToken's owner and its issuer), and one qualification is strong while another is weak, the account is treated as a strong TSH — a later weak nomination can never downgrade an account that has already been added as strong.

Strong TSH vs Weak TSH ​

Every TSH is classified as either strong or weak. The strength decides when the account's hooks run and whether they can stop the transaction.

Strong TSH. The account's hooks run before the transaction is applied to the ledger, in the same pass as the originating account's own hooks. A strong TSH's hook can rollback, and doing so rejects the whole transaction before it takes effect — so a strong TSH can veto a transaction that names it. If a strong TSH's hook chain does not accept (it rolls back or otherwise fails), the transaction is rejected for everyone.

Weak TSH. The account's hooks run only as a collect call, after the transaction has already been applied to the ledger. A weak TSH observes the transaction but cannot change its outcome — whatever a weak hook returns, the transaction stands.

An account added as strong for a transaction is run only in the strong (pre-apply) pass, and an account added as weak only in the weak (post-apply) pass; the two never mix for the same account on the same transaction.

Fees also differ by strength. A strong TSH's hook-chain fee is folded into the transaction's required fee up front, so it is paid by the originating account. A weak TSH instead pays its own hook-chain fee out of its own balance when its collect call runs (subject to the affordability check described below).

A related but distinct path: a strong hook (the originator's own, or a strong TSH's) can call hook_again() during its strong run to request one extra weak re-execution of itself after the transaction applies. This runs in the same post-apply phase as weak TSHs, but because it re-runs the same hook that asked for it — not a new account — it is not subject to the two-condition opt-in described below.

A separate, amendment-gated source of weak TSHs ​

When the featureIOUIssuerWeakTSH amendment is enabled, there is an additional source of weak TSHs on top of the per-transaction-type rules: any account whose IOU or MPT trust-line balance is changed by the transaction — regardless of transaction type — becomes a weak TSH.

Before that amendment is enabled, the accounts eligible for the weak pass are re-derived after the transaction has applied (rather than reusing the set computed before the strong pass), so that TSH rules depending on post-application ledger state resolve correctly.

Conditions for Weak TSH execution ​

Being nominated as a weak TSH is necessary but not sufficient for that account's hooks to actually run. Two independent conditions must both hold for a specific installed hook to execute during a weak (collect) pass:

  1. The account has opted in with the asfTshCollect account flag. The account turns this on with an AccountSet transaction (sfSetFlag = asfTshCollect, value 11), which sets lsfTshCollect (0x02000000) on its account root. During a collect pass, an account whose lsfTshCollect is not set has its entire hook chain skipped and is charged nothing. The account must also be able to afford the chain's fee plus its reserve out of its current balance, or its chain is skipped for lack of funds.
  2. The specific hook was installed with the hsfCOLLECT flag (0x04). Even when an account's chain is allowed to run as a collect call, any individual hook in that chain that was installed without hsfCOLLECT is skipped. The fee a weak TSH is charged counts only the hooks that carry hsfCOLLECT, so it matches exactly the set of hooks that actually run.

If either condition is not met — the account never set asfTshCollect, or the particular hook was installed without hsfCOLLECT — that hook simply does not run for that weak-TSH pass. No error is raised and no metadata is produced for the skipped hook; it is as if it were never installed for this transaction.

Worked example. Bob wants his hook to fire as a collect call whenever he is a weak TSH on someone else's Payment:

  1. Bob sends AccountSet with sfSetFlag = asfTshCollect (11) — this sets lsfTshCollect on Bob's account root.
  2. Bob installs (or updates) his hook with a SetHook hsoCREATE/hsoUPDATE entry whose sfFlags includes hsfCOLLECT (0x04), alongside whatever HookOn/HookOnIncoming bits select the transaction types he cares about — see Flags for the full hsfOVERRIDE/hsfNSDELETE/hsfCOLLECT flag reference.
  3. Now, whenever a Payment (or any other transaction type that nominates Bob as a weak TSH — see the table below) is processed, Bob's hook runs after the transaction applies, with a TxMeta reflecting the already-applied result, and cannot reject the transaction.

If Bob skips step 1, or installs the hook without hsfCOLLECT, his hook is never invoked as a TSH — only as the originating account's own hook chain when Bob himself sends a transaction.

TSH by transaction type ​

The table below gives the TSH rule for every transaction type, grouping types that share the same rule. "Strong"/"Weak" is the strength each account is added with; entries marked conditional depend on ledger state, transaction flags, or an amendment, as noted.

Transaction type(s)TSH account(s)StrengthNotes
ttCRONsfOwnerWeak
ttREMITsfDestinationStrongIf present.
sfInformWeakOnly if sfInform differs from both the sender and the destination.
Issuer of each sfURITokenIDs entryStrong if lsfBurnable set on the token, else WeakOnly if the token exists and its issuer differs from both its owner and the destination.
ttIMPORTsfIssuerWeak under fixXahauV2, Strong otherwiseOnly if sfIssuer present.
ttURITOKEN_BURNnone—If token issuer == owner, no TSH is added (already covered by the burner being the otxn account).
Under fixXahauV1: the non-burning party (issuer if owner burns, owner if issuer burns)WeakOnly added if that account exists on ledger.
Pre-fixXahauV1 (legacy): the non-burning partyStrong
ttURITOKEN_BUYCurrent ownerStrongOnly if owner differs from the buyer (sfAccount).
IssuerStrong if lsfBurnable, else WeakOnly if issuer differs from owner.
ttURITOKEN_MINTsfDestinationStrongOnly under fixXahauV2, and only if sfDestination present.
ttURITOKEN_CANCEL_SELL_OFFERURIToken's sfDestinationWeakOnly under fixXahauV2, and only if the token's sell offer carries a sfDestination.
ttURITOKEN_CREATE_SELL_OFFERIssuerStrong if lsfBurnable, else WeakOnly if issuer differs from owner.
sfDestination (of the tx)StrongOnly if present.
ttACCOUNT_SET, ttOFFER_CANCEL, ttTICKET_CREATE, ttHOOK_SET, ttOFFER_CREATEnone—Self-transactions: no TSH is ever added. OFFER_CREATE's counterparties from order-book crossing are not TSHes.
ttREGULAR_KEY_SETsfRegularKeyStrongOnly if sfRegularKey is present.
ttDEPOSIT_PREAUTHsfAuthorizeStrongOnly if sfAuthorize is present.
ttPAYMENT, ttESCROW_CREATE, ttCHECK_CREATE, ttACCOUNT_DELETE, ttPAYCHAN_CREATE, ttINVOKEsfDestinationStrongOnly if sfDestination present.
ttTRUST_SETIssuer of sfLimitAmountWeakOnly if sfLimitAmount present.
ttESCROW_CANCEL, ttESCROW_FINISHEscrow's sfAccount (source)StrongResolved via sfOwner+sfOfferSequence or (under fixXahauV1) sfEscrowID.
Escrow's sfDestinationStrong for ttESCROW_FINISH, Weak for ttESCROW_CANCELOnly if destination differs from source.
ttPAYCHAN_FUND, ttPAYCHAN_CLAIMChannel's sfAccountStrongResolved via sfChannel.
Channel's sfDestinationWeak
ttCHECK_CASH, ttCHECK_CANCELCheck's sfAccountStrongResolved via sfCheckID.
Check's sfDestinationWeak
ttSIGNER_LIST_SETEach sfSignerEntries[].sfAccountStrongLets a listed signer's hooks veto being added to the list.
ttGENESIS_MINTEach sfGenesisMints[].sfDestinationWeak
ttCLAWBACKsfHolder (for MPT amounts) or the clawed-back amount's issuer (for IOU)Weak
ttCRON_SETnone—
ttORACLE_SET, ttORACLE_DELETEnone—
ttREMARKS_SETnone—
ttAMENDMENT, ttFEE, ttUNL_MODIFY, ttEMIT_FAILURE, ttUNL_REPORTnone—Pseudo-transactions; no originating account to compare against in the usual sense.

This page's scope currently stops short of every transaction type: NFT, AMM, cross-chain bridge, DID, MPToken, credential, and permissioned-domain operations, among others, are not yet broken out in the table above. Some of these carry real TSH rules and some currently nominate no TSH at all — the TSH nomination logic in the Xahau source is authoritative for a transaction type not listed here.

Relationship to HookOn/HookOnIncoming ​

The TSH rules decide whose hooks get a chance to run for a transaction, and they also classify each account as outgoing (the originator) or incoming (a TSH). That direction selects which HookOn field applies: an outgoing account's hooks are filtered by sfHookOnOutgoing, an incoming (TSH) account's by sfHookOnIncoming. If the applicable field does not select this transaction's type, the hook is skipped.

HookOn (or its HookOnIncoming/HookOnOutgoing split under featureHookOnV2_1) then decides whether a specific installed hook fires for this transaction's type, independent of TSH strength. A hook on a strong TSH account whose HookOnIncoming excludes this transaction type never runs, even though its account was nominated as a strong TSH; conversely hsfCOLLECT (see Conditions for Weak TSH execution) only matters once HookOn/HookOnIncoming has already allowed the hook to be considered. See HookOn and HookOnIncoming/Outgoing for the bit-selection rules themselves.