worm-blossom made this. come see what else we make!

Willow Transfer Protocol

Status: Proposal (as of 16.10.2026)

The Willow data model specifies how to arrange data, but it does not prescribe how peers can exchange Entries and their Payloads. The Willow Transfer Protocol fills that gap.

This document assumes familiarity with the Willow data model.

Introduction

The WTP is a message-based protocol where two peer can send and request Entries and Payloads to and from each other. Requests can be both one-shot or stay open, allowing for eager streaming of changes if desired.

In addition to requesting individual Entries, peers can also query for whole Areas or AreaSlices. Peers can further request that requests for groupings containing many Entries are answered not with those Entries but merely with a hash of those Entries — a mechanism that enables region-based set reconciliation.

Requests for Entries must be accompanied by read capabilities, peers should never send data to peers who are not authorised to access that data. Metadata, such as capabilities themselves, or requests for specific groupings of Entries are transmitted naively, however; peers must be mindful of possibly leaking information this way.

The WTP is flexible in how much state peers need to maintain. For every message that acts as a response which refers back to the state set up by some request, there is also an alternate encoding in which the response is fully self-contained. Peers can choose for themselves whether to operate in a bandwidth-efficient stateful way or in a more bandwidth-costly but stateless way.

The WTP runs over any reliable, ordered, bidirectional, byte-oriented communication channel. We highly recommend encrypting the communication, and provide a recommended scheme for doing so.

Parameters

See Willow’25 for a default recommendation of parameters.The WTP is generic over specific cryptographic primitives. In order to use it, one must first specify a full suite of instantiations of the parameters of the core Willow data model. The hash_payload function must be a member of the Bab family of hash functions. Additionally, the WTP requires the following parameters:

A type ReadCapability of read capabilities,We recommend the Meadowcap McCapabilities with an access mode of read as the type of ReadCapabilities. a type Receiver of receivers, and the type Signature of signatures issued by the Receiver.

A type Fingerprint of Fingerprints (i.e., of hashes of LengthyAuthorisedEntries), and a hash function hash_lengthy_authorised_entries from finite sets of LengthyAuthorisedEntries to Fingerprint.

Protocol

The WTP starts with the handshake specified here, whose parameter instantiation must use Receiver as the type PublicKey. After the handshake, further communication may or may not be encrypted as specified; peers should encrypt if and only if the underlying communication channel is unencrypted. All communication after the handshake is message-based.

Peers might receive invalid messages, both syntactically (i.e., invalid encodings) and semantically (i.e., logically inconsistent messages). In both cases, the peer to detect this behaviour must abort the communication session. We indicate such situations by writing that something “is an error”. Whenever we state that a message must fulfil some criteria, but a peer receives a message that does not fulfil these criteria, that is an error.

Messages

We define all messages as purely logical data types first, with the actual wire encoding defined last.

The WTP uses unsigned 64-bit WtpMessageIds to identify messages. They are, however, optional on a per-message basis. Each message indicates whether it has a WtpMessageId or not. A message without a WtpMessageId cannot be referenced by any other messages. This means that the peers need not maintain any state for such a message.

For messages that do have a WtpMessageId, these WtpMessageIds are assigned implicitly: each peer maintains an unsigned 64-bit counter, initialised to zero. When sending a message that indicates that it has a WtpMessageId, the current value of the counter becomes its WtpMessageId, and then the counter is increased by one. If the counter would overflow11This will never happen in practice. is a pretty large number., it is set to zero again.

Some message types never have a WtpMessageId; these are exactly the message types that do not have a boolean flag whether the message should be assigned a WtpMessageId or not.

The different message kinds are the following:

RequestEntries

The RequestEntries messages let peers request Entries in a grouping (AreaOfInterest or AreaSlice). The basic information consists of the grouping, a NamespaceId, and a ReadCapability authenticating the request. The message carries various further options to make it more expressive.

First, additional options for requesting the payloads of the Entries in the grouping: a payload length threshold up to which the receiver should automatically send the payloads without further requests, and the value k for the Bab k-grouping used when sending these payloads.

