Menu

STREAM on Dassie

STREAM is how money and data travel over Interledger: it splits a payment into small packets, encrypts what rides in them, and has the receiver say what arrived. Dassie speaks it the standard way, so the Interledger tools you know can pay a Dassie node and be paid by one.

On this page
  1. Compatibility
  2. Paying a Dassie node
  3. Where it differs
  4. The STREAM Toy

Compatibility

Dassie implements STREAM as RFC 29 describes it, in one Rust crate, dassie-stream. Everything in Dassie that speaks STREAM runs that one crate: a node's receivers, which take payments into its ledgers; its payers, which send them; the quotes that measure a route's rate; and the STREAM Toy on this site.

On the wire it matches ilp-protocol-stream, the reference implementation most Interledger software runs. Three kinds of test keep it that way:

  • Interop tests run dassie-stream against ilp-protocol-stream in both directions, as a native build and as the WebAssembly build browsers load.
  • End-to-end tests pay real Dassie nodes with ilp-protocol-stream over ilp-plugin-btp, across one node and across several.
  • Golden vectors pin the bytes of STREAM packets and their encryption.

How Dassie is tested against the reference implementations

Paying a Dassie node

You get STREAM credentials from a node through Open Payments: an incoming payment lists a method of type ilp, with an ilpAddress and a sharedSecret in base64url. Hand both to your STREAM sender.

  • One set of credentials, one payment. Each time you read an incoming payment, the node hands out fresh credentials, and each set is a STREAM connection of its own. The receiver tells each connection what it delivered, so two payments never take each other's money for their own. Do not pay twice with the same credentials: read the incoming payment again.
  • Money from the first packet. The receiver accepts money on a new stream right away. The incoming payment, not STREAM's stream limit, bounds what the connection may deliver.
  • Paid within a tolerance. Hops round down, so no sender can hit an amount to the unit. An incoming payment counts as paid once what it received is within a tiny tolerance of its amount, under or over.
  • Too much is refused and explained. A packet over what the receiver told its streams, by more than the tolerance, is refused with F99, and the reply says how much it still takes. Send that instead.
  • Full or stopped, said plainly. When nothing is left, the receiver closes the connection: with NoError when the incoming payment got all it asked for, and with ApplicationError when it was completed or closed before it got that.
  • No receipts. Dassie issues no STREAM receipts (RFC 39 is deprecated in favor of Open Payments). It still reads them, so a Dassie payer can pay a receiver that sends them.

From your own code, the shortest path is a BTP connection to a node and ilp-protocol-stream on top of it. With a BTP token from the node's owner (see Connect an ILP client over BTP) and an incoming payment's credentials, after npm install ilp-plugin-btp ilp-protocol-stream:

pay.mjs
import BtpPlugin from "ilp-plugin-btp";
import { createConnection } from "ilp-protocol-stream";

// The btp+wss:// URL of your token, and the incoming payment's ilp method.
const plugin = new BtpPlugin({ server: process.env.BTP_URL });
await plugin.connect();
const connection = await createConnection({
  plugin,
  destinationAccount: process.env.ILP_ADDRESS,
  sharedSecret: Buffer.from(process.env.SHARED_SECRET, "base64url"),
});
const stream = connection.createStream();
await stream.sendTotal(1_000_000_000); // in the ledger's smallest units
await connection.end();
await plugin.disconnect();

The script may stay alive for about 35 seconds after the payment. ilp-protocol-stream sends a last packet or two as it closes, and ilp-plugin-btp waits for answers that cannot come once it is disconnected. Exit with process.exit() once the payment is done, or answer those calls yourself, as tests/e2e/btp.ts does.

On a local dev network you can do this without touching a public network.

Where it differs

Dassie keeps to the wire format exactly. Where RFC 29 leaves room, or where ilp-protocol-stream does something that works less well, Dassie chooses on its own. None of it changes a byte another implementation reads.

Dassie Why it helps
Sends several packets at once, and adjusts how many by how they fare. ilp-protocol-stream sends one at a time. Several are faster on a slow path.
Every response, Fulfill or Reject, carries every open stream's limits and totals. The sender always knows where it stands, whatever happened to its packet.
When one stream in a packet is full, its share goes to the others. A packet fails only when all its streams together cannot take it.
Small leftover amounts wait only while other money is in flight. A payment below the usual threshold still completes.
After a too-low rate or a final error from the path, sends the money again, and stops after a long run of failures with nothing delivered. Each packet may take a different path, so one poor node does not end a payment.
Takes a receive maximum that went down, from the receiver's latest reply. A refused packet costs one round trip, and the next one fits.
Measures a rate only when asked, with exact integer amounts throughout. No rounding surprises, and no probes nobody needs.

One rule shows when a sender that is not Dassie meets a Dassie receiver. RFC 29 says the most a stream is told it may receive never goes down, and a Dassie receiver keeps to that. So when two payments race for the last part of one incoming payment, the receiver does not tell the slower one less. It closes that connection with NoError, which every sender understands, and its reply still says what arrived. The incoming payment may then stay a little short, and open for the rest.

Every difference, and the reasons for each

The codes a node or its receivers send, and what a sender does about each, are on Error codes.

The STREAM Toy

The STREAM Toy lets you watch a STREAM payment cross a network, packet by packet, in your browser. The sender and the receiver are dassie-stream's real state machines, compiled to WebAssembly, the same build the tests load. So what you see is what Dassie does, not a picture of it.

Between them sits a simulated connector. You choose:

  • The network: the largest packet the connector takes (larger ones get F08), the median round trip, how much round trips vary, and how many packets may be in flight (more get T03).
  • The payment: how much the sender sends.
  • The simulation: how fast it plays, and a seed. The same settings and seed always play the same run.

The log decrypts the STREAM packets inside each ILP packet with the connection's shared secret, so you can read every frame: the money, the limits each side announces, and the close at the end. Try a small largest packet to watch the sender learn it from F08, or few packets in flight to watch it back off.

Everything runs on the page. Nothing leaves your browser, and no money moves, test or otherwise.

A kopje of granite blocks against the sun, in veld grass; one dassie climbs its slab, one keeps lookout on top inside the sun, and one sleeps on the next rock; on a wide screen, a pair sits on a kopje to the left