Bitcoin Book of BIPs

BIP 324

Introduction

Abstract

This document proposes a new Bitcoin P2P transport protocol, which features opportunistic encryption, a mild bandwidth reduction, and the ability to negotiate upgrades before exchanging application messages.

This document is licensed under the 3-clause BSD license.

Motivation

Bitcoin is a permissionless network whose purpose is to reach consensus over public data. Since all data relayed in the Bitcoin P2P network is inherently public, and the protocol lacks a notion of cryptographic identities, peers talk to each other over unencrypted and unauthenticated connections. Nevertheless, this plaintext nature of the current P2P protocol (referred to as v1 in this document) has severe drawbacks in the presence of attackers:

This proposal for a new P2P protocol version (v2) aims to improve upon this by raising the costs for performing these attacks substantially, primarily through the use of unauthenticated, opportunistic transport encryption. In addition, the bytestream on the wire is made pseudorandom (i.e., indistinguishable from uniformly random bytes) to a passive eavesdropper.

Why encrypt without authentication?

As we have argued above, unauthenticated encryptionWhat does authentication mean in this context? Unfortunately, the term authentication in the context of secure channel protocols is ambiguous. It can refer to:

When it comes to authentication, the situation is not as clear as for encryption. Due to Bitcoin’s permissionless nature, authentication will always be restricted to specific scenarios (e.g., connections between peers belonging to the same operator), and whether some form of (possibly partially anonymous) authentication is desired depends on the specific requirements of the involved peers. As a consequence, we believe that authentication should be addressed separately (if desired), and this proposal aims to provide a solid technical basis for future protocol upgrades, including the addition of optional authentication (see Private authentication protocols).

Why have a pseudorandom bytestream when traffic analysis is still possible?

Traffic analysis, e.g., observing packet lengths and timing, as well as active attacks can still reveal that the Bitcoin v2 P2P protocol is in use. Nevertheless, a pseudorandom bytestream raises the cost of fingerprinting the protocol substantially, and may force some intermediaries to attack any protocol they cannot identify, causing collateral cost.

A pseudorandom bytestream is not self-identifying. Moreover, it is unopinionated and thus a canonical choice for similar protocols. As a result, Bitcoin P2P traffic will be indistinguishable from traffic of other protocols which make the same choice (e.g., obfs4 and a recently proposed cTLS extension). Moreover, traffic shapers and protocol wrappers (for example, making the traffic look like HTTPS or SSH) can further mitigate traffic analysis and active attacks but are out of scope for this proposal.

Why not use a secure tunnel protocol?

Our goal includes making opportunistic encryption ubiquitously available, as that provides the best defense against large-scale attacks. That implies protecting both the manual, deliberate connections node operators instruct their software to make, and the automatic connections Bitcoin nodes make with each other based on IP addresses obtained via gossip. While encryption per se is already possible with proxy networks or VPN protocols, these are not desirable or applicable for automatic connections at scale:

Thus, to achieve our goal, we need a solution that has minimal costs, works without configuration, and is always enabled – on top of any network layer rather than be part of the network layer.

Why not use a general-purpose transport encryption protocol?

While it would be possible to rely on an off-the-shelf transport encryption protocol such as TLS or Noise, the specific requirements of the Bitcoin P2P network laid out above make these protocols an unsuitable choice.

The primary requirement which existing protocols fail to meet is a sufficiently modular treatment of encryption and authentication. As we argue above, whether and which form of authentication is desired in the Bitcoin P2P network will depend on the specific requirements of the involved peers (resulting in a mix of authenticated and unauthenticated connections), and thus the question of authentication should be decoupled from encryption. However, native support for a handful of standard authentication scenarios (e.g., using digital signatures and certificates) is at the core of the design of existing general-purpose transport encryption protocols. This focus on authentication would not provide clear benefits for the Bitcoin P2P network but would come with a large amount of additional complexity.

In contrast, our proposal instead aims for a simple modular design that makes it possible to address authentication separately. Our proposal provides a foundation for authentication by exporting a session ID that uniquely identifies the encrypted channel. After an encrypted channel has been established, the two endpoints are able to use any authentication protocol to confirm that they have the same session ID. (This is sometimes called channel binding because the session ID binds the encrypted channel to the authentication protocol.) Since in our proposal, any authentication needs to run after an encrypted connection has been established, the price we pay for this modularity is a possibly higher number of roundtrips as opposed to other protocols that perform authentication alongside the Diffie-Hellman key exchange.Do other protocols not support exporting a session ID? While Noise and TLS (as a draft) offer similar protocol extensions for exporting session IDs, using channel binding for authentication is not at the focus of their design and would not avoid the bulk of additional complexity due to the native support of authentication methods. However, the resulting increase in connection establishment latency is a not a concern for Bitcoin’s long-lived connections, which typically live for hours or even weeks.