Second, options regarding streaming responses. The responses always concern the entries the receiver has at the point of responding, but an additional flag can indicate that the request is long-running, asking the the receiver to keep forwarding new Entries it obtains in the future. For the immediate response, the requester can also ask for the Entries to be sent in sortedIn set reconciliation, the part where a peer sends everything they had in a range except for the items they just received can be implemented more efficiently if the received items arrive in sorted order. order.

Third, and finally, options pertaining to This specification assumes familiarity with our page on region-based set reconciliation.region-based set reconciliation:

  • The sender can attach the Fingerprint over their own LengthyAuthorisedEntries in the requested grouping — the receiver can then compute the fingerprint over their LengthyAuthorisedEntries and skip sending anything in case of a match.
  • The sender can specify a maximum Entry count and a maximum total Payload size. If the Entries of the receiver exceeds those thresholds, the receiver should answer with a summary (Fingerprint, number of Entries, and their total Payload size) instead of the data itself.
  • The sender can allow the receiver to respond (if the count or size thresholds are exceeded) not with metadata but to instead partition the requested grouping into smaller AreaSlices and respond with RequestEntries messages of its own; this implements the symmetric, collaborative drilling-down to differences of RBSR. Such responses state which message they are responding to, as well as the original non-response message that kicked things off — this lets peers efficiently track progress on their original requests.
 
Whether this message is assigned a WtpMessageId or not.
 
A valid ReadCapability whose receiver is the Receiver authenticated in the opening handshake (ini_spk for the initiator, res_spk for the responder), whose granted area includes the grouping of the request, and whose granted namespace is the namespace_id of the request.
 
The namespace in which to request entries.
 
The grouping in which to request entries.
 
The receiver should include Payloads of this length or less in the responses, whereas larger Payloads should be excluded.
 
The value k for Bab k-grouping when sending Payloads in response to this request.
 
k: U8,
 

If true, the receiver should send all matching entries it has and then signal the end of the response. If false, the receiver should keep sending new matching entries as it obtains them.

If the response consists of metadata (or a set of RequestEntries messages as “counter-requests” for set reconciliation), this flag is ignored; such responses are always fire-and-forget.

 
The fingerprint over all LengthyAuthorisedEntries the sender of the message has in the grouping. Or simply none if the sender does not want to compute this.
 
If the receiver has this many or more entries in the requested grouping, they should reply with metadata (or partition the grouping and send RequestEntries message of their own, depending on allow_symmetric_rbsr) instead of sending the entries.
 
If the receiver has entries in the requested grouping whose total payload_length (the actual field of the Entry, not the length of the Payload prefix that is actually available) is equal to or greater than this, they should reply with metadata (or partition the grouping and send RequestEntries message of their own, depending on allow_symmetric_rbsr) instead of sending the entries.
 
If true, when the threshold_count or threshold_size are exceeded by the receiver, they should partition the grouping into smaller subgroupings and send their own RequestEntries messages instead of responding with metadata.
 
If this message is sent as a response to another RequestEntries as per that message’s allow_symmetric_rbsr flag, and that prior message has a WtpMessageId, then this field conveys further information about what is being responded to. Otherwise, it must be none.
}
Provenance metadata when sending a RequestEntries message in response to another RequestEntries message. This information is not required for the correctness of set reconciliation, but it allows peers to efficiently track progress. It is also important for the encoding of long-running (i.e., non-oneshot) responses.
 
The WtpMessageId of the RequestEntries this is in response to. It is an error if this WtpMessageId is not the id of a RequestEntries message.
 
Whether this message is the final RequestEntries message among those sent in response to the same parent message.
 
The WtpMessageId of the root message of the tree of responses, i.e., the id you arrive at by transitively following the parent ids until you reach a RequestEntries message that was not in response to any other RequestEntries message.
 
Whether the root message of the tree of response was originally sent by the same peer as this very message. This flag disambiguates the root field; unlike all other fields in the protocol it is not clear from context alone whose peer’s message is being referred to.
}

