Hkchain

Attach a Travel Rule payload to a transfer

Question. A user wants to send HKD to another address. Hong Kong regulation requires Travel Rule information on every transfer — no threshold. What exactly rides on the tx, how does the chain verify it, and how does the requirement shift as the amount crosses the HKD 8,000 line?

Short answer. Every HKD transfer tx carries a TravelRulePayload as a tx extension option, not as a separate message. The TravelRuleDecorator reads the payload out of tx.GetExtensionOptions() before the tx executes. Below HKD 8,000, a minimal tier — originator_ref + beneficiary_ref (IVMS101 hashes) — satisfies the check. At or above HKD 8,000, the full tier is required: additionally originator_vasp + beneficiary_vasp identifiers. Missing, under-tiered, or incomplete payloads are rejected at the AnteHandler; the tx never reaches x/bank. On pass, the payload is anchored in the TravelRuleRegistry keyed by the tx hash — the chain holds the hashes only; the full IVMS101 record stays off-chain at the originator VASP.

Why a tx extension, not a separate message

The IVMS101 payload is intrinsic to the transfer it describes — one transfer, one payload, one atomic unit. The alternatives and why they were rejected:

  • Separate MsgSubmitTravelRulePayload ahead of the transfer: creates a two-tx coupling where the second tx's compliance depends on the first's inclusion. Ordering, replay, and "orphan transfers with no payload" all become edge cases.
  • Separate payload submitted after the transfer: the transfer executes first, meaning compliance enforcement happens after value has moved. Unacceptable for zero-threshold Travel Rule.
  • Tx extension option on the transfer tx: atomic. The decorator sees the payload before the tx produces any effect; rejection means no value moves; acceptance means the payload is anchored at the same block the value moves.

The extension-option design is the consequence of Hong Kong's zero-threshold rule. There is no "low-value fast path" the chain could skip, so the payload has to be present inline on every transfer.

The flow, step by step

1. The sender builds the tx with the payload attached

A transfer-capable client (wallet, service backend, bridge) builds the transfer message and attaches TravelRulePayload to the tx's extension options:

tx {
  messages: [
    MsgSend { from: alice, to: bob, amount: [100_00 hkd.hsbc] }
  ],
  extension_options: [
    Any {
      type_url: "/hkchain.compliance.v1.TravelRulePayload",
      value: TravelRulePayload {
        originator_ref: "sha256:<hash of alice's IVMS101 record>",
        beneficiary_ref: "sha256:<hash of bob's IVMS101 record>",
        tier: "minimal"
      }
    }
  ],
  ...signatures
}

The originator_ref / beneficiary_ref are SHA-256 hashes of the parties' IVMS101 records (name, address, identifier, account). The actual records stay with the originator VASP and are shared with the beneficiary VASP through an off-chain channel (TRP, TRUST, Sygna Bridge, etc. depending on the corridor); the chain never sees the PII. tier is "minimal" or "full" — selected by the sender based on the transfer amount.

The tx_hash field on the payload is optional at submission time — the chain derives it from the raw tx bytes at AnteHandler time and uses that as the storage key.

2. The TravelRuleDecorator validates the payload

Before the message reaches its handler, the decorator:

  • Iterates the tx's transfer-like messages. Non-transfer txs (governance, compliance admin, reviewer workflow) are ignored — Travel Rule applies to value-moving flows.
  • Rejects a tx with more than one transfer message that claims a single TravelRulePayload — one payload cannot unambiguously describe multiple transfers.
  • Looks up TravelRulePayload in tx.GetExtensionOptions() by its type URL. A transfer tx without a payload is rejected: zero threshold, no exception.
  • Validates the payload against the transfer amount. See the next sections for the tier check.
  • Cross-checks against x/kyc: both VASP identifiers, when present, must equal the sender's / recipient's Identity.vasp_identifier registered at KYC onboarding; and for tier="minimal", the originator_ref / beneficiary_ref must equal the parties' Identity.ivms101_off_chain_ref. Forged institution binding or an invented minimal-tier ref is rejected at the chain, not left to off-chain audit.
  • On pass, writes the (derived-tx-hash-keyed) record into TravelRuleRegistry.

The decorator runs inside the AnteHandler; rejection means the tx never executes and no state changes. Accepted tx state changes and the registry write commit together.

3. Tier is bonded to the sender's KYC level

The tier on a payload is not a caller-chosen label. The chain reads the sender's KYC level from x/kyc.LevelDefinition.TravelRuleTier, treats that as the authoritative tier, and runs two pre-checks before any structural validation:

  1. Claim gate — payload.tier must equal the KYC-registered tier. An L2 retail_basic sender attesting "full", or an L4 institutional sender attesting "minimal", is rejected with "payload tier=… does not match sender's KYC-registered tier=…". Over-supplying tier is not allowed — the attestation has to tell the truth.
  2. Capability gate — the KYC tier (rank: full > minimal > none) must be at least what the amount demands (full at or above TravelRuleFullThreshold, else minimal). An L2 retail_basic sender can never originate a transfer at or above the threshold no matter what they attest; they'd need to upgrade to L3+ first. Rejection message: "sender KYC level=… cannot originate transfer requiring tier=…".

