Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ members = [
"test-validator",
"thread-manager",
"tls-utils",
"tlv",
"tlv/tlv-mac",
"tokens",
"tps-client",
"tpu-client",
Expand Down Expand Up @@ -566,6 +568,7 @@ solana-sysvar-id = "3.0.0"
solana-test-validator = { path = "test-validator", version = "=4.0.0-alpha.0" }
solana-time-utils = "3.0.0"
solana-tls-utils = { path = "tls-utils", version = "=4.0.0-alpha.0", features = ["agave-unstable-api"] }
solana-tlv = { path = "tlv", version = "=4.0.0-alpha.0" }
solana-tps-client = { path = "tps-client", version = "=4.0.0-alpha.0", features = ["agave-unstable-api"] }
solana-tpu-client = { path = "tpu-client", version = "=4.0.0-alpha.0", default-features = false, features = ["agave-unstable-api"] }
solana-tpu-client-next = { path = "tpu-client-next", version = "=4.0.0-alpha.0", features = ["agave-unstable-api"] }
Expand Down
35 changes: 35 additions & 0 deletions tlv/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "solana-tlv"
description = "Solana TLV implementation"
documentation = "https://docs.rs/solana-tlv"
version = { workspace = true }
authors = { workspace = true }
repository = { workspace = true }
homepage = { workspace = true }
license = { workspace = true }
edition = { workspace = true }
publish = false

[features]
agave-unstable-api = []

[dependencies]
bytes = { workspace = true }
chacha20 = "0.9.1"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to have those in workspace imports as well?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bw-solana what is the way to go here?

chacha20poly1305 = { version = "0.10.1" }
poly1305 = "0.8.0"
thiserror = { workspace = true }
wincode = { workspace = true, features = ["derive"] }
solana-short-vec = { workspace = true }

[dev-dependencies]
bencher = { workspace = true }
bincode = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_with = { workspace = true, features = ["macros"] }
[lints]
workspace = true

[[bench]]
name = "tlv_vs_wincode"
harness = false
64 changes: 64 additions & 0 deletions tlv/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
## Tag-Length-Value data support for Solana

TLV (Type Length Value) is a well-established format to encode binary data on the wire, offering major advantages compared to most alternatives:
1. Ability to evolve existing protocols without hard version switch
2. Efficient parsing and serialization
3. Perfect forward compatibility

This is somewhat similar to protobuf, except that the receiver does not need to be
able to parse all of the records to be able to read the others.

## Wire format

A packet consists of a sequence of byte-aligned records. Each record contains:
* tag:u8 - 1 byte, can not be zero
* length:u16 - 1-3 bytes on the wire (1 byte if less than 127 bytes, uses solana-short-vec impl)
* value - 1..MTU bytes

The records can be appended as needed to form compound packets. As a safety precaution,
maximal size of any value is capped at MAX_VALUE_LENGTH = 1500 bytes.

## Defining enums

Any rust enum can be turned into a TLV compatible encoding with a macro:
```rust
use solana_tlv::{define_tlv_enum, signature::Signature};
use bytes::Bytes;

define_tlv_enum! (pub(crate) enum Extension {
1=>Thing(u64), // this will use bincode
3=>DoGood(()), // this will store the tag and no data
4=>Mac(Signature<16>), // and this allows to sign packets
#[raw]
5=>ByteArray(Bytes), // this will get bytes included verbatim
});
```

Variant tags must be unique. Reusing them causes parsing errors.

Intended workflow:
```rust
use bytes::{Bytes, BytesMut};

let tlv_data = vec![
Extension::Thing(42),
Extension::ByteArray(Bytes::from(vec![77u8; 256])),
];
let mut buffer = BytesMut::with_capacity(2000);
serialize_into_buffer(&tlv_data, &mut buffer).unwrap();
// send buffer over the wire
let recovered_data: Vec<ExtensionNew> = deserialize_from_buffer(buffer.freeze()).collect();
```

## Signatures

A `solana_tlv_mac::Signature` entry can be attached to the end of a message to sign the whole message.

## Performance

This crate has not been heavily optimized and likely has room for further improvement

## Caveats

Since the `define_tlv_enum` is a macro, you need to include serde into dependencies of any crate using the
macro.
179 changes: 179 additions & 0 deletions tlv/benches/tlv_vs_wincode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#![allow(clippy::arithmetic_side_effects)]

#[macro_use]
extern crate bencher;

use {
bencher::Bencher,
/*bytes::BytesMut,
serde_with::serde_as,
solana_short_vec as short_vec,
solana_tlv::*,
std::mem::MaybeUninit,
wincode::{
containers::{self, Pod},
io,
len::ShortU16Len,
SchemaRead,
},
wincode_derive::{SchemaRead, SchemaWrite},*/
};

fn tlv_roundtrip(_slot_num: u64) {
/* use serde::{Deserialize, Serialize};

#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct Finalize {
pubkey: [u8; 32],
#[serde_as(as = "[_; 96]")]
bls_signature: [u8; 96],
}

#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
struct NotarizeCert {
#[serde_as(as = "[_; 96]")]
bls_signature: [u8; 96],
#[serde(with = "short_vec")]
bitmap: Vec<u8>,
}

define_tlv_enum! (pub(crate) enum AlpenglowVotor {
1=>Slot(u64),
10=>Finalize(Finalize),
11=>NotarizeCert(NotarizeCert),
});
let notar_cert = NotarizeCert {
bitmap: vec![42u8; 2000 / 8],
bls_signature: [7; 96],
};
let final_vote = Finalize {
pubkey: [3; 32],
bls_signature: [7; 96],
};

// allocate space for a packet and fill it with data
let mut buffer = BytesMut::with_capacity(1200);
let entries = [
AlpenglowVotor::Slot(slot_num),
AlpenglowVotor::Finalize(final_vote),
AlpenglowVotor::NotarizeCert(notar_cert),
];
serialize_into_buffer(&entries, &mut buffer).unwrap();

let buffer = buffer.freeze();
let mut recovered = vec![];
for (_size, maybe_record) in TlvIter::new(buffer) {
let maybe_record: Result<AlpenglowVotor, _> = maybe_record.try_into();
let record = maybe_record.unwrap();
recovered.push(record);
}
assert_eq!(entries.as_slice(), recovered.as_slice());
*/
}

fn wincode_roundtrip(_slot_num: u64) {
/*
#[derive(Debug, Clone, PartialEq, Eq, SchemaWrite, SchemaRead)]
struct Finalize {
pubkey: [u8; 32],
bls_signature: [u8; 96],
}

#[derive(Debug, Clone, PartialEq, Eq, SchemaWrite, SchemaRead)]
struct NotarizeCert {
bls_signature: [u8; 96],
#[wincode(with = "containers::Vec<Pod<_>, ShortU16Len>")]
bitmap: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq, SchemaWrite, SchemaRead)]
pub(crate) struct TlvRecord {
// type
pub(crate) typ: u8,
// length and serialized bytes of the value
#[wincode(with = "containers::Vec<Pod<_>, ShortU16Len>")]
pub(crate) bytes: Vec<u8>,
}

#[derive(Debug, Eq, PartialEq)]
enum AlpenglowVotor {
Slot(u64),
Finalize(Finalize),
NotarizeCert(NotarizeCert),
}
let notar_cert = NotarizeCert {
bitmap: vec![42u8; 2000 / 8],
bls_signature: [7; 96],
};
let final_vote = Finalize {
pubkey: [3; 32],
bls_signature: [7; 96],
};
let entries = [
AlpenglowVotor::Slot(slot_num),
AlpenglowVotor::Finalize(final_vote),
AlpenglowVotor::NotarizeCert(notar_cert),
];
let mut buffer = BytesMut::with_capacity(1200);

for e in entries.iter() {
let (typ, val) = match e {
AlpenglowVotor::Slot(slot) => (1, wincode::serialize(&slot)),
AlpenglowVotor::Finalize(finalize) => (10, wincode::serialize(&finalize)),
AlpenglowVotor::NotarizeCert(notiarize_cert) => {
(11, wincode::serialize(&notiarize_cert))
}
};
let val = val.unwrap();
let tlv = TlvRecord { typ, bytes: val };

let len = wincode::serialize_into(&tlv, buffer.spare_capacity_mut()).unwrap();
unsafe {
buffer.set_len(buffer.len() + len);
}
}

let mut buffer = io::Reader::new(&buffer);

let mut recovered = vec![];
loop {
let mut tlv_record: MaybeUninit<TlvRecord> = MaybeUninit::uninit();

let read_res = TlvRecord::read(&mut buffer, &mut tlv_record);
if read_res.is_err() {
break;
} else {
let tlv_record = unsafe { tlv_record.assume_init() };
let payload = match tlv_record.typ {
1 => AlpenglowVotor::Slot(wincode::deserialize(&tlv_record.bytes).unwrap()),
10 => AlpenglowVotor::Finalize(wincode::deserialize(&tlv_record.bytes).unwrap()),
11 => {
AlpenglowVotor::NotarizeCert(wincode::deserialize(&tlv_record.bytes).unwrap())
}
_ => panic!(),
};
recovered.push(payload);
}
}
assert_eq!(&recovered, &entries);
*/
}
fn tlv(bench: &mut Bencher) {
let mut counter = 0;
bench.iter(|| {
tlv_roundtrip(counter);
counter += 1;
})
}

fn wincode(bench: &mut Bencher) {
let mut counter = 0;
bench.iter(|| {
wincode_roundtrip(counter);
counter += 1;
});
}

benchmark_group!(benches, tlv, wincode);
benchmark_main!(benches);
Loading
Loading