MESSAGE-ENCAPSULATION-MECHANISM

FieldValue
NameMessage Encapsulation Mechanism
Slug91
Statusraw
CategoryStandards Track
EditorMarcin Pawlowski
ContributorsYoungjoon Lee [email protected], Alexander Mozeika [email protected], Mehmet Gonen [email protected], Álvaro Castro-Castilla [email protected], Daniel Kashepava [email protected], Daniel Sanchez Quiros [email protected], Filip Dimitrijevic [email protected]

Timeline

  • 2026-05-2967e498e — chore: fix math issues (#350)
  • 2026-05-28d45eed2 — Chore: mirror blochain specs into github/mdbook (#347)

Revision History

VersionChangesDate
1.0.0Initial revision.2026-04-09

Introduction

The message encapsulation mechanism is part of the Blend Protocol and it describes the cryptographic operations necessary for building and processing messages by a Blend node.

This document is part of the Formatting section. Please read through that document to better understand the context of the encapsulation mechanism and constructions used here.

Overview

The Message Encapsulation Mechanism is a core component of the Blend Protocol that ensures privacy and security during node-to-node message transmission. By implementing multiple encryption layers and cryptographic operations, this mechanism keeps messages confidential while concealing their origins.

The encapsulation process includes:

  • Building a multi-layered structure with public headers, private headers, and encrypted payloads
  • Using cryptographic keys and proofs for layer security and authentication
  • Applying verifiable random node selection for message routing
  • Using shared key derivation for secure inter-node communication

This document outlines the cryptographic notation, data structures, and algorithms essential to the encapsulation process, providing a complete specification for implementing this mechanism within the Blend Protocol.

Notation

  • is a collection of key pairs for a node with proofs of quota, where is the -th public key and is its corresponding private key, and is its proof of quota.
Ed25519PublicKey = bytes
Ed25519PrivateKey = bytes
KEY_SIZE = 32
ProofOfQuota = bytes
PROOF_OF_QUOTA_SIZE = 160

KeyCollection = List[KeyPair]

class KeyPair:
    signing_public_key: Ed25519PublicKey
    signing_private_key: Ed25519PrivateKey
    proof_of_quota: ProofOfQuota

class ProofOfQuota:
  key_nullifier: zkhash # 32 bytes
  proof: bytes # 128 bytes

For more information about key generation mechanism please refer to [1.0.0] Key Types and Generation.

For more information about proof of quota please refer to Proof of Quota.

  • is a public key of the node , which is globally accessible using the Service Declaration Protocol (SDP). We are using this notation to distinguish the origin of the key, hence the following simplified notation. For more information about Service Declaration Protocol please refer to [1.0.0] Service Declaration Protocol.

  • is the set of nodes globally accessible using the SDP.

Nodes = set[Ed25519PublicKey]  # set of signing public keys
  • is the number of nodes globally accessible using the SDP.
  • , is a shared key calculated between node and node using the -th key of the node , is the public key of the node retrieved from the SDP protocol and is its corresponding private key.
SharedKey = bytes  # KEY_SIZE
  • is the proof of selection of the public key to the node index from a set of all nodes .
ProofOfSelection = bytes
PROOF_OF_SELECTION_SIZE = 32

For more information about the proof of selection, please refer to Proof of Selection.

  • is a domain-separated hash function dedicated to the node index selection (the implementation of the hash function is blake2b).
def hashds(domain=b"BlendNode", data: bytes) -> bytes:
    return Blake2B.hash512(domain + data)
  • is a domain-separated hash function dedicated to the initialization of the blend header (the implementation of the hash function is blake2b).
def hashds(domain=b"BlendInitialization", data: bytes) -> bytes:
    return Blake2b.hash512(domain + data)
  • is a domain-separated hash function dedicated to the blend header encryption operations (the implementation of the hash function is blake2b).
def hashds(domain=b"BlendHeader", data: bytes) -> bytes:
    return Blake2b.hash512(domain + data)
  • is a domain-separated hash function dedicated to the payload encryption operations (the implementation of the hash function is blake2b).
def hashds(domain=b"BlendPayload", data: bytes) -> bytes:
    return Blake2b.hash512(domain + data)
  • is the maximal number of blending headers in the private header.
ENCAPSULATION_COUNT: int
def pseudo_random(domain: bytes, key: bytes, size: int) -> bytes:
    rand = BlakeRng.from_seed(hashds(domain, key)).generate(size)
    assert len(rand) == size
    return rand
  • returns the length of the expressed in bytes.
  • is a XOR operation.
def xor(a: bytes, b: bytes) -> bytes:
    assert len(a) == len(b)
    return bytes(x ^ y for x, y in zip(a, b))
  • is an encryption that uses a cryptographically secure pseudo-random bytes generator with a secret and payload .
def encrypt(data: bytes, key: bytes -> bytes:
    return xor(data, pseudo_random(b"BlendEncapsulation", key, len(data))))
  • is a decryption that uses using cryptographically secure pseudo-random bytes generator with a secret and payload .
def decrypt(data: bytes, key: bytes) -> bytes:
    return xor(data, pseudo_random(b"BlendEncapsulation", key, len(data))))

Construction

Message Structure

We start with a definition of the message structure that must be used to provide the protocol with the envisioned capabilities.

A node constructs a message according to the format presented below.

Diagram

class Message:
    public_header: PublicHeader
    private_header: PrivateHeader
    payload: EncryptedPayload
  1. is a public header:
  2. , version of the header, it is set to .
  3. , a public key from the set .
  4. , a corresponding proof of quota for the key from the set and contains its proof nullifier.
  5. , a signature of the concatenation of the -th encapsulation of the payload and the private header , that can be verified by the public key .
Signature = bytes
SIGNATURE_SIZE = 64

class PublicHeader:
    version: int = 1  # u8
    signing_public_key: Ed25519PublicKey
    proof_of_quota: ProofOfQuota
    signature: Signature
  1. is an encrypted private header:
  2. is a blending header:
    1. , a public key from the set .
    2. , a corresponding proof of quota for the key from the and contains its proof nullifier.
    3. , a signature of the concatenation of the -th encapsulation of the payload and the private header , that can be verified by public key .
    4. , a proof of selection of the node index assuming public key .
    5. , a flag that indicates that this is the last blending header.
PrivateHeader = List[EncryptedBlendingHeader]  # length: ENCAPSULATION_COUNT
EncryptedBlendingHeader = bytes

class BlendingHeader:
    signing_public_key: Ed25519PublicKey
    proof_of_quota: ProofOfQuota
    signature: Signature
    proof_of_selection: ProofOfSelection
    is_last: bool  # 1 byte
  1. is a payload.
EncryptedPayload = bytes

PAYLOAD_BODY_SIZE = 34 * 1024

class Payload:
    header: PayloadHeader
    body: bytes  # PAYLOAD_BODY_SIZE

class PayloadHeader:
    payload_type: PayloadType  # 1 byte
    body_len: int  # u16

class PayloadType(Enum):
    COVER = 0x00
    DATA = 0x01

Keys and Proof Generation

For simplicity of the presentation, we do not distinguish between signing and encryption keys. However, in practice, such a distinction is necessary, that is:

  • The contains Ephemeral Signing Keys (ESK) that are part of the PoQ generation and are used for message signing; these are included in the public and private headers.
  • Shared secret keys used for encryption of messages are generated from an Ephemeral Encryption Key (sender), which is derived from the ESK, and from a Non-ephemeral Encryption Key (NEK) (receiver), which is derived from a Non-ephemeral Signing Key (NSK) retrieved from the SDP protocol.

For more information, look at [1.0.0] Key Types and Generation.

The first step is to generate a set of keys alongside all necessary proofs that will be used in the next steps of the algorithm.

  1. Generate the collection , where defines the number of encapsulation layers such that .
def generate_key_collection(num_layers: int) -> List[KeyPair]:
    assert num_layers <= ENCAPSULATION_COUNT
    # Generate `num_layers` random KeyPairs non-deterministically.
    return [KeyPair.random() for _ in range(num_layers)]

The key collection generation requires generation of Proof of Quota ([1.0.1] Proof of Quota) for each key, as defined in the following steps.

  • The ProofOfQuotaPublic (Public values) structure must be filled with public information:

    1. session, core_quota, leader_quota, core_root, pol_epoch_nonce, pol_t0, pol_t1, pol_ledger_aged are retrieved from the blockchain.

    2. K_part_one and K_part_two are first and second part of the signature key (KeyPair) generated by the above generate_key_collection.

class ProofOfQuotaPublic:
        session: int          # Session number (uint64)
        core_quota: int       # Allowed messages per session for core nodes
        leader_quota: int     # Allowed messages per session for potential leaders
        core_root: zkhash     # Merkle root of zk_id of the core nodes
        K_part_one: int       # First part of the signature public key (16 bytes)
        K_part_two: int       # Second part of the signature public key (16 bytes)
        pol_epoch_nonce: int  # PoL Epoch nonce
        pol_t0: int           # PoL constant t0
        pol_t1: int           # PoL constant t1
        pol_ledger_aged: zkhash # Merkle root of the PoL eligible notes
        # Outputs:
        key_nullifier: zkhash   # derived from session, private index and private sk
  • The ProofOfQuotaWitness (Witness) structure must be filled as follows:

    1. If the message contains cover traffic then:

      1. We assume that the core quota is used and the selector=0 value must be specified.
      2. The index counts the number of cover messages and must be below core_quota.
      3. The core_sk, core_path, core_path_selector are filled by the node to prove that the node is the core node.
      4. The rest of the ProofOfQuotaWitness, is filled with arbitrary data.
    2. If the message contains data then:

      1. We assume that the leader quota is used and the selector=1 value must be specified.
      2. The index counts the number of data messages and must be below leader_quota.
      3. The core_sk, core_path, core_path_selector are filled with arbitrary data.
      4. The rest is filled with Proof of Leadership (PoL — [1.1.0] Proof of Leadership) related data.
class ProofOfQuotaWitness:
        index: int                            # This is the index of the generated key. Limiting this index limits the maximum number of key generated. (20 bits)
        selector: int                         # Indicates if it's a leader (=1) or a core node (=0)
        # This part is filled randomly by potential leaders
        core_sk: zkhash                       # sk corresponding to the zk_id of the core node
        core_path: list[zkhash]               # Merkle path proving zk_id membership (len = 20)
        core_path_selectors: list[bool]       # Indicates how to read the core_path (if Merkle nodes are left or right in the path)
        # This part is filled randomly by core nodes
        pol_sl: int                           # PoL slot
        pol_sk_starting_slot: int             # PoL starting slot of the slot secrets
        pol_note_value: int                   # PoL note value
        pol_note_tx_hash: zkhash              # PoL note transaction
        pol_note_output_number: int           # PoL note transaction output number
        pol_noteid_path: list[zkhash]         # PoL Merkle path proving noteID membership in ledger aged (len = 32)
        pol_noteid_path_selectors: list[bool] # Indicates how to read the note_path (if Merkle nodes are left or right in the path)
        pol_slot_secret: int                  # PoL slot secret corresponding to sl
        pol_slot_secret_path: list[zkhash]    # PoL slot secret Merkle path to sk_secrets_root (len = 25)
  • The ProofOfQuotaPublic and ProofOfQuotaWitness are passed to the zero-knowledge circuits that generate the proof which derives the key_nullifier () from session, private index, private secret key during proof generation.
  1. Select nodes from the set of nodes in a random and verifiable manner. For , select , where is a selection randomness (using little-endian encoding), a shared secret derived during Proof of Quota generation, the output of the is returns 8 bytes (little-endian).
def select_nodes(key_collection: List[KeyPair], nodes: List[Node]) -> List[Node]:
    selected_nodes = []
    for keypair in key_collection:
        rand = pseudo_random(
          b"BlendNode",
            selection_randomness,
            8
        )
        index = modular_bytes(rand, NUM_NODES)
        selected_nodes.append(nodes[index])
    return selected_nodes

def modular_bytes(data: bytes, modulus: int) -> int:
    # Convert data into an unsigned big integer using little-endian.
    return int.from_bytes(data, byteorder='little') % modulus
  1. Generate proofs of selection for , which proves that the public key correctly maps to the index from the set of nodes .
  2. For , retrieve public keys for all selected nodes using the SDP protocol (defined as provider_id in Identifiers).
def blend_node_signing_public_keys(selected_nodes: List[Node]) -> List[Ed25519PublicKey]:
    return [node.signing_public_key for node in selected_nodes]
  1. For , calculate shared keys from a set of public keys of selected nodes .
def derive_shared_keys(key_collection: List[KeyPair], blend_node_signing_public_keys: List[Ed25519PublicKey]) -> List[SharedKey]:
    assert len(key_collection) == len(blend_node_signing_public_keys)
    assert len(key_collection) <= ENCAPSULATION_COUNT

    shared_keys = []
    for (keypair, blend_node_signing_public_key) in zip(key_collection, blend_node_signing_public_keys):
        encryption_private_key = signing_private_key.derive_x25519()
        blend_node_encryption_public_key = blend_node_signing_public_key
        shared_key = diffie_hellman(encryption_private_key, blend_node_encryption_public_key)
        shared_keys.append(shared_key)
    return shared_keys

In step 2 of the algorithm above, the sender constructs a blending path from nodes sampled at random but in a verifiable manner. The nodes are selected deterministically (and randomly) by the key value. The key to node mapping is proven in step 3.

The node selection proof is constructed in such a way that it proves only the fact that the key used for the encryption maps correctly to the node index from the stable set of nodes . This proof should be considered a private proof intended only for the recipient blend node.

This mechanism intends to limit the possibility of “double spending” the emission token. This restricts the sender's ability to use the same emission token twice, first for constructing and emitting a message and then for claiming a reward for it.

For more information about proof of selection please refer to Proof of Selection.

Message Initialization

The second step is to create an empty message and fill the private header with random values.

  1. Create an empty message (filled with zeros).
  2. Randomize the private header: For , set , where is some random value.
def randomize_private_header() -> PrivateHeader:
    blending_headers = []
    for _ in range(ENCAPSULATION_COUNT):
        blending_header = pseudo_random(b"BlendRandom", entropy(), BlendingHeader.SIZE)
        blending_headers.append(blending_header)
    return blending_headers
  1. Fill the last blend headers with reconstructable payloads: For , do the following:
  2. .
def fill_last_blending_headers(private_header: PrivateHeader, shared_keys: List[SharedKeys]) -> PrivateHeader:
    assert len(private_header) == ENCAPSULATION_COUNT
    assert len(shared_keys) <= ENCAPSULATION_COUNT

    pseudo_random_blending_headers = []
    for shared_key in shared_keys:
        r1 = pseudo_random(b"BlendInitialization", shared_key + b"\x01", KEY_SIZE)
        r2 = pseudo_random(b"BlendInitialization", shared_key + b"\x02", PROOF_OF_QUOTA_SIZE)
        r3 = pseudo_random(b"BlendInitialization", shared_key + b"\x03", SIGNATURE_SIZE)
        r4 = pseudo_random(b"BlendInitialization", shared_key + b"\x04", PROOF_OF_SELECTION_SIZE)
        is_last = False
        pseudo_random_blending_headers.append(r1 + r2 + r3 + r4 + is_last)

    # Replace the last `len(shared_keys)` blending headers.
    private_header[-num_layers:] = pseudo_random_blending_headers
    return private_header
  1. Encrypt the last blend headers in a reconstructable manner: For , for , encrypt blend header .
def encrypt_last_blending_headers(private_header: PrivateHeader, shared_keys: List[SharedKeys]) -> PrivateHeader:
    assert len(private_header) == ENCAPSULATION_COUNT
    assert len(shared_keys) <= ENCAPSULATION_COUNT

    for i, _ in enumerate(shared_keys):
        index = len(private_header) - i - 1
        for shared_key in shared_keys[:i + 1]:
            private_header[index] = encrypt(private_header[index], shared_key)

    return private_header

This prevents leakage of the encryption sequence when a message is encapsulated less than times, and enables us to encode the header in a way that it can be reconstructed during the decapsulation.

Message Encapsulation

The final part of the algorithm is the true encapsulation o f the payload. That is, given the payload and number of encapsulations we do the following.

For do the following:

  1. If then generate a new ephemeral key pair: .

  2. Calculate the signature of the concatenation of the current header and payload: .

  3. Using the shared key , encrypt the payload: Note that the uniqueness of the key stream is preserved as the encryption is done on a domain separated checksum of the shared key, which is renders a different key stream than the encryption of the header.

  4. Shift blending headers by one downward: for . The first blending header is now empty, and the last blending header is truncated.

  5. Fill the blending header , where refers to the top position:

  6. If then:

    1. Fill the proof of quota with random data:
    2. Set the last flag to 1:
  7. Else set the last flag to 0:

  8. .

  9. Using shared key , encrypt the private header : For each using a shared key , encrypt the blending header: .

Fill in the public header: .

The message is encapsulated.

def encapsulate(
        private_header: PrivateHeader,
        payload: Payload,
        shared_keys: List[SharedKeys],
        key_collection: List[KeyPair],
        list_of_pos: List[ProofOfSelection]
) -> bytes:
    # Step 1 ~ 6: Encapsulate private header and payload
    prev_keypair = KeyPair.random()
    is_first_selected = True
    for shared_key, keypair, proof_of_selection) in zip(shared_keys, key_collection, list_of_pos):
        private_header, payload = encapsulate_private_part(
            private_header,
            payload.bytes(),
            shared_key,
            prev_keypair.signing_private_key,
            prev_keypair.proof_of_quota,
            proof_of_selection,
            # The first encapsulation is for the last decapsulation.
            is_last=is_first_selected,
        )
        prev_keypair = keypair
        is_first = False

    # Fill in the public header
    public_header = PublicHeader(
        prev_keypair.signing_public_key,
        prev_keypair.proof_of_quota,
        signature=sign(private_part, prev_keypair.signing_private_key),
        version=1,
    )

    return public_header.bytes() + b"".join(private_headers) + payload

