Wire Formats & BCS Conventions

For every route below, two equivalent descriptions are given:

  • Rust client: code snippet assuming you depend on sui-types (and consensus-core for /v1/accepted).
  • Raw BCS schema: structural description for clients that cannot import Sui crates.

The raw schemas reference sui-types primitives (SenderSignedData, TransactionEffects, TransactionEvents, etc.) which are themselves complex nested BCS types. The canonical authoritative definitions live in the Sui source tree under crates/sui-types/src/; many language ecosystems also ship Sui-SDK packages with type-equivalent decoders.

BCS encoding conventions

  • Fields are emitted in struct-declaration order with no field names.
  • Fixed-size [u8; N] arrays serialize as N length-prefixed bytes — the length prefix is a ULEB128 byte (0x20 for 32-byte digests).
  • u8/u16/u32/u64 are little-endian.
  • Vec<T> = ULEB128 length, then length consecutive T values.
  • Option<T> = 0x00 (None) or 0x01 <T> (Some).
  • Enums = ULEB128 variant index, then variant payload.
  • String = Vec<u8> (UTF-8 bytes with a length prefix).

/v1/incoming — BCS Transaction

Each WS frame is one BCS-encoded sui_types::transaction::Transaction, byte-identical to the bytes the submitting client sent to the validator’s gRPC endpoint.

Transaction is Envelope<SenderSignedData, EmptySignInfo>. EmptySignInfo is a zero-sized unit struct, so the on-wire bytes equal a bare SenderSignedData.

Rust client:

use sui_types::transaction::Transaction;

let tx: Transaction = bcs::from_bytes(&frame)?;
let digest = tx.digest();                       // &TransactionDigest
let sender = tx.transaction_data().sender();    // SuiAddress

Raw BCS schema:

Transaction := SenderSignedData

SenderSignedData := Vec<SenderSignedTransaction>

SenderSignedTransaction := {
    intent_message:  IntentMessage<TransactionData>,
    tx_signatures:   Vec<GenericSignature>,
}

IntentMessage<T> := {
    intent: Intent (3 bytes — scope, version, app_id, each u8),
    value:  T,
}

TransactionData, GenericSignature, etc. are defined in sui-types/src/transaction.rs and sui-types/src/crypto.rs. Most Sui SDKs already include decoders.

/v1/accepted — BCS SignedBlock

Each WS frame is one BCS-encoded consensus_core::block::SignedBlock — exactly the bytes the validator received on the wire from a peer. One block contains many transactions; each transaction inside is itself a BCS-encoded ConsensusTransaction.

Rust client (using consensus-core’s helper):

use sui_types::messages_consensus::ConsensusTransaction;
use consensus_core::extract_transactions_from_signed_block_bytes;

let raw_txs = extract_transactions_from_signed_block_bytes(&frame)?;
for raw in raw_txs {
    let consensus_tx: ConsensusTransaction = bcs::from_bytes(&raw)?;
    if let Some(user_tx) = consensus_tx.kind.into_user_transaction() {
        let digest = user_tx.digest();
        let sender_signed = user_tx.into_data();
        // … your processing …
    }
    // else: system message, skip
}

Raw BCS schema:

SignedBlock := {
    inner:     Block,
    signature: Vec<u8>,    // BCS Bytes — length-prefixed
}

Block := enum {
    0x00: BlockV1,
    0x01: BlockV2,
}

BlockV1 := {
    epoch:               u64,
    round:               u32,
    author:              u32,                  // AuthorityIndex
    timestamp_ms:        u64,
    ancestors:           Vec<BlockRef>,
    transactions:        Vec<ConsensusTx>,     // user/system txs in the block
    commit_votes:        Vec<CommitVote>,
    misbehavior_reports: Vec<MisbehaviorReport>,
}

BlockV2 := {
    epoch:               u64,
    round:               u32,
    author:              u32,
    timestamp_ms:        u64,
    ancestors:           Vec<BlockRef>,
    transactions:        Vec<ConsensusTx>,
    transaction_votes:   Vec<BlockTransactionVotes>,   // extra vs V1
    commit_votes:        Vec<CommitVote>,
    misbehavior_reports: Vec<MisbehaviorReport>,
}

ConsensusTx := {
    data: Vec<u8>,    // BCS-encoded ConsensusTransaction (see below)
}

BlockRef := {
    round:  u32,
    author: u32,
    digest: [u8; 32],    // BCS: 0x20 prefix + 32 digest bytes
}

ConsensusTransaction := {
    tracking_id: [u8; 8],
    kind:        ConsensusTransactionKind,
}

ConsensusTransactionKind := enum {
    // … many system-message variants — skip …
    UserTransaction(Transaction),                      // legacy V1
    UserTransactionV2(TransactionWithClaims),
    // … more system variants — skip …
}

TransactionWithClaims := {
    tx:     SenderSignedData,
    claims: Vec<TransactionClaim>,
}

The variant indices for ConsensusTransactionKind are stable; the exact index of UserTransaction and UserTransactionV2 is defined in sui-types/src/messages_consensus.rs. Non-user variants can be skipped.

For each UserTransaction* you encounter, the inner SenderSignedData is identical in shape to the /v1/incoming payload.

CommitVote, BlockTransactionVotes, MisbehaviorReport, TransactionClaim are defined in consensus-core/src/{block,commit}.rs and sui-types/src/messages_consensus.rs.

/v1/pending — BCS PendingTxStreamMessage

pub struct PendingTxStreamMessage {
    pub tx_digest:          TransactionDigest,
    pub sender_signed_data: SenderSignedData,
    pub consensus_round:    u64,
    pub timestamp_ms:       u64,
}

Rust client:

let msg: PendingTxStreamMessage = bcs::from_bytes(&frame)?;

Raw BCS schema:

PendingTxStreamMessage := {
    tx_digest:          [u8; 32],         // wire: 0x20 prefix + 32 bytes
    sender_signed_data: SenderSignedData, // same shape as /v1/incoming
    consensus_round:    u64,
    timestamp_ms:       u64,              // milliseconds since unix epoch
}

System transactions are filtered server-side and never appear on this stream.

/v1/executed — BCS TxStreamMessage

pub struct TxStreamMessage {
    pub tx_digest:          TransactionDigest,
    pub sender:             SuiAddress,
    pub sender_signed_data: SenderSignedData,
    pub effects:            TransactionEffects,
    pub events:             TransactionEvents,
    pub timestamp_ms:       u64,
}

Rust client:

let msg: TxStreamMessage = bcs::from_bytes(&frame)?;

Raw BCS schema:

TxStreamMessage := {
    tx_digest:          [u8; 32],
    sender:             [u8; 32],          // SuiAddress
    sender_signed_data: SenderSignedData,
    effects:            TransactionEffects, // enum, versioned
    events:             TransactionEvents,
    timestamp_ms:       u64,
}

TransactionEffects is a versioned enum (V1 / V2); BCS encodes the variant index as a ULEB128 byte followed by the variant payload. TransactionEvents is Vec<Event>. Both are defined in sui-types/src/effects.rs.

System transactions are filtered server-side and never appear on this stream.

What's next?