2. change(rpc): Add some transaction fields to the `getblocktemplate` RPC (#5496)

* Add documentation for the getblocktemplate RPC

* Add a new mempool::Request::Transactions

* Add conversions from Vec<UnminedTx> to merkle::Root and AuthDataRoot

* Fill in the merkle root and auth data root fields

* Delete the Coinbase type, it's the same as Transaction

* Fill in some other existing types

* Add Hex serialization support to some zebra-chain types

* Add TransactionTemplate fields and fill some in

* Fix test hangs by spawning async tasks

* Add temporary workaround for no transactions in the block

* Encode hashes and roots as hex

* Update RPC snapshots

* Add a missing Request::Transactions handler

* Fix doc warnings

* Fix fee serialization

* Update snapshots for serialization changes

* Add a missing Cargo.lock change

* Change depends type

* Remove duplicate feature entry

* Document the new RPC feature

* Fix a comment typo

Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>

* Update default roots docs

* Fix comment typo

* Fix a comment typo

Co-authored-by: Arya <aryasolhi@gmail.com>

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>
Co-authored-by: Arya <aryasolhi@gmail.com>
This commit is contained in:
teor 2022-11-03 13:25:01 +10:00 committed by GitHub
parent 26d0455d02
commit 142411508b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 770 additions and 146 deletions

View File

@ -7,7 +7,6 @@
use std::{
cmp::Ordering,
convert::{TryFrom, TryInto},
hash::{Hash, Hasher},
marker::PhantomData,
ops::RangeInclusive,
@ -28,7 +27,8 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
/// A runtime validated type for representing amounts of zatoshis
#[derive(Clone, Copy, Serialize, Deserialize)]
#[serde(try_from = "i64")]
#[serde(bound = "C: Constraint")]
#[serde(into = "i64")]
#[serde(bound = "C: Constraint + Clone")]
pub struct Amount<C = NegativeAllowed>(
/// The inner amount value.
i64,
@ -498,6 +498,26 @@ impl Constraint for NonNegative {
}
}
/// Marker type for `Amount` that requires negative or zero values.
///
/// Used for coinbase transactions in `getblocktemplate` RPCs.
///
/// ```
/// # use zebra_chain::amount::{Constraint, MAX_MONEY, NegativeOrZero};
/// assert_eq!(
/// NegativeOrZero::valid_range(),
/// -MAX_MONEY..=0,
/// );
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct NegativeOrZero;
impl Constraint for NegativeOrZero {
fn valid_range() -> RangeInclusive<i64> {
-MAX_MONEY..=0
}
}
/// Number of zatoshis in 1 ZEC
pub const COIN: i64 = 100_000_000;

View File

@ -1,5 +1,6 @@
//! The Commitment enum, used for the corresponding block header field.
use hex::{FromHex, ToHex};
use thiserror::Error;
use crate::{
@ -159,6 +160,62 @@ impl From<ChainHistoryMmrRootHash> for [u8; 32] {
}
}
impl ChainHistoryMmrRootHash {
/// Return the hash bytes in big-endian byte-order suitable for printing out byte by byte.
///
/// Zebra displays transaction and block hashes in big-endian byte-order,
/// following the u256 convention set by Bitcoin and zcashd.
pub fn bytes_in_display_order(&self) -> [u8; 32] {
let mut reversed_bytes = self.0;
reversed_bytes.reverse();
reversed_bytes
}
/// Convert bytes in big-endian byte-order into a `ChainHistoryMmrRootHash`.
///
/// Zebra displays transaction and block hashes in big-endian byte-order,
/// following the u256 convention set by Bitcoin and zcashd.
pub fn from_bytes_in_display_order(
bytes_in_display_order: &[u8; 32],
) -> ChainHistoryMmrRootHash {
let mut internal_byte_order = *bytes_in_display_order;
internal_byte_order.reverse();
ChainHistoryMmrRootHash(internal_byte_order)
}
}
impl ToHex for &ChainHistoryMmrRootHash {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex_upper()
}
}
impl ToHex for ChainHistoryMmrRootHash {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex_upper()
}
}
impl FromHex for ChainHistoryMmrRootHash {
type Error = <[u8; 32] as FromHex>::Error;
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
let mut hash = <[u8; 32]>::from_hex(hex)?;
hash.reverse();
Ok(hash.into())
}
}
/// A block commitment to chain history and transaction auth.
/// - the chain history tree for all ancestors in the current network upgrade,
/// and
@ -212,6 +269,60 @@ impl ChainHistoryBlockTxAuthCommitmentHash {
.expect("32 byte array");
Self(hash_block_commitments)
}
/// Return the hash bytes in big-endian byte-order suitable for printing out byte by byte.
///
/// Zebra displays transaction and block hashes in big-endian byte-order,
/// following the u256 convention set by Bitcoin and zcashd.
pub fn bytes_in_display_order(&self) -> [u8; 32] {
let mut reversed_bytes = self.0;
reversed_bytes.reverse();
reversed_bytes
}
/// Convert bytes in big-endian byte-order into a `ChainHistoryBlockTxAuthCommitmentHash`.
///
/// Zebra displays transaction and block hashes in big-endian byte-order,
/// following the u256 convention set by Bitcoin and zcashd.
pub fn from_bytes_in_display_order(
bytes_in_display_order: &[u8; 32],
) -> ChainHistoryBlockTxAuthCommitmentHash {
let mut internal_byte_order = *bytes_in_display_order;
internal_byte_order.reverse();
ChainHistoryBlockTxAuthCommitmentHash(internal_byte_order)
}
}
impl ToHex for &ChainHistoryBlockTxAuthCommitmentHash {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex_upper()
}
}
impl ToHex for ChainHistoryBlockTxAuthCommitmentHash {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex_upper()
}
}
impl FromHex for ChainHistoryBlockTxAuthCommitmentHash {
type Error = <[u8; 32] as FromHex>::Error;
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
let mut hash = <[u8; 32]>::from_hex(hex)?;
hash.reverse();
Ok(hash.into())
}
}
/// Errors that can occur when checking RootHash consensus rules.

