mirror of
https://github.com/hinto-janai/cuprate.git
synced 2024-12-22 11:39:30 +00:00
This commit is contained in:
parent
c38daa4497
commit
d4b30333bb
14 changed files with 220 additions and 20 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1142,6 +1142,7 @@ dependencies = [
|
|||
name = "cuprate-types"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"cuprate-epee-encoding",
|
||||
|
|
|
@ -18,6 +18,9 @@ pub const VERSION_BUILD: &str = if cfg!(debug_assertions) {
|
|||
pub const PANIC_CRITICAL_SERVICE_ERROR: &str =
|
||||
"A service critical to Cuprate's function returned an unexpected error.";
|
||||
|
||||
/// The error message returned when an unsupported RPC call is requested.
|
||||
pub const UNSUPPORTED_RPC_CALL: &str = "This RPC call is not supported by Cuprate.";
|
||||
|
||||
pub const EXAMPLE_CONFIG: &str = include_str!("../Cuprated.toml");
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -58,7 +58,7 @@ use cuprate_types::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
constants::VERSION_BUILD,
|
||||
constants::{UNSUPPORTED_RPC_CALL, VERSION_BUILD},
|
||||
rpc::{
|
||||
helper,
|
||||
request::{address_book, blockchain, blockchain_context, blockchain_manager, txpool},
|
||||
|
@ -122,9 +122,10 @@ pub(super) async fn map_request(
|
|||
Req::GetMinerData(r) => Resp::GetMinerData(get_miner_data(state, r).await?),
|
||||
Req::PruneBlockchain(r) => Resp::PruneBlockchain(prune_blockchain(state, r).await?),
|
||||
Req::CalcPow(r) => Resp::CalcPow(calc_pow(state, r).await?),
|
||||
Req::FlushCache(r) => Resp::FlushCache(flush_cache(state, r).await?),
|
||||
Req::AddAuxPow(r) => Resp::AddAuxPow(add_aux_pow(state, r).await?),
|
||||
Req::GetTxIdsLoose(r) => Resp::GetTxIdsLoose(get_tx_ids_loose(state, r).await?),
|
||||
|
||||
// Unsupported RPC calls.
|
||||
Req::GetTxIdsLoose(_) | Req::FlushCache(_) => return Err(anyhow!(UNSUPPORTED_RPC_CALL)),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -34,15 +34,16 @@ use cuprate_rpc_types::{
|
|||
};
|
||||
use cuprate_types::{
|
||||
rpc::{KeyImageSpentStatus, OutKey, PoolInfo, PoolTxInfo},
|
||||
TxInPool,
|
||||
TxInPool, TxRelayChecks,
|
||||
};
|
||||
use monero_serai::transaction::Transaction;
|
||||
use monero_serai::transaction::{Input, Transaction};
|
||||
|
||||
use crate::{
|
||||
rpc::CupratedRpcHandler,
|
||||
constants::UNSUPPORTED_RPC_CALL,
|
||||
rpc::{
|
||||
helper,
|
||||
request::{blockchain, blockchain_context, blockchain_manager, txpool},
|
||||
CupratedRpcHandler,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -84,7 +85,6 @@ pub(super) async fn map_request(
|
|||
Req::InPeers(r) => Resp::InPeers(in_peers(state, r).await?),
|
||||
Req::GetNetStats(r) => Resp::GetNetStats(get_net_stats(state, r).await?),
|
||||
Req::GetOuts(r) => Resp::GetOuts(get_outs(state, r).await?),
|
||||
Req::Update(r) => Resp::Update(update(state, r).await?),
|
||||
Req::PopBlocks(r) => Resp::PopBlocks(pop_blocks(state, r).await?),
|
||||
Req::GetTransactionPoolHashes(r) => {
|
||||
Resp::GetTransactionPoolHashes(get_transaction_pool_hashes(state, r).await?)
|
||||
|
@ -92,12 +92,11 @@ pub(super) async fn map_request(
|
|||
Req::GetPublicNodes(r) => Resp::GetPublicNodes(get_public_nodes(state, r).await?),
|
||||
|
||||
// Unsupported requests.
|
||||
Req::StartMining(_)
|
||||
Req::Update(_)
|
||||
| Req::StartMining(_)
|
||||
| Req::StopMining(_)
|
||||
| Req::MiningStatus(_)
|
||||
| Req::SetLogHashRate(_) => {
|
||||
return Err(anyhow!("Mining RPC calls are not supported by Cuprate"))
|
||||
}
|
||||
| Req::SetLogHashRate(_) => return Err(anyhow!(UNSUPPORTED_RPC_CALL)),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -329,13 +328,134 @@ async fn is_key_image_spent(
|
|||
|
||||
/// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server.cpp#L1307-L1411>
|
||||
async fn send_raw_transaction(
|
||||
state: CupratedRpcHandler,
|
||||
mut state: CupratedRpcHandler,
|
||||
request: SendRawTransactionRequest,
|
||||
) -> Result<SendRawTransactionResponse, Error> {
|
||||
Ok(SendRawTransactionResponse {
|
||||
let mut resp = SendRawTransactionResponse {
|
||||
base: AccessResponseBase::OK,
|
||||
..todo!()
|
||||
})
|
||||
double_spend: false,
|
||||
fee_too_low: false,
|
||||
invalid_input: false,
|
||||
invalid_output: false,
|
||||
low_mixin: false,
|
||||
nonzero_unlock_time: false,
|
||||
not_relayed: request.do_not_relay,
|
||||
overspend: false,
|
||||
reason: String::new(),
|
||||
sanity_check_failed: false,
|
||||
too_big: false,
|
||||
too_few_outputs: false,
|
||||
tx_extra_too_big: false,
|
||||
};
|
||||
|
||||
let tx = {
|
||||
let blob = hex::decode(request.tx_as_hex)?;
|
||||
Transaction::read(&mut blob.as_slice())?
|
||||
};
|
||||
|
||||
if request.do_sanity_checks {
|
||||
// FIXME: these checks could be defined elsewhere.
|
||||
//
|
||||
// <https://github.com/monero-project/monero/blob/893916ad091a92e765ce3241b94e706ad012b62a/src/cryptonote_core/tx_sanity_check.cpp#L42>
|
||||
fn tx_sanity_check(tx: &Transaction, rct_outs_available: u64) -> Result<(), &'static str> {
|
||||
let Some(input) = tx.prefix().inputs.get(0) else {
|
||||
return Err("No inputs");
|
||||
};
|
||||
|
||||
let mut rct_indices = BTreeSet::new();
|
||||
let n_indices: usize = 0;
|
||||
|
||||
for input in tx.prefix().inputs {
|
||||
match input {
|
||||
Input::Gen(_) => return Err("Transaction is coinbase"),
|
||||
Input::ToKey {
|
||||
amount,
|
||||
key_offsets,
|
||||
key_image,
|
||||
} => {
|
||||
let Some(amount) = amount else {
|
||||
continue;
|
||||
};
|
||||
|
||||
n_indices += key_offsets.len();
|
||||
let absolute = todo!();
|
||||
rct_indices.extend(absolute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if n_indices <= 10 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if rct_outs_available < 10_000 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rct_indices_len = rct_indices.len();
|
||||
if rct_indices_len < n_indices * 8 / 10 {
|
||||
return Err("amount of unique indices is too low (amount of rct indices is {rct_indices_len} out of total {n_indices} indices.");
|
||||
}
|
||||
|
||||
let offsets = Vec::with_capacity(rct_indices_len);
|
||||
let median = todo!();
|
||||
if median < rct_outs_available * 6 / 10 {
|
||||
return Err("median offset index is too low (median is {median} out of total {rct_outs_available} offsets). Transactions should contain a higher fraction of recent outputs.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let rct_outs_available = blockchain::total_rct_outputs(&mut state.blockchain_read).await?;
|
||||
|
||||
if let Err(e) = tx_sanity_check(&tx, rct_outs_available) {
|
||||
resp.base.response_base.status = Status::Failed;
|
||||
resp.reason.push_str(&format!("Sanity check failed: {e}"));
|
||||
resp.sanity_check_failed = true;
|
||||
return Ok(resp);
|
||||
}
|
||||
}
|
||||
|
||||
let tx_relay_checks =
|
||||
txpool::check_maybe_relay_local(&mut state.txpool_manager, tx, !request.do_not_relay)
|
||||
.await?;
|
||||
|
||||
if tx_relay_checks.is_empty() {
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server.cpp#L124>
|
||||
fn add_reason(reasons: &mut String, reason: &'static str) {
|
||||
if !reasons.is_empty() {
|
||||
reasons.push_str(", ");
|
||||
}
|
||||
reasons.push_str(reason);
|
||||
}
|
||||
|
||||
let mut reasons = String::new();
|
||||
|
||||
#[rustfmt::skip]
|
||||
let array = [
|
||||
(&mut resp.double_spend, TxRelayChecks::DOUBLE_SPEND, "double spend"),
|
||||
(&mut resp.fee_too_low, TxRelayChecks::FEE_TOO_LOW, "fee too low"),
|
||||
(&mut resp.invalid_input, TxRelayChecks::INVALID_INPUT, "invalid input"),
|
||||
(&mut resp.invalid_output, TxRelayChecks::INVALID_OUTPUT, "invalid output"),
|
||||
(&mut resp.low_mixin, TxRelayChecks::LOW_MIXIN, "bad ring size"),
|
||||
(&mut resp.nonzero_unlock_time, TxRelayChecks::NONZERO_UNLOCK_TIME, "tx unlock time is not zero"),
|
||||
(&mut resp.overspend, TxRelayChecks::OVERSPEND, "overspend"),
|
||||
(&mut resp.too_big, TxRelayChecks::TOO_BIG, "too big"),
|
||||
(&mut resp.too_few_outputs, TxRelayChecks::TOO_FEW_OUTPUTS, "too few outputs"),
|
||||
(&mut resp.tx_extra_too_big, TxRelayChecks::TX_EXTRA_TOO_BIG, "tx-extra too big"),
|
||||
];
|
||||
|
||||
for (field, flag, reason) in array {
|
||||
if tx_relay_checks.contains(flag) {
|
||||
*field = true;
|
||||
add_reason(&mut reasons, reason);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
/// <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/src/rpc/core_rpc_server.cpp#L1413-L1462>
|
||||
|
|
|
@ -394,3 +394,19 @@ pub(crate) async fn transactions(
|
|||
|
||||
Ok((txs, missed_txs))
|
||||
}
|
||||
|
||||
/// [`BlockchainReadRequest::TotalRctOutputs`].
|
||||
pub(crate) async fn total_rct_outputs(
|
||||
blockchain_read: &mut BlockchainReadHandle,
|
||||
) -> Result<u64, Error> {
|
||||
let BlockchainResponse::TotalRctOutputs(total_rct_outputs) = blockchain_read
|
||||
.ready()
|
||||
.await?
|
||||
.call(BlockchainReadRequest::TotalRctOutputs)
|
||||
.await?
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
Ok(total_rct_outputs)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use std::{convert::Infallible, num::NonZero};
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use monero_serai::transaction::Transaction;
|
||||
use tower::{Service, ServiceExt};
|
||||
|
||||
use cuprate_helper::cast::usize_to_u64;
|
||||
|
@ -15,7 +16,7 @@ use cuprate_txpool::{
|
|||
};
|
||||
use cuprate_types::{
|
||||
rpc::{PoolInfo, PoolInfoFull, PoolInfoIncremental, PoolTxInfo},
|
||||
TxInPool,
|
||||
TxInPool, TxRelayChecks,
|
||||
};
|
||||
|
||||
// FIXME: use `anyhow::Error` over `tower::BoxError` in txpool.
|
||||
|
@ -145,3 +146,12 @@ pub(crate) async fn relay(
|
|||
todo!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// TODO
|
||||
pub(crate) async fn check_maybe_relay_local(
|
||||
txpool_manager: &mut Infallible,
|
||||
tx: Transaction,
|
||||
relay: bool,
|
||||
) -> Result<TxRelayChecks, Error> {
|
||||
Ok(todo!())
|
||||
}
|
||||
|
|
|
@ -36,6 +36,9 @@ pub const CORE_RPC_STATUS_NOT_MINING: &str = "NOT MINING";
|
|||
#[doc = monero_definition_link!("cc73fe71162d564ffda8e549b79a350bca53c454", "/rpc/core_rpc_server_commands_defs.h", 81)]
|
||||
pub const CORE_RPC_STATUS_PAYMENT_REQUIRED: &str = "PAYMENT REQUIRED";
|
||||
|
||||
/// Not defined in `monerod` although used frequently.
|
||||
pub const CORE_RPC_STATUS_FAILED: &str = "Failed";
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Versions
|
||||
#[doc = monero_definition_link!("cc73fe71162d564ffda8e549b79a350bca53c454", "/rpc/core_rpc_server_commands_defs.h", 90)]
|
||||
/// RPC major version.
|
||||
|
|
|
@ -24,7 +24,7 @@ pub mod misc;
|
|||
pub mod other;
|
||||
|
||||
pub use constants::{
|
||||
CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK,
|
||||
CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_FAILED, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK,
|
||||
CORE_RPC_STATUS_PAYMENT_REQUIRED, CORE_RPC_VERSION, CORE_RPC_VERSION_MAJOR,
|
||||
CORE_RPC_VERSION_MINOR,
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@ use cuprate_epee_encoding::{
|
|||
};
|
||||
|
||||
use crate::constants::{
|
||||
CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK,
|
||||
CORE_RPC_STATUS_BUSY, CORE_RPC_STATUS_FAILED, CORE_RPC_STATUS_NOT_MINING, CORE_RPC_STATUS_OK,
|
||||
CORE_RPC_STATUS_PAYMENT_REQUIRED,
|
||||
};
|
||||
|
||||
|
@ -40,24 +40,28 @@ use crate::constants::{
|
|||
/// let other = Status::Other("OTHER".into());
|
||||
///
|
||||
/// assert_eq!(to_string(&Status::Ok).unwrap(), r#""OK""#);
|
||||
/// assert_eq!(to_string(&Status::Failed).unwrap(), r#""Failed""#);
|
||||
/// assert_eq!(to_string(&Status::Busy).unwrap(), r#""BUSY""#);
|
||||
/// assert_eq!(to_string(&Status::NotMining).unwrap(), r#""NOT MINING""#);
|
||||
/// assert_eq!(to_string(&Status::PaymentRequired).unwrap(), r#""PAYMENT REQUIRED""#);
|
||||
/// assert_eq!(to_string(&other).unwrap(), r#""OTHER""#);
|
||||
///
|
||||
/// assert_eq!(Status::Ok.as_ref(), CORE_RPC_STATUS_OK);
|
||||
/// assert_eq!(Status::Failed.as_ref(), CORE_RPC_STATUS_FAILED);
|
||||
/// assert_eq!(Status::Busy.as_ref(), CORE_RPC_STATUS_BUSY);
|
||||
/// assert_eq!(Status::NotMining.as_ref(), CORE_RPC_STATUS_NOT_MINING);
|
||||
/// assert_eq!(Status::PaymentRequired.as_ref(), CORE_RPC_STATUS_PAYMENT_REQUIRED);
|
||||
/// assert_eq!(other.as_ref(), "OTHER");
|
||||
///
|
||||
/// assert_eq!(format!("{}", Status::Ok), CORE_RPC_STATUS_OK);
|
||||
/// assert_eq!(format!("{}", Status::Failed), CORE_RPC_STATUS_FAILED);
|
||||
/// assert_eq!(format!("{}", Status::Busy), CORE_RPC_STATUS_BUSY);
|
||||
/// assert_eq!(format!("{}", Status::NotMining), CORE_RPC_STATUS_NOT_MINING);
|
||||
/// assert_eq!(format!("{}", Status::PaymentRequired), CORE_RPC_STATUS_PAYMENT_REQUIRED);
|
||||
/// assert_eq!(format!("{}", other), "OTHER");
|
||||
///
|
||||
/// assert_eq!(format!("{:?}", Status::Ok), "Ok");
|
||||
/// assert_eq!(format!("{:?}", Status::Failed), "Failed");
|
||||
/// assert_eq!(format!("{:?}", Status::Busy), "Busy");
|
||||
/// assert_eq!(format!("{:?}", Status::NotMining), "NotMining");
|
||||
/// assert_eq!(format!("{:?}", Status::PaymentRequired), "PaymentRequired");
|
||||
|
@ -74,6 +78,10 @@ pub enum Status {
|
|||
#[default]
|
||||
Ok,
|
||||
|
||||
/// Generic request failure.
|
||||
#[cfg_attr(feature = "serde", serde(rename = "Failed"))]
|
||||
Failed,
|
||||
|
||||
/// The daemon is busy, try later; [`CORE_RPC_STATUS_BUSY`].
|
||||
#[cfg_attr(feature = "serde", serde(rename = "BUSY"))]
|
||||
Busy,
|
||||
|
@ -101,6 +109,7 @@ impl From<String> for Status {
|
|||
CORE_RPC_STATUS_BUSY => Self::Busy,
|
||||
CORE_RPC_STATUS_NOT_MINING => Self::NotMining,
|
||||
CORE_RPC_STATUS_PAYMENT_REQUIRED => Self::PaymentRequired,
|
||||
CORE_RPC_STATUS_FAILED => Self::Failed,
|
||||
_ => Self::Other(s),
|
||||
}
|
||||
}
|
||||
|
@ -110,6 +119,7 @@ impl AsRef<str> for Status {
|
|||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::Ok => CORE_RPC_STATUS_OK,
|
||||
Self::Failed => CORE_RPC_STATUS_FAILED,
|
||||
Self::Busy => CORE_RPC_STATUS_BUSY,
|
||||
Self::NotMining => CORE_RPC_STATUS_NOT_MINING,
|
||||
Self::PaymentRequired => CORE_RPC_STATUS_PAYMENT_REQUIRED,
|
||||
|
|
|
@ -51,7 +51,9 @@ use crate::{
|
|||
free::{compact_history_genesis_not_included, compact_history_index_to_height_offset},
|
||||
types::{BlockchainReadHandle, ResponseResult},
|
||||
},
|
||||
tables::{AltBlockHeights, BlockHeights, BlockInfos, OpenTables, Tables, TablesIter},
|
||||
tables::{
|
||||
AltBlockHeights, BlockHeights, BlockInfos, OpenTables, RctOutputs, Tables, TablesIter,
|
||||
},
|
||||
types::{
|
||||
AltBlockHeight, Amount, AmountIndex, BlockHash, BlockHeight, KeyImage, PreRctOutputId,
|
||||
},
|
||||
|
@ -135,6 +137,7 @@ fn map_request(
|
|||
R::AltChains => alt_chains(env),
|
||||
R::AltChainCount => alt_chain_count(env),
|
||||
R::Transactions { tx_hashes } => transactions(env, tx_hashes),
|
||||
R::TotalRctOutputs => total_rct_outputs(env),
|
||||
}
|
||||
|
||||
/* SOMEDAY: post-request handling, run some code for each request? */
|
||||
|
@ -779,3 +782,13 @@ fn transactions(env: &ConcreteEnv, tx_hashes: HashSet<[u8; 32]>) -> ResponseResu
|
|||
missed_txs: todo!(),
|
||||
})
|
||||
}
|
||||
|
||||
/// [`BlockchainReadRequest::TotalRctOutputs`]
|
||||
fn total_rct_outputs(env: &ConcreteEnv) -> ResponseResult {
|
||||
// Single-threaded, no `ThreadLocal` required.
|
||||
let env_inner = env.env_inner();
|
||||
let tx_ro = env_inner.tx_ro()?;
|
||||
let len = env_inner.open_db_ro::<RctOutputs>(&tx_ro)?.len()?;
|
||||
|
||||
Ok(BlockchainResponse::TotalRctOutputs(len))
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ cuprate-helper = { workspace = true, optional = true, features = ["cast"]
|
|||
cuprate-fixed-bytes = { workspace = true, features = ["std", "serde"] }
|
||||
cuprate-hex = { workspace = true, optional = true }
|
||||
|
||||
bitflags = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
cfg-if = { workspace = true }
|
||||
curve25519-dalek = { workspace = true }
|
||||
|
|
|
@ -163,6 +163,9 @@ pub enum BlockchainReadRequest {
|
|||
|
||||
/// TODO
|
||||
Transactions { tx_hashes: HashSet<[u8; 32]> },
|
||||
|
||||
/// TODO
|
||||
TotalRctOutputs,
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- WriteRequest
|
||||
|
@ -358,6 +361,9 @@ pub enum BlockchainResponse {
|
|||
missed_txs: Vec<[u8; 32]>,
|
||||
},
|
||||
|
||||
/// Response to [`BlockchainReadRequest::TotalRctOutputs`].
|
||||
TotalRctOutputs(u64),
|
||||
|
||||
//------------------------------------------------------ Writes
|
||||
/// A generic Ok response to indicate a request was successfully handled.
|
||||
///
|
||||
|
|
|
@ -25,7 +25,8 @@ pub use transaction_verification_data::{
|
|||
};
|
||||
pub use types::{
|
||||
AltBlockInformation, BlockTemplate, Chain, ChainId, ExtendedBlockHeader, OutputOnChain,
|
||||
TxInBlockchain, TxInPool, TxsInBlock, VerifiedBlockInformation, VerifiedTransactionInformation,
|
||||
TxInBlockchain, TxInPool, TxRelayChecks, TxsInBlock, VerifiedBlockInformation,
|
||||
VerifiedTransactionInformation,
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Feature-gated
|
||||
|
|
|
@ -191,6 +191,21 @@ pub struct TxInPool {
|
|||
pub relayed: bool,
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
pub struct TxRelayChecks: u16 {
|
||||
const DOUBLE_SPEND = 1;
|
||||
const FEE_TOO_LOW = 1 << 1;
|
||||
const INVALID_INPUT = 1 << 2;
|
||||
const INVALID_OUTPUT = 1 << 3;
|
||||
const LOW_MIXIN = 1 << 4;
|
||||
const NONZERO_UNLOCK_TIME = 1 << 5;
|
||||
const OVERSPEND = 1 << 6;
|
||||
const TOO_BIG = 1 << 7;
|
||||
const TOO_FEW_OUTPUTS = 1 << 8;
|
||||
const TX_EXTRA_TOO_BIG = 1 << 9;
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------- Tests
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
|
Loading…
Reference in a new issue