def encapsulate_private_part(
    private_header: PrivateHeader,
    payload: EncryptedPayload,
    shared_key: SharedKey,
    signing_private_key: Ed25519PrivateKey,
    proof_of_quota: ProofOfQuota,
    proof_of_selection: ProofOfSelection,
    is_last: bool
) -> bytes:
    # Step 2: Calculate a signature on `private_header + payload`.
    signature = sign(
        signing_body(private_header, payload),
        signing_private_key
    )
    # Step 3: Encrypt the payload
    payload = encrypt(payload, shared_key)
    # Step 4: Shift blending headers by one rightward.
    private_header.pop()  # Remove the last blending header
    # Step 5: Add the new blending header to the front.
    blending_header = BlendingHeader(
        signing_private_key.public(),
        proof_of_quota,
        signature,
        proof_of_selection,
        is_last
    )
    private_header.insert(0, blending_header.bytes())
    # Step 6: Encrypt the private header
    for i, _ in enumerate(private_header):
        private_header[i] = encrypt(private_header[i], shared_key)

    return private_header, payload


def signing_body(private_header: PrivateHeader, payload: EncryptedPayload) -> bytes:
    return b"".join(private_headers) + payload

Message Decapsulation

If a message is received by the node and its public header is correct - that is, it was verified according to the relay logic defined here: Relaying - then the node executes the following logic:

  1. Calculate the shared secret. Using the key from the public header of the message and the private key of the node calculate: .

  2. Decrypt the private header using the shared key . For each using a shared key decrypt the blending header: .

  3. Verify the header:

  4. If the proof is not correct, discard the message. That is, if the node index does not correspond to the , then the message must be rejected.

  5. If the key was already seen, discard the message.

  6. If the proof is incorrect, discard the message.

  7. If equals , then stop processing the message and process the payload.

  8. Using the blending header , set the public header: .

  9. Decrypt the payload, using the shared key : .

  10. Reconstruct the blend header:

  11. .

  12. Encrypt the blending header:

  13. Shift blending headers by one upward: for . The first blending header is truncated, and the last blending header is empty.

  14. Reconstruct the private header: , .

  15. If the signature from the public header does not match the signature of the reconstructed header and the decrypted payload, discard the message: .

  16. The message is decapsulated.

  17. Follow the message processing logic: Processing.