Note that the symmetric nature of region-based set reconciliation can lead to redundancies: if both peers send RequestEntries messages for the same grouping and then both are recursively split and reconciled, a lot of work is unnecessarily duplicated. We recommend employing some sort of symmetry-breaking. For example, the peer who initiated the connection could be responsible for detecting duplicates and stopping to process redundant messages (if both peers did this simultaneously, then symmetric requests would end up being dropped completely).

RespondToRequestEntries

The RespondToRequestEntries messages let peers respond to RequestEntries messages. Somewhat unconventionally, a single RequestEntries is not responded to by a single RespondToRequestEntries. Instead, the process of fully responding to a RequestEntries is a sequence of steps, forming a (simple and small) state machine. RequestEntries messages indicate transitions in that state machine. When we write that a particular kind of RespondToRequestEntries “terminates the response”, that means that a final state has been reached, the request has been completely responded to and will not be interacted with again. Both peers can then clear up any state associated with that request.

Indicate progress through the process of responding to a RequestEntries message.
 
The WtpMessageId of the RequestEntries message this is a response to.
 
The transition in the reponse state machine.
}

The state transitions are straightforward. After receiving a RequestEntries message, there are five options:

  • The first option does not involve RespondToRequestEntries messages at all: if you reply with other RespondToRequestEntries messages instead, the response is terminated once the final such message (with the is_final_response flag set to true) has been sent.
  • The second option is the Decline transition, which indicates that the request will not be processed (any further) and immediately terminates the response. This transition can be taken at any time, not only as the very first transition.
  • The third option is the Done transition, which signals that the locally computed Fingerprint over the requested grouping equals the own_fingerprint field of the RequestEntries message and terminates the response.
  • The fourth option is the Metadata transition, which carries metadata summarising the grouping and then terminates the response.
  • The final option is the ImmediateEntries transition which moves to a new state. In this state, you can send SendEntry and SendPayloadSlice messages pertaining to the request.

After sending a ImmediateEntries response, there are two further transitions. One is the Done transition, indicating that all entries and payloads have been sent and terminating the response. The other option is the LiveEntries transition. It must only be taken if the request did not set the is_oneshot flag.

After a LiveEntries transition, you can continue sending SendEntry and SendPayloadSlice messages, with the difference that these are now encoded relative to the root request and need not adhere to any promises of a particular order of entry transmissions any longer. The intuition is that the switch from ImmediateEntries to LiveEntries marks going from sending the collection of stored entries to forwarding live updates. Finally, the Done transition can be used to terminate the response.

A depiction of the transitions as a finite state machine, according to the description above.

The ImmediateEntries transition serves a second purpose: it can optionally let the sender of the transition request the LengthyAuthorisedEntries the other peer has in the same grouping.This makes the set reconciliation symmetric: both peers should send their items in each small range. The receiver is encouraged to wait with its response until a subsequent LiveEntries or Done transition and to omit from its response those entries sent by the other peer.

The different transitions in the state machine that describes the process of responding to a RequestEntries message.
 
Indicates that the RequestEntries message will not be processed (any further). Terminates the response.
 
 
If true, indicates that the other peer might have better luck if they tried the same request at a later time, preferrably with exponential backoff between tries. If false, the other peer need not bother trying again.
 
 
retry: Bool,
 
}
 
Responds to the RequestEntries message by supplying metadata about the grouping. Terminates the response.
 
 
The Fingerprint over the LengthyAuthorisedEntries the sender has in the grouping.
 
 
The number of entries the sender has in the grouping.
 
 
count: U64,
 
 
The sum of the payload_lengths of the entries the sender has in the grouping (the length as given by the Entries themselves, not how many Payload bytes are actually locally available).
 
 
size: U64,
 
}
 
Indicates that the responder will reply with SendEntry and SendPayloadSlice messages.
 
 
Whether to promise that the immediate entries will be transmitted in sorted order, according to the reconciliation order.
 
 

Whether this message should also serve as an implicit RequestEntries message in addition to the regular state transition semantics.

If this is none, it does not serve as an implicit RequestEntries message. If this is not none, then both peers should act as if this message was a RequestEntries message whose fields are equal to that of the RequestEntries message to whose response this state transition belongs, except that

