4 Commits

Author SHA1 Message Date
Luke Parker
6357bc0ed4 Remove unused dep from processor 2024-07-06 04:27:30 -04:00
Luke Parker
2334725ec8 Correct the accidental swap of stagenet/testnet address bytes 2024-07-06 04:26:44 -04:00
Luke Parker
0631607b8f Tidy inlined epee code in the RPC 2024-07-06 04:21:06 -04:00
Luke Parker
d847ec5efb Reject torsioned spend keys to ensure we can spend the outputs we scan 2024-07-06 03:48:45 -04:00
13 changed files with 108 additions and 74 deletions

1
Cargo.lock generated
View File

@@ -8337,7 +8337,6 @@ dependencies = [
"serai-env",
"serai-message-queue",
"serai-processor-messages",
"serde",
"serde_json",
"sp-application-crypto",
"thiserror",

View File

@@ -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.

View File

@@ -0,0 +1,3 @@
// TODO
#[test]
fn test() {}

View File

@@ -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"),
};

View File

@@ -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())

View File

@@ -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.

View File

@@ -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(),
);

View File

@@ -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))

View File

@@ -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"] }

View File

@@ -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)]

View File

@@ -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;

View File

@@ -411,6 +411,7 @@ impl Coordinator {
rpc
.generate_blocks(
&ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE))
.unwrap()
.legacy_address(Network::Mainnet),
1,
)

View File

@@ -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");