Skip to main content

zebra_rpc/
lightwalletd.rs

1//! A tonic gRPC server that implements the lightwalletd `CompactTxStreamer` client API.
2//!
3//! The interface is defined by the [`zcash/lightwalletd`] proto files, so Zcash light
4//! clients that normally connect to a `lightwalletd` instance can connect directly to
5//! Zebra instead.
6//!
7//! [`zcash/lightwalletd`]: https://github.com/zcash/lightwalletd/tree/master/walletrpc
8
9use zebra_chain::{block, transaction, transaction::Transaction};
10
11#[cfg(test)]
12mod tests;
13
14pub mod methods;
15pub mod server;
16pub mod tracing;
17
18// The generated lightwalletd proto for the `cash.z.wallet.sdk.rpc` package.
19//
20// The generated code only has documentation where the proto files have comments,
21// so the `missing_docs` lint is allowed for the whole module.
22#[allow(missing_docs)]
23mod wire {
24    tonic::include_proto!("cash.z.wallet.sdk.rpc");
25}
26pub use wire::*;
27
28pub(crate) const FILE_DESCRIPTOR_SET: &[u8] =
29    tonic::include_file_descriptor_set!("lightwalletd_descriptor");
30
31/// The number of bytes of an encrypted note ciphertext included in compact
32/// outputs and actions, from the `compact_formats.proto` specification.
33const COMPACT_CIPHERTEXT_SIZE: usize = 52;
34
35/// Returns true if the transaction contains any data that belongs in a
36/// [`CompactTx`]: Sapling spends or outputs, or Orchard or Ironwood actions.
37pub(crate) fn has_compact_data(tx: &Transaction) -> bool {
38    tx.sapling_nullifiers().next().is_some()
39        || tx.sapling_outputs().next().is_some()
40        || tx.orchard_actions().next().is_some()
41        || tx.ironwood_actions().next().is_some()
42}
43
44/// Returns true if the transaction contains any nullifiers, so it belongs in a
45/// nullifiers-only [`CompactBlock`].
46///
47/// Outputs are omitted in that mode, so a transaction with only Sapling outputs would
48/// otherwise be included as a [`CompactTx`] carrying nothing but an index and a hash.
49pub(crate) fn has_nullifiers(tx: &Transaction) -> bool {
50    tx.sapling_nullifiers().next().is_some()
51        || tx.orchard_actions().next().is_some()
52        || tx.ironwood_actions().next().is_some()
53}
54
55/// Builds a [`CompactOrchardAction`] from an Orchard-encoded action.
56///
57/// Ironwood reuses the Orchard action encoding, so the same conversion serves both pools.
58/// If `nullifiers_only` is true, only the nullifier is included.
59fn compact_action(
60    action: &::orchard::Action<
61        <::orchard::bundle::Authorized as ::orchard::bundle::Authorization>::SpendAuth,
62    >,
63    nullifiers_only: bool,
64) -> CompactOrchardAction {
65    let mut compact_action = CompactOrchardAction {
66        nullifier: action.nullifier().to_bytes().to_vec(),
67        ..Default::default()
68    };
69
70    if !nullifiers_only {
71        compact_action.cmx = action.cmx().to_bytes().to_vec();
72        compact_action.ephemeral_key = action.encrypted_note().epk_bytes.to_vec();
73        compact_action.ciphertext =
74            action.encrypted_note().enc_ciphertext.0[..COMPACT_CIPHERTEXT_SIZE].to_vec();
75    }
76
77    compact_action
78}
79
80impl CompactBlock {
81    /// Builds a [`CompactBlock`] from a full block and the note commitment tree sizes
82    /// at the end of that block.
83    ///
84    /// If `nullifiers_only` is true, compact outputs are omitted and compact actions
85    /// only contain nullifiers, matching the lightwalletd `GetBlockNullifiers` and
86    /// `GetBlockRangeNullifiers` methods.
87    pub fn from_block(
88        block: &block::Block,
89        hash: block::Hash,
90        height: block::Height,
91        sapling_tree_size: u64,
92        orchard_tree_size: u64,
93        ironwood_tree_size: u64,
94        nullifiers_only: bool,
95    ) -> Self {
96        let vtx = block
97            .transactions
98            .iter()
99            .enumerate()
100            .filter(|(_, tx)| {
101                if nullifiers_only {
102                    has_nullifiers(tx)
103                } else {
104                    has_compact_data(tx)
105                }
106            })
107            .map(|(index, tx)| {
108                CompactTx::from_transaction(index as u64, tx.hash(), tx, nullifiers_only)
109            })
110            .collect();
111
112        CompactBlock {
113            proto_version: 0,
114            height: height.0.into(),
115            hash: hash.0.to_vec(),
116            prev_hash: block.header.previous_block_hash.0.to_vec(),
117            // Cast is safe until 2106: consensus rules restrict block times to 32-bit values.
118            time: block.header.time.timestamp() as u32,
119            header: Vec::new(),
120            vtx,
121            chain_metadata: Some(ChainMetadata {
122                // Casts are safe: note commitment trees hold at most `2^32 - 1` notes,
123                // since note positions are 32-bit values.
124                sapling_commitment_tree_size: sapling_tree_size as u32,
125                orchard_commitment_tree_size: orchard_tree_size as u32,
126                ironwood_commitment_tree_size: ironwood_tree_size as u32,
127            }),
128        }
129    }
130}
131
132impl CompactTx {
133    /// Builds a [`CompactTx`] from a transaction, its precomputed mined transaction
134    /// ID, and its index within its block.
135    ///
136    /// If `nullifiers_only` is true, compact outputs are omitted and compact actions
137    /// only contain nullifiers.
138    pub fn from_transaction(
139        index: u64,
140        hash: transaction::Hash,
141        tx: &Transaction,
142        nullifiers_only: bool,
143    ) -> Self {
144        let spends = tx
145            .sapling_nullifiers()
146            .map(|nullifier| CompactSaplingSpend {
147                nf: <[u8; 32]>::from(nullifier).to_vec(),
148            })
149            .collect();
150
151        let outputs = if nullifiers_only {
152            Vec::new()
153        } else {
154            tx.sapling_outputs()
155                .map(|output| CompactSaplingOutput {
156                    cmu: output.cmu().to_bytes().to_vec(),
157                    ephemeral_key: output.ephemeral_key().0.to_vec(),
158                    ciphertext: output.enc_ciphertext()[..COMPACT_CIPHERTEXT_SIZE].to_vec(),
159                })
160                .collect()
161        };
162
163        let actions = tx
164            .orchard_actions()
165            .map(|action| compact_action(action, nullifiers_only))
166            .collect();
167
168        let ironwood_actions = tx
169            .ironwood_actions()
170            .map(|action| compact_action(action, nullifiers_only))
171            .collect();
172
173        CompactTx {
174            index,
175            hash: hash.0.to_vec(),
176            // The fee is optional in the protocol, and stateless servers can't
177            // calculate it for transactions with transparent inputs, so it is unset.
178            fee: 0,
179            spends,
180            outputs,
181            actions,
182            ironwood_actions,
183        }
184    }
185}