diff --git a/.github/workflows/memcheck.yml b/.github/workflows/memcheck.yml index 3eca5f494d..e6556b9f57 100644 --- a/.github/workflows/memcheck.yml +++ b/.github/workflows/memcheck.yml @@ -73,7 +73,7 @@ jobs: run: ./target/release/ant --log-output-dest=data-dir file upload --public "./the-test-data.zip" > ./upload_output 2>&1 env: ANT_LOG: "v" - timeout-minutes: 5 + timeout-minutes: 15 - name: showing the upload terminal output run: cat upload_output diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 89cb422225..60faed6af6 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -1322,7 +1322,7 @@ jobs: run: ./target/release/ant --log-output-dest data-dir file upload "./test_data_1.tar.gz" > ./upload_output 2>&1 env: ANT_LOG: "v" - timeout-minutes: 5 + timeout-minutes: 15 - name: showing the upload terminal output run: cat upload_output diff --git a/Cargo.lock b/Cargo.lock index 3967d148c3..1b1c00c3cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,15 +118,16 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8ebf106e84a1c37f86244df7da0c7587e697b71a0d565cce079449b85ac6f8" +checksum = "02b0561294ccedc6181e5528b850b4579e3fbde696507baa00109bfd9054c5bb" dependencies = [ "alloy-consensus", "alloy-contract", "alloy-core", "alloy-eips", "alloy-genesis", + "alloy-json-rpc", "alloy-network", "alloy-node-bindings", "alloy-provider", @@ -152,25 +153,40 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed961a48297c732a5d97ee321aa8bb5009ecadbcb077d8bec90cb54e651629" +checksum = "a101d4d016f47f13890a74290fdd17b05dd175191d9337bc600791fb96e4dea8" dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", "alloy-serde", + "alloy-trie", "auto_impl", "c-kzg", "derive_more", "serde", ] +[[package]] +name = "alloy-consensus-any" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa60357dda9a3d0f738f18844bd6d0f4a5924cc5cf00bfad2ff1369897966123" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "serde", +] + [[package]] name = "alloy-contract" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460ab80ce4bda1c80bcf96fe7460520476f2c7b734581c6567fac2708e2a60ef" +checksum = "2869e4fb31331d3b8c58c7db567d1e4e4e94ef64640beda3b6dd9b7045690941" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -183,7 +199,7 @@ dependencies = [ "alloy-transport", "futures", "futures-util", - "thiserror 1.0.69", + "thiserror 2.0.6", ] [[package]] @@ -229,9 +245,9 @@ dependencies = [ [[package]] name = "alloy-eip7702" -version = "0.3.2" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ffc577390ce50234e02d841214b3dc0bea6aaaae8e04bbf3cb82e9a45da9eb" +checksum = "4c986539255fb839d1533c128e190e557e52ff652c9ef62939e233a81dd93f7e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -241,9 +257,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69e06cf9c37be824b9d26d6d101114fdde6af0c87de2828b414c05c4b3daa71" +checksum = "8b6755b093afef5925f25079dd5a7c8d096398b804ba60cb5275397b06b31689" dependencies = [ "alloy-eip2930", "alloy-eip7702", @@ -259,12 +275,13 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde15e14944a88bd6a57d325e9a49b75558746fe16aaccc79713ae50a6a9574c" +checksum = "aeec8e6eab6e52b7c9f918748c9b811e87dbef7312a2e3a2ca1729a92966a6af" dependencies = [ "alloy-primitives", "alloy-serde", + "alloy-trie", "serde", ] @@ -282,29 +299,31 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af5979e0d5a7bf9c7eb79749121e8256e59021af611322aee56e77e20776b4b3" +checksum = "4fa077efe0b834bcd89ff4ba547f48fb081e4fdc3673dd7da1b295a2cf2bb7b7" dependencies = [ "alloy-primitives", "alloy-sol-types", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.6", "tracing", ] [[package]] name = "alloy-network" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "204237129086ce5dc17a58025e93739b01b45313841f98fa339eb1d780511e57" +checksum = "209a1882a08e21aca4aac6e2a674dc6fcf614058ef8cb02947d63782b1899552" dependencies = [ "alloy-consensus", + "alloy-consensus-any", "alloy-eips", "alloy-json-rpc", "alloy-network-primitives", "alloy-primitives", + "alloy-rpc-types-any", "alloy-rpc-types-eth", "alloy-serde", "alloy-signer", @@ -312,14 +331,16 @@ dependencies = [ "async-trait", "auto_impl", "futures-utils-wasm", - "thiserror 1.0.69", + "serde", + "serde_json", + "thiserror 2.0.6", ] [[package]] name = "alloy-network-primitives" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514f70ee2a953db21631cd817b13a1571474ec77ddc03d47616d5e8203489fde" +checksum = "c20219d1ad261da7a6331c16367214ee7ded41d001fabbbd656fbf71898b2773" dependencies = [ "alloy-consensus", "alloy-eips", @@ -330,9 +351,9 @@ dependencies = [ [[package]] name = "alloy-node-bindings" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27444ea67d360508753022807cdd0b49a95c878924c9c5f8f32668b7d7768245" +checksum = "bffcf33dd319f21cd6f066d81cbdef0326d4bdaaf7cfe91110bc090707858e9f" dependencies = [ "alloy-genesis", "alloy-primitives", @@ -340,7 +361,7 @@ dependencies = [ "rand 0.8.5", "serde_json", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.6", "tracing", "url", ] @@ -375,9 +396,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4814d141ede360bb6cd1b4b064f1aab9de391e7c4d0d4d50ac89ea4bc1e25fbd" +checksum = "9eefa6f4c798ad01f9b4202d02cea75f5ec11fa180502f4701e2b47965a8c0bb" dependencies = [ "alloy-chains", "alloy-consensus", @@ -407,11 +428,11 @@ dependencies = [ "schnellru", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.6", "tokio", "tracing", "url", - "wasmtimer", + "wasmtimer 0.4.1", ] [[package]] @@ -438,9 +459,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc2bd1e7403463a5f2c61e955bcc9d3072b63aa177442b0f9aa6a6d22a941e3" +checksum = "ed30bf1041e84cabc5900f52978ca345dd9969f2194a945e6fdec25b0620705c" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -456,17 +477,16 @@ dependencies = [ "tower 0.5.2", "tracing", "url", - "wasmtimer", + "wasmtimer 0.4.1", ] [[package]] name = "alloy-rpc-types" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea9bf1abdd506f985a53533f5ac01296bcd6102c5e139bbc5d40bc468d2c916" +checksum = "5ab686b0fa475d2a4f5916c5f07797734a691ec58e44f0f55d4746ea39cbcefb" dependencies = [ "alloy-primitives", - "alloy-rpc-types-anvil", "alloy-rpc-types-eth", "alloy-serde", "serde", @@ -474,22 +494,35 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2382fc63fb0cf3e02818d547b80cb66cc49a31f8803d0c328402b2008bc13650" +checksum = "d33bc190844626c08e21897736dbd7956ab323c09e6f141b118d1c8b7aff689e" dependencies = [ "alloy-primitives", + "alloy-rpc-types-eth", "alloy-serde", "serde", ] +[[package]] +name = "alloy-rpc-types-any" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200661999b6e235d9840be5d60a6e8ae2f0af9eb2a256dd378786744660e36ec" +dependencies = [ + "alloy-consensus-any", + "alloy-rpc-types-eth", + "alloy-serde", +] + [[package]] name = "alloy-rpc-types-eth" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b034779a4850b4b03f5be5ea674a1cf7d746b2da762b34d1860ab45e48ca27" +checksum = "a0600b8b5e2dc0cab12cbf91b5a885c35871789fb7b3a57b434bd4fced5b7a8b" dependencies = [ "alloy-consensus", + "alloy-consensus-any", "alloy-eips", "alloy-network-primitives", "alloy-primitives", @@ -504,9 +537,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028e72eaa9703e4882344983cfe7636ce06d8cce104a78ea62fd19b46659efc4" +checksum = "9afa753a97002a33b2ccb707d9f15f31c81b8c1b786c95b73cc62bb1d1fd0c3f" dependencies = [ "alloy-primitives", "serde", @@ -515,23 +548,23 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592c185d7100258c041afac51877660c7bf6213447999787197db4842f0e938e" +checksum = "9b2cbff01a673936c2efd7e00d4c0e9a4dbbd6d600e2ce298078d33efbb19cd7" dependencies = [ "alloy-primitives", "async-trait", "auto_impl", "elliptic-curve 0.13.8", "k256", - "thiserror 1.0.69", + "thiserror 2.0.6", ] [[package]] name = "alloy-signer-local" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6614f02fc1d5b079b2a4a5320018317b506fd0a6d67c1fd5542a71201724986c" +checksum = "bd6d988cb6cd7d2f428a74476515b1a6e901e08c796767f9f93311ab74005c8b" dependencies = [ "alloy-consensus", "alloy-network", @@ -540,7 +573,7 @@ dependencies = [ "async-trait", "k256", "rand 0.8.5", - "thiserror 1.0.69", + "thiserror 2.0.6", ] [[package]] @@ -618,9 +651,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be77579633ebbc1266ae6fd7694f75c408beb1aeb6865d0b18f22893c265a061" +checksum = "d69d36982b9e46075ae6b792b0f84208c6c2c15ad49f6c500304616ef67b70e0" dependencies = [ "alloy-json-rpc", "base64 0.22.1", @@ -628,20 +661,20 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.6", "tokio", "tower 0.5.2", "tracing", "url", "wasm-bindgen-futures", - "wasmtimer", + "wasmtimer 0.4.1", ] [[package]] name = "alloy-transport-http" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fd1a5d0827939847983b46f2f79510361f901dc82f8e3c38ac7397af142c6e" +checksum = "2e02ffd5d93ffc51d72786e607c97de3b60736ca3e636ead0ec1f7dce68ea3fd" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -652,6 +685,22 @@ dependencies = [ "url", ] +[[package]] +name = "alloy-trie" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5fd8fea044cc9a8c8a50bb6f28e31f0385d820f116c5b98f6f4e55d6e5590b" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arrayvec", + "derive_more", + "nybbles", + "serde", + "smallvec", + "tracing", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -743,7 +792,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", - "wasmtimer", + "wasmtimer 0.2.1", "wiremock", ] @@ -791,7 +840,6 @@ dependencies = [ name = "ant-evm" version = "0.1.4" dependencies = [ - "alloy", "custom_debug", "evmlib", "hex 0.4.3", @@ -807,7 +855,7 @@ dependencies = [ "tiny-keccak", "tokio", "tracing", - "wasmtimer", + "wasmtimer 0.2.1", "xor_name", ] @@ -855,7 +903,6 @@ name = "ant-networking" version = "0.19.5" dependencies = [ "aes-gcm-siv", - "alloy", "ant-bootstrap", "ant-build-info", "ant-evm", @@ -895,7 +942,7 @@ dependencies = [ "void", "walkdir", "wasm-bindgen-futures", - "wasmtimer", + "wasmtimer 0.2.1", "xor_name", ] @@ -903,7 +950,6 @@ dependencies = [ name = "ant-node" version = "0.112.6" dependencies = [ - "alloy", "ant-bootstrap", "ant-build-info", "ant-evm", @@ -1029,7 +1075,6 @@ dependencies = [ name = "ant-protocol" version = "0.17.15" dependencies = [ - "alloy", "ant-build-info", "ant-evm", "ant-registers", @@ -1303,6 +1348,9 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] [[package]] name = "ascii" @@ -6537,6 +6585,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "nybbles" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f06be0417d97f81fe4e5c86d7d01b392655a9cac9c19a848aa033e18937b23" +dependencies = [ + "alloy-rlp", + "const-hex", + "proptest", + "serde", + "smallvec", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -8900,6 +8961,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "sn_bls_ckd" @@ -10349,6 +10413,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wasmtimer" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0048ad49a55b9deb3953841fa1fc5858f0efbcb7a18868c899a360269fac1b23" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + [[package]] name = "web-sys" version = "0.3.76" diff --git a/ant-evm/Cargo.toml b/ant-evm/Cargo.toml index 9934e550bc..6e184a6ee1 100644 --- a/ant-evm/Cargo.toml +++ b/ant-evm/Cargo.toml @@ -15,7 +15,6 @@ external-signer = ["evmlib/external-signer"] test-utils = [] [dependencies] -alloy = { version = "0.5.3", default-features = false, features = ["std", "reqwest-rustls-tls", "provider-anvil-node", "sol-types", "json", "signers", "contract", "signer-local", "network"] } custom_debug = "~0.6.1" evmlib = { path = "../evmlib", version = "0.1.4" } hex = "~0.4.3" diff --git a/ant-evm/src/data_payments.rs b/ant-evm/src/data_payments.rs index 63f61d9015..48f904f8d4 100644 --- a/ant-evm/src/data_payments.rs +++ b/ant-evm/src/data_payments.rs @@ -6,16 +6,14 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. -use crate::{AttoTokens, EvmError}; -use alloy::primitives::U256; -use evmlib::common::TxHash; +use crate::EvmError; use evmlib::{ common::{Address as RewardsAddress, QuoteHash}, + quoting_metrics::QuotingMetrics, utils::dummy_address, }; use libp2p::{identity::PublicKey, PeerId}; use serde::{Deserialize, Serialize}; -use std::fmt::{Debug, Formatter, Result as FmtResult}; #[cfg(not(target_arch = "wasm32"))] pub use std::time::SystemTime; #[cfg(target_arch = "wasm32")] @@ -28,66 +26,89 @@ pub const QUOTE_EXPIRATION_SECS: u64 = 3600; /// The margin allowed for live_time const LIVE_TIME_MARGIN: u64 = 10; +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)] +pub struct EncodedPeerId(Vec); + +impl EncodedPeerId { + pub fn to_peer_id(&self) -> Result { + PeerId::from_bytes(&self.0) + } +} + +impl From for EncodedPeerId { + fn from(peer_id: PeerId) -> Self { + let bytes = peer_id.to_bytes(); + EncodedPeerId(bytes) + } +} + /// The proof of payment for a data payment #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)] pub struct ProofOfPayment { - /// The Quote we're paying for - pub quote: PaymentQuote, - /// The transaction hash - pub tx_hash: TxHash, + pub peer_quotes: Vec<(EncodedPeerId, PaymentQuote)>, } impl ProofOfPayment { - pub fn to_peer_id_payee(&self) -> Option { - let pub_key = PublicKey::try_decode_protobuf(&self.quote.pub_key).ok()?; - Some(PeerId::from_public_key(&pub_key)) + /// returns a short digest of the proof of payment to use for verification + pub fn digest(&self) -> Vec<(QuoteHash, QuotingMetrics, RewardsAddress)> { + self.peer_quotes + .clone() + .into_iter() + .map(|(_, quote)| (quote.hash(), quote.quoting_metrics, quote.rewards_address)) + .collect() } -} -/// Quoting metrics that got used to generate a quote, or to track peer's status. -#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct QuotingMetrics { - /// the records stored - pub close_records_stored: usize, - /// the max_records configured - pub max_records: usize, - /// number of times that got paid - pub received_payment_count: usize, - /// the duration that node keeps connected to the network, measured in hours - pub live_time: u64, - /// network density from this node's perspective, which is the responsible_range as well - /// This could be calculated via sampling, or equation calculation. - pub network_density: Option<[u8; 32]>, - /// estimated network size - pub network_size: Option, -} + /// returns the list of payees + pub fn payees(&self) -> Vec { + self.peer_quotes + .iter() + .filter_map(|(peer_id, _)| peer_id.to_peer_id().ok()) + .collect() + } -impl Debug for QuotingMetrics { - fn fmt(&self, formatter: &mut Formatter) -> FmtResult { - let density_u256 = self.network_density.map(U256::from_be_bytes); + /// has the quote expired + pub fn has_expired(&self) -> bool { + self.peer_quotes + .iter() + .any(|(_, quote)| quote.has_expired()) + } - write!(formatter, "QuotingMetrics {{ close_records_stored: {}, max_records: {}, received_payment_count: {}, live_time: {}, network_density: {density_u256:?}, network_size: {:?} }}", - self.close_records_stored, self.max_records, self.received_payment_count, self.live_time, self.network_size) + /// Returns all quotes by given peer id + pub fn quotes_by_peer(&self, peer_id: &PeerId) -> Vec<&PaymentQuote> { + self.peer_quotes + .iter() + .filter_map(|(_id, quote)| { + if let Ok(quote_peer_id) = quote.peer_id() { + if *peer_id == quote_peer_id { + return Some(quote); + } + } + None + }) + .collect() } -} -impl QuotingMetrics { - /// construct an empty QuotingMetrics - pub fn new() -> Self { - Self { - close_records_stored: 0, - max_records: 0, - received_payment_count: 0, - live_time: 0, - network_density: None, - network_size: None, + /// verifies the proof of payment is valid for the given peer id + pub fn verify_for(&self, peer_id: PeerId) -> bool { + // make sure I am in the list of payees + if !self.payees().contains(&peer_id) { + return false; } - } -} -impl Default for QuotingMetrics { - fn default() -> Self { - Self::new() + // verify all signatures + for (encoded_peer_id, quote) in self.peer_quotes.iter() { + let peer_id = match encoded_peer_id.to_peer_id() { + Ok(peer_id) => peer_id, + Err(e) => { + warn!("Invalid encoded peer id: {e}"); + return false; + } + }; + if !quote.check_is_signed_by_claimed_peer(peer_id) { + return false; + } + } + true } } @@ -98,17 +119,10 @@ impl Default for QuotingMetrics { pub struct PaymentQuote { /// the content paid for pub content: XorName, - /// how much the node demands for storing the content - /// TODO: to be removed once swtich to `client querying smart_contract` - pub cost: AttoTokens, /// the local node time when the quote was created pub timestamp: SystemTime, /// quoting metrics being used to generate this quote pub quoting_metrics: QuotingMetrics, - /// list of bad_nodes that client shall not pick as a payee - /// in `serialised` format to avoid cyclic dependent on ant_protocol - #[debug(skip)] - pub bad_nodes: Vec, /// the node's wallet address pub rewards_address: RewardsAddress, /// the node's libp2p identity public key in bytes (PeerId) @@ -124,10 +138,8 @@ impl PaymentQuote { pub fn zero() -> Self { Self { content: Default::default(), - cost: AttoTokens::zero(), timestamp: SystemTime::now(), quoting_metrics: Default::default(), - bad_nodes: vec![], rewards_address: dummy_address(), pub_key: vec![], signature: vec![], @@ -144,14 +156,11 @@ impl PaymentQuote { /// returns the bytes to be signed from the given parameters pub fn bytes_for_signing( xorname: XorName, - cost: AttoTokens, timestamp: SystemTime, quoting_metrics: &QuotingMetrics, - serialised_bad_nodes: &[u8], rewards_address: &RewardsAddress, ) -> Vec { let mut bytes = xorname.to_vec(); - bytes.extend_from_slice(&cost.to_bytes()); bytes.extend_from_slice( ×tamp .duration_since(SystemTime::UNIX_EPOCH) @@ -161,7 +170,6 @@ impl PaymentQuote { ); let serialised_quoting_metrics = rmp_serde::to_vec(quoting_metrics).unwrap_or_default(); bytes.extend_from_slice(&serialised_quoting_metrics); - bytes.extend_from_slice(serialised_bad_nodes); bytes.extend_from_slice(rewards_address.as_slice()); bytes } @@ -170,10 +178,8 @@ impl PaymentQuote { pub fn bytes_for_sig(&self) -> Vec { Self::bytes_for_signing( self.content, - self.cost, self.timestamp, &self.quoting_metrics, - &self.bad_nodes, &self.rewards_address, ) } @@ -214,7 +220,7 @@ impl PaymentQuote { true } - /// Returns true) if the quote has not yet expired + /// Returns true if the quote has expired pub fn has_expired(&self) -> bool { let now = SystemTime::now(); @@ -226,13 +232,11 @@ impl PaymentQuote { } /// test utility to create a dummy quote - pub fn test_dummy(xorname: XorName, cost: AttoTokens) -> Self { + pub fn test_dummy(xorname: XorName) -> Self { Self { content: xorname, - cost, timestamp: SystemTime::now(), quoting_metrics: Default::default(), - bad_nodes: vec![], pub_key: vec![], signature: vec![], rewards_address: dummy_address(), @@ -314,6 +318,14 @@ mod tests { use libp2p::identity::Keypair; use std::{thread::sleep, time::Duration}; + #[test] + fn test_encode_decode_peer_id() { + let id = PeerId::random(); + let encoded = EncodedPeerId::from(id); + let decoded = encoded.to_peer_id().expect("decode to work"); + assert_eq!(id, decoded); + } + #[test] fn test_is_newer_than() { let old_quote = PaymentQuote::zero(); diff --git a/ant-evm/src/lib.rs b/ant-evm/src/lib.rs index 45185101fb..ece2c36083 100644 --- a/ant-evm/src/lib.rs +++ b/ant-evm/src/lib.rs @@ -12,7 +12,9 @@ extern crate tracing; pub use evmlib::common::Address as RewardsAddress; pub use evmlib::common::Address as EvmAddress; pub use evmlib::common::QuotePayment; +pub use evmlib::common::U256; pub use evmlib::common::{QuoteHash, TxHash}; +pub use evmlib::contract::payment_vault; pub use evmlib::cryptography; #[cfg(feature = "external-signer")] pub use evmlib::external_signer; @@ -28,7 +30,8 @@ mod amount; mod data_payments; mod error; -pub use data_payments::{PaymentQuote, ProofOfPayment, QuotingMetrics, QUOTE_EXPIRATION_SECS}; +pub use data_payments::{EncodedPeerId, PaymentQuote, ProofOfPayment, QUOTE_EXPIRATION_SECS}; +pub use evmlib::quoting_metrics::QuotingMetrics; /// Types used in the public API pub use amount::{Amount, AttoTokens}; diff --git a/ant-networking/Cargo.toml b/ant-networking/Cargo.toml index b483cadb5d..8849a3752b 100644 --- a/ant-networking/Cargo.toml +++ b/ant-networking/Cargo.toml @@ -20,7 +20,6 @@ upnp = ["libp2p/upnp"] [dependencies] aes-gcm-siv = "0.11.1" -alloy = { version = "0.5.3", default-features = false, features = ["std", "reqwest-rustls-tls", "provider-anvil-node", "sol-types", "json", "signers", "contract", "signer-local", "network"] } ant-bootstrap = { path = "../ant-bootstrap", version = "0.1.0" } ant-build-info = { path = "../ant-build-info", version = "0.1.19" } ant-evm = { path = "../ant-evm", version = "0.1.4" } diff --git a/ant-networking/src/cmd.rs b/ant-networking/src/cmd.rs index c30416aa37..9a694f0650 100644 --- a/ant-networking/src/cmd.rs +++ b/ant-networking/src/cmd.rs @@ -13,8 +13,7 @@ use crate::{ log_markers::Marker, multiaddr_pop_p2p, GetRecordCfg, GetRecordError, MsgResponder, NetworkEvent, CLOSE_GROUP_SIZE, }; -use alloy::primitives::U256; -use ant_evm::{AttoTokens, PaymentQuote, QuotingMetrics}; +use ant_evm::{PaymentQuote, QuotingMetrics, U256}; use ant_protocol::{ convert_distance_to_u256, messages::{Cmd, Request, Response}, @@ -100,10 +99,11 @@ pub enum LocalSwarmCmd { key: RecordKey, sender: oneshot::Sender>, }, - /// GetLocalStoreCost for this node, also with the bad_node list close to the target - GetLocalStoreCost { + /// GetLocalQuotingMetrics for this node + /// Returns the quoting metrics and whether the record at `key` is already stored locally + GetLocalQuotingMetrics { key: RecordKey, - sender: oneshot::Sender<(AttoTokens, QuotingMetrics, Vec)>, + sender: oneshot::Sender<(QuotingMetrics, bool)>, }, /// Notify the node received a payment. PaymentReceived, @@ -243,8 +243,8 @@ impl Debug for LocalSwarmCmd { "LocalSwarmCmd::GetCloseGroupLocalPeers {{ key: {key:?} }}" ) } - LocalSwarmCmd::GetLocalStoreCost { .. } => { - write!(f, "LocalSwarmCmd::GetLocalStoreCost") + LocalSwarmCmd::GetLocalQuotingMetrics { .. } => { + write!(f, "LocalSwarmCmd::GetLocalQuotingMetrics") } LocalSwarmCmd::PaymentReceived => { write!(f, "LocalSwarmCmd::PaymentReceived") @@ -575,8 +575,8 @@ impl SwarmDriver { cmd_string = "TriggerIntervalReplication"; self.try_interval_replication()?; } - LocalSwarmCmd::GetLocalStoreCost { key, sender } => { - cmd_string = "GetLocalStoreCost"; + LocalSwarmCmd::GetLocalQuotingMetrics { key, sender } => { + cmd_string = "GetLocalQuotingMetrics"; let ( _index, _total_peers, @@ -586,15 +586,14 @@ impl SwarmDriver { ) = self.kbuckets_status(); let estimated_network_size = Self::estimate_network_size(peers_in_non_full_buckets, num_of_full_buckets); - let (cost, quoting_metrics) = self + let (quoting_metrics, is_already_stored) = self .swarm .behaviour_mut() .kademlia .store_mut() - .store_cost(&key, Some(estimated_network_size as u64)); + .quoting_metrics(&key, Some(estimated_network_size as u64)); - self.record_metrics(Marker::StoreCost { - cost: cost.as_atto(), + self.record_metrics(Marker::QuotingMetrics { quoting_metrics: "ing_metrics, }); @@ -632,7 +631,7 @@ impl SwarmDriver { .retain(|peer_addr| key_address.distance(peer_addr) < boundary_distance); } - let _res = sender.send((cost, quoting_metrics, bad_nodes)); + let _res = sender.send((quoting_metrics, is_already_stored)); } LocalSwarmCmd::PaymentReceived => { cmd_string = "PaymentReceived"; diff --git a/ant-networking/src/driver.rs b/ant-networking/src/driver.rs index e1ac2d3d13..4534b49110 100644 --- a/ant-networking/src/driver.rs +++ b/ant-networking/src/driver.rs @@ -29,9 +29,8 @@ use crate::{ use crate::{ metrics::service::run_metrics_server, metrics::NetworkMetricsRecorder, MetricsRegistries, }; -use alloy::primitives::U256; use ant_bootstrap::BootstrapCacheStore; -use ant_evm::PaymentQuote; +use ant_evm::{PaymentQuote, U256}; use ant_protocol::{ convert_distance_to_u256, messages::{ChunkProof, Nonce, Request, Response}, diff --git a/ant-networking/src/error.rs b/ant-networking/src/error.rs index 9835e8f1d2..c683ff4432 100644 --- a/ant-networking/src/error.rs +++ b/ant-networking/src/error.rs @@ -178,7 +178,7 @@ pub enum NetworkError { OutgoingResponseDropped(Response), #[error("Error setting up behaviour: {0}")] - BahviourErr(String), + BehaviourErr(String), #[error("Register already exists at this address")] RegisterAlreadyExists, diff --git a/ant-networking/src/event/request_response.rs b/ant-networking/src/event/request_response.rs index d7a210821b..ce6755e8dc 100644 --- a/ant-networking/src/event/request_response.rs +++ b/ant-networking/src/event/request_response.rs @@ -48,30 +48,6 @@ impl SwarmDriver { self.add_keys_to_replication_fetcher(holder, keys); } - Request::Cmd(ant_protocol::messages::Cmd::QuoteVerification { - quotes, - .. - }) => { - let response = Response::Cmd( - ant_protocol::messages::CmdResponse::QuoteVerification(Ok(())), - ); - self.queue_network_swarm_cmd(NetworkSwarmCmd::SendResponse { - resp: response, - channel: MsgResponder::FromPeer(channel), - }); - - // The keypair is required to verify the quotes, - // hence throw it up to Network layer for further actions. - let quotes = quotes - .iter() - .filter_map(|(peer_address, quote)| { - peer_address - .as_peer_id() - .map(|peer_id| (peer_id, quote.clone())) - }) - .collect(); - self.send_event(NetworkEvent::QuoteVerification { quotes }) - } Request::Cmd(ant_protocol::messages::Cmd::PeerConsideredAsBad { detected_by, bad_peer, diff --git a/ant-networking/src/lib.rs b/ant-networking/src/lib.rs index cfe81e6b0b..a02767594c 100644 --- a/ant-networking/src/lib.rs +++ b/ant-networking/src/lib.rs @@ -40,7 +40,7 @@ pub use self::{ }, error::{GetRecordError, NetworkError}, event::{MsgResponder, NetworkEvent}, - record_store::{calculate_cost_for_records, NodeRecordStore}, + record_store::NodeRecordStore, transactions::get_transactions_from_record, }; #[cfg(feature = "open-metrics")] @@ -48,10 +48,10 @@ pub use metrics::service::MetricsRegistries; pub use target_arch::{interval, sleep, spawn, Instant, Interval}; use self::{cmd::NetworkSwarmCmd, error::Result}; -use ant_evm::{AttoTokens, PaymentQuote, QuotingMetrics, RewardsAddress}; +use ant_evm::{PaymentQuote, QuotingMetrics}; use ant_protocol::{ error::Error as ProtocolError, - messages::{ChunkProof, Cmd, Nonce, Query, QueryResponse, Request, Response}, + messages::{ChunkProof, Nonce, Query, QueryResponse, Request, Response}, storage::{RecordType, RetryStrategy, Scratchpad}, NetworkAddress, PrettyPrintKBucketKey, PrettyPrintRecordKey, CLOSE_GROUP_SIZE, }; @@ -83,9 +83,6 @@ use { std::collections::HashSet, }; -/// The type of quote for a selected payee. -pub type PayeeQuote = (PeerId, RewardsAddress, PaymentQuote); - /// Majority of a given group (i.e. > 1/2). #[inline] pub const fn close_group_majority() -> usize { @@ -378,11 +375,11 @@ impl Network { /// /// Ignore the quote from any peers from `ignore_peers`. /// This is useful if we want to repay a different PeerId on failure. - pub async fn get_store_costs_from_network( + pub async fn get_store_quote_from_network( &self, record_address: NetworkAddress, ignore_peers: Vec, - ) -> Result { + ) -> Result> { // The requirement of having at least CLOSE_GROUP_SIZE // close nodes will be checked internally automatically. let mut close_nodes = self @@ -392,12 +389,12 @@ impl Network { close_nodes.retain(|peer_id| !ignore_peers.contains(peer_id)); if close_nodes.is_empty() { - error!("Cann't get store_cost of {record_address:?}, as all close_nodes are ignored"); + error!("Can't get store_cost of {record_address:?}, as all close_nodes are ignored"); return Err(NetworkError::NoStoreCostResponses); } // Client shall decide whether to carry out storage verification or not. - let request = Request::Query(Query::GetStoreCost { + let request = Request::Query(Query::GetStoreQuote { key: record_address.clone(), nonce: None, difficulty: 0, @@ -406,64 +403,58 @@ impl Network { .send_and_get_responses(&close_nodes, &request, true) .await; - // loop over responses, generating an average fee and storing all responses along side - let mut all_costs = vec![]; + // consider data to be already paid for if 1/2 of the close nodes already have it + let mut peer_already_have_it = 0; + let enough_peers_already_have_it = close_nodes.len() / 2; + + // loop over responses let mut all_quotes = vec![]; - for response in responses.into_values().flatten() { - info!( - "StoreCostReq for {record_address:?} received response: {:?}", - response - ); + let mut quotes_to_pay = vec![]; + for (peer, response) in responses { + info!("StoreCostReq for {record_address:?} received response: {response:?}"); match response { - Response::Query(QueryResponse::GetStoreCost { + Ok(Response::Query(QueryResponse::GetStoreQuote { quote: Ok(quote), - payment_address, peer_address, storage_proofs, - }) => { + })) => { if !storage_proofs.is_empty() { - debug!("Storage proofing during GetStoreCost to be implemented."); + debug!("Storage proofing during GetStoreQuote to be implemented."); } // Check the quote itself is valid. - if quote.cost - != AttoTokens::from_u64(calculate_cost_for_records( - quote.quoting_metrics.close_records_stored, - )) - { + if !quote.check_is_signed_by_claimed_peer(peer) { warn!("Received invalid quote from {peer_address:?}, {quote:?}"); continue; } - all_costs.push((peer_address.clone(), payment_address, quote.clone())); - all_quotes.push((peer_address, quote)); + all_quotes.push((peer_address.clone(), quote.clone())); + quotes_to_pay.push((peer, quote)); } - Response::Query(QueryResponse::GetStoreCost { + Ok(Response::Query(QueryResponse::GetStoreQuote { quote: Err(ProtocolError::RecordExists(_)), - payment_address, peer_address, storage_proofs, - }) => { + })) => { if !storage_proofs.is_empty() { - debug!("Storage proofing during GetStoreCost to be implemented."); + debug!("Storage proofing during GetStoreQuote to be implemented."); + } + peer_already_have_it += 1; + info!("Address {record_address:?} was already paid for according to {peer_address:?} ({peer_already_have_it}/{enough_peers_already_have_it})"); + if peer_already_have_it >= enough_peers_already_have_it { + info!("Address {record_address:?} was already paid for according to {peer_already_have_it} peers, ending quote request"); + return Ok(vec![]); } - all_costs.push((peer_address, payment_address, PaymentQuote::zero())); + } + Err(err) => { + error!("Got an error while requesting quote from peer {peer:?}: {err}"); } _ => { - error!("Non store cost response received, was {:?}", response); + error!("Got an unexpected response while requesting quote from peer {peer:?}: {response:?}"); } } } - for peer_id in close_nodes.iter() { - let request = Request::Cmd(Cmd::QuoteVerification { - target: NetworkAddress::from_peer(*peer_id), - quotes: all_quotes.clone(), - }); - - self.send_req_ignore_reply(request, *peer_id); - } - - get_fees_from_store_cost_responses(all_costs) + Ok(quotes_to_pay) } /// Get register from network. @@ -776,13 +767,13 @@ impl Network { Ok(None) } - /// Get the cost of storing the next record from the network - pub async fn get_local_storecost( + /// Get the quoting metrics for storing the next record from the network + pub async fn get_local_quoting_metrics( &self, key: RecordKey, - ) -> Result<(AttoTokens, QuotingMetrics, Vec)> { + ) -> Result<(QuotingMetrics, bool)> { let (sender, receiver) = oneshot::channel(); - self.send_local_swarm_cmd(LocalSwarmCmd::GetLocalStoreCost { key, sender }); + self.send_local_swarm_cmd(LocalSwarmCmd::GetLocalQuotingMetrics { key, sender }); receiver .await @@ -1209,42 +1200,6 @@ impl Network { } } -/// Given `all_costs` it will return the closest / lowest cost -/// Closest requiring it to be within CLOSE_GROUP nodes -fn get_fees_from_store_cost_responses( - all_costs: Vec<(NetworkAddress, RewardsAddress, PaymentQuote)>, -) -> Result { - // Find the minimum cost using a linear scan with random tie break - let mut rng = rand::thread_rng(); - let payee = all_costs - .into_iter() - .min_by( - |(_address_a, _main_key_a, cost_a), (_address_b, _main_key_b, cost_b)| { - let cmp = cost_a.cost.cmp(&cost_b.cost); - if cmp == std::cmp::Ordering::Equal { - if rng.gen() { - std::cmp::Ordering::Less - } else { - std::cmp::Ordering::Greater - } - } else { - cmp - } - }, - ) - .ok_or(NetworkError::NoStoreCostResponses)?; - - info!("Final fees calculated as: {payee:?}"); - // we dont need to have the address outside of here for now - let payee_id = if let Some(peer_id) = payee.0.as_peer_id() { - peer_id - } else { - error!("Can't get PeerId from payee {:?}", payee.0); - return Err(NetworkError::NoStoreCostResponses); - }; - Ok((payee_id, payee.1, payee.2)) -} - /// Get the value of the provided Quorum pub fn get_quorum_value(quorum: &Quorum) -> usize { match quorum { @@ -1369,69 +1324,7 @@ pub(crate) fn send_network_swarm_cmd( #[cfg(test)] mod tests { - use eyre::bail; - use super::*; - use ant_evm::PaymentQuote; - - #[test] - fn test_get_fee_from_store_cost_responses() -> Result<()> { - // for a vec of different costs of CLOSE_GROUP size - // ensure we return the CLOSE_GROUP / 2 indexed price - let mut costs = vec![]; - for i in 1..CLOSE_GROUP_SIZE { - let addr = ant_evm::utils::dummy_address(); - costs.push(( - NetworkAddress::from_peer(PeerId::random()), - addr, - PaymentQuote::test_dummy(Default::default(), AttoTokens::from_u64(i as u64)), - )); - } - let expected_price = costs[0].2.cost.as_atto(); - let (_peer_id, _key, price) = get_fees_from_store_cost_responses(costs)?; - - assert_eq!( - price.cost.as_atto(), - expected_price, - "price should be {expected_price}" - ); - - Ok(()) - } - - #[test] - fn test_get_some_fee_from_store_cost_responses_even_if_one_errs_and_sufficient( - ) -> eyre::Result<()> { - // for a vec of different costs of CLOSE_GROUP size - let responses_count = CLOSE_GROUP_SIZE as u64 - 1; - let mut costs = vec![]; - for i in 1..responses_count { - // push random addr and Nano - let addr = ant_evm::utils::dummy_address(); - costs.push(( - NetworkAddress::from_peer(PeerId::random()), - addr, - PaymentQuote::test_dummy(Default::default(), AttoTokens::from_u64(i)), - )); - println!("price added {i}"); - } - - // this should be the lowest price - let expected_price = costs[0].2.cost.as_atto(); - - let (_peer_id, _key, price) = match get_fees_from_store_cost_responses(costs) { - Err(_) => bail!("Should not have errored as we have enough responses"), - Ok(cost) => cost, - }; - - assert_eq!( - price.cost.as_atto(), - expected_price, - "price should be {expected_price}" - ); - - Ok(()) - } #[test] fn test_network_sign_verify() -> eyre::Result<()> { diff --git a/ant-networking/src/log_markers.rs b/ant-networking/src/log_markers.rs index 99bcd6726d..71787c0a65 100644 --- a/ant-networking/src/log_markers.rs +++ b/ant-networking/src/log_markers.rs @@ -6,7 +6,7 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. -use ant_evm::{Amount, QuotingMetrics}; +use ant_evm::QuotingMetrics; use libp2p::PeerId; // this gets us to_string easily enough use strum::Display; @@ -19,12 +19,8 @@ use strum::Display; pub enum Marker<'a> { /// Close records held (Used in VDash) CloseRecordsLen(usize), - /// Store cost - StoreCost { - /// Cost - cost: Amount, - quoting_metrics: &'a QuotingMetrics, - }, + /// Quoting metrics + QuotingMetrics { quoting_metrics: &'a QuotingMetrics }, /// The peer has been considered as bad PeerConsideredAsBad { bad_peer: &'a PeerId }, /// We have been flagged as a bad node by a peer. diff --git a/ant-networking/src/metrics/mod.rs b/ant-networking/src/metrics/mod.rs index 43a5b73f16..ef9f636bcb 100644 --- a/ant-networking/src/metrics/mod.rs +++ b/ant-networking/src/metrics/mod.rs @@ -45,8 +45,7 @@ pub(crate) struct NetworkMetricsRecorder { pub(crate) peers_in_routing_table: Gauge, pub(crate) records_stored: Gauge, - // store cost - store_cost: Gauge, + // quoting metrics relevant_records: Gauge, max_records: Gauge, received_payment_count: Gauge, @@ -149,13 +148,7 @@ impl NetworkMetricsRecorder { process_cpu_usage_percentage.clone(), ); - // store cost - let store_cost = Gauge::default(); - sub_registry.register( - "store_cost", - "The store cost of the node", - store_cost.clone(), - ); + // quoting metrics let relevant_records = Gauge::default(); sub_registry.register( "relevant_records", @@ -222,7 +215,6 @@ impl NetworkMetricsRecorder { connected_peers, open_connections, peers_in_routing_table, - store_cost, relevant_records, max_records, received_payment_count, @@ -292,11 +284,7 @@ impl NetworkMetricsRecorder { } }); } - Marker::StoreCost { - cost, - quoting_metrics, - } => { - let _ = self.store_cost.set(cost.try_into().unwrap_or(i64::MAX)); + Marker::QuotingMetrics { quoting_metrics } => { let _ = self.relevant_records.set( quoting_metrics .close_records_stored diff --git a/ant-networking/src/record_store.rs b/ant-networking/src/record_store.rs index 115b9dc727..b4ab4ff6b3 100644 --- a/ant-networking/src/record_store.rs +++ b/ant-networking/src/record_store.rs @@ -16,8 +16,7 @@ use aes_gcm_siv::{ aead::{Aead, KeyInit}, Aes256GcmSiv, Key as AesKey, Nonce, }; -use alloy::primitives::U256; -use ant_evm::{AttoTokens, QuotingMetrics}; +use ant_evm::{QuotingMetrics, U256}; use ant_protocol::{ convert_distance_to_u256, storage::{RecordHeader, RecordKind, RecordType}, @@ -63,12 +62,6 @@ const MAX_RECORDS_CACHE_SIZE: usize = 25; /// File name of the recorded historical quoting metrics. const HISTORICAL_QUOTING_METRICS_FILENAME: &str = "historic_quoting_metrics"; -/// Max store cost for a chunk. -const MAX_STORE_COST: u64 = 1_000_000; - -// Min store cost for a chunk. -const MIN_STORE_COST: u64 = 1; - fn derive_aes256gcm_siv_from_seed(seed: &[u8; 16]) -> (Aes256GcmSiv, [u8; 4]) { // shall be unique for purpose. let salt = b"autonomi_record_store"; @@ -727,12 +720,13 @@ impl NodeRecordStore { Ok(()) } - /// Calculate the cost to store data for our current store state - pub(crate) fn store_cost( + /// Return the quoting metrics used to calculate the cost of storing a record + /// and whether the record is already stored locally + pub(crate) fn quoting_metrics( &self, key: &Key, network_size: Option, - ) -> (AttoTokens, QuotingMetrics) { + ) -> (QuotingMetrics, bool) { let records_stored = self.records.len(); let live_time = if let Ok(elapsed) = self.timestamp.elapsed() { @@ -761,15 +755,12 @@ impl NodeRecordStore { info!("Basing cost of _total_ records stored."); }; - let cost = if self.contains(key) { - 0 - } else { - calculate_cost_for_records(quoting_metrics.close_records_stored) - }; + // NB TODO tell happybeing! // vdash metric (if modified please notify at https://github.com/happybeing/vdash/issues): - info!("Cost is now {cost:?} for quoting_metrics {quoting_metrics:?}"); + info!("Quoting_metrics {quoting_metrics:?}"); - (AttoTokens::from_u64(cost), quoting_metrics) + let is_stored = self.contains(key); + (quoting_metrics, is_stored) } /// Notify the node received a payment. @@ -1005,39 +996,13 @@ impl RecordStore for ClientRecordStore { fn remove_provider(&mut self, _key: &Key, _provider: &PeerId) {} } -// Using a linear growth function tweaked by `max_records`, -// and gives an exponential pricing curve when storage reaches high. -// and give extra reward (lower the quoting price to gain a better chance) to long lived nodes. -pub fn calculate_cost_for_records(records_stored: usize) -> u64 { - use std::cmp::{max, min}; - - let max_records = MAX_RECORDS_COUNT; - - let ori_cost = positive_input_0_1_sigmoid(records_stored as f64 / max_records as f64) - * MAX_STORE_COST as f64; - - // Deploy a lower cap safe_guard to the store_cost - let charge = max(MIN_STORE_COST, ori_cost as u64); - // Deploy an upper cap safe_guard to the store_cost - min(MAX_STORE_COST, charge) -} - -fn positive_input_0_1_sigmoid(x: f64) -> f64 { - 1.0 / (1.0 + (-30.0 * (x - 0.5)).exp()) -} - #[expect(trivial_casts)] #[cfg(test)] mod tests { - - use crate::get_fees_from_store_cost_responses; - use super::*; use bls::SecretKey; use xor_name::XorName; - use ant_evm::utils::dummy_address; - use ant_evm::{PaymentQuote, RewardsAddress}; use ant_protocol::convert_distance_to_u256; use ant_protocol::storage::{ try_deserialize_record, try_serialize_record, Chunk, ChunkAddress, Scratchpad, @@ -1047,12 +1012,9 @@ mod tests { TempDir, }; use bytes::Bytes; - use eyre::{bail, ContextCompat}; - use libp2p::kad::K_VALUE; + use eyre::ContextCompat; use libp2p::{core::multihash::Multihash, kad::RecordKey}; use quickcheck::*; - use std::collections::BTreeMap; - use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use tokio::runtime::Runtime; use tokio::time::{sleep, Duration}; @@ -1091,70 +1053,6 @@ mod tests { } } - #[test] - fn test_calculate_max_cost_for_records() { - let sut = calculate_cost_for_records(MAX_RECORDS_COUNT + 1); - assert_eq!(sut, MAX_STORE_COST - 1); - } - - #[test] - fn test_calculate_50_percent_cost_for_records() { - let percent = MAX_RECORDS_COUNT * 50 / 100; - let sut = calculate_cost_for_records(percent); - - // at this point we should be at max cost - assert_eq!(sut, 500000); - } - #[test] - fn test_calculate_60_percent_cost_for_records() { - let percent = MAX_RECORDS_COUNT * 60 / 100; - let sut = calculate_cost_for_records(percent); - - // at this point we should be at max cost - assert_eq!(sut, 952541); - } - - #[test] - fn test_calculate_65_percent_cost_for_records() { - let percent = MAX_RECORDS_COUNT * 65 / 100; - let sut = calculate_cost_for_records(percent); - - // at this point we should be at max cost - assert_eq!(sut, 989001); - } - - #[test] - fn test_calculate_70_percent_cost_for_records() { - let percent = MAX_RECORDS_COUNT * 70 / 100; - let sut = calculate_cost_for_records(percent); - - // at this point we should be at max cost - assert_eq!(sut, 997523); - } - - #[test] - fn test_calculate_80_percent_cost_for_records() { - let percent = MAX_RECORDS_COUNT * 80 / 100; - let sut = calculate_cost_for_records(percent); - - // at this point we should be at max cost - assert_eq!(sut, 999876); - } - - #[test] - fn test_calculate_90_percent_cost_for_records() { - let percent = MAX_RECORDS_COUNT * 90 / 100; - let sut = calculate_cost_for_records(percent); - // at this point we should be at max cost - assert_eq!(sut, 999993); - } - - #[test] - fn test_calculate_min_cost_for_records() { - let sut = calculate_cost_for_records(0); - assert_eq!(sut, MIN_STORE_COST); - } - #[test] fn put_get_remove_record() { fn prop(r: ArbitraryRecord) { @@ -1180,16 +1078,9 @@ mod tests { swarm_cmd_sender, ); - let store_cost_before = store.store_cost(&r.key, None); // An initial unverified put should not write to disk assert!(store.put(r.clone()).is_ok()); assert!(store.get(&r.key).is_none()); - // Store cost should not change if no PUT has been added - assert_eq!( - store.store_cost(&r.key, None).0, - store_cost_before.0, - "store cost should not change over unverified put" - ); let returned_record = if let Some(event) = network_event_receiver.recv().await { if let NetworkEvent::UnverifiedRecord(record) = event { @@ -1740,255 +1631,4 @@ mod tests { Ok(()) } - - struct PeerStats { - address: NetworkAddress, - rewards_addr: RewardsAddress, - records_stored: AtomicUsize, - nanos_earned: AtomicU64, - payments_received: AtomicUsize, - } - - // takes a long time to run - #[ignore] - #[test] - fn address_distribution_sim() { - use rayon::prelude::*; - - // as network saturates, we can see that peers all eventually earn similarly - let num_of_peers = 5_000; - let num_of_chunks_per_hour = 1_000_000; - let max_hours = 50; - - // - let k = K_VALUE.get(); - - let replication_group_size = k / 3; - - // Initialize peers with random addresses - let mut peers: Vec = (0..num_of_peers) - .into_par_iter() - .map(|_| PeerStats { - address: NetworkAddress::from_peer(PeerId::random()), - records_stored: AtomicUsize::new(0), - nanos_earned: AtomicU64::new(0), - payments_received: AtomicUsize::new(0), - rewards_addr: dummy_address(), - }) - .collect(); - - let mut hour = 0; - let mut total_received_payment_count = 0; - - let peers_len = peers.len(); - - // Generate a random sorting target address - let sorting_target_address = - NetworkAddress::from_chunk_address(ChunkAddress::new(XorName::default())); - - // Sort all peers based on their distance to the sorting target - peers.par_sort_by(|a, b| { - sorting_target_address - .distance(&a.address) - .cmp(&sorting_target_address.distance(&b.address)) - }); - - loop { - // Parallel processing of chunks - let _chunk_results: Vec<_> = (0..num_of_chunks_per_hour) - .into_par_iter() - .map(|_| { - // Generate a random chunk address - let name = xor_name::rand::random(); - let chunk_address = NetworkAddress::from_chunk_address(ChunkAddress::new(name)); - - let chunk_distance_to_sorting = sorting_target_address.distance(&chunk_address); - // Binary search to find the insertion point for the chunk - let partition_point = peers.partition_point(|peer| { - sorting_target_address.distance(&peer.address) < chunk_distance_to_sorting - }); - - // Collect close_group_size closest peers - let mut close_group = Vec::with_capacity(replication_group_size); - let mut left = partition_point; - let mut right = partition_point; - - while close_group.len() < replication_group_size - && (left > 0 || right < peers_len) - { - if left > 0 { - left -= 1; - close_group.push(left); - } - if close_group.len() < replication_group_size && right < peers_len { - close_group.push(right); - right += 1; - } - } - - // Truncate to ensure we have exactly close_group_size peers - close_group.truncate(replication_group_size); - - // Find the cheapest payee among the close group - let Ok((payee_index, cost)) = pick_cheapest_payee(&peers, &close_group) else { - bail!("Failed to find a payee"); - }; - - for &peer_index in &close_group { - let peer = &peers[peer_index]; - peer.records_stored.fetch_add(1, Ordering::Relaxed); - - if peer_index == payee_index { - peer.nanos_earned.fetch_add( - cost.as_atto().try_into().unwrap_or(u64::MAX), - Ordering::Relaxed, - ); - peer.payments_received.fetch_add(1, Ordering::Relaxed); - } - } - - Ok(()) - }) - .collect(); - - // Parallel reduction to calculate statistics - let ( - received_payment_count, - empty_earned_nodes, - min_earned, - max_earned, - min_store_cost, - max_store_cost, - ) = peers - .par_iter() - .map(|peer| { - let cost = - calculate_cost_for_records(peer.records_stored.load(Ordering::Relaxed)); - let earned = peer.nanos_earned.load(Ordering::Relaxed); - ( - peer.payments_received.load(Ordering::Relaxed), - if earned == 0 { 1 } else { 0 }, - earned, - earned, - cost, - cost, - ) - }) - .reduce( - || (0, 0, u64::MAX, 0, u64::MAX, 0), - |a, b| { - let ( - a_received_payment_count, - a_empty_earned_nodes, - a_min_earned, - a_max_earned, - a_min_store_cost, - a_max_store_cost, - ) = a; - let ( - b_received_payment_count, - b_empty_earned_nodes, - b_min_earned, - b_max_earned, - b_min_store_cost, - b_max_store_cost, - ) = b; - ( - a_received_payment_count + b_received_payment_count, - a_empty_earned_nodes + b_empty_earned_nodes, - a_min_earned.min(b_min_earned), - a_max_earned.max(b_max_earned), - a_min_store_cost.min(b_min_store_cost), - a_max_store_cost.max(b_max_store_cost), - ) - }, - ); - - total_received_payment_count += num_of_chunks_per_hour; - assert_eq!(total_received_payment_count, received_payment_count); - - println!("After the completion of hour {hour} with {num_of_chunks_per_hour} chunks put, there are {empty_earned_nodes} nodes which earned nothing"); - println!("\t\t with storecost variation of (min {min_store_cost} - max {max_store_cost}), and earned variation of (min {min_earned} - max {max_earned})"); - - hour += 1; - - // Check termination condition - if hour == max_hours { - let acceptable_percentage = 0.01; //% - - // Calculate acceptable empty nodes based on % of total nodes - let acceptable_empty_nodes = - (num_of_peers as f64 * acceptable_percentage).ceil() as usize; - - // Assert conditions for termination - assert!( - empty_earned_nodes <= acceptable_empty_nodes, - "More than {acceptable_percentage}% of nodes ({acceptable_empty_nodes}) still not earning: {empty_earned_nodes}" - ); - assert!( - (max_store_cost / min_store_cost) < 1000000, - "store cost is not 'balanced', expected ratio max/min to be < 1000000, but was {}", - max_store_cost / min_store_cost - ); - assert!( - (max_earned / min_earned) < 500000000, - "earning distribution is not balanced, expected to be < 500000000, but was {}", - max_earned / min_earned - ); - break; - } - } - } - - fn pick_cheapest_payee( - peers: &[PeerStats], - close_group: &[usize], - ) -> eyre::Result<(usize, AttoTokens)> { - let mut costs_vec = Vec::with_capacity(close_group.len()); - let mut address_to_index = BTreeMap::new(); - - for &i in close_group { - let peer = &peers[i]; - address_to_index.insert(peer.address.clone(), i); - - let close_records_stored = peer.records_stored.load(Ordering::Relaxed); - let cost = AttoTokens::from(calculate_cost_for_records(close_records_stored)); - - let quote = PaymentQuote { - content: XorName::default(), // unimportant for cost calc - cost, - timestamp: std::time::SystemTime::now(), - quoting_metrics: QuotingMetrics { - close_records_stored: peer.records_stored.load(Ordering::Relaxed), - max_records: MAX_RECORDS_COUNT, - received_payment_count: 1, // unimportant for cost calc - live_time: 0, // unimportant for cost calc - network_density: None, - network_size: None, - }, - bad_nodes: vec![], - pub_key: bls::SecretKey::random().public_key().to_bytes().to_vec(), - signature: vec![], - rewards_address: peer.rewards_addr, // unimportant for cost calc - }; - - costs_vec.push((peer.address.clone(), peer.rewards_addr, quote)); - } - - // sort by address first - costs_vec.sort_by(|(a_addr, _, _), (b_addr, _, _)| a_addr.cmp(b_addr)); - - let Ok((recip_id, _pk, q)) = get_fees_from_store_cost_responses(costs_vec) else { - bail!("Failed to get fees from store cost responses") - }; - - let Some(index) = address_to_index - .get(&NetworkAddress::from_peer(recip_id)) - .copied() - else { - bail!("Cannot find the index for the cheapest payee"); - }; - - Ok((index, q.cost)) - } } diff --git a/ant-networking/src/record_store_api.rs b/ant-networking/src/record_store_api.rs index bf391d8293..0955d5499f 100644 --- a/ant-networking/src/record_store_api.rs +++ b/ant-networking/src/record_store_api.rs @@ -8,8 +8,7 @@ #![allow(clippy::mutable_key_type)] // for the Bytes in NetworkAddress use crate::record_store::{ClientRecordStore, NodeRecordStore}; -use alloy::primitives::U256; -use ant_evm::{AttoTokens, QuotingMetrics}; +use ant_evm::{QuotingMetrics, U256}; use ant_protocol::{storage::RecordType, NetworkAddress}; use libp2p::kad::{ store::{RecordStore, Result}, @@ -112,17 +111,19 @@ impl UnifiedRecordStore { } } - pub(crate) fn store_cost( + /// Return the quoting metrics used to calculate the cost of storing a record + /// and whether the record is already stored locally + pub(crate) fn quoting_metrics( &self, key: &RecordKey, network_size: Option, - ) -> (AttoTokens, QuotingMetrics) { + ) -> (QuotingMetrics, bool) { match self { Self::Client(_) => { - warn!("Calling store cost calculation at Client. This should not happen"); - (AttoTokens::zero(), Default::default()) + warn!("Calling quoting metrics calculation at Client. This should not happen"); + Default::default() } - Self::Node(store) => store.store_cost(key, network_size), + Self::Node(store) => store.quoting_metrics(key, network_size), } } diff --git a/ant-networking/src/replication_fetcher.rs b/ant-networking/src/replication_fetcher.rs index 51132213b9..a009209451 100644 --- a/ant-networking/src/replication_fetcher.rs +++ b/ant-networking/src/replication_fetcher.rs @@ -9,7 +9,7 @@ use crate::target_arch::spawn; use crate::{event::NetworkEvent, target_arch::Instant}; -use alloy::primitives::U256; +use ant_evm::U256; use ant_protocol::{ convert_distance_to_u256, storage::RecordType, NetworkAddress, PrettyPrintRecordKey, }; diff --git a/ant-node/Cargo.toml b/ant-node/Cargo.toml index 6d43fec5eb..09741f2fb9 100644 --- a/ant-node/Cargo.toml +++ b/ant-node/Cargo.toml @@ -26,7 +26,6 @@ otlp = ["ant-logging/otlp"] upnp = ["ant-networking/upnp"] [dependencies] -alloy = { version = "0.5.3", default-features = false, features = ["std", "reqwest-rustls-tls", "provider-anvil-node", "sol-types", "json", "signers", "contract", "signer-local", "network"] } ant-bootstrap = { path = "../ant-bootstrap", version = "0.1.0" } ant-build-info = { path = "../ant-build-info", version = "0.1.19" } ant-evm = { path = "../ant-evm", version = "0.1.4" } @@ -84,7 +83,7 @@ walkdir = "~2.5.0" xor_name = "5.0.0" [dev-dependencies] -ant-protocol = { path = "../ant-protocol", version = "0.17.15", features = ["rpc"]} +ant-protocol = { path = "../ant-protocol", version = "0.17.15", features = ["rpc"] } assert_fs = "1.0.0" evmlib = { path = "../evmlib", version = "0.1.4" } autonomi = { path = "../autonomi", version = "0.2.4", features = ["registers"] } diff --git a/ant-node/src/error.rs b/ant-node/src/error.rs index a0aa2a6a48..6cc7f3baf1 100644 --- a/ant-node/src/error.rs +++ b/ant-node/src/error.rs @@ -6,6 +6,7 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. +use ant_evm::AttoTokens; use ant_protocol::{NetworkAddress, PrettyPrintRecordKey}; use thiserror::Error; @@ -54,6 +55,25 @@ pub enum Error { InvalidQuoteSignature, #[error("The payment quote expired for {0:?}")] QuoteExpired(NetworkAddress), + /// Payment proof received has no inputs + #[error( + "Payment proof received with record:{0:?}. No payment for our node in its transaction" + )] + NoPaymentToOurNode(PrettyPrintRecordKey<'static>), + /// Missing network royalties payment + #[error("Missing network royalties payment in proof received with record: {0:?}.")] + NoNetworkRoyaltiesPayment(PrettyPrintRecordKey<'static>), + #[error("The amount paid is less than the storecost, paid {paid}, expected {expected}")] + PaymentInsufficientAmount { + paid: AttoTokens, + expected: AttoTokens, + }, + #[error("A payment we received contains cash notes already confirmed to be spent")] + ReusedPayment, + + // ---------- Initialize Errors + #[error("Failed to generate a reward key")] + FailedToGenerateRewardKey, // ---------- Miscellaneous Errors #[error("Failed to obtain node's current port")] diff --git a/ant-node/src/node.rs b/ant-node/src/node.rs index 342ace58bc..4908c0bc23 100644 --- a/ant-node/src/node.rs +++ b/ant-node/src/node.rs @@ -12,9 +12,8 @@ use super::{ #[cfg(feature = "open-metrics")] use crate::metrics::NodeMetricsRecorder; use crate::RunningNode; -use alloy::primitives::U256; use ant_bootstrap::BootstrapCacheStore; -use ant_evm::{AttoTokens, RewardsAddress}; +use ant_evm::RewardsAddress; #[cfg(feature = "open-metrics")] use ant_networking::MetricsRegistries; use ant_networking::{Instant, Network, NetworkBuilder, NetworkEvent, NodeIssue, SwarmDriver}; @@ -48,7 +47,7 @@ use tokio::{ task::{spawn, JoinSet}, }; -use ant_evm::EvmNetwork; +use ant_evm::{EvmNetwork, U256}; /// Interval to trigger replication of all records to all peers. /// This is the max time it should take. Minimum interval at any node will be half this @@ -569,16 +568,17 @@ impl Node { payment_address: RewardsAddress, ) -> Response { let resp: QueryResponse = match query { - Query::GetStoreCost { + Query::GetStoreQuote { key, nonce, difficulty, } => { - debug!("Got GetStoreCost request for {key:?} with difficulty {difficulty}"); + debug!("Got GetStoreQuote request for {key:?} with difficulty {difficulty}"); let record_key = key.to_record_key(); let self_id = network.peer_id(); - let store_cost = network.get_local_storecost(record_key.clone()).await; + let maybe_quoting_metrics = + network.get_local_quoting_metrics(record_key.clone()).await; let storage_proofs = if let Some(nonce) = nonce { Self::respond_x_closest_record_proof( @@ -593,39 +593,37 @@ impl Node { vec![] }; - match store_cost { - Ok((cost, quoting_metrics, bad_nodes)) => { - if cost == AttoTokens::zero() { - QueryResponse::GetStoreCost { + match maybe_quoting_metrics { + Ok((quoting_metrics, is_already_stored)) => { + if is_already_stored { + QueryResponse::GetStoreQuote { quote: Err(ProtocolError::RecordExists( PrettyPrintRecordKey::from(&record_key).into_owned(), )), - payment_address, peer_address: NetworkAddress::from_peer(self_id), storage_proofs, } } else { - QueryResponse::GetStoreCost { + QueryResponse::GetStoreQuote { quote: Self::create_quote_for_storecost( network, - cost, &key, "ing_metrics, - bad_nodes, &payment_address, ), - payment_address, peer_address: NetworkAddress::from_peer(self_id), storage_proofs, } } } - Err(_) => QueryResponse::GetStoreCost { - quote: Err(ProtocolError::GetStoreCostFailed), - payment_address, - peer_address: NetworkAddress::from_peer(self_id), - storage_proofs, - }, + Err(err) => { + warn!("GetStoreQuote failed for {key:?}: {err}"); + QueryResponse::GetStoreQuote { + quote: Err(ProtocolError::GetStoreQuoteFailed), + peer_address: NetworkAddress::from_peer(self_id), + storage_proofs, + } + } } } Query::GetRegisterRecord { requester, key } => { diff --git a/ant-node/src/put_validation.rs b/ant-node/src/put_validation.rs index 002652faa0..9beec8b740 100644 --- a/ant-node/src/put_validation.rs +++ b/ant-node/src/put_validation.rs @@ -7,7 +7,8 @@ // permissions and limitations relating to use of the SAFE Network Software. use crate::{node::Node, Error, Marker, Result}; -use ant_evm::{ProofOfPayment, QUOTE_EXPIRATION_SECS}; +use ant_evm::payment_vault::verify_data_payment; +use ant_evm::{AttoTokens, ProofOfPayment}; use ant_networking::NetworkError; use ant_protocol::storage::Transaction; use ant_protocol::{ @@ -19,7 +20,6 @@ use ant_protocol::{ }; use ant_registers::SignedRegister; use libp2p::kad::{Record, RecordKey}; -use std::time::{Duration, UNIX_EPOCH}; use xor_name::XorName; impl Node { @@ -652,41 +652,46 @@ impl Node { debug!("Validating record payment for {pretty_key}"); // check if the quote is valid - let storecost = payment.quote.cost; let self_peer_id = self.network().peer_id(); - if !payment.quote.check_is_signed_by_claimed_peer(self_peer_id) { - warn!("Payment quote signature is not valid for record {pretty_key}"); + if !payment.verify_for(self_peer_id) { + warn!("Payment is not valid for record {pretty_key}"); return Err(Error::InvalidRequest(format!( - "Payment quote signature is not valid for record {pretty_key}" + "Payment is not valid for record {pretty_key}" ))); } - debug!("Payment quote signature is valid for record {pretty_key}"); - - // verify quote timestamp - let quote_timestamp = payment.quote.timestamp; - let quote_expiration_time = quote_timestamp + Duration::from_secs(QUOTE_EXPIRATION_SECS); - let quote_expiration_time_in_secs = quote_expiration_time - .duration_since(UNIX_EPOCH) - .map_err(|e| { - Error::InvalidRequest(format!( - "Payment quote timestamp is invalid for record {pretty_key}: {e}" - )) - })? - .as_secs(); + debug!("Payment is valid for record {pretty_key}"); + // verify quote expiration + if payment.has_expired() { + warn!("Payment quote has expired for record {pretty_key}"); + return Err(Error::InvalidRequest(format!( + "Payment quote has expired for record {pretty_key}" + ))); + } + + // verify the claimed payees are all known to us within the certain range. + let closest_k_peers = self.network().get_closest_k_value_local_peers().await?; + let mut payees = payment.payees(); + payees.retain(|peer_id| !closest_k_peers.contains(peer_id)); + if !payees.is_empty() { + return Err(Error::InvalidRequest(format!( + "Payment quote has out-of-range payees {payees:?}" + ))); + } + + let owned_payment_quotes = payment + .quotes_by_peer(&self_peer_id) + .iter() + .map(|quote| quote.hash()) + .collect(); // check if payment is valid on chain + let payments_to_verify = payment.digest(); debug!("Verifying payment for record {pretty_key}"); - self.evm_network() - .verify_data_payment( - payment.tx_hash, - payment.quote.hash(), - *self.reward_address(), - storecost.as_atto(), - quote_expiration_time_in_secs, - ) - .await - .map_err(|e| Error::EvmNetwork(format!("Failed to verify chunk payment: {e}")))?; - debug!("Payment is valid for record {pretty_key}"); + let reward_amount = + verify_data_payment(self.evm_network(), owned_payment_quotes, payments_to_verify) + .await + .map_err(|e| Error::EvmNetwork(format!("Failed to verify chunk payment: {e}")))?; + debug!("Payment of {reward_amount:?} is valid for record {pretty_key}"); // Notify `record_store` that the node received a payment. self.network().notify_payment_received(); @@ -696,22 +701,27 @@ impl Node { // FIXME: We would reach the MAX if the storecost is scaled up. let current_value = metrics_recorder.current_reward_wallet_balance.get(); let new_value = - current_value.saturating_add(storecost.as_atto().try_into().unwrap_or(i64::MAX)); + current_value.saturating_add(reward_amount.try_into().unwrap_or(i64::MAX)); let _ = metrics_recorder .current_reward_wallet_balance .set(new_value); } self.events_channel() - .broadcast(crate::NodeEvent::RewardReceived(storecost, address.clone())); + .broadcast(crate::NodeEvent::RewardReceived( + AttoTokens::from(reward_amount), + address.clone(), + )); // vdash metric (if modified please notify at https://github.com/happybeing/vdash/issues): - info!("Total payment of {storecost:?} atto tokens accepted for record {pretty_key}"); + info!("Total payment of {reward_amount:?} atto tokens accepted for record {pretty_key}"); // loud mode: print a celebratory message to console #[cfg(feature = "loud")] { println!("🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟 RECEIVED REWARD 🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟"); - println!("Total payment of {storecost:?} atto tokens accepted for record {pretty_key}"); + println!( + "Total payment of {reward_amount:?} atto tokens accepted for record {pretty_key}" + ); println!("🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟"); } diff --git a/ant-node/src/quote.rs b/ant-node/src/quote.rs index fa3defd843..f7c61b2af8 100644 --- a/ant-node/src/quote.rs +++ b/ant-node/src/quote.rs @@ -7,8 +7,8 @@ // permissions and limitations relating to use of the SAFE Network Software. use crate::{node::Node, Error, Result}; -use ant_evm::{AttoTokens, PaymentQuote, QuotingMetrics, RewardsAddress}; -use ant_networking::{calculate_cost_for_records, Network, NodeIssue}; +use ant_evm::{PaymentQuote, QuotingMetrics, RewardsAddress}; +use ant_networking::Network; use ant_protocol::{error::Error as ProtocolError, storage::ChunkAddress, NetworkAddress}; use libp2p::PeerId; use std::time::Duration; @@ -16,23 +16,14 @@ use std::time::Duration; impl Node { pub(crate) fn create_quote_for_storecost( network: &Network, - cost: AttoTokens, address: &NetworkAddress, quoting_metrics: &QuotingMetrics, - bad_nodes: Vec, payment_address: &RewardsAddress, ) -> Result { let content = address.as_xorname().unwrap_or_default(); let timestamp = std::time::SystemTime::now(); - let serialised_bad_nodes = rmp_serde::to_vec(&bad_nodes).unwrap_or_default(); - let bytes = PaymentQuote::bytes_for_signing( - content, - cost, - timestamp, - quoting_metrics, - &serialised_bad_nodes, - payment_address, - ); + let bytes = + PaymentQuote::bytes_for_signing(content, timestamp, quoting_metrics, payment_address); let Ok(signature) = network.sign(&bytes) else { return Err(ProtocolError::QuoteGenerationFailed); @@ -40,10 +31,8 @@ impl Node { let quote = PaymentQuote { content, - cost, timestamp, quoting_metrics: quoting_metrics.clone(), - bad_nodes: serialised_bad_nodes, pub_key: network.get_pub_key(), rewards_address: *payment_address, signature, @@ -87,8 +76,7 @@ pub(crate) fn verify_quote_for_storecost( // 3, quote is no longer valid // // Following metrics will be considered as node's bad quote. -// 1, Price calculation is incorrect -// 2, QuoteMetrics doesn't match the historical quotes collected by self +// 1, QuoteMetrics doesn't match the historical quotes collected by self pub(crate) async fn quotes_verification(network: &Network, quotes: Vec<(PeerId, PaymentQuote)>) { // Do nothing if self is not one of the quoters. if let Some((_, self_quote)) = quotes @@ -98,12 +86,11 @@ pub(crate) async fn quotes_verification(network: &Network, quotes: Vec<(PeerId, let target_address = NetworkAddress::from_chunk_address(ChunkAddress::new(self_quote.content)); if verify_quote_for_storecost(network, self_quote.clone(), &target_address).is_ok() { - let mut quotes_for_nodes_duty: Vec<_> = quotes + let quotes_for_nodes_duty: Vec<_> = quotes .iter() .filter(|(peer_id, quote)| { let is_same_target = quote.content == self_quote.content; let is_not_self = *peer_id != network.peer_id(); - let is_not_zero_quote = quote.cost != AttoTokens::zero(); let time_gap = Duration::from_secs(10); let is_around_same_time = if quote.timestamp > self_quote.timestamp { @@ -117,25 +104,12 @@ pub(crate) async fn quotes_verification(network: &Network, quotes: Vec<(PeerId, is_same_target && is_not_self - && is_not_zero_quote && is_around_same_time && is_signed_by_the_claimed_peer }) .cloned() .collect(); - quotes_for_nodes_duty.retain(|(peer_id, quote)| { - let cost = calculate_cost_for_records(quote.quoting_metrics.close_records_stored); - let is_same_as_expected = quote.cost == AttoTokens::from_u64(cost); - - if !is_same_as_expected { - info!("Quote from {peer_id:?} using a different quoting_metrics to achieve the claimed cost. Quote {quote:?} can only result in cost {cost:?}"); - network.record_node_issues(*peer_id, NodeIssue::BadQuoting); - } - - is_same_as_expected - }); - // Pass down to swarm_driver level for further bad quote detection // against historical collected quotes. network.historical_verify_quotes(quotes_for_nodes_duty); diff --git a/ant-protocol/Cargo.toml b/ant-protocol/Cargo.toml index 340f05292e..c8c4b6808d 100644 --- a/ant-protocol/Cargo.toml +++ b/ant-protocol/Cargo.toml @@ -11,10 +11,9 @@ version = "0.17.15" [features] default = [] -rpc=["tonic", "prost"] +rpc = ["tonic", "prost"] [dependencies] -alloy = { version = "0.5.3", default-features = false, features = ["std", "reqwest-rustls-tls", "provider-anvil-node", "sol-types", "json", "signers", "contract", "signer-local", "network"] } ant-build-info = { path = "../ant-build-info", version = "0.1.19" } ant-evm = { path = "../ant-evm", version = "0.1.4" } ant-registers = { path = "../ant-registers", version = "0.4.3" } @@ -31,15 +30,15 @@ libp2p = { version = "0.54.1", features = ["identify", "kad"] } # # watch out updating this, protoc compiler needs to be installed on all build systems # # arm builds + musl are very problematic # prost and tonic are needed for the RPC server messages, not the underlying protocol -prost = { version = "0.9" , optional=true } +prost = { version = "0.9", optional = true } rmp-serde = "1.1.1" -serde = { version = "1.0.133", features = [ "derive", "rc" ]} +serde = { version = "1.0.133", features = ["derive", "rc"] } serde_json = "1.0" sha2 = "0.10.7" thiserror = "1.0.23" -tiny-keccak = { version = "~2.0.2", features = [ "sha3" ] } +tiny-keccak = { version = "~2.0.2", features = ["sha3"] } tracing = { version = "~0.1.26" } -tonic = { version = "0.6.2", optional=true, default-features = false, features = ["prost", "tls", "codegen"]} +tonic = { version = "0.6.2", optional = true, default-features = false, features = ["prost", "tls", "codegen"] } xor_name = "5.0.0" [build-dependencies] diff --git a/ant-protocol/src/error.rs b/ant-protocol/src/error.rs index 7db10f9612..bc784860e1 100644 --- a/ant-protocol/src/error.rs +++ b/ant-protocol/src/error.rs @@ -57,7 +57,7 @@ pub enum Error { // ---------- payment errors #[error("There was an error getting the storecost from kademlia store")] - GetStoreCostFailed, + GetStoreQuoteFailed, #[error("There was an error generating the payment quote")] QuoteGenerationFailed, diff --git a/ant-protocol/src/lib.rs b/ant-protocol/src/lib.rs index 522abc3835..936d474246 100644 --- a/ant-protocol/src/lib.rs +++ b/ant-protocol/src/lib.rs @@ -36,17 +36,17 @@ use self::storage::{ChunkAddress, RegisterAddress, TransactionAddress}; /// Re-export of Bytes used throughout the protocol pub use bytes::Bytes; -use alloy::primitives::U256; +use ant_evm::U256; use libp2p::{ kad::{KBucketDistance as Distance, KBucketKey as Key, RecordKey}, multiaddr::Protocol, Multiaddr, PeerId, }; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::str::FromStr; use std::{ borrow::Cow, fmt::{self, Debug, Display, Formatter, Write}, - str::FromStr, }; use xor_name::XorName; diff --git a/ant-protocol/src/messages/cmd.rs b/ant-protocol/src/messages/cmd.rs index 5e8e78d7a4..f0f5e089b4 100644 --- a/ant-protocol/src/messages/cmd.rs +++ b/ant-protocol/src/messages/cmd.rs @@ -8,7 +8,6 @@ #![allow(clippy::mutable_key_type)] // for Bytes in NetworkAddress use crate::{storage::RecordType, NetworkAddress}; -pub use ant_evm::PaymentQuote; use serde::{Deserialize, Serialize}; /// Ant protocol cmds @@ -28,11 +27,6 @@ pub enum Cmd { /// Keys of copy that shall be replicated. keys: Vec<(NetworkAddress, RecordType)>, }, - /// Write operation to notify nodes a list of PaymentQuote collected. - QuoteVerification { - target: NetworkAddress, - quotes: Vec<(NetworkAddress, PaymentQuote)>, - }, /// Notify the peer it is now being considered as BAD due to the included behaviour PeerConsideredAsBad { detected_by: NetworkAddress, @@ -52,11 +46,6 @@ impl std::fmt::Debug for Cmd { .field("first_ten_keys", &first_ten_keys) .finish() } - Cmd::QuoteVerification { target, quotes } => f - .debug_struct("Cmd::QuoteVerification") - .field("target", target) - .field("quotes_len", "es.len()) - .finish(), Cmd::PeerConsideredAsBad { detected_by, bad_peer, @@ -76,7 +65,6 @@ impl Cmd { pub fn dst(&self) -> NetworkAddress { match self { Cmd::Replicate { holder, .. } => holder.clone(), - Cmd::QuoteVerification { target, .. } => target.clone(), Cmd::PeerConsideredAsBad { bad_peer, .. } => bad_peer.clone(), } } @@ -93,13 +81,6 @@ impl std::fmt::Display for Cmd { keys.len() ) } - Cmd::QuoteVerification { target, quotes } => { - write!( - f, - "Cmd::QuoteVerification(sent to {target:?} has {} quotes)", - quotes.len() - ) - } Cmd::PeerConsideredAsBad { detected_by, bad_peer, diff --git a/ant-protocol/src/messages/query.rs b/ant-protocol/src/messages/query.rs index a71e24e500..f38500bd41 100644 --- a/ant-protocol/src/messages/query.rs +++ b/ant-protocol/src/messages/query.rs @@ -7,7 +7,7 @@ // permissions and limitations relating to use of the SAFE Network Software. use crate::{messages::Nonce, NetworkAddress}; -use alloy::primitives::U256; +use ant_evm::U256; use serde::{Deserialize, Serialize}; /// Data queries - retrieving data and inspecting their structure. @@ -18,9 +18,9 @@ use serde::{Deserialize, Serialize}; /// [`protocol`]: crate #[derive(Eq, PartialEq, PartialOrd, Clone, Serialize, Deserialize, Debug)] pub enum Query { - /// Retrieve the cost of storing a record at the given address. + /// Retrieve the quote to store a record at the given address. /// The storage verification is optional to be undertaken - GetStoreCost { + GetStoreQuote { /// The Address of the record to be stored. key: NetworkAddress, /// The random nonce that nodes use to produce the Proof (i.e., hash(record+nonce)) @@ -87,7 +87,7 @@ impl Query { Query::CheckNodeInProblem(address) => address.clone(), // Shall not be called for this, as this is a `one-to-one` message, // and the destination shall be decided by the requester already. - Query::GetStoreCost { key, .. } + Query::GetStoreQuote { key, .. } | Query::GetReplicatedRecord { key, .. } | Query::GetRegisterRecord { key, .. } | Query::GetChunkExistenceProof { key, .. } @@ -99,12 +99,12 @@ impl Query { impl std::fmt::Display for Query { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Query::GetStoreCost { + Query::GetStoreQuote { key, nonce, difficulty, } => { - write!(f, "Query::GetStoreCost({key:?} {nonce:?} {difficulty})") + write!(f, "Query::GetStoreQuote({key:?} {nonce:?} {difficulty})") } Query::GetReplicatedRecord { key, requester } => { write!(f, "Query::GetReplicatedRecord({requester:?} {key:?})") diff --git a/ant-protocol/src/messages/response.rs b/ant-protocol/src/messages/response.rs index a7f8bf9220..48b332c60b 100644 --- a/ant-protocol/src/messages/response.rs +++ b/ant-protocol/src/messages/response.rs @@ -9,7 +9,7 @@ use crate::{error::Result, NetworkAddress}; use super::ChunkProof; -use ant_evm::{PaymentQuote, RewardsAddress}; +use ant_evm::PaymentQuote; use bytes::Bytes; use core::fmt; use libp2p::Multiaddr; @@ -19,16 +19,14 @@ use std::fmt::Debug; /// The response to a query, containing the query result. #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum QueryResponse { - // ===== GetStoreCost ===== + // ===== GetStoreQuote ===== // - /// Response to [`GetStoreCost`] + /// Response to [`GetStoreQuote`] /// - /// [`GetStoreCost`]: crate::messages::Query::GetStoreCost - GetStoreCost { + /// [`GetStoreQuote`]: crate::messages::Query::GetStoreQuote + GetStoreQuote { /// The store cost quote for storing the next record. quote: Result, - /// The rewards address to pay this node's store cost to. - payment_address: RewardsAddress, /// Node's Peer Address peer_address: NetworkAddress, /// Storage proofs based on requested target address and difficulty @@ -80,15 +78,15 @@ pub enum QueryResponse { impl Debug for QueryResponse { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - QueryResponse::GetStoreCost { + QueryResponse::GetStoreQuote { quote, - payment_address, peer_address, storage_proofs, } => { + let payment_address = quote.as_ref().map(|q| q.rewards_address).ok(); write!( f, - "GetStoreCost(quote: {quote:?}, from {peer_address:?} w/ payment_address: {payment_address:?}, and {} storage proofs)", + "GetStoreQuote(quote: {quote:?}, from {peer_address:?} w/ payment_address: {payment_address:?}, and {} storage proofs)", storage_proofs.len() ) } @@ -152,11 +150,6 @@ pub enum CmdResponse { /// Response to replication cmd Replicate(Result<()>), // - // ===== QuoteVerification ===== - // - /// Response to quote verification cmd - QuoteVerification(Result<()>), - // // ===== PeerConsideredAsBad ===== // /// Response to the considered as bad notification diff --git a/autonomi/Cargo.toml b/autonomi/Cargo.toml index 00978f1628..d5089d14bc 100644 --- a/autonomi/Cargo.toml +++ b/autonomi/Cargo.toml @@ -59,7 +59,7 @@ wasm-bindgen-futures = "0.4.43" xor_name = "5.0.0" [dev-dependencies] -alloy = { version = "0.5.3", default-features = false, features = ["std", "reqwest-rustls-tls", "provider-anvil-node", "sol-types", "json", "signers", "contract", "signer-local", "network"] } +alloy = { version = "0.7.3", default-features = false, features = ["contract", "json-rpc", "network", "node-bindings", "provider-http", "reqwest-rustls-tls", "rpc-client", "rpc-types", "signer-local", "std"] } ant-logging = { path = "../ant-logging", version = "0.2.40" } eyre = "0.6.5" sha2 = "0.10.6" diff --git a/autonomi/src/client/data/mod.rs b/autonomi/src/client/data/mod.rs index b85f54a68e..e1967f0c95 100644 --- a/autonomi/src/client/data/mod.rs +++ b/autonomi/src/client/data/mod.rs @@ -84,6 +84,8 @@ pub enum PutError { VaultBadOwner, #[error("Payment unexpectedly invalid for {0:?}")] PaymentUnexpectedlyInvalid(NetworkAddress), + #[error("The payment proof contains no payees.")] + PayeesMissing, } /// Errors that can occur during the pay operation. @@ -121,8 +123,12 @@ pub enum CostError { CouldNotGetStoreQuote(XorName), #[error("Could not get store costs: {0:?}")] CouldNotGetStoreCosts(NetworkError), + #[error("Not enough node quotes for {0:?}, got: {1:?} and need at least {2:?}")] + NotEnoughNodeQuotes(XorName, usize, usize), #[error("Failed to serialize {0}")] Serialization(String), + #[error("Market price error: {0:?}")] + MarketPriceError(#[from] ant_evm::payment_vault::error::Error), } /// Private data on the network can be accessed with this @@ -209,7 +215,7 @@ impl Client { if let Some(channel) = self.client_event_sender.as_ref() { let tokens_spent = receipt .values() - .map(|proof| proof.quote.cost.as_atto()) + .map(|(_, cost)| cost.as_atto()) .sum::(); let summary = UploadSummary { diff --git a/autonomi/src/client/data/public.rs b/autonomi/src/client/data/public.rs index a4ff4e1a40..9f758edde8 100644 --- a/autonomi/src/client/data/public.rs +++ b/autonomi/src/client/data/public.rs @@ -8,15 +8,12 @@ use bytes::Bytes; use libp2p::kad::Quorum; +use std::collections::HashSet; -use std::collections::{HashMap, HashSet}; -use xor_name::XorName; - -use crate::client::payment::PaymentOption; +use crate::client::payment::{PaymentOption, Receipt}; use crate::client::utils::process_tasks_with_max_concurrency; use crate::client::{ClientEvent, UploadSummary}; use crate::{self_encryption::encrypt, Client}; -use ant_evm::ProofOfPayment; use ant_evm::{Amount, AttoTokens}; use ant_networking::{GetRecordCfg, NetworkError}; use ant_protocol::{ @@ -96,7 +93,7 @@ impl Client { if let Some(channel) = self.client_event_sender.as_ref() { let tokens_spent = receipt .values() - .map(|proof| proof.quote.cost.as_atto()) + .map(|(_proof, price)| price.as_atto()) .sum::(); let summary = UploadSummary { @@ -163,17 +160,19 @@ impl Client { content_addrs.len() ); - let cost_map = self + let store_quote = self .get_store_quotes(content_addrs.into_iter()) .await .inspect_err(|err| error!("Error getting store quotes: {err:?}"))?; + let total_cost = AttoTokens::from_atto( - cost_map + store_quote + .0 .values() - .map(|quote| quote.2.cost.as_atto()) + .map(|quote| quote.price()) .sum::(), ); - debug!("Total cost calculated: {total_cost:?}"); + Ok(total_cost) } @@ -181,7 +180,7 @@ impl Client { pub(crate) async fn upload_chunks_with_retries<'a>( &self, mut chunks: Vec<&'a Chunk>, - receipt: &HashMap, + receipt: &Receipt, ) -> Vec<(&'a Chunk, PutError)> { let mut current_attempt: usize = 1; @@ -191,7 +190,7 @@ impl Client { let self_clone = self.clone(); let address = *chunk.address(); - let Some(proof) = receipt.get(chunk.name()) else { + let Some((proof, _)) = receipt.get(chunk.name()) else { debug!("Chunk at {address:?} was already paid for so skipping"); continue; }; diff --git a/autonomi/src/client/data_private.rs b/autonomi/src/client/data_private.rs new file mode 100644 index 0000000000..d1288bb193 --- /dev/null +++ b/autonomi/src/client/data_private.rs @@ -0,0 +1,130 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use std::hash::{DefaultHasher, Hash, Hasher}; + +use ant_evm::Amount; +use ant_protocol::storage::Chunk; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; + +use super::data::{GetError, PutError}; +use crate::client::payment::PaymentOption; +use crate::client::{ClientEvent, UploadSummary}; +use crate::{self_encryption::encrypt, Client}; + +/// Private data on the network can be accessed with this +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct PrivateDataAccess(Chunk); + +impl PrivateDataAccess { + pub fn to_hex(&self) -> String { + hex::encode(self.0.value()) + } + + pub fn from_hex(hex: &str) -> Result { + let data = hex::decode(hex)?; + Ok(Self(Chunk::new(Bytes::from(data)))) + } + + /// Get a private address for [`PrivateDataAccess`]. Note that this is not a network address, it is only used for refering to private data client side. + pub fn address(&self) -> String { + hash_to_short_string(&self.to_hex()) + } +} + +fn hash_to_short_string(input: &str) -> String { + let mut hasher = DefaultHasher::new(); + input.hash(&mut hasher); + let hash_value = hasher.finish(); + hash_value.to_string() +} + +impl Client { + /// Fetch a blob of private data from the network + pub async fn private_data_get(&self, data_map: PrivateDataAccess) -> Result { + info!( + "Fetching private data from Data Map {:?}", + data_map.0.address() + ); + let data = self.fetch_from_data_map_chunk(data_map.0.value()).await?; + + Ok(data) + } + + /// Upload a piece of private data to the network. This data will be self-encrypted. + /// Returns the [`PrivateDataAccess`] containing the map to the encrypted chunks. + /// This data is private and only accessible with the [`PrivateDataAccess`]. + pub async fn private_data_put( + &self, + data: Bytes, + payment_option: PaymentOption, + ) -> Result { + let now = ant_networking::target_arch::Instant::now(); + let (data_map_chunk, chunks) = encrypt(data)?; + debug!("Encryption took: {:.2?}", now.elapsed()); + + // Pay for all chunks + let xor_names: Vec<_> = chunks.iter().map(|chunk| *chunk.name()).collect(); + info!("Paying for {} addresses", xor_names.len()); + let receipt = self + .pay_for_content_addrs(xor_names.into_iter(), payment_option) + .await + .inspect_err(|err| error!("Error paying for data: {err:?}"))?; + + // Upload the chunks with the payments + debug!("Uploading {} chunks", chunks.len()); + + let mut failed_uploads = self + .upload_chunks_with_retries(chunks.iter().collect(), &receipt) + .await; + + // Return the last chunk upload error + if let Some(last_chunk_fail) = failed_uploads.pop() { + tracing::error!( + "Error uploading chunk ({:?}): {:?}", + last_chunk_fail.0.address(), + last_chunk_fail.1 + ); + return Err(last_chunk_fail.1); + } + + let record_count = chunks.len(); + + // Reporting + if let Some(channel) = self.client_event_sender.as_ref() { + let tokens_spent = receipt + .values() + .map(|(_proof, price)| price.as_atto()) + .sum::(); + + let summary = UploadSummary { + record_count, + tokens_spent, + }; + if let Err(err) = channel.send(ClientEvent::UploadComplete(summary)).await { + error!("Failed to send client event: {err:?}"); + } + } + + Ok(PrivateDataAccess(data_map_chunk)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hex() { + let data_map = PrivateDataAccess(Chunk::new(Bytes::from_static(b"hello"))); + let hex = data_map.to_hex(); + let data_map2 = PrivateDataAccess::from_hex(&hex).expect("Failed to decode hex"); + assert_eq!(data_map, data_map2); + } +} diff --git a/autonomi/src/client/external_signer.rs b/autonomi/src/client/external_signer.rs index d3b7ede67d..30114712f3 100644 --- a/autonomi/src/client/external_signer.rs +++ b/autonomi/src/client/external_signer.rs @@ -1,40 +1,39 @@ use crate::client::data::PutError; -use crate::client::utils::extract_quote_payments; use crate::self_encryption::encrypt; use crate::Client; -use ant_evm::{PaymentQuote, QuotePayment}; +use ant_evm::QuotePayment; use ant_protocol::storage::Chunk; use bytes::Bytes; use std::collections::HashMap; use xor_name::XorName; -use crate::utils::cost_map_to_quotes; #[allow(unused_imports)] pub use ant_evm::external_signer::*; +use super::quote::QuoteForAddress; + impl Client { /// Get quotes for data. /// Returns a cost map, data payments to be executed and a list of free (already paid for) chunks. pub async fn get_quotes_for_content_addresses( &self, - content_addrs: impl Iterator, + content_addrs: impl Iterator + Clone, ) -> Result< ( - HashMap, + HashMap, Vec, Vec, ), PutError, > { - let cost_map = self.get_store_quotes(content_addrs).await?; - let (quote_payments, free_chunks) = extract_quote_payments(&cost_map); - let quotes = cost_map_to_quotes(cost_map); + let quote = self.get_store_quotes(content_addrs.clone()).await?; + let payments = quote.payments(); + let free_chunks = content_addrs + .filter(|addr| !quote.0.contains_key(addr)) + .collect(); + let quotes_per_addr: HashMap<_, _> = quote.0.into_iter().collect(); - debug!( - "Got the quotes , quote_payments and freechunks from the network {:?}", - (quotes.clone(), quote_payments.clone(), free_chunks.clone()) - ); - Ok((quotes, quote_payments, free_chunks)) + Ok((quotes_per_addr, payments, free_chunks)) } } diff --git a/autonomi/src/client/mod.rs b/autonomi/src/client/mod.rs index d14964f9f1..8a233b8085 100644 --- a/autonomi/src/client/mod.rs +++ b/autonomi/src/client/mod.rs @@ -11,6 +11,7 @@ pub mod address; pub mod payment; +pub mod quote; pub mod data; #[cfg(feature = "external-signer")] @@ -28,11 +29,13 @@ pub mod vault; pub mod wasm; // private module with utility functions +mod rate_limiter; mod utils; use ant_bootstrap::{BootstrapCacheConfig, BootstrapCacheStore}; pub use ant_evm::Amount; +use ant_evm::EvmNetwork; use ant_networking::{interval, multiaddr_is_global, Network, NetworkBuilder, NetworkEvent}; use ant_protocol::{version::IDENTIFY_PROTOCOL_STR, CLOSE_GROUP_SIZE}; use libp2p::{identity::Keypair, Multiaddr}; @@ -63,6 +66,7 @@ const CLIENT_EVENT_CHANNEL_SIZE: usize = 100; pub struct Client { pub(crate) network: Network, pub(crate) client_event_sender: Arc>>, + pub(crate) evm_network: EvmNetwork, } /// Error returned by [`Client::connect`]. @@ -117,6 +121,7 @@ impl Client { Ok(Self { network, client_event_sender: Arc::new(None), + evm_network: Default::default(), }) } @@ -129,6 +134,10 @@ impl Client { client_event_receiver } + + pub fn set_evm_network(&mut self, evm_network: EvmNetwork) { + self.evm_network = evm_network; + } } fn build_client_and_run_swarm(local: bool) -> (Network, mpsc::Receiver) { diff --git a/autonomi/src/client/payment.rs b/autonomi/src/client/payment.rs index f91f71678f..29a8f11576 100644 --- a/autonomi/src/client/payment.rs +++ b/autonomi/src/client/payment.rs @@ -1,11 +1,39 @@ use crate::client::data::PayError; +use crate::client::quote::StoreQuote; use crate::Client; -use ant_evm::{EvmWallet, ProofOfPayment}; +use ant_evm::{AttoTokens, EncodedPeerId, EvmWallet, ProofOfPayment}; use std::collections::HashMap; use xor_name::XorName; -/// Contains the proof of payment for XOR addresses. -pub type Receipt = HashMap; +/// Contains the proof of payments for each XOR address and the amount paid +pub type Receipt = HashMap; + +pub fn receipt_from_store_quotes(quotes: StoreQuote) -> Receipt { + let mut receipt = Receipt::new(); + + for (content_addr, quote_for_address) in quotes.0 { + let price = AttoTokens::from_atto(quote_for_address.price()); + + let mut proof_of_payment = ProofOfPayment { + peer_quotes: vec![], + }; + + for (peer_id, quote, _amount) in quote_for_address.0 { + proof_of_payment + .peer_quotes + .push((EncodedPeerId::from(peer_id), quote)); + } + + // skip empty proofs + if proof_of_payment.peer_quotes.is_empty() { + continue; + } + + receipt.insert(content_addr, (proof_of_payment, price)); + } + + receipt +} /// Payment options for data payments. #[derive(Clone)] @@ -35,16 +63,12 @@ impl From for PaymentOption { impl Client { pub(crate) async fn pay_for_content_addrs( &self, - content_addrs: impl Iterator, + content_addrs: impl Iterator + Clone, payment_option: PaymentOption, ) -> Result { match payment_option { PaymentOption::Wallet(wallet) => { - let (receipt, _) = self.pay(content_addrs, &wallet).await?; - debug!( - "Paid for content addresses with wallet and the receipt is {:?}", - receipt - ); + let receipt = self.pay(content_addrs, &wallet).await?; Ok(receipt) } PaymentOption::Receipt(receipt) => Ok(receipt), diff --git a/autonomi/src/client/quote.rs b/autonomi/src/client/quote.rs new file mode 100644 index 0000000000..9794f165d7 --- /dev/null +++ b/autonomi/src/client/quote.rs @@ -0,0 +1,209 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use super::{data::CostError, Client}; +use crate::client::rate_limiter::RateLimiter; +use ant_evm::payment_vault::get_market_price; +use ant_evm::{Amount, EvmNetwork, PaymentQuote, QuotePayment, QuotingMetrics}; +use ant_networking::{Network, NetworkError}; +use ant_protocol::{storage::ChunkAddress, NetworkAddress}; +use libp2p::PeerId; +use std::collections::HashMap; +use xor_name::XorName; + +/// A quote for a single address +pub struct QuoteForAddress(pub(crate) Vec<(PeerId, PaymentQuote, Amount)>); + +impl QuoteForAddress { + pub fn price(&self) -> Amount { + self.0.iter().map(|(_, _, price)| price).sum() + } +} + +/// A quote for many addresses +pub struct StoreQuote(pub HashMap); + +impl StoreQuote { + pub fn price(&self) -> Amount { + self.0.values().map(|quote| quote.price()).sum() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn payments(&self) -> Vec { + let mut quote_payments = vec![]; + for (_address, quote) in self.0.iter() { + for (_peer, quote, price) in quote.0.iter() { + quote_payments.push((quote.hash(), quote.rewards_address, *price)); + } + } + quote_payments + } +} + +impl Client { + pub(crate) async fn get_store_quotes( + &self, + content_addrs: impl Iterator, + ) -> Result { + // get all quotes from nodes + let futures: Vec<_> = content_addrs + .into_iter() + .map(|content_addr| fetch_store_quote_with_retries(&self.network, content_addr)) + .collect(); + let raw_quotes_per_addr = futures::future::try_join_all(futures).await?; + + // choose the quotes to pay for each address + let mut quotes_to_pay_per_addr = HashMap::new(); + + let mut rate_limiter = RateLimiter::new(); + + for (content_addr, raw_quotes) in raw_quotes_per_addr { + // FIXME: find better way to deal with paid content addrs and feedback to the user + // assume that content addr is already paid for and uploaded + if raw_quotes.is_empty() { + continue; + } + + // ask smart contract for the market price + let quoting_metrics: Vec = raw_quotes + .clone() + .iter() + .map(|(_, q)| q.quoting_metrics.clone()) + .collect(); + + let all_prices = get_market_price_with_rate_limiter_and_retries( + &self.evm_network, + &mut rate_limiter, + quoting_metrics.clone(), + ) + .await?; + + let mut prices: Vec<(PeerId, PaymentQuote, Amount)> = all_prices + .into_iter() + .zip(raw_quotes.into_iter()) + .map(|(price, (peer, quote))| (peer, quote, price)) + .collect(); + + // sort by price + prices.sort_by(|(_, _, price_a), (_, _, price_b)| price_a.cmp(price_b)); + + // we need at least 5 valid quotes to pay for the data + const MINIMUM_QUOTES_TO_PAY: usize = 5; + match &prices[..] { + [first, second, third, fourth, fifth, ..] => { + let (p1, q1, _) = first; + let (p2, q2, _) = second; + + // don't pay for the cheapest 2 quotes but include them + let first = (*p1, q1.clone(), Amount::ZERO); + let second = (*p2, q2.clone(), Amount::ZERO); + + // pay for the rest + quotes_to_pay_per_addr.insert( + content_addr, + QuoteForAddress(vec![ + first, + second, + third.clone(), + fourth.clone(), + fifth.clone(), + ]), + ); + } + _ => { + return Err(CostError::NotEnoughNodeQuotes( + content_addr, + prices.len(), + MINIMUM_QUOTES_TO_PAY, + )); + } + } + } + + Ok(StoreQuote(quotes_to_pay_per_addr)) + } +} + +/// Fetch a store quote for a content address. +async fn fetch_store_quote( + network: &Network, + content_addr: XorName, +) -> Result, NetworkError> { + network + .get_store_quote_from_network( + NetworkAddress::from_chunk_address(ChunkAddress::new(content_addr)), + vec![], + ) + .await +} + +/// Fetch a store quote for a content address with a retry strategy. +async fn fetch_store_quote_with_retries( + network: &Network, + content_addr: XorName, +) -> Result<(XorName, Vec<(PeerId, PaymentQuote)>), CostError> { + let mut retries = 0; + + loop { + match fetch_store_quote(network, content_addr).await { + Ok(quote) => { + break Ok((content_addr, quote)); + } + Err(err) if retries < 2 => { + retries += 1; + error!("Error while fetching store quote: {err:?}, retry #{retries}"); + } + Err(err) => { + error!( + "Error while fetching store quote: {err:?}, stopping after {retries} retries" + ); + break Err(CostError::CouldNotGetStoreQuote(content_addr)); + } + } + } +} + +async fn get_market_price_with_rate_limiter_and_retries( + evm_network: &EvmNetwork, + rate_limiter: &mut RateLimiter, + quoting_metrics: Vec, +) -> Result, ant_evm::payment_vault::error::Error> { + const MAX_RETRIES: u64 = 2; + let mut retries: u64 = 0; + let mut interval_in_ms: u64 = 1000; + + loop { + rate_limiter + .wait_interval_since_last_request(interval_in_ms) + .await; + + match get_market_price(evm_network, quoting_metrics.clone()).await { + Ok(amounts) => { + break Ok(amounts); + } + Err(err) => { + if err.to_string().contains("429") && retries < MAX_RETRIES { + retries += 1; + interval_in_ms *= retries * 2; + error!("Error while fetching quote market price: {err:?}, retry #{retries}"); + continue; + } else { + error!("Error while fetching quote market price: {err:?}, stopping after {retries} retries"); + break Err(err); + } + } + } + } +} diff --git a/autonomi/src/client/rate_limiter.rs b/autonomi/src/client/rate_limiter.rs new file mode 100644 index 0000000000..b1935f6e83 --- /dev/null +++ b/autonomi/src/client/rate_limiter.rs @@ -0,0 +1,27 @@ +use ant_networking::target_arch::{sleep, Duration, Instant}; + +pub struct RateLimiter { + last_request_time: Option, +} + +impl RateLimiter { + pub fn new() -> Self { + Self { + last_request_time: None, + } + } + + pub async fn wait_interval_since_last_request(&mut self, interval_in_ms: u64) { + if let Some(last_request_time) = self.last_request_time { + let elapsed_time = last_request_time.elapsed(); + + let interval = Duration::from_millis(interval_in_ms); + + if elapsed_time < interval { + sleep(interval - elapsed_time).await; + } + } + + self.last_request_time = Some(Instant::now()); + } +} diff --git a/autonomi/src/client/registers.rs b/autonomi/src/client/registers.rs index 0d19fb27fe..fa353d4873 100644 --- a/autonomi/src/client/registers.rs +++ b/autonomi/src/client/registers.rs @@ -49,6 +49,8 @@ pub enum RegisterError { CouldNotSign(#[source] ant_registers::Error), #[error("Received invalid quote from node, this node is possibly malfunctioning, try another node by trying another register name")] InvalidQuote, + #[error("The payment proof contains no payees.")] + PayeesMissing, } #[derive(Clone, Debug)] @@ -247,11 +249,13 @@ impl Client { // get cost to store register // NB TODO: register should be priced differently from other data - let cost_map = self.get_store_quotes(std::iter::once(reg_xor)).await?; + let store_quote = self.get_store_quotes(std::iter::once(reg_xor)).await?; + let total_cost = AttoTokens::from_atto( - cost_map + store_quote + .0 .values() - .map(|quote| quote.2.cost.as_atto()) + .map(|quote| quote.price()) .sum::(), ); debug!("Calculated the cost to create register with name: {name} is {total_cost}"); @@ -302,24 +306,30 @@ impl Client { let reg_xor = address.xorname(); debug!("Paying for register at address: {address}"); - let (payment_proofs, _skipped) = self + let payment_proofs = self .pay(std::iter::once(reg_xor), wallet) .await .inspect_err(|err| { error!("Failed to pay for register at address: {address} : {err}") })?; - let proof = if let Some(proof) = payment_proofs.get(®_xor) { - proof + let (proof, price) = if let Some((proof, price)) = payment_proofs.get(®_xor) { + (proof, price) } else { // register was skipped, meaning it was already paid for error!("Register at address: {address} was already paid for"); return Err(RegisterError::Network(NetworkError::RegisterAlreadyExists)); }; - let payee = proof - .to_peer_id_payee() - .ok_or(RegisterError::InvalidQuote) - .inspect_err(|err| error!("Failed to get payee from payment proof: {err}"))?; + let payees = proof.payees(); + + if payees.is_empty() { + error!( + "Failed to get payees from payment proof: {:?}", + RegisterError::PayeesMissing + ); + return Err(RegisterError::PayeesMissing); + } + let signed_register = register.signed_reg.clone(); let record = Record { @@ -341,10 +351,11 @@ impl Client { expected_holders: Default::default(), is_register: true, }; + let put_cfg = PutRecordCfg { put_quorum: Quorum::All, retry_strategy: None, - use_put_record_to: Some(vec![payee]), + use_put_record_to: Some(payees), verification: Some((VerificationKind::Network, get_cfg)), }; @@ -359,7 +370,7 @@ impl Client { if let Some(channel) = self.client_event_sender.as_ref() { let summary = UploadSummary { record_count: 1, - tokens_spent: proof.quote.cost.as_atto(), + tokens_spent: price.as_atto(), }; if let Err(err) = channel.send(ClientEvent::UploadComplete(summary)).await { error!("Failed to send client event: {err}"); diff --git a/autonomi/src/client/utils.rs b/autonomi/src/client/utils.rs index 9207b035c2..ad2aeececb 100644 --- a/autonomi/src/client/utils.rs +++ b/autonomi/src/client/utils.rs @@ -6,27 +6,23 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. -use crate::client::payment::Receipt; -use crate::utils::receipt_from_cost_map_and_payments; -use ant_evm::{EvmWallet, ProofOfPayment, QuotePayment}; -use ant_networking::{ - GetRecordCfg, Network, NetworkError, PayeeQuote, PutRecordCfg, VerificationKind, -}; +use crate::client::payment::{receipt_from_store_quotes, Receipt}; +use ant_evm::{EvmWallet, ProofOfPayment}; +use ant_networking::{GetRecordCfg, PutRecordCfg, VerificationKind}; use ant_protocol::{ messages::ChunkProof, - storage::{try_serialize_record, Chunk, ChunkAddress, RecordKind, RetryStrategy}, - NetworkAddress, + storage::{try_serialize_record, Chunk, RecordKind, RetryStrategy}, }; use bytes::Bytes; use futures::stream::{FuturesUnordered, StreamExt}; use libp2p::kad::{Quorum, Record}; use rand::{thread_rng, Rng}; use self_encryption::{decrypt_full_set, DataMap, EncryptedChunk}; -use std::{collections::HashMap, future::Future, num::NonZero}; +use std::{future::Future, num::NonZero}; use xor_name::XorName; use super::{ - data::{CostError, GetError, PayError, PutError, CHUNK_DOWNLOAD_BATCH_SIZE}, + data::{GetError, PayError, PutError, CHUNK_DOWNLOAD_BATCH_SIZE}, Client, }; use crate::self_encryption::DataMapLevel; @@ -104,9 +100,13 @@ impl Client { chunk: &Chunk, payment: ProofOfPayment, ) -> Result<(), PutError> { - let storing_node = payment.to_peer_id_payee().expect("Missing node Peer ID"); + let storing_nodes = payment.payees(); + + if storing_nodes.is_empty() { + return Err(PutError::PayeesMissing); + } - debug!("Storing chunk: {chunk:?} to {:?}", storing_node); + debug!("Storing chunk: {chunk:?} to {:?}", storing_nodes); let key = chunk.network_address().to_record_key(); @@ -151,23 +151,22 @@ impl Client { let put_cfg = PutRecordCfg { put_quorum: Quorum::One, retry_strategy: Some(RetryStrategy::Balanced), - use_put_record_to: Some(vec![storing_node]), + use_put_record_to: Some(storing_nodes.clone()), verification, }; let payment_upload = Ok(self.network.put_record(record, &put_cfg).await?); - debug!("Successfully stored chunk: {chunk:?} to {storing_node:?}"); + debug!("Successfully stored chunk: {chunk:?} to {storing_nodes:?}"); payment_upload } /// Pay for the chunks and get the proof of payment. pub(crate) async fn pay( &self, - content_addrs: impl Iterator, + content_addrs: impl Iterator + Clone, wallet: &EvmWallet, - ) -> Result<(Receipt, Vec), PayError> { - let cost_map = self.get_store_quotes(content_addrs).await?; - - let (quote_payments, skipped_chunks) = extract_quote_payments(&cost_map); + ) -> Result { + let number_of_content_addrs = content_addrs.clone().count(); + let quotes = self.get_store_quotes(content_addrs).await?; // Make sure nobody else can use the wallet while we are paying debug!("Waiting for wallet lock"); @@ -177,8 +176,8 @@ impl Client { // TODO: the error might contain some succeeded quote payments as well. These should be returned on err, so that they can be skipped when retrying. // TODO: retry when it fails? // Execute chunk payments - let payments = wallet - .pay_for_quotes(quote_payments) + let _payments = wallet + .pay_for_quotes(quotes.payments()) .await .map_err(|err| PayError::from(err.0))?; @@ -186,89 +185,19 @@ impl Client { drop(lock_guard); debug!("Unlocked wallet"); - let proofs = receipt_from_cost_map_and_payments(cost_map, &payments); - + let skipped_chunks = number_of_content_addrs - quotes.len(); trace!( "Chunk payments of {} chunks completed. {} chunks were free / already paid for", - proofs.len(), - skipped_chunks.len() + quotes.len(), + skipped_chunks ); - Ok((proofs, skipped_chunks)) - } + let receipt = receipt_from_store_quotes(quotes); - pub(crate) async fn get_store_quotes( - &self, - content_addrs: impl Iterator, - ) -> Result, CostError> { - let futures: Vec<_> = content_addrs - .into_iter() - .map(|content_addr| fetch_store_quote_with_retries(&self.network, content_addr)) - .collect(); - - let quotes = futures::future::try_join_all(futures).await?; - - Ok(quotes.into_iter().collect::>()) + Ok(receipt) } } -/// Fetch a store quote for a content address with a retry strategy. -async fn fetch_store_quote_with_retries( - network: &Network, - content_addr: XorName, -) -> Result<(XorName, PayeeQuote), CostError> { - let mut retries = 0; - - loop { - match fetch_store_quote(network, content_addr).await { - Ok(quote) => { - break Ok((content_addr, quote)); - } - Err(err) if retries < 2 => { - retries += 1; - error!("Error while fetching store quote: {err:?}, retry #{retries}"); - } - Err(err) => { - error!( - "Error while fetching store quote: {err:?}, stopping after {retries} retries" - ); - break Err(CostError::CouldNotGetStoreQuote(content_addr)); - } - } - } -} - -/// Fetch a store quote for a content address. -async fn fetch_store_quote( - network: &Network, - content_addr: XorName, -) -> Result { - network - .get_store_costs_from_network( - NetworkAddress::from_chunk_address(ChunkAddress::new(content_addr)), - vec![], - ) - .await -} - -/// Form to be executed payments and already executed payments from a cost map. -pub(crate) fn extract_quote_payments( - cost_map: &HashMap, -) -> (Vec, Vec) { - let mut to_be_paid = vec![]; - let mut already_paid = vec![]; - - for (chunk_address, (_, _, quote)) in cost_map.iter() { - if quote.cost.is_zero() { - already_paid.push(*chunk_address); - } else { - to_be_paid.push((quote.hash(), quote.rewards_address, quote.cost.as_atto())); - } - } - - (to_be_paid, already_paid) -} - pub(crate) async fn process_tasks_with_max_concurrency(tasks: I, batch_size: usize) -> Vec where I: IntoIterator, diff --git a/autonomi/src/client/vault.rs b/autonomi/src/client/vault.rs index 83553e3e16..dd69f8f9d7 100644 --- a/autonomi/src/client/vault.rs +++ b/autonomi/src/client/vault.rs @@ -152,11 +152,13 @@ impl Client { let vault_xor = scratch.network_address().as_xorname().unwrap_or_default(); // NB TODO: vault should be priced differently from other data - let cost_map = self.get_store_quotes(std::iter::once(vault_xor)).await?; + let store_quote = self.get_store_quotes(std::iter::once(vault_xor)).await?; + let total_cost = AttoTokens::from_atto( - cost_map + store_quote + .0 .values() - .map(|quote| quote.2.cost.as_atto()) + .map(|quote| quote.price()) .sum::(), ); @@ -197,12 +199,12 @@ impl Client { error!("Failed to pay for new vault at addr: {scratch_address:?} : {err}"); })?; - let proof = match receipt.values().next() { + let (proof, price) = match receipt.values().next() { Some(proof) => proof, None => return Err(PutError::PaymentUnexpectedlyInvalid(scratch_address)), }; - total_cost = proof.quote.cost; + total_cost = *price; Record { key: scratch_key, diff --git a/autonomi/src/lib.rs b/autonomi/src/lib.rs index 7f200df9cc..f612146f1d 100644 --- a/autonomi/src/lib.rs +++ b/autonomi/src/lib.rs @@ -64,14 +64,11 @@ extern crate tracing; pub mod client; mod self_encryption; -mod utils; pub use ant_evm::get_evm_network_from_env; pub use ant_evm::EvmNetwork as Network; pub use ant_evm::EvmWallet as Wallet; pub use ant_evm::RewardsAddress; -#[cfg(feature = "external-signer")] -pub use utils::receipt_from_quotes_and_payments; #[doc(no_inline)] // Place this under 'Re-exports' in the docs. pub use bytes::Bytes; diff --git a/autonomi/src/utils.rs b/autonomi/src/utils.rs deleted file mode 100644 index 1348c0c685..0000000000 --- a/autonomi/src/utils.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::client::payment::Receipt; -use ant_evm::{PaymentQuote, ProofOfPayment, QuoteHash, TxHash}; -use ant_networking::PayeeQuote; -use std::collections::{BTreeMap, HashMap}; -use xor_name::XorName; - -pub fn cost_map_to_quotes( - cost_map: HashMap, -) -> HashMap { - cost_map.into_iter().map(|(k, (_, _, v))| (k, v)).collect() -} - -pub fn receipt_from_cost_map_and_payments( - cost_map: HashMap, - payments: &BTreeMap, -) -> Receipt { - let quotes = cost_map_to_quotes(cost_map); - receipt_from_quotes_and_payments("es, payments) -} - -pub fn receipt_from_quotes_and_payments( - quotes: &HashMap, - payments: &BTreeMap, -) -> Receipt { - quotes - .iter() - .filter_map(|(xor_name, quote)| { - payments.get("e.hash()).map(|tx_hash| { - ( - *xor_name, - ProofOfPayment { - quote: quote.clone(), - tx_hash: *tx_hash, - }, - ) - }) - }) - .collect() -} diff --git a/autonomi/tests/external_signer.rs b/autonomi/tests/external_signer.rs index 58722c5d45..6b918f9370 100644 --- a/autonomi/tests/external_signer.rs +++ b/autonomi/tests/external_signer.rs @@ -6,10 +6,11 @@ use ant_evm::{QuoteHash, TxHash}; use ant_logging::LogBuilder; use autonomi::client::external_signer::encrypt_data; use autonomi::client::files::archive::{Metadata, PrivateArchive}; -use autonomi::client::payment::Receipt; +use autonomi::client::payment::{receipt_from_store_quotes, Receipt}; +use autonomi::client::quote::StoreQuote; use autonomi::client::vault::user_data::USER_DATA_VAULT_CONTENT_IDENTIFIER; use autonomi::client::vault::VaultSecretKey; -use autonomi::{receipt_from_quotes_and_payments, Client, Wallet}; +use autonomi::{Client, Wallet}; use bytes::Bytes; use std::collections::BTreeMap; use std::time::Duration; @@ -34,7 +35,7 @@ async fn pay_for_data(client: &Client, wallet: &Wallet, data: Bytes) -> eyre::Re async fn pay_for_content_addresses( client: &Client, wallet: &Wallet, - content_addrs: impl Iterator, + content_addrs: impl Iterator + Clone, ) -> eyre::Result { let (quotes, quote_payments, _free_chunks) = client .get_quotes_for_content_addresses(content_addrs) @@ -93,7 +94,7 @@ async fn pay_for_content_addresses( } // Payment proofs - Ok(receipt_from_quotes_and_payments("es, &payments)) + Ok(receipt_from_store_quotes(StoreQuote(quotes))) } // Example of how put would be done using external signers. diff --git a/evmlib/Cargo.toml b/evmlib/Cargo.toml index cb567e24e3..5e4a5b805e 100644 --- a/evmlib/Cargo.toml +++ b/evmlib/Cargo.toml @@ -14,7 +14,7 @@ local = [] external-signer = [] [dependencies] -alloy = { version = "0.5.3", default-features = false, features = ["std", "reqwest-rustls-tls", "provider-anvil-node", "sol-types", "json", "signers", "contract", "signer-local", "network"] } +alloy = { version = "0.7.3", default-features = false, features = ["contract", "json-rpc", "network", "node-bindings", "provider-http", "reqwest-rustls-tls", "rpc-client", "rpc-types", "signer-local", "std"] } dirs-next = "~2.0.0" serde = "=1.0.210" serde_with = { version = "3.11.0", features = ["macros"] } diff --git a/evmlib/abi/IPaymentVault.json b/evmlib/abi/IPaymentVault.json new file mode 100644 index 0000000000..d1ca0a9f67 --- /dev/null +++ b/evmlib/abi/IPaymentVault.json @@ -0,0 +1,208 @@ +[ + { + "inputs": [], + "name": "AntTokenNull", + "type": "error" + }, + { + "inputs": [], + "name": "BatchLimitExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInputLength", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "rewardsAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "quoteHash", + "type": "bytes32" + } + ], + "name": "DataPaymentMade", + "type": "event" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "closeRecordsStored", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRecords", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "receivedPaymentCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liveTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "networkDensity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "networkSize", + "type": "uint256" + } + ], + "internalType": "struct IPaymentVault.QuotingMetrics[]", + "name": "_metrics", + "type": "tuple[]" + } + ], + "name": "getQuote", + "outputs": [ + { + "internalType": "uint256[]", + "name": "prices", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "rewardsAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "quoteHash", + "type": "bytes32" + } + ], + "internalType": "struct IPaymentVault.DataPayment[]", + "name": "_payments", + "type": "tuple[]" + } + ], + "name": "payForQuotes", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "closeRecordsStored", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRecords", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "receivedPaymentCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liveTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "networkDensity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "networkSize", + "type": "uint256" + } + ], + "internalType": "struct IPaymentVault.QuotingMetrics", + "name": "metrics", + "type": "tuple" + }, + { + "internalType": "address", + "name": "rewardsAddress", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "quoteHash", + "type": "bytes32" + } + ], + "internalType": "struct IPaymentVault.PaymentVerification[]", + "name": "_payments", + "type": "tuple[]" + } + ], + "name": "verifyPayment", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "quoteHash", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amountPaid", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isValid", + "type": "bool" + } + ], + "internalType": "struct IPaymentVault.PaymentVerificationResult[3]", + "name": "verificationResults", + "type": "tuple[3]" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/evmlib/artifacts/DataPayments.json b/evmlib/artifacts/DataPayments.json deleted file mode 100644 index a72afa0b8b..0000000000 --- a/evmlib/artifacts/DataPayments.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "_format": "hh-sol-artifact-1", - "contractName": "DataPayments", - "sourceName": "contracts/DataPayments.sol", - "abi": [ - { - "inputs": [ - { - "internalType": "address", - "name": "_tokenAddress", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "rewardsAddress", - "type": "address" - }, - { - "indexed": true, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "indexed": true, - "internalType": "bytes32", - "name": "quoteHash", - "type": "bytes32" - } - ], - "name": "DataPaymentMade", - "type": "event" - }, - { - "inputs": [], - "name": "PAYMENT_TOKEN_ADDRESS", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "address", - "name": "rewardsAddress", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "quoteHash", - "type": "bytes32" - } - ], - "internalType": "struct DataPayments.DataPayment[]", - "name": "dataPayments", - "type": "tuple[]" - } - ], - "name": "submitDataPayments", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ], - "bytecode": "0x60a060405234801561001057600080fd5b506040516105f73803806105f783398101604081905261002f916100a6565b6001600160a01b0381166100955760405162461bcd60e51b8152602060048201526024808201527f546f6b656e20616464726573732063616e6e6f74206265207a65726f206164646044820152637265737360e01b606482015260840160405180910390fd5b6001600160a01b03166080526100d6565b6000602082840312156100b857600080fd5b81516001600160a01b03811681146100cf57600080fd5b9392505050565b6080516104f26101056000396000818160400152818161015101528181610253015261035301526104f26000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80635c0d32861461003b578063dee1dfa01461007e575b600080fd5b6100627f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b03909116815260200160405180910390f35b61009161008c3660046103c6565b610093565b005b60005b8181101561012b57368383838181106100b1576100b161043b565b6060029190910191506100d79050336100cd6020840184610451565b8360200135610130565b6040810135602082018035906100ed9084610451565b6001600160a01b03167ff998960b1c6f0e0e89b7bbe6b6fbf3e03e6f08eee5b8430877d8adb8e149d58060405160405180910390a450600101610096565b505050565b6040516370a0823160e01b81526001600160a01b03848116600483015282917f0000000000000000000000000000000000000000000000000000000000000000909116906370a0823190602401602060405180830381865afa15801561019a573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101be9190610481565b101561021c5760405162461bcd60e51b815260206004820152602260248201527f57616c6c657420646f6573206e6f74206861766520656e6f75676820746f6b656044820152616e7360f01b60648201526084015b60405180910390fd5b6001600160a01b038316301461032557604051636eb1769f60e11b81526001600160a01b03848116600483015230602483015282917f00000000000000000000000000000000000000000000000000000000000000009091169063dd62ed3e90604401602060405180830381865afa15801561029c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102c09190610481565b10156103255760405162461bcd60e51b815260206004820152602e60248201527f436f6e7472616374206973206e6f7420616c6c6f77656420746f207370656e6460448201526d20656e6f75676820746f6b656e7360901b6064820152608401610213565b6040516323b872dd60e01b81526001600160a01b0384811660048301528381166024830152604482018390527f000000000000000000000000000000000000000000000000000000000000000016906323b872dd906064016020604051808303816000875af115801561039c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103c0919061049a565b50505050565b600080602083850312156103d957600080fd5b823567ffffffffffffffff808211156103f157600080fd5b818501915085601f83011261040557600080fd5b81358181111561041457600080fd5b86602060608302850101111561042957600080fd5b60209290920196919550909350505050565b634e487b7160e01b600052603260045260246000fd5b60006020828403121561046357600080fd5b81356001600160a01b038116811461047a57600080fd5b9392505050565b60006020828403121561049357600080fd5b5051919050565b6000602082840312156104ac57600080fd5b8151801515811461047a57600080fdfea26469706673582212206f3a305284dc687832455d7d49b202dcf22b32d76aff5ccd14c3c8539596bcf464736f6c63430008180033", - "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c80635c0d32861461003b578063dee1dfa01461007e575b600080fd5b6100627f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b03909116815260200160405180910390f35b61009161008c3660046103c6565b610093565b005b60005b8181101561012b57368383838181106100b1576100b161043b565b6060029190910191506100d79050336100cd6020840184610451565b8360200135610130565b6040810135602082018035906100ed9084610451565b6001600160a01b03167ff998960b1c6f0e0e89b7bbe6b6fbf3e03e6f08eee5b8430877d8adb8e149d58060405160405180910390a450600101610096565b505050565b6040516370a0823160e01b81526001600160a01b03848116600483015282917f0000000000000000000000000000000000000000000000000000000000000000909116906370a0823190602401602060405180830381865afa15801561019a573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101be9190610481565b101561021c5760405162461bcd60e51b815260206004820152602260248201527f57616c6c657420646f6573206e6f74206861766520656e6f75676820746f6b656044820152616e7360f01b60648201526084015b60405180910390fd5b6001600160a01b038316301461032557604051636eb1769f60e11b81526001600160a01b03848116600483015230602483015282917f00000000000000000000000000000000000000000000000000000000000000009091169063dd62ed3e90604401602060405180830381865afa15801561029c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102c09190610481565b10156103255760405162461bcd60e51b815260206004820152602e60248201527f436f6e7472616374206973206e6f7420616c6c6f77656420746f207370656e6460448201526d20656e6f75676820746f6b656e7360901b6064820152608401610213565b6040516323b872dd60e01b81526001600160a01b0384811660048301528381166024830152604482018390527f000000000000000000000000000000000000000000000000000000000000000016906323b872dd906064016020604051808303816000875af115801561039c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103c0919061049a565b50505050565b600080602083850312156103d957600080fd5b823567ffffffffffffffff808211156103f157600080fd5b818501915085601f83011261040557600080fd5b81358181111561041457600080fd5b86602060608302850101111561042957600080fd5b60209290920196919550909350505050565b634e487b7160e01b600052603260045260246000fd5b60006020828403121561046357600080fd5b81356001600160a01b038116811461047a57600080fd5b9392505050565b60006020828403121561049357600080fd5b5051919050565b6000602082840312156104ac57600080fd5b8151801515811461047a57600080fdfea26469706673582212206f3a305284dc687832455d7d49b202dcf22b32d76aff5ccd14c3c8539596bcf464736f6c63430008180033", - "linkReferences": {}, - "deployedLinkReferences": {} -} \ No newline at end of file diff --git a/evmlib/artifacts/PaymentVaultNoProxy.json b/evmlib/artifacts/PaymentVaultNoProxy.json new file mode 100644 index 0000000000..9b006d274e --- /dev/null +++ b/evmlib/artifacts/PaymentVaultNoProxy.json @@ -0,0 +1,339 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "PaymentVault", + "sourceName": "contracts/PaymentVaultNoProxy.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "_antToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_batchLimit", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "AntTokenNull", + "type": "error" + }, + { + "inputs": [], + "name": "BatchLimitExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInputLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "rewardsAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "quoteHash", + "type": "bytes32" + } + ], + "name": "DataPaymentMade", + "type": "event" + }, + { + "inputs": [], + "name": "antToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "batchLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "closeRecordsStored", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRecords", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "receivedPaymentCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liveTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "networkDensity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "networkSize", + "type": "uint256" + } + ], + "internalType": "struct IPaymentVault.QuotingMetrics[]", + "name": "", + "type": "tuple[]" + } + ], + "name": "getQuote", + "outputs": [ + { + "internalType": "uint256[]", + "name": "prices", + "type": "uint256[]" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "rewardsAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "quoteHash", + "type": "bytes32" + } + ], + "internalType": "struct IPaymentVault.DataPayment[]", + "name": "_payments", + "type": "tuple[]" + } + ], + "name": "payForQuotes", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "payments", + "outputs": [ + { + "internalType": "address", + "name": "rewardsAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "quoteHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "requiredPaymentVerificationLength", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "closeRecordsStored", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxRecords", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "receivedPaymentCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liveTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "networkDensity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "networkSize", + "type": "uint256" + } + ], + "internalType": "struct IPaymentVault.QuotingMetrics", + "name": "metrics", + "type": "tuple" + }, + { + "internalType": "address", + "name": "rewardsAddress", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "quoteHash", + "type": "bytes32" + } + ], + "internalType": "struct IPaymentVault.PaymentVerification[]", + "name": "_payments", + "type": "tuple[]" + } + ], + "name": "verifyPayment", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "quoteHash", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amountPaid", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isValid", + "type": "bool" + } + ], + "internalType": "struct IPaymentVault.PaymentVerificationResult[3]", + "name": "verificationResults", + "type": "tuple[3]" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x6080604052348015600f57600080fd5b50604051610dce380380610dce833981016040819052602c91607f565b6001600160a01b038216605257604051632d06160b60e21b815260040160405180910390fd5b600180546001600160a01b0319166001600160a01b039390931692909217909155600055600560035560b7565b60008060408385031215609157600080fd5b82516001600160a01b038116811460a757600080fd5b6020939093015192949293505050565b610d08806100c66000396000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c806380a38d971161005b57806380a38d9714610128578063b6c2141b14610148578063c7170bb61461015d578063f69c32cd1461016657600080fd5b80630716326d14610082578063474740b1146100e65780634ec42e8e146100fd575b600080fd5b6100bc6100903660046108fc565b60026020819052600091825260409091208054600182015491909201546001600160a01b039092169183565b604080516001600160a01b0390941684526020840192909252908201526060015b60405180910390f35b6100ef60005481565b6040519081526020016100dd565b600154610110906001600160a01b031681565b6040516001600160a01b0390911681526020016100dd565b61013b610136366004610915565b610186565b6040516100dd919061098c565b61015b6101563660046109cf565b6101d3565b005b6100ef60035481565b610179610174366004610a36565b6102c3565b6040516100dd9190610a9d565b60408051600180825281830190925260609160009190602080830190803683370190505090506001816000815181106101c1576101c1610aed565b60209081029190910101529392505050565b60005481908111156101f857604051630d67f41160e21b815260040160405180910390fd5b60005b818110156102bd573684848381811061021657610216610aed565b60600291909101915061024a9050336102326020840184610b28565b6001546001600160a01b03169190602085013561045c565b604080820135600090815260026020522081906102678282610b45565b505060408101356020820180359061027f9084610b28565b6001600160a01b03167ff998960b1c6f0e0e89b7bbe6b6fbf3e03e6f08eee5b8430877d8adb8e149d58060405160405180910390a4506001016101fb565b50505050565b6102cb610838565b60035482146102ed57604051637db491eb60e01b815260040160405180910390fd5b60006102f984846104b6565b905060005b60038110156104545760006002600084846003811061031f5761031f610aed565b602090810291909101516040908101518352828201939093529082016000908120835160608101855281546001600160a01b0316815260018201549381018490526002909101549381018490529350911515919015159085856003811061038857610388610aed565b6020020151602001516001600160a01b031684600001516001600160a01b03161480156103d9575060008686600381106103c4576103c4610aed565b6020020151602001516001600160a01b031614155b9050600060405180606001604052808888600381106103fa576103fa610aed565b60200201516040015181526020018660200151815260200185801561041c5750845b80156104255750835b1515905290508088876003811061043e5761043e610aed565b60200201525050600190930192506102fe915050565b505092915050565b604080516001600160a01b0385811660248301528416604482015260648082018490528251808303909101815260849091019091526020810180516001600160e01b03166323b872dd60e01b1790526102bd908590610691565b6104be610877565b60005b8281101561068a576000600260008686858181106104e1576104e1610aed565b9050610100020160e0013581526020019081526020016000206040518060600160405290816000820160009054906101000a90046001600160a01b03166001600160a01b03166001600160a01b03168152602001600182015481526020016002820154815250509050600260008460006003811061056157610561610aed565b602002015160400151815260200190815260200160002060010154816020015111156105cd576020830180516040850152835190528484838181106105a8576105a8610aed565b905061010002018036038101906105bf9190610beb565b8360005b6020020152610681565b602080840151604090810151600090815260028352206001015490820151111561062c576020830151604084015284848381811061060d5761060d610aed565b905061010002018036038101906106249190610beb565b8360016105c3565b604080840151810151600090815260026020908152919020600101549082015111156106815784848381811061066457610664610aed565b9050610100020180360381019061067b9190610beb565b60408401525b506001016104c1565b5092915050565b60006106a66001600160a01b038416836106fe565b905080516000141580156106cb5750808060200190518101906106c99190610c81565b155b156106f957604051635274afe760e01b81526001600160a01b03841660048201526024015b60405180910390fd5b505050565b606061070c83836000610713565b9392505050565b6060814710156107385760405163cd78605960e01b81523060048201526024016106f0565b600080856001600160a01b031684866040516107549190610ca3565b60006040518083038185875af1925050503d8060008114610791576040519150601f19603f3d011682016040523d82523d6000602084013e610796565b606091505b50915091506107a68683836107b0565b9695505050505050565b6060826107c5576107c08261080c565b61070c565b81511580156107dc57506001600160a01b0384163b155b1561080557604051639996b31560e01b81526001600160a01b03851660048201526024016106f0565b508061070c565b80511561081c5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b50565b60405180606001604052806003905b60408051606081018252600080825260208083018290529282015282526000199092019101816108475790505090565b60405180606001604052806003905b61088e6108a4565b8152602001906001900390816108865790505090565b60405180606001604052806108e86040518060c001604052806000815260200160008152602001600081526020016000815260200160008152602001600081525090565b815260006020820181905260409091015290565b60006020828403121561090e57600080fd5b5035919050565b6000806020838503121561092857600080fd5b823567ffffffffffffffff81111561093f57600080fd5b8301601f8101851361095057600080fd5b803567ffffffffffffffff81111561096757600080fd5b85602060c08302840101111561097c57600080fd5b6020919091019590945092505050565b602080825282518282018190526000918401906040840190835b818110156109c45783518352602093840193909201916001016109a6565b509095945050505050565b600080602083850312156109e257600080fd5b823567ffffffffffffffff8111156109f957600080fd5b8301601f81018513610a0a57600080fd5b803567ffffffffffffffff811115610a2157600080fd5b85602060608302840101111561097c57600080fd5b60008060208385031215610a4957600080fd5b823567ffffffffffffffff811115610a6057600080fd5b8301601f81018513610a7157600080fd5b803567ffffffffffffffff811115610a8857600080fd5b8560208260081b840101111561097c57600080fd5b6101208101818360005b6003811015610ae4578151805184526020810151602085015260408101511515604085015250606083019250602082019150600181019050610aa7565b50505092915050565b634e487b7160e01b600052603260045260246000fd5b6001600160a01b038116811461083557600080fd5b8035610b2381610b03565b919050565b600060208284031215610b3a57600080fd5b813561070c81610b03565b8135610b5081610b03565b81546001600160a01b0319166001600160a01b039190911617815560208201356001820155604090910135600290910155565b6040516060810167ffffffffffffffff81118282101715610bb457634e487b7160e01b600052604160045260246000fd5b60405290565b60405160c0810167ffffffffffffffff81118282101715610bb457634e487b7160e01b600052604160045260246000fd5b600081830361010081128015610c0057600080fd5b506000610c0b610b83565b60c0831215610c18578182fd5b610c20610bba565b853581526020808701359082015260408087013590820152606080870135908201526080808701359082015260a080870135908201528082529250610c6760c08601610b18565b602082015260e09490940135604085015250919392505050565b600060208284031215610c9357600080fd5b8151801515811461070c57600080fd5b6000825160005b81811015610cc45760208186018101518583015201610caa565b50600092019182525091905056fea26469706673582212207d1a9d88b0ba14ca908470a69ea19a09d2c7617056be2605039bc4d121f4fc4b64736f6c634300081c0033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061007d5760003560e01c806380a38d971161005b57806380a38d9714610128578063b6c2141b14610148578063c7170bb61461015d578063f69c32cd1461016657600080fd5b80630716326d14610082578063474740b1146100e65780634ec42e8e146100fd575b600080fd5b6100bc6100903660046108fc565b60026020819052600091825260409091208054600182015491909201546001600160a01b039092169183565b604080516001600160a01b0390941684526020840192909252908201526060015b60405180910390f35b6100ef60005481565b6040519081526020016100dd565b600154610110906001600160a01b031681565b6040516001600160a01b0390911681526020016100dd565b61013b610136366004610915565b610186565b6040516100dd919061098c565b61015b6101563660046109cf565b6101d3565b005b6100ef60035481565b610179610174366004610a36565b6102c3565b6040516100dd9190610a9d565b60408051600180825281830190925260609160009190602080830190803683370190505090506001816000815181106101c1576101c1610aed565b60209081029190910101529392505050565b60005481908111156101f857604051630d67f41160e21b815260040160405180910390fd5b60005b818110156102bd573684848381811061021657610216610aed565b60600291909101915061024a9050336102326020840184610b28565b6001546001600160a01b03169190602085013561045c565b604080820135600090815260026020522081906102678282610b45565b505060408101356020820180359061027f9084610b28565b6001600160a01b03167ff998960b1c6f0e0e89b7bbe6b6fbf3e03e6f08eee5b8430877d8adb8e149d58060405160405180910390a4506001016101fb565b50505050565b6102cb610838565b60035482146102ed57604051637db491eb60e01b815260040160405180910390fd5b60006102f984846104b6565b905060005b60038110156104545760006002600084846003811061031f5761031f610aed565b602090810291909101516040908101518352828201939093529082016000908120835160608101855281546001600160a01b0316815260018201549381018490526002909101549381018490529350911515919015159085856003811061038857610388610aed565b6020020151602001516001600160a01b031684600001516001600160a01b03161480156103d9575060008686600381106103c4576103c4610aed565b6020020151602001516001600160a01b031614155b9050600060405180606001604052808888600381106103fa576103fa610aed565b60200201516040015181526020018660200151815260200185801561041c5750845b80156104255750835b1515905290508088876003811061043e5761043e610aed565b60200201525050600190930192506102fe915050565b505092915050565b604080516001600160a01b0385811660248301528416604482015260648082018490528251808303909101815260849091019091526020810180516001600160e01b03166323b872dd60e01b1790526102bd908590610691565b6104be610877565b60005b8281101561068a576000600260008686858181106104e1576104e1610aed565b9050610100020160e0013581526020019081526020016000206040518060600160405290816000820160009054906101000a90046001600160a01b03166001600160a01b03166001600160a01b03168152602001600182015481526020016002820154815250509050600260008460006003811061056157610561610aed565b602002015160400151815260200190815260200160002060010154816020015111156105cd576020830180516040850152835190528484838181106105a8576105a8610aed565b905061010002018036038101906105bf9190610beb565b8360005b6020020152610681565b602080840151604090810151600090815260028352206001015490820151111561062c576020830151604084015284848381811061060d5761060d610aed565b905061010002018036038101906106249190610beb565b8360016105c3565b604080840151810151600090815260026020908152919020600101549082015111156106815784848381811061066457610664610aed565b9050610100020180360381019061067b9190610beb565b60408401525b506001016104c1565b5092915050565b60006106a66001600160a01b038416836106fe565b905080516000141580156106cb5750808060200190518101906106c99190610c81565b155b156106f957604051635274afe760e01b81526001600160a01b03841660048201526024015b60405180910390fd5b505050565b606061070c83836000610713565b9392505050565b6060814710156107385760405163cd78605960e01b81523060048201526024016106f0565b600080856001600160a01b031684866040516107549190610ca3565b60006040518083038185875af1925050503d8060008114610791576040519150601f19603f3d011682016040523d82523d6000602084013e610796565b606091505b50915091506107a68683836107b0565b9695505050505050565b6060826107c5576107c08261080c565b61070c565b81511580156107dc57506001600160a01b0384163b155b1561080557604051639996b31560e01b81526001600160a01b03851660048201526024016106f0565b508061070c565b80511561081c5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b50565b60405180606001604052806003905b60408051606081018252600080825260208083018290529282015282526000199092019101816108475790505090565b60405180606001604052806003905b61088e6108a4565b8152602001906001900390816108865790505090565b60405180606001604052806108e86040518060c001604052806000815260200160008152602001600081526020016000815260200160008152602001600081525090565b815260006020820181905260409091015290565b60006020828403121561090e57600080fd5b5035919050565b6000806020838503121561092857600080fd5b823567ffffffffffffffff81111561093f57600080fd5b8301601f8101851361095057600080fd5b803567ffffffffffffffff81111561096757600080fd5b85602060c08302840101111561097c57600080fd5b6020919091019590945092505050565b602080825282518282018190526000918401906040840190835b818110156109c45783518352602093840193909201916001016109a6565b509095945050505050565b600080602083850312156109e257600080fd5b823567ffffffffffffffff8111156109f957600080fd5b8301601f81018513610a0a57600080fd5b803567ffffffffffffffff811115610a2157600080fd5b85602060608302840101111561097c57600080fd5b60008060208385031215610a4957600080fd5b823567ffffffffffffffff811115610a6057600080fd5b8301601f81018513610a7157600080fd5b803567ffffffffffffffff811115610a8857600080fd5b8560208260081b840101111561097c57600080fd5b6101208101818360005b6003811015610ae4578151805184526020810151602085015260408101511515604085015250606083019250602082019150600181019050610aa7565b50505092915050565b634e487b7160e01b600052603260045260246000fd5b6001600160a01b038116811461083557600080fd5b8035610b2381610b03565b919050565b600060208284031215610b3a57600080fd5b813561070c81610b03565b8135610b5081610b03565b81546001600160a01b0319166001600160a01b039190911617815560208201356001820155604090910135600290910155565b6040516060810167ffffffffffffffff81118282101715610bb457634e487b7160e01b600052604160045260246000fd5b60405290565b60405160c0810167ffffffffffffffff81118282101715610bb457634e487b7160e01b600052604160045260246000fd5b600081830361010081128015610c0057600080fd5b506000610c0b610b83565b60c0831215610c18578182fd5b610c20610bba565b853581526020808701359082015260408087013590820152606080870135908201526080808701359082015260a080870135908201528082529250610c6760c08601610b18565b602082015260e09490940135604085015250919392505050565b600060208284031215610c9357600080fd5b8151801515811461070c57600080fd5b6000825160005b81811015610cc45760208186018101518583015201610caa565b50600092019182525091905056fea26469706673582212207d1a9d88b0ba14ca908470a69ea19a09d2c7617056be2605039bc4d121f4fc4b64736f6c634300081c0033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/evmlib/src/contract/data_payments/error.rs b/evmlib/src/contract/data_payments/error.rs deleted file mode 100644 index 95ec1c1c27..0000000000 --- a/evmlib/src/contract/data_payments/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::contract::network_token; -use alloy::transports::{RpcError, TransportErrorKind}; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - ContractError(#[from] alloy::contract::Error), - #[error(transparent)] - RpcError(#[from] RpcError), - #[error(transparent)] - NetworkTokenError(#[from] network_token::Error), - #[error(transparent)] - PendingTransactionError(#[from] alloy::providers::PendingTransactionError), - #[error("The transfer limit of 256 has been exceeded")] - TransferLimitExceeded, -} diff --git a/evmlib/src/contract/mod.rs b/evmlib/src/contract/mod.rs index d428880800..405f0c7fd5 100644 --- a/evmlib/src/contract/mod.rs +++ b/evmlib/src/contract/mod.rs @@ -6,5 +6,5 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. -pub mod data_payments; pub mod network_token; +pub mod payment_vault; diff --git a/evmlib/src/contract/payment_vault/error.rs b/evmlib/src/contract/payment_vault/error.rs new file mode 100644 index 0000000000..f4a5b76cce --- /dev/null +++ b/evmlib/src/contract/payment_vault/error.rs @@ -0,0 +1,15 @@ +use alloy::transports::{RpcError, TransportErrorKind}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + ContractError(#[from] alloy::contract::Error), + #[error(transparent)] + RpcError(#[from] RpcError), + #[error(transparent)] + PendingTransactionError(#[from] alloy::providers::PendingTransactionError), + #[error("Payment is invalid.")] + PaymentInvalid, + #[error("Payment verification length must be 3.")] + PaymentVerificationLengthInvalid, +} diff --git a/evmlib/src/contract/payment_vault/handler.rs b/evmlib/src/contract/payment_vault/handler.rs new file mode 100644 index 0000000000..1000d7d108 --- /dev/null +++ b/evmlib/src/contract/payment_vault/handler.rs @@ -0,0 +1,109 @@ +use crate::common::{Address, Amount, Calldata, TxHash}; +use crate::contract::payment_vault::error::Error; +use crate::contract::payment_vault::interface::IPaymentVault; +use crate::contract::payment_vault::interface::IPaymentVault::IPaymentVaultInstance; +use alloy::network::{Network, TransactionBuilder}; +use alloy::providers::Provider; +use alloy::transports::Transport; + +pub struct PaymentVaultHandler, N: Network> { + pub contract: IPaymentVaultInstance, +} + +impl PaymentVaultHandler +where + T: Transport + Clone, + P: Provider, + N: Network, +{ + /// Create a new PaymentVaultHandler instance from a (proxy) contract's address + pub fn new(contract_address: Address, provider: P) -> Self { + let contract = IPaymentVault::new(contract_address, provider); + Self { contract } + } + + /// Set the provider + pub fn set_provider(&mut self, provider: P) { + let address = *self.contract.address(); + self.contract = IPaymentVault::new(address, provider); + } + + /// Fetch a quote from the contract + pub async fn get_quote>>( + &self, + metrics: I, + ) -> Result, Error> { + let metrics: Vec<_> = metrics.into_iter().map(|v| v.into()).collect(); + let mut amounts = self.contract.getQuote(metrics.clone()).call().await?.prices; + + // FIXME: temporary logic until the smart contract gets updated + if amounts.len() == 1 { + let value = amounts[0]; + amounts.resize(metrics.len(), value); + } + + Ok(amounts) + } + + /// Pay for quotes. + pub async fn pay_for_quotes>>( + &self, + data_payments: I, + ) -> Result { + let (calldata, to) = self.pay_for_quotes_calldata(data_payments)?; + + let transaction_request = self + .contract + .provider() + .transaction_request() + .with_to(to) + .with_input(calldata); + + let tx_hash = self + .contract + .provider() + .send_transaction(transaction_request) + .await? + .watch() + .await?; + + Ok(tx_hash) + } + + /// Returns the pay for quotes transaction calldata. + pub fn pay_for_quotes_calldata>>( + &self, + data_payments: I, + ) -> Result<(Calldata, Address), Error> { + let data_payments: Vec = + data_payments.into_iter().map(|item| item.into()).collect(); + + let calldata = self + .contract + .payForQuotes(data_payments) + .calldata() + .to_owned(); + + Ok((calldata, *self.contract.address())) + } + + /// Verify if payments are valid + pub async fn verify_payment>>( + &self, + payment_verifications: I, + ) -> Result<[IPaymentVault::PaymentVerificationResult; 3], Error> { + let payment_verifications: Vec = payment_verifications + .into_iter() + .map(|v| v.into()) + .collect(); + + let results = self + .contract + .verifyPayment(payment_verifications) + .call() + .await? + .verificationResults; + + Ok(results) + } +} diff --git a/evmlib/src/contract/payment_vault/implementation.rs b/evmlib/src/contract/payment_vault/implementation.rs new file mode 100644 index 0000000000..64fd9da1f9 --- /dev/null +++ b/evmlib/src/contract/payment_vault/implementation.rs @@ -0,0 +1,30 @@ +use crate::common::{Address, U256}; +use alloy::network::Network; +use alloy::providers::Provider; +use alloy::sol; +use alloy::transports::Transport; + +sol!( + #[allow(missing_docs)] + #[sol(rpc)] + PaymentVaultImplementation, + "artifacts/PaymentVaultNoProxy.json" +); + +/// Deploys the payment vault contract and returns the contract address +pub async fn deploy( + provider: &P, + network_token_address: Address, + batch_limit: U256, +) -> Address +where + T: Transport + Clone, + P: Provider, + N: Network, +{ + let contract = PaymentVaultImplementation::deploy(provider, network_token_address, batch_limit) + .await + .expect("Could not deploy payment vault implementation contract"); + + *contract.address() +} diff --git a/evmlib/src/contract/payment_vault/interface.rs b/evmlib/src/contract/payment_vault/interface.rs new file mode 100644 index 0000000000..1e2e0f1e7c --- /dev/null +++ b/evmlib/src/contract/payment_vault/interface.rs @@ -0,0 +1,46 @@ +use crate::common::{Address, Amount, QuoteHash, U256}; +use crate::quoting_metrics::QuotingMetrics; +use alloy::primitives::FixedBytes; +use alloy::sol; + +sol!( + #[allow(missing_docs)] + #[derive(Debug)] + #[sol(rpc)] + IPaymentVault, + "abi/IPaymentVault.json" +); + +impl From<(QuoteHash, QuotingMetrics, Address)> for IPaymentVault::PaymentVerification { + fn from(value: (QuoteHash, QuotingMetrics, Address)) -> Self { + Self { + metrics: value.1.into(), + rewardsAddress: value.2, + quoteHash: value.0, + } + } +} + +impl From<(QuoteHash, Address, Amount)> for IPaymentVault::DataPayment { + fn from(value: (QuoteHash, Address, Amount)) -> Self { + Self { + rewardsAddress: value.1, + amount: value.2, + quoteHash: value.0, + } + } +} + +impl From for IPaymentVault::QuotingMetrics { + fn from(value: QuotingMetrics) -> Self { + Self { + closeRecordsStored: U256::from(value.close_records_stored), + maxRecords: U256::from(value.max_records), + receivedPaymentCount: U256::from(value.received_payment_count), + liveTime: U256::from(value.live_time), + networkDensity: FixedBytes::<32>::from(value.network_density.unwrap_or_default()) + .into(), + networkSize: value.network_size.map(U256::from).unwrap_or_default(), + } + } +} diff --git a/evmlib/src/contract/payment_vault/mod.rs b/evmlib/src/contract/payment_vault/mod.rs new file mode 100644 index 0000000000..9b7d1eed3b --- /dev/null +++ b/evmlib/src/contract/payment_vault/mod.rs @@ -0,0 +1,55 @@ +use crate::common::{Address, Amount, QuoteHash}; +use crate::contract::payment_vault::handler::PaymentVaultHandler; +use crate::quoting_metrics::QuotingMetrics; +use crate::utils::http_provider; +use crate::Network; + +pub mod error; +pub mod handler; +pub mod implementation; +pub mod interface; + +pub const MAX_TRANSFERS_PER_TRANSACTION: usize = 256; + +/// Helper function to return a quote for the given quoting metrics. +pub async fn get_market_price( + network: &Network, + quoting_metrics: Vec, +) -> Result, error::Error> { + let provider = http_provider(network.rpc_url().clone()); + let payment_vault = PaymentVaultHandler::new(*network.data_payments_address(), provider); + payment_vault.get_quote(quoting_metrics).await +} + +/// Helper function to verify whether a data payment is valid. +/// Returns the amount paid to the owned quote hashes. +pub async fn verify_data_payment( + network: &Network, + owned_quote_hashes: Vec, + payment: Vec<(QuoteHash, QuotingMetrics, Address)>, +) -> Result { + let provider = http_provider(network.rpc_url().clone()); + let payment_vault = PaymentVaultHandler::new(*network.data_payments_address(), provider); + + let mut amount = Amount::ZERO; + + let payment_verifications: Vec<_> = payment + .into_iter() + .map(interface::IPaymentVault::PaymentVerification::from) + .collect(); + + let payment_verification_results = payment_vault.verify_payment(payment_verifications).await?; + + for payment_verification_result in payment_verification_results { + // TODO we currently fail on a single invalid payment, maybe we should deal with this in a different way + if !payment_verification_result.isValid { + return Err(error::Error::PaymentInvalid); + } + + if owned_quote_hashes.contains(&payment_verification_result.quoteHash) { + amount += payment_verification_result.amountPaid; + } + } + + Ok(amount) +} diff --git a/evmlib/src/event.rs b/evmlib/src/event.rs deleted file mode 100644 index 5cdda3d91e..0000000000 --- a/evmlib/src/event.rs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::common::{Address, Hash, U256}; -use alloy::primitives::{b256, FixedBytes}; -use alloy::rpc::types::Log; - -// Should be updated when the smart contract changes! -pub(crate) const DATA_PAYMENT_EVENT_SIGNATURE: FixedBytes<32> = - b256!("f998960b1c6f0e0e89b7bbe6b6fbf3e03e6f08eee5b8430877d8adb8e149d580"); // DevSkim: ignore DS173237 - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("Topics amount is unexpected. Was expecting 4")] - TopicsAmountUnexpected, - #[error("Event signature is missing")] - EventSignatureMissing, - #[error("Event signature does not match")] - EventSignatureDoesNotMatch, -} - -/// Struct for the ChunkPaymentEvent emitted by the ChunkPayments smart contract. -#[derive(Debug)] -pub(crate) struct ChunkPaymentEvent { - pub rewards_address: Address, - pub amount: U256, - pub quote_hash: Hash, -} - -impl TryFrom for ChunkPaymentEvent { - type Error = Error; - - fn try_from(log: Log) -> Result { - // Verify the amount of topics - if log.topics().len() != 4 { - error!("Topics amount is unexpected. Was expecting 4"); - return Err(Error::TopicsAmountUnexpected); - } - - let topic0 = log - .topics() - .first() - .ok_or(Error::EventSignatureMissing) - .inspect_err(|_| error!("Event signature is missing"))?; - - // Verify the event signature - if topic0 != &DATA_PAYMENT_EVENT_SIGNATURE { - error!( - "Event signature does not match. Expected: {:?}, got: {:?}", - DATA_PAYMENT_EVENT_SIGNATURE, topic0 - ); - return Err(Error::EventSignatureDoesNotMatch); - } - - // Extract the data - let rewards_address = Address::from_slice(&log.topics()[1][12..]); - let amount = U256::from_be_slice(&log.topics()[2][12..]); - let quote_hash = Hash::from_slice(log.topics()[3].as_slice()); - - Ok(Self { - rewards_address, - amount, - quote_hash, - }) - } -} diff --git a/evmlib/src/external_signer.rs b/evmlib/src/external_signer.rs index 20c3aa95df..b7f7ce9b6d 100644 --- a/evmlib/src/external_signer.rs +++ b/evmlib/src/external_signer.rs @@ -7,9 +7,8 @@ // permissions and limitations relating to use of the SAFE Network Software. use crate::common::{Address, Amount, Calldata, QuoteHash, QuotePayment, U256}; -use crate::contract::data_payments::{DataPaymentsHandler, MAX_TRANSFERS_PER_TRANSACTION}; -use crate::contract::network_token::NetworkToken; -use crate::contract::{data_payments, network_token}; +use crate::contract::network_token::{self, NetworkToken}; +use crate::contract::payment_vault::MAX_TRANSFERS_PER_TRANSACTION; use crate::utils::http_provider; use crate::Network; use serde::{Deserialize, Serialize}; @@ -20,7 +19,7 @@ pub enum Error { #[error("Network token contract error: {0}")] NetworkTokenContract(#[from] network_token::Error), #[error("Data payments contract error: {0}")] - DataPaymentsContract(#[from] data_payments::error::Error), + DataPaymentsContract(#[from] crate::contract::payment_vault::error::Error), } /// Approve an address / smart contract to spend this wallet's payment tokens. @@ -73,7 +72,10 @@ pub fn pay_for_quotes_calldata>( let approve_amount = total_amount; let provider = http_provider(network.rpc_url().clone()); - let data_payments = DataPaymentsHandler::new(*network.data_payments_address(), provider); + let data_payments = crate::contract::payment_vault::handler::PaymentVaultHandler::new( + *network.data_payments_address(), + provider, + ); // Divide transfers over multiple transactions if they exceed the max per transaction. let chunks = payments.chunks(MAX_TRANSFERS_PER_TRANSACTION); diff --git a/evmlib/src/lib.rs b/evmlib/src/lib.rs index e0df96d466..abd5d3309a 100644 --- a/evmlib/src/lib.rs +++ b/evmlib/src/lib.rs @@ -6,8 +6,7 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. -use crate::common::{Address, QuoteHash, TxHash, U256}; -use crate::transaction::verify_data_payment; +use crate::common::Address; use alloy::primitives::address; use alloy::transports::http::reqwest; use serde::{Deserialize, Serialize}; @@ -21,11 +20,10 @@ extern crate tracing; pub mod common; pub mod contract; pub mod cryptography; -pub(crate) mod event; #[cfg(feature = "external-signer")] pub mod external_signer; +pub mod quoting_metrics; pub mod testnet; -pub mod transaction; pub mod utils; pub mod wallet; @@ -49,10 +47,10 @@ const ARBITRUM_SEPOLIA_PAYMENT_TOKEN_ADDRESS: Address = // Should be updated when the smart contract changes! const ARBITRUM_ONE_DATA_PAYMENTS_ADDRESS: Address = - address!("887930F30EDEb1B255Cd2273C3F4400919df2EFe"); + address!("607483B50C5F06c25cDC316b6d1E071084EeC9f5"); const ARBITRUM_SEPOLIA_DATA_PAYMENTS_ADDRESS: Address = - address!("Dd56b03Dae2Ab8594D80269EC4518D13F1A110BD"); + address!("993C7739f50899A997fEF20860554b8a28113634"); #[serde_as] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -133,23 +131,4 @@ impl Network { Network::Custom(custom) => &custom.data_payments_address, } } - - pub async fn verify_data_payment( - &self, - tx_hash: TxHash, - quote_hash: QuoteHash, - reward_addr: Address, - amount: U256, - quote_expiration_timestamp_in_secs: u64, - ) -> Result<(), transaction::Error> { - verify_data_payment( - self, - tx_hash, - quote_hash, - reward_addr, - amount, - quote_expiration_timestamp_in_secs, - ) - .await - } } diff --git a/evmlib/src/quoting_metrics.rs b/evmlib/src/quoting_metrics.rs new file mode 100644 index 0000000000..c4971a1b03 --- /dev/null +++ b/evmlib/src/quoting_metrics.rs @@ -0,0 +1,58 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use crate::common::U256; +use serde::{Deserialize, Serialize}; +use std::fmt::{Debug, Formatter, Result as FmtResult}; + +/// Quoting metrics used to generate a quote, or to track peer's status. +#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct QuotingMetrics { + /// the records stored + pub close_records_stored: usize, + /// the max_records configured + pub max_records: usize, + /// number of times that got paid + pub received_payment_count: usize, + /// the duration that node keeps connected to the network, measured in hours + pub live_time: u64, + /// network density from this node's perspective, which is the responsible_range as well + /// This could be calculated via sampling, or equation calculation. + pub network_density: Option<[u8; 32]>, + /// estimated network size + pub network_size: Option, +} + +impl QuotingMetrics { + /// construct an empty QuotingMetrics + pub fn new() -> Self { + Self { + close_records_stored: 0, + max_records: 0, + received_payment_count: 0, + live_time: 0, + network_density: None, + network_size: None, + } + } +} + +impl Default for QuotingMetrics { + fn default() -> Self { + Self::new() + } +} + +impl Debug for QuotingMetrics { + fn fmt(&self, formatter: &mut Formatter) -> FmtResult { + let density_u256 = self.network_density.map(U256::from_be_bytes); + + write!(formatter, "QuotingMetrics {{ close_records_stored: {}, max_records: {}, received_payment_count: {}, live_time: {}, network_density: {density_u256:?}, network_size: {:?} }}", + self.close_records_stored, self.max_records, self.received_payment_count, self.live_time, self.network_size) + } +} diff --git a/evmlib/src/testnet.rs b/evmlib/src/testnet.rs index e5f1f79708..d9c25bcffd 100644 --- a/evmlib/src/testnet.rs +++ b/evmlib/src/testnet.rs @@ -6,9 +6,10 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. -use crate::common::Address; -use crate::contract::data_payments::DataPaymentsHandler; +use crate::common::{Address, Amount}; use crate::contract::network_token::NetworkToken; +use crate::contract::payment_vault; +use crate::contract::payment_vault::handler::PaymentVaultHandler; use crate::reqwest::Url; use crate::{CustomNetwork, Network}; use alloy::hex::ToHexExt; @@ -21,6 +22,8 @@ use alloy::providers::{Identity, ProviderBuilder, ReqwestProvider}; use alloy::signers::local::PrivateKeySigner; use alloy::transports::http::{Client, Http}; +const BATCH_LIMIT: u16 = 256; + pub struct Testnet { anvil: AnvilInstance, rpc_url: Url, @@ -120,7 +123,7 @@ pub async fn deploy_data_payments_contract( rpc_url: &Url, anvil: &AnvilInstance, token_address: Address, -) -> DataPaymentsHandler< +) -> PaymentVaultHandler< Http, FillProvider< JoinFill< @@ -146,5 +149,10 @@ pub async fn deploy_data_payments_contract( .on_http(rpc_url.clone()); // Deploy the contract. - DataPaymentsHandler::deploy(provider, token_address).await + let payment_vault_contract_address = + payment_vault::implementation::deploy(&provider, token_address, Amount::from(BATCH_LIMIT)) + .await; + + // Create a handler for the deployed contract + PaymentVaultHandler::new(payment_vault_contract_address, provider) } diff --git a/evmlib/src/transaction.rs b/evmlib/src/transaction.rs deleted file mode 100644 index 7e09e4495f..0000000000 --- a/evmlib/src/transaction.rs +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::common::{Address, QuoteHash, TxHash, U256}; -use crate::event::{ChunkPaymentEvent, DATA_PAYMENT_EVENT_SIGNATURE}; -use crate::Network; -use alloy::eips::BlockNumberOrTag; -use alloy::primitives::FixedBytes; -use alloy::providers::{Provider, ProviderBuilder}; -use alloy::rpc::types::{Block, Filter, Log, TransactionReceipt}; -use alloy::transports::{RpcError, TransportErrorKind}; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - RpcError(#[from] RpcError), - #[error("Transaction is not confirmed")] - TransactionUnconfirmed, - #[error("Transaction was not found")] - TransactionNotFound, - #[error("Transaction has not been included in a block yet")] - TransactionNotInBlock, - #[error("Block was not found")] - BlockNotFound, - #[error("No event proof found")] - EventProofNotFound, - #[error("Payment was done after the quote expired")] - QuoteExpired, -} - -/// Get a transaction receipt by its hash. -pub async fn get_transaction_receipt_by_hash( - network: &Network, - transaction_hash: TxHash, -) -> Result, Error> { - let provider = ProviderBuilder::new() - .with_recommended_fillers() - .on_http(network.rpc_url().clone()); - let maybe_receipt = provider - .get_transaction_receipt(transaction_hash) - .await - .inspect_err(|err| error!("Error getting transaction receipt for transaction_hash: {transaction_hash:?} : {err:?}", ))?; - debug!("Transaction receipt for {transaction_hash:?}: {maybe_receipt:?}"); - Ok(maybe_receipt) -} - -/// Get a block by its block number. -async fn get_block_by_number(network: &Network, block_number: u64) -> Result, Error> { - let provider = ProviderBuilder::new() - .with_recommended_fillers() - .on_http(network.rpc_url().clone()); - let block = provider - .get_block_by_number(BlockNumberOrTag::Number(block_number), true) - .await - .inspect_err(|err| error!("Error getting block by number for {block_number} : {err:?}",))?; - Ok(block) -} - -/// Get transaction logs using a filter. -async fn get_transaction_logs(network: &Network, filter: Filter) -> Result, Error> { - let provider = ProviderBuilder::new() - .with_recommended_fillers() - .on_http(network.rpc_url().clone()); - let logs = provider - .get_logs(&filter) - .await - .inspect_err(|err| error!("Error getting logs for filter: {filter:?} : {err:?}"))?; - Ok(logs) -} - -/// Get a DataPaymentMade event, filtered by a hashed chunk address and a node address. -/// Useful for a node if it wants to check if payment for a certain chunk has been made. -async fn get_data_payment_event( - network: &Network, - block_number: u64, - quote_hash: QuoteHash, - reward_addr: Address, - amount: U256, -) -> Result, Error> { - debug!( - "Getting data payment event for quote_hash: {quote_hash:?}, reward_addr: {reward_addr:?}" - ); - let topic1: FixedBytes<32> = FixedBytes::left_padding_from(reward_addr.as_slice()); - - let filter = Filter::new() - .event_signature(DATA_PAYMENT_EVENT_SIGNATURE) - .topic1(topic1) - .topic2(amount) - .topic3(quote_hash) - .from_block(block_number) - .to_block(block_number); - - get_transaction_logs(network, filter).await -} - -/// Verify if a data payment is confirmed. -pub async fn verify_data_payment( - network: &Network, - tx_hash: TxHash, - quote_hash: QuoteHash, - reward_addr: Address, - amount: U256, - quote_expiration_timestamp_in_secs: u64, -) -> Result<(), Error> { - debug!("Verifying data payment for tx_hash: {tx_hash:?}"); - let transaction = get_transaction_receipt_by_hash(network, tx_hash) - .await? - .ok_or(Error::TransactionNotFound)?; - - // If the status is True, it means the tx is confirmed. - if !transaction.status() { - error!("Transaction {tx_hash:?} is not confirmed"); - return Err(Error::TransactionUnconfirmed); - } - - let block_number = transaction - .block_number - .ok_or(Error::TransactionNotInBlock) - .inspect_err(|_| error!("Transaction {tx_hash:?} has not been included in a block yet"))?; - - let block = get_block_by_number(network, block_number) - .await? - .ok_or(Error::BlockNotFound)?; - - // Check if payment was done within the quote expiration timeframe. - if quote_expiration_timestamp_in_secs < block.header.timestamp { - error!("Payment for tx_hash: {tx_hash:?} was done after the quote expired"); - return Err(Error::QuoteExpired); - } - - let logs = - get_data_payment_event(network, block_number, quote_hash, reward_addr, amount).await?; - - for log in logs { - if log.transaction_hash != Some(tx_hash) { - // Wrong transaction. - continue; - } - - if let Ok(event) = ChunkPaymentEvent::try_from(log) { - // Check if the event matches what we expect. - if event.quote_hash == quote_hash - && event.rewards_address == reward_addr - && event.amount >= amount - { - return Ok(()); - } - } - } - - error!("No event proof found for tx_hash: {tx_hash:?}"); - - Err(Error::EventProofNotFound) -} - -#[cfg(test)] -mod tests { - use crate::common::{Address, U256}; - use crate::transaction::{ - get_data_payment_event, get_transaction_receipt_by_hash, verify_data_payment, - }; - use crate::Network; - use alloy::hex::FromHex; - use alloy::primitives::b256; - - #[tokio::test] - async fn test_get_transaction_receipt_by_hash() { - let network = Network::ArbitrumOne; - - let tx_hash = b256!("3304465f38fa0bd9670a426108dd1ddd193e059dcb7c13982d31424646217a36"); // DevSkim: ignore DS173237 - - assert!(get_transaction_receipt_by_hash(&network, tx_hash) - .await - .unwrap() - .is_some()); - } - - #[tokio::test] - async fn test_get_data_payment_event() { - let network = Network::ArbitrumOne; - - let block_number: u64 = 260246302; - let reward_address = Address::from_hex("8AB15A43305854e4AE4E6FBEa0CD1CC0AB4ecB2A").unwrap(); // DevSkim: ignore DS173237 - let amount = U256::from(1); - let quote_hash = b256!("EBD943C38C0422901D4CF22E677DD95F2591CA8D6EBFEA8BAF1BFE9FF5506ECE"); // DevSkim: ignore DS173237 - - let logs = - get_data_payment_event(&network, block_number, quote_hash, reward_address, amount) - .await - .unwrap(); - - assert_eq!(logs.len(), 1); - } - - #[tokio::test] - async fn test_verify_data_payment() { - let network = Network::ArbitrumOne; - - let tx_hash = b256!("3304465f38fa0bd9670a426108dd1ddd193e059dcb7c13982d31424646217a36"); // DevSkim: ignore DS173237 - let quote_hash = b256!("EBD943C38C0422901D4CF22E677DD95F2591CA8D6EBFEA8BAF1BFE9FF5506ECE"); // DevSkim: ignore DS173237 - let reward_address = Address::from_hex("8AB15A43305854e4AE4E6FBEa0CD1CC0AB4ecB2A").unwrap(); // DevSkim: ignore DS173237 - let amount = U256::from(1); - - let result = verify_data_payment( - &network, - tx_hash, - quote_hash, - reward_address, - amount, - 4102441200, - ) - .await; - - assert!(result.is_ok(), "Error: {:?}", result.err()); - } -} diff --git a/evmlib/src/utils.rs b/evmlib/src/utils.rs index f212b466d5..4e3133713f 100644 --- a/evmlib/src/utils.rs +++ b/evmlib/src/utils.rs @@ -165,7 +165,7 @@ fn local_evm_network_from_csv() -> Result { } #[allow(clippy::type_complexity)] -pub(crate) fn http_provider( +pub fn http_provider( rpc_url: reqwest::Url, ) -> FillProvider< JoinFill< diff --git a/evmlib/src/wallet.rs b/evmlib/src/wallet.rs index 643d14bdf9..0f6ba3acea 100644 --- a/evmlib/src/wallet.rs +++ b/evmlib/src/wallet.rs @@ -7,9 +7,10 @@ // permissions and limitations relating to use of the SAFE Network Software. use crate::common::{Address, Amount, QuoteHash, QuotePayment, TxHash, U256}; -use crate::contract::data_payments::{DataPaymentsHandler, MAX_TRANSFERS_PER_TRANSACTION}; use crate::contract::network_token::NetworkToken; -use crate::contract::{data_payments, network_token}; +use crate::contract::payment_vault::handler::PaymentVaultHandler; +use crate::contract::payment_vault::MAX_TRANSFERS_PER_TRANSACTION; +use crate::contract::{network_token, payment_vault}; use crate::utils::http_provider; use crate::Network; use alloy::hex::ToHexExt; @@ -36,7 +37,7 @@ pub enum Error { #[error("Network token contract error: {0}")] NetworkTokenContract(#[from] network_token::Error), #[error("Chunk payments contract error: {0}")] - ChunkPaymentsContract(#[from] data_payments::error::Error), + ChunkPaymentsContract(#[from] payment_vault::error::Error), } #[derive(Clone)] @@ -119,26 +120,13 @@ impl Wallet { approve_to_spend_tokens(self.wallet.clone(), &self.network, spender, amount).await } - /// Pays for a single quote. Returns transaction hash of the payment. - pub async fn pay_for_quote( - &self, - quote_hash: QuoteHash, - rewards_addr: Address, - amount: U256, - ) -> Result { - self.pay_for_quotes([(quote_hash, rewards_addr, amount)]) - .await - .map(|v| v.values().last().cloned().expect("Infallible")) - .map_err(|err| err.0) - } - /// Function for batch payments of quotes. It accepts an iterator of QuotePayment and returns /// transaction hashes of the payments by quotes. pub async fn pay_for_quotes>( &self, - data_payments: I, + quote_payments: I, ) -> Result, PayForQuotesError> { - pay_for_quotes(self.wallet.clone(), &self.network, data_payments).await + pay_for_quotes(self.wallet.clone(), &self.network, quote_payments).await } /// Build a provider using this wallet. @@ -331,15 +319,22 @@ pub async fn pay_for_quotes>( } let provider = http_provider_with_wallet(network.rpc_url().clone(), wallet); - let data_payments = DataPaymentsHandler::new(*network.data_payments_address(), provider); + let data_payments = PaymentVaultHandler::new(*network.data_payments_address(), provider); + + // remove payments with 0 amount as they don't need to be paid for + let payment_for_batch: Vec = payments + .into_iter() + .filter(|(_, _, amount)| *amount > Amount::ZERO) + .collect(); // Divide transfers over multiple transactions if they exceed the max per transaction. - let chunks = payments.chunks(MAX_TRANSFERS_PER_TRANSACTION); + let chunks = payment_for_batch.chunks(MAX_TRANSFERS_PER_TRANSACTION); let mut tx_hashes_by_quote = BTreeMap::new(); for batch in chunks { let batch: Vec = batch.to_vec(); + debug!( "Paying for batch of quotes of len: {}, {batch:?}", batch.len() @@ -349,6 +344,7 @@ pub async fn pay_for_quotes>( .pay_for_quotes(batch.clone()) .await .map_err(|err| PayForQuotesError(Error::from(err), tx_hashes_by_quote.clone()))?; + info!("Paid for batch of quotes with final tx hash: {tx_hash}"); for (quote_hash, _, _) in batch { diff --git a/evmlib/tests/common/quote.rs b/evmlib/tests/common/quote.rs index 21d05cf189..28f8cbd3a8 100644 --- a/evmlib/tests/common/quote.rs +++ b/evmlib/tests/common/quote.rs @@ -5,6 +5,6 @@ use evmlib::utils::{dummy_address, dummy_hash}; pub fn random_quote_payment() -> QuotePayment { let quote_hash = dummy_hash(); let reward_address = dummy_address(); - let amount = Amount::from(200); + let amount = Amount::from(1); (quote_hash, reward_address, amount) } diff --git a/evmlib/tests/data_payments.rs b/evmlib/tests/payment_vault.rs similarity index 61% rename from evmlib/tests/data_payments.rs rename to evmlib/tests/payment_vault.rs index 26223cfcc1..41c5881cbb 100644 --- a/evmlib/tests/data_payments.rs +++ b/evmlib/tests/payment_vault.rs @@ -11,11 +11,15 @@ use alloy::providers::fillers::{ use alloy::providers::{Identity, ProviderBuilder, ReqwestProvider, WalletProvider}; use alloy::signers::local::{LocalSigner, PrivateKeySigner}; use alloy::transports::http::{Client, Http}; -use evmlib::common::U256; -use evmlib::contract::data_payments::{DataPaymentsHandler, MAX_TRANSFERS_PER_TRANSACTION}; +use evmlib::common::{Amount, U256}; use evmlib::contract::network_token::NetworkToken; +use evmlib::contract::payment_vault::handler::PaymentVaultHandler; +use evmlib::contract::payment_vault::{interface, MAX_TRANSFERS_PER_TRANSACTION}; +use evmlib::quoting_metrics::QuotingMetrics; use evmlib::testnet::{deploy_data_payments_contract, deploy_network_token_contract, start_node}; +use evmlib::utils::http_provider; use evmlib::wallet::wallet_address; +use evmlib::Network; async fn setup() -> ( AnvilInstance, @@ -38,7 +42,7 @@ async fn setup() -> ( >, Ethereum, >, - DataPaymentsHandler< + PaymentVaultHandler< Http, FillProvider< JoinFill< @@ -111,9 +115,66 @@ async fn test_deploy() { setup().await; } +#[tokio::test] +async fn test_proxy_reachable() { + let network = Network::ArbitrumOne; + let provider = http_provider(network.rpc_url().clone()); + let payment_vault = PaymentVaultHandler::new(*network.data_payments_address(), provider); + + let amount = payment_vault + .get_quote(vec![QuotingMetrics::default()]) + .await + .unwrap(); + + assert_eq!(amount, vec![Amount::from(1)]); +} + +#[tokio::test] +async fn test_verify_payment() { + let (_anvil, network_token, mut payment_vault) = setup().await; + + let mut quote_payments = vec![]; + + for _ in 0..5 { + let quote_payment = random_quote_payment(); + quote_payments.push(quote_payment); + } + + let _ = network_token + .approve(*payment_vault.contract.address(), U256::MAX) + .await + .unwrap(); + + // Contract provider has a different account coupled to it, + // so we set it to the same as the network token contract + payment_vault.set_provider(network_token.contract.provider().clone()); + + let result = payment_vault.pay_for_quotes(quote_payments.clone()).await; + + assert!(result.is_ok(), "Failed with error: {:?}", result.err()); + + let payment_verifications: Vec<_> = quote_payments + .into_iter() + .map(|v| interface::IPaymentVault::PaymentVerification { + metrics: QuotingMetrics::default().into(), + rewardsAddress: v.1, + quoteHash: v.0, + }) + .collect(); + + let results = payment_vault + .verify_payment(payment_verifications) + .await + .expect("Verify payment failed"); + + for result in results { + assert!(result.isValid); + } +} + #[tokio::test] async fn test_pay_for_quotes() { - let (_anvil, network_token, mut data_payments) = setup().await; + let (_anvil, network_token, mut payment_vault) = setup().await; let mut quote_payments = vec![]; @@ -123,15 +184,15 @@ async fn test_pay_for_quotes() { } let _ = network_token - .approve(*data_payments.contract.address(), U256::MAX) + .approve(*payment_vault.contract.address(), U256::MAX) .await .unwrap(); // Contract provider has a different account coupled to it, // so we set it to the same as the network token contract - data_payments.set_provider(network_token.contract.provider().clone()); + payment_vault.set_provider(network_token.contract.provider().clone()); - let result = data_payments.pay_for_quotes(quote_payments).await; + let result = payment_vault.pay_for_quotes(quote_payments).await; assert!(result.is_ok(), "Failed with error: {:?}", result.err()); } diff --git a/evmlib/tests/wallet.rs b/evmlib/tests/wallet.rs index 905f719fc3..e9e5f0a077 100644 --- a/evmlib/tests/wallet.rs +++ b/evmlib/tests/wallet.rs @@ -8,9 +8,9 @@ use alloy::providers::ext::AnvilApi; use alloy::providers::{ProviderBuilder, WalletProvider}; use alloy::signers::local::{LocalSigner, PrivateKeySigner}; use evmlib::common::{Amount, TxHash}; -use evmlib::contract::data_payments::MAX_TRANSFERS_PER_TRANSACTION; +use evmlib::contract::payment_vault::{verify_data_payment, MAX_TRANSFERS_PER_TRANSACTION}; +use evmlib::quoting_metrics::QuotingMetrics; use evmlib::testnet::{deploy_data_payments_contract, deploy_network_token_contract, start_node}; -use evmlib::transaction::verify_data_payment; use evmlib::wallet::{transfer_tokens, wallet_address, Wallet}; use evmlib::{CustomNetwork, Network}; use std::collections::HashSet; @@ -67,7 +67,6 @@ async fn funded_wallet(network: &Network, genesis_wallet: EthereumWallet) -> Wal #[tokio::test] async fn test_pay_for_quotes_and_data_payment_verification() { const TRANSFERS: usize = 600; - const EXPIRATION_TIMESTAMP_IN_SECS: u64 = 4102441200; // The year 2100 let (_anvil, network, genesis_wallet) = local_testnet().await; let wallet = funded_wallet(&network, genesis_wallet).await; @@ -87,23 +86,17 @@ async fn test_pay_for_quotes_and_data_payment_verification() { unique_tx_hashes.len(), TRANSFERS.div_ceil(MAX_TRANSFERS_PER_TRANSACTION) ); - - for quote_payment in quote_payments.iter() { - let tx_hash = *tx_hashes.get("e_payment.0).unwrap(); - + for (quote_hash, reward_addr, _) in quote_payments.iter() { let result = verify_data_payment( &network, - tx_hash, - quote_payment.0, - quote_payment.1, - quote_payment.2, - EXPIRATION_TIMESTAMP_IN_SECS, + vec![*quote_hash], + vec![(*quote_hash, QuotingMetrics::default(), *reward_addr)], ) .await; assert!( result.is_ok(), - "Verification failed for: {quote_payment:?}. Error: {:?}", + "Verification failed for: {quote_hash:?}. Error: {:?}", result.err() ); }