Besides this fundamentally different treatment of authentication, further technical issues arise when applying TLS or Noise to our desired use case:

While existing protocols could be amended to address all of the aforementioned issues, this would negate the benefits of using them as off-the-shelf solution, e.g., the possibility to re-use existing implementations and security analyses.

Goals

This proposal aims to achieve the following properties:

Specification

The specification consists of three parts:

Transport layer specification

In this section, we define the encryption protocol for messages between peers.

Overview and design

We first give an informal overview of the entire protocol flow and packet encryption.

Protocol flow overview

Given a newly established connection (typically TCP/IP) between two v2 P2P nodes, there are 3 phases the connection goes through. The first starts immediately, i.e. there are no v1 messages or any other bytes exchanged on the link beforehand. The two parties are called the initiator (who established the connection) and the responder (who accepted the connection).

  1. The Key exchange phase, where nodes exchange data to establish shared secrets.
    • The initiator:
      • Generates a random ephemeral secp256k1 private key and sends a corresponding 64-byte ElligatorSwiftWhat is ElligatorSwift and why use it? The SwiftEC paper describes a method called ElligatorSwift which allows encoding elliptic curve points in a way that is indistinguishable from a uniformly distributed bitstream. While a random 256-bit string has about 50% chance of being a valid X coordinate on the secp256k1 curve, every 512-bit string is a valid ElligatorSwift encoding of a curve point, making the encoded point indistinguishable from random when using an encoder that can sample uniformly.How fast is ElligatorSwift? Our benchmarks show that ElligatorSwift encoded ECDH is about 50% more expensive than unencoded ECDH. Given the fast performance of ECDH and the low frequency of new connections, we found the performance trade-off acceptable for the pseudorandom bytestream and future censorship resistance it can enable.-encoded public key to the responder.
      • May send up to 4095How was the limit of 4095 bytes garbage chosen? It is a balance between having sufficient freedom to hide information, and allowing it to be large enough so that the necessary 64 bytes of public key is small compared to it on the one hand, and bandwidth waste on the other hand. bytes of arbitrary data after their public key, called garbage, providing a form of shapability and avoiding a recognizable pattern of exactly 64 bytes.Why does the affordance for garbage exist in the protocol? The garbage strings after the public keys are needed for shapability of the handshake. Neither peer can send decoy packets before having received at least the other peer’s public key, i.e., neither peer can send more than 64 bytes before having received 64 bytes.
    • The responder:
      • Waits until one byte is received which does not match the 16 bytes consisting of the network magic followed by “version”. If the first 16 bytes do match, the connection is treated as using the v1 protocol instead.What if a v2 initiator’s public key starts accidentally with these 16 bytes? This is so unlikely (probability of 2-128) to happen randomly in the v2 protocol that the initiator does not need to specifically avoid it. The optional detection of wrong-network v1 peers has a probability of 2-96, which is still negligible compared to random network failures.Bitcoin Core versions <=0.4.0 and >=22.0 ignore valid P2P messages that are received prior to a VERSION message. Bitcoin Core versions between 0.4.0 and 22.0 assign a misbehavior score to the peer upon receiving such messages. v2 clients implementing this proposal will interpret any message other than VERSION received as the first message to be the initiation of a v2 connection, and will result in disconnection for v1 initiators that send any message type other than VERSION as the first message. We are not aware of any implementations where this could pose a problem.
      • If the first 4 received bytes do not match the network magic, but the 12 bytes after that do match the version message encoding above, implementations may interpret this as a v1 peer of a different network, and disconnect them.
      • Similarly generates a random ephemeral private key and sends a corresponding 64-byte ElligatorSwift-encoded public key to the initiator.
      • Similarly may send up to 4095 bytes of garbage data after their public key.
    • Both parties:
      • Receive (the remainder of) the full 64-byte public key from the other side.
      • Use X-onlyWhy use X-only ECDH? Using only the X coordinate provides the same security as using a full encoding of the secret curve point but allows for more efficient implementation by avoiding the need for square roots to compute Y coordinates. ECDH to compute a shared secret from their private key and the exchanged public keysWhy is the shared secret computation a function of the exact 64-byte public encodings sent? This makes sure that an attacker cannot modify the public key encoding used without modifying the rest of the stream. If a third party wants the ability to modify stream bytes, they need to perform a full MitM attack on the connection., and deterministically derive from the secret 4 encryption keys (two in each direction: one for packet lengths, one for content encryption), a session id, and two 16-byte garbage terminatorsWhat length is sufficient for garbage terminators? The length of the garbage terminators determines the probability of accidental termination of a legitimate v2 connection due to garbage bytes (sent prior to ECDH) inadvertently including the terminator. 16 byte terminators with 4095 bytes of garbage yield a negligible probability of such collision which is likely orders of magnitude lower than random connection failure on the Internet.What does a garbage terminator in the wild look like?
        A garbage terminator model TX-v2 in the wild… sent by the responder

