diff --git a/migrations/2019-04-16-150207_initialize/up.sql b/migrations/2019-04-16-150207_initialize/up.sql index 50e4378..a126c37 100644 --- a/migrations/2019-04-16-150207_initialize/up.sql +++ b/migrations/2019-04-16-150207_initialize/up.sql @@ -1,6 +1,6 @@ CREATE TABLE accounts ( - accountid VARCHAR(56) PRIMARY KEY, + accountid VARCHAR(56) PRIMARY KEY NOT NULL, balance BIGINT NOT NULL CHECK (balance >= 0), seqnum BIGINT NOT NULL, numsubentries INT NOT NULL CHECK (numsubentries >= 0), @@ -9,8 +9,8 @@ CREATE TABLE accounts thresholds TEXT NOT NULL, flags INT NOT NULL, lastmodified INT NOT NULL, - buyingliabilities BIGINT CHECK (buyingliabilities >= 0), - sellingliabilities BIGINT CHECK (sellingliabilities >= 0), + buyingliabilities BIGINT NOT NULL CHECK (buyingliabilities >= 0), + sellingliabilities BIGINT NOT NULL CHECK (sellingliabilities >= 0), signers TEXT ); diff --git a/src/crypto/error.rs b/src/crypto/error.rs index 73ced88..f173b1f 100644 --- a/src/crypto/error.rs +++ b/src/crypto/error.rs @@ -8,12 +8,6 @@ use std::str; /// The Errors that can occur. #[derive(Debug)] pub enum Error { - /// Error that can occur when parsing a key. - InvalidStrKey, - /// Invalid version byte in key. - InvalidStrKeyVersionByte, - /// Invalid checksum in key. - InvalidStrKeyChecksum, /// Invalid keypair seed. InvalidSeed, /// Invalid Asset code. diff --git a/src/crypto/keypair.rs b/src/crypto/keypair.rs index 9c7062a..7af6237 100644 --- a/src/crypto/keypair.rs +++ b/src/crypto/keypair.rs @@ -1,9 +1,9 @@ -use super::error::{Error, Result}; -use super::strkey; +use crate::stellar_base::strkey; +use super::error::{Result, Error}; use ed25519_dalek::{Keypair, PublicKey, SecretKey}; pub fn from_secret_seed(data: &str) -> Result { - let bytes = strkey::decode_secret_seed(&data)?; + let bytes = strkey::decode_secret_seed(&data).or(Err(Error::InvalidSeed))?; let secret = SecretKey::from_bytes(&bytes).or(Err(Error::InvalidSeed))?; let public = PublicKey::from(&secret); Ok(Keypair { diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 183ef07..0cf8e51 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,7 +1,6 @@ #![allow(dead_code)] -mod error; mod keypair; -mod strkey; +mod error; pub use self::keypair::from_secret_seed; diff --git a/src/database/mod.rs b/src/database/mod.rs index b71e74c..50f0ef9 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] pub use self::models::peer::Peer; +pub use self::models::account::{Account, BASE_RESERVE}; mod models; mod repository; diff --git a/src/database/models/account.rs b/src/database/models/account.rs new file mode 100644 index 0000000..b4fdeb1 --- /dev/null +++ b/src/database/models/account.rs @@ -0,0 +1,114 @@ +use super::{db_conn, schema::accounts}; +use diesel::prelude::*; + +// https://www.stellar.org/developers/guides/concepts/accounts.html + +// TODO: We should take base reserve from ledger header +pub const BASE_RESERVE: i64 = 5000000; + +#[derive(Identifiable, Queryable, Debug)] +#[primary_key(accountid)] +pub struct Account { + /// The public key that was first used to create the account. You can replace the key used for signing the account’s transactions with a different public key, but the original account ID will always be used to identify the account. + pub accountid: String, + /// The number of lumens held by the account. The balance is denominated in 1/10,000,000th of a lumen, the smallest divisible unit of a lumen. + pub balance: i64, + /// The current transaction sequence number of the account. This number starts equal to the ledger number at which the account was created. + pub seqnum: i64, + /// Number of other entries the account owns. This number is used to calculate the account’s minimum balance. + pub numsubentries: i32, + /// (optional) Account designated to receive inflation. Every account with a balance of at least 100 XLM can vote to send inflation to a destination account. + pub inflationdest: Option, + /// A domain name that can optionally be added to the account. Clients can look up a stellar.toml from this domain. This should be in the format of a fully qualified domain name such as example.com. + /// The federation protocol can use the home domain to look up more details about a transaction’s memo or address details about an account. For more on federation, see the federation guide. + pub homedomain: String, + /// Operations have varying levels of access. This field specifies thresholds for low-, medium-, and high-access levels, as well as the weight of the master key. For more info, see multi-sig. + pub thresholds: String, + /// Currently there are three flags, used by issuers of assets. + /// - Authorization required (0x1): Requires the issuing account to give other accounts permission before they can hold the issuing account’s credit. + /// - Authorization revocable (0x2): Allows the issuing account to revoke its credit held by other accounts. + /// - Authorization immutable (0x4): If this is set then none of the authorization flags can be set and the account can never be deleted. + pub flags: i32, + /// updated_at field + pub lastmodified: i32, + /// Starting in protocol version 10, each account also tracks its lumen liabilities. Buying liabilities equal the total amount of lumens offered to buy aggregated over all offers owned by this account, and selling liabilities equal the total amount of lumens offered to sell aggregated over all offers owned by this account. An account must always have balance sufficiently above the minimum reserve to satisfy its lumen selling liabilities, and a balance sufficiently below the maximum to accomodate its lumen buying liabilities + pub buyingliabilities: i64, + pub sellingliabilities: i64, + /// Used for multi-sig. This field lists other public keys and their weights, which can be used to authorize transactions for this account. + pub signers: Option, +} + +type Result = std::result::Result; + +impl Account { + pub fn all() -> Result> { + use self::accounts::dsl::*; + + accounts.load::(&*db_conn()) + } + + pub fn create(accountid: String) -> Result { + let new_account = NewAccount::new(accountid); + diesel::insert_into(accounts::table) + .values(&new_account) + .execute(&*db_conn()) + } + + pub fn get(g_accountid: &str) -> Result { + use self::accounts::dsl::*; + + accounts + .filter(accountid.eq(g_accountid)) + .first::(&*db_conn()) + } + + pub fn delete(g_accountid: &str) -> Result { + use self::accounts::dsl::*; + + diesel::delete(accounts.filter(accountid.eq(g_accountid))).execute(&*db_conn()) + } + + // TODO: this should take base reserve from particular + // ledger header into account + // TODO: consider ledger version lower than 8, formulae was + // different back then + pub fn spendable_balance(&self) -> i64 { + self.balance - ((2 + i64::from(self.numsubentries)) * BASE_RESERVE) + } +} + +#[derive(Insertable)] +#[table_name = "accounts"] +pub struct NewAccount { + pub accountid: String, + pub balance: i64, + pub seqnum: i64, + pub numsubentries: i32, + pub inflationdest: Option, + pub homedomain: String, + pub thresholds: String, + pub flags: i32, + pub lastmodified: i32, + pub buyingliabilities: i64, + pub sellingliabilities: i64, + pub signers: Option, +} + +impl NewAccount { + pub fn new(accountid: String) -> Self { + NewAccount { + accountid, + balance: 0, + seqnum: 0, // TODO: This number starts equal to the ledger number at which the account was created + numsubentries: 0, + inflationdest: None, + homedomain: String::from("example.com"), + thresholds: String::from(""), + flags: 1, + lastmodified: 0, + buyingliabilities: 0, + sellingliabilities: 0, + signers: None, + } + } +} diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index bb3557e..4644efe 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -1,4 +1,5 @@ #![allow(dead_code, unused_must_use)] pub(crate) mod peer; +pub(crate) mod account; pub(crate) use super::{db_conn, schema, CONFIG}; diff --git a/src/factories/internal_xdr.rs b/src/factories/internal_xdr.rs index 45b6475..c2d7275 100644 --- a/src/factories/internal_xdr.rs +++ b/src/factories/internal_xdr.rs @@ -1,4 +1,6 @@ use crate::xdr; +use rand::rngs::OsRng; +use ed25519_dalek::Keypair; use serde::ser::Serialize; use serde_xdr::to_bytes; @@ -57,6 +59,13 @@ pub fn build_public_key() -> xdr::PublicKey { ])) } +pub fn random_public_key() -> xdr::PublicKey { + let mut rng = OsRng::new().unwrap(); + let random_keypair = Keypair::generate(&mut rng); + + xdr::PublicKey::Ed25519(xdr::Uint256(*random_keypair.public.as_bytes())) +} + pub fn build_operation_result_code() -> xdr::OperationResultCode { xdr::OperationResultCode::OpNoAccount } diff --git a/src/lib.rs b/src/lib.rs index 068a9b9..cc9690b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,3 +13,5 @@ pub(crate) mod network; pub(crate) mod overlay; pub(crate) mod schema; pub(crate) mod scp; +pub(crate) mod stellar_base; +pub(crate) mod transactions; diff --git a/src/main.rs b/src/main.rs index f9f9ac4..dca2f06 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod network; mod overlay; mod schema; mod scp; +mod stellar_base; mod xdr; fn main() { diff --git a/src/schema.rs b/src/schema.rs index 9e3bc18..877dffb 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -9,7 +9,7 @@ table! { table! { accounts (accountid) { - accountid -> Nullable, + accountid -> Text, balance -> BigInt, seqnum -> BigInt, numsubentries -> Integer, @@ -18,8 +18,8 @@ table! { thresholds -> Text, flags -> Integer, lastmodified -> Integer, - buyingliabilities -> Nullable, - sellingliabilities -> Nullable, + buyingliabilities -> BigInt, + sellingliabilities -> BigInt, signers -> Nullable, } } diff --git a/src/stellar_base/mod.rs b/src/stellar_base/mod.rs new file mode 100644 index 0000000..d4e61b7 --- /dev/null +++ b/src/stellar_base/mod.rs @@ -0,0 +1 @@ +pub mod strkey; diff --git a/src/crypto/strkey.rs b/src/stellar_base/strkey.rs similarity index 87% rename from src/crypto/strkey.rs rename to src/stellar_base/strkey.rs index 09f37ee..5cee854 100644 --- a/src/crypto/strkey.rs +++ b/src/stellar_base/strkey.rs @@ -1,4 +1,4 @@ -use super::error::{Error, Result}; +use crate::xdr::{AccountId}; use base32; use byteorder::{ByteOrder, LittleEndian}; use crc16::{State, XMODEM}; @@ -10,6 +10,24 @@ const SHA256_HASH_VERSION_BYTE: u8 = 23 << 3; // X static ALPHABET: base32::Alphabet = base32::Alphabet::RFC4648 { padding: false }; +#[derive(Debug)] +pub enum Error { + /// Error that can occur when parsing a key. + InvalidStrKey, + /// Invalid version byte in key. + InvalidStrKeyVersionByte, + /// Invalid checksum in key. + InvalidStrKeyChecksum +} + +pub type Result = std::result::Result; + +pub fn encode_ed25519_public_key(key: AccountId) -> Result { + match key { + AccountId::Ed25519(opaque) => encode_account_id(&opaque.0) + } +} + pub fn encode_account_id(data: &[u8]) -> Result { encode_check(ACCOUNT_ID_VERSION_BYTE, data) } diff --git a/src/transactions/create_account_operation.rs b/src/transactions/create_account_operation.rs new file mode 100644 index 0000000..7977acc --- /dev/null +++ b/src/transactions/create_account_operation.rs @@ -0,0 +1,77 @@ +use crate::xdr; +use crate::database::{db_conn, Account, BASE_RESERVE}; +use crate::stellar_base::strkey; +use super::utils; +// TODO: I believe, everything should work without using `prelude`. But it doesn't +use diesel::prelude::*; +use diesel::result::Error::NotFound; + +pub(crate) fn create_account_operation(source_account: Option, account_operation: xdr::CreateAccountOp) -> xdr::CreateAccountResultCode { + let dest_account_id = strkey::encode_ed25519_public_key(account_operation.destination).unwrap(); + + if account_operation.starting_balance < BASE_RESERVE { + xdr::CreateAccountResultCode::CreateAccountLowReserve + } else if Account::get(&dest_account_id).is_ok() { + xdr::CreateAccountResultCode::CreateAccountAlreadyExist + } else { + match source_account { + Some(public_key) => { + let source_account_id = strkey::encode_ed25519_public_key(public_key).unwrap(); + + match Account::get(&source_account_id) { + Ok(account) => { + if account.spendable_balance() < account_operation.starting_balance { + xdr::CreateAccountResultCode::CreateAccountUnderfunded + } else { + // Actually apply operation + use crate::schema::accounts::dsl::balance; + + let new_balance = utils::add_balance(&account, account_operation.starting_balance).unwrap(); + diesel::update(&account) + .set(balance.eq(new_balance)) + .execute(&*db_conn()) + .unwrap(); + + xdr::CreateAccountResultCode::CreateAccountSuccess + } + }, + Err(e) => { + match e { + NotFound => { + xdr::CreateAccountResultCode::CreateAccountMalformed + }, + _ => { + // TODO: What do we do in this case? + xdr::CreateAccountResultCode::CreateAccountMalformed + } + } + } + } + } + None => { + xdr::CreateAccountResultCode::CreateAccountUnderfunded + } + } + } +} + + +#[cfg(test)] +mod tests { + use super::{xdr}; + use crate::factories::internal_xdr; + + #[test] + fn test_account_operation_with_zero_balance() { + let source_account = internal_xdr::random_public_key(); + + let operation = xdr::CreateAccountOp { + destination: internal_xdr::random_public_key(), + starting_balance: 0, + }; + + let result = super::create_account_operation(Some(source_account), operation); + + assert_eq!(result, xdr::CreateAccountResultCode::CreateAccountLowReserve); + } +} diff --git a/src/transactions/mod.rs b/src/transactions/mod.rs new file mode 100644 index 0000000..c0914b6 --- /dev/null +++ b/src/transactions/mod.rs @@ -0,0 +1,21 @@ +// pub(crate) use crate::config::CONFIG; +// pub(crate) use crate::crypto; +// pub(crate) use crate::network::Network; +use crate::xdr; + +mod create_account_operation; +mod utils; + +use create_account_operation::create_account_operation; +// pub(crate) use lazy_static::lazy_static; + +// pub(crate) use crate::database::models::account; + +pub fn do_apply(operation: xdr::Operation) { + match operation.body { + xdr::OperationBody::CreateAccountOp(acccount_operation) => { + create_account_operation(operation.source_account, acccount_operation); + }, + _ => {} + } +} diff --git a/src/transactions/utils.rs b/src/transactions/utils.rs new file mode 100644 index 0000000..2cc1c91 --- /dev/null +++ b/src/transactions/utils.rs @@ -0,0 +1,38 @@ +use std::result::Result; +use crate::database::{Account, BASE_RESERVE}; + +#[derive(Debug)] +pub enum Error { + InsufficientBalance, + MaxBalanceExceeded, + TooMuchSellingLiabilities, + TooMuchBuyingLiabilities +} + +// `delta` can be negative for charging account +pub fn add_balance(account: &Account, delta: i64) -> Result { + if delta == 0 { + return Ok(account.balance); + } + + // check, whether we want to subtract too much + if account.balance + delta < 0 { + return Err(Error::InsufficientBalance); + } + + if delta + account.balance > std::i64::MAX { + return Err(Error::MaxBalanceExceeded); + } + + let new_balance = account.balance + delta; + + if delta < 0 && new_balance - BASE_RESERVE < account.sellingliabilities { + return Err(Error::TooMuchSellingLiabilities); + } + + if delta > 0 && new_balance + account.buyingliabilities > std::i64::MAX { + return Err(Error::TooMuchBuyingLiabilities); + } + + Ok(new_balance) +} diff --git a/stellar.db b/stellar.db new file mode 100644 index 0000000..e69de29