Skip to content

Commit

Permalink
nmt: switch NS convention to big-endian
Browse files Browse the repository at this point in the history
The canonical representation of a namespace ID is [u8; N] (N=4 for now).
However, it seems to be useful at least for debugging purposes to
parse and display the namespace as an unsigned integer.

Prior this change, the integer form was defined as little-endian, just
because it's a default. Most of modern architectures are LE by default,
wasm is LE, etc. Mostly it doesn't matter, performance differences
are neglegible either way 99% of the time. I suppose the reason for
this is that LE has a marginal advantage because of the things like
that you don't need byte shuffle when reinterpreting value type (e.g.
u32 and u16) behind the same pointer and stuff like that.

However, big endian also has it's benefits. First is a human readable
representation, which is important here since this is more of a
developer convenience representation. Another, benefit is that
BE corresponds to the byte ordering. E.g., 256 > 255 (0x0100 > 0x00ff),
which doesn't hold in LE. And we do care about the ordering of
namespaces, at present because the NMT must literally be built in
order, but in future I also think it would be a useful property to
have blobs sorted.

At the same time, the performance gains of using LE are neglegible.

So given all of that I propose we change the canonical integer representation
to BE.
  • Loading branch information
pepyakin committed Nov 30, 2023
1 parent 0d2da10 commit c5dccf5
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 38 deletions.
2 changes: 1 addition & 1 deletion sugondat-chain/pallets/blobs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ pub mod pallet {
let blobs = blobs_metadata
.iter()
.map(|blob| sugondat_nmt::BlobMetadata {
namespace: sugondat_nmt::Namespace::with_namespace_id(blob.namespace_id),
namespace: sugondat_nmt::Namespace::from_u32_be(blob.namespace_id),
leaf: sugondat_nmt::NmtLeaf {
extrinsic_index: blob.extrinsic_index,
who: blob.who.encode().try_into().unwrap(),
Expand Down
46 changes: 30 additions & 16 deletions sugondat-nmt/src/ns.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,48 @@
use crate::NS_ID_SIZE;
use core::fmt;

/// The namespace. A blob is submitted into a namespace. A namespace is a 4 byte vector.
/// The convention is that the namespace id is a 4-byte little-endian integer.
/// Namespace identifier type.
///
/// Blobs are submitted into a namespace. Namepsace is a 4 byte vector. Namespaces define ordering
/// lexicographically.
///
/// For convenience, a namespace can be created from an unsigned 32-bit integer. Conventionally,
/// big-endian representation of the integer is used as that is more intuitive. As one may expect:
///
/// ```
/// # use sugondat_nmt::Namespace;
/// assert!(Namespace::from_u32_be(0x0100) > Namespace::from_u32_be(0x00FF));
/// ````
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Namespace(u32);
pub struct Namespace([u8; NS_ID_SIZE]);

impl Namespace {
pub fn from_raw_bytes(raw_namespace_id: [u8; 4]) -> Self {
let namespace_id = u32::from_le_bytes(raw_namespace_id);
Self(namespace_id)
/// Creates a namespace from the given raw bytes.
pub fn from_raw_bytes(raw_namespace_id: [u8; NS_ID_SIZE]) -> Self {
Self(raw_namespace_id)
}

/// Returns a namespace with the given namespace id.
pub fn with_namespace_id(namespace_id: u32) -> Self {
Self(namespace_id)
/// Returns the raw bytes of the namespace ID.
pub fn to_raw_bytes(&self) -> [u8; NS_ID_SIZE] {
self.0
}

pub(crate) fn with_nmt_namespace_id(nmt_namespace_id: nmt_rs::NamespaceId<NS_ID_SIZE>) -> Self {
let namespace_id = u32::from_le_bytes(nmt_namespace_id.0);
Self(namespace_id)
/// A convenience function to create a namespace from an unsigned 32-bit integer.
///
/// This function will take the given integer (which is assumed to be in host byte order), and
/// take its big-endian representation as the namespace ID.
pub fn from_u32_be(namespace_id: u32) -> Self {
Self(namespace_id.to_be_bytes())
}

pub fn namespace_id(&self) -> u32 {
self.0
/// Reinterpret the namespace ID as a big-endian 32-bit integer and return.
pub fn to_u32_be(&self) -> u32 {
u32::from_be_bytes(self.0)
}

pub fn to_raw_bytes(&self) -> [u8; 4] {
self.0.to_le_bytes()
pub(crate) fn with_nmt_namespace_id(nmt_namespace_id: nmt_rs::NamespaceId<NS_ID_SIZE>) -> Self {
Self(nmt_namespace_id.0)
}

pub(crate) fn nmt_namespace_id(&self) -> nmt_rs::NamespaceId<NS_ID_SIZE> {
Expand Down
28 changes: 14 additions & 14 deletions sugondat-nmt/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,55 +30,55 @@ impl MockBuilder {
#[test]
fn two_same_blobs() {
let mut b = MockBuilder::new();
b.push_blob([1u8; 32], Namespace::with_namespace_id(1), [2u8; 32]);
b.push_blob([1u8; 32], Namespace::with_namespace_id(1), [2u8; 32]);
b.push_blob([1u8; 32], Namespace::from_u32_be(1), [2u8; 32]);
b.push_blob([1u8; 32], Namespace::from_u32_be(1), [2u8; 32]);
let mut tree = b.tree();
let proof = tree.proof(Namespace::with_namespace_id(1));
let proof = tree.proof(Namespace::from_u32_be(1));
assert!(proof
.verify(
&[[2u8; 32], [2u8; 32]],
tree.root(),
Namespace::with_namespace_id(1)
Namespace::from_u32_be(1)
)
.is_ok());
}

#[test]
fn empty() {
let mut tree = MockBuilder::new().tree();
let proof = tree.proof(Namespace::with_namespace_id(1));
let proof = tree.proof(Namespace::from_u32_be(1));
assert!(proof
.verify(&[], tree.root(), Namespace::with_namespace_id(1))
.verify(&[], tree.root(), Namespace::from_u32_be(1))
.is_ok());
}

#[test]
fn empty_absent_namespace_id() {
let mut tree = MockBuilder::new().tree();
let proof = tree.proof(Namespace::with_namespace_id(1));
let proof = tree.proof(Namespace::from_u32_be(1));
assert!(proof
.verify(&[], tree.root(), Namespace::with_namespace_id(2))
.verify(&[], tree.root(), Namespace::from_u32_be(2))
.is_ok());
}

#[test]
fn proof_absent_namespace_id() {
let mut b = MockBuilder::new();
b.push_blob([1u8; 32], Namespace::with_namespace_id(1), [2u8; 32]);
b.push_blob([1u8; 32], Namespace::from_u32_be(1), [2u8; 32]);
let mut tree = b.tree();
let proof = tree.proof(Namespace::with_namespace_id(2));
let proof = tree.proof(Namespace::from_u32_be(2));
assert!(proof
.verify(&[], tree.root(), Namespace::with_namespace_id(2))
.verify(&[], tree.root(), Namespace::from_u32_be(2))
.is_ok());
}

#[test]
fn wrong_namespace_id() {
let mut b = MockBuilder::new();
b.push_blob([1u8; 32], Namespace::with_namespace_id(1), [2u8; 32]);
b.push_blob([1u8; 32], Namespace::from_u32_be(1), [2u8; 32]);
let mut tree = b.tree();
let proof = tree.proof(Namespace::with_namespace_id(2));
let proof = tree.proof(Namespace::from_u32_be(2));
assert!(proof
.verify(&[], tree.root(), Namespace::with_namespace_id(1))
.verify(&[], tree.root(), Namespace::from_u32_be(1))
.is_err());
}
2 changes: 1 addition & 1 deletion sugondat-nmt/src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ impl TreeBuilder {
pub fn new() -> Self {
Self {
tree: NamespaceMerkleTree::new(),
last_namespace: Namespace::with_namespace_id(0),
last_namespace: Namespace::from_u32_be(0),
}
}

Expand Down
4 changes: 2 additions & 2 deletions sugondat-shim/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ pub mod query {
/// The namespace to submit the blob into.
///
/// The namespace can be specified either as a 4-byte vector, or as an unsigned 32-bit
/// integer. To distinguish between the two, the byte vector must be prefixed with
/// `0x`.
/// big-endian integer. To distinguish between the two, the byte vector must be prefixed
/// with `0x`.
#[clap(long, short, env = ENV_SUGONDAT_NAMESPACE)]
pub namespace: String,

Expand Down
4 changes: 2 additions & 2 deletions sugondat-shim/src/cmd/query/submit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fn read_blob(path: &str) -> anyhow::Result<Vec<u8>> {
/// a more human-readable format, which is an unsigned 32-bit integer. To distinguish between the
/// two, the byte vector must be prefixed with `0x`.
///
/// The integer is interpreted as little-endian.
/// The integer is interpreted as big-endian.
fn read_namespace(namespace: &str) -> anyhow::Result<sugondat_nmt::Namespace> {
if let Some(hex) = namespace.strip_prefix("0x") {
let namespace = hex::decode(hex)?;
Expand All @@ -57,5 +57,5 @@ fn read_namespace(namespace: &str) -> anyhow::Result<sugondat_nmt::Namespace> {
let namespace_id = namespace
.parse::<u32>()
.with_context(|| format!("cannot parse namespace id '{}'", namespace))?;
Ok(sugondat_nmt::Namespace::with_namespace_id(namespace_id))
Ok(sugondat_nmt::Namespace::from_u32_be(namespace_id))
}
4 changes: 2 additions & 2 deletions sugondat-shim/src/sugondat_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ impl Client {
namespace: sugondat_nmt::Namespace,
key: Keypair,
) -> anyhow::Result<[u8; 32]> {
let namespace_id = namespace.namespace_id();
let namespace_id = namespace.to_u32_be();
let extrinsic = sugondat_subxt::sugondat::tx()
.blob()
.submit_blob(namespace_id, BoundedVec(blob));
Expand Down Expand Up @@ -154,7 +154,7 @@ fn extract_blobs(
let data = submit_blob_extrinsic.blob.0;
blobs.push(Blob {
extrinsic_index: extrinsic_index as u32,
namespace: sugondat_nmt::Namespace::with_namespace_id(
namespace: sugondat_nmt::Namespace::from_u32_be(
submit_blob_extrinsic.namespace_id,
),
sender,
Expand Down

0 comments on commit c5dccf5

Please sign in to comment.