Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: transactions in client API #2518

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ant-networking/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ pub enum NetworkError {

#[error("Register already exists at this address")]
RegisterAlreadyExists,

#[error("Transaction already exists at this address")]
TransactionAlreadyExists,
}

#[cfg(test)]
Expand Down
57 changes: 34 additions & 23 deletions ant-protocol/src/storage/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// permissions and limitations relating to use of the SAFE Network Software.

use super::address::TransactionAddress;
use bls::SecretKey;
use serde::{Deserialize, Serialize};

// re-exports
Expand All @@ -27,13 +28,15 @@ pub struct Transaction {
}

impl Transaction {
/// Create a new transaction, signing it with the provided secret key.
pub fn new(
owner: PublicKey,
parents: Vec<PublicKey>,
content: TransactionContent,
outputs: Vec<(PublicKey, TransactionContent)>,
signature: Signature,
signing_key: &SecretKey,
) -> Self {
let signature = signing_key.sign(bytes_for_signature(&owner, &parents, &content, &outputs));
Self {
owner,
parents,
Expand All @@ -47,33 +50,41 @@ impl Transaction {
TransactionAddress::from_owner(self.owner)
}

/// Get the bytes that the signature is calculated from.
pub fn bytes_for_signature(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.owner.to_bytes());
bytes.extend_from_slice("parent".as_bytes());
bytes.extend_from_slice(
&self
.parents
.iter()
.map(|p| p.to_bytes())
.collect::<Vec<_>>()
.concat(),
);
bytes.extend_from_slice("content".as_bytes());
bytes.extend_from_slice(&self.content);
bytes.extend_from_slice("outputs".as_bytes());
bytes.extend_from_slice(
&self
.outputs
.iter()
.flat_map(|(p, c)| [&p.to_bytes(), c.as_slice()].concat())
.collect::<Vec<_>>(),
);
bytes
bytes_for_signature(&self.owner, &self.parents, &self.content, &self.outputs)
}

pub fn verify(&self) -> bool {
self.owner
.verify(&self.signature, self.bytes_for_signature())
}
}

fn bytes_for_signature(
owner: &PublicKey,
parents: &[PublicKey],
content: &[u8],
outputs: &[(PublicKey, TransactionContent)],
) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&owner.to_bytes());
bytes.extend_from_slice("parent".as_bytes());
bytes.extend_from_slice(
&parents
.iter()
.map(|p| p.to_bytes())
.collect::<Vec<_>>()
.concat(),
);
bytes.extend_from_slice("content".as_bytes());
bytes.extend_from_slice(content);
bytes.extend_from_slice("outputs".as_bytes());
bytes.extend_from_slice(
&outputs
.iter()
.flat_map(|(p, c)| [&p.to_bytes(), c.as_slice()].concat())
.collect::<Vec<_>>(),
);
bytes
}
3 changes: 2 additions & 1 deletion autonomi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ default = ["vault"]
external-signer = ["ant-evm/external-signer"]
extension-module = ["pyo3/extension-module"]
fs = ["tokio/fs"]
full = ["registers", "vault", "fs"]
full = ["registers", "vault", "fs", "transactions"]
local = ["ant-networking/local", "ant-evm/local"]
loud = []
registers = []
transactions = []
vault = ["registers"]

[dependencies]
Expand Down
3 changes: 3 additions & 0 deletions autonomi/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub mod files;
#[cfg(feature = "registers")]
#[cfg_attr(docsrs, doc(cfg(feature = "registers")))]
pub mod registers;
#[cfg(feature = "transactions")]
#[cfg_attr(docsrs, doc(cfg(feature = "transactions")))]
pub mod transactions;
#[cfg(feature = "vault")]
#[cfg_attr(docsrs, doc(cfg(feature = "vault")))]
pub mod vault;
Expand Down
155 changes: 155 additions & 0 deletions autonomi/src/client/transactions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// 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::client::data::PayError;
use crate::client::Client;
use crate::client::ClientEvent;
use crate::client::UploadSummary;

pub use ant_protocol::storage::Transaction;
use ant_protocol::storage::TransactionAddress;
pub use bls::SecretKey as TransactionSecretKey;

use ant_evm::{EvmWallet, EvmWalletError};
use ant_networking::{GetRecordCfg, NetworkError, PutRecordCfg, VerificationKind};
use ant_protocol::{
storage::{try_serialize_record, RecordKind, RetryStrategy},
NetworkAddress,
};
use libp2p::kad::{Quorum, Record};

use super::data::CostError;

