|
| 1 | +# Tx Centrifuge & Pull-Fiction |
| 2 | + |
| 3 | +`tx-centrifuge` is a high-performance load generator for Cardano, built on top of the protocol-agnostic **Pull-Fiction** library. |
| 4 | + |
| 5 | +Unlike traditional load generators that "push" data at a fixed rate, this system is designed for **pull-based protocols**. It does not generate load by itself; instead, it acts as a **policer** that reacts to requests from downstream consumers, admitting or delaying them to enforce a configured rate ceiling. |
| 6 | + |
| 7 | +### Minimal Configuration Example |
| 8 | + |
| 9 | +A basic configuration defines how to load initial resources, how to build payloads, the desired rate, and where to send the results: |
| 10 | + |
| 11 | +```json |
| 12 | +{ |
| 13 | + "initial_inputs": { |
| 14 | + "type": "genesis_utxo_keys", |
| 15 | + "params": { |
| 16 | + "network_magic": 42, |
| 17 | + "signing_keys_file": "funds.json" |
| 18 | + } |
| 19 | + }, |
| 20 | + "builder": { "type": "value", "params": { "fee": 1000000 } }, |
| 21 | + "rate_limit": { "type": "token_bucket", "params": { "tps": 10 } }, |
| 22 | + "workloads": { |
| 23 | + "group-A": { |
| 24 | + "targets": { |
| 25 | + "node-1": { "addr": "127.0.0.1", "port": 30000 }, |
| 26 | + "node-2": { "addr": "127.0.0.1", "port": 30001 } |
| 27 | + } |
| 28 | + } |
| 29 | + }, |
| 30 | + "nodeConfig": "config.json", |
| 31 | + "protocolParametersFile": "pp.json" |
| 32 | +} |
| 33 | +``` |
| 34 | + |
| 35 | +## Core Concepts: The Pull-Fiction Engine |
| 36 | + |
| 37 | +The underlying `pull-fiction` library implements a reactive rate-limiting strategy. It only produces data when a consumer asks for it, and only as fast as the rate limiter allows. |
| 38 | + |
| 39 | +### Reactive Rate Limiting |
| 40 | +- **Downstream Driven**: Load is only dispensed in response to an explicit pull from a target. If the target doesn't ask, the engine stays idle. |
| 41 | +- **Ceiling Enforcement**: The rate limiter enforces a tokens-per-second (TPS) ceiling. Even if a consumer pulls aggressively, the engine ensures the dispensed items never exceed the configured limit. |
| 42 | +- **Fairness**: Token slots are claimed in a single atomic STM transaction, providing FIFO-fair scheduling across multiple workers sharing the same limiter. |
| 43 | + |
| 44 | +### Workloads and Targets |
| 45 | +The configuration is organized into a hierarchy that defines the concurrency model: |
| 46 | + |
| 47 | +- **Target**: A single network endpoint (e.g., a Cardano node). Each target has a dedicated **Worker thread** that manages the network connection and handles requests. |
| 48 | +- **Workload**: A logical grouping of targets. |
| 49 | + - All targets within a workload share the same **Builder thread** and the same **Payload Queue**. |
| 50 | + - **Transaction Profiles**: Each workload can define its own `builder` configuration. This allows you to generate different "profiles" of transactions (e.g., different sizes, complexities, or fees) for different groups of nodes. |
| 51 | + - **Isolation**: By using multiple workloads, you can isolate different groups of targets. For example, one workload could simulate high-volume "small" transactions for one group of nodes, while another generates "heavy" transactions for another. |
| 52 | + |
| 53 | +### Pipeline Architecture |
| 54 | +The engine operates as a decoupled production pipeline using generic `input` and `payload` types: |
| 55 | +1. **Initial Inputs**: Starting resources (of type `input`) are partitioned across workloads. |
| 56 | +2. **Input Queue (Unbounded)**: Holds available `input` items. |
| 57 | +3. **Builder (One per Workload)**: A dedicated thread that pulls `input`s, produces a `payload`, and pairs it with any `[input]`s to be recycled. It pushes the `(payload, [input])` pair to the payload queue. |
| 58 | +4. **Payload Queue (Bounded)**: The sole source of **backpressure**. The builder blocks here if consumers are slower than the production rate. |
| 59 | +5. **Workers (One per Target)**: Threads that manage the consumer connection. They pull from the payload queue via a rate-limited fetcher. |
| 60 | + |
| 61 | +### Resource Recycling |
| 62 | +To enable indefinite-duration runs with finite resources, inputs must be returned to the `Input Queue`. There are two main patterns: |
| 63 | + |
| 64 | +1. **Optimistic Recycling (Builder-level)**: The builder immediately returns resources to the `Input Queue` as soon as the payload is constructed. This is the highest-throughput mode but assumes the payload will be successfully processed downstream. |
| 65 | +2. **Standard Recycling (On-Fetch)**: The builder pairs the payload with the resources to be **recycled**. The library then automatically returns those resources to the `Input Queue` as soon as a worker **fetches** the payload from the internal queue to deliver it to a target. |
| 66 | + |
| 67 | +## Configuration |
| 68 | + |
| 69 | +### Initial Inputs (`initial_inputs`) |
| 70 | +The generator requires a set of initial UTxOs, configured in the `initial_inputs` section of the main configuration file. |
| 71 | + |
| 72 | +- **`type`**: The input loader variant (e.g., `"genesis_utxo_keys"`). |
| 73 | +- **`params`**: |
| 74 | +- - **`network_magic`**: Required for deriving UTxO references from keys (e.g., `42` for testnet). |
| 75 | +- - **`signing_keys_file`**: Path to a JSON file (e.g., `funds.json`) containing the actual fund data. |
| 76 | + |
| 77 | +#### `funds.json` entry types |
| 78 | +The file contains an array of fund objects. There are two distinct types: |
| 79 | + |
| 80 | +1. **Genesis Funds** (Key-only): Identified only by their signing key. The `TxIn` is derived automatically. |
| 81 | + ```json |
| 82 | + { "signing_key": "genesis.skey", "value": 1500000000000 } |
| 83 | + ``` |
| 84 | +2. **Payment Funds** (Explicit UTxO): Requires a specific transaction reference. |
| 85 | + ```json |
| 86 | + { "signing_key": "payment.skey", "value": 1000000, "tx_in": "df6...#0" } |
| 87 | + ``` |
| 88 | + |
| 89 | +**Design Note**: The `funds.json` format is designed to be compatible with the output of `cardano-cli conway create-testnet-data --utxo-keys`. This allows you to immediately use an arbitrary large set of Shelley genesis keys created during testnet bootstrapping as the initial fund pool for the generator, without needing to manually create UTxOs once the network is live. |
| 90 | + |
| 91 | +### Rate Limiting Scopes |
| 92 | +The `rate_limit` scope determines the granularity of the TPS ceiling: |
| 93 | +- **`shared`**: A single global rate limiter is shared by all targets across all workloads. |
| 94 | +- **`per_workload`**: Each workload gets its own independent rate limiter (shared by its targets). |
| 95 | +- **`per_target`**: Every connection to a target gets its own independent rate limiter at the full configured TPS. |
| 96 | + |
| 97 | +### Batching and Flow Control |
| 98 | +- **`max_batch_size`**: Limits the number of items (e.g., transactions) the generator will announce to a target in a single protocol request. |
| 99 | + - This acts as a safety cap: even if a target's protocol allows for 500 items, a `max_batch_size` of 100 ensures the generator doesn't commit too much capacity to a single connection at once. |
| 100 | + - This helps distribute the available "payload queue" more evenly across multiple targets and prevents a single aggressive node from starving others. |
| 101 | +- **`on_exhaustion`**: |
| 102 | + - `block`: The worker thread waits until the builder produces a new payload. |
| 103 | + - `error`: The generator fails immediately if the builder cannot keep up with the requested TPS. |
| 104 | + |
| 105 | +## Cardano Implementation (`tx-centrifuge`) |
| 106 | + |
| 107 | +### Value Builder Parameters |
| 108 | +These parameters define the **transaction profile** for a workload: |
| 109 | +- `inputs_per_tx` / `outputs_per_tx`: Controls the transaction structure (size and complexity). |
| 110 | +- `fee`: Fixed Lovelace fee per transaction. |
| 111 | +- `optimistic_recycle`: |
| 112 | + - `true`: Output UTxOs are recycled immediately by the builder. |
| 113 | + - `false`: Output UTxOs are recycled by the worker as soon as the transaction is announced to the node. |
| 114 | + |
| 115 | +## Usage |
| 116 | + |
| 117 | +```bash |
| 118 | +tx-centrifuge config.json |
| 119 | +``` |
| 120 | + |
| 121 | +## Detailed Examples |
| 122 | + |
| 123 | +### 1. High-Throughput (Optimistic Recycling) |
| 124 | +Optimized for maximum TPS using simple 1-in/1-out transactions. |
| 125 | + |
| 126 | +**`config.json` snippet:** |
| 127 | +```json |
| 128 | +{ |
| 129 | + "initial_inputs": { |
| 130 | + "type": "genesis_utxo_keys", |
| 131 | + "params": { |
| 132 | + "network_magic": 42, |
| 133 | + "signing_keys_file": "funds.1.json" |
| 134 | + } |
| 135 | + }, |
| 136 | + "builder": { |
| 137 | + "type": "value", |
| 138 | + "params": { |
| 139 | + "inputs_per_tx": 1, |
| 140 | + "outputs_per_tx": 1, |
| 141 | + "fee": 1000000, |
| 142 | + "optimistic_recycle": true |
| 143 | + } |
| 144 | + }, |
| 145 | + "rate_limit": { |
| 146 | + "type": "token_bucket", |
| 147 | + "scope": "shared", |
| 148 | + "params": { "tps": 1000 } |
| 149 | + }, |
| 150 | + "workloads": { |
| 151 | + "simulation": { |
| 152 | + "targets": { |
| 153 | + "node-0": { "addr": "127.0.0.1", "port": 30000 } |
| 154 | + } |
| 155 | + } |
| 156 | + }, |
| 157 | + "nodeConfig": "config.json", |
| 158 | + "protocolParametersFile": "pp.json" |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +**`funds.1.json` snippet:** |
| 163 | +```json |
| 164 | +[ |
| 165 | + {"signing_key": "utxo1.skey", "value": 1500000000000}, |
| 166 | + {"signing_key": "utxo2.skey", "value": 1500000000000} |
| 167 | +] |
| 168 | +``` |
| 169 | + |
| 170 | +### 2. Large Transactions (Target-Specific Limits) |
| 171 | +Uses complex transactions with independent rate limits for each target connection. |
| 172 | + |
| 173 | +**`config.json` snippet:** |
| 174 | +```json |
| 175 | +{ |
| 176 | + "initial_inputs": { |
| 177 | + "type": "genesis_utxo_keys", |
| 178 | + "params": { |
| 179 | + "network_magic": 42, |
| 180 | + "signing_keys_file": "funds.2.json" |
| 181 | + } |
| 182 | + }, |
| 183 | + "builder": { |
| 184 | + "type": "value", |
| 185 | + "params": { |
| 186 | + "inputs_per_tx": 5, |
| 187 | + "outputs_per_tx": 5, |
| 188 | + "fee": 2000000, |
| 189 | + "optimistic_recycle": false |
| 190 | + } |
| 191 | + }, |
| 192 | + "rate_limit": { |
| 193 | + "type": "token_bucket", |
| 194 | + "scope": "per_target", |
| 195 | + "params": { "tps": 5 } |
| 196 | + }, |
| 197 | + "max_batch_size": 50, |
| 198 | + "on_exhaustion": "block", |
| 199 | + "workloads": { |
| 200 | + "heavy-load": { |
| 201 | + "targets": { |
| 202 | + "edge-node": { "addr": "192.168.1.10", "port": 30001 } |
| 203 | + } |
| 204 | + } |
| 205 | + } |
| 206 | +} |
| 207 | +``` |
| 208 | + |
| 209 | +**`funds.2.json` snippet:** |
| 210 | +```json |
| 211 | +[ |
| 212 | + {"signing_key": "utxo1.skey", "value": 1000000000}, |
| 213 | + {"signing_key": "utxo2.skey", "value": 1000000000}, |
| 214 | + {"signing_key": "utxo3.skey", "value": 1000000000} |
| 215 | +] |
| 216 | +``` |
0 commit comments