-
Notifications
You must be signed in to change notification settings - Fork 106
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,8 @@ | |
/// Supported opcodes | ||
/// | ||
/// <https://github.com/zcash/zcash/blob/8b16094f6672d8268ff25b2d7bddd6a6207873f7/src/script/script.h#L39> | ||
|
||
#[allow(missing_docs)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -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}; | ||||||||
|
@@ -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}; | ||||||||
|
||||||||
|
@@ -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) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
/// 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
/// 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) | ||||||||
|
@@ -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)) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||||||||
|
@@ -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]. | ||||||||
|
There was a problem hiding this comment.
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.