forked from solana-labs/solana
-
Notifications
You must be signed in to change notification settings - Fork 1k
TLV: better impl and a separate crate #7694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
alexpyattaev
wants to merge
7
commits into
anza-xyz:master
Choose a base branch
from
alexpyattaev:tlv
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
1e10c18
Add TLV crate
alexpyattaev aa8d011
moving to wincode
alexpyattaev d76814f
address comments from Zach
alexpyattaev d659356
zach
alexpyattaev 39939d4
new way is definitely better
alexpyattaev 443994e
progress
alexpyattaev d9f3ed8
missing files
alexpyattaev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(¬iarize_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); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?