diff --git a/Cargo.toml b/Cargo.toml index 7e916fca..dce48f79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,3 @@ members = [ "tests/e2e", "spaces-cli", ] - -[patch.crates-io] -# TODO: replace -eip-712 = {git="https://github.com/darioush/EIP-712.git"} \ No newline at end of file diff --git a/spaces-cli/Cargo.toml b/spaces-cli/Cargo.toml index 53fd1d8b..01b9d5ff 100644 --- a/spaces-cli/Cargo.toml +++ b/spaces-cli/Cargo.toml @@ -11,6 +11,7 @@ homepage = "https://avax.network" [dependencies] avalanche-types = { version = "0.0.138", features = ["subnet"] } clap = { version = "4.0", features = ["derive"] } +ethers-core = { version = "1.0.0", default-features = false, features = ["eip712", "macros"] } hex = "0.4.3" jsonrpc-core = "18.0.0" jsonrpc-core-client = { version = "18.0.0" } diff --git a/spaces-cli/src/main.rs b/spaces-cli/src/main.rs index 80615022..1d7a14ef 100644 --- a/spaces-cli/src/main.rs +++ b/spaces-cli/src/main.rs @@ -7,6 +7,7 @@ use std::{ use avalanche_types::key; use clap::{Parser, Subcommand}; +use ethers_core::types::transaction::eip712::Eip712; use jsonrpc_client_transports::{transports, RpcError}; use jsonrpc_core::futures; use spacesvm::{ @@ -14,7 +15,7 @@ use spacesvm::{ DecodeTxArgs, IssueTxArgs, IssueTxResponse, PingResponse, ResolveArgs, ResolveResponse, ServiceClient as Client, }, - chain::tx::{decoder, tx::TransactionType, unsigned::TransactionData}, + chain::tx::{tx::TransactionType, unsigned::TransactionData}, }; #[derive(Parser)] @@ -167,8 +168,8 @@ async fn sign_and_submit( let typed_data = &resp.typed_data; - let dh = decoder::hash_structured_data(typed_data)?; - let sig = pk.sign_digest(&dh.as_bytes())?; + let dh = typed_data.struct_hash().unwrap(); + let sig = pk.sign_digest(&dh)?; client .issue_tx(IssueTxArgs { diff --git a/spacesvm/Cargo.toml b/spacesvm/Cargo.toml index 063a4a57..4da93dfb 100644 --- a/spacesvm/Cargo.toml +++ b/spacesvm/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "spacesvm" -version = "0.0.1" +version = "0.0.0" edition = "2021" rust-version = "1.65" -publish = true -description = "Authenticated, hierarchical key-value store w/EIP-712 compatibility" +publish = false +description = "Authenticated hierarchical key-value store w/EIP-712 compatibility" license = "BSD-3-Clause" homepage = "https://avax.network" repository = "https://github.com/ava-labs/spacesvm-rs" -readme = "README.md" +readme = "../README.md" [[bin]] name = "spacesvm" @@ -18,12 +18,11 @@ path = "src/bin/spaces/main.rs" avalanche-types = { version = "0.0.138", features = ["subnet"] } byteorder = "1.4.3" chrono = "0.4.22" +clap = { version = "4.0.23", features = ["cargo", "derive"] } crossbeam-channel = "0.5.6" +ethers-core = { version = "1.0.0", default-features = false, features = ["eip712", "macros"] } derivative = "2.2.0" dyn-clone = "1.0.9" -ethereum-types = { version = "0.14.0" } -clap = { version = "4.0.22", features = ["cargo", "derive"] } -eip-712 = "0.1.0" env_logger = "0.9.3" hex = "0.4.3" http = "0.2.8" diff --git a/spacesvm/src/api/mod.rs b/spacesvm/src/api/mod.rs index 506f3ba0..92f2158d 100644 --- a/spacesvm/src/api/mod.rs +++ b/spacesvm/src/api/mod.rs @@ -1,13 +1,13 @@ pub mod service; use avalanche_types::ids; +use ethers_core::types::transaction::eip712::TypedData; use jsonrpc_core::{BoxFuture, Error, ErrorCode, Result}; use jsonrpc_derive::rpc; use serde::{Deserialize, Serialize}; use crate::chain::{ storage::ValueMeta, - tx::decoder::TypedData, tx::{self}, }; diff --git a/spacesvm/src/api/service.rs b/spacesvm/src/api/service.rs index 3aab0038..c9bf4b2e 100644 --- a/spacesvm/src/api/service.rs +++ b/spacesvm/src/api/service.rs @@ -2,7 +2,10 @@ use std::sync::Arc; use crate::{ api::*, - chain::{self, storage, tx::Transaction}, + chain::{ + self, storage, + tx::{decoder::parse_typed_data, Transaction}, + }, vm::inner::Inner, }; @@ -34,10 +37,7 @@ impl crate::api::Service for Service { Box::pin(async move { let mut inner = vm.write().await; - let unsigned_tx = params - .typed_data - .parse_typed_data() - .map_err(create_jsonrpc_error)?; + let unsigned_tx = parse_typed_data(¶ms.typed_data).map_err(create_jsonrpc_error)?; let mut tx = chain::tx::tx::Transaction::new(unsigned_tx, params.signature); tx.init().await.map_err(create_jsonrpc_error)?; @@ -82,7 +82,7 @@ impl crate::api::Service for Service { .map_err(create_jsonrpc_error)?; utx.set_block_id(*last_accepted).await; - let typed_data = utx.typed_data().await; + let typed_data = utx.typed_data().await.map_err(create_jsonrpc_error)?; let string = serde_json::to_string(&typed_data).unwrap(); diff --git a/spacesvm/src/chain/storage.rs b/spacesvm/src/chain/storage.rs index 46a78986..6a50b698 100644 --- a/spacesvm/src/chain/storage.rs +++ b/spacesvm/src/chain/storage.rs @@ -348,7 +348,7 @@ async fn test_raw_space() { #[tokio::test] async fn test_space_info_rt() { use super::tx::claim::Info; - use ethereum_types::H160; + use ethers_core::types::H160; env_logger::init_from_env( env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "debug"), diff --git a/spacesvm/src/chain/tx/claim.rs b/spacesvm/src/chain/tx/claim.rs index 0c11c5f5..9c564058 100644 --- a/spacesvm/src/chain/tx/claim.rs +++ b/spacesvm/src/chain/tx/claim.rs @@ -1,14 +1,16 @@ -use std::{ - collections::HashMap, - io::{Error, ErrorKind, Result}, -}; +use std::io::{Error, ErrorKind, Result}; use avalanche_types::ids; +use ethers_core::types::{ + transaction::eip712::{Eip712DomainType as Type, TypedData}, + H160, +}; + use serde::{Deserialize, Serialize}; use crate::chain::{ storage::{has_space, put_space_info}, - tx::decoder::{create_typed_data, MessageValue, Type, TypedData}, + tx::decoder::{create_typed_data, TypedDataMessage}, }; use super::{ @@ -26,7 +28,7 @@ pub struct Info { #[serde(deserialize_with = "ids::short::must_deserialize_id")] pub raw_space: ids::short::Id, - pub owner: ethereum_types::H160, + pub owner: H160, } /// Creates a space, which acts as a logical key-space root. @@ -86,31 +88,31 @@ impl unsigned::Transaction for Tx { return put_space_info(&mut db, self.space.as_bytes(), new_info, 0).await; } - async fn typed_data(&self) -> TypedData { + async fn typed_data(&self) -> Result { let mut tx_fields: Vec = Vec::new(); tx_fields.push(Type { name: TD_SPACE.to_owned(), - type_: TD_STRING.to_owned(), + r#type: TD_STRING.to_owned(), }); tx_fields.push(Type { name: TD_BLOCK_ID.to_owned(), - type_: TD_STRING.to_owned(), + r#type: TD_STRING.to_owned(), }); - let mut message: HashMap = HashMap::with_capacity(1); + let mut message = TypedDataMessage::new(); message.insert( TD_SPACE.to_owned(), - MessageValue::Vec(self.space.as_bytes().to_vec()), + serde_json::Value::String(self.space.clone()), ); - let value = MessageValue::Vec(self.base_tx.block_id.to_vec()); - log::debug!("typed_data: message value: {:?}", value); - log::debug!("typed_data: id vec: {:?}", self.base_tx.block_id.to_vec()); - log::debug!("typed_data: id: {}", self.base_tx.block_id); message.insert( TD_BLOCK_ID.to_owned(), - MessageValue::Vec(self.base_tx.block_id.to_vec()), + serde_json::Value::String(self.base_tx.block_id.to_string()), ); - return create_typed_data(super::tx::TransactionType::Claim, tx_fields, message); + Ok(create_typed_data( + super::tx::TransactionType::Claim, + tx_fields, + message, + )) } } diff --git a/spacesvm/src/chain/tx/decoder.rs b/spacesvm/src/chain/tx/decoder.rs index e6b73fb3..39531590 100644 --- a/spacesvm/src/chain/tx/decoder.rs +++ b/spacesvm/src/chain/tx/decoder.rs @@ -1,13 +1,13 @@ use std::{ - collections::HashMap, + collections::BTreeMap, io::{Error, ErrorKind, Result}, + str::FromStr, }; -use avalanche_types::{hash, ids}; -use eip_712::Type as ParserType; -use ethereum_types::H256; -use serde::{de, Deserialize, Serialize}; -use serde_json::to_value; +use avalanche_types::ids; +use ethers_core::types::transaction::eip712::{ + EIP712Domain, Eip712DomainType as Type, TypedData, Types, +}; use super::{base, claim, delete, set, tx::TransactionType, unsigned}; @@ -19,114 +19,7 @@ pub const TD_SPACE: &str = "space"; pub const TD_KEY: &str = "key"; pub const TD_VALUE: &str = "value"; -pub type Type = eip_712::FieldType; - -pub type Types = HashMap>; - -pub type TypedDataMessage = HashMap; - -// TypedDataDomain represents the domain part of an EIP-712 message. -#[derive(Deserialize, Serialize, Debug, Clone, Default)] -pub struct TypedDataDomain { - pub name: String, - pub magic: String, -} - -pub fn mini_kvvm_domain(_m: u64) -> TypedDataDomain { - TypedDataDomain { - name: "MiniKvvm".to_string(), - magic: "0x00".to_string(), // radix(m, 10).to_string(), - } -} - -#[derive(Debug, Clone)] -pub enum MessageValue { - // TODO:combine? - Vec(Vec), - Bytes(Vec), -} - -impl MessageValue { - pub fn to_string(self) -> String { - match self { - MessageValue::Vec(v) => String::from_utf8_lossy(&v).to_string(), - MessageValue::Bytes(v) => String::from_utf8_lossy(&v).to_string(), - } - } - pub fn to_vec(self) -> Vec { - match self { - MessageValue::Vec(v) => v, - MessageValue::Bytes(v) => v, - } - } -} - -impl Serialize for MessageValue { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - match self { - MessageValue::Vec(v) => serializer.serialize_str(&hex::encode(v)), - MessageValue::Bytes(v) => { - serializer.serialize_str(format!("0x{}", &hex::encode(v)).as_str()) - } - } - } -} - -impl<'de> Deserialize<'de> for MessageValue { - fn deserialize>( - deserializer: D, - ) -> std::result::Result { - struct MessageValueVisitor; - impl<'de> de::Visitor<'de> for MessageValueVisitor { - type Value = MessageValue; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a potential or array of potentials") - } - - fn visit_string(self, v: String) -> std::result::Result { - if v.starts_with("0x") { - match hex::decode(&v[2..]) { - Ok(s) => Ok(MessageValue::Bytes(s)), - Err(e) => Err(E::custom(e.to_string())), - } - } else { - match hex::decode(v) { - Ok(s) => Ok(MessageValue::Vec(s)), - Err(e) => Err(E::custom(e.to_string())), - } - } - } - - fn visit_str(self, v: &str) -> std::result::Result { - if v.starts_with("0x") { - match hex::decode(&v[2..]) { - Ok(s) => Ok(MessageValue::Bytes(s)), - Err(e) => Err(E::custom(e.to_string())), - } - } else { - match hex::decode(v) { - Ok(s) => Ok(MessageValue::Vec(s)), - Err(e) => Err(E::custom(e.to_string())), - } - } - } - } - - deserializer.deserialize_any(MessageValueVisitor) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default)] -pub struct TypedData { - pub types: Types, - pub primary_type: TransactionType, - pub domain: TypedDataDomain, - pub message: TypedDataMessage, -} +pub type TypedDataMessage = BTreeMap; pub fn create_typed_data( tx_type: TransactionType, @@ -140,139 +33,122 @@ pub fn create_typed_data( vec![ Type { name: "name".to_owned(), - type_: "string".to_owned(), + r#type: "string".to_owned(), }, Type { name: "magic".to_owned(), - type_: "uint64".to_owned(), + r#type: "uint64".to_owned(), }, ], ); + + let domain = EIP712Domain { + name: Some("SpacesVm".to_owned()), + version: None, + chain_id: None, + verifying_contract: None, + salt: None, + }; return TypedData { types, message, - domain: mini_kvvm_domain(0), // TODO: pass magic - primary_type: tx_type, + domain, + primary_type: tx_type.to_string(), }; } -impl TypedData { - // Attempts to return the base tx from typed data. - pub fn parse_base_tx(&self) -> Result { - let r_block_id = self - .get_typed_message_vec(TD_BLOCK_ID.to_owned()) - .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?; - - let block_id = ids::Id::from_slice(&r_block_id); - - Ok(base::Tx { block_id }) +// Attempts to return the base tx from typed data. +pub fn parse_base_tx(typed_data: &TypedData) -> Result { + if let Some(r_block_id) = typed_data.message.get(TD_BLOCK_ID) { + if let Some(id) = r_block_id.as_str() { + let block_id = ids::Id::from_str(id).map_err(|e| { + Error::new( + ErrorKind::InvalidData, + format!("failed to parse id from string: {}: {}", id, e), + ) + })?; + return Ok(base::Tx { block_id }); + } } - // Attempts to return and unsigned transaction from typed data. - pub fn parse_typed_data(&self) -> Result> { - let base_tx = self.parse_base_tx().map_err(|e| { - Error::new( - ErrorKind::InvalidData, - format!("failed to parse base tx: {:?}", e), - ) - })?; - - match self.primary_type { - TransactionType::Claim => { - let space = self - .get_typed_message(TD_SPACE.to_owned()) - .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?; - Ok(Box::new(claim::Tx { base_tx, space })) - } - - TransactionType::Set => { - let space = self - .get_typed_message(TD_SPACE.to_owned()) - .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?; - let key = self - .get_typed_message(TD_KEY.to_owned()) - .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?; - let value = self - .get_typed_message(TD_VALUE.to_owned()) - .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?; - Ok(Box::new(set::Tx { - base_tx, - space, - key: key, - value: value.as_bytes().to_vec(), - })) - } + Err(Error::new( + ErrorKind::InvalidData, + format!("invalid typed data: {}", TD_BLOCK_ID), + )) +} - TransactionType::Delete => { - let space = self - .get_typed_message(TD_SPACE.to_owned()) - .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?; - let key = self - .get_typed_message(TD_KEY.to_owned()) - .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?; - Ok(Box::new(delete::Tx { +// Attempts to return and unsigned transaction from typed data. +pub fn parse_typed_data( + typed_data: &TypedData, +) -> Result> { + let base_tx = parse_base_tx(&typed_data).map_err(|e| { + Error::new( + ErrorKind::InvalidData, + format!("failed to parse base tx: {:?}", e), + ) + })?; + + // each tx has space and key + let space = get_message_value(&typed_data.message, TD_SPACE.to_owned()) + .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?; + + let key = get_message_value(&typed_data.message, TD_SPACE.to_owned()) + .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?; + + match typed_data.primary_type.as_str() { + // claim tx + tx if tx == TransactionType::Claim.as_str() => Ok(Box::new(claim::Tx { + base_tx, + space: space.to_string(), + })), + + // set tx + tx if tx == TransactionType::Set.as_str() => { + let value = get_message_value(&typed_data.message, TD_VALUE.to_owned()) + .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?; + if let Some(value_str) = value.as_str() { + return Ok(Box::new(set::Tx { base_tx, - space, - key: key, - })) + space: space.to_string(), + key: key.to_string(), + value: Vec::from(value_str), + })); } - TransactionType::Unknown => Err(Error::new( - ErrorKind::Other, - "transaction type Unknown is not valid", - )), - } - } - - pub fn get_typed_message(&self, key: String) -> Result { - match self.message.get(&key) { - Some(value) => Ok(value.to_owned().to_string()), - None => Err(Error::new( - ErrorKind::NotFound, - format!("typed data key missing: {:?}", key), - )), + return Err(Error::new( + ErrorKind::InvalidData, + format!("invalid typed data: {}", TD_VALUE), + )); } - } - pub fn get_typed_message_vec(&self, key: String) -> Result> { - match self.message.get(&key) { - Some(value) => Ok(value.to_owned().to_vec()), - None => Err(Error::new( - ErrorKind::NotFound, - format!("typed data key missing: {:?}", key), - )), - } + // delete tx + tx if tx == TransactionType::Delete.as_str() => Ok(Box::new(delete::Tx { + base_tx, + space: space.to_string(), + key: key.to_string(), + })), + + _ => Err(Error::new( + ErrorKind::Other, + "transaction type Unknown is not valid", + )), } } -pub fn hash_structured_data(typed_data: &TypedData) -> Result { - // EIP-191 compliant - let error_handling = |e: eip_712::Error| Error::new(ErrorKind::Other, e.to_string()); - let prefix = (b"\x19\x01").to_vec(); - let domain = to_value(&typed_data.domain).unwrap(); - let message = to_value(&typed_data.message).unwrap(); - let (domain_hash, data_hash) = ( - eip_712::encode_data( - &ParserType::Custom("EIP712Domain".into()), - &typed_data.types, - &domain, - None, - ) - .map_err(error_handling)?, - eip_712::encode_data( - &ParserType::Custom(typed_data.primary_type.to_string()), - &typed_data.types, - &message, - None, - ) - .map_err(error_handling)?, - ); - let concat = [&prefix[..], &domain_hash[..], &data_hash[..]].concat(); - Ok(hash::keccak256(concat)) +/// Attempts to check for a key in the message map. If it exists return the value. +pub fn get_message_value(message: &TypedDataMessage, key: String) -> Result { + match message.get(&key) { + Some(value) => Ok(value.to_owned()), + None => Err(Error::new( + ErrorKind::NotFound, + format!("typed data key missing: {:?}", key), + )), + } } #[tokio::test] async fn signature_recovers() { use avalanche_types::key; + use ethers_core::types::transaction::eip712::Eip712; let secret_key = key::secp256k1::private_key::Key::generate().unwrap(); let public_key = secret_key.to_public_key(); @@ -286,11 +162,12 @@ async fn signature_recovers() { let resp = tx_data.decode(); assert!(resp.is_ok()); let utx = resp.unwrap(); - let hash = hash_structured_data(&utx.typed_data().await).unwrap(); + // let hash = hash_structured_data(&utx.typed_data().await).unwrap(); + let typed_data = utx.typed_data().await.unwrap(); + let hash = typed_data.struct_hash().unwrap(); - let sig = secret_key.sign_digest(hash.as_bytes()).unwrap(); - let sender = - key::secp256k1::public_key::Key::from_signature(hash.as_bytes(), &sig.to_bytes()).unwrap(); + let sig = secret_key.sign_digest(&hash).unwrap(); + let sender = key::secp256k1::public_key::Key::from_signature(&hash, &sig.to_bytes()).unwrap(); assert_eq!(public_key.to_string(), sender.to_string()); assert_eq!(public_key, sender,); @@ -303,14 +180,13 @@ async fn signature_recovers() { let resp = tx_data.decode(); assert!(resp.is_ok()); let mut utx = resp.unwrap(); - utx.set_block_id(ids::Id::from_slice("duuuu".as_bytes())) + utx.set_block_id(avalanche_types::ids::Id::from_slice("duuuu".as_bytes())) .await; - let hash = hash_structured_data(&utx.typed_data().await).unwrap(); + let typed_data = utx.typed_data().await.unwrap(); + let hash = typed_data.struct_hash().unwrap(); - let sig = secret_key.sign_digest(hash.as_bytes()).unwrap(); - let hash = hash_structured_data(&utx.typed_data().await).unwrap(); - let sender = - key::secp256k1::public_key::Key::from_signature(hash.as_bytes(), &sig.to_bytes()).unwrap(); + let sig = secret_key.sign_digest(&hash).unwrap(); + let sender = key::secp256k1::public_key::Key::from_signature(&hash, &sig.to_bytes()).unwrap(); assert_eq!(public_key.to_string(), sender.to_string()); assert_eq!(public_key, sender,); } diff --git a/spacesvm/src/chain/tx/delete.rs b/spacesvm/src/chain/tx/delete.rs index b58d807b..ecf684fe 100644 --- a/spacesvm/src/chain/tx/delete.rs +++ b/spacesvm/src/chain/tx/delete.rs @@ -1,18 +1,14 @@ -use std::{ - collections::HashMap, - io::{Error, ErrorKind, Result}, -}; +use std::io::{Error, ErrorKind, Result}; use serde::{Deserialize, Serialize}; -use crate::chain::{ - storage, - tx::decoder::{create_typed_data, MessageValue, Type, TypedData}, -}; +use crate::chain::{storage, tx::decoder::create_typed_data}; + +use ethers_core::types::transaction::eip712::{Eip712DomainType as Type, TypedData}; use super::{ base, - decoder::{TD_BLOCK_ID, TD_KEY, TD_SPACE, TD_STRING}, + decoder::{TypedDataMessage, TD_BLOCK_ID, TD_KEY, TD_SPACE, TD_STRING}, tx::TransactionType, unsigned, }; @@ -92,35 +88,39 @@ impl unsigned::Transaction for Tx { Ok(()) } - async fn typed_data(&self) -> TypedData { + async fn typed_data(&self) -> Result { let mut tx_fields: Vec = Vec::new(); tx_fields.push(Type { name: TD_SPACE.to_owned(), - type_: TD_STRING.to_owned(), + r#type: TD_STRING.to_owned(), }); tx_fields.push(Type { name: TD_BLOCK_ID.to_owned(), - type_: TD_STRING.to_owned(), + r#type: TD_STRING.to_owned(), }); tx_fields.push(Type { name: TD_KEY.to_owned(), - type_: TD_STRING.to_owned(), + r#type: TD_STRING.to_owned(), }); - let mut message: HashMap = HashMap::with_capacity(1); + let mut message = TypedDataMessage::new(); message.insert( TD_SPACE.to_owned(), - MessageValue::Vec(self.space.as_bytes().to_vec()), + serde_json::Value::String(self.space.clone()), ); message.insert( TD_KEY.to_owned(), - MessageValue::Vec(self.key.as_bytes().to_vec()), + serde_json::Value::String(self.key.clone()), ); message.insert( TD_BLOCK_ID.to_owned(), - MessageValue::Vec(self.base_tx.block_id.to_vec()), + serde_json::Value::String(self.base_tx.block_id.to_string()), ); - return create_typed_data(super::tx::TransactionType::Delete, tx_fields, message); + Ok(create_typed_data( + super::tx::TransactionType::Delete, + tx_fields, + message, + )) } } diff --git a/spacesvm/src/chain/tx/set.rs b/spacesvm/src/chain/tx/set.rs index f682c628..0605e751 100644 --- a/spacesvm/src/chain/tx/set.rs +++ b/spacesvm/src/chain/tx/set.rs @@ -1,19 +1,20 @@ use std::{ - collections::HashMap, - io::{Error, ErrorKind}, + io::{Error, ErrorKind, Result}, + str::from_utf8, }; +use ethers_core::types::transaction::eip712::{Eip712DomainType as Type, TypedData}; use serde::{Deserialize, Serialize}; use sha3::Digest; use crate::chain::{ storage::{self, get_space_info, put_space_info, put_space_key, ValueMeta}, - tx::decoder::{create_typed_data, MessageValue, Type, TypedData}, + tx::decoder::create_typed_data, }; use super::{ base, - decoder::{TD_BLOCK_ID, TD_BYTES, TD_KEY, TD_SPACE, TD_STRING, TD_VALUE}, + decoder::{TypedDataMessage, TD_BLOCK_ID, TD_KEY, TD_SPACE, TD_STRING, TD_VALUE}, tx::TransactionType, unsigned::{self}, }; @@ -138,44 +139,56 @@ impl unsigned::Transaction for Tx { Ok(()) } - async fn typed_data(&self) -> TypedData { + async fn typed_data(&self) -> Result { let mut tx_fields: Vec = vec![]; tx_fields.push(Type { name: TD_SPACE.to_owned(), - type_: TD_STRING.to_owned(), + r#type: TD_STRING.to_owned(), }); tx_fields.push(Type { name: TD_KEY.to_owned(), - type_: TD_STRING.to_owned(), + r#type: TD_STRING.to_owned(), }); tx_fields.push(Type { name: TD_VALUE.to_owned(), - type_: TD_BYTES.to_owned(), + r#type: TD_STRING.to_owned(), }); tx_fields.push(Type { name: TD_BLOCK_ID.to_owned(), - type_: TD_STRING.to_owned(), + r#type: TD_STRING.to_owned(), }); - let mut message = HashMap::with_capacity(3); + let mut message = TypedDataMessage::new(); message.insert( TD_SPACE.to_owned(), - MessageValue::Vec(self.space.as_bytes().to_vec()), + serde_json::Value::String(self.space.clone()), ); message.insert( TD_KEY.to_owned(), - MessageValue::Vec(self.key.as_bytes().to_vec()), + serde_json::Value::String(self.key.clone()), ); + + let value_str = from_utf8(&self.value).map_err(|e| { + Error::new( + ErrorKind::InvalidData, + format!("failed to convert value to string: {}", e), + ) + })?; + message.insert( TD_VALUE.to_owned(), - MessageValue::Bytes(self.value.to_vec()), + serde_json::Value::String(value_str.to_owned()), ); message.insert( TD_BLOCK_ID.to_owned(), - MessageValue::Vec(self.base_tx.block_id.to_vec()), + serde_json::Value::String(self.base_tx.block_id.to_string()), ); - return create_typed_data(super::tx::TransactionType::Set, tx_fields, message); + Ok(create_typed_data( + super::tx::TransactionType::Set, + tx_fields, + message, + )) } } @@ -189,6 +202,7 @@ fn value_hash(value: &[u8]) -> String { #[tokio::test] async fn set_tx_test() { use super::unsigned::Transaction; + use ethers_core::types::Address; use std::str::FromStr; // set tx space not found @@ -197,7 +211,7 @@ async fn set_tx_test() { db, block_time: 0, tx_id: avalanche_types::ids::Id::empty(), - sender: ethereum_types::Address::zero(), + sender: Address::zero(), }; let tx = Tx { base_tx: base::Tx::default(), @@ -214,7 +228,7 @@ async fn set_tx_test() { db: db.clone(), block_time: 0, tx_id: avalanche_types::ids::Id::empty(), - sender: ethereum_types::Address::zero(), + sender: Address::zero(), }; let tx = crate::chain::tx::claim::Tx { base_tx: base::Tx::default(), @@ -224,8 +238,7 @@ async fn set_tx_test() { assert!(resp.is_ok()); // try to update key from a different sender - let other_account = - ethereum_types::Address::from_str("0000000000000000000000000000000000000001").unwrap(); + let other_account = Address::from_str("0000000000000000000000000000000000000001").unwrap(); let ut_ctx = unsigned::TransactionContext { db: db.clone(), block_time: 0, @@ -246,7 +259,7 @@ async fn set_tx_test() { db: db.clone(), block_time: 0, tx_id: avalanche_types::ids::Id::empty(), - sender: ethereum_types::Address::zero(), + sender: Address::zero(), }; let tx = Tx { base_tx: base::Tx::default(), @@ -261,7 +274,7 @@ async fn set_tx_test() { db: db.clone(), block_time: 0, tx_id: avalanche_types::ids::Id::empty(), - sender: ethereum_types::Address::zero(), + sender: Address::zero(), }; let tx = Tx { base_tx: base::Tx::default(), diff --git a/spacesvm/src/chain/tx/tx.rs b/spacesvm/src/chain/tx/tx.rs index f806abb6..eaa6f0b1 100644 --- a/spacesvm/src/chain/tx/tx.rs +++ b/spacesvm/src/chain/tx/tx.rs @@ -4,12 +4,12 @@ use std::{ }; use avalanche_types::{hash, ids, key, subnet}; -use ethereum_types::Address; +use ethers_core::types::{transaction::eip712::Eip712, Address, H160}; use serde::{Deserialize, Serialize}; use crate::{block::Block, chain::storage::set_transaction}; -use super::{decoder, unsigned::TransactionContext}; +use super::unsigned::TransactionContext; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(tag = "type")] @@ -30,6 +30,17 @@ impl Default for TransactionType { } } +impl TransactionType { + pub fn as_str(&self) -> &'static str { + match self { + TransactionType::Claim => "claim", + TransactionType::Set => "set", + TransactionType::Delete => "delete", + TransactionType::Unknown => "unknown", + } + } +} + impl fmt::Display for TransactionType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -47,7 +58,7 @@ pub struct Transaction { pub signature: Vec, #[serde(skip)] - pub digest_hash: Vec, + pub digest_hash: [u8; 32], #[serde(skip)] pub bytes: Vec, @@ -70,7 +81,7 @@ impl Transaction { Self { unsigned_transaction, signature, - digest_hash: vec![], + digest_hash: [0; 32], bytes: vec![], id: ids::Id::empty(), size: 0, @@ -86,18 +97,21 @@ impl crate::chain::tx::Transaction for Transaction { let stx = serde_json::to_vec(&self).map_err(|e| Error::new(ErrorKind::Other, e.to_string()))?; - let typed_data = &self.unsigned_transaction.typed_data().await; - let digest_hash = decoder::hash_structured_data(typed_data)?; + let typed_data = &self.unsigned_transaction.typed_data().await?; + let digest_hash = typed_data.struct_hash().map_err(|e| { + Error::new( + ErrorKind::Other, + format!("failed to hash typed data: {}", e), + ) + })?; - let sender = key::secp256k1::public_key::Key::from_signature( - digest_hash.as_bytes(), - &self.signature, - )?; + let sender = + key::secp256k1::public_key::Key::from_signature(&digest_hash, &self.signature)?; self.bytes = stx; self.id = ids::Id::from_slice(hash::keccak256(&self.bytes).as_bytes()); self.size = self.bytes.len() as u64; - self.digest_hash = digest_hash.as_bytes().to_vec(); - self.sender = sender.to_h160(); + self.digest_hash = digest_hash; + self.sender = H160::from(sender.to_h160().as_fixed_bytes()); Ok(()) } @@ -151,7 +165,7 @@ pub fn new_tx( signature, // defaults - digest_hash: vec![], + digest_hash: [0; 32], bytes: vec![], id: ids::Id::empty(), size: 0, diff --git a/spacesvm/src/chain/tx/unsigned.rs b/spacesvm/src/chain/tx/unsigned.rs index 7fe41cdf..325b580a 100644 --- a/spacesvm/src/chain/tx/unsigned.rs +++ b/spacesvm/src/chain/tx/unsigned.rs @@ -5,10 +5,9 @@ use std::{ use avalanche_types::{ids::Id, subnet}; use dyn_clone::DynClone; +use ethers_core::types::{transaction::eip712::TypedData, Address}; use serde::{Deserialize, Serialize}; -use crate::chain::tx::decoder::TypedData; - use super::{base, claim, delete, set, tx::TransactionType}; #[typetag::serde(tag = "type")] @@ -19,7 +18,7 @@ pub trait Transaction: Debug + DynClone + Send + Sync { async fn get_value(&self) -> Option>; async fn set_value(&mut self, value: Vec) -> Result<()>; async fn execute(&self, txn_ctx: TransactionContext) -> Result<()>; - async fn typed_data(&self) -> TypedData; + async fn typed_data(&self) -> Result; async fn typ(&self) -> TransactionType; } @@ -30,7 +29,7 @@ pub struct TransactionContext { pub db: Box, pub block_time: u64, pub tx_id: Id, - pub sender: ethereum_types::Address, + pub sender: Address, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -67,3 +66,43 @@ impl TransactionData { } } } + +#[tokio::test] +async fn test_hash_claim_tx() { + use ethers_core::types::transaction::eip712::Eip712; + + let tx_data = crate::chain::tx::unsigned::TransactionData { + typ: TransactionType::Claim, + space: "kvs".to_string(), + key: "foo".to_string(), + value: Vec::new(), + }; + let resp = tx_data.decode(); + assert!(resp.is_ok()); + let mut utx = resp.unwrap(); + utx.set_block_id(avalanche_types::ids::Id::from_slice("duuuu".as_bytes())) + .await; + let typed_data = utx.typed_data().await.unwrap(); + let resp = typed_data.struct_hash(); + assert!(resp.is_ok()); +} + +#[tokio::test] +async fn test_hash_set_tx() { + use ethers_core::types::transaction::eip712::Eip712; + + let tx_data = crate::chain::tx::unsigned::TransactionData { + typ: TransactionType::Set, + space: "kvs".to_string(), + key: "foo".to_string(), + value: "bar".as_bytes().to_vec(), + }; + let resp = tx_data.decode(); + assert!(resp.is_ok()); + let mut utx = resp.unwrap(); + utx.set_block_id(avalanche_types::ids::Id::from_slice("duuuu".as_bytes())) + .await; + let typed_data = utx.typed_data().await.unwrap(); + let resp = typed_data.struct_hash(); + assert!(resp.is_ok()); +} diff --git a/spacesvm/src/mempool/mod.rs b/spacesvm/src/mempool/mod.rs index ebf485aa..2de09982 100644 --- a/spacesvm/src/mempool/mod.rs +++ b/spacesvm/src/mempool/mod.rs @@ -254,7 +254,8 @@ impl Mempool { #[tokio::test] async fn test_mempool() { - use crate::chain::tx::{decoder, tx::TransactionType, unsigned}; + use crate::chain::tx::{tx::TransactionType, unsigned}; + use ethers_core::types::transaction::eip712::Eip712; // init mempool let mempool = Mempool::new(10); @@ -271,8 +272,9 @@ async fn test_mempool() { assert!(resp.is_ok()); let utx_1 = resp.unwrap(); let secret_key = avalanche_types::key::secp256k1::private_key::Key::generate().unwrap(); - let dh_1 = decoder::hash_structured_data(&utx_1.typed_data().await).unwrap(); - let sig_1 = secret_key.sign_digest(dh_1.as_bytes()).unwrap(); + let typed_data = utx_1.typed_data().await.unwrap(); + let dh_1 = typed_data.struct_hash().unwrap(); + let sig_1 = secret_key.sign_digest(&dh_1).unwrap(); let tx_1 = Transaction::new(utx_1, sig_1.to_bytes().to_vec()); // add tx_1 to mempool @@ -296,8 +298,9 @@ async fn test_mempool() { let resp = tx_data_2.decode(); assert!(resp.is_ok()); let utx_2 = resp.unwrap(); - let dh_2 = decoder::hash_structured_data(&utx_2.typed_data().await).unwrap(); - let sig_2 = secret_key.sign_digest(dh_2.as_bytes()).unwrap(); + let typed_data = utx_2.typed_data().await.unwrap(); + let dh_2 = typed_data.struct_hash().unwrap(); + let sig_2 = secret_key.sign_digest(&dh_2).unwrap(); let mut tx_2 = Transaction::new(utx_2, sig_2.to_bytes().to_vec()); tx_2.id = ids::Id::from_slice("sup".as_bytes()); @@ -323,7 +326,8 @@ async fn test_mempool() { #[tokio::test] async fn test_mempool_threads() { - use crate::chain::tx::{decoder, tx::TransactionType, unsigned}; + use crate::chain::tx::{tx::TransactionType, unsigned}; + use ethers_core::types::transaction::eip712::Eip712; use tokio::time::sleep; let vm = crate::vm::ChainVm::new(); @@ -341,8 +345,9 @@ async fn test_mempool_threads() { assert!(resp.is_ok()); let utx = resp.unwrap(); let secret_key = avalanche_types::key::secp256k1::private_key::Key::generate().unwrap(); - let dh = decoder::hash_structured_data(&utx.typed_data().await).unwrap(); - let sig = secret_key.sign_digest(&dh.as_bytes()).unwrap(); + let typed_data = utx.typed_data().await.unwrap(); + let dh = typed_data.struct_hash().unwrap(); + let sig = secret_key.sign_digest(&dh).unwrap(); let tx = Transaction::new(utx, sig.to_bytes().to_vec()); // add tx to mempool