#![allow(clippy::result_unit_err)]
pub mod error;
pub mod json_rpc;
pub mod methods;
pub mod utils;
use bitcoin_hashes::hex::FromHex;
use std::convert::{TryFrom, TryInto};
use tracing::debug;
use error::Error;
pub use json_rpc::Message;
pub use methods::{client_to_server, server_to_client, Method, MethodError, ParsingMethodError};
use utils::{Extranonce, HexU32Be};
pub trait IsServer<'a> {
fn handle_message(
&mut self,
client_id: Option<usize>,
msg: json_rpc::Message,
) -> Result<Option<json_rpc::Response>, Error<'a>>
where
Self: std::marker::Sized,
{
match msg {
Message::StandardRequest(_) => {
self.handle_request(client_id, msg)
}
Message::Notification(_) => {
self.handle_request(client_id, msg)
}
_ => {
Err(Error::InvalidJsonRpcMessageKind)
}
}
}
fn handle_request(
&mut self,
client_id: Option<usize>,
msg: json_rpc::Message,
) -> Result<Option<json_rpc::Response>, Error<'a>>
where
Self: std::marker::Sized,
{
let request = msg.try_into()?;
match request {
methods::Client2Server::SuggestDifficulty() => Ok(None),
methods::Client2Server::Authorize(authorize) => {
let authorized = self.handle_authorize(client_id, &authorize);
if authorized {
self.authorize(client_id, &authorize.name);
}
Ok(Some(authorize.respond(authorized)))
}
methods::Client2Server::Configure(configure) => {
debug!("{:?}", configure);
self.set_version_rolling_mask(client_id, configure.version_rolling_mask());
self.set_version_rolling_min_bit(
client_id,
configure.version_rolling_min_bit_count(),
);
let (version_rolling, min_diff) = self.handle_configure(client_id, &configure);
Ok(Some(configure.respond(version_rolling, min_diff)))
}
methods::Client2Server::ExtranonceSubscribe(_) => {
self.handle_extranonce_subscribe();
Ok(None)
}
methods::Client2Server::Submit(submit) => {
let has_valid_version_bits = match &submit.version_bits {
Some(a) => {
if let Some(version_rolling_mask) = self.version_rolling_mask(client_id) {
version_rolling_mask.check_mask(a)
} else {
false
}
}
None => self.version_rolling_mask(client_id).is_none(),
};
let is_valid_submission = self.is_authorized(client_id, &submit.user_name)
&& self.extranonce2_size(client_id) == submit.extra_nonce2.len()
&& has_valid_version_bits;
if is_valid_submission {
let accepted = self.handle_submit(client_id, &submit);
Ok(Some(submit.respond(accepted)))
} else {
Err(Error::InvalidSubmission)
}
}
methods::Client2Server::Subscribe(subscribe) => {
let subscriptions = self.handle_subscribe(client_id, &subscribe);
let extra_n1 = self.set_extranonce1(client_id, None);
let extra_n2_size = self.set_extranonce2_size(client_id, None);
Ok(Some(subscribe.respond(
subscriptions,
extra_n1,
extra_n2_size,
)))
}
}
}
fn handle_configure(
&mut self,
client_id: Option<usize>,
request: &client_to_server::Configure,
) -> (Option<server_to_client::VersionRollingParams>, Option<bool>);
fn handle_subscribe(
&self,
client_id: Option<usize>,
request: &client_to_server::Subscribe,
) -> Vec<(String, String)>;
fn handle_authorize(
&self,
client_id: Option<usize>,
request: &client_to_server::Authorize,
) -> bool;
fn handle_submit(
&self,
client_id: Option<usize>,
request: &client_to_server::Submit<'a>,
) -> bool;
fn handle_extranonce_subscribe(&self);
fn is_authorized(&self, client_id: Option<usize>, name: &str) -> bool;
fn authorize(&mut self, client_id: Option<usize>, name: &str);
fn set_extranonce1(
&mut self,
client_id: Option<usize>,
extranonce1: Option<Extranonce<'a>>,
) -> Extranonce<'a>;
fn extranonce1(&self, client_id: Option<usize>) -> Extranonce<'a>;
fn set_extranonce2_size(
&mut self,
client_id: Option<usize>,
extra_nonce2_size: Option<usize>,
) -> usize;
fn extranonce2_size(&self, client_id: Option<usize>) -> usize;
fn version_rolling_mask(&self, client_id: Option<usize>) -> Option<HexU32Be>;
fn set_version_rolling_mask(&mut self, client_id: Option<usize>, mask: Option<HexU32Be>);
fn set_version_rolling_min_bit(&mut self, client_id: Option<usize>, mask: Option<HexU32Be>);
fn update_extranonce(
&mut self,
client_id: Option<usize>,
extra_nonce1: Extranonce<'a>,
extra_nonce2_size: usize,
) -> Result<json_rpc::Message, Error<'a>> {
self.set_extranonce1(client_id, Some(extra_nonce1.clone()));
self.set_extranonce2_size(client_id, Some(extra_nonce2_size));
Ok(server_to_client::SetExtranonce {
extra_nonce1,
extra_nonce2_size,
}
.into())
}
fn notify(&mut self, client_id: Option<usize>) -> Result<json_rpc::Message, Error<'_>>;
fn handle_set_difficulty(
&mut self,
_client_id: Option<usize>,
value: f64,
) -> Result<json_rpc::Message, Error<'_>> {
let set_difficulty = server_to_client::SetDifficulty { value };
Ok(set_difficulty.into())
}
}
pub trait IsClient<'a> {
fn handle_message(
&mut self,
server_id: Option<usize>,
msg: json_rpc::Message,
) -> Result<Option<json_rpc::Message>, Error<'a>>
where
Self: std::marker::Sized,
{
let method: Result<Method<'a>, MethodError<'a>> = msg.try_into();
match method {
Ok(m) => match m {
Method::Server2ClientResponse(response) => {
let response = self.update_response(server_id, response)?;
self.handle_response(server_id, response)
}
Method::Server2Client(request) => self.handle_request(server_id, request),
Method::Client2Server(_) => Err(Error::InvalidReceiver(m.into())),
Method::ErrorMessage(msg) => self.handle_error_message(server_id, msg),
},
Err(e) => Err(e.into()),
}
}
fn update_response(
&mut self,
server_id: Option<usize>,
response: methods::Server2ClientResponse<'a>,
) -> Result<methods::Server2ClientResponse<'a>, Error<'a>> {
match &response {
methods::Server2ClientResponse::GeneralResponse(general) => {
let is_authorize = self.id_is_authorize(server_id, &general.id);
let is_submit = self.id_is_submit(server_id, &general.id);
match (is_authorize, is_submit) {
(Some(prev_name), false) => {
let authorize = general.clone().into_authorize(prev_name);
Ok(methods::Server2ClientResponse::Authorize(authorize))
}
(None, false) => Ok(methods::Server2ClientResponse::Submit(
general.clone().into_submit(),
)),
_ => Err(Error::UnknownID(general.id)),
}
}
_ => Ok(response),
}
}
fn handle_request(
&mut self,
server_id: Option<usize>,
request: methods::Server2Client<'a>,
) -> Result<Option<json_rpc::Message>, Error<'a>>
where
Self: std::marker::Sized,
{
match request {
methods::Server2Client::Notify(notify) => {
self.handle_notify(server_id, notify)?;
Ok(None)
}
methods::Server2Client::SetDifficulty(mut set_diff) => {
self.handle_set_difficulty(server_id, &mut set_diff)?;
Ok(None)
}
methods::Server2Client::SetExtranonce(mut set_extra_nonce) => {
self.handle_set_extranonce(server_id, &mut set_extra_nonce)?;
Ok(None)
}
methods::Server2Client::SetVersionMask(mut set_version_mask) => {
self.handle_set_version_mask(server_id, &mut set_version_mask)?;
Ok(None)
}
}
}
fn handle_response(
&mut self,
server_id: Option<usize>,
response: methods::Server2ClientResponse<'a>,
) -> Result<Option<json_rpc::Message>, Error<'a>>
where
Self: std::marker::Sized,
{
match response {
methods::Server2ClientResponse::Configure(mut configure) => {
self.handle_configure(server_id, &mut configure)?;
self.set_version_rolling_mask(server_id, configure.version_rolling_mask());
self.set_version_rolling_min_bit(server_id, configure.version_rolling_min_bit());
self.set_status(server_id, ClientStatus::Configured);
debug!("NOTICE: Subscribe extranonce is hardcoded by server");
let subscribe = self
.subscribe(
server_id,
configure.id,
Some(Extranonce::try_from(
Vec::<u8>::from_hex("08000002").map_err(Error::HexError)?,
)?),
)
.ok();
Ok(subscribe)
}
methods::Server2ClientResponse::Subscribe(subscribe) => {
self.handle_subscribe(server_id, &subscribe)?;
self.set_extranonce1(server_id, subscribe.extra_nonce1);
self.set_extranonce2_size(server_id, subscribe.extra_nonce2_size);
self.set_status(server_id, ClientStatus::Subscribed);
Ok(None)
}
methods::Server2ClientResponse::Authorize(authorize) => {
if authorize.is_ok() {
self.authorize_user_name(server_id, authorize.user_name());
};
Ok(None)
}
methods::Server2ClientResponse::Submit(_) => Ok(None),
methods::Server2ClientResponse::GeneralResponse(_) => panic!(),
methods::Server2ClientResponse::SetDifficulty(_) => Ok(None),
}
}
fn handle_error_message(
&mut self,
server_id: Option<usize>,
message: Message,
) -> Result<Option<json_rpc::Message>, Error<'a>>;
fn id_is_authorize(&mut self, server_id: Option<usize>, id: &u64) -> Option<String>;
fn id_is_submit(&mut self, server_id: Option<usize>, id: &u64) -> bool;
fn handle_notify(
&mut self,
server_id: Option<usize>,
notify: server_to_client::Notify<'a>,
) -> Result<(), Error<'a>>;
fn handle_configure(
&mut self,
server_id: Option<usize>,
conf: &mut server_to_client::Configure,
) -> Result<(), Error<'a>>;
fn handle_set_difficulty(
&mut self,
server_id: Option<usize>,
m: &mut server_to_client::SetDifficulty,
) -> Result<(), Error<'a>>;
fn handle_set_extranonce(
&mut self,
server_id: Option<usize>,
m: &mut server_to_client::SetExtranonce,
) -> Result<(), Error<'a>>;
fn handle_set_version_mask(
&mut self,
server_id: Option<usize>,
m: &mut server_to_client::SetVersionMask,
) -> Result<(), Error<'a>>;
fn handle_subscribe(
&mut self,
server_id: Option<usize>,
subscribe: &server_to_client::Subscribe<'a>,
) -> Result<(), Error<'a>>;
fn set_extranonce1(&mut self, server_id: Option<usize>, extranonce1: Extranonce<'a>);
fn extranonce1(&self, server_id: Option<usize>) -> Extranonce<'a>;
fn set_extranonce2_size(&mut self, server_id: Option<usize>, extra_nonce2_size: usize);
fn extranonce2_size(&self, server_id: Option<usize>) -> usize;
fn version_rolling_mask(&self, server_id: Option<usize>) -> Option<HexU32Be>;
fn set_version_rolling_mask(&mut self, server_id: Option<usize>, mask: Option<HexU32Be>);
fn set_version_rolling_min_bit(&mut self, server_id: Option<usize>, min: Option<HexU32Be>);
fn version_rolling_min_bit(&mut self, server_id: Option<usize>) -> Option<HexU32Be>;
fn set_status(&mut self, server_id: Option<usize>, status: ClientStatus);
fn signature(&self, server_id: Option<usize>) -> String;
fn status(&self, server_id: Option<usize>) -> ClientStatus;
fn last_notify(&self, server_id: Option<usize>) -> Option<server_to_client::Notify<'_>>;
#[allow(clippy::ptr_arg)]
fn is_authorized(&self, server_id: Option<usize>, name: &String) -> bool;
fn authorize_user_name(&mut self, server_id: Option<usize>, name: String);
fn configure(&mut self, server_id: Option<usize>, id: u64) -> json_rpc::Message {
if self.version_rolling_min_bit(server_id).is_none()
&& self.version_rolling_mask(server_id).is_none()
{
client_to_server::Configure::void(id).into()
} else {
client_to_server::Configure::new(
id,
self.version_rolling_mask(server_id),
self.version_rolling_min_bit(server_id),
)
.into()
}
}
fn subscribe(
&mut self,
server_id: Option<usize>,
id: u64,
extranonce1: Option<Extranonce<'a>>,
) -> Result<json_rpc::Message, Error<'a>> {
match self.status(server_id) {
ClientStatus::Init => Err(Error::IncorrectClientStatus("mining.subscribe".to_string())),
_ => Ok(client_to_server::Subscribe {
id,
agent_signature: self.signature(server_id),
extranonce1,
}
.try_into()?),
}
}
fn authorize(
&mut self,
server_id: Option<usize>,
id: u64,
name: String,
password: String,
) -> Result<json_rpc::Message, Error<'_>> {
match self.status(server_id) {
ClientStatus::Init => Err(Error::IncorrectClientStatus("mining.authorize".to_string())),
_ => Ok(client_to_server::Authorize { id, name, password }.into()),
}
}
#[allow(clippy::too_many_arguments)]
fn submit(
&mut self,
server_id: Option<usize>,
id: u64,
user_name: String,
extra_nonce2: Extranonce<'a>,
time: i64,
nonce: i64,
version_bits: Option<HexU32Be>,
) -> Result<json_rpc::Message, Error<'a>> {
match self.status(server_id) {
ClientStatus::Init => Err(Error::IncorrectClientStatus("mining.submit".to_string())),
_ => {
if let Some(notify) = self.last_notify(server_id) {
if !self.is_authorized(server_id, &user_name) {
return Err(Error::UnauthorizedClient(user_name));
}
Ok(client_to_server::Submit {
job_id: notify.job_id,
user_name,
extra_nonce2,
time: HexU32Be(time as u32),
nonce: HexU32Be(nonce as u32),
version_bits,
id,
}
.into())
} else {
Err(Error::IncorrectClientStatus(
"No Notify instance found".to_string(),
))
}
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ClientStatus {
Init,
Configured,
Subscribed,
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
struct TestServer<'a> {
authorized_users: HashSet<String>,
extranonce1: Extranonce<'a>,
extranonce2_size: usize,
version_rolling_mask: Option<HexU32Be>,
version_rolling_min_bit: Option<HexU32Be>,
}
impl<'a> TestServer<'a> {
fn new(extranonce1: Extranonce<'a>, extranonce2_size: usize) -> Self {
Self {
authorized_users: HashSet::new(),
extranonce1,
extranonce2_size,
version_rolling_mask: None,
version_rolling_min_bit: None,
}
}
}
impl<'a> IsServer<'a> for TestServer<'a> {
fn handle_configure(
&mut self,
_client_id: Option<usize>,
_request: &client_to_server::Configure,
) -> (Option<server_to_client::VersionRollingParams>, Option<bool>) {
(None, None)
}
fn handle_subscribe(
&self,
_client_id: Option<usize>,
_request: &client_to_server::Subscribe,
) -> Vec<(String, String)> {
vec![("mining.notify".to_string(), "1".to_string())]
}
fn handle_authorize(
&self,
_client_id: Option<usize>,
_request: &client_to_server::Authorize,
) -> bool {
true
}
fn notify(&mut self, _client_id: Option<usize>) -> Result<json_rpc::Message, Error<'_>> {
Ok(json_rpc::Message::StandardRequest(
json_rpc::StandardRequest {
id: 1,
method: "mining.notify".to_string(),
params: serde_json::json!([]),
},
))
}
fn handle_submit(
&self,
_client_id: Option<usize>,
_request: &client_to_server::Submit<'a>,
) -> bool {
true
}
fn handle_extranonce_subscribe(&self) {}
fn is_authorized(&self, _client_id: Option<usize>, name: &str) -> bool {
self.authorized_users.contains(name)
}
fn authorize(&mut self, _client_id: Option<usize>, name: &str) {
self.authorized_users.insert(name.to_string());
}
fn set_extranonce1(
&mut self,
_client_id: Option<usize>,
extranonce1: Option<Extranonce<'a>>,
) -> Extranonce<'a> {
if let Some(extranonce1) = extranonce1 {
self.extranonce1 = extranonce1;
}
self.extranonce1.clone()
}
fn extranonce1(&self, _client_id: Option<usize>) -> Extranonce<'a> {
self.extranonce1.clone()
}
fn set_extranonce2_size(
&mut self,
_client_id: Option<usize>,
extra_nonce2_size: Option<usize>,
) -> usize {
if let Some(extra_nonce2_size) = extra_nonce2_size {
self.extranonce2_size = extra_nonce2_size;
}
self.extranonce2_size
}
fn extranonce2_size(&self, _client_id: Option<usize>) -> usize {
self.extranonce2_size
}
fn version_rolling_mask(&self, _client_id: Option<usize>) -> Option<HexU32Be> {
None
}
fn set_version_rolling_mask(&mut self, _client_id: Option<usize>, mask: Option<HexU32Be>) {
self.version_rolling_mask = mask;
}
fn set_version_rolling_min_bit(
&mut self,
_client_id: Option<usize>,
mask: Option<HexU32Be>,
) {
self.version_rolling_min_bit = mask;
}
}
#[test]
fn test_server_handle_invalid_message() {
let extranonce1 = Extranonce::try_from(Vec::<u8>::from_hex("08000002").unwrap()).unwrap();
let mut server = TestServer::new(extranonce1, 4);
let request_message = json_rpc::Message::StandardRequest(json_rpc::StandardRequest {
id: 42,
method: "mining.subscribe_bad".to_string(),
params: serde_json::json!([]),
});
let result = server.handle_message(None, request_message);
assert!(result.is_err());
match result.unwrap_err() {
Error::Method(inner) => match *inner {
MethodError::MethodNotFound(_) => {}
other => panic!("Expected MethodNotFound error, got {:?}", other),
},
other => panic!("Expected Error::Method, got {:?}", other),
}
}
#[test]
fn version_mask_invalid_len() {
let raw = serde_json::json!([
"mining.set_version_mask",
["123456789"] ]);
let msg: Result<Message, _> = serde_json::from_value(raw);
if let Ok(msg) = msg {
let result = Method::try_from(msg);
assert!(result.is_err(), "Expected error for invalid hex length");
match result.unwrap_err() {
MethodError::ParsingMethodError((ParsingMethodError::InvalidHexLen(_), _)) => {}
other => panic!("Expected InvalidHexLen, got {:?}", other),
}
} else {
panic!("Message parsing failed unexpectedly");
}
}
}