Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements createrawtransaction RPC method #9017

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions zebra-chain/src/block/height.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ impl Height {
/// height and above.
pub const MAX_EXPIRY_HEIGHT: Height = Height(499_999_999);

/// The number of blocks within expiry height when a tx is considered
/// to be expiring soon .
pub const BLOCK_EXPIRY_HEIGHT_THRESHOLD: u32 = 3;

/// Returns the next [`Height`].
///
/// # Panics
Expand Down
3 changes: 2 additions & 1 deletion zebra-chain/src/transparent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use crate::{

mod address;
mod keys;
mod opcodes;

pub mod opcodes;
mod script;
mod serialize;
mod utxo;
Expand Down
2 changes: 2 additions & 0 deletions zebra-chain/src/transparent/opcodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
/// Supported opcodes
///
/// <https://github.com/zcash/zcash/blob/8b16094f6672d8268ff25b2d7bddd6a6207873f7/src/script/script.h#L39>

#[allow(missing_docs)]
pub enum OpCode {
// Opcodes used to generate P2SH scripts.
Equal = 0x87,
Expand Down
227 changes: 222 additions & 5 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
//! Some parts of the `zcashd` RPC documentation are outdated.
//! So this implementation follows the `zcashd` server and `lightwalletd` client implementations.

use std::{collections::HashSet, fmt::Debug, sync::Arc};
use std::{
collections::{HashMap, HashSet},
fmt::Debug,
hash::Hash,
sync::Arc,
};

use chrono::Utc;
use futures::{stream::FuturesOrdered, FutureExt, StreamExt, TryFutureExt};
Expand All @@ -20,16 +25,21 @@

use zcash_primitives::consensus::Parameters;
use zebra_chain::{
amount::{Amount, NonNegative, COIN},
block::{self, Height, SerializedBlock},
chain_tip::{ChainTip, NetworkChainTipHeightEstimator},
parameters::{ConsensusBranchId, Network, NetworkUpgrade},
serialization::ZcashDeserialize,
serialization::{ZcashDeserialize, ZcashSerialize},
subtree::NoteCommitmentSubtreeIndex,
transaction::{self, SerializedTransaction, Transaction, UnminedTx},
transparent::{self, Address},
transaction::{self, LockTime, SerializedTransaction, Transaction, UnminedTx},
transparent::{self, opcodes::OpCode, Address, Input, OutPoint, Script},
};

use zebra_node_services::mempool;
use zebra_state::{HashOrHeight, MinedTx, OutputIndex, OutputLocation, TransactionLocation};
use zebra_state::{
GetBlockTemplateChainInfo, HashOrHeight, MinedTx, OutputIndex, OutputLocation,

Check failure on line 40 in zebra-rpc/src/methods.rs

View workflow job for this annotation

GitHub Actions / Build zebrad crate

unresolved import `zebra_state::GetBlockTemplateChainInfo`
TransactionLocation,
};

use crate::{
constants::{INVALID_PARAMETERS_ERROR_CODE, MISSING_BLOCK_ERROR_CODE},
Expand Down Expand Up @@ -235,6 +245,35 @@
limit: Option<NoteCommitmentSubtreeIndex>,
) -> BoxFuture<Result<GetSubtrees>>;

/// Returns the hex string of the raw transaction based on the given inputs `transactions`, `addresses`,
/// `locktime` and `expiryheight`.
///
/// zcashd reference: [`z_create_raw_transaction`](https://zcash.github.io/rpc/createrawtransaction.html)
/// method: post
/// tags: blockchain
///
/// # Parameters
///
/// - `transactions`: (string, required) A json array of json objects
/// - `addresses`: (string, required) A json object object with addresses as keys and amounts as values.
/// ["address": x.xxx] (numeric, required) They key is the Zcash address, the value is the ZEC amount
/// - `locktime`: (numeric, optional, default=0) Raw locktime. Non-0 value also locktime-actives inputs
/// - `expiryheight`: (numeric, optional, default=nextblockheight+20 (pre-Blossom) or nextblockheight+40 (post-Blossom)) Expiry height of
/// transaction (if Overwinter is active)
///
/// # Notes
/// The transaction's inputs are not signed, and it is not stored in the wallet or transmitted
/// to the network.
/// Transaction IDs for inputs are in hex format
#[rpc(name = "createrawtransaction")]
fn create_raw_transaction(
&self,
transactions: Vec<TxInput>,
addresses: HashMap<String, f64>,
locktime: Option<u32>,
expiryheight: Option<u32>,
) -> BoxFuture<Result<String>>;

/// Returns the raw transaction data, as a [`GetRawTransaction`] JSON string or structure.
///
/// zcashd reference: [`getrawtransaction`](https://zcash.github.io/rpc/getrawtransaction.html)
Expand Down Expand Up @@ -1005,6 +1044,175 @@
.boxed()
}

fn create_raw_transaction(
&self,
transactions: Vec<TxInput>,
addresses: HashMap<String, f64>,
locktime: Option<u32>,
expiryheight: Option<u32>,
) -> BoxFuture<Result<String>> {
let mut state = self.state.clone();
let network = self.network.clone();

async move {
let chain_info_request = zebra_state::ReadRequest::ChainInfo;

Check failure on line 1058 in zebra-rpc/src/methods.rs

View workflow job for this annotation

GitHub Actions / Build zebrad crate

no variant or associated item named `ChainInfo` found for enum `zebra_state::ReadRequest` in the current scope
let response: zebra_state::ReadResponse = state
.ready()
.and_then(|service| service.call(chain_info_request))
.await
.map_server_error()?;

let zebra_state::ReadResponse::ChainInfo(GetBlockTemplateChainInfo {

Check failure on line 1065 in zebra-rpc/src/methods.rs

View workflow job for this annotation

GitHub Actions / Build zebrad crate

no variant or associated item named `ChainInfo` found for enum `zebra_state::ReadResponse` in the current scope
tip_height, ..
}) = response
else {
unreachable!("unmatched response to a chain info request")
};

let lock_time = if let Some(lock_time) = locktime {
LockTime::Height(block::Height(lock_time))
} else {
LockTime::Height(block::Height(0))
};

// Use next block height if exceeds MAX_EXPIRY_HEIGHT or is beyond tip
let next_block_height = tip_height.0 + 1;
let current_upgrade = NetworkUpgrade::current(&network, tip_height);
let next_network_upgrade = NetworkUpgrade::current(&network, block::Height(next_block_height));

let expiry_height_as_u32 = if let Some(expiry_height) = expiryheight {
// DoS mitigation: reject transactions expiring soon (as is done in zcashd)
if expiry_height != 0 && next_block_height + block::Height::BLOCK_EXPIRY_HEIGHT_THRESHOLD > expiry_height {
Error::invalid_params(format!("invalid parameter, expiryheight should be at least {} to avoid
transaction expiring soon", next_block_height + block::Height::BLOCK_EXPIRY_HEIGHT_THRESHOLD));
}

if next_network_upgrade < NetworkUpgrade::Overwinter {
Error::invalid_params("invalid parameter, expiryheight can only be used if Overwinter is active when the transaction is mined");
}

if block::Height(expiry_height) > block::Height::MAX_EXPIRY_HEIGHT
|| expiry_height > tip_height.0
{
Error::invalid_params("invalid parameter, expiryheight out of range");
}
expiry_height
} else {
next_block_height
};

// Set a default sequence based on the lock time.
let default_tx_input_sequence = if lock_time == LockTime::Height(block::Height(0)) {
u32::MAX
} else {
u32::MAX - 1
};

// Compute tx inputs
let tx_inputs: Vec<Input> = transactions
.iter()
.map(|input| {
Ok(Input::PrevOut {
outpoint: OutPoint {
hash: transaction::Hash::from_hex(&input.txid).map_err(|_| {
Error::invalid_params(format!("invalid parameter, transaction id {} is an invalid hex string", input.txid))
})?,
index: input.vout,
},
unlock_script: Script::new(&[]),
sequence: input.sequence.unwrap_or(default_tx_input_sequence),
})
})
.collect::<Result<Vec<Input>>>()?;

// Comput tx outputs
let mut unique_addresses = HashSet::new();
let mut tx_outputs: Vec<zebra_chain::transparent::Output> = Vec::new();

for (address, amount) in addresses {
if !unique_addresses.insert(address.clone()) {
return Err(Error::invalid_params("invalid parameter, invalid duplicate address"));
}

let address = address.parse().map_server_error()?;

let lock_script = match address {
Address::PayToScriptHash { network_kind: _, script_hash } => {
let mut script_bytes = vec![];
script_bytes.push(OpCode::Hash160 as u8);
script_bytes.extend_from_slice(&script_hash);
script_bytes.push(OpCode::Equal as u8);
Script::new(&script_bytes)
}
Address::PayToPublicKeyHash { network_kind: _, pub_key_hash } => {
let mut script_bytes = vec![];
script_bytes.push(OpCode::Dup as u8);
script_bytes.push(OpCode::Hash160 as u8);
script_bytes.extend_from_slice(&pub_key_hash);
script_bytes.push(OpCode::EqualVerify as u8);
script_bytes.push(OpCode::CheckSig as u8);
Script::new(&script_bytes)
}
};

let zatoshi_amount = (amount * COIN as f64) as i64;
let value = Amount::<NonNegative>::try_from(zatoshi_amount)
.map_err(|e| Error::invalid_params(format!("invalid amount: {}", e)))?;

let tx_out = zebra_chain::transparent::Output {
value,
lock_script
};
tx_outputs.push(tx_out);
}

let tx = match current_upgrade {
NetworkUpgrade::Genesis | NetworkUpgrade::BeforeOverwinter => Transaction::V1 {
inputs: tx_inputs,
outputs: tx_outputs,
lock_time,
},
NetworkUpgrade::Overwinter => Transaction::V3 {
inputs: tx_inputs,
outputs: tx_outputs,
lock_time,
expiry_height: block::Height(expiry_height_as_u32 + 20),
joinsplit_data: None,
},
NetworkUpgrade::Sapling => Transaction::V4 {
inputs: tx_inputs,
outputs: tx_outputs,
lock_time,
expiry_height: block::Height(expiry_height_as_u32 + 20),
joinsplit_data: None,
sapling_shielded_data: None,
},
NetworkUpgrade::Blossom | NetworkUpgrade::Heartwood | NetworkUpgrade::Canopy => {
Transaction::V4 {
inputs: tx_inputs,
outputs: tx_outputs,
lock_time,
expiry_height: block::Height(expiry_height_as_u32 + 40),
joinsplit_data: None,
sapling_shielded_data: None,
}
}
NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 => Transaction::V5 {
network_upgrade: current_upgrade,
lock_time,
expiry_height: block::Height(expiry_height_as_u32 + 40),
inputs: tx_inputs,
outputs: tx_outputs,
sapling_shielded_data: None,
orchard_shielded_data: None,
},
};

Ok(hex::encode(tx.zcash_serialize_to_vec().map_server_error()?))
}
.boxed()
}

// TODO: use HexData or SentTransactionHash to handle the transaction ID
fn get_raw_transaction(
&self,
Expand Down Expand Up @@ -1703,6 +1911,15 @@
}
}

/// A struct used for the `transactions` parameter for
/// [`Rpc::create_raw_transaction` method].
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct TxInput {
txid: String,
vout: u32,
sequence: Option<u32>,
}

/// Response to a `getrawtransaction` RPC request.
///
/// See the notes for the [`Rpc::get_raw_transaction` method].
Expand Down
Loading