Wire Formats & BCS Conventions
For every route below, two equivalent descriptions are given:
- Rust client: code snippet assuming you depend on
sui-types(andconsensus-corefor/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 asNlength-prefixed bytes — the length prefix is a ULEB128 byte (0x20for 32-byte digests). u8/u16/u32/u64are little-endian.Vec<T>= ULEB128 length, thenlengthconsecutiveTvalues.Option<T>=0x00(None) or0x01 <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(); // SuiAddressRaw 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?
Updated 3 days ago