View File

@ -1,15 +1,17 @@
//! The Bitcoin-inherited Merkle tree of transactions.
#![allow(clippy::unit_arg)]
use std::iter;
use std::{fmt, io::Write};
use std::{fmt, io::Write, iter};
use hex::{FromHex, ToHex};
use crate::{
serialization::sha256d,
transaction::{self, Transaction, UnminedTx, UnminedTxId},
};
#[cfg(any(any(test, feature = "proptest-impl"), feature = "proptest-impl"))]
use proptest_derive::Arbitrary;
use crate::serialization::sha256d;
use crate::transaction::{self, Transaction};
/// The root of the Bitcoin-inherited transaction Merkle tree, binding the
/// block header to the transactions in the block.
///
@ -77,6 +79,72 @@ impl fmt::Debug for Root {
}
}
impl From<[u8; 32]> for Root {
fn from(hash: [u8; 32]) -> Self {
Root(hash)
}
}
impl From<Root> for [u8; 32] {
fn from(hash: Root) -> Self {
hash.0
}
}
impl Root {
/// Return the hash bytes in big-endian byte-order suitable for printing out byte by byte.
///
/// Zebra displays transaction and block hashes in big-endian byte-order,
/// following the u256 convention set by Bitcoin and zcashd.
pub fn bytes_in_display_order(&self) -> [u8; 32] {
let mut reversed_bytes = self.0;
reversed_bytes.reverse();
reversed_bytes
}
/// Convert bytes in big-endian byte-order into a [`merkle::Root`](crate::block::merkle::Root).
///
/// Zebra displays transaction and block hashes in big-endian byte-order,
/// following the u256 convention set by Bitcoin and zcashd.
pub fn from_bytes_in_display_order(bytes_in_display_order: &[u8; 32]) -> Root {
let mut internal_byte_order = *bytes_in_display_order;
internal_byte_order.reverse();
Root(internal_byte_order)
}
}
impl ToHex for &Root {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex_upper()
}
}
impl ToHex for Root {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex_upper()
}
}
impl FromHex for Root {
type Error = <[u8; 32] as FromHex>::Error;
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
let mut hash = <[u8; 32]>::from_hex(hex)?;
hash.reverse();
Ok(hash.into())
}
}
fn hash(h1: &[u8; 32], h2: &[u8; 32]) -> [u8; 32] {
let mut w = sha256d::Writer::default();
w.write_all(h1).unwrap();
@ -115,7 +183,32 @@ where
}
}
impl std::iter::FromIterator<UnminedTx> for Root {
fn from_iter<I>(transactions: I) -> Self
where
I: IntoIterator<Item = UnminedTx>,
{
transactions
.into_iter()
.map(|tx| tx.id.mined_id())
.collect()
}
}
impl std::iter::FromIterator<UnminedTxId> for Root {
fn from_iter<I>(tx_ids: I) -> Self
where
I: IntoIterator<Item = UnminedTxId>,
{
tx_ids.into_iter().map(|tx_id| tx_id.mined_id()).collect()
}
}
impl std::iter::FromIterator<transaction::Hash> for Root {
/// # Panics
///
/// When there are no transactions in the iterator.
/// This is impossible, because every block must have a coinbase transaction.
fn from_iter<I>(hashes: I) -> Self
where
I: IntoIterator<Item = transaction::Hash>,
@ -166,6 +259,71 @@ impl From<AuthDataRoot> for [u8; 32] {
}
}
impl AuthDataRoot {
/// Return the hash bytes in big-endian byte-order suitable for printing out byte by byte.
///
/// Zebra displays transaction and block hashes in big-endian byte-order,
/// following the u256 convention set by Bitcoin and zcashd.
pub fn bytes_in_display_order(&self) -> [u8; 32] {
let mut reversed_bytes = self.0;
reversed_bytes.reverse();
reversed_bytes
}
/// Convert bytes in big-endian byte-order into a [`merkle::AuthDataRoot`](crate::block::merkle::AuthDataRoot).
///
/// Zebra displays transaction and block hashes in big-endian byte-order,
/// following the u256 convention set by Bitcoin and zcashd.
pub fn from_bytes_in_display_order(bytes_in_display_order: &[u8; 32]) -> AuthDataRoot {
let mut internal_byte_order = *bytes_in_display_order;
internal_byte_order.reverse();
AuthDataRoot(internal_byte_order)
}
}
impl ToHex for &AuthDataRoot {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex_upper()
}
}
impl ToHex for AuthDataRoot {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex_upper()
}
}
impl FromHex for AuthDataRoot {
type Error = <[u8; 32] as FromHex>::Error;
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
let mut hash = <[u8; 32]>::from_hex(hex)?;
hash.reverse();
Ok(hash.into())
}
}
/// The placeholder used for the [`AuthDigest`](transaction::AuthDigest) of pre-v5 transactions.
///
/// # Consensus
///
/// > For transaction versions before v5, a placeholder value consisting
/// > of 32 bytes of 0xFF is used in place of the authorizing data commitment.
/// > This is only used in the tree committed to by hashAuthDataRoot.
///
/// <https://zips.z.cash/zip-0244#authorizing-data-commitment>
pub const AUTH_DIGEST_PLACEHOLDER: transaction::AuthDigest = transaction::AuthDigest([0xFF; 32]);
impl<T> std::iter::FromIterator<T> for AuthDataRoot
where
T: std::convert::AsRef<Transaction>,
@ -174,17 +332,33 @@ where
where
I: IntoIterator<Item = T>,
{
// > For transaction versions before v5, a placeholder value consisting
// > of 32 bytes of 0xFF is used in place of the authorizing data commitment.
// > This is only used in the tree committed to by hashAuthDataRoot.
// https://zips.z.cash/zip-0244#authorizing-data-commitment
transactions
.into_iter()
.map(|tx| {
tx.as_ref()
.auth_digest()
.unwrap_or(transaction::AuthDigest([0xFF; 32]))
})
.map(|tx| tx.as_ref().auth_digest().unwrap_or(AUTH_DIGEST_PLACEHOLDER))
.collect()
}
}
impl std::iter::FromIterator<UnminedTx> for AuthDataRoot {
fn from_iter<I>(transactions: I) -> Self
where
I: IntoIterator<Item = UnminedTx>,
{
transactions
.into_iter()
.map(|tx| tx.id.auth_digest().unwrap_or(AUTH_DIGEST_PLACEHOLDER))
.collect()
}
}
impl std::iter::FromIterator<UnminedTxId> for AuthDataRoot {
fn from_iter<I>(tx_ids: I) -> Self
where
I: IntoIterator<Item = UnminedTxId>,
{
tx_ids
.into_iter()
.map(|tx_id| tx_id.auth_digest().unwrap_or(AUTH_DIGEST_PLACEHOLDER))
.collect()
}
}