def decapsulate(
    message: bytes,
    signing_private_key: Ed25519PrivateKey
) -> bytes:
    # Step 1: Derive the shared key.
    encryption_private_key = signing_private_key.derive_x25519()
    public_header = PublicHeader.from_bytes(
        message[Header.SIZE : Header.SIZE + PublicHeader.SIZE]
    )
    shared_key = diffie_hellman(
        encryption_private_key,
        public_header.signing_public_key.derive_x25519()
    )

    # Step 2: Decrypt the private header
    private_header = message[
        Header.SIZE + PublicHeader.SIZE:
        Header.SIZE + PublicHeader.SIZE + (BlendingHeader.SIZE * ENCAPSULATION_COUNT)
    ]
    for i, _ in enumerate(private_header):
        private_header[i] = decrypt(private_header[i], shared_key)

    # Step 3: Verify the first blending header
    first_blending_header = BlendingHeader.from_bytes(private_header[0])
    first_blending_header.validate()

    # Step 4: Construct the new public header
    public_header = PublicHeader(
        first_blending_header.signing_public_key,
        first_blending_header.proof_of_quota,
        first_blending_header.signature,
        version= 1,
    )

    # Step 5: Decrypt the payload
    payload_offset = (
        Header.SIZE + PublicHeader.SIZE + (BlendingHeader.SIZE * ENCAPSULATION_COUNT)
    )
    payload = message[payload_offset:]
    payload = decrypt(payload, shared_key)

    # Step 6: Reconstruct the new blending header
    r1 = pseudo_random(b"BlendInitialization", shared_key + b"\x01", KEY_SIZE)
    r2 = pseudo_random(b"BlendInitialization", shared_key + b"\x02", PROOF_OF_QUOTA_SIZE)
    r3 = pseudo_random(b"BlendInitialization", shared_key + b"\x03", SIGNATURE_SIZE)
    r4 = pseudo_random(b"BlendInitialization", shared_key + b"\x04", PROOF_OF_SELECTION_SIZE)
    is_last = False

    # Step 7: Encrypt the new blending header
    encrypted_new_blending_header = encrypt(r1 + r2 + r3 + r4 + is_last, shared_key)

    # Step 8: Shift blending headers by one leftward.
    private_header.pop(0)  # Remove the first blending header.

    # Step 9: Add the new blending header to the end.
    private_header.append(encrypted_new_blending_header)

    # Step 10: Verify the signature
    verify_signature(
        first_blending_header.signature,
        signing_body(private_header, payload)
        first_blending_header.signing_public_key,
    )

    header = message[0:Header.SIZE]
    return header + public_header.bytes() + b"".join(private_header) + payload

