From f9052142f818cffafa6827461a6addce70f15a92 Mon Sep 17 00:00:00 2001 From: KPrasch Date: Mon, 2 Jun 2025 20:27:18 +0200 Subject: [PATCH 1/4] Signing Interfaces --- nucypher-core-python/Cargo.toml | 1 + .../nucypher_core/__init__.py | 6 + .../nucypher_core/__init__.pyi | 196 +++++++ nucypher-core-python/src/lib.rs | 378 ++++++++++++++ nucypher-core/src/lib.rs | 6 + nucypher-core/src/signature_request.rs | 492 ++++++++++++++++++ 6 files changed, 1079 insertions(+) create mode 100644 nucypher-core/src/signature_request.rs diff --git a/nucypher-core-python/Cargo.toml b/nucypher-core-python/Cargo.toml index 1f2274a6..a73acc5a 100644 --- a/nucypher-core-python/Cargo.toml +++ b/nucypher-core-python/Cargo.toml @@ -5,6 +5,7 @@ version = "0.15.0" edition = "2018" [lib] +name = "_nucypher_core" crate-type = ["cdylib"] [dependencies] diff --git a/nucypher-core-python/nucypher_core/__init__.py b/nucypher-core-python/nucypher_core/__init__.py index fe122c44..4f657a6d 100644 --- a/nucypher-core-python/nucypher_core/__init__.py +++ b/nucypher-core-python/nucypher_core/__init__.py @@ -29,4 +29,10 @@ SessionStaticSecret, SessionSecretFactory, encrypt_for_dkg, + EIP191SignatureRequest, + UserOperation, + UserOperationSignatureRequest, + PackedUserOperation, + PackedUserOperationSignatureRequest, + SignatureResponse, ) diff --git a/nucypher-core-python/nucypher_core/__init__.pyi b/nucypher-core-python/nucypher_core/__init__.pyi index f515df52..831e774e 100644 --- a/nucypher-core-python/nucypher_core/__init__.pyi +++ b/nucypher-core-python/nucypher_core/__init__.pyi @@ -663,3 +663,199 @@ class SessionSecretFactory: def make_key(self, label: bytes) -> SessionStaticSecret: ... + + +# +# Signature request/response types +# + +class SignatureRequestType: + USEROP: int = 0 + PACKED_USER_OP: int = 1 + EIP_191: int = 2 + EIP_712: int = 3 + + +class AAVersion: + V08: str = "0.8.0" + MDT: str = "mdt" + + +class EIP191SignatureRequest: + + def __init__(self, data: bytes, cohort_id: int, chain_id: int, context: Optional[Context]): + ... + + @property + def data(self) -> bytes: + ... + + @property + def cohort_id(self) -> int: + ... + + @property + def chain_id(self) -> int: + ... + + @property + def context(self) -> Optional[Context]: + ... + + @property + def signature_type(self) -> int: + ... + + @staticmethod + def from_bytes(data: bytes) -> EIP191SignatureRequest: + ... + + def __bytes__(self) -> bytes: + ... + + +class UserOperation: + + def __init__(self, data: str): + ... + + @property + def data(self) -> str: + ... + + def to_bytes(self) -> bytes: + ... + + @staticmethod + def from_bytes(data: bytes) -> UserOperation: + ... + + +class UserOperationSignatureRequest: + + def __init__( + self, + user_op: UserOperation, + cohort_id: int, + chain_id: int, + aa_version: str, + context: Optional[Context] + ): + ... + + @property + def user_op(self) -> UserOperation: + ... + + @property + def cohort_id(self) -> int: + ... + + @property + def chain_id(self) -> int: + ... + + @property + def aa_version(self) -> str: + ... + + @property + def context(self) -> Optional[Context]: + ... + + @property + def signature_type(self) -> int: + ... + + @staticmethod + def from_bytes(data: bytes) -> UserOperationSignatureRequest: + ... + + def __bytes__(self) -> bytes: + ... + + +class PackedUserOperation: + + def __init__(self, data: str): + ... + + @property + def data(self) -> str: + ... + + def to_bytes(self) -> bytes: + ... + + @staticmethod + def from_bytes(data: bytes) -> PackedUserOperation: + ... + + +class PackedUserOperationSignatureRequest: + + def __init__( + self, + packed_user_op: PackedUserOperation, + cohort_id: int, + chain_id: int, + aa_version: str, + context: Optional[Context] + ): + ... + + @property + def packed_user_op(self) -> PackedUserOperation: + ... + + @property + def cohort_id(self) -> int: + ... + + @property + def chain_id(self) -> int: + ... + + @property + def aa_version(self) -> str: + ... + + @property + def context(self) -> Optional[Context]: + ... + + @property + def signature_type(self) -> int: + ... + + @staticmethod + def from_bytes(data: bytes) -> PackedUserOperationSignatureRequest: + ... + + def __bytes__(self) -> bytes: + ... + + +class SignatureResponse: + + def __init__(self, hash: bytes, signature: bytes, signature_type: int): + ... + + @property + def hash(self) -> bytes: + ... + + @property + def signature(self) -> bytes: + ... + + @property + def signature_type(self) -> int: + ... + + @staticmethod + def from_bytes(data: bytes) -> SignatureResponse: + ... + + def __bytes__(self) -> bytes: + ... diff --git a/nucypher-core-python/src/lib.rs b/nucypher-core-python/src/lib.rs index ab80477c..f4b1f09b 100644 --- a/nucypher-core-python/src/lib.rs +++ b/nucypher-core-python/src/lib.rs @@ -1553,6 +1553,376 @@ impl MetadataResponse { } } +// +// SignatureRequestType enum support +// + +fn signature_request_type_to_u8(variant: &nucypher_core::SignatureRequestType) -> u8 { + match variant { + nucypher_core::SignatureRequestType::UserOp => 0, + nucypher_core::SignatureRequestType::PackedUserOp => 1, + nucypher_core::SignatureRequestType::EIP191 => 2, + nucypher_core::SignatureRequestType::EIP712 => 3, + } +} + +fn u8_to_signature_request_type(variant: u8) -> PyResult { + match variant { + 0 => Ok(nucypher_core::SignatureRequestType::UserOp), + 1 => Ok(nucypher_core::SignatureRequestType::PackedUserOp), + 2 => Ok(nucypher_core::SignatureRequestType::EIP191), + 3 => Ok(nucypher_core::SignatureRequestType::EIP712), + _ => Err(PyValueError::new_err(format!( + "Invalid signature request type: {}", + variant + ))), + } +} + +// +// AAVersion enum support +// + +fn aa_version_to_str(version: &nucypher_core::AAVersion) -> &'static str { + match version { + nucypher_core::AAVersion::V08 => "0.8.0", + nucypher_core::AAVersion::MDT => "mdt", + } +} + +fn str_to_aa_version(version: &str) -> PyResult { + match version { + "0.8.0" => Ok(nucypher_core::AAVersion::V08), + "mdt" => Ok(nucypher_core::AAVersion::MDT), + _ => Err(PyValueError::new_err(format!( + "Invalid AA version: {}", + version + ))), + } +} + +// +// EIP191SignatureRequest +// + +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct EIP191SignatureRequest { + backend: nucypher_core::EIP191SignatureRequest, +} + +#[pymethods] +impl EIP191SignatureRequest { + #[new] + pub fn new(data: &[u8], cohort_id: u32, chain_id: u32, context: Option<&Context>) -> Self { + Self { + backend: nucypher_core::EIP191SignatureRequest::new( + data, + cohort_id, + chain_id, + context.map(|c| c.backend.clone()), + ), + } + } + + #[getter] + fn data(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.data).into() + } + + #[getter] + fn cohort_id(&self) -> u32 { + self.backend.cohort_id + } + + #[getter] + fn chain_id(&self) -> u32 { + self.backend.chain_id + } + + #[getter] + fn context(&self) -> Option { + self.backend.context.clone().map(|context| Context { backend: context }) + } + + #[getter] + fn signature_type(&self) -> u8 { + signature_request_type_to_u8(&self.backend.signature_type) + } + + #[staticmethod] + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, nucypher_core::EIP191SignatureRequest>(data) + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) + } +} + +// +// UserOperation +// + +#[pyclass(module = "nucypher_core")] +pub struct UserOperation { + backend: nucypher_core::UserOperation, +} + +#[pymethods] +impl UserOperation { + #[new] + pub fn new(data: String) -> Self { + Self { + backend: nucypher_core::UserOperation::new(data), + } + } + + #[getter] + fn data(&self) -> &str { + &self.backend.data + } + + fn to_bytes(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.to_bytes()).into() + } + + #[staticmethod] + fn from_bytes(data: &[u8]) -> PyResult { + nucypher_core::UserOperation::from_bytes(data) + .map(|backend| Self { backend }) + .map_err(|e| PyValueError::new_err(e)) + } +} + +// +// UserOperationSignatureRequest +// + +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct UserOperationSignatureRequest { + backend: nucypher_core::UserOperationSignatureRequest, +} + +#[pymethods] +impl UserOperationSignatureRequest { + #[new] + pub fn new( + user_op: &UserOperation, + cohort_id: u32, + chain_id: u32, + aa_version: &str, + context: Option<&Context>, + ) -> PyResult { + let aa_version = str_to_aa_version(aa_version)?; + Ok(Self { + backend: nucypher_core::UserOperationSignatureRequest::new( + user_op.backend.clone(), + cohort_id, + chain_id, + aa_version, + context.map(|c| c.backend.clone()), + ), + }) + } + + #[getter] + fn user_op(&self) -> UserOperation { + UserOperation { + backend: self.backend.user_op.clone(), + } + } + + #[getter] + fn cohort_id(&self) -> u32 { + self.backend.cohort_id + } + + #[getter] + fn chain_id(&self) -> u32 { + self.backend.chain_id + } + + #[getter] + fn aa_version(&self) -> &'static str { + aa_version_to_str(&self.backend.aa_version) + } + + #[getter] + fn context(&self) -> Option { + self.backend.context.clone().map(|context| Context { backend: context }) + } + + #[getter] + fn signature_type(&self) -> u8 { + signature_request_type_to_u8(&self.backend.signature_type) + } + + #[staticmethod] + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, nucypher_core::UserOperationSignatureRequest>(data) + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) + } +} + +// +// PackedUserOperation +// + +#[pyclass(module = "nucypher_core")] +pub struct PackedUserOperation { + backend: nucypher_core::PackedUserOperation, +} + +#[pymethods] +impl PackedUserOperation { + #[new] + pub fn new(data: String) -> Self { + Self { + backend: nucypher_core::PackedUserOperation::new(data), + } + } + + #[getter] + fn data(&self) -> &str { + &self.backend.data + } + + fn to_bytes(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.to_bytes()).into() + } + + #[staticmethod] + fn from_bytes(data: &[u8]) -> PyResult { + nucypher_core::PackedUserOperation::from_bytes(data) + .map(|backend| Self { backend }) + .map_err(|e| PyValueError::new_err(e)) + } +} + +// +// PackedUserOperationSignatureRequest +// + +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct PackedUserOperationSignatureRequest { + backend: nucypher_core::PackedUserOperationSignatureRequest, +} + +#[pymethods] +impl PackedUserOperationSignatureRequest { + #[new] + pub fn new( + packed_user_op: &PackedUserOperation, + cohort_id: u32, + chain_id: u32, + aa_version: &str, + context: Option<&Context>, + ) -> PyResult { + let aa_version = str_to_aa_version(aa_version)?; + Ok(Self { + backend: nucypher_core::PackedUserOperationSignatureRequest::new( + packed_user_op.backend.clone(), + cohort_id, + chain_id, + aa_version, + context.map(|c| c.backend.clone()), + ), + }) + } + + #[getter] + fn packed_user_op(&self) -> PackedUserOperation { + PackedUserOperation { + backend: self.backend.packed_user_op.clone(), + } + } + + #[getter] + fn cohort_id(&self) -> u32 { + self.backend.cohort_id + } + + #[getter] + fn chain_id(&self) -> u32 { + self.backend.chain_id + } + + #[getter] + fn aa_version(&self) -> &'static str { + aa_version_to_str(&self.backend.aa_version) + } + + #[getter] + fn context(&self) -> Option { + self.backend.context.clone().map(|context| Context { backend: context }) + } + + #[getter] + fn signature_type(&self) -> u8 { + signature_request_type_to_u8(&self.backend.signature_type) + } + + #[staticmethod] + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, nucypher_core::PackedUserOperationSignatureRequest>(data) + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) + } +} + +// +// SignatureResponse +// + +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct SignatureResponse { + backend: nucypher_core::SignatureResponse, +} + +#[pymethods] +impl SignatureResponse { + #[new] + pub fn new(hash: &[u8], signature: &[u8], signature_type: u8) -> PyResult { + let signature_type = u8_to_signature_request_type(signature_type)?; + Ok(Self { + backend: nucypher_core::SignatureResponse::new(hash, signature, signature_type), + }) + } + + #[getter] + fn hash(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.hash).into() + } + + #[getter] + fn signature(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.signature).into() + } + + #[getter] + fn signature_type(&self) -> u8 { + signature_request_type_to_u8(&self.backend.signature_type) + } + + #[staticmethod] + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, nucypher_core::SignatureResponse>(data) + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) + } +} + /// A Python module implemented in Rust. #[pymodule] fn _nucypher_core(py: Python, core_module: &PyModule) -> PyResult<()> { @@ -1587,6 +1957,14 @@ fn _nucypher_core(py: Python, core_module: &PyModule) -> PyResult<()> { core_module.add_class::()?; core_module.add_function(wrap_pyfunction!(encrypt_for_dkg, core_module)?)?; + // Add signature request/response classes + core_module.add_class::()?; + core_module.add_class::()?; + core_module.add_class::()?; + core_module.add_class::()?; + core_module.add_class::()?; + core_module.add_class::()?; + // Build the umbral module let umbral_module = PyModule::new(py, "umbral")?; diff --git a/nucypher-core/src/lib.rs b/nucypher-core/src/lib.rs index 8d7870c1..6f6c5e30 100644 --- a/nucypher-core/src/lib.rs +++ b/nucypher-core/src/lib.rs @@ -22,6 +22,7 @@ mod revocation_order; mod secret_box; mod test_utils; mod threshold_message_kit; +mod signature_request; mod treasure_map; mod versioning; @@ -48,6 +49,11 @@ pub use reencryption::{ReencryptionRequest, ReencryptionResponse}; pub use retrieval_kit::RetrievalKit; pub use revocation_order::RevocationOrder; pub use threshold_message_kit::ThresholdMessageKit; +pub use signature_request::{ + AAVersion, BaseSignatureRequest, EIP191SignatureRequest, PackedUserOperation, + PackedUserOperationSignatureRequest, SignatureRequestType, SignatureResponse, UserOperation, + UserOperationSignatureRequest, +}; pub use treasure_map::{EncryptedTreasureMap, TreasureMap}; pub use versioning::ProtocolObject; diff --git a/nucypher-core/src/signature_request.rs b/nucypher-core/src/signature_request.rs new file mode 100644 index 00000000..d2025e13 --- /dev/null +++ b/nucypher-core/src/signature_request.rs @@ -0,0 +1,492 @@ +use alloc::boxed::Box; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::fmt; + +use serde::{Deserialize, Serialize}; +use umbral_pre::serde_bytes; + +use crate::conditions::Context; +use crate::versioning::{ + messagepack_deserialize, messagepack_serialize, ProtocolObject, ProtocolObjectInner, +}; + +/// Enum for different signature types. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum SignatureRequestType { + /// UserOperation signature request + UserOp, + /// Packed UserOperation signature request + PackedUserOp, + /// EIP-191 signature request + #[serde(rename = "eip-191")] + EIP191, + /// EIP-712 signature request + #[serde(rename = "eip-712")] + EIP712, +} + +impl fmt::Display for SignatureRequestType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UserOp => write!(f, "userop"), + Self::PackedUserOp => write!(f, "packedUserOp"), + Self::EIP191 => write!(f, "eip-191"), + Self::EIP712 => write!(f, "eip-712"), + } + } +} + +/// AA version enum for Account Abstraction versions +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum AAVersion { + /// Version 0.8.0 + #[serde(rename = "0.8.0")] + V08, + /// MDT version + #[serde(rename = "mdt")] + MDT, +} + +impl fmt::Display for AAVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::V08 => write!(f, "0.8.0"), + Self::MDT => write!(f, "mdt"), + } + } +} + +/// Base trait for signature requests +pub trait BaseSignatureRequest: Serialize + for<'de> Deserialize<'de> { + /// Returns the cohort ID for this signature request + fn cohort_id(&self) -> u32; + /// Returns the chain ID for this signature request + fn chain_id(&self) -> u32; + /// Returns the signature type for this signature request + fn signature_type(&self) -> SignatureRequestType; + /// Returns the optional context for this signature request + fn context(&self) -> Option<&Context>; +} + +/// EIP-191 signature request +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EIP191SignatureRequest { + /// Data to be signed + #[serde(with = "serde_bytes::as_base64")] + pub data: Box<[u8]>, + /// Cohort ID + pub cohort_id: u32, + /// Chain ID + pub chain_id: u32, + /// Optional context + pub context: Option, + /// Signature type (always EIP-191) + pub signature_type: SignatureRequestType, +} + +impl EIP191SignatureRequest { + /// Creates a new EIP-191 signature request + pub fn new(data: &[u8], cohort_id: u32, chain_id: u32, context: Option) -> Self { + Self { + data: data.to_vec().into_boxed_slice(), + cohort_id, + chain_id, + context, + signature_type: SignatureRequestType::EIP191, + } + } +} + +impl BaseSignatureRequest for EIP191SignatureRequest { + fn cohort_id(&self) -> u32 { + self.cohort_id + } + + fn chain_id(&self) -> u32 { + self.chain_id + } + + fn signature_type(&self) -> SignatureRequestType { + self.signature_type + } + + fn context(&self) -> Option<&Context> { + self.context.as_ref() + } +} + +/// UserOperation for signature requests +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserOperation { + /// The serialized user operation data + pub data: String, +} + +impl UserOperation { + /// Creates a new UserOperation + pub fn new(data: String) -> Self { + Self { data } + } + + /// Serializes to bytes + pub fn to_bytes(&self) -> Vec { + self.data.as_bytes().to_vec() + } + + /// Deserializes from bytes + pub fn from_bytes(bytes: &[u8]) -> Result { + String::from_utf8(bytes.to_vec()) + .map(|data| Self { data }) + .map_err(|e| e.to_string()) + } +} + +/// UserOperation signature request +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserOperationSignatureRequest { + /// User operation to sign + pub user_op: UserOperation, + /// Cohort ID + pub cohort_id: u32, + /// Chain ID + pub chain_id: u32, + /// AA version + pub aa_version: AAVersion, + /// Optional context + pub context: Option, + /// Signature type (always UserOp) + pub signature_type: SignatureRequestType, +} + +impl UserOperationSignatureRequest { + /// Creates a new UserOperation signature request + pub fn new( + user_op: UserOperation, + cohort_id: u32, + chain_id: u32, + aa_version: AAVersion, + context: Option, + ) -> Self { + Self { + user_op, + cohort_id, + chain_id, + aa_version, + context, + signature_type: SignatureRequestType::UserOp, + } + } +} + +impl BaseSignatureRequest for UserOperationSignatureRequest { + fn cohort_id(&self) -> u32 { + self.cohort_id + } + + fn chain_id(&self) -> u32 { + self.chain_id + } + + fn signature_type(&self) -> SignatureRequestType { + self.signature_type + } + + fn context(&self) -> Option<&Context> { + self.context.as_ref() + } +} + +/// Packed UserOperation for signature requests +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PackedUserOperation { + /// The serialized packed user operation data + pub data: String, +} + +impl PackedUserOperation { + /// Creates a new PackedUserOperation + pub fn new(data: String) -> Self { + Self { data } + } + + /// Serializes to bytes + pub fn to_bytes(&self) -> Vec { + self.data.as_bytes().to_vec() + } + + /// Deserializes from bytes + pub fn from_bytes(bytes: &[u8]) -> Result { + String::from_utf8(bytes.to_vec()) + .map(|data| Self { data }) + .map_err(|e| e.to_string()) + } +} + +/// Packed UserOperation signature request +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PackedUserOperationSignatureRequest { + /// Packed user operation to sign + pub packed_user_op: PackedUserOperation, + /// Cohort ID + pub cohort_id: u32, + /// Chain ID + pub chain_id: u32, + /// AA version + pub aa_version: AAVersion, + /// Optional context + pub context: Option, + /// Signature type (always PackedUserOp) + pub signature_type: SignatureRequestType, +} + +impl PackedUserOperationSignatureRequest { + /// Creates a new PackedUserOperation signature request + pub fn new( + packed_user_op: PackedUserOperation, + cohort_id: u32, + chain_id: u32, + aa_version: AAVersion, + context: Option, + ) -> Self { + Self { + packed_user_op, + cohort_id, + chain_id, + aa_version, + context, + signature_type: SignatureRequestType::PackedUserOp, + } + } +} + +impl BaseSignatureRequest for PackedUserOperationSignatureRequest { + fn cohort_id(&self) -> u32 { + self.cohort_id + } + + fn chain_id(&self) -> u32 { + self.chain_id + } + + fn signature_type(&self) -> SignatureRequestType { + self.signature_type + } + + fn context(&self) -> Option<&Context> { + self.context.as_ref() + } +} + +/// Signature response +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SignatureResponse { + /// Message hash + #[serde(rename = "message_hash", with = "serde_bytes::as_base64")] + pub hash: Box<[u8]>, + /// Signature + #[serde(with = "serde_bytes::as_base64")] + pub signature: Box<[u8]>, + /// Signature type + pub signature_type: SignatureRequestType, +} + +impl SignatureResponse { + /// Creates a new signature response + pub fn new(hash: &[u8], signature: &[u8], signature_type: SignatureRequestType) -> Self { + Self { + hash: hash.to_vec().into_boxed_slice(), + signature: signature.to_vec().into_boxed_slice(), + signature_type, + } + } +} + +// ProtocolObject implementations + +impl<'a> ProtocolObjectInner<'a> for EIP191SignatureRequest { + fn brand() -> [u8; 4] { + *b"E191" + } + + fn version() -> (u16, u16) { + (1, 0) + } + + fn unversioned_to_bytes(&self) -> Box<[u8]> { + messagepack_serialize(&self) + } + + fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option> { + if minor_version == 0 { + Some(messagepack_deserialize(bytes)) + } else { + None + } + } +} + +impl<'a> ProtocolObject<'a> for EIP191SignatureRequest {} + +impl<'a> ProtocolObjectInner<'a> for UserOperationSignatureRequest { + fn brand() -> [u8; 4] { + *b"UOSR" + } + + fn version() -> (u16, u16) { + (1, 0) + } + + fn unversioned_to_bytes(&self) -> Box<[u8]> { + messagepack_serialize(&self) + } + + fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option> { + if minor_version == 0 { + Some(messagepack_deserialize(bytes)) + } else { + None + } + } +} + +impl<'a> ProtocolObject<'a> for UserOperationSignatureRequest {} + +impl<'a> ProtocolObjectInner<'a> for PackedUserOperationSignatureRequest { + fn brand() -> [u8; 4] { + *b"PUOS" + } + + fn version() -> (u16, u16) { + (1, 0) + } + + fn unversioned_to_bytes(&self) -> Box<[u8]> { + messagepack_serialize(&self) + } + + fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option> { + if minor_version == 0 { + Some(messagepack_deserialize(bytes)) + } else { + None + } + } +} + +impl<'a> ProtocolObject<'a> for PackedUserOperationSignatureRequest {} + +impl<'a> ProtocolObjectInner<'a> for SignatureResponse { + fn brand() -> [u8; 4] { + *b"SigR" + } + + fn version() -> (u16, u16) { + (1, 0) + } + + fn unversioned_to_bytes(&self) -> Box<[u8]> { + messagepack_serialize(&self) + } + + fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option> { + if minor_version == 0 { + Some(messagepack_deserialize(bytes)) + } else { + None + } + } +} + +impl<'a> ProtocolObject<'a> for SignatureResponse {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_eip191_signature_request_serialization() { + let data = b"test data"; + let request = EIP191SignatureRequest::new(data, 1, 137, None); + + let bytes = request.to_bytes(); + let deserialized = EIP191SignatureRequest::from_bytes(&bytes).unwrap(); + + assert_eq!(request, deserialized); + assert_eq!(deserialized.data.as_ref(), data); + assert_eq!(deserialized.cohort_id, 1); + assert_eq!(deserialized.chain_id, 137); + assert_eq!(deserialized.signature_type, SignatureRequestType::EIP191); + } + + #[test] + fn test_user_operation_signature_request_serialization() { + let user_op = UserOperation::new("test_user_op_data".to_string()); + let request = UserOperationSignatureRequest::new( + user_op.clone(), + 1, + 137, + AAVersion::V08, + Some(Context::new("test_context")), + ); + + let bytes = request.to_bytes(); + let deserialized = UserOperationSignatureRequest::from_bytes(&bytes).unwrap(); + + assert_eq!(request, deserialized); + assert_eq!(deserialized.user_op.data, "test_user_op_data"); + assert_eq!(deserialized.aa_version, AAVersion::V08); + assert_eq!(deserialized.context.as_ref().unwrap().as_ref(), "test_context"); + } + + #[test] + fn test_signature_response_serialization() { + let hash = b"test_hash"; + let signature = b"test_signature"; + let response = SignatureResponse::new(hash, signature, SignatureRequestType::UserOp); + + let bytes = response.to_bytes(); + let deserialized = SignatureResponse::from_bytes(&bytes).unwrap(); + + assert_eq!(response, deserialized); + assert_eq!(deserialized.hash.as_ref(), hash); + assert_eq!(deserialized.signature.as_ref(), signature); + assert_eq!(deserialized.signature_type, SignatureRequestType::UserOp); + } + + #[test] + fn test_aa_version_serialization() { + // Test V08 + let user_op = UserOperation::new("test_v08".to_string()); + let request_v08 = UserOperationSignatureRequest::new( + user_op, + 1, + 137, + AAVersion::V08, + None, + ); + + let bytes = request_v08.to_bytes(); + let deserialized_v08 = UserOperationSignatureRequest::from_bytes(&bytes).unwrap(); + assert_eq!(deserialized_v08.aa_version, AAVersion::V08); + + // Test MDT + let user_op_mdt = UserOperation::new("test_mdt".to_string()); + let request_mdt = UserOperationSignatureRequest::new( + user_op_mdt, + 2, + 137, + AAVersion::MDT, + None, + ); + + let bytes_mdt = request_mdt.to_bytes(); + let deserialized_mdt = UserOperationSignatureRequest::from_bytes(&bytes_mdt).unwrap(); + assert_eq!(deserialized_mdt.aa_version, AAVersion::MDT); + + // Test Display trait + assert_eq!(AAVersion::V08.to_string(), "0.8.0"); + assert_eq!(AAVersion::MDT.to_string(), "mdt"); + } +} \ No newline at end of file From c5c1cc7e3123a090d6b8ee901b511e47412090ba Mon Sep 17 00:00:00 2001 From: KPrasch Date: Wed, 4 Jun 2025 10:52:56 +0200 Subject: [PATCH 2/4] signature request interfces --- Cargo.lock | 198 +++-- nucypher-core-python/Cargo.toml | 2 + .../nucypher_core/__init__.py | 18 + .../nucypher_core/__init__.pyi | 307 ++++++- nucypher-core-python/src/lib.rs | 587 +++++++++++-- nucypher-core/Cargo.toml | 1 + nucypher-core/src/address.rs | 85 +- nucypher-core/src/lib.rs | 7 +- nucypher-core/src/signature_request.rs | 795 ++++++++++++++++-- test_gas_limits_corrected.py | 1 + test_signed_packed_user_operation.py | 64 ++ 11 files changed, 1836 insertions(+), 229 deletions(-) create mode 100644 test_gas_limits_corrected.py create mode 100644 test_signed_packed_user_operation.py diff --git a/Cargo.lock b/Cargo.lock index 89d6f85c..9c627aa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,12 +24,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -198,9 +192,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "block-buffer" @@ -225,18 +219,19 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.32" +version = "1.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chacha20" @@ -264,15 +259,14 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -364,7 +358,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -412,7 +406,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -434,7 +428,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -449,9 +443,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", "serde", @@ -476,7 +470,7 @@ checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -624,6 +618,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + [[package]] name = "fnv" version = "1.0.7" @@ -785,9 +785,9 @@ dependencies = [ [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" dependencies = [ "rustversion", ] @@ -809,9 +809,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" dependencies = [ "once_cell", "wasm-bindgen", @@ -856,9 +856,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "measure_time" @@ -916,6 +916,7 @@ dependencies = [ "rand_core 0.6.4", "rmp-serde", "serde", + "serde_json", "serde_with 1.14.0", "sha2", "sha3", @@ -930,9 +931,11 @@ version = "0.15.0" dependencies = [ "derive_more", "ferveo-nucypher", + "hex", "nucypher-core", "pyo3", - "pyo3-build-config", + "pyo3-build-config 0.26.0", + "serde_json", "umbral-pre", ] @@ -1057,9 +1060,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.97" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -1076,7 +1079,7 @@ dependencies = [ "libc", "memoffset", "parking_lot", - "pyo3-build-config", + "pyo3-build-config 0.18.3", "pyo3-ffi", "pyo3-macros", "unindent", @@ -1089,7 +1092,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cb946f5ac61bb61a5014924910d936ebd2b23b705f7a4a3c40b05c720b079a3" dependencies = [ "once_cell", - "target-lexicon", + "target-lexicon 0.12.16", +] + +[[package]] +name = "pyo3-build-config" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f" +dependencies = [ + "target-lexicon 0.13.3", ] [[package]] @@ -1099,7 +1111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd4d7c5337821916ea2a1d21d1092e8443cf34879e53a0ac653fbb98f44ff65c" dependencies = [ "libc", - "pyo3-build-config", + "pyo3-build-config 0.18.3", ] [[package]] @@ -1327,14 +1339,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -1389,7 +1401,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1479,9 +1491,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.105" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1494,6 +1506,12 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + [[package]] name = "thiserror" version = "1.0.69" @@ -1511,17 +1529,16 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde", @@ -1531,15 +1548,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -1579,9 +1596,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unindent" @@ -1629,27 +1646,28 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -1677,9 +1695,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" dependencies = [ "cfg-if", "js-sys", @@ -1690,9 +1708,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1700,31 +1718,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.50" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3" +checksum = "80cc7f8a4114fdaa0c58383caf973fc126cf004eba25c9dc639bccd3880d55ad" dependencies = [ "js-sys", "minicov", @@ -1735,20 +1753,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.50" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" +checksum = "c5ada2ab788d46d4bda04c9d567702a79c8ced14f51f221646a16ed39d0e6a5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" dependencies = [ "js-sys", "wasm-bindgen", @@ -1756,9 +1774,9 @@ dependencies = [ [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] @@ -1771,7 +1789,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -1784,7 +1802,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1795,7 +1813,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1804,13 +1822,19 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -1819,16 +1843,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" dependencies = [ - "windows-targets", + "windows-link 0.2.0", ] [[package]] @@ -1909,22 +1933,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] [[package]] @@ -1944,5 +1968,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.105", + "syn 2.0.106", ] diff --git a/nucypher-core-python/Cargo.toml b/nucypher-core-python/Cargo.toml index a73acc5a..7ec013bc 100644 --- a/nucypher-core-python/Cargo.toml +++ b/nucypher-core-python/Cargo.toml @@ -14,6 +14,8 @@ nucypher-core = { path = "../nucypher-core" } umbral-pre = { version = "0.11.0", features = ["bindings-python"] } ferveo = { package = "ferveo-nucypher", version = "0.4.0", features = ["bindings-python"] } derive_more = { version = "0.99", default-features = false, features = ["from", "as_ref"] } +hex = "0.4" +serde_json = "1.0" [build-dependencies] pyo3-build-config = "*" diff --git a/nucypher-core-python/nucypher_core/__init__.py b/nucypher-core-python/nucypher_core/__init__.py index 4f657a6d..594da25c 100644 --- a/nucypher-core-python/nucypher_core/__init__.py +++ b/nucypher-core-python/nucypher_core/__init__.py @@ -30,9 +30,27 @@ SessionSecretFactory, encrypt_for_dkg, EIP191SignatureRequest, + SignedEIP191SignatureRequest, UserOperation, UserOperationSignatureRequest, PackedUserOperation, + SignedPackedUserOperation, PackedUserOperationSignatureRequest, SignatureResponse, + deserialize_signature_request, ) + +# Constants for signature request types +class SignatureRequestType: + """Constants for signature request types.""" + USEROP = 0 + PACKED_USER_OP = 1 + EIP_191 = 2 + EIP_712 = 3 + + +# Constants for AA versions +class AAVersion: + """Constants for AA (Account Abstraction) versions.""" + V08 = "0.8.0" + MDT = "mdt" diff --git a/nucypher-core-python/nucypher_core/__init__.pyi b/nucypher-core-python/nucypher_core/__init__.pyi index 831e774e..551a1392 100644 --- a/nucypher-core-python/nucypher_core/__init__.pyi +++ b/nucypher-core-python/nucypher_core/__init__.pyi @@ -1,4 +1,4 @@ -from typing import Dict, List, Mapping, Optional, Sequence, Set, Tuple, final +from typing import Dict, List, Mapping, Optional, Sequence, Set, Tuple, Union, final, Any from .ferveo import ( Ciphertext, @@ -714,20 +714,135 @@ class EIP191SignatureRequest: ... -class UserOperation: - - def __init__(self, data: str): +class SignedEIP191SignatureRequest: + """Signed EIP-191 signature request - combines an EIP191SignatureRequest with a signature.""" + + def __init__( + self, + request: EIP191SignatureRequest, + signature: bytes, + ) -> None: + """Create a new SignedEIP191SignatureRequest.""" ... - + @property - def data(self) -> str: + def request(self) -> EIP191SignatureRequest: + """The EIP-191 signature request without signature.""" ... - - def to_bytes(self) -> bytes: + + @property + def signature(self) -> bytes: + """The signature over the request.""" + ... + + def into_parts(self) -> Tuple[EIP191SignatureRequest, bytes]: + """Returns the request and signature as separate components.""" + ... + + def __bytes__(self) -> bytes: + """Serialize to bytes.""" + ... + + @staticmethod + def from_bytes(data: bytes) -> 'SignedEIP191SignatureRequest': + """Deserialize from bytes.""" ... + +class UserOperation: + """UserOperation for signature requests.""" + + def __init__( + self, + sender: str, + nonce: int, + init_code: bytes = b"", + call_data: bytes = b"", + call_gas_limit: int = 0, + verification_gas_limit: int = 0, + pre_verification_gas: int = 0, + max_fee_per_gas: int = 0, + max_priority_fee_per_gas: int = 0, + paymaster: Optional[str] = None, + paymaster_verification_gas_limit: int = 0, + paymaster_post_op_gas_limit: int = 0, + paymaster_data: bytes = b"", + ) -> None: + """Create a new UserOperation with u128 gas limits.""" + ... + + @property + def sender(self) -> str: + """Address of the sender (smart contract account).""" + ... + + @property + def nonce(self) -> int: + """Nonce for replay protection.""" + ... + + @property + def init_code(self) -> bytes: + """Factory and data for account creation.""" + ... + + @property + def call_data(self) -> bytes: + """The calldata to execute.""" + ... + + @property + def call_gas_limit(self) -> int: + """Gas limit for the call (u128 value).""" + ... + + @property + def verification_gas_limit(self) -> int: + """Gas limit for verification (u128 value).""" + ... + + @property + def pre_verification_gas(self) -> int: + """Gas to cover overhead (u128 value).""" + ... + + @property + def max_fee_per_gas(self) -> int: + """Maximum fee per gas unit (u128 value).""" + ... + + @property + def max_priority_fee_per_gas(self) -> int: + """Maximum priority fee per gas unit (u128 value).""" + ... + + @property + def paymaster(self) -> Optional[str]: + """Paymaster address (optional).""" + ... + + @property + def paymaster_verification_gas_limit(self) -> int: + """Gas limit for paymaster verification (u128 value).""" + ... + + @property + def paymaster_post_op_gas_limit(self) -> int: + """Gas limit for paymaster post-operation (u128 value).""" + ... + + @property + def paymaster_data(self) -> bytes: + """Paymaster-specific data.""" + ... + + def __bytes__(self) -> bytes: + """Serialize to bytes.""" + ... + @staticmethod - def from_bytes(data: bytes) -> UserOperation: + def from_bytes(data: bytes) -> 'UserOperation': + """Deserialize from bytes.""" ... @@ -776,19 +891,153 @@ class UserOperationSignatureRequest: class PackedUserOperation: - - def __init__(self, data: str): + """Packed UserOperation for optimized format with u128 gas limits.""" + + def __init__( + self, + sender: str, + nonce: int, + init_code: bytes, + call_data: bytes, + account_gas_limits: bytes, + pre_verification_gas: int, + gas_fees: bytes, + paymaster_and_data: bytes, + ) -> None: + """Create a new PackedUserOperation with u128 pre_verification_gas.""" + ... + + @staticmethod + def from_user_operation(user_op: UserOperation) -> 'PackedUserOperation': + """Create a PackedUserOperation from a UserOperation.""" ... - + + @staticmethod + def _pack_account_gas_limits(call_gas_limit: int, verification_gas_limit: int) -> bytes: + """Pack account gas limits into a 32-byte value (u128 values).""" + ... + + @staticmethod + def _pack_gas_fees(max_fee_per_gas: int, max_priority_fee_per_gas: int) -> bytes: + """Pack gas fees into a 32-byte value (u128 values).""" + ... + + @staticmethod + def _pack_paymaster_and_data( + paymaster: Optional[str], + paymaster_verification_gas_limit: int, + paymaster_post_op_gas_limit: int, + paymaster_data: bytes, + ) -> bytes: + """Pack paymaster and data with u128 gas limits.""" + ... + + def to_eip712_struct(self, aa_version: str, chain_id: int) -> Dict[str, Any]: + """Convert to EIP-712 struct format.""" + ... + + def _to_eip712_message(self, aa_version: str) -> Dict[str, Any]: + """Convert to EIP-712 message format.""" + ... + + def _get_domain(self, aa_version: str, chain_id: int) -> Dict[str, Any]: + """Get the EIP-712 domain.""" + ... + @property - def data(self) -> str: + def sender(self) -> str: + """Address of the sender (smart contract account).""" ... - - def to_bytes(self) -> bytes: + + @property + def nonce(self) -> int: + """Nonce for replay protection.""" + ... + + @property + def init_code(self) -> bytes: + """Factory and data for account creation.""" ... + + @property + def call_data(self) -> bytes: + """The calldata to execute.""" + ... + + @property + def account_gas_limits(self) -> bytes: + """Packed gas limits (verification gas limit << 128 | call gas limit).""" + ... + + @property + def pre_verification_gas(self) -> int: + """Gas to cover overhead (u128 value).""" + ... + + @property + def gas_fees(self) -> bytes: + """Packed gas fees (max priority fee << 128 | max fee).""" + ... + + @property + def paymaster_and_data(self) -> bytes: + """Packed paymaster data.""" + ... + + def __bytes__(self) -> bytes: + """Serialize to bytes.""" + ... + + @staticmethod + def from_bytes(data: bytes) -> 'PackedUserOperation': + """Deserialize from bytes.""" + ... + +class SignedPackedUserOperation: + """Signed Packed UserOperation - combines a PackedUserOperation with a signature.""" + + def __init__( + self, + operation: PackedUserOperation, + signature: bytes, + ) -> None: + """Create a new SignedPackedUserOperation.""" + ... + + @property + def operation(self) -> PackedUserOperation: + """The packed user operation without signature.""" + ... + + @property + def signature(self) -> bytes: + """The signature over the operation.""" + ... + + def into_parts(self) -> Tuple[PackedUserOperation, bytes]: + """Returns the operation and signature as separate components.""" + ... + + def to_eip712_struct(self, aa_version: str, chain_id: int) -> Dict[str, Any]: + """Convert to EIP-712 struct format.""" + ... + + def _to_eip712_message(self, aa_version: str) -> Dict[str, Any]: + """Convert to EIP-712 message format.""" + ... + + def _get_domain(self, aa_version: str, chain_id: int) -> Dict[str, Any]: + """Get the EIP-712 domain.""" + ... + + def __bytes__(self) -> bytes: + """Serialize to bytes.""" + ... + @staticmethod - def from_bytes(data: bytes) -> PackedUserOperation: + def from_bytes(data: bytes) -> 'SignedPackedUserOperation': + """Deserialize from bytes.""" ... @@ -837,25 +1086,37 @@ class PackedUserOperationSignatureRequest: class SignatureResponse: - - def __init__(self, hash: bytes, signature: bytes, signature_type: int): + """Response object containing signature hash, signature bytes, and type.""" + + def __init__(self, hash: bytes, signature: bytes, signature_type: int) -> None: + """Create a new SignatureResponse.""" ... - + @property def hash(self) -> bytes: + """Get the hash that was signed.""" ... - + @property def signature(self) -> bytes: + """Get the signature bytes.""" ... - + @property def signature_type(self) -> int: + """Get the signature type as integer.""" ... - + @staticmethod def from_bytes(data: bytes) -> SignatureResponse: + """Deserialize from bytes.""" ... - + def __bytes__(self) -> bytes: + """Serialize to bytes.""" ... + + +def deserialize_signature_request(data: bytes) -> Union[EIP191SignatureRequest, UserOperationSignatureRequest, PackedUserOperationSignatureRequest]: + """Utility function to deserialize any signature request from bytes and return the specific type directly.""" + ... diff --git a/nucypher-core-python/src/lib.rs b/nucypher-core-python/src/lib.rs index f4b1f09b..f53c306f 100644 --- a/nucypher-core-python/src/lib.rs +++ b/nucypher-core-python/src/lib.rs @@ -10,6 +10,7 @@ use ferveo::bindings_python::{ Ciphertext, CiphertextHeader, DkgPublicKey, FerveoPublicKey, FerveoPythonError, FerveoVariant, SharedSecret, }; +use hex; use pyo3::class::basic::CompareOp; use pyo3::exceptions::{PyTypeError, PyValueError}; use pyo3::prelude::*; @@ -20,6 +21,13 @@ use umbral_pre::bindings_python::{ VerifiedCapsuleFrag, VerifiedKeyFrag, }; +use nucypher_core as rust_nucypher_core; +use rust_nucypher_core::{ + UserOperation as SignatureRequestUserOperation, + PackedUserOperation as SignatureRequestPackedUserOperation, + SignedPackedUserOperation as SignatureRequestSignedPackedUserOperation, +}; + use nucypher_core::ProtocolObject; fn to_bytes<'a, T, U>(obj: &T) -> PyObject @@ -71,6 +79,27 @@ where builtins.getattr("hash")?.call1(((arg1, arg2),))?.extract() }) } + +// Helper functions for Address conversion +fn address_to_string(addr: &rust_nucypher_core::Address) -> String { + addr.to_checksum_address() +} + +fn string_to_address(hex_str: &str) -> PyResult { + let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str); + let bytes = hex::decode(hex_str) + .map_err(|e| PyValueError::new_err(format!("Invalid hex address: {}", e)))?; + if bytes.len() != 20 { + return Err(PyValueError::new_err(format!( + "Invalid address length: expected 20 bytes, got {}", + bytes.len() + ))); + } + let mut array = [0u8; 20]; + array.copy_from_slice(&bytes); + Ok(rust_nucypher_core::Address::new(&array)) +} + #[pyclass(module = "nucypher_core")] #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, derive_more::AsRef)] pub struct Address { @@ -757,12 +786,12 @@ impl AuthenticatedData { } } - pub fn aad(&self, py: Python) -> PyResult { + pub fn aad(&self) -> PyResult> { let result = self .backend .aad() .map_err(|err| PyValueError::new_err(format!("{err}")))?; - Ok(PyBytes::new(py, &result).into()) + Ok(result.to_vec()) } #[getter] @@ -815,31 +844,34 @@ impl AccessControlPolicy { #[new] pub fn new(auth_data: &AuthenticatedData, authorization: &[u8]) -> Self { Self { - backend: nucypher_core::AccessControlPolicy::new(auth_data.as_ref(), authorization), + backend: nucypher_core::AccessControlPolicy::new(&auth_data.backend, authorization), } } - pub fn aad(&self, py: Python) -> PyResult { - let result = self - .backend + pub fn aad(&self) -> PyResult> { + self.backend .aad() - .map_err(|err| PyValueError::new_err(format!("{err}")))?; - Ok(PyBytes::new(py, &result).into()) + .map(|aad| aad.to_vec()) + .map_err(|err| { + PyValueError::new_err(format!("Failed to get authenticated data: {err}")) + }) } #[getter] pub fn public_key(&self) -> DkgPublicKey { - self.backend.auth_data.public_key.into() + self.backend.public_key().into() } #[getter] pub fn conditions(&self) -> Conditions { - self.backend.auth_data.conditions.clone().into() + Conditions { + backend: self.backend.conditions(), + } } #[getter] - pub fn authorization(&self) -> &[u8] { - self.backend.authorization.as_ref() + pub fn authorization(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.authorization).into() } #[staticmethod] @@ -866,28 +898,32 @@ impl ThresholdMessageKit { #[new] pub fn new(ciphertext: &Ciphertext, acp: &AccessControlPolicy) -> Self { Self { - backend: nucypher_core::ThresholdMessageKit::new(ciphertext.as_ref(), acp.as_ref()), + backend: nucypher_core::ThresholdMessageKit::new( + ciphertext.as_ref(), + &acp.backend + ), } } #[getter] pub fn ciphertext_header(&self) -> PyResult { - let header = self - .backend + self.backend .ciphertext_header() - .map_err(FerveoPythonError::from)?; - Ok(CiphertextHeader::from(header)) + .map(|header| header.into()) + .map_err(|err| PyValueError::new_err(format!("Failed to get ciphertext header: {}", err))) } #[getter] pub fn acp(&self) -> AccessControlPolicy { - self.backend.acp.clone().into() + AccessControlPolicy { + backend: self.backend.acp.clone(), + } } pub fn decrypt_with_shared_secret(&self, shared_secret: &SharedSecret) -> PyResult> { self.backend .decrypt_with_shared_secret(shared_secret.as_ref()) - .map_err(|err| FerveoPythonError::FerveoError(err).into()) + .map_err(|err| PyValueError::new_err(format!("Failed to decrypt: {}", err))) } #[staticmethod] @@ -925,7 +961,7 @@ impl ThresholdDecryptionRequest { ritual_id, ciphertext_header.as_ref(), acp.as_ref(), - context.map(|context| context.backend.clone()).as_ref(), + context.map(|context| &context.backend), variant.into(), ), }) @@ -1614,7 +1650,7 @@ pub struct EIP191SignatureRequest { #[pymethods] impl EIP191SignatureRequest { #[new] - pub fn new(data: &[u8], cohort_id: u32, chain_id: u32, context: Option<&Context>) -> Self { + pub fn new(data: &[u8], cohort_id: u32, chain_id: u64, context: Option<&Context>) -> Self { Self { backend: nucypher_core::EIP191SignatureRequest::new( data, @@ -1636,7 +1672,7 @@ impl EIP191SignatureRequest { } #[getter] - fn chain_id(&self) -> u32 { + fn chain_id(&self) -> u64 { self.backend.chain_id } @@ -1664,34 +1700,127 @@ impl EIP191SignatureRequest { // UserOperation // +/// Python bindings for UserOperation #[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] pub struct UserOperation { - backend: nucypher_core::UserOperation, + backend: SignatureRequestUserOperation, } #[pymethods] impl UserOperation { #[new] - pub fn new(data: String) -> Self { - Self { - backend: nucypher_core::UserOperation::new(data), - } + #[pyo3(signature = (sender, nonce, init_code=None, call_data=None, call_gas_limit=None, verification_gas_limit=None, pre_verification_gas=None, max_fee_per_gas=None, max_priority_fee_per_gas=None, paymaster=None, paymaster_verification_gas_limit=None, paymaster_post_op_gas_limit=None, paymaster_data=None))] + pub fn new( + sender: String, + nonce: u64, + init_code: Option<&[u8]>, + call_data: Option<&[u8]>, + call_gas_limit: Option, + verification_gas_limit: Option, + pre_verification_gas: Option, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + paymaster: Option, + paymaster_verification_gas_limit: Option, + paymaster_post_op_gas_limit: Option, + paymaster_data: Option<&[u8]>, + ) -> PyResult { + // Convert hex string to Address + let sender_address = string_to_address(&sender)?; + let paymaster_address = paymaster.as_ref().map(|p| string_to_address(p)).transpose()?; + + Ok(Self { + backend: SignatureRequestUserOperation::new( + sender_address, + nonce, + init_code, + call_data, + call_gas_limit, + verification_gas_limit, + pre_verification_gas, + max_fee_per_gas, + max_priority_fee_per_gas, + paymaster_address, + paymaster_verification_gas_limit, + paymaster_post_op_gas_limit, + paymaster_data, + ), + }) + } + + #[getter] + pub fn sender(&self) -> String { + address_to_string(&self.backend.sender) + } + + #[getter] + pub fn nonce(&self) -> u64 { + self.backend.nonce + } + + #[getter] + pub fn init_code(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.init_code).into() + } + + #[getter] + pub fn call_data(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.call_data).into() + } + + #[getter] + pub fn call_gas_limit(&self) -> u128 { + self.backend.call_gas_limit + } + + #[getter] + pub fn verification_gas_limit(&self) -> u128 { + self.backend.verification_gas_limit + } + + #[getter] + pub fn pre_verification_gas(&self) -> u128 { + self.backend.pre_verification_gas + } + + #[getter] + pub fn max_fee_per_gas(&self) -> u128 { + self.backend.max_fee_per_gas + } + + #[getter] + pub fn max_priority_fee_per_gas(&self) -> u128 { + self.backend.max_priority_fee_per_gas + } + + #[getter] + pub fn paymaster(&self) -> Option { + self.backend.paymaster.as_ref().map(address_to_string) + } + + #[getter] + pub fn paymaster_verification_gas_limit(&self) -> u128 { + self.backend.paymaster_verification_gas_limit } #[getter] - fn data(&self) -> &str { - &self.backend.data + pub fn paymaster_post_op_gas_limit(&self) -> u128 { + self.backend.paymaster_post_op_gas_limit } - fn to_bytes(&self, py: Python) -> PyObject { - PyBytes::new(py, &self.backend.to_bytes()).into() + #[getter] + pub fn paymaster_data(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.paymaster_data).into() + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) } #[staticmethod] - fn from_bytes(data: &[u8]) -> PyResult { - nucypher_core::UserOperation::from_bytes(data) - .map(|backend| Self { backend }) - .map_err(|e| PyValueError::new_err(e)) + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, SignatureRequestUserOperation>(data) } } @@ -1711,7 +1840,7 @@ impl UserOperationSignatureRequest { pub fn new( user_op: &UserOperation, cohort_id: u32, - chain_id: u32, + chain_id: u64, aa_version: &str, context: Option<&Context>, ) -> PyResult { @@ -1729,9 +1858,7 @@ impl UserOperationSignatureRequest { #[getter] fn user_op(&self) -> UserOperation { - UserOperation { - backend: self.backend.user_op.clone(), - } + UserOperation::from(self.backend.user_op.clone()) } #[getter] @@ -1740,7 +1867,7 @@ impl UserOperationSignatureRequest { } #[getter] - fn chain_id(&self) -> u32 { + fn chain_id(&self) -> u64 { self.backend.chain_id } @@ -1773,34 +1900,258 @@ impl UserOperationSignatureRequest { // PackedUserOperation // +/// Python bindings for PackedUserOperation #[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] pub struct PackedUserOperation { - backend: nucypher_core::PackedUserOperation, + backend: SignatureRequestPackedUserOperation, } #[pymethods] impl PackedUserOperation { #[new] - pub fn new(data: String) -> Self { + pub fn new( + sender: String, + nonce: u64, + init_code: &[u8], + call_data: &[u8], + account_gas_limits: &[u8], + pre_verification_gas: u128, + gas_fees: &[u8], + paymaster_and_data: &[u8], + ) -> PyResult { + // Convert hex string to Address + let sender_address = string_to_address(&sender)?; + + Ok(Self { + backend: SignatureRequestPackedUserOperation::new( + sender_address, + nonce, + init_code, + call_data, + account_gas_limits, + pre_verification_gas, + gas_fees, + paymaster_and_data, + ), + }) + } + + #[staticmethod] + pub fn from_user_operation(user_op: &UserOperation) -> Self { Self { - backend: nucypher_core::PackedUserOperation::new(data), + backend: SignatureRequestPackedUserOperation::from_user_operation(&user_op.backend), + } + } + + #[staticmethod] + #[pyo3(name = "_pack_account_gas_limits")] + pub fn pack_account_gas_limits(py: Python, call_gas_limit: u128, verification_gas_limit: u128) -> PyObject { + let mut result = [0u8; 32]; + // Pack as: verification_gas_limit << 128 | call_gas_limit + // Each value is u128, so verification goes in upper 16 bytes, call in lower 16 bytes + result[0..16].copy_from_slice(&verification_gas_limit.to_be_bytes()); + result[16..32].copy_from_slice(&call_gas_limit.to_be_bytes()); + PyBytes::new(py, &result).into() + } + + #[staticmethod] + #[pyo3(name = "_pack_gas_fees")] + pub fn pack_gas_fees(py: Python, max_fee_per_gas: u128, max_priority_fee_per_gas: u128) -> PyObject { + let mut result = [0u8; 32]; + // Pack as: max_priority_fee_per_gas << 128 | max_fee_per_gas + // Each value is u128, so priority goes in upper 16 bytes, max_fee in lower 16 bytes + result[0..16].copy_from_slice(&max_priority_fee_per_gas.to_be_bytes()); + result[16..32].copy_from_slice(&max_fee_per_gas.to_be_bytes()); + PyBytes::new(py, &result).into() + } + + #[staticmethod] + #[pyo3(name = "_pack_paymaster_and_data", signature = (paymaster, paymaster_verification_gas_limit, paymaster_post_op_gas_limit, paymaster_data))] + pub fn pack_paymaster_and_data( + py: Python, + paymaster: Option, + paymaster_verification_gas_limit: u128, + paymaster_post_op_gas_limit: u128, + paymaster_data: &[u8], + ) -> PyResult { + match paymaster { + None => Ok(PyBytes::new(py, &[]).into()), + Some(addr_str) => { + let addr = string_to_address(&addr_str)?; + let mut result = Vec::with_capacity(20 + 16 + 16 + paymaster_data.len()); + result.extend_from_slice(addr.as_ref()); + + // Verification gas limit as 16 bytes big-endian (full u128) + result.extend_from_slice(&paymaster_verification_gas_limit.to_be_bytes()); + + // Post-op gas limit as 16 bytes big-endian (full u128) + result.extend_from_slice(&paymaster_post_op_gas_limit.to_be_bytes()); + + result.extend_from_slice(paymaster_data); + Ok(PyBytes::new(py, &result).into()) + } } } #[getter] - fn data(&self) -> &str { - &self.backend.data + pub fn sender(&self) -> String { + address_to_string(&self.backend.sender) } - fn to_bytes(&self, py: Python) -> PyObject { - PyBytes::new(py, &self.backend.to_bytes()).into() + #[getter] + pub fn nonce(&self) -> u64 { + self.backend.nonce + } + + #[getter] + pub fn init_code(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.init_code).into() + } + + #[getter] + pub fn call_data(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.call_data).into() + } + + #[getter] + pub fn account_gas_limits(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.account_gas_limits).into() + } + + #[getter] + pub fn pre_verification_gas(&self) -> u128 { + self.backend.pre_verification_gas + } + + #[getter] + pub fn gas_fees(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.gas_fees).into() + } + + #[getter] + pub fn paymaster_and_data(&self, py: Python) -> PyObject { + PyBytes::new(py, &self.backend.paymaster_and_data).into() + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) } #[staticmethod] - fn from_bytes(data: &[u8]) -> PyResult { - nucypher_core::PackedUserOperation::from_bytes(data) - .map(|backend| Self { backend }) - .map_err(|e| PyValueError::new_err(e)) + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, SignatureRequestPackedUserOperation>(data) + } + + pub fn to_eip712_struct(&self, aa_version: &str, chain_id: u64) -> PyResult { + let aa_version = str_to_aa_version(aa_version)?; + let eip712_struct = self.backend.to_eip712_struct(&aa_version, chain_id); + + Python::with_gil(|py| { + json_to_pyobject(py, &serde_json::Value::Object(eip712_struct)) + }) + } + + #[pyo3(name = "_to_eip712_message")] + pub fn to_eip712_message(&self, aa_version: &str) -> PyResult { + let aa_version = str_to_aa_version(aa_version)?; + let message = self.backend.to_eip712_message(&aa_version); + + Python::with_gil(|py| { + json_to_pyobject(py, &serde_json::Value::Object(message)) + }) + } + + #[pyo3(name = "_get_domain")] + pub fn get_domain(&self, aa_version: &str, chain_id: u64) -> PyResult { + let aa_version = str_to_aa_version(aa_version)?; + let domain = self.backend.get_domain(&aa_version, chain_id); + + Python::with_gil(|py| { + json_to_pyobject(py, &serde_json::Value::Object(domain)) + }) + } +} + +// +// SignedPackedUserOperation +// + +/// Python bindings for SignedPackedUserOperation +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct SignedPackedUserOperation { + backend: SignatureRequestSignedPackedUserOperation, +} + +#[pymethods] +impl SignedPackedUserOperation { + #[new] + pub fn new(operation: &PackedUserOperation, signature: &[u8]) -> Self { + Self { + backend: SignatureRequestSignedPackedUserOperation::new( + operation.backend.clone(), + signature, + ), + } + } + + #[getter] + pub fn operation(&self) -> PackedUserOperation { + PackedUserOperation::from(self.backend.operation().clone()) + } + + #[getter] + pub fn signature(&self, py: Python) -> PyObject { + PyBytes::new(py, self.backend.signature()).into() + } + + pub fn into_parts(&self) -> (PackedUserOperation, PyObject) { + let (operation, signature) = (self.backend.operation().clone(), self.backend.signature()); + Python::with_gil(|py| { + ( + PackedUserOperation::from(operation), + PyBytes::new(py, signature).into(), + ) + }) + } + + pub fn to_eip712_struct(&self, aa_version: &str, chain_id: u64) -> PyResult { + let aa_version = str_to_aa_version(aa_version)?; + let eip712_struct = self.backend.to_eip712_struct(&aa_version, chain_id); + + Python::with_gil(|py| { + json_to_pyobject(py, &serde_json::Value::Object(eip712_struct)) + }) + } + + #[pyo3(name = "_to_eip712_message")] + pub fn to_eip712_message(&self, aa_version: &str) -> PyResult { + let aa_version = str_to_aa_version(aa_version)?; + let message = self.backend.to_eip712_message(&aa_version); + + Python::with_gil(|py| { + json_to_pyobject(py, &serde_json::Value::Object(message)) + }) + } + + #[pyo3(name = "_get_domain")] + pub fn get_domain(&self, aa_version: &str, chain_id: u64) -> PyResult { + let aa_version = str_to_aa_version(aa_version)?; + let domain = self.backend.get_domain(&aa_version, chain_id); + + Python::with_gil(|py| { + json_to_pyobject(py, &serde_json::Value::Object(domain)) + }) + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) + } + + #[staticmethod] + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, SignatureRequestSignedPackedUserOperation>(data) } } @@ -1820,7 +2171,7 @@ impl PackedUserOperationSignatureRequest { pub fn new( packed_user_op: &PackedUserOperation, cohort_id: u32, - chain_id: u32, + chain_id: u64, aa_version: &str, context: Option<&Context>, ) -> PyResult { @@ -1838,9 +2189,7 @@ impl PackedUserOperationSignatureRequest { #[getter] fn packed_user_op(&self) -> PackedUserOperation { - PackedUserOperation { - backend: self.backend.packed_user_op.clone(), - } + PackedUserOperation::from(self.backend.packed_user_op.clone()) } #[getter] @@ -1849,7 +2198,7 @@ impl PackedUserOperationSignatureRequest { } #[getter] - fn chain_id(&self) -> u32 { + fn chain_id(&self) -> u64 { self.backend.chain_id } @@ -1959,11 +2308,14 @@ fn _nucypher_core(py: Python, core_module: &PyModule) -> PyResult<()> { // Add signature request/response classes core_module.add_class::()?; + core_module.add_class::()?; core_module.add_class::()?; core_module.add_class::()?; core_module.add_class::()?; + core_module.add_class::()?; core_module.add_class::()?; core_module.add_class::()?; + core_module.add_function(wrap_pyfunction!(deserialize_signature_request, core_module)?)?; // Build the umbral module let umbral_module = PyModule::new(py, "umbral")?; @@ -1998,3 +2350,126 @@ fn _nucypher_core(py: Python, core_module: &PyModule) -> PyResult<()> { Ok(()) } + +// Helper function to convert JSON value to Python object +fn json_to_pyobject(py: Python, value: &serde_json::Value) -> PyResult { + use pyo3::types::{PyDict, PyList}; + use serde_json::Value; + + match value { + Value::Null => Ok(py.None()), + Value::Bool(b) => Ok(b.to_object(py)), + Value::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(i.to_object(py)) + } else if let Some(u) = n.as_u64() { + Ok(u.to_object(py)) + } else if let Some(f) = n.as_f64() { + Ok(f.to_object(py)) + } else { + Err(PyValueError::new_err("Invalid number")) + } + } + Value::String(s) => Ok(s.to_object(py)), + Value::Array(arr) => { + let list = PyList::empty(py); + for item in arr { + list.append(json_to_pyobject(py, item)?)?; + } + Ok(list.to_object(py)) + } + Value::Object(map) => { + let dict = PyDict::new(py); + for (k, v) in map { + dict.set_item(k, json_to_pyobject(py, v)?)?; + } + Ok(dict.to_object(py)) + } + } +} + +// +// Signature Request Deserializer +// + +/// Utility function to deserialize any signature request from bytes - returns specific type directly +#[pyfunction] +pub fn deserialize_signature_request(data: &[u8]) -> PyResult { + let direct_request = nucypher_core::deserialize_signature_request(data) + .map_err(|err| PyValueError::new_err(format!("Failed to deserialize signature request: {}", err)))?; + + // Convert to the specific Python type + match direct_request { + nucypher_core::DirectSignatureRequest::EIP191(req) => { + Python::with_gil(|py| { + let python_req = EIP191SignatureRequest { backend: req }; + Ok(python_req.into_py(py)) + }) + } + nucypher_core::DirectSignatureRequest::UserOp(req) => { + Python::with_gil(|py| { + let python_req = UserOperationSignatureRequest { backend: req }; + Ok(python_req.into_py(py)) + }) + } + nucypher_core::DirectSignatureRequest::PackedUserOp(req) => { + Python::with_gil(|py| { + let python_req = PackedUserOperationSignatureRequest { backend: req }; + Ok(python_req.into_py(py)) + }) + } + } +} + +// +// SignedEIP191SignatureRequest +// + +/// Python bindings for SignedEIP191SignatureRequest +#[pyclass(module = "nucypher_core")] +#[derive(derive_more::From, derive_more::AsRef)] +pub struct SignedEIP191SignatureRequest { + backend: nucypher_core::SignedEIP191SignatureRequest, +} + +#[pymethods] +impl SignedEIP191SignatureRequest { + #[new] + pub fn new(request: &EIP191SignatureRequest, signature: &[u8]) -> Self { + Self { + backend: nucypher_core::SignedEIP191SignatureRequest::new( + request.backend.clone(), + signature, + ), + } + } + + #[getter] + pub fn request(&self) -> EIP191SignatureRequest { + EIP191SignatureRequest::from(self.backend.request().clone()) + } + + #[getter] + pub fn signature(&self, py: Python) -> PyObject { + PyBytes::new(py, self.backend.signature()).into() + } + + pub fn into_parts(&self) -> (EIP191SignatureRequest, PyObject) { + let (request, signature) = (self.backend.request().clone(), self.backend.signature()); + Python::with_gil(|py| { + ( + EIP191SignatureRequest::from(request), + PyBytes::new(py, signature).into(), + ) + }) + } + + fn __bytes__(&self) -> PyObject { + to_bytes(self) + } + + #[staticmethod] + pub fn from_bytes(data: &[u8]) -> PyResult { + from_bytes::<_, nucypher_core::SignedEIP191SignatureRequest>(data) + } +} diff --git a/nucypher-core/Cargo.toml b/nucypher-core/Cargo.toml index 79405951..d666d1ec 100644 --- a/nucypher-core/Cargo.toml +++ b/nucypher-core/Cargo.toml @@ -14,6 +14,7 @@ umbral-pre = { version = "0.11.0", features = ["serde"] } ferveo = { package = "ferveo-nucypher", version = "0.4.0" } ark-std = "0.4" serde = { version = "1", default-features = false, features = ["derive"] } +serde_json = { version = "1" } generic-array = { version = "0.14", features = ["zeroize"] } sha3 = "0.10" rmp-serde = "1" diff --git a/nucypher-core/src/address.rs b/nucypher-core/src/address.rs index eadb4e1c..a47f9d1e 100644 --- a/nucypher-core/src/address.rs +++ b/nucypher-core/src/address.rs @@ -1,3 +1,5 @@ +use alloc::string::String; + use generic_array::{ sequence::Split, typenum::{U12, U20}, @@ -8,16 +10,16 @@ use sha3::{digest::Update, Digest, Keccak256}; use umbral_pre::{serde_bytes, PublicKey}; // We could use the third-party `ethereum_types::Address` here, -// but it has an inefficient `serde` implementation (serializes as hex instead of bytes). -// So for simplicity we just use our own type since we only need the size check. -// Later a conversion method can be easily defined to/from `ethereum_types::Address`. +// but since it's just a wrapper around `[u8; 20]` it's not worth +// adding an extra dependency. Same for `PublicKeyAddress` - we're not burdening this crate +// with web3 primitives, it can be derived later using web3 crate if needed. /// Represents an Ethereum address (20 bytes). #[derive(PartialEq, Debug, Serialize, Deserialize, Copy, Clone, PartialOrd, Eq, Ord)] pub struct Address(#[serde(with = "serde_bytes::as_hex")] [u8; Address::SIZE]); impl Address { - /// Size of canonical Ethereum address, in bytes. + /// Number of bytes in an address. pub const SIZE: usize = 20; /// Creates an address from a fixed-length array. @@ -25,26 +27,83 @@ impl Address { Self(*bytes) } - pub(crate) fn from_public_key(pk: &PublicKey) -> Self { - // Canonical address is the last 20 bytes of keccak256 hash - // of the uncompressed public key (without the header, so 64 bytes in total). - let pk_bytes = pk.to_uncompressed_bytes(); - let digest = Keccak256::new().chain(&pk_bytes[1..]).finalize(); + /// Creates an address from a verification key. + pub fn from_public_key(public_key: &PublicKey) -> Self { + let public_key_bytes = public_key.to_compressed_bytes(); + + let digest = Keccak256::new() + .chain(b"ECDSA") + .chain(public_key_bytes) + .finalize(); let (_prefix, address): (GenericArray, GenericArray) = digest.split(); Self(address.into()) } + + /// Returns the EIP-55 checksummed representation of the address. + pub fn to_checksum_address(&self) -> String { + let hex_address = hex::encode(self.0); + let hash = Keccak256::digest(hex_address.as_bytes()); + + let mut result = String::with_capacity(42); + result.push_str("0x"); + + for (i, ch) in hex_address.chars().enumerate() { + if ch.is_alphabetic() { + let hash_byte = hash[i / 2]; + let hash_nibble = if i % 2 == 0 { + hash_byte >> 4 + } else { + hash_byte & 0x0f + }; + + if hash_nibble >= 8 { + result.push(ch.to_ascii_uppercase()); + } else { + result.push(ch); + } + } else { + result.push(ch); + } + } + + result + } } impl AsRef<[u8]> for Address { fn as_ref(&self) -> &[u8] { - self.0.as_ref() + &self.0 } } -impl From
for [u8; Address::SIZE] { - fn from(address: Address) -> [u8; Address::SIZE] { - address.0 +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_checksum_address() { + // Test case from EIP-55 + let address_bytes = hex::decode("5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed").unwrap(); + let mut array = [0u8; 20]; + array.copy_from_slice(&address_bytes); + let address = Address::new(&array); + + assert_eq!( + address.to_checksum_address(), + "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" + ); + + // Test with all lowercase input + let address_bytes2 = hex::decode("fb6916095ca1df60bb79ce92ce3ea74c37c5d359").unwrap(); + let mut array2 = [0u8; 20]; + array2.copy_from_slice(&address_bytes2); + let address2 = Address::new(&array2); + + assert_eq!( + address2.to_checksum_address(), + "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359" + ); } } diff --git a/nucypher-core/src/lib.rs b/nucypher-core/src/lib.rs index 6f6c5e30..afe35166 100644 --- a/nucypher-core/src/lib.rs +++ b/nucypher-core/src/lib.rs @@ -50,9 +50,10 @@ pub use retrieval_kit::RetrievalKit; pub use revocation_order::RevocationOrder; pub use threshold_message_kit::ThresholdMessageKit; pub use signature_request::{ - AAVersion, BaseSignatureRequest, EIP191SignatureRequest, PackedUserOperation, - PackedUserOperationSignatureRequest, SignatureRequestType, SignatureResponse, UserOperation, - UserOperationSignatureRequest, + AAVersion, BaseSignatureRequest, DirectSignatureRequest, EIP191SignatureRequest, + PackedUserOperation, PackedUserOperationSignatureRequest, SignatureRequestType, + SignatureResponse, SignedEIP191SignatureRequest, SignedPackedUserOperation, UserOperation, + UserOperationSignatureRequest, deserialize_signature_request, }; pub use treasure_map::{EncryptedTreasureMap, TreasureMap}; pub use versioning::ProtocolObject; diff --git a/nucypher-core/src/signature_request.rs b/nucypher-core/src/signature_request.rs index d2025e13..1d73323c 100644 --- a/nucypher-core/src/signature_request.rs +++ b/nucypher-core/src/signature_request.rs @@ -1,11 +1,15 @@ use alloc::boxed::Box; use alloc::string::{String, ToString}; use alloc::vec::Vec; +use alloc::{format, vec}; use core::fmt; +use hex; use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; use umbral_pre::serde_bytes; +use crate::address::Address; use crate::conditions::Context; use crate::versioning::{ messagepack_deserialize, messagepack_serialize, ProtocolObject, ProtocolObjectInner, @@ -63,7 +67,7 @@ pub trait BaseSignatureRequest: Serialize + for<'de> Deserialize<'de> { /// Returns the cohort ID for this signature request fn cohort_id(&self) -> u32; /// Returns the chain ID for this signature request - fn chain_id(&self) -> u32; + fn chain_id(&self) -> u64; /// Returns the signature type for this signature request fn signature_type(&self) -> SignatureRequestType; /// Returns the optional context for this signature request @@ -79,7 +83,7 @@ pub struct EIP191SignatureRequest { /// Cohort ID pub cohort_id: u32, /// Chain ID - pub chain_id: u32, + pub chain_id: u64, /// Optional context pub context: Option, /// Signature type (always EIP-191) @@ -88,7 +92,7 @@ pub struct EIP191SignatureRequest { impl EIP191SignatureRequest { /// Creates a new EIP-191 signature request - pub fn new(data: &[u8], cohort_id: u32, chain_id: u32, context: Option) -> Self { + pub fn new(data: &[u8], cohort_id: u32, chain_id: u64, context: Option) -> Self { Self { data: data.to_vec().into_boxed_slice(), cohort_id, @@ -104,7 +108,7 @@ impl BaseSignatureRequest for EIP191SignatureRequest { self.cohort_id } - fn chain_id(&self) -> u32 { + fn chain_id(&self) -> u64 { self.chain_id } @@ -117,29 +121,107 @@ impl BaseSignatureRequest for EIP191SignatureRequest { } } -/// UserOperation for signature requests +/// Signed EIP-191 signature request - combines an EIP191SignatureRequest with a signature #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserOperation { - /// The serialized user operation data - pub data: String, +pub struct SignedEIP191SignatureRequest { + /// The EIP-191 signature request without signature + pub request: EIP191SignatureRequest, + /// The signature over the request + #[serde(with = "serde_bytes::as_base64")] + pub signature: Box<[u8]>, } -impl UserOperation { - /// Creates a new UserOperation - pub fn new(data: String) -> Self { - Self { data } +impl SignedEIP191SignatureRequest { + /// Creates a new SignedEIP191SignatureRequest + pub fn new(request: EIP191SignatureRequest, signature: &[u8]) -> Self { + Self { + request, + signature: signature.to_vec().into_boxed_slice(), + } } - /// Serializes to bytes - pub fn to_bytes(&self) -> Vec { - self.data.as_bytes().to_vec() + /// Gets a reference to the request part + pub fn request(&self) -> &EIP191SignatureRequest { + &self.request } - /// Deserializes from bytes - pub fn from_bytes(bytes: &[u8]) -> Result { - String::from_utf8(bytes.to_vec()) - .map(|data| Self { data }) - .map_err(|e| e.to_string()) + /// Gets a reference to the signature + pub fn signature(&self) -> &[u8] { + &self.signature + } + + /// Returns the request and signature as separate components + pub fn into_parts(self) -> (EIP191SignatureRequest, Box<[u8]>) { + (self.request, self.signature) + } +} + +/// UserOperation for signature requests +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserOperation { + /// Address of the sender (smart contract account) + pub sender: Address, + /// Nonce for replay protection + pub nonce: u64, + /// Factory and data for account creation (empty for existing accounts) + #[serde(with = "serde_bytes::as_base64")] + pub init_code: Box<[u8]>, + /// The calldata to execute + #[serde(with = "serde_bytes::as_base64")] + pub call_data: Box<[u8]>, + /// Gas limit for the call + pub call_gas_limit: u128, + /// Gas limit for verification + pub verification_gas_limit: u128, + /// Gas to cover overhead + pub pre_verification_gas: u128, + /// Maximum fee per gas unit + pub max_fee_per_gas: u128, + /// Maximum priority fee per gas unit + pub max_priority_fee_per_gas: u128, + /// Paymaster address (optional) + pub paymaster: Option
, + /// Gas limit for paymaster verification + pub paymaster_verification_gas_limit: u128, + /// Gas limit for paymaster post-operation + pub paymaster_post_op_gas_limit: u128, + /// Paymaster-specific data + #[serde(with = "serde_bytes::as_base64")] + pub paymaster_data: Box<[u8]>, +} + +impl UserOperation { + /// Creates a new UserOperation + pub fn new( + sender: Address, + nonce: u64, + init_code: Option<&[u8]>, + call_data: Option<&[u8]>, + call_gas_limit: Option, + verification_gas_limit: Option, + pre_verification_gas: Option, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + paymaster: Option
, + paymaster_verification_gas_limit: Option, + paymaster_post_op_gas_limit: Option, + paymaster_data: Option<&[u8]>, + ) -> Self { + Self { + sender, + nonce, + init_code: init_code.unwrap_or_default().into(), + call_data: call_data.unwrap_or_default().into(), + call_gas_limit: call_gas_limit.unwrap_or(0), + verification_gas_limit: verification_gas_limit.unwrap_or(0), + pre_verification_gas: pre_verification_gas.unwrap_or(0), + max_fee_per_gas: max_fee_per_gas.unwrap_or(0), + max_priority_fee_per_gas: max_priority_fee_per_gas.unwrap_or(0), + paymaster, + paymaster_verification_gas_limit: paymaster_verification_gas_limit.unwrap_or(0), + paymaster_post_op_gas_limit: paymaster_post_op_gas_limit.unwrap_or(0), + paymaster_data: paymaster_data.unwrap_or_default().into(), + } } } @@ -151,7 +233,7 @@ pub struct UserOperationSignatureRequest { /// Cohort ID pub cohort_id: u32, /// Chain ID - pub chain_id: u32, + pub chain_id: u64, /// AA version pub aa_version: AAVersion, /// Optional context @@ -165,7 +247,7 @@ impl UserOperationSignatureRequest { pub fn new( user_op: UserOperation, cohort_id: u32, - chain_id: u32, + chain_id: u64, aa_version: AAVersion, context: Option, ) -> Self { @@ -185,7 +267,7 @@ impl BaseSignatureRequest for UserOperationSignatureRequest { self.cohort_id } - fn chain_id(&self) -> u32 { + fn chain_id(&self) -> u64 { self.chain_id } @@ -201,26 +283,285 @@ impl BaseSignatureRequest for UserOperationSignatureRequest { /// Packed UserOperation for signature requests #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PackedUserOperation { - /// The serialized packed user operation data - pub data: String, + /// Address of the sender (smart contract account) + pub sender: Address, + /// Nonce for replay protection + pub nonce: u64, + /// Factory and data for account creation + #[serde(with = "serde_bytes::as_base64")] + pub init_code: Box<[u8]>, + /// The calldata to execute + #[serde(with = "serde_bytes::as_base64")] + pub call_data: Box<[u8]>, + /// Packed gas limits (verification gas limit << 128 | call gas limit) + #[serde(with = "serde_bytes::as_base64")] + pub account_gas_limits: Box<[u8]>, + /// Gas to cover overhead + pub pre_verification_gas: u128, + /// Packed gas fees (max priority fee << 128 | max fee) + #[serde(with = "serde_bytes::as_base64")] + pub gas_fees: Box<[u8]>, + /// Packed paymaster data (address, verification gas limit, post-op gas limit, data) + #[serde(with = "serde_bytes::as_base64")] + pub paymaster_and_data: Box<[u8]>, } impl PackedUserOperation { /// Creates a new PackedUserOperation - pub fn new(data: String) -> Self { - Self { data } + pub fn new( + sender: Address, + nonce: u64, + init_code: &[u8], + call_data: &[u8], + account_gas_limits: &[u8], + pre_verification_gas: u128, + gas_fees: &[u8], + paymaster_and_data: &[u8], + ) -> Self { + Self { + sender, + nonce, + init_code: init_code.to_vec().into_boxed_slice(), + call_data: call_data.to_vec().into_boxed_slice(), + account_gas_limits: account_gas_limits.to_vec().into_boxed_slice(), + pre_verification_gas, + gas_fees: gas_fees.to_vec().into_boxed_slice(), + paymaster_and_data: paymaster_and_data.to_vec().into_boxed_slice(), + } } - /// Serializes to bytes - pub fn to_bytes(&self) -> Vec { - self.data.as_bytes().to_vec() + /// Packs account gas limits into a 32-byte value (u128 verification_gas_limit << 128 | u128 call_gas_limit) + fn pack_account_gas_limits(call_gas_limit: u128, verification_gas_limit: u128) -> [u8; 32] { + let mut result = [0u8; 32]; + // Pack as: verification_gas_limit << 128 | call_gas_limit + // Each value is u128, so verification goes in upper 16 bytes, call in lower 16 bytes + result[0..16].copy_from_slice(&verification_gas_limit.to_be_bytes()); + result[16..32].copy_from_slice(&call_gas_limit.to_be_bytes()); + result } - /// Deserializes from bytes - pub fn from_bytes(bytes: &[u8]) -> Result { - String::from_utf8(bytes.to_vec()) - .map(|data| Self { data }) - .map_err(|e| e.to_string()) + /// Packs gas fees into a 32-byte value (u128 max_priority_fee_per_gas << 128 | u128 max_fee_per_gas) + fn pack_gas_fees(max_fee_per_gas: u128, max_priority_fee_per_gas: u128) -> [u8; 32] { + let mut result = [0u8; 32]; + // Pack as: max_priority_fee_per_gas << 128 | max_fee_per_gas + // Each value is u128, so priority goes in upper 16 bytes, max_fee in lower 16 bytes + result[0..16].copy_from_slice(&max_priority_fee_per_gas.to_be_bytes()); + result[16..32].copy_from_slice(&max_fee_per_gas.to_be_bytes()); + result + } + + /// Packs paymaster data with u128 gas limits + fn pack_paymaster_and_data( + paymaster: Option<&Address>, + paymaster_verification_gas_limit: u128, + paymaster_post_op_gas_limit: u128, + paymaster_data: &[u8], + ) -> Vec { + match paymaster { + None => Vec::new(), + Some(addr) => { + let mut result = Vec::with_capacity(20 + 16 + 16 + paymaster_data.len()); + result.extend_from_slice(addr.as_ref()); + + // Verification gas limit as 16 bytes big-endian (full u128) + result.extend_from_slice(&paymaster_verification_gas_limit.to_be_bytes()); + + // Post-op gas limit as 16 bytes big-endian (full u128) + result.extend_from_slice(&paymaster_post_op_gas_limit.to_be_bytes()); + + result.extend_from_slice(paymaster_data); + result + } + } + } + + /// Creates a PackedUserOperation from a UserOperation + pub fn from_user_operation(user_op: &UserOperation) -> Self { + let account_gas_limits = Self::pack_account_gas_limits( + user_op.call_gas_limit, + user_op.verification_gas_limit, + ); + + let gas_fees = Self::pack_gas_fees( + user_op.max_fee_per_gas, + user_op.max_priority_fee_per_gas, + ); + + let paymaster_and_data = Self::pack_paymaster_and_data( + user_op.paymaster.as_ref(), + user_op.paymaster_verification_gas_limit, + user_op.paymaster_post_op_gas_limit, + &user_op.paymaster_data, + ); + + Self { + sender: user_op.sender, + nonce: user_op.nonce, + init_code: user_op.init_code.clone(), + call_data: user_op.call_data.clone(), + account_gas_limits: account_gas_limits.to_vec().into_boxed_slice(), + pre_verification_gas: user_op.pre_verification_gas, + gas_fees: gas_fees.to_vec().into_boxed_slice(), + paymaster_and_data: paymaster_and_data.into_boxed_slice(), + } + } + + /// Converts to EIP-712 message format + pub fn to_eip712_message(&self, aa_version: &AAVersion) -> serde_json::Map { + let mut message = serde_json::Map::new(); + message.insert("sender".into(), JsonValue::String(format!("0x{}", hex::encode(self.sender.as_ref())))); + message.insert("nonce".into(), JsonValue::Number(self.nonce.into())); + message.insert("initCode".into(), JsonValue::String(format!("0x{}", hex::encode(&self.init_code)))); + message.insert("callData".into(), JsonValue::String(format!("0x{}", hex::encode(&self.call_data)))); + message.insert("accountGasLimits".into(), JsonValue::String(format!("0x{}", hex::encode(&self.account_gas_limits)))); + message.insert("preVerificationGas".into(), JsonValue::String(self.pre_verification_gas.to_string())); + message.insert("gasFees".into(), JsonValue::String(format!("0x{}", hex::encode(&self.gas_fees)))); + message.insert("paymasterAndData".into(), JsonValue::String(format!("0x{}", hex::encode(&self.paymaster_and_data)))); + + if *aa_version == AAVersion::MDT { + message.insert("entryPoint".into(), JsonValue::String("0x0000000071727de22e5e9d8baf0edac6f37da032".into())); + } + + message + } + + /// Gets the EIP-712 domain + pub fn get_domain(&self, aa_version: &AAVersion, chain_id: u64) -> serde_json::Map { + let mut domain = serde_json::Map::new(); + + let name = if *aa_version != AAVersion::MDT { "ERC4337" } else { "MultiSigDeleGator" }; + domain.insert("name".into(), JsonValue::String(name.into())); + domain.insert("version".into(), JsonValue::String("1".into())); + domain.insert("chainId".into(), JsonValue::Number(chain_id.into())); + + let verifying_contract = if *aa_version != AAVersion::MDT { + "0x4337084d9e255ff0702461cf8895ce9e3b5ff108".into() + } else { + format!("0x{}", hex::encode(self.sender.as_ref())) + }; + domain.insert("verifyingContract".into(), JsonValue::String(verifying_contract)); + + domain + } + + /// Converts to EIP-712 struct format + pub fn to_eip712_struct(&self, aa_version: &AAVersion, chain_id: u64) -> serde_json::Map { + let mut result = serde_json::Map::new(); + + // Create types + let mut types = serde_json::Map::new(); + + // EIP712Domain type + let mut domain_type = Vec::new(); + let mut name_field = serde_json::Map::new(); + name_field.insert("name".into(), JsonValue::String("name".into())); + name_field.insert("type".into(), JsonValue::String("string".into())); + domain_type.push(JsonValue::Object(name_field)); + + let mut version_field = serde_json::Map::new(); + version_field.insert("name".into(), JsonValue::String("version".into())); + version_field.insert("type".into(), JsonValue::String("string".into())); + domain_type.push(JsonValue::Object(version_field)); + + let mut chain_id_field = serde_json::Map::new(); + chain_id_field.insert("name".into(), JsonValue::String("chainId".into())); + chain_id_field.insert("type".into(), JsonValue::String("uint256".into())); + domain_type.push(JsonValue::Object(chain_id_field)); + + let mut verifying_contract_field = serde_json::Map::new(); + verifying_contract_field.insert("name".into(), JsonValue::String("verifyingContract".into())); + verifying_contract_field.insert("type".into(), JsonValue::String("address".into())); + domain_type.push(JsonValue::Object(verifying_contract_field)); + + types.insert("EIP712Domain".into(), JsonValue::Array(domain_type)); + + // PackedUserOperation type + let mut packed_user_op_type = Vec::new(); + + let field_specs = vec![ + ("sender", "address"), + ("nonce", "uint256"), + ("initCode", "bytes"), + ("callData", "bytes"), + ("accountGasLimits", "bytes32"), + ("preVerificationGas", "uint256"), + ("gasFees", "bytes32"), + ("paymasterAndData", "bytes"), + ]; + + for (name, type_str) in field_specs { + let mut field = serde_json::Map::new(); + field.insert("name".into(), JsonValue::String(name.into())); + field.insert("type".into(), JsonValue::String(type_str.into())); + packed_user_op_type.push(JsonValue::Object(field)); + } + + if *aa_version == AAVersion::MDT { + let mut entry_point_field = serde_json::Map::new(); + entry_point_field.insert("name".into(), JsonValue::String("entryPoint".into())); + entry_point_field.insert("type".into(), JsonValue::String("address".into())); + packed_user_op_type.push(JsonValue::Object(entry_point_field)); + } + + types.insert("PackedUserOperation".into(), JsonValue::Array(packed_user_op_type)); + + // Build final result + result.insert("types".into(), JsonValue::Object(types)); + result.insert("primaryType".into(), JsonValue::String("PackedUserOperation".into())); + result.insert("domain".into(), JsonValue::Object(self.get_domain(aa_version, chain_id))); + result.insert("message".into(), JsonValue::Object(self.to_eip712_message(aa_version))); + + result + } +} + +/// Signed Packed UserOperation - combines a PackedUserOperation with a signature +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SignedPackedUserOperation { + /// The packed user operation without signature + pub operation: PackedUserOperation, + /// The signature over the operation + #[serde(with = "serde_bytes::as_base64")] + pub signature: Box<[u8]>, +} + +impl SignedPackedUserOperation { + /// Creates a new SignedPackedUserOperation + pub fn new(operation: PackedUserOperation, signature: &[u8]) -> Self { + Self { + operation, + signature: signature.to_vec().into_boxed_slice(), + } + } + + /// Gets a reference to the operation part + pub fn operation(&self) -> &PackedUserOperation { + &self.operation + } + + /// Gets a reference to the signature + pub fn signature(&self) -> &[u8] { + &self.signature + } + + /// Returns the operation and signature as separate components + pub fn into_parts(self) -> (PackedUserOperation, Box<[u8]>) { + (self.operation, self.signature) + } + + /// Converts to EIP-712 message format (delegates to operation) + pub fn to_eip712_message(&self, aa_version: &AAVersion) -> serde_json::Map { + self.operation.to_eip712_message(aa_version) + } + + /// Gets the EIP-712 domain (delegates to operation) + pub fn get_domain(&self, aa_version: &AAVersion, chain_id: u64) -> serde_json::Map { + self.operation.get_domain(aa_version, chain_id) + } + + /// Converts to EIP-712 struct format (delegates to operation) + pub fn to_eip712_struct(&self, aa_version: &AAVersion, chain_id: u64) -> serde_json::Map { + self.operation.to_eip712_struct(aa_version, chain_id) } } @@ -232,7 +573,7 @@ pub struct PackedUserOperationSignatureRequest { /// Cohort ID pub cohort_id: u32, /// Chain ID - pub chain_id: u32, + pub chain_id: u64, /// AA version pub aa_version: AAVersion, /// Optional context @@ -246,7 +587,7 @@ impl PackedUserOperationSignatureRequest { pub fn new( packed_user_op: PackedUserOperation, cohort_id: u32, - chain_id: u32, + chain_id: u64, aa_version: AAVersion, context: Option, ) -> Self { @@ -266,7 +607,7 @@ impl BaseSignatureRequest for PackedUserOperationSignatureRequest { self.cohort_id } - fn chain_id(&self) -> u32 { + fn chain_id(&self) -> u64 { self.chain_id } @@ -329,6 +670,30 @@ impl<'a> ProtocolObjectInner<'a> for EIP191SignatureRequest { impl<'a> ProtocolObject<'a> for EIP191SignatureRequest {} +impl<'a> ProtocolObjectInner<'a> for SignedEIP191SignatureRequest { + fn brand() -> [u8; 4] { + *b"SE19" + } + + fn version() -> (u16, u16) { + (1, 0) + } + + fn unversioned_to_bytes(&self) -> Box<[u8]> { + messagepack_serialize(&self) + } + + fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option> { + if minor_version == 0 { + Some(messagepack_deserialize(bytes)) + } else { + None + } + } +} + +impl<'a> ProtocolObject<'a> for SignedEIP191SignatureRequest {} + impl<'a> ProtocolObjectInner<'a> for UserOperationSignatureRequest { fn brand() -> [u8; 4] { *b"UOSR" @@ -401,14 +766,181 @@ impl<'a> ProtocolObjectInner<'a> for SignatureResponse { impl<'a> ProtocolObject<'a> for SignatureResponse {} +impl<'a> ProtocolObjectInner<'a> for UserOperation { + fn brand() -> [u8; 4] { + *b"UOPR" + } + + fn version() -> (u16, u16) { + (1, 0) + } + + fn unversioned_to_bytes(&self) -> Box<[u8]> { + messagepack_serialize(&self) + } + + fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option> { + if minor_version == 0 { + Some(messagepack_deserialize(bytes)) + } else { + None + } + } +} + +impl<'a> ProtocolObject<'a> for UserOperation {} + +impl<'a> ProtocolObjectInner<'a> for PackedUserOperation { + fn brand() -> [u8; 4] { + *b"PUOP" + } + + fn version() -> (u16, u16) { + (1, 0) + } + + fn unversioned_to_bytes(&self) -> Box<[u8]> { + messagepack_serialize(&self) + } + + fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option> { + if minor_version == 0 { + Some(messagepack_deserialize(bytes)) + } else { + None + } + } +} + +impl<'a> ProtocolObject<'a> for PackedUserOperation {} + +impl<'a> ProtocolObjectInner<'a> for SignedPackedUserOperation { + fn brand() -> [u8; 4] { + *b"SPUO" + } + + fn version() -> (u16, u16) { + (1, 0) + } + + fn unversioned_to_bytes(&self) -> Box<[u8]> { + messagepack_serialize(&self) + } + + fn unversioned_from_bytes(minor_version: u16, bytes: &[u8]) -> Option> { + if minor_version == 0 { + Some(messagepack_deserialize(bytes)) + } else { + None + } + } +} + +impl<'a> ProtocolObject<'a> for SignedPackedUserOperation {} + +/// Enum to hold any type of signature request for direct returns +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DirectSignatureRequest { + /// EIP-191 signature request + EIP191(EIP191SignatureRequest), + /// UserOperation signature request + UserOp(UserOperationSignatureRequest), + /// PackedUserOperation signature request + PackedUserOp(PackedUserOperationSignatureRequest), +} + +impl DirectSignatureRequest { + /// Deserialize any signature request from bytes by checking the brand identifier + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < 4 { + return Err("Insufficient bytes for brand identifier".into()); + } + + // Extract the 4-byte brand identifier + let brand = [bytes[0], bytes[1], bytes[2], bytes[3]]; + + match &brand { + b"E191" => { + EIP191SignatureRequest::from_bytes(bytes) + .map(Self::EIP191) + .map_err(|e| format!("Failed to deserialize EIP191SignatureRequest: {}", e)) + } + b"UOSR" => { + UserOperationSignatureRequest::from_bytes(bytes) + .map(Self::UserOp) + .map_err(|e| format!("Failed to deserialize UserOperationSignatureRequest: {}", e)) + } + b"PUOS" => { + PackedUserOperationSignatureRequest::from_bytes(bytes) + .map(Self::PackedUserOp) + .map_err(|e| format!("Failed to deserialize PackedUserOperationSignatureRequest: {}", e)) + } + _ => Err(format!("Unknown signature request brand: {:?}", brand)), + } + } + + /// Get the signature type for this request + pub fn signature_type(&self) -> SignatureRequestType { + match self { + Self::EIP191(_) => SignatureRequestType::EIP191, + Self::UserOp(_) => SignatureRequestType::UserOp, + Self::PackedUserOp(_) => SignatureRequestType::PackedUserOp, + } + } + + /// Get the cohort ID for this request + pub fn cohort_id(&self) -> u32 { + match self { + Self::EIP191(req) => req.cohort_id(), + Self::UserOp(req) => req.cohort_id(), + Self::PackedUserOp(req) => req.cohort_id(), + } + } + + /// Get the chain ID for this request + pub fn chain_id(&self) -> u64 { + match self { + Self::EIP191(req) => req.chain_id(), + Self::UserOp(req) => req.chain_id(), + Self::PackedUserOp(req) => req.chain_id(), + } + } + + /// Get the optional context for this request + pub fn context(&self) -> Option<&Context> { + match self { + Self::EIP191(req) => req.context(), + Self::UserOp(req) => req.context(), + Self::PackedUserOp(req) => req.context(), + } + } +} + +/// Utility function to deserialize any signature request from bytes +pub fn deserialize_signature_request(bytes: &[u8]) -> Result { + DirectSignatureRequest::from_bytes(bytes) +} + #[cfg(test)] mod tests { use super::*; + use alloc::string::ToString; + use hex; + + // Helper function to create an Address from a hex string + fn address_from_hex(hex_str: &str) -> Address { + let bytes = hex::decode(hex_str).unwrap(); + let mut array = [0u8; 20]; + array.copy_from_slice(&bytes); + Address::new(&array) + } #[test] fn test_eip191_signature_request_serialization() { let data = b"test data"; - let request = EIP191SignatureRequest::new(data, 1, 137, None); + // Test with a large chain ID like the example provided (131277322940537) + let large_chain_id = 131277322940537u64; + let request = EIP191SignatureRequest::new(data, 1, large_chain_id, None); let bytes = request.to_bytes(); let deserialized = EIP191SignatureRequest::from_bytes(&bytes).unwrap(); @@ -416,13 +948,62 @@ mod tests { assert_eq!(request, deserialized); assert_eq!(deserialized.data.as_ref(), data); assert_eq!(deserialized.cohort_id, 1); - assert_eq!(deserialized.chain_id, 137); + assert_eq!(deserialized.chain_id, large_chain_id); assert_eq!(deserialized.signature_type, SignatureRequestType::EIP191); } + #[test] + fn test_signed_eip191_signature_request() { + let data = b"test data for signing"; + let context = Some(Context::new("test_context")); + let request = EIP191SignatureRequest::new(data, 456, 1, context); + let test_signature = b"test_eip191_signature"; + + // Test creating SignedEIP191SignatureRequest + let signed_request = SignedEIP191SignatureRequest::new(request.clone(), test_signature); + + assert_eq!(signed_request.signature(), test_signature); + assert_eq!(signed_request.request().data.as_ref(), data); + assert_eq!(signed_request.request().cohort_id, 456); + assert_eq!(signed_request.request().chain_id, 1); + + // Test into_parts method + let (reconstructed_request, reconstructed_signature) = signed_request.clone().into_parts(); + assert_eq!(reconstructed_signature.as_ref(), test_signature); + assert_eq!(reconstructed_request.data.as_ref(), data); + assert_eq!(reconstructed_request.cohort_id, 456); + assert_eq!(reconstructed_request.chain_id, 1); + + // Test serialization + let bytes = signed_request.to_bytes(); + let deserialized = SignedEIP191SignatureRequest::from_bytes(&bytes).unwrap(); + assert_eq!(signed_request, deserialized); + assert_eq!(deserialized.signature(), test_signature); + assert_eq!(deserialized.request().data.as_ref(), data); + assert_eq!(deserialized.request().cohort_id, 456); + assert_eq!(deserialized.request().chain_id, 1); + } + #[test] fn test_user_operation_signature_request_serialization() { - let user_op = UserOperation::new("test_user_op_data".to_string()); + let sender = address_from_hex("1234567890123456789012345678901234567890"); + let paymaster = Some(address_from_hex("abcdefabcdefabcdefabcdefabcdefabcdefabcd")); + + let user_op = UserOperation::new( + sender, + 42, + Some(b"init_code"), + Some(b"call_data"), + Some(100000), + Some(200000), + Some(50000), + Some(20_000_000_000), // 20 gwei + Some(1_000_000_000), // 1 gwei + paymaster, + Some(300000), + Some(100000), + Some(b"paymaster_data"), + ); let request = UserOperationSignatureRequest::new( user_op.clone(), 1, @@ -435,7 +1016,8 @@ mod tests { let deserialized = UserOperationSignatureRequest::from_bytes(&bytes).unwrap(); assert_eq!(request, deserialized); - assert_eq!(deserialized.user_op.data, "test_user_op_data"); + assert_eq!(deserialized.user_op.sender, sender); + assert_eq!(deserialized.user_op.nonce, 42); assert_eq!(deserialized.aa_version, AAVersion::V08); assert_eq!(deserialized.context.as_ref().unwrap().as_ref(), "test_context"); } @@ -458,13 +1040,28 @@ mod tests { #[test] fn test_aa_version_serialization() { // Test V08 - let user_op = UserOperation::new("test_v08".to_string()); + let sender = address_from_hex("789abcdef0123456789abcdef0123456789abcde"); + let user_op = UserOperation::new( + sender, + 1, + Some(b""), + Some(b""), + Some(0), + Some(0), + Some(0), + Some(0), + Some(0), + None, + Some(0), + Some(0), + Some(b""), + ); let request_v08 = UserOperationSignatureRequest::new( - user_op, + user_op.clone(), 1, 137, AAVersion::V08, - None, + Some(Context::new("test_context")), ); let bytes = request_v08.to_bytes(); @@ -472,13 +1069,28 @@ mod tests { assert_eq!(deserialized_v08.aa_version, AAVersion::V08); // Test MDT - let user_op_mdt = UserOperation::new("test_mdt".to_string()); + let sender_mdt = address_from_hex("abcdef0123456789abcdef0123456789abcdef01"); + let user_op_mdt = UserOperation::new( + sender_mdt, + 2, + Some(b""), + Some(b""), + Some(0), + Some(0), + Some(0), + Some(0), + Some(0), + None, + Some(0), + Some(0), + Some(b""), + ); let request_mdt = UserOperationSignatureRequest::new( user_op_mdt, 2, 137, AAVersion::MDT, - None, + Some(Context::new("test_context")), ); let bytes_mdt = request_mdt.to_bytes(); @@ -489,4 +1101,93 @@ mod tests { assert_eq!(AAVersion::V08.to_string(), "0.8.0"); assert_eq!(AAVersion::MDT.to_string(), "mdt"); } + + #[test] + fn test_packed_user_operation_conversion() { + let sender = address_from_hex("1234567890123456789012345678901234567890"); + let paymaster = Some(address_from_hex("abcdefabcdefabcdefabcdefabcdefabcdefabcd")); + + let user_op = UserOperation::new( + sender, + 100, + Some(b"factory_code"), + Some(b"execution_data"), + Some(150000), + Some(250000), + Some(60000), + Some(30_000_000_000), + Some(2_000_000_000), + paymaster, + Some(400000), + Some(200000), + Some(b"paymaster_specific_data"), + ); + + let packed = PackedUserOperation::from_user_operation(&user_op); + + assert_eq!(packed.sender, user_op.sender); + assert_eq!(packed.nonce, user_op.nonce); + assert_eq!(packed.init_code, user_op.init_code); + assert_eq!(packed.call_data, user_op.call_data); + assert_eq!(packed.pre_verification_gas, user_op.pre_verification_gas); + + // Check account gas limits packing + assert_eq!(packed.account_gas_limits.len(), 32); + + // Check gas fees packing + assert_eq!(packed.gas_fees.len(), 32); + + // Check paymaster data packing (20 bytes address + 16 bytes + 16 bytes + data) + assert_eq!(packed.paymaster_and_data.len(), 20 + 16 + 16 + b"paymaster_specific_data".len()); + } + + #[test] + fn test_signed_packed_user_operation() { + let sender = address_from_hex("1234567890123456789012345678901234567890"); + let paymaster = Some(address_from_hex("abcdefabcdefabcdefabcdefabcdefabcdefabcd")); + + let user_op = UserOperation::new( + sender, + 123, + Some(b"init_factory"), + Some(b"call_data_test"), + Some(200000), + Some(300000), + Some(70000), + Some(25_000_000_000), + Some(1_500_000_000), + paymaster, + Some(500000), + Some(250000), + Some(b"paymaster_test_data"), + ); + + let packed = PackedUserOperation::from_user_operation(&user_op); + let test_signature = b"test_signature"; + + // Test creating SignedPackedUserOperation + let signed_packed = SignedPackedUserOperation::new(packed.clone(), test_signature); + + assert_eq!(signed_packed.signature(), test_signature); + assert_eq!(signed_packed.operation().sender, sender); + assert_eq!(signed_packed.operation().nonce, 123); + + // Test into_parts method + let (reconstructed_operation, reconstructed_signature) = signed_packed.clone().into_parts(); + assert_eq!(reconstructed_signature.as_ref(), test_signature); + assert_eq!(reconstructed_operation.sender, sender); + assert_eq!(reconstructed_operation.nonce, 123); + + // Test serialization + let bytes = signed_packed.to_bytes(); + let deserialized = SignedPackedUserOperation::from_bytes(&bytes).unwrap(); + assert_eq!(signed_packed, deserialized); + assert_eq!(deserialized.signature(), test_signature); + assert_eq!(deserialized.operation().sender, sender); + + // Test EIP-712 methods delegate correctly + let eip712_message = signed_packed.to_eip712_message(&AAVersion::V08); + let operation_message = signed_packed.operation().to_eip712_message(&AAVersion::V08); + assert_eq!(eip712_message, operation_message); + } } \ No newline at end of file diff --git a/test_gas_limits_corrected.py b/test_gas_limits_corrected.py new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/test_gas_limits_corrected.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test_signed_packed_user_operation.py b/test_signed_packed_user_operation.py new file mode 100644 index 00000000..38eb602f --- /dev/null +++ b/test_signed_packed_user_operation.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +import nucypher_core +from nucypher_core import PackedUserOperation, SignedPackedUserOperation + +def test_signed_packed_user_operation(): + print("Testing SignedPackedUserOperation...") + + # Test creating a PackedUserOperation + packed_op = PackedUserOperation( + sender='0x1234567890123456789012345678901234567890', + nonce=123, + init_code=b'test_init', + call_data=b'test_call', + account_gas_limits=b'\x00' * 32, + pre_verification_gas=50000, + gas_fees=b'\x00' * 32, + paymaster_and_data=b'', + signature=b'original_signature' + ) + + # Test creating a SignedPackedUserOperation + signed_op = SignedPackedUserOperation(packed_op, b'new_signature') + print('✓ Created SignedPackedUserOperation successfully') + + # Test accessing properties + print(f'✓ Signature length: {len(signed_op.signature)}') + print(f'✓ Operation sender: {signed_op.operation.sender}') + print(f'✓ Operation nonce: {signed_op.operation.nonce}') + + # Test that the operation part has no signature + assert len(signed_op.operation.signature) == 0, "Operation should have empty signature" + print('✓ Operation part has empty signature as expected') + + # Test from_packed_user_operation + signed_from_existing = SignedPackedUserOperation.from_packed_user_operation(packed_op) + print(f'✓ Extracted signature: {signed_from_existing.signature}') + assert signed_from_existing.signature == b'original_signature' + + # Test converting back to PackedUserOperation + reconstructed = signed_op.to_packed_user_operation() + print(f'✓ Reconstructed signature: {reconstructed.signature}') + assert reconstructed.signature == b'new_signature' + + # Test serialization + serialized = bytes(signed_op) + deserialized = SignedPackedUserOperation.from_bytes(serialized) + assert deserialized.signature == signed_op.signature + print('✓ Serialization test passed') + + # Test EIP-712 methods work + try: + eip712_message = signed_op._to_eip712_message("0.8.0") + domain = signed_op._get_domain("0.8.0", 1) + eip712_struct = signed_op.to_eip712_struct("0.8.0", 1) + print('✓ EIP-712 methods work correctly') + except Exception as e: + print(f'✗ EIP-712 methods failed: {e}') + raise + + print('✅ All SignedPackedUserOperation tests passed!') + +if __name__ == '__main__': + test_signed_packed_user_operation() \ No newline at end of file From 01477b8a682aa16b3261b2ddd89aef125167ac56 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 11 Sep 2025 16:19:28 -0400 Subject: [PATCH 3/4] Fix changes that caused `nucypher` tests to fail; aad, decryption, signing etc. --- nucypher-core-python/src/lib.rs | 33 ++++++++++++++------------------- nucypher-core/src/address.rs | 13 +++++-------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/nucypher-core-python/src/lib.rs b/nucypher-core-python/src/lib.rs index f53c306f..1faf8e3e 100644 --- a/nucypher-core-python/src/lib.rs +++ b/nucypher-core-python/src/lib.rs @@ -786,12 +786,12 @@ impl AuthenticatedData { } } - pub fn aad(&self) -> PyResult> { + pub fn aad(&self, py: Python) -> PyResult { let result = self .backend .aad() .map_err(|err| PyValueError::new_err(format!("{err}")))?; - Ok(result.to_vec()) + Ok(PyBytes::new(py, &result).into()) } #[getter] @@ -848,13 +848,12 @@ impl AccessControlPolicy { } } - pub fn aad(&self) -> PyResult> { - self.backend + pub fn aad(&self, py: Python) -> PyResult { + let result = self + .backend .aad() - .map(|aad| aad.to_vec()) - .map_err(|err| { - PyValueError::new_err(format!("Failed to get authenticated data: {err}")) - }) + .map_err(|err| PyValueError::new_err(format!("{err}")))?; + Ok(PyBytes::new(py, &result).into()) } #[getter] @@ -898,32 +897,28 @@ impl ThresholdMessageKit { #[new] pub fn new(ciphertext: &Ciphertext, acp: &AccessControlPolicy) -> Self { Self { - backend: nucypher_core::ThresholdMessageKit::new( - ciphertext.as_ref(), - &acp.backend - ), + backend: nucypher_core::ThresholdMessageKit::new(ciphertext.as_ref(), &acp.backend), } } #[getter] pub fn ciphertext_header(&self) -> PyResult { - self.backend + let header = self + .backend .ciphertext_header() - .map(|header| header.into()) - .map_err(|err| PyValueError::new_err(format!("Failed to get ciphertext header: {}", err))) + .map_err(FerveoPythonError::from)?; + Ok(CiphertextHeader::from(header)) } #[getter] pub fn acp(&self) -> AccessControlPolicy { - AccessControlPolicy { - backend: self.backend.acp.clone(), - } + self.backend.acp.clone().into() } pub fn decrypt_with_shared_secret(&self, shared_secret: &SharedSecret) -> PyResult> { self.backend .decrypt_with_shared_secret(shared_secret.as_ref()) - .map_err(|err| PyValueError::new_err(format!("Failed to decrypt: {}", err))) + .map_err(|err| FerveoPythonError::FerveoError(err).into()) } #[staticmethod] diff --git a/nucypher-core/src/address.rs b/nucypher-core/src/address.rs index a47f9d1e..347328ce 100644 --- a/nucypher-core/src/address.rs +++ b/nucypher-core/src/address.rs @@ -27,14 +27,11 @@ impl Address { Self(*bytes) } - /// Creates an address from a verification key. - pub fn from_public_key(public_key: &PublicKey) -> Self { - let public_key_bytes = public_key.to_compressed_bytes(); - - let digest = Keccak256::new() - .chain(b"ECDSA") - .chain(public_key_bytes) - .finalize(); + pub(crate) fn from_public_key(pk: &PublicKey) -> Self { + // Canonical address is the last 20 bytes of keccak256 hash + // of the uncompressed public key (without the header, so 64 bytes in total). + let pk_bytes = pk.to_uncompressed_bytes(); + let digest = Keccak256::new().chain(&pk_bytes[1..]).finalize(); let (_prefix, address): (GenericArray, GenericArray) = digest.split(); From 5490090cdd58dc0f440845a9f7c17116e45f7dcd Mon Sep 17 00:00:00 2001 From: derekpierre Date: Thu, 11 Sep 2025 16:24:46 -0400 Subject: [PATCH 4/4] Make linting and other updates based on pre-commit checks that were failing. --- nucypher-core-python/src/lib.rs | 101 +++++----- nucypher-core/src/address.rs | 14 +- nucypher-core/src/lib.rs | 12 +- nucypher-core/src/signature_request.rs | 258 +++++++++++++++---------- 4 files changed, 227 insertions(+), 158 deletions(-) diff --git a/nucypher-core-python/src/lib.rs b/nucypher-core-python/src/lib.rs index 1faf8e3e..fd8d27b9 100644 --- a/nucypher-core-python/src/lib.rs +++ b/nucypher-core-python/src/lib.rs @@ -10,7 +10,6 @@ use ferveo::bindings_python::{ Ciphertext, CiphertextHeader, DkgPublicKey, FerveoPublicKey, FerveoPythonError, FerveoVariant, SharedSecret, }; -use hex; use pyo3::class::basic::CompareOp; use pyo3::exceptions::{PyTypeError, PyValueError}; use pyo3::prelude::*; @@ -23,9 +22,9 @@ use umbral_pre::bindings_python::{ use nucypher_core as rust_nucypher_core; use rust_nucypher_core::{ - UserOperation as SignatureRequestUserOperation, PackedUserOperation as SignatureRequestPackedUserOperation, SignedPackedUserOperation as SignatureRequestSignedPackedUserOperation, + UserOperation as SignatureRequestUserOperation, }; use nucypher_core::ProtocolObject; @@ -1673,7 +1672,10 @@ impl EIP191SignatureRequest { #[getter] fn context(&self) -> Option { - self.backend.context.clone().map(|context| Context { backend: context }) + self.backend + .context + .clone() + .map(|context| Context { backend: context }) } #[getter] @@ -1706,6 +1708,7 @@ pub struct UserOperation { impl UserOperation { #[new] #[pyo3(signature = (sender, nonce, init_code=None, call_data=None, call_gas_limit=None, verification_gas_limit=None, pre_verification_gas=None, max_fee_per_gas=None, max_priority_fee_per_gas=None, paymaster=None, paymaster_verification_gas_limit=None, paymaster_post_op_gas_limit=None, paymaster_data=None))] + #[allow(clippy::too_many_arguments)] pub fn new( sender: String, nonce: u64, @@ -1723,7 +1726,10 @@ impl UserOperation { ) -> PyResult { // Convert hex string to Address let sender_address = string_to_address(&sender)?; - let paymaster_address = paymaster.as_ref().map(|p| string_to_address(p)).transpose()?; + let paymaster_address = paymaster + .as_ref() + .map(|p| string_to_address(p)) + .transpose()?; Ok(Self { backend: SignatureRequestUserOperation::new( @@ -1873,7 +1879,10 @@ impl UserOperationSignatureRequest { #[getter] fn context(&self) -> Option { - self.backend.context.clone().map(|context| Context { backend: context }) + self.backend + .context + .clone() + .map(|context| Context { backend: context }) } #[getter] @@ -1905,6 +1914,7 @@ pub struct PackedUserOperation { #[pymethods] impl PackedUserOperation { #[new] + #[allow(clippy::too_many_arguments)] pub fn new( sender: String, nonce: u64, @@ -1941,7 +1951,11 @@ impl PackedUserOperation { #[staticmethod] #[pyo3(name = "_pack_account_gas_limits")] - pub fn pack_account_gas_limits(py: Python, call_gas_limit: u128, verification_gas_limit: u128) -> PyObject { + pub fn pack_account_gas_limits( + py: Python, + call_gas_limit: u128, + verification_gas_limit: u128, + ) -> PyObject { let mut result = [0u8; 32]; // Pack as: verification_gas_limit << 128 | call_gas_limit // Each value is u128, so verification goes in upper 16 bytes, call in lower 16 bytes @@ -1952,7 +1966,11 @@ impl PackedUserOperation { #[staticmethod] #[pyo3(name = "_pack_gas_fees")] - pub fn pack_gas_fees(py: Python, max_fee_per_gas: u128, max_priority_fee_per_gas: u128) -> PyObject { + pub fn pack_gas_fees( + py: Python, + max_fee_per_gas: u128, + max_priority_fee_per_gas: u128, + ) -> PyObject { let mut result = [0u8; 32]; // Pack as: max_priority_fee_per_gas << 128 | max_fee_per_gas // Each value is u128, so priority goes in upper 16 bytes, max_fee in lower 16 bytes @@ -2042,9 +2060,7 @@ impl PackedUserOperation { let aa_version = str_to_aa_version(aa_version)?; let eip712_struct = self.backend.to_eip712_struct(&aa_version, chain_id); - Python::with_gil(|py| { - json_to_pyobject(py, &serde_json::Value::Object(eip712_struct)) - }) + Python::with_gil(|py| json_to_pyobject(py, &serde_json::Value::Object(eip712_struct))) } #[pyo3(name = "_to_eip712_message")] @@ -2052,9 +2068,7 @@ impl PackedUserOperation { let aa_version = str_to_aa_version(aa_version)?; let message = self.backend.to_eip712_message(&aa_version); - Python::with_gil(|py| { - json_to_pyobject(py, &serde_json::Value::Object(message)) - }) + Python::with_gil(|py| json_to_pyobject(py, &serde_json::Value::Object(message))) } #[pyo3(name = "_get_domain")] @@ -2062,9 +2076,7 @@ impl PackedUserOperation { let aa_version = str_to_aa_version(aa_version)?; let domain = self.backend.get_domain(&aa_version, chain_id); - Python::with_gil(|py| { - json_to_pyobject(py, &serde_json::Value::Object(domain)) - }) + Python::with_gil(|py| json_to_pyobject(py, &serde_json::Value::Object(domain))) } } @@ -2115,9 +2127,7 @@ impl SignedPackedUserOperation { let aa_version = str_to_aa_version(aa_version)?; let eip712_struct = self.backend.to_eip712_struct(&aa_version, chain_id); - Python::with_gil(|py| { - json_to_pyobject(py, &serde_json::Value::Object(eip712_struct)) - }) + Python::with_gil(|py| json_to_pyobject(py, &serde_json::Value::Object(eip712_struct))) } #[pyo3(name = "_to_eip712_message")] @@ -2125,9 +2135,7 @@ impl SignedPackedUserOperation { let aa_version = str_to_aa_version(aa_version)?; let message = self.backend.to_eip712_message(&aa_version); - Python::with_gil(|py| { - json_to_pyobject(py, &serde_json::Value::Object(message)) - }) + Python::with_gil(|py| json_to_pyobject(py, &serde_json::Value::Object(message))) } #[pyo3(name = "_get_domain")] @@ -2135,9 +2143,7 @@ impl SignedPackedUserOperation { let aa_version = str_to_aa_version(aa_version)?; let domain = self.backend.get_domain(&aa_version, chain_id); - Python::with_gil(|py| { - json_to_pyobject(py, &serde_json::Value::Object(domain)) - }) + Python::with_gil(|py| json_to_pyobject(py, &serde_json::Value::Object(domain))) } fn __bytes__(&self) -> PyObject { @@ -2204,7 +2210,10 @@ impl PackedUserOperationSignatureRequest { #[getter] fn context(&self) -> Option { - self.backend.context.clone().map(|context| Context { backend: context }) + self.backend + .context + .clone() + .map(|context| Context { backend: context }) } #[getter] @@ -2310,7 +2319,10 @@ fn _nucypher_core(py: Python, core_module: &PyModule) -> PyResult<()> { core_module.add_class::()?; core_module.add_class::()?; core_module.add_class::()?; - core_module.add_function(wrap_pyfunction!(deserialize_signature_request, core_module)?)?; + core_module.add_function(wrap_pyfunction!( + deserialize_signature_request, + core_module + )?)?; // Build the umbral module let umbral_module = PyModule::new(py, "umbral")?; @@ -2390,29 +2402,24 @@ fn json_to_pyobject(py: Python, value: &serde_json::Value) -> PyResult /// Utility function to deserialize any signature request from bytes - returns specific type directly #[pyfunction] pub fn deserialize_signature_request(data: &[u8]) -> PyResult { - let direct_request = nucypher_core::deserialize_signature_request(data) - .map_err(|err| PyValueError::new_err(format!("Failed to deserialize signature request: {}", err)))?; + let direct_request = nucypher_core::deserialize_signature_request(data).map_err(|err| { + PyValueError::new_err(format!("Failed to deserialize signature request: {}", err)) + })?; // Convert to the specific Python type match direct_request { - nucypher_core::DirectSignatureRequest::EIP191(req) => { - Python::with_gil(|py| { - let python_req = EIP191SignatureRequest { backend: req }; - Ok(python_req.into_py(py)) - }) - } - nucypher_core::DirectSignatureRequest::UserOp(req) => { - Python::with_gil(|py| { - let python_req = UserOperationSignatureRequest { backend: req }; - Ok(python_req.into_py(py)) - }) - } - nucypher_core::DirectSignatureRequest::PackedUserOp(req) => { - Python::with_gil(|py| { - let python_req = PackedUserOperationSignatureRequest { backend: req }; - Ok(python_req.into_py(py)) - }) - } + nucypher_core::DirectSignatureRequest::EIP191(req) => Python::with_gil(|py| { + let python_req = EIP191SignatureRequest { backend: req }; + Ok(python_req.into_py(py)) + }), + nucypher_core::DirectSignatureRequest::UserOp(req) => Python::with_gil(|py| { + let python_req = UserOperationSignatureRequest { backend: req }; + Ok(python_req.into_py(py)) + }), + nucypher_core::DirectSignatureRequest::PackedUserOp(req) => Python::with_gil(|py| { + let python_req = PackedUserOperationSignatureRequest { backend: req }; + Ok(python_req.into_py(py)) + }), } } diff --git a/nucypher-core/src/address.rs b/nucypher-core/src/address.rs index 347328ce..7f928214 100644 --- a/nucypher-core/src/address.rs +++ b/nucypher-core/src/address.rs @@ -42,10 +42,10 @@ impl Address { pub fn to_checksum_address(&self) -> String { let hex_address = hex::encode(self.0); let hash = Keccak256::digest(hex_address.as_bytes()); - + let mut result = String::with_capacity(42); result.push_str("0x"); - + for (i, ch) in hex_address.chars().enumerate() { if ch.is_alphabetic() { let hash_byte = hash[i / 2]; @@ -54,7 +54,7 @@ impl Address { } else { hash_byte & 0x0f }; - + if hash_nibble >= 8 { result.push(ch.to_ascii_uppercase()); } else { @@ -64,7 +64,7 @@ impl Address { result.push(ch); } } - + result } } @@ -86,18 +86,18 @@ mod tests { let mut array = [0u8; 20]; array.copy_from_slice(&address_bytes); let address = Address::new(&array); - + assert_eq!( address.to_checksum_address(), "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" ); - + // Test with all lowercase input let address_bytes2 = hex::decode("fb6916095ca1df60bb79ce92ce3ea74c37c5d359").unwrap(); let mut array2 = [0u8; 20]; array2.copy_from_slice(&address_bytes2); let address2 = Address::new(&array2); - + assert_eq!( address2.to_checksum_address(), "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359" diff --git a/nucypher-core/src/lib.rs b/nucypher-core/src/lib.rs index afe35166..0a7fd75f 100644 --- a/nucypher-core/src/lib.rs +++ b/nucypher-core/src/lib.rs @@ -20,9 +20,9 @@ mod reencryption; mod retrieval_kit; mod revocation_order; mod secret_box; +mod signature_request; mod test_utils; mod threshold_message_kit; -mod signature_request; mod treasure_map; mod versioning; @@ -48,13 +48,13 @@ pub use node_metadata::{ pub use reencryption::{ReencryptionRequest, ReencryptionResponse}; pub use retrieval_kit::RetrievalKit; pub use revocation_order::RevocationOrder; -pub use threshold_message_kit::ThresholdMessageKit; pub use signature_request::{ - AAVersion, BaseSignatureRequest, DirectSignatureRequest, EIP191SignatureRequest, - PackedUserOperation, PackedUserOperationSignatureRequest, SignatureRequestType, - SignatureResponse, SignedEIP191SignatureRequest, SignedPackedUserOperation, UserOperation, - UserOperationSignatureRequest, deserialize_signature_request, + deserialize_signature_request, AAVersion, BaseSignatureRequest, DirectSignatureRequest, + EIP191SignatureRequest, PackedUserOperation, PackedUserOperationSignatureRequest, + SignatureRequestType, SignatureResponse, SignedEIP191SignatureRequest, + SignedPackedUserOperation, UserOperation, UserOperationSignatureRequest, }; +pub use threshold_message_kit::ThresholdMessageKit; pub use treasure_map::{EncryptedTreasureMap, TreasureMap}; pub use versioning::ProtocolObject; diff --git a/nucypher-core/src/signature_request.rs b/nucypher-core/src/signature_request.rs index 1d73323c..563fbc05 100644 --- a/nucypher-core/src/signature_request.rs +++ b/nucypher-core/src/signature_request.rs @@ -4,7 +4,6 @@ use alloc::vec::Vec; use alloc::{format, vec}; use core::fmt; -use hex; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use umbral_pre::serde_bytes; @@ -192,6 +191,7 @@ pub struct UserOperation { impl UserOperation { /// Creates a new UserOperation + #[allow(clippy::too_many_arguments)] pub fn new( sender: Address, nonce: u64, @@ -308,6 +308,7 @@ pub struct PackedUserOperation { impl PackedUserOperation { /// Creates a new PackedUserOperation + #[allow(clippy::too_many_arguments)] pub fn new( sender: Address, nonce: u64, @@ -362,13 +363,13 @@ impl PackedUserOperation { Some(addr) => { let mut result = Vec::with_capacity(20 + 16 + 16 + paymaster_data.len()); result.extend_from_slice(addr.as_ref()); - + // Verification gas limit as 16 bytes big-endian (full u128) result.extend_from_slice(&paymaster_verification_gas_limit.to_be_bytes()); - + // Post-op gas limit as 16 bytes big-endian (full u128) result.extend_from_slice(&paymaster_post_op_gas_limit.to_be_bytes()); - + result.extend_from_slice(paymaster_data); result } @@ -377,23 +378,19 @@ impl PackedUserOperation { /// Creates a PackedUserOperation from a UserOperation pub fn from_user_operation(user_op: &UserOperation) -> Self { - let account_gas_limits = Self::pack_account_gas_limits( - user_op.call_gas_limit, - user_op.verification_gas_limit, - ); - - let gas_fees = Self::pack_gas_fees( - user_op.max_fee_per_gas, - user_op.max_priority_fee_per_gas, - ); - + let account_gas_limits = + Self::pack_account_gas_limits(user_op.call_gas_limit, user_op.verification_gas_limit); + + let gas_fees = + Self::pack_gas_fees(user_op.max_fee_per_gas, user_op.max_priority_fee_per_gas); + let paymaster_and_data = Self::pack_paymaster_and_data( user_op.paymaster.as_ref(), user_op.paymaster_verification_gas_limit, user_op.paymaster_post_op_gas_limit, &user_op.paymaster_data, ); - + Self { sender: user_op.sender, nonce: user_op.nonce, @@ -409,75 +406,115 @@ impl PackedUserOperation { /// Converts to EIP-712 message format pub fn to_eip712_message(&self, aa_version: &AAVersion) -> serde_json::Map { let mut message = serde_json::Map::new(); - message.insert("sender".into(), JsonValue::String(format!("0x{}", hex::encode(self.sender.as_ref())))); + message.insert( + "sender".into(), + JsonValue::String(format!("0x{}", hex::encode(self.sender.as_ref()))), + ); message.insert("nonce".into(), JsonValue::Number(self.nonce.into())); - message.insert("initCode".into(), JsonValue::String(format!("0x{}", hex::encode(&self.init_code)))); - message.insert("callData".into(), JsonValue::String(format!("0x{}", hex::encode(&self.call_data)))); - message.insert("accountGasLimits".into(), JsonValue::String(format!("0x{}", hex::encode(&self.account_gas_limits)))); - message.insert("preVerificationGas".into(), JsonValue::String(self.pre_verification_gas.to_string())); - message.insert("gasFees".into(), JsonValue::String(format!("0x{}", hex::encode(&self.gas_fees)))); - message.insert("paymasterAndData".into(), JsonValue::String(format!("0x{}", hex::encode(&self.paymaster_and_data)))); - + message.insert( + "initCode".into(), + JsonValue::String(format!("0x{}", hex::encode(&self.init_code))), + ); + message.insert( + "callData".into(), + JsonValue::String(format!("0x{}", hex::encode(&self.call_data))), + ); + message.insert( + "accountGasLimits".into(), + JsonValue::String(format!("0x{}", hex::encode(&self.account_gas_limits))), + ); + message.insert( + "preVerificationGas".into(), + JsonValue::String(self.pre_verification_gas.to_string()), + ); + message.insert( + "gasFees".into(), + JsonValue::String(format!("0x{}", hex::encode(&self.gas_fees))), + ); + message.insert( + "paymasterAndData".into(), + JsonValue::String(format!("0x{}", hex::encode(&self.paymaster_and_data))), + ); + if *aa_version == AAVersion::MDT { - message.insert("entryPoint".into(), JsonValue::String("0x0000000071727de22e5e9d8baf0edac6f37da032".into())); + message.insert( + "entryPoint".into(), + JsonValue::String("0x0000000071727de22e5e9d8baf0edac6f37da032".into()), + ); } - + message } /// Gets the EIP-712 domain - pub fn get_domain(&self, aa_version: &AAVersion, chain_id: u64) -> serde_json::Map { + pub fn get_domain( + &self, + aa_version: &AAVersion, + chain_id: u64, + ) -> serde_json::Map { let mut domain = serde_json::Map::new(); - - let name = if *aa_version != AAVersion::MDT { "ERC4337" } else { "MultiSigDeleGator" }; + + let name = if *aa_version != AAVersion::MDT { + "ERC4337" + } else { + "MultiSigDeleGator" + }; domain.insert("name".into(), JsonValue::String(name.into())); domain.insert("version".into(), JsonValue::String("1".into())); domain.insert("chainId".into(), JsonValue::Number(chain_id.into())); - + let verifying_contract = if *aa_version != AAVersion::MDT { "0x4337084d9e255ff0702461cf8895ce9e3b5ff108".into() } else { format!("0x{}", hex::encode(self.sender.as_ref())) }; - domain.insert("verifyingContract".into(), JsonValue::String(verifying_contract)); - + domain.insert( + "verifyingContract".into(), + JsonValue::String(verifying_contract), + ); + domain } /// Converts to EIP-712 struct format - pub fn to_eip712_struct(&self, aa_version: &AAVersion, chain_id: u64) -> serde_json::Map { + pub fn to_eip712_struct( + &self, + aa_version: &AAVersion, + chain_id: u64, + ) -> serde_json::Map { let mut result = serde_json::Map::new(); - + // Create types let mut types = serde_json::Map::new(); - + // EIP712Domain type let mut domain_type = Vec::new(); let mut name_field = serde_json::Map::new(); name_field.insert("name".into(), JsonValue::String("name".into())); name_field.insert("type".into(), JsonValue::String("string".into())); domain_type.push(JsonValue::Object(name_field)); - + let mut version_field = serde_json::Map::new(); version_field.insert("name".into(), JsonValue::String("version".into())); version_field.insert("type".into(), JsonValue::String("string".into())); domain_type.push(JsonValue::Object(version_field)); - + let mut chain_id_field = serde_json::Map::new(); chain_id_field.insert("name".into(), JsonValue::String("chainId".into())); chain_id_field.insert("type".into(), JsonValue::String("uint256".into())); domain_type.push(JsonValue::Object(chain_id_field)); - + let mut verifying_contract_field = serde_json::Map::new(); - verifying_contract_field.insert("name".into(), JsonValue::String("verifyingContract".into())); + verifying_contract_field + .insert("name".into(), JsonValue::String("verifyingContract".into())); verifying_contract_field.insert("type".into(), JsonValue::String("address".into())); domain_type.push(JsonValue::Object(verifying_contract_field)); - + types.insert("EIP712Domain".into(), JsonValue::Array(domain_type)); - + // PackedUserOperation type let mut packed_user_op_type = Vec::new(); - + let field_specs = vec![ ("sender", "address"), ("nonce", "uint256"), @@ -488,29 +525,41 @@ impl PackedUserOperation { ("gasFees", "bytes32"), ("paymasterAndData", "bytes"), ]; - + for (name, type_str) in field_specs { let mut field = serde_json::Map::new(); field.insert("name".into(), JsonValue::String(name.into())); field.insert("type".into(), JsonValue::String(type_str.into())); packed_user_op_type.push(JsonValue::Object(field)); } - + if *aa_version == AAVersion::MDT { let mut entry_point_field = serde_json::Map::new(); entry_point_field.insert("name".into(), JsonValue::String("entryPoint".into())); entry_point_field.insert("type".into(), JsonValue::String("address".into())); packed_user_op_type.push(JsonValue::Object(entry_point_field)); } - - types.insert("PackedUserOperation".into(), JsonValue::Array(packed_user_op_type)); - + + types.insert( + "PackedUserOperation".into(), + JsonValue::Array(packed_user_op_type), + ); + // Build final result result.insert("types".into(), JsonValue::Object(types)); - result.insert("primaryType".into(), JsonValue::String("PackedUserOperation".into())); - result.insert("domain".into(), JsonValue::Object(self.get_domain(aa_version, chain_id))); - result.insert("message".into(), JsonValue::Object(self.to_eip712_message(aa_version))); - + result.insert( + "primaryType".into(), + JsonValue::String("PackedUserOperation".into()), + ); + result.insert( + "domain".into(), + JsonValue::Object(self.get_domain(aa_version, chain_id)), + ); + result.insert( + "message".into(), + JsonValue::Object(self.to_eip712_message(aa_version)), + ); + result } } @@ -555,12 +604,20 @@ impl SignedPackedUserOperation { } /// Gets the EIP-712 domain (delegates to operation) - pub fn get_domain(&self, aa_version: &AAVersion, chain_id: u64) -> serde_json::Map { + pub fn get_domain( + &self, + aa_version: &AAVersion, + chain_id: u64, + ) -> serde_json::Map { self.operation.get_domain(aa_version, chain_id) } /// Converts to EIP-712 struct format (delegates to operation) - pub fn to_eip712_struct(&self, aa_version: &AAVersion, chain_id: u64) -> serde_json::Map { + pub fn to_eip712_struct( + &self, + aa_version: &AAVersion, + chain_id: u64, + ) -> serde_json::Map { self.operation.to_eip712_struct(aa_version, chain_id) } } @@ -860,21 +917,20 @@ impl DirectSignatureRequest { let brand = [bytes[0], bytes[1], bytes[2], bytes[3]]; match &brand { - b"E191" => { - EIP191SignatureRequest::from_bytes(bytes) - .map(Self::EIP191) - .map_err(|e| format!("Failed to deserialize EIP191SignatureRequest: {}", e)) - } - b"UOSR" => { - UserOperationSignatureRequest::from_bytes(bytes) - .map(Self::UserOp) - .map_err(|e| format!("Failed to deserialize UserOperationSignatureRequest: {}", e)) - } - b"PUOS" => { - PackedUserOperationSignatureRequest::from_bytes(bytes) - .map(Self::PackedUserOp) - .map_err(|e| format!("Failed to deserialize PackedUserOperationSignatureRequest: {}", e)) - } + b"E191" => EIP191SignatureRequest::from_bytes(bytes) + .map(Self::EIP191) + .map_err(|e| format!("Failed to deserialize EIP191SignatureRequest: {}", e)), + b"UOSR" => UserOperationSignatureRequest::from_bytes(bytes) + .map(Self::UserOp) + .map_err(|e| format!("Failed to deserialize UserOperationSignatureRequest: {}", e)), + b"PUOS" => PackedUserOperationSignatureRequest::from_bytes(bytes) + .map(Self::PackedUserOp) + .map_err(|e| { + format!( + "Failed to deserialize PackedUserOperationSignatureRequest: {}", + e + ) + }), _ => Err(format!("Unknown signature request brand: {:?}", brand)), } } @@ -941,10 +997,10 @@ mod tests { // Test with a large chain ID like the example provided (131277322940537) let large_chain_id = 131277322940537u64; let request = EIP191SignatureRequest::new(data, 1, large_chain_id, None); - + let bytes = request.to_bytes(); let deserialized = EIP191SignatureRequest::from_bytes(&bytes).unwrap(); - + assert_eq!(request, deserialized); assert_eq!(deserialized.data.as_ref(), data); assert_eq!(deserialized.cohort_id, 1); @@ -958,22 +1014,22 @@ mod tests { let context = Some(Context::new("test_context")); let request = EIP191SignatureRequest::new(data, 456, 1, context); let test_signature = b"test_eip191_signature"; - + // Test creating SignedEIP191SignatureRequest let signed_request = SignedEIP191SignatureRequest::new(request.clone(), test_signature); - + assert_eq!(signed_request.signature(), test_signature); assert_eq!(signed_request.request().data.as_ref(), data); assert_eq!(signed_request.request().cohort_id, 456); assert_eq!(signed_request.request().chain_id, 1); - + // Test into_parts method let (reconstructed_request, reconstructed_signature) = signed_request.clone().into_parts(); assert_eq!(reconstructed_signature.as_ref(), test_signature); assert_eq!(reconstructed_request.data.as_ref(), data); assert_eq!(reconstructed_request.cohort_id, 456); assert_eq!(reconstructed_request.chain_id, 1); - + // Test serialization let bytes = signed_request.to_bytes(); let deserialized = SignedEIP191SignatureRequest::from_bytes(&bytes).unwrap(); @@ -988,7 +1044,7 @@ mod tests { fn test_user_operation_signature_request_serialization() { let sender = address_from_hex("1234567890123456789012345678901234567890"); let paymaster = Some(address_from_hex("abcdefabcdefabcdefabcdefabcdefabcdefabcd")); - + let user_op = UserOperation::new( sender, 42, @@ -1011,15 +1067,18 @@ mod tests { AAVersion::V08, Some(Context::new("test_context")), ); - + let bytes = request.to_bytes(); let deserialized = UserOperationSignatureRequest::from_bytes(&bytes).unwrap(); - + assert_eq!(request, deserialized); assert_eq!(deserialized.user_op.sender, sender); assert_eq!(deserialized.user_op.nonce, 42); assert_eq!(deserialized.aa_version, AAVersion::V08); - assert_eq!(deserialized.context.as_ref().unwrap().as_ref(), "test_context"); + assert_eq!( + deserialized.context.as_ref().unwrap().as_ref(), + "test_context" + ); } #[test] @@ -1027,10 +1086,10 @@ mod tests { let hash = b"test_hash"; let signature = b"test_signature"; let response = SignatureResponse::new(hash, signature, SignatureRequestType::UserOp); - + let bytes = response.to_bytes(); let deserialized = SignatureResponse::from_bytes(&bytes).unwrap(); - + assert_eq!(response, deserialized); assert_eq!(deserialized.hash.as_ref(), hash); assert_eq!(deserialized.signature.as_ref(), signature); @@ -1063,11 +1122,11 @@ mod tests { AAVersion::V08, Some(Context::new("test_context")), ); - + let bytes = request_v08.to_bytes(); let deserialized_v08 = UserOperationSignatureRequest::from_bytes(&bytes).unwrap(); assert_eq!(deserialized_v08.aa_version, AAVersion::V08); - + // Test MDT let sender_mdt = address_from_hex("abcdef0123456789abcdef0123456789abcdef01"); let user_op_mdt = UserOperation::new( @@ -1092,11 +1151,11 @@ mod tests { AAVersion::MDT, Some(Context::new("test_context")), ); - + let bytes_mdt = request_mdt.to_bytes(); let deserialized_mdt = UserOperationSignatureRequest::from_bytes(&bytes_mdt).unwrap(); assert_eq!(deserialized_mdt.aa_version, AAVersion::MDT); - + // Test Display trait assert_eq!(AAVersion::V08.to_string(), "0.8.0"); assert_eq!(AAVersion::MDT.to_string(), "mdt"); @@ -1106,7 +1165,7 @@ mod tests { fn test_packed_user_operation_conversion() { let sender = address_from_hex("1234567890123456789012345678901234567890"); let paymaster = Some(address_from_hex("abcdefabcdefabcdefabcdefabcdefabcdefabcd")); - + let user_op = UserOperation::new( sender, 100, @@ -1122,30 +1181,33 @@ mod tests { Some(200000), Some(b"paymaster_specific_data"), ); - + let packed = PackedUserOperation::from_user_operation(&user_op); - + assert_eq!(packed.sender, user_op.sender); assert_eq!(packed.nonce, user_op.nonce); assert_eq!(packed.init_code, user_op.init_code); assert_eq!(packed.call_data, user_op.call_data); assert_eq!(packed.pre_verification_gas, user_op.pre_verification_gas); - + // Check account gas limits packing assert_eq!(packed.account_gas_limits.len(), 32); - + // Check gas fees packing assert_eq!(packed.gas_fees.len(), 32); - + // Check paymaster data packing (20 bytes address + 16 bytes + 16 bytes + data) - assert_eq!(packed.paymaster_and_data.len(), 20 + 16 + 16 + b"paymaster_specific_data".len()); + assert_eq!( + packed.paymaster_and_data.len(), + 20 + 16 + 16 + b"paymaster_specific_data".len() + ); } #[test] fn test_signed_packed_user_operation() { let sender = address_from_hex("1234567890123456789012345678901234567890"); let paymaster = Some(address_from_hex("abcdefabcdefabcdefabcdefabcdefabcdefabcd")); - + let user_op = UserOperation::new( sender, 123, @@ -1161,33 +1223,33 @@ mod tests { Some(250000), Some(b"paymaster_test_data"), ); - + let packed = PackedUserOperation::from_user_operation(&user_op); let test_signature = b"test_signature"; - + // Test creating SignedPackedUserOperation let signed_packed = SignedPackedUserOperation::new(packed.clone(), test_signature); - + assert_eq!(signed_packed.signature(), test_signature); assert_eq!(signed_packed.operation().sender, sender); assert_eq!(signed_packed.operation().nonce, 123); - + // Test into_parts method let (reconstructed_operation, reconstructed_signature) = signed_packed.clone().into_parts(); assert_eq!(reconstructed_signature.as_ref(), test_signature); assert_eq!(reconstructed_operation.sender, sender); assert_eq!(reconstructed_operation.nonce, 123); - + // Test serialization let bytes = signed_packed.to_bytes(); let deserialized = SignedPackedUserOperation::from_bytes(&bytes).unwrap(); assert_eq!(signed_packed, deserialized); assert_eq!(deserialized.signature(), test_signature); assert_eq!(deserialized.operation().sender, sender); - + // Test EIP-712 methods delegate correctly let eip712_message = signed_packed.to_eip712_message(&AAVersion::V08); let operation_message = signed_packed.operation().to_eip712_message(&AAVersion::V08); assert_eq!(eip712_message, operation_message); } -} \ No newline at end of file +}