View File

@ -2,8 +2,7 @@
use std::{fmt, sync::Arc};
#[cfg(any(test, feature = "proptest-impl"))]
use proptest_derive::Arbitrary;
use hex::{FromHex, ToHex};
use crate::{
primitives::zcash_primitives::auth_digest,
@ -14,6 +13,9 @@ use crate::{
use super::Transaction;
#[cfg(any(test, feature = "proptest-impl"))]
use proptest_derive::Arbitrary;
/// An authorizing data commitment hash as specified in [ZIP-244].
///
/// Note: Zebra displays transaction and block hashes in big-endian byte-order,
@ -24,6 +26,29 @@ use super::Transaction;
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
pub struct AuthDigest(pub [u8; 32]);
impl AuthDigest {
/// Return the hash bytes in big-endian byte-order suitable for printing out byte by byte.
///
/// Zebra displays transaction and block hashes in big-endian byte-order,
/// following the u256 convention set by Bitcoin and zcashd.
pub fn bytes_in_display_order(&self) -> [u8; 32] {
let mut reversed_bytes = self.0;
reversed_bytes.reverse();
reversed_bytes
}
/// Convert bytes in big-endian byte-order into a [`transaction::AuthDigest`](crate::transaction::AuthDigest).
///
/// Zebra displays transaction and block hashes in big-endian byte-order,
/// following the u256 convention set by Bitcoin and zcashd.
pub fn from_bytes_in_display_order(bytes_in_display_order: &[u8; 32]) -> AuthDigest {
let mut internal_byte_order = *bytes_in_display_order;
internal_byte_order.reverse();
AuthDigest(internal_byte_order)
}
}
impl From<Transaction> for AuthDigest {
/// Computes the authorizing data commitment for a transaction.
///
@ -76,20 +101,47 @@ impl From<&AuthDigest> for [u8; 32] {
}
}
impl ToHex for &AuthDigest {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex_upper()
}
}
impl ToHex for AuthDigest {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex_upper()
}
}
impl FromHex for AuthDigest {
type Error = <[u8; 32] as FromHex>::Error;
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
let mut hash = <[u8; 32]>::from_hex(hex)?;
hash.reverse();
Ok(hash.into())
}
}
impl fmt::Display for AuthDigest {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut reversed_bytes = self.0;
reversed_bytes.reverse();
f.write_str(&hex::encode(reversed_bytes))
f.write_str(&self.encode_hex::<String>())
}
}
impl fmt::Debug for AuthDigest {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut reversed_bytes = self.0;
reversed_bytes.reverse();
f.debug_tuple("AuthDigest")
.field(&hex::encode(reversed_bytes))
.field(&self.encode_hex::<String>())
.finish()
}
}

View File

@ -5,6 +5,7 @@ use std::{borrow::Borrow, convert::TryInto, io, sync::Arc};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use halo2::pasta::{group::ff::PrimeField, pallas};
use hex::FromHex;
use crate::{
amount,
@ -1018,3 +1019,13 @@ impl From<Vec<u8>> for SerializedTransaction {
Self { bytes }
}
}
impl FromHex for SerializedTransaction {
type Error = <Vec<u8> as FromHex>::Error;
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
let bytes = <Vec<u8>>::from_hex(hex)?;
Ok(bytes.into())
}
}

View File

