Compare commits
No commits in common. "92f2966e253cd2b6a526aa195f9e97c0e4c0c3d1" and "2ba837470ef31dd015300a803559023910f57ac6" have entirely different histories.
92f2966e25
...
2ba837470e
|
|
@ -78,7 +78,8 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
# TODO: Windows was removed for now, see https://github.com/ZcashFoundation/zebra/issues/3801
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
rust: [stable, beta]
|
||||
# TODO: When vars.EXPERIMENTAL_FEATURES has features in it, add it here.
|
||||
# Or work out a way to trim the space from the variable: GitHub doesn't allow empty variables.
|
||||
|
|
|
|||
36
Cargo.lock
36
Cargo.lock
|
|
@ -1077,9 +1077,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.113"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "048948e14bc2c2652ec606c8e3bb913407f0187288fb351a0b2d972beaf12070"
|
||||
checksum = "bbe98ba1789d56fb3db3bee5e032774d4f421b685de7ba703643584ba24effbe"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxxbridge-flags",
|
||||
|
|
@ -1089,9 +1089,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cxx-gen"
|
||||
version = "0.7.121"
|
||||
version = "0.7.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "383ecb9f96a536a1c7a2a61c5786f583da84f9240da149d78d005a4413c9a71e"
|
||||
checksum = "54b629c0d006c7e44c1444dd17d18a458c9390d32276b758ac7abd21a75c99b0"
|
||||
dependencies = [
|
||||
"codespan-reporting",
|
||||
"proc-macro2",
|
||||
|
|
@ -1101,15 +1101,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.113"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af40b0467c68d3d9fb7550ef984edc8ad47252f703ef0f1f2d1052e0e4af8793"
|
||||
checksum = "20888d9e1d2298e2ff473cee30efe7d5036e437857ab68bbfea84c74dba91da2"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.113"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7743446286141c9f6d4497c493c01234eb848e14d2e20866ae9811eae0630cb9"
|
||||
checksum = "2fa16a70dd58129e4dfffdff535fb1bce66673f7bbeec4a5a1765a504e1ccd84"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -3809,9 +3809,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.11"
|
||||
version = "0.21.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4"
|
||||
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring 0.17.8",
|
||||
|
|
@ -5670,9 +5670,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zcash_client_backend"
|
||||
version = "0.10.0"
|
||||
version = "0.10.0-rc.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6a382af39be9ee5a3788157145c404b7cd19acc440903f6c34b09fb44f0e991"
|
||||
checksum = "ecc33f71747a93d509f7e1c047961e359a271bdf4869cc07f7f65ee1ba7df8c2"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bech32",
|
||||
|
|
@ -5737,9 +5737,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zcash_primitives"
|
||||
version = "0.13.0"
|
||||
version = "0.13.0-rc.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d17e4c94ca8d69d2fcf2be97522da5732a580eb2125cda3b150761952f8df8e6"
|
||||
checksum = "0cc4391d9325e0a51a7cbff02b5c4b5472d66087bd9c903ddb12dea7ec22f3e0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bip0039",
|
||||
|
|
@ -5773,9 +5773,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zcash_proofs"
|
||||
version = "0.13.0"
|
||||
version = "0.13.0-rc.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df0c99f65a840ff256c106b28d67d702d9759d206112473d4982c92003262406"
|
||||
checksum = "48f22eff3bdc382327ef28f809024ddc89ec6d903ba71be629b2cbea34afdda2"
|
||||
dependencies = [
|
||||
"bellman",
|
||||
"blake2b_simd",
|
||||
|
|
@ -5804,9 +5804,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zcash_script"
|
||||
version = "0.1.15"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e3de6aece21108f502f724183955d244e02338613eaa4f9010386c63618a3a8"
|
||||
checksum = "8deff8ea47cbe2a008abefedc1a2d9c0e48a87844379759ace270a0b53353c71"
|
||||
dependencies = [
|
||||
"bellman",
|
||||
"bindgen",
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ orchard = "0.6.0"
|
|||
zcash_encoding = "0.2.0"
|
||||
zcash_history = "0.4.0"
|
||||
zcash_note_encryption = "0.4.0"
|
||||
zcash_primitives = { version = "0.13.0", features = ["transparent-inputs"] }
|
||||
zcash_primitives = { version = "0.13.0-rc.1", features = ["transparent-inputs"] }
|
||||
|
||||
# Time
|
||||
chrono = { version = "0.4.38", default-features = false, features = ["clock", "std", "serde"] }
|
||||
|
|
|
|||
|
|
@ -138,13 +138,15 @@ impl fmt::Display for NetworkKind {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Network> for &'a str {
|
||||
fn from(network: &'a Network) -> &'a str {
|
||||
impl From<&Network> for &'static str {
|
||||
fn from(network: &Network) -> &'static str {
|
||||
match network {
|
||||
Network::Mainnet => "Mainnet",
|
||||
// TODO:
|
||||
// - Add a `name` field to use here instead of checking `is_default_testnet()`
|
||||
// - zcashd calls the Regtest cache dir 'regtest' (#8327).
|
||||
Network::Testnet(params) => params.network_name(),
|
||||
Network::Testnet(params) if params.is_default_testnet() => "Testnet",
|
||||
Network::Testnet(_params) => "UnknownTestnet",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! Types and implementation for Testnet consensus parameters
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use zcash_primitives::constants as zp_constants;
|
||||
|
||||
|
|
@ -11,19 +11,6 @@ use crate::{
|
|||
},
|
||||
};
|
||||
|
||||
/// Reserved network names that should not be allowed for configured Testnets.
|
||||
pub const RESERVED_NETWORK_NAMES: [&str; 6] = [
|
||||
"Mainnet",
|
||||
"Testnet",
|
||||
"Regtest",
|
||||
"MainnetKind",
|
||||
"TestnetKind",
|
||||
"RegtestKind",
|
||||
];
|
||||
|
||||
/// Maximum length for a configured network name.
|
||||
pub const MAX_NETWORK_NAME_LENGTH: usize = 30;
|
||||
|
||||
/// Configurable activation heights for Regtest and configured Testnets.
|
||||
#[derive(Deserialize, Default)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
|
|
@ -48,8 +35,6 @@ pub struct ConfiguredActivationHeights {
|
|||
/// Builder for the [`Parameters`] struct.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct ParametersBuilder {
|
||||
/// The name of this network to be used by the `Display` trait impl.
|
||||
network_name: String,
|
||||
/// The network upgrade activation heights for this network, see [`Parameters::activation_heights`] for more details.
|
||||
activation_heights: BTreeMap<Height, NetworkUpgrade>,
|
||||
/// Sapling extended spending key human-readable prefix for this network
|
||||
|
|
@ -63,7 +48,6 @@ pub struct ParametersBuilder {
|
|||
impl Default for ParametersBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
network_name: "UnknownTestnet".to_string(),
|
||||
// # Correctness
|
||||
//
|
||||
// `Genesis` network upgrade activation height must always be 0
|
||||
|
|
@ -85,33 +69,9 @@ impl Default for ParametersBuilder {
|
|||
}
|
||||
|
||||
impl ParametersBuilder {
|
||||
/// Sets the network name to be used in the [`Parameters`] being built.
|
||||
pub fn with_network_name(mut self, network_name: impl fmt::Display) -> Self {
|
||||
self.network_name = network_name.to_string();
|
||||
|
||||
assert!(
|
||||
!RESERVED_NETWORK_NAMES.contains(&self.network_name.as_str()),
|
||||
"cannot use reserved network name '{network_name}' as configured Testnet name, reserved names: {RESERVED_NETWORK_NAMES:?}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
self.network_name.len() <= MAX_NETWORK_NAME_LENGTH,
|
||||
"network name {network_name} is too long, must be {MAX_NETWORK_NAME_LENGTH} characters or less"
|
||||
);
|
||||
|
||||
assert!(
|
||||
self.network_name
|
||||
.chars()
|
||||
.all(|x| x.is_alphanumeric() || x == '_'),
|
||||
"network name must include only alphanumeric characters or '_'"
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Checks that the provided network upgrade activation heights are in the correct order, then
|
||||
/// sets them as the new network upgrade activation heights.
|
||||
pub fn with_activation_heights(
|
||||
pub fn activation_heights(
|
||||
mut self,
|
||||
ConfiguredActivationHeights {
|
||||
// TODO: Find out if `BeforeOverwinter` is required at Height(1), allow for
|
||||
|
|
@ -139,6 +99,7 @@ impl ParametersBuilder {
|
|||
.chain(heartwood.into_iter().map(|h| (h, Heartwood)))
|
||||
.chain(canopy.into_iter().map(|h| (h, Canopy)))
|
||||
.chain(nu5.into_iter().map(|h| (h, Nu5)))
|
||||
.filter(|&(_, nu)| nu != NetworkUpgrade::BeforeOverwinter)
|
||||
.map(|(h, nu)| (h.try_into().expect("activation height must be valid"), nu))
|
||||
.collect();
|
||||
|
||||
|
|
@ -175,14 +136,12 @@ impl ParametersBuilder {
|
|||
/// Converts the builder to a [`Parameters`] struct
|
||||
pub fn finish(self) -> Parameters {
|
||||
let Self {
|
||||
network_name,
|
||||
activation_heights,
|
||||
hrp_sapling_extended_spending_key,
|
||||
hrp_sapling_extended_full_viewing_key,
|
||||
hrp_sapling_payment_address,
|
||||
} = self;
|
||||
Parameters {
|
||||
network_name,
|
||||
activation_heights,
|
||||
hrp_sapling_extended_spending_key,
|
||||
hrp_sapling_extended_full_viewing_key,
|
||||
|
|
@ -199,8 +158,6 @@ impl ParametersBuilder {
|
|||
/// Network consensus parameters for test networks such as Regtest and the default Testnet.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct Parameters {
|
||||
/// The name of this network to be used by the `Display` trait impl.
|
||||
network_name: String,
|
||||
/// The network upgrade activation heights for this network.
|
||||
///
|
||||
/// Note: This value is ignored by `Network::activation_list()` when `zebra-chain` is
|
||||
|
|
@ -219,9 +176,13 @@ impl Default for Parameters {
|
|||
/// Returns an instance of the default public testnet [`Parameters`].
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
network_name: "Testnet".to_string(),
|
||||
activation_heights: TESTNET_ACTIVATION_HEIGHTS.iter().cloned().collect(),
|
||||
..Self::build().finish()
|
||||
hrp_sapling_extended_spending_key:
|
||||
zp_constants::testnet::HRP_SAPLING_EXTENDED_SPENDING_KEY.to_string(),
|
||||
hrp_sapling_extended_full_viewing_key:
|
||||
zp_constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY.to_string(),
|
||||
hrp_sapling_payment_address: zp_constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -237,11 +198,6 @@ impl Parameters {
|
|||
self == &Self::default()
|
||||
}
|
||||
|
||||
/// Returns the network name
|
||||
pub fn network_name(&self) -> &str {
|
||||
&self.network_name
|
||||
}
|
||||
|
||||
/// Returns the network upgrade activation heights
|
||||
pub fn activation_heights(&self) -> &BTreeMap<Height, NetworkUpgrade> {
|
||||
&self.activation_heights
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ use zcash_primitives::consensus::{self as zp_consensus, Parameters};
|
|||
use crate::{
|
||||
block::Height,
|
||||
parameters::{
|
||||
testnet::{
|
||||
self, ConfiguredActivationHeights, MAX_NETWORK_NAME_LENGTH, RESERVED_NETWORK_NAMES,
|
||||
},
|
||||
testnet::{self, ConfiguredActivationHeights},
|
||||
Network, NetworkUpgrade, NETWORK_UPGRADES_IN_ORDER,
|
||||
},
|
||||
};
|
||||
|
|
@ -99,7 +97,7 @@ fn check_parameters_impl() {
|
|||
fn activates_network_upgrades_correctly() {
|
||||
let expected_activation_height = 1;
|
||||
let network = testnet::Parameters::build()
|
||||
.with_activation_heights(ConfiguredActivationHeights {
|
||||
.activation_heights(ConfiguredActivationHeights {
|
||||
nu5: Some(expected_activation_height),
|
||||
..Default::default()
|
||||
})
|
||||
|
|
@ -127,58 +125,3 @@ fn activates_network_upgrades_correctly() {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that configured testnet names are validated and used correctly.
|
||||
#[test]
|
||||
fn check_network_name() {
|
||||
// Sets a no-op panic hook to avoid long output.
|
||||
std::panic::set_hook(Box::new(|_| {}));
|
||||
|
||||
// Checks that reserved network names cannot be used for configured testnets.
|
||||
for reserved_network_name in RESERVED_NETWORK_NAMES {
|
||||
std::panic::catch_unwind(|| {
|
||||
testnet::Parameters::build().with_network_name(reserved_network_name)
|
||||
})
|
||||
.expect_err("should panic when attempting to set network name as a reserved name");
|
||||
}
|
||||
|
||||
// Check that max length is enforced, and that network names may only contain alphanumeric characters and '_'.
|
||||
for invalid_network_name in [
|
||||
"a".repeat(MAX_NETWORK_NAME_LENGTH + 1),
|
||||
"!!!!non-alphanumeric-name".to_string(),
|
||||
] {
|
||||
std::panic::catch_unwind(|| {
|
||||
testnet::Parameters::build().with_network_name(invalid_network_name)
|
||||
})
|
||||
.expect_err("should panic when setting network name that's too long or contains non-alphanumeric characters (except '_')");
|
||||
}
|
||||
|
||||
// Checks that network names are displayed correctly
|
||||
assert_eq!(
|
||||
Network::new_default_testnet().to_string(),
|
||||
"Testnet",
|
||||
"default testnet should be displayed as 'Testnet'"
|
||||
);
|
||||
assert_eq!(
|
||||
Network::Mainnet.to_string(),
|
||||
"Mainnet",
|
||||
"Mainnet should be displayed as 'Mainnet'"
|
||||
);
|
||||
|
||||
// TODO: Check Regtest
|
||||
|
||||
// Check that network name can contain alphanumeric characters and '_'.
|
||||
let expected_name = "ConfiguredTestnet_1";
|
||||
let network = testnet::Parameters::build()
|
||||
// Check that network name can contain `MAX_NETWORK_NAME_LENGTH` characters
|
||||
.with_network_name("a".repeat(MAX_NETWORK_NAME_LENGTH))
|
||||
.with_network_name(expected_name)
|
||||
.to_network();
|
||||
|
||||
// Check that configured network name is displayed
|
||||
assert_eq!(
|
||||
network.to_string(),
|
||||
expected_name,
|
||||
"network must be displayed as configured network name"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@
|
|||
//!
|
||||
//! Usage: <https://docs.rs/zcash_address/0.2.0/zcash_address/trait.TryFromAddress.html#examples>
|
||||
|
||||
use zcash_address::unified::{self, Container, Receiver};
|
||||
use zcash_address::unified::{self, Container};
|
||||
use zcash_primitives::sapling;
|
||||
|
||||
use crate::{parameters::NetworkKind, transparent, BoxError};
|
||||
|
||||
/// Zcash address variants
|
||||
pub enum Address {
|
||||
/// Transparent address
|
||||
Transparent(transparent::Address),
|
||||
|
||||
/// Sapling address
|
||||
Sapling {
|
||||
/// Address' network kind
|
||||
|
|
@ -31,6 +34,9 @@ pub enum Address {
|
|||
|
||||
/// Sapling address
|
||||
sapling: Option<sapling::PaymentAddress>,
|
||||
|
||||
/// Transparent address
|
||||
transparent: Option<transparent::Address>,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -38,6 +44,26 @@ impl zcash_address::TryFromAddress for Address {
|
|||
// TODO: crate::serialization::SerializationError
|
||||
type Error = BoxError;
|
||||
|
||||
fn try_from_transparent_p2pkh(
|
||||
network: zcash_address::Network,
|
||||
data: [u8; 20],
|
||||
) -> Result<Self, zcash_address::ConversionError<Self::Error>> {
|
||||
Ok(Self::Transparent(transparent::Address::from_pub_key_hash(
|
||||
NetworkKind::from_zcash_address(network),
|
||||
data,
|
||||
)))
|
||||
}
|
||||
|
||||
fn try_from_transparent_p2sh(
|
||||
network: zcash_address::Network,
|
||||
data: [u8; 20],
|
||||
) -> Result<Self, zcash_address::ConversionError<Self::Error>> {
|
||||
Ok(Self::Transparent(transparent::Address::from_script_hash(
|
||||
NetworkKind::from_zcash_address(network),
|
||||
data,
|
||||
)))
|
||||
}
|
||||
|
||||
fn try_from_sapling(
|
||||
network: zcash_address::Network,
|
||||
data: [u8; 43],
|
||||
|
|
@ -55,6 +81,7 @@ impl zcash_address::TryFromAddress for Address {
|
|||
let network = NetworkKind::from_zcash_address(network);
|
||||
let mut orchard = None;
|
||||
let mut sapling = None;
|
||||
let mut transparent = None;
|
||||
|
||||
for receiver in unified_address.items().into_iter() {
|
||||
match receiver {
|
||||
|
|
@ -82,10 +109,15 @@ impl zcash_address::TryFromAddress for Address {
|
|||
.into());
|
||||
}
|
||||
}
|
||||
unified::Receiver::P2pkh(data) => {
|
||||
transparent = Some(transparent::Address::from_pub_key_hash(network, data));
|
||||
}
|
||||
unified::Receiver::P2sh(data) => {
|
||||
transparent = Some(transparent::Address::from_script_hash(network, data));
|
||||
}
|
||||
unified::Receiver::Unknown { .. } => {
|
||||
return Err(BoxError::from("Unsupported receiver in a Unified Address.").into());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +126,7 @@ impl zcash_address::TryFromAddress for Address {
|
|||
unified_address,
|
||||
orchard,
|
||||
sapling,
|
||||
transparent,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -102,6 +135,7 @@ impl Address {
|
|||
/// Returns the network for the address.
|
||||
pub fn network(&self) -> NetworkKind {
|
||||
match &self {
|
||||
Self::Transparent(address) => address.network_kind(),
|
||||
Self::Sapling { network, .. } | Self::Unified { network, .. } => *network,
|
||||
}
|
||||
}
|
||||
|
|
@ -110,16 +144,23 @@ impl Address {
|
|||
/// Returns false if the address is PayToPublicKeyHash or shielded.
|
||||
pub fn is_script_hash(&self) -> bool {
|
||||
match &self {
|
||||
Self::Transparent(address) => address.is_script_hash(),
|
||||
Self::Sapling { .. } | Self::Unified { .. } => false,
|
||||
_ => true
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if address is of the [`Address::Transparent`] variant.
|
||||
/// Returns false if otherwise.
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
matches!(self, Self::Transparent(_))
|
||||
}
|
||||
|
||||
/// Returns the payment address for transparent or sapling addresses.
|
||||
pub fn payment_address(&self) -> Option<String> {
|
||||
use zcash_address::{ToAddress, ZcashAddress};
|
||||
|
||||
match &self {
|
||||
Self::Transparent(address) => Some(address.to_string()),
|
||||
Self::Sapling { address, network } => {
|
||||
let data = address.to_bytes();
|
||||
let address = ZcashAddress::from_sapling(network.to_zcash_address(), data);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ tokio-stream = "0.1.15"
|
|||
tower = { version = "0.4.13", features = ["util", "buffer"] }
|
||||
color-eyre = "0.6.3"
|
||||
|
||||
zcash_primitives = { version = "0.13.0" }
|
||||
zcash_primitives = { version = "0.13.0-rc.1" }
|
||||
|
||||
zebra-node-services = { path = "../zebra-node-services", version = "1.0.0-beta.36", features = ["shielded-scan"] }
|
||||
zebra-chain = { path = "../zebra-chain" , version = "1.0.0-beta.36" }
|
||||
|
|
|
|||
|
|
@ -629,9 +629,8 @@ impl<'de> Deserialize<'de> for Config {
|
|||
{
|
||||
#[derive(Deserialize)]
|
||||
struct DTestnetParameters {
|
||||
network_name: Option<String>,
|
||||
#[serde(default)]
|
||||
activation_heights: ConfiguredActivationHeights,
|
||||
pub(super) activation_heights: ConfiguredActivationHeights,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -679,25 +678,15 @@ impl<'de> Deserialize<'de> for Config {
|
|||
} = DConfig::deserialize(deserializer)?;
|
||||
|
||||
// TODO: Panic here if the initial testnet peers are the default initial testnet peers.
|
||||
let network = if let Some(DTestnetParameters {
|
||||
network_name,
|
||||
activation_heights,
|
||||
}) = testnet_parameters
|
||||
{
|
||||
let network = if let Some(DTestnetParameters { activation_heights }) = testnet_parameters {
|
||||
assert_eq!(
|
||||
network_kind,
|
||||
NetworkKind::Testnet,
|
||||
"set network to 'Testnet' to use configured testnet parameters"
|
||||
);
|
||||
|
||||
let mut params_builder = testnet::Parameters::build();
|
||||
|
||||
if let Some(network_name) = network_name {
|
||||
params_builder = params_builder.with_network_name(network_name)
|
||||
}
|
||||
|
||||
params_builder
|
||||
.with_activation_heights(activation_heights)
|
||||
testnet::Parameters::build()
|
||||
.activation_heights(activation_heights)
|
||||
.to_network()
|
||||
} else {
|
||||
// Convert to default `Network` for a `NetworkKind` if there are no testnet parameters.
|
||||
|
|
|
|||
|
|
@ -1091,6 +1091,11 @@ where
|
|||
}
|
||||
};
|
||||
|
||||
// we want to match zcashd's behaviour
|
||||
if !address.is_transparent() {
|
||||
return Ok(validate_address::Response::invalid());
|
||||
}
|
||||
|
||||
if address.network() == network.kind() {
|
||||
Ok(validate_address::Response {
|
||||
address: Some(raw_address),
|
||||
|
|
@ -1303,6 +1308,8 @@ where
|
|||
},
|
||||
)?;
|
||||
|
||||
let mut p2pkh = String::new();
|
||||
let mut p2sh = String::new();
|
||||
let mut orchard = String::new();
|
||||
let mut sapling = String::new();
|
||||
|
||||
|
|
@ -1319,12 +1326,26 @@ where
|
|||
.expect("using data already decoded as valid");
|
||||
sapling = addr.payment_address().unwrap_or_default();
|
||||
}
|
||||
zcash_address::unified::Receiver::P2pkh(data) => {
|
||||
let addr = zebra_chain::primitives::Address::try_from_transparent_p2pkh(
|
||||
network, data,
|
||||
)
|
||||
.expect("using data already decoded as valid");
|
||||
p2pkh = addr.payment_address().unwrap_or_default();
|
||||
}
|
||||
zcash_address::unified::Receiver::P2sh(data) => {
|
||||
let addr = zebra_chain::primitives::Address::try_from_transparent_p2sh(
|
||||
network, data,
|
||||
)
|
||||
.expect("using data already decoded as valid");
|
||||
p2sh = addr.payment_address().unwrap_or_default();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(unified_address::Response::new(
|
||||
orchard, sapling,
|
||||
orchard, sapling, p2pkh, p2sh,
|
||||
))
|
||||
}
|
||||
.boxed()
|
||||
|
|
|
|||
|
|
@ -7,14 +7,20 @@ pub struct Response {
|
|||
orchard: String,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
sapling: String,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
p2pkh: String,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
p2sh: String,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Create a new response for z_listunifiedreceivers given individual addresses.
|
||||
pub fn new(orchard: String, sapling: String) -> Response {
|
||||
pub fn new(orchard: String, sapling: String, p2pkh: String, p2sh: String) -> Response {
|
||||
Response {
|
||||
orchard,
|
||||
sapling,
|
||||
p2pkh,
|
||||
p2sh,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,4 +41,22 @@ impl Response {
|
|||
false => Some(self.sapling.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Return the p2pkh payment address from a response, if any.
|
||||
pub fn p2pkh(&self) -> Option<String> {
|
||||
match self.p2pkh.is_empty() {
|
||||
true => None,
|
||||
false => Some(self.p2pkh.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Return the p2sh payment address from a response, if any.
|
||||
pub fn p2sh(&self) -> Option<String> {
|
||||
match self.p2sh.is_empty() {
|
||||
true => None,
|
||||
false => Some(self.p2sh.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ impl Response {
|
|||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AddressType {
|
||||
/// The `p2pkh` address type.
|
||||
P2pkh,
|
||||
/// The `p2sh` address type.
|
||||
P2sh,
|
||||
/// The `sapling` address type.
|
||||
Sapling,
|
||||
/// The `unified` address type.
|
||||
|
|
@ -48,6 +52,13 @@ pub enum AddressType {
|
|||
impl From<&Address> for AddressType {
|
||||
fn from(address: &Address) -> Self {
|
||||
match address {
|
||||
Address::Transparent(_) => {
|
||||
if address.is_script_hash() {
|
||||
Self::P2sh
|
||||
} else {
|
||||
Self::P2pkh
|
||||
}
|
||||
}
|
||||
Address::Sapling { .. } => Self::Sapling,
|
||||
Address::Unified { .. } => Self::Unified,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1903,9 +1903,19 @@ async fn rpc_z_listunifiedreceivers() {
|
|||
"zs1mrhc9y7jdh5r9ece8u5khgvj9kg0zgkxzdduyv0whkg7lkcrkx5xqem3e48avjq9wn2rukydkwn"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
response.p2pkh(),
|
||||
Some(String::from("t1V9mnyk5Z5cTNMCkLbaDwSskgJZucTLdgW"))
|
||||
);
|
||||
assert_eq!(response.p2sh(), None);
|
||||
|
||||
// address taken from https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/test-vectors/zcash/unified_address.json#L39
|
||||
let response = get_block_template_rpc.z_list_unified_receivers("u12acx92vw49jek4lwwnjtzm0cssn2wxfneu7ryj4amd8kvnhahdrq0htsnrwhqvl92yg92yut5jvgygk0rqfs4lgthtycsewc4t57jyjn9p2g6ffxek9rdg48xe5kr37hxxh86zxh2ef0u2lu22n25xaf3a45as6mtxxlqe37r75mndzu9z2fe4h77m35c5mrzf4uqru3fjs39ednvw9ay8nf9r8g9jx8rgj50mj098exdyq803hmqsek3dwlnz4g5whc88mkvvjnfmjldjs9hm8rx89ctn5wxcc2e05rcz7m955zc7trfm07gr7ankf96jxwwfcqppmdefj8gc6508gep8ndrml34rdpk9tpvwzgdcv7lk2d70uh5jqacrpk6zsety33qcc554r3cls4ajktg03d9fye6exk8gnve562yadzsfmfh9d7v6ctl5ufm9ewpr6se25c47huk4fh2hakkwerkdd2yy3093snsgree5lt6smejfvse8v".to_string()).await.unwrap();
|
||||
assert_eq!(response.orchard(), Some(String::from("u10c5q7qkhu6f0ktaz7jqu4sfsujg0gpsglzudmy982mku7t0uma52jmsaz8h24a3wa7p0jwtsjqt8shpg25cvyexzlsw3jtdz4v6w70lv")));
|
||||
assert_eq!(response.sapling(), None);
|
||||
assert_eq!(
|
||||
response.p2pkh(),
|
||||
Some(String::from("t1dMjwmwM2a6NtavQ6SiPP8i9ofx4cgfYYP"))
|
||||
);
|
||||
assert_eq!(response.p2sh(), None);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,14 +23,13 @@ use super::super::*;
|
|||
|
||||
/// Test that the JSON-RPC server spawns when configured with a single thread.
|
||||
#[test]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn rpc_server_spawn_single_thread() {
|
||||
rpc_server_spawn(false)
|
||||
}
|
||||
|
||||
/// Test that the JSON-RPC server spawns when configured with multiple threads.
|
||||
#[test]
|
||||
fn rpc_server_spawn_parallel_threads() {
|
||||
fn rpc_sever_spawn_parallel_threads() {
|
||||
rpc_server_spawn(true)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ tracing = "0.1.39"
|
|||
futures = "0.3.30"
|
||||
|
||||
zcash_client_backend = "0.10.0-rc.1"
|
||||
zcash_primitives = "0.13.0"
|
||||
zcash_primitives = "0.13.0-rc.1"
|
||||
|
||||
zebra-chain = { path = "../zebra-chain", version = "1.0.0-beta.36", features = ["shielded-scan"] }
|
||||
zebra-state = { path = "../zebra-state", version = "1.0.0-beta.36", features = ["shielded-scan"] }
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ keywords = ["zebra", "zcash"]
|
|||
categories = ["api-bindings", "cryptography::cryptocurrencies"]
|
||||
|
||||
[dependencies]
|
||||
zcash_script = "0.1.15"
|
||||
zcash_script = "0.1.14"
|
||||
|
||||
zebra-chain = { path = "../zebra-chain", version = "1.0.0-beta.36" }
|
||||
|
||||
|
|
|
|||
|
|
@ -935,6 +935,31 @@ pub enum ReadRequest {
|
|||
limit: Option<NoteCommitmentSubtreeIndex>,
|
||||
},
|
||||
|
||||
/// Looks up the balance of a set of transparent addresses.
|
||||
///
|
||||
/// Returns an [`Amount`](zebra_chain::amount::Amount) with the total
|
||||
/// balance of the set of addresses.
|
||||
AddressBalance(HashSet<transparent::Address>),
|
||||
|
||||
/// Looks up transaction hashes that were sent or received from addresses,
|
||||
/// in an inclusive blockchain height range.
|
||||
///
|
||||
/// Returns
|
||||
///
|
||||
/// * An ordered, unique map of transaction locations and hashes.
|
||||
/// * An empty map if no transactions were found for the given arguments.
|
||||
///
|
||||
/// Returned txids are in the order they appear in blocks,
|
||||
/// which ensures that they are topologically sorted
|
||||
/// (i.e. parent txids will appear before child txids).
|
||||
TransactionIdsByAddresses {
|
||||
/// The requested addresses.
|
||||
addresses: HashSet<transparent::Address>,
|
||||
|
||||
/// The blocks to be queried for transactions.
|
||||
height_range: RangeInclusive<block::Height>,
|
||||
},
|
||||
|
||||
/// Looks up utxos for the provided addresses.
|
||||
///
|
||||
/// Returns a type with found utxos and transaction information.
|
||||
|
|
@ -1007,6 +1032,8 @@ impl ReadRequest {
|
|||
ReadRequest::OrchardTree { .. } => "orchard_tree",
|
||||
ReadRequest::SaplingSubtrees { .. } => "sapling_subtrees",
|
||||
ReadRequest::OrchardSubtrees { .. } => "orchard_subtrees",
|
||||
ReadRequest::AddressBalance { .. } => "address_balance",
|
||||
ReadRequest::TransactionIdsByAddresses { .. } => "transaction_ids_by_addesses",
|
||||
ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses",
|
||||
ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => {
|
||||
"best_chain_tip_nullifiers_anchors"
|
||||
|
|
|
|||
|
|
@ -1616,6 +1616,64 @@ impl Service<ReadRequest> for ReadStateService {
|
|||
.wait_for_panics()
|
||||
}
|
||||
|
||||
// For the get_address_balance RPC.
|
||||
ReadRequest::AddressBalance(addresses) => {
|
||||
let state = self.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
span.in_scope(move || {
|
||||
let balance = state.non_finalized_state_receiver.with_watch_data(
|
||||
|non_finalized_state| {
|
||||
read::transparent_balance(
|
||||
non_finalized_state.best_chain().cloned(),
|
||||
&state.db,
|
||||
addresses,
|
||||
)
|
||||
},
|
||||
)?;
|
||||
|
||||
// The work is done in the future.
|
||||
timer.finish(module_path!(), line!(), "ReadRequest::AddressBalance");
|
||||
|
||||
Ok(ReadResponse::AddressBalance(balance))
|
||||
})
|
||||
})
|
||||
.wait_for_panics()
|
||||
}
|
||||
|
||||
// For the get_address_tx_ids RPC.
|
||||
ReadRequest::TransactionIdsByAddresses {
|
||||
addresses,
|
||||
height_range,
|
||||
} => {
|
||||
let state = self.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
span.in_scope(move || {
|
||||
let tx_ids = state.non_finalized_state_receiver.with_watch_data(
|
||||
|non_finalized_state| {
|
||||
read::transparent_tx_ids(
|
||||
non_finalized_state.best_chain(),
|
||||
&state.db,
|
||||
addresses,
|
||||
height_range,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
// The work is done in the future.
|
||||
timer.finish(
|
||||
module_path!(),
|
||||
line!(),
|
||||
"ReadRequest::TransactionIdsByAddresses",
|
||||
);
|
||||
|
||||
tx_ids.map(ReadResponse::AddressesTransactionIds)
|
||||
})
|
||||
})
|
||||
.wait_for_panics()
|
||||
}
|
||||
|
||||
// For the get_address_utxos RPC.
|
||||
ReadRequest::UtxosByAddresses(addresses) => {
|
||||
let state = self.clone();
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ use crate::{
|
|||
TransactionLocation, ValidateContextError,
|
||||
};
|
||||
|
||||
use self::index::TransparentTransfers;
|
||||
|
||||
pub mod index;
|
||||
|
||||
|
|
@ -180,6 +181,13 @@ pub struct ChainInner {
|
|||
pub(crate) sapling_nullifiers: HashSet<sapling::Nullifier>,
|
||||
/// The Orchard nullifiers revealed by `blocks`.
|
||||
pub(crate) orchard_nullifiers: HashSet<orchard::Nullifier>,
|
||||
|
||||
// Transparent Transfers
|
||||
// TODO: move to the transparent section
|
||||
//
|
||||
/// Partial transparent address index data from `blocks`.
|
||||
pub(super) partial_transparent_transfers: HashMap<transparent::Address, TransparentTransfers>,
|
||||
|
||||
// Chain Work
|
||||
//
|
||||
/// The cumulative work represented by `blocks`.
|
||||
|
|
@ -232,6 +240,7 @@ impl Chain {
|
|||
sprout_nullifiers: Default::default(),
|
||||
sapling_nullifiers: Default::default(),
|
||||
orchard_nullifiers: Default::default(),
|
||||
partial_transparent_transfers: Default::default(),
|
||||
partial_cumulative_work: Default::default(),
|
||||
history_trees_by_height: Default::default(),
|
||||
chain_value_pools: finalized_tip_chain_value_pools,
|
||||
|
|
@ -1238,6 +1247,116 @@ impl Chain {
|
|||
}
|
||||
|
||||
// Address index queries
|
||||
|
||||
/// Returns the transparent transfers for `addresses` in this non-finalized chain.
|
||||
///
|
||||
/// If none of the addresses have an address index, returns an empty iterator.
|
||||
///
|
||||
/// # Correctness
|
||||
///
|
||||
/// Callers should apply the returned indexes to the corresponding finalized state indexes.
|
||||
///
|
||||
/// The combined result will only be correct if the chains match.
|
||||
/// The exact type of match varies by query.
|
||||
pub fn partial_transparent_indexes<'a>(
|
||||
&'a self,
|
||||
addresses: &'a HashSet<transparent::Address>,
|
||||
) -> impl Iterator<Item = &TransparentTransfers> {
|
||||
addresses
|
||||
.iter()
|
||||
.flat_map(|address| self.partial_transparent_transfers.get(address))
|
||||
}
|
||||
|
||||
/// Returns the transparent balance change for `addresses` in this non-finalized chain.
|
||||
///
|
||||
/// If the balance doesn't change for any of the addresses, returns zero.
|
||||
///
|
||||
/// # Correctness
|
||||
///
|
||||
/// Callers should apply this balance change to the finalized state balance for `addresses`.
|
||||
///
|
||||
/// The total balance will only be correct if this partial chain matches the finalized state.
|
||||
/// Specifically, the root of this partial chain must be a child block of the finalized tip.
|
||||
pub fn partial_transparent_balance_change(
|
||||
&self,
|
||||
addresses: &HashSet<transparent::Address>,
|
||||
) -> Amount<NegativeAllowed> {
|
||||
let balance_change: Result<Amount<NegativeAllowed>, _> = self
|
||||
.partial_transparent_indexes(addresses)
|
||||
.map(|transfers| transfers.balance())
|
||||
.sum();
|
||||
|
||||
balance_change.expect(
|
||||
"unexpected amount overflow: value balances are valid, so partial sum should be valid",
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the transparent UTXO changes for `addresses` in this non-finalized chain.
|
||||
///
|
||||
/// If the UTXOs don't change for any of the addresses, returns empty lists.
|
||||
///
|
||||
/// # Correctness
|
||||
///
|
||||
/// Callers should apply these non-finalized UTXO changes to the finalized state UTXOs.
|
||||
///
|
||||
/// The UTXOs will only be correct if the non-finalized chain matches or overlaps with
|
||||
/// the finalized state.
|
||||
///
|
||||
/// Specifically, a block in the partial chain must be a child block of the finalized tip.
|
||||
/// (But the child block does not have to be the partial chain root.)
|
||||
pub fn partial_transparent_utxo_changes(
|
||||
&self,
|
||||
addresses: &HashSet<transparent::Address>,
|
||||
) -> (
|
||||
BTreeMap<OutputLocation, transparent::Output>,
|
||||
BTreeSet<OutputLocation>,
|
||||
) {
|
||||
let created_utxos = self
|
||||
.partial_transparent_indexes(addresses)
|
||||
.flat_map(|transfers| transfers.created_utxos())
|
||||
.map(|(out_loc, output)| (*out_loc, output.clone()))
|
||||
.collect();
|
||||
|
||||
let spent_utxos = self
|
||||
.partial_transparent_indexes(addresses)
|
||||
.flat_map(|transfers| transfers.spent_utxos())
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
(created_utxos, spent_utxos)
|
||||
}
|
||||
|
||||
/// Returns the [`transaction::Hash`]es used by `addresses` to receive or spend funds,
|
||||
/// in the non-finalized chain, filtered using the `query_height_range`.
|
||||
///
|
||||
/// If none of the addresses receive or spend funds in this partial chain, returns an empty list.
|
||||
///
|
||||
/// # Correctness
|
||||
///
|
||||
/// Callers should combine these non-finalized transactions with the finalized state transactions.
|
||||
///
|
||||
/// The transaction IDs will only be correct if the non-finalized chain matches or overlaps with
|
||||
/// the finalized state.
|
||||
///
|
||||
/// Specifically, a block in the partial chain must be a child block of the finalized tip.
|
||||
/// (But the child block does not have to be the partial chain root.)
|
||||
///
|
||||
/// This condition does not apply if there is only one address.
|
||||
/// Since address transactions are only appended by blocks,
|
||||
/// and the finalized state query reads them in order,
|
||||
/// it is impossible to get inconsistent transactions for a single address.
|
||||
pub fn partial_transparent_tx_ids(
|
||||
&self,
|
||||
addresses: &HashSet<transparent::Address>,
|
||||
query_height_range: RangeInclusive<Height>,
|
||||
) -> BTreeMap<TransactionLocation, transaction::Hash> {
|
||||
self.partial_transparent_indexes(addresses)
|
||||
.flat_map(|transfers| {
|
||||
transfers.tx_ids(&self.tx_loc_by_hash, query_height_range.clone())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Update the chain tip with the `contextually_valid` block,
|
||||
/// running note commitment tree updates in parallel with other updates.
|
||||
///
|
||||
|
|
@ -1406,6 +1525,11 @@ impl Chain {
|
|||
"transactions must be unique within a single chain"
|
||||
);
|
||||
|
||||
// add the utxos this produced
|
||||
self.update_chain_tip_with(&(outputs, &transaction_hash, new_outputs))?;
|
||||
// delete the utxos this consumed
|
||||
self.update_chain_tip_with(&(inputs, &transaction_hash, spent_outputs))?;
|
||||
|
||||
// add the shielded data
|
||||
self.update_chain_tip_with(joinsplit_data)?;
|
||||
self.update_chain_tip_with(sapling_shielded_data_per_spend_anchor)?;
|
||||
|
|
@ -1552,6 +1676,11 @@ impl UpdateWith<ContextuallyVerifiedBlock> for Chain {
|
|||
),
|
||||
};
|
||||
|
||||
// remove the utxos this produced
|
||||
self.revert_chain_with(&(outputs, transaction_hash, new_outputs), position);
|
||||
// reset the utxos this consumed
|
||||
self.revert_chain_with(&(inputs, transaction_hash, spent_outputs), position);
|
||||
|
||||
// TODO: move this to the history tree UpdateWith.revert...()?
|
||||
// remove `transaction.hash` from `tx_loc_by_hash`
|
||||
assert!(
|
||||
|
|
@ -1579,6 +1708,219 @@ impl UpdateWith<ContextuallyVerifiedBlock> for Chain {
|
|||
}
|
||||
}
|
||||
|
||||
// Created UTXOs
|
||||
//
|
||||
// TODO: replace arguments with a struct
|
||||
impl
|
||||
UpdateWith<(
|
||||
// The outputs from a transaction in this block
|
||||
&Vec<transparent::Output>,
|
||||
// The hash of the transaction that the outputs are from
|
||||
&transaction::Hash,
|
||||
// The UTXOs for all outputs created by this transaction (or block)
|
||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||||
)> for Chain
|
||||
{
|
||||
#[allow(clippy::unwrap_in_result)]
|
||||
fn update_chain_tip_with(
|
||||
&mut self,
|
||||
&(created_outputs, creating_tx_hash, block_created_outputs): &(
|
||||
&Vec<transparent::Output>,
|
||||
&transaction::Hash,
|
||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||||
),
|
||||
) -> Result<(), ValidateContextError> {
|
||||
for output_index in 0..created_outputs.len() {
|
||||
let outpoint = transparent::OutPoint {
|
||||
hash: *creating_tx_hash,
|
||||
index: output_index.try_into().expect("valid indexes fit in u32"),
|
||||
};
|
||||
let created_utxo = block_created_outputs
|
||||
.get(&outpoint)
|
||||
.expect("new_outputs contains all created UTXOs");
|
||||
|
||||
// Update the chain's created UTXOs
|
||||
let previous_entry = self.created_utxos.insert(outpoint, created_utxo.clone());
|
||||
assert_eq!(
|
||||
previous_entry, None,
|
||||
"unexpected created output: duplicate update or duplicate UTXO",
|
||||
);
|
||||
|
||||
// Update the address index with this UTXO
|
||||
if let Some(receiving_address) = created_utxo.utxo.output.address(&self.network) {
|
||||
let address_transfers = self
|
||||
.partial_transparent_transfers
|
||||
.entry(receiving_address)
|
||||
.or_default();
|
||||
|
||||
address_transfers.update_chain_tip_with(&(&outpoint, created_utxo))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn revert_chain_with(
|
||||
&mut self,
|
||||
&(created_outputs, creating_tx_hash, block_created_outputs): &(
|
||||
&Vec<transparent::Output>,
|
||||
&transaction::Hash,
|
||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||||
),
|
||||
position: RevertPosition,
|
||||
) {
|
||||
for output_index in 0..created_outputs.len() {
|
||||
let outpoint = transparent::OutPoint {
|
||||
hash: *creating_tx_hash,
|
||||
index: output_index.try_into().expect("valid indexes fit in u32"),
|
||||
};
|
||||
let created_utxo = block_created_outputs
|
||||
.get(&outpoint)
|
||||
.expect("new_outputs contains all created UTXOs");
|
||||
|
||||
// Revert the chain's created UTXOs
|
||||
let removed_entry = self.created_utxos.remove(&outpoint);
|
||||
assert!(
|
||||
removed_entry.is_some(),
|
||||
"unexpected revert of created output: duplicate revert or duplicate UTXO",
|
||||
);
|
||||
|
||||
// Revert the address index for this UTXO
|
||||
if let Some(receiving_address) = created_utxo.utxo.output.address(&self.network) {
|
||||
let address_transfers = self
|
||||
.partial_transparent_transfers
|
||||
.get_mut(&receiving_address)
|
||||
.expect("block has previously been applied to the chain");
|
||||
|
||||
address_transfers.revert_chain_with(&(&outpoint, created_utxo), position);
|
||||
|
||||
// Remove this transfer if it is now empty
|
||||
if address_transfers.is_empty() {
|
||||
self.partial_transparent_transfers
|
||||
.remove(&receiving_address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transparent inputs
|
||||
//
|
||||
// TODO: replace arguments with a struct
|
||||
impl
|
||||
UpdateWith<(
|
||||
// The inputs from a transaction in this block
|
||||
&Vec<transparent::Input>,
|
||||
// The hash of the transaction that the inputs are from
|
||||
// (not the transaction the spent output was created by)
|
||||
&transaction::Hash,
|
||||
// The outputs for all inputs spent in this transaction (or block)
|
||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||||
)> for Chain
|
||||
{
|
||||
fn update_chain_tip_with(
|
||||
&mut self,
|
||||
&(spending_inputs, spending_tx_hash, spent_outputs): &(
|
||||
&Vec<transparent::Input>,
|
||||
&transaction::Hash,
|
||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||||
),
|
||||
) -> Result<(), ValidateContextError> {
|
||||
for spending_input in spending_inputs.iter() {
|
||||
let spent_outpoint = if let Some(spent_outpoint) = spending_input.outpoint() {
|
||||
spent_outpoint
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Index the spent outpoint in the chain
|
||||
let first_spend = self.spent_utxos.insert(spent_outpoint);
|
||||
assert!(
|
||||
first_spend,
|
||||
"unexpected duplicate spent output: should be checked earlier"
|
||||
);
|
||||
|
||||
// TODO: fix tests to supply correct spent outputs, then turn this into an expect()
|
||||
let spent_output = if let Some(spent_output) = spent_outputs.get(&spent_outpoint) {
|
||||
spent_output
|
||||
} else if !cfg!(test) {
|
||||
panic!("unexpected missing spent output: all spent outputs must be indexed");
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Index the spent output for the address
|
||||
if let Some(spending_address) = spent_output.utxo.output.address(&self.network) {
|
||||
let address_transfers = self
|
||||
.partial_transparent_transfers
|
||||
.entry(spending_address)
|
||||
.or_default();
|
||||
|
||||
address_transfers.update_chain_tip_with(&(
|
||||
spending_input,
|
||||
spending_tx_hash,
|
||||
spent_output,
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn revert_chain_with(
|
||||
&mut self,
|
||||
&(spending_inputs, spending_tx_hash, spent_outputs): &(
|
||||
&Vec<transparent::Input>,
|
||||
&transaction::Hash,
|
||||
&HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
|
||||
),
|
||||
position: RevertPosition,
|
||||
) {
|
||||
for spending_input in spending_inputs.iter() {
|
||||
let spent_outpoint = if let Some(spent_outpoint) = spending_input.outpoint() {
|
||||
spent_outpoint
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Revert the spent outpoint in the chain
|
||||
let spent_outpoint_was_removed = self.spent_utxos.remove(&spent_outpoint);
|
||||
assert!(
|
||||
spent_outpoint_was_removed,
|
||||
"spent_utxos must be present if block was added to chain"
|
||||
);
|
||||
|
||||
// TODO: fix tests to supply correct spent outputs, then turn this into an expect()
|
||||
let spent_output = if let Some(spent_output) = spent_outputs.get(&spent_outpoint) {
|
||||
spent_output
|
||||
} else if !cfg!(test) {
|
||||
panic!(
|
||||
"unexpected missing reverted spent output: all spent outputs must be indexed"
|
||||
);
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Revert the spent output for the address
|
||||
if let Some(receiving_address) = spent_output.utxo.output.address(&self.network) {
|
||||
let address_transfers = self
|
||||
.partial_transparent_transfers
|
||||
.get_mut(&receiving_address)
|
||||
.expect("block has previously been applied to the chain");
|
||||
|
||||
address_transfers
|
||||
.revert_chain_with(&(spending_input, spending_tx_hash, spent_output), position);
|
||||
|
||||
// Remove this transfer if it is now empty
|
||||
if address_transfers.is_empty() {
|
||||
self.partial_transparent_transfers
|
||||
.remove(&receiving_address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateWith<Option<transaction::JoinSplitData<Groth16Proof>>> for Chain {
|
||||
#[instrument(skip(self, joinsplit_data))]
|
||||
fn update_chain_tip_with(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,284 @@ use crate::{OutputLocation, TransactionLocation, ValidateContextError};
|
|||
|
||||
use super::{RevertPosition, UpdateWith};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TransparentTransfers {
|
||||
/// The partial chain balance for a transparent address.
|
||||
balance: Amount<NegativeAllowed>,
|
||||
|
||||
/// The partial list of transactions that spent or received UTXOs to a transparent address.
|
||||
///
|
||||
/// Since transactions can only be added to this set, it does not need
|
||||
/// special handling for
|
||||
/// [`ReadStateService`](crate::service::ReadStateService) response
|
||||
/// inconsistencies.
|
||||
///
|
||||
/// The `getaddresstxids` RPC needs these transaction IDs to be sorted in chain order.
|
||||
tx_ids: MultiSet<transaction::Hash>,
|
||||
|
||||
/// The partial list of UTXOs received by a transparent address.
|
||||
///
|
||||
/// The `getaddressutxos` RPC doesn't need these transaction IDs to be sorted in chain order,
|
||||
/// but it might in future. So Zebra does it anyway.
|
||||
///
|
||||
/// Optional TODOs:
|
||||
/// - store `Utxo`s in the chain, and just store the created locations for this address
|
||||
/// - if we add an OutputLocation to UTXO, remove this OutputLocation,
|
||||
/// and use the inner OutputLocation to sort Utxos in chain order
|
||||
created_utxos: BTreeMap<OutputLocation, transparent::Output>,
|
||||
|
||||
/// The partial list of UTXOs spent by a transparent address.
|
||||
///
|
||||
/// The `getaddressutxos` RPC doesn't need these transaction IDs to be sorted in chain order,
|
||||
/// but it might in future. So Zebra does it anyway.
|
||||
///
|
||||
/// Optional TODO:
|
||||
/// - store spent `Utxo`s by location in the chain, use the chain spent UTXOs to filter,
|
||||
/// and stop storing spent UTXOs by address
|
||||
spent_utxos: BTreeSet<OutputLocation>,
|
||||
}
|
||||
|
||||
// A created UTXO
|
||||
//
|
||||
// TODO: replace arguments with a struct
|
||||
impl
|
||||
UpdateWith<(
|
||||
// The location of the UTXO
|
||||
&transparent::OutPoint,
|
||||
// The UTXO data
|
||||
// Includes the location of the transaction that created the output
|
||||
&transparent::OrderedUtxo,
|
||||
)> for TransparentTransfers
|
||||
{
|
||||
#[allow(clippy::unwrap_in_result)]
|
||||
fn update_chain_tip_with(
|
||||
&mut self,
|
||||
&(outpoint, created_utxo): &(&transparent::OutPoint, &transparent::OrderedUtxo),
|
||||
) -> Result<(), ValidateContextError> {
|
||||
self.balance = (self.balance
|
||||
+ created_utxo
|
||||
.utxo
|
||||
.output
|
||||
.value()
|
||||
.constrain()
|
||||
.expect("NonNegative values are always valid NegativeAllowed values"))
|
||||
.expect("total UTXO value has already been checked");
|
||||
|
||||
let transaction_location = transaction_location(created_utxo);
|
||||
let output_location = OutputLocation::from_outpoint(transaction_location, outpoint);
|
||||
|
||||
let previous_entry = self
|
||||
.created_utxos
|
||||
.insert(output_location, created_utxo.utxo.output.clone());
|
||||
assert_eq!(
|
||||
previous_entry, None,
|
||||
"unexpected created output: duplicate update or duplicate UTXO",
|
||||
);
|
||||
|
||||
self.tx_ids.insert(outpoint.hash);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn revert_chain_with(
|
||||
&mut self,
|
||||
&(outpoint, created_utxo): &(&transparent::OutPoint, &transparent::OrderedUtxo),
|
||||
_position: RevertPosition,
|
||||
) {
|
||||
self.balance = (self.balance
|
||||
- created_utxo
|
||||
.utxo
|
||||
.output
|
||||
.value()
|
||||
.constrain()
|
||||
.expect("NonNegative values are always valid NegativeAllowed values"))
|
||||
.expect("reversing previous balance changes is always valid");
|
||||
|
||||
let transaction_location = transaction_location(created_utxo);
|
||||
let output_location = OutputLocation::from_outpoint(transaction_location, outpoint);
|
||||
|
||||
let removed_entry = self.created_utxos.remove(&output_location);
|
||||
assert!(
|
||||
removed_entry.is_some(),
|
||||
"unexpected revert of created output: duplicate update or duplicate UTXO",
|
||||
);
|
||||
|
||||
let tx_id_was_removed = self.tx_ids.remove(&outpoint.hash);
|
||||
assert!(
|
||||
tx_id_was_removed,
|
||||
"unexpected revert of created output transaction: \
|
||||
duplicate revert, or revert of an output that was never updated",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A transparent input
|
||||
//
|
||||
// TODO: replace arguments with a struct
|
||||
impl
|
||||
UpdateWith<(
|
||||
// The transparent input data
|
||||
&transparent::Input,
|
||||
// The hash of the transaction the input is from
|
||||
// (not the transaction the spent output was created by)
|
||||
&transaction::Hash,
|
||||
// The output spent by the input
|
||||
// Includes the location of the transaction that created the output
|
||||
&transparent::OrderedUtxo,
|
||||
)> for TransparentTransfers
|
||||
{
|
||||
#[allow(clippy::unwrap_in_result)]
|
||||
fn update_chain_tip_with(
|
||||
&mut self,
|
||||
&(spending_input, spending_tx_hash, spent_output): &(
|
||||
&transparent::Input,
|
||||
&transaction::Hash,
|
||||
&transparent::OrderedUtxo,
|
||||
),
|
||||
) -> Result<(), ValidateContextError> {
|
||||
// Spending a UTXO subtracts value from the balance
|
||||
self.balance = (self.balance
|
||||
- spent_output
|
||||
.utxo
|
||||
.output
|
||||
.value()
|
||||
.constrain()
|
||||
.expect("NonNegative values are always valid NegativeAllowed values"))
|
||||
.expect("total UTXO value has already been checked");
|
||||
|
||||
let spent_outpoint = spending_input.outpoint().expect("checked by caller");
|
||||
|
||||
let spent_output_tx_loc = transaction_location(spent_output);
|
||||
let output_location = OutputLocation::from_outpoint(spent_output_tx_loc, &spent_outpoint);
|
||||
let spend_was_inserted = self.spent_utxos.insert(output_location);
|
||||
assert!(
|
||||
spend_was_inserted,
|
||||
"unexpected spent output: duplicate update or duplicate spend",
|
||||
);
|
||||
|
||||
self.tx_ids.insert(*spending_tx_hash);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn revert_chain_with(
|
||||
&mut self,
|
||||
&(spending_input, spending_tx_hash, spent_output): &(
|
||||
&transparent::Input,
|
||||
&transaction::Hash,
|
||||
&transparent::OrderedUtxo,
|
||||
),
|
||||
_position: RevertPosition,
|
||||
) {
|
||||
self.balance = (self.balance
|
||||
+ spent_output
|
||||
.utxo
|
||||
.output
|
||||
.value()
|
||||
.constrain()
|
||||
.expect("NonNegative values are always valid NegativeAllowed values"))
|
||||
.expect("reversing previous balance changes is always valid");
|
||||
|
||||
let spent_outpoint = spending_input.outpoint().expect("checked by caller");
|
||||
|
||||
let spent_output_tx_loc = transaction_location(spent_output);
|
||||
let output_location = OutputLocation::from_outpoint(spent_output_tx_loc, &spent_outpoint);
|
||||
let spend_was_removed = self.spent_utxos.remove(&output_location);
|
||||
assert!(
|
||||
spend_was_removed,
|
||||
"unexpected revert of spent output: \
|
||||
duplicate revert, or revert of a spent output that was never updated",
|
||||
);
|
||||
|
||||
let tx_id_was_removed = self.tx_ids.remove(spending_tx_hash);
|
||||
assert!(
|
||||
tx_id_was_removed,
|
||||
"unexpected revert of spending input transaction: \
|
||||
duplicate revert, or revert of an input that was never updated",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl TransparentTransfers {
|
||||
/// Returns true if there are no transfers for this address.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.balance == Amount::<NegativeAllowed>::zero()
|
||||
&& self.tx_ids.is_empty()
|
||||
&& self.created_utxos.is_empty()
|
||||
&& self.spent_utxos.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the partial balance for this address.
|
||||
#[allow(dead_code)]
|
||||
pub fn balance(&self) -> Amount<NegativeAllowed> {
|
||||
self.balance
|
||||
}
|
||||
|
||||
/// Returns the [`transaction::Hash`]es of the transactions that sent or
|
||||
/// received transparent transfers to this address, in this partial chain,
|
||||
/// filtered by `query_height_range`.
|
||||
///
|
||||
/// The transactions are returned in chain order.
|
||||
///
|
||||
/// `chain_tx_loc_by_hash` should be the `tx_loc_by_hash` field from the
|
||||
/// [`Chain`][1] containing this index.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If `chain_tx_loc_by_hash` is missing some transaction hashes from this
|
||||
/// index.
|
||||
///
|
||||
/// [1]: super::super::Chain
|
||||
pub fn tx_ids(
|
||||
&self,
|
||||
chain_tx_loc_by_hash: &HashMap<transaction::Hash, TransactionLocation>,
|
||||
query_height_range: RangeInclusive<Height>,
|
||||
) -> BTreeMap<TransactionLocation, transaction::Hash> {
|
||||
self.tx_ids
|
||||
.distinct_elements()
|
||||
.filter_map(|tx_hash| {
|
||||
let tx_loc = *chain_tx_loc_by_hash
|
||||
.get(tx_hash)
|
||||
.expect("all hashes are indexed");
|
||||
|
||||
if query_height_range.contains(&tx_loc.height) {
|
||||
Some((tx_loc, *tx_hash))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the new transparent outputs sent to this address,
|
||||
/// in this partial chain, in chain order.
|
||||
///
|
||||
/// Some of these outputs might already be spent.
|
||||
/// [`TransparentTransfers::spent_utxos`] returns spent UTXOs.
|
||||
#[allow(dead_code)]
|
||||
pub fn created_utxos(&self) -> &BTreeMap<OutputLocation, transparent::Output> {
|
||||
&self.created_utxos
|
||||
}
|
||||
|
||||
/// Returns the [`OutputLocation`]s of the spent transparent outputs sent to this address,
|
||||
/// in this partial chain, in chain order.
|
||||
#[allow(dead_code)]
|
||||
pub fn spent_utxos(&self) -> &BTreeSet<OutputLocation> {
|
||||
&self.spent_utxos
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TransparentTransfers {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
balance: Amount::zero(),
|
||||
tx_ids: Default::default(),
|
||||
created_utxos: Default::default(),
|
||||
spent_utxos: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the transaction location for an [`transparent::OrderedUtxo`].
|
||||
pub fn transaction_location(ordered_utxo: &transparent::OrderedUtxo) -> TransactionLocation {
|
||||
TransactionLocation::from_usize(ordered_utxo.utxo.height, ordered_utxo.tx_index_in_block)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ pub mod difficulty;
|
|||
mod tests;
|
||||
|
||||
pub use address::{
|
||||
balance::transparent_balance,
|
||||
tx_id::transparent_tx_ids,
|
||||
utxo::{address_utxos, AddressUtxos},
|
||||
};
|
||||
pub use block::{
|
||||
|
|
|
|||
|
|
@ -26,6 +26,116 @@ use crate::{
|
|||
BoxError,
|
||||
};
|
||||
|
||||
/// Returns the total transparent balance for the supplied [`transparent::Address`]es.
|
||||
///
|
||||
/// If the addresses do not exist in the non-finalized `chain` or finalized `db`, returns zero.
|
||||
pub fn transparent_balance(
|
||||
chain: Option<Arc<Chain>>,
|
||||
db: &ZebraDb,
|
||||
addresses: HashSet<transparent::Address>,
|
||||
) -> Result<Amount<NonNegative>, BoxError> {
|
||||
let mut balance_result = finalized_transparent_balance(db, &addresses);
|
||||
|
||||
// Retry the finalized balance query if it was interrupted by a finalizing block
|
||||
//
|
||||
// TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn
|
||||
for _ in 0..FINALIZED_STATE_QUERY_RETRIES {
|
||||
if balance_result.is_ok() {
|
||||
break;
|
||||
}
|
||||
|
||||
balance_result = finalized_transparent_balance(db, &addresses);
|
||||
}
|
||||
|
||||
let (mut balance, finalized_tip) = balance_result?;
|
||||
|
||||
// Apply the non-finalized balance changes
|
||||
if let Some(chain) = chain {
|
||||
let chain_balance_change =
|
||||
chain_transparent_balance_change(chain, &addresses, finalized_tip);
|
||||
|
||||
balance = apply_balance_change(balance, chain_balance_change).expect(
|
||||
"unexpected amount overflow: value balances are valid, so partial sum should be valid",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
/// Returns the total transparent balance for `addresses` in the finalized chain,
|
||||
/// and the finalized tip height the balances were queried at.
|
||||
///
|
||||
/// If the addresses do not exist in the finalized `db`, returns zero.
|
||||
//
|
||||
// TODO: turn the return type into a struct?
|
||||
fn finalized_transparent_balance(
|
||||
db: &ZebraDb,
|
||||
addresses: &HashSet<transparent::Address>,
|
||||
) -> Result<(Amount<NonNegative>, Option<Height>), BoxError> {
|
||||
// # Correctness
|
||||
//
|
||||
// The StateService can commit additional blocks while we are querying address balances.
|
||||
|
||||
// Check if the finalized state changed while we were querying it
|
||||
let original_finalized_tip = db.tip();
|
||||
|
||||
let finalized_balance = db.partial_finalized_transparent_balance(addresses);
|
||||
|
||||
let finalized_tip = db.tip();
|
||||
|
||||
if original_finalized_tip != finalized_tip {
|
||||
// Correctness: Some balances might be from before the block, and some after
|
||||
return Err("unable to get balance: state was committing a block".into());
|
||||
}
|
||||
|
||||
let finalized_tip = finalized_tip.map(|(height, _hash)| height);
|
||||
|
||||
Ok((finalized_balance, finalized_tip))
|
||||
}
|
||||
|
||||
/// Returns the total transparent balance change for `addresses` in the non-finalized chain,
|
||||
/// matching the balance for the `finalized_tip`.
|
||||
///
|
||||
/// If the addresses do not exist in the non-finalized `chain`, returns zero.
|
||||
fn chain_transparent_balance_change(
|
||||
mut chain: Arc<Chain>,
|
||||
addresses: &HashSet<transparent::Address>,
|
||||
finalized_tip: Option<Height>,
|
||||
) -> Amount<NegativeAllowed> {
|
||||
// # Correctness
|
||||
//
|
||||
// Find the balance adjustment that corrects for overlapping finalized and non-finalized blocks.
|
||||
|
||||
// Check if the finalized and non-finalized states match
|
||||
let required_chain_root = finalized_tip
|
||||
.map(|tip| (tip + 1).unwrap())
|
||||
.unwrap_or(Height(0));
|
||||
|
||||
let chain = Arc::make_mut(&mut chain);
|
||||
|
||||
assert!(
|
||||
chain.non_finalized_root_height() <= required_chain_root,
|
||||
"unexpected chain gap: the best chain is updated after its previous root is finalized"
|
||||
);
|
||||
|
||||
let chain_tip = chain.non_finalized_tip_height();
|
||||
|
||||
// If we've already committed this entire chain, ignore its balance changes.
|
||||
// This is more likely if the non-finalized state is just getting started.
|
||||
if chain_tip < required_chain_root {
|
||||
return Amount::zero();
|
||||
}
|
||||
|
||||
// Correctness: some balances might have duplicate creates or spends,
|
||||
// so we pop root blocks from `chain` until the chain root is a child of the finalized tip.
|
||||
while chain.non_finalized_root_height() < required_chain_root {
|
||||
// TODO: just revert the transparent balances, to improve performance
|
||||
chain.pop_root();
|
||||
}
|
||||
|
||||
chain.partial_transparent_balance_change(addresses)
|
||||
}
|
||||
|
||||
/// Add the supplied finalized and non-finalized balances together,
|
||||
/// and return the result.
|
||||
fn apply_balance_change(
|
||||
|
|
|
|||
|
|
@ -25,6 +25,249 @@ use crate::{
|
|||
BoxError, TransactionLocation,
|
||||
};
|
||||
|
||||
/// Returns the transaction IDs that sent or received funds from the supplied [`transparent::Address`]es,
|
||||
/// within `query_height_range`, in chain order.
|
||||
///
|
||||
/// If the addresses do not exist in the non-finalized `chain` or finalized `db`,
|
||||
/// or the `query_height_range` is totally outside both the `chain` and `db` range,
|
||||
/// returns an empty list.
|
||||
pub fn transparent_tx_ids<C>(
|
||||
chain: Option<C>,
|
||||
db: &ZebraDb,
|
||||
addresses: HashSet<transparent::Address>,
|
||||
query_height_range: RangeInclusive<Height>,
|
||||
) -> Result<BTreeMap<TransactionLocation, transaction::Hash>, BoxError>
|
||||
where
|
||||
C: AsRef<Chain>,
|
||||
{
|
||||
let mut tx_id_error = None;
|
||||
|
||||
// Retry the finalized tx ID query if it was interrupted by a finalizing block,
|
||||
// and the non-finalized chain doesn't overlap the changed heights.
|
||||
//
|
||||
// TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn
|
||||
for _ in 0..=FINALIZED_STATE_QUERY_RETRIES {
|
||||
let (finalized_tx_ids, finalized_tip_range) =
|
||||
finalized_transparent_tx_ids(db, &addresses, query_height_range.clone());
|
||||
|
||||
// Apply the non-finalized tx ID changes.
|
||||
let chain_tx_id_changes = chain_transparent_tx_id_changes(
|
||||
chain.as_ref(),
|
||||
&addresses,
|
||||
finalized_tip_range,
|
||||
query_height_range.clone(),
|
||||
);
|
||||
|
||||
// If the tx IDs are valid, return them, otherwise, retry or return an error.
|
||||
match chain_tx_id_changes {
|
||||
Ok(chain_tx_id_changes) => {
|
||||
let tx_ids = apply_tx_id_changes(finalized_tx_ids, chain_tx_id_changes);
|
||||
|
||||
return Ok(tx_ids);
|
||||
}
|
||||
|
||||
Err(error) => tx_id_error = Some(Err(error)),
|
||||
}
|
||||
}
|
||||
|
||||
tx_id_error.expect("unexpected missing error: attempts should set error or return")
|
||||
}
|
||||
|
||||
/// Returns the [`transaction::Hash`]es for `addresses` in the finalized chain `query_height_range`,
|
||||
/// and the finalized tip heights the transaction IDs were queried at.
|
||||
///
|
||||
/// If the addresses do not exist in the finalized `db`, returns an empty list.
|
||||
//
|
||||
// TODO: turn the return type into a struct?
|
||||
fn finalized_transparent_tx_ids(
|
||||
db: &ZebraDb,
|
||||
addresses: &HashSet<transparent::Address>,
|
||||
query_height_range: RangeInclusive<Height>,
|
||||
) -> (
|
||||
BTreeMap<TransactionLocation, transaction::Hash>,
|
||||
Option<RangeInclusive<Height>>,
|
||||
) {
|
||||
// # Correctness
|
||||
//
|
||||
// The StateService can commit additional blocks while we are querying transaction IDs.
|
||||
|
||||
// Check if the finalized state changed while we were querying it
|
||||
let start_finalized_tip = db.finalized_tip_height();
|
||||
|
||||
let finalized_tx_ids = db.partial_finalized_transparent_tx_ids(addresses, query_height_range);
|
||||
|
||||
let end_finalized_tip = db.finalized_tip_height();
|
||||
|
||||
let finalized_tip_range = if let (Some(start_finalized_tip), Some(end_finalized_tip)) =
|
||||
(start_finalized_tip, end_finalized_tip)
|
||||
{
|
||||
Some(start_finalized_tip..=end_finalized_tip)
|
||||
} else {
|
||||
// State is empty
|
||||
None
|
||||
};
|
||||
|
||||
(finalized_tx_ids, finalized_tip_range)
|
||||
}
|
||||
|
||||
/// Returns the extra transaction IDs for `addresses` in the non-finalized chain `query_height_range`,
|
||||
/// matching or overlapping the transaction IDs for the `finalized_tip_range`,
|
||||
///
|
||||
/// If the addresses do not exist in the non-finalized `chain`, returns an empty list.
|
||||
//
|
||||
// TODO: turn the return type into a struct?
|
||||
fn chain_transparent_tx_id_changes<C>(
|
||||
chain: Option<C>,
|
||||
addresses: &HashSet<transparent::Address>,
|
||||
finalized_tip_range: Option<RangeInclusive<Height>>,
|
||||
query_height_range: RangeInclusive<Height>,
|
||||
) -> Result<BTreeMap<TransactionLocation, transaction::Hash>, BoxError>
|
||||
where
|
||||
C: AsRef<Chain>,
|
||||
{
|
||||
let address_count = addresses.len();
|
||||
|
||||
let finalized_tip_range = match finalized_tip_range {
|
||||
Some(finalized_tip_range) => finalized_tip_range,
|
||||
None => {
|
||||
assert!(
|
||||
chain.is_none(),
|
||||
"unexpected non-finalized chain when finalized state is empty"
|
||||
);
|
||||
|
||||
debug!(
|
||||
?finalized_tip_range,
|
||||
?address_count,
|
||||
"chain address tx ID query: state is empty, no tx IDs available",
|
||||
);
|
||||
|
||||
return Ok(Default::default());
|
||||
}
|
||||
};
|
||||
|
||||
// # Correctness
|
||||
//
|
||||
// We can compensate for addresses with mismatching blocks,
|
||||
// by adding the overlapping non-finalized transaction IDs.
|
||||
//
|
||||
// If there is only one address, mismatches aren't possible,
|
||||
// because tx IDs are added to the finalized state in chain order (and never removed),
|
||||
// and they are queried in chain order.
|
||||
|
||||
// Check if the finalized and non-finalized states match or overlap
|
||||
let required_min_non_finalized_root = finalized_tip_range.start().0 + 1;
|
||||
|
||||
// Work out if we need to compensate for finalized query results from multiple heights:
|
||||
// - Ok contains the finalized tip height (no need to compensate)
|
||||
// - Err contains the required non-finalized chain overlap
|
||||
let finalized_tip_status = required_min_non_finalized_root..=finalized_tip_range.end().0;
|
||||
let finalized_tip_status = if finalized_tip_status.is_empty() {
|
||||
let finalized_tip_height = *finalized_tip_range.end();
|
||||
Ok(finalized_tip_height)
|
||||
} else {
|
||||
let required_non_finalized_overlap = finalized_tip_status;
|
||||
Err(required_non_finalized_overlap)
|
||||
};
|
||||
|
||||
if chain.is_none() {
|
||||
if address_count <= 1 || finalized_tip_status.is_ok() {
|
||||
debug!(
|
||||
?finalized_tip_status,
|
||||
?required_min_non_finalized_root,
|
||||
?finalized_tip_range,
|
||||
?address_count,
|
||||
"chain address tx ID query: \
|
||||
finalized chain is consistent, and non-finalized chain is empty",
|
||||
);
|
||||
|
||||
return Ok(Default::default());
|
||||
} else {
|
||||
// We can't compensate for inconsistent database queries,
|
||||
// because the non-finalized chain is empty.
|
||||
debug!(
|
||||
?finalized_tip_status,
|
||||
?required_min_non_finalized_root,
|
||||
?finalized_tip_range,
|
||||
?address_count,
|
||||
"chain address tx ID query: \
|
||||
finalized tip query was inconsistent, but non-finalized chain is empty",
|
||||
);
|
||||
|
||||
return Err("unable to get tx IDs: \
|
||||
state was committing a block, and non-finalized chain is empty"
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
let chain = chain.unwrap();
|
||||
let chain = chain.as_ref();
|
||||
|
||||
let non_finalized_root = chain.non_finalized_root_height();
|
||||
let non_finalized_tip = chain.non_finalized_tip_height();
|
||||
|
||||
assert!(
|
||||
non_finalized_root.0 <= required_min_non_finalized_root,
|
||||
"unexpected chain gap: the best chain is updated after its previous root is finalized",
|
||||
);
|
||||
|
||||
match finalized_tip_status {
|
||||
Ok(finalized_tip_height) => {
|
||||
// If we've already committed this entire chain, ignore its UTXO changes.
|
||||
// This is more likely if the non-finalized state is just getting started.
|
||||
if finalized_tip_height >= non_finalized_tip {
|
||||
debug!(
|
||||
?non_finalized_root,
|
||||
?non_finalized_tip,
|
||||
?finalized_tip_status,
|
||||
?finalized_tip_range,
|
||||
?address_count,
|
||||
"chain address tx ID query: \
|
||||
non-finalized blocks have all been finalized, no new UTXO changes",
|
||||
);
|
||||
|
||||
return Ok(Default::default());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ref required_non_finalized_overlap) => {
|
||||
// We can't compensate for inconsistent database queries,
|
||||
// because the non-finalized chain is below the inconsistent query range.
|
||||
if address_count > 1 && *required_non_finalized_overlap.end() > non_finalized_tip.0 {
|
||||
debug!(
|
||||
?non_finalized_root,
|
||||
?non_finalized_tip,
|
||||
?finalized_tip_status,
|
||||
?finalized_tip_range,
|
||||
?address_count,
|
||||
"chain address tx ID query: \
|
||||
finalized tip query was inconsistent, \
|
||||
some inconsistent blocks are missing from the non-finalized chain, \
|
||||
and the query has multiple addresses",
|
||||
);
|
||||
|
||||
return Err("unable to get tx IDs: \
|
||||
state was committing a block, \
|
||||
that is missing from the non-finalized chain, \
|
||||
and the query has multiple addresses"
|
||||
.into());
|
||||
}
|
||||
|
||||
// Correctness: some finalized UTXOs might have duplicate creates or spends,
|
||||
// but we've just checked they can be corrected by applying the non-finalized UTXO changes.
|
||||
assert!(
|
||||
address_count <= 1
|
||||
|| required_non_finalized_overlap
|
||||
.clone()
|
||||
.all(|height| chain.blocks.contains_key(&Height(height))),
|
||||
"tx ID query inconsistency: \
|
||||
chain must contain required overlap blocks \
|
||||
or query must only have one address",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(chain.partial_transparent_tx_ids(addresses, query_height_range))
|
||||
}
|
||||
|
||||
/// Returns the combined finalized and non-finalized transaction IDs.
|
||||
fn apply_tx_id_changes(
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ tokio = { version = "1.37.0", features = ["full"], optional = true }
|
|||
|
||||
jsonrpc = { version = "0.18.0", optional = true }
|
||||
|
||||
zcash_primitives = { version = "0.13.0", optional = true }
|
||||
zcash_primitives = { version = "0.13.0-rc.1", optional = true }
|
||||
zcash_client_backend = {version = "0.10.0-rc.1", optional = true}
|
||||
|
||||
# For the openapi generator
|
||||
|
|
|
|||
|
|
@ -622,7 +622,6 @@ fn config_tests() -> Result<()> {
|
|||
invalid_generated_config()?;
|
||||
|
||||
// Check that we have a current version of the config stored
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
last_config_is_stored()?;
|
||||
|
||||
// Check that Zebra's previous configurations still work
|
||||
|
|
|
|||
|
|
@ -60,9 +60,6 @@ max_connections_per_ip = 1
|
|||
network = "Testnet"
|
||||
peerset_initial_target_size = 25
|
||||
|
||||
[network.testnet_parameters]
|
||||
network_name = "ConfiguredTestnet_1"
|
||||
|
||||
[network.testnet_parameters.activation_heights]
|
||||
BeforeOverwinter = 1
|
||||
Overwinter = 207_500
|
||||
|
|
|
|||
Loading…
Reference in New Issue