Only after those two gates pass does the decorator run the structural checks on payload refs + VASPs. The downstream logic keys off the KYC-authoritative tier, not payload.tier — i.e. the input field is a claim to verify, not a source of truth. The compliance model sets out why the registered tier, and never the submitted one, is what the chain acts on.

4. Structural requirements by tier

The tier applied (from KYC, per §3 above) dictates which fields must be populated:

FieldRequired at minimal tierRequired at full tier
originator_refyesyes
beneficiary_refyesyes
originator_vaspnoyes
beneficiary_vaspnoyes

Plus the minimal-tier KYC cross-check: originator_ref must equal the sender's Identity.ivms101_off_chain_ref, and beneficiary_ref must equal the recipient's. Full-tier refs are per-transaction and VASP-authored, so the chain accepts per-tx hashes that differ from the per-person KYC refs — the VASP-id binding is the integrity anchor for that tier.

TravelRuleFullThreshold lives in x/compliance.Params.Rules and is governance-managed. The default is 800,000 fen (= HKD 8,000) — matching the HKMA broader-information line exactly.

The payload's VASP identifiers are FATF-style identifiers for the originating and beneficiary institutions. They are separate from the parties' own addresses; a transfer between two customers of the same VASP still carries two identical VASP entries, so the compliance trail reflects the intermediation relationship.

4a. The EVM path — same validation, different carrier

An HKD transfer originated over JSON-RPC (MetaMask, ethers.js, any ERC-20-aware tool calling through a dApp) goes through the hkchain-extended ERC-20 precompile at the issuer denom's token-pair address (one per hkd.<issuer> denom; read it from the network's token-pair registry rather than hard-coding it). The precompile exposes the full IERC20 ABI plus two hkchain methods:

function transferWithTravelRule(address to, uint256 amount, bytes payload) external returns (bool);
function transferFromWithTravelRule(address from, address to, uint256 amount, bytes payload) external returns (bool);

Vanilla transfer(to, amount) and transferFrom(from, to, amount) are present in the ABI but unconditionally revert with "use transferWithTravelRule". There is no EVM method on HKD that moves funds without an attested Travel Rule payload.

When transferWithTravelRule is invoked, the precompile:

  • Decodes payload as a protobuf-encoded TravelRulePayload.
  • Synthesizes a banktypes.MsgSend (from = EVM caller, to = recipient, amount = amount of that issuer denom) and attaches the decoded payload as a tx extension option — the same shape a native Cosmos tx would carry.
  • Runs app.ComplianceAnteDecorators(ComplianceKeeper, KycKeeper) — the identical slice used on the cosmos-branch AnteHandler. KycGate, Sanctions, TravelRule (with the KYC cross-check above), Threshold, Velocity, RoleGate all see the EVM transfer on the same terms as a native tx.
  • On rejection, the EVM call reverts with the decorator's error message; no state changes.
  • On pass, delegates the actual balance movement to the upstream precompile's transfer logic, which routes into x/bank.

The TravelRuleRegistry anchoring (§5 below) happens identically: the tx hash used as the registry key is the Cosmos tx hash of the wrapping MsgEthereumTx, so the EVM call and its payload remain bound across both the EVM and Cosmos views of block state.

This is dual-path parity — the chain-level enforcement principle made operational: one compliance slice, two entry points, same rules.

5. Registry anchoring on pass

On successful validation, the payload is written to TravelRuleRegistry[tx_hash] where tx_hash is hex(sha256(tx_bytes)). The registry is a single record per tx — over-writing is not part of normal operation.

The registry lets compliance review after the fact reconstruct which IVMS101 record (by hash) was claimed against which transfer (by hash). A reviewer with access to both ends of the VASP channel (originator and beneficiary) can verify that the off-chain PII record hashes to exactly the anchored value — closing the loop between on-chain evidence and off-chain identity.

What the Travel Rule enforcement does not do

Verify the PII contents. The chain holds the hash, not the record. A hash anchored to the right structure is cryptographically verifiable only against the record the VASP holds. The integrity guarantee is "this hash matches this record when the record is produced" — not "this record is true." Identity attestation is a VASP responsibility under the licensing framework; the chain binds to it, it does not replace it.

Enforce cross-VASP channel existence. Whether the two VASPs involved actually have a functioning IVMS101 exchange channel is an off-chain reality. The chain's requirement is satisfied by a well-formed payload — if the beneficiary VASP cannot retrieve the off-chain record, that's a service-level failure between VASPs, not a chain-state question.

MVP: the chain accepts any well-formed payload regardless of the real-world existence of the claimed IVMS101 channel. finalProduct may layer a VASP-registry freshness check (is originator_vasp currently licensed? is the channel currently operational?) as a follow-up primitive — pending VASP-registry design, out of MVP scope.