@ -193,12 +193,12 @@ impl UnminedTxId {
/// (But it might still need semantic or contextual verification.)
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UnminedTx {
/// A unique identifier for this unmined transaction.
pub id: UnminedTxId,
/// The unmined transaction itself.
pub transaction: Arc<Transaction>,
/// A unique identifier for this unmined transaction.
pub id: UnminedTxId,
/// The size in bytes of the serialized transaction data
pub size: usize,
}

View File

@ -6,5 +6,9 @@ version = "1.0.0-beta.17"
edition = "2021"
repository = "https://github.com/ZcashFoundation/zebra"
[features]
default = []
getblocktemplate-rpcs = []
[dependencies]
zebra-chain = { path = "../zebra-chain" }

View File

@ -9,6 +9,7 @@ use zebra_chain::transaction::{Hash, UnminedTx, UnminedTxId};
use crate::BoxError;
mod gossip;
pub use self::gossip::Gossip;
/// A mempool service request.
@ -20,7 +21,6 @@ pub use self::gossip::Gossip;
/// Requests can't modify the mempool directly,
/// because all mempool transactions must be verified.
#[derive(Debug, Eq, PartialEq)]
#[allow(dead_code)]
pub enum Request {
/// Query all transaction IDs in the mempool.
TransactionIds,
@ -35,6 +35,12 @@ pub enum Request {
/// the [`AuthDigest`](zebra_chain::transaction::AuthDigest).
TransactionsByMinedId(HashSet<Hash>),
/// Get all the transactions in the mempool.
///
/// Equivalent to `TransactionsById(TransactionIds)`.
#[cfg(feature = "getblocktemplate-rpcs")]
Transactions,
/// Query matching cached rejected transaction IDs in the mempool,
/// using a unique set of [`UnminedTxId`]s.
RejectedTransactionIds(HashSet<UnminedTxId>),

View File

@ -9,8 +9,10 @@ edition = "2021"
[features]
default = []
getblocktemplate-rpcs = ["zebra-state/getblocktemplate-rpcs", "zebra-node-services/getblocktemplate-rpcs"]
# Test-only features
proptest-impl = ["proptest", "proptest-derive", "zebra-chain/proptest-impl", "zebra-state/proptest-impl"]
getblocktemplate-rpcs = ["zebra-state/getblocktemplate-rpcs"]
[dependencies]
chrono = { version = "0.4.22", default-features = false, features = ["clock", "std"] }

View File

@ -5,12 +5,13 @@ use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result};
use jsonrpc_derive::rpc;
use tower::{buffer::Buffer, Service, ServiceExt};
use zebra_chain::{block::Height, chain_tip::ChainTip};
use zebra_chain::{amount::Amount, block::Height, chain_tip::ChainTip};
use zebra_node_services::mempool;
use crate::methods::{
get_block_template_rpcs::types::{
coinbase::Coinbase, default_roots::DefaultRoots, get_block_template::GetBlockTemplate,
default_roots::DefaultRoots, get_block_template::GetBlockTemplate,
transaction::TransactionTemplate,
},
GetBlockHash, MISSING_BLOCK_ERROR_CODE,
};
@ -49,13 +50,28 @@ pub trait GetBlockTemplateRpc {
#[rpc(name = "getblockhash")]
fn get_block_hash(&self, index: i32) -> BoxFuture<Result<GetBlockHash>>;
/// Documentation to be filled as we go.
/// Returns a block template for mining new Zcash blocks.
///
/// # Parameters
///
/// - `jsonrequestobject`: (string, optional) A JSON object containing arguments.
///
/// zcashd reference: [`getblocktemplate`](https://zcash-rpc.github.io/getblocktemplate.html)
///
/// # Notes
///
/// - This rpc method is available only if zebra is built with `--features getblocktemplate-rpcs`.
/// Arguments to this RPC are currently ignored.
/// Long polling, block proposals, server lists, and work IDs are not supported.
///
/// Miners can make arbitrary changes to blocks, as long as:
/// - the data sent to `submitblock` is a valid Zcash block, and
/// - the parent block is a valid block that Zebra already has, or will receive soon.
///
/// Zebra verifies blocks in parallel, and keeps recent chains in parallel,
/// so moving between chains is very cheap. (But forking a new chain may take some time,
/// until bug #4794 is fixed.)
///
/// This rpc method is available only if zebra is built with `--features getblocktemplate-rpcs`.
#[rpc(name = "getblocktemplate")]
fn get_block_template(&self) -> BoxFuture<Result<GetBlockTemplate>>;
}
@ -84,7 +100,6 @@ where
// Services
//
/// A handle to the mempool service.
#[allow(dead_code)]
mempool: Buffer<Mempool, mempool::Request>,
/// A handle to the state service.
@ -194,33 +209,94 @@ where
}
fn get_block_template(&self) -> BoxFuture<Result<GetBlockTemplate>> {
async move {
let empty_string = String::from("");
let mempool = self.mempool.clone();
// Returns empty `GetBlockTemplate`
// Since this is a very large RPC, we use separate functions for each group of fields.
async move {
// TODO: put this in a separate get_mempool_transactions() function
let request = mempool::Request::Transactions;
let response = mempool.oneshot(request).await.map_err(|error| Error {
code: ErrorCode::ServerError(0),
message: error.to_string(),
data: None,
})?;
let transactions = if let mempool::Response::Transactions(transactions) = response {
// TODO: select transactions according to ZIP-317 (#5473)
transactions
} else {
unreachable!("unmatched response to a mempool::Transactions request");
};
let merkle_root;
let auth_data_root;
// TODO: add the coinbase transaction to these lists, and delete the is_empty() check
if !transactions.is_empty() {
merkle_root = transactions.iter().cloned().collect();
auth_data_root = transactions.iter().cloned().collect();
} else {
merkle_root = [0; 32].into();
auth_data_root = [0; 32].into();
}
let transactions = transactions.iter().map(Into::into).collect();
let empty_string = String::from("");
Ok(GetBlockTemplate {
capabilities: vec![],
version: 0,
previous_block_hash: empty_string.clone(),
block_commitments_hash: empty_string.clone(),
light_client_root_hash: empty_string.clone(),
final_sapling_root_hash: empty_string.clone(),
previous_block_hash: GetBlockHash([0; 32].into()),
block_commitments_hash: [0; 32].into(),
light_client_root_hash: [0; 32].into(),
final_sapling_root_hash: [0; 32].into(),
default_roots: DefaultRoots {
merkle_root: empty_string.clone(),
chain_history_root: empty_string.clone(),
auth_data_root: empty_string.clone(),
block_commitments_hash: empty_string.clone(),
merkle_root,
chain_history_root: [0; 32].into(),
auth_data_root,
block_commitments_hash: [0; 32].into(),
},
transactions: vec![],
coinbase_txn: Coinbase {},
transactions,
// TODO: move to a separate function in the transactions module
coinbase_txn: TransactionTemplate {
// TODO: generate coinbase transaction data
data: vec![].into(),
// TODO: calculate from transaction data
hash: [0; 32].into(),
auth_digest: [0; 32].into(),
// Always empty for coinbase transactions.
depends: Vec::new(),
// TODO: negative sum of transactions.*.fee
fee: Amount::zero(),
// TODO: sigops used by the generated transaction data
sigops: 0,
required: true,
},
target: empty_string.clone(),
min_time: 0,
mutable: vec![],
nonce_range: empty_string.clone(),
sigop_limit: 0,
size_limit: 0,
cur_time: 0,
bits: empty_string,
height: 0,
})
}

View File

@ -1,6 +1,5 @@
//! Types used in mining RPC methods.
pub(crate) mod coinbase;
pub(crate) mod default_roots;
pub(crate) mod get_block_template;
pub(crate) mod transaction;

View File

@ -1,5 +0,0 @@
//! The `Coinbase` type is part of the `getblocktemplate` RPC method output.
/// documentation and fields to be added in #5453.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Coinbase {}

View File

@ -1,18 +1,35 @@
//! The `DefaultRoots` type is part of the `getblocktemplate` RPC method output.
use zebra_chain::block::{
merkle::{self, AuthDataRoot},
ChainHistoryBlockTxAuthCommitmentHash, ChainHistoryMmrRootHash,
};
/// Documentation to be added in #5452 or #5455.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DefaultRoots {
/// Add documentation.
/// The merkle root of the transaction IDs in the block.
/// Used in the new block's header.
#[serde(rename = "merkleroot")]
pub merkle_root: String,
/// Add documentation.
#[serde(with = "hex")]
pub merkle_root: merkle::Root,
/// The root of the merkle mountain range of the chain history roots from the last network upgrade to the previous block.
/// Unlike the other roots, this not cover any data from this new block, only from previous blocks.
#[serde(rename = "chainhistoryroot")]
pub chain_history_root: String,
/// Add documentation.
#[serde(with = "hex")]
pub chain_history_root: ChainHistoryMmrRootHash,
/// The merkle root of the authorizing data hashes of the transactions in the new block.
#[serde(rename = "authdataroot")]
pub auth_data_root: String,
/// Add documentation.
#[serde(with = "hex")]
pub auth_data_root: AuthDataRoot,
/// The block commitment for the new block's header.
/// This hash covers `chain_history_root` and `auth_data_root`.
///
/// `merkle_root` has its own field in the block header.
#[serde(rename = "blockcommitmentshash")]
pub block_commitments_hash: String,
#[serde(with = "hex")]
pub block_commitments_hash: ChainHistoryBlockTxAuthCommitmentHash,
}

View File

@ -1,7 +1,12 @@
//! The `GetBlockTempate` type is the output of the `getblocktemplate` RPC method.
use crate::methods::get_block_template_rpcs::types::{
coinbase::Coinbase, default_roots::DefaultRoots, transaction::Transaction,
use zebra_chain::{amount, block::ChainHistoryBlockTxAuthCommitmentHash};
use crate::methods::{
get_block_template_rpcs::types::{
default_roots::DefaultRoots, transaction::TransactionTemplate,
},
GetBlockHash,
};
/// Documentation to be added after we document all the individual fields.
@ -9,49 +14,83 @@ use crate::methods::get_block_template_rpcs::types::{
pub struct GetBlockTemplate {
/// Add documentation.
pub capabilities: Vec<String>,
/// Add documentation.
pub version: usize,
/// The version of the block format.
/// Always 4 for new Zcash blocks.
//
// TODO: add a default block version constant to zebra-chain.
pub version: u32,
/// Add documentation.
#[serde(rename = "previousblockhash")]
pub previous_block_hash: String,
pub previous_block_hash: GetBlockHash,
/// Add documentation.
#[serde(rename = "blockcommitmentshash")]
pub block_commitments_hash: String,
#[serde(with = "hex")]
pub block_commitments_hash: ChainHistoryBlockTxAuthCommitmentHash,
/// Add documentation.
#[serde(rename = "lightclientroothash")]
pub light_client_root_hash: String,
#[serde(with = "hex")]
pub light_client_root_hash: ChainHistoryBlockTxAuthCommitmentHash,
/// Add documentation.
#[serde(rename = "finalsaplingroothash")]
pub final_sapling_root_hash: String,
#[serde(with = "hex")]
pub final_sapling_root_hash: ChainHistoryBlockTxAuthCommitmentHash,
/// Add documentation.
#[serde(rename = "defaultroots")]
pub default_roots: DefaultRoots,
/// Add documentation.
pub transactions: Vec<Transaction>,
/// Add documentation.
/// The non-coinbase transactions selected for this block template.
///
/// TODO: select these transactions using ZIP-317 (#5473)
pub transactions: Vec<TransactionTemplate<amount::NonNegative>>,
/// The coinbase transaction generated from `transactions` and `height`.
#[serde(rename = "coinbasetxn")]
pub coinbase_txn: Coinbase,
pub coinbase_txn: TransactionTemplate<amount::NegativeOrZero>,
/// Add documentation.
// TODO: use ExpandedDifficulty type.
pub target: String,
/// Add documentation.
#[serde(rename = "mintime")]
// TODO: use DateTime32 type?
pub min_time: u32,
/// Add documentation.
pub mutable: Vec<String>,
/// Add documentation.
#[serde(rename = "noncerange")]
pub nonce_range: String,
/// Add documentation.
///
/// The same as `MAX_BLOCK_SIGOPS`.
#[serde(rename = "sigoplimit")]
pub sigop_limit: u32,
pub sigop_limit: u64,
/// Add documentation.
///
/// The same as `MAX_BLOCK_BYTES`.
#[serde(rename = "sizelimit")]
pub size_limit: u32,
pub size_limit: u64,
/// Add documentation.
// TODO: use DateTime32 type?
#[serde(rename = "curtime")]
pub cur_time: u32,
/// Add documentation.
// TODO: use CompactDifficulty type.
pub bits: String,
/// Add documentation.
// TODO: use Height type?
pub height: u32,
}

View File

@ -1,5 +1,84 @@
//! The `Transaction` type is part of the `getblocktemplate` RPC method output.
//! The `TransactionTemplate` type is part of the `getblocktemplate` RPC method output.
/// Documentation and fields to be added in #5454.
use zebra_chain::{
amount::{self, Amount, NonNegative},
block::merkle::AUTH_DIGEST_PLACEHOLDER,
transaction::{self, SerializedTransaction, UnminedTx},
};
/// Transaction data and fields needed to generate blocks using the `getblocktemplate` RPC.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Transaction {}
#[serde(bound = "FeeConstraint: amount::Constraint + Clone")]
pub struct TransactionTemplate<FeeConstraint>
where
FeeConstraint: amount::Constraint + Clone,
{
/// The hex-encoded serialized data for this transaction.
#[serde(with = "hex")]
pub(crate) data: SerializedTransaction,
/// The transaction ID of this transaction.
#[serde(with = "hex")]
pub(crate) hash: transaction::Hash,
/// The authorizing data digest of a v5 transaction, or a placeholder for older versions.
#[serde(rename = "authdigest")]
#[serde(with = "hex")]
pub(crate) auth_digest: transaction::AuthDigest,
/// The transactions in this block template that this transaction depends upon.
/// These are 1-based indexes in the `transactions` list.
///
/// Zebra's mempool does not support transaction dependencies, so this list is always empty.
///
/// We use `u16` because 2 MB blocks are limited to around 39,000 transactions.
pub(crate) depends: Vec<u16>,
/// The fee for this transaction.
///
/// Non-coinbase transactions must be `NonNegative`.
/// The Coinbase transaction `fee` is the negative sum of the fees of the transactions in
/// the block, so their fee must be `NegativeOrZero`.
//
// TODO: add a fee field to mempool transactions, based on the verifier response.
pub(crate) fee: Amount<FeeConstraint>,
/// The number of transparent signature operations in this transaction.
//
// TODO: add a sigops field to mempool transactions, based on the verifier response.
pub(crate) sigops: u64,
/// Is this transaction required in the block?
///
/// Coinbase transactions are required, all other transactions are not.
pub(crate) required: bool,
}
// Convert from a mempool transaction to a transaction template.
impl From<&UnminedTx> for TransactionTemplate<NonNegative> {
fn from(tx: &UnminedTx) -> Self {
Self {
data: tx.transaction.as_ref().into(),
hash: tx.id.mined_id(),
auth_digest: tx.id.auth_digest().unwrap_or(AUTH_DIGEST_PLACEHOLDER),
// Always empty, not supported by Zebra's mempool.
depends: Vec::new(),
// TODO: add a fee field to mempool transactions, based on the verifier response.
fee: Amount::zero(),
// TODO: add a sigops field to mempool transactions, based on the verifier response.
sigops: 0,
// Zebra does not require any transactions except the coinbase transaction.
required: false,
}
}
}
impl From<UnminedTx> for TransactionTemplate<NonNegative> {
fn from(tx: UnminedTx) -> Self {
Self::from(&tx)
}
}

View File

@ -16,7 +16,7 @@ use zebra_test::mock_service::{MockService, PanicAssertion};
use crate::methods::{GetBlockHash, GetBlockTemplateRpc, GetBlockTemplateRpcImpl};
pub async fn test_responses<State>(
mempool: MockService<
mut mempool: MockService<
mempool::Request,
mempool::Response,
PanicAssertion,
@ -58,10 +58,18 @@ pub async fn test_responses<State>(
snapshot_rpc_getblockhash(get_block_hash, &settings);
// `getblocktemplate`
let get_block_template = get_block_template_rpc
.get_block_template()
let get_block_template = tokio::spawn(get_block_template_rpc.get_block_template());
mempool
.expect_request(mempool::Request::Transactions)
.await
.expect("We should have a GetBlockTemplate struct");
.respond(mempool::Response::Transactions(vec![]));
let get_block_template = get_block_template
.await
.expect("unexpected panic in getblocktemplate RPC task")
.expect("unexpected error in getblocktemplate RPC call");
snapshot_rpc_getblocktemplate(get_block_template, &settings);
}

View File

@ -5,18 +5,26 @@ expression: block_template
{
"capabilities": [],
"version": 0,
"previousblockhash": "",
"blockcommitmentshash": "",
"lightclientroothash": "",
"finalsaplingroothash": "",
"previousblockhash": "0000000000000000000000000000000000000000000000000000000000000000",
"blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000",
"lightclientroothash": "0000000000000000000000000000000000000000000000000000000000000000",
"finalsaplingroothash": "0000000000000000000000000000000000000000000000000000000000000000",
"defaultroots": {
"merkleroot": "",
"chainhistoryroot": "",
"authdataroot": "",
"blockcommitmentshash": ""
"merkleroot": "0000000000000000000000000000000000000000000000000000000000000000",
"chainhistoryroot": "0000000000000000000000000000000000000000000000000000000000000000",
"authdataroot": "0000000000000000000000000000000000000000000000000000000000000000",
"blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000"
},
"transactions": [],
"coinbasetxn": {},
"coinbasetxn": {
"data": "",
"hash": "0000000000000000000000000000000000000000000000000000000000000000",
"authdigest": "0000000000000000000000000000000000000000000000000000000000000000",
"depends": [],
"fee": 0,
"sigops": 0,
"required": true
},
"target": "",
"mintime": 0,
"mutable": [],

View File

@ -5,18 +5,26 @@ expression: block_template
{
"capabilities": [],
"version": 0,
"previousblockhash": "",
"blockcommitmentshash": "",
"lightclientroothash": "",
"finalsaplingroothash": "",
"previousblockhash": "0000000000000000000000000000000000000000000000000000000000000000",
"blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000",
"lightclientroothash": "0000000000000000000000000000000000000000000000000000000000000000",
"finalsaplingroothash": "0000000000000000000000000000000000000000000000000000000000000000",
"defaultroots": {
"merkleroot": "",
"chainhistoryroot": "",
"authdataroot": "",
"blockcommitmentshash": ""
"merkleroot": "0000000000000000000000000000000000000000000000000000000000000000",
"chainhistoryroot": "0000000000000000000000000000000000000000000000000000000000000000",
"authdataroot": "0000000000000000000000000000000000000000000000000000000000000000",
"blockcommitmentshash": "0000000000000000000000000000000000000000000000000000000000000000"
},
"transactions": [],
"coinbasetxn": {},
"coinbasetxn": {
"data": "",
"hash": "0000000000000000000000000000000000000000000000000000000000000000",
"authdigest": "0000000000000000000000000000000000000000000000000000000000000000",
"depends": [],
"fee": 0,
"sigops": 0,
"required": true
},
"target": "",
"mintime": 0,
"mutable": [],

View File

@ -755,32 +755,21 @@ async fn rpc_getblocktemplate() {
latest_chain_tip.clone(),
);
let get_block_template = get_block_template_rpc
.get_block_template()
let get_block_template = tokio::spawn(get_block_template_rpc.get_block_template());
mempool
.expect_request(mempool::Request::Transactions)
.await
.expect("We should have a GetBlockTemplate struct");
.respond(mempool::Response::Transactions(vec![]));
let get_block_template = get_block_template
.await
.expect("unexpected panic in getblocktemplate RPC task")
.expect("unexpected error in getblocktemplate RPC call");
assert!(get_block_template.capabilities.is_empty());
assert_eq!(get_block_template.version, 0);
assert!(get_block_template.previous_block_hash.is_empty());
assert!(get_block_template.block_commitments_hash.is_empty());
assert!(get_block_template.light_client_root_hash.is_empty());
assert!(get_block_template.final_sapling_root_hash.is_empty());
assert!(get_block_template.default_roots.merkle_root.is_empty());
assert!(get_block_template
.default_roots
.chain_history_root
.is_empty());
assert!(get_block_template.default_roots.auth_data_root.is_empty());
assert!(get_block_template
.default_roots
.block_commitments_hash
.is_empty());
assert!(get_block_template.transactions.is_empty());
assert_eq!(
get_block_template.coinbase_txn,
get_block_template_rpcs::types::coinbase::Coinbase {}
);
assert!(get_block_template.target.is_empty());
assert_eq!(get_block_template.min_time, 0);
assert!(get_block_template.mutable.is_empty());

View File

@ -4,7 +4,7 @@ expression: stored_address_balances
---
[
("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd", AddressBalanceLocation(
balance: Amount(12500),
balance: 12500,
location: OutputLocation(
transaction_location: TransactionLocation(
height: Height(1),

View File

@ -4,7 +4,7 @@ expression: stored_address_balances
---
[
("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd", AddressBalanceLocation(
balance: Amount(37500),
balance: 37500,
location: OutputLocation(
transaction_location: TransactionLocation(
height: Height(1),

View File

@ -4,7 +4,7 @@ expression: stored_address_balances
---
[
("t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi", AddressBalanceLocation(
balance: Amount(12500),
balance: 12500,
location: OutputLocation(
transaction_location: TransactionLocation(
height: Height(1),

View File

@ -4,7 +4,7 @@ expression: stored_address_balances
---
[
("t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi", AddressBalanceLocation(
balance: Amount(37500),
balance: 37500,
location: OutputLocation(
transaction_location: TransactionLocation(
height: Height(1),

View File

@ -5,7 +5,7 @@ expression: stored_address_utxos
[
("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd", [
Output(
value: Amount(12500),
value: 12500,
lock_script: Script("a9147d46a730d31f97b1930d3368a967c309bd4d136a87"),
),
]),

View File

@ -5,11 +5,11 @@ expression: stored_address_utxos
[
("t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd", [
Output(
value: Amount(12500),
value: 12500,
lock_script: Script("a9147d46a730d31f97b1930d3368a967c309bd4d136a87"),
),
Output(
value: Amount(25000),
value: 25000,
lock_script: Script("a9147d46a730d31f97b1930d3368a967c309bd4d136a87"),
),
]),

View File

@ -5,7 +5,7 @@ expression: stored_address_utxos
[
("t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi", [
Output(
value: Amount(12500),
value: 12500,
lock_script: Script("a914ef775f1f997f122a062fff1a2d7443abd1f9c64287"),
),
]),

View File

@ -5,11 +5,11 @@ expression: stored_address_utxos
[
("t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi", [
Output(
value: Amount(12500),
value: 12500,
lock_script: Script("a914ef775f1f997f122a062fff1a2d7443abd1f9c64287"),
),
Output(
value: Amount(25000),
value: 25000,
lock_script: Script("a914ef775f1f997f122a062fff1a2d7443abd1f9c64287"),
),
]),

View File

@ -18,7 +18,7 @@ expression: stored_utxos
output_index: OutputIndex(0),
), Some(Utxo(
output: Output(
value: Amount(50000),
value: 50000,
lock_script: Script("21027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875ac"),
),
height: Height(1),
@ -32,7 +32,7 @@ expression: stored_utxos
output_index: OutputIndex(1),
), Some(Utxo(
output: Output(
value: Amount(12500),
value: 12500,
lock_script: Script("a9147d46a730d31f97b1930d3368a967c309bd4d136a87"),
),
height: Height(1),

View File

@ -18,7 +18,7 @@ expression: stored_utxos
output_index: OutputIndex(0),
), Some(Utxo(
output: Output(
value: Amount(50000),
value: 50000,
lock_script: Script("21027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875ac"),
),
height: Height(1),
@ -32,7 +32,7 @@ expression: stored_utxos
output_index: OutputIndex(1),
), Some(Utxo(
output: Output(
value: Amount(12500),
value: 12500,
lock_script: Script("a9147d46a730d31f97b1930d3368a967c309bd4d136a87"),
),
height: Height(1),
@ -46,7 +46,7 @@ expression: stored_utxos
output_index: OutputIndex(0),
), Some(Utxo(
output: Output(
value: Amount(100000),
value: 100000,
lock_script: Script("21027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875ac"),
),
height: Height(2),
@ -60,7 +60,7 @@ expression: stored_utxos
output_index: OutputIndex(1),
), Some(Utxo(
output: Output(
value: Amount(25000),
value: 25000,
lock_script: Script("a9147d46a730d31f97b1930d3368a967c309bd4d136a87"),
),
height: Height(2),

View File

@ -18,7 +18,7 @@ expression: stored_utxos
output_index: OutputIndex(0),
), Some(Utxo(
output: Output(
value: Amount(50000),
value: 50000,
lock_script: Script("21025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99ac"),
),
height: Height(1),
@ -32,7 +32,7 @@ expression: stored_utxos
output_index: OutputIndex(1),
), Some(Utxo(
output: Output(
value: Amount(12500),
value: 12500,
lock_script: Script("a914ef775f1f997f122a062fff1a2d7443abd1f9c64287"),
),
height: Height(1),

View File

@ -18,7 +18,7 @@ expression: stored_utxos
output_index: OutputIndex(0),
), Some(Utxo(
output: Output(
value: Amount(50000),
value: 50000,
lock_script: Script("21025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99ac"),
),
height: Height(1),
@ -32,7 +32,7 @@ expression: stored_utxos
output_index: OutputIndex(1),
), Some(Utxo(
output: Output(
value: Amount(12500),
value: 12500,
lock_script: Script("a914ef775f1f997f122a062fff1a2d7443abd1f9c64287"),
),
height: Height(1),
@ -46,7 +46,7 @@ expression: stored_utxos
output_index: OutputIndex(0),
), Some(Utxo(
output: Output(
value: Amount(100000),
value: 100000,
lock_script: Script("2102acce9f6c16986c525fd34759d851ef5b4b85b5019a57bd59747be0ef1ba62523ac"),
),
height: Height(2),
@ -60,7 +60,7 @@ expression: stored_utxos
output_index: OutputIndex(1),
), Some(Utxo(
output: Output(
value: Amount(25000),
value: 25000,
lock_script: Script("a914ef775f1f997f122a062fff1a2d7443abd1f9c64287"),
),
height: Height(2),

View File

@ -15,9 +15,15 @@ default-run = "zebrad"
[features]
# In release builds, don't compile debug logging code, to improve performance.
default = ["release_max_level_info"]
getblocktemplate-rpcs = ["zebra-rpc/getblocktemplate-rpcs"]
# Production features that activate extra dependencies
# Production features that activate extra dependencies, or extra features in dependencies
# Experimental mining RPC support
getblocktemplate-rpcs = [
"zebra-rpc/getblocktemplate-rpcs",
"zebra-state/getblocktemplate-rpcs",
"zebra-node-services/getblocktemplate-rpcs",
]
sentry = ["dep:sentry", "sentry-tracing"]
flamegraph = ["tracing-flame", "inferno"]

View File

@ -424,6 +424,7 @@ impl Service<Request> for Mempool {
async move { Ok(Response::TransactionIds(res)) }.boxed()
}
Request::TransactionsById(ref ids) => {
trace!(?req, "got mempool request");
@ -445,6 +446,17 @@ impl Service<Request> for Mempool {
async move { Ok(Response::Transactions(res)) }.boxed()
}
#[cfg(feature = "getblocktemplate-rpcs")]
Request::Transactions => {
trace!(?req, "got mempool request");
let res: Vec<_> = storage.transactions().cloned().collect();
trace!(?req, res_count = ?res.len(), "answered mempool request");
async move { Ok(Response::Transactions(res)) }.boxed()
}
Request::RejectedTransactionIds(ref ids) => {
trace!(?req, "got mempool request");
@ -491,8 +503,12 @@ impl Service<Request> for Mempool {
let resp = match req {
// Empty Queries
Request::TransactionIds => Response::TransactionIds(Default::default()),
Request::TransactionsById(_) => Response::Transactions(Default::default()),
Request::TransactionsByMinedId(_) => Response::Transactions(Default::default()),
#[cfg(feature = "getblocktemplate-rpcs")]
Request::Transactions => Response::Transactions(Default::default()),
Request::RejectedTransactionIds(_) => {
Response::RejectedTransactionIds(Default::default())
}

View File

@ -55,6 +55,10 @@
//!
//! The following `zebrad` feature flags are available at compile time:
//!
//! ### JSON-RPC
//!
//! * `getblocktemplate-rpcs`: Experimental mining pool RPC support (currently incomplete)
//!
//! ### Metrics
//!
//! * `prometheus`: export metrics to prometheus.