#[derive(Debug, thiserror::Error)]
pub enum TransactionError {
#[error("Cost error: {0}")]
Cost(#[from] CostError),
#[error("Network error")]
Network(#[from] NetworkError),
#[error("Serialization error")]
Serialization,
#[error("Transaction could not be verified (corrupt)")]
FailedVerification,
#[error("Payment failure occurred during transaction creation.")]
Pay(#[from] PayError),
#[error("Failed to retrieve wallet payment")]
Wallet(#[from] EvmWalletError),
#[error("Received invalid quote from node, this node is possibly malfunctioning, try another node by trying another transaction name")]
InvalidQuote,
}

impl Client {
/// Fetches a Transaction from the network.
pub async fn transaction_get(
&self,
address: TransactionAddress,
) -> Result<Vec<Transaction>, TransactionError> {
let transactions = self.network.get_transactions(address).await?;

Ok(transactions)
}

pub async fn transaction_put(
&self,
transaction: Transaction,
wallet: &EvmWallet,
) -> Result<(), TransactionError> {
let address = transaction.address();

let xor_name = address.xorname();
debug!("Paying for transaction at address: {address:?}");
let (payment_proofs, _skipped) = self
.pay(std::iter::once(*xor_name), wallet)
.await
.inspect_err(|err| {
error!("Failed to pay for transaction at address: {address:?} : {err}")
})?;
let proof = if let Some(proof) = payment_proofs.get(xor_name) {
proof
} else {
// transaction was skipped, meaning it was already paid for
error!("Transaction at address: {address:?} was already paid for");
return Err(TransactionError::Network(
NetworkError::TransactionAlreadyExists,
));
};
let payee = proof
.to_peer_id_payee()
.ok_or(TransactionError::InvalidQuote)
.inspect_err(|err| error!("Failed to get payee from payment proof: {err}"))?;

let record = Record {
key: NetworkAddress::from_transaction_address(address).to_record_key(),
value: try_serialize_record(&(proof, &transaction), RecordKind::TransactionWithPayment)
.map_err(|_| TransactionError::Serialization)?
.to_vec(),
publisher: None,
expires: None,
};

let get_cfg = GetRecordCfg {
get_quorum: Quorum::Majority,
retry_strategy: Some(RetryStrategy::default()),
target_record: None,
expected_holders: Default::default(),
is_register: false,
};
let put_cfg = PutRecordCfg {
put_quorum: Quorum::All,
retry_strategy: None,
use_put_record_to: Some(vec![payee]),
verification: Some((VerificationKind::Network, get_cfg)),
};

debug!("Storing transaction at address {address:?} to the network");
self.network
.put_record(record, &put_cfg)
.await
.inspect_err(|err| {
error!("Failed to put record - transaction {address:?} to the network: {err}")
})?;

if let Some(channel) = self.client_event_sender.as_ref() {
let summary = UploadSummary {
record_count: 1,
tokens_spent: proof.quote.cost.as_atto(),
};
if let Err(err) = channel.send(ClientEvent::UploadComplete(summary)).await {
error!("Failed to send client event: {err}");
}
}

Ok(())
}

// /// Get the cost to create a transaction
// pub async fn transaction_cost(
// &self,
// name: String,
// owner: TransactionSecretKey,
// ) -> Result<AttoTokens, TransactionError> {
// info!("Getting cost for transaction with name: {name}");
// // get transaction address
// let pk = owner.public_key();
// let name = XorName::from_content_parts(&[name.as_bytes()]);
// let transaction = Transaction::new(None, name, owner, permissions)?;
// let reg_xor = transaction.address().xorname();

// // get cost to store transaction
// // NB TODO: transaction should be priced differently from other data

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
// let cost_map = self.get_store_quotes(std::iter::once(reg_xor)).await?;
// let total_cost = AttoTokens::from_atto(
// cost_map
// .values()
// .map(|quote| quote.2.cost.as_atto())
// .sum::<Amount>(),
// );
// debug!("Calculated the cost to create transaction with name: {name} is {total_cost}");
// Ok(total_cost)
// }
}
31 changes: 31 additions & 0 deletions autonomi/tests/transaction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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.

#![cfg(feature = "transactions")]

use ant_logging::LogBuilder;
use ant_protocol::storage::Transaction;
use autonomi::Client;
use eyre::Result;
use test_utils::{evm::get_funded_wallet, peers_from_env};

#[tokio::test]
async fn transaction() -> Result<()> {
let _log_appender_guard = LogBuilder::init_single_threaded_tokio_test("transaction", false);

let client = Client::connect(&peers_from_env()?).await?;
let wallet = get_funded_wallet();

let key = bls::SecretKey::random();
let content = [0u8; 32];
let mut transaction = Transaction::new(key.public_key(), vec![], content, vec![], &key);

client.transaction_put(transaction, &wallet).await?;

Ok(())
}
Loading