Check identity completeness beyond the referenced fields. The decorator validates that the referenced fields are non-empty; it does not enforce that the claimed IVMS101 record has all fields HKMA expects. Record-level completeness is audited at the VASP layer, checked by KYC onboarding (x/kyc.MsgEnroll), and reconciled during regulatory inspection — not re-checked per tx.

Mutate the transferred tokens. Travel Rule is a pre-execution gate, not a post-execution modifier. A transfer that passes runs through x/bank unchanged; a transfer that fails produces no effect.

The audit trail, end to end

A complete HKD transfer produces, in one block:

  1. The tx itself — inputs, outputs, signatures, fees.
  2. TravelRuleRegistry[tx_hash] — the anchored IVMS101 hashes and tier claim.
  3. AuditEntry { tx_type: "transfer", subject: originator, counterparty: beneficiary, amount } — the compliance-log summary of the movement (stamped by the VelocityDecorator).
  4. MsgSend's standard transfer events from x/bank.

For a rejected transfer (missing or mis-tiered payload), no state changes but the rejection is observable through the chain's rejection stream with a decorator-labelled error, sufficient for supervisory review of enforcement behaviour.

Edge cases and what happens

Payload with tx_hash already set

Accepted; the payload's own tx_hash is overwritten with the derived value before the registry write. This matches the documented flexibility: the field is optional at submission, and the chain is the authoritative source of the hash.

Payload with unknown extra fields

Accepted. Proto unknown fields are preserved at the wire level but are not interpreted by the decorator. Clients may extend the payload format (within the proto's forward-compatibility rules) without breaking existing validation.

Multiple transfer messages in one tx

Rejected. A single payload cannot disambiguate which of several transfers it applies to. In practice this pattern is rare — most transfer tx have a single message — and the rejection preserves the one-payload-per-transfer invariant the registry relies on.

Non-transfer tx (governance, flag review, rule update, ...)

The decorator skips non-transfer messages entirely. These tx do not need to carry a payload; requiring one would be zero-threshold expansion into a layer the regulation does not cover.

Self-transfer (originator === beneficiary)

Accepted as long as the payload is well-formed. An address sending HKD to itself still produces a transfer event and still carries Travel Rule information — the information is trivially the same originator and beneficiary reference, which is both accurate and supervisable.

Transfers at exactly the threshold

The threshold check is inclusive: exactly HKD 8,000 requires full tier. A transfer of 7,999.99 HKD is minimal-tier-acceptable; 8,000.00 HKD and above require full.

Missing tier string

A payload with empty tier is rejected with a tier-value error. The field is a validation witness — omitting it signals "sender did not commit to a tier," which the chain does not resolve on the sender's behalf.

Upgrading from minimal to full in a sequence of transfers

Each tx is independent; the payload rides on that tx only. Two consecutive transfers between the same parties can use different tiers if their amounts straddle the threshold. There is no aggregate-tier check at the chain layer — structured transfers designed to evade the threshold are a transaction-monitoring concern handled by the flagging decorators and the reviewer workflow, not by the Travel Rule decorator.

On-chain state summary

State objectTouched on successful Travel Rule pass
x/compliance.TravelRuleRegistry[tx_hash]payload record written
x/compliance.AuditLogtransfer entry appended (velocity-layer stamp)
x/bank balancestransfer executes normally
Tx acceptancethe tx proceeds to its handler

On rejection (missing, mis-tiered, or incomplete payload), no state is touched; the tx is rejected at AnteHandler with a Travel-Rule-labelled error.

MVP vs. finalProduct gap

MVP behaviour: the caller — wallet, dApp, VASP backend — always supplies the full TravelRulePayload explicitly, both on the native Cosmos path (via the tx extension option) and on the EVM path (via transferWithTravelRule's bytes payload argument). The chain cross-checks the supplied payload against x/kyc: VASP identifiers must match the sender's / recipient's KYC-registered VASP, and minimal-tier refs must equal the KYC-registered ivms101_off_chain_ref.

finalProduct roadmap: the minimal-tier payload is entirely derivable from on-chain state — both VASP identifiers and both ivms101_off_chain_ref hashes already live in x/kyc.Identity. A later iteration can drop the caller-provided payload for tier="minimal" transfers entirely: the decorator (and/or the precompile on the EVM path) synthesizes the minimal payload from the KYC registry, anchors it into TravelRuleRegistry, and the caller only needs to attach a payload for tier="full" (≥ HKD 8,000) — where per-transaction IVMS101 fields (purpose of transfer, beneficiary account identifier, remittance info) are authored by the VASP compliance stack and genuinely cannot be derived from the registry.

Why not in MVP: auto-derivation changes the regulator-facing claim from "caller positively attests to a Travel Rule payload on every transfer" to "chain synthesizes a minimal payload from registry state on behalf of the caller." Both are defensible, but the former is a simpler story to bring to the first HKMA demo, and the cross-check already gives us forgery resistance. Collapse to auto-derive once the supervisory conversation is settled on explicit-attestation semantics.