(one in each direction) using HKDF-SHA256.

#** Send their 16-byte garbage terminator.Why does the protocol need a garbage terminator? While it is in principle possible to use the first packet after the garbage directly as a terminator (scan until a valid packet follows), this would be significantly slower than just scanning for a fixed byte sequence, as it would require recomputing a Poly1305 tag after every received byte.

#** Receive up to 4111 bytes, stopping when encountering the garbage terminator.

#* At this point, both parties have the same keys, and all further communication proceeds in the form of encrypted packets.

#** Encrypted packets have an ignore bit, which makes them decoy packets if set. Decoy packets are to be ignored by the receiver apart from verifying they decrypt correctly. Either peer may send such decoy packets at any point from here on. These form the primary shapability mechanism in the protocol. How and when to use them is out of scope for this document.

#** For each of the two directions, the first encrypted packet that will be sent in that direction (regardless of it being a decoy packet or not) will make use of the associated authenticated data (AAD) feature of the AEAD to authenticate the garbage that has been sent in that direction.Why does the protocol authenticate the garbage? Without garbage authentication, the garbage would be modifiable by a third party without consequences. We want to force any active attacker to have to maintain a full protocol state. In addition, such malleability without the consequence of connection termination could enable protocol fingerprinting.

  1. The Version negotiation phase, where parties negotiate what transport version they will use, as well as data defined by that version.What features could be added in future protocol versions? Examples of features that could be added in future versions include post-quantum cryptography upgrades to the handshake, and optional authentication.
    • The responder:
      • Sends a version packet with empty content, to indicate support for the v2 P2P protocol proposed by this document. Any other value for content is reserved for future versions.
    • The initiator:
      • Receives a packet, ignores its contents. The idea is that features added by future versions get negotiated based on what is supported by both parties. Since there is just one version so far, the contents here can simply be ignored. But in the future, receiving a non-empty contents here may trigger other behavior; we defer specifying the encoding for such version content until there is a need for it.How will future versions encode version numbers in the version packet? Future versions could, for example, specify that the contents of the version packet is to be interpreted as an integer version number (with empty representing 0), and if the minimum of both numbers is N, that being interpreted as choosing a “v2.N” protocol version. Alternatively, certain bytes of the version packet contents could be interpreted as a bitvector of optional features.
      • Sends a version packet with empty content as well, to indicate support for the v2 P2P protocol.
    • The responder:
      • Receives a packet, ignores its contents.
  2. The Application phase, where the packets exchanged have contents to be interpreted as application data.
    • Whenever either peer has a message to send, it sends a packet with that application message as contents.

To avoid the recognizable pattern of first messages being at least 64 bytes, a future backwards-compatible upgrade to this protocol may allow both peers to send their public key + garbage + garbage terminator in multiple rounds, slicing those bytes up into messages arbitrarily, as long as progress is guaranteed.How can progress be guaranteed in a backwards-compatible way? In order to guarantee progress, it must be ensured that no deadlock occurs, i.e., no state is reached in which each party waits for the other party indefinitely. For example, any upgrade that adheres to the following conditions will guarantee progress:

Since the protocol as specified here adheres to these conditions, any upgrade which also adheres to these conditions will be backwards-compatible.

Note that the version negotiation phase does not need to wait for the key exchange phase to complete; version packets can be sent immediately after sending the garbage terminator. So the first two phases together, jointly called the handshake, comprise just 1.5 roundtrips:

Packet encryption overview

All data on the wire after the garbage terminators takes the form of encrypted packets. Every packet encodes an encrypted variable-length byte array, called the contents, as well as an ignore bit as mentioned before. The total size of a packet is 20 bytes plus the length of its contents.

Each packet consists of:

The encryption of the plaintext uses ChaCha20Poly1305Why is ChaCha20Poly1305 chosen as the basis for packet encryption? It is a very widely used authenticated encryption cipher (used among others in SSH, TLS 1.2, TLS 1.3, QUIC, Noise, and WireGuard; in the latter it is currently even the only supported cipher), with very good performance in general purpose software implementations. While AES-based ciphers (including the winners in the CAESAR competition in non-lightweight categories) perform significantly better on systems with AES hardware acceleration, they are also significantly slower in pure software implementations. We choose to optimize for the weakest hardware., an authenticated encryption with associated data (AEAD) cipher specified in RFC 8439. Every packet’s plaintext is treated as a separate AEAD message, with a different nonce for each.

