mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-10 21:19:24 +00:00
Compare commits
4 Commits
b2c962cd3e
...
6357bc0ed4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6357bc0ed4 | ||
|
|
2334725ec8 | ||
|
|
0631607b8f | ||
|
|
d847ec5efb |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -8337,7 +8337,6 @@ dependencies = [
|
||||
"serai-env",
|
||||
"serai-message-queue",
|
||||
"serai-processor-messages",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sp-application-crypto",
|
||||
"thiserror",
|
||||
|
||||
@@ -220,23 +220,6 @@ fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
|
||||
.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)
|
||||
}
|
||||
|
||||
/// An RPC connection to a Monero daemon.
|
||||
///
|
||||
/// This is abstract such that users can use an HTTP library (which being their choice), a
|
||||
@@ -511,26 +494,29 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||
|
||||
/// Get the output indexes of the specified transaction.
|
||||
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 {
|
||||
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";
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -550,29 +536,58 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||
|
||||
(|| {
|
||||
let mut res = None;
|
||||
let mut is_okay = false;
|
||||
let mut has_status = 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>> {
|
||||
// Read the amount of fields
|
||||
let fields = read_byte(reader)? >> 2;
|
||||
|
||||
for _ in 0 .. fields {
|
||||
// Read the length of the field's name
|
||||
let name_len = read_byte(reader)?;
|
||||
// Read the name of the field
|
||||
let name = read_raw_vec(read_byte, name_len.into(), reader)?;
|
||||
|
||||
let type_with_array_flag = read_byte(reader)?;
|
||||
// The type of this field, without the potentially set array flag
|
||||
let kind = type_with_array_flag & (!0x80);
|
||||
let has_array_flag = type_with_array_flag != kind;
|
||||
|
||||
let iters = if type_with_array_flag != kind { read_epee_vi(reader)? } else { 1 };
|
||||
// Read this many instances of the field
|
||||
let iters = if has_array_flag { read_epee_vi(reader)? } else { 1 };
|
||||
|
||||
if (&name == b"o_indexes") && (kind != 5) {
|
||||
Err(io::Error::other("o_indexes weren't u64s"))?;
|
||||
// Check the field type
|
||||
{
|
||||
#[allow(clippy::match_same_arms)]
|
||||
let (expected_type, expected_array_flag) = match name.as_slice() {
|
||||
b"o_indexes" => (5, true),
|
||||
b"status" => (10, false),
|
||||
b"untrusted" => (11, false),
|
||||
b"credits" => (5, false),
|
||||
b"top_hash" => (10, false),
|
||||
// On-purposely prints name as a byte vector to prevent printing arbitrary strings
|
||||
// This is a self-describing format so we don't have to error here, yet we don't
|
||||
// claim this to be a complete deserialization function
|
||||
// To ensure it works for this specific use case, it's best to ensure it's limited
|
||||
// to this specific use case (ensuring we have less variables to deal with)
|
||||
_ => Err(io::Error::other(format!("unrecognized field in get_o_indexes: {name:?}")))?,
|
||||
};
|
||||
if (expected_type != kind) || (expected_array_flag != has_array_flag) {
|
||||
let fmt_array_bool = |array_bool| if array_bool { "array" } else { "not array" };
|
||||
Err(io::Error::other(format!(
|
||||
"field {name:?} was {kind} ({}), expected {expected_type} ({})",
|
||||
fmt_array_bool(has_array_flag),
|
||||
fmt_array_bool(expected_array_flag)
|
||||
)))?;
|
||||
}
|
||||
}
|
||||
|
||||
let f = match kind {
|
||||
let read_field_as_bytes = match kind {
|
||||
/*
|
||||
// i64
|
||||
1 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
|
||||
// i32
|
||||
@@ -581,8 +596,10 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||
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
|
||||
@@ -591,6 +608,7 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||
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)?;
|
||||
@@ -602,55 +620,47 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||
},
|
||||
// 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)?);
|
||||
bytes_res.push(read_field_as_bytes(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"))?,
|
||||
));
|
||||
actual_res.push(read_u64(&mut o_index.as_slice())?);
|
||||
}
|
||||
res = Some(actual_res);
|
||||
}
|
||||
b"status" => {
|
||||
if bytes_res
|
||||
.first()
|
||||
.ok_or_else(|| io::Error::other("status wasn't a string"))?
|
||||
.ok_or_else(|| io::Error::other("status was a 0-length array"))?
|
||||
.as_slice() !=
|
||||
b"OK"
|
||||
{
|
||||
// TODO: Better handle non-OK responses
|
||||
Err(io::Error::other("response wasn't OK"))?;
|
||||
}
|
||||
is_okay = true;
|
||||
has_status = true;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
if is_okay && res.is_some() {
|
||||
break;
|
||||
b"untrusted" | b"credits" | b"top_hash" => continue,
|
||||
_ => Err(io::Error::other("unrecognized field in get_o_indexes"))?,
|
||||
}
|
||||
}
|
||||
|
||||
// Didn't return a response with a status
|
||||
// (if the status wasn't okay, we would've already errored)
|
||||
if !is_okay {
|
||||
if !has_status {
|
||||
Err(io::Error::other("response didn't contain a status"))?;
|
||||
}
|
||||
|
||||
@@ -661,7 +671,7 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||
|
||||
read_object(&mut indexes)
|
||||
})()
|
||||
.map_err(|_| RpcError::InvalidNode("invalid binary response".to_string()))
|
||||
.map_err(|e| RpcError::InvalidNode(format!("invalid binary response: {e:?}")))
|
||||
}
|
||||
|
||||
/// Get the output distribution.
|
||||
|
||||
3
coins/monero/tests/tests.rs
Normal file
3
coins/monero/tests/tests.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
// TODO
|
||||
#[test]
|
||||
fn test() {}
|
||||
@@ -169,11 +169,11 @@ const MONERO_MAINNET_BYTES: AddressBytes = match AddressBytes::new(18, 19, 42, 7
|
||||
Some(bytes) => bytes,
|
||||
None => panic!("mainnet byte constants conflicted"),
|
||||
};
|
||||
const MONERO_STAGENET_BYTES: AddressBytes = match AddressBytes::new(53, 54, 63, 111) {
|
||||
const MONERO_STAGENET_BYTES: AddressBytes = match AddressBytes::new(24, 25, 36, 86) {
|
||||
Some(bytes) => bytes,
|
||||
None => panic!("stagenet byte constants conflicted"),
|
||||
};
|
||||
const MONERO_TESTNET_BYTES: AddressBytes = match AddressBytes::new(24, 25, 36, 86) {
|
||||
const MONERO_TESTNET_BYTES: AddressBytes = match AddressBytes::new(53, 54, 63, 111) {
|
||||
Some(bytes) => bytes,
|
||||
None => panic!("testnet byte constants conflicted"),
|
||||
};
|
||||
|
||||
@@ -172,7 +172,6 @@ impl InternalScanner {
|
||||
// Our subtracting of a prime-order element means any torsion will be preserved
|
||||
// If someone wanted to malleate output keys with distinct torsions, only one will be
|
||||
// scanned accordingly (the one which has matching torsion of the spend key)
|
||||
// TODO: If there's a torsioned spend key, can we spend outputs to it?
|
||||
let subaddress_spend_key =
|
||||
output_key - (&output_derivations.shared_key * ED25519_BASEPOINT_TABLE);
|
||||
self.subaddresses.get(&subaddress_spend_key.compress())
|
||||
|
||||
@@ -9,6 +9,21 @@ use crate::{
|
||||
address::{Network, AddressType, SubaddressIndex, MoneroAddress},
|
||||
};
|
||||
|
||||
/// An error while working with a ViewPair.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||
pub enum ViewPairError {
|
||||
/// The spend key was torsioned.
|
||||
///
|
||||
/// Torsioned spend keys are of questionable spendability. This library avoids that question by
|
||||
/// rejecting such ViewPairs.
|
||||
// CLSAG seems to support it if the challenge does a torsion clear, FCMP++ should ship with a
|
||||
// torsion clear, yet it's not worth it to modify CLSAG sign to generate challenges until the
|
||||
// torsion clears and ensure spendability (nor can we reasonably guarantee that in the future)
|
||||
#[cfg_attr(feature = "std", error("torsioned spend key"))]
|
||||
TorsionedSpendKey,
|
||||
}
|
||||
|
||||
/// The pair of keys necessary to scan transactions.
|
||||
///
|
||||
/// This is composed of the public spend key and the private view key.
|
||||
@@ -20,8 +35,11 @@ pub struct ViewPair {
|
||||
|
||||
impl ViewPair {
|
||||
/// Create a new ViewPair.
|
||||
pub fn new(spend: EdwardsPoint, view: Zeroizing<Scalar>) -> Self {
|
||||
ViewPair { spend, view }
|
||||
pub fn new(spend: EdwardsPoint, view: Zeroizing<Scalar>) -> Result<Self, ViewPairError> {
|
||||
if !spend.is_torsion_free() {
|
||||
Err(ViewPairError::TorsionedSpendKey)?;
|
||||
}
|
||||
Ok(ViewPair { spend, view })
|
||||
}
|
||||
|
||||
/// The public spend key for this ViewPair.
|
||||
@@ -86,8 +104,8 @@ pub struct GuaranteedViewPair(pub(crate) ViewPair);
|
||||
|
||||
impl GuaranteedViewPair {
|
||||
/// Create a new GuaranteedViewPair.
|
||||
pub fn new(spend: EdwardsPoint, view: Zeroizing<Scalar>) -> Self {
|
||||
GuaranteedViewPair(ViewPair::new(spend, view))
|
||||
pub fn new(spend: EdwardsPoint, view: Zeroizing<Scalar>) -> Result<Self, ViewPairError> {
|
||||
ViewPair::new(spend, view).map(GuaranteedViewPair)
|
||||
}
|
||||
|
||||
/// The public spend key for this GuaranteedViewPair.
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn random_address() -> (Scalar, ViewPair, MoneroAddress) {
|
||||
let view = Zeroizing::new(Scalar::random(&mut OsRng));
|
||||
(
|
||||
spend,
|
||||
ViewPair::new(spend_pub, view.clone()),
|
||||
ViewPair::new(spend_pub, view.clone()).unwrap(),
|
||||
MoneroAddress::new(
|
||||
Network::Mainnet,
|
||||
AddressType::Legacy,
|
||||
@@ -52,7 +52,7 @@ pub fn random_guaranteed_address() -> (Scalar, GuaranteedViewPair, MoneroAddress
|
||||
let view = Zeroizing::new(Scalar::random(&mut OsRng));
|
||||
(
|
||||
spend,
|
||||
GuaranteedViewPair::new(spend_pub, view.clone()),
|
||||
GuaranteedViewPair::new(spend_pub, view.clone()).unwrap(),
|
||||
MoneroAddress::new(
|
||||
Network::Mainnet,
|
||||
AddressType::Legacy,
|
||||
@@ -240,7 +240,7 @@ macro_rules! test {
|
||||
let view_priv = Zeroizing::new(Scalar::random(&mut OsRng));
|
||||
let mut outgoing_view = Zeroizing::new([0; 32]);
|
||||
OsRng.fill_bytes(outgoing_view.as_mut());
|
||||
let view = ViewPair::new(spend_pub, view_priv.clone());
|
||||
let view = ViewPair::new(spend_pub, view_priv.clone()).unwrap();
|
||||
let addr = view.legacy_address(Network::Mainnet);
|
||||
|
||||
let miner_tx = get_miner_tx_output(&rpc, &view).await;
|
||||
@@ -258,7 +258,7 @@ macro_rules! test {
|
||||
&ViewPair::new(
|
||||
&Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE,
|
||||
Zeroizing::new(Scalar::random(&mut OsRng))
|
||||
),
|
||||
).unwrap(),
|
||||
),
|
||||
rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(),
|
||||
);
|
||||
|
||||
@@ -109,7 +109,8 @@ test!(
|
||||
let mut outgoing_view = Zeroizing::new([0; 32]);
|
||||
OsRng.fill_bytes(outgoing_view.as_mut());
|
||||
let change_view =
|
||||
ViewPair::new(&Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, view_priv.clone());
|
||||
ViewPair::new(&Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, view_priv.clone())
|
||||
.unwrap();
|
||||
|
||||
let mut builder = SignableTransactionBuilder::new(
|
||||
rct_type,
|
||||
@@ -123,7 +124,8 @@ test!(
|
||||
let sub_view = ViewPair::new(
|
||||
&Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE,
|
||||
Zeroizing::new(Scalar::random(&mut OsRng)),
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
builder
|
||||
.add_payment(sub_view.subaddress(Network::Mainnet, SubaddressIndex::new(0, 1).unwrap()), 1);
|
||||
(builder.build().unwrap(), (change_view, sub_view))
|
||||
|
||||
@@ -21,7 +21,6 @@ workspace = true
|
||||
async-trait = { version = "0.1", default-features = false }
|
||||
zeroize = { version = "1", default-features = false, features = ["std"] }
|
||||
thiserror = { version = "1", default-features = false }
|
||||
serde = { version = "1", default-features = false, features = ["std", "derive"] }
|
||||
|
||||
# Libs
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std", "getrandom"] }
|
||||
|
||||
@@ -256,7 +256,7 @@ impl Monero {
|
||||
}
|
||||
|
||||
fn view_pair(spend: EdwardsPoint) -> GuaranteedViewPair {
|
||||
GuaranteedViewPair::new(spend.0, Zeroizing::new(additional_key::<Monero>(0).0))
|
||||
GuaranteedViewPair::new(spend.0, Zeroizing::new(additional_key::<Monero>(0).0)).unwrap()
|
||||
}
|
||||
|
||||
fn address_internal(spend: EdwardsPoint, subaddress: Option<SubaddressIndex>) -> Address {
|
||||
@@ -351,6 +351,7 @@ impl Monero {
|
||||
payments.push(Payment {
|
||||
address: Address::new(
|
||||
ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0))
|
||||
.unwrap()
|
||||
.legacy_address(MoneroNetwork::Mainnet),
|
||||
)
|
||||
.unwrap(),
|
||||
@@ -413,7 +414,7 @@ impl Monero {
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_view_pair() -> ViewPair {
|
||||
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0))
|
||||
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -90,6 +90,7 @@ async fn mint_and_burn_test() {
|
||||
use monero_wallet::{rpc::Rpc, ViewPair, address::Network};
|
||||
|
||||
let addr = ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE))
|
||||
.unwrap()
|
||||
.legacy_address(Network::Mainnet);
|
||||
|
||||
let rpc = producer_handles.monero(ops).await;
|
||||
@@ -353,7 +354,7 @@ async fn mint_and_burn_test() {
|
||||
|
||||
// Grab the first output on the chain
|
||||
let rpc = handles[0].monero(&ops).await;
|
||||
let view_pair = ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE));
|
||||
let view_pair = ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE)).unwrap();
|
||||
let mut scanner = Scanner::new(view_pair.clone());
|
||||
let output = scanner
|
||||
.scan(&rpc, &rpc.get_block_by_number(1).await.unwrap())
|
||||
@@ -578,7 +579,8 @@ async fn mint_and_burn_test() {
|
||||
{
|
||||
use monero_wallet::{transaction::Transaction, rpc::Rpc, ViewPair, Scanner};
|
||||
let rpc = handles[0].monero(&ops).await;
|
||||
let mut scanner = Scanner::new(ViewPair::new(monero_spend, Zeroizing::new(monero_view)));
|
||||
let mut scanner =
|
||||
Scanner::new(ViewPair::new(monero_spend, Zeroizing::new(monero_view)).unwrap());
|
||||
|
||||
// Check for up to 5 minutes
|
||||
let mut found = false;
|
||||
|
||||
@@ -411,6 +411,7 @@ impl Coordinator {
|
||||
rpc
|
||||
.generate_blocks(
|
||||
&ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE))
|
||||
.unwrap()
|
||||
.legacy_address(Network::Mainnet),
|
||||
1,
|
||||
)
|
||||
|
||||
@@ -194,7 +194,7 @@ impl Wallet {
|
||||
let view_key = Scalar::random(&mut OsRng);
|
||||
|
||||
let view_pair =
|
||||
ViewPair::new(ED25519_BASEPOINT_POINT * spend_key, Zeroizing::new(view_key));
|
||||
ViewPair::new(ED25519_BASEPOINT_POINT * spend_key, Zeroizing::new(view_key)).unwrap();
|
||||
|
||||
let rpc = SimpleRequestRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user