Files
serai/coins/monero/rpc/src/lib.rs

864 lines
28 KiB
Rust
Raw Normal View History

2024-06-16 18:40:15 -04:00
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
2024-06-16 19:59:25 -04:00
#![deny(missing_docs)]
2024-06-16 18:40:15 -04:00
#![cfg_attr(not(feature = "std"), no_std)]
use core::fmt::Debug;
#[cfg(not(feature = "std"))]
use alloc::boxed::Box;
use std_shims::{
vec::Vec,
io,
string::{String, ToString},
};
2024-06-16 18:40:15 -04:00
use zeroize::Zeroize;
use async_trait::async_trait;
use curve25519_dalek::edwards::EdwardsPoint;
use serde::{Serialize, Deserialize, de::DeserializeOwned};
2022-11-14 21:49:49 -05:00
use serde_json::{Value, json};
2024-06-16 18:40:15 -04:00
use monero_serai::{
io::*,
2022-07-15 01:26:07 -04:00
transaction::{Input, Timelock, Transaction},
block::Block,
};
// Number of blocks the fee estimate will be valid for
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
// src/wallet/wallet2.cpp#L121
const GRACE_BLOCKS_FOR_FEE_ESTIMATE: u64 = 10;
2024-06-16 19:59:25 -04:00
/// A struct containing a fee rate.
///
/// The fee rate is defined as a per-weight cost, along with a mask for rounding purposes.
2024-06-16 18:40:15 -04:00
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
2024-06-16 19:59:25 -04:00
pub struct FeeRate {
/// The fee per-weight of the transaction.
2024-06-16 18:40:15 -04:00
pub per_weight: u64,
2024-06-16 19:59:25 -04:00
/// The mask to round with.
2024-06-16 18:40:15 -04:00
pub mask: u64,
}
2024-06-16 19:59:25 -04:00
impl FeeRate {
/// Construct a new fee rate.
pub fn new(per_weight: u64, mask: u64) -> Result<FeeRate, RpcError> {
if (per_weight == 0) || (mask == 0) {
Err(RpcError::InvalidFee)?;
}
Ok(FeeRate { per_weight, mask })
}
/// Calculate the fee to use from the weight.
///
/// This function may panic if any of the `FeeRate`'s fields are zero.
2024-06-16 18:40:15 -04:00
pub fn calculate_fee_from_weight(&self, weight: usize) -> u64 {
2024-06-16 19:59:25 -04:00
let fee = self.per_weight * u64::try_from(weight).unwrap();
let fee = ((fee + self.mask - 1) / self.mask) * self.mask;
2024-06-16 18:40:15 -04:00
debug_assert_eq!(weight, self.calculate_weight_from_fee(fee), "Miscalculated weight from fee");
fee
}
2024-06-16 19:59:25 -04:00
/// Calculate the weight from the fee.
///
/// This function may panic if any of the `FeeRate`'s fields are zero.
2024-06-16 18:40:15 -04:00
pub fn calculate_weight_from_fee(&self, fee: u64) -> usize {
usize::try_from(fee / self.per_weight).unwrap()
}
}
2024-06-16 19:59:25 -04:00
/// The priority for the fee.
///
/// Higher-priority transactions will be included in blocks earlier.
2024-06-16 18:40:15 -04:00
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[allow(non_camel_case_types)]
pub enum FeePriority {
2024-06-16 19:59:25 -04:00
/// The `Unimportant` priority, as defined by Monero.
2024-06-16 18:40:15 -04:00
Unimportant,
2024-06-16 19:59:25 -04:00
/// The `Normal` priority, as defined by Monero.
2024-06-16 18:40:15 -04:00
Normal,
2024-06-16 19:59:25 -04:00
/// The `Elevated` priority, as defined by Monero.
2024-06-16 18:40:15 -04:00
Elevated,
2024-06-16 19:59:25 -04:00
/// The `Priority` priority, as defined by Monero.
2024-06-16 18:40:15 -04:00
Priority,
2024-06-16 19:59:25 -04:00
/// A custom priority.
Custom {
/// The numeric representation of the priority, as used within the RPC.
priority: u32,
},
2024-06-16 18:40:15 -04:00
}
/// https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/
/// src/simplewallet/simplewallet.cpp#L161
impl FeePriority {
pub(crate) fn fee_priority(&self) -> u32 {
match self {
FeePriority::Unimportant => 1,
FeePriority::Normal => 2,
FeePriority::Elevated => 3,
FeePriority::Priority => 4,
FeePriority::Custom { priority, .. } => *priority,
}
}
}
#[derive(Deserialize, Debug)]
2024-06-16 19:59:25 -04:00
struct EmptyResponse {}
#[derive(Deserialize, Debug)]
2024-06-16 19:59:25 -04:00
struct JsonRpcResponse<T> {
2022-07-15 01:26:07 -04:00
result: T,
}
#[derive(Deserialize, Debug)]
struct TransactionResponse {
tx_hash: String,
as_hex: String,
pruned_as_hex: String,
}
#[derive(Deserialize, Debug)]
struct TransactionsResponse {
#[serde(default)]
missed_tx: Vec<String>,
txs: Vec<TransactionResponse>,
}
2024-06-16 19:59:25 -04:00
/// The response to an output query.
#[derive(Deserialize, Debug)]
pub struct OutputResponse {
2024-06-16 19:59:25 -04:00
/// The height of the block this output was added to the chain in.
pub height: usize,
2024-06-16 19:59:25 -04:00
/// If the output is unlocked, per the node's local view.
pub unlocked: bool,
2024-06-16 19:59:25 -04:00
/// The output's key.
pub key: String,
/// The output's commitment.
pub mask: String,
/// The transaction which created this output.
pub txid: String,
}
2024-06-16 19:59:25 -04:00
/// An error from the RPC.
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum RpcError {
2024-06-16 19:59:25 -04:00
/// An internal error.
#[cfg_attr(feature = "std", error("internal error ({0})"))]
2022-12-09 09:50:00 -05:00
InternalError(&'static str),
2024-06-16 19:59:25 -04:00
/// A connection error with the node.
#[cfg_attr(feature = "std", error("connection error ({0})"))]
ConnectionError(String),
2024-06-16 19:59:25 -04:00
/// The node is invalid per the expected protocol.
#[cfg_attr(feature = "std", error("invalid node ({0})"))]
InvalidNode(String),
2024-06-16 19:59:25 -04:00
/// Requested transactions weren't found.
#[cfg_attr(feature = "std", error("transactions not found"))]
TransactionsNotFound(Vec<[u8; 32]>),
2024-06-16 19:59:25 -04:00
/// The transaction was pruned.
///
/// Pruned transactions are not supported at this time.
#[cfg_attr(feature = "std", error("pruned transaction"))]
2022-05-21 23:16:06 -04:00
PrunedTransaction,
2024-06-16 19:59:25 -04:00
/// A transaction (sent or received) was invalid.
#[cfg_attr(feature = "std", error("invalid transaction ({0:?})"))]
2022-07-15 01:26:07 -04:00
InvalidTransaction([u8; 32]),
2024-06-16 19:59:25 -04:00
/// The returned fee was unusable.
#[cfg_attr(feature = "std", error("unexpected fee response"))]
InvalidFee,
2024-06-16 19:59:25 -04:00
/// The priority intended for use wasn't usable.
#[cfg_attr(feature = "std", error("invalid priority"))]
InvalidPriority,
}
fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
hex::decode(value).map_err(|_| RpcError::InvalidNode("expected hex wasn't hex".to_string()))
}
fn hash_hex(hash: &str) -> Result<[u8; 32], RpcError> {
rpc_hex(hash)?.try_into().map_err(|_| RpcError::InvalidNode("hash wasn't 32-bytes".to_string()))
}
fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
decompress_point(
2024-06-16 19:59:25 -04:00
rpc_hex(point)?
.try_into()
.map_err(|_| RpcError::InvalidNode(format!("invalid point: {point}")))?,
2022-07-15 01:26:07 -04:00
)
2024-06-16 19:59:25 -04:00
.ok_or_else(|| RpcError::InvalidNode(format!("invalid point: {point}")))
}
// Read an EPEE VarInt, distinct from the VarInts used throughout the rest of the protocol
fn read_epee_vi<R: io::Read>(reader: &mut R) -> io::Result<u64> {
let vi_start = read_byte(reader)?;
let len = match vi_start & 0b11 {
0 => 1,
1 => 2,
2 => 4,
3 => 8,
_ => unreachable!(),
};
let mut vi = u64::from(vi_start >> 2);
for i in 1 .. len {
vi |= u64::from(read_byte(reader)?) << (((i - 1) * 8) + 6);
}
Ok(vi)
}
2024-06-16 19:59:25 -04:00
/// An RPC connection to a Monero daemon.
///
/// This is abstract such that users can use an HTTP library (which being their choice), a
/// Tor/i2p-based transport, or even a memory buffer an external service somehow routes.
2024-06-19 08:36:26 -04:00
///
/// While no implementors are directly provided, [monero-simple-request-rpc](
/// https://github.com/serai-dex/serai/tree/develop/coins/monero/rpc/simple-request
/// ) is recommended.
#[async_trait]
2024-06-16 19:59:25 -04:00
pub trait Rpc: Sync + Clone + Debug {
/// Perform a POST request to the specified route with the specified body.
///
/// The implementor is left to handle anything such as authentication.
async fn post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError>;
2024-06-16 18:40:15 -04:00
/// Perform a RPC call to the specified route with the provided parameters.
///
/// This is NOT a JSON-RPC call. They use a route of "json_rpc" and are available via
2022-11-14 21:49:49 -05:00
/// `json_rpc_call`.
2024-06-16 19:59:25 -04:00
async fn rpc_call<Params: Send + Serialize + Debug, Response: DeserializeOwned + Debug>(
2022-07-15 01:26:07 -04:00
&self,
route: &str,
2022-07-15 01:26:07 -04:00
params: Option<Params>,
) -> Result<Response, RpcError> {
let res = self
.post(
route,
if let Some(params) = params {
serde_json::to_string(&params).unwrap().into_bytes()
} else {
vec![]
},
)
.await?;
let res_str = std_shims::str::from_utf8(&res)
.map_err(|_| RpcError::InvalidNode("response wasn't utf-8".to_string()))?;
serde_json::from_str(res_str)
.map_err(|_| RpcError::InvalidNode(format!("response wasn't json: {res_str}")))
}
2024-06-16 19:59:25 -04:00
/// Perform a JSON-RPC call with the specified method with the provided parameters.
async fn json_rpc_call<Response: DeserializeOwned + Debug>(
2022-11-14 21:49:49 -05:00
&self,
method: &str,
params: Option<Value>,
) -> Result<Response, RpcError> {
let mut req = json!({ "method": method });
if let Some(params) = params {
req.as_object_mut().unwrap().insert("params".into(), params);
}
Ok(self.rpc_call::<_, JsonRpcResponse<Response>>("json_rpc", Some(req)).await?.result)
}
/// Perform a binary call to the specified route with the provided parameters.
2024-06-16 19:59:25 -04:00
async fn bin_call(&self, route: &str, params: Vec<u8>) -> Result<Vec<u8>, RpcError> {
self.post(route, params).await
}
/// Get the active blockchain protocol version.
2024-06-21 14:14:15 -04:00
///
/// This is specifically the major version within the most recent block header.
async fn get_protocol(&self) -> Result<u8, RpcError> {
#[derive(Deserialize, Debug)]
struct ProtocolResponse {
2024-06-22 14:37:43 -04:00
hardfork_version: u8,
}
#[derive(Deserialize, Debug)]
struct LastHeaderResponse {
block_header: ProtocolResponse,
}
Ok(
2024-06-21 14:14:15 -04:00
self
2022-11-14 21:49:49 -05:00
.json_rpc_call::<LastHeaderResponse>("get_last_block_header", None)
.await?
.block_header
2024-06-22 14:37:43 -04:00
.hardfork_version,
)
}
2024-06-16 19:59:25 -04:00
/// Get the height of the Monero blockchain.
///
/// The height is defined as the amount of blocks on the blockchain. For a blockchain with only
/// its genesis block, the height will be 1.
async fn get_height(&self) -> Result<usize, RpcError> {
#[derive(Deserialize, Debug)]
struct HeightResponse {
2022-07-15 01:26:07 -04:00
height: usize,
}
Ok(self.rpc_call::<Option<()>, HeightResponse>("get_height", None).await?.height)
}
2024-06-16 19:59:25 -04:00
/// Get the specified transactions.
///
/// The received transactions will be hashed in order to verify the correct transactions were
/// returned.
async fn get_transactions(&self, hashes: &[[u8; 32]]) -> Result<Vec<Transaction>, RpcError> {
if hashes.is_empty() {
return Ok(vec![]);
}
let mut hashes_hex = hashes.iter().map(hex::encode).collect::<Vec<_>>();
let mut all_txs = Vec::with_capacity(hashes.len());
while !hashes_hex.is_empty() {
// Monero errors if more than 100 is requested unless using a non-restricted RPC
const TXS_PER_REQUEST: usize = 100;
let this_count = TXS_PER_REQUEST.min(hashes_hex.len());
let txs: TransactionsResponse = self
.rpc_call(
"get_transactions",
Some(json!({
"txs_hashes": hashes_hex.drain(.. this_count).collect::<Vec<_>>(),
})),
)
.await?;
if !txs.missed_tx.is_empty() {
Err(RpcError::TransactionsNotFound(
txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::<Result<_, _>>()?,
))?;
}
all_txs.extend(txs.txs);
}
all_txs
.iter()
.enumerate()
.map(|(i, res)| {
let tx = Transaction::read::<&[u8]>(
&mut rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })?
.as_ref(),
)
.map_err(|_| match hash_hex(&res.tx_hash) {
Ok(hash) => RpcError::InvalidTransaction(hash),
Err(err) => err,
})?;
// https://github.com/monero-project/monero/issues/8311
if res.as_hex.is_empty() {
2024-06-23 10:08:51 -04:00
match tx.prefix().inputs.first() {
Some(Input::Gen { .. }) => (),
_ => Err(RpcError::PrunedTransaction)?,
}
}
// This does run a few keccak256 hashes, which is pointless if the node is trusted
// In exchange, this provides resilience against invalid/malicious nodes
if tx.hash() != hashes[i] {
Err(RpcError::InvalidNode(
"replied with transaction wasn't the requested transaction".to_string(),
))?;
}
Ok(tx)
})
.collect()
}
2024-06-16 19:59:25 -04:00
/// Get the specified transaction.
///
/// The received transaction will be hashed in order to verify the correct transaction was
/// returned.
async fn get_transaction(&self, tx: [u8; 32]) -> Result<Transaction, RpcError> {
self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0))
}
2024-06-16 19:59:25 -04:00
/// Get the hash of a block from the node.
///
/// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block,
/// `height - 1` for the latest block).
async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
#[derive(Deserialize, Debug)]
struct BlockHeaderResponse {
hash: String,
}
#[derive(Deserialize, Debug)]
struct BlockHeaderByHeightResponse {
block_header: BlockHeaderResponse,
}
let header: BlockHeaderByHeightResponse =
self.json_rpc_call("get_block_header_by_height", Some(json!({ "height": number }))).await?;
hash_hex(&header.block_header.hash)
}
/// Get a block from the node by its hash.
2024-06-16 19:59:25 -04:00
///
/// The received block will be hashed in order to verify the correct block was returned.
async fn get_block(&self, hash: [u8; 32]) -> Result<Block, RpcError> {
#[derive(Deserialize, Debug)]
struct BlockResponse {
2022-07-15 01:26:07 -04:00
blob: String,
}
let res: BlockResponse =
self.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await?;
let block = Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref())
.map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?;
Monero: support for legacy transactions (#308) * add mlsag * fix last commit * fix miner v1 txs * fix non-miner v1 txs * add borromean + fix mlsag * add block hash calculations * fix for the jokester that added unreduced scalars to the borromean signature of 2368d846e671bf79a1f84c6d3af9f0bfe296f043f50cf17ae5e485384a53707b * Add Borromean range proof verifying functionality * Add MLSAG verifying functionality * fmt & clippy :) * update MLSAG, ss2_elements will always be 2 * Add MgSig proving * Tidy block.rs * Tidy Borromean, fix bugs in last commit, replace todo! with unreachable! * Mark legacy EcdhInfo amount decryption as experimental * Correct comments * Write a new impl of the merkle algorithm This one tries to be understandable. * Only pull in things only needed for experimental when experimental * Stop caching the Monero block hash now in processor that we have Block::hash * Corrections for recent processor commit * Use a clearer algorithm for the merkle Should also be more efficient due to not shifting as often. * Tidy Mlsag * Remove verify_rct_* from Mlsag Both methods were ports from Monero, overtly specific without clear documentation. They need to be added back in, with documentation, or included in a node which provides the necessary further context for them to be naturally understandable. * Move mlsag/mod.rs to mlsag.rs This should only be a folder if it has multiple files. * Replace EcdhInfo terminology The ECDH encrypted the amount, yet this struct contained the encrypted amount, not some ECDH. Also corrects the types on the original EcdhInfo struct. * Correct handling of commitment masks when scanning * Route read_array through read_raw_vec * Misc lint * Make a proper RctType enum No longer caches RctType in the RctSignatures as well. * Replace Vec<Bulletproofs> with Bulletproofs Monero uses aggregated range proofs, so there's only ever one Bulletproof. This is enforced with a consensus rule as well, making this safe. As for why Monero uses a vec, it's probably due to the lack of variadic typing used. Its effectively an Option for them, yet we don't need an Option since we do have variadic typing (enums). * Add necessary checks to Eventuality re: supported protocols * Fix for block 202612 and fix merkel root calculations * MLSAG (de)serialisation fix ss_2_elements will not always be 2 as rct type 1 transactions are not enforced to have one input * Revert "MLSAG (de)serialisation fix" This reverts commit 5e710e0c96658092c6ecfe5e4ea5a9c3dbee3ab3. here it checks number of MGs == number of inputs: https://github.com/monero-project/monero/blob/0a1eaf26f9dd6b762c2582ee12603b2a4671c735/src/cryptonote_core/tx_verification_utils.cpp#L60-59 and here it checks for RctTypeFull number of MGs == 1: https://github.com/monero-project/monero/blob/0a1eaf26f9dd6b762c2582ee12603b2a4671c735/src/ringct/rctSigs.cpp#L1325 so number of inputs == 1 so ss_2_elements == 2 * update `MlsagAggregate` comment * cargo update Resolves a yanked crate * Move location of serai-client in Cargo.toml --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
2023-07-04 21:18:05 +00:00
if block.hash() != hash {
Err(RpcError::InvalidNode("different block than requested (hash)".to_string()))?;
Monero: support for legacy transactions (#308) * add mlsag * fix last commit * fix miner v1 txs * fix non-miner v1 txs * add borromean + fix mlsag * add block hash calculations * fix for the jokester that added unreduced scalars to the borromean signature of 2368d846e671bf79a1f84c6d3af9f0bfe296f043f50cf17ae5e485384a53707b * Add Borromean range proof verifying functionality * Add MLSAG verifying functionality * fmt & clippy :) * update MLSAG, ss2_elements will always be 2 * Add MgSig proving * Tidy block.rs * Tidy Borromean, fix bugs in last commit, replace todo! with unreachable! * Mark legacy EcdhInfo amount decryption as experimental * Correct comments * Write a new impl of the merkle algorithm This one tries to be understandable. * Only pull in things only needed for experimental when experimental * Stop caching the Monero block hash now in processor that we have Block::hash * Corrections for recent processor commit * Use a clearer algorithm for the merkle Should also be more efficient due to not shifting as often. * Tidy Mlsag * Remove verify_rct_* from Mlsag Both methods were ports from Monero, overtly specific without clear documentation. They need to be added back in, with documentation, or included in a node which provides the necessary further context for them to be naturally understandable. * Move mlsag/mod.rs to mlsag.rs This should only be a folder if it has multiple files. * Replace EcdhInfo terminology The ECDH encrypted the amount, yet this struct contained the encrypted amount, not some ECDH. Also corrects the types on the original EcdhInfo struct. * Correct handling of commitment masks when scanning * Route read_array through read_raw_vec * Misc lint * Make a proper RctType enum No longer caches RctType in the RctSignatures as well. * Replace Vec<Bulletproofs> with Bulletproofs Monero uses aggregated range proofs, so there's only ever one Bulletproof. This is enforced with a consensus rule as well, making this safe. As for why Monero uses a vec, it's probably due to the lack of variadic typing used. Its effectively an Option for them, yet we don't need an Option since we do have variadic typing (enums). * Add necessary checks to Eventuality re: supported protocols * Fix for block 202612 and fix merkel root calculations * MLSAG (de)serialisation fix ss_2_elements will not always be 2 as rct type 1 transactions are not enforced to have one input * Revert "MLSAG (de)serialisation fix" This reverts commit 5e710e0c96658092c6ecfe5e4ea5a9c3dbee3ab3. here it checks number of MGs == number of inputs: https://github.com/monero-project/monero/blob/0a1eaf26f9dd6b762c2582ee12603b2a4671c735/src/cryptonote_core/tx_verification_utils.cpp#L60-59 and here it checks for RctTypeFull number of MGs == 1: https://github.com/monero-project/monero/blob/0a1eaf26f9dd6b762c2582ee12603b2a4671c735/src/ringct/rctSigs.cpp#L1325 so number of inputs == 1 so ss_2_elements == 2 * update `MlsagAggregate` comment * cargo update Resolves a yanked crate * Move location of serai-client in Cargo.toml --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
2023-07-04 21:18:05 +00:00
}
Ok(block)
}
2024-06-16 19:59:25 -04:00
/// Get a block from the node by its number.
///
/// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block,
/// `height - 1` for the latest block).
async fn get_block_by_number(&self, number: usize) -> Result<Block, RpcError> {
Monero: add more legacy verify functions (#383) * Add v1 ring sig verifying * allow calculating signature hash for v1 txs * add unreduced scalar type with recovery I have added this type for borromen sigs, the ee field can be a normal scalar as in the verify function the ee field is checked against a reduced scalar mean for it to verify as correct ee must be reduced * change block major/ minor versions to u8 this matches Monero I have also changed a couple varint functions to accept the `VarInt` trait * expose `serialize_hashable` on `Block` * add back MLSAG verifying functions I still need to revert the commit removing support for >1 input MLSAG FULL This adds a new rct type to separate Full and simple rct * add back support for multiple inputs for RCT FULL * comment `non_adjacent_form` function also added `#[allow(clippy::needless_range_loop)]` around a loop as without a re-write satisfying clippy without it will make the function worse. * Improve Mlsag verifying API * fix rebase errors * revert the changes on `reserialize_chain` plus other misc changes * fix no-std * Reduce the amount of rpc calls needed for `get_block_by_number`. This function was causing me problems, every now and then a node would return a block with a different number than requested. * change `serialize_hashable` to give the POW hashing blob. Monero calculates the POW hash and the block hash using *slightly* different blobs :/ * make ring_signatures public and add length check when verifying. * Misc improvements and bug fixes --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
2023-11-12 15:18:18 +00:00
#[derive(Deserialize, Debug)]
struct BlockResponse {
blob: String,
}
let res: BlockResponse =
self.json_rpc_call("get_block", Some(json!({ "height": number }))).await?;
let block = Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref())
.map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?;
Monero: add more legacy verify functions (#383) * Add v1 ring sig verifying * allow calculating signature hash for v1 txs * add unreduced scalar type with recovery I have added this type for borromen sigs, the ee field can be a normal scalar as in the verify function the ee field is checked against a reduced scalar mean for it to verify as correct ee must be reduced * change block major/ minor versions to u8 this matches Monero I have also changed a couple varint functions to accept the `VarInt` trait * expose `serialize_hashable` on `Block` * add back MLSAG verifying functions I still need to revert the commit removing support for >1 input MLSAG FULL This adds a new rct type to separate Full and simple rct * add back support for multiple inputs for RCT FULL * comment `non_adjacent_form` function also added `#[allow(clippy::needless_range_loop)]` around a loop as without a re-write satisfying clippy without it will make the function worse. * Improve Mlsag verifying API * fix rebase errors * revert the changes on `reserialize_chain` plus other misc changes * fix no-std * Reduce the amount of rpc calls needed for `get_block_by_number`. This function was causing me problems, every now and then a node would return a block with a different number than requested. * change `serialize_hashable` to give the POW hashing blob. Monero calculates the POW hash and the block hash using *slightly* different blobs :/ * make ring_signatures public and add length check when verifying. * Misc improvements and bug fixes --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
2023-11-12 15:18:18 +00:00
// Make sure this is actually the block for this number
2024-06-23 10:08:51 -04:00
match block.miner_tx.prefix().inputs.first() {
Monero: add more legacy verify functions (#383) * Add v1 ring sig verifying * allow calculating signature hash for v1 txs * add unreduced scalar type with recovery I have added this type for borromen sigs, the ee field can be a normal scalar as in the verify function the ee field is checked against a reduced scalar mean for it to verify as correct ee must be reduced * change block major/ minor versions to u8 this matches Monero I have also changed a couple varint functions to accept the `VarInt` trait * expose `serialize_hashable` on `Block` * add back MLSAG verifying functions I still need to revert the commit removing support for >1 input MLSAG FULL This adds a new rct type to separate Full and simple rct * add back support for multiple inputs for RCT FULL * comment `non_adjacent_form` function also added `#[allow(clippy::needless_range_loop)]` around a loop as without a re-write satisfying clippy without it will make the function worse. * Improve Mlsag verifying API * fix rebase errors * revert the changes on `reserialize_chain` plus other misc changes * fix no-std * Reduce the amount of rpc calls needed for `get_block_by_number`. This function was causing me problems, every now and then a node would return a block with a different number than requested. * change `serialize_hashable` to give the POW hashing blob. Monero calculates the POW hash and the block hash using *slightly* different blobs :/ * make ring_signatures public and add length check when verifying. * Misc improvements and bug fixes --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
2023-11-12 15:18:18 +00:00
Some(Input::Gen(actual)) => {
if usize::try_from(*actual).unwrap() == number {
Ok(block)
} else {
Err(RpcError::InvalidNode("different block than requested (number)".to_string()))
}
2023-02-15 20:39:22 -05:00
}
_ => Err(RpcError::InvalidNode(
"block's miner_tx didn't have an input of kind Input::Gen".to_string(),
)),
}
}
2024-06-16 19:59:25 -04:00
/// Get the transactions within a block.
///
/// This function returns all transactions in the block, including the miner's transaction.
///
/// This function does not verify the returned transactions are the ones committed to by the
/// block's header.
async fn get_block_transactions(&self, hash: [u8; 32]) -> Result<Vec<Transaction>, RpcError> {
let block = self.get_block(hash).await?;
let mut res = vec![block.miner_tx];
res.extend(self.get_transactions(&block.txs).await?);
Ok(res)
}
2024-06-16 19:59:25 -04:00
/// Get the transactions within a block.
///
/// This function returns all transactions in the block, including the miner's transaction.
///
/// This function does not verify the returned transactions are the ones committed to by the
/// block's header.
async fn get_block_transactions_by_number(
&self,
number: usize,
) -> Result<Vec<Transaction>, RpcError> {
self.get_block_transactions(self.get_block_hash(number).await?).await
}
/// Get the output indexes of the specified transaction.
2024-06-16 19:59:25 -04:00
async fn get_o_indexes(&self, hash: [u8; 32]) -> Result<Vec<u64>, RpcError> {
/*
TODO: Use these when a suitable epee serde lib exists
#[derive(Serialize, Debug)]
struct Request {
2022-07-15 01:26:07 -04:00
txid: [u8; 32],
}
#[derive(Deserialize, Debug)]
struct OIndexes {
o_indexes: Vec<u64>,
}
*/
// Given the immaturity of Rust epee libraries, this is a homegrown one which is only validated
// to work against this specific function
// Header for EPEE, an 8-byte magic and a version
const EPEE_HEADER: &[u8] = b"\x01\x11\x01\x01\x01\x01\x02\x01\x01";
let mut request = EPEE_HEADER.to_vec();
// Number of fields (shifted over 2 bits as the 2 LSBs are reserved for metadata)
request.push(1 << 2);
// Length of field name
request.push(4);
// Field name
request.extend(b"txid");
// Type of field
request.push(10);
// Length of string, since this byte array is technically a string
request.push(32 << 2);
// The "string"
request.extend(hash);
let indexes_buf = self.bin_call("get_o_indexes.bin", request).await?;
let mut indexes: &[u8] = indexes_buf.as_ref();
(|| {
let mut res = None;
let mut is_okay = false;
if read_bytes::<_, { EPEE_HEADER.len() }>(&mut indexes)? != EPEE_HEADER {
Err(io::Error::other("invalid header"))?;
}
let read_object = |reader: &mut &[u8]| -> io::Result<Vec<u64>> {
let fields = read_byte(reader)? >> 2;
for _ in 0 .. fields {
let name_len = read_byte(reader)?;
let name = read_raw_vec(read_byte, name_len.into(), reader)?;
let type_with_array_flag = read_byte(reader)?;
let kind = type_with_array_flag & (!0x80);
let iters = if type_with_array_flag != kind { read_epee_vi(reader)? } else { 1 };
if (&name == b"o_indexes") && (kind != 5) {
Err(io::Error::other("o_indexes weren't u64s"))?;
}
let f = match kind {
// i64
1 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
// i32
2 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader),
// i16
3 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader),
// i8
4 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
// u64
5 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
// u32
6 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader),
// u16
7 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader),
// u8
8 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
// double
9 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
// string, or any collection of bytes
10 => |reader: &mut &[u8]| {
let len = read_epee_vi(reader)?;
read_raw_vec(
read_byte,
len.try_into().map_err(|_| io::Error::other("u64 length exceeded usize"))?,
reader,
)
},
// bool
11 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
// object, errors here as it shouldn't be used on this call
12 => {
|_: &mut &[u8]| Err(io::Error::other("node used object in reply to get_o_indexes"))
}
// array, so far unused
13 => |_: &mut &[u8]| Err(io::Error::other("node used the unused array type")),
_ => |_: &mut &[u8]| Err(io::Error::other("node used an invalid type")),
};
let mut bytes_res = vec![];
for _ in 0 .. iters {
bytes_res.push(f(reader)?);
}
let mut actual_res = Vec::with_capacity(bytes_res.len());
match name.as_slice() {
b"o_indexes" => {
for o_index in bytes_res {
actual_res.push(u64::from_le_bytes(
o_index
.try_into()
.map_err(|_| io::Error::other("node didn't provide 8 bytes for a u64"))?,
));
}
res = Some(actual_res);
}
b"status" => {
if bytes_res
.first()
.ok_or_else(|| io::Error::other("status wasn't a string"))?
.as_slice() !=
b"OK"
{
// TODO: Better handle non-OK responses
Err(io::Error::other("response wasn't OK"))?;
}
is_okay = true;
}
_ => continue,
}
if is_okay && res.is_some() {
break;
}
}
// Didn't return a response with a status
// (if the status wasn't okay, we would've already errored)
if !is_okay {
Err(io::Error::other("response didn't contain a status"))?;
}
// If the Vec was empty, it would've been omitted, hence the unwrap_or
// TODO: Test against a 0-output TX, such as the ones found in block 202612
Ok(res.unwrap_or(vec![]))
};
read_object(&mut indexes)
})()
.map_err(|_| RpcError::InvalidNode("invalid binary response".to_string()))
}
2024-06-16 19:59:25 -04:00
/// Get the output distribution.
///
/// `from` and `to` are heights, not block numbers, and inclusive.
async fn get_output_distribution(&self, from: usize, to: usize) -> Result<Vec<u64>, RpcError> {
#[derive(Deserialize, Debug)]
struct Distribution {
2022-07-15 01:26:07 -04:00
distribution: Vec<u64>,
}
#[derive(Deserialize, Debug)]
struct Distributions {
2022-07-15 01:26:07 -04:00
distributions: Vec<Distribution>,
}
2022-11-14 21:49:49 -05:00
let mut distributions: Distributions = self
.json_rpc_call(
"get_output_distribution",
2022-07-15 01:26:07 -04:00
Some(json!({
2022-11-14 21:49:49 -05:00
"binary": false,
"amounts": [0],
"cumulative": true,
"from_height": from,
"to_height": to,
2022-07-15 01:26:07 -04:00
})),
)
.await?;
2022-11-14 21:49:49 -05:00
Ok(distributions.distributions.swap_remove(0).distribution)
}
2024-06-16 19:59:25 -04:00
/// Get the specified outputs from the RingCT (zero-amount) pool.
async fn get_outs(&self, indexes: &[u64]) -> Result<Vec<OutputResponse>, RpcError> {
#[derive(Deserialize, Debug)]
struct OutsResponse {
status: String,
outs: Vec<OutputResponse>,
}
let res: OutsResponse = self
2022-07-15 01:26:07 -04:00
.rpc_call(
"get_outs",
Some(json!({
"get_txid": true,
"outputs": indexes.iter().map(|o| json!({
"amount": 0,
"index": o
})).collect::<Vec<_>>()
})),
)
.await?;
if res.status != "OK" {
Err(RpcError::InvalidNode("bad response to get_outs".to_string()))?;
}
Ok(res.outs)
}
/// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their
/// timelock has been satisfied.
///
/// The timelock being satisfied is distinct from being free of the 10-block lock applied to all
/// Monero transactions.
2024-06-16 19:59:25 -04:00
///
/// The node is trusted for if the output is unlocked unless `fingerprintable_canonical` is set
/// to true. If `fingerprintable_canonical` is set to true, the node's local view isn't used, yet
/// the transaction's timelock is checked to be unlocked at the specified `height`. This offers a
/// canonical decoy selection, yet is fingerprintable as time-based timelocks aren't evaluated
/// (and considered locked, preventing their selection).
async fn get_unlocked_outputs(
&self,
indexes: &[u64],
height: usize,
fingerprintable_canonical: bool,
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> {
let outs: Vec<OutputResponse> = self.get_outs(indexes).await?;
// Only need to fetch txs to do canonical check on timelock
let txs = if fingerprintable_canonical {
self
.get_transactions(
&outs.iter().map(|out| hash_hex(&out.txid)).collect::<Result<Vec<_>, _>>()?,
)
.await?
} else {
Vec::new()
};
// TODO: https://github.com/serai-dex/serai/issues/104
2022-07-15 01:26:07 -04:00
outs
.iter()
.enumerate()
.map(|(i, out)| {
// Allow keys to be invalid, though if they are, return None to trigger selection of a new
// decoy
// Only valid keys can be used in CLSAG proofs, hence the need for re-selection, yet
// invalid keys may honestly exist on the blockchain
// Only a recent hard fork checked output keys were valid points
let Some(key) = decompress_point(
rpc_hex(&out.key)?
.try_into()
.map_err(|_| RpcError::InvalidNode("non-32-byte point".to_string()))?,
) else {
return Ok(None);
};
Ok(Some([key, rpc_point(&out.mask)?]).filter(|_| {
if fingerprintable_canonical {
2024-06-23 10:08:51 -04:00
Timelock::Block(height) >= txs[i].prefix().timelock
} else {
out.unlocked
}
}))
2022-07-15 01:26:07 -04:00
})
.collect()
}
2024-06-16 19:59:25 -04:00
/// Get the currently estimated fee rate from the node.
///
/// This may be manipulated to unsafe levels and MUST be sanity checked.
// TODO: Take a sanity check argument
2024-06-21 14:14:15 -04:00
async fn get_fee_rate(&self, priority: FeePriority) -> Result<FeeRate, RpcError> {
#[derive(Deserialize, Debug)]
struct FeeResponse {
status: String,
fees: Option<Vec<u64>>,
fee: u64,
quantization_mask: u64,
}
2024-06-21 14:14:15 -04:00
let res: FeeResponse = self
.json_rpc_call(
"get_fee_estimate",
Some(json!({ "grace_blocks": GRACE_BLOCKS_FOR_FEE_ESTIMATE })),
)
.await?;
2024-06-21 14:14:15 -04:00
if res.status != "OK" {
Err(RpcError::InvalidFee)?;
}
if let Some(fees) = res.fees {
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
// src/wallet/wallet2.cpp#L7615-L7620
let priority_idx = usize::try_from(if priority.fee_priority() >= 4 {
3
} else {
priority.fee_priority().saturating_sub(1)
})
.map_err(|_| RpcError::InvalidPriority)?;
2024-06-21 14:14:15 -04:00
if priority_idx >= fees.len() {
Err(RpcError::InvalidPriority)
} else {
2024-06-21 14:14:15 -04:00
FeeRate::new(fees[priority_idx], res.quantization_mask)
}
} else {
2024-06-21 14:14:15 -04:00
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
// src/wallet/wallet2.cpp#L7569-L7584
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
// src/wallet/wallet2.cpp#L7660-L7661
let priority_idx =
usize::try_from(if priority.fee_priority() == 0 { 1 } else { priority.fee_priority() - 1 })
.map_err(|_| RpcError::InvalidPriority)?;
let multipliers = [1, 5, 25, 1000];
if priority_idx >= multipliers.len() {
// though not an RPC error, it seems sensible to treat as such
Err(RpcError::InvalidPriority)?;
}
let fee_multiplier = multipliers[priority_idx];
FeeRate::new(res.fee * fee_multiplier, res.quantization_mask)
}
}
2024-06-16 19:59:25 -04:00
/// Publish a transaction.
async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> {
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct SendRawResponse {
status: String,
double_spend: bool,
fee_too_low: bool,
invalid_input: bool,
invalid_output: bool,
low_mixin: bool,
not_relayed: bool,
overspend: bool,
too_big: bool,
too_few_outputs: bool,
2022-07-15 01:26:07 -04:00
reason: String,
}
2022-07-15 01:26:07 -04:00
let res: SendRawResponse = self
Processor (#259) * Initial work on a message box * Finish message-box (untested) * Expand documentation * Embed the recipient in the signature challenge Prevents a message from A -> B from being read as from A -> C. * Update documentation by bifurcating sender/receiver * Panic on receiving an invalid signature If we've received an invalid signature in an authenticated system, a service is malicious, critically faulty (equivalent to malicious), or the message layer has been compromised (or is otherwise critically faulty). Please note a receiver who handles a message they shouldn't will trigger this. That falls under being critically faulty. * Documentation and helper methods SecureMessage::new and SecureMessage::serialize. Secure Debug for MessageBox. * Have SecureMessage not be serialized by default Allows passing around in-memory, if desired, and moves the error from decrypt to new (which performs deserialization). Decrypt no longer has an error since it panics if given an invalid signature, due to this being intranet code. * Explain and improve nonce handling Includes a missing zeroize call. * Rebase to latest develop Updates to transcript 0.2.0. * Add a test for the MessageBox * Export PrivateKey and PublicKey * Also test serialization * Add a key_gen binary to message_box * Have SecureMessage support Serde * Add encrypt_to_bytes and decrypt_from_bytes * Support String ser via base64 * Rename encrypt/decrypt to encrypt_bytes/decrypt_to_bytes * Directly operate with values supporting Borsh * Use bincode instead of Borsh By staying inside of serde, we'll support many more structs. While bincode isn't canonical, we don't need canonicity on an authenticated, internal system. * Turn PrivateKey, PublicKey into structs Uses Zeroizing for the PrivateKey per #150. * from_string functions intended for loading from an env * Use &str for PublicKey from_string (now from_str) The PrivateKey takes the String to take ownership of its memory and zeroize it. That isn't needed with PublicKeys. * Finish updating from develop * Resolve warning * Use ZeroizingAlloc on the key_gen binary * Move message-box from crypto/ to common/ * Move key serialization functions to ser * add/remove functions in MessageBox * Implement Hash on dalek_ff_group Points * Make MessageBox generic to its key Exposes a &'static str variant for internal use and a RistrettoPoint variant for external use. * Add Private to_string as deprecated Stub before more competent tooling is deployed. * Private to_public * Test both Internal and External MessageBox, only use PublicKey in the pub API * Remove panics on invalid signatures Leftover from when this was solely internal which is now unsafe. * Chicken scratch a Scanner task * Add a write function to the DKG library Enables writing directly to a file. Also modifies serialize to return Zeroizing<Vec<u8>> instead of just Vec<u8>. * Make dkg::encryption pub * Remove encryption from MessageBox * Use a 64-bit block number in Substrate We use a 64-bit block number in general since u32 only works for 120 years (with a 1 second block time). As some chains even push the 1 second threshold, especially ones based on DAG consensus, this becomes potentially as low as 60 years. While that should still be plenty, it's not worth wondering/debating. Since Serai uses 64-bit block numbers elsewhere, this ensures consistency. * Misc crypto lints * Get the scanner scratch to compile * Initial scanner test * First few lines of scheduler * Further work on scheduler, solidify API * Define Scheduler TX format * Branch creation algorithm * Document when the branch algorithm isn't perfect * Only scanned confirmed blocks * Document Coin * Remove Canonical/ChainNumber from processor The processor should be abstracted from canonical numbers thanks to the coordinator, making this unnecessary. * Add README documenting processor flow * Use Zeroize on substrate primitives * Define messages from/to the processor * Correct over-specified versioning * Correct build re: in_instructions::primitives * Debug/some serde in crypto/ * Use a struct for ValidatorSetInstance * Add a processor key_gen task Redos DB handling code. * Replace trait + impl with wrapper struct * Add a key confirmation flow to the key gen task * Document concerns on key_gen * Start on a signer task * Add Send to FROST traits * Move processor lib.rs to main.rs Adds a dummy main to reduce clippy dead_code warnings. * Further flesh out main.rs * Move the DB trait to AsRef<[u8]> * Signer task * Remove a panic in bitcoin when there's insufficient funds Unchecked underflow. * Have Monero's mine_block mine one block, not 10 It was initially a nicety to deal with the 10 block lock. C::CONFIRMATIONS should be used for that instead. * Test signer * Replace channel expects with log statements The expects weren't problematic and had nicer code. They just clutter test output. * Remove the old wallet file It predates the coordinator design and shouldn't be used. * Rename tests/scan.rs to tests/scanner.rs * Add a wallet test Complements the recently removed wallet file by adding a test for the scanner, scheduler, and signer together. * Work on a run function Triggers a clippy ICE. * Resolve clippy ICE The issue was the non-fully specified lambda in signer. * Add KeyGenEvent and KeyGenOrder Needed so we get KeyConfirmed messages from the key gen task. While we could've read the CoordinatorMessage to see that, routing through the key gen tasks ensures we only handle it once it's been successfully saved to disk. * Expand scanner test * Clarify processor documentation * Have the Scanner load keys on boot/save outputs to disk * Use Vec<u8> for Block ID Much more flexible. * Panic if we see the same output multiple times * Have the Scanner DB mark itself as corrupt when doing a multi-put This REALLY should be a TX. Since we don't have a TX API right now, this at least offers detection. * Have DST'd DB keys accept AsRef<[u8]> * Restore polling all signers Writes a custom future to do so. Also loads signers on boot using what the scanner claims are active keys. * Schedule OutInstructions Adds a data field to Payment. Also cleans some dead code. * Panic if we create an invalid transaction Saves the TX once it's successfully signed so if we do panic, we have a copy. * Route coordinator messages to their respective signer Requires adding key to the SignId. * Send SignTransaction orders for all plans * Add a timer to retry sign_plans when prepare_send fails * Minor fmt'ing * Basic Fee API * Move the change key into Plan * Properly route activation_number * Remove ScannerEvent::Block It's not used under current designs * Nicen logs * Add utilities to get a block's number * Have main issue AckBlock Also has a few misc lints. * Parse instructions out of outputs * Tweak TODOs and remove an unwrap * Update Bitcoin max input/output quantity * Only read one piece of data from Monero Due to output randomization, it's infeasible. * Embed plan IDs into the TXs they create We need to stop attempting signing if we've already signed a protocol. Ideally, any one of the participating signers should be able to provide a proof the TX was successfully signed. We can't just run a second signing protocol though as a single malicious signer could complete the TX signature, and publish it, yet not complete the secondary signature. The TX itself has to be sufficient to show that the TX matches the plan. This is done by embedding the ID, so matching addresses/amounts plans are distinguished, and by allowing verification a TX actually matches a set of addresses/amounts. For Monero, this will need augmenting with the ephemeral keys (or usage of a static seed for them). * Don't use OP_RETURN to encode the plan ID on Bitcoin We can use the inputs to distinguih identical-output plans without issue. * Update OP_RETURN data access It's not required to be the last output. * Add Eventualities to Monero An Eventuality is an effective equivalent to a SignableTransaction. That is declared not by the inputs it spends, yet the outputs it creates. Eventualities are also bound to a 32-byte RNG seed, enabling usage of a hash-based identifier in a SignableTransaction, allowing multiple SignableTransactions with the same output set to have different Eventualities. In order to prevent triggering the burning bug, the RNG seed is hashed with the planned-to-be-used inputs' output keys. While this does bind to them, it's only loosely bound. The TX actually created may use different inputs entirely if a forgery is crafted (which requires no brute forcing). Binding to the key images would provide a strong binding, yet would require knowing the key images, which requires active communication with the spend key. The purpose of this is so a multisig can identify if a Transaction the entire group planned has been executed by a subset of the group or not. Once a plan is created, it can have an Eventuality made. The Eventuality's extra is able to be inserted into a HashMap, so all new on-chain transactions can be trivially checked as potential candidates. Once a potential candidate is found, a check involving ECC ops can be performed. While this is arguably a DoS vector, the underlying Monero blockchain would need to be spammed with transactions to trigger it. Accordingly, it becomes a Monero blockchain DoS vector, when this code is written on the premise of the Monero blockchain functioning. Accordingly, it is considered handled. If a forgery does match, it must have created the exact same outputs the multisig would've. Accordingly, it's argued the multisig shouldn't mind. This entire suite of code is only necessary due to the lack of outgoing view keys, yet it's able to avoid an interactive protocol to communicate key images on every single received output. While this could be locked to the multisig feature, there's no practical benefit to doing so. * Add support for encoding Monero address to instructions * Move Serai's Monero address encoding into serai-client serai-client is meant to be a single library enabling using Serai. While it was originally written as an RPC client for Serai, apps actually using Serai will primarily be sending transactions on connected networks. Sending those transactions require proper {In, Out}Instructions, including proper address encoding. Not only has address encoding been moved, yet the subxt client is now behind a feature. coin integrations have their own features, which are on by default. primitives are always exposed. * Reorganize file layout a bit, add feature flags to processor * Tidy up ETH Dockerfile * Add Bitcoin address encoding * Move Bitcoin::Address to serai-client's * Comment where tweaking needs to happen * Add an API to check if a plan was completed in a specific TX This allows any participating signer to submit the TX ID to prevent further signing attempts. Also performs some API cleanup. * Minimize FROST dependencies * Use a seeded RNG for key gen * Tweak keys from Key gen * Test proper usage of Branch/Change addresses Adds a more descriptive error to an error case in decoys, and pads Monero payments as needed. * Also test spending the change output * Add queued_plans to the Scheduler queued_plans is for payments to be issued when an amount appears, yet the amount is currently pre-fee. One the output is actually created, the Scheduler should be notified of the amount it was created with, moving from queued_plans to plans under the actual amount. Also tightens debug_asserts to asserts for invariants which may are at risk of being exclusive to prod. * Add missing tweak_keys call * Correct decoy selection height handling * Add a few log statements to the scheduler * Simplify test's get_block_number * Simplify, while making more robust, branch address handling in Scheduler * Have fees deducted from payments Corrects Monero's handling of fees when there's no change address. Adds a DUST variable, as needed due to 1_00_000_000 not being enough to pay its fee on Monero. * Add comment to Monero * Consolidate BTC/XMR prepare_send code These aren't fully consolidated. We'd need a SignableTransaction trait for that. This is a lot cleaner though. * Ban integrated addresses The reasoning why is accordingly documented. * Tidy TODOs/dust handling * Update README TODO * Use a determinisitic protocol version in Monero * Test rebuilt KeyGen machines function as expected * Use a more robust KeyGen entropy system * Add DB TXNs Also load entropy from env * Add a loop for processing messages from substrate Allows detecting if we're behind, and if so, waiting to handle the message * Set Monero MAX_INPUTS properly The previous number was based on an old hard fork. With the ring size having increased, transactions have since got larger. * Distinguish TODOs into TODO and TODO2s TODO2s are for after protonet * Zeroize secret share repr in ThresholdCore write * Work on Eventualities Adds serialization and stops signing when an eventuality is proven. * Use a more robust DB key schema * Update to {k, p}256 0.12 * cargo +nightly clippy * cargo update * Slight message-box tweaks * Update to recent Monero merge * Add a Coordinator trait for communication with coordinator * Remove KeyGenHandle for just KeyGen While KeyGen previously accepted instructions over a channel, this breaks the ack flow needed for coordinator communication. Now, KeyGen is the direct object with a handle() function for messages. Thankfully, this ended up being rather trivial for KeyGen as it has no background tasks. * Add a handle function to Signer Enables determining when it's finished handling a CoordinatorMessage and therefore creating an acknowledgement. * Save transactions used to complete eventualities * Use a more intelligent sleep in the signer * Emit SignedTransaction with the first ID *we can still get from our node* * Move Substrate message handling into the new coordinator recv loop * Add handle function to Scanner * Remove the plans timer Enables ensuring the ordring on the handling of plans. * Remove the outputs function which panicked if a precondition wasn't met The new API only returns outputs upon satisfaction of the precondition. * Convert SignerOrder::SignTransaction to a function * Remove the key_gen object from sign_plans * Refactor out get_fee/prepare_send into dedicated functions * Save plans being signed to the DB * Reload transactions being signed on boot * Stop reloading TXs being signed (and report it to peers) * Remove message-box from the processor branch We don't use it here yet. * cargo +nightly fmt * Move back common/zalloc * Update subxt to 0.27 * Zeroize ^1.5, not 1 * Update GitHub workflow * Remove usage of SignId in completed
2023-03-16 22:59:40 -04:00
.rpc_call("send_raw_transaction", Some(json!({ "tx_as_hex": hex::encode(tx.serialize()) })))
2022-07-15 01:26:07 -04:00
.await?;
if res.status != "OK" {
2022-05-21 23:16:06 -04:00
Err(RpcError::InvalidTransaction(tx.hash()))?;
}
Ok(())
}
2024-06-16 19:59:25 -04:00
/// Generate blocks, with the specified address receiving the block reward.
///
/// Returns the hashes of the generated blocks and the last block's number.
// TODO: Take &Address, not &str?
2024-06-16 19:59:25 -04:00
async fn generate_blocks(
&self,
address: &str,
block_count: usize,
) -> Result<(Vec<[u8; 32]>, usize), RpcError> {
#[derive(Debug, Deserialize)]
struct BlocksResponse {
blocks: Vec<String>,
height: usize,
}
let res = self
.json_rpc_call::<BlocksResponse>(
"generateblocks",
Some(json!({
"wallet_address": address,
"amount_of_blocks": block_count
})),
)
.await?;
let mut blocks = Vec::with_capacity(res.blocks.len());
for block in res.blocks {
blocks.push(hash_hex(&block)?);
}
Ok((blocks, res.height))
}
}