The length must be dealt with specially, as it is needed to determine packet boundaries before the whole packet is received and authenticated. As we want a stream that is pseudorandom to a passive attacker, it still needs encryption. We use unauthenticatedWhy is the length encryption not separately authenticated? Informally, the relevant security goal we aim for is to hide the number of packets and their lengths (i.e., the packet boundaries) against a passive attacker that receives the bytestream without timing or fragmentation information. (A formal definition can be found for example in Hansen 2016 (Definition 22) under the name “boundary hiding against chosen-plaintext attacks (BH-CPA)”.) However, we do not aim to hide packet boundaries against active attackers because active attackers can always exploit the fact that the Bitcoin P2P protocol is largely query-response based: they can trickle the bytes on the stream one-by-one unmodified and observe when a response comes (see Hansen 2016 (Section 3.9) for a in-depth discussion). With that in mind, we accept that an active (non-MitM) attacker is able to figure out some information about packet boundaries by flipping certain bits in the unauthenticated length field, and observing the other side disconnecting immediately or later. Thus, we choose to use unauthenticated encryption for the length data, which is sufficient to achieve boundary hiding against passive attackers, and saves 16 bytes of bandwidth per packet. ChaCha20 encryption for this, with an independent key. Note that the plaintext length is still implicitly authenticated by the encryption of the plaintext, but this can only be verified after receiving the whole packet. This design is inspired by that of the ChaCha20Poly1305 cipher suite in OpenSSH.How does packet encryption differ from the OpenSSH design? The differences are:

Because only fixed-length chunks (3-byte length fields) are encrypted, we do not need to treat all length chunks as separate messages. Instead, a single cipher (with the same nonce) is used for multiple consecutive length fields. This avoids wasting 61 pseudorandom bytes per packet, and makes the cost of having a separate cipher for length encryption negligible.Is it acceptable to use a less standard construction for length encryption? The fact that multiple (non-overlapping) bytes generated by a single ChaCha20 cipher are used for the encryption of multiple consecutive length fields is uncommon. We feel the performance cost gained by this deviation is worth it (especially for small packets, which are very common in Bitcoin's P2P protocol), given the low guarantees that are feasible for length encryption in the first place, and the result is still sufficient to provide pseudorandomness from the view of passive attackers. For plaintext encryption, we independently use a very standard construction, as the stakes for confidentiality and integrity there are much higher.

In order to provide forward securityWhat value does forward security provide? Re-keying ensures forward secrecy within a session, i.e., an attacker compromising the current session secrets cannot derive past encryption keys in the same session.Why have a cipher with forward secrecy but no periodical refresh of the ECDH key exchange? Our cipher ratchets encryption keys forward in order to protect messages encrypted under past encryption keys. In contrast, re-performing ECDH key exchange would protect messages encrypted under future encryption keys, i.e., it would re-establish security after the attacker had compromised one of the peers temporarily (e.g., the attacker obtains a memory dump). We do not believe protecting against that is a priority: an attacker that, for whatever reason, is capable of an attack that reveals encryption keys (or other session secrets) of a peer once is likely capable of performing the same attack again after peers have re-performed the ECDH key exchange. Thus, we do not believe the benefits of re-performing key exchange outweigh the additional complexity that comes with the necessary coordination between the peers. We note that the initiator could choose to close and re-open the entire connection to force a refresh of the ECDH key exchange, but that introduces other issues: a connection slot needs to be kept open at the responder side, it is not cryptographically guaranteed that really the same initiator will use it, and the observable TCP reset and handshake may create a detectable pattern., the encryption keys for both plaintext and length encryption are cycled every 224 messages, by switching to a new key that is generated by the key stream using the old key.

Handshake: key exchange and version negotiation

Next we specify the handshake of a connection in detail.

As explained before, these messages are sent to set up the connection:

 ----------------------------------------------------------------------------------------------------
 | Initiator                         Responder                                                      |
 |                                                                                                  |
 | x, ellswift_X = ellswift_create()                                                                |
 |                                                                                                  |
 |    ---- ellswift_X + initiator_garbage (initiator_garbage_len bytes; max 4095) --->              |
 |                                                                                                  |
 |                                   y, ellswift_Y = ellswift_create()                              |
 |                                   ecdh_secret = v2_ecdh(                                         |
 |                                                     y, ellswift_X, ellswift_Y, initiating=False) |
 |                                   initialize_v2_transport(                                       |
 |                                       initiator, ecdh_secret, initiating=False)                  |
 |                                                                                                  |
 |    <--- ellswift_Y + responder_garbage (responder_garbage_len bytes; max 4095) +                 |
 |             responder_garbage_terminator (16 bytes) +                                            |
 |             v2_enc_packet(initiator, RESPONDER_TRANSPORT_VERSION, aad=responder_garbage) ----    |
 |                                                                                                  |
 | ecdh_secret = v2_ecdh(x, ellswift_Y, ellswift_X, initiating=True)                                |
 | initialize_v2_transport(responder, ecdh_secret, initiating=True)                                 |
 |                                                                                                  |
 |     ---- initiator_garbage_terminator (16 bytes) +                                               |
 |              v2_enc_packet(responder, INITIATOR_TRANSPORT_VERSION, aad=initiator_garbage) --->   |
 |                                                                                                  |
 ----------------------------------------------------------------------------------------------------
Shared secret computation

The peers derive their shared secret through X-only ECDH, hashed together with the exactly 64-byte public keys’ encodings sent over the wire.

def v2_ecdh(priv, ellswift_theirs, ellswift_ours, initiating):
    ecdh_point_x32 = ellswift_ecdh_xonly(ellswift_theirs, priv)
    if initiating:
        # Initiating, place our public key encoding first.
        return sha256_tagged("bip324_ellswift_xonly_ecdh", ellswift_ours + ellswift_theirs + ecdh_point_x32)
    else:
        # Responding, place their public key encoding first.
        return sha256_tagged("bip324_ellswift_xonly_ecdh", ellswift_theirs + ellswift_ours + ecdh_point_x32)

Here, sha256_tagged(tag, x) returns a tagged hash value SHA256(SHA256(tag) || SHA256(tag) || x) as in BIP340.

ElligatorSwift encoding of curve X coordinates

The functions ellswift_create and ellswift_ecdh_xonly encapsulate the construction of ElligatorSwift-encoded public keys, and the computation of X-only ECDH with ElligatorSwift-encoded public keys.

First we define a constant:

To define the needed functions, we first introduce a helper function, matching the XSwiftEC function from the SwiftEC paper, instantiated for the secp256k1 curve, with minor modifications. It maps pairs of integers (u, t) (both in range 0..p-1) to valid X coordinates on the curve. Note that the specification here does not attempt to be constant time, as it does not operate on secret data. In what follows, we use the notation from BIP340.

To find encodings of a given X coordinate x, we first need the inverse of XSwiftEC. The function XSwiftECInv(x, u, case) either returns t such that XSwiftEC(u, t) = x, or None. The case variable is an integer in range 0..7, which selects which of the up to 8 valid such t values to return:

The overall XElligatorSwift algorithm, matching the name used in the paper, then uses this inverse to randomlyCan the ElligatorSwift encoding be used to construct public key encodings that satisfy a certain structure (and not pseudorandom)? The algorithm chooses the first 32 bytes (i.e., the valueu) and then computes a correspondingtsuch that the mapping to the curve point holds. In general, pickingufrom a uniformly random distribution provides pseudorandomness. But we can also fix any of the 32 bytes inu, and the algorithm will still find a correspondingt. The fact that it is possible to fix the first 32 bytes, combined with the garbage bytes in the handshake, provides a limited but very simple method of parroting other protocols such as TLS 1.3, which can be deployed by one of the peers without explicit support from the other peer. More general methods of parroting, e.g., introduced by defining new protocol or a protocol upgrade, are not precluded. sample encodings ofx’’:

This is used to define the ellswift_create algorithm used in the previous section; it generates a random private key, along with a uniformly sampled 64-byte ElligatorSwift-encoded public key corresponding to it:

Finally the ellswift_ecdh_xonly algorithm is:

Keys and session ID derivation

The authenticated encryption construction proposed here requires two 32-byte keys per communication direction. These (in addition to a session ID) are computed using HKDFWhy use HKDF for deriving key material? The shared secret already involves a hash function to make sure the public key encodings contribute to it, which negates some of the need for HKDF already. We still use it as it is the standard mechanism for deriving many keys from a single secret, and its computational cost is low enough to be negligible compared to the rest of a connection setup. as specified in RFC 5869 with SHA256 as the hash function:

def initialize_v2_transport(peer, ecdh_secret, initiating):
    # Include NETWORK_MAGIC to ensure a connection between nodes on different networks will immediately fail
    prk = HKDF_Extract(Hash=sha256, salt=b'bitcoin_v2_shared_secret' + NETWORK_MAGIC, ikm=ecdh_secret)

    peer.session_id = HKDF_Expand(Hash=sha256, PRK=prk, info=b'session_id', L=32)

    # Initialize the packet encryption ciphers.
    initiator_L = HKDF_Expand(Hash=sha256, PRK=prk, info=b'initiator_L', L=32)
    initiator_P = HKDF_Expand(Hash=sha256, PRK=prk, info=b'initiator_P', L=32)
    responder_L = HKDF_Expand(Hash=sha256, PRK=prk, info=b'responder_L', L=32)
    responder_P = HKDF_Expand(Hash=sha256, PRK=prk, info=b'responder_P', L=32)
    garbage_terminators = HKDF_Expand(Hash=sha256, PRK=prk, info=b'garbage_terminators', L=32)
    initiator_garbage_terminator = garbage_terminators[:16]
    responder_garbage_terminator = garbage_terminators[16:]

    if initiating:
        peer.send_L = FSChaCha20(initiator_L)
        peer.send_P = FSChaCha20Poly1305(initiator_P)
        peer.send_garbage_terminator = initiator_garbage_terminator
        peer.recv_L = FSChaCha20(responder_L)
        peer.recv_P = FSChaCha20Poly1305(responder_P)
        peer.recv_garbage_terminator = responder_garbage_terminator
    else:
        peer.send_L = FSChaCha20(responder_L)
        peer.send_P = FSChaCha20Poly1305(responder_P)
        peer.send_garbage_terminator = responder_garbage_terminator
        peer.recv_L = FSChaCha20(initiator_L)
        peer.recv_P = FSChaCha20Poly1305(initiator_P)
        peer.recv_garbage_terminator = initiator_garbage_terminator

    # To achieve forward secrecy we must wipe the key material used to initialize the ciphers:
    memory_cleanse(ecdh_secret, prk, initiator_L, initiator_P, responder_L, responder_P)

The session ID uniquely identifies the encrypted channel. v2 clients supporting this proposal may present the entire session ID (encoded as a hex string) to the node operator to allow for manual, out of band comparison with the peer node operator. Future transport versions may introduce optional authentication methods that compare the session ID as seen by the two endpoints in order to bind the encrypted channel to the authentication.

Overall handshake pseudocode

To establish a v2 encrypted connection, the initiator generates an ephemeral secp256k1 keypair and sends an unencrypted ElligatorSwift encoding of the public key to the responding peer followed by unencrypted pseudorandom bytes initiator_garbage of length garbage_len < 4096.

def initiate_v2_handshake(peer, garbage_len):
    peer.privkey_ours, peer.ellswift_ours = ellswift_create()
    peer.sent_garbage = rand_bytes(garbage_len)
    send(peer, peer.ellswift_ours + peer.sent_garbage)

The responder generates an ephemeral keypair for itself and derives the shared ECDH secret (using the first 64 received bytes) which enables it to instantiate the encrypted transport. It then sends 64 bytes of the unencrypted ElligatorSwift encoding of its own public key and its own responder_garbage also of length garbage_len < 4096. If the first 16 bytes received match the v1 prefix, the v1 protocol is used instead.

TRANSPORT_VERSION = b''
NETWORK_MAGIC = b'\xf9\xbe\xb4\xd9' # Mainnet network magic; differs on other networks.
V1_PREFIX = NETWORK_MAGIC + b'version\x00\x00\x00\x00\x00'

def respond_v2_handshake(peer, garbage_len):
    peer.received_prefix = b""
    while len(peer.received_prefix) < len(V1_PREFIX):
        peer.received_prefix += receive(peer, 1)
        if peer.received_prefix[-1] != V1_PREFIX[len(peer.received_prefix) - 1]:
            peer.privkey_ours, peer.ellswift_ours = ellswift_create()
            peer.sent_garbage = rand_bytes(garbage_len)
            send(peer, peer.ellswift_ours + peer.sent_garbage)
            return
    use_v1_protocol()

Upon receiving the encoded responder public key, the initiator derives the shared ECDH secret and instantiates the encrypted transport. It then sends the derived 16-byte initiator_garbage_terminator, optionally followed by an arbitrary number of decoy packets. Afterwards, it receives the responder’s garbage (delimited by the garbage terminator). The responder performs very similar steps but includes the earlier received prefix bytes in the public key. Both the initiator and the responder set the AAD of the first encrypted packet they send after the garbage terminator (i.e., either an optional decoy packet or the version packet) to the garbage they have just sent, not including the garbage terminator.

def complete_handshake(peer, initiating, decoy_content_lengths=[]):
    received_prefix = b'' if initiating else peer.received_prefix
    ellswift_theirs = received_prefix + receive(peer, 64 - len(received_prefix))
    if not initiating and ellswift_theirs[4:16] == V1_PREFIX[4:16]:
        # Looks like a v1 peer from the wrong network.
        disconnect(peer)
    ecdh_secret = v2_ecdh(peer.privkey_ours, ellswift_theirs, peer.ellswift_ours,
                          initiating=initiating)
    initialize_v2_transport(peer, ecdh_secret, initiating=initiating)
    # Send garbage terminator
    send(peer, peer.send_garbage_terminator)
    # Optionally send decoy packets after garbage terminator.
    aad = peer.sent_garbage
    for decoy_content_len in decoy_content_lengths:
        send(v2_enc_packet(peer, decoy_content_len * b'\x00', aad=aad))
        aad = b''
    # Send version packet.
    send(v2_enc_packet(peer, TRANSPORT_VERSION, aad=aad))
    # Skip garbage, until encountering garbage terminator.
    received_garbage = recv(peer, 16)
    for i in range(4096):
        if received_garbage[-16:] == peer.recv_garbage_terminator:
            # Receive, decode, and ignore version packet.
            # This includes skipping decoys and authenticating the received garbage.
            v2_receive_packet(peer, aad=received_garbage[:-16])
            return
        else:
            received_garbage += recv(peer, 1)
    # Garbage terminator was not seen after 4 KiB of garbage.
    disconnect(peer)
Packet encryption

Lastly, we specify the packet encryption cipher in detail.

Existing cryptographic primitives

Packet encryption is built on two existing primitives:

These will be used for plaintext encryption and length encryption, respectively.

Rekeying wrappers: FSChaCha20Poly1305 and FSChaCha20

To provide re-keying every 224 packets, we specify two wrappers.

The first is FSChaCha20Poly1305, which represents a ChaCha20Poly1305 AEAD, which automatically changes the nonce after every message, and rekeys every 224 messages by encrypting 32 zero bytesWhy is rekeying implemented in terms of an invocation of the AEAD? This means the FSChaCha20Poly1305 wrapper can be thought of as a pure layer around the ChaCha20Poly1305 AEAD. Actual implementations can take advantage of the fact that this formulation is equivalent to using byte 64 through 95 of the keystream output of the underlying ChaCha20 cipher as new key, avoiding the need for Poly1305 in the process., and using the first 32 bytes of the result. Each message will be used for one packet. Note that in our protocol, any FSChaCha20Poly1305 instance is always either exclusively encryption or exclusively decryption, as separate instances are used for each direction of the protocol. The nonce used for a message is composed of the 32-bit little-endian encoding of the number of messages with the current key, followed by the 64-bit little-endian encoding of the number of rekeyings performed. For rekeying, the first 32-bit integer is set to 0xffffffff.

REKEY_INTERVAL = 224

class FSChaCha20Poly1305:
    """Rekeying wrapper AEAD around ChaCha20Poly1305."""

    def __init__(self, initial_key):
        self.key = initial_key
        self.packet_counter = 0

    def crypt(self, aad, text, is_decrypt):
        nonce = ((self.packet_counter % REKEY_INTERVAL).to_bytes(4, 'little') +
                 (self.packet_counter // REKEY_INTERVAL).to_bytes(8, 'little'))
        if is_decrypt:
            ret = aead_chacha20_poly1305_decrypt(self.key, nonce, aad, text)
        else:
            ret = aead_chacha20_poly1305_encrypt(self.key, nonce, aad, text)
        if (self.packet_counter + 1) % REKEY_INTERVAL == 0:
            rekey_nonce = b"\xFF\xFF\xFF\xFF" + nonce[4:]
            self.key = aead_chacha20_poly1305_encrypt(self.key, rekey_nonce, b"", b"\x00" * 32)[:32]
        self.packet_counter += 1
        return ret

    def decrypt(self, aad, ciphertext):
        return self.crypt(aad, ciphertext, True)

    def encrypt(self, aad, plaintext):
        return self.crypt(aad, plaintext, False)

The second is FSChaCha20, a (single) stream cipher which is used for the lengths of all packets. Encryption and decryption are identical here, so a single function crypt is exposed. It XORs the input with bytes generated using the ChaCha20 block function, rekeying every 224 chunks using the next 32 bytes of the block function output as new key. A chunk refers here to a single invocation of crypt. As explained before, the same cipher is used for 224 consecutive chunks, to avoid wasting cipher output. The nonce used for these batches of 224 chunks is composed of 4 zero bytes followed by the 64-bit little-endian encoding of the number of rekeyings performed. The block counter is reset to 0 after every rekeying.

class FSChaCha20:
    """Rekeying wrapper stream cipher around ChaCha20."""

    def __init__(self, initial_key):
        self.key = initial_key
        self.block_counter = 0
        self.chunk_counter = 0
        self.keystream = b''

    def get_keystream_bytes(self, nbytes):
        while len(self.keystream) < nbytes:
            nonce = ((0).to_bytes(4, 'little') +
                     (self.chunk_counter // REKEY_INTERVAL).to_bytes(8, 'little'))
            self.keystream += chacha20_block(self.key, nonce, self.block_counter)
            self.block_counter += 1
        ret = self.keystream[:nbytes]
        self.keystream = self.keystream[nbytes:]
        return ret

    def crypt(self, chunk):
        ks = self.get_keystream_bytes(len(chunk))
        ret = bytes([ks[i] ^ chunk[i] for i in range(len(chunk))])
        if ((self.chunk_counter + 1) % REKEY_INTERVAL) == 0:
            self.key = self.get_keystream_bytes(32)
            self.block_counter = 0
        self.chunk_counter += 1
        return ret
Overall packet encryption and decryption pseudocode

Encryption and decryption of packets then follow by composing the ciphers from the previous section as building blocks.

LENGTH_FIELD_LEN = 3
HEADER_LEN = 1
IGNORE_BIT_POS = 7

def v2_enc_packet(peer, contents, aad=b'', ignore=False):
    assert len(contents) <= 2**24 - 1
    header = (ignore << IGNORE_BIT_POS).to_bytes(HEADER_LEN, 'little')
    plaintext = header + contents
    aead_ciphertext = peer.send_P.encrypt(aad, plaintext)
    enc_contents_len = peer.send_L.encrypt(len(contents).to_bytes(LENGTH_FIELD_LEN, 'little'))
    return enc_contents_len + aead_ciphertext

CHACHA20POLY1305_EXPANSION = 16

def v2_receive_packet(peer, aad=b''):
    while True:
        enc_contents_len = receive(peer, LENGTH_FIELD_LEN)
        contents_len = int.from_bytes(peer.recv_L.crypt(enc_contents_len), 'little')
        aead_ciphertext = receive(peer, HEADER_LEN + contents_len + CHACHA20POLY1305_EXPANSION)
        plaintext = peer.recv_P.decrypt(aad, aead_ciphertext)
        if plaintext is None:
            disconnect(peer)
            break
        # Only the first packet is expected to have non-empty AAD.
        aad = b''
        header = plaintext[:HEADER_LEN]
        if not (header[0] & (1 << IGNORE_BIT_POS)):
            return plaintext[HEADER_LEN:]
Performance

Each v1 P2P message uses a double-SHA256 checksum truncated to 4 bytes. Roughly the same amount of computation power is required for encrypting and authenticating a v2 P2P message as proposed.

Application layer specification

v2 Bitcoin P2P message structure

v2 Bitcoin P2P transport layer packets use the encrypted message structure shown above. An unencrypted application layer contents is composed of:

Field Size in bytes Comments
message_type 1 or 13 either a one byte ID in range 1..255 or b'\x00' followed by a 12-byte ASCII message type (as in the v1 P2P protocol)
message_payload message_length message payload

If the first byte of message_type is b'\x00', the following 12 bytes are interpreted as an ASCII message type (as in the v1 P2P protocol), trailing padded with b'\x00' as necessary. If the first byte of message_type is in the range 1..255, it is interpreted as a message type ID. This structure results in smaller messages than the v1 protocol, as most messages sent/received will have a message type ID. We recommend reserving 1-byte type IDs for message types that are sent more than once per direction per connection.How do the lengths between v1 and v2 compare? For messages that use the 1-byte short message type ID, v2 packets use 3 bytes less per message than v1.Why not allow variable length long message type IDs? Allowing for variable length long IDs reduces the available 1-byte ID space by 12 (to encode the length itself) and incentivizes less descriptive message types. In addition, limiting message types to fixed lengths of 1 or 13 hampers traffic analysis.

The value of message_length is length minus the size of the message_type.

The following table lists currently defined message type IDs and the 12-byte ASCII message type (trimmed of trailing padding) that they are treated as equivalent to:

0 1 2 3
+0 (12 bytes follow) addr block blocktxn
+4 cmpctblock feefilter filteradd filterclear
+8 filterload getblocks getblocktxn getdata
+12 getheaders headers inv mempool
+16 merkleblock notfound ping pong
+20 sendcmpct tx getcfilters cfilter
+24 getcfheaders cfheaders getcfcheckpt cfcheckpt
+28 addrv2
≥29 | colspan=“4” | (undefined)

When a message type has both a 1-byte encoding and a 13-byte encoding defined, peers that support receiving that message type should accept messages using either encoding (e.g., if the “getblocktxn” message type is supported, then both the 1-byte b'\x0a' encoding and the 13-byte b'\x00getblocktxn\x00' should be supported, and behavior should not depend on which of the two encodings is received).

Additional message type IDs may be defined by other BIPs. They should be added to the message type IDs table to ease coordination.

Signaling specification

Signaling v2 support

Peers supporting the v2 transport protocol signal support by advertising the NODE_P2P_V2 = (1 << 11) service flag in addr relay. If met with immediate disconnection when establishing a v2 connection, clients implementing this proposal are encouraged to retry connecting using the v1 protocol.Why are v2 clients met with immediate disconnection encouraged to retry with a v1 connection? Service flags propagated through untrusted intermediaries using ADDR and ADDRV2 P2P messages and are OR’ed when received from multiple sources. An untrusted intermediary could falsely advertise a potential peer as supportive of v2 connections. Connection downgrades to v1 mitigate the risk of a network participant being blackholed via false advertising.

Test Vectors

For development and testing purposes, we provide a collection of test vectors in CSV format, and a naive, highly inefficient, reference implementation of the relevant algorithms. This code is for demonstration purposes only:

Changelog

* 1.0.2 (2026-01-30)
  * Add message type ID table in auxiliary file
* 1.0.1 (2026-01-16)
  * Specify equivalence of 1-byte and 13-byte `message_type`
* 1.0.0 (2024-07-10)
  * Marked as Final

Rationale and References

Acknowledgements

Thanks to everyone (last name order) that helped invent and develop the ideas in this proposal:

  BIP: 339
  Layer: Peer Services
  Title: WTXID-based transaction relay
  Authors: Suhas Daftuar <sdaftuar@chaincode.com>
  Status: Deployed
  Type: Specification
  Assigned: 2020-02-03
  License: BSD-2-Clause