Appendix

Example

Let us look at an example of the above mechanism. Let us assume that . We are omitting protocol version in the header for simplicity.

Initialization

  1. Create an empty message:
  2. Randomize the private header: ,

,

,

,

.

  1. Fill the last blend headers with reconstructable payloads: ,

,

,

,

.

  1. Encrypt the last blend headers in a reconstructable manner: ,

,

,

,

.

Encapsulation

:

  1. Generate a new ephemeral key pair: .
  2. Calculate the signature of the header and the payload: .
  3. Using shared key encrypt the payload: .
  4. Shift blending headers by one down:     ,

    ,

    ,

    ,

    .

  1. Fill the first blending header (the in this case):     ,

    ,

    ,

    ,

    .

  1. Using shared key encrypt the private header:     ,

    ,

    ,

    ,

    .

:

  1. .
  2. Calculate the signature of the header and the payload: .
  3. Using shared key encrypt the payload: .
  4. Shift blending headers by one down:     ,

    ,

    ,

    ,

    .

  1. Fill the first blending header:     ,

    ,

    ,

    ,

    .

  1. Using shared key encrypt the private header:     ,

    ,

    ,

    ,

    .

:

  1. .
  2. Calculate the signature of the header and the payload: .
  3. Using shared key encrypt the payload: .
  4. Shift blending headers by one down:     ,

    ,

    ,

    ,

    .

  1. Fill the first blending header:     ,

    ,

    ,

    ,

    .

  1. Using shared key encrypt the private header:     ,

    ,

    ,

    ,

    .

