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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put a link to zcashd where this constant is defined and used ? I want to make sure they are using the same number.


/// 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)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just add 1 line docs here instead?

pub enum OpCode {
// Opcodes used to generate P2SH scripts.
Equal = 0x87,
Expand Down
214 changes: 210 additions & 4 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,14 +25,16 @@ use tracing::Instrument;

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, Output, Script},
};

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

Expand Down Expand Up @@ -235,6 +242,35 @@ pub trait Rpc {
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// zcashd reference: [`z_create_raw_transaction`](https://zcash.github.io/rpc/createrawtransaction.html)
/// zcashd reference: [`createrawtransaction`](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
Comment on lines +255 to +256
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check how other addresses fields are documented for Zebra, for example https://github.com/ZcashFoundation/zebra/blob/main/zebra-rpc/src/methods.rs#L100-L101

/// - `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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// # Notes
/// # 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 +1041,167 @@ where
.boxed()
}

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

async move {
let tip_height = best_chain_tip_height(&latest_chain_tip).map_server_error()?;

let lock_time = if let Some(lock_time) = locktime {
LockTime::Height(block::Height(lock_time))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you intend to get the locktime of the tip here instead ?

} 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, Height(next_block_height));

let expiry_height_as_u32 = if let Some(expiry_height) = expiryheight {
if next_network_upgrade < NetworkUpgrade::Overwinter {
return Err(Error::invalid_params(
"invalid parameter, expiryheight can only be used if Overwinter is active when the transaction is mined"
)).map_server_error();
}

if block::Height(expiry_height) >= block::Height::MAX_EXPIRY_HEIGHT {
return Err(Error::invalid_params(
format!("Invalid parameter, expiryheight must be nonnegative and less than {:?}.",
block::Height::MAX_EXPIRY_HEIGHT)
));
}

// DoS mitigation: reject transactions expiring soon (as is done in zcashd)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a reference link

if expiry_height != 0 && next_block_height + block::Height::BLOCK_EXPIRY_HEIGHT_THRESHOLD > expiry_height {
return Err(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)
));
}

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

// Handle 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>>>()?;

// Handle tx outputs
let mut tx_outputs: Vec<Output> = Vec::new();

// Check if addresses contained in params are valid
let address_strings = AddressStrings { addresses: addresses.clone().keys().cloned().collect() };
let _result = address_strings.valid_addresses().map_err(|e| e.to_string()).map_server_error()?;

for (address, amount) in addresses {
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 = Output {
value,
lock_script
};
tx_outputs.push(tx_out);
}

let tx = match current_upgrade {
NetworkUpgrade::Genesis | NetworkUpgrade::BeforeOverwinter => Transaction::V1 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unsure if we need to support creating transactions that are below V4.

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 +1900,15 @@ impl Default for GetBlockHash {
}
}

/// A struct used for the `transactions` parameter for
/// [`Rpc::create_raw_transaction` method].
#[derive(Clone, 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
Loading