diff --git a/Cargo.toml b/Cargo.toml index 2de5800e6f..c5a31c0394 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -203,7 +203,7 @@ scopeguard = "1.1.0" second-stack = "0.3" sendgrid = "0.21" serde = { version = "1.0.136", features = ["derive"] } -serde_json = { version = "1.0.87", features = ["raw_value"] } +serde_json = { version = "1.0.128", features = ["raw_value", "arbitrary_precision"] } serde_path_to_error = "0.1.9" serde_with = { version = "3.3.0", features = ["base64", "hex"] } serial_test = "2.0.0" diff --git a/crates/bench/src/lib.rs b/crates/bench/src/lib.rs index 7a7dafb8a4..e9a16f5f57 100644 --- a/crates/bench/src/lib.rs +++ b/crates/bench/src/lib.rs @@ -16,7 +16,7 @@ mod tests { sqlite::SQLite, ResultBench, }; - use std::{io, sync::Once}; + use std::{io, path::Path, sync::Once}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; static INIT: Once = Once::new(); @@ -39,6 +39,15 @@ mod tests { .with(fmt_layer) .with(env_filter_layer) .init(); + + // Remove cached data from previous runs. + // This directory is only reused to speed up runs with Callgrind. In tests, it's fine to wipe it. + let mut bench_dot_spacetime = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf(); + bench_dot_spacetime.push(".spacetime"); + if std::fs::metadata(&bench_dot_spacetime).is_ok() { + std::fs::remove_dir_all(bench_dot_spacetime) + .expect("failed to wipe Spacetimedb/crates/bench/.spacetime"); + } }); } diff --git a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs index 01b6eddaae..be0512968d 100644 --- a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs +++ b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs @@ -6,6 +6,15 @@ namespace SpacetimeDB; using SpacetimeDB.BSATN; using SpacetimeDB.Internal; +internal static class Util +{ + // Same as `Convert.ToHexString`, but that method is not available in .NET Standard + // which we need to target for Unity support. + public static string ToHex(T val) + where T : struct => + BitConverter.ToString(MemoryMarshal.AsBytes([val]).ToArray()).Replace("-", ""); +} + public readonly partial struct Unit { // Custom BSATN that returns an inline empty product type that can be recognised by SpacetimeDB. @@ -63,69 +72,69 @@ string wrapperPropertyName ); } -public record Address : BytesWrapper +public readonly record struct Address { - protected override int SIZE => 16; - - public Address() { } + private readonly U128 value; - private Address(byte[] bytes) - : base(bytes) { } + internal Address(U128 v) => value = v; public static Address? From(byte[] bytes) { - if (bytes.All(b => b == 0)) - { - return null; - } - return new(bytes); + Debug.Assert(bytes.Length == 16); + var addr = new Address(MemoryMarshal.Read(bytes)); + return addr == default ? null : addr; } public static Address Random() { var random = new Random(); - var addr = new Address(); - random.NextBytes(addr.bytes); - return addr; + var bytes = new byte[16]; + random.NextBytes(bytes); + return Address.From(bytes) ?? default; } public readonly struct BSATN : IReadWrite
{ - public Address Read(BinaryReader reader) => new(ReadRaw(reader)); + public Address Read(BinaryReader reader) => + new(new SpacetimeDB.BSATN.U128Stdb().Read(reader)); - public void Write(BinaryWriter writer, Address value) => value.Write(writer); + public void Write(BinaryWriter writer, Address value) => + new SpacetimeDB.BSATN.U128Stdb().Write(writer, value.value); public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => - BytesWrapper.GetAlgebraicType(registrar, "__address_bytes"); + new AlgebraicType.Product([new("__address__", new AlgebraicType.U128(default))]); } - // This must be explicitly forwarded to base, otherwise record will generate a new implementation. - public override string ToString() => base.ToString(); + public override string ToString() => Util.ToHex(value); } -public record Identity : BytesWrapper +public readonly record struct Identity { - protected override int SIZE => 32; + private readonly U256 value; - public Identity() { } + internal Identity(U256 val) => value = val; public Identity(byte[] bytes) - : base(bytes) { } + { + Debug.Assert(bytes.Length == 32); + value = MemoryMarshal.Read(bytes); + } public static Identity From(byte[] bytes) => new(bytes); public readonly struct BSATN : IReadWrite { - public Identity Read(BinaryReader reader) => new(ReadRaw(reader)); + public Identity Read(BinaryReader reader) => new(new SpacetimeDB.BSATN.U256().Read(reader)); - public void Write(BinaryWriter writer, Identity value) => value.Write(writer); + public void Write(BinaryWriter writer, Identity value) => + new SpacetimeDB.BSATN.U256().Write(writer, value.value); public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) => - BytesWrapper.GetAlgebraicType(registrar, "__identity_bytes"); + new AlgebraicType.Product([new("__identity__", new AlgebraicType.U256(default))]); } // This must be explicitly forwarded to base, otherwise record will generate a new implementation. - public override string ToString() => base.ToString(); + public override string ToString() => Util.ToHex(value); } // We store time information in microseconds in internal usages. diff --git a/crates/bindings/src/rt.rs b/crates/bindings/src/rt.rs index d55fe130b3..b081734c1d 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -348,7 +348,7 @@ extern "C" fn __describe_module__(description: BytesSink) { /// - `sender_3` contains bytes `[24..32]`. /// /// The `address_{0-1}` are the pieces of a `[u8; 16]` (`u128`) representing the callers's `Address`. -/// They are encoded as follows (assuming `identity.__address_bytes: [u8; 16]`): +/// They are encoded as follows (assuming `address.__address__: u128`): /// - `address_0` contains bytes `[0 ..8 ]`. /// - `address_1` contains bytes `[8 ..16]`. /// diff --git a/crates/cli/src/subcommands/subscribe.rs b/crates/cli/src/subcommands/subscribe.rs index 3fa3f6c7c3..39155a54fa 100644 --- a/crates/cli/src/subcommands/subscribe.rs +++ b/crates/cli/src/subcommands/subscribe.rs @@ -99,6 +99,7 @@ fn reformat_update<'a>( let table_ty = schema.typespace.resolve(table_schema.product_type_ref); let reformat_row = |row: &str| -> anyhow::Result { + // TODO: can the following two calls be merged into a single call to reduce allocations? let row = serde_json::from_str::(row)?; let row = serde::de::DeserializeSeed::deserialize(SeedWrapper(table_ty), row)?; let row = table_ty.with_value(&row); diff --git a/crates/core/src/client/messages.rs b/crates/core/src/client/messages.rs index 36d5fa043f..372f0cb8e7 100644 --- a/crates/core/src/client/messages.rs +++ b/crates/core/src/client/messages.rs @@ -157,7 +157,7 @@ impl ToProtocol for TransactionUpdateMessage { }, energy_quanta_used: event.energy_quanta_used, host_execution_duration_micros: event.host_execution_duration.as_micros() as u64, - caller_address: event.caller_address.unwrap_or(Address::zero()), + caller_address: event.caller_address.unwrap_or(Address::ZERO), }; ws::ServerMessage::TransactionUpdate(tx_update) diff --git a/crates/core/src/db/datastore/locking_tx_datastore/datastore.rs b/crates/core/src/db/datastore/locking_tx_datastore/datastore.rs index b158e7b99c..aa0790bbe7 100644 --- a/crates/core/src/db/datastore/locking_tx_datastore/datastore.rs +++ b/crates/core/src/db/datastore/locking_tx_datastore/datastore.rs @@ -1042,7 +1042,7 @@ mod tests { } fn get_datastore() -> Result { - Locking::bootstrap(Address::zero()) + Locking::bootstrap(Address::ZERO) } fn col(col: u16) -> ColList { @@ -1358,15 +1358,15 @@ mod tests { ColRow { table: ST_CONSTRAINT_ID.into(), pos: 2, name: "table_id", ty: TableId::get_type() }, ColRow { table: ST_CONSTRAINT_ID.into(), pos: 3, name: "constraint_data", ty: resolved_type_via_v9::() }, - ColRow { table: ST_MODULE_ID.into(), pos: 0, name: "database_address", ty: AlgebraicType::bytes() }, - ColRow { table: ST_MODULE_ID.into(), pos: 1, name: "owner_identity", ty: AlgebraicType::bytes() }, + ColRow { table: ST_MODULE_ID.into(), pos: 0, name: "database_address", ty: AlgebraicType::U128 }, + ColRow { table: ST_MODULE_ID.into(), pos: 1, name: "owner_identity", ty: AlgebraicType::U256 }, ColRow { table: ST_MODULE_ID.into(), pos: 2, name: "program_kind", ty: AlgebraicType::U8 }, - ColRow { table: ST_MODULE_ID.into(), pos: 3, name: "program_hash", ty: AlgebraicType::bytes() }, + ColRow { table: ST_MODULE_ID.into(), pos: 3, name: "program_hash", ty: AlgebraicType::U256 }, ColRow { table: ST_MODULE_ID.into(), pos: 4, name: "program_bytes", ty: AlgebraicType::bytes() }, ColRow { table: ST_MODULE_ID.into(), pos: 5, name: "module_version", ty: AlgebraicType::String }, - ColRow { table: ST_CLIENT_ID.into(), pos: 0, name: "identity", ty: AlgebraicType::bytes()}, - ColRow { table: ST_CLIENT_ID.into(), pos: 1, name: "address", ty: AlgebraicType::bytes()}, + ColRow { table: ST_CLIENT_ID.into(), pos: 0, name: "identity", ty: AlgebraicType::U256}, + ColRow { table: ST_CLIENT_ID.into(), pos: 1, name: "address", ty: AlgebraicType::U128}, ColRow { table: ST_VAR_ID.into(), pos: 0, name: "name", ty: AlgebraicType::String }, ColRow { table: ST_VAR_ID.into(), pos: 1, name: "value", ty: resolved_type_via_v9::() }, diff --git a/crates/core/src/db/datastore/system_tables.rs b/crates/core/src/db/datastore/system_tables.rs index 0318870136..94b2b2a719 100644 --- a/crates/core/src/db/datastore/system_tables.rs +++ b/crates/core/src/db/datastore/system_tables.rs @@ -27,7 +27,7 @@ use spacetimedb_sats::algebraic_value::ser::value_serialize; use spacetimedb_sats::hash::Hash; use spacetimedb_sats::product_value::InvalidFieldError; use spacetimedb_sats::{ - impl_deserialize, impl_serialize, impl_st, AlgebraicType, AlgebraicValue, ArrayValue, SumValue, + impl_deserialize, impl_serialize, impl_st, u256, AlgebraicType, AlgebraicValue, ArrayValue, SumValue, }; use spacetimedb_schema::def::{BTreeAlgorithm, ConstraintData, IndexAlgorithm, ModuleDef, UniqueConstraintData}; use spacetimedb_schema::schema::{ @@ -804,11 +804,11 @@ impl_st!([] ModuleKind, AlgebraicType::U8); /// A wrapper for `Address` that acts like `AlgebraicType::bytes()` for serialization purposes. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct AddressViaBytes(pub Address); -impl_serialize!([] AddressViaBytes, (self, ser) => self.0.as_slice().serialize(ser)); -impl_deserialize!([] AddressViaBytes, de => <[u8; 16]>::deserialize(de).map(Address::from_slice).map(AddressViaBytes)); -impl_st!([] AddressViaBytes, AlgebraicType::bytes()); -impl From
for AddressViaBytes { +pub struct AddressViaU128(pub Address); +impl_serialize!([] AddressViaU128, (self, ser) => self.0.to_u128().serialize(ser)); +impl_deserialize!([] AddressViaU128, de => ::deserialize(de).map(Address::from_u128).map(AddressViaU128)); +impl_st!([] AddressViaU128, AlgebraicType::U128); +impl From
for AddressViaU128 { fn from(addr: Address) -> Self { Self(addr) } @@ -816,11 +816,11 @@ impl From
for AddressViaBytes { /// A wrapper for `Identity` that acts like `AlgebraicType::bytes()` for serialization purposes. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct IdentityViaBytes(pub Identity); -impl_serialize!([] IdentityViaBytes, (self, ser) => self.0.as_bytes().serialize(ser)); -impl_deserialize!([] IdentityViaBytes, de => <[u8; 32]>::deserialize(de).map(|arr| Identity::from_slice(&arr[..])).map(IdentityViaBytes)); -impl_st!([] IdentityViaBytes, AlgebraicType::bytes()); -impl From for IdentityViaBytes { +pub struct IdentityViaU256(pub Identity); +impl_serialize!([] IdentityViaU256, (self, ser) => self.0.to_u256().serialize(ser)); +impl_deserialize!([] IdentityViaU256, de => ::deserialize(de).map(Identity::from_u256).map(IdentityViaU256)); +impl_st!([] IdentityViaU256, AlgebraicType::U256); +impl From for IdentityViaU256 { fn from(id: Identity) -> Self { Self(id) } @@ -843,8 +843,8 @@ impl From for IdentityViaBytes { #[derive(Clone, Debug, Eq, PartialEq, SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct StModuleRow { - pub(crate) database_address: AddressViaBytes, - pub(crate) owner_identity: IdentityViaBytes, + pub(crate) database_address: AddressViaU128, + pub(crate) owner_identity: IdentityViaU256, pub(crate) program_kind: ModuleKind, pub(crate) program_hash: Hash, pub(crate) program_bytes: Box<[u8]>, @@ -867,23 +867,24 @@ pub fn read_bytes_from_col(row: RowRef<'_>, col: impl StFields) -> Result, col: impl StFields) -> Result { - read_bytes_from_col(row, col).map(Address::from_slice) + let val: u128 = row.read_col(col.col_id())?; + Ok(val.into()) } /// Read an [`Identity`] directly from the column `col` in `row`. /// /// The [`Identity`] is assumed to be stored as a flat byte array. pub fn read_identity_from_col(row: RowRef<'_>, col: impl StFields) -> Result { - read_bytes_from_col(row, col).map(|bytes| Identity::from_slice(&bytes)) + Ok(Identity::from_u256(row.read_col(col.col_id())?)) } /// Read a [`Hash`] directly from the column `col` in `row`. /// /// The [`Hash`] is assumed to be stored as a flat byte array. pub fn read_hash_from_col(row: RowRef<'_>, col: impl StFields) -> Result { - read_bytes_from_col(row, col).map(|bytes| Hash::from_slice(&bytes)) + Ok(Hash::from_u256(row.read_col(col.col_id())?)) } impl TryFrom> for StModuleRow { @@ -908,13 +909,18 @@ impl From for ProductValue { #[derive(Clone, Debug, Eq, PartialEq, SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct StClientRow { - pub(crate) identity: IdentityViaBytes, - pub(crate) address: AddressViaBytes, + pub(crate) identity: IdentityViaU256, + pub(crate) address: AddressViaU128, } +impl From for ProductValue { + fn from(var: StClientRow) -> Self { + to_product_value(&var) + } +} impl From<&StClientRow> for ProductValue { - fn from(x: &StClientRow) -> Self { - to_product_value(x) + fn from(var: &StClientRow) -> Self { + to_product_value(var) } } diff --git a/crates/core/src/host/wasmtime/wasmtime_module.rs b/crates/core/src/host/wasmtime/wasmtime_module.rs index 9ecc9e8809..888693df98 100644 --- a/crates/core/src/host/wasmtime/wasmtime_module.rs +++ b/crates/core/src/host/wasmtime/wasmtime_module.rs @@ -194,8 +194,8 @@ impl module_host_actor::WasmInstance for WasmtimeInstance { set_store_fuel(store, budget.into()); // Prepare sender identity and address. - let [sender_0, sender_1, sender_2, sender_3] = bytemuck::must_cast(*op.caller_identity.as_bytes()); - let [address_0, address_1] = bytemuck::must_cast(*op.caller_address.as_slice()); + let [sender_0, sender_1, sender_2, sender_3] = bytemuck::must_cast(op.caller_identity.to_byte_array()); + let [address_0, address_1] = bytemuck::must_cast(op.caller_address.as_byte_array()); // Prepare arguments to the reducer + the error sink & start timings. let (args_source, errors_sink) = store.data_mut().start_reducer(op.name, op.arg_bytes); diff --git a/crates/core/src/sql/compiler.rs b/crates/core/src/sql/compiler.rs index 659668e6b2..1b830e99c9 100644 --- a/crates/core/src/sql/compiler.rs +++ b/crates/core/src/sql/compiler.rs @@ -237,7 +237,7 @@ mod tests { use spacetimedb_lib::{Address, Identity}; use spacetimedb_primitives::{col_list, ColList, TableId}; use spacetimedb_sats::{ - product, satn, AlgebraicType, AlgebraicValue, ProductType, ProductTypeElement, Typespace, ValueWithType, + product, satn, AlgebraicType, AlgebraicValue, GroundSpacetimeType as _, ProductType, Typespace, ValueWithType, }; use spacetimedb_vm::expr::{ColumnOp, IndexJoin, IndexScan, JoinExpr, Query}; use std::convert::From; @@ -402,25 +402,19 @@ mod tests { #[test] fn output_identity_address() -> ResultTest<()> { let row = product![AlgebraicValue::from(Identity::__dummy())]; - let kind = ProductType::new(Box::new([ProductTypeElement::new( - Identity::get_type(), - Some("i".into()), - )])); + let kind: ProductType = [("i", Identity::get_type())].into(); let ty = Typespace::EMPTY.with_type(&kind); let out = ty .with_values(&row) .map(|value| satn::PsqlWrapper { ty: &kind, value }.to_string()) .collect::>() .join(", "); - assert_eq!( - out, - "0x0000000000000000000000000000000000000000000000000000000000000000" - ); + assert_eq!(out, "0"); // Check tuples let kind = [ ("a", AlgebraicType::String), - ("b", AlgebraicType::bytes()), + ("b", AlgebraicType::U256), ("o", Identity::get_type()), ("p", Address::get_type()), ] @@ -428,30 +422,30 @@ mod tests { let value = AlgebraicValue::product([ AlgebraicValue::String("a".into()), - AlgebraicValue::Bytes((*Identity::ZERO.as_bytes()).into()), - AlgebraicValue::Bytes((*Identity::ZERO.as_bytes()).into()), - AlgebraicValue::Bytes((*Address::__DUMMY.as_slice()).into()), + Identity::ZERO.to_u256().into(), + Identity::ZERO.to_u256().into(), + Address::__DUMMY.to_u128().into(), ]); assert_eq!( satn::PsqlWrapper { ty: &kind, value }.to_string().as_str(), - "(0 = \"a\", 1 = 0x0000000000000000000000000000000000000000000000000000000000000000, 2 = 0x0000000000000000000000000000000000000000000000000000000000000000, 3 = 0x00000000000000000000000000000000)" + "(0 = \"a\", 1 = 0, 2 = 0, 3 = 0)" ); let ty = Typespace::EMPTY.with_type(&kind); // Check struct let value = product![ - AlgebraicValue::String("a".into()), - AlgebraicValue::Bytes((*Identity::ZERO.as_bytes()).into()), - AlgebraicValue::product([AlgebraicValue::Bytes((*Identity::ZERO.as_bytes()).into())]), - AlgebraicValue::product([AlgebraicValue::Bytes((*Address::__DUMMY.as_slice()).into())]), + "a", + Identity::ZERO.to_u256(), + AlgebraicValue::product([Identity::ZERO.to_u256().into()]), + AlgebraicValue::product([Address::__DUMMY.to_u128().into()]), ]; let value = ValueWithType::new(ty, &value); assert_eq!( satn::PsqlWrapper { ty: ty.ty(), value }.to_string().as_str(), - "(a = \"a\", b = 0x0000000000000000000000000000000000000000000000000000000000000000, o = 0x0000000000000000000000000000000000000000000000000000000000000000, p = 0x00000000000000000000000000000000)" + "(a = \"a\", b = 0, o = 0, p = 0)" ); Ok(()) diff --git a/crates/core/src/sql/type_check.rs b/crates/core/src/sql/type_check.rs index 1412914046..29cb5799a4 100644 --- a/crates/core/src/sql/type_check.rs +++ b/crates/core/src/sql/type_check.rs @@ -110,8 +110,10 @@ fn resolve_type(field: &FieldExpr, ty: AlgebraicType) -> Result return Ok(Some(AlgebraicType::U128)), + AlgebraicValue::U256(_) => return Ok(Some(AlgebraicType::U256)), + _ => {} } } Ok(Some(ty)) @@ -147,7 +149,7 @@ fn check_both(op: OpQuery, lhs: &Typed, rhs: &Typed) -> Result<(), PlanError> { fn patch_type(lhs: &FieldOp, ty_lhs: &mut Typed, ty_rhs: &Typed) -> Result<(), PlanError> { if let FieldOp::Field(lhs_field) = lhs { if let Some(ty) = ty_rhs.ty() { - if ty.is_sum() || ty.as_product().map_or(false, |x| x.is_special()) { + if ty.is_sum() || ty.as_product().is_some_and(|x| x.is_special()) { ty_lhs.set_ty(resolve_type(lhs_field, ty.clone())?); } } diff --git a/crates/lib/proptest-regressions/address.txt b/crates/lib/proptest-regressions/address.txt new file mode 100644 index 0000000000..ef4feadfc4 --- /dev/null +++ b/crates/lib/proptest-regressions/address.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 4dc1661cd0b78ee7036f894d2c7cf52955f05e41bb0322095ec00edf9b6fca77 # shrinks to val = 18446744073709551616 diff --git a/crates/lib/src/address.rs b/crates/lib/src/address.rs index d50a8d65b8..f08f14b7b9 100644 --- a/crates/lib/src/address.rs +++ b/crates/lib/src/address.rs @@ -3,11 +3,11 @@ use core::{fmt, net::Ipv6Addr}; use spacetimedb_bindings_macro::{Deserialize, Serialize}; use spacetimedb_lib::from_hex_pad; use spacetimedb_sats::hex::HexString; -use spacetimedb_sats::product_type::ADDRESS_TAG; -use spacetimedb_sats::{impl_deserialize, impl_serialize, impl_st, AlgebraicType, AlgebraicValue, ProductValue}; +use spacetimedb_sats::{impl_deserialize, impl_serialize, impl_st, AlgebraicType, AlgebraicValue}; /// This is the address for a SpacetimeDB database or client connection. /// +/// TODO: This is wrong; the address can change, but the Identity cannot. /// It is a unique identifier for a particular database and once set for a database, /// does not change. /// @@ -19,10 +19,10 @@ use spacetimedb_sats::{impl_deserialize, impl_serialize, impl_st, AlgebraicType, // This is likely #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct Address { - __address_bytes: [u8; 16], + __address__: u128, } -impl_st!([] Address, AlgebraicType::product([(ADDRESS_TAG, AlgebraicType::bytes())])); +impl_st!([] Address, AlgebraicType::address()); #[cfg(feature = "metrics_impls")] impl spacetimedb_metrics::typed_prometheus::AsPrometheusLabel for Address { @@ -50,25 +50,22 @@ impl fmt::Debug for Address { } impl Address { - pub const ZERO: Self = Self { - __address_bytes: [0; 16], - }; + pub const ZERO: Self = Self::from_u128(0); - /// Get the special `AlgebraicType` for `Address`. - pub fn get_type() -> AlgebraicType { - AlgebraicType::product([(ADDRESS_TAG, AlgebraicType::bytes())]) + pub const fn from_u128(__address__: u128) -> Self { + Self { __address__ } } - pub fn from_byte_array(arr: [u8; 16]) -> Self { - Self { __address_bytes: arr } + pub const fn to_u128(&self) -> u128 { + self.__address__ } - pub const fn zero() -> Self { - Self::ZERO + pub const fn from_byte_array(arr: [u8; 16]) -> Self { + Self::from_u128(u128::from_le_bytes(arr)) } - pub fn from_u128(u: u128) -> Self { - Self::from_byte_array(u.to_be_bytes()) + pub const fn as_byte_array(&self) -> [u8; 16] { + self.__address__.to_le_bytes() } pub fn from_hex(hex: &str) -> Result { @@ -78,11 +75,11 @@ impl Address { } pub fn to_hex(self) -> HexString<16> { - spacetimedb_sats::hex::encode(self.as_slice()) + spacetimedb_sats::hex::encode(&self.as_byte_array()) } pub fn abbreviate(&self) -> [u8; 8] { - self.as_slice()[..8].try_into().unwrap() + self.as_byte_array()[..8].try_into().unwrap() } pub fn to_abbreviated_hex(self) -> HexString<8> { @@ -96,12 +93,8 @@ impl Address { Self::from_byte_array(dst) } - pub fn as_slice(&self) -> &[u8; 16] { - &self.__address_bytes - } - pub fn to_ipv6(self) -> Ipv6Addr { - Ipv6Addr::from(self.__address_bytes) + Ipv6Addr::from(self.__address__) } #[allow(dead_code)] @@ -112,10 +105,6 @@ impl Address { #[doc(hidden)] pub const __DUMMY: Self = Self::ZERO; - pub fn to_u128(&self) -> u128 { - u128::from_be_bytes(self.__address_bytes) - } - pub fn none_if_zero(self) -> Option { (self != Self::ZERO).then_some(self) } @@ -129,9 +118,7 @@ impl From for Address { impl From
for AlgebraicValue { fn from(value: Address) -> Self { - AlgebraicValue::Product(ProductValue::from(AlgebraicValue::Bytes( - value.__address_bytes.to_vec().into(), - ))) + AlgebraicValue::product([value.to_u128().into()]) } } @@ -140,7 +127,7 @@ pub struct AddressForUrl(u128); impl From
for AddressForUrl { fn from(addr: Address) -> Self { - AddressForUrl(u128::from_be_bytes(addr.__address_bytes)) + AddressForUrl(addr.to_u128()) } } @@ -150,9 +137,9 @@ impl From for Address { } } -impl_serialize!([] AddressForUrl, (self, ser) => self.0.to_be_bytes().serialize(ser)); -impl_deserialize!([] AddressForUrl, de => <[u8; 16]>::deserialize(de).map(|v| Self(u128::from_be_bytes(v)))); -impl_st!([] AddressForUrl, AlgebraicType::bytes()); +impl_serialize!([] AddressForUrl, (self, ser) => self.0.serialize(ser)); +impl_deserialize!([] AddressForUrl, de => u128::deserialize(de).map(Self)); +impl_st!([] AddressForUrl, AlgebraicType::U128); #[cfg(feature = "serde")] impl serde::Serialize for AddressForUrl { @@ -160,7 +147,7 @@ impl serde::Serialize for AddressForUrl { where S: serde::Serializer, { - spacetimedb_sats::ser::serde::serialize_to(&Address::from(*self).as_slice(), serializer) + spacetimedb_sats::ser::serde::serialize_to(&Address::from(*self).as_byte_array(), serializer) } } @@ -181,7 +168,7 @@ impl serde::Serialize for Address { where S: serde::Serializer, { - spacetimedb_sats::ser::serde::serialize_to(&self.as_slice(), serializer) + spacetimedb_sats::ser::serde::serialize_to(&self.as_byte_array(), serializer) } } @@ -199,14 +186,18 @@ impl<'de> serde::Deserialize<'de> for Address { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; use spacetimedb_sats::bsatn; + use spacetimedb_sats::GroundSpacetimeType as _; - #[test] - fn test_bsatn_roundtrip() { - let addr = Address::from_u128(rand::random()); - let ser = bsatn::to_vec(&addr).unwrap(); - let de = bsatn::from_slice(&ser).unwrap(); - assert_eq!(addr, de); + proptest! { + #[test] + fn test_bsatn_roundtrip(val: u128) { + let addr = Address::from_u128(val); + let ser = bsatn::to_vec(&addr).unwrap(); + let de = bsatn::from_slice(&ser).unwrap(); + assert_eq!(addr, de); + } } #[test] @@ -217,15 +208,45 @@ mod tests { #[cfg(feature = "serde")] mod serde { use super::*; + use crate::sats::{algebraic_value::de::ValueDeserializer, de::Deserialize, Typespace}; + use crate::ser::serde::SerializeWrapper; + use crate::WithTypespace; + + proptest! { + /// Tests the round-trip used when using the `spacetime subscribe` + /// CLI command. + /// Somewhat confusingly, this is distinct from the ser-de path + /// in `test_serde_roundtrip`. + #[test] + fn test_wrapper_roundtrip(val: u128) { + let addr = Address::from_u128(val); + let wrapped = SerializeWrapper::new(&addr); + + let ser = serde_json::to_string(&wrapped).unwrap(); + let empty = Typespace::default(); + let address_ty = Address::get_type(); + let address_ty = WithTypespace::new(&empty, &address_ty); + let row = serde_json::from_str::(&ser[..])?; + let de = ::serde::de::DeserializeSeed::deserialize( + crate::de::serde::SeedWrapper( + address_ty + ), + row)?; + let de = Address::deserialize(ValueDeserializer::new(de)).unwrap(); + prop_assert_eq!(addr, de); + } + } - #[test] - fn test_serde_roundtrip() { - let addr = Address::from_u128(rand::random()); - let to_url = AddressForUrl::from(addr); - let ser = serde_json::to_vec(&to_url).unwrap(); - let de = serde_json::from_slice::(&ser).unwrap(); - let from_url = Address::from(de); - assert_eq!(addr, from_url); + proptest! { + #[test] + fn test_serde_roundtrip(val: u128) { + let addr = Address::from_u128(val); + let to_url = AddressForUrl::from(addr); + let ser = serde_json::to_vec(&to_url).unwrap(); + let de = serde_json::from_slice::(&ser).unwrap(); + let from_url = Address::from(de); + prop_assert_eq!(addr, from_url); + } } } } diff --git a/crates/lib/src/identity.rs b/crates/lib/src/identity.rs index 4879fddb35..b2e262d126 100644 --- a/crates/lib/src/identity.rs +++ b/crates/lib/src/identity.rs @@ -1,8 +1,8 @@ use crate::from_hex_pad; +use core::mem; use spacetimedb_bindings_macro::{Deserialize, Serialize}; use spacetimedb_sats::hex::HexString; -use spacetimedb_sats::product_type::IDENTITY_TAG; -use spacetimedb_sats::{hash, impl_st, AlgebraicType, AlgebraicValue, ProductValue}; +use spacetimedb_sats::{hash, impl_st, u256, AlgebraicType, AlgebraicValue}; use std::{fmt, str::FromStr}; pub type RequestId = u32; @@ -35,10 +35,10 @@ impl AuthCtx { /// This is a special type. #[derive(Default, Eq, PartialEq, PartialOrd, Ord, Clone, Copy, Hash, Serialize, Deserialize)] pub struct Identity { - __identity_bytes: [u8; 32], + __identity__: u256, } -impl_st!([] Identity, AlgebraicType::product([(IDENTITY_TAG, AlgebraicType::bytes())])); +impl_st!([] Identity, AlgebraicType::identity()); #[cfg(feature = "metrics_impls")] impl spacetimedb_metrics::typed_prometheus::AsPrometheusLabel for Identity { @@ -48,15 +48,23 @@ impl spacetimedb_metrics::typed_prometheus::AsPrometheusLabel for Identity { } impl Identity { - pub const ZERO: Self = Self { - __identity_bytes: [0; 32], - }; + pub const ZERO: Self = Self::from_u256(u256::ZERO); /// Returns an `Identity` defined as the given `bytes` byte array. pub const fn from_byte_array(bytes: [u8; 32]) -> Self { - Self { - __identity_bytes: bytes, - } + // SAFETY: The transmute is an implementation of `u256::from_ne_bytes`, + // but works in a const context. + Self::from_u256(u256::from_le(unsafe { mem::transmute(bytes) })) + } + + /// Converts `__identity__: u256` to `Identity`. + pub const fn from_u256(__identity__: u256) -> Self { + Self { __identity__ } + } + + /// Converts this identity to an `u256`. + pub const fn to_u256(&self) -> u256 { + self.__identity__ } /// Returns an `Identity` defined as the given byte `slice`. @@ -66,33 +74,24 @@ impl Identity { #[doc(hidden)] pub fn __dummy() -> Self { - Self::from_byte_array([0; 32]) - } - - /// Get the special `AlgebraicType` for `Identity`. - pub fn get_type() -> AlgebraicType { - AlgebraicType::product([(IDENTITY_TAG, AlgebraicType::bytes())]) - } - - /// Returns a borrowed view of the byte array defining this `Identity`. - pub fn as_bytes(&self) -> &[u8; 32] { - &self.__identity_bytes + Self::ZERO } - pub fn to_vec(&self) -> Vec { - self.__identity_bytes.to_vec() + /// Returns this `Identity` as a byte array. + pub fn to_byte_array(&self) -> [u8; 32] { + self.__identity__.to_le_bytes() } pub fn to_hex(&self) -> HexString<32> { - spacetimedb_sats::hex::encode(&self.__identity_bytes) + spacetimedb_sats::hex::encode(&self.to_byte_array()) } - pub fn abbreviate(&self) -> &[u8; 8] { - self.__identity_bytes[..8].try_into().unwrap() + pub fn abbreviate(&self) -> [u8; 8] { + self.to_byte_array()[..8].try_into().unwrap() } pub fn to_abbreviated_hex(&self) -> HexString<8> { - spacetimedb_sats::hex::encode(self.abbreviate()) + spacetimedb_sats::hex::encode(&self.abbreviate()) } pub fn from_hex(hex: impl AsRef<[u8]>) -> Result { @@ -100,7 +99,7 @@ impl Identity { } pub fn from_hashing_bytes(bytes: impl AsRef<[u8]>) -> Self { - Identity::from_byte_array(hash::hash_bytes(bytes).data) + Self::from_byte_array(hash::hash_bytes(bytes).data) } } @@ -120,8 +119,7 @@ impl hex::FromHex for Identity { type Error = hex::FromHexError; fn from_hex>(hex: T) -> Result { - let data = from_hex_pad(hex)?; - Ok(Identity { __identity_bytes: data }) + from_hex_pad(hex).map(Identity::from_byte_array) } } @@ -135,14 +133,14 @@ impl FromStr for Identity { impl From for AlgebraicValue { fn from(value: Identity) -> Self { - AlgebraicValue::Product(ProductValue::from(AlgebraicValue::Bytes(value.to_vec().into()))) + AlgebraicValue::product([value.to_u256().into()]) } } #[cfg(feature = "serde")] impl serde::Serialize for Identity { fn serialize(&self, serializer: S) -> Result { - spacetimedb_sats::ser::serde::serialize_to(self.as_bytes(), serializer) + spacetimedb_sats::ser::serde::serialize_to(&self.to_byte_array(), serializer) } } @@ -157,6 +155,7 @@ impl<'de> serde::Deserialize<'de> for Identity { #[cfg(test)] mod tests { use super::*; + use spacetimedb_sats::GroundSpacetimeType as _; #[test] fn identity_is_special() { diff --git a/crates/lib/tests/serde.rs b/crates/lib/tests/serde.rs index c6ea2fa4b4..dfc6eef095 100644 --- a/crates/lib/tests/serde.rs +++ b/crates/lib/tests/serde.rs @@ -3,7 +3,7 @@ use spacetimedb_lib::de::DeserializeSeed; use spacetimedb_lib::{AlgebraicType, Identity, ProductType, ProductTypeElement, ProductValue, SumType}; use spacetimedb_sats::algebraic_value::de::ValueDeserializer; use spacetimedb_sats::algebraic_value::ser::value_serialize; -use spacetimedb_sats::{satn::Satn, SumTypeVariant, Typespace, WithTypespace}; +use spacetimedb_sats::{satn::Satn, GroundSpacetimeType as _, SumTypeVariant, Typespace, WithTypespace}; macro_rules! de_json_snapshot { ($schema:expr, $json:expr) => { @@ -72,6 +72,7 @@ fn test_json_mappings() { ("and_peggy", AlgebraicType::option(AlgebraicType::F64)), ("identity", Identity::get_type()), ]); + let data = r#" { "foo": 42, @@ -79,10 +80,11 @@ fn test_json_mappings() { "baz": ["heyyyyyy", "hooo"], "quux": { "Hash": "54a3e6d2b0959deaacf102292b1cbd6fcbb8cf237f73306e27ed82c3153878aa" }, "and_peggy": { "some": 3.141592653589793238426 }, - "identity": ["0000000000000000000000000000000000000000000000000000000000000000"] + "identity": ["0x0"] } "#; // all of those ^^^^^^ digits are from memory de_json_snapshot!(schema, data); + let data = r#" { "foo": 5654, @@ -90,7 +92,7 @@ fn test_json_mappings() { "baz": ["it's 🥶°C"], "quux": { "Unit": [] }, "and_peggy": null, - "identity": ["0000000000000000000000000000000000000000000000000000000000000000"] + "identity": ["0x0"] } "#; de_json_snapshot!(schema, data); diff --git a/crates/lib/tests/snapshots/serde__json_mappings-2.snap b/crates/lib/tests/snapshots/serde__json_mappings-2.snap index e337775bbd..db77cada11 100644 --- a/crates/lib/tests/snapshots/serde__json_mappings-2.snap +++ b/crates/lib/tests/snapshots/serde__json_mappings-2.snap @@ -1,6 +1,6 @@ --- source: crates/lib/tests/serde.rs -expression: "de_json({\n \"foo\": 5654,\n \"bar\": [1, 15, 44],\n \"baz\": [\"it's 🥶°C\"],\n \"quux\": { \"Unit\": [] },\n \"and_peggy\": null,\n \"identity\": [\"0000000000000000000000000000000000000000000000000000000000000000\"]\n})" +expression: "de_json({\n \"foo\": 5654,\n \"bar\": [1, 15, 44],\n \"baz\": [\"it's 🥶°C\"],\n \"quux\": { \"Unit\": [] },\n \"and_peggy\": null,\n \"identity\": [\"0x0\"]\n})" --- ( foo = 5654, @@ -15,6 +15,6 @@ expression: "de_json({\n \"foo\": 5654,\n \"bar\": [1, 15, 44],\n \"baz none = (), ), identity = ( - __identity_bytes = 0x0000000000000000000000000000000000000000000000000000000000000000, + __identity__ = 0, ), ) diff --git a/crates/lib/tests/snapshots/serde__json_mappings.snap b/crates/lib/tests/snapshots/serde__json_mappings.snap index 5c2270a6b4..4bd8dcf498 100644 --- a/crates/lib/tests/snapshots/serde__json_mappings.snap +++ b/crates/lib/tests/snapshots/serde__json_mappings.snap @@ -1,6 +1,6 @@ --- source: crates/lib/tests/serde.rs -expression: "de_json({\n \"foo\": 42,\n \"bar\": \"404040FFFF0A48656C6C6F\",\n \"baz\": [\"heyyyyyy\", \"hooo\"],\n \"quux\": { \"Hash\": \"54a3e6d2b0959deaacf102292b1cbd6fcbb8cf237f73306e27ed82c3153878aa\" },\n \"and_peggy\": { \"some\": 3.141592653589793238426 },\n \"identity\": [\"0000000000000000000000000000000000000000000000000000000000000000\"]\n})" +expression: "de_json({\n \"foo\": 42,\n \"bar\": \"404040FFFF0A48656C6C6F\",\n \"baz\": [\"heyyyyyy\", \"hooo\"],\n \"quux\": { \"Hash\": \"54a3e6d2b0959deaacf102292b1cbd6fcbb8cf237f73306e27ed82c3153878aa\" },\n \"and_peggy\": { \"some\": 3.141592653589793238426 },\n \"identity\": [\"0x0\"]\n})" --- ( foo = 42, @@ -16,6 +16,6 @@ expression: "de_json({\n \"foo\": 42,\n \"bar\": \"404040FFFF0A48656C6C6F\ some = 3.141592653589793, ), identity = ( - __identity_bytes = 0x0000000000000000000000000000000000000000000000000000000000000000, + __identity__ = 0, ), ) diff --git a/crates/sats/src/algebraic_type.rs b/crates/sats/src/algebraic_type.rs index 3ad0039173..e52402e249 100644 --- a/crates/sats/src/algebraic_type.rs +++ b/crates/sats/src/algebraic_type.rs @@ -304,12 +304,12 @@ impl AlgebraicType { /// Construct a copy of the `Identity` type. pub fn identity() -> Self { - AlgebraicType::product([(IDENTITY_TAG, AlgebraicType::bytes())]) + AlgebraicType::product([(IDENTITY_TAG, AlgebraicType::U256)]) } /// Construct a copy of the `Address` type. pub fn address() -> Self { - AlgebraicType::product([(ADDRESS_TAG, AlgebraicType::bytes())]) + AlgebraicType::product([(ADDRESS_TAG, AlgebraicType::U128)]) } /// Returns a sum type of unit variants with names taken from `var_names`. diff --git a/crates/sats/src/hash.rs b/crates/sats/src/hash.rs index 6493d5c57e..f3899a0651 100644 --- a/crates/sats/src/hash.rs +++ b/crates/sats/src/hash.rs @@ -1,5 +1,5 @@ use crate::hex::HexString; -use crate::{impl_deserialize, impl_serialize, impl_st, AlgebraicType}; +use crate::{impl_deserialize, impl_serialize, impl_st, u256, AlgebraicType}; use core::fmt; use sha3::{Digest, Keccak256}; @@ -11,9 +11,9 @@ pub struct Hash { pub data: [u8; HASH_SIZE], } -impl_st!([] Hash, AlgebraicType::bytes()); -impl_serialize!([] Hash, (self, ser) => self.data.serialize(ser)); -impl_deserialize!([] Hash, de => Ok(Self { data: <_>::deserialize(de)? })); +impl_st!([] Hash, AlgebraicType::U256); +impl_serialize!([] Hash, (self, ser) => u256::from_le_bytes(self.data).serialize(ser)); +impl_deserialize!([] Hash, de => Ok(Self { data: <_>::deserialize(de).map(u256::to_le_bytes)? })); #[cfg(feature = "metrics_impls")] impl spacetimedb_metrics::typed_prometheus::AsPrometheusLabel for Hash { @@ -23,19 +23,18 @@ impl spacetimedb_metrics::typed_prometheus::AsPrometheusLabel for Hash { } impl Hash { - pub const ZERO: Self = Self { data: [0; HASH_SIZE] }; + pub const ZERO: Self = Self::from_byte_array([0; HASH_SIZE]); - pub fn from_arr(arr: &[u8; HASH_SIZE]) -> Self { - Self { data: *arr } + pub const fn from_byte_array(data: [u8; HASH_SIZE]) -> Self { + Self { data } } - pub fn from_slice(slice: &[u8]) -> Self { - Self { - data: slice.try_into().unwrap(), - } + pub fn from_u256(val: u256) -> Self { + Self::from_byte_array(val.to_le_bytes()) } - pub fn to_vec(&self) -> Vec { - self.data.to_vec() + + pub fn to_u256(self) -> u256 { + u256::from_le_bytes(self.data) } pub fn to_hex(&self) -> HexString<32> { @@ -45,27 +44,10 @@ impl Hash { pub fn abbreviate(&self) -> &[u8; 16] { self.data[..16].try_into().unwrap() } - - pub fn to_abbreviated_hex(&self) -> HexString<16> { - crate::hex::encode(self.abbreviate()) - } - - pub fn as_slice(&self) -> &[u8] { - self.data.as_slice() - } - - pub fn from_hex(hex: impl AsRef<[u8]>) -> Result { - hex::FromHex::from_hex(hex) - } - - pub fn is_zero(&self) -> bool { - self == &Self::ZERO - } } pub fn hash_bytes(bytes: impl AsRef<[u8]>) -> Hash { - let data: [u8; HASH_SIZE] = Keccak256::digest(bytes).into(); - Hash { data } + Hash::from_byte_array(Keccak256::digest(bytes).into()) } impl fmt::Display for Hash { diff --git a/crates/sats/src/product_type.rs b/crates/sats/src/product_type.rs index 5af2ec9ce0..30bc21d464 100644 --- a/crates/sats/src/product_type.rs +++ b/crates/sats/src/product_type.rs @@ -8,9 +8,9 @@ use crate::product_value::InvalidFieldError; use crate::{AlgebraicType, AlgebraicValue, ProductTypeElement, SpacetimeType, ValueWithType, WithTypespace}; /// The tag used inside the special `Identity` product type. -pub const IDENTITY_TAG: &str = "__identity_bytes"; +pub const IDENTITY_TAG: &str = "__identity__"; /// The tag used inside the special `Address` product type. -pub const ADDRESS_TAG: &str = "__address_bytes"; +pub const ADDRESS_TAG: &str = "__address__"; /// A structural product type of the factors given by `elements`. /// @@ -53,17 +53,17 @@ impl ProductType { /// Returns the unit product type. pub fn unit() -> Self { - Self { elements: Box::new([]) } + Self::new([].into()) } - /// Returns whether this is a "newtype" over bytes. + /// Returns whether this is a "newtype" with `label` and satisfying `inner`. /// Does not follow `Ref`s. - fn is_bytes_newtype(&self, check: &str) -> bool { + fn is_newtype(&self, check: &str, inner: impl FnOnce(&AlgebraicType) -> bool) -> bool { match &*self.elements { [ProductTypeElement { name: Some(name), algebraic_type, - }] => &**name == check && algebraic_type.is_bytes(), + }] => &**name == check && inner(algebraic_type), _ => false, } } @@ -71,13 +71,13 @@ impl ProductType { /// Returns whether this is the special case of `spacetimedb_lib::Identity`. /// Does not follow `Ref`s. pub fn is_identity(&self) -> bool { - self.is_bytes_newtype(IDENTITY_TAG) + self.is_newtype(IDENTITY_TAG, |i| i.is_u256()) } /// Returns whether this is the special case of `spacetimedb_lib::Address`. /// Does not follow `Ref`s. pub fn is_address(&self) -> bool { - self.is_bytes_newtype(ADDRESS_TAG) + self.is_newtype(ADDRESS_TAG, |i| i.is_u128()) } /// Returns whether this is a special known `tag`, currently `Address` or `Identity`. diff --git a/crates/standalone/src/control_db.rs b/crates/standalone/src/control_db.rs index 422583ca24..b338751deb 100644 --- a/crates/standalone/src/control_db.rs +++ b/crates/standalone/src/control_db.rs @@ -1,7 +1,7 @@ use spacetimedb::address::Address; use spacetimedb::hash::hash_bytes; use spacetimedb::identity::Identity; -use spacetimedb::messages::control_db::{Database, EnergyBalance, Node, Replica}; +use spacetimedb::messages::control_db::{Database, EnergyBalance, Node, Replica, IdentityEmail}; use spacetimedb::{energy, stdb_path}; use spacetimedb_client_api_messages::name::{ @@ -85,7 +85,7 @@ impl ControlDb { pub fn spacetime_reverse_dns(&self, address: &Address) -> Result> { let tree = self.db.open_tree("reverse_dns")?; - let value = tree.get(address.as_slice())?; + let value = tree.get(address.as_byte_array())?; if let Some(value) = value { let vec: Vec = serde_json::from_slice(&value[..])?; return Ok(vec); @@ -140,18 +140,19 @@ impl ControlDb { } } + let addr_bytes = address.as_byte_array(); let tree = self.db.open_tree("dns")?; - tree.insert(domain.to_lowercase().as_bytes(), &address.as_slice()[..])?; + tree.insert(domain.to_lowercase().as_bytes(), &addr_bytes)?; let tree = self.db.open_tree("reverse_dns")?; - match tree.get(address.as_slice())? { + match tree.get(addr_bytes)? { Some(value) => { let mut vec: Vec = serde_json::from_slice(&value[..])?; vec.push(domain.clone()); - tree.insert(address.as_slice(), serde_json::to_string(&vec)?.as_bytes())?; + tree.insert(addr_bytes, serde_json::to_string(&vec)?.as_bytes())?; } None => { - tree.insert(address.as_slice(), serde_json::to_string(&vec![&domain])?.as_bytes())?; + tree.insert(addr_bytes, serde_json::to_string(&vec![&domain])?.as_bytes())?; } } @@ -177,7 +178,7 @@ impl ControlDb { } } None => { - tree.insert(key, owner_identity.as_bytes())?; + tree.insert(key, &owner_identity.to_byte_array())?; Ok(RegisterTldResult::Success { domain: tld }) } } @@ -205,10 +206,47 @@ impl ControlDb { let name = b"clockworklabs:"; let bytes = [name, bytes].concat(); let hash = hash_bytes(bytes); - let address = Address::from_slice(&hash.as_slice()[..16]); + let address = Address::from_slice(hash.abbreviate()); Ok(address) } + pub async fn associate_email_spacetime_identity(&self, identity: Identity, email: &str) -> Result<()> { + // Lowercase the email before storing + let email = email.to_lowercase(); + + let tree = self.db.open_tree("email")?; + let identity_email = IdentityEmail { identity, email }; + let buf = bsatn::to_vec(&identity_email).unwrap(); + tree.insert(identity.to_byte_array(), buf)?; + Ok(()) + } + + pub fn get_identities_for_email(&self, email: &str) -> Result> { + let mut result = Vec::::new(); + let tree = self.db.open_tree("email")?; + for i in tree.iter() { + let (_, value) = i?; + let iemail: IdentityEmail = bsatn::from_slice(&value)?; + if iemail.email.eq_ignore_ascii_case(email) { + result.push(iemail); + } + } + Ok(result) + } + + pub fn get_emails_for_identity(&self, identity: &Identity) -> Result> { + let mut result = Vec::::new(); + let tree = self.db.open_tree("email")?; + for i in tree.iter() { + let (_, value) = i?; + let iemail: IdentityEmail = bsatn::from_slice(&value)?; + if &iemail.identity == identity { + result.push(iemail); + } + } + Ok(result) + } + pub fn get_databases(&self) -> Result> { let tree = self.db.open_tree("database")?; let mut databases = Vec::new(); @@ -430,7 +468,7 @@ impl ControlDb { /// `control_budget`, where a cached copy is stored along with business logic for managing it. pub fn get_energy_balance(&self, identity: &Identity) -> Result> { let tree = self.db.open_tree("energy_budget")?; - let value = tree.get(identity.as_bytes())?; + let value = tree.get(identity.to_byte_array())?; if let Some(value) = value { let arr = <[u8; 16]>::try_from(value.as_ref()).map_err(|_| bsatn::DecodeError::BufferLength { for_type: "Identity".into(), @@ -449,7 +487,7 @@ impl ControlDb { /// `control_budget`, where a cached copy is stored along with business logic for managing it. pub fn set_energy_balance(&self, identity: Identity, energy_balance: energy::EnergyBalance) -> Result<()> { let tree = self.db.open_tree("energy_budget")?; - tree.insert(identity.as_bytes(), &energy_balance.get().to_be_bytes())?; + tree.insert(identity.to_byte_array(), &energy_balance.get().to_be_bytes())?; Ok(()) } diff --git a/crates/standalone/src/control_db/tests.rs b/crates/standalone/src/control_db/tests.rs index e903ab5e77..00bb501f93 100644 --- a/crates/standalone/src/control_db/tests.rs +++ b/crates/standalone/src/control_db/tests.rs @@ -48,7 +48,7 @@ fn test_domain() -> anyhow::Result<()> { let cdb = ControlDb::at(tmp.path())?; - let addr = Address::zero(); + let addr = Address::ZERO; let res = cdb.spacetime_insert_domain(&addr, domain.clone(), *ALICE, true)?; assert!(matches!(res, InsertDomainResult::Success { .. })); diff --git a/smoketests/tests/identity.py b/smoketests/tests/identity.py index 63a7a0f549..b311638f7e 100644 --- a/smoketests/tests/identity.py +++ b/smoketests/tests/identity.py @@ -76,3 +76,83 @@ def test_set_default(self): default_identity = next(filter(lambda s: "***" in s, identities), "") self.assertIn(identity, default_identity) + def test_set_email(self): + """Ensure that we are able to associate an email with an identity""" + + self.fingerprint() + + # Create a new identity + identity = self.new_identity(email=None) + email = random_email() + token = self.token(identity) + + # Reset our config so we lose this identity + self.reset_config() + + # Import this identity, and set it as the default identity + self.import_identity(identity, token, default=True) + + # Configure our email + output = self.spacetime("identity", "set-email", "--identity", identity, email) + self.assertEqual(extract_field(output, "IDENTITY"), identity) + self.assertEqual(extract_field(output, "EMAIL").lower(), email.lower()) + + # Reset config again + self.reset_config() + + # Find our identity by its email + output = self.spacetime("identity", "find", email) + self.assertEqual(extract_field(output, "IDENTITY"), identity) + self.assertEqual(extract_field(output, "EMAIL").lower(), email.lower()) + + +class IdentityFormatting(Smoketest): + MODULE_CODE = """ +use log::info; +use spacetimedb::{Address, Identity, ReducerContext, Table}; + +#[spacetimedb::table(name = connected_client)] +pub struct ConnectedClient { + identity: Identity, + address: Address, +} + +#[spacetimedb::reducer(client_connected)] +fn on_connect(ctx: &ReducerContext) { + ctx.db.connected_client().insert(ConnectedClient { + identity: ctx.sender, + address: ctx.address.expect("sender address unset"), + }); +} + +#[spacetimedb::reducer(client_disconnected)] +fn on_disconnect(ctx: &ReducerContext) { + let sender_identity = &ctx.sender; + let sender_address = ctx.address.as_ref().expect("sender address unset"); + let match_client = |row: &ConnectedClient| { + &row.identity == sender_identity && &row.address == sender_address + }; + if let Some(client) = ctx.db.connected_client().iter().find(match_client) { + ctx.db.connected_client().delete(client); + } +} + +#[spacetimedb::reducer] +fn print_num_connected(ctx: &ReducerContext) { + let n = ctx.db.connected_client().count(); + info!("CONNECTED CLIENTS: {n}") +} +""" + + def test_identity_formatting(self): + """Tests formatting of Identity.""" + + # Start two subscribers + self.subscribe("SELECT * FROM connected_client", n=2) + self.subscribe("SELECT * FROM connected_client", n=2) + + # Assert that we have two clients + the reducer call + self.call("print_num_connected") + logs = self.logs(10) + self.assertEqual("CONNECTED CLIENTS: 3", logs.pop()) +