The above calculations give us the final message where:

,

,

,

,

,

,

.

Decapsulation

Now let us take the above message and decapsulate it. We verify that the node doing the processing is the rightful recipient of the message and that the public header is correct.

:

  1. Calculate shared secret: , where and is the private part of the public key of the node .
  2. Decrypt the header:     ,

    ,

    ,

    ,

    

  1. Verify the header: 1. If the proof of selection fails then stop. 2. If the key was already seen, discard the message. 3. If the proof is incorrect, discard the message.

  2. Reconstruct the public header:     

  3. Decrypt the payload: .

  4. Reconstruct the blend header:     ,

    ,

    ,

    ,

    ,

    

  1. Encrypt the blend header: .
  2. Shift blending headers by one upward, the first blending header is discarded: .
  3. Reconstruct the private header:     ,

    ,

    ,

    ,

    

  1. If the signature from the public header does not match the signature of the reconstructed header and the decrypted payload then discard the message:
  2. The message is decapsulated.
  3. Follow the processing logic: Processing.

:

  1. Calculate shared secret: , where and is the private part of the public key of the node .
  2. Decrypt the header:     ,

    ,

    ,

    ,

    

  1. Verify the header: 1. If the proof of selection fails then stop. 2. If the key was already seen, discard the message. 3. If the proof is incorrect, discard the message.

  2. Reconstruct the public header:     

  3. Decrypt the payload: .

  4. Reconstruct the blend header:     ,

    ,

    ,

    ,

    ,

    

  1. Encrypt the reconstructed blend header: .
  2. Shift blending headers by one upward, the first blending header is discarded: .
  3. Reconstruct the private header:     ,

    ,

    ,

    ,

    

  1. Check the signature:
  2. The message is decapsulated.
  3. Follow the message processing logic: Processing.

:

  1. Calculate shared secret: , where and is the private part of the public key of the node .
  2. Decrypt the private header:     ,

    ,

    ,

    ,

    

  1. Verify the header: 1. If the proof of selection fails then stop. 2. If the key was already seen, discard the message. 3. If the proof is incorrect, discard the message.

  2. Reconstruct the public header:     

  3. Decrypt the payload: .

  4. Reconstruct the blend header:     ,

    ,

    ,

    ,

    ,

    

  1. Encrypt the reconstructed blend header: .
  2. Shift blending headers by one upward, the first blending header is discarded: .
  3. Reconstruct the private header:     ,

    ,

    ,

    ,

    

  1. If the signature from the public header does not match the signature of the reconstructed header and the decrypted payload then discard the message: .
  2. The message is decapsulated.
  3. Follow the message processing logic: Processing.