pub(crate) mod node_backed_indexer;
use crate::SendFut;
use tokio::{sync::mpsc, time::timeout};
use tracing::warn;
use zaino_fetch::jsonrpsee::response::{
address_deltas::{GetAddressDeltasParams, GetAddressDeltasResponse},
block_deltas::BlockDeltas,
block_header::GetBlockHeader,
block_subsidy::GetBlockSubsidy,
chain_tips::GetChainTipsResponse,
mining_info::GetMiningInfoWire,
peer_info::GetPeerInfo,
z_validate_address::{
InvalidZValidateAddress, KnownZValidateAddress, ZValidateAddressResponse,
DEPRECATION_NOTICE as Z_VALIDATE_DEPRECATION,
},
GetMempoolInfoResponse, GetNetworkSolPsResponse, GetSpentInfoRequest, GetSpentInfoResponse,
GetSubtreesResponse, GetTxOutSetInfoResponse,
};
use zaino_proto::proto::{
compact_formats::CompactBlock,
service::{
AddressList, Balance, BlockId, BlockRange, Duration, GetAddressUtxosArg,
GetAddressUtxosReplyList, GetMempoolTxRequest, GetSubtreeRootsArg, LightdInfo,
PingResponse, RawTransaction, SendResponse, ShieldedProtocol, SubtreeRoot,
TransparentAddressBlockFilter, TreeState, TxFilter,
},
};
use zebra_chain::{
block::Height, serialization::BytesInDisplayOrder as _, subtree::NoteCommitmentSubtreeIndex,
};
use zebra_rpc::{
client::{
GetSubtreesByIndexResponse, GetTreestateResponse, SubtreeRpcData, ValidateAddressResponse,
},
methods::{
AddressBalance, GetAddressBalanceRequest, GetAddressTxIdsRequest, GetAddressUtxos,
GetBlock, GetBlockHash, GetBlockchainInfoResponse, GetInfo, GetRawTransaction,
SentTransactionHash,
},
};
use crate::{
status::Status,
stream::{
AddressStream, CompactBlockStream, CompactTransactionStream, RawTransactionStream,
SubtreeRootReplyStream, UtxoReplyStream,
},
};
#[derive(Clone)]
pub struct IndexerService<Service: ZcashService> {
service: Service,
}
impl<Service> IndexerService<Service>
where
Service: ZcashService,
{
pub async fn spawn(
config: Service::Config,
) -> Result<Self, <Service::Subscriber as ZcashIndexer>::Error> {
Ok(IndexerService {
service: Service::spawn(config)
.await
.map_err(Into::<tonic::Status>::into)?,
})
}
pub fn inner_ref(&self) -> &Service {
&self.service
}
pub fn inner(self) -> Service {
self.service
}
}
pub trait ZcashService: Sized + Status {
type Subscriber: Clone + ZcashIndexer + LightWalletIndexer + Status;
type Config: Clone;
fn spawn(
config: Self::Config,
) -> impl SendFut<Result<Self, <Self::Subscriber as ZcashIndexer>::Error>>;
fn get_subscriber(&self) -> IndexerSubscriber<Self::Subscriber>;
fn close(&mut self);
}
#[derive(Clone)]
pub struct IndexerSubscriber<Subscriber: Clone + ZcashIndexer + LightWalletIndexer + Send + Sync> {
subscriber: Subscriber,
}
impl<Subscriber> IndexerSubscriber<Subscriber>
where
Subscriber: Clone + ZcashIndexer + LightWalletIndexer,
{
pub fn new(subscriber: Subscriber) -> Self {
IndexerSubscriber { subscriber }
}
pub fn inner_ref(&self) -> &Subscriber {
&self.subscriber
}
pub fn inner_clone(&self) -> Subscriber {
self.subscriber.clone()
}
pub fn inner(self) -> Subscriber {
self.subscriber
}
}
pub trait ZcashIndexer: Send + Sync + 'static {
type Error: std::error::Error
+ From<tonic::Status>
+ Into<tonic::Status>
+ Send
+ Sync
+ 'static;
fn get_info(&self) -> impl SendFut<Result<GetInfo, Self::Error>>;
fn get_address_deltas(
&self,
params: GetAddressDeltasParams,
) -> impl SendFut<Result<GetAddressDeltasResponse, Self::Error>>;
fn get_blockchain_info(&self) -> impl SendFut<Result<GetBlockchainInfoResponse, Self::Error>>;
fn get_difficulty(&self) -> impl SendFut<Result<f64, Self::Error>>;
fn get_block_subsidy(&self, height: u32) -> impl SendFut<Result<GetBlockSubsidy, Self::Error>>;
fn get_mempool_info(&self) -> impl SendFut<Result<GetMempoolInfoResponse, Self::Error>>;
fn get_peer_info(&self) -> impl SendFut<Result<GetPeerInfo, Self::Error>>;
fn z_get_address_balance(
&self,
address_strings: GetAddressBalanceRequest,
) -> impl SendFut<Result<AddressBalance, Self::Error>>;
fn send_raw_transaction(
&self,
raw_transaction_hex: String,
) -> impl SendFut<Result<SentTransactionHash, Self::Error>>;
fn get_block_header(
&self,
hash: String,
verbose: bool,
) -> impl SendFut<Result<GetBlockHeader, Self::Error>>;
fn z_get_block(
&self,
hash_or_height: String,
verbosity: Option<u8>,
) -> impl SendFut<Result<GetBlock, Self::Error>>;
fn get_block_deltas(&self, hash: String) -> impl SendFut<Result<BlockDeltas, Self::Error>>;
fn get_block_count(&self) -> impl SendFut<Result<Height, Self::Error>>;
fn get_chain_tips(&self) -> impl SendFut<Result<GetChainTipsResponse, Self::Error>>;
fn validate_address(
&self,
address: String,
) -> impl SendFut<Result<ValidateAddressResponse, Self::Error>>;
#[deprecated(note = "https://github.com/zingolabs/zaino/issues/992#issuecomment-4245596178")]
fn z_validate_address(
&self,
address: String,
) -> impl SendFut<Result<ZValidateAddressResponse, Self::Error>>;
fn get_best_blockhash(&self) -> impl SendFut<Result<GetBlockHash, Self::Error>>;
fn get_raw_mempool(&self) -> impl SendFut<Result<Vec<String>, Self::Error>>;
fn z_get_treestate(
&self,
hash_or_height: String,
) -> impl SendFut<Result<GetTreestateResponse, Self::Error>>;
fn z_get_subtrees_by_index(
&self,
pool: String,
start_index: NoteCommitmentSubtreeIndex,
limit: Option<NoteCommitmentSubtreeIndex>,
) -> impl SendFut<Result<GetSubtreesByIndexResponse, Self::Error>>;
fn get_raw_transaction(
&self,
txid_hex: String,
verbose: Option<u8>,
) -> impl SendFut<Result<GetRawTransaction, Self::Error>>;
fn get_tx_out(
&self,
txid: String,
n: u32,
include_mempool: Option<bool>,
) -> impl SendFut<Result<zaino_fetch::jsonrpsee::response::GetTxOutResponse, Self::Error>>;
fn get_spent_info(
&self,
request: GetSpentInfoRequest,
) -> impl SendFut<Result<GetSpentInfoResponse, Self::Error>>;
fn get_address_tx_ids(
&self,
request: GetAddressTxIdsRequest,
) -> impl SendFut<Result<Vec<String>, Self::Error>>;
fn z_get_address_utxos(
&self,
address_strings: GetAddressBalanceRequest,
) -> impl SendFut<Result<Vec<GetAddressUtxos>, Self::Error>>;
fn get_mining_info(&self) -> impl SendFut<Result<GetMiningInfoWire, Self::Error>>;
fn get_tx_out_set_info(&self) -> impl SendFut<Result<GetTxOutSetInfoResponse, Self::Error>>;
fn get_network_sol_ps(
&self,
blocks: Option<i32>,
height: Option<i32>,
) -> impl SendFut<Result<GetNetworkSolPsResponse, Self::Error>>;
fn chain_height(&self) -> impl SendFut<Result<Height, Self::Error>>;
fn get_taddress_txids_helper(
&self,
request: TransparentAddressBlockFilter,
) -> impl SendFut<Result<Vec<String>, Self::Error>> {
async move {
let chain_height = self.chain_height().await?;
let (start, end) = match request.range {
Some(range) => {
let start = if let Some(start) = range.start {
match u32::try_from(start.height) {
Ok(height) => Some(height.min(chain_height.0)),
Err(_) => {
return Err(Self::Error::from(tonic::Status::invalid_argument(
"Error: Start height out of range. Failed to convert to u32.",
)))
}
}
} else {
None
};
let end = if let Some(end) = range.end {
match u32::try_from(end.height) {
Ok(height) => Some(height.min(chain_height.0)),
Err(_) => {
return Err(Self::Error::from(tonic::Status::invalid_argument(
"Error: End height out of range. Failed to convert to u32.",
)))
}
}
} else {
None
};
match (start, end) {
(Some(start), Some(end)) => {
if start > end {
(Some(end), Some(start))
} else {
(Some(start), Some(end))
}
}
_ => (start, end),
}
}
None => {
return Err(Self::Error::from(tonic::Status::invalid_argument(
"Error: No block range given.",
)))
}
};
self.get_address_tx_ids(GetAddressTxIdsRequest::new(
vec![request.address],
start,
end,
))
.await
}
}
}
pub trait LightWalletIndexer: Send + Sync + Clone + ZcashIndexer + 'static {
fn get_latest_block(&self) -> impl SendFut<Result<BlockId, Self::Error>>;
fn get_block(&self, request: BlockId) -> impl SendFut<Result<CompactBlock, Self::Error>>;
fn get_block_nullifiers(
&self,
request: BlockId,
) -> impl SendFut<Result<CompactBlock, Self::Error>>;
fn get_block_range(
&self,
request: BlockRange,
) -> impl SendFut<Result<CompactBlockStream, Self::Error>>;
fn get_block_range_nullifiers(
&self,
request: BlockRange,
) -> impl SendFut<Result<CompactBlockStream, Self::Error>>;
fn get_transaction(
&self,
request: TxFilter,
) -> impl SendFut<Result<RawTransaction, Self::Error>>;
fn send_transaction(
&self,
request: RawTransaction,
) -> impl SendFut<Result<SendResponse, Self::Error>>;
fn get_taddress_transactions(
&self,
request: TransparentAddressBlockFilter,
) -> impl SendFut<Result<RawTransactionStream, Self::Error>>;
fn get_taddress_txids(
&self,
request: TransparentAddressBlockFilter,
) -> impl SendFut<Result<RawTransactionStream, Self::Error>>;
fn get_taddress_balance(
&self,
request: AddressList,
) -> impl SendFut<Result<Balance, Self::Error>>;
fn get_taddress_balance_stream(
&self,
request: AddressStream,
) -> impl SendFut<Result<Balance, Self::Error>>;
fn get_mempool_tx(
&self,
request: GetMempoolTxRequest,
) -> impl SendFut<Result<CompactTransactionStream, Self::Error>>;
fn get_mempool_stream(&self) -> impl SendFut<Result<RawTransactionStream, Self::Error>>;
fn get_tree_state(&self, request: BlockId) -> impl SendFut<Result<TreeState, Self::Error>>;
fn get_latest_tree_state(&self) -> impl SendFut<Result<TreeState, Self::Error>>;
fn timeout_channel_size(&self) -> (u32, u32);
fn get_subtree_roots(
&self,
request: GetSubtreeRootsArg,
) -> impl SendFut<Result<SubtreeRootReplyStream, <Self as ZcashIndexer>::Error>> {
async move {
let pool = match ShieldedProtocol::try_from(request.shielded_protocol) {
Ok(protocol) => protocol.as_str_name(),
Err(_) => {
return Err(<Self as ZcashIndexer>::Error::from(
tonic::Status::invalid_argument("Error: Invalid shielded protocol value."),
))
}
};
let start_index = match u16::try_from(request.start_index) {
Ok(value) => value,
Err(_) => {
return Err(<Self as ZcashIndexer>::Error::from(
tonic::Status::invalid_argument(
"Error: start_index value exceeds u16 range.",
),
))
}
};
let limit = if request.max_entries == 0 {
None
} else {
match u16::try_from(request.max_entries) {
Ok(value) => Some(value),
Err(_) => {
return Err(<Self as ZcashIndexer>::Error::from(
tonic::Status::invalid_argument(
"Error: max_entries value exceeds u16 range.",
),
))
}
}
};
let service_clone = self.clone();
let subtrees = service_clone
.z_get_subtrees_by_index(
pool.to_string(),
NoteCommitmentSubtreeIndex(start_index),
limit.map(NoteCommitmentSubtreeIndex),
)
.await?;
let (service_timeout, service_channel_size) = self.timeout_channel_size();
let (channel_tx, channel_rx) = mpsc::channel(service_channel_size as usize);
tokio::spawn(async move {
let timeout = timeout(
std::time::Duration::from_secs((service_timeout * 4) as u64),
async {
for subtree in subtrees.subtrees() {
match service_clone
.z_get_block(subtree.end_height.0.to_string(), Some(1))
.await
{
Ok(GetBlock::Object(block_object)) => {
let checked_height = match block_object.height() {
Some(h) => h.0 as u64,
None => {
match channel_tx
.send(Err(tonic::Status::unknown(
"Error: No block height returned by node.",
)))
.await
{
Ok(_) => break,
Err(e) => {
warn!(
%e,
"GetSubtreeRoots channel closed unexpectedly"
);
break;
}
}
}
};
let checked_root_hash = match hex::decode(&subtree.root) {
Ok(hash) => hash,
Err(e) => {
match channel_tx
.send(Err(tonic::Status::unknown(format!(
"Error: Failed to hex decode root hash: {e}."
))))
.await
{
Ok(_) => break,
Err(e) => {
warn!(
%e,
"GetSubtreeRoots channel closed unexpectedly"
);
break;
}
}
}
};
if channel_tx
.send(Ok(SubtreeRoot {
root_hash: checked_root_hash,
completing_block_hash: block_object
.hash()
.bytes_in_display_order()
.to_vec(),
completing_block_height: checked_height,
}))
.await
.is_err()
{
break;
}
}
Ok(GetBlock::Raw(_)) => {
if channel_tx
.send(Err(tonic::Status::unknown(
"Error: Received raw block type, this should not be possible.",
)))
.await
.is_err()
{
break;
}
}
Err(e) => {
if channel_tx
.send(Err(tonic::Status::unknown(format!(
"Error: Could not fetch block at height [{}] from node: {}",
subtree.end_height.0, e
))))
.await
.is_err()
{
break;
}
}
}
}
},
)
.await;
match timeout {
Ok(_) => {}
Err(_) => {
channel_tx
.send(Err(tonic::Status::deadline_exceeded(
"Error: get_mempool_stream gRPC request timed out",
)))
.await
.ok();
}
}
});
Ok(SubtreeRootReplyStream::new(channel_rx))
}
}
fn get_address_utxos(
&self,
request: GetAddressUtxosArg,
) -> impl SendFut<Result<GetAddressUtxosReplyList, Self::Error>>;
fn get_address_utxos_stream(
&self,
request: GetAddressUtxosArg,
) -> impl SendFut<Result<UtxoReplyStream, Self::Error>>;
fn get_lightd_info(&self) -> impl SendFut<Result<LightdInfo, Self::Error>>;
fn ping(&self, request: Duration) -> impl SendFut<Result<PingResponse, Self::Error>>;
}
pub trait LightWalletService: Sized + ZcashService<Subscriber: LightWalletIndexer> {}
impl<T> LightWalletService for T where T: ZcashService {}
pub(crate) async fn handle_raw_transaction<Indexer: LightWalletIndexer>(
chain_height: u64,
transaction: Result<GetRawTransaction, Indexer::Error>,
transmitter: mpsc::Sender<Result<RawTransaction, tonic::Status>>,
) -> Result<(), mpsc::error::SendError<Result<RawTransaction, tonic::Status>>> {
match transaction {
Ok(GetRawTransaction::Object(transaction_obj)) => {
let height: u64 = match transaction_obj.height() {
Some(h) => h as u64,
None => chain_height,
};
transmitter
.send(Ok(RawTransaction {
data: transaction_obj.hex().as_ref().to_vec(),
height,
}))
.await
}
Ok(GetRawTransaction::Raw(_)) => {
transmitter
.send(Err(tonic::Status::unknown(
"Received raw transaction type, this should not be impossible.",
)))
.await
}
Err(e) => {
transmitter
.send(Err(tonic::Status::unknown(e.to_string())))
.await
}
}
}
fn address_network_type(
network: &zebra_chain::parameters::Network,
) -> zcash_protocol::consensus::NetworkType {
use zcash_protocol::consensus::NetworkType;
use zebra_chain::parameters::NetworkKind;
match network.kind() {
NetworkKind::Mainnet => NetworkType::Main,
NetworkKind::Testnet => NetworkType::Test,
NetworkKind::Regtest => NetworkType::Regtest,
}
}
pub(crate) fn validate_address(
raw_address: String,
network: &zebra_chain::parameters::Network,
) -> ValidateAddressResponse {
use zcash_keys::address::Address;
use zcash_transparent::address::TransparentAddress;
let Ok(address) = raw_address.parse::<zcash_address::ZcashAddress>() else {
return ValidateAddressResponse::invalid();
};
let address = match address.convert_if_network::<Address>(address_network_type(network)) {
Ok(address) => address,
Err(err) => {
tracing::debug!(?err, "conversion error");
return ValidateAddressResponse::invalid();
}
};
match address {
Address::Transparent(taddr) => ValidateAddressResponse::new(
true,
Some(raw_address),
Some(matches!(taddr, TransparentAddress::ScriptHash(_))),
),
_ => ValidateAddressResponse::invalid(),
}
}
pub(crate) fn z_validate_address(
address: String,
network: &zebra_chain::parameters::Network,
) -> ZValidateAddressResponse {
use zcash_keys::address::Address;
use zcash_keys::encoding::AddressCodec as _;
use zcash_transparent::address::TransparentAddress;
tracing::warn!("{}", Z_VALIDATE_DEPRECATION);
let invalid = || {
ZValidateAddressResponse::Known(KnownZValidateAddress::Invalid(
InvalidZValidateAddress::new(),
))
};
let Ok(parsed_address) = address.parse::<zcash_address::ZcashAddress>() else {
return invalid();
};
let converted_address =
match parsed_address.convert_if_network::<Address>(address_network_type(network)) {
Ok(address) => address,
Err(err) => {
tracing::debug!(?err, "conversion error");
return invalid();
}
};
match converted_address {
Address::Transparent(TransparentAddress::PublicKeyHash(_)) => {
ZValidateAddressResponse::p2pkh(address)
}
Address::Transparent(TransparentAddress::ScriptHash(_)) => {
ZValidateAddressResponse::p2sh(address)
}
Address::Unified(u) => ZValidateAddressResponse::unified(u.encode(network)),
Address::Sapling(s) => {
let (diversifier, pk_d) = sapling_key_bytes(&s);
ZValidateAddressResponse::sapling(
s.encode(network),
Some(hex::encode(diversifier)),
Some(hex::encode(pk_d)),
)
}
_ => invalid(),
}
}
pub(crate) fn sapling_key_bytes(s: &sapling_crypto::PaymentAddress) -> ([u8; 11], [u8; 32]) {
let bytes = s.to_bytes();
let diversifier: [u8; 11] = bytes[..11]
.try_into()
.expect("PaymentAddress::to_bytes always returns 43 bytes: diversifier is the first 11");
let mut pk_d: [u8; 32] = bytes[11..]
.try_into()
.expect("PaymentAddress::to_bytes always returns 43 bytes: pk_d is the last 32");
pk_d.reverse();
(diversifier, pk_d)
}
pub(crate) fn build_subtrees_by_index_response(
pool: String,
start_index: NoteCommitmentSubtreeIndex,
roots: Vec<([u8; 32], u32)>,
) -> GetSubtreesByIndexResponse {
use hex::ToHex as _;
let subtrees = roots
.into_iter()
.map(|(root, end_height)| {
SubtreeRpcData {
root: root.encode_hex(),
end_height: Height(end_height),
}
.into()
})
.collect();
GetSubtreesResponse {
pool,
start_index,
subtrees,
}
.into()
}
fn tree_state_from_treestate_response(
network: String,
treestate_response: zebra_rpc::client::GetTreestateResponse,
) -> zaino_proto::proto::service::TreeState {
let sapling_tree = hex::encode(
treestate_response
.sapling()
.commitments()
.final_state()
.clone()
.unwrap_or_default(),
);
let orchard_tree = hex::encode(
treestate_response
.orchard()
.commitments()
.final_state()
.clone()
.unwrap_or_default(),
);
let ironwood_tree = treestate_response
.ironwood()
.clone()
.and_then(|treestate| treestate.commitments().final_state().clone())
.map(hex::encode)
.unwrap_or_default();
zaino_proto::proto::service::TreeState {
network,
height: treestate_response.height().0 as u64,
hash: treestate_response.hash().to_string(),
time: treestate_response.time(),
sapling_tree,
orchard_tree,
ironwood_tree,
}
}
fn build_treestate_response(
hash: zebra_chain::block::Hash,
height: zebra_chain::block::Height,
time: u32,
(sapling, orchard, ironwood): (
Option<crate::chain_index::source::PoolTreestate>,
Option<crate::chain_index::source::PoolTreestate>,
Option<crate::chain_index::source::PoolTreestate>,
),
) -> zebra_rpc::client::GetTreestateResponse {
fn treestate(
pool: Option<crate::chain_index::source::PoolTreestate>,
) -> zebra_rpc::client::Treestate {
let (final_root, final_state) = match pool {
Some(pool) => (pool.final_root, Some(pool.final_state)),
None => (None, None),
};
zebra_rpc::client::Treestate::new(zebra_rpc::client::Commitments::new(
final_root,
final_state,
))
}
let sprout_treestate = None;
let ironwood_treestate = ironwood.map(|pool| treestate(Some(pool)));
zebra_rpc::client::GetTreestateResponse::new(
hash,
height,
time,
sprout_treestate,
treestate(sapling),
treestate(orchard),
ironwood_treestate,
)
}
fn latest_network_upgrade(
upgrades: &indexmap::IndexMap<
zebra_rpc::methods::ConsensusBranchIdHex,
zebra_rpc::methods::NetworkUpgradeInfo,
>,
) -> Result<&zebra_rpc::methods::NetworkUpgradeInfo, tonic::Status> {
upgrades.last().map(|(_, upgrade)| upgrade).ok_or_else(|| {
tonic::Status::failed_precondition("validator returned no network upgrade metadata")
})
}
const UTXO_MAX_ADDRESSES: usize = 1000;
fn validate_utxo_address_count(count: usize) -> Result<(), tonic::Status> {
if count > UTXO_MAX_ADDRESSES {
return Err(tonic::Status::invalid_argument(format!(
"Error: too many addresses in request: {count} exceeds the maximum of {UTXO_MAX_ADDRESSES}."
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn latest_network_upgrade_rejects_empty_metadata() {
let upgrades = indexmap::IndexMap::new();
let err = super::latest_network_upgrade(&upgrades).expect_err("empty upgrades must fail");
assert_eq!(err.code(), tonic::Code::FailedPrecondition);
assert_eq!(
err.message(),
"validator returned no network upgrade metadata"
);
}
#[test]
fn utxo_address_count_within_limit_is_accepted() {
assert!(super::validate_utxo_address_count(0).is_ok());
assert!(super::validate_utxo_address_count(super::UTXO_MAX_ADDRESSES).is_ok());
}
#[test]
fn utxo_address_count_over_limit_is_rejected() {
let err = super::validate_utxo_address_count(super::UTXO_MAX_ADDRESSES + 1)
.expect_err("over-limit address count must fail");
assert_eq!(err.code(), tonic::Code::InvalidArgument);
}
#[test]
fn build_subtrees_by_index_response_hex_encodes_roots() {
let roots = vec![([0xabu8; 32], 100u32), ([0xcdu8; 32], 200u32)];
let response = build_subtrees_by_index_response(
"orchard".to_string(),
NoteCommitmentSubtreeIndex(5),
roots,
);
assert_eq!(response.pool().as_str(), "orchard");
assert_eq!(response.start_index(), NoteCommitmentSubtreeIndex(5));
let subtrees = response.subtrees();
assert_eq!(subtrees.len(), 2);
assert_eq!(subtrees[0].root, hex::encode([0xabu8; 32]));
assert_eq!(subtrees[0].end_height, Height(100));
assert_eq!(subtrees[1].root, hex::encode([0xcdu8; 32]));
assert_eq!(subtrees[1].end_height, Height(200));
}
#[derive(Debug, PartialEq)]
enum ByteRelation {
Equal,
FullByteReversal,
PerByteBitReversal,
ChunkSwap16,
ChunkSwap32,
ChunkSwap64,
Unrecognized,
}
impl std::fmt::Display for ByteRelation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Equal => write!(f, "equal"),
Self::FullByteReversal => write!(f, "full byte-reversal (endian swap)"),
Self::PerByteBitReversal => write!(f, "per-byte bit-reversal"),
Self::ChunkSwap16 => write!(f, "16-bit pairwise byte-swap"),
Self::ChunkSwap32 => write!(f, "32-bit chunk byte-reversal"),
Self::ChunkSwap64 => write!(f, "64-bit chunk byte-reversal"),
Self::Unrecognized => write!(f, "unrecognized mismatch"),
}
}
}
#[allow(clippy::manual_is_multiple_of)]
fn classify_byte_relation(actual: &[u8], expected: &[u8]) -> ByteRelation {
if actual == expected {
return ByteRelation::Equal;
}
let chunk_swap = |size: usize| -> Vec<u8> {
actual
.chunks(size)
.flat_map(|c| c.iter().rev())
.copied()
.collect()
};
let mut reversed = actual.to_vec();
reversed.reverse();
if reversed == expected {
return ByteRelation::FullByteReversal;
}
let bit_reversed: Vec<u8> = actual.iter().map(|b| b.reverse_bits()).collect();
if bit_reversed == expected {
return ByteRelation::PerByteBitReversal;
}
if actual.len() % 2 == 0 && chunk_swap(2) == expected {
return ByteRelation::ChunkSwap16;
}
if actual.len() % 4 == 0 && chunk_swap(4) == expected {
return ByteRelation::ChunkSwap32;
}
if actual.len() % 8 == 0 && chunk_swap(8) == expected {
return ByteRelation::ChunkSwap64;
}
ByteRelation::Unrecognized
}
#[test]
fn sapling_pk_d_byte_order_matches_test_vector() {
use crate::indexer::sapling_key_bytes;
use zcash_keys::address::Address;
use zcash_protocol::consensus::NetworkType;
const SAPLING_ADDRESS: &str = "zregtestsapling1jalqhycwumq3unfxlzyzcktq3n478n82k2wacvl8gwfxk6ahshkxmtp2034qj28n7gl92ka5wca";
const EXPECTED_DIVERSIFIER: &str = "977e0b930ee6c11e4d26f8";
const EXPECTED_PK_D: &str =
"553ef2f328096a7c2aac6dec85b76b6b9243e733dc9db2eacce3eb8c60592c88";
let parsed: zcash_address::ZcashAddress = SAPLING_ADDRESS.parse().unwrap();
let converted = parsed
.convert_if_network::<Address>(NetworkType::Regtest)
.unwrap();
let Address::Sapling(s) = converted else {
panic!("expected Sapling address");
};
let (diversifier, pk_d) = sapling_key_bytes(&s);
let expected_diversifier = hex::decode(EXPECTED_DIVERSIFIER).unwrap();
let expected_pk_d = hex::decode(EXPECTED_PK_D).unwrap();
match classify_byte_relation(&diversifier, &expected_diversifier) {
ByteRelation::Equal => {}
relation => panic!(
"diversifier mismatch.\n relation: {relation}\n actual: {}\n expected: {}",
hex::encode(diversifier),
hex::encode(expected_diversifier),
),
}
match classify_byte_relation(&pk_d, &expected_pk_d) {
ByteRelation::Equal => {}
relation => panic!(
"pk_d mismatch — upstream serialization may have changed.\
\n relation: {relation}\n actual: {}\n expected: {}",
hex::encode(pk_d),
hex::encode(expected_pk_d),
),
}
}
#[test]
fn classify_byte_relation_detects_known_transforms() {
let original = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
assert_eq!(
classify_byte_relation(&original, &original),
ByteRelation::Equal,
);
let mut reversed = original.to_vec();
reversed.reverse();
assert_eq!(
classify_byte_relation(&original, &reversed),
ByteRelation::FullByteReversal,
);
let bit_rev: Vec<u8> = original.iter().map(|b| b.reverse_bits()).collect();
assert_eq!(
classify_byte_relation(&original, &bit_rev),
ByteRelation::PerByteBitReversal,
);
let swapped_16: Vec<u8> = original
.chunks(2)
.flat_map(|c| c.iter().rev())
.copied()
.collect();
assert_eq!(
classify_byte_relation(&original, &swapped_16),
ByteRelation::ChunkSwap16,
);
let garbage = [0xFF; 8];
assert_eq!(
classify_byte_relation(&original, &garbage),
ByteRelation::Unrecognized,
);
}
}