In particular, both peers must allocate a WtpMessageId for this implicit message.

 
 
If this is not none, it it gives the value of the payload_lazyness field of the implicit RequestEntries message. Must be none if symmetric_capability is none.
 
 
If this is not none, it it gives the value of the k field of the implicit RequestEntries message. Must be none if symmetric_capability is none.
 
 
Gives the value of the is_oneshot field of the implicit RequestEntries message. Ignored if symmetric_capability is none.
 
}
 
Indicates that further entries and payloads will be transmitted without ordering guarantees and encoded relative to the root request rather than the actual request being responded to.
 
Indicates that no more entries or payloads will be transmitted. Terminates the response.
}

RequestPayloadSlice

The RequestPayloadSlice messages let peers request a slice of the Payload of a specific Entry. Specific here means that not only its namespace_id, subspace_id, and path are used to address it, but also its payload_digest.

Request a verifiable Bab stream for a slice of the Payload of a specific Entry.
 
Whether this message is assigned a WtpMessageId or not.
 
A valid ReadCapability whose receiver is the Receiver authenticated in the opening handshake (ini_spk for the initiator, res_spk for the responder), whose granted area includes the Entry whose Payload (slice) is requested.
 
The namespace_id of the Entry.
 
The subspace_id of the Entry.
 
The path of the Entry.
 
The payload_digest of the Entry.
 
The start index (in Bab chunks) of the slice to request.
 
The length (in Bab chunks) of the slice to request.
 
The value k for the Bab k-grouping to request for the verifiable slice stream.
 
k: U8,
 
The left_skip for the verifiable slice stream requested by this message.
 
The right_skip for the verifiable slice stream requested by this message.
}

RespondToRequestPayloadSlice

The RespondToRequestPayloadSlice messages let peers respond to RequestPayloadSlice messages. Analogous to RespondToRequestEntries messages, the actual Payload slice data is carried by different messages (SendPayloadSlice), the RespondToRequestPayloadSlice merely handle error reporting and request termination by stepping through a simple state machine.

Indicate progress through the process of responding to a RequestPayloadSlice message.
 
The WtpMessageId of the RequestPayloadSlice message this is a response to.
 
The transition in the reponse state machine.
}

The state transitions are very simple. The Decline transition, which indicates that the request will not be processed (any further) and immediately terminates the response. This transition can be taken at any time, not only as the very first transition.

The Found transition indicates that the responder has the Entry whose Payload (subslice) was requested. After doing the Found transition, the responder can then send SendPayloadSlice messages to fulfil the request.

Finally, the Done transition can be taken after the Found transition to signal that no more slices will be transmitted, terminating the response. If the responder has the Entry but not its Payload (or at least not the requested slice), the Done transition can be sent immediately after the Found transition, and likewise if the Payload of the Entry is the empty string.

Alternatively, the Done transition can be taken before the Found transition, signalling that the responder does not have the Entry matching the request.

A depiction of the transitions as a finite state machine, according to the description above.
The different transitions in the state machine that describes the process of responding to a RequestPayloadSlice message.
 
Indicates that the RequestPayloadSlice message will not be processed (any further). Terminates the response.
 
 
If true, indicates that the other peer might have better luck if they tried the same request at a later time, preferrably with exponential backoff between tries. If false, the other peer need not bother trying again.
 
 
retry: Bool,
 
}
 
Indicates that the responder has a matching Entry.
 
Indicates either that the responder has no matching Entry (if sent before a Found transition), or that the responder has transmitted as much of the requested Payload slice as possible.
}

SendEntry

The SendEntry messages let peers transmit LengthyAuthorisedEntries. They may specifically indicate a RequestEntries message they are responding to, or they can be sent as standalone messages.

Note that the available field of the transmitted LengthyAuthorisedEntry gives the length of the available prefix in Bab chunks, not in bytes.

Transmit a LengthyAuthorisedEntry, optionally in response to a specific RequestEntries message.
struct SendEntry {
 
The WtpMessageId of the RequestEntries this is in response to, or none if this message is standalone. It is an error if this is a WtpMessageId but the corresponding message is not a RequestEntries message.
 
The LengthyAuthorisedEntry to transmit, with the available field giving the length of the available prefix in Bab chunks, not in bytes.
}

