diff --git a/Cargo.lock b/Cargo.lock index 3cd62a4c..4ac6ed38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7246,6 +7246,7 @@ version = "0.9.2" dependencies = [ "askama", "insta", + "parity-scale-codec", "scale-info", ] diff --git a/rs/ethexe/Cargo.lock b/rs/ethexe/Cargo.lock index f82578ce..e5a3fc97 100644 --- a/rs/ethexe/Cargo.lock +++ b/rs/ethexe/Cargo.lock @@ -4253,6 +4253,7 @@ dependencies = [ name = "sails-idl-meta" version = "0.9.2" dependencies = [ + "parity-scale-codec", "scale-info", ] diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_allow_attrs.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_allow_attrs.snap index 1a3a54f8..1b96fe6f 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_allow_attrs.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_allow_attrs.snap @@ -166,7 +166,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -193,7 +193,16 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_basics.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_basics.snap index 9baac3f1..af0d86c4 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_basics.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_basics.snap @@ -163,7 +163,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -190,7 +190,16 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_crate_path.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_crate_path.snap index 8340c11f..6840f7cc 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_crate_path.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_crate_path.snap @@ -163,7 +163,7 @@ impl sails_rename::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rename::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rename::meta::InterfaceId = { let mut final_hash = sails_rename::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rename::keccak_const::Keccak256::new(); @@ -192,7 +192,16 @@ impl sails_rename::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rename::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_docs.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_docs.snap index 90d87567..1bd1cc2e 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_docs.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_docs.snap @@ -166,7 +166,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -193,7 +193,16 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_events.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_events.snap index 959a4e43..b1457d0f 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_events.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_events.snap @@ -183,7 +183,7 @@ impl sails_rs::meta::ServiceMeta for MyServiceWithEvents { type EventsMeta = my_service_with_events_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = false; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -206,7 +206,16 @@ impl sails_rs::meta::ServiceMeta for MyServiceWithEvents { final_hash = final_hash .update(&::HASH); let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod my_service_with_events_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_export.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_export.snap index 8c4ebb16..219a630f 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_export.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_export.snap @@ -178,7 +178,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -207,7 +207,16 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_extends.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_extends.snap index 8d216ee3..d8b3e405 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_extends.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_extends.snap @@ -212,7 +212,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { ]; const ASYNC: bool = ::ASYNC || ::ASYNC; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -224,11 +224,20 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } final_hash = final_hash - .update(&::INTERFACE_ID); + .update(&::INTERFACE_ID.0); final_hash = final_hash - .update(&::INTERFACE_ID); + .update(&::INTERFACE_ID.0); let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_extends_and_lifetimes.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_extends_and_lifetimes.snap index 7695893d..9786bc1a 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_extends_and_lifetimes.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_extends_and_lifetimes.snap @@ -221,7 +221,7 @@ impl<'a> sails_rs::meta::ServiceMeta for ExtendedWithLifetime<'a> { sails_rs::meta::AnyServiceMeta::new::, ]; const ASYNC: bool = ::ASYNC; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -242,9 +242,18 @@ impl<'a> sails_rs::meta::ServiceMeta for ExtendedWithLifetime<'a> { final_hash = final_hash.update(&fn_hash.finalize()); } final_hash = final_hash - .update(&::INTERFACE_ID); + .update(&::INTERFACE_ID.0); let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod extended_with_lifetime_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_lifetimes_and_events.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_lifetimes_and_events.snap index 36a33b6e..b1d73fff 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_lifetimes_and_events.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_lifetimes_and_events.snap @@ -149,7 +149,7 @@ where type EventsMeta = my_generic_events_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = false; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -163,7 +163,16 @@ where final_hash = final_hash .update(&::HASH); let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod my_generic_events_service_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_lifetimes_and_generics.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_lifetimes_and_generics.snap index 90da25b8..5b16a9df 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_lifetimes_and_generics.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_lifetimes_and_generics.snap @@ -136,7 +136,7 @@ where type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = false; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -148,7 +148,16 @@ where final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_methods_with_lifetimes.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_methods_with_lifetimes.snap index 9d024a75..1d7807f0 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_methods_with_lifetimes.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_methods_with_lifetimes.snap @@ -313,7 +313,7 @@ impl sails_rs::meta::ServiceMeta for ReferenceService { type EventsMeta = reference_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -377,7 +377,16 @@ impl sails_rs::meta::ServiceMeta for ReferenceService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod reference_service_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_reply_with_value.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_reply_with_value.snap index d0d952a5..7a5aa7d1 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_reply_with_value.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_reply_with_value.snap @@ -166,7 +166,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -193,7 +193,16 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_special_lifetimes_and_events.snap b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_special_lifetimes_and_events.snap index e9538b41..3a62e509 100644 --- a/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_special_lifetimes_and_events.snap +++ b/rs/ethexe/macros-tests/tests/snapshots/service_insta__works_with_special_lifetimes_and_events.snap @@ -149,7 +149,7 @@ where type EventsMeta = my_generic_events_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = false; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -163,7 +163,16 @@ where final_hash = final_hash .update(&::HASH); let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod my_generic_events_service_meta { diff --git a/rs/idl-gen/tests/generator.rs b/rs/idl-gen/tests/generator.rs index aebc4105..197b7662 100644 --- a/rs/idl-gen/tests/generator.rs +++ b/rs/idl-gen/tests/generator.rs @@ -2,7 +2,7 @@ use gprimitives::*; use meta_params::*; use sails_idl_gen::{program, service}; use sails_idl_meta::{ - AnyServiceMeta, AnyServiceMetaFn, ProgramMeta, ServiceMeta as RtlServiceMeta, + AnyServiceMeta, AnyServiceMetaFn, InterfaceId, ProgramMeta, ServiceMeta as RtlServiceMeta, }; use scale_info::{StaticTypeInfo, TypeInfo}; use std::{collections::BTreeMap, result::Result as StdResult}; @@ -192,7 +192,7 @@ impl RtlServiceMeta type EventsMeta = E; const BASE_SERVICES: &'static [AnyServiceMetaFn] = &[]; const ASYNC: bool = false; - const INTERFACE_ID: [u8; 8] = [0u8; 8]; + const INTERFACE_ID: InterfaceId = InterfaceId([0u8; 8]); } struct ServiceMetaWithBase { @@ -210,7 +210,7 @@ impl type EventsMeta = E; const BASE_SERVICES: &'static [AnyServiceMetaFn] = &[AnyServiceMeta::new::]; const ASYNC: bool = false; - const INTERFACE_ID: [u8; 8] = [0u8; 8]; + const INTERFACE_ID: InterfaceId = InterfaceId([0u8; 8]); } type TestServiceMeta = ServiceMeta; diff --git a/rs/idl-meta/Cargo.toml b/rs/idl-meta/Cargo.toml index 10bd7c7d..7f66230e 100644 --- a/rs/idl-meta/Cargo.toml +++ b/rs/idl-meta/Cargo.toml @@ -12,6 +12,7 @@ rust-version.workspace = true [dependencies] askama = { workspace = true, optional = true } scale-info.workspace = true +parity-scale-codec = { workspace = true, features = ["derive"] } [dev-dependencies] insta.workspace = true diff --git a/rs/idl-meta/src/lib.rs b/rs/idl-meta/src/lib.rs index 6b15ef29..b7030bbf 100644 --- a/rs/idl-meta/src/lib.rs +++ b/rs/idl-meta/src/lib.rs @@ -7,17 +7,89 @@ mod ast; #[cfg(feature = "ast")] pub use ast::*; +use parity_scale_codec::{Decode, Encode, Error}; use scale_info::{MetaType, StaticTypeInfo, prelude::vec::Vec}; pub type AnyServiceMetaFn = fn() -> AnyServiceMeta; +/// Unique identifier for a service (or "interface" in terms of sails binary protocol). +/// +/// For more information about interface IDs, see the interface ID spec. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InterfaceId(pub [u8; 8]); + +impl InterfaceId { + /// Create a zeroed interface ID. + pub fn zero() -> Self { + Self([0u8; 8]) + } + + /// Get interface ID as a byte slice + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Deserialize from bytes, advancing the slice + pub fn try_read_bytes(bytes: &mut &[u8]) -> Result { + if bytes.len() < 8 { + return Err("Insufficient bytes for interface ID"); + } + + let mut id = [0u8; 8]; + id.copy_from_slice(&bytes[0..8]); + *bytes = &bytes[8..]; + Ok(Self(id)) + } + + /// Deserialize from bytes without mutating the input + pub fn try_from_bytes(bytes: &[u8]) -> Result { + let mut slice = bytes; + Self::try_read_bytes(&mut slice) + } +} + +impl Encode for InterfaceId { + fn encode_to(&self, dest: &mut O) { + dest.write(self.as_bytes()); + } +} + +impl Decode for InterfaceId { + fn decode(input: &mut I) -> Result { + let mut bytes = [0u8; 8]; + input.read(&mut bytes)?; + let mut slice = bytes.as_slice(); + Self::try_read_bytes(&mut slice).map_err(Error::from) + } +} + +/// Compile-time service identifier information +#[derive(Copy, Clone)] +pub struct AnyServiceIds { + pub base_services: &'static [AnyServiceIds], + pub interface_id: [u8; 8], +} + +impl AnyServiceIds { + /// Create new service IDs from a service meta. + pub const fn new() -> Self { + Self { + base_services: S::BASE_SERVICES_IDS, + interface_id: S::INTERFACE_ID.0, + } + } +} + pub trait ServiceMeta { type CommandsMeta: StaticTypeInfo; type QueriesMeta: StaticTypeInfo; type EventsMeta: StaticTypeInfo; + /// The order of base services here is lexicographical by their names const BASE_SERVICES: &'static [AnyServiceMetaFn]; + /// The order of base services here is lexicographical by their names + const BASE_SERVICES_IDS: &'static [AnyServiceIds]; const ASYNC: bool; - const INTERFACE_ID: [u8; 8]; + const INTERFACE_ID: InterfaceId; fn commands() -> MetaType { MetaType::new::() @@ -83,3 +155,57 @@ pub trait ProgramMeta { Self::SERVICES.iter().map(|(s, f)| (*s, f())) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn interface_id_codec() { + let inner = [1u8, 2, 3, 4, 5, 6, 7, 8]; + let id = InterfaceId(inner); + + let encoded = id.encode(); + assert_eq!(inner.encode(), encoded); + + let decoded = Decode::decode(&mut &encoded[..]).unwrap(); + assert_eq!(id, decoded); + } + + #[test] + fn interface_id_serde() { + let inner = [1u8, 2, 3, 4, 5, 6, 7, 8]; + let mut slice = inner.as_slice(); + let id = InterfaceId::try_read_bytes(&mut slice).unwrap(); + assert_eq!(inner, id.0); + assert_eq!(slice.len(), 0); + assert_eq!(id.as_bytes(), inner); + } + + #[test] + fn interface_id_try_read_bytes() { + // Read from a slice with extra data + let data = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let mut slice = data.as_slice(); + + let id = InterfaceId::try_read_bytes(&mut slice).unwrap(); + assert_eq!(id.0, [1, 2, 3, 4, 5, 6, 7, 8]); + assert_eq!(slice, &[9, 10]); + + // Read from a slice with insufficient data + let data = [1u8, 2, 3, 4, 5, 6, 7]; + let mut slice = data.as_slice(); + let result = InterfaceId::try_read_bytes(&mut slice); + assert_eq!(result, Err("Insufficient bytes for interface ID")); + } + + #[test] + fn interface_id_try_from_bytes() { + let data = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let slice = data.as_slice(); + + let id = InterfaceId::try_from_bytes(slice).unwrap(); + assert_eq!(id.0, [1, 2, 3, 4, 5, 6, 7, 8]); + assert_eq!(slice.len(), data.len()); // Original slice should remain unchanged + } +} diff --git a/rs/macros/core/src/program/mod.rs b/rs/macros/core/src/program/mod.rs index 56ea5413..fe98df9f 100644 --- a/rs/macros/core/src/program/mod.rs +++ b/rs/macros/core/src/program/mod.rs @@ -143,7 +143,6 @@ impl ProgramBuilder { let mut services_route = Vec::new(); let mut services_meta = Vec::new(); let mut meta_asyncness = Vec::new(); - let mut invocation_dispatches = Vec::new(); let mut routes = BTreeMap::new(); // only used for ethexe #[allow(unused_mut)] @@ -158,33 +157,29 @@ impl ProgramBuilder { meta_asyncness.push(quote!(true)); } - let item_impl = self - .program_impl - .items - .iter() - .enumerate() - .filter_map(|(idx, impl_item)| { - if let ImplItem::Fn(fn_item) = impl_item - && service_ctor_predicate(fn_item) + // Collect all data we need in one pass without holding borrows + let mut modifications = Vec::new(); + let mut services_count_data = Vec::new(); // Store data for count_base_services calls + let mut services_ids_data = Vec::new(); // Store data for AnyServiceIds + let mut route_dispatch_data = Vec::new(); // Store data for route dispatches + + for (idx, impl_item) in self.program_impl.items.iter().enumerate() { + if let ImplItem::Fn(fn_item) = impl_item + && service_ctor_predicate(fn_item) + { + let (span, route, unwrap_result, _) = shared::invocation_export_or_default(fn_item); + if let Some(duplicate) = routes.insert(route.clone(), fn_item.sig.ident.to_string()) { - let (span, route, unwrap_result, _) = - shared::invocation_export_or_default(fn_item); - if let Some(duplicate) = - routes.insert(route.clone(), fn_item.sig.ident.to_string()) - { - abort!( - span, - "`export` attribute conflicts with one already assigned to '{}'", - duplicate - ); - } - return Some((idx, route, fn_item, unwrap_result)); + abort!( + span, + "`export` attribute conflicts with one already assigned to '{}'", + duplicate + ); } - None - }) - .map(|(idx, route, fn_item, unwrap_result)| { + let fn_builder = FnBuilder::from(route, true, fn_item, unwrap_result, self.sails_path()); + let original_service_ctor_fn = fn_builder.original_service_ctor_fn(); let wrapping_service_ctor_fn = fn_builder.wrapping_service_ctor_fn(&original_service_ctor_fn.sig.ident); @@ -193,31 +188,36 @@ impl ProgramBuilder { services_meta.push(fn_builder.service_meta()); if !has_async_ctor { - // If there are no async constructors, we can't push the asyncness as false, - // as there could be async handlers in services. meta_asyncness.push(fn_builder.service_meta_asyncness()); } - invocation_dispatches.push(fn_builder.service_invocation()); + #[cfg(feature = "ethexe")] solidity_dispatchers.push(fn_builder.sol_service_invocation()); - (idx, original_service_ctor_fn, wrapping_service_ctor_fn) - }) - .collect::>(); + // Extract data needed later (not the fn_builder itself) + let service_type = fn_builder.result_type.clone(); + let service_ctor_ident = fn_builder.ident.clone(); - if meta_asyncness.is_empty() { - // In case non of constructors is async and there are no services exposed. - meta_asyncness.push(quote!(false)); + services_count_data.push(service_type.clone()); + services_ids_data.push(service_type.clone()); + route_dispatch_data.push((service_ctor_ident, service_type)); + + modifications.push((idx, original_service_ctor_fn, wrapping_service_ctor_fn)); + } } - // replace service ctor fn impls - for (idx, original_service_ctor_fn, wrapping_service_ctor_fn, ..) in item_impl { + // Apply modifications to self - no more borrows from items after this point + for (idx, original_service_ctor_fn, wrapping_service_ctor_fn) in modifications { self.program_impl.items[idx] = ImplItem::Fn(original_service_ctor_fn); self.program_impl .items .push(ImplItem::Fn(wrapping_service_ctor_fn)); } + if meta_asyncness.is_empty() { + meta_asyncness.push(quote!(false)); + } + let handle_reply_fn = self.handle_reply_fn().map(|item_fn| { let handle_reply_fn_ident = &item_fn.sig.ident; quote! { @@ -250,9 +250,84 @@ impl ProgramBuilder { } }; - invocation_dispatches.push(quote! { - { gstd::unknown_input_panic("Unexpected service", &input) } - }); + // Generate the INTERFACE_IDS registry using extracted data + let services_count_expr = { + let count_exprs = services_count_data.iter().map(|service_type| { + quote! { + + #sails_path::count_base_services::<#service_type>() + } + }); + + let base_count = services_count_data.len(); + + quote! { + const SERVICES_COUNT: usize = #base_count #(#count_exprs)*; + } + }; + + let services_ids_expr = { + let ids_exprs = services_ids_data.iter().map(|service_type| { + quote! { + #sails_path::meta::AnyServiceIds::new::<#service_type>(), + } + }); + + quote! { + const INTERFACE_IDS: &'static [(#sails_path::meta::InterfaceId, u8)] = + &#sails_path::interface_ids::(&[ + #(#ids_exprs)* + ]); + } + }; + + // Generate route_id match arms using extracted data + let route_dispatches = route_dispatch_data + .iter() + .enumerate() + .map(|(idx, (service_ctor_ident, service_type))| { + let route_id = (idx + 1) as u8; + + quote! { + #route_id => { + let svc = program_ref.#service_ctor_ident(); + let is_async = <#service_type as #sails_path::gstd::services::Service>::Exposure::check_asyncness(interface_id, entry_id) + .unwrap_or_else(|| { + gstd::unknown_input_panic("Unknown call", &[]) + }); + if is_async { + gstd::message_loop(async move { + svc + .try_handle_async( + interface_id, + entry_id, + payload, + |encoded_result, value| { + gstd::msg::reply_bytes(encoded_result, value) + .expect("Failed to send output"); + }, + ) + .await + .unwrap_or_else(|| { + gstd::unknown_input_panic("Unknown request", &[]) + }); + }); + } else { + svc + .try_handle( + interface_id, + entry_id, + payload, + |encoded_result, value| { + gstd::msg::reply_bytes(encoded_result, value) + .expect("Failed to send output"); + }, + ) + .unwrap_or_else(|| gstd::unknown_input_panic("Unknown request", &[])); + } + } + } + }) + .collect::>(); let solidity_main = self.sol_main(solidity_dispatchers.as_slice()); @@ -265,18 +340,29 @@ impl ProgramBuilder { }); let main_fn = quote!( + #services_count_expr + #services_ids_expr + #[unsafe(no_mangle)] extern "C" fn handle() { #payable - let mut input = gstd::msg::load_bytes().expect("Failed to read input"); + let sails_message = gstd::msg::load::<#sails_path::SailsMessage>() + .expect("Failed to read sails message"); + let program_ref = unsafe { #program_ident.as_mut() }.expect("Program not initialized"); #solidity_main - #(#invocation_dispatches)else*; - } + let (interface_id, route_id, entry_id, payload) = sails_message + .try_match_interfaces(INTERFACE_IDS) + .expect("Failed to find matching service"); + match route_id { + #(#route_dispatches)* + _ => gstd::unknown_input_panic("Unknown route_id", &[route_id]) + } + } ); let handle_reply_fn = quote! { @@ -578,41 +664,6 @@ impl FnBuilder<'_> { ) } - fn service_invocation(&self) -> TokenStream2 { - let route_ident = &self.route_ident(); - let service_ctor_ident = self.ident; - quote! { - if input.starts_with(& #route_ident) { - let mut service = program_ref.#service_ctor_ident(); - let is_async = service - .check_asyncness(&input[#route_ident .len()..]) - .unwrap_or_else(|| { - gstd::unknown_input_panic("Unknown call", &input[#route_ident .len()..]) - }); - if is_async { - gstd::message_loop(async move { - service - .try_handle_async(&input[#route_ident .len()..], |encoded_result, value| { - gstd::msg::reply_bytes(encoded_result, value) - .expect("Failed to send output"); - }) - .await - .unwrap_or_else(|| { - gstd::unknown_input_panic("Unknown request", &input) - }); - }); - } else { - service - .try_handle(&input[#route_ident .len()..], |encoded_result, value| { - gstd::msg::reply_bytes(encoded_result, value) - .expect("Failed to send output"); - }) - .unwrap_or_else(|| gstd::unknown_input_panic("Unknown request", &input)); - } - } - } - } - fn original_service_ctor_fn(&self) -> ImplItemFn { let mut original_service_ctor_fn = self.impl_fn.clone(); let original_service_ctor_fn_ident = Ident::new( diff --git a/rs/macros/core/src/service/exposure.rs b/rs/macros/core/src/service/exposure.rs index a612adec..adca8637 100644 --- a/rs/macros/core/src/service/exposure.rs +++ b/rs/macros/core/src/service/exposure.rs @@ -70,7 +70,6 @@ impl ServiceBuilder<'_> { } pub(super) fn exposure_impl(&self) -> TokenStream { - let sails_path = self.sails_path; let exposure_ident = &self.exposure_ident; let generics = &self.generics; let service_type_path = self.type_path; @@ -97,10 +96,6 @@ impl ServiceBuilder<'_> { impl #generics #exposure_ident< #service_type_path > #service_type_constraints { #( #exposure_funcs )* - pub fn check_asyncness(&self, input: &[u8]) -> Option { - ::check_asyncness(input) - } - #try_handle_impl #try_handle_solidity_impl @@ -114,8 +109,10 @@ impl ServiceBuilder<'_> { pub(super) fn try_handle_impl(&self) -> TokenStream { let sails_path = self.sails_path; + let service_type_path = self.type_path; let inner_ident = &self.inner_ident; - let input_ident = &self.input_ident; + let meta_module_ident = &self.meta_module_ident; + let impl_inner = |is_async: bool| { let (name_ident, asyncness, await_token) = if is_async { ( @@ -127,9 +124,48 @@ impl ServiceBuilder<'_> { (quote!(try_handle), None, None) }; - let invocation_dispatches = self.service_handlers.iter().filter_map(|fn_builder| { + // Generate match arms for each handler's entry_id + let invocation_dispatches = self.service_handlers_with_ids.iter().filter_map(|(fn_builder, entry_id)| { if is_async == fn_builder.is_async() { - Some(fn_builder.try_handle_branch_impl(&self.meta_module_ident, input_ident)) + let handler_func_ident = fn_builder.ident; + let params_struct_ident = &fn_builder.params_struct_ident; + let handler_func_params = fn_builder + .params_idents() + .iter() + .map(|ident| quote!(request.#ident)); + + let (result_type, reply_with_value) = fn_builder.result_type_with_value(); + let unwrap_token = fn_builder.unwrap_result.then(|| quote!(.unwrap())); + + let handle_token = if reply_with_value { + quote! { + let command_reply: CommandReply< #result_type > = self.#handler_func_ident(#(#handler_func_params),*)#await_token #unwrap_token.into(); + let (result, value) = command_reply.to_tuple(); + } + } else { + quote! { + let result = self.#handler_func_ident(#(#handler_func_params),*)#await_token #unwrap_token; + let value = 0u128; + } + }; + + let result_type_static = fn_builder.result_type_with_static_lifetime(); + + Some(quote! { + #entry_id => { + let request = <#meta_module_ident::#params_struct_ident as #sails_path::gstd::InvocationIo>::decode_params(&input) + .expect("Failed to decode params"); + #handle_token + if !<#meta_module_ident::#params_struct_ident as #sails_path::gstd::InvocationIo>::is_empty_tuple::<#result_type_static>() { + <#meta_module_ident::#params_struct_ident as #sails_path::gstd::InvocationIo>::with_optimized_encode( + &result, + self.route().as_ref(), + |encoded_result| result_handler(encoded_result, value), + ); + } + return Some(()); + } + }) } else { None } @@ -138,18 +174,39 @@ impl ServiceBuilder<'_> { let base_invocation = if self.base_types.is_empty() { None } else { + // Sort base services lexicographically + let mut base_services_with_index: Vec<_> = self + .base_types + .iter() + .enumerate() + .map(|(idx, base_type)| (idx, base_type, shared::remove_lifetimes(base_type))) + .collect(); + + base_services_with_index.sort_by_key(|(_, _, path_wo_lifetimes)| { + path_wo_lifetimes + .segments + .last() + .expect("Base service path should have at least one segment") + .ident + .to_string() + .to_lowercase() + }); + let base_types = self.base_types; - let base_exposure_invocations = base_types.iter().enumerate().map(|(idx, _)| { - let idx_token = if base_types.len() == 1 { None } else { - let idx_literal = Literal::usize_unsuffixed(idx); + let base_exposure_invocations = base_services_with_index.iter().map(|(idx, _, _)| { + let idx_token = if base_types.len() == 1 { + None + } else { + let idx_literal = Literal::usize_unsuffixed(*idx); Some(quote! { . #idx_literal }) }; quote! { - if base_services #idx_token .expose(self.route) . #name_ident(#input_ident, result_handler) #await_token.is_some() { + if base_services #idx_token .expose(self.route) . #name_ident(interface_id, entry_id, input.clone(), result_handler) #await_token.is_some() { return Some(()); } } }); + // Base Services, as `Into` tuple from Service Some(quote! { let base_services: ( #( #base_types ),* ) = self. #inner_ident .into(); @@ -158,12 +215,25 @@ impl ServiceBuilder<'_> { }; quote! { - pub #asyncness fn #name_ident(mut self, #input_ident : &[u8], result_handler: fn(&[u8], u128)) -> Option<()> { - use #sails_path::gstd::InvocationIo; + pub #asyncness fn #name_ident( + mut self, + interface_id: #sails_path::meta::InterfaceId, + entry_id: u16, + input: #sails_path::Vec, + result_handler: fn(&[u8], u128) + ) -> Option<()> { + use #sails_path::gstd::{InvocationIo, CommandReply}; use #sails_path::gstd::services::{Service, Exposure}; - #( #invocation_dispatches )* - #base_invocation - None + + if interface_id == <#service_type_path as #sails_path::meta::ServiceMeta>::INTERFACE_ID { + match entry_id { + #( #invocation_dispatches )* + _ => None, + } + } else { + #base_invocation + None + } } } }; @@ -203,7 +273,6 @@ impl ServiceBuilder<'_> { fn check_asyncness_impl(&self) -> TokenStream { let sails_path = self.sails_path; - let input_ident = &self.input_ident; // Here `T` is Service Type let service_asyncness_check = quote! { @@ -215,31 +284,56 @@ impl ServiceBuilder<'_> { } }; - let asyncness_checks = self.service_handlers.iter().map(|fn_builder| { - fn_builder.check_asyncness_branch_impl(&self.meta_module_ident, input_ident) + // Generate match arms for each handler's entry_id + let asyncness_checks = + self.service_handlers_with_ids + .iter() + .map(|(fn_builder, entry_id)| { + let is_async = fn_builder.is_async(); + quote! { + #entry_id => Some(#is_async), + } + }); + + // Sort base services lexicographically + let mut base_services_sorted = self + .base_types + .iter() + .map(shared::remove_lifetimes) + .collect::>(); + base_services_sorted.sort_by_key(|path_wo_lifetimes| { + path_wo_lifetimes + .segments + .last() + .expect("Base service path should have at least one segment") + .ident + .to_string() + .to_lowercase() }); - let base_services_asyncness_checks = self.base_types.iter().map(|base_type| { - let path_wo_lifetimes = shared::remove_lifetimes(base_type); + let base_services_asyncness_checks = base_services_sorted.iter().map(|base_type| { quote! { - if let Some(is_async) = <<#path_wo_lifetimes as Service>::Exposure as Exposure>::check_asyncness(#input_ident) { + if let Some(is_async) = <<#base_type as Service>::Exposure as Exposure>::check_asyncness(interface_id, entry_id) { return Some(is_async); } } }); quote! { - fn check_asyncness(#input_ident : &[u8]) -> Option { - use #sails_path::gstd::InvocationIo; + fn check_asyncness(interface_id: #sails_path::meta::InterfaceId, entry_id: u16) -> Option { use #sails_path::gstd::services::{Service, Exposure}; #service_asyncness_check - #( #asyncness_checks )* - - #( #base_services_asyncness_checks )* - - None + if interface_id == T::INTERFACE_ID { + match entry_id { + #( #asyncness_checks )* + _ => None, + } + } else { + #( #base_services_asyncness_checks )* + None + } } } } diff --git a/rs/macros/core/src/service/meta.rs b/rs/macros/core/src/service/meta.rs index 161135ca..06ea7516 100644 --- a/rs/macros/core/src/service/meta.rs +++ b/rs/macros/core/src/service/meta.rs @@ -10,13 +10,34 @@ impl ServiceBuilder<'_> { let service_type_constraints = self.type_constraints(); let meta_module_ident = &self.meta_module_ident; - let base_services_meta = self.base_types.iter().map(|base_type| { - let path_wo_lifetimes = shared::remove_lifetimes(base_type); + // Sort base services lexicographically + let mut base_services_sorted = self + .base_types + .iter() + .map(|base_type| (base_type, shared::remove_lifetimes(base_type))) + .collect::>(); + base_services_sorted.sort_by_key(|(_, path_wo_lifetimes)| { + path_wo_lifetimes + .segments + .last() + .expect("Base service path should have at least one segment") + .ident + .to_string() + .to_lowercase() + }); + + let base_services_meta = base_services_sorted.iter().map(|(_, path_wo_lifetimes)| { quote! { #sails_path::meta::AnyServiceMeta::new::< #path_wo_lifetimes > } }); + let base_services_ids = base_services_sorted.iter().map(|(_, path_wo_lifetimes)| { + quote! { + #sails_path::meta::AnyServiceIds::new::< #path_wo_lifetimes >() + } + }); + let has_async_handler = self .service_handlers .iter() @@ -27,8 +48,7 @@ impl ServiceBuilder<'_> { } else if self.base_types.is_empty() { quote!(false) } else { - let base_asyncness = self.base_types.iter().map(|base_type| { - let path_wo_lifetimes = shared::remove_lifetimes(base_type); + let base_asyncness = base_services_sorted.iter().map(|(_, path_wo_lifetimes)| { quote! { <#path_wo_lifetimes as #sails_path::meta::ServiceMeta>::ASYNC } @@ -46,8 +66,11 @@ impl ServiceBuilder<'_> { const BASE_SERVICES: &'static [#sails_path::meta::AnyServiceMetaFn] = &[ #( #base_services_meta ),* ]; + const BASE_SERVICES_IDS: &'static [#sails_path::meta::AnyServiceIds] = &[ + #( #base_services_ids ),* + ]; const ASYNC: bool = #service_meta_asyncness ; - const INTERFACE_ID: [u8; 8] = #interface_id_computation; + const INTERFACE_ID: #sails_path::meta::InterfaceId = #interface_id_computation; } } } @@ -182,7 +205,7 @@ impl ServiceBuilder<'_> { }); let base_service_ids = base_services.into_iter().map(|base_type_no_lifetime| { - quote!(final_hash = final_hash.update(&<#base_type_no_lifetime as #sails_path::meta::ServiceMeta>::INTERFACE_ID);) + quote!(final_hash = final_hash.update(&<#base_type_no_lifetime as #sails_path::meta::ServiceMeta>::INTERFACE_ID.0);) }); quote!(#(#base_service_ids)*) @@ -204,7 +227,7 @@ impl ServiceBuilder<'_> { #base_services_hash let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + #sails_path::meta::InterfaceId([hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]]) } } } diff --git a/rs/macros/core/src/service/mod.rs b/rs/macros/core/src/service/mod.rs index 37179656..e849dfcd 100644 --- a/rs/macros/core/src/service/mod.rs +++ b/rs/macros/core/src/service/mod.rs @@ -65,10 +65,11 @@ struct ServiceBuilder<'a> { type_path: &'a TypePath, events_type: Option<&'a Path>, service_handlers: Vec>, + /// Handlers with assigned entry IDs (sorted: commands then queries) + service_handlers_with_ids: Vec<(FnBuilder<'a>, u16)>, exposure_ident: Ident, route_ident: Ident, inner_ident: Ident, - input_ident: Ident, meta_module_ident: Ident, } @@ -82,6 +83,10 @@ impl<'a> ServiceBuilder<'a> { let (type_path, _type_args, service_ident) = shared::impl_type_refs(service_impl.self_ty.as_ref()); let service_handlers = discover_service_handlers(service_impl, sails_path); + + // Assign entry IDs: commands (sorted) get 0..N-1, queries (sorted) get N..M-1 + let service_handlers_with_ids = Self::assign_entry_ids(&service_handlers); + let exposure_name = format!( "{}Exposure", service_ident.to_string().to_case(Case::Pascal) @@ -89,7 +94,6 @@ impl<'a> ServiceBuilder<'a> { let exposure_ident = Ident::new(&exposure_name, Span::call_site()); let route_ident = Ident::new("route", Span::call_site()); let inner_ident = Ident::new("inner", Span::call_site()); - let input_ident = Ident::new("input", Span::call_site()); let meta_module_name = format!("{}_meta", service_ident.to_string().to_case(Case::Snake)); let meta_module_ident = Ident::new(&meta_module_name, Span::call_site()); @@ -102,14 +106,38 @@ impl<'a> ServiceBuilder<'a> { type_path, events_type: service_args.events_type(), service_handlers, + service_handlers_with_ids, exposure_ident, route_ident, inner_ident, - input_ident, meta_module_ident, } } + fn assign_entry_ids(handlers: &[FnBuilder<'a>]) -> Vec<(FnBuilder<'a>, u16)> { + let (mut commands, mut queries): (Vec<_>, Vec<_>) = + handlers.iter().partition(|h| !h.is_query()); + + // Sort by name (lowercase for case-insensitive) + commands.sort_by_key(|h| h.route.to_lowercase()); + queries.sort_by_key(|h| h.route.to_lowercase()); + + let mut result = Vec::new(); + let mut entry_id = 0u16; + + for cmd in commands { + result.push((cmd.clone(), entry_id)); + entry_id += 1; + } + + for query in queries { + result.push((query.clone(), entry_id)); + entry_id += 1; + } + + result + } + fn type_constraints(&self) -> Option<&WhereClause> { self.type_constraints.as_ref() } @@ -232,65 +260,6 @@ impl FnBuilder<'_> { } ) } - - fn try_handle_branch_impl( - &self, - meta_module_ident: &Ident, - input_ident: &Ident, - ) -> TokenStream { - let handler_func_ident = self.ident; - - let params_struct_ident = &self.params_struct_ident; - let handler_func_params = self - .params_idents() - .iter() - .map(|ident| quote!(request.#ident)); - - let (result_type, reply_with_value) = self.result_type_with_value(); - let await_token = self.is_async().then(|| quote!(.await)); - let unwrap_token = self.unwrap_result.then(|| quote!(.unwrap())); - - let handle_token = if reply_with_value { - quote! { - let command_reply: CommandReply< #result_type > = self.#handler_func_ident(#(#handler_func_params),*)#await_token #unwrap_token.into(); - let (result, value) = command_reply.to_tuple(); - } - } else { - quote! { - let result = self.#handler_func_ident(#(#handler_func_params),*)#await_token #unwrap_token; - let value = 0u128; - } - }; - - let result_type = self.result_type_with_static_lifetime(); - quote! { - if let Ok(request) = #meta_module_ident::#params_struct_ident::decode_params( #input_ident) { - #handle_token - if !#meta_module_ident::#params_struct_ident::is_empty_tuple::<#result_type>() { - #meta_module_ident::#params_struct_ident::with_optimized_encode( - &result, - self.route().as_ref(), - |encoded_result| result_handler(encoded_result, value), - ); - } - return Some(()); - } - } - } - - fn check_asyncness_branch_impl( - &self, - meta_module_ident: &Ident, - input_ident: &Ident, - ) -> TokenStream { - let params_struct_ident = &self.params_struct_ident; - - quote! { - if let Ok(is_async) = #meta_module_ident::#params_struct_ident::check_asyncness( #input_ident) { - return Some(is_async); - } - } - } } #[cfg(test)] diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_allow_attrs.snap b/rs/macros/core/tests/snapshots/gservice__works_with_allow_attrs.snap index 7a0c4758..221e2049 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_allow_attrs.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_allow_attrs.snap @@ -110,7 +110,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -137,7 +137,16 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_basics.snap b/rs/macros/core/tests/snapshots/gservice__works_with_basics.snap index a26d3802..6674d69d 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_basics.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_basics.snap @@ -107,7 +107,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -134,7 +134,16 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_crate_path.snap b/rs/macros/core/tests/snapshots/gservice__works_with_crate_path.snap index ea83db7f..6ff4615d 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_crate_path.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_crate_path.snap @@ -107,7 +107,7 @@ impl sails_rename::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rename::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rename::meta::InterfaceId = { let mut final_hash = sails_rename::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rename::keccak_const::Keccak256::new(); @@ -136,7 +136,16 @@ impl sails_rename::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rename::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_docs.snap b/rs/macros/core/tests/snapshots/gservice__works_with_docs.snap index 402d1636..bba45f04 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_docs.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_docs.snap @@ -110,7 +110,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -137,7 +137,16 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_events.snap b/rs/macros/core/tests/snapshots/gservice__works_with_events.snap index 5ff19056..1a0c499b 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_events.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_events.snap @@ -115,7 +115,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = false; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -138,7 +138,16 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash .update(&::HASH); let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_export.snap b/rs/macros/core/tests/snapshots/gservice__works_with_export.snap index 3d4a8c27..4ee20238 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_export.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_export.snap @@ -117,7 +117,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -146,7 +146,16 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_extends.snap b/rs/macros/core/tests/snapshots/gservice__works_with_extends.snap index 766e2b73..361aa239 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_extends.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_extends.snap @@ -144,7 +144,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { ]; const ASYNC: bool = ::ASYNC || ::ASYNC; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -156,11 +156,20 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } final_hash = final_hash - .update(&::INTERFACE_ID); + .update(&::INTERFACE_ID.0); final_hash = final_hash - .update(&::INTERFACE_ID); + .update(&::INTERFACE_ID.0); let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_extends_and_lifetimes.snap b/rs/macros/core/tests/snapshots/gservice__works_with_extends_and_lifetimes.snap index 3479fa30..3432f65c 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_extends_and_lifetimes.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_extends_and_lifetimes.snap @@ -143,7 +143,7 @@ impl<'a> sails_rs::meta::ServiceMeta for ExtendedLifetime<'a> { sails_rs::meta::AnyServiceMeta::new::, ]; const ASYNC: bool = ::ASYNC; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -164,9 +164,20 @@ impl<'a> sails_rs::meta::ServiceMeta for ExtendedLifetime<'a> { final_hash = final_hash.update(&fn_hash.finalize()); } final_hash = final_hash - .update(&::INTERFACE_ID); + .update( + &::INTERFACE_ID.0, + ); let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod extended_lifetime_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_lifetimes_and_events.snap b/rs/macros/core/tests/snapshots/gservice__works_with_lifetimes_and_events.snap index a608a4a0..0cc9e897 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_lifetimes_and_events.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_lifetimes_and_events.snap @@ -109,7 +109,7 @@ where type EventsMeta = my_generic_events_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = false; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -123,7 +123,16 @@ where final_hash = final_hash .update(&::HASH); let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod my_generic_events_service_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_lifetimes_and_generics.snap b/rs/macros/core/tests/snapshots/gservice__works_with_lifetimes_and_generics.snap index b0ae803f..8ab0d4a7 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_lifetimes_and_generics.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_lifetimes_and_generics.snap @@ -100,7 +100,7 @@ where type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = false; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -112,7 +112,16 @@ where final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_methods_with_lifetimes.snap b/rs/macros/core/tests/snapshots/gservice__works_with_methods_with_lifetimes.snap index b8f499d2..e245efa0 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_methods_with_lifetimes.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_methods_with_lifetimes.snap @@ -196,7 +196,7 @@ impl sails_rs::meta::ServiceMeta for ReferenceService { type EventsMeta = reference_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -260,7 +260,16 @@ impl sails_rs::meta::ServiceMeta for ReferenceService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod reference_service_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_reply_with_value.snap b/rs/macros/core/tests/snapshots/gservice__works_with_reply_with_value.snap index 29b75a63..746e465c 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_reply_with_value.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_reply_with_value.snap @@ -110,7 +110,7 @@ impl sails_rs::meta::ServiceMeta for SomeService { type EventsMeta = some_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = true; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -137,7 +137,16 @@ impl sails_rs::meta::ServiceMeta for SomeService { final_hash = final_hash.update(&fn_hash.finalize()); } let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod some_service_meta { diff --git a/rs/macros/core/tests/snapshots/gservice__works_with_special_lifetimes_and_events.snap b/rs/macros/core/tests/snapshots/gservice__works_with_special_lifetimes_and_events.snap index 258f3ace..935704f9 100644 --- a/rs/macros/core/tests/snapshots/gservice__works_with_special_lifetimes_and_events.snap +++ b/rs/macros/core/tests/snapshots/gservice__works_with_special_lifetimes_and_events.snap @@ -109,7 +109,7 @@ where type EventsMeta = my_generic_events_service_meta::EventsMeta; const BASE_SERVICES: &'static [sails_rs::meta::AnyServiceMetaFn] = &[]; const ASYNC: bool = false; - const INTERFACE_ID: [u8; 8] = { + const INTERFACE_ID: sails_rs::meta::InterfaceId = { let mut final_hash = sails_rs::keccak_const::Keccak256::new(); { let mut fn_hash = sails_rs::keccak_const::Keccak256::new(); @@ -123,7 +123,16 @@ where final_hash = final_hash .update(&::HASH); let hash = final_hash.finalize(); - [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]] + sails_rs::meta::InterfaceId([ + hash[0], + hash[1], + hash[2], + hash[3], + hash[4], + hash[5], + hash[6], + hash[7], + ]) }; } mod my_generic_events_service_meta { diff --git a/rs/src/gstd/services.rs b/rs/src/gstd/services.rs index f13f57e5..bc00d2b7 100644 --- a/rs/src/gstd/services.rs +++ b/rs/src/gstd/services.rs @@ -1,4 +1,4 @@ -use crate::gstd::EventEmitter; +use crate::{gstd::EventEmitter, meta::InterfaceId}; pub trait Service { type Exposure: Exposure; @@ -8,7 +8,7 @@ pub trait Service { pub trait Exposure { fn route(&self) -> &'static [u8]; - fn check_asyncness(input: &[u8]) -> Option; + fn check_asyncness(interface_id: InterfaceId, entry_id: u16) -> Option; } pub trait ExposureWithEvents: Exposure { diff --git a/rs/src/header.rs b/rs/src/header.rs new file mode 100644 index 00000000..fa1d4af7 --- /dev/null +++ b/rs/src/header.rs @@ -0,0 +1,780 @@ +use crate::{ + Vec, + meta::InterfaceId, + scale_codec::{Decode, Encode, Error, Input, Output}, +}; + +/// Sails protocol highest supported version. +pub const HIGHEST_SUPPORTED_VERSION: u8 = 1; + +/// Sails protocol magic bytes. +/// Bytes stand for "GM" utf-8 string. +pub const MAGIC_BYTES: [u8; 2] = [0x47, 0x4D]; + +/// Minimal Sails message header length in bytes. +pub const MINIMAL_HLEN: u8 = 16; + +/// Sails message header. +/// +/// The header is a feature of an IDLv2. It gives opportunity for on-top of blockchain +/// services to trace sails messages and programs. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SailsMessageHeader { + version: Version, + hlen: HeaderLength, + interface_id: InterfaceId, + route_id: u8, + entry_id: u16, +} + +impl SailsMessageHeader { + /// Creates a new Sails message header. + pub fn new( + version: Version, + hlen: HeaderLength, + interface_id: InterfaceId, + route_id: u8, + entry_id: u16, + ) -> Self { + Self { + version, + hlen, + interface_id, + route_id, + entry_id, + } + } + + /// Gets the version of the header. + pub fn version(&self) -> Version { + self.version + } + + /// Gets the header length. + pub fn hlen(&self) -> HeaderLength { + self.hlen + } + + /// Gets the interface ID. + pub fn interface_id(&self) -> InterfaceId { + self.interface_id + } + + /// Gets the route ID. + pub fn route_id(&self) -> u8 { + self.route_id + } + + /// Gets the entry ID. + pub fn entry_id(&self) -> u16 { + self.entry_id + } +} + +// Serialization and deserialization +impl SailsMessageHeader { + /// Serialize header to bytes. + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(self.hlen.inner() as usize); + bytes.extend_from_slice(Magic::new().as_bytes()); + bytes.push(self.version.inner()); + bytes.push(self.hlen.inner()); + bytes.extend_from_slice(self.interface_id.as_bytes()); + bytes.extend_from_slice(&self.entry_id.to_le_bytes()); + bytes.push(self.route_id); + // Reserved byte + bytes.push(0); + + bytes + } + + /// Deserialize header from bytes advancing the slice. + pub fn try_read_bytes(bytes: &mut &[u8]) -> Result { + if bytes.len() < MINIMAL_HLEN as usize { + return Err("Insufficient bytes for header"); + } + + // Validate and consume magic bytes. + Magic::try_read_bytes(bytes)?; + + let version = Version::try_read_bytes(bytes)?; + let hlen = HeaderLength::try_read_bytes(bytes)?; + let interface_id = InterfaceId::try_read_bytes(bytes)?; + + let entry_id = u16::from_le_bytes([bytes[0], bytes[1]]); + let route_id = bytes[2]; + let reserved = bytes[3]; + + if version == Version::v1() && reserved != 0 { + return Err("Reserved byte must be zero in version 1"); + } + + // Read 4 bytes for entry_id, route_id and reserved. + *bytes = &bytes[4..]; + + Ok(Self { + version, + hlen, + interface_id, + route_id, + entry_id, + }) + } + + /// Deserialize header from bytes (expects magic bytes at the start) without mutating the input. + pub fn try_from_bytes(bytes: &[u8]) -> Result { + let mut slice = bytes; + Self::try_read_bytes(&mut slice) + } + + /// Tries to match the header's interface ID and route ID against a list of known interfaces in the program. + pub fn try_match_interfaces( + self, + interfaces: &[(InterfaceId, u8)], + ) -> Result { + let Self { + interface_id, + route_id: message_route_id, + entry_id, + .. + } = self; + + let (same_interface_ids, has_route) = interfaces + .iter() + .filter_map(|(id, r_id)| (*id == interface_id).then_some(*r_id)) + .fold((0, false), |(count, found), program_route_id| { + let new_count = count + 1; + + let new_found = if !found { + message_route_id == program_route_id + } else { + found + }; + + (new_count, new_found) + }); + + if same_interface_ids == 0 { + Err("No matching interface ID found") + } else if message_route_id == 0 && same_interface_ids > 1 { + Err("Can't infer the interface by route id 0, many instances") + } else if !has_route && message_route_id != 0 { + // In case of route_id == 0, the has_route is always false + Err("No matching route ID found for the interface ID") + } else { + Ok(MatchedInterface { + interface_id, + entry_id, + route_id: message_route_id, + }) + } + } +} + +impl Encode for SailsMessageHeader { + fn encode_to(&self, dest: &mut O) { + let bytes = self.to_bytes(); + dest.write(&bytes); + } +} + +impl Decode for SailsMessageHeader { + fn decode(input: &mut I) -> Result { + let mut header_bytes = [0u8; MINIMAL_HLEN as usize]; // Include magic bytes + input.read(&mut header_bytes)?; + + let mut slice = header_bytes.as_slice(); + Self::try_read_bytes(&mut slice).map_err(Error::from) + } +} + +/// Sails message header's protocol magic bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode)] +pub struct Magic([u8; 2]); + +impl Magic { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self(MAGIC_BYTES) + } + + /// Get magic bytes as a byte slice. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Deserialize from bytes, advancing the slice. + pub fn try_read_bytes(bytes: &mut &[u8]) -> Result { + if bytes.len() < MAGIC_BYTES.len() { + return Err("Insufficient bytes for magic"); + } + + let magic = [bytes[0], bytes[1]]; + if magic != MAGIC_BYTES { + return Err("Invalid Sails magic bytes"); + } + + *bytes = &bytes[2..]; + Ok(Self(magic)) + } + + /// Deserialize from bytes without mutating the input. + pub fn try_from_bytes(bytes: &[u8]) -> Result { + let mut slice = bytes; + Self::try_read_bytes(&mut slice) + } +} + +impl Decode for Magic { + fn decode(input: &mut I) -> Result { + let mut magic = [0u8; 2]; + input.read(&mut magic)?; + + let mut slice = magic.as_slice(); + Self::try_read_bytes(&mut slice).map_err(Error::from) + } +} + +/// Sails message header's protocol version. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode)] +pub struct Version(u8); + +impl Version { + /// Instantiates the type with version 1. + pub fn v1() -> Self { + Self(1) + } + + /// Instantiates the type with the latest supported version. + pub fn latest() -> Self { + Self(HIGHEST_SUPPORTED_VERSION) + } + + /// Creates a new version instance if the version is supported. + /// + /// Returns error if the version is unsupported, i.e. if: + /// - version is 0 + /// - version is greater than highest supported version + pub fn new(version: u8) -> Result { + if version == 0 || version > HIGHEST_SUPPORTED_VERSION { + Err("Unsupported Sails version") + } else { + Ok(Self(version)) + } + } + + /// Get inner version type. + pub fn inner(&self) -> u8 { + self.0 + } + + /// Deserialize from bytes, advancing the slice. + pub fn try_read_bytes(bytes: &mut &[u8]) -> Result { + if bytes.is_empty() { + return Err("Insufficient bytes for version"); + } + + let version = bytes[0]; + *bytes = &bytes[1..]; + Self::new(version) + } + + /// Deserialize from bytes without mutating the input. + pub fn try_from_bytes(bytes: &[u8]) -> Result { + let mut slice = bytes; + Self::try_read_bytes(&mut slice) + } +} + +impl Decode for Version { + fn decode(input: &mut I) -> Result { + let version = input.read_byte()?; + let version_array = [version]; + + Self::try_read_bytes(&mut version_array.as_slice()).map_err(Error::from) + } +} + +/// Sails message header length. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode)] +pub struct HeaderLength(u8); + +impl HeaderLength { + pub fn new(hlen: u8) -> Result { + if hlen < MINIMAL_HLEN { + Err("Header length is less than minimal Sails header length") + } else { + Ok(Self(hlen)) + } + } + + /// Get the header length as a u8. + pub fn inner(&self) -> u8 { + self.0 + } + + /// Deserialize from bytes, advancing the slice. + pub fn try_read_bytes(bytes: &mut &[u8]) -> Result { + if bytes.is_empty() { + return Err("Insufficient bytes for header length"); + } + + let hlen = bytes[0]; + *bytes = &bytes[1..]; + Self::new(hlen) + } + + /// Deserialize from bytes without mutating the input. + pub fn try_from_bytes(bytes: &[u8]) -> Result { + let mut slice = bytes; + Self::try_read_bytes(&mut slice) + } +} + +impl Decode for HeaderLength { + fn decode(input: &mut I) -> Result { + let hlen = input.read_byte()?; + + let hlen_array = [hlen]; + Self::try_read_bytes(&mut hlen_array.as_slice()).map_err(Error::from) + } +} + +/// The outcome of matching a message header against known interfaces. +/// +/// Contains the matched interface ID, route ID, and entry ID to be executed. +/// +/// The type is only instantiated upon successful matching. This guarantees, that +/// the contained values are against known interfaces map. +#[derive(Debug)] +pub struct MatchedInterface { + interface_id: InterfaceId, + route_id: u8, + entry_id: u16, +} + +impl MatchedInterface { + /// Consumes the matched interface and returns its components. + pub fn into_inner(self) -> (InterfaceId, u8, u16) { + (self.interface_id, self.route_id, self.entry_id) + } +} + +/// Sails message wrapper that owns both header and payload. +/// +/// This type is designed to be decoded from incoming messages and provides +/// convenient access to routing information while owning the payload data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SailsMessage { + header: SailsMessageHeader, + payload: Vec, +} + +impl SailsMessage { + /// Creates a new Sails message form header and encodable payload type. + pub fn new(header: SailsMessageHeader, payload: impl Encode) -> Self { + Self { + header, + payload: payload.encode(), + } + } + + /// Gets a reference to the header. + pub fn header(&self) -> &SailsMessageHeader { + &self.header + } + + /// Gets a reference to the payload. + pub fn payload(&self) -> &[u8] { + &self.payload + } + + /// Matches the message header against known interfaces and returns routing information. + /// + /// Returns `(interface_id, route_id, entry_id, payload)` on success. + pub fn try_match_interfaces( + self, + interfaces: &[(InterfaceId, u8)], + ) -> Result<(InterfaceId, u8, u16, Vec), &'static str> { + let matched = self.header.try_match_interfaces(interfaces)?; + let (interface_id, route_id, entry_id) = matched.into_inner(); + Ok((interface_id, route_id, entry_id, self.payload)) + } +} + +impl Decode for SailsMessage { + fn decode(input: &mut I) -> Result { + // Decode the header + let header = SailsMessageHeader::decode(input)?; + let payload = Decode::decode(input)?; + + Ok(Self { header, payload }) + } +} + +impl Encode for SailsMessage { + fn encode_to(&self, dest: &mut O) { + self.header.encode_to(dest); + dest.write(&self.payload); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use scale_info::prelude::vec; + + #[test] + fn try_from_bytes_does_not_move_offset() { + let magic = Magic::new(); + let bytes = magic.as_bytes(); + + let _ = Magic::try_from_bytes(bytes).expect("same bytes"); + assert_eq!(bytes, [0x47, 0x4D]); + } + + #[test] + fn magic_codec() { + let magic = Magic::new(); + + let encoded = magic.encode(); + assert_eq!(MAGIC_BYTES.encode(), encoded); + let decoded = Magic::decode(&mut &encoded[..]).unwrap(); + + assert_eq!(magic, decoded); + } + + #[test] + fn magic_serde() { + let magic = Magic::new(); + + let mut serialized = magic.as_bytes(); + assert_eq!(MAGIC_BYTES, serialized); + let deserialized = Magic::try_read_bytes(&mut serialized).unwrap(); + + assert_eq!(magic, deserialized); + } + + #[test] + fn magic_try_read_fails() { + // Invalid bytes + let invalid_bytes = [0x00, 0x00]; + let mut slice = invalid_bytes.as_slice(); + let result = Magic::try_read_bytes(&mut slice); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Invalid Sails magic bytes"); + + // Insufficient bytes + let short_bytes = [0x47]; + let mut slice = short_bytes.as_slice(); + let result = Magic::try_read_bytes(&mut slice); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Insufficient bytes for magic"); + } + + #[test] + fn version_serde() { + let version = Version::new(1).unwrap(); + + let serialized = version.inner(); + assert_eq!(1u8, serialized); + let deserialized = Version::try_read_bytes(&mut [serialized].as_slice()).unwrap(); + + assert_eq!(version, deserialized); + } + + #[test] + fn version_codec() { + let version = Version::new(1).unwrap(); + + let encoded = version.encode(); + assert_eq!(1u8.encode(), encoded); + let decoded = Version::decode(&mut &encoded[..]).unwrap(); + + assert_eq!(version, decoded); + } + + #[test] + fn version_try_read_fails() { + // Unsupported version + let bytes1 = [255]; + let bytes2 = [0]; + let mut slice1 = bytes1.as_slice(); + let mut slice2 = bytes2.as_slice(); + + let result1 = Version::try_read_bytes(&mut slice1); + let result2 = Version::try_read_bytes(&mut slice2); + assert!(result1.is_err()); + assert!(result2.is_err()); + + assert_eq!(result1.unwrap_err(), "Unsupported Sails version"); + assert_eq!(result2.unwrap_err(), "Unsupported Sails version"); + + // Insufficient bytes + let bytes: [u8; 0] = []; + let mut slice = bytes.as_slice(); + let result = Version::try_read_bytes(&mut slice); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Insufficient bytes for version"); + } + + #[test] + fn version_latest() { + let version = Version::latest(); + assert_eq!(version.inner(), HIGHEST_SUPPORTED_VERSION); + } + + #[test] + fn header_length_serde() { + let hlen = HeaderLength::new(20).unwrap(); + + let serialized = hlen.inner(); + assert_eq!(20u8, serialized); + let deserialized = HeaderLength::try_read_bytes(&mut [serialized].as_slice()).unwrap(); + + assert_eq!(hlen, deserialized); + } + + #[test] + fn header_length_codec() { + let hlen = HeaderLength::new(20).unwrap(); + + let encoded = hlen.encode(); + assert_eq!(20u8.encode(), encoded); + let decoded = HeaderLength::decode(&mut &encoded[..]).unwrap(); + + assert_eq!(hlen, decoded); + } + + #[test] + fn header_try_read_fails() { + // Header length less than minimal + let bytes1 = [MINIMAL_HLEN - 1]; + let mut slice1 = bytes1.as_slice(); + let result1 = HeaderLength::try_read_bytes(&mut slice1); + + assert!(result1.is_err()); + assert_eq!( + result1.unwrap_err(), + "Header length is less than minimal Sails header length" + ); + + // Insufficient bytes + let bytes: [u8; 0] = []; + let mut slice = bytes.as_slice(); + let result2 = HeaderLength::try_read_bytes(&mut slice); + + assert!(result2.is_err()); + assert_eq!(result2.unwrap_err(), "Insufficient bytes for header length"); + } + + #[test] + fn message_header_serde() { + let header = SailsMessageHeader { + version: Version::new(1).unwrap(), + hlen: HeaderLength::new(MINIMAL_HLEN).unwrap(), + interface_id: InterfaceId([1, 2, 3, 4, 5, 6, 7, 8]), + route_id: 42, + entry_id: 1234, + }; + + let bytes = header.to_bytes(); + assert_eq!(bytes.len(), MINIMAL_HLEN as usize); + assert_eq!( + bytes, + vec![ + 0x47, 0x4D, // magic ("GM") + 1, // version + 16, // hlen + 1, 2, 3, 4, 5, 6, 7, 8, // interface_id + 210, 4, // entry_id (1234 in little-endian) + 42, // route_id + 0, // reserved + ] + ); + + let mut slice = bytes.as_slice(); + let deserialized = SailsMessageHeader::try_read_bytes(&mut slice).unwrap(); + + assert_eq!(header, deserialized); + assert_eq!(slice.len(), 0); // all bytes consumed + } + + #[test] + fn message_header_try_read_fails_invalid_magic() { + // Insufficient bytes (no route id) + let bytes = [0x47, 0x4D, 1, 15, 1, 2, 3, 4, 5, 6, 7, 8, 210, 4]; + + let mut slice = bytes.as_slice(); + let result = SailsMessageHeader::try_read_bytes(&mut slice); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Insufficient bytes for header"); + } + + #[test] + fn message_header_serde_with_surplus() { + let header_bytes = vec![ + 0x47, 0x4D, // magic ("GM") + 1, // version + 16, // hlen + 1, 2, 3, 4, 5, 6, 7, 8, // interface_id + 210, 4, // entry_id (1234 in little-endian) + 42, // route_id + 0, // reserved + // Surplus bytes (payload) + 99, 100, 101, + ]; + let mut slice = header_bytes.as_slice(); + let deserialized = SailsMessageHeader::try_read_bytes(&mut slice).unwrap(); + assert_eq!(deserialized.version, Version::new(1).unwrap()); + assert_eq!(deserialized.hlen, HeaderLength::new(MINIMAL_HLEN).unwrap()); + assert_eq!( + deserialized.interface_id, + InterfaceId([1, 2, 3, 4, 5, 6, 7, 8]) + ); + assert_eq!(deserialized.entry_id, 1234); + assert_eq!(deserialized.route_id, 42); + assert_eq!(slice, &[99, 100, 101]); // Surplus bytes + } + + #[test] + fn message_header_with_non_zero_reserved_fails() { + // Reserved byte is non-zero when version is 1 + let header_bytes = vec![ + 0x47, 0x4D, // magic ("GM") + 1, // version + 16, // hlen + 1, 2, 3, 4, 5, 6, 7, 8, // interface_id + 210, 4, // entry_id (1234 in little-endian) + 42, // route_id + 1, // reserved (non-zero) + ]; + let mut slice = header_bytes.as_slice(); + let result = SailsMessageHeader::try_read_bytes(&mut slice); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + "Reserved byte must be zero in version 1" + ); + + // Reserved byte is non-zero when version is 2 (unsupported currently version) + let header_bytes = vec![ + 0x47, 0x4D, // magic ("GM") + 2, // version + 16, // hlen + 1, 2, 3, 4, 5, 6, 7, 8, // interface_id + 210, 4, // entry_id (1234 in little-endian) + 42, // route_id + 1, // reserved (non-zero) + ]; + let mut slice = header_bytes.as_slice(); + let result = SailsMessageHeader::try_read_bytes(&mut slice); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Unsupported Sails version"); + } + + #[test] + fn match_interfaces_works() { + // Simple test case + let header = SailsMessageHeader { + version: Version::new(1).unwrap(), + hlen: HeaderLength::new(16).unwrap(), + interface_id: InterfaceId([1, 2, 3, 4, 5, 6, 7, 8]), + route_id: 1, + entry_id: 100, + }; + + let interfaces = [(InterfaceId([1, 2, 3, 4, 5, 6, 7, 8]), 1)]; + let result = header.try_match_interfaces(&interfaces).unwrap(); + let (iid, rid, eid) = result.into_inner(); + + assert_eq!(iid, InterfaceId([1, 2, 3, 4, 5, 6, 7, 8])); + assert_eq!(rid, 1); + assert_eq!(eid, 100); + + // Route id zero with single matching interface + let header = SailsMessageHeader { + version: Version::new(1).unwrap(), + hlen: HeaderLength::new(16).unwrap(), + interface_id: InterfaceId([9, 8, 7, 6, 5, 4, 3, 2]), + route_id: 0, + entry_id: 200, + }; + + let interfaces = [ + (InterfaceId([9, 8, 7, 6, 5, 4, 3, 2]), 42), + (InterfaceId([1, 2, 3, 4, 5, 6, 7, 8]), 42), + ]; + + let result = header.try_match_interfaces(&interfaces).unwrap(); + let (iid, rid, eid) = result.into_inner(); + assert_eq!(iid, InterfaceId([9, 8, 7, 6, 5, 4, 3, 2])); + assert_eq!(rid, 0); + assert_eq!(eid, 200); + } + + #[test] + fn match_interfaces_no_match() { + let header = SailsMessageHeader { + version: Version::new(1).unwrap(), + hlen: HeaderLength::new(16).unwrap(), + interface_id: InterfaceId([1, 2, 3, 4, 5, 6, 7, 8]), + route_id: 1, + entry_id: 100, + }; + + let interfaces = [(InterfaceId([9, 9, 9, 9, 9, 9, 9, 9]), 1)]; + let result = header.try_match_interfaces(&interfaces); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "No matching interface ID found"); + } + + #[test] + fn match_interfaces_multiple_same_interface_with_route_zero() { + let header = SailsMessageHeader { + version: Version::new(1).unwrap(), + hlen: HeaderLength::new(16).unwrap(), + interface_id: InterfaceId([1, 2, 3, 4, 5, 6, 7, 8]), + route_id: 0, + entry_id: 100, + }; + + let interfaces = [ + (InterfaceId([1, 2, 3, 4, 5, 6, 7, 8]), 1), + (InterfaceId([1, 2, 3, 4, 5, 6, 7, 8]), 2), + ]; + let result = header.try_match_interfaces(&interfaces); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + "Can't infer the interface by route id 0, many instances" + ); + } + + #[test] + fn match_interfaces_route_mismatch() { + let header = SailsMessageHeader { + version: Version::new(1).unwrap(), + hlen: HeaderLength::new(16).unwrap(), + interface_id: InterfaceId([1, 2, 3, 4, 5, 6, 7, 8]), + route_id: 5, + entry_id: 100, + }; + + let interfaces = [(InterfaceId([1, 2, 3, 4, 5, 6, 7, 8]), 1)]; + let result = header.try_match_interfaces(&interfaces); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + "No matching route ID found for the interface ID" + ); + } +} diff --git a/rs/src/lib.rs b/rs/src/lib.rs index d0391329..c9157048 100644 --- a/rs/src/lib.rs +++ b/rs/src/lib.rs @@ -21,6 +21,7 @@ pub use spin; mod builder; pub mod client; pub mod errors; +pub mod header; #[cfg(feature = "gclient")] #[cfg(not(target_arch = "wasm32"))] pub use gclient; @@ -33,6 +34,7 @@ pub use gtest; #[cfg(not(target_arch = "wasm32"))] pub use mockall; pub mod prelude; +pub mod routing; #[cfg(feature = "ethexe")] pub mod solidity; mod types; diff --git a/rs/src/prelude.rs b/rs/src/prelude.rs index 652b38de..30ca47ee 100644 --- a/rs/src/prelude.rs +++ b/rs/src/prelude.rs @@ -46,6 +46,8 @@ pub use crate::gstd::{ CommandReply, EventEmitter, SailsEvent, Syscall, event, export, program, service, services::Exposure as _, services::ExposureWithEvents as _, }; +pub use crate::header::SailsMessage; +pub use crate::routing::{count_base_services, interface_ids}; pub use crate::types::*; pub use gear_core_errors::{ self as gear_core_errors, ErrorReplyReason, ReplyCode, SignalCode, SimpleExecutionError, diff --git a/rs/src/routing.rs b/rs/src/routing.rs new file mode 100644 index 00000000..e9da05e3 --- /dev/null +++ b/rs/src/routing.rs @@ -0,0 +1,64 @@ +use crate::meta::{AnyServiceIds, InterfaceId, ServiceMeta}; + +/// Count the total number of base services recursively for a service +pub const fn count_base_services() -> usize { + let mut counter = 0; + + let direct_base_services = S::BASE_SERVICES_IDS; + let mut idx = 0; + while idx != direct_base_services.len() { + let any_svc_meta_fn = direct_base_services[idx]; + count_base_services_recursive(&mut counter, any_svc_meta_fn); + idx += 1; + } + + counter +} + +const fn count_base_services_recursive(counter: &mut usize, base: AnyServiceIds) { + *counter += 1; + + let base_services = base.base_services; + let mut idx = 0; + while idx != base_services.len() { + count_base_services_recursive(counter, base_services[idx]); + idx += 1; + } +} + +/// Generate interface IDs array from exposed services +pub const fn interface_ids( + exposed_services: &'static [AnyServiceIds], +) -> [(InterfaceId, u8); N] { + let mut output = [(InterfaceId([0u8; 8]), 0u8); N]; + + let mut exposed_svc_idx = 0; + let mut output_offset = 0; + let mut route_id = 1; + while exposed_svc_idx != exposed_services.len() { + let service = exposed_services[exposed_svc_idx]; + fill_interface_ids_recursive(&mut output, &mut output_offset, service, route_id); + exposed_svc_idx += 1; + route_id += 1; + } + + assert!(output_offset == N, "Mismatched interface IDs count"); + + output +} + +const fn fill_interface_ids_recursive( + arr: &mut [(InterfaceId, u8)], + offset: &mut usize, + service: AnyServiceIds, + route_id: u8, +) { + arr[*offset] = (InterfaceId(service.interface_id), route_id); + *offset += 1; + let base_services = service.base_services; + let mut idx = 0; + while idx != base_services.len() { + fill_interface_ids_recursive(arr, offset, base_services[idx], route_id); + idx += 1; + } +}