diff --git a/Cargo.lock b/Cargo.lock index cc776ac..51694b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -394,9 +394,11 @@ dependencies = [ "anyhow", "chaincash_app", "clap", + "directories", "human-panic", "tokio", "tracing", + "tracing-appender", "tracing-log", "tracing-subscriber", ] @@ -562,6 +564,25 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-bigint" version = "0.4.9" @@ -756,6 +777,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "dlv-list" version = "0.3.0" @@ -1606,6 +1648,17 @@ version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + [[package]] name = "libsqlite3-sys" version = "0.27.0" @@ -2014,6 +2067,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-multimap" version = "0.4.3" @@ -2284,6 +2343,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.2" @@ -3169,6 +3239,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.27" diff --git a/Cargo.toml b/Cargo.toml index 1970cc4..6ebd812 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = ["crates/*"] +resolver = "2" [workspace.package] version = "0.1.0" diff --git a/README.md b/README.md index 3fb7f1a..27b269e 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,13 @@ Currently the following predicates are supported: #### Whitelist -A `whitelist` predicate evaluates to `true` if any of the suppplied agents are the current owner of the note. +A `whitelist` predicate evaluates to `true` if any of the suppplied agents match depending on the `kind` field. + +`whitelist` has subtypes defined in the `kind` field, the following are supported: + +- `owner` whitelist is based on the current note holder +- `issuer` whitelist is based on the note issuer +- `historical` whitelist checks each holder of the note If the owner is known to us and trusted we can accept the note without any other consideration. @@ -83,6 +89,25 @@ For example, this could be configured like so: ```toml type = "whitelist" +kind = "owner" +agents = ["PK1", "OWNER2"] +``` + +#### Blacklist + +A `blacklist` predicate evaluates to `true` if none of the suppplied agents match depending on the `kind` field. + +`blacklist` has subtypes defined in the `kind` field, the following are supported: + +- `owner` blacklist is based on the current note holder +- `issuer` blacklist is based on the note issuer +- `historical` blacklist checks each holder of the note + +If we want to blacklist all notes issued by `PK1` this could be done like so: + +```toml +type = "blacklist" +kind = "issuer" agents = ["PK1", "OWNER2"] ``` @@ -111,7 +136,7 @@ For example, if we want to express that a note is accepted if the note is over c type = "or" conditions = [ # the owner of the note is either PK1 or PK2 - {type = "whitelist", agents = ["PK1", "PK2"]}, + {type = "whitelist", kind = "owner", agents = ["PK1", "PK2"]}, # the note has at least 100% collateral {type = "collateral", percent = 100} ] diff --git a/crates/chaincash_app/src/lib.rs b/crates/chaincash_app/src/lib.rs index d92e627..680f534 100644 --- a/crates/chaincash_app/src/lib.rs +++ b/crates/chaincash_app/src/lib.rs @@ -1,5 +1,5 @@ use chaincash_offchain::node::node_from_config; -use chaincash_predicate::Predicate; +use chaincash_predicate::predicates::Predicate; use chaincash_server::{Server, ServerState}; use chaincash_store::{ChainCashStore, Update}; use config::{Environment, File}; diff --git a/crates/chaincash_offchain/src/transactions.rs b/crates/chaincash_offchain/src/transactions.rs index 8fae6cf..2fe4f83 100644 --- a/crates/chaincash_offchain/src/transactions.rs +++ b/crates/chaincash_offchain/src/transactions.rs @@ -7,7 +7,7 @@ use self::notes::{mint_note_transaction, MintNoteRequest}; use self::reserves::{mint_reserve_transaction, MintReserveRequest}; use ergo_client::node::NodeClient; use ergo_lib::chain::ergo_box::box_builder::ErgoBoxCandidateBuilderError; -use ergo_lib::ergo_chain_types::{blake2b256_hash, Digest32}; +use ergo_lib::ergo_chain_types::blake2b256_hash; use ergo_lib::ergotree_ir::chain::address::AddressEncoderError; use ergo_lib::ergotree_ir::chain::ergo_box::box_value::BoxValue; use ergo_lib::ergotree_ir::chain::ergo_box::{box_value::BoxValueError, ErgoBox}; diff --git a/crates/chaincash_predicate/src/collateral.rs b/crates/chaincash_predicate/src/collateral.rs deleted file mode 100644 index 53143e4..0000000 --- a/crates/chaincash_predicate/src/collateral.rs +++ /dev/null @@ -1,57 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{Accept, NoteContext}; - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct Collateral { - pub(crate) percent: u16, -} - -impl Accept for Collateral { - fn accept(&self, context: &NoteContext) -> bool { - let ratio = (context.liabilities as f64 / context.value as f64) * 100.0; - - ratio >= self.percent as f64 - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_returns_true_if_collaterized() { - let context = NoteContext { - owner: "PK1".to_string(), - value: 50, - liabilities: 50, - }; - let p = Collateral { percent: 100 }; - - assert!(p.accept(&context)) - } - - #[test] - fn test_returns_true_if_over_collaterized() { - let context = NoteContext { - owner: "PK1".to_string(), - value: 50, - liabilities: 60, - }; - let p = Collateral { percent: 100 }; - - assert!(p.accept(&context)) - } - - #[test] - fn test_returns_true_if_not_collaterized() { - let context = NoteContext { - owner: "PK1".to_string(), - value: 50, - liabilities: 48, - }; - let p = Collateral { percent: 100 }; - - assert!(!p.accept(&context)) - } -} diff --git a/crates/chaincash_predicate/src/context.rs b/crates/chaincash_predicate/src/context.rs new file mode 100644 index 0000000..49c6c8f --- /dev/null +++ b/crates/chaincash_predicate/src/context.rs @@ -0,0 +1,66 @@ +pub type NanoErg = u64; +pub type PubKeyHex = String; + +/// Context related to a note that holds information required by predicates +/// to determine if the note is acceptable. +#[derive(Debug, Clone)] +pub struct NoteContext { + /// The nanoerg value of the related note + /// The denomination of the note converted to its erg value + pub nanoerg: NanoErg, + /// Owner of the note as hex encoded public key + pub owner: PubKeyHex, + /// Issuer of the note as hex encoded public key + pub issuer: PubKeyHex, + /// Agents that have signed and traded the note + pub signers: Vec, +} + +/// Implementors provide a way to access extra context when processing a note +/// inside a predicate. +pub trait ContextProvider { + /// Get all notes as `NoteContext` issued by the specified agent + fn agent_issued_notes(&self, agent: &str) -> Vec; + + /// Get the amount of reserves the specified agent has + fn agent_reserves_nanoerg(&self, agent: &str) -> NanoErg; +} + +/// Context passed to predicates during evaluation +pub struct PredicateContext { + pub note: NoteContext, + pub provider: P, +} + +#[cfg(test)] +pub(crate) mod test_util { + use super::*; + + pub struct TestAgent { + pub pk: String, + pub issued_notes: Vec, + pub reserves: u64, + } + + pub struct TestContextProvider { + pub agents: Vec, + } + + impl ContextProvider for TestContextProvider { + fn agent_issued_notes(&self, agent: &str) -> Vec { + self.agents + .iter() + .find(|n| n.pk == agent) + .map(|a| a.issued_notes.clone()) + .unwrap_or_default() + } + + fn agent_reserves_nanoerg(&self, agent: &str) -> u64 { + self.agents + .iter() + .find(|n| n.pk == agent) + .map(|a| a.reserves) + .unwrap_or_default() + } + } +} diff --git a/crates/chaincash_predicate/src/lib.rs b/crates/chaincash_predicate/src/lib.rs index 3402bf0..9adf30d 100644 --- a/crates/chaincash_predicate/src/lib.rs +++ b/crates/chaincash_predicate/src/lib.rs @@ -1,14 +1,11 @@ -pub mod collateral; -pub mod or; -pub mod whitelist; +pub mod context; +pub mod predicates; use std::path::PathBuf; -use serde::{Deserialize, Serialize}; - #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("Predicate deserialization failed due to: {0}")] + #[error("Predicate deserialization failed")] Deserialization(#[from] toml::de::Error), #[error("Failed to load predicate from file '{path}'")] @@ -23,69 +20,3 @@ pub struct Config { /// Path to enabled predicate configuration files pub predicates: Vec, } - -pub struct NoteContext { - owner: String, - value: u64, - liabilities: u64, -} - -pub trait Accept { - fn accept(&self, context: &NoteContext) -> bool; -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum Predicate { - Or(or::Or), - Whitelist(whitelist::Whitelist), - Collateral(collateral::Collateral), -} - -impl Predicate { - pub fn from_file(path: &PathBuf) -> Result { - let s = std::fs::read_to_string(path).map_err(|e| Error::LoadFromFile { - source: e, - path: path.display().to_string(), - })?; - - Ok(toml::from_str(&s)?) - } -} - -impl Accept for Predicate { - fn accept(&self, context: &NoteContext) -> bool { - match self { - Predicate::Or(p) => p.accept(context), - Predicate::Whitelist(p) => p.accept(context), - Predicate::Collateral(p) => p.accept(context), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_predicate_deser() { - let s = r#" - type = "or" - conditions = [ - {type = "whitelist", agents = ["PK1", "PK2"]}, - {type = "collateral", percent = 110} - ] - "#; - let p = toml::from_str::(s).unwrap(); - let mut context = NoteContext { - owner: "PK0".to_string(), - value: 1, - liabilities: 1, - }; - - assert!(!p.accept(&context)); - - context.owner = "PK1".to_string(); - assert!(p.accept(&context)) - } -} diff --git a/crates/chaincash_predicate/src/or.rs b/crates/chaincash_predicate/src/or.rs deleted file mode 100644 index 9bf665d..0000000 --- a/crates/chaincash_predicate/src/or.rs +++ /dev/null @@ -1,63 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{Accept, NoteContext, Predicate}; - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct Or { - conditions: Vec, -} - -impl Accept for Or { - fn accept(&self, context: &NoteContext) -> bool { - for condition in &self.conditions { - if condition.accept(context) { - return true; - } - } - - false - } -} - -#[cfg(test)] -mod tests { - use crate::{collateral::Collateral, whitelist::Whitelist}; - - use super::*; - - #[test] - fn test_returns_true_if_any_condition_returns_true() { - let context = NoteContext { - owner: "PK1".to_string(), - value: 1, - liabilities: 1, - }; - let p1 = Whitelist { - agents: vec!["PK2".to_string()], - }; - let p2 = Collateral { percent: 100 }; - let p = Or { - conditions: vec![Predicate::Whitelist(p1), Predicate::Collateral(p2)], - }; - - assert!(p.accept(&context)) - } - - #[test] - fn test_returns_false_if_all_conditions_return_false() { - let context = NoteContext { - owner: "PK1".to_string(), - value: 1, - liabilities: 1, - }; - let p1 = Whitelist { - agents: vec!["PK2".to_string()], - }; - let p2 = Collateral { percent: 200 }; - let p = Or { - conditions: vec![Predicate::Whitelist(p1), Predicate::Collateral(p2)], - }; - - assert!(!p.accept(&context)) - } -} diff --git a/crates/chaincash_predicate/src/predicates.rs b/crates/chaincash_predicate/src/predicates.rs new file mode 100644 index 0000000..01fbe0f --- /dev/null +++ b/crates/chaincash_predicate/src/predicates.rs @@ -0,0 +1,61 @@ +use crate::context::{ContextProvider, PredicateContext}; +use crate::Error; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +pub mod blacklist; +pub mod collateral; +pub mod or; +pub mod whitelist; + +pub trait Accept { + fn accept(&self, context: &PredicateContext

) -> bool; +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Predicate { + Or(or::Or), + Whitelist(whitelist::Whitelist), + Blacklist(blacklist::Blacklist), + Collateral(collateral::Collateral), +} + +impl Predicate { + pub fn from_file(path: &PathBuf) -> Result { + let s = std::fs::read_to_string(path).map_err(|e| Error::LoadFromFile { + source: e, + path: path.display().to_string(), + })?; + + Ok(toml::from_str(&s)?) + } +} + +impl Accept for Predicate { + fn accept(&self, context: &PredicateContext

) -> bool { + match self { + Predicate::Or(p) => p.accept(context), + Predicate::Whitelist(p) => p.accept(context), + Predicate::Blacklist(p) => p.accept(context), + Predicate::Collateral(p) => p.accept(context), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_predicate_deser() { + let s = r#" + type = "or" + conditions = [ + {type = "whitelist", kind = "owner", agents = ["PK1", "PK2"]}, + {type = "collateral", percent = 110} + ] + "#; + assert!(toml::from_str::(s).is_ok()) + } +} diff --git a/crates/chaincash_predicate/src/predicates/blacklist.rs b/crates/chaincash_predicate/src/predicates/blacklist.rs new file mode 100644 index 0000000..a19ebe4 --- /dev/null +++ b/crates/chaincash_predicate/src/predicates/blacklist.rs @@ -0,0 +1,152 @@ +use crate::context::{ContextProvider, PredicateContext}; +use crate::predicates::Accept; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum BlacklistKind { + Issuer, + Owner, + Historical, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Blacklist { + pub(crate) agents: Vec, + pub(crate) kind: BlacklistKind, +} + +impl Accept for Blacklist { + fn accept(&self, context: &PredicateContext

) -> bool { + match self.kind { + BlacklistKind::Issuer => !self.agents.contains(&context.note.issuer), + BlacklistKind::Owner => !self.agents.contains(&context.note.owner), + BlacklistKind::Historical => context + .note + .signers + .iter() + .all(|s| !self.agents.contains(s)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::context::{test_util::*, NoteContext}; + + #[test] + fn test_returns_false_if_owner_blacklisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Blacklist { + agents: vec!["PK0".to_string(), "owner1".to_string()], + kind: BlacklistKind::Owner, + }; + + assert!(!p.accept(&context)) + } + + #[test] + fn test_returns_true_if_owner_not_blacklisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Blacklist { + agents: vec!["PK0".to_string(), "PK2".to_string()], + kind: BlacklistKind::Owner, + }; + assert!(p.accept(&context)) + } + + #[test] + fn test_returns_false_if_issuer_blacklisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Blacklist { + agents: vec!["PK0".to_string(), "issuer1".to_string()], + kind: BlacklistKind::Issuer, + }; + + assert!(!p.accept(&context)) + } + + #[test] + fn test_returns_true_if_issuer_not_blacklisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Blacklist { + agents: vec!["PK0".to_string(), "PK2".to_string()], + kind: BlacklistKind::Issuer, + }; + assert!(p.accept(&context)) + } + + #[test] + fn test_returns_false_if_historical_signer_blacklisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![ + issuer_pk.clone(), + "signer1".to_owned(), + "next_owner".to_owned(), + ], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Blacklist { + agents: vec!["PK0".to_string(), "signer1".to_string()], + kind: BlacklistKind::Historical, + }; + + assert!(!p.accept(&context)) + } + + #[test] + fn test_returns_true_if_historical_signer_not_blacklisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone(), "another1".to_owned()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Blacklist { + agents: vec!["PK0".to_string(), "PK2".to_string(), "owner1".to_owned()], + kind: BlacklistKind::Historical, + }; + assert!(p.accept(&context)) + } +} diff --git a/crates/chaincash_predicate/src/predicates/collateral.rs b/crates/chaincash_predicate/src/predicates/collateral.rs new file mode 100644 index 0000000..059d221 --- /dev/null +++ b/crates/chaincash_predicate/src/predicates/collateral.rs @@ -0,0 +1,213 @@ +use crate::context::{ContextProvider, PredicateContext}; +use crate::predicates::Accept; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum CollateralAlgorithm { + Initial, +} + +impl Default for CollateralAlgorithm { + fn default() -> Self { + Self::Initial + } +} + +impl CollateralAlgorithm { + fn initial(&self, percent: u16, context: &PredicateContext

) -> bool { + let issuer_note_tally: u64 = context + .provider + .agent_issued_notes(&context.note.issuer) + .iter() + .map(|n| n.nanoerg) + .sum(); + let issuer_reserves = context + .provider + .agent_reserves_nanoerg(&context.note.issuer); + let issuer_collateral = (issuer_reserves as f64 / issuer_note_tally as f64) * 100.0; + + if issuer_collateral >= percent as f64 { + return true; + } + + for signer in context.note.signers.iter().skip(1) { + let signer_notes = context.provider.agent_issued_notes(signer); + let highest_value_note = signer_notes.iter().max_by_key(|n| n.nanoerg); + + if let Some(signer_note) = highest_value_note { + let signer_reserves = context.provider.agent_reserves_nanoerg(signer); + let signer_collateral = + (signer_reserves as f64 / signer_note.nanoerg as f64) * 100.0; + + if signer_collateral >= percent as f64 { + return true; + } + } + } + + false + } + + pub fn eval(&self, percent: u16, context: &PredicateContext

) -> bool { + match self { + CollateralAlgorithm::Initial => self.initial(percent, context), + } + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Collateral { + #[serde(default = "CollateralAlgorithm::default")] + algorithm: CollateralAlgorithm, + pub(crate) percent: u16, +} + +impl Accept for Collateral { + fn accept(&self, context: &PredicateContext

) -> bool { + self.algorithm.eval(self.percent, context) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::context::{test_util::*, NoteContext}; + + // * first, take value of notes issued by issuer of a note of interest is divided by its reserves, if collateralization is enough (e.g. 100%), finish + #[test] + fn test_initial_returns_true_if_collaterized_by_issuer() { + let issuer_pk = "issuer1".to_owned(); + let note_of_interest = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone()], + }; + // issuer of note of interest + // has 90% reserves of note of interest + let issuer = TestAgent { + pk: issuer_pk, + issued_notes: vec![note_of_interest.clone()], + reserves: 900, + }; + let provider = TestContextProvider { + agents: vec![issuer], + }; + let context = PredicateContext { + note: note_of_interest, + provider, + }; + // only require 86% + let p = Collateral { + percent: 86, + algorithm: CollateralAlgorithm::Initial, + }; + // acceptable + assert!(p.accept(&context)) + } + + // * if not, take max of notes issued (not passed through) by second signer divided by its reserves, third etc, stop when signer with enough collateralization found + #[test] + fn test_initial_returns_true_if_collaterized_by_signer() { + let issuer_pk = "issuer1".to_owned(); + let signer_pk = "signer2".to_owned(); + // ownership: + // issuer1 -> signer2 -> owner5 + let note_of_interest = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner5".to_owned(), + signers: vec![issuer_pk.clone(), signer_pk.clone()], + }; + // issuer, only has 10% reserves for note of interest + // which is not enough + let issuer = TestAgent { + pk: issuer_pk, + issued_notes: vec![note_of_interest.clone()], + reserves: 100, + }; + let signer_note = NoteContext { + nanoerg: 1000, + issuer: signer_pk.clone(), + owner: "owner5".to_owned(), + signers: vec![signer_pk.clone()], + }; + // has 1000 reserves, has one note worth 1000 + // thus has 100% collateral + let signer = TestAgent { + pk: signer_pk, + issued_notes: vec![signer_note], + reserves: 1000, + }; + let provider = TestContextProvider { + agents: vec![issuer, signer], + }; + let context = PredicateContext { + note: note_of_interest, + provider, + }; + // requires 100% + let p = Collateral { + percent: 100, + algorithm: CollateralAlgorithm::Initial, + }; + // acceptable + assert!(p.accept(&context)) + } + + // * if not found in the whole signatures-chain, acceptance predicate returns false + #[test] + fn test_initial_returns_false_if_no_required_collateral() { + let issuer_pk = "issuer1".to_owned(); + let signer_pk = "signer2".to_owned(); + // ownership: + // issuer -> signer2 -> owner5 + let note_of_interest = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner5".to_owned(), + signers: vec![issuer_pk.clone(), signer_pk.clone()], + }; + // issuer only has 10% collateral + let issuer = TestAgent { + pk: issuer_pk, + issued_notes: vec![note_of_interest.clone()], + reserves: 100, + }; + let signer_note = NoteContext { + nanoerg: 1000, + issuer: signer_pk.clone(), + owner: "owner5".to_owned(), + signers: vec![signer_pk.clone()], + }; + // this signer note is 100% collaterized, which meets the `percent` requirement + // but it is not the highest valued note issued by signer so it is not considered. + let signer_note2 = NoteContext { + nanoerg: 800, + issuer: signer_pk.clone(), + owner: "owner5".to_owned(), + signers: vec![signer_pk.clone()], + }; + // signer has 800 reserves but their max value note is 1000 value + // 80% collateral - we require 100% + let signer = TestAgent { + pk: signer_pk, + issued_notes: vec![signer_note2, signer_note], + reserves: 800, + }; + let provider = TestContextProvider { + agents: vec![issuer, signer], + }; + let context = PredicateContext { + note: note_of_interest, + provider, + }; + let p = Collateral { + percent: 100, + algorithm: CollateralAlgorithm::Initial, + }; + // not acceptable, issuer and signer dont have 100% collateral + assert!(!p.accept(&context)) + } +} diff --git a/crates/chaincash_predicate/src/predicates/or.rs b/crates/chaincash_predicate/src/predicates/or.rs new file mode 100644 index 0000000..ad14be0 --- /dev/null +++ b/crates/chaincash_predicate/src/predicates/or.rs @@ -0,0 +1,85 @@ +use crate::context::{ContextProvider, PredicateContext}; +use crate::predicates::{Accept, Predicate}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Or { + conditions: Vec, +} + +impl Accept for Or { + fn accept(&self, context: &PredicateContext

) -> bool { + for condition in &self.conditions { + if condition.accept(context) { + return true; + } + } + + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::context::{test_util::TestContextProvider, NoteContext}; + use crate::predicates::whitelist::{Whitelist, WhitelistKind}; + + #[test] + fn test_returns_true_if_any_condition_returns_true() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let acceptable = Whitelist { + agents: vec!["PK0".to_string(), "owner1".to_string()], + kind: WhitelistKind::Owner, + }; + let unacceptable = Whitelist { + agents: vec!["PK0".to_string(), "notowner".to_string()], + kind: WhitelistKind::Owner, + }; + let p = Or { + conditions: vec![ + Predicate::Whitelist(unacceptable), + Predicate::Whitelist(acceptable), + ], + }; + + assert!(p.accept(&context)) + } + + #[test] + fn test_returns_false_if_all_conditions_return_false() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let unacceptable2 = Whitelist { + agents: vec!["PK0".to_string(), "alsonotowner".to_string()], + kind: WhitelistKind::Owner, + }; + let unacceptable = Whitelist { + agents: vec!["PK0".to_string(), "notowner".to_string()], + kind: WhitelistKind::Owner, + }; + let p = Or { + conditions: vec![ + Predicate::Whitelist(unacceptable), + Predicate::Whitelist(unacceptable2), + ], + }; + + assert!(!p.accept(&context)) + } +} diff --git a/crates/chaincash_predicate/src/predicates/whitelist.rs b/crates/chaincash_predicate/src/predicates/whitelist.rs new file mode 100644 index 0000000..e2b96a1 --- /dev/null +++ b/crates/chaincash_predicate/src/predicates/whitelist.rs @@ -0,0 +1,150 @@ +use crate::context::{ContextProvider, PredicateContext}; +use crate::predicates::Accept; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum WhitelistKind { + Issuer, + Owner, + Historical, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Whitelist { + pub(crate) agents: Vec, + pub(crate) kind: WhitelistKind, +} + +impl Accept for Whitelist { + fn accept(&self, context: &PredicateContext

) -> bool { + match self.kind { + WhitelistKind::Issuer => self.agents.contains(&context.note.issuer), + WhitelistKind::Owner => self.agents.contains(&context.note.owner), + WhitelistKind::Historical => { + context.note.signers.iter().any(|s| self.agents.contains(s)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::context::{test_util::*, NoteContext}; + + #[test] + fn test_returns_true_if_owner_whitelisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Whitelist { + agents: vec!["PK0".to_string(), "owner1".to_string()], + kind: WhitelistKind::Owner, + }; + + assert!(p.accept(&context)) + } + + #[test] + fn test_returns_false_if_owner_not_whitelisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Whitelist { + agents: vec!["PK0".to_string(), "PK2".to_string()], + kind: WhitelistKind::Owner, + }; + assert!(!p.accept(&context)) + } + + #[test] + fn test_returns_true_if_issuer_whitelisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Whitelist { + agents: vec!["PK0".to_string(), "issuer1".to_string()], + kind: WhitelistKind::Issuer, + }; + + assert!(p.accept(&context)) + } + + #[test] + fn test_returns_false_if_issuer_not_whitelisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Whitelist { + agents: vec!["PK0".to_string(), "PK2".to_string()], + kind: WhitelistKind::Issuer, + }; + assert!(!p.accept(&context)) + } + + #[test] + fn test_returns_true_if_historical_signer_whitelisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![ + issuer_pk.clone(), + "signer1".to_owned(), + "next_owner".to_owned(), + ], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Whitelist { + agents: vec!["PK0".to_string(), "signer1".to_string()], + kind: WhitelistKind::Historical, + }; + + assert!(p.accept(&context)) + } + + #[test] + fn test_returns_false_if_historical_signer_not_whitelisted() { + let issuer_pk = "issuer1".to_owned(); + let note = NoteContext { + nanoerg: 1000, + issuer: issuer_pk.clone(), + owner: "owner1".to_owned(), + signers: vec![issuer_pk.clone(), "another1".to_owned()], + }; + let provider = TestContextProvider { agents: vec![] }; + let context = PredicateContext { note, provider }; + let p = Whitelist { + agents: vec!["PK0".to_string(), "PK2".to_string(), "owner1".to_owned()], + kind: WhitelistKind::Historical, + }; + assert!(!p.accept(&context)) + } +} diff --git a/crates/chaincash_predicate/src/whitelist.rs b/crates/chaincash_predicate/src/whitelist.rs deleted file mode 100644 index ad83bbd..0000000 --- a/crates/chaincash_predicate/src/whitelist.rs +++ /dev/null @@ -1,47 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::{Accept, NoteContext}; - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct Whitelist { - pub(crate) agents: Vec, -} - -impl Accept for Whitelist { - fn accept(&self, context: &NoteContext) -> bool { - self.agents.contains(&context.owner) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_returns_true_if_owner_whitelisted() { - let context = NoteContext { - owner: "PK1".to_string(), - value: 1, - liabilities: 1, - }; - let p = Whitelist { - agents: vec!["PK0".to_string(), "PK1".to_string()], - }; - - assert!(p.accept(&context)) - } - - #[test] - fn test_returns_false_if_owner_not_whitelisted() { - let context = NoteContext { - owner: "PK3".to_string(), - value: 1, - liabilities: 1, - }; - let p = Whitelist { - agents: vec!["PK1".to_string()], - }; - - assert!(!p.accept(&context)) - } -} diff --git a/crates/chaincash_server/src/app.rs b/crates/chaincash_server/src/app.rs index 442871e..b2f8ede 100644 --- a/crates/chaincash_server/src/app.rs +++ b/crates/chaincash_server/src/app.rs @@ -1,7 +1,7 @@ //! ChainCash payment server creation and serving. use axum::{routing::get, Router}; use chaincash_offchain::TransactionService; -use chaincash_predicate::Predicate; +use chaincash_predicate::predicates::Predicate; use chaincash_store::ChainCashStore; use ergo_client::node::NodeClient; use tracing::info; diff --git a/docs/MODEL.md b/docs/MODEL.md index 5fe2113..3598fd7 100644 --- a/docs/MODEL.md +++ b/docs/MODEL.md @@ -7,12 +7,13 @@ erDiagram NOTE { int id PK int box_id FK - byte[] owner "Encoded group element representing the current owners public key" + string owner "Hex encoded public key of the current owner" + string issuer "Hex encoded public key of the notes issuer" } RESERVE { int id PK int box_id FK - byte[] issuer "Encoded group element representing the issuers public key" + string owner "Hex encoded public key of the owner of the reserves" } ERGO_BOX { int id PK @@ -22,8 +23,7 @@ erDiagram OWNERSHIP_ENTRY { int id PK int note_id FK - byte[] owner "Encoded group element representing the note owners public key at this point in time" - int height "Blockchain height the agent came into onwership of the note" + string owner "Hex encoded public key of the note owner at this point in time" } SIGNATURE { int id PK