SendPayloadSlice

The SendPayloadSlice messages let peers transmit verifiable slice streams for (parts of) Payloads. They may specifically indicate a RequestPayloadSlice or RequestEntries message they are responding to, or they can be sent as standalone messages.

Transmit a Bab verifiable (subslice of a) Payload, optionally in response to a specific RequestPayloadSlice or RequestEntries message.
 
The WtpMessageId of the RequestPayloadSlice or RequestEntries this is in response to, or none if this message is standalone. It is an error if this is a WtpMessageId but the corresponding message is not a RequestPayloadSlice or RequestEntries message.
 
The namespace_id of the Entry whose Payload (slice) is sent.
 
The subspace_id of the Entry whose Payload (slice) is sent.
 
The path of the Entry whose Payload (slice) is sent.
 
The payload_digest of the Entry whose Payload (slice) is sent.
 
The start (in Bab chunks) of the slice to transmit.
 
The length (in Bab chunks) of the slice to transmit.
 
The value k for the Bab k-grouping used in the verifiable slice stream transmitted in this message.
 
k: U8,
 
The left_skip for the verifiable slice stream transmitted in this message.
 
The right_skip for the verifiable slice stream transmitted in this message.
 
The bytes making up the verifiable slice stream for the indicated slice of the Payload.
 
bytes: &[U8],
}

When sent as a response, many of the fields can often be derived from context and are thus omitted from the actual message encoding. Specifically:

For the first SendPayloadSlice message sent in response to a RequestPayloadSlice message, after a RespondToRequestPayloadSlice message with response Found for that RequestPayloadSlice message, the following fields of the SendPayloadSlice message are predetermined:

For every subsequent SendPayloadSlice message pertaining to the same RequestPayloadSlice message, the start is the sum of the start and the length of the preceding SendPayloadSlice message pertaining to the same RequestPayloadSlice message, and the left_skip is maximal so that only the verification metadata is contained in the transmitted slice stream that has not been covered by previous SendPayloadSlice messages yet, and the other predetermined fields stay the same.

In other words, the predetermined values make it so that peers always respond with exactly the verifiable slice stream that was requested, split over arbitrarily many slicesThis is so that peers cannot be forced to send huge individual messages. of arbitrary lengths.

When sending a SendPayloadSlice message in response to a RequestEntries message, after having sent a RespondToRequestEntries message with response ImmediateEntries or LiveEntries for that RequestEntries message, and then having sent at least one SendEntry message in response to that same RequestEntries message, some fields of the SendPayloadSlice are predetermined. For the first SendPayloadSlice per SendEntry message:

For every subsequent SendPayloadSlice message pertaining to the same Payload message,This is fully analogous to sneding multiple SendPayloadSlice messages in response to the same RequestPayloadSlice message. the start is the sum of the start and the length of the preceding SendPayloadSlice message pertaining to that same Payload, and the left_skip is maximal so that only the verification metadata is contained in the transmitted slice stream that has not been covered by previous SendPayloadSlice messages yet, and the other predetermined fields stay the same.

CancelOwnRequest

The CancelOwnRequest messages let peers signal that they are no longer interested in the response(s) to a RequestEntries or RequestPayloadSlice message that they themselves had originally sent.

Notify the receiver that the sender is no longer interested in the response(s) to a RequestEntries or RequestPayloadSlice message the sender of this message had originally sent.
 
The WtpMessageId of the RequestEntries or RequestPayloadSlice message this is cancelling.
}

UpdatePayloadLazyness

The UpdatePayloadLazyness messages let peers adjust the payload_lazyness of a RequestEntries message they had previously sent. This allows peers to implement the Plumtree algorithm (Leitao et al., 2007).

Adjust the payload_lazyness of a RequestEntries message the sender of this message had previously sent.
 
The WtpMessageId of the RequestEntries message this is adjusting.
 
The value to set the payload_lazyness of the addressed RequestEntries message.
}

Encodings

alj: TODO

References