diff --git a/zebra-chain/src/transparent.rs b/zebra-chain/src/transparent.rs index 5bb5a0e0..4834afb5 100644 --- a/zebra-chain/src/transparent.rs +++ b/zebra-chain/src/transparent.rs @@ -2,6 +2,7 @@ mod address; mod keys; +mod opcodes; mod script; mod serialize; mod utxo; diff --git a/zebra-chain/src/transparent/address.rs b/zebra-chain/src/transparent/address.rs index c21af557..01b78320 100644 --- a/zebra-chain/src/transparent/address.rs +++ b/zebra-chain/src/transparent/address.rs @@ -10,7 +10,7 @@ use sha2::Sha256; use crate::{ parameters::Network, serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}, - transparent::Script, + transparent::{opcodes::OpCode, Script}, }; #[cfg(test)] @@ -254,6 +254,33 @@ impl Address { payload[..].copy_from_slice(&ripe_hash[..]); payload } + + /// Given a transparent address (P2SH or a P2PKH), create a script that can be used in a coinbase + /// transaction output. + pub fn create_script_from_address(&self) -> Script { + let mut script_bytes = Vec::new(); + + match self { + // https://developer.bitcoin.org/devguide/transactions.html#pay-to-script-hash-p2sh + Address::PayToScriptHash { .. } => { + script_bytes.push(OpCode::Hash160 as u8); + script_bytes.push(OpCode::Push20Bytes as u8); + script_bytes.extend(self.hash_bytes()); + script_bytes.push(OpCode::Equal as u8); + } + // https://developer.bitcoin.org/devguide/transactions.html#pay-to-public-key-hash-p2pkh + Address::PayToPublicKeyHash { .. } => { + script_bytes.push(OpCode::Dup as u8); + script_bytes.push(OpCode::Hash160 as u8); + script_bytes.push(OpCode::Push20Bytes as u8); + script_bytes.extend(self.hash_bytes()); + script_bytes.push(OpCode::EqualVerify as u8); + script_bytes.push(OpCode::CheckSig as u8); + } + }; + + Script::new(&script_bytes) + } } #[cfg(test)] diff --git a/zebra-chain/src/transparent/opcodes.rs b/zebra-chain/src/transparent/opcodes.rs new file mode 100644 index 00000000..aa6366e2 --- /dev/null +++ b/zebra-chain/src/transparent/opcodes.rs @@ -0,0 +1,15 @@ +//! Zebra script opcodes. + +/// Supported opcodes +/// +/// +pub enum OpCode { + // Opcodes used to generate P2SH scripts. + Equal = 0x87, + Hash160 = 0xa9, + Push20Bytes = 0x14, + // Additional opcodes used to generate P2PKH scripts. + Dup = 0x76, + EqualVerify = 0x88, + CheckSig = 0xac, +} diff --git a/zebra-consensus/src/block/subsidy/funding_streams.rs b/zebra-consensus/src/block/subsidy/funding_streams.rs index 867f8908..8ce85bcc 100644 --- a/zebra-consensus/src/block/subsidy/funding_streams.rs +++ b/zebra-consensus/src/block/subsidy/funding_streams.rs @@ -6,7 +6,6 @@ use zebra_chain::{ amount::{Amount, Error, NonNegative}, block::Height, parameters::{Network, NetworkUpgrade::*}, - serialization::ZcashSerialize, transaction::Transaction, transparent::{Address, Output, Script}, }; @@ -141,25 +140,14 @@ pub fn new_coinbase_script(address: Address) -> Script { assert!( address.is_script_hash(), "incorrect coinbase script address: {address} \ - Zebra only supports transparent 'pay to script hash' (P2SH) addresses", + Funding streams only support transparent 'pay to script hash' (P2SH) addresses", ); - let address_hash = address - .zcash_serialize_to_vec() - .expect("we should get address bytes here"); - // > The “prescribed way” to pay a transparent P2SH address is to use a standard P2SH script // > of the form OP_HASH160 fs.RedeemScriptHash(height) OP_EQUAL as the scriptPubKey. // // [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams - let mut script_bytes = Vec::new(); - - script_bytes.push(OpCode::Hash160 as u8); - script_bytes.push(OpCode::Push20Bytes as u8); - script_bytes.extend(&address_hash[2..22]); - script_bytes.push(OpCode::Equal as u8); - - Script::new(&script_bytes) + address.create_script_from_address() } /// Returns a list of outputs in `Transaction`, which have a script address equal to `Address`. @@ -171,10 +159,3 @@ pub fn filter_outputs_by_address(transaction: &Transaction, address: Address) -> .cloned() .collect() } - -/// Script opcodes needed to compare the `lock_script` with the funding stream reward address. -pub enum OpCode { - Equal = 0x87, - Hash160 = 0xa9, - Push20Bytes = 0x14, -} diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index e15677e1..d5f672f7 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -21,10 +21,12 @@ use zebra_chain::{ transaction::{Transaction, UnminedTx, VerifiedUnminedTx}, transparent, }; + use zebra_consensus::{ - funding_stream_address, funding_stream_values, miner_subsidy, new_coinbase_script, - VerifyChainError, MAX_BLOCK_SIGOPS, + funding_stream_address, funding_stream_values, miner_subsidy, VerifyChainError, + MAX_BLOCK_SIGOPS, }; + use zebra_node_services::mempool; use zebra_state::{ReadRequest, ReadResponse}; @@ -659,7 +661,7 @@ pub fn standard_coinbase_outputs( coinbase_outputs .iter() - .map(|(amount, address)| (*amount, new_coinbase_script(*address))) + .map(|(amount, address)| (*amount, address.create_script_from_address())) .collect() } diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/config.rs b/zebra-rpc/src/methods/get_block_template_rpcs/config.rs index 91fb7a35..9e169c11 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/config.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/config.rs @@ -9,7 +9,7 @@ use zebra_chain::transparent; #[serde(deny_unknown_fields, default)] pub struct Config { /// The address used for miner payouts. - /// Zebra currently only supports single-signature P2SH transparent addresses. + /// Zebra currently only supports P2SH and P2PKH transparent addresses. /// /// Zebra sends mining fees and miner rewards to this address in the /// `getblocktemplate` RPC coinbase transaction. diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index df5c6ef2..d0e95dfd 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -877,12 +877,20 @@ async fn rpc_getnetworksolps() { #[cfg(feature = "getblocktemplate-rpcs")] #[tokio::test(flavor = "multi_thread")] async fn rpc_getblocktemplate() { + // test getblocktemplate with a miner P2SH address + rpc_getblocktemplate_mining_address(true).await; + // test getblocktemplate with a miner P2PKH address + rpc_getblocktemplate_mining_address(false).await; +} + +#[cfg(feature = "getblocktemplate-rpcs")] +async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) { use std::panic; - use chrono::{TimeZone, Utc}; + use chrono::TimeZone; use zebra_chain::{ - amount::{Amount, NonNegative}, + amount::NonNegative, block::{Hash, MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION}, chain_tip::mock::MockChainTip, work::difficulty::{CompactDifficulty, ExpandedDifficulty, U256}, @@ -911,10 +919,13 @@ async fn rpc_getblocktemplate() { let mut mock_sync_status = MockSyncStatus::default(); mock_sync_status.set_is_close_to_tip(true); - let mining_config = get_block_template_rpcs::config::Config { - miner_address: Some(transparent::Address::from_script_hash(Mainnet, [0x7e; 20])), + let miner_address = match use_p2pkh { + false => Some(transparent::Address::from_script_hash(Mainnet, [0x7e; 20])), + true => Some(transparent::Address::from_pub_key_hash(Mainnet, [0x7e; 20])), }; + let mining_config = get_block_template_rpcs::config::Config { miner_address }; + // nu5 block height let fake_tip_height = NetworkUpgrade::Nu5.activation_height(Mainnet).unwrap(); // nu5 block hash @@ -1007,8 +1018,13 @@ async fn rpc_getblocktemplate() { assert!(get_block_template.coinbase_txn.required); assert!(!get_block_template.coinbase_txn.data.as_ref().is_empty()); assert_eq!(get_block_template.coinbase_txn.depends.len(), 0); - // TODO: should a coinbase transaction have sigops? - assert_eq!(get_block_template.coinbase_txn.sigops, 0); + if use_p2pkh { + // there is one sig operation if miner address is p2pkh. + assert_eq!(get_block_template.coinbase_txn.sigops, 1); + } else { + // everything in the coinbase is p2sh. + assert_eq!(get_block_template.coinbase_txn.sigops, 0); + } // Coinbase transaction checks for empty blocks. assert_eq!( get_block_template.coinbase_txn.fee, diff --git a/zebra-rpc/src/server.rs b/zebra-rpc/src/server.rs index 23835215..bfa373e6 100644 --- a/zebra-rpc/src/server.rs +++ b/zebra-rpc/src/server.rs @@ -133,11 +133,6 @@ impl RpcServer { network.network {network} and miner address network {} must match", miner_address.network(), ); - assert!( - miner_address.is_script_hash(), - "incorrect miner address config: {miner_address} \ - Zebra only supports transparent 'pay to script hash' (P2SH) addresses", - ); } // Initialize the getblocktemplate rpc method handler