Skip to content

Commit e3b050b

Browse files
committed
rpc: align getblock with zcashd behaviour
1 parent f415a5a commit e3b050b

17 files changed

+610
-44
lines changed

CHANGELOG.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ by syncing Zebra from scratch, or by using the `copy-state` command to create a
109109
command, first make a copy Zebra's Testnet configuration with a different cache directory path, for example, if Zebra's configuration is at the
110110
default path, by running `cp ~/.config/zebrad.toml ./zebrad-copy-target.toml`, then opening the new configuration file and editing the
111111
`cache_dir` path in the `state` section. Once there's a copy of Zebra's configuration with the new state cache directory path, run:
112-
`zebrad copy-state --target-config-path "./zebrad-copy-target.toml" --max-source-height "2975999"`, and then update the original
112+
`zebrad copy-state --target-config-path "./zebrad-copy-target.toml" --max-source-height "2975999"`, and then update the original
113113
Zebra configuration to use the new state cache directory.
114114

115115
### Added
@@ -155,7 +155,7 @@ Thank you to everyone who contributed to this release, we couldn't make Zebra wi
155155
- Support for custom Testnets and Regtest is greatly enhanced.
156156
- Windows is now back in the second tier of supported platforms.
157157
- The end-of-support time interval is set to match `zcashd`'s 16 weeks.
158-
- The RPC serialization of empty treestates matches `zcashd`.
158+
- The RPC serialization of empty treestates matches `zcashd`.
159159

160160
### Added
161161

@@ -221,11 +221,11 @@ Thank you to everyone who contributed to this release, we couldn't make Zebra wi
221221

222222
## [Zebra 1.6.1](https://github.com/ZcashFoundation/zebra/releases/tag/v1.6.1) - 2024-04-15
223223

224-
This release adds an OpenAPI specification for Zebra's RPC methods and startup logs about Zebra's storage usage and other database information.
224+
This release adds an OpenAPI specification for Zebra's RPC methods and startup logs about Zebra's storage usage and other database information.
225225

226226
It also includes:
227227
- Bug fixes and improved error messages for some zebra-scan gRPC methods
228-
- A performance improvement in Zebra's `getblock` RPC method
228+
- A performance improvement in Zebra's `getblock` RPC method
229229

230230
### Added
231231

zebra-rpc/src/constants.rs

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ use jsonrpc_core::{Error, ErrorCode};
1111
/// <https://github.com/s-nomp/node-stratum-pool/blob/d86ae73f8ff968d9355bb61aac05e0ebef36ccb5/lib/pool.js#L459>
1212
pub const INVALID_PARAMETERS_ERROR_CODE: ErrorCode = ErrorCode::ServerError(-1);
1313

14+
/// The RPC error code used by `zcashd` for missing blocks, when looked up
15+
/// by hash.
16+
pub const INVALID_ADDRESS_OR_KEY_ERROR_CODE: ErrorCode = ErrorCode::ServerError(-5);
17+
1418
/// The RPC error code used by `zcashd` for missing blocks.
1519
///
1620
/// `lightwalletd` expects error code `-8` when a block is not found:

zebra-rpc/src/methods.rs

+236-12
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ use zebra_node_services::mempool;
3737
use zebra_state::{HashOrHeight, MinedTx, OutputIndex, OutputLocation, TransactionLocation};
3838

3939
use crate::{
40-
constants::{INVALID_PARAMETERS_ERROR_CODE, MISSING_BLOCK_ERROR_CODE},
40+
constants::{
41+
INVALID_ADDRESS_OR_KEY_ERROR_CODE, INVALID_PARAMETERS_ERROR_CODE, MISSING_BLOCK_ERROR_CODE,
42+
},
4143
methods::trees::{GetSubtrees, GetTreestate, SubtreeRpcData},
4244
queue::Queue,
4345
};
@@ -145,7 +147,8 @@ pub trait Rpc {
145147

146148
/// Returns the requested block by hash or height, as a [`GetBlock`] JSON string.
147149
/// If the block is not in Zebra's state, returns
148-
/// [error code `-8`.](https://github.com/zcash/zcash/issues/5758)
150+
/// [error code `-8`.](https://github.com/zcash/zcash/issues/5758) if a height was
151+
/// passed or -5 if a hash was passed.
149152
///
150153
/// zcashd reference: [`getblock`](https://zcash.github.io/rpc/getblock.html)
151154
/// method: post
@@ -154,16 +157,19 @@ pub trait Rpc {
154157
/// # Parameters
155158
///
156159
/// - `hash_or_height`: (string, required, example="1") The hash or height for the block to be returned.
157-
/// - `verbosity`: (number, optional, default=1, example=1) 0 for hex encoded data, 1 for a json object, and 2 for json object with transaction data.
160+
/// - `verbosity`: (number, optional, default=1, example=1) 0 for hex encoded data, 1 for a json object, and 2 for json object with transaction data, and 3 for a partially filled json object (which is faster and useful for lightwalletd-only usage)
158161
///
159162
/// # Notes
160163
///
161-
/// With verbosity=1, [`lightwalletd` only reads the `tx` field of the
162-
/// result](https://github.com/zcash/lightwalletd/blob/dfac02093d85fb31fb9a8475b884dd6abca966c7/common/common.go#L152),
163-
/// and other clients only read the `hash` and `confirmations` fields,
164-
/// so we only return a few fields for now.
164+
/// Zebra previously partially supported verbosity=1 by returning only the
165+
/// fields required by lightwalletd ([`lightwalletd` only reads the `tx`
166+
/// field of the
167+
/// result](https://github.com/zcash/lightwalletd/blob/dfac02093d85fb31fb9a8475b884dd6abca966c7/common/common.go#L152)).
168+
/// That verbosity level was migrated to "3"; so while lightwalletd will
169+
/// still work by using verbosity=1, it will sync faster if it is changed to
170+
/// use verbosity=3.
165171
///
166-
/// `lightwalletd` and mining clients also do not use verbosity=2, so we don't support it.
172+
/// The undocumented `chainwork` field is not returned.
167173
#[rpc(name = "getblock")]
168174
fn get_block(
169175
&self,
@@ -172,6 +178,9 @@ pub trait Rpc {
172178
) -> BoxFuture<Result<GetBlock>>;
173179

174180
/// Returns the requested block header by hash or height, as a [`GetBlockHeader`] JSON string.
181+
/// If the block is not in Zebra's state,
182+
/// returns [error code `-8`.](https://github.com/zcash/zcash/issues/5758)
183+
/// if a height was passed or -5 if a hash was passed.
175184
///
176185
/// zcashd reference: [`getblockheader`](https://zcash.github.io/rpc/getblockheader.html)
177186
/// method: post
@@ -181,6 +190,10 @@ pub trait Rpc {
181190
///
182191
/// - `hash_or_height`: (string, required, example="1") The hash or height for the block to be returned.
183192
/// - `verbose`: (bool, optional, default=false, example=true) false for hex encoded data, true for a json object
193+
///
194+
/// # Notes
195+
///
196+
/// The undocumented `chainwork` field is not returned.
184197
#[rpc(name = "getblockheader")]
185198
fn get_block_header(
186199
&self,
@@ -738,7 +751,9 @@ where
738751

739752
let mut state = self.state.clone();
740753
let verbosity = verbosity.unwrap_or(DEFAULT_GETBLOCK_VERBOSITY);
754+
let self_clone = self.clone();
741755

756+
let original_hash_or_height = hash_or_height.clone();
742757
async move {
743758
let hash_or_height: HashOrHeight = hash_or_height.parse().map_server_error()?;
744759

@@ -766,6 +781,99 @@ where
766781
_ => unreachable!("unmatched response to a block request"),
767782
}
768783
} else if verbosity == 1 || verbosity == 2 {
784+
let r: Result<GetBlockHeader> = self_clone
785+
.get_block_header(original_hash_or_height, Some(true))
786+
.await;
787+
788+
let GetBlockHeader::Object(h) = r? else {
789+
panic!("must return Object")
790+
};
791+
let hash = h.hash.0;
792+
793+
// # Concurrency
794+
//
795+
// We look up by block hash so the hash, transaction IDs, and confirmations
796+
// are consistent.
797+
let requests = vec![
798+
// Get transaction IDs from the transaction index by block hash
799+
//
800+
// # Concurrency
801+
//
802+
// A block's transaction IDs are never modified, so all possible responses are
803+
// valid. Clients that query block heights must be able to handle chain forks,
804+
// including getting transaction IDs from any chain fork.
805+
zebra_state::ReadRequest::TransactionIdsForBlock(hash.into()),
806+
// Sapling trees
807+
zebra_state::ReadRequest::SaplingTree(hash.into()),
808+
// Orchard trees
809+
zebra_state::ReadRequest::OrchardTree(hash.into()),
810+
];
811+
812+
let mut futs = FuturesOrdered::new();
813+
814+
for request in requests {
815+
futs.push_back(state.clone().oneshot(request));
816+
}
817+
818+
let tx_ids_response = futs.next().await.expect("`futs` should not be empty");
819+
let tx = match tx_ids_response.map_server_error()? {
820+
zebra_state::ReadResponse::TransactionIdsForBlock(tx_ids) => tx_ids
821+
.ok_or_server_error("Block not found")?
822+
.iter()
823+
.map(|tx_id| tx_id.encode_hex())
824+
.collect(),
825+
_ => unreachable!("unmatched response to a transaction_ids_for_block request"),
826+
};
827+
828+
let sapling_tree_response = futs.next().await.expect("`futs` should not be empty");
829+
let sapling_note_commitment_tree_count =
830+
match sapling_tree_response.map_server_error()? {
831+
zebra_state::ReadResponse::SaplingTree(Some(nct)) => nct.count(),
832+
zebra_state::ReadResponse::SaplingTree(None) => 0,
833+
_ => unreachable!("unmatched response to a SaplingTree request"),
834+
};
835+
836+
let orchard_tree_response = futs.next().await.expect("`futs` should not be empty");
837+
let orchard_note_commitment_tree_count =
838+
match orchard_tree_response.map_server_error()? {
839+
zebra_state::ReadResponse::OrchardTree(Some(nct)) => nct.count(),
840+
zebra_state::ReadResponse::OrchardTree(None) => 0,
841+
_ => unreachable!("unmatched response to a OrchardTree request"),
842+
};
843+
844+
let sapling = SaplingTrees {
845+
size: sapling_note_commitment_tree_count,
846+
};
847+
848+
let orchard = OrchardTrees {
849+
size: orchard_note_commitment_tree_count,
850+
};
851+
852+
let trees = GetBlockTrees { sapling, orchard };
853+
854+
Ok(GetBlock::Object {
855+
hash: h.hash,
856+
confirmations: h.confirmations,
857+
height: Some(h.height),
858+
version: Some(h.version),
859+
merkle_root: Some(h.merkle_root),
860+
time: Some(h.time),
861+
nonce: Some(h.nonce),
862+
solution: Some(h.solution),
863+
bits: Some(h.bits),
864+
difficulty: Some(h.difficulty),
865+
// TODO
866+
tx,
867+
trees,
868+
// TODO
869+
size: None,
870+
final_sapling_root: Some(h.final_sapling_root),
871+
// TODO
872+
final_orchard_root: None,
873+
previous_block_hash: Some(h.previous_block_hash),
874+
next_block_hash: h.next_block_hash,
875+
})
876+
} else if verbosity == 3 {
769877
// # Performance
770878
//
771879
// This RPC is used in `lightwalletd`'s initial sync of 2 million blocks,
@@ -920,6 +1028,17 @@ where
9201028
time,
9211029
tx,
9221030
trees,
1031+
size: None,
1032+
version: None,
1033+
merkle_root: None,
1034+
final_sapling_root: None,
1035+
final_orchard_root: None,
1036+
nonce: None,
1037+
bits: None,
1038+
difficulty: None,
1039+
previous_block_hash: None,
1040+
next_block_hash: None,
1041+
solution: None,
9231042
})
9241043
} else {
9251044
Err(Error {
@@ -952,7 +1071,18 @@ where
9521071
.clone()
9531072
.oneshot(zebra_state::ReadRequest::BlockHeader(hash_or_height))
9541073
.await
955-
.map_server_error()?
1074+
.map_err(|_| Error {
1075+
// Compatibility with zcashd. Note that since this function
1076+
// is reused by getblock(), we return the errors expected
1077+
// by it (they differ whether a hash or a height was passed)
1078+
code: if hash_or_height.hash().is_some() {
1079+
INVALID_ADDRESS_OR_KEY_ERROR_CODE
1080+
} else {
1081+
MISSING_BLOCK_ERROR_CODE
1082+
},
1083+
message: "block height not in best chain".to_string(),
1084+
data: None,
1085+
})?
9561086
else {
9571087
panic!("unexpected response to BlockHeader request")
9581088
};
@@ -1688,8 +1818,9 @@ impl Default for SentTransactionHash {
16881818
/// Response to a `getblock` RPC request.
16891819
///
16901820
/// See the notes for the [`Rpc::get_block`] method.
1691-
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
1821+
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
16921822
#[serde(untagged)]
1823+
#[allow(clippy::large_enum_variant)] //TODO: create a struct for the Object and Box it
16931824
pub enum GetBlock {
16941825
/// The request block, hex-encoded.
16951826
Raw(#[serde(with = "hex")] SerializedBlock),
@@ -1702,21 +1833,84 @@ pub enum GetBlock {
17021833
/// or -1 if it is not in the best chain.
17031834
confirmations: i64,
17041835

1836+
/// The block size. TODO: fill it
1837+
#[serde(skip_serializing_if = "Option::is_none")]
1838+
size: Option<i64>,
1839+
17051840
/// The height of the requested block.
17061841
#[serde(skip_serializing_if = "Option::is_none")]
17071842
height: Option<Height>,
17081843

1709-
/// The height of the requested block.
1844+
/// The version field of the requested block.
17101845
#[serde(skip_serializing_if = "Option::is_none")]
1711-
time: Option<i64>,
1846+
version: Option<u32>,
17121847

1848+
/// The merkle root of the requesteed block.
1849+
#[serde(with = "opthex", rename = "merkleroot")]
1850+
#[serde(skip_serializing_if = "Option::is_none")]
1851+
merkle_root: Option<block::merkle::Root>,
1852+
1853+
// `blockcommitments` would be here. Undocumented. TODO: decide if we want to support it
1854+
// `authdataroot` would be here. Undocumented. TODO: decide if we want to support it
1855+
//
1856+
/// The root of the Sapling commitment tree after applying this block.
1857+
#[serde(with = "opthex", rename = "finalsaplingroot")]
1858+
#[serde(skip_serializing_if = "Option::is_none")]
1859+
final_sapling_root: Option<[u8; 32]>,
1860+
1861+
/// The root of the Orchard commitment tree after applying this block.
1862+
#[serde(with = "opthex", rename = "finalorchardroot")]
1863+
#[serde(skip_serializing_if = "Option::is_none")]
1864+
final_orchard_root: Option<[u8; 32]>,
1865+
1866+
// `chainhistoryroot` would be here. Undocumented. TODO: decide if we want to support it
1867+
//
17131868
/// List of transaction IDs in block order, hex-encoded.
17141869
//
17151870
// TODO: use a typed Vec<transaction::Hash> here
1871+
// TODO: support Objects
17161872
tx: Vec<String>,
17171873

1874+
/// The height of the requested block.
1875+
#[serde(skip_serializing_if = "Option::is_none")]
1876+
time: Option<i64>,
1877+
1878+
/// The nonce of the requested block header.
1879+
#[serde(with = "opthex")]
1880+
#[serde(skip_serializing_if = "Option::is_none")]
1881+
nonce: Option<[u8; 32]>,
1882+
1883+
/// The Equihash solution in the requested block header.
1884+
/// Note: presence of this field in getblock is not documented in zcashd.
1885+
#[serde(with = "opthex")]
1886+
#[serde(skip_serializing_if = "Option::is_none")]
1887+
solution: Option<Solution>,
1888+
1889+
/// The difficulty threshold of the requested block header displayed in compact form.
1890+
#[serde(with = "opthex")]
1891+
#[serde(skip_serializing_if = "Option::is_none")]
1892+
bits: Option<CompactDifficulty>,
1893+
1894+
/// Floating point number that represents the difficulty limit for this block as a multiple
1895+
/// of the minimum difficulty for the network.
1896+
#[serde(skip_serializing_if = "Option::is_none")]
1897+
difficulty: Option<f64>,
1898+
1899+
// `chainwork` would be here, but we don't plan on supporting it
1900+
// `anchor` would be here. Undocumented. TODO: decide if we want to support it
1901+
// `chainSupply` would be here, TODO: implement
1902+
// `valuePools` would be here, TODO: implement
1903+
//
17181904
/// Information about the note commitment trees.
17191905
trees: GetBlockTrees,
1906+
1907+
/// The previous block hash of the requested block header.
1908+
#[serde(rename = "previousblockhash", skip_serializing_if = "Option::is_none")]
1909+
previous_block_hash: Option<GetBlockHash>,
1910+
1911+
/// The next block hash after the requested block header.
1912+
#[serde(rename = "nextblockhash", skip_serializing_if = "Option::is_none")]
1913+
next_block_hash: Option<GetBlockHash>,
17201914
},
17211915
}
17221916

@@ -1729,6 +1923,17 @@ impl Default for GetBlock {
17291923
time: None,
17301924
tx: Vec::new(),
17311925
trees: GetBlockTrees::default(),
1926+
size: None,
1927+
version: None,
1928+
merkle_root: None,
1929+
final_sapling_root: None,
1930+
final_orchard_root: None,
1931+
nonce: None,
1932+
bits: None,
1933+
difficulty: None,
1934+
previous_block_hash: None,
1935+
next_block_hash: None,
1936+
solution: None,
17321937
}
17331938
}
17341939
}
@@ -2088,3 +2293,22 @@ pub fn height_from_signed_int(index: i32, tip_height: Height) -> Result<Height>
20882293
Ok(Height(sanitized_height))
20892294
}
20902295
}
2296+
2297+
mod opthex {
2298+
use hex::ToHex;
2299+
use serde::Serializer;
2300+
2301+
pub fn serialize<S, T>(data: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
2302+
where
2303+
S: Serializer,
2304+
T: ToHex,
2305+
{
2306+
match data {
2307+
Some(data) => {
2308+
let s = data.encode_hex::<String>();
2309+
serializer.serialize_str(&s)
2310+
}
2311+
None => serializer.serialize_none(),
2312+
}
2313+
}
2314+
}

0 commit comments

Comments
 (0)