From ac6132ea5156f051c7568d663153cfb55a96742b Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Tue, 12 Sep 2023 18:15:03 +0200 Subject: [PATCH] Add `UnsignedBody` and `UnsignedExternalMessage` --- src/abi/contract.rs | 269 +++++++++++++++++++++++++++++++++++++---- src/abi/mod.rs | 3 + src/abi/signature.rs | 26 ++++ src/abi/ty.rs | 7 ++ src/abi/value/ser.rs | 28 ----- src/models/currency.rs | 10 ++ 6 files changed, 294 insertions(+), 49 deletions(-) create mode 100644 src/abi/signature.rs diff --git a/src/abi/contract.rs b/src/abi/contract.rs index 89a40353..bc654eef 100644 --- a/src/abi/contract.rs +++ b/src/abi/contract.rs @@ -5,30 +5,57 @@ use sha2::Digest; use crate::abi::value::ser::AbiSerializer; use crate::abi::AbiHeader; use crate::cell::{Cell, CellBuilder, CellSlice, DefaultFinalizer, Store}; -use crate::models::{IntAddr, StdAddr}; +use crate::models::{ + ExtInMsgInfo, IntAddr, MsgInfo, OwnedMessage, OwnedRelaxedMessage, RelaxedIntMsgInfo, + RelaxedMsgInfo, StateInit, StdAddr, +}; use crate::num::Tokens; -use crate::prelude::HashBytes; +use crate::prelude::{CellFamily, CellSliceRange, HashBytes}; use super::error::AbiError; use super::{AbiHeaderType, AbiValue, AbiVersion, NamedAbiType, NamedAbiValue, ShortAbiTypeSize}; +/// Contract ABI definition. pub struct Contract { + /// ABI version. pub abi_version: AbiVersion, + + /// List of headers for external messages. + /// + /// NOTE: header order matters. pub header: Vec, + + /// A mapping with all contract methods by name. pub functions: HashMap, } +/// Contract method ABI definition. pub struct Function { + /// ABI version (same as contract ABI version). pub abi_version: AbiVersion, + + /// List of headers for external messages. + /// Must be the same as in contract. + /// + /// NOTE: header order matters. pub header: Vec, + + /// Method name. pub name: String, + + /// Method input argument types. pub inputs: Vec, + /// Method output types. pub outputs: Vec, + + /// Function id in incoming messages (with input). pub input_id: u32, + /// Function id in outgoing messages (with output). pub output_id: u32, } impl Function { + /// Computes function id from the full function signature. pub fn compute_function_id( abi_version: AbiVersion, name: &str, @@ -50,6 +77,7 @@ impl Function { u32::from_be_bytes(hash[0..4].try_into().unwrap()) } + /// Encodes message body without headers. pub fn encode_internal_msg_body( version: AbiVersion, id: u32, @@ -69,21 +97,22 @@ impl Function { serializer.finalize(finalizer).map_err(From::from) } + /// Encodes an unsigned body with invocation of this method with as an external message. pub fn encode_external_input( &self, tokens: &[NamedAbiValue], now: u64, expire_at: u32, - key: Option<(&ed25519_dalek::SecretKey, Option)>, + pubkey: Option<&ed25519_dalek::VerifyingKey>, address: Option<&StdAddr>, - ) -> Result<(CellBuilder, HashBytes)> { + ) -> Result { ok!(NamedAbiValue::check_types(tokens, &self.inputs)); let mut serializer = AbiSerializer::new(self.abi_version); serializer.add_offset(if self.abi_version.major == 1 { // Reserve reference for signature ShortAbiTypeSize { bits: 0, refs: 1 } - } else if key.is_some() { + } else if pubkey.is_some() { let bits = if self.abi_version >= AbiVersion::V2_3 { // Reserve only for address as it also ensures the the signature will fit IntAddr::BITS_MAX @@ -97,11 +126,7 @@ impl Function { ShortAbiTypeSize { bits: 1, refs: 0 } }); - let signing_key = key.map(|(key, signature_id)| { - (Box::new(ed25519_dalek::SigningKey::from(key)), signature_id) - }); - - serializer.reserve_headers(&self.header, key.is_some()); + serializer.reserve_headers(&self.header, pubkey.is_some()); for token in tokens { serializer.reserve_value(&token.value); } @@ -110,50 +135,90 @@ impl Function { serializer.write_header_value(&match header { AbiHeaderType::Time => AbiHeader::Time(now), AbiHeaderType::Expire => AbiHeader::Expire(expire_at), - AbiHeaderType::PublicKey => AbiHeader::PublicKey( - signing_key - .as_ref() - .map(|(key, _)| Box::new(ed25519_dalek::VerifyingKey::from(key.as_ref()))), - ), + AbiHeaderType::PublicKey => AbiHeader::PublicKey(pubkey.map(|key| Box::new(*key))), })?; } let finalizer = &mut Cell::default_finalizer(); - serializer.write_tuple(&tokens, finalizer)?; + serializer.write_tuple(tokens, finalizer)?; let builder = serializer.finalize(finalizer)?; - let hash = if self.abi_version >= AbiVersion::V2_3 { + let (hash, payload) = if self.abi_version >= AbiVersion::V2_3 { let mut to_sign = CellBuilder::new(); match address { Some(address) => address.store_into(&mut to_sign, finalizer)?, None => anyhow::bail!(AbiError::AddressNotProvided), }; to_sign.store_builder(&builder)?; - *to_sign.build_ext(finalizer)?.repr_hash() + let hash = *to_sign.build_ext(finalizer)?.repr_hash(); + (hash, builder.build()?) } else { - *builder.clone().build_ext(finalizer)?.repr_hash() + let payload = builder.clone().build_ext(finalizer)?; + (*payload.repr_hash(), payload) }; - Ok((builder, hash)) + Ok(UnsignedBody { + abi_version: self.abi_version, + expire_at, + payload, + hash, + }) + } + + /// Encodes an unsigned external message with invocation of this method. + pub fn encode_external_message( + &self, + tokens: &[NamedAbiValue], + now: u64, + expire_at: u32, + pubkey: Option<&ed25519_dalek::VerifyingKey>, + address: &StdAddr, + ) -> Result { + Ok(self + .encode_external_input(tokens, now, expire_at, pubkey, Some(address))? + .with_dst(address.clone())) } + /// Encodes a message body with invocation of this method with as an internal message. pub fn encode_internal_input(&self, tokens: &[NamedAbiValue]) -> Result { ok!(NamedAbiValue::check_types(tokens, &self.inputs)); Self::encode_internal_msg_body(self.abi_version, self.input_id, tokens) } + /// Encodes an internal message with invocation of this method. pub fn encode_internal_message( &self, tokens: &[NamedAbiValue], dst: IntAddr, value: Tokens, - ) -> Result { + bounce: bool, + state_init: Option<&StateInit>, + ) -> Result> { + let body = self.encode_internal_input(tokens)?; + let cell = body.build()?; + let range = CellSliceRange::full(cell.as_ref()); + + Ok(Box::new(OwnedRelaxedMessage { + info: RelaxedMsgInfo::Int(RelaxedIntMsgInfo { + dst, + bounce, + value: value.into(), + ..Default::default() + }), + body: (cell, range), + init: state_init.cloned(), + layout: None, + })) } + /// Tries to parse input arguments for this method from an internal message body. + /// + /// NOTE: The slice is required to contain nothing other than these arguments. pub fn decode_internal_input(&self, mut slice: CellSlice<'_>) -> Result> { self.decode_internal_input_ext(&mut slice, false) } + /// Tries to parse input arguments for this method from an internal message body. pub fn decode_internal_input_ext( &self, slice: &mut CellSlice<'_>, @@ -178,15 +243,20 @@ impl Function { Ok(res) } + /// Encodes a message body with result of invocation of this method. pub fn encode_output(&self, tokens: &[NamedAbiValue]) -> Result { ok!(NamedAbiValue::check_types(tokens, &self.outputs)); Self::encode_internal_msg_body(self.abi_version, self.output_id, tokens) } + /// Tries to parse output arguments of this method from a message body. + /// + /// NOTE: The slice is required to contain nothing other than these arguments. pub fn decode_output(&self, mut slice: CellSlice<'_>) -> Result> { self.decode_output_ext(&mut slice, false) } + /// Tries to parse output arguments of this method from a message body. pub fn decode_output_ext( &self, slice: &mut CellSlice<'_>, @@ -211,6 +281,7 @@ impl Function { Ok(res) } + /// Returns an object which can be used to display the normalized signature of this method. pub fn display_signature(&self) -> impl std::fmt::Display + '_ { FunctionSignatureRaw { abi_version: self.abi_version, @@ -222,6 +293,162 @@ impl Function { } } +/// Unsigned external message. +pub struct UnsignedExternalMessage { + /// Destination contract address. + pub dst: StdAddr, + /// Unsigned payload. + pub body: UnsignedBody, + /// Optional initial contract state. + pub init: Option, +} + +impl UnsignedExternalMessage { + /// Updates the state init of the external message to the specified one. + pub fn set_state_init(&mut self, init: Option) { + self.init = init; + } + + /// Returns an external message with the specified state init. + pub fn with_state_init(mut self, init: StateInit) -> Self { + self.set_state_init(Some(init)); + self + } + + /// Returns the expiration timestamp of this message. + #[inline] + pub fn expire_at(&self) -> u32 { + self.body.expire_at + } + + /// Signs the payload and returns an external message with filled signature. + pub fn sign( + self, + key: &ed25519_dalek::SigningKey, + signature_id: Option, + ) -> Result { + let signature = + crate::abi::sign_with_signature_id(key, self.body.hash.as_slice(), signature_id); + self.with_signature(&signature) + } + + /// Returns an external message with filled signature. + pub fn with_signature(self, signature: &ed25519_dalek::Signature) -> Result { + self.into_signed(Some(&signature.to_bytes())) + } + + /// Returns an external message with signature filled with zero bytes. + pub fn with_fake_signature(self) -> Result { + self.into_signed(Some(&[0u8; 64])) + } + + /// Returns an external message with empty signature. + pub fn without_signature(self) -> Result { + self.into_signed(None) + } + + /// Returns an external message with filled signature. + pub fn fill_signature(&self, signature: Option<&[u8; 64]>) -> Result { + let body = self.body.fill_signature(signature)?; + let range = CellSliceRange::full(body.as_ref()); + Ok(OwnedMessage { + info: MsgInfo::ExtIn(ExtInMsgInfo { + dst: IntAddr::Std(self.dst.clone()), + ..Default::default() + }), + body: (body, range), + init: self.init.clone(), + layout: None, + }) + } + + /// Converts an unsigned message into an external message with filled signature. + fn into_signed(self, signature: Option<&[u8; 64]>) -> Result { + let body = self.body.fill_signature(signature)?; + let range = CellSliceRange::full(body.as_ref()); + Ok(OwnedMessage { + info: MsgInfo::ExtIn(ExtInMsgInfo { + dst: IntAddr::Std(self.dst), + ..Default::default() + }), + body: (body, range), + init: self.init, + layout: None, + }) + } +} + +/// Unsigned external message payload. +pub struct UnsignedBody { + /// ABI version used during encoding. + pub abi_version: AbiVersion, + /// Unsigned payload. + pub payload: Cell, + /// A hash to sign. + pub hash: HashBytes, + /// Message expiration timestamp (in seconds). + pub expire_at: u32, +} + +impl UnsignedBody { + /// Extends message with the specified destination address and returns an + /// unsigned external message. + pub fn with_dst(self, dst: StdAddr) -> UnsignedExternalMessage { + UnsignedExternalMessage { + dst, + body: self, + init: None, + } + } + + /// Signs the payload and returns a body cell with filled signature. + pub fn sign(self, key: &ed25519_dalek::SigningKey, signature_id: Option) -> Result { + let signature = crate::abi::sign_with_signature_id(key, self.hash.as_slice(), signature_id); + self.with_signature(&signature) + } + + /// Returns a body cell with filled signature. + pub fn with_signature(self, signature: &ed25519_dalek::Signature) -> Result { + self.fill_signature(Some(&signature.to_bytes())) + } + + /// Returns a body cell with signature filled with zero bytes. + pub fn with_fake_signature(self) -> Result { + self.fill_signature(Some(&[0u8; 64])) + } + + /// Returns a body cell with empty signature. + pub fn without_signature(self) -> Result { + self.fill_signature(None) + } + + /// Returns a body cell with filled signature. + pub fn fill_signature(&self, signature: Option<&[u8; 64]>) -> Result { + let mut builder = CellBuilder::new(); + + if self.abi_version.major == 1 { + builder.store_reference(match signature { + Some(signature) => { + // TODO: extend with public key? + CellBuilder::from_raw_data(signature, 512).and_then(CellBuilder::build)? + } + None => Cell::empty_cell(), + })?; + } else { + match signature { + Some(signature) => { + builder.store_bit_one()?; + builder.store_raw(signature, 512)?; + } + None => builder.store_bit_zero()?, + } + builder.store_slice(self.payload.as_slice()?)?; + } + + builder.build().map_err(From::from) + } +} + struct FunctionSignatureRaw<'a> { abi_version: AbiVersion, name: &'a str, diff --git a/src/abi/mod.rs b/src/abi/mod.rs index 6646cf5a..79a550c5 100644 --- a/src/abi/mod.rs +++ b/src/abi/mod.rs @@ -2,6 +2,8 @@ use std::str::FromStr; +pub use self::contract::{Contract, Function, UnsignedBody, UnsignedExternalMessage}; +pub use self::signature::{extend_signature_with_id, sign_with_signature_id}; pub use self::traits::{IntoAbi, IntoPlainAbi, WithAbiType, WithPlainAbiType}; pub use self::ty::{ AbiHeaderType, AbiType, FullAbiTypeSize, NamedAbiType, PlainAbiType, ShortAbiTypeSize, @@ -11,6 +13,7 @@ pub use self::value::{AbiHeader, AbiValue, NamedAbiValue, PlainAbiValue}; pub mod error; mod contract; +mod signature; mod traits; mod ty; mod value; diff --git a/src/abi/signature.rs b/src/abi/signature.rs new file mode 100644 index 00000000..ad59fba7 --- /dev/null +++ b/src/abi/signature.rs @@ -0,0 +1,26 @@ +use std::borrow::Cow; + +use ed25519_dalek::Signer; + +/// Signs arbitrary data using the key and optional signature id. +pub fn sign_with_signature_id( + key: &ed25519_dalek::SigningKey, + data: &[u8], + signature_id: Option, +) -> ed25519_dalek::Signature { + let data = extend_signature_with_id(data, signature_id); + key.sign(&data) +} + +/// Prepares arbitrary data for signing. +pub fn extend_signature_with_id(data: &[u8], signature_id: Option) -> Cow<'_, [u8]> { + match signature_id { + Some(signature_id) => { + let mut result = Vec::with_capacity(4 + data.len()); + result.extend_from_slice(&signature_id.to_be_bytes()); + result.extend_from_slice(data); + Cow::Owned(result) + } + None => Cow::Borrowed(data), + } +} diff --git a/src/abi/ty.rs b/src/abi/ty.rs index 8616934b..06c91167 100644 --- a/src/abi/ty.rs +++ b/src/abi/ty.rs @@ -613,9 +613,12 @@ impl std::fmt::Display for PlainAbiType { } } +/// Short type stats. #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] pub struct ShortAbiTypeSize { + /// Total number of bits that the value of this type can occupy. pub bits: u16, + /// Total number refs that the value of this type can occupy. pub refs: u8, } @@ -686,9 +689,12 @@ impl std::ops::SubAssign for ShortAbiTypeSize { } } +/// Extended type stats. #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] pub struct FullAbiTypeSize { + /// Total number of bits that the value of this type can occupy across multiple cells. pub bits: usize, + /// Total number refs that the value of this type can occupy across multiple cells. pub refs: usize, } @@ -702,6 +708,7 @@ impl FullAbiTypeSize { self.bits <= MAX_BIT_LEN as _ && self.refs <= MAX_REF_COUNT } + /// Tries to narrow the stats to fit into one cell. #[inline] pub const fn try_into_cell_size(self) -> Option<(u16, u8)> { if self.fits_into_cell() { diff --git a/src/abi/value/ser.rs b/src/abi/value/ser.rs index c5c61263..42f3c538 100644 --- a/src/abi/value/ser.rs +++ b/src/abi/value/ser.rs @@ -217,34 +217,6 @@ impl AbiValue { } } -impl AbiHeader { - fn compute_size(&self, version: AbiVersion) -> ShortAbiTypeSize { - if version.use_max_size() { - self.compute_max_size() - } else { - self.compute_exact_size() - } - } - - fn compute_exact_size(&self) -> ShortAbiTypeSize { - let bits = match self { - Self::Time(_) => 64, - Self::Expire(_) => 32, - Self::PublicKey(value) => 1 + if value.is_some() { 256 } else { 0 }, - }; - ShortAbiTypeSize { bits, refs: 0 } - } - - fn compute_max_size(&self) -> ShortAbiTypeSize { - let bits = match self { - Self::Time(_) => 64, - Self::Expire(_) => 32, - Self::PublicKey(_) => 1 + 256, - }; - ShortAbiTypeSize { bits, refs: 0 } - } -} - pub(crate) struct AbiSerializer { version: AbiVersion, current: ShortAbiTypeSize, diff --git a/src/models/currency.rs b/src/models/currency.rs index 50c68acd..4fa82e43 100644 --- a/src/models/currency.rs +++ b/src/models/currency.rs @@ -43,6 +43,16 @@ impl CurrencyCollection { } } +impl From for CurrencyCollection { + #[inline] + fn from(tokens: Tokens) -> Self { + Self { + tokens, + other: ExtraCurrencyCollection::new(), + } + } +} + impl<'a> AugDictSkipValue<'a> for CurrencyCollection { #[inline] fn skip_value(slice: &mut CellSlice<'a>) -> bool {