diff --git a/README.md b/README.md index ac1e5b2..1d1a61b 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ ## Postgres DB setup - Install postgresql locally. - Run `psql`. -- Create a user `dev` to manage db. -- Create arcpay database which will host all our tables: `create database arcpay` and `create database proven_arcpay`. +- Create a user `dev` to manage db with `create user dev;`. +- Create arcpay database which will host all our tables: `create database arcpay;` and `create database proven_arcpay;`. - Copy ABI file to server repo. So something like this: `cp arcpay/out/ArcPay.sol/ArcPay.json ../arcpay-server`. - Running `cargo run -- --merkle new` will delete existing tables and create them again. ```sql diff --git a/deploy.yaml b/deploy.yaml new file mode 100644 index 0000000..675bb5d --- /dev/null +++ b/deploy.yaml @@ -0,0 +1,245 @@ +# Codedeploy Parameters +# _______________________________________ +# CodeDeploy and CodeBuild settings largely copied from https://rrawat.com/blog/aws-cloudformation-cicd-nodejs +# TODO: separate into separate nested stacks for clarity +Parameters: + SSHKeyPairKeyName: + Type: AWS::EC2::KeyPair::KeyName + Description: This secure string parameter holds our application password + Default: NodejsDeploymentKeyPair + +# Main Infrastructure +# _______________________________________ +Resources: + EC2SecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Enable HTTP and HTTPS access + SecurityGroupIngress: + - CidrIp: 0.0.0.0/0 # SSH access from everywhere shouldn't be allowed. TODO: use AWS Systems Manager Session Manager or similar instead of SSH + FromPort: 22 + IpProtocol: tcp + ToPort: 22 + - IpProtocol: tcp + FromPort: '80' + ToPort: '80' + CidrIp: 0.0.0.0/0 + - IpProtocol: tcp + FromPort: '443' + ToPort: '443' + CidrIp: 0.0.0.0/0 + + FrontendEC2Instance: + Type: AWS::EC2::Instance + DependsOn: + - EC2SecurityGroup + Properties: + # IamInstanceProfile: !Ref EC2IAMInstanceProfile + KeyName: !Ref SSHKeyPairKeyName # Dynamic input allows changing the value during stack creation without touching the template + ImageId: ami-05552d2dcf89c9b24 + InstanceType: t2.medium + SecurityGroups: + - !Ref EC2SecurityGroup + UserData: !Base64 | + #!/bin/bash -xe + sudo yum update -y + curl -sL https://rpm.nodesource.com/setup_16.x | sudo bash - + sudo yum install -y ruby wget nodejs + wget https://aws-codedeploy-eu-west-1.s3.eu-west-1.amazonaws.com/latest/install + chmod +x ./install + sudo ./install auto + node -e "console.log('Running Node.js ' + process.version)" + npm i pm2 -g + Tags: # CodeDeploy uses these tags to find instances to deploy our changes + - Key: environment + Value: development + - Key: name + Value: webserver + + BackendEC2Instance: + Type: AWS::EC2::Instance + Properties: + KeyName: !Ref SSHKeyPairKeyName # Dynamic input allows changing the value during stack creation without touching the template + InstanceType: t2.medium + ImageId: ami-05552d2dcf89c9b24 + SecurityGroups: + - !Ref EC2SecurityGroup + + RDSInstance: + Type: AWS::RDS::DBInstance + Properties: + DBInstanceIdentifier: ArcPayDB + AllocatedStorage: '5' + DBInstanceClass: db.t3.micro + Engine: postgres + MasterUsername: blake + MasterUserPassword: password1234 # TODO: use a hidden parameter + + RabbitMQBroker: + Type: "AWS::AmazonMQ::Broker" + Properties: + BrokerName: MyRabbitMQBroker + EngineType: RABBITMQ + EngineVersion: '3.8.6' # You can set this to your desired version + DeploymentMode: SINGLE_INSTANCE # For cost saving, but consider CLUSTER for production + HostInstanceType: mq.t3.micro + PubliclyAccessible: true + AutoMinorVersionUpgrade: true + Logs: + General: true + Users: + - Username: blake + Password: password1234 # TODO: use a hidden parameter + + RabbitMQSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Enable RabbitMQ access + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: '5672' + ToPort: '5672' + CidrIp: 0.0.0.0/0 + + SecurityGroupEgress: + Type: AWS::EC2::SecurityGroupEgress + Properties: + GroupId: !GetAtt RabbitMQSecurityGroup.GroupId + IpProtocol: -1 + CidrIp: 0.0.0.0/0 + + # # Codebuild resources + # # _______________________________________ + # BuildArtifactS3Bucket: + # Type: AWS::S3::Bucket + # Properties: + # AccessControl: Private + # BucketName: arcpay-frontend-cfn-codebuild-artifacts + # VersioningConfiguration: + # Status: Enabled + + # IAMRoleForCodeBuild: + # Type: AWS::IAM::Role + # Properties: + # Path: / + # AssumeRolePolicyDocument: + # Version: '2012-10-17' + # Statement: + # - Action: ['sts:AssumeRole'] + # Effect: Allow + # Principal: + # Service: [codebuild.amazonaws.com] + # Policies: + # - PolicyName: "CodeBuildAccess" + # PolicyDocument: + # Version: "2012-10-17" + # Statement: + # - Action: + # - 'ssm:GetParameters' + # - 'logs:*' + # - 's3:*' + # - 'codedeploy:*' + # Effect: "Allow" + # Resource: "*" + + # CodeBuildProject: + # Type: AWS::CodeBuild::Project + # DependsOn: + # - BuildArtifactS3Bucket + # - IAMRoleForCodeBuild + # Properties: + # ServiceRole: !GetAtt IAMRoleForCodeBuild.Arn + # Artifacts: + # Type: S3 + # Location: arcpay-frontend-cfn-codebuild-artifacts + # Name: buildArtifact.zip + # Packaging: ZIP + # Path: deploy-nodejs-cicd + # Environment: + # Type: LINUX_CONTAINER + # ComputeType: BUILD_GENERAL1_SMALL + # Image: aws/codebuild/standard:6.0 + # EnvironmentVariables: + # - Name: PASSWORD + # Value: /Production/AppPassword + # Type: PARAMETER_STORE + # Source: + # Type: GITHUB + # Location: https://github.com/arcpay/demo.git + # Auth: + # Type: OAUTH + # SourceVersion: cloudformation + # Triggers: + # Webhook: true # docs: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-webhookfilter.html + + # # Codedeploy resources + # # _______________________________________ + # CodeRevisionS3Bucket: + # Type: AWS::S3::Bucket + # Properties: + # AccessControl: Private + # BucketName: arcpay-frontend-cfn-codedeploy-revisions + # VersioningConfiguration: + # Status: Enabled + + # EC2IAMRoleForCodeDeploy: + # Type: AWS::IAM::Role + # Properties: + # Path: / + # AssumeRolePolicyDocument: + # Version: '2012-10-17' + # Statement: + # - Action: ['sts:AssumeRole'] + # Effect: Allow + # Principal: + # Service: [ec2.amazonaws.com] + # Policies: + # - PolicyName: "CodeDeployAccess" + # PolicyDocument: + # Version: "2012-10-17" + # Statement: + # - Action: + # - 's3:*' + # - 's3-object-lambda:*' + # Effect: "Allow" + # Resource: "arn:aws:s3:::arcpay-frontend-cfn-codedeploy-revisions/*" + + # EC2IAMInstanceProfile: + # Type: AWS::IAM::InstanceProfile + # Properties: + # Path: / + # Roles: + # - !Ref EC2IAMRoleForCodeDeploy + + # CodeDeployServiceRole: + # Type: AWS::IAM::Role + # Properties: + # Path: / + # ManagedPolicyArns: + # - arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole + # AssumeRolePolicyDocument: + # Version: '2012-10-17' + # Statement: + # - Action: ['sts:AssumeRole'] + # Effect: Allow + # Principal: + # Service: [codedeploy.amazonaws.com] + + # CodeDeployApplication: + # Type: AWS::CodeDeploy::Application + # DependsOn: FrontendEC2Instance + # Properties: + # ApplicationName: arcpay-frontend-cfn-codedeploy-application + + # CodeDeployDeploymentGroup: + # Type: AWS::CodeDeploy::DeploymentGroup + # DependsOn: CodeDeployApplication + # Properties: + # ApplicationName: arcpay-frontend-cfn-codedeploy-application + # ServiceRoleArn: !GetAtt CodeDeployServiceRole.Arn + # Ec2TagFilters: + # - Key: environment + # Type: KEY_AND_VALUE + # Value: development + # DeploymentGroupName: development + # DeploymentConfigName: CodeDeployDefault.OneAtATime \ No newline at end of file diff --git a/src/contract_owner.rs b/src/contract_owner.rs index 090a07a..91b6536 100644 --- a/src/contract_owner.rs +++ b/src/contract_owner.rs @@ -33,6 +33,7 @@ impl ContractOwner { let tx = self.contract.update_state(state_root, mint_time); let pending_tx = tx.send().await?; let _mined_tx = pending_tx.await?; + dbg!(&_mined_tx); // TODO change to below before launch. This waits for 3 blocks. // This slows down manual testing. // let _mined_tx = pending_tx.confirmations(3).await?; diff --git a/src/main.rs b/src/main.rs index 6506b3b..3d78e9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ mod mint; mod model; mod routes; mod send_consumer; +mod transactions; mod user_balance; #[derive(Parser, Debug)] @@ -62,7 +63,7 @@ const QUEUE_NAME: &str = "user_requests"; /// Maximum time gap (in seconds) between two proof submissions. /// Note that it's not strict and depends on the number of requests. /// Set it half the max time set in the contract. -const MAX_SINCE_LAST_PROVE: usize = 30; // TODO adjust based on traffic +const MAX_SINCE_LAST_PROVE: u64 = 100; // TODO adjust based on traffic abigen!(ArcPayContract, "ArcPay.json"); diff --git a/src/model.rs b/src/model.rs index 8386f40..ce5a6ad 100644 --- a/src/model.rs +++ b/src/model.rs @@ -11,8 +11,15 @@ use serde_with::serde_as; use crate::{ merkle::{MyPoseidon, PostgresDBConfig}, send_consumer::verify_ecdsa, + transactions::{ + multicoin::{SignedMultiCoinSend}, + RichTransaction, + }, ApiContext, QueueMessage, QUEUE_NAME, }; +use ethers::types::transaction::eip712::Eip712; + +use ethers::prelude::{Eip712, EthAbiType}; pub(crate) type ServiceSchema = Schema; pub(crate) struct QueryRoot; @@ -27,7 +34,22 @@ impl QueryRoot { } // TODO: add some authentication for user privacy, maybe a signature. - async fn get_ownership_proofs(&self, ctx: &Context<'_>, address: [u8; 20]) -> Vec { + async fn get_ownership_proofs( + &self, + ctx: &Context<'_>, + address: [u8; 20], + sig: Signature, + expiry: String, + ) -> Vec { + let msg = Login712 { expiry }; + + let msg_hash = msg.encode_eip712().unwrap(); + + let ethsig = ethers::prelude::Signature::from(sig); + let signer = ethsig.recover(msg_hash).unwrap(); + assert_eq!(signer, address.into()); + // TODO: check expiry time + let merkle_db = &ctx.data_unchecked::().mt.read().await.db; merkle_db.get_for_address(&address).await } @@ -41,6 +63,17 @@ impl QueryRoot { } } +#[derive(Eip712, EthAbiType, Clone, Debug)] +#[eip712( + name = "ArcPay", + version = "0", + chain_id = 11155111, + verifying_contract = "0x21843937646d779E1e27A5f94fF5972F80C942bD" +)] +struct Login712 { + expiry: String, +} + pub(crate) struct MutationRoot; /// Leaf structure of the merkle tree. @@ -61,7 +94,7 @@ pub(crate) struct CoinRange { // `serde` crate can't serialize large arrays, hence using specially designed `serde_with`. #[serde_as] -#[derive(Debug, InputObject, Serialize, Deserialize)] +#[derive(Debug, InputObject, Serialize, Deserialize, Clone)] pub(crate) struct Signature { #[serde_as(as = "[_; 32]")] pub r: [u8; 32], @@ -181,7 +214,7 @@ pub(crate) async fn send_in_merkle( let from_proof = to_my_fr(mt.proof(index as usize).await.unwrap().0); proofs[0] = from_proof; } - match highest_coin_to_send == leaf.low_coin { + match highest_coin_to_send == leaf.high_coin { true => mt.set(index as usize, MyPoseidon::default_leaf(), None), false => mt.set( index as usize, @@ -233,6 +266,81 @@ pub(crate) async fn send_in_merkle( #[Object] impl MutationRoot { + async fn multi_coin_send( + &self, + ctx: &Context<'_>, + multi_coin_tx: SignedMultiCoinSend, + ) -> Vec { + let api_context = ctx.data_unchecked::(); + let mut mt = api_context.mt.write().await; + + multi_coin_tx.authorized().unwrap(); + + let components = multi_coin_tx.decompose(); + + let mut root = vec![]; + for primitive_tx in components.iter() { + let index = primitive_tx.leaf_id(); + let leaf = Leaf { + address: primitive_tx.sender(), + low_coin: primitive_tx.low_coin(), + high_coin: primitive_tx.high_coin(), + }; + let highest_coin_to_send = primitive_tx.fee_upper_bound() - 1; // TODO: move everything to fee upper bound + let recipient = primitive_tx.receiver(); + + let proofs = send_in_merkle( + &mut mt, + index, + &leaf, + highest_coin_to_send, + &recipient, + true, + ) + .await + .ok_or("proofs should be returned") + .unwrap(); + + root = MyPoseidon::serialize(mt.root()); + + // Queue the send request to be received by ZK prover at the other end. + let channel = &api_context.channel; + + let queue_message = bincode::serialize(&QueueMessage::Send(( + leaf, + index as usize, + highest_coin_to_send, + recipient, + proofs, + ))) + .expect("unsafe_send: queue message should be serialized"); + + let confirm = channel + .basic_publish( + "", + QUEUE_NAME, + BasicPublishOptions { + mandatory: true, + ..BasicPublishOptions::default() + }, + queue_message.as_slice(), + BasicProperties::default(), + ) + .await + .expect("basic_publish") + .await + .expect("publisher-confirms"); + + assert!(confirm.is_ack()); + // when `mandatory` is on, if the message is not sent to a queue for any reason + // (example, queues are full), the message is returned back. + // If the message isn't received back, then a queue has received the message. + assert_eq!(confirm.take_message(), None); + } + + root + } + /// Send coins `[leaf.low_coin, highest_coin_to_send]` to `receiver` from `leaf`. /// The send should be authorized by `leaf.address` through ECDSA signature `sig`. async fn unsafe_send( diff --git a/src/send_consumer.rs b/src/send_consumer.rs index 692a6ad..881a4f3 100644 --- a/src/send_consumer.rs +++ b/src/send_consumer.rs @@ -1,7 +1,7 @@ -use ethers::abi::{encode, Token}; -use ethers::types::transaction::eip712::{EIP712Domain, Eip712}; -use ethers::utils::keccak256; +use ethers::types::transaction::eip712::{Eip712}; + + use ethers::{ prelude::{Eip712, EthAbiType, U256}, types::Address, @@ -13,20 +13,22 @@ use pmtree::Hasher; use tokio::sync::RwLock; use tokio_postgres::IsolationLevel; -use crate::QueueMessage; use crate::{ contract_owner::ContractOwner, merkle::{MyPoseidon, PostgresDBConfig}, model::{mint_in_merkle, Leaf}, model::{send_in_merkle, Signature}, }; +use crate::{QueueMessage, MAX_SINCE_LAST_PROVE}; + +use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Eip712, EthAbiType, Clone, Debug)] #[eip712( name = "ArcPay", version = "0", chain_id = 11155111, - verifying_contract = "0x82B766D0a234489a299BBdA3DBe6ba206d77D35F" + verifying_contract = "0x21843937646d779E1e27A5f94fF5972F80C942bD" )] struct Send712 { owner: Address, @@ -140,6 +142,9 @@ pub(crate) async fn send_consumer( ) { let mut mint_time: U256 = U256::default(); let arcpay_owner = ContractOwner::new().await.unwrap(); + + let mut last_finalize_time: u64 = 0; + while let Some(delivery) = consumer.next().await { let delivery = delivery.expect("error in consumer"); let mesg: QueueMessage = bincode::deserialize(delivery.data.as_slice()) @@ -160,7 +165,7 @@ pub(crate) async fn send_consumer( mint_in_merkle(&mut mt, leaf).await; } QueueMessage::Send(send) => { - let (leaf, index, highest_coin_to_send, recipient, proofs) = send; + let (leaf, index, highest_coin_to_send, recipient, _proofs) = send; send_in_merkle( &mut mt, index as u64, @@ -171,7 +176,7 @@ pub(crate) async fn send_consumer( ) .await; } - QueueMessage::Withdraw((leaf, index)) => { + QueueMessage::Withdraw((_leaf, index)) => { mt.set(index, MyPoseidon::default_leaf(), None) .await .unwrap(); @@ -182,7 +187,12 @@ pub(crate) async fn send_consumer( // drop(mt); // Check now - last proof time > MAX_SINCE_LAST_PROOF. // If yes, prove the nova proof for groth16 and then issue the below transaction: - { + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + + if current_time - last_finalize_time > MAX_SINCE_LAST_PROVE { let state_root = U256::from_big_endian(&state_root); let state_root_updated = arcpay_owner.update_state_root(state_root, mint_time).await; @@ -206,7 +216,6 @@ pub(crate) async fn send_consumer( // update the finalized merkle tree by copying the proven merkle dbs to finalized dbs. // may be optimized later once we have large tables. - /* { let mut client = mt.db.client.write().await; let tx = client @@ -249,7 +258,8 @@ pub(crate) async fn send_consumer( tx.execute(&statement, &[]).await.unwrap(); tx.commit().await.expect("fin::copy error"); - } */ + last_finalize_time = current_time; + } } } } diff --git a/src/transactions/mod.rs b/src/transactions/mod.rs new file mode 100644 index 0000000..2e4b331 --- /dev/null +++ b/src/transactions/mod.rs @@ -0,0 +1,11 @@ +use self::primitive::PrimitiveTransaction; + +pub mod multicoin; +pub mod primitive; + +pub trait RichTransaction { + type Error: std::fmt::Debug; // TODO: do we also need to implement Error, Send, and Sync? + + fn decompose(&self) -> Vec; + fn authorized(&self) -> Result<(), Self::Error>; +} diff --git a/src/transactions/multicoin.rs b/src/transactions/multicoin.rs new file mode 100644 index 0000000..7b42846 --- /dev/null +++ b/src/transactions/multicoin.rs @@ -0,0 +1,177 @@ +use crate::{merkle::MyPoseidon, model::Signature}; +use async_graphql::InputObject; +use ethers::types::transaction::eip712::Eip712; +use ethers::types::{SignatureError, H160}; +use ethers::{ + prelude::{Eip712, EthAbiType, U256}, + types::Address, +}; +use pmtree::Hasher; +use rln::circuit::Fr; +use serde::{Deserialize, Serialize}; + +use super::primitive::{PrimitiveTransaction, PrimitiveTransactionError}; +use super::RichTransaction; + +// Multi coin sends get around the "change" problem, where a user +// has loose change in the form of many coin ranges. A multi coin send +// lets the user spend multiple coin ranges with a single signature. +// +// MultiCoinSend is the succinct representation of the transactions that +// is signed by the spender. +#[derive(Debug, InputObject, Serialize, Deserialize)] +pub struct MultiCoinSend { + receiver: [u8; 20], + amount: u64, + fee: u64, + detail: [u8; 32], +} + +impl MultiCoinSend { + // Required to convert from a type that works with graphql to a type where a signature can be recovered + fn encode_as_signable(&self) -> MultiCoinSend712 { + let receiver: Address = self.receiver.into(); + MultiCoinSend712 { + receiver, + amount: self.amount.into(), + fee: self.fee.into(), + detail: self.detail.into(), + } + } +} + +#[derive(Eip712, EthAbiType, Clone, Debug)] +#[eip712( + name = "ArcPay", + version = "0", + chain_id = 11155111, + verifying_contract = "0x21843937646d779E1e27A5f94fF5972F80C942bD" +)] +pub struct MultiCoinSend712 { + receiver: Address, + amount: U256, + fee: U256, + detail: U256, +} + +// A SignedMultiCoinSend is a full representation of the multi coin send with +// enough information to tell if the child transactions were properly authorized +// +// TODO: split out summary/detail vs signature validation logic so that we can easily implement multicoin transactions for any account type +#[derive(Debug, InputObject, Serialize, Deserialize)] +pub struct SignedMultiCoinSend { + summary: MultiCoinSend, + signature: Signature, + child_transactions: Vec, +} + +#[derive(Debug)] +pub enum MultiSendAuthorizationError { + WrongHash { claimed: Fr, actual: Fr }, + EIP712Error(::Error), + InvalidSignature(SignatureError), + InvalidChildTransaction(PrimitiveTransactionError), + WrongSender { signer: H160, sender: H160 }, + DifferentReceivers([u8; 20], [u8; 20]), + SpendMismatch { claimed: u64, actual: u64 }, + FeeMismatch { claimed: u64, actual: u64 }, +} + +impl RichTransaction for SignedMultiCoinSend { + type Error = MultiSendAuthorizationError; + + fn decompose(&self) -> Vec { + self.child_transactions.to_vec() + } + + // Checks whether the child transactions are appropriately authorized by the top level signature. + // The summary must correctly represent the child transactions, or the user hasn't given informed consent. + // This will be checked in the authorization circuit, and doesn't guarantee that the transactions are valid + // for a given state, as the signer may not own the leaves they're trying to spend. + fn authorized(&self) -> Result<(), MultiSendAuthorizationError> { + // Ensure that the given child transactions hash to the claimed hash + if self.child_hash() != MyPoseidon::deserialize(self.summary.detail.to_vec()) { + return Err(MultiSendAuthorizationError::WrongHash { + claimed: self.child_hash(), + actual: MyPoseidon::deserialize(self.summary.detail.to_vec()), + }); + } + + // Check the signature + let msg_hash = match self.summary.encode_as_signable().encode_eip712() { + Ok(m) => m, + Err(e) => return Err(MultiSendAuthorizationError::EIP712Error(e)), + }; + + let ethsig = ethers::prelude::Signature::from(self.signature.clone()); + let signer = match ethsig.recover(msg_hash) { + Ok(s) => s, + Err(e) => return Err(MultiSendAuthorizationError::InvalidSignature(e)), + }; + + for child in self.child_transactions.iter() { + // Ensure that every child transaction is valid + match child.valid() { + Ok(()) => (), + Err(e) => return Err(MultiSendAuthorizationError::InvalidChildTransaction(e)), + } + + // Ensure that the sender of every child transaction is the signer + let child_sender: H160 = child.sender().into(); + if child_sender != signer { + return Err(MultiSendAuthorizationError::WrongSender { + signer, + sender: child_sender, + }); + } + + // Ensure that the receiver of every child transaction is the receiver in the top level transaction + if child.receiver() != self.summary.receiver { + return Err(MultiSendAuthorizationError::DifferentReceivers( + child.receiver(), + self.summary.receiver, + )); + } + } + + // Ensure that the claimed amount spend is the sum of the child transactions + let sum = self.child_transactions.iter().fold( + 0u64, + |acc: u64, next: &PrimitiveTransaction| -> u64 { + acc + next.upper_bound() - next.low_coin() + }, + ); + if sum != self.summary.amount { + return Err(MultiSendAuthorizationError::SpendMismatch { + claimed: self.summary.amount, + actual: sum, + }); + } + + // The claimed amount spend is different to the of the child transactions + let sum = self.child_transactions.iter().fold( + 0u64, + |acc: u64, next: &PrimitiveTransaction| -> u64 { + acc + next.fee_upper_bound() - next.upper_bound() + }, + ); + if sum != self.summary.fee { + return Err(MultiSendAuthorizationError::FeeMismatch { + claimed: self.summary.fee, + actual: sum, + }); + } + + Ok(()) + } +} + +impl SignedMultiCoinSend { + fn child_hash(&self) -> Fr { + self.child_transactions + .iter() + .fold(Fr::from(0), |acc: Fr, next: &PrimitiveTransaction| -> Fr { + MyPoseidon::hash(&[next.hash(), acc]) + }) + } +} diff --git a/src/transactions/primitive.rs b/src/transactions/primitive.rs new file mode 100644 index 0000000..f5a7f8e --- /dev/null +++ b/src/transactions/primitive.rs @@ -0,0 +1,104 @@ +use crate::merkle::MyPoseidon; +use async_graphql::InputObject; +use pmtree::Hasher; +use rln::circuit::Fr; +use serde::{Deserialize, Serialize}; + +// Primitive transactions is the basic form of transaction that is processed by the main +// state transition circuit. All other transaction types ultimately compile down into +// primitive transactions. +// +// The primitive transaction spends a single leaf, giving the first part to the receiver, +// the second part to the operator, and the third part back to the sender. +// Note, the lowest_coin is not strictly necessary because it's uniquely specified by the leaf_id, +// but it's convenient for validation purposes. +#[derive(Debug, InputObject, Serialize, Deserialize, Clone)] +pub struct PrimitiveTransaction { + sender: [u8; 20], + receiver: [u8; 20], + leaf_id: u64, + low_coin: u64, + high_coin: u64, + upper_bound: u64, + fee_upper_bound: u64, +} + +#[derive(Debug)] +pub enum PrimitiveTransactionError { + NegativeAmountSent { + low_coin: u64, + upper_bound: u64, + }, + NegativeFee { + upper_bound: u64, + fee_upper_bound: u64, + }, + UnavailabeFunds { + high_coin: u64, + fee_upper_bound: u64, + }, +} + +impl PrimitiveTransaction { + pub fn valid(&self) -> Result<(), PrimitiveTransactionError> { + if self.low_coin > self.upper_bound { + Err(PrimitiveTransactionError::NegativeAmountSent { + low_coin: self.low_coin, + upper_bound: self.upper_bound, + }) + } else if self.upper_bound > self.fee_upper_bound { + Err(PrimitiveTransactionError::NegativeFee { + upper_bound: self.upper_bound, + fee_upper_bound: self.fee_upper_bound, + }) + } else if self.high_coin + 1 < self.fee_upper_bound { // TODO: make high_coin exclusive in the Merkle tree to avoid off by one issues + Err(PrimitiveTransactionError::UnavailabeFunds { + high_coin: self.high_coin, + fee_upper_bound: self.fee_upper_bound, + }) + } else { + Ok(()) + } + } + + pub fn hash(&self) -> Fr { + MyPoseidon::hash(&[ + MyPoseidon::deserialize(self.sender.to_vec()), + MyPoseidon::deserialize(self.receiver.to_vec()), + Fr::from(self.leaf_id), + Fr::from(self.low_coin), + Fr::from(self.high_coin), + Fr::from(self.upper_bound), + Fr::from(self.fee_upper_bound), + ]) + } + + // Getters (TODO: use getset crate) + pub fn sender(&self) -> [u8; 20] { + self.sender + } + + pub fn receiver(&self) -> [u8; 20] { + self.receiver + } + + pub fn leaf_id(&self) -> u64 { + self.leaf_id + } + + pub fn low_coin(&self) -> u64 { + self.low_coin + } + + pub fn high_coin(&self) -> u64 { + self.high_coin + } + + pub fn upper_bound(&self) -> u64 { + self.upper_bound + } + + pub fn fee_upper_bound(&self) -> u64 { + self.fee_upper_bound + } +}