Build a static, interactive, scrollytelling explainer titled “The Anatomy of SATP” that opens up the Secure Asset Transfer Protocol as implemented in Hyperledger Cacti, step by step:
client app -> gateway -> proposal -> commencement -> lock -> commit -> finalisation
Primary audience: software developers who know roughly what a blockchain is and want a deep, visual, step-by-step understanding of how two gateways move an asset across two ledgers that share no consensus.
The site must feel like a polished interactive science explainer — not a dashboard, not a blog post, not a slide deck, not API documentation.
Reference for tone, structure and interaction design: https://www.royvanrijn.com/anatomy-of-an-llm/ (source: https://github.com/royvanrijn/anatomy-of-an-llm — read its AGENTS.md, docs/visual-style.md and docs/chapter-writing-style.md for the design philosophy, but do not copy its stack; see the constraints below.)
assets/data/. All example messages are hardcoded dummy data and must be visibly labelled as illustrative, exactly as the reference project labels toy data.file:// (inline JSON fallback if fetch fails). JavaScript is allowed and expected for progressive enhancement, but the static HTML remains the baseline experience.index.html remains a standalone file, mirror the shared navigation, recent-post links and credits with static relative links, then keep them in sync with _includes/navbar.html and _includes/footer.html.satp-demo/. The initial scaffold and this prompt are the only exceptions made before implementation begins.rafaelapb/rafaelapb.github.io, an existing Jekyll site with _config.yml, shared layouts, and root-level static pages. The SATP implementation being explained remains Hyperledger Cacti’s packages/cactus-plugin-satp-hermes in the separate hyperledger-cacti/cacti repository.satp-demo/, served at /satp-demo/. Use a plain index.html with no front matter so Jekyll copies it unchanged and the same file opens cleanly over file://. Use relative asset URLs._config.yml, shared layouts, global assets, or generated _site/ files. The demo owns all of its HTML, CSS, JavaScript, JSON, and SVG.feat: add interactive SATP demo for the final change unless the maintainer requests another title.Source of truth for stages, steps and ordering is the upstream hyperledger-cacti/cacti repository. Read these files from GitHub main before implementation. Do not invent steps or rename tags.
| File | What it gives you |
|---|---|
packages/cactus-plugin-satp-hermes/src/main/typescript/core/satp-protocol-map.ts | Canonical SATP_PROTOCOL_MAP: stage → ordered steps → tag, description, role, sequence, MessageType |
.../src/main/proto/cacti/satp/v02/common/message.proto | CommonSatp, TransferClaims, NetworkCapabilities, Asset, NetworkId, and all enums |
.../src/main/proto/cacti/satp/v02/service/stage_0.proto … stage_3.proto | Per-stage request/response messages and gRPC services |
.../core/stage-services/client/stage1-client-service.ts | How Stage 1 client messages are built, signed, hashed, logged |
.../core/stage-services/server/stage1-server-service.ts | How Stage 1 messages are validated and answered |
.../core/stage-handlers/stage1-handler.ts | Handler orchestration and adapter hook points |
.../core/session-utils.ts | saveMessageInSessionData, SessionData.satpMessages.stageN |
.../docs/api3-adapter-spec.md, package README.md | Adapter/webhook hook points per stage+step |
External references:
@url at the top of satp-protocol-map.ts).Stage 0 — Transfer Initiation and Negotiation (explicitly experimental; not in draft v2; subject to change) newSessionRequest C·18 → checkNewSessionRequest S → newSessionResponse S·19 → checkNewSessionResponse C → preSATPTransferRequest C·20 → checkPreSATPTransferRequest S → preSATPTransferResponse S·21 → checkPreSATPTransferResponse C
Stage 1 — Transfer Initiation and Commencement Flows transferProposalRequest C·INIT_PROPOSAL=6 → checkTransferProposalRequestMessage S → transferProposalResponse S·INIT_RECEIPT=7 / INIT_REJECT=8 → checkTransferProposalResponse C → transferCommenceRequest C·9 → checkTransferCommenceRequestMessage S → transferCommenceResponse S·10 (ACK-Commence) → checkTransferCommenceResponse C (implemented in Stage2ClientService)
Stage 2 — Asset Locking and Escrow lockAsset C (ledger operation, no wire message) → lockAssertionRequest C·11 → checkLockAssertionRequest S → lockAssertionResponse S·12 → checkLockAssertionResponse C (implemented in Stage3ClientService)
Stage 3 — Commitment and Finalization commitPreparation C·13 → checkCommitPreparationRequest S → mintAsset S (ledger op, destination) → commitReadyResponse S·14 → checkCommitPreparationResponse C → burnAsset C (ledger op, source) → commitFinalAssertion C·15 → checkCommitFinalAssertionRequest S → assignAsset S (ledger op, destination) → commitFinalAcknowledgementReceiptResponse S·16 → checkCommitFinalAssertionResponse C → transferComplete C·17 → checkTransferCompleteRequest S → transferCompleteResponse S·22 → checkTransferCompleteResponse C
Stage 1 wire formats (stage_1.proto; service SatpStage1Service with RPCs TransferProposal, TransferCommence):
TransferProposalRequest { common, transfer_init_claims, transfer_init_claims_format,
network_capabilities, multiple_claims_allowed,
multiple_cancels_allowed, client_signature }
TransferProposalResponse { common, hash_transfer_init_claims, transfer_counter_claims,
timestamp, server_signature }
TransferCommenceRequest { common, hash_transfer_init_claims, client_transfer_number,
client_signature }
TransferCommenceResponse { common, server_transfer_number, server_signature }
CommonSatp: version, message_type, session_id, transfer_context_id, sequence_number, resource_url, action_response, credential_block, payload_profile, payload, payload_hash, client_gateway_pubkey, server_gateway_pubkey, hash_previous_message, error, error_code
TransferClaims: digital_asset_id, asset_profile_id, verified_originator_entity_id, verified_beneficiary_entity_id, originator_pubkey, beneficiary_pubkey, sender_gateway_network_id, recipient_gateway_network_id, client_gateway_pubkey, server_gateway_pubkey, sender_gateway_owner_id, receiver_gateway_owner_id, max_retries, max_timeout, amount_from_originator, amount_to_beneficiary, process_policies[], merge_policies[]
NetworkCapabilities: sender_gateway_network_id, signature_algorithm, supported_signature_algorithms[], lock_type, lock_expiration_time, permissions, developer_urn, credential_profile, application_profile, logging_profile, access_control_profile, subsequent_calls, history[]
Enums to render symbolically and numerically: MessageType (0–22), LockType, SignatureAlgorithm, CredentialProfile, ClaimFormat, Error (34 codes), ERCTokenStandard, TokenType.
Per-message lifecycle inside a gateway — client side: build message → sign(Signer, safeStableStringify(msg)) → saveSignature → saveHash → saveTimestamp(PROCESSED) → dbLogger.persistLogEntry({sessionId, type, operation, data, sequenceNumber}) → send. Server side: adapter hook before → check*Message() (including checkNetworkCapabilities, checkTransferClaims, DLT support check against bridgeManagerClient.getAvailableEndPoints()) → saveMessageInSessionData → build and sign response → adapter hook after. Rejection sets session state REJECTED or CONDITIONAL_REJECTED.
The demo’s overall visual language must be recognisably similar to The Anatomy of an LLM: match its editorial pacing, typographic hierarchy, paper-like surfaces, generous whitespace and sticky prose-with-figure rhythm. Adapt that language to SATP and the Cacti palette; do not copy its implementation or visual assets.
Adopt the reference project’s “Light Organic Editorial” direction, re-tinted for Cacti. All values as CSS custom properties in one :root block.
Character — light paper-like background (not pure white), soft card edges, subtle texture gradients, serif-forward display headings with clean sans body, restrained accent usage for meaning not decoration, generous vertical rhythm.
Avoid — neon/cyberpunk visual language, dashboard-heavy UI, glowing effects, over-styled gradients, gradient text on headings.
Palette — page #FBFAF5, deep section tint #F3F0E6, base surface #FFFDF7, active surface #F4EFE2, primary text #171717, secondary #3F3F46, muted #71717A, border rgba(39,39,42,0.14). Also ship a “Night Paper” dark variant (warm dark ink #17191D with parchment-tinted cards, muted highlights, no neon) under prefers-color-scheme: dark plus a manual toggle.
Semantic accents — one colour per concept, used consistently everywhere: Stage 0 negotiation, Stage 1 agreement, Stage 2 locking, Stage 3 commitment; plus client-gateway vs server-gateway role, and source-network vs destination-network. Accents mark active traces, selected states and concept mappings only — never large background fills.
Typography — display: "Fraunces", "Iowan Old Style", Georgia, serif; body/UI: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif; eyebrow text in uppercase body font with letter spacing. Body line length 55–75 characters. Use only system/webfont-free fallbacks — no font CDN.
Layout — sections are narrative beats, not app panels. Each chapter section targets ≥1 viewport height. Sticky visual region beside scrolling prose. Navigation jumps to real section anchors. Motion explains state change and is never decorative filler; transitions subtle and short; prefers-reduced-motion: reduce disables all non-essential motion while preserving comprehension.
Do not build each chapter as a one-off widget. Implement a small vanilla-JS component library in assets/js/primitives/, each a factory returning { mount, update, reset, destroy }:
| Primitive | Purpose |
|---|---|
ChapterShell | Section wrapper: eyebrow, title, subtitle, prose column, figure slot |
StickyVisual | Sticky figure that stays in view while adjacent prose scrolls |
StepperControls | Play / pause / prev / next / restart / speed, keyboard + ARIA |
LaneDiagram | The SVG actor-lane canvas: actors, lanes, arrows, ledger bands |
PacketGlyph | An animated message travelling along an arrow |
LedgerBand | A chain band with block glyphs and an asset-state badge |
ActorNode | Client app / gateway / server app node with icon, label, role tint |
StepList | Ordered, clickable, highlight-current list of protocol steps |
MessageCard | Collapsible, annotated, syntax-highlighted JSON message view |
FieldAnnotation | Hover/focus tooltip: field name, proto type, plain-language meaning |
SourceChip | Inline monospace link to an exact file/line on GitHub main |
ToyDataBanner | Persistent “these values are illustrative” label |
Shared design tokens: consistent border, radius, shadow, text scale and spacing ramps. Visuals must teach, not decorate.
Every chapter opens in this exact order, before any control or piece of jargon appears:
Language rules: no implementation detail in the first line (never open with INIT_PROPOSAL or CommonSatp); subtitles concept-first and human-readable; short sentences; concrete examples. Bad: “How TransferProposalRequest carries TransferClaims and NetworkCapabilities.” Better: “How two gateways agree on the terms of a transfer before anything moves.” Then later: “This agreement travels in a message called TransferProposalRequest.”
Before Chapter 1, include a site introduction (not a chapter, not in chapter nav) that explains what the page is for, how to read and navigate it, and sets the expectation: from an application asking for a transfer to an asset existing on another chain.
satp-demo/
index.html # standalone semantic HTML, prose, mount points
README.md # how to preview, where data comes from, sync rules
ATTRIBUTION.md # every icon, its source project and licence
scratch.html # primitive harness, removed or retained as dev aid
assets/css/satp-tutorial.css # tokens + all styling
assets/js/satp-tutorial.js # bootstrap, nav, theme, scroll observer
assets/js/primitives/*.js # shared component library (section 6)
assets/js/chapter-02-stages.js # Chapter 2 controller
assets/js/chapter-03-messages.js # Chapter 3 controller
assets/data/satp-stages.json # mirror of SATP_PROTOCOL_MAP
assets/data/stage1-messages.json # hardcoded Stage 1 example messages
assets/data/field-annotations.json
assets/img/icons.svg # local sprite of <symbol> icons
plans/progress.md # milestone tracker
assets/data/satp-stages.json is a hand-maintained transcription of SATP_PROTOCOL_MAP. For each step record tag, description, role, sequence, messageType (numeric) and messageTypeName (symbolic), plus a derived kind:
"validation" — tag starts with check"ledger" — no messageType (lockAsset, mintAsset, burnAsset, assignAsset)"message" — everything elseBecause standard JSON does not allow comments, put a top-level _meta object in that file stating that it mirrors core/satp-protocol-map.ts, naming the upstream URL, and requiring both to be updated together. State the same rule in README.md.
Use Tabler Icons (MIT — https://github.com/tabler/tabler-icons/blob/main/LICENSE), copied as raw SVG paths into a single local sprite assets/img/icons.svg as <symbol id="icon-...">, referenced with <use href="assets/img/icons.svg#icon-gateway">. No CDN, no runtime third-party fetch. Fall back to Lucide (MIT) for any missing glyph.
| Concept | Tabler icon |
|---|---|
| Gateway | gateway / router |
| Blockchain / ledger | box, cube, boxes (chained) |
| Asset / token | coin, wallet |
| Client application | app-window, browser |
| Message / packet | mail, package, mail-forward |
| Lock / mint / burn / assign | lock, circle-plus, flame, arrow-right-circle |
| Signature / hash / key | signature, hash, key |
| Accept / reject | check, x |
Record every icon used, with source set and licence, in ATTRIBUTION.md. Do not assume Cacti or Hyperledger marks exist locally; any copied upstream mark must be stored under satp-demo/assets/img/ and have its exact source and licence recorded.
6–9 substantive paragraphs following the opening pattern of section 7, covering:
SATPSession: session id, transfer_context_id, monotonic sequence_number, separate client- and server-side session data, messages filed under SessionData.satpMessages.stageN.lockAsset, mintAsset, burnAsset, assignAsset as the four ledger primitives; the Asset / NetworkId / TokenType / ERCTokenStandard model.safeStableStringify), signature, hash, timestamp, durable log entry — all before the message is sent. hash_previous_message chains messages so neither party can rewrite history; the log is what makes crash recovery possible.before/during/after/rollback at any stage+step — e.g. pausing at checkTransferProposalRequestMessage for a compliance decision.Every technical claim carries an inline SourceChip linking to the exact file and, where stable, line range on GitHub main.
Close the chapter with a reference table of all stages and steps, built at load time from satp-stages.json: Stage · Seq · Step tag · Role · Kind · MessageType · Description, with a text filter and role/kind toggles. It must render as a plain HTML table without JS.
A full-bleed StickyVisual containing a LaneDiagram, driven by satp-stages.json.
Actors, top to bottom:
Client Application — icon app-windowGateway 1 — client role — icon gatewayGateway 2 — server role — icon gatewayServer Application — icon app-windowPlus two LedgerBands: Network A (source) and Network B (destination), each with chained block glyphs and an asset badge whose state label updates through the transfer — A: available → locked → burned; B: — → minted → assigned.
The client application initiating the transfer into Gateway 1 and the server application being settled by Gateway 2 must both be visible. This is a four-party picture, not a two-gateway picture. Model the geometry on the IETF flow diagram.
Behaviour
0 | 1 | 2 | 3 plus an “All stages” mode.kind: "message" steps animate a PacketGlyph along the arrow, in protocol order, tinted by stage and direction.kind: "validation" steps pulse inside the receiving gateway node with a check badge — making explicit that validation happens at the receiver, not on the wire.kind: "ledger" steps descend from the acting gateway into its LedgerBand with the matching icon and update the asset badge. Stage 2 shows lockAsset on Network A. Stage 3 shows mintAsset (B), then burnAsset (A), then assignAsset (B), in order.StepperControls: play/pause, step back/forward, restart, speed 0.5× / 1× / 2×.StepList beside the diagram highlights the current step and is clickable to jump.SourceChip to the implementing service file.aria-live region announces the current step.A message explorer covering Stage 1 only — four wire messages: TransferProposalRequest (INIT_PROPOSAL=6), TransferProposalResponse (INIT_RECEIPT=7, with the INIT_REJECT=8 variant behind a toggle), TransferCommenceRequest (=9), TransferCommenceResponse (ACK-Commence, =10).
Behaviour
LaneDiagram at the top, the four exchanges as clickable arrows, with the interleaved check* steps shown as receiver-side badges.MessageCard: the hardcoded example message as pretty-printed, syntax-highlighted JSON. Field names match the proto exactly (snake_case), including nested common (CommonSatp), transfer_init_claims / transfer_counter_claims (TransferClaims) and network_capabilities (NetworkCapabilities). Nested objects collapse. Enums render as LOCK_TYPE_HASHLOCKTIME (4) — symbolic name plus numeric value.FieldAnnotation on hover/focus for every field of the four messages and the three nested types: proto type plus a plain-language explanation of what it is for and who sets it. Author these in field-annotations.json. This is the pedagogical core of the chapter — do not stub it.hash_previous_message in each message and draws connectors back to the message it commits to, plus payload_hash and hash_transfer_init_claims, showing how Stage 1 becomes tamper-evident.client_signature / server_signature and shows the signing pipeline (safeStableStringify → sign → saveSignature → saveHash → saveTimestamp → persistLogEntry) with SourceChips to each function.error, error_code (ERROR_...) and session state REJECTED / CONDITIONAL_REJECTED, pointing at checkTransferProposalRequestMessage.MessageCard footer carries three SourceChips: the proto definition, the client-side builder function, the server-side check* validator.Dummy data — one coherent fictional scenario reused across the whole page: 100 units of an ERC-20 token moving from a Hyperledger Besu network (“Network A”) to a Hyperledger Fabric network (“Network B”). A UUID-shaped session id, plausible hex pubkeys and signatures shown truncated with an ellipsis, sequence_number incrementing correctly across the four messages, and hash_previous_message values that are internally consistent between messages. A ToyDataBanner must be visible stating these values are illustrative and were not produced by a real gateway.
Where Cacti’s proto has an applicable field, seed the scenario with values from the IETF draft’s examples in Sections 8.1–8.7: session id d66a567c-11f2-4729-a0e9-17ce1faf47c1, transfer context id 89e04e71-bba2-4363-933c-262f42ec07a0, digital asset id 2c949e3c-5edb-4a2c-9ef4-20de64b9960d, asset profile id 38561, Alice and Bob distinguished names, the published example public keys, ES256, HASH_TIME_LOCK, a 120-second lock, TLS_AES_128_GCM_SHA256, timestamp 2024-10-03T12:02+00Z, and the published Stage 1 hashes. Adapt names and nesting to the exact Cacti v02 proto fields; the draft’s camelCase JSON examples are explanatory spec objects, not Cacti wire dumps. Preserve the draft values where semantics align, add the 100-unit Besu-to-Fabric context only in Cacti fields that support it, and never invent a mapping merely to retain a spec field. Document each adaptation in stage1-messages.json under _meta.specAdaptations.
Work one milestone at a time. Create and maintain satp-demo/plans/progress.md. After each milestone, update the plan and report changed files, verification performed, and open questions. Keep diffs focused. Stop after the requested milestone.
satp-stages.json transcription + Chapter 1 prose and reference table.satp-protocol-map.ts exactly.A task is not complete if the build passes but the UI is broken at runtime. Report: pages checked, interactions performed, console errors found (or none).
Live gateway connections, real transactions, wallet integration, user accounts, backend, database, analytics, authentication, crash-recovery deep dive, Stage 2/3 message-level detail (Chapter 3 is Stage 1 only), Weaver, or any other Cacti plugin.
One pull request titled feat: add interactive SATP demo, with a description walking through each chapter, listing the shared primitives, stating that all displayed data is illustrative, and noting the sync requirement between assets/data/satp-stages.json and core/satp-protocol-map.ts.