Merge pull request #694 from nuttycom/wallet/zip_317
Add ZIP 317 fee calculation strategy.
This commit is contained in:
commit
d8cedd28cc
|
@ -28,6 +28,7 @@ and this library adheres to Rust's notion of
|
||||||
- `AddressMetadata`
|
- `AddressMetadata`
|
||||||
- `zcash_client_backend::data_api`:
|
- `zcash_client_backend::data_api`:
|
||||||
- `PoolType`
|
- `PoolType`
|
||||||
|
- `ShieldedPool`
|
||||||
- `Recipient`
|
- `Recipient`
|
||||||
- `SentTransactionOutput`
|
- `SentTransactionOutput`
|
||||||
- `WalletRead::get_unified_full_viewing_keys`
|
- `WalletRead::get_unified_full_viewing_keys`
|
||||||
|
@ -42,6 +43,11 @@ and this library adheres to Rust's notion of
|
||||||
- `WalletWrite::get_next_available_address`
|
- `WalletWrite::get_next_available_address`
|
||||||
- `WalletWrite::put_received_transparent_utxo`
|
- `WalletWrite::put_received_transparent_utxo`
|
||||||
- `impl From<prost::DecodeError> for error::Error`
|
- `impl From<prost::DecodeError> for error::Error`
|
||||||
|
- `chain::error`: a module containing error types type that that can occur only
|
||||||
|
in chain validation and sync have been separated out from errors related to
|
||||||
|
other wallet operations.
|
||||||
|
- `input_selection`: a module containing types related to the process
|
||||||
|
of selecting inputs to be spent, given a transaction request.
|
||||||
- `zcash_client_backend::decrypt`:
|
- `zcash_client_backend::decrypt`:
|
||||||
- `TransferType`
|
- `TransferType`
|
||||||
- `zcash_client_backend::proto`:
|
- `zcash_client_backend::proto`:
|
||||||
|
@ -50,6 +56,7 @@ and this library adheres to Rust's notion of
|
||||||
- gRPC bindings for the `lightwalletd` server, behind a `lightwalletd-tonic`
|
- gRPC bindings for the `lightwalletd` server, behind a `lightwalletd-tonic`
|
||||||
feature flag.
|
feature flag.
|
||||||
- `zcash_client_backend::zip321::TransactionRequest` methods:
|
- `zcash_client_backend::zip321::TransactionRequest` methods:
|
||||||
|
- `TransactionRequest::empty` for constructing a new empty request.
|
||||||
- `TransactionRequest::new` for constructing a request from `Vec<Payment>`.
|
- `TransactionRequest::new` for constructing a request from `Vec<Payment>`.
|
||||||
- `TransactionRequest::payments` for accessing the `Payments` that make up a
|
- `TransactionRequest::payments` for accessing the `Payments` that make up a
|
||||||
request.
|
request.
|
||||||
|
@ -64,6 +71,10 @@ and this library adheres to Rust's notion of
|
||||||
- `TransactionBalance`
|
- `TransactionBalance`
|
||||||
- `BasicFixedFeeChangeStrategy` - a `ChangeStrategy` implementation that
|
- `BasicFixedFeeChangeStrategy` - a `ChangeStrategy` implementation that
|
||||||
reproduces current wallet change behavior
|
reproduces current wallet change behavior
|
||||||
|
- `fixed`, a new module containing of change selection strategies for the
|
||||||
|
existing fixed fee rule.
|
||||||
|
- `zip317`, a new module containing change selection strategies for the ZIP
|
||||||
|
317 fee rule.
|
||||||
- New experimental APIs that should be considered unstable, and are
|
- New experimental APIs that should be considered unstable, and are
|
||||||
likely to be modified and/or moved to a different module in a future
|
likely to be modified and/or moved to a different module in a future
|
||||||
release:
|
release:
|
||||||
|
@ -72,6 +83,7 @@ and this library adheres to Rust's notion of
|
||||||
- `zcash_client_backend::encoding::AddressCodec`
|
- `zcash_client_backend::encoding::AddressCodec`
|
||||||
- `zcash_client_backend::encoding::encode_payment_address`
|
- `zcash_client_backend::encoding::encode_payment_address`
|
||||||
- `zcash_client_backend::encoding::encode_transparent_address`
|
- `zcash_client_backend::encoding::encode_transparent_address`
|
||||||
|
- `impl Eq for zcash_client_backend::address::UnifiedAddress`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- MSRV is now 1.56.1.
|
- MSRV is now 1.56.1.
|
||||||
|
@ -90,13 +102,8 @@ and this library adheres to Rust's notion of
|
||||||
been replaced by `ephemeral_key`.
|
been replaced by `ephemeral_key`.
|
||||||
- `zcash_client_backend::proto::compact_formats::CompactSaplingOutput`: the
|
- `zcash_client_backend::proto::compact_formats::CompactSaplingOutput`: the
|
||||||
`epk` method has been replaced by `ephemeral_key`.
|
`epk` method has been replaced by `ephemeral_key`.
|
||||||
- `data_api::wallet::spend_to_address` now takes a `min_confirmations`
|
|
||||||
parameter, which the caller can provide to specify the minimum number of
|
|
||||||
confirmations required for notes being selected. A default value of 10
|
|
||||||
confirmations is recommended.
|
|
||||||
- Renamed the following in `zcash_client_backend::data_api` to use lower-case
|
- Renamed the following in `zcash_client_backend::data_api` to use lower-case
|
||||||
abbreviations (matching Rust naming conventions):
|
abbreviations (matching Rust naming conventions):
|
||||||
- `error::Error::InvalidExtSK` to `Error::InvalidExtSk`
|
|
||||||
- `testing::MockWalletDB` to `testing::MockWalletDb`
|
- `testing::MockWalletDB` to `testing::MockWalletDb`
|
||||||
- Changes to the `data_api::WalletRead` trait:
|
- Changes to the `data_api::WalletRead` trait:
|
||||||
- `WalletRead::get_target_and_anchor_heights` now takes
|
- `WalletRead::get_target_and_anchor_heights` now takes
|
||||||
|
@ -104,9 +111,13 @@ and this library adheres to Rust's notion of
|
||||||
the anchor height being returned; this had previously been hardcoded to
|
the anchor height being returned; this had previously been hardcoded to
|
||||||
`data_api::wallet::ANCHOR_OFFSET`.
|
`data_api::wallet::ANCHOR_OFFSET`.
|
||||||
- `WalletRead::get_spendable_notes` has been renamed to
|
- `WalletRead::get_spendable_notes` has been renamed to
|
||||||
`get_spendable_sapling_notes`
|
`get_spendable_sapling_notes` and now takes as an argument a vector of
|
||||||
|
note IDs to be excluded from consideration.
|
||||||
- `WalletRead::select_spendable_notes` has been renamed to
|
- `WalletRead::select_spendable_notes` has been renamed to
|
||||||
`select_spendable_sapling_notes`
|
`select_spendable_sapling_notes` and now takes as an argument a vector of
|
||||||
|
note IDs to be excluded from consideration.
|
||||||
|
- The `WalletRead::NoteRef` and `WalletRead::TxRef` associated types are now
|
||||||
|
required to implement `Eq` and `Ord`
|
||||||
- The `zcash_client_backend::data_api::SentTransaction` type has been
|
- The `zcash_client_backend::data_api::SentTransaction` type has been
|
||||||
substantially modified to accommodate handling of transparent inputs.
|
substantially modified to accommodate handling of transparent inputs.
|
||||||
Per-output data has been split out into a new struct `SentTransactionOutput`
|
Per-output data has been split out into a new struct `SentTransactionOutput`
|
||||||
|
@ -116,24 +127,28 @@ and this library adheres to Rust's notion of
|
||||||
`store_decrypted_tx`.
|
`store_decrypted_tx`.
|
||||||
- `data_api::ReceivedTransaction` has been renamed to `DecryptedTransaction`,
|
- `data_api::ReceivedTransaction` has been renamed to `DecryptedTransaction`,
|
||||||
and its `outputs` field has been renamed to `sapling_outputs`.
|
and its `outputs` field has been renamed to `sapling_outputs`.
|
||||||
- `data_api::error::Error::Protobuf` now wraps `prost::DecodeError` instead of
|
- `data_api::error::Error` has been substantially modified. It now wraps database,
|
||||||
`protobuf::ProtobufError`.
|
note selection, builder, and other errors
|
||||||
- `data_api::error::Error` has the following additional cases:
|
- `Error::DataSource` has been added.
|
||||||
- `Error::BalanceError` in the case of amount addition overflow
|
- `Error::NoteSelection` has been added.
|
||||||
or subtraction underflow.
|
- `Error::BalanceError` has been added.
|
||||||
- `Error::MemoForbidden` to report the condition where a memo was
|
- `Error::MemoForbidden` has been added.
|
||||||
specified to be sent to a transparent recipient.
|
- `Error::AddressNotRecognized` has been added.
|
||||||
- `Error::TransparentInputsNotSupported` to represent the condition
|
- `Error::ChildIndexOutOfRange` has been added.
|
||||||
where a transparent spend has been requested of a wallet compiled without
|
- `Error::NoteMismatch` has been added.
|
||||||
the `transparent-inputs` feature.
|
- `Error::InsufficientBalance` has been renamed `InsufficientFunds` and
|
||||||
- `Error::AddressNotRecognized` to indicate that a transparent address from
|
restructured to have named fields.
|
||||||
which funds are being requested to be spent does not appear to be associated
|
- `Error::Protobuf` has been removed; these decoding errors are now
|
||||||
with this wallet.
|
produced as data source and/or block-source implementation-specific
|
||||||
- `Error::ChildIndexOutOfRange` to indicate that a diversifier index for an
|
errors.
|
||||||
address is out of range for valid transparent child indices.
|
- `Error::InvalidChain` has been removed; its former purpose is now served
|
||||||
- `Error::NoteMismatch` to indicate that a note being spent is not associated
|
by `zcash_client_backend::data_api::chain::ChainError`.
|
||||||
with either the internal or external full viewing keys corresponding to the
|
- `Error::InvalidNewWitnessAnchor` and `Error::InvalidWitnessAnchor` have
|
||||||
provided spending key.
|
been moved to `zcash_client_backend::data_api::chain::error::ContinuityError`
|
||||||
|
- `Error::InvalidExtSk` (now unused) has been removed.
|
||||||
|
- `Error::KeyNotFound` (now unused) has been removed.
|
||||||
|
- `Error::KeyDerivationError` (now unused) has been removed.
|
||||||
|
- `Error::SaplingNotActive` (now unused) has been removed.
|
||||||
- `zcash_client_backend::decrypt`:
|
- `zcash_client_backend::decrypt`:
|
||||||
- `decrypt_transaction` now takes a `HashMap<_, UnifiedFullViewingKey>`
|
- `decrypt_transaction` now takes a `HashMap<_, UnifiedFullViewingKey>`
|
||||||
instead of `HashMap<_, ExtendedFullViewingKey>`.
|
instead of `HashMap<_, ExtendedFullViewingKey>`.
|
||||||
|
@ -160,8 +175,38 @@ and this library adheres to Rust's notion of
|
||||||
- `decode_extended_spending_key`
|
- `decode_extended_spending_key`
|
||||||
- `decode_extended_full_viewing_key`
|
- `decode_extended_full_viewing_key`
|
||||||
- `decode_payment_address`
|
- `decode_payment_address`
|
||||||
- `data_api::wallet::create_spend_to_address` has been modified to use a unified
|
- `zcash_client_backend::wallet::SpendableNote` is now parameterized by a note
|
||||||
spending key rather than a Sapling extended spending key.
|
identifier type and has an additional `note_id` field that is used to hold the
|
||||||
|
identifier used to refer to the note in the wallet database.
|
||||||
|
- `zcash_client_backend::data_api::BlockSource` has been moved to the
|
||||||
|
`zcash_client_backend::data_api::chain` module.
|
||||||
|
- The types of the `with_row` callback argument to `BlockSource::with_blocks`
|
||||||
|
and the return type of this method have been modified to return
|
||||||
|
`zcash_client_backend::data_api::chain::error::Error`.
|
||||||
|
- `zcash_client_backend::data_api::testing::MockBlockSource` has been moved to
|
||||||
|
`zcash_client_backend::data_api::chain::testing::MockBlockSource` module.
|
||||||
|
- `zcash_client_backend::data_api::chain::{validate_chain, scan_cached_blocks}`
|
||||||
|
have altered parameters and result types. The latter have been modified to
|
||||||
|
return`zcash_client_backend::data_api::chain::error::Error` instead of
|
||||||
|
abstract error types. This new error type now wraps the errors of the block
|
||||||
|
source and wallet database to which these methods delegate IO operations
|
||||||
|
directly, which simplifies error handling in cases where callback functions
|
||||||
|
are involved.
|
||||||
|
- `zcash_client_backend::data_api::error::ChainInvalid` has been moved to
|
||||||
|
`zcash_client_backend::data_api::chain::error`.
|
||||||
|
- The error type of `zcash_client_backend::data_api::wallet::decrypt_and_store_transaction`
|
||||||
|
has been changed; the error type produced by the provided `WalletWrite` instance is
|
||||||
|
returned directly.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- `zcash_client_backend::data_api::wallet::create_spend_to_address` has been
|
||||||
|
deprecated. Use `zcash_client_backend::data_api::wallet::spend` instead. If
|
||||||
|
you wish to continue using `create_spend_to_address`, note that the arguments
|
||||||
|
to the function has been modified to take a unified spending key instead of a
|
||||||
|
Sapling extended spending key, and now also requires a `min_confirmations`
|
||||||
|
argument that the caller can provide to specify a minimum number of
|
||||||
|
confirmations required for notes being selected. A minimum of 10
|
||||||
|
confirmations is recommended.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- `zcash_client_backend::data_api`:
|
- `zcash_client_backend::data_api`:
|
||||||
|
|
|
@ -76,6 +76,7 @@ tonic-build = "0.8"
|
||||||
which = "4"
|
which = "4"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
assert_matches = "1.5"
|
||||||
gumdrop = "0.8"
|
gumdrop = "0.8"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
jubjub = "0.9"
|
jubjub = "0.9"
|
||||||
|
|
|
@ -36,7 +36,7 @@ impl AddressMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Unified Address.
|
/// A Unified Address.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct UnifiedAddress {
|
pub struct UnifiedAddress {
|
||||||
orchard: Option<orchard::Address>,
|
orchard: Option<orchard::Address>,
|
||||||
sapling: Option<PaymentAddress>,
|
sapling: Option<PaymentAddress>,
|
||||||
|
|
|
@ -12,7 +12,10 @@ use zcash_primitives::{
|
||||||
memo::{Memo, MemoBytes},
|
memo::{Memo, MemoBytes},
|
||||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||||
sapling::{Node, Nullifier, PaymentAddress},
|
sapling::{Node, Nullifier, PaymentAddress},
|
||||||
transaction::{components::Amount, Transaction, TxId},
|
transaction::{
|
||||||
|
components::{amount::Amount, OutPoint},
|
||||||
|
Transaction, TxId,
|
||||||
|
},
|
||||||
zip32::{AccountId, ExtendedFullViewingKey},
|
zip32::{AccountId, ExtendedFullViewingKey},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,13 +23,9 @@ use crate::{
|
||||||
address::{AddressMetadata, UnifiedAddress},
|
address::{AddressMetadata, UnifiedAddress},
|
||||||
decrypt::DecryptedOutput,
|
decrypt::DecryptedOutput,
|
||||||
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||||
proto::compact_formats::CompactBlock,
|
|
||||||
wallet::{SpendableNote, WalletTransparentOutput, WalletTx},
|
wallet::{SpendableNote, WalletTransparentOutput, WalletTx},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
|
||||||
use zcash_primitives::transaction::components::transparent::OutPoint;
|
|
||||||
|
|
||||||
pub mod chain;
|
pub mod chain;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
|
@ -44,14 +43,14 @@ pub trait WalletRead {
|
||||||
///
|
///
|
||||||
/// For example, this might be a database identifier type
|
/// For example, this might be a database identifier type
|
||||||
/// or a UUID.
|
/// or a UUID.
|
||||||
type NoteRef: Copy + Debug;
|
type NoteRef: Copy + Debug + Eq + Ord;
|
||||||
|
|
||||||
/// Backend-specific transaction identifier.
|
/// Backend-specific transaction identifier.
|
||||||
///
|
///
|
||||||
/// For example, this might be a database identifier type
|
/// For example, this might be a database identifier type
|
||||||
/// or a TxId if the backend is able to support that type
|
/// or a TxId if the backend is able to support that type
|
||||||
/// directly.
|
/// directly.
|
||||||
type TxRef: Copy + Debug;
|
type TxRef: Copy + Debug + Eq + Ord;
|
||||||
|
|
||||||
/// Returns the minimum and maximum block heights for stored blocks.
|
/// Returns the minimum and maximum block heights for stored blocks.
|
||||||
///
|
///
|
||||||
|
@ -188,7 +187,8 @@ pub trait WalletRead {
|
||||||
&self,
|
&self,
|
||||||
account: AccountId,
|
account: AccountId,
|
||||||
anchor_height: BlockHeight,
|
anchor_height: BlockHeight,
|
||||||
) -> Result<Vec<SpendableNote>, Self::Error>;
|
exclude: &[Self::NoteRef],
|
||||||
|
) -> Result<Vec<SpendableNote<Self::NoteRef>>, Self::Error>;
|
||||||
|
|
||||||
/// Returns a list of spendable Sapling notes sufficient to cover the specified
|
/// Returns a list of spendable Sapling notes sufficient to cover the specified
|
||||||
/// target value, if possible.
|
/// target value, if possible.
|
||||||
|
@ -197,7 +197,8 @@ pub trait WalletRead {
|
||||||
account: AccountId,
|
account: AccountId,
|
||||||
target_value: Amount,
|
target_value: Amount,
|
||||||
anchor_height: BlockHeight,
|
anchor_height: BlockHeight,
|
||||||
) -> Result<Vec<SpendableNote>, Self::Error>;
|
exclude: &[Self::NoteRef],
|
||||||
|
) -> Result<Vec<SpendableNote<Self::NoteRef>>, Self::Error>;
|
||||||
|
|
||||||
/// Returns the set of all transparent receivers associated with the given account.
|
/// Returns the set of all transparent receivers associated with the given account.
|
||||||
///
|
///
|
||||||
|
@ -215,6 +216,7 @@ pub trait WalletRead {
|
||||||
&self,
|
&self,
|
||||||
address: &TransparentAddress,
|
address: &TransparentAddress,
|
||||||
max_height: BlockHeight,
|
max_height: BlockHeight,
|
||||||
|
exclude: &[OutPoint],
|
||||||
) -> Result<Vec<WalletTransparentOutput>, Self::Error>;
|
) -> Result<Vec<WalletTransparentOutput>, Self::Error>;
|
||||||
|
|
||||||
/// Returns a mapping from transparent receiver to not-yet-shielded UTXO balance,
|
/// Returns a mapping from transparent receiver to not-yet-shielded UTXO balance,
|
||||||
|
@ -227,7 +229,9 @@ pub trait WalletRead {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The subset of information that is relevant to this wallet that has been
|
/// The subset of information that is relevant to this wallet that has been
|
||||||
/// decrypted and extracted from a [CompactBlock].
|
/// decrypted and extracted from a [`CompactBlock`].
|
||||||
|
///
|
||||||
|
/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
|
||||||
pub struct PrunedBlock<'a> {
|
pub struct PrunedBlock<'a> {
|
||||||
pub block_height: BlockHeight,
|
pub block_height: BlockHeight,
|
||||||
pub block_hash: BlockHash,
|
pub block_hash: BlockHash,
|
||||||
|
@ -268,6 +272,7 @@ pub enum PoolType {
|
||||||
Transparent,
|
Transparent,
|
||||||
/// The Sapling value pool
|
/// The Sapling value pool
|
||||||
Sapling,
|
Sapling,
|
||||||
|
// TODO: Orchard
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A type that represents the recipient of a transaction output; a recipient address (and, for
|
/// A type that represents the recipient of a transaction output; a recipient address (and, for
|
||||||
|
@ -386,23 +391,6 @@ pub trait WalletWrite: WalletRead {
|
||||||
) -> Result<Self::UtxoRef, Self::Error>;
|
) -> Result<Self::UtxoRef, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This trait provides sequential access to raw blockchain data via a callback-oriented
|
|
||||||
/// API.
|
|
||||||
pub trait BlockSource {
|
|
||||||
type Error;
|
|
||||||
|
|
||||||
/// Scan the specified `limit` number of blocks from the blockchain, starting at
|
|
||||||
/// `from_height`, applying the provided callback to each block.
|
|
||||||
fn with_blocks<F>(
|
|
||||||
&self,
|
|
||||||
from_height: BlockHeight,
|
|
||||||
limit: Option<u32>,
|
|
||||||
with_row: F,
|
|
||||||
) -> Result<(), Self::Error>
|
|
||||||
where
|
|
||||||
F: FnMut(CompactBlock) -> Result<(), Self::Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "test-dependencies")]
|
#[cfg(feature = "test-dependencies")]
|
||||||
pub mod testing {
|
pub mod testing {
|
||||||
use secrecy::{ExposeSecret, SecretVec};
|
use secrecy::{ExposeSecret, SecretVec};
|
||||||
|
@ -415,46 +403,27 @@ pub mod testing {
|
||||||
memo::Memo,
|
memo::Memo,
|
||||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||||
sapling::{Node, Nullifier},
|
sapling::{Node, Nullifier},
|
||||||
transaction::{components::Amount, Transaction, TxId},
|
transaction::{
|
||||||
|
components::{Amount, OutPoint},
|
||||||
|
Transaction, TxId,
|
||||||
|
},
|
||||||
zip32::{AccountId, ExtendedFullViewingKey},
|
zip32::{AccountId, ExtendedFullViewingKey},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
address::{AddressMetadata, UnifiedAddress},
|
address::{AddressMetadata, UnifiedAddress},
|
||||||
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||||
proto::compact_formats::CompactBlock,
|
|
||||||
wallet::{SpendableNote, WalletTransparentOutput},
|
wallet::{SpendableNote, WalletTransparentOutput},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{DecryptedTransaction, PrunedBlock, SentTransaction, WalletRead, WalletWrite};
|
||||||
error::Error, BlockSource, DecryptedTransaction, PrunedBlock, SentTransaction, WalletRead,
|
|
||||||
WalletWrite,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct MockBlockSource {}
|
|
||||||
|
|
||||||
impl BlockSource for MockBlockSource {
|
|
||||||
type Error = Error<u32>;
|
|
||||||
|
|
||||||
fn with_blocks<F>(
|
|
||||||
&self,
|
|
||||||
_from_height: BlockHeight,
|
|
||||||
_limit: Option<u32>,
|
|
||||||
_with_row: F,
|
|
||||||
) -> Result<(), Self::Error>
|
|
||||||
where
|
|
||||||
F: FnMut(CompactBlock) -> Result<(), Self::Error>,
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct MockWalletDb {
|
pub struct MockWalletDb {
|
||||||
pub network: Network,
|
pub network: Network,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WalletRead for MockWalletDb {
|
impl WalletRead for MockWalletDb {
|
||||||
type Error = Error<u32>;
|
type Error = ();
|
||||||
type NoteRef = u32;
|
type NoteRef = u32;
|
||||||
type TxRef = TxId;
|
type TxRef = TxId;
|
||||||
|
|
||||||
|
@ -514,7 +483,7 @@ pub mod testing {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_transaction(&self, _id_tx: Self::TxRef) -> Result<Transaction, Self::Error> {
|
fn get_transaction(&self, _id_tx: Self::TxRef) -> Result<Transaction, Self::Error> {
|
||||||
Err(Error::ScanRequired) // wrong error but we'll fix it later.
|
Err(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_commitment_tree(
|
fn get_commitment_tree(
|
||||||
|
@ -544,7 +513,8 @@ pub mod testing {
|
||||||
&self,
|
&self,
|
||||||
_account: AccountId,
|
_account: AccountId,
|
||||||
_anchor_height: BlockHeight,
|
_anchor_height: BlockHeight,
|
||||||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
_exclude: &[Self::NoteRef],
|
||||||
|
) -> Result<Vec<SpendableNote<Self::NoteRef>>, Self::Error> {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -553,7 +523,8 @@ pub mod testing {
|
||||||
_account: AccountId,
|
_account: AccountId,
|
||||||
_target_value: Amount,
|
_target_value: Amount,
|
||||||
_anchor_height: BlockHeight,
|
_anchor_height: BlockHeight,
|
||||||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
_exclude: &[Self::NoteRef],
|
||||||
|
) -> Result<Vec<SpendableNote<Self::NoteRef>>, Self::Error> {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -568,6 +539,7 @@ pub mod testing {
|
||||||
&self,
|
&self,
|
||||||
_address: &TransparentAddress,
|
_address: &TransparentAddress,
|
||||||
_anchor_height: BlockHeight,
|
_anchor_height: BlockHeight,
|
||||||
|
_exclude: &[OutPoint],
|
||||||
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
|
@ -591,7 +563,7 @@ pub mod testing {
|
||||||
let account = AccountId::from(0);
|
let account = AccountId::from(0);
|
||||||
UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account)
|
UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account)
|
||||||
.map(|k| (account, k))
|
.map(|k| (account, k))
|
||||||
.map_err(|_| Error::KeyDerivationError(account))
|
.map_err(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_next_available_address(
|
fn get_next_available_address(
|
||||||
|
|
|
@ -12,42 +12,47 @@
|
||||||
//!
|
//!
|
||||||
//! use zcash_client_backend::{
|
//! use zcash_client_backend::{
|
||||||
//! data_api::{
|
//! data_api::{
|
||||||
//! BlockSource, WalletRead, WalletWrite,
|
//! WalletRead, WalletWrite,
|
||||||
//! chain::{
|
//! chain::{
|
||||||
//! validate_chain,
|
//! BlockSource,
|
||||||
|
//! error::Error,
|
||||||
//! scan_cached_blocks,
|
//! scan_cached_blocks,
|
||||||
|
//! validate_chain,
|
||||||
|
//! testing as chain_testing,
|
||||||
//! },
|
//! },
|
||||||
//! error::Error,
|
|
||||||
//! testing,
|
//! testing,
|
||||||
//! },
|
//! },
|
||||||
//! };
|
//! };
|
||||||
//!
|
//!
|
||||||
|
//! # use std::convert::Infallible;
|
||||||
|
//!
|
||||||
//! # fn main() {
|
//! # fn main() {
|
||||||
//! # test();
|
//! # test();
|
||||||
//! # }
|
//! # }
|
||||||
//! #
|
//! #
|
||||||
//! # fn test() -> Result<(), Error<u32>> {
|
//! # fn test() -> Result<(), Error<(), Infallible, u32>> {
|
||||||
//! let network = Network::TestNetwork;
|
//! let network = Network::TestNetwork;
|
||||||
//! let db_cache = testing::MockBlockSource {};
|
//! let block_source = chain_testing::MockBlockSource;
|
||||||
//! let mut db_data = testing::MockWalletDb {
|
//! let mut db_data = testing::MockWalletDb {
|
||||||
//! network: Network::TestNetwork
|
//! network: Network::TestNetwork
|
||||||
//! };
|
//! };
|
||||||
//!
|
//!
|
||||||
//! // 1) Download new CompactBlocks into db_cache.
|
//! // 1) Download new CompactBlocks into block_source.
|
||||||
//!
|
//!
|
||||||
//! // 2) Run the chain validator on the received blocks.
|
//! // 2) Run the chain validator on the received blocks.
|
||||||
//! //
|
//! //
|
||||||
//! // Given that we assume the server always gives us correct-at-the-time blocks, any
|
//! // Given that we assume the server always gives us correct-at-the-time blocks, any
|
||||||
//! // errors are in the blocks we have previously cached or scanned.
|
//! // errors are in the blocks we have previously cached or scanned.
|
||||||
//! if let Err(e) = validate_chain(&network, &db_cache, db_data.get_max_height_hash()?) {
|
//! let max_height_hash = db_data.get_max_height_hash().map_err(Error::Wallet)?;
|
||||||
|
//! if let Err(e) = validate_chain(&network, &block_source, max_height_hash) {
|
||||||
//! match e {
|
//! match e {
|
||||||
//! Error::InvalidChain(lower_bound, _) => {
|
//! Error::Chain(e) => {
|
||||||
//! // a) Pick a height to rewind to.
|
//! // a) Pick a height to rewind to.
|
||||||
//! //
|
//! //
|
||||||
//! // This might be informed by some external chain reorg information, or
|
//! // This might be informed by some external chain reorg information, or
|
||||||
//! // heuristics such as the platform, available bandwidth, size of recent
|
//! // heuristics such as the platform, available bandwidth, size of recent
|
||||||
//! // CompactBlocks, etc.
|
//! // CompactBlocks, etc.
|
||||||
//! let rewind_height = lower_bound - 10;
|
//! let rewind_height = e.at_height() - 10;
|
||||||
//!
|
//!
|
||||||
//! // b) Rewind scanned block information.
|
//! // b) Rewind scanned block information.
|
||||||
//! db_data.rewind_to_height(rewind_height);
|
//! db_data.rewind_to_height(rewind_height);
|
||||||
|
@ -74,12 +79,12 @@
|
||||||
//! // At this point, the cache and scanned data are locally consistent (though not
|
//! // At this point, the cache and scanned data are locally consistent (though not
|
||||||
//! // necessarily consistent with the latest chain tip - this would be discovered the
|
//! // necessarily consistent with the latest chain tip - this would be discovered the
|
||||||
//! // next time this codepath is executed after new blocks are received).
|
//! // next time this codepath is executed after new blocks are received).
|
||||||
//! scan_cached_blocks(&network, &db_cache, &mut db_data, None)
|
//! scan_cached_blocks(&network, &block_source, &mut db_data, None)
|
||||||
//! # }
|
//! # }
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::convert::Infallible;
|
||||||
|
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
block::BlockHash,
|
block::BlockHash,
|
||||||
|
@ -90,56 +95,75 @@ use zcash_primitives::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data_api::{
|
data_api::{PrunedBlock, WalletWrite},
|
||||||
error::{ChainInvalid, Error},
|
|
||||||
BlockSource, PrunedBlock, WalletWrite,
|
|
||||||
},
|
|
||||||
proto::compact_formats::CompactBlock,
|
proto::compact_formats::CompactBlock,
|
||||||
scan::BatchRunner,
|
scan::BatchRunner,
|
||||||
wallet::WalletTx,
|
wallet::WalletTx,
|
||||||
welding_rig::{add_block_to_runner, scan_block_with_runner},
|
welding_rig::{add_block_to_runner, scan_block_with_runner},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
use error::{ChainError, Error};
|
||||||
|
|
||||||
|
/// This trait provides sequential access to raw blockchain data via a callback-oriented
|
||||||
|
/// API.
|
||||||
|
pub trait BlockSource {
|
||||||
|
type Error;
|
||||||
|
|
||||||
|
/// Scan the specified `limit` number of blocks from the blockchain, starting at
|
||||||
|
/// `from_height`, applying the provided callback to each block.
|
||||||
|
///
|
||||||
|
/// * `WalletErrT`: the types of errors produced by the wallet operations performed
|
||||||
|
/// as part of processing each row.
|
||||||
|
/// * `NoteRefT`: the type of note identifiers in the wallet data store, for use in
|
||||||
|
/// reporting errors related to specific notes.
|
||||||
|
fn with_blocks<F, WalletErrT, NoteRefT>(
|
||||||
|
&self,
|
||||||
|
from_height: BlockHeight,
|
||||||
|
limit: Option<u32>,
|
||||||
|
with_row: F,
|
||||||
|
) -> Result<(), error::Error<WalletErrT, Self::Error, NoteRefT>>
|
||||||
|
where
|
||||||
|
F: FnMut(CompactBlock) -> Result<(), error::Error<WalletErrT, Self::Error, NoteRefT>>;
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks that the scanned blocks in the data database, when combined with the recent
|
/// Checks that the scanned blocks in the data database, when combined with the recent
|
||||||
/// `CompactBlock`s in the cache database, form a valid chain.
|
/// `CompactBlock`s in the block_source database, form a valid chain.
|
||||||
///
|
///
|
||||||
/// This function is built on the core assumption that the information provided in the
|
/// This function is built on the core assumption that the information provided in the
|
||||||
/// cache database is more likely to be accurate than the previously-scanned information.
|
/// block source is more likely to be accurate than the previously-scanned information.
|
||||||
/// This follows from the design (and trust) assumption that the `lightwalletd` server
|
/// This follows from the design (and trust) assumption that the `lightwalletd` server
|
||||||
/// provides accurate block information as of the time it was requested.
|
/// provides accurate block information as of the time it was requested.
|
||||||
///
|
///
|
||||||
/// Arguments:
|
/// Arguments:
|
||||||
/// - `parameters` Network parameters
|
/// - `parameters` Network parameters
|
||||||
/// - `cache` Source of compact blocks
|
/// - `block_source` Source of compact blocks
|
||||||
/// - `from_tip` Height & hash of last validated block; if no validation has previously
|
/// - `from_tip` Height & hash of last validated block; if no validation has previously
|
||||||
/// been performed, this will begin scanning from `sapling_activation_height - 1`
|
/// been performed, this will begin scanning from `sapling_activation_height - 1`
|
||||||
///
|
///
|
||||||
/// Returns:
|
/// Returns:
|
||||||
/// - `Ok(())` if the combined chain is valid.
|
/// - `Ok(())` if the combined chain is valid.
|
||||||
/// - `Err(ErrorKind::InvalidChain(upper_bound, cause))` if the combined chain is invalid.
|
/// - `Err(Error::Chain(cause))` if the combined chain is invalid.
|
||||||
/// `upper_bound` is the height of the highest invalid block (on the assumption that the
|
|
||||||
/// highest block in the cache database is correct).
|
|
||||||
/// - `Err(e)` if there was an error during validation unrelated to chain validity.
|
/// - `Err(e)` if there was an error during validation unrelated to chain validity.
|
||||||
///
|
///
|
||||||
/// This function does not mutate either of the databases.
|
/// This function does not mutate either of the databases.
|
||||||
pub fn validate_chain<N, E, P, C>(
|
pub fn validate_chain<ParamsT, BlockSourceT>(
|
||||||
parameters: &P,
|
parameters: &ParamsT,
|
||||||
cache: &C,
|
block_source: &BlockSourceT,
|
||||||
validate_from: Option<(BlockHeight, BlockHash)>,
|
validate_from: Option<(BlockHeight, BlockHash)>,
|
||||||
) -> Result<(), E>
|
) -> Result<(), Error<Infallible, BlockSourceT::Error, Infallible>>
|
||||||
where
|
where
|
||||||
E: From<Error<N>>,
|
ParamsT: consensus::Parameters,
|
||||||
P: consensus::Parameters,
|
BlockSourceT: BlockSource,
|
||||||
C: BlockSource<Error = E>,
|
|
||||||
{
|
{
|
||||||
let sapling_activation_height = parameters
|
let sapling_activation_height = parameters
|
||||||
.activation_height(NetworkUpgrade::Sapling)
|
.activation_height(NetworkUpgrade::Sapling)
|
||||||
.ok_or(Error::SaplingNotActive)?;
|
.expect("Sapling activation height must be known.");
|
||||||
|
|
||||||
// The cache will contain blocks above the `validate_from` height. Validate from that maximum
|
// The block source will contain blocks above the `validate_from` height. Validate from that
|
||||||
// height up to the chain tip, returning the hash of the block found in the cache at the
|
// maximum height up to the chain tip, returning the hash of the block found in the block
|
||||||
// `validate_from` height, which can then be used to verify chain integrity by comparing
|
// source at the `validate_from` height, which can then be used to verify chain integrity by
|
||||||
// against the `validate_from` hash.
|
// comparing against the `validate_from` hash.
|
||||||
let from_height = validate_from
|
let from_height = validate_from
|
||||||
.map(|(height, _)| height)
|
.map(|(height, _)| height)
|
||||||
.unwrap_or(sapling_activation_height - 1);
|
.unwrap_or(sapling_activation_height - 1);
|
||||||
|
@ -147,74 +171,74 @@ where
|
||||||
let mut prev_height = from_height;
|
let mut prev_height = from_height;
|
||||||
let mut prev_hash: Option<BlockHash> = validate_from.map(|(_, hash)| hash);
|
let mut prev_hash: Option<BlockHash> = validate_from.map(|(_, hash)| hash);
|
||||||
|
|
||||||
cache.with_blocks(from_height, None, move |block| {
|
block_source.with_blocks::<_, Infallible, Infallible>(from_height, None, move |block| {
|
||||||
let current_height = block.height();
|
let current_height = block.height();
|
||||||
let result = if current_height != prev_height + 1 {
|
let result = if current_height != prev_height + 1 {
|
||||||
Err(ChainInvalid::block_height_discontinuity(
|
Err(ChainError::block_height_discontinuity(prev_height + 1, current_height).into())
|
||||||
prev_height + 1,
|
|
||||||
current_height,
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
match prev_hash {
|
match prev_hash {
|
||||||
None => Ok(()),
|
None => Ok(()),
|
||||||
Some(h) if h == block.prev_hash() => Ok(()),
|
Some(h) if h == block.prev_hash() => Ok(()),
|
||||||
Some(_) => Err(ChainInvalid::prev_hash_mismatch(current_height)),
|
Some(_) => Err(ChainError::prev_hash_mismatch(current_height).into()),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
prev_height = current_height;
|
prev_height = current_height;
|
||||||
prev_hash = Some(block.hash());
|
prev_hash = Some(block.hash());
|
||||||
result.map_err(E::from)
|
result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_doctest_main)]
|
/// Scans at most `limit` new blocks added to the block source for any transactions received by the
|
||||||
/// Scans at most `limit` new blocks added to the cache for any transactions received by
|
/// tracked accounts.
|
||||||
/// the tracked accounts.
|
|
||||||
///
|
///
|
||||||
/// This function will return without error after scanning at most `limit` new blocks, to
|
/// This function will return without error after scanning at most `limit` new blocks, to enable
|
||||||
/// enable the caller to update their UI with scanning progress. Repeatedly calling this
|
/// the caller to update their UI with scanning progress. Repeatedly calling this function will
|
||||||
/// function will process sequential ranges of blocks, and is equivalent to calling
|
/// process sequential ranges of blocks, and is equivalent to calling `scan_cached_blocks` and
|
||||||
/// `scan_cached_blocks` and passing `None` for the optional `limit` value.
|
/// passing `None` for the optional `limit` value.
|
||||||
///
|
///
|
||||||
/// This function pays attention only to cached blocks with heights greater than the
|
/// This function pays attention only to cached blocks with heights greater than the highest
|
||||||
/// highest scanned block in `data`. Cached blocks with lower heights are not verified
|
/// scanned block in `data`. Cached blocks with lower heights are not verified against
|
||||||
/// against previously-scanned blocks. In particular, this function **assumes** that the
|
/// previously-scanned blocks. In particular, this function **assumes** that the caller is handling
|
||||||
/// caller is handling rollbacks.
|
/// rollbacks.
|
||||||
///
|
///
|
||||||
/// For brand-new light client databases, this function starts scanning from the Sapling
|
/// For brand-new light client databases, this function starts scanning from the Sapling activation
|
||||||
/// activation height. This height can be fast-forwarded to a more recent block by
|
/// height. This height can be fast-forwarded to a more recent block by initializing the client
|
||||||
/// initializing the client database with a starting block (for example, calling
|
/// database with a starting block (for example, calling `init_blocks_table` before this function
|
||||||
/// `init_blocks_table` before this function if using `zcash_client_sqlite`).
|
/// if using `zcash_client_sqlite`).
|
||||||
///
|
///
|
||||||
/// Scanned blocks are required to be height-sequential. If a block is missing from the
|
/// Scanned blocks are required to be height-sequential. If a block is missing from the block
|
||||||
/// cache, an error will be returned with kind [`ChainInvalid::BlockHeightDiscontinuity`].
|
/// source, an error will be returned with cause [`error::Cause::BlockHeightDiscontinuity`].
|
||||||
pub fn scan_cached_blocks<E, N, P, C, D>(
|
#[allow(clippy::type_complexity)]
|
||||||
params: &P,
|
pub fn scan_cached_blocks<ParamsT, DbT, BlockSourceT>(
|
||||||
cache: &C,
|
params: &ParamsT,
|
||||||
data: &mut D,
|
block_source: &BlockSourceT,
|
||||||
|
data_db: &mut DbT,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> Result<(), E>
|
) -> Result<(), Error<DbT::Error, BlockSourceT::Error, DbT::NoteRef>>
|
||||||
where
|
where
|
||||||
P: consensus::Parameters + Send + 'static,
|
ParamsT: consensus::Parameters + Send + 'static,
|
||||||
C: BlockSource<Error = E>,
|
BlockSourceT: BlockSource,
|
||||||
D: WalletWrite<Error = E, NoteRef = N>,
|
DbT: WalletWrite,
|
||||||
N: Copy + Debug,
|
|
||||||
E: From<Error<N>>,
|
|
||||||
{
|
{
|
||||||
let sapling_activation_height = params
|
let sapling_activation_height = params
|
||||||
.activation_height(NetworkUpgrade::Sapling)
|
.activation_height(NetworkUpgrade::Sapling)
|
||||||
.ok_or(Error::SaplingNotActive)?;
|
.expect("Sapling activation height is known.");
|
||||||
|
|
||||||
// Recall where we synced up to previously.
|
// Recall where we synced up to previously.
|
||||||
// If we have never synced, use sapling activation height to select all cached CompactBlocks.
|
// If we have never synced, use sapling activation height to select all cached CompactBlocks.
|
||||||
let mut last_height = data.block_height_extrema().map(|opt| {
|
let mut last_height = data_db
|
||||||
opt.map(|(_, max)| max)
|
.block_height_extrema()
|
||||||
.unwrap_or(sapling_activation_height - 1)
|
.map(|opt| {
|
||||||
})?;
|
opt.map(|(_, max)| max)
|
||||||
|
.unwrap_or(sapling_activation_height - 1)
|
||||||
|
})
|
||||||
|
.map_err(Error::Wallet)?;
|
||||||
|
|
||||||
// Fetch the UnifiedFullViewingKeys we are tracking
|
// Fetch the UnifiedFullViewingKeys we are tracking
|
||||||
let ufvks = data.get_unified_full_viewing_keys()?;
|
let ufvks = data_db
|
||||||
|
.get_unified_full_viewing_keys()
|
||||||
|
.map_err(Error::Wallet)?;
|
||||||
// TODO: Change `scan_block` to also scan Orchard.
|
// TODO: Change `scan_block` to also scan Orchard.
|
||||||
// https://github.com/zcash/librustzcash/issues/403
|
// https://github.com/zcash/librustzcash/issues/403
|
||||||
let dfvks: Vec<_> = ufvks
|
let dfvks: Vec<_> = ufvks
|
||||||
|
@ -223,15 +247,16 @@ where
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Get the most recent CommitmentTree
|
// Get the most recent CommitmentTree
|
||||||
let mut tree = data
|
let mut tree = data_db
|
||||||
.get_commitment_tree(last_height)
|
.get_commitment_tree(last_height)
|
||||||
.map(|t| t.unwrap_or_else(CommitmentTree::empty))?;
|
.map(|t| t.unwrap_or_else(CommitmentTree::empty))
|
||||||
|
.map_err(Error::Wallet)?;
|
||||||
|
|
||||||
// Get most recent incremental witnesses for the notes we are tracking
|
// Get most recent incremental witnesses for the notes we are tracking
|
||||||
let mut witnesses = data.get_witnesses(last_height)?;
|
let mut witnesses = data_db.get_witnesses(last_height).map_err(Error::Wallet)?;
|
||||||
|
|
||||||
// Get the nullifiers for the notes we are tracking
|
// Get the nullifiers for the notes we are tracking
|
||||||
let mut nullifiers = data.get_nullifiers()?;
|
let mut nullifiers = data_db.get_nullifiers().map_err(Error::Wallet)?;
|
||||||
|
|
||||||
let mut batch_runner = BatchRunner::<_, _, _, ()>::new(
|
let mut batch_runner = BatchRunner::<_, _, _, ()>::new(
|
||||||
100,
|
100,
|
||||||
|
@ -246,91 +271,133 @@ where
|
||||||
.map(|(tag, ivk)| (tag, PreparedIncomingViewingKey::new(&ivk))),
|
.map(|(tag, ivk)| (tag, PreparedIncomingViewingKey::new(&ivk))),
|
||||||
);
|
);
|
||||||
|
|
||||||
cache.with_blocks(last_height, limit, |block: CompactBlock| {
|
block_source.with_blocks::<_, DbT::Error, DbT::NoteRef>(
|
||||||
add_block_to_runner(params, block, &mut batch_runner);
|
last_height,
|
||||||
Ok(())
|
limit,
|
||||||
})?;
|
|block: CompactBlock| {
|
||||||
|
add_block_to_runner(params, block, &mut batch_runner);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
batch_runner.flush();
|
batch_runner.flush();
|
||||||
|
|
||||||
cache.with_blocks(last_height, limit, |block: CompactBlock| {
|
block_source.with_blocks::<_, DbT::Error, DbT::NoteRef>(
|
||||||
let current_height = block.height();
|
last_height,
|
||||||
|
limit,
|
||||||
|
|block: CompactBlock| {
|
||||||
|
let current_height = block.height();
|
||||||
|
|
||||||
// Scanned blocks MUST be height-sequential.
|
// Scanned blocks MUST be height-sequential.
|
||||||
if current_height != (last_height + 1) {
|
if current_height != (last_height + 1) {
|
||||||
return Err(
|
return Err(ChainError::block_height_discontinuity(
|
||||||
ChainInvalid::block_height_discontinuity(last_height + 1, current_height).into(),
|
last_height + 1,
|
||||||
);
|
current_height,
|
||||||
}
|
)
|
||||||
|
.into());
|
||||||
let block_hash = BlockHash::from_slice(&block.hash);
|
|
||||||
let block_time = block.time;
|
|
||||||
|
|
||||||
let txs: Vec<WalletTx<Nullifier>> = {
|
|
||||||
let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.1).collect();
|
|
||||||
|
|
||||||
scan_block_with_runner(
|
|
||||||
params,
|
|
||||||
block,
|
|
||||||
&dfvks,
|
|
||||||
&nullifiers,
|
|
||||||
&mut tree,
|
|
||||||
&mut witness_refs[..],
|
|
||||||
Some(&mut batch_runner),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Enforce that all roots match. This is slow, so only include in debug builds.
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
{
|
|
||||||
let cur_root = tree.root();
|
|
||||||
for row in &witnesses {
|
|
||||||
if row.1.root() != cur_root {
|
|
||||||
return Err(Error::InvalidWitnessAnchor(row.0, current_height).into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for tx in &txs {
|
|
||||||
for output in tx.shielded_outputs.iter() {
|
let block_hash = BlockHash::from_slice(&block.hash);
|
||||||
if output.witness.root() != cur_root {
|
let block_time = block.time;
|
||||||
return Err(Error::InvalidNewWitnessAnchor(
|
|
||||||
output.index,
|
let txs: Vec<WalletTx<Nullifier>> = {
|
||||||
tx.txid,
|
let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.1).collect();
|
||||||
current_height,
|
|
||||||
output.witness.root(),
|
scan_block_with_runner(
|
||||||
)
|
params,
|
||||||
.into());
|
block,
|
||||||
|
&dfvks,
|
||||||
|
&nullifiers,
|
||||||
|
&mut tree,
|
||||||
|
&mut witness_refs[..],
|
||||||
|
Some(&mut batch_runner),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enforce that all roots match. This is slow, so only include in debug builds.
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
let cur_root = tree.root();
|
||||||
|
for row in &witnesses {
|
||||||
|
if row.1.root() != cur_root {
|
||||||
|
return Err(
|
||||||
|
ChainError::invalid_witness_anchor(current_height, row.0).into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for tx in &txs {
|
||||||
|
for output in tx.shielded_outputs.iter() {
|
||||||
|
if output.witness.root() != cur_root {
|
||||||
|
return Err(ChainError::invalid_new_witness_anchor(
|
||||||
|
current_height,
|
||||||
|
tx.txid,
|
||||||
|
output.index,
|
||||||
|
output.witness.root(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let new_witnesses = data.advance_by_block(
|
let new_witnesses = data_db
|
||||||
&(PrunedBlock {
|
.advance_by_block(
|
||||||
block_height: current_height,
|
&(PrunedBlock {
|
||||||
block_hash,
|
block_height: current_height,
|
||||||
block_time,
|
block_hash,
|
||||||
commitment_tree: &tree,
|
block_time,
|
||||||
transactions: &txs,
|
commitment_tree: &tree,
|
||||||
}),
|
transactions: &txs,
|
||||||
&witnesses,
|
}),
|
||||||
)?;
|
&witnesses,
|
||||||
|
)
|
||||||
|
.map_err(Error::Wallet)?;
|
||||||
|
|
||||||
let spent_nf: Vec<Nullifier> = txs
|
let spent_nf: Vec<Nullifier> = txs
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|tx| tx.shielded_spends.iter().map(|spend| spend.nf))
|
.flat_map(|tx| tx.shielded_spends.iter().map(|spend| spend.nf))
|
||||||
.collect();
|
.collect();
|
||||||
nullifiers.retain(|(_, nf)| !spent_nf.contains(nf));
|
nullifiers.retain(|(_, nf)| !spent_nf.contains(nf));
|
||||||
nullifiers.extend(
|
nullifiers.extend(
|
||||||
txs.iter()
|
txs.iter()
|
||||||
.flat_map(|tx| tx.shielded_outputs.iter().map(|out| (out.account, out.nf))),
|
.flat_map(|tx| tx.shielded_outputs.iter().map(|out| (out.account, out.nf))),
|
||||||
);
|
);
|
||||||
|
|
||||||
witnesses.extend(new_witnesses);
|
witnesses.extend(new_witnesses);
|
||||||
|
|
||||||
last_height = current_height;
|
last_height = current_height;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "test-dependencies")]
|
||||||
|
pub mod testing {
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use zcash_primitives::consensus::BlockHeight;
|
||||||
|
|
||||||
|
use crate::proto::compact_formats::CompactBlock;
|
||||||
|
|
||||||
|
use super::{error::Error, BlockSource};
|
||||||
|
|
||||||
|
pub struct MockBlockSource;
|
||||||
|
|
||||||
|
impl BlockSource for MockBlockSource {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
fn with_blocks<F, DbErrT, NoteRef>(
|
||||||
|
&self,
|
||||||
|
_from_height: BlockHeight,
|
||||||
|
_limit: Option<u32>,
|
||||||
|
_with_row: F,
|
||||||
|
) -> Result<(), Error<DbErrT, Infallible, NoteRef>>
|
||||||
|
where
|
||||||
|
F: FnMut(CompactBlock) -> Result<(), Error<DbErrT, Infallible, NoteRef>>,
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
//! Types for chain scanning error handling.
|
||||||
|
|
||||||
|
use std::error;
|
||||||
|
use std::fmt::{self, Debug, Display};
|
||||||
|
|
||||||
|
use zcash_primitives::{consensus::BlockHeight, sapling, transaction::TxId};
|
||||||
|
|
||||||
|
/// The underlying cause of a [`ChainError`].
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum Cause<NoteRef> {
|
||||||
|
/// The hash of the parent block given by a proposed new chain tip does not match the hash of
|
||||||
|
/// the current chain tip.
|
||||||
|
PrevHashMismatch,
|
||||||
|
|
||||||
|
/// The block height field of the proposed new chain tip is not equal to the height of the
|
||||||
|
/// previous chain tip + 1. This variant stores a copy of the incorrect height value for
|
||||||
|
/// reporting purposes.
|
||||||
|
BlockHeightDiscontinuity(BlockHeight),
|
||||||
|
|
||||||
|
/// The root of an output's witness tree in a newly arrived transaction does not correspond to
|
||||||
|
/// root of the stored commitment tree at the recorded height.
|
||||||
|
///
|
||||||
|
/// This error is currently only produced when performing the slow checks that are enabled by
|
||||||
|
/// compiling with `-C debug-assertions`.
|
||||||
|
InvalidNewWitnessAnchor {
|
||||||
|
/// The id of the transaction containing the mismatched witness.
|
||||||
|
txid: TxId,
|
||||||
|
/// The index of the shielded output within the transaction where the witness root does not
|
||||||
|
/// match.
|
||||||
|
index: usize,
|
||||||
|
/// The root of the witness that failed to match the root of the current note commitment
|
||||||
|
/// tree.
|
||||||
|
node: sapling::Node,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The root of an output's witness tree in a previously stored transaction does not correspond
|
||||||
|
/// to root of the current commitment tree.
|
||||||
|
///
|
||||||
|
/// This error is currently only produced when performing the slow checks that are enabled by
|
||||||
|
/// compiling with `-C debug-assertions`.
|
||||||
|
InvalidWitnessAnchor(NoteRef),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors that may occur in chain scanning or validation.
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct ChainError<NoteRef> {
|
||||||
|
at_height: BlockHeight,
|
||||||
|
cause: Cause<NoteRef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: Display> fmt::Display for ChainError<N> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match &self.cause {
|
||||||
|
Cause::PrevHashMismatch => write!(
|
||||||
|
f,
|
||||||
|
"The parent hash of proposed block does not correspond to the block hash at height {}.",
|
||||||
|
self.at_height
|
||||||
|
),
|
||||||
|
Cause::BlockHeightDiscontinuity(h) => {
|
||||||
|
write!(f, "Block height discontinuity at height {}; next height is : {}", self.at_height, h)
|
||||||
|
}
|
||||||
|
Cause::InvalidNewWitnessAnchor { txid, index, node } => write!(
|
||||||
|
f,
|
||||||
|
"New witness for output {} in tx {} at height {} has incorrect anchor: {:?}",
|
||||||
|
index, txid, self.at_height, node,
|
||||||
|
),
|
||||||
|
Cause::InvalidWitnessAnchor(id_note) => {
|
||||||
|
write!(f, "Witness for note {} has incorrect anchor for height {}", id_note, self.at_height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<NoteRef> ChainError<NoteRef> {
|
||||||
|
/// Constructs an error that indicates block hashes failed to chain.
|
||||||
|
///
|
||||||
|
/// * `at_height` the height of the block whose parent hash does not match the hash of the
|
||||||
|
/// previous block
|
||||||
|
pub fn prev_hash_mismatch(at_height: BlockHeight) -> Self {
|
||||||
|
ChainError {
|
||||||
|
at_height,
|
||||||
|
cause: Cause::PrevHashMismatch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an error that indicates a gap in block heights.
|
||||||
|
///
|
||||||
|
/// * `at_height` the height of the block being added to the chain.
|
||||||
|
/// * `prev_chain_tip` the height of the previous chain tip.
|
||||||
|
pub fn block_height_discontinuity(at_height: BlockHeight, prev_chain_tip: BlockHeight) -> Self {
|
||||||
|
ChainError {
|
||||||
|
at_height,
|
||||||
|
cause: Cause::BlockHeightDiscontinuity(prev_chain_tip),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an error that indicates a mismatch between an updated note's witness and the
|
||||||
|
/// root of the current note commitment tree.
|
||||||
|
pub fn invalid_witness_anchor(at_height: BlockHeight, note_ref: NoteRef) -> Self {
|
||||||
|
ChainError {
|
||||||
|
at_height,
|
||||||
|
cause: Cause::InvalidWitnessAnchor(note_ref),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an error that indicates a mismatch between a new note's witness and the root of
|
||||||
|
/// the current note commitment tree.
|
||||||
|
pub fn invalid_new_witness_anchor(
|
||||||
|
at_height: BlockHeight,
|
||||||
|
txid: TxId,
|
||||||
|
index: usize,
|
||||||
|
node: sapling::Node,
|
||||||
|
) -> Self {
|
||||||
|
ChainError {
|
||||||
|
at_height,
|
||||||
|
cause: Cause::InvalidNewWitnessAnchor { txid, index, node },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the block height at which this error was discovered.
|
||||||
|
pub fn at_height(&self) -> BlockHeight {
|
||||||
|
self.at_height
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the cause of this error.
|
||||||
|
pub fn cause(&self) -> &Cause<NoteRef> {
|
||||||
|
&self.cause
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors related to chain validation and scanning.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error<WalletError, BlockSourceError, NoteRef> {
|
||||||
|
/// An error that was produced by wallet operations in the course of scanning the chain.
|
||||||
|
Wallet(WalletError),
|
||||||
|
|
||||||
|
/// An error that was produced by the underlying block data store in the process of validation
|
||||||
|
/// or scanning.
|
||||||
|
BlockSource(BlockSourceError),
|
||||||
|
|
||||||
|
/// A block that was received violated rules related to chain continuity or contained note
|
||||||
|
/// commitments that could not be reconciled with the note commitment tree(s) maintained by the
|
||||||
|
/// wallet.
|
||||||
|
Chain(ChainError<NoteRef>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<WE: fmt::Display, BE: fmt::Display, N: Display> fmt::Display for Error<WE, BE, N> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match &self {
|
||||||
|
Error::Wallet(e) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"The underlying datasource produced the following error: {}",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Error::BlockSource(e) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"The underlying block store produced the following error: {}",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Error::Chain(err) => {
|
||||||
|
write!(f, "{}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<WE, BE, N> error::Error for Error<WE, BE, N>
|
||||||
|
where
|
||||||
|
WE: Debug + Display + error::Error + 'static,
|
||||||
|
BE: Debug + Display + error::Error + 'static,
|
||||||
|
N: Debug + Display,
|
||||||
|
{
|
||||||
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||||
|
match &self {
|
||||||
|
Error::Wallet(e) => Some(e),
|
||||||
|
Error::BlockSource(e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<WE, BSE, N> From<ChainError<N>> for Error<WE, BSE, N> {
|
||||||
|
fn from(e: ChainError<N>) -> Self {
|
||||||
|
Error::Chain(e)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,36 +1,32 @@
|
||||||
//! Types for wallet error handling.
|
//! Types for wallet error handling.
|
||||||
|
|
||||||
use std::error;
|
use std::error;
|
||||||
use std::fmt;
|
use std::fmt::{self, Debug, Display};
|
||||||
use zcash_address::unified::Typecode;
|
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
consensus::BlockHeight,
|
|
||||||
sapling::Node,
|
|
||||||
transaction::{
|
transaction::{
|
||||||
builder,
|
builder,
|
||||||
components::amount::{Amount, BalanceError},
|
components::{
|
||||||
TxId,
|
amount::{Amount, BalanceError},
|
||||||
|
sapling, transparent,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
zip32::AccountId,
|
zip32::AccountId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::data_api::wallet::input_selection::InputSelectorError;
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
use zcash_primitives::{legacy::TransparentAddress, zip32::DiversifierIndex};
|
use zcash_primitives::{legacy::TransparentAddress, zip32::DiversifierIndex};
|
||||||
|
|
||||||
|
/// Errors that can occur as a consequence of wallet operations.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ChainInvalid {
|
pub enum Error<DataSourceError, SelectionError, FeeError, NoteRef> {
|
||||||
/// The hash of the parent block given by a proposed new chain tip does
|
/// An error occurred retrieving data from the underlying data source
|
||||||
/// not match the hash of the current chain tip.
|
DataSource(DataSourceError),
|
||||||
PrevHashMismatch,
|
|
||||||
|
|
||||||
/// The block height field of the proposed new chain tip is not equal
|
/// An error in note selection
|
||||||
/// to the height of the previous chain tip + 1. This variant stores
|
NoteSelection(SelectionError),
|
||||||
/// a copy of the incorrect height value for reporting purposes.
|
|
||||||
BlockHeightDiscontinuity(BlockHeight),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error<NoteId> {
|
|
||||||
/// No account could be found corresponding to a provided spending key.
|
/// No account could be found corresponding to a provided spending key.
|
||||||
KeyNotRecognized,
|
KeyNotRecognized,
|
||||||
|
|
||||||
|
@ -41,60 +37,21 @@ pub enum Error<NoteId> {
|
||||||
BalanceError(BalanceError),
|
BalanceError(BalanceError),
|
||||||
|
|
||||||
/// Unable to create a new spend because the wallet balance is not sufficient.
|
/// Unable to create a new spend because the wallet balance is not sufficient.
|
||||||
/// The first argument is the amount available, the second is the amount needed
|
InsufficientFunds { available: Amount, required: Amount },
|
||||||
/// to construct a valid transaction.
|
|
||||||
InsufficientBalance(Amount, Amount),
|
|
||||||
|
|
||||||
/// Chain validation detected an error in the block at the specified block height.
|
|
||||||
InvalidChain(BlockHeight, ChainInvalid),
|
|
||||||
|
|
||||||
/// A provided extsk is not associated with the specified account.
|
|
||||||
InvalidExtSk(AccountId),
|
|
||||||
|
|
||||||
/// The root of an output's witness tree in a newly arrived transaction does
|
|
||||||
/// not correspond to root of the stored commitment tree at the recorded height.
|
|
||||||
///
|
|
||||||
/// The `usize` member of this struct is the index of the shielded output within
|
|
||||||
/// the transaction where the witness root does not match.
|
|
||||||
InvalidNewWitnessAnchor(usize, TxId, BlockHeight, Node),
|
|
||||||
|
|
||||||
/// The root of an output's witness tree in a previously stored transaction
|
|
||||||
/// does not correspond to root of the current commitment tree.
|
|
||||||
InvalidWitnessAnchor(NoteId, BlockHeight),
|
|
||||||
|
|
||||||
/// No key of the given type was associated with the specified account.
|
|
||||||
KeyNotFound(AccountId, Typecode),
|
|
||||||
|
|
||||||
/// The wallet must first perform a scan of the blockchain before other
|
/// The wallet must first perform a scan of the blockchain before other
|
||||||
/// operations can be performed.
|
/// operations can be performed.
|
||||||
ScanRequired,
|
ScanRequired,
|
||||||
|
|
||||||
/// An error occurred building a new transaction.
|
/// An error occurred building a new transaction.
|
||||||
Builder(builder::Error),
|
Builder(builder::Error<FeeError>),
|
||||||
|
|
||||||
/// An error occurred decoding a protobuf message.
|
|
||||||
Protobuf(prost::DecodeError),
|
|
||||||
|
|
||||||
/// The wallet attempted a sapling-only operation at a block
|
|
||||||
/// height when Sapling was not yet active.
|
|
||||||
SaplingNotActive,
|
|
||||||
|
|
||||||
/// It is forbidden to provide a memo when constructing a transparent output.
|
/// It is forbidden to provide a memo when constructing a transparent output.
|
||||||
MemoForbidden,
|
MemoForbidden,
|
||||||
|
|
||||||
/// An error occurred deriving a spending key from a seed and an account
|
|
||||||
/// identifier.
|
|
||||||
KeyDerivationError(AccountId),
|
|
||||||
|
|
||||||
/// A note being spent does not correspond to either the internal or external
|
/// A note being spent does not correspond to either the internal or external
|
||||||
/// full viewing key for an account.
|
/// full viewing key for an account.
|
||||||
// TODO: Return the note id for the note that caused the failure
|
NoteMismatch(NoteRef),
|
||||||
NoteMismatch,
|
|
||||||
|
|
||||||
/// An error indicating that a call was attempted to a method providing
|
|
||||||
/// support
|
|
||||||
#[cfg(not(feature = "transparent-inputs"))]
|
|
||||||
TransparentInputsNotSupported,
|
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
AddressNotRecognized(TransparentAddress),
|
AddressNotRecognized(TransparentAddress),
|
||||||
|
@ -103,95 +60,119 @@ pub enum Error<NoteId> {
|
||||||
ChildIndexOutOfRange(DiversifierIndex),
|
ChildIndexOutOfRange(DiversifierIndex),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChainInvalid {
|
impl<DE, SE, FE, N> fmt::Display for Error<DE, SE, FE, N>
|
||||||
pub fn prev_hash_mismatch<N>(at_height: BlockHeight) -> Error<N> {
|
where
|
||||||
Error::InvalidChain(at_height, ChainInvalid::PrevHashMismatch)
|
DE: fmt::Display,
|
||||||
}
|
SE: fmt::Display,
|
||||||
|
FE: fmt::Display,
|
||||||
pub fn block_height_discontinuity<N>(at_height: BlockHeight, found: BlockHeight) -> Error<N> {
|
N: fmt::Display,
|
||||||
Error::InvalidChain(at_height, ChainInvalid::BlockHeightDiscontinuity(found))
|
{
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: fmt::Display> fmt::Display for Error<N> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match &self {
|
match &self {
|
||||||
|
Error::DataSource(e) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"The underlying datasource produced the following error: {}",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Error::NoteSelection(e) => {
|
||||||
|
write!(f, "Note selection encountered the following error: {}", e)
|
||||||
|
}
|
||||||
Error::KeyNotRecognized => {
|
Error::KeyNotRecognized => {
|
||||||
write!(f, "Wallet does not contain an account corresponding to the provided spending key")
|
write!(
|
||||||
|
f,
|
||||||
|
"Wallet does not contain an account corresponding to the provided spending key"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Error::AccountNotFound(account) => {
|
Error::AccountNotFound(account) => {
|
||||||
write!(f, "Wallet does not contain account {}", u32::from(*account))
|
write!(f, "Wallet does not contain account {}", u32::from(*account))
|
||||||
}
|
}
|
||||||
Error::BalanceError(e) => write!(
|
Error::BalanceError(e) => write!(
|
||||||
f,
|
f,
|
||||||
"The value lies outside the valid range of Zcash amounts: {:?}.", e
|
"The value lies outside the valid range of Zcash amounts: {:?}.",
|
||||||
|
e
|
||||||
),
|
),
|
||||||
Error::InsufficientBalance(have, need) => write!(
|
Error::InsufficientFunds { available, required } => write!(
|
||||||
f,
|
f,
|
||||||
"Insufficient balance (have {}, need {} including fee)",
|
"Insufficient balance (have {}, need {} including fee)",
|
||||||
i64::from(*have), i64::from(*need)
|
i64::from(*available),
|
||||||
|
i64::from(*required)
|
||||||
),
|
),
|
||||||
Error::InvalidChain(upper_bound, cause) => {
|
|
||||||
write!(f, "Invalid chain (upper bound: {}): {:?}", u32::from(*upper_bound), cause)
|
|
||||||
}
|
|
||||||
Error::InvalidExtSk(account) => {
|
|
||||||
write!(f, "Incorrect ExtendedSpendingKey for account {}", u32::from(*account))
|
|
||||||
}
|
|
||||||
Error::InvalidNewWitnessAnchor(output, txid, last_height, anchor) => write!(
|
|
||||||
f,
|
|
||||||
"New witness for output {} in tx {} has incorrect anchor after scanning block {}: {:?}",
|
|
||||||
output, txid, last_height, anchor,
|
|
||||||
),
|
|
||||||
Error::InvalidWitnessAnchor(id_note, last_height) => write!(
|
|
||||||
f,
|
|
||||||
"Witness for note {} has incorrect anchor after scanning block {}",
|
|
||||||
id_note, last_height
|
|
||||||
),
|
|
||||||
Error::KeyNotFound(account, typecode) => {
|
|
||||||
write!(f, "No {:?} key was available for account {}", typecode, u32::from(*account))
|
|
||||||
}
|
|
||||||
Error::ScanRequired => write!(f, "Must scan blocks first"),
|
Error::ScanRequired => write!(f, "Must scan blocks first"),
|
||||||
Error::Builder(e) => write!(f, "{:?}", e),
|
Error::Builder(e) => write!(f, "An error occurred building the transaction: {}", e),
|
||||||
Error::Protobuf(e) => write!(f, "{}", e),
|
|
||||||
Error::SaplingNotActive => write!(f, "Could not determine Sapling upgrade activation height."),
|
|
||||||
Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."),
|
Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."),
|
||||||
Error::KeyDerivationError(acct_id) => write!(f, "Key derivation failed for account {:?}", acct_id),
|
Error::NoteMismatch(n) => write!(f, "A note being spent ({}) does not correspond to either the internal or external full viewing key for the provided spending key.", n),
|
||||||
Error::NoteMismatch => write!(f, "A note being spent does not correspond to either the internal or external full viewing key for the provided spending key."),
|
|
||||||
|
|
||||||
#[cfg(not(feature = "transparent-inputs"))]
|
|
||||||
Error::TransparentInputsNotSupported => {
|
|
||||||
write!(f, "This wallet does not support spending or manipulating transparent UTXOs.")
|
|
||||||
}
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
Error::AddressNotRecognized(_) => {
|
Error::AddressNotRecognized(_) => {
|
||||||
write!(f, "The specified transparent address was not recognized as belonging to the wallet.")
|
write!(f, "The specified transparent address was not recognized as belonging to the wallet.")
|
||||||
}
|
}
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
Error::ChildIndexOutOfRange(i) => {
|
Error::ChildIndexOutOfRange(i) => {
|
||||||
write!(f, "The diversifier index {:?} is out of range for transparent addresses.", i)
|
write!(
|
||||||
|
f,
|
||||||
|
"The diversifier index {:?} is out of range for transparent addresses.",
|
||||||
|
i
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<N: error::Error + 'static> error::Error for Error<N> {
|
impl<DE, SE, FE, N> error::Error for Error<DE, SE, FE, N>
|
||||||
|
where
|
||||||
|
DE: Debug + Display + error::Error + 'static,
|
||||||
|
SE: Debug + Display + error::Error + 'static,
|
||||||
|
FE: Debug + Display + 'static,
|
||||||
|
N: Debug + Display,
|
||||||
|
{
|
||||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||||
match &self {
|
match &self {
|
||||||
|
Error::DataSource(e) => Some(e),
|
||||||
|
Error::NoteSelection(e) => Some(e),
|
||||||
Error::Builder(e) => Some(e),
|
Error::Builder(e) => Some(e),
|
||||||
Error::Protobuf(e) => Some(e),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<N> From<builder::Error> for Error<N> {
|
impl<DE, SE, FE, N> From<builder::Error<FE>> for Error<DE, SE, FE, N> {
|
||||||
fn from(e: builder::Error) -> Self {
|
fn from(e: builder::Error<FE>) -> Self {
|
||||||
Error::Builder(e)
|
Error::Builder(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<N> From<prost::DecodeError> for Error<N> {
|
impl<DE, SE, FE, N> From<BalanceError> for Error<DE, SE, FE, N> {
|
||||||
fn from(e: prost::DecodeError) -> Self {
|
fn from(e: BalanceError) -> Self {
|
||||||
Error::Protobuf(e)
|
Error::BalanceError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DE, SE, FE, N> From<InputSelectorError<DE, SE>> for Error<DE, SE, FE, N> {
|
||||||
|
fn from(e: InputSelectorError<DE, SE>) -> Self {
|
||||||
|
match e {
|
||||||
|
InputSelectorError::DataSource(e) => Error::DataSource(e),
|
||||||
|
InputSelectorError::Selection(e) => Error::NoteSelection(e),
|
||||||
|
InputSelectorError::InsufficientFunds {
|
||||||
|
available,
|
||||||
|
required,
|
||||||
|
} => Error::InsufficientFunds {
|
||||||
|
available,
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DE, SE, FE, N> From<sapling::builder::Error> for Error<DE, SE, FE, N> {
|
||||||
|
fn from(e: sapling::builder::Error) -> Self {
|
||||||
|
Error::Builder(builder::Error::SaplingBuild(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DE, SE, FE, N> From<transparent::builder::Error> for Error<DE, SE, FE, N> {
|
||||||
|
fn from(e: transparent::builder::Error) -> Self {
|
||||||
|
Error::Builder(builder::Error::TransparentBuild(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
consensus::{self, NetworkUpgrade},
|
consensus::{self, NetworkUpgrade},
|
||||||
memo::MemoBytes,
|
memo::MemoBytes,
|
||||||
sapling::prover::TxProver,
|
merkle_tree::MerklePath,
|
||||||
|
sapling::{self, prover::TxProver as SaplingProver},
|
||||||
transaction::{
|
transaction::{
|
||||||
builder::Builder,
|
builder::Builder,
|
||||||
components::amount::{Amount, BalanceError, DEFAULT_FEE},
|
components::amount::{Amount, BalanceError},
|
||||||
|
fees::{fixed, FeeRule},
|
||||||
Transaction,
|
Transaction,
|
||||||
},
|
},
|
||||||
zip32::Scope,
|
zip32::{sapling::DiversifiableFullViewingKey, sapling::ExtendedSpendingKey, Scope},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -18,29 +21,31 @@ use crate::{
|
||||||
SentTransactionOutput, WalletWrite,
|
SentTransactionOutput, WalletWrite,
|
||||||
},
|
},
|
||||||
decrypt_transaction,
|
decrypt_transaction,
|
||||||
fees::{BasicFixedFeeChangeStrategy, ChangeError, ChangeStrategy, ChangeValue},
|
fees::{self, ChangeValue, DustOutputPolicy},
|
||||||
keys::UnifiedSpendingKey,
|
keys::UnifiedSpendingKey,
|
||||||
wallet::OvkPolicy,
|
wallet::{OvkPolicy, SpendableNote},
|
||||||
zip321::{Payment, TransactionRequest},
|
zip321::{self, Payment},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod input_selection;
|
||||||
|
use input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector};
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
use {
|
use zcash_primitives::{
|
||||||
crate::wallet::WalletTransparentOutput,
|
legacy::TransparentAddress, sapling::keys::OutgoingViewingKey,
|
||||||
zcash_primitives::{legacy::TransparentAddress, sapling::keys::OutgoingViewingKey},
|
transaction::components::amount::NonNegativeAmount,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
|
/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
|
||||||
/// the wallet, and saves it to the wallet.
|
/// the wallet, and saves it to the wallet.
|
||||||
pub fn decrypt_and_store_transaction<N, E, P, D>(
|
pub fn decrypt_and_store_transaction<ParamsT, DbT>(
|
||||||
params: &P,
|
params: &ParamsT,
|
||||||
data: &mut D,
|
data: &mut DbT,
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
) -> Result<(), E>
|
) -> Result<(), DbT::Error>
|
||||||
where
|
where
|
||||||
E: From<Error<N>>,
|
ParamsT: consensus::Parameters,
|
||||||
P: consensus::Parameters,
|
DbT: WalletWrite,
|
||||||
D: WalletWrite<Error = E>,
|
|
||||||
{
|
{
|
||||||
// Fetch the UnifiedFullViewingKeys we are tracking
|
// Fetch the UnifiedFullViewingKeys we are tracking
|
||||||
let ufvks = data.get_unified_full_viewing_keys()?;
|
let ufvks = data.get_unified_full_viewing_keys()?;
|
||||||
|
@ -53,7 +58,7 @@ where
|
||||||
.block_height_extrema()?
|
.block_height_extrema()?
|
||||||
.map(|(_, max_height)| max_height + 1))
|
.map(|(_, max_height)| max_height + 1))
|
||||||
.or_else(|| params.activation_height(NetworkUpgrade::Sapling))
|
.or_else(|| params.activation_height(NetworkUpgrade::Sapling))
|
||||||
.ok_or(Error::SaplingNotActive)?;
|
.expect("Sapling activation height must be known.");
|
||||||
|
|
||||||
data.store_decrypted_tx(&DecryptedTransaction {
|
data.store_decrypted_tx(&DecryptedTransaction {
|
||||||
tx,
|
tx,
|
||||||
|
@ -93,7 +98,7 @@ where
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
/// * `wallet_db`: A read/write reference to the wallet database
|
/// * `wallet_db`: A read/write reference to the wallet database
|
||||||
/// * `params`: Consensus parameters
|
/// * `params`: Consensus parameters
|
||||||
/// * `prover`: The TxProver to use in constructing the shielded transaction.
|
/// * `prover`: The [`sapling::TxProver`] to use in constructing the shielded transaction.
|
||||||
/// * `usk`: The unified spending key that controls the funds that will be spent
|
/// * `usk`: The unified spending key that controls the funds that will be spent
|
||||||
/// in the resulting transaction. This procedure will return an error if the
|
/// in the resulting transaction. This procedure will return an error if the
|
||||||
/// USK does not correspond to an account known to the wallet.
|
/// USK does not correspond to an account known to the wallet.
|
||||||
|
@ -125,11 +130,18 @@ where
|
||||||
/// wallet::OvkPolicy,
|
/// wallet::OvkPolicy,
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
|
/// # use std::convert::Infallible;
|
||||||
|
/// # use zcash_primitives::transaction::components::amount::BalanceError;
|
||||||
|
/// # use zcash_client_backend::{
|
||||||
|
/// # data_api::wallet::input_selection::GreedyInputSelectorError,
|
||||||
|
/// # };
|
||||||
|
/// #
|
||||||
/// # fn main() {
|
/// # fn main() {
|
||||||
/// # test();
|
/// # test();
|
||||||
/// # }
|
/// # }
|
||||||
/// #
|
/// #
|
||||||
/// # fn test() -> Result<TxId, Error<u32>> {
|
/// # #[allow(deprecated)]
|
||||||
|
/// # fn test() -> Result<TxId, Error<(), GreedyInputSelectorError<BalanceError, u32>, Infallible, u32>> {
|
||||||
///
|
///
|
||||||
/// let tx_prover = match LocalTxProver::with_default_location() {
|
/// let tx_prover = match LocalTxProver::with_default_location() {
|
||||||
/// Some(tx_prover) => tx_prover,
|
/// Some(tx_prover) => tx_prover,
|
||||||
|
@ -161,25 +173,35 @@ where
|
||||||
/// # }
|
/// # }
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
|
/// [`sapling::TxProver`]: zcash_primitives::sapling::prover::TxProver
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn create_spend_to_address<E, N, P, D, R>(
|
#[allow(clippy::type_complexity)]
|
||||||
wallet_db: &mut D,
|
#[deprecated(note = "Use `spend` instead.")]
|
||||||
params: &P,
|
pub fn create_spend_to_address<DbT, ParamsT>(
|
||||||
prover: impl TxProver,
|
wallet_db: &mut DbT,
|
||||||
|
params: &ParamsT,
|
||||||
|
prover: impl SaplingProver,
|
||||||
usk: &UnifiedSpendingKey,
|
usk: &UnifiedSpendingKey,
|
||||||
to: &RecipientAddress,
|
to: &RecipientAddress,
|
||||||
amount: Amount,
|
amount: Amount,
|
||||||
memo: Option<MemoBytes>,
|
memo: Option<MemoBytes>,
|
||||||
ovk_policy: OvkPolicy,
|
ovk_policy: OvkPolicy,
|
||||||
min_confirmations: u32,
|
min_confirmations: u32,
|
||||||
) -> Result<R, E>
|
) -> Result<
|
||||||
|
DbT::TxRef,
|
||||||
|
Error<
|
||||||
|
DbT::Error,
|
||||||
|
GreedyInputSelectorError<BalanceError, DbT::NoteRef>,
|
||||||
|
core::convert::Infallible,
|
||||||
|
DbT::NoteRef,
|
||||||
|
>,
|
||||||
|
>
|
||||||
where
|
where
|
||||||
E: From<Error<N>>,
|
ParamsT: consensus::Parameters + Clone,
|
||||||
P: consensus::Parameters + Clone,
|
DbT: WalletWrite,
|
||||||
R: Copy + Debug,
|
DbT::NoteRef: Copy + Eq + Ord,
|
||||||
D: WalletWrite<Error = E, TxRef = R>,
|
|
||||||
{
|
{
|
||||||
let req = TransactionRequest::new(vec![Payment {
|
let req = zip321::TransactionRequest::new(vec![Payment {
|
||||||
recipient_address: to.clone(),
|
recipient_address: to.clone(),
|
||||||
amount,
|
amount,
|
||||||
memo,
|
memo,
|
||||||
|
@ -191,12 +213,14 @@ where
|
||||||
"It should not be possible for this to violate ZIP 321 request construction invariants.",
|
"It should not be possible for this to violate ZIP 321 request construction invariants.",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let change_strategy = fees::fixed::SingleOutputChangeStrategy::new(fixed::FeeRule::standard());
|
||||||
spend(
|
spend(
|
||||||
wallet_db,
|
wallet_db,
|
||||||
params,
|
params,
|
||||||
prover,
|
prover,
|
||||||
|
&GreedyInputSelector::<DbT, _>::new(change_strategy, DustOutputPolicy::default()),
|
||||||
usk,
|
usk,
|
||||||
&req,
|
req,
|
||||||
ovk_policy,
|
ovk_policy,
|
||||||
min_confirmations,
|
min_confirmations,
|
||||||
)
|
)
|
||||||
|
@ -235,7 +259,10 @@ where
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
/// * `wallet_db`: A read/write reference to the wallet database
|
/// * `wallet_db`: A read/write reference to the wallet database
|
||||||
/// * `params`: Consensus parameters
|
/// * `params`: Consensus parameters
|
||||||
/// * `prover`: The TxProver to use in constructing the shielded transaction.
|
/// * `prover`: The [`sapling::TxProver`] to use in constructing the shielded transaction.
|
||||||
|
/// * `input_selector`: The [`InputSelector`] that will be used to select available
|
||||||
|
/// inputs from the wallet database, choose change amounts and compute required
|
||||||
|
/// transaction fees.
|
||||||
/// * `usk`: The unified spending key that controls the funds that will be spent
|
/// * `usk`: The unified spending key that controls the funds that will be spent
|
||||||
/// in the resulting transaction. This procedure will return an error if the
|
/// in the resulting transaction. This procedure will return an error if the
|
||||||
/// USK does not correspond to an account known to the wallet.
|
/// USK does not correspond to an account known to the wallet.
|
||||||
|
@ -246,24 +273,33 @@ where
|
||||||
/// * `min_confirmations`: The minimum number of confirmations that a previously
|
/// * `min_confirmations`: The minimum number of confirmations that a previously
|
||||||
/// received note must have in the blockchain in order to be considered for being
|
/// received note must have in the blockchain in order to be considered for being
|
||||||
/// spent. A value of 10 confirmations is recommended.
|
/// spent. A value of 10 confirmations is recommended.
|
||||||
|
///
|
||||||
|
/// [`sapling::TxProver`]: zcash_primitives::sapling::prover::TxProver
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn spend<E, N, P, D, R>(
|
#[allow(clippy::type_complexity)]
|
||||||
wallet_db: &mut D,
|
pub fn spend<DbT, ParamsT, InputsT>(
|
||||||
params: &P,
|
wallet_db: &mut DbT,
|
||||||
prover: impl TxProver,
|
params: &ParamsT,
|
||||||
|
prover: impl SaplingProver,
|
||||||
|
input_selector: &InputsT,
|
||||||
usk: &UnifiedSpendingKey,
|
usk: &UnifiedSpendingKey,
|
||||||
request: &TransactionRequest,
|
request: zip321::TransactionRequest,
|
||||||
ovk_policy: OvkPolicy,
|
ovk_policy: OvkPolicy,
|
||||||
min_confirmations: u32,
|
min_confirmations: u32,
|
||||||
) -> Result<R, E>
|
) -> Result<
|
||||||
|
DbT::TxRef,
|
||||||
|
Error<DbT::Error, InputsT::Error, <InputsT::FeeRule as FeeRule>::Error, DbT::NoteRef>,
|
||||||
|
>
|
||||||
where
|
where
|
||||||
E: From<Error<N>>,
|
DbT: WalletWrite,
|
||||||
P: consensus::Parameters + Clone,
|
DbT::TxRef: Copy + Debug,
|
||||||
R: Copy + Debug,
|
DbT::NoteRef: Copy + Eq + Ord,
|
||||||
D: WalletWrite<Error = E, TxRef = R>,
|
ParamsT: consensus::Parameters + Clone,
|
||||||
|
InputsT: InputSelector<DataSource = DbT>,
|
||||||
{
|
{
|
||||||
let account = wallet_db
|
let account = wallet_db
|
||||||
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())?
|
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
|
||||||
|
.map_err(Error::DataSource)?
|
||||||
.ok_or(Error::KeyNotRecognized)?;
|
.ok_or(Error::KeyNotRecognized)?;
|
||||||
|
|
||||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||||
|
@ -278,135 +314,74 @@ where
|
||||||
// Target the next block, assuming we are up-to-date.
|
// Target the next block, assuming we are up-to-date.
|
||||||
let (target_height, anchor_height) = wallet_db
|
let (target_height, anchor_height) = wallet_db
|
||||||
.get_target_and_anchor_heights(min_confirmations)
|
.get_target_and_anchor_heights(min_confirmations)
|
||||||
.and_then(|x| x.ok_or_else(|| Error::ScanRequired.into()))?;
|
.map_err(Error::DataSource)
|
||||||
|
.and_then(|x| x.ok_or(Error::ScanRequired))?;
|
||||||
|
|
||||||
let value = request
|
let proposal = input_selector.propose_transaction(
|
||||||
.payments()
|
params,
|
||||||
.iter()
|
wallet_db,
|
||||||
.map(|p| p.amount)
|
account,
|
||||||
.sum::<Option<Amount>>()
|
anchor_height,
|
||||||
.ok_or_else(|| E::from(Error::BalanceError(BalanceError::Overflow)))?;
|
target_height,
|
||||||
let target_value = (value + DEFAULT_FEE)
|
request,
|
||||||
.ok_or_else(|| E::from(Error::BalanceError(BalanceError::Overflow)))?;
|
)?;
|
||||||
let spendable_notes =
|
|
||||||
wallet_db.select_spendable_sapling_notes(account, target_value, anchor_height)?;
|
|
||||||
|
|
||||||
// Confirm we were able to select sufficient value
|
// Create the transaction. The type of the proposal ensures that there
|
||||||
let selected_value = spendable_notes
|
// are no possible transparent inputs, so we ignore those
|
||||||
.iter()
|
|
||||||
.map(|n| n.note_value)
|
|
||||||
.sum::<Option<_>>()
|
|
||||||
.ok_or_else(|| E::from(Error::BalanceError(BalanceError::Overflow)))?;
|
|
||||||
if selected_value < target_value {
|
|
||||||
return Err(E::from(Error::InsufficientBalance(
|
|
||||||
selected_value,
|
|
||||||
target_value,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the transaction
|
|
||||||
let mut builder = Builder::new(params.clone(), target_height);
|
let mut builder = Builder::new(params.clone(), target_height);
|
||||||
for selected in spendable_notes {
|
for selected in proposal.sapling_inputs() {
|
||||||
let merkle_path = selected.witness.path().expect("the tree is not empty");
|
let (note, key, merkle_path) = select_key_for_note(selected, usk.sapling(), &dfvk)
|
||||||
|
.ok_or(Error::NoteMismatch(selected.note_id))?;
|
||||||
|
|
||||||
// Attempt to reconstruct the note being spent using both the internal and external dfvks
|
builder.add_sapling_spend(key, selected.diversifier, note, merkle_path)?;
|
||||||
// corresponding to the unified spending key, checking against the witness we are using
|
|
||||||
// to spend the note that we've used the correct key.
|
|
||||||
let (note, key) = {
|
|
||||||
let external_note = dfvk
|
|
||||||
.diversified_address(selected.diversifier)
|
|
||||||
.and_then(|addr| addr.create_note(selected.note_value.into(), selected.rseed));
|
|
||||||
let internal_note = dfvk
|
|
||||||
.diversified_change_address(selected.diversifier)
|
|
||||||
.and_then(|addr| addr.create_note(selected.note_value.into(), selected.rseed));
|
|
||||||
|
|
||||||
let expected_root = selected.witness.root();
|
|
||||||
external_note
|
|
||||||
.filter(|n| expected_root == merkle_path.root(n.commitment()))
|
|
||||||
.map(|n| (n, usk.sapling().clone()))
|
|
||||||
.or_else(|| {
|
|
||||||
internal_note
|
|
||||||
.filter(|n| expected_root == merkle_path.root(n.commitment()))
|
|
||||||
.map(|n| (n, usk.sapling().derive_internal()))
|
|
||||||
})
|
|
||||||
.ok_or_else(|| E::from(Error::NoteMismatch))
|
|
||||||
}?;
|
|
||||||
|
|
||||||
builder
|
|
||||||
.add_sapling_spend(key, selected.diversifier, note, merkle_path)
|
|
||||||
.map_err(Error::Builder)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for payment in request.payments() {
|
for payment in proposal.transaction_request().payments() {
|
||||||
match &payment.recipient_address {
|
match &payment.recipient_address {
|
||||||
RecipientAddress::Unified(ua) => builder
|
RecipientAddress::Unified(ua) => {
|
||||||
.add_sapling_output(
|
builder.add_sapling_output(
|
||||||
ovk,
|
ovk,
|
||||||
ua.sapling()
|
ua.sapling()
|
||||||
.expect("TODO: Add Orchard support to builder")
|
.expect("TODO: Add Orchard support to builder")
|
||||||
.clone(),
|
.clone(),
|
||||||
payment.amount,
|
payment.amount,
|
||||||
payment.memo.clone().unwrap_or_else(MemoBytes::empty),
|
payment.memo.clone().unwrap_or_else(MemoBytes::empty),
|
||||||
)
|
)?;
|
||||||
.map_err(Error::Builder),
|
}
|
||||||
RecipientAddress::Shielded(to) => builder
|
RecipientAddress::Shielded(to) => {
|
||||||
.add_sapling_output(
|
builder.add_sapling_output(
|
||||||
ovk,
|
ovk,
|
||||||
to.clone(),
|
to.clone(),
|
||||||
payment.amount,
|
payment.amount,
|
||||||
payment.memo.clone().unwrap_or_else(MemoBytes::empty),
|
payment.memo.clone().unwrap_or_else(MemoBytes::empty),
|
||||||
)
|
)?;
|
||||||
.map_err(Error::Builder),
|
}
|
||||||
RecipientAddress::Transparent(to) => {
|
RecipientAddress::Transparent(to) => {
|
||||||
if payment.memo.is_some() {
|
if payment.memo.is_some() {
|
||||||
Err(Error::MemoForbidden)
|
return Err(Error::MemoForbidden);
|
||||||
} else {
|
} else {
|
||||||
builder
|
builder.add_transparent_output(to, payment.amount)?;
|
||||||
.add_transparent_output(to, payment.amount)
|
|
||||||
.map_err(Error::Builder)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}?
|
|
||||||
}
|
|
||||||
|
|
||||||
let fee_strategy = BasicFixedFeeChangeStrategy::new(DEFAULT_FEE);
|
|
||||||
let balance = fee_strategy
|
|
||||||
.compute_balance(
|
|
||||||
params,
|
|
||||||
target_height,
|
|
||||||
builder.transparent_inputs(),
|
|
||||||
builder.transparent_outputs(),
|
|
||||||
builder.sapling_inputs(),
|
|
||||||
builder.sapling_outputs(),
|
|
||||||
)
|
|
||||||
.map_err(|e| match e {
|
|
||||||
ChangeError::InsufficientFunds {
|
|
||||||
available,
|
|
||||||
required,
|
|
||||||
} => Error::InsufficientBalance(available, required),
|
|
||||||
ChangeError::StrategyError(e) => Error::BalanceError(e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for change_value in balance.proposed_change() {
|
|
||||||
match change_value {
|
|
||||||
ChangeValue::Sapling(amount) => {
|
|
||||||
builder
|
|
||||||
.add_sapling_output(
|
|
||||||
Some(dfvk.to_ovk(Scope::Internal)),
|
|
||||||
dfvk.change_address().1,
|
|
||||||
*amount,
|
|
||||||
MemoBytes::empty(),
|
|
||||||
)
|
|
||||||
.map_err(Error::Builder)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (tx, tx_metadata) = builder
|
for change_value in proposal.balance().proposed_change() {
|
||||||
.build(&prover, &fee_strategy.fee_rule())
|
match change_value {
|
||||||
.map_err(Error::Builder)?;
|
ChangeValue::Sapling(amount) => {
|
||||||
|
builder.add_sapling_output(
|
||||||
|
Some(dfvk.to_ovk(Scope::Internal)),
|
||||||
|
dfvk.change_address().1,
|
||||||
|
*amount,
|
||||||
|
MemoBytes::empty(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let sent_outputs = request.payments().iter().enumerate().map(|(i, payment)| {
|
let (tx, tx_metadata) = builder.build(&prover, proposal.fee_rule())?;
|
||||||
|
|
||||||
|
let sent_outputs = proposal.transaction_request().payments().iter().enumerate().map(|(i, payment)| {
|
||||||
let (output_index, recipient) = match &payment.recipient_address {
|
let (output_index, recipient) = match &payment.recipient_address {
|
||||||
// Sapling outputs are shuffled, so we need to look up where the output ended up.
|
// Sapling outputs are shuffled, so we need to look up where the output ended up.
|
||||||
RecipientAddress::Shielded(addr) => {
|
RecipientAddress::Shielded(addr) => {
|
||||||
|
@ -443,15 +418,17 @@ where
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
wallet_db.store_sent_tx(&SentTransaction {
|
wallet_db
|
||||||
tx: &tx,
|
.store_sent_tx(&SentTransaction {
|
||||||
created: time::OffsetDateTime::now_utc(),
|
tx: &tx,
|
||||||
account,
|
created: time::OffsetDateTime::now_utc(),
|
||||||
outputs: sent_outputs,
|
account,
|
||||||
fee_amount: balance.fee_required(),
|
outputs: sent_outputs,
|
||||||
#[cfg(feature = "transparent-inputs")]
|
fee_amount: proposal.balance().fee_required(),
|
||||||
utxos_spent: vec![],
|
#[cfg(feature = "transparent-inputs")]
|
||||||
})
|
utxos_spent: vec![],
|
||||||
|
})
|
||||||
|
.map_err(Error::DataSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a transaction that consumes available transparent UTXOs belonging to
|
/// Constructs a transaction that consumes available transparent UTXOs belonging to
|
||||||
|
@ -465,13 +442,18 @@ where
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
/// * `wallet_db`: A read/write reference to the wallet database
|
/// * `wallet_db`: A read/write reference to the wallet database
|
||||||
/// * `params`: Consensus parameters
|
/// * `params`: Consensus parameters
|
||||||
/// * `prover`: The TxProver to use in constructing the shielded transaction.
|
/// * `prover`: The [`sapling::TxProver`] to use in constructing the shielded transaction.
|
||||||
|
/// * `input_selector`: The [`InputSelector`] to for note selection and change and fee
|
||||||
|
/// determination
|
||||||
/// * `usk`: The unified spending key that will be used to detect and spend transparent UTXOs,
|
/// * `usk`: The unified spending key that will be used to detect and spend transparent UTXOs,
|
||||||
/// and that will provide the shielded address to which funds will be sent. Funds will be
|
/// and that will provide the shielded address to which funds will be sent. Funds will be
|
||||||
/// shielded to the internal (change) address associated with the most preferred shielded
|
/// shielded to the internal (change) address associated with the most preferred shielded
|
||||||
/// receiver corresponding to this account, or if no shielded receiver can be used for this
|
/// receiver corresponding to this account, or if no shielded receiver can be used for this
|
||||||
/// account, this function will return an error. This procedure will return an error if the
|
/// account, this function will return an error. This procedure will return an error if the
|
||||||
/// USK does not correspond to an account known to the wallet.
|
/// USK does not correspond to an account known to the wallet.
|
||||||
|
/// * `from_addrs`: The list of transparent addresses that will be used to filter transaparent
|
||||||
|
/// UTXOs received by the wallet. Only UTXOs received at one of the provided addresses will
|
||||||
|
/// be selected to be shielded.
|
||||||
/// * `memo`: A memo to be included in the output to the (internal) recipient.
|
/// * `memo`: A memo to be included in the output to the (internal) recipient.
|
||||||
/// This can be used to take notes about auto-shielding operations internal
|
/// This can be used to take notes about auto-shielding operations internal
|
||||||
/// to the wallet that the wallet can use to improve how it represents those
|
/// to the wallet that the wallet can use to improve how it represents those
|
||||||
|
@ -479,25 +461,33 @@ where
|
||||||
/// * `min_confirmations`: The minimum number of confirmations that a previously
|
/// * `min_confirmations`: The minimum number of confirmations that a previously
|
||||||
/// received UTXO must have in the blockchain in order to be considered for being
|
/// received UTXO must have in the blockchain in order to be considered for being
|
||||||
/// spent.
|
/// spent.
|
||||||
|
///
|
||||||
|
/// [`sapling::TxProver`]: zcash_primitives::sapling::prover::TxProver
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn shield_transparent_funds<E, N, P, D, R, U>(
|
#[allow(clippy::type_complexity)]
|
||||||
wallet_db: &mut D,
|
pub fn shield_transparent_funds<DbT, ParamsT, InputsT>(
|
||||||
params: &P,
|
wallet_db: &mut DbT,
|
||||||
prover: impl TxProver,
|
params: &ParamsT,
|
||||||
|
prover: impl SaplingProver,
|
||||||
|
input_selector: &InputsT,
|
||||||
usk: &UnifiedSpendingKey,
|
usk: &UnifiedSpendingKey,
|
||||||
from_addrs: &[TransparentAddress],
|
from_addrs: &[TransparentAddress],
|
||||||
memo: &MemoBytes,
|
memo: &MemoBytes,
|
||||||
min_confirmations: u32,
|
min_confirmations: u32,
|
||||||
) -> Result<D::TxRef, E>
|
) -> Result<
|
||||||
|
DbT::TxRef,
|
||||||
|
Error<DbT::Error, InputsT::Error, <InputsT::FeeRule as FeeRule>::Error, DbT::NoteRef>,
|
||||||
|
>
|
||||||
where
|
where
|
||||||
E: From<Error<N>>,
|
ParamsT: consensus::Parameters,
|
||||||
P: consensus::Parameters,
|
DbT: WalletWrite,
|
||||||
R: Copy + Debug,
|
DbT::NoteRef: Copy + Eq + Ord,
|
||||||
D: WalletWrite<Error = E, TxRef = R, UtxoRef = U>,
|
InputsT: InputSelector<DataSource = DbT>,
|
||||||
{
|
{
|
||||||
let account = wallet_db
|
let account = wallet_db
|
||||||
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())?
|
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
|
||||||
|
.map_err(Error::DataSource)?
|
||||||
.ok_or(Error::KeyNotRecognized)?;
|
.ok_or(Error::KeyNotRecognized)?;
|
||||||
|
|
||||||
let shielding_address = usk
|
let shielding_address = usk
|
||||||
|
@ -507,29 +497,39 @@ where
|
||||||
.1;
|
.1;
|
||||||
let (target_height, latest_anchor) = wallet_db
|
let (target_height, latest_anchor) = wallet_db
|
||||||
.get_target_and_anchor_heights(min_confirmations)
|
.get_target_and_anchor_heights(min_confirmations)
|
||||||
.and_then(|x| x.ok_or_else(|| Error::ScanRequired.into()))?;
|
.map_err(Error::DataSource)
|
||||||
|
.and_then(|x| x.ok_or(Error::ScanRequired))?;
|
||||||
|
|
||||||
|
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||||
let account_pubkey = usk.transparent().to_account_pubkey();
|
let account_pubkey = usk.transparent().to_account_pubkey();
|
||||||
let ovk = OutgoingViewingKey(account_pubkey.internal_ovk().as_bytes());
|
let ovk = OutgoingViewingKey(account_pubkey.internal_ovk().as_bytes());
|
||||||
|
|
||||||
// get UTXOs from DB for each address
|
// get UTXOs from DB for each address
|
||||||
let mut utxos: Vec<WalletTransparentOutput> = vec![];
|
let proposal = input_selector.propose_shielding(
|
||||||
for from_addr in from_addrs {
|
params,
|
||||||
let mut outputs = wallet_db.get_unspent_transparent_outputs(from_addr, latest_anchor)?;
|
wallet_db,
|
||||||
utxos.append(&mut outputs);
|
NonNegativeAmount::from_u64(100000).unwrap(),
|
||||||
}
|
from_addrs,
|
||||||
|
latest_anchor,
|
||||||
|
target_height,
|
||||||
|
)?;
|
||||||
|
|
||||||
let _total_amount = utxos
|
let known_addrs = wallet_db
|
||||||
.iter()
|
.get_transparent_receivers(account)
|
||||||
.map(|utxo| utxo.txout().value)
|
.map_err(Error::DataSource)?;
|
||||||
.sum::<Option<Amount>>()
|
|
||||||
.ok_or_else(|| E::from(Error::BalanceError(BalanceError::Overflow)))?;
|
|
||||||
|
|
||||||
let addr_metadata = wallet_db.get_transparent_receivers(account)?;
|
|
||||||
let mut builder = Builder::new(params.clone(), target_height);
|
let mut builder = Builder::new(params.clone(), target_height);
|
||||||
|
|
||||||
for utxo in &utxos {
|
let mut utxos = vec![];
|
||||||
let diversifier_index = addr_metadata
|
for selected in proposal.sapling_inputs() {
|
||||||
|
let (note, key, merkle_path) = select_key_for_note(selected, usk.sapling(), &dfvk)
|
||||||
|
.ok_or(Error::NoteMismatch(selected.note_id))?;
|
||||||
|
|
||||||
|
builder.add_sapling_spend(key, selected.diversifier, note, merkle_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for utxo in proposal.transparent_inputs() {
|
||||||
|
utxos.push(utxo);
|
||||||
|
let diversifier_index = known_addrs
|
||||||
.get(utxo.recipient_address())
|
.get(utxo.recipient_address())
|
||||||
.ok_or_else(|| Error::AddressNotRecognized(*utxo.recipient_address()))?
|
.ok_or_else(|| Error::AddressNotRecognized(*utxo.recipient_address()))?
|
||||||
.diversifier_index();
|
.diversifier_index();
|
||||||
|
@ -542,64 +542,85 @@ where
|
||||||
.derive_external_secret_key(child_index)
|
.derive_external_secret_key(child_index)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
builder
|
builder.add_transparent_input(secret_key, utxo.outpoint().clone(), utxo.txout().clone())?;
|
||||||
.add_transparent_input(secret_key, utxo.outpoint().clone(), utxo.txout().clone())
|
|
||||||
.map_err(Error::Builder)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute the balance of the transaction. We have only added inputs, so the total change
|
for change_value in proposal.balance().proposed_change() {
|
||||||
// amount required will be the total of the UTXOs minus fees.
|
|
||||||
let fee_strategy = BasicFixedFeeChangeStrategy::new(DEFAULT_FEE);
|
|
||||||
let balance = fee_strategy
|
|
||||||
.compute_balance(
|
|
||||||
params,
|
|
||||||
target_height,
|
|
||||||
builder.transparent_inputs(),
|
|
||||||
builder.transparent_outputs(),
|
|
||||||
builder.sapling_inputs(),
|
|
||||||
builder.sapling_outputs(),
|
|
||||||
)
|
|
||||||
.map_err(|e| match e {
|
|
||||||
ChangeError::InsufficientFunds {
|
|
||||||
available,
|
|
||||||
required,
|
|
||||||
} => Error::InsufficientBalance(available, required),
|
|
||||||
ChangeError::StrategyError(e) => Error::BalanceError(e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let fee = balance.fee_required();
|
|
||||||
let mut total_out = Amount::zero();
|
|
||||||
for change_value in balance.proposed_change() {
|
|
||||||
total_out = (total_out + change_value.value())
|
|
||||||
.ok_or(Error::BalanceError(BalanceError::Overflow))?;
|
|
||||||
match change_value {
|
match change_value {
|
||||||
ChangeValue::Sapling(amount) => {
|
ChangeValue::Sapling(amount) => {
|
||||||
builder
|
builder.add_sapling_output(
|
||||||
.add_sapling_output(Some(ovk), shielding_address.clone(), *amount, memo.clone())
|
Some(ovk),
|
||||||
.map_err(Error::Builder)?;
|
shielding_address.clone(),
|
||||||
|
*amount,
|
||||||
|
memo.clone(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The transaction build process will check that the inputs and outputs balance
|
// The transaction build process will check that the inputs and outputs balance
|
||||||
let (tx, tx_metadata) = builder
|
let (tx, tx_metadata) = builder.build(&prover, proposal.fee_rule())?;
|
||||||
.build(&prover, &fee_strategy.fee_rule())
|
|
||||||
.map_err(Error::Builder)?;
|
|
||||||
let output_index = tx_metadata.output_index(0).expect(
|
|
||||||
"No sapling note was created in autoshielding transaction. This is a programming error.",
|
|
||||||
);
|
|
||||||
|
|
||||||
wallet_db.store_sent_tx(&SentTransaction {
|
wallet_db
|
||||||
tx: &tx,
|
.store_sent_tx(&SentTransaction {
|
||||||
created: time::OffsetDateTime::now_utc(),
|
tx: &tx,
|
||||||
account,
|
created: time::OffsetDateTime::now_utc(),
|
||||||
outputs: vec![SentTransactionOutput {
|
account,
|
||||||
output_index,
|
// TODO: After Orchard is implemented, this will need to change to correctly
|
||||||
recipient: Recipient::InternalAccount(account, PoolType::Sapling),
|
// determine the Sapling output indices; `enumerate` will no longer suffice
|
||||||
value: total_out,
|
outputs: proposal
|
||||||
memo: Some(memo.clone()),
|
.balance()
|
||||||
}],
|
.proposed_change()
|
||||||
fee_amount: fee,
|
.iter()
|
||||||
utxos_spent: utxos.iter().map(|utxo| utxo.outpoint().clone()).collect(),
|
.enumerate()
|
||||||
})
|
.map(|(idx, change_value)| match change_value {
|
||||||
|
ChangeValue::Sapling(value) => {
|
||||||
|
let output_index = tx_metadata.output_index(idx).expect(
|
||||||
|
"Missing Sapling output of autoshielding transaction. This is a programming error.",
|
||||||
|
);
|
||||||
|
SentTransactionOutput {
|
||||||
|
output_index,
|
||||||
|
recipient: Recipient::InternalAccount(account, PoolType::Sapling),
|
||||||
|
value: *value,
|
||||||
|
memo: Some(memo.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
fee_amount: proposal.balance().fee_required(),
|
||||||
|
utxos_spent: utxos.iter().map(|utxo| utxo.outpoint().clone()).collect(),
|
||||||
|
})
|
||||||
|
.map_err(Error::DataSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_key_for_note<N>(
|
||||||
|
selected: &SpendableNote<N>,
|
||||||
|
extsk: &ExtendedSpendingKey,
|
||||||
|
dfvk: &DiversifiableFullViewingKey,
|
||||||
|
) -> Option<(
|
||||||
|
sapling::Note,
|
||||||
|
ExtendedSpendingKey,
|
||||||
|
MerklePath<sapling::Node>,
|
||||||
|
)> {
|
||||||
|
let merkle_path = selected.witness.path().expect("the tree is not empty");
|
||||||
|
|
||||||
|
// Attempt to reconstruct the note being spent using both the internal and external dfvks
|
||||||
|
// corresponding to the unified spending key, checking against the witness we are using
|
||||||
|
// to spend the note that we've used the correct key.
|
||||||
|
let external_note = dfvk
|
||||||
|
.diversified_address(selected.diversifier)
|
||||||
|
.and_then(|addr| addr.create_note(selected.note_value.into(), selected.rseed));
|
||||||
|
let internal_note = dfvk
|
||||||
|
.diversified_change_address(selected.diversifier)
|
||||||
|
.and_then(|addr| addr.create_note(selected.note_value.into(), selected.rseed));
|
||||||
|
|
||||||
|
let expected_root = selected.witness.root();
|
||||||
|
external_note
|
||||||
|
.filter(|n| expected_root == merkle_path.root(n.commitment()))
|
||||||
|
.map(|n| (n, extsk.clone(), merkle_path.clone()))
|
||||||
|
.or_else(|| {
|
||||||
|
internal_note
|
||||||
|
.filter(|n| expected_root == merkle_path.root(n.commitment()))
|
||||||
|
.map(|n| (n, extsk.derive_internal(), merkle_path))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,452 @@
|
||||||
|
//! Types related to the process of selecting inputs to be spent given a transaction request.
|
||||||
|
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use zcash_primitives::{
|
||||||
|
consensus::{self, BlockHeight},
|
||||||
|
legacy::TransparentAddress,
|
||||||
|
transaction::{
|
||||||
|
components::{
|
||||||
|
amount::{Amount, BalanceError, NonNegativeAmount},
|
||||||
|
sapling::fees as sapling,
|
||||||
|
OutPoint, TxOut,
|
||||||
|
},
|
||||||
|
fees::FeeRule,
|
||||||
|
},
|
||||||
|
zip32::AccountId,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
address::{RecipientAddress, UnifiedAddress},
|
||||||
|
data_api::WalletRead,
|
||||||
|
fees::{ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance},
|
||||||
|
wallet::{SpendableNote, WalletTransparentOutput},
|
||||||
|
zip321::TransactionRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The type of errors that may be produced in input selection.
|
||||||
|
pub enum InputSelectorError<DbErrT, SelectorErrT> {
|
||||||
|
/// An error occurred accessing the underlying data store.
|
||||||
|
DataSource(DbErrT),
|
||||||
|
/// An error occurred specific to the provided input selector's selection rules.
|
||||||
|
Selection(SelectorErrT),
|
||||||
|
/// Insufficient funds were available to satisfy the payment request that inputs were being
|
||||||
|
/// selected to attempt to satisfy.
|
||||||
|
InsufficientFunds { available: Amount, required: Amount },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE, SE> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match &self {
|
||||||
|
InputSelectorError::DataSource(e) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"The underlying datasource produced the following error: {}",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
InputSelectorError::Selection(e) => {
|
||||||
|
write!(f, "Note selection encountered the following error: {}", e)
|
||||||
|
}
|
||||||
|
InputSelectorError::InsufficientFunds {
|
||||||
|
available,
|
||||||
|
required,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"Insufficient balance (have {}, need {} including fee)",
|
||||||
|
i64::from(*available),
|
||||||
|
i64::from(*required)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A data structure that describes the inputs to be consumed and outputs to
|
||||||
|
/// be produced in a proposed transaction.
|
||||||
|
pub struct Proposal<FeeRuleT, TransparentInput, NoteRef> {
|
||||||
|
transaction_request: TransactionRequest,
|
||||||
|
transparent_inputs: Vec<TransparentInput>,
|
||||||
|
sapling_inputs: Vec<SpendableNote<NoteRef>>,
|
||||||
|
balance: TransactionBalance,
|
||||||
|
fee_rule: FeeRuleT,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FeeRuleT, TransparentInput, NoteRef> Proposal<FeeRuleT, TransparentInput, NoteRef> {
|
||||||
|
/// Returns the transaction request that describes the payments to be made.
|
||||||
|
pub fn transaction_request(&self) -> &TransactionRequest {
|
||||||
|
&self.transaction_request
|
||||||
|
}
|
||||||
|
/// Returns the transparent inputs that have been selected to fund the transaction.
|
||||||
|
pub fn transparent_inputs(&self) -> &[TransparentInput] {
|
||||||
|
&self.transparent_inputs
|
||||||
|
}
|
||||||
|
/// Returns the Sapling inputs that have been selected to fund the transaction.
|
||||||
|
pub fn sapling_inputs(&self) -> &[SpendableNote<NoteRef>] {
|
||||||
|
&self.sapling_inputs
|
||||||
|
}
|
||||||
|
/// Returns the change outputs to be added to the transaction and the fee to be paid.
|
||||||
|
pub fn balance(&self) -> &TransactionBalance {
|
||||||
|
&self.balance
|
||||||
|
}
|
||||||
|
/// Returns the fee rule to be used by the transaction builder.
|
||||||
|
pub fn fee_rule(&self) -> &FeeRuleT {
|
||||||
|
&self.fee_rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A strategy for selecting transaction inputs and proposing transaction outputs.
|
||||||
|
///
|
||||||
|
/// Proposals should include only economically useful inputs, as determined by `Self::FeeRule`;
|
||||||
|
/// that is, do not return inputs that cause fees to increase by an amount greater than the value
|
||||||
|
/// of the input.
|
||||||
|
pub trait InputSelector {
|
||||||
|
/// The type of errors that may be generated in input selection
|
||||||
|
type Error;
|
||||||
|
/// The type of data source that the input selector expects to access to obtain input notes and
|
||||||
|
/// UTXOs. This associated type permits input selectors that may use specialized knowledge of
|
||||||
|
/// the internals of a particular backing data store, if the generic API of `WalletRead` does
|
||||||
|
/// not provide sufficiently fine-grained operations for a particular backing store to
|
||||||
|
/// optimally perform input selection.
|
||||||
|
type DataSource: WalletRead;
|
||||||
|
/// The type of the fee rule that this input selector uses when computing fees.
|
||||||
|
type FeeRule: FeeRule;
|
||||||
|
|
||||||
|
/// Performs input selection and returns a proposal for transaction construction including
|
||||||
|
/// change and fee outputs.
|
||||||
|
///
|
||||||
|
/// Implementations of this method should return inputs sufficient to satisfy the given
|
||||||
|
/// transaction request using a best-effort strategy to preserve user privacy, as follows:
|
||||||
|
/// * If it is possible to satisfy the specified transaction request by creating
|
||||||
|
/// a fully-shielded transaction without requiring value to cross pool boundaries,
|
||||||
|
/// return the inputs necessary to construct such a transaction; otherwise
|
||||||
|
/// * If it is possible to satisfy the transaction request by creating a fully-shielded
|
||||||
|
/// transaction with some amounts crossing between shielded pools, return the inputs
|
||||||
|
/// necessary.
|
||||||
|
///
|
||||||
|
/// If insufficient funds are available to satisfy the required outputs for the shielding
|
||||||
|
/// request, this operation must fail and return [`InputSelectorError::InsufficientFunds`].
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn propose_transaction<ParamsT>(
|
||||||
|
&self,
|
||||||
|
params: &ParamsT,
|
||||||
|
wallet_db: &Self::DataSource,
|
||||||
|
account: AccountId,
|
||||||
|
anchor_height: BlockHeight,
|
||||||
|
target_height: BlockHeight,
|
||||||
|
transaction_request: TransactionRequest,
|
||||||
|
) -> Result<
|
||||||
|
Proposal<
|
||||||
|
Self::FeeRule,
|
||||||
|
std::convert::Infallible,
|
||||||
|
<<Self as InputSelector>::DataSource as WalletRead>::NoteRef,
|
||||||
|
>,
|
||||||
|
InputSelectorError<<<Self as InputSelector>::DataSource as WalletRead>::Error, Self::Error>,
|
||||||
|
>
|
||||||
|
where
|
||||||
|
ParamsT: consensus::Parameters;
|
||||||
|
|
||||||
|
/// Performs input selection and returns a proposal for the construction of a shielding
|
||||||
|
/// transaction.
|
||||||
|
///
|
||||||
|
/// Implementations should return the maximum possible number of economically useful inputs
|
||||||
|
/// required to supply at least the requested value, choosing only inputs received at the
|
||||||
|
/// specified source addresses. If insufficient funds are available to satisfy the required
|
||||||
|
/// outputs for the shielding request, this operation must fail and return
|
||||||
|
/// [`InputSelectorError::InsufficientFunds`].
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn propose_shielding<ParamsT>(
|
||||||
|
&self,
|
||||||
|
params: &ParamsT,
|
||||||
|
wallet_db: &Self::DataSource,
|
||||||
|
shielding_threshold: NonNegativeAmount,
|
||||||
|
source_addrs: &[TransparentAddress],
|
||||||
|
confirmed_height: BlockHeight,
|
||||||
|
target_height: BlockHeight,
|
||||||
|
) -> Result<
|
||||||
|
Proposal<
|
||||||
|
Self::FeeRule,
|
||||||
|
WalletTransparentOutput,
|
||||||
|
<<Self as InputSelector>::DataSource as WalletRead>::NoteRef,
|
||||||
|
>,
|
||||||
|
InputSelectorError<<<Self as InputSelector>::DataSource as WalletRead>::Error, Self::Error>,
|
||||||
|
>
|
||||||
|
where
|
||||||
|
ParamsT: consensus::Parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors that can occur as a consequence of greedy input selection.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT> {
|
||||||
|
/// An intermediate value overflowed or underflowed the valid monetary range.
|
||||||
|
Balance(BalanceError),
|
||||||
|
/// A unified address did not contain a supported receiver.
|
||||||
|
UnsupportedAddress(Box<UnifiedAddress>),
|
||||||
|
/// An error was encountered in change selection.
|
||||||
|
Change(ChangeError<ChangeStrategyErrT, NoteRefT>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DbErrT, ChangeStrategyErrT, NoteRefT>
|
||||||
|
From<GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
|
||||||
|
for InputSelectorError<DbErrT, GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
|
||||||
|
{
|
||||||
|
fn from(err: GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>) -> Self {
|
||||||
|
InputSelectorError::Selection(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DbErrT, ChangeStrategyErrT, NoteRefT> From<ChangeError<ChangeStrategyErrT, NoteRefT>>
|
||||||
|
for InputSelectorError<DbErrT, GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
|
||||||
|
{
|
||||||
|
fn from(err: ChangeError<ChangeStrategyErrT, NoteRefT>) -> Self {
|
||||||
|
InputSelectorError::Selection(GreedyInputSelectorError::Change(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DbErrT, ChangeStrategyErrT, NoteRefT> From<BalanceError>
|
||||||
|
for InputSelectorError<DbErrT, GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
|
||||||
|
{
|
||||||
|
fn from(err: BalanceError) -> Self {
|
||||||
|
InputSelectorError::Selection(GreedyInputSelectorError::Balance(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SaplingPayment(Amount);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl SaplingPayment {
|
||||||
|
pub(crate) fn new(amount: Amount) -> Self {
|
||||||
|
SaplingPayment(amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sapling::OutputView for SaplingPayment {
|
||||||
|
fn value(&self) -> Amount {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An [`InputSelector`] implementation that uses a greedy strategy to select between available
|
||||||
|
/// notes.
|
||||||
|
///
|
||||||
|
/// This implementation performs input selection using methods available via the [`WalletRead`]
|
||||||
|
/// interface.
|
||||||
|
pub struct GreedyInputSelector<DbT, ChangeT> {
|
||||||
|
change_strategy: ChangeT,
|
||||||
|
dust_output_policy: DustOutputPolicy,
|
||||||
|
_ds_type: PhantomData<DbT>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DbT, ChangeT: ChangeStrategy> GreedyInputSelector<DbT, ChangeT> {
|
||||||
|
/// Constructs a new greedy input selector that uses the provided change strategy to determine
|
||||||
|
/// change values and fee amounts.
|
||||||
|
pub fn new(change_strategy: ChangeT, dust_output_policy: DustOutputPolicy) -> Self {
|
||||||
|
GreedyInputSelector {
|
||||||
|
change_strategy,
|
||||||
|
dust_output_policy,
|
||||||
|
_ds_type: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DbT, ChangeT> InputSelector for GreedyInputSelector<DbT, ChangeT>
|
||||||
|
where
|
||||||
|
DbT: WalletRead,
|
||||||
|
ChangeT: ChangeStrategy,
|
||||||
|
ChangeT::FeeRule: Clone,
|
||||||
|
{
|
||||||
|
type Error = GreedyInputSelectorError<ChangeT::Error, DbT::NoteRef>;
|
||||||
|
type DataSource = DbT;
|
||||||
|
type FeeRule = ChangeT::FeeRule;
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn propose_transaction<ParamsT>(
|
||||||
|
&self,
|
||||||
|
params: &ParamsT,
|
||||||
|
wallet_db: &Self::DataSource,
|
||||||
|
account: AccountId,
|
||||||
|
anchor_height: BlockHeight,
|
||||||
|
target_height: BlockHeight,
|
||||||
|
transaction_request: TransactionRequest,
|
||||||
|
) -> Result<
|
||||||
|
Proposal<Self::FeeRule, std::convert::Infallible, DbT::NoteRef>,
|
||||||
|
InputSelectorError<DbT::Error, Self::Error>,
|
||||||
|
>
|
||||||
|
where
|
||||||
|
ParamsT: consensus::Parameters,
|
||||||
|
{
|
||||||
|
let mut transparent_outputs = vec![];
|
||||||
|
let mut sapling_outputs = vec![];
|
||||||
|
let mut output_total = Amount::zero();
|
||||||
|
for payment in transaction_request.payments() {
|
||||||
|
output_total = (output_total + payment.amount).ok_or(BalanceError::Overflow)?;
|
||||||
|
|
||||||
|
let mut push_transparent = |taddr: TransparentAddress| {
|
||||||
|
transparent_outputs.push(TxOut {
|
||||||
|
value: payment.amount,
|
||||||
|
script_pubkey: taddr.script(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let mut push_sapling = || {
|
||||||
|
sapling_outputs.push(SaplingPayment(payment.amount));
|
||||||
|
};
|
||||||
|
|
||||||
|
match &payment.recipient_address {
|
||||||
|
RecipientAddress::Transparent(addr) => {
|
||||||
|
push_transparent(*addr);
|
||||||
|
}
|
||||||
|
RecipientAddress::Shielded(_) => {
|
||||||
|
push_sapling();
|
||||||
|
}
|
||||||
|
RecipientAddress::Unified(addr) => {
|
||||||
|
if addr.sapling().is_some() {
|
||||||
|
push_sapling();
|
||||||
|
} else if let Some(addr) = addr.transparent() {
|
||||||
|
push_transparent(*addr);
|
||||||
|
} else {
|
||||||
|
return Err(InputSelectorError::Selection(
|
||||||
|
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr.clone())),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sapling_inputs: Vec<SpendableNote<DbT::NoteRef>> = vec![];
|
||||||
|
let mut prior_available = Amount::zero();
|
||||||
|
let mut amount_required = Amount::zero();
|
||||||
|
let mut exclude: Vec<DbT::NoteRef> = vec![];
|
||||||
|
// This loop is guaranteed to terminate because on each iteration we check that the amount
|
||||||
|
// of funds selected is strictly increasing. The loop will either return a successful
|
||||||
|
// result or the wallet will eventually run out of funds to select.
|
||||||
|
loop {
|
||||||
|
let balance = self.change_strategy.compute_balance(
|
||||||
|
params,
|
||||||
|
target_height,
|
||||||
|
&Vec::<WalletTransparentOutput>::new(),
|
||||||
|
&transparent_outputs,
|
||||||
|
&sapling_inputs,
|
||||||
|
&sapling_outputs,
|
||||||
|
&self.dust_output_policy,
|
||||||
|
);
|
||||||
|
|
||||||
|
match balance {
|
||||||
|
Ok(balance) => {
|
||||||
|
return Ok(Proposal {
|
||||||
|
transaction_request,
|
||||||
|
transparent_inputs: vec![],
|
||||||
|
sapling_inputs,
|
||||||
|
balance,
|
||||||
|
fee_rule: (*self.change_strategy.fee_rule()).clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(ChangeError::DustInputs { mut sapling, .. }) => {
|
||||||
|
exclude.append(&mut sapling);
|
||||||
|
}
|
||||||
|
Err(ChangeError::InsufficientFunds { required, .. }) => {
|
||||||
|
amount_required = required;
|
||||||
|
}
|
||||||
|
Err(other) => return Err(other.into()),
|
||||||
|
}
|
||||||
|
|
||||||
|
sapling_inputs = wallet_db
|
||||||
|
.select_spendable_sapling_notes(account, amount_required, anchor_height, &exclude)
|
||||||
|
.map_err(InputSelectorError::DataSource)?;
|
||||||
|
|
||||||
|
let new_available = sapling_inputs
|
||||||
|
.iter()
|
||||||
|
.map(|n| n.note_value)
|
||||||
|
.sum::<Option<Amount>>()
|
||||||
|
.ok_or(BalanceError::Overflow)?;
|
||||||
|
|
||||||
|
if new_available <= prior_available {
|
||||||
|
return Err(InputSelectorError::InsufficientFunds {
|
||||||
|
required: amount_required,
|
||||||
|
available: new_available,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If the set of selected inputs has changed after selection, we will loop again
|
||||||
|
// and see whether we now have enough funds.
|
||||||
|
prior_available = new_available;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn propose_shielding<ParamsT>(
|
||||||
|
&self,
|
||||||
|
params: &ParamsT,
|
||||||
|
wallet_db: &Self::DataSource,
|
||||||
|
shielding_threshold: NonNegativeAmount,
|
||||||
|
source_addrs: &[TransparentAddress],
|
||||||
|
confirmed_height: BlockHeight,
|
||||||
|
target_height: BlockHeight,
|
||||||
|
) -> Result<
|
||||||
|
Proposal<Self::FeeRule, WalletTransparentOutput, DbT::NoteRef>,
|
||||||
|
InputSelectorError<DbT::Error, Self::Error>,
|
||||||
|
>
|
||||||
|
where
|
||||||
|
ParamsT: consensus::Parameters,
|
||||||
|
{
|
||||||
|
let mut transparent_inputs: Vec<WalletTransparentOutput> = source_addrs
|
||||||
|
.iter()
|
||||||
|
.map(|taddr| wallet_db.get_unspent_transparent_outputs(taddr, confirmed_height, &[]))
|
||||||
|
.collect::<Result<Vec<Vec<_>>, _>>()
|
||||||
|
.map_err(InputSelectorError::DataSource)?
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|v| v.into_iter())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let trial_balance = self.change_strategy.compute_balance(
|
||||||
|
params,
|
||||||
|
target_height,
|
||||||
|
&transparent_inputs,
|
||||||
|
&Vec::<TxOut>::new(),
|
||||||
|
&Vec::<SpendableNote<DbT::NoteRef>>::new(),
|
||||||
|
&Vec::<SaplingPayment>::new(),
|
||||||
|
&self.dust_output_policy,
|
||||||
|
);
|
||||||
|
|
||||||
|
let balance = match trial_balance {
|
||||||
|
Ok(balance) => balance,
|
||||||
|
Err(ChangeError::DustInputs { transparent, .. }) => {
|
||||||
|
let exclusions: BTreeSet<OutPoint> = transparent.into_iter().collect();
|
||||||
|
transparent_inputs = transparent_inputs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|i| !exclusions.contains(i.outpoint()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
self.change_strategy.compute_balance(
|
||||||
|
params,
|
||||||
|
target_height,
|
||||||
|
&transparent_inputs,
|
||||||
|
&Vec::<TxOut>::new(),
|
||||||
|
&Vec::<SpendableNote<DbT::NoteRef>>::new(),
|
||||||
|
&Vec::<SaplingPayment>::new(),
|
||||||
|
&self.dust_output_policy,
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
Err(other) => {
|
||||||
|
return Err(other.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if balance.total() >= shielding_threshold.into() {
|
||||||
|
Ok(Proposal {
|
||||||
|
transaction_request: TransactionRequest::empty(),
|
||||||
|
transparent_inputs,
|
||||||
|
sapling_inputs: vec![],
|
||||||
|
balance,
|
||||||
|
fee_rule: (*self.change_strategy.fee_rule()).clone(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(InputSelectorError::InsufficientFunds {
|
||||||
|
available: balance.total(),
|
||||||
|
required: shielding_threshold.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,12 +5,17 @@ use zcash_primitives::{
|
||||||
amount::{Amount, BalanceError},
|
amount::{Amount, BalanceError},
|
||||||
sapling::fees as sapling,
|
sapling::fees as sapling,
|
||||||
transparent::fees as transparent,
|
transparent::fees as transparent,
|
||||||
|
OutPoint,
|
||||||
},
|
},
|
||||||
fees::{FeeRule, FixedFeeRule},
|
fees::FeeRule,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod fixed;
|
||||||
|
pub mod zip317;
|
||||||
|
|
||||||
/// A proposed change amount and output pool.
|
/// A proposed change amount and output pool.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum ChangeValue {
|
pub enum ChangeValue {
|
||||||
Sapling(Amount),
|
Sapling(Amount),
|
||||||
}
|
}
|
||||||
|
@ -26,18 +31,26 @@ impl ChangeValue {
|
||||||
/// The amount of change and fees required to make a transaction's inputs and
|
/// The amount of change and fees required to make a transaction's inputs and
|
||||||
/// outputs balance under a specific fee rule, as computed by a particular
|
/// outputs balance under a specific fee rule, as computed by a particular
|
||||||
/// [`ChangeStrategy`] that is aware of that rule.
|
/// [`ChangeStrategy`] that is aware of that rule.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct TransactionBalance {
|
pub struct TransactionBalance {
|
||||||
proposed_change: Vec<ChangeValue>,
|
proposed_change: Vec<ChangeValue>,
|
||||||
fee_required: Amount,
|
fee_required: Amount,
|
||||||
|
total: Amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TransactionBalance {
|
impl TransactionBalance {
|
||||||
/// Constructs a new balance from its constituent parts.
|
/// Constructs a new balance from its constituent parts.
|
||||||
pub fn new(proposed_change: Vec<ChangeValue>, fee_required: Amount) -> Self {
|
pub fn new(proposed_change: Vec<ChangeValue>, fee_required: Amount) -> Option<Self> {
|
||||||
TransactionBalance {
|
proposed_change
|
||||||
proposed_change,
|
.iter()
|
||||||
fee_required,
|
.map(|v| v.value())
|
||||||
}
|
.chain(Some(fee_required))
|
||||||
|
.sum::<Option<Amount>>()
|
||||||
|
.map(|total| TransactionBalance {
|
||||||
|
proposed_change,
|
||||||
|
fee_required,
|
||||||
|
total,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The change values proposed by the [`ChangeStrategy`] that computed this balance.
|
/// The change values proposed by the [`ChangeStrategy`] that computed this balance.
|
||||||
|
@ -50,14 +63,95 @@ impl TransactionBalance {
|
||||||
pub fn fee_required(&self) -> Amount {
|
pub fn fee_required(&self) -> Amount {
|
||||||
self.fee_required
|
self.fee_required
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the sum of the proposed change outputs and the required fee.
|
||||||
|
pub fn total(&self) -> Amount {
|
||||||
|
self.total
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors that can occur in computing suggested change and/or fees.
|
/// Errors that can occur in computing suggested change and/or fees.
|
||||||
pub enum ChangeError<E> {
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
InsufficientFunds { available: Amount, required: Amount },
|
pub enum ChangeError<E, NoteRefT> {
|
||||||
|
/// Insufficient inputs were provided to change selection to fund the
|
||||||
|
/// required outputs and fees.
|
||||||
|
InsufficientFunds {
|
||||||
|
/// The total of the inputs provided to change selection
|
||||||
|
available: Amount,
|
||||||
|
/// The total amount of input value required to fund the requested outputs,
|
||||||
|
/// including the required fees.
|
||||||
|
required: Amount,
|
||||||
|
},
|
||||||
|
/// Some of the inputs provided to the transaction were determined to currently have no
|
||||||
|
/// economic value (i.e. their inclusion in a transaction causes fees to rise in an amount
|
||||||
|
/// greater than their value.)
|
||||||
|
DustInputs {
|
||||||
|
/// The outpoints corresponding to transparent inputs having no current economic value.
|
||||||
|
transparent: Vec<OutPoint>,
|
||||||
|
/// The identifiers for Sapling inputs having not current economic value
|
||||||
|
sapling: Vec<NoteRefT>,
|
||||||
|
},
|
||||||
|
/// An error occurred that was specific to the change selection strategy in use.
|
||||||
StrategyError(E),
|
StrategyError(E),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<NoteRefT> From<BalanceError> for ChangeError<BalanceError, NoteRefT> {
|
||||||
|
fn from(err: BalanceError) -> ChangeError<BalanceError, NoteRefT> {
|
||||||
|
ChangeError::StrategyError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An enumeration of actions to tak when a transaction would potentially create dust
|
||||||
|
/// outputs (outputs that are likely to be without economic value due to fee rules.)
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum DustAction {
|
||||||
|
/// Do not allow creation of dust outputs; instead, require that additional inputs be provided.
|
||||||
|
Reject,
|
||||||
|
/// Explicitly allow the creation of dust change amounts greater than the specified value.
|
||||||
|
AllowDustChange,
|
||||||
|
/// Allow dust amounts to be added to the transaction fee
|
||||||
|
AddDustToFee,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A policy describing how a [`ChangeStrategy`] should treat potentially dust-valued change
|
||||||
|
/// outputs (outputs that are likely to be without economic value due to fee rules.)
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct DustOutputPolicy {
|
||||||
|
action: DustAction,
|
||||||
|
dust_threshold: Option<Amount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DustOutputPolicy {
|
||||||
|
/// Constructs a new dust output policy.
|
||||||
|
///
|
||||||
|
/// A dust policy created with `None` as the dust threshold will delegate determination
|
||||||
|
/// of the dust threshold to the change strategy that is evaluating the strategy; this
|
||||||
|
/// recommended, but an explicit value (including zero) may be provided to explicitly
|
||||||
|
/// override the determination of the change strategy.
|
||||||
|
pub fn new(action: DustAction, dust_threshold: Option<Amount>) -> Self {
|
||||||
|
Self {
|
||||||
|
action,
|
||||||
|
dust_threshold,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the action to take in the event that a dust change amount would be produced
|
||||||
|
pub fn action(&self) -> DustAction {
|
||||||
|
self.action
|
||||||
|
}
|
||||||
|
/// Returns a value that will be used to override the dust determination logic of the
|
||||||
|
/// change policy, if any.
|
||||||
|
pub fn dust_threshold(&self) -> Option<Amount> {
|
||||||
|
self.dust_threshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DustOutputPolicy {
|
||||||
|
fn default() -> Self {
|
||||||
|
DustOutputPolicy::new(DustAction::Reject, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A trait that represents the ability to compute the suggested change and fees that must be paid
|
/// A trait that represents the ability to compute the suggested change and fees that must be paid
|
||||||
/// by a transaction having a specified set of inputs and outputs.
|
/// by a transaction having a specified set of inputs and outputs.
|
||||||
pub trait ChangeStrategy {
|
pub trait ChangeStrategy {
|
||||||
|
@ -66,7 +160,7 @@ pub trait ChangeStrategy {
|
||||||
|
|
||||||
/// Returns the fee rule that this change strategy will respect when performing
|
/// Returns the fee rule that this change strategy will respect when performing
|
||||||
/// balance computations.
|
/// balance computations.
|
||||||
fn fee_rule(&self) -> Self::FeeRule;
|
fn fee_rule(&self) -> &Self::FeeRule;
|
||||||
|
|
||||||
/// Computes the totals of inputs, suggested change amounts, and fees given the
|
/// Computes the totals of inputs, suggested change amounts, and fees given the
|
||||||
/// provided inputs and outputs being used to construct a transaction.
|
/// provided inputs and outputs being used to construct a transaction.
|
||||||
|
@ -75,89 +169,52 @@ pub trait ChangeStrategy {
|
||||||
/// change outputs recommended by this operation. If insufficient funds are available to
|
/// change outputs recommended by this operation. If insufficient funds are available to
|
||||||
/// supply the requested outputs and required fees, implementations should return
|
/// supply the requested outputs and required fees, implementations should return
|
||||||
/// [`ChangeError::InsufficientFunds`].
|
/// [`ChangeError::InsufficientFunds`].
|
||||||
fn compute_balance<P: consensus::Parameters>(
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
|
||||||
&self,
|
&self,
|
||||||
params: &P,
|
params: &P,
|
||||||
target_height: BlockHeight,
|
target_height: BlockHeight,
|
||||||
transparent_inputs: &[impl transparent::InputView],
|
transparent_inputs: &[impl transparent::InputView],
|
||||||
transparent_outputs: &[impl transparent::OutputView],
|
transparent_outputs: &[impl transparent::OutputView],
|
||||||
sapling_inputs: &[impl sapling::InputView],
|
sapling_inputs: &[impl sapling::InputView<NoteRefT>],
|
||||||
sapling_outputs: &[impl sapling::OutputView],
|
sapling_outputs: &[impl sapling::OutputView],
|
||||||
) -> Result<TransactionBalance, ChangeError<Self::Error>>;
|
dust_output_policy: &DustOutputPolicy,
|
||||||
|
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A change strategy that uses a fixed fee amount and proposes change as a single output
|
#[cfg(test)]
|
||||||
/// to the most current supported pool.
|
pub(crate) mod tests {
|
||||||
pub struct BasicFixedFeeChangeStrategy {
|
use zcash_primitives::transaction::components::{
|
||||||
fixed_fee: Amount,
|
amount::Amount,
|
||||||
}
|
sapling::fees as sapling,
|
||||||
|
transparent::{fees as transparent, OutPoint, TxOut},
|
||||||
|
};
|
||||||
|
|
||||||
impl BasicFixedFeeChangeStrategy {
|
pub(crate) struct TestTransparentInput {
|
||||||
// Constructs a new [`BasicFixedFeeChangeStrategy`] with the specified fixed fee
|
pub outpoint: OutPoint,
|
||||||
// amount.
|
pub coin: TxOut,
|
||||||
pub fn new(fixed_fee: Amount) -> Self {
|
|
||||||
Self { fixed_fee }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChangeStrategy for BasicFixedFeeChangeStrategy {
|
|
||||||
type FeeRule = FixedFeeRule;
|
|
||||||
type Error = BalanceError;
|
|
||||||
|
|
||||||
fn fee_rule(&self) -> Self::FeeRule {
|
|
||||||
FixedFeeRule::new(self.fixed_fee)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_balance<P: consensus::Parameters>(
|
impl transparent::InputView for TestTransparentInput {
|
||||||
&self,
|
fn outpoint(&self) -> &OutPoint {
|
||||||
_params: &P,
|
&self.outpoint
|
||||||
_target_height: BlockHeight,
|
}
|
||||||
transparent_inputs: &[impl transparent::InputView],
|
fn coin(&self) -> &TxOut {
|
||||||
transparent_outputs: &[impl transparent::OutputView],
|
&self.coin
|
||||||
sapling_inputs: &[impl sapling::InputView],
|
}
|
||||||
sapling_outputs: &[impl sapling::OutputView],
|
}
|
||||||
) -> Result<TransactionBalance, ChangeError<Self::Error>> {
|
|
||||||
let overflow = || ChangeError::StrategyError(BalanceError::Overflow);
|
|
||||||
let underflow = || ChangeError::StrategyError(BalanceError::Underflow);
|
|
||||||
|
|
||||||
let t_in = transparent_inputs
|
pub(crate) struct TestSaplingInput {
|
||||||
.iter()
|
pub note_id: u32,
|
||||||
.map(|t_in| t_in.coin().value)
|
pub value: Amount,
|
||||||
.sum::<Option<_>>()
|
}
|
||||||
.ok_or_else(overflow)?;
|
|
||||||
let t_out = transparent_outputs
|
|
||||||
.iter()
|
|
||||||
.map(|t_out| t_out.value())
|
|
||||||
.sum::<Option<_>>()
|
|
||||||
.ok_or_else(overflow)?;
|
|
||||||
let sapling_in = sapling_inputs
|
|
||||||
.iter()
|
|
||||||
.map(|s_in| s_in.value())
|
|
||||||
.sum::<Option<_>>()
|
|
||||||
.ok_or_else(overflow)?;
|
|
||||||
let sapling_out = sapling_outputs
|
|
||||||
.iter()
|
|
||||||
.map(|s_out| s_out.value())
|
|
||||||
.sum::<Option<_>>()
|
|
||||||
.ok_or_else(overflow)?;
|
|
||||||
|
|
||||||
let total_in = (t_in + sapling_in).ok_or_else(overflow)?;
|
impl sapling::InputView<u32> for TestSaplingInput {
|
||||||
let total_out = [t_out, sapling_out, self.fixed_fee]
|
fn note_id(&self) -> &u32 {
|
||||||
.iter()
|
&self.note_id
|
||||||
.sum::<Option<Amount>>()
|
}
|
||||||
.ok_or_else(overflow)?;
|
fn value(&self) -> Amount {
|
||||||
|
self.value
|
||||||
let proposed_change = (total_in - total_out).ok_or_else(underflow)?;
|
|
||||||
if proposed_change < Amount::zero() {
|
|
||||||
Err(ChangeError::InsufficientFunds {
|
|
||||||
available: total_in,
|
|
||||||
required: total_out,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(TransactionBalance::new(
|
|
||||||
vec![ChangeValue::Sapling(proposed_change)],
|
|
||||||
self.fixed_fee,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,234 @@
|
||||||
|
//! Change strategies designed for use with a fixed fee.
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use zcash_primitives::{
|
||||||
|
consensus::{self, BlockHeight},
|
||||||
|
transaction::{
|
||||||
|
components::{
|
||||||
|
amount::{Amount, BalanceError},
|
||||||
|
sapling::fees as sapling,
|
||||||
|
transparent::fees as transparent,
|
||||||
|
},
|
||||||
|
fees::{fixed::FeeRule as FixedFeeRule, FeeRule},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, TransactionBalance,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A change strategy that and proposes change as a single output to the most current supported
|
||||||
|
/// shielded pool and delegates fee calculation to the provided fee rule.
|
||||||
|
pub struct SingleOutputChangeStrategy {
|
||||||
|
fee_rule: FixedFeeRule,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SingleOutputChangeStrategy {
|
||||||
|
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified fee rule.
|
||||||
|
pub fn new(fee_rule: FixedFeeRule) -> Self {
|
||||||
|
Self { fee_rule }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChangeStrategy for SingleOutputChangeStrategy {
|
||||||
|
type FeeRule = FixedFeeRule;
|
||||||
|
type Error = BalanceError;
|
||||||
|
|
||||||
|
fn fee_rule(&self) -> &Self::FeeRule {
|
||||||
|
&self.fee_rule
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
|
||||||
|
&self,
|
||||||
|
params: &P,
|
||||||
|
target_height: BlockHeight,
|
||||||
|
transparent_inputs: &[impl transparent::InputView],
|
||||||
|
transparent_outputs: &[impl transparent::OutputView],
|
||||||
|
sapling_inputs: &[impl sapling::InputView<NoteRefT>],
|
||||||
|
sapling_outputs: &[impl sapling::OutputView],
|
||||||
|
dust_output_policy: &DustOutputPolicy,
|
||||||
|
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
|
||||||
|
let t_in = transparent_inputs
|
||||||
|
.iter()
|
||||||
|
.map(|t_in| t_in.coin().value)
|
||||||
|
.sum::<Option<_>>()
|
||||||
|
.ok_or(BalanceError::Overflow)?;
|
||||||
|
let t_out = transparent_outputs
|
||||||
|
.iter()
|
||||||
|
.map(|t_out| t_out.value())
|
||||||
|
.sum::<Option<_>>()
|
||||||
|
.ok_or(BalanceError::Overflow)?;
|
||||||
|
let sapling_in = sapling_inputs
|
||||||
|
.iter()
|
||||||
|
.map(|s_in| s_in.value())
|
||||||
|
.sum::<Option<_>>()
|
||||||
|
.ok_or(BalanceError::Overflow)?;
|
||||||
|
let sapling_out = sapling_outputs
|
||||||
|
.iter()
|
||||||
|
.map(|s_out| s_out.value())
|
||||||
|
.sum::<Option<_>>()
|
||||||
|
.ok_or(BalanceError::Overflow)?;
|
||||||
|
|
||||||
|
let fee_amount = self
|
||||||
|
.fee_rule
|
||||||
|
.fee_required(
|
||||||
|
params,
|
||||||
|
target_height,
|
||||||
|
transparent_inputs,
|
||||||
|
transparent_outputs,
|
||||||
|
sapling_inputs.len(),
|
||||||
|
sapling_outputs.len() + 1,
|
||||||
|
)
|
||||||
|
.unwrap(); // fixed::FeeRule::fee_required is infallible.
|
||||||
|
|
||||||
|
let total_in = (t_in + sapling_in).ok_or(BalanceError::Overflow)?;
|
||||||
|
|
||||||
|
if (!transparent_inputs.is_empty() || !sapling_inputs.is_empty()) && fee_amount > total_in {
|
||||||
|
// For the fixed-fee selection rule, the only time we consider inputs dust is when the fee
|
||||||
|
// exceeds the value of all input values.
|
||||||
|
Err(ChangeError::DustInputs {
|
||||||
|
transparent: transparent_inputs
|
||||||
|
.iter()
|
||||||
|
.map(|i| i.outpoint())
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
sapling: sapling_inputs
|
||||||
|
.iter()
|
||||||
|
.map(|i| i.note_id())
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let total_out = [t_out, sapling_out, fee_amount]
|
||||||
|
.iter()
|
||||||
|
.sum::<Option<Amount>>()
|
||||||
|
.ok_or(BalanceError::Overflow)?;
|
||||||
|
|
||||||
|
let proposed_change = (total_in - total_out).ok_or(BalanceError::Underflow)?;
|
||||||
|
match proposed_change.cmp(&Amount::zero()) {
|
||||||
|
Ordering::Less => Err(ChangeError::InsufficientFunds {
|
||||||
|
available: total_in,
|
||||||
|
required: total_out,
|
||||||
|
}),
|
||||||
|
Ordering::Equal => TransactionBalance::new(vec![], fee_amount)
|
||||||
|
.ok_or_else(|| BalanceError::Overflow.into()),
|
||||||
|
Ordering::Greater => {
|
||||||
|
let dust_threshold = dust_output_policy
|
||||||
|
.dust_threshold()
|
||||||
|
.unwrap_or_else(|| self.fee_rule.fixed_fee());
|
||||||
|
|
||||||
|
if dust_threshold > proposed_change {
|
||||||
|
match dust_output_policy.action() {
|
||||||
|
DustAction::Reject => {
|
||||||
|
let shortfall = (dust_threshold - proposed_change)
|
||||||
|
.ok_or(BalanceError::Underflow)?;
|
||||||
|
Err(ChangeError::InsufficientFunds {
|
||||||
|
available: total_in,
|
||||||
|
required: (total_in + shortfall)
|
||||||
|
.ok_or(BalanceError::Overflow)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
DustAction::AllowDustChange => TransactionBalance::new(
|
||||||
|
vec![ChangeValue::Sapling(proposed_change)],
|
||||||
|
fee_amount,
|
||||||
|
)
|
||||||
|
.ok_or_else(|| BalanceError::Overflow.into()),
|
||||||
|
DustAction::AddDustToFee => TransactionBalance::new(
|
||||||
|
vec![],
|
||||||
|
(fee_amount + proposed_change).unwrap(),
|
||||||
|
)
|
||||||
|
.ok_or_else(|| BalanceError::Overflow.into()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TransactionBalance::new(
|
||||||
|
vec![ChangeValue::Sapling(proposed_change)],
|
||||||
|
fee_amount,
|
||||||
|
)
|
||||||
|
.ok_or_else(|| BalanceError::Overflow.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use zcash_primitives::{
|
||||||
|
consensus::{Network, NetworkUpgrade, Parameters},
|
||||||
|
transaction::{
|
||||||
|
components::{amount::Amount, transparent::TxOut},
|
||||||
|
fees::fixed::FeeRule as FixedFeeRule,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::SingleOutputChangeStrategy;
|
||||||
|
use crate::{
|
||||||
|
data_api::wallet::input_selection::SaplingPayment,
|
||||||
|
fees::{
|
||||||
|
tests::{TestSaplingInput, TestTransparentInput},
|
||||||
|
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_without_dust() {
|
||||||
|
let change_strategy = SingleOutputChangeStrategy::new(FixedFeeRule::standard());
|
||||||
|
|
||||||
|
// spend a single Sapling note that is sufficient to pay the fee
|
||||||
|
let result = change_strategy.compute_balance(
|
||||||
|
&Network::TestNetwork,
|
||||||
|
Network::TestNetwork
|
||||||
|
.activation_height(NetworkUpgrade::Nu5)
|
||||||
|
.unwrap(),
|
||||||
|
&Vec::<TestTransparentInput>::new(),
|
||||||
|
&Vec::<TxOut>::new(),
|
||||||
|
&[TestSaplingInput {
|
||||||
|
note_id: 0,
|
||||||
|
value: Amount::from_u64(45000).unwrap(),
|
||||||
|
}],
|
||||||
|
&[SaplingPayment::new(Amount::from_u64(40000).unwrap())],
|
||||||
|
&DustOutputPolicy::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
result,
|
||||||
|
Ok(balance) if balance.proposed_change() == [ChangeValue::Sapling(Amount::from_u64(4000).unwrap())]
|
||||||
|
&& balance.fee_required() == Amount::from_u64(1000).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dust_change() {
|
||||||
|
let change_strategy = SingleOutputChangeStrategy::new(FixedFeeRule::standard());
|
||||||
|
|
||||||
|
// spend a single Sapling note that is sufficient to pay the fee
|
||||||
|
let result = change_strategy.compute_balance(
|
||||||
|
&Network::TestNetwork,
|
||||||
|
Network::TestNetwork
|
||||||
|
.activation_height(NetworkUpgrade::Nu5)
|
||||||
|
.unwrap(),
|
||||||
|
&Vec::<TestTransparentInput>::new(),
|
||||||
|
&Vec::<TxOut>::new(),
|
||||||
|
&[
|
||||||
|
TestSaplingInput {
|
||||||
|
note_id: 0,
|
||||||
|
value: Amount::from_u64(40000).unwrap(),
|
||||||
|
},
|
||||||
|
// enough to pay a fee, plus dust
|
||||||
|
TestSaplingInput {
|
||||||
|
note_id: 0,
|
||||||
|
value: Amount::from_u64(1100).unwrap(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
&[SaplingPayment::new(Amount::from_u64(40000).unwrap())],
|
||||||
|
&DustOutputPolicy::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
result,
|
||||||
|
Err(ChangeError::InsufficientFunds { available, required })
|
||||||
|
if available == Amount::from_u64(41100).unwrap() && required == Amount::from_u64(42000).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,337 @@
|
||||||
|
//! Change strategies designed to implement the ZIP 317 fee rules.
|
||||||
|
//!
|
||||||
|
//! Change selection in ZIP 317 requires careful handling of low-valued inputs
|
||||||
|
//! to ensure that inputs added to a transaction do not cause fees to rise by
|
||||||
|
//! an amount greater than their value.
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
|
||||||
|
use zcash_primitives::{
|
||||||
|
consensus::{self, BlockHeight},
|
||||||
|
transaction::{
|
||||||
|
components::{
|
||||||
|
amount::{Amount, BalanceError},
|
||||||
|
sapling::fees as sapling,
|
||||||
|
transparent::fees as transparent,
|
||||||
|
},
|
||||||
|
fees::{
|
||||||
|
zip317::{FeeError as Zip317FeeError, FeeRule as Zip317FeeRule},
|
||||||
|
FeeRule,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, TransactionBalance,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A change strategy that and proposes change as a single output to the most current supported
|
||||||
|
/// shielded pool and delegates fee calculation to the provided fee rule.
|
||||||
|
pub struct SingleOutputChangeStrategy {
|
||||||
|
fee_rule: Zip317FeeRule,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SingleOutputChangeStrategy {
|
||||||
|
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
|
||||||
|
/// fee parameters.
|
||||||
|
pub fn new(fee_rule: Zip317FeeRule) -> Self {
|
||||||
|
Self { fee_rule }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChangeStrategy for SingleOutputChangeStrategy {
|
||||||
|
type FeeRule = Zip317FeeRule;
|
||||||
|
type Error = Zip317FeeError;
|
||||||
|
|
||||||
|
fn fee_rule(&self) -> &Self::FeeRule {
|
||||||
|
&self.fee_rule
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
|
||||||
|
&self,
|
||||||
|
params: &P,
|
||||||
|
target_height: BlockHeight,
|
||||||
|
transparent_inputs: &[impl transparent::InputView],
|
||||||
|
transparent_outputs: &[impl transparent::OutputView],
|
||||||
|
sapling_inputs: &[impl sapling::InputView<NoteRefT>],
|
||||||
|
sapling_outputs: &[impl sapling::OutputView],
|
||||||
|
dust_output_policy: &DustOutputPolicy,
|
||||||
|
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
|
||||||
|
let mut transparent_dust: Vec<_> = transparent_inputs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|i| {
|
||||||
|
// for now, we're just assuming p2pkh inputs, so we don't check the size of the input
|
||||||
|
// script
|
||||||
|
if i.coin().value < self.fee_rule.marginal_fee() {
|
||||||
|
Some(i.outpoint().clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut sapling_dust: Vec<_> = sapling_inputs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|i| {
|
||||||
|
if i.value() < self.fee_rule.marginal_fee() {
|
||||||
|
Some(i.note_id().clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Depending on the shape of the transaction, we may be able to spend up to
|
||||||
|
// `grace_actions - 1` dust inputs. If we don't have any dust inputs though,
|
||||||
|
// we don't need to worry about any of that.
|
||||||
|
if !(transparent_dust.is_empty() && sapling_dust.is_empty()) {
|
||||||
|
let t_non_dust = transparent_inputs.len() - transparent_dust.len();
|
||||||
|
let t_allowed_dust = transparent_outputs.len().saturating_sub(t_non_dust);
|
||||||
|
|
||||||
|
// We add one to the sapling outputs for the (single) change output Note that this
|
||||||
|
// means that wallet-internal shielding transactions are an opportunity to spend a dust
|
||||||
|
// note.
|
||||||
|
let s_non_dust = sapling_inputs.len() - sapling_dust.len();
|
||||||
|
let s_allowed_dust = (sapling_outputs.len() + 1).saturating_sub(s_non_dust);
|
||||||
|
|
||||||
|
let available_grace_inputs = self
|
||||||
|
.fee_rule
|
||||||
|
.grace_actions()
|
||||||
|
.saturating_sub(t_non_dust)
|
||||||
|
.saturating_sub(s_non_dust);
|
||||||
|
|
||||||
|
let mut t_disallowed_dust = transparent_dust.len().saturating_sub(t_allowed_dust);
|
||||||
|
let mut s_disallowed_dust = sapling_dust.len().saturating_sub(s_allowed_dust);
|
||||||
|
|
||||||
|
if available_grace_inputs > 0 {
|
||||||
|
// If we have available grace inputs, allocate them first to transparent dust
|
||||||
|
// and then to Sapling dust. The caller has provided inputs that it is willing
|
||||||
|
// to spend, so we don't need to consider privacy effects at this layer.
|
||||||
|
let t_grace_dust = available_grace_inputs.saturating_sub(t_disallowed_dust);
|
||||||
|
t_disallowed_dust = t_disallowed_dust.saturating_sub(t_grace_dust);
|
||||||
|
|
||||||
|
let s_grace_dust = available_grace_inputs
|
||||||
|
.saturating_sub(t_grace_dust)
|
||||||
|
.saturating_sub(s_disallowed_dust);
|
||||||
|
s_disallowed_dust = s_disallowed_dust.saturating_sub(s_grace_dust);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate the lists of inputs to be disregarded in input selection to just the
|
||||||
|
// disallowed lengths. This has the effect of prioritizing inputs for inclusion by the
|
||||||
|
// order of the original input slices, with the most preferred inputs first.
|
||||||
|
transparent_dust.reverse();
|
||||||
|
transparent_dust.truncate(t_disallowed_dust);
|
||||||
|
sapling_dust.reverse();
|
||||||
|
sapling_dust.truncate(s_disallowed_dust);
|
||||||
|
|
||||||
|
if !(transparent_dust.is_empty() && sapling_dust.is_empty()) {
|
||||||
|
return Err(ChangeError::DustInputs {
|
||||||
|
transparent: transparent_dust,
|
||||||
|
sapling: sapling_dust,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let overflow = || ChangeError::StrategyError(Zip317FeeError::from(BalanceError::Overflow));
|
||||||
|
let underflow =
|
||||||
|
|| ChangeError::StrategyError(Zip317FeeError::from(BalanceError::Underflow));
|
||||||
|
|
||||||
|
let t_in = transparent_inputs
|
||||||
|
.iter()
|
||||||
|
.map(|t_in| t_in.coin().value)
|
||||||
|
.sum::<Option<_>>()
|
||||||
|
.ok_or_else(overflow)?;
|
||||||
|
let t_out = transparent_outputs
|
||||||
|
.iter()
|
||||||
|
.map(|t_out| t_out.value())
|
||||||
|
.sum::<Option<_>>()
|
||||||
|
.ok_or_else(overflow)?;
|
||||||
|
let sapling_in = sapling_inputs
|
||||||
|
.iter()
|
||||||
|
.map(|s_in| s_in.value())
|
||||||
|
.sum::<Option<_>>()
|
||||||
|
.ok_or_else(overflow)?;
|
||||||
|
let sapling_out = sapling_outputs
|
||||||
|
.iter()
|
||||||
|
.map(|s_out| s_out.value())
|
||||||
|
.sum::<Option<_>>()
|
||||||
|
.ok_or_else(overflow)?;
|
||||||
|
|
||||||
|
let fee_amount = self
|
||||||
|
.fee_rule
|
||||||
|
.fee_required(
|
||||||
|
params,
|
||||||
|
target_height,
|
||||||
|
transparent_inputs,
|
||||||
|
transparent_outputs,
|
||||||
|
sapling_inputs.len(),
|
||||||
|
sapling_outputs.len() + 1,
|
||||||
|
)
|
||||||
|
.map_err(ChangeError::StrategyError)?;
|
||||||
|
|
||||||
|
let total_in = (t_in + sapling_in).ok_or_else(overflow)?;
|
||||||
|
|
||||||
|
let total_out = [t_out, sapling_out, fee_amount]
|
||||||
|
.iter()
|
||||||
|
.sum::<Option<Amount>>()
|
||||||
|
.ok_or_else(overflow)?;
|
||||||
|
|
||||||
|
let proposed_change = (total_in - total_out).ok_or_else(underflow)?;
|
||||||
|
match proposed_change.cmp(&Amount::zero()) {
|
||||||
|
Ordering::Less => Err(ChangeError::InsufficientFunds {
|
||||||
|
available: total_in,
|
||||||
|
required: total_out,
|
||||||
|
}),
|
||||||
|
Ordering::Equal => TransactionBalance::new(vec![], fee_amount).ok_or_else(overflow),
|
||||||
|
Ordering::Greater => {
|
||||||
|
let dust_threshold = dust_output_policy
|
||||||
|
.dust_threshold()
|
||||||
|
.unwrap_or_else(|| self.fee_rule.marginal_fee());
|
||||||
|
|
||||||
|
if dust_threshold > proposed_change {
|
||||||
|
match dust_output_policy.action() {
|
||||||
|
DustAction::Reject => {
|
||||||
|
let shortfall =
|
||||||
|
(dust_threshold - proposed_change).ok_or_else(underflow)?;
|
||||||
|
|
||||||
|
Err(ChangeError::InsufficientFunds {
|
||||||
|
available: total_in,
|
||||||
|
required: (total_in + shortfall).ok_or_else(overflow)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
DustAction::AllowDustChange => TransactionBalance::new(
|
||||||
|
vec![ChangeValue::Sapling(proposed_change)],
|
||||||
|
fee_amount,
|
||||||
|
)
|
||||||
|
.ok_or_else(overflow),
|
||||||
|
DustAction::AddDustToFee => {
|
||||||
|
TransactionBalance::new(vec![], (fee_amount + proposed_change).unwrap())
|
||||||
|
.ok_or_else(overflow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TransactionBalance::new(vec![ChangeValue::Sapling(proposed_change)], fee_amount)
|
||||||
|
.ok_or_else(overflow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use zcash_primitives::{
|
||||||
|
consensus::{Network, NetworkUpgrade, Parameters},
|
||||||
|
transaction::{
|
||||||
|
components::{amount::Amount, transparent::TxOut},
|
||||||
|
fees::zip317::FeeRule as Zip317FeeRule,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::SingleOutputChangeStrategy;
|
||||||
|
use crate::{
|
||||||
|
data_api::wallet::input_selection::SaplingPayment,
|
||||||
|
fees::{
|
||||||
|
tests::{TestSaplingInput, TestTransparentInput},
|
||||||
|
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_without_dust() {
|
||||||
|
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard());
|
||||||
|
|
||||||
|
// spend a single Sapling note that is sufficient to pay the fee
|
||||||
|
let result = change_strategy.compute_balance(
|
||||||
|
&Network::TestNetwork,
|
||||||
|
Network::TestNetwork
|
||||||
|
.activation_height(NetworkUpgrade::Nu5)
|
||||||
|
.unwrap(),
|
||||||
|
&Vec::<TestTransparentInput>::new(),
|
||||||
|
&Vec::<TxOut>::new(),
|
||||||
|
&[TestSaplingInput {
|
||||||
|
note_id: 0,
|
||||||
|
value: Amount::from_u64(55000).unwrap(),
|
||||||
|
}],
|
||||||
|
&[SaplingPayment::new(Amount::from_u64(40000).unwrap())],
|
||||||
|
&DustOutputPolicy::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
result,
|
||||||
|
Ok(balance) if balance.proposed_change() == [ChangeValue::Sapling(Amount::from_u64(5000).unwrap())]
|
||||||
|
&& balance.fee_required() == Amount::from_u64(10000).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_with_allowable_dust() {
|
||||||
|
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard());
|
||||||
|
|
||||||
|
// spend a single Sapling note that is sufficient to pay the fee
|
||||||
|
let result = change_strategy.compute_balance(
|
||||||
|
&Network::TestNetwork,
|
||||||
|
Network::TestNetwork
|
||||||
|
.activation_height(NetworkUpgrade::Nu5)
|
||||||
|
.unwrap(),
|
||||||
|
&Vec::<TestTransparentInput>::new(),
|
||||||
|
&Vec::<TxOut>::new(),
|
||||||
|
&[
|
||||||
|
TestSaplingInput {
|
||||||
|
note_id: 0,
|
||||||
|
value: Amount::from_u64(49000).unwrap(),
|
||||||
|
},
|
||||||
|
TestSaplingInput {
|
||||||
|
note_id: 1,
|
||||||
|
value: Amount::from_u64(1000).unwrap(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
&[SaplingPayment::new(Amount::from_u64(40000).unwrap())],
|
||||||
|
&DustOutputPolicy::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
result,
|
||||||
|
Ok(balance) if balance.proposed_change().is_empty()
|
||||||
|
&& balance.fee_required() == Amount::from_u64(10000).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_with_disallowed_dust() {
|
||||||
|
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard());
|
||||||
|
|
||||||
|
// spend a single Sapling note that is sufficient to pay the fee
|
||||||
|
let result = change_strategy.compute_balance(
|
||||||
|
&Network::TestNetwork,
|
||||||
|
Network::TestNetwork
|
||||||
|
.activation_height(NetworkUpgrade::Nu5)
|
||||||
|
.unwrap(),
|
||||||
|
&Vec::<TestTransparentInput>::new(),
|
||||||
|
&Vec::<TxOut>::new(),
|
||||||
|
&[
|
||||||
|
TestSaplingInput {
|
||||||
|
note_id: 0,
|
||||||
|
value: Amount::from_u64(29000).unwrap(),
|
||||||
|
},
|
||||||
|
TestSaplingInput {
|
||||||
|
note_id: 1,
|
||||||
|
value: Amount::from_u64(20000).unwrap(),
|
||||||
|
},
|
||||||
|
TestSaplingInput {
|
||||||
|
note_id: 2,
|
||||||
|
value: Amount::from_u64(1000).unwrap(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
&[SaplingPayment::new(Amount::from_u64(40000).unwrap())],
|
||||||
|
&DustOutputPolicy::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// We will get an error here, because the dust input now isn't free to add
|
||||||
|
// to the transaction.
|
||||||
|
assert_matches!(
|
||||||
|
result,
|
||||||
|
Err(ChangeError::DustInputs { sapling, .. }) if sapling == vec![2]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,3 +21,7 @@ pub mod welding_rig;
|
||||||
pub mod zip321;
|
pub mod zip321;
|
||||||
|
|
||||||
pub use decrypt::{decrypt_transaction, DecryptedOutput, TransferType};
|
pub use decrypt::{decrypt_transaction, DecryptedOutput, TransferType};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate assert_matches;
|
||||||
|
|
|
@ -10,7 +10,8 @@ use zcash_primitives::{
|
||||||
sapling::{Diversifier, Node, Note, Nullifier, PaymentAddress, Rseed},
|
sapling::{Diversifier, Node, Note, Nullifier, PaymentAddress, Rseed},
|
||||||
transaction::{
|
transaction::{
|
||||||
components::{
|
components::{
|
||||||
transparent::{OutPoint, TxOut},
|
sapling,
|
||||||
|
transparent::{self, OutPoint, TxOut},
|
||||||
Amount,
|
Amount,
|
||||||
},
|
},
|
||||||
TxId,
|
TxId,
|
||||||
|
@ -69,6 +70,19 @@ impl WalletTransparentOutput {
|
||||||
pub fn recipient_address(&self) -> &TransparentAddress {
|
pub fn recipient_address(&self) -> &TransparentAddress {
|
||||||
&self.recipient_address
|
&self.recipient_address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> Amount {
|
||||||
|
self.txout.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl transparent::fees::InputView for WalletTransparentOutput {
|
||||||
|
fn outpoint(&self) -> &OutPoint {
|
||||||
|
&self.outpoint
|
||||||
|
}
|
||||||
|
fn coin(&self) -> &TxOut {
|
||||||
|
&self.txout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A subset of a [`SpendDescription`] relevant to wallets and light clients.
|
/// A subset of a [`SpendDescription`] relevant to wallets and light clients.
|
||||||
|
@ -97,13 +111,24 @@ pub struct WalletShieldedOutput<N> {
|
||||||
|
|
||||||
/// Information about a note that is tracked by the wallet that is available for spending,
|
/// Information about a note that is tracked by the wallet that is available for spending,
|
||||||
/// with sufficient information for use in note selection.
|
/// with sufficient information for use in note selection.
|
||||||
pub struct SpendableNote {
|
pub struct SpendableNote<NoteRef> {
|
||||||
|
pub note_id: NoteRef,
|
||||||
pub diversifier: Diversifier,
|
pub diversifier: Diversifier,
|
||||||
pub note_value: Amount,
|
pub note_value: Amount,
|
||||||
pub rseed: Rseed,
|
pub rseed: Rseed,
|
||||||
pub witness: IncrementalWitness<Node>,
|
pub witness: IncrementalWitness<Node>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<NoteRef> sapling::fees::InputView<NoteRef> for SpendableNote<NoteRef> {
|
||||||
|
fn note_id(&self) -> &NoteRef {
|
||||||
|
&self.note_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value(&self) -> Amount {
|
||||||
|
self.note_value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Describes a policy for which outgoing viewing key should be able to decrypt
|
/// Describes a policy for which outgoing viewing key should be able to decrypt
|
||||||
/// transaction outputs.
|
/// transaction outputs.
|
||||||
///
|
///
|
||||||
|
|
|
@ -115,6 +115,11 @@ pub struct TransactionRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TransactionRequest {
|
impl TransactionRequest {
|
||||||
|
/// Constructs a new empty transaction request.
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self { payments: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
/// Constructs a new transaction request that obeys the ZIP-321 invariants
|
/// Constructs a new transaction request that obeys the ZIP-321 invariants
|
||||||
pub fn new(payments: Vec<Payment>) -> Result<TransactionRequest, Zip321Error> {
|
pub fn new(payments: Vec<Payment>) -> Result<TransactionRequest, Zip321Error> {
|
||||||
let request = TransactionRequest { payments };
|
let request = TransactionRequest { payments };
|
||||||
|
|
|
@ -22,6 +22,8 @@ and this library adheres to Rust's notion of
|
||||||
to initialize the accounts table with a noncontiguous set of account identifiers.
|
to initialize the accounts table with a noncontiguous set of account identifiers.
|
||||||
- `SqliteClientError::AccountIdOutOfRange`, to report when the maximum account
|
- `SqliteClientError::AccountIdOutOfRange`, to report when the maximum account
|
||||||
identifier has been reached.
|
identifier has been reached.
|
||||||
|
- `SqliteClientError::Protobuf`, to support handling of errors in serialized
|
||||||
|
protobuf data decoding.
|
||||||
- An `unstable` feature flag; this is added to parts of the API that may change
|
- An `unstable` feature flag; this is added to parts of the API that may change
|
||||||
in any release. It enables `zcash_client_backend`'s `unstable` feature flag.
|
in any release. It enables `zcash_client_backend`'s `unstable` feature flag.
|
||||||
- New summary views that may be directly accessed in the sqlite database.
|
- New summary views that may be directly accessed in the sqlite database.
|
||||||
|
@ -40,6 +42,7 @@ and this library adheres to Rust's notion of
|
||||||
this block source.
|
this block source.
|
||||||
- `zcash_client_sqlite::chain::init::init_blockmeta_db` creates the required
|
- `zcash_client_sqlite::chain::init::init_blockmeta_db` creates the required
|
||||||
metadata cache database.
|
metadata cache database.
|
||||||
|
- Implementations of `PartialEq`, `Eq`, `PartialOrd`, and `Ord` for `NoteId`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Various **BREAKING CHANGES** have been made to the database tables. These will
|
- Various **BREAKING CHANGES** have been made to the database tables. These will
|
||||||
|
@ -76,6 +79,9 @@ and this library adheres to Rust's notion of
|
||||||
- `zcash_client_sqlite::wallet`:
|
- `zcash_client_sqlite::wallet`:
|
||||||
- `get_spendable_notes` to `get_spendable_sapling_notes`.
|
- `get_spendable_notes` to `get_spendable_sapling_notes`.
|
||||||
- `select_spendable_notes` to `select_spendable_sapling_notes`.
|
- `select_spendable_notes` to `select_spendable_sapling_notes`.
|
||||||
|
- `zcash_client_sqlite::wallet::{get_spendable_sapling_notes, select_spendable_sapling_notes}`
|
||||||
|
have also been changed to take a parameter that permits the caller to
|
||||||
|
specify a set of notes to exclude from consideration.
|
||||||
- `zcash_client_sqlite::wallet::init_wallet_db` has been modified to
|
- `zcash_client_sqlite::wallet::init_wallet_db` has been modified to
|
||||||
take the wallet seed as an argument so that it can correctly perform
|
take the wallet seed as an argument so that it can correctly perform
|
||||||
migrations that require re-deriving key material. In particular for
|
migrations that require re-deriving key material. In particular for
|
||||||
|
@ -95,6 +101,10 @@ and this library adheres to Rust's notion of
|
||||||
- `delete_utxos_above` (use `WalletWrite::rewind_to_height` instead)
|
- `delete_utxos_above` (use `WalletWrite::rewind_to_height` instead)
|
||||||
- `zcash_client_sqlite::with_blocks` (use
|
- `zcash_client_sqlite::with_blocks` (use
|
||||||
`zcash_client_backend::data_api::BlockSource::with_blocks` instead)
|
`zcash_client_backend::data_api::BlockSource::with_blocks` instead)
|
||||||
|
- `zcash_client_sqlite::error::SqliteClientError` variants:
|
||||||
|
- `SqliteClientError::IncorrectHrpExtFvk`
|
||||||
|
- `SqliteClientError::Base58`
|
||||||
|
- `SqliteClientError::BackendError`
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- The `zcash_client_backend::data_api::WalletRead::get_address` implementation
|
- The `zcash_client_backend::data_api::WalletRead::get_address` implementation
|
||||||
|
|
|
@ -32,7 +32,7 @@ secrecy = "0.8"
|
||||||
# - SQLite databases
|
# - SQLite databases
|
||||||
group = "0.12"
|
group = "0.12"
|
||||||
jubjub = "0.9"
|
jubjub = "0.9"
|
||||||
rusqlite = { version = "0.25", features = ["bundled", "time"] }
|
rusqlite = { version = "0.25", features = ["bundled", "time", "array"] }
|
||||||
schemer = "0.2"
|
schemer = "0.2"
|
||||||
schemer-rusqlite = "0.2"
|
schemer-rusqlite = "0.2"
|
||||||
time = "0.2"
|
time = "0.2"
|
||||||
|
@ -42,6 +42,7 @@ uuid = "1.1"
|
||||||
# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.)
|
# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.)
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
assert_matches = "1.5"
|
||||||
proptest = "1.0.0"
|
proptest = "1.0.0"
|
||||||
rand_core = "0.6"
|
rand_core = "0.6"
|
||||||
regex = "1.4"
|
regex = "1.4"
|
||||||
|
|
|
@ -5,13 +5,13 @@ use rusqlite::params;
|
||||||
|
|
||||||
use zcash_primitives::consensus::BlockHeight;
|
use zcash_primitives::consensus::BlockHeight;
|
||||||
|
|
||||||
use zcash_client_backend::{data_api::error::Error, proto::compact_formats::CompactBlock};
|
use zcash_client_backend::{data_api::chain::error::Error, proto::compact_formats::CompactBlock};
|
||||||
|
|
||||||
use crate::{error::SqliteClientError, BlockDb};
|
use crate::{error::SqliteClientError, BlockDb};
|
||||||
|
|
||||||
#[cfg(feature = "unstable")]
|
#[cfg(feature = "unstable")]
|
||||||
use {
|
use {
|
||||||
crate::{BlockHash, FsBlockDb},
|
crate::{BlockHash, FsBlockDb, FsBlockDbError},
|
||||||
rusqlite::Connection,
|
rusqlite::Connection,
|
||||||
std::fs::File,
|
std::fs::File,
|
||||||
std::io::Read,
|
std::io::Read,
|
||||||
|
@ -21,53 +21,51 @@ use {
|
||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod migrations;
|
pub mod migrations;
|
||||||
|
|
||||||
struct CompactBlockRow {
|
|
||||||
height: BlockHeight,
|
|
||||||
data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implements a traversal of `limit` blocks of the block cache database.
|
/// Implements a traversal of `limit` blocks of the block cache database.
|
||||||
///
|
///
|
||||||
/// Starting at the next block above `last_scanned_height`, the `with_row` callback is invoked with
|
/// Starting at the next block above `last_scanned_height`, the `with_row` callback is invoked with
|
||||||
/// each block retrieved from the backing store. If the `limit` value provided is `None`, all
|
/// each block retrieved from the backing store. If the `limit` value provided is `None`, all
|
||||||
/// blocks are traversed up to the maximum height.
|
/// blocks are traversed up to the maximum height.
|
||||||
pub(crate) fn blockdb_with_blocks<F>(
|
pub(crate) fn blockdb_with_blocks<F, DbErrT, NoteRef>(
|
||||||
cache: &BlockDb,
|
block_source: &BlockDb,
|
||||||
last_scanned_height: BlockHeight,
|
last_scanned_height: BlockHeight,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
mut with_row: F,
|
mut with_row: F,
|
||||||
) -> Result<(), SqliteClientError>
|
) -> Result<(), Error<DbErrT, SqliteClientError, NoteRef>>
|
||||||
where
|
where
|
||||||
F: FnMut(CompactBlock) -> Result<(), SqliteClientError>,
|
F: FnMut(CompactBlock) -> Result<(), Error<DbErrT, SqliteClientError, NoteRef>>,
|
||||||
{
|
{
|
||||||
// Fetch the CompactBlocks we need to scan
|
fn to_chain_error<D, E: Into<SqliteClientError>, N>(err: E) -> Error<D, SqliteClientError, N> {
|
||||||
let mut stmt_blocks = cache.0.prepare(
|
Error::BlockSource(err.into())
|
||||||
"SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC LIMIT ?",
|
}
|
||||||
)?;
|
|
||||||
|
|
||||||
let rows = stmt_blocks.query_map(
|
// Fetch the CompactBlocks we need to scan
|
||||||
params![
|
let mut stmt_blocks = block_source
|
||||||
|
.0
|
||||||
|
.prepare(
|
||||||
|
"SELECT height, data FROM compactblocks
|
||||||
|
WHERE height > ?
|
||||||
|
ORDER BY height ASC LIMIT ?",
|
||||||
|
)
|
||||||
|
.map_err(to_chain_error)?;
|
||||||
|
|
||||||
|
let mut rows = stmt_blocks
|
||||||
|
.query(params![
|
||||||
u32::from(last_scanned_height),
|
u32::from(last_scanned_height),
|
||||||
limit.unwrap_or(u32::max_value()),
|
limit.unwrap_or(u32::max_value()),
|
||||||
],
|
])
|
||||||
|row| {
|
.map_err(to_chain_error)?;
|
||||||
Ok(CompactBlockRow {
|
|
||||||
height: BlockHeight::from_u32(row.get(0)?),
|
|
||||||
data: row.get(1)?,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
for row_result in rows {
|
while let Some(row) = rows.next().map_err(to_chain_error)? {
|
||||||
let cbr = row_result?;
|
let height = BlockHeight::from_u32(row.get(0).map_err(to_chain_error)?);
|
||||||
let block = CompactBlock::decode(&cbr.data[..]).map_err(Error::from)?;
|
let data: Vec<u8> = row.get(1).map_err(to_chain_error)?;
|
||||||
|
let block = CompactBlock::decode(&data[..]).map_err(to_chain_error)?;
|
||||||
if block.height() != cbr.height {
|
if block.height() != height {
|
||||||
return Err(SqliteClientError::CorruptedData(format!(
|
return Err(to_chain_error(SqliteClientError::CorruptedData(format!(
|
||||||
"Block height {} did not match row's height field value {}",
|
"Block height {} did not match row's height field value {}",
|
||||||
block.height(),
|
block.height(),
|
||||||
cbr.height
|
height
|
||||||
)));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
with_row(block)?;
|
with_row(block)?;
|
||||||
|
@ -160,53 +158,65 @@ pub(crate) fn blockmetadb_get_max_cached_height(
|
||||||
/// invoked with each block retrieved from the backing store. If the `limit` value provided is
|
/// invoked with each block retrieved from the backing store. If the `limit` value provided is
|
||||||
/// `None`, all blocks are traversed up to the maximum height for which metadata is available.
|
/// `None`, all blocks are traversed up to the maximum height for which metadata is available.
|
||||||
#[cfg(feature = "unstable")]
|
#[cfg(feature = "unstable")]
|
||||||
pub(crate) fn fsblockdb_with_blocks<F>(
|
pub(crate) fn fsblockdb_with_blocks<F, DbErrT, NoteRef>(
|
||||||
cache: &FsBlockDb,
|
cache: &FsBlockDb,
|
||||||
last_scanned_height: BlockHeight,
|
last_scanned_height: BlockHeight,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
mut with_block: F,
|
mut with_block: F,
|
||||||
) -> Result<(), SqliteClientError>
|
) -> Result<(), Error<DbErrT, FsBlockDbError, NoteRef>>
|
||||||
where
|
where
|
||||||
F: FnMut(CompactBlock) -> Result<(), SqliteClientError>,
|
F: FnMut(CompactBlock) -> Result<(), Error<DbErrT, FsBlockDbError, NoteRef>>,
|
||||||
{
|
{
|
||||||
|
fn to_chain_error<D, E: Into<FsBlockDbError>, N>(err: E) -> Error<D, FsBlockDbError, N> {
|
||||||
|
Error::BlockSource(err.into())
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch the CompactBlocks we need to scan
|
// Fetch the CompactBlocks we need to scan
|
||||||
let mut stmt_blocks = cache.conn.prepare(
|
let mut stmt_blocks = cache
|
||||||
"SELECT height, blockhash, time, sapling_outputs_count, orchard_actions_count
|
.conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT height, blockhash, time, sapling_outputs_count, orchard_actions_count
|
||||||
FROM compactblocks_meta
|
FROM compactblocks_meta
|
||||||
WHERE height > ?
|
WHERE height > ?
|
||||||
ORDER BY height ASC LIMIT ?",
|
ORDER BY height ASC LIMIT ?",
|
||||||
)?;
|
)
|
||||||
|
.map_err(to_chain_error)?;
|
||||||
|
|
||||||
let rows = stmt_blocks.query_map(
|
let rows = stmt_blocks
|
||||||
params![
|
.query_map(
|
||||||
u32::from(last_scanned_height),
|
params![
|
||||||
limit.unwrap_or(u32::max_value()),
|
u32::from(last_scanned_height),
|
||||||
],
|
limit.unwrap_or(u32::max_value()),
|
||||||
|row| {
|
],
|
||||||
Ok(BlockMeta {
|
|row| {
|
||||||
height: BlockHeight::from_u32(row.get(0)?),
|
Ok(BlockMeta {
|
||||||
block_hash: BlockHash::from_slice(&row.get::<_, Vec<_>>(1)?),
|
height: BlockHeight::from_u32(row.get(0)?),
|
||||||
block_time: row.get(2)?,
|
block_hash: BlockHash::from_slice(&row.get::<_, Vec<_>>(1)?),
|
||||||
sapling_outputs_count: row.get(3)?,
|
block_time: row.get(2)?,
|
||||||
orchard_actions_count: row.get(4)?,
|
sapling_outputs_count: row.get(3)?,
|
||||||
})
|
orchard_actions_count: row.get(4)?,
|
||||||
},
|
})
|
||||||
)?;
|
},
|
||||||
|
)
|
||||||
|
.map_err(to_chain_error)?;
|
||||||
|
|
||||||
for row_result in rows {
|
for row_result in rows {
|
||||||
let cbr = row_result?;
|
let cbr = row_result.map_err(to_chain_error)?;
|
||||||
let mut block_file = File::open(cbr.block_file_path(&cache.blocks_dir))?;
|
let mut block_file =
|
||||||
|
File::open(cbr.block_file_path(&cache.blocks_dir)).map_err(to_chain_error)?;
|
||||||
let mut block_data = vec![];
|
let mut block_data = vec![];
|
||||||
block_file.read_to_end(&mut block_data)?;
|
block_file
|
||||||
|
.read_to_end(&mut block_data)
|
||||||
|
.map_err(to_chain_error)?;
|
||||||
|
|
||||||
let block = CompactBlock::decode(&block_data[..]).map_err(Error::from)?;
|
let block = CompactBlock::decode(&block_data[..]).map_err(to_chain_error)?;
|
||||||
|
|
||||||
if block.height() != cbr.height {
|
if block.height() != cbr.height {
|
||||||
return Err(SqliteClientError::CorruptedData(format!(
|
return Err(to_chain_error(FsBlockDbError::CorruptedData(format!(
|
||||||
"Block height {} did not match row's height field value {}",
|
"Block height {} did not match row's height field value {}",
|
||||||
block.height(),
|
block.height(),
|
||||||
cbr.height
|
cbr.height
|
||||||
)));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
with_block(block)?;
|
with_block(block)?;
|
||||||
|
@ -225,21 +235,20 @@ mod tests {
|
||||||
block::BlockHash, transaction::components::Amount, zip32::ExtendedSpendingKey,
|
block::BlockHash, transaction::components::Amount, zip32::ExtendedSpendingKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use zcash_client_backend::data_api::WalletRead;
|
use zcash_client_backend::data_api::chain::{
|
||||||
use zcash_client_backend::data_api::{
|
error::{Cause, Error},
|
||||||
chain::{scan_cached_blocks, validate_chain},
|
scan_cached_blocks, validate_chain,
|
||||||
error::{ChainInvalid, Error},
|
|
||||||
};
|
};
|
||||||
|
use zcash_client_backend::data_api::WalletRead;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
chain::init::init_cache_database,
|
chain::init::init_cache_database,
|
||||||
error::SqliteClientError,
|
|
||||||
tests::{
|
tests::{
|
||||||
self, fake_compact_block, fake_compact_block_spending, init_test_accounts_table,
|
self, fake_compact_block, fake_compact_block_spending, init_test_accounts_table,
|
||||||
insert_into_cache, sapling_activation_height, AddressType,
|
insert_into_cache, sapling_activation_height, AddressType,
|
||||||
},
|
},
|
||||||
wallet::{get_balance, init::init_wallet_db, rewind_to_height},
|
wallet::{get_balance, init::init_wallet_db, rewind_to_height},
|
||||||
AccountId, BlockDb, NoteId, WalletDb,
|
AccountId, BlockDb, WalletDb,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -385,16 +394,13 @@ mod tests {
|
||||||
insert_into_cache(&db_cache, &cb4);
|
insert_into_cache(&db_cache, &cb4);
|
||||||
|
|
||||||
// Data+cache chain should be invalid at the data/cache boundary
|
// Data+cache chain should be invalid at the data/cache boundary
|
||||||
match validate_chain(
|
let val_result = validate_chain(
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
&db_cache,
|
&db_cache,
|
||||||
db_data.get_max_height_hash().unwrap(),
|
db_data.get_max_height_hash().unwrap(),
|
||||||
) {
|
);
|
||||||
Err(SqliteClientError::BackendError(Error::InvalidChain(lower_bound, _))) => {
|
|
||||||
assert_eq!(lower_bound, sapling_activation_height() + 2)
|
assert_matches!(val_result, Err(Error::Chain(e)) if e.at_height() == sapling_activation_height() + 2);
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -459,16 +465,13 @@ mod tests {
|
||||||
insert_into_cache(&db_cache, &cb4);
|
insert_into_cache(&db_cache, &cb4);
|
||||||
|
|
||||||
// Data+cache chain should be invalid inside the cache
|
// Data+cache chain should be invalid inside the cache
|
||||||
match validate_chain(
|
let val_result = validate_chain(
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
&db_cache,
|
&db_cache,
|
||||||
db_data.get_max_height_hash().unwrap(),
|
db_data.get_max_height_hash().unwrap(),
|
||||||
) {
|
);
|
||||||
Err(SqliteClientError::BackendError(Error::InvalidChain(lower_bound, _))) => {
|
|
||||||
assert_eq!(lower_bound, sapling_activation_height() + 3)
|
assert_matches!(val_result, Err(Error::Chain(e)) if e.at_height() == sapling_activation_height() + 3);
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -590,14 +593,11 @@ mod tests {
|
||||||
);
|
);
|
||||||
insert_into_cache(&db_cache, &cb3);
|
insert_into_cache(&db_cache, &cb3);
|
||||||
match scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None) {
|
match scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None) {
|
||||||
Err(SqliteClientError::BackendError(e)) => {
|
Err(Error::Chain(e)) => {
|
||||||
assert_eq!(
|
assert_matches!(
|
||||||
e.to_string(),
|
e.cause(),
|
||||||
ChainInvalid::block_height_discontinuity::<NoteId>(
|
Cause::BlockHeightDiscontinuity(h) if *h
|
||||||
sapling_activation_height() + 1,
|
== sapling_activation_height() + 2
|
||||||
sapling_activation_height() + 2
|
|
||||||
)
|
|
||||||
.to_string()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(_) | Err(_) => panic!("Should have failed"),
|
Ok(_) | Err(_) => panic!("Should have failed"),
|
||||||
|
|
|
@ -3,13 +3,10 @@
|
||||||
use std::error;
|
use std::error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::encoding::{Bech32DecodeError, TransparentCodecError};
|
||||||
data_api,
|
|
||||||
encoding::{Bech32DecodeError, TransparentCodecError},
|
|
||||||
};
|
|
||||||
use zcash_primitives::{consensus::BlockHeight, zip32::AccountId};
|
use zcash_primitives::{consensus::BlockHeight, zip32::AccountId};
|
||||||
|
|
||||||
use crate::{NoteId, PRUNING_HEIGHT};
|
use crate::PRUNING_HEIGHT;
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
use zcash_primitives::legacy::TransparentAddress;
|
use zcash_primitives::legacy::TransparentAddress;
|
||||||
|
@ -23,8 +20,8 @@ pub enum SqliteClientError {
|
||||||
/// Decoding of a stored value from its serialized form has failed.
|
/// Decoding of a stored value from its serialized form has failed.
|
||||||
CorruptedData(String),
|
CorruptedData(String),
|
||||||
|
|
||||||
/// Decoding of the extended full viewing key has failed (for the specified network)
|
/// An error occurred decoding a protobuf message.
|
||||||
IncorrectHrpExtFvk,
|
Protobuf(prost::DecodeError),
|
||||||
|
|
||||||
/// The rcm value for a note cannot be decoded to a valid JubJub point.
|
/// The rcm value for a note cannot be decoded to a valid JubJub point.
|
||||||
InvalidNote,
|
InvalidNote,
|
||||||
|
@ -43,14 +40,12 @@ pub enum SqliteClientError {
|
||||||
/// A Bech32-encoded key or address decoding error
|
/// A Bech32-encoded key or address decoding error
|
||||||
Bech32DecodeError(Bech32DecodeError),
|
Bech32DecodeError(Bech32DecodeError),
|
||||||
|
|
||||||
/// Base58 decoding error
|
/// An error produced in legacy transparent address derivation
|
||||||
Base58(bs58::decode::Error),
|
|
||||||
|
|
||||||
///
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
HdwalletError(hdwallet::error::Error),
|
HdwalletError(hdwallet::error::Error),
|
||||||
|
|
||||||
/// Base58 decoding error
|
/// An error encountered in decoding a transparent address from its
|
||||||
|
/// serialized form.
|
||||||
TransparentAddress(TransparentCodecError),
|
TransparentAddress(TransparentCodecError),
|
||||||
|
|
||||||
/// Wrapper for rusqlite errors.
|
/// Wrapper for rusqlite errors.
|
||||||
|
@ -67,9 +62,6 @@ pub enum SqliteClientError {
|
||||||
/// (safe rewind height, requested height).
|
/// (safe rewind height, requested height).
|
||||||
RequestedRewindInvalid(BlockHeight, BlockHeight),
|
RequestedRewindInvalid(BlockHeight, BlockHeight),
|
||||||
|
|
||||||
/// Wrapper for errors from zcash_client_backend
|
|
||||||
BackendError(data_api::error::Error<NoteId>),
|
|
||||||
|
|
||||||
/// The space of allocatable diversifier indices has been exhausted for
|
/// The space of allocatable diversifier indices has been exhausted for
|
||||||
/// the given account.
|
/// the given account.
|
||||||
DiversifierIndexOutOfRange,
|
DiversifierIndexOutOfRange,
|
||||||
|
@ -109,14 +101,13 @@ impl fmt::Display for SqliteClientError {
|
||||||
SqliteClientError::CorruptedData(reason) => {
|
SqliteClientError::CorruptedData(reason) => {
|
||||||
write!(f, "Data DB is corrupted: {}", reason)
|
write!(f, "Data DB is corrupted: {}", reason)
|
||||||
}
|
}
|
||||||
SqliteClientError::IncorrectHrpExtFvk => write!(f, "Incorrect HRP for extfvk"),
|
SqliteClientError::Protobuf(e) => write!(f, "Failed to parse protobuf-encoded record: {}", e),
|
||||||
SqliteClientError::InvalidNote => write!(f, "Invalid note"),
|
SqliteClientError::InvalidNote => write!(f, "Invalid note"),
|
||||||
SqliteClientError::InvalidNoteId =>
|
SqliteClientError::InvalidNoteId =>
|
||||||
write!(f, "The note ID associated with an inserted witness must correspond to a received note."),
|
write!(f, "The note ID associated with an inserted witness must correspond to a received note."),
|
||||||
SqliteClientError::RequestedRewindInvalid(h, r) =>
|
SqliteClientError::RequestedRewindInvalid(h, r) =>
|
||||||
write!(f, "A rewind must be either of less than {} blocks, or at least back to block {} for your wallet; the requested height was {}.", PRUNING_HEIGHT, h, r),
|
write!(f, "A rewind must be either of less than {} blocks, or at least back to block {} for your wallet; the requested height was {}.", PRUNING_HEIGHT, h, r),
|
||||||
SqliteClientError::Bech32DecodeError(e) => write!(f, "{}", e),
|
SqliteClientError::Bech32DecodeError(e) => write!(f, "{}", e),
|
||||||
SqliteClientError::Base58(e) => write!(f, "{}", e),
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
SqliteClientError::HdwalletError(e) => write!(f, "{:?}", e),
|
SqliteClientError::HdwalletError(e) => write!(f, "{:?}", e),
|
||||||
SqliteClientError::TransparentAddress(e) => write!(f, "{}", e),
|
SqliteClientError::TransparentAddress(e) => write!(f, "{}", e),
|
||||||
|
@ -126,7 +117,6 @@ impl fmt::Display for SqliteClientError {
|
||||||
SqliteClientError::DbError(e) => write!(f, "{}", e),
|
SqliteClientError::DbError(e) => write!(f, "{}", e),
|
||||||
SqliteClientError::Io(e) => write!(f, "{}", e),
|
SqliteClientError::Io(e) => write!(f, "{}", e),
|
||||||
SqliteClientError::InvalidMemo(e) => write!(f, "{}", e),
|
SqliteClientError::InvalidMemo(e) => write!(f, "{}", e),
|
||||||
SqliteClientError::BackendError(e) => write!(f, "{}", e),
|
|
||||||
SqliteClientError::DiversifierIndexOutOfRange => write!(f, "The space of available diversifier indices is exhausted"),
|
SqliteClientError::DiversifierIndexOutOfRange => write!(f, "The space of available diversifier indices is exhausted"),
|
||||||
SqliteClientError::KeyDerivationError(acct_id) => write!(f, "Key derivation failed for account {:?}", acct_id),
|
SqliteClientError::KeyDerivationError(acct_id) => write!(f, "Key derivation failed for account {:?}", acct_id),
|
||||||
SqliteClientError::AccountIdDiscontinuity => write!(f, "Wallet account identifiers must be sequential."),
|
SqliteClientError::AccountIdDiscontinuity => write!(f, "Wallet account identifiers must be sequential."),
|
||||||
|
@ -155,9 +145,9 @@ impl From<Bech32DecodeError> for SqliteClientError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<bs58::decode::Error> for SqliteClientError {
|
impl From<prost::DecodeError> for SqliteClientError {
|
||||||
fn from(e: bs58::decode::Error) -> Self {
|
fn from(e: prost::DecodeError) -> Self {
|
||||||
SqliteClientError::Base58(e)
|
SqliteClientError::Protobuf(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,9 +169,3 @@ impl From<zcash_primitives::memo::Error> for SqliteClientError {
|
||||||
SqliteClientError::InvalidMemo(e)
|
SqliteClientError::InvalidMemo(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<data_api::error::Error<NoteId>> for SqliteClientError {
|
|
||||||
fn from(e: data_api::error::Error<NoteId>) -> Self {
|
|
||||||
SqliteClientError::BackendError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
//!
|
//!
|
||||||
//! [`WalletRead`]: zcash_client_backend::data_api::WalletRead
|
//! [`WalletRead`]: zcash_client_backend::data_api::WalletRead
|
||||||
//! [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite
|
//! [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite
|
||||||
//! [`BlockSource`]: zcash_client_backend::data_api::BlockSource
|
//! [`BlockSource`]: zcash_client_backend::data_api::chain::BlockSource
|
||||||
//! [`CompactBlock`]: zcash_client_backend::proto::compact_formats::CompactBlock
|
//! [`CompactBlock`]: zcash_client_backend::proto::compact_formats::CompactBlock
|
||||||
//! [`init_cache_database`]: crate::chain::init::init_cache_database
|
//! [`init_cache_database`]: crate::chain::init::init_cache_database
|
||||||
|
|
||||||
|
@ -45,15 +45,18 @@ use zcash_primitives::{
|
||||||
memo::Memo,
|
memo::Memo,
|
||||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||||
sapling::{Node, Nullifier},
|
sapling::{Node, Nullifier},
|
||||||
transaction::{components::Amount, Transaction, TxId},
|
transaction::{
|
||||||
|
components::{amount::Amount, OutPoint},
|
||||||
|
Transaction, TxId,
|
||||||
|
},
|
||||||
zip32::{AccountId, DiversifierIndex, ExtendedFullViewingKey},
|
zip32::{AccountId, DiversifierIndex, ExtendedFullViewingKey},
|
||||||
};
|
};
|
||||||
|
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
address::{AddressMetadata, UnifiedAddress},
|
address::{AddressMetadata, UnifiedAddress},
|
||||||
data_api::{
|
data_api::{
|
||||||
BlockSource, DecryptedTransaction, PoolType, PrunedBlock, Recipient, SentTransaction,
|
self, chain::BlockSource, DecryptedTransaction, PoolType, PrunedBlock, Recipient,
|
||||||
WalletRead, WalletWrite,
|
SentTransaction, WalletRead, WalletWrite,
|
||||||
},
|
},
|
||||||
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||||
proto::compact_formats::CompactBlock,
|
proto::compact_formats::CompactBlock,
|
||||||
|
@ -63,9 +66,6 @@ use zcash_client_backend::{
|
||||||
|
|
||||||
use crate::error::SqliteClientError;
|
use crate::error::SqliteClientError;
|
||||||
|
|
||||||
#[cfg(not(feature = "transparent-inputs"))]
|
|
||||||
use zcash_client_backend::data_api::error::Error;
|
|
||||||
|
|
||||||
#[cfg(feature = "unstable")]
|
#[cfg(feature = "unstable")]
|
||||||
use {
|
use {
|
||||||
crate::chain::{fsblockdb_with_blocks, BlockMeta},
|
crate::chain::{fsblockdb_with_blocks, BlockMeta},
|
||||||
|
@ -87,7 +87,7 @@ pub(crate) const PRUNING_HEIGHT: u32 = 100;
|
||||||
|
|
||||||
/// A newtype wrapper for sqlite primary key values for the notes
|
/// A newtype wrapper for sqlite primary key values for the notes
|
||||||
/// table.
|
/// table.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum NoteId {
|
pub enum NoteId {
|
||||||
SentNoteId(i64),
|
SentNoteId(i64),
|
||||||
ReceivedNoteId(i64),
|
ReceivedNoteId(i64),
|
||||||
|
@ -116,7 +116,10 @@ pub struct WalletDb<P> {
|
||||||
impl<P: consensus::Parameters> WalletDb<P> {
|
impl<P: consensus::Parameters> WalletDb<P> {
|
||||||
/// Construct a connection to the wallet database stored at the specified path.
|
/// Construct a connection to the wallet database stored at the specified path.
|
||||||
pub fn for_path<F: AsRef<Path>>(path: F, params: P) -> Result<Self, rusqlite::Error> {
|
pub fn for_path<F: AsRef<Path>>(path: F, params: P) -> Result<Self, rusqlite::Error> {
|
||||||
Connection::open(path).map(move |conn| WalletDb { conn, params })
|
Connection::open(path).and_then(move |conn| {
|
||||||
|
rusqlite::vtab::array::load_module(&conn)?;
|
||||||
|
Ok(WalletDb { conn, params })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given a wallet database connection, obtain a handle for the write operations
|
/// Given a wallet database connection, obtain a handle for the write operations
|
||||||
|
@ -230,9 +233,10 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
||||||
&self,
|
&self,
|
||||||
account: AccountId,
|
account: AccountId,
|
||||||
anchor_height: BlockHeight,
|
anchor_height: BlockHeight,
|
||||||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
exclude: &[Self::NoteRef],
|
||||||
|
) -> Result<Vec<SpendableNote<Self::NoteRef>>, Self::Error> {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
wallet::transact::get_spendable_sapling_notes(self, account, anchor_height)
|
wallet::transact::get_spendable_sapling_notes(self, account, anchor_height, exclude)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_spendable_sapling_notes(
|
fn select_spendable_sapling_notes(
|
||||||
|
@ -240,9 +244,16 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
||||||
account: AccountId,
|
account: AccountId,
|
||||||
target_value: Amount,
|
target_value: Amount,
|
||||||
anchor_height: BlockHeight,
|
anchor_height: BlockHeight,
|
||||||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
exclude: &[Self::NoteRef],
|
||||||
|
) -> Result<Vec<SpendableNote<Self::NoteRef>>, Self::Error> {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
wallet::transact::select_spendable_sapling_notes(self, account, target_value, anchor_height)
|
wallet::transact::select_spendable_sapling_notes(
|
||||||
|
self,
|
||||||
|
account,
|
||||||
|
target_value,
|
||||||
|
anchor_height,
|
||||||
|
exclude,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_transparent_receivers(
|
fn get_transparent_receivers(
|
||||||
|
@ -253,23 +264,24 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
||||||
return wallet::get_transparent_receivers(&self.params, &self.conn, _account);
|
return wallet::get_transparent_receivers(&self.params, &self.conn, _account);
|
||||||
|
|
||||||
#[cfg(not(feature = "transparent-inputs"))]
|
#[cfg(not(feature = "transparent-inputs"))]
|
||||||
return Err(SqliteClientError::BackendError(
|
panic!(
|
||||||
Error::TransparentInputsNotSupported,
|
"The wallet must be compiled with the transparent-inputs feature to use this method."
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_unspent_transparent_outputs(
|
fn get_unspent_transparent_outputs(
|
||||||
&self,
|
&self,
|
||||||
_address: &TransparentAddress,
|
_address: &TransparentAddress,
|
||||||
_max_height: BlockHeight,
|
_max_height: BlockHeight,
|
||||||
|
_exclude: &[OutPoint],
|
||||||
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
return wallet::get_unspent_transparent_outputs(self, _address, _max_height);
|
return wallet::get_unspent_transparent_outputs(self, _address, _max_height, _exclude);
|
||||||
|
|
||||||
#[cfg(not(feature = "transparent-inputs"))]
|
#[cfg(not(feature = "transparent-inputs"))]
|
||||||
return Err(SqliteClientError::BackendError(
|
panic!(
|
||||||
Error::TransparentInputsNotSupported,
|
"The wallet must be compiled with the transparent-inputs feature to use this method."
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_transparent_balances(
|
fn get_transparent_balances(
|
||||||
|
@ -281,9 +293,9 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
||||||
return wallet::get_transparent_balances(self, _account, _max_height);
|
return wallet::get_transparent_balances(self, _account, _max_height);
|
||||||
|
|
||||||
#[cfg(not(feature = "transparent-inputs"))]
|
#[cfg(not(feature = "transparent-inputs"))]
|
||||||
return Err(SqliteClientError::BackendError(
|
panic!(
|
||||||
Error::TransparentInputsNotSupported,
|
"The wallet must be compiled with the transparent-inputs feature to use this method."
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,9 +387,10 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
|
||||||
&self,
|
&self,
|
||||||
account: AccountId,
|
account: AccountId,
|
||||||
anchor_height: BlockHeight,
|
anchor_height: BlockHeight,
|
||||||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
exclude: &[Self::NoteRef],
|
||||||
|
) -> Result<Vec<SpendableNote<Self::NoteRef>>, Self::Error> {
|
||||||
self.wallet_db
|
self.wallet_db
|
||||||
.get_spendable_sapling_notes(account, anchor_height)
|
.get_spendable_sapling_notes(account, anchor_height, exclude)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_spendable_sapling_notes(
|
fn select_spendable_sapling_notes(
|
||||||
|
@ -385,9 +398,10 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
|
||||||
account: AccountId,
|
account: AccountId,
|
||||||
target_value: Amount,
|
target_value: Amount,
|
||||||
anchor_height: BlockHeight,
|
anchor_height: BlockHeight,
|
||||||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
exclude: &[Self::NoteRef],
|
||||||
|
) -> Result<Vec<SpendableNote<Self::NoteRef>>, Self::Error> {
|
||||||
self.wallet_db
|
self.wallet_db
|
||||||
.select_spendable_sapling_notes(account, target_value, anchor_height)
|
.select_spendable_sapling_notes(account, target_value, anchor_height, exclude)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_transparent_receivers(
|
fn get_transparent_receivers(
|
||||||
|
@ -401,9 +415,10 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
|
||||||
&self,
|
&self,
|
||||||
address: &TransparentAddress,
|
address: &TransparentAddress,
|
||||||
max_height: BlockHeight,
|
max_height: BlockHeight,
|
||||||
|
exclude: &[OutPoint],
|
||||||
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
||||||
self.wallet_db
|
self.wallet_db
|
||||||
.get_unspent_transparent_outputs(address, max_height)
|
.get_unspent_transparent_outputs(address, max_height, exclude)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_transparent_balances(
|
fn get_transparent_balances(
|
||||||
|
@ -745,9 +760,9 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
||||||
return wallet::put_received_transparent_utxo(self, _output);
|
return wallet::put_received_transparent_utxo(self, _output);
|
||||||
|
|
||||||
#[cfg(not(feature = "transparent-inputs"))]
|
#[cfg(not(feature = "transparent-inputs"))]
|
||||||
return Err(SqliteClientError::BackendError(
|
panic!(
|
||||||
Error::TransparentInputsNotSupported,
|
"The wallet must be compiled with the transparent-inputs feature to use this method."
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -764,14 +779,17 @@ impl BlockDb {
|
||||||
impl BlockSource for BlockDb {
|
impl BlockSource for BlockDb {
|
||||||
type Error = SqliteClientError;
|
type Error = SqliteClientError;
|
||||||
|
|
||||||
fn with_blocks<F>(
|
fn with_blocks<F, DbErrT, NoteRef>(
|
||||||
&self,
|
&self,
|
||||||
from_height: BlockHeight,
|
from_height: BlockHeight,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
with_row: F,
|
with_row: F,
|
||||||
) -> Result<(), Self::Error>
|
) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error, NoteRef>>
|
||||||
where
|
where
|
||||||
F: FnMut(CompactBlock) -> Result<(), Self::Error>,
|
F: FnMut(
|
||||||
|
CompactBlock,
|
||||||
|
)
|
||||||
|
-> Result<(), data_api::chain::error::Error<DbErrT, Self::Error, NoteRef>>,
|
||||||
{
|
{
|
||||||
chain::blockdb_with_blocks(self, from_height, limit, with_row)
|
chain::blockdb_with_blocks(self, from_height, limit, with_row)
|
||||||
}
|
}
|
||||||
|
@ -788,7 +806,7 @@ impl BlockSource for BlockDb {
|
||||||
///
|
///
|
||||||
/// where `<block_height>` is the decimal value of the height at which the block was mined, and
|
/// where `<block_height>` is the decimal value of the height at which the block was mined, and
|
||||||
/// `<block_hash>` is the hexadecimal representation of the block hash, as produced by the
|
/// `<block_hash>` is the hexadecimal representation of the block hash, as produced by the
|
||||||
/// [`Display`] implementation for [`zcash_primitives::block::BlockHash`].
|
/// [`fmt::Display`] implementation for [`zcash_primitives::block::BlockHash`].
|
||||||
///
|
///
|
||||||
/// This block source is intended to be used with the following data flow:
|
/// This block source is intended to be used with the following data flow:
|
||||||
/// * When the cache is being filled:
|
/// * When the cache is being filled:
|
||||||
|
@ -828,6 +846,7 @@ pub struct FsBlockDb {
|
||||||
pub enum FsBlockDbError {
|
pub enum FsBlockDbError {
|
||||||
FsError(io::Error),
|
FsError(io::Error),
|
||||||
DbError(rusqlite::Error),
|
DbError(rusqlite::Error),
|
||||||
|
Protobuf(prost::DecodeError),
|
||||||
InvalidBlockstoreRoot(PathBuf),
|
InvalidBlockstoreRoot(PathBuf),
|
||||||
InvalidBlockPath(PathBuf),
|
InvalidBlockPath(PathBuf),
|
||||||
CorruptedData(String),
|
CorruptedData(String),
|
||||||
|
@ -847,6 +866,13 @@ impl From<rusqlite::Error> for FsBlockDbError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "unstable")]
|
||||||
|
impl From<prost::DecodeError> for FsBlockDbError {
|
||||||
|
fn from(e: prost::DecodeError) -> Self {
|
||||||
|
FsBlockDbError::Protobuf(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "unstable")]
|
#[cfg(feature = "unstable")]
|
||||||
impl FsBlockDb {
|
impl FsBlockDb {
|
||||||
/// Creates a filesystem-backed block store at the given path.
|
/// Creates a filesystem-backed block store at the given path.
|
||||||
|
@ -896,21 +922,28 @@ impl FsBlockDb {
|
||||||
|
|
||||||
#[cfg(feature = "unstable")]
|
#[cfg(feature = "unstable")]
|
||||||
impl BlockSource for FsBlockDb {
|
impl BlockSource for FsBlockDb {
|
||||||
type Error = SqliteClientError;
|
type Error = FsBlockDbError;
|
||||||
|
|
||||||
fn with_blocks<F>(
|
fn with_blocks<F, DbErrT, NoteRef>(
|
||||||
&self,
|
&self,
|
||||||
from_height: BlockHeight,
|
from_height: BlockHeight,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
with_row: F,
|
with_row: F,
|
||||||
) -> Result<(), Self::Error>
|
) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error, NoteRef>>
|
||||||
where
|
where
|
||||||
F: FnMut(CompactBlock) -> Result<(), Self::Error>,
|
F: FnMut(
|
||||||
|
CompactBlock,
|
||||||
|
)
|
||||||
|
-> Result<(), data_api::chain::error::Error<DbErrT, Self::Error, NoteRef>>,
|
||||||
{
|
{
|
||||||
fsblockdb_with_blocks(self, from_height, limit, with_row)
|
fsblockdb_with_blocks(self, from_height, limit, with_row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[macro_use]
|
||||||
|
extern crate assert_matches;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
@ -27,7 +27,7 @@ use zcash_primitives::{
|
||||||
|
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
address::{RecipientAddress, UnifiedAddress},
|
address::{RecipientAddress, UnifiedAddress},
|
||||||
data_api::{error::Error, PoolType, Recipient, SentTransactionOutput},
|
data_api::{PoolType, Recipient, SentTransactionOutput},
|
||||||
keys::UnifiedFullViewingKey,
|
keys::UnifiedFullViewingKey,
|
||||||
wallet::{WalletShieldedOutput, WalletTx},
|
wallet::{WalletShieldedOutput, WalletTx},
|
||||||
DecryptedOutput,
|
DecryptedOutput,
|
||||||
|
@ -42,6 +42,7 @@ use crate::{
|
||||||
use {
|
use {
|
||||||
crate::UtxoId,
|
crate::UtxoId,
|
||||||
rusqlite::{params, Connection},
|
rusqlite::{params, Connection},
|
||||||
|
std::collections::BTreeSet,
|
||||||
zcash_client_backend::{
|
zcash_client_backend::{
|
||||||
address::AddressMetadata, encoding::AddressCodec, wallet::WalletTransparentOutput,
|
address::AddressMetadata, encoding::AddressCodec, wallet::WalletTransparentOutput,
|
||||||
},
|
},
|
||||||
|
@ -745,7 +746,7 @@ pub(crate) fn rewind_to_height<P: consensus::Parameters>(
|
||||||
let sapling_activation_height = wdb
|
let sapling_activation_height = wdb
|
||||||
.params
|
.params
|
||||||
.activation_height(NetworkUpgrade::Sapling)
|
.activation_height(NetworkUpgrade::Sapling)
|
||||||
.ok_or(SqliteClientError::BackendError(Error::SaplingNotActive))?;
|
.expect("Sapling activation height mutst be available.");
|
||||||
|
|
||||||
// Recall where we synced up to previously.
|
// Recall where we synced up to previously.
|
||||||
let last_scanned_height = wdb
|
let last_scanned_height = wdb
|
||||||
|
@ -955,6 +956,7 @@ pub(crate) fn get_unspent_transparent_outputs<P: consensus::Parameters>(
|
||||||
wdb: &WalletDb<P>,
|
wdb: &WalletDb<P>,
|
||||||
address: &TransparentAddress,
|
address: &TransparentAddress,
|
||||||
max_height: BlockHeight,
|
max_height: BlockHeight,
|
||||||
|
exclude: &[OutPoint],
|
||||||
) -> Result<Vec<WalletTransparentOutput>, SqliteClientError> {
|
) -> Result<Vec<WalletTransparentOutput>, SqliteClientError> {
|
||||||
let mut stmt_blocks = wdb.conn.prepare(
|
let mut stmt_blocks = wdb.conn.prepare(
|
||||||
"SELECT u.prevout_txid, u.prevout_idx, u.script,
|
"SELECT u.prevout_txid, u.prevout_idx, u.script,
|
||||||
|
@ -971,6 +973,7 @@ pub(crate) fn get_unspent_transparent_outputs<P: consensus::Parameters>(
|
||||||
|
|
||||||
let mut utxos = Vec::<WalletTransparentOutput>::new();
|
let mut utxos = Vec::<WalletTransparentOutput>::new();
|
||||||
let mut rows = stmt_blocks.query(params![addr_str, u32::from(max_height)])?;
|
let mut rows = stmt_blocks.query(params![addr_str, u32::from(max_height)])?;
|
||||||
|
let excluded: BTreeSet<OutPoint> = exclude.iter().cloned().collect();
|
||||||
while let Some(row) = rows.next()? {
|
while let Some(row) = rows.next()? {
|
||||||
let txid: Vec<u8> = row.get(0)?;
|
let txid: Vec<u8> = row.get(0)?;
|
||||||
let mut txid_bytes = [0u8; 32];
|
let mut txid_bytes = [0u8; 32];
|
||||||
|
@ -981,8 +984,13 @@ pub(crate) fn get_unspent_transparent_outputs<P: consensus::Parameters>(
|
||||||
let value = Amount::from_i64(row.get(3)?).unwrap();
|
let value = Amount::from_i64(row.get(3)?).unwrap();
|
||||||
let height: u32 = row.get(4)?;
|
let height: u32 = row.get(4)?;
|
||||||
|
|
||||||
|
let outpoint = OutPoint::new(txid_bytes, index);
|
||||||
|
if excluded.contains(&outpoint) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let output = WalletTransparentOutput::from_parts(
|
let output = WalletTransparentOutput::from_parts(
|
||||||
OutPoint::new(txid_bytes, index),
|
outpoint,
|
||||||
TxOut {
|
TxOut {
|
||||||
value,
|
value,
|
||||||
script_pubkey,
|
script_pubkey,
|
||||||
|
@ -1394,7 +1402,8 @@ mod tests {
|
||||||
super::get_unspent_transparent_outputs(
|
super::get_unspent_transparent_outputs(
|
||||||
&db_data,
|
&db_data,
|
||||||
taddr,
|
taddr,
|
||||||
BlockHeight::from_u32(12345)
|
BlockHeight::from_u32(12345),
|
||||||
|
&[]
|
||||||
),
|
),
|
||||||
Ok(utxos) if utxos.is_empty()
|
Ok(utxos) if utxos.is_empty()
|
||||||
));
|
));
|
||||||
|
@ -1403,7 +1412,8 @@ mod tests {
|
||||||
super::get_unspent_transparent_outputs(
|
super::get_unspent_transparent_outputs(
|
||||||
&db_data,
|
&db_data,
|
||||||
taddr,
|
taddr,
|
||||||
BlockHeight::from_u32(34567)
|
BlockHeight::from_u32(34567),
|
||||||
|
&[]
|
||||||
),
|
),
|
||||||
Ok(utxos) if {
|
Ok(utxos) if {
|
||||||
utxos.len() == 1 &&
|
utxos.len() == 1 &&
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
//! Functions for creating transactions.
|
//! Functions for creating transactions.
|
||||||
//!
|
//!
|
||||||
use rusqlite::{named_params, Row};
|
use rusqlite::{named_params, types::Value, Row};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use group::ff::PrimeField;
|
use group::ff::PrimeField;
|
||||||
|
|
||||||
|
@ -14,11 +15,12 @@ use zcash_primitives::{
|
||||||
|
|
||||||
use zcash_client_backend::wallet::SpendableNote;
|
use zcash_client_backend::wallet::SpendableNote;
|
||||||
|
|
||||||
use crate::{error::SqliteClientError, WalletDb};
|
use crate::{error::SqliteClientError, NoteId, WalletDb};
|
||||||
|
|
||||||
fn to_spendable_note(row: &Row) -> Result<SpendableNote, SqliteClientError> {
|
fn to_spendable_note(row: &Row) -> Result<SpendableNote<NoteId>, SqliteClientError> {
|
||||||
|
let note_id = NoteId::ReceivedNoteId(row.get(0)?);
|
||||||
let diversifier = {
|
let diversifier = {
|
||||||
let d: Vec<_> = row.get(0)?;
|
let d: Vec<_> = row.get(1)?;
|
||||||
if d.len() != 11 {
|
if d.len() != 11 {
|
||||||
return Err(SqliteClientError::CorruptedData(
|
return Err(SqliteClientError::CorruptedData(
|
||||||
"Invalid diversifier length".to_string(),
|
"Invalid diversifier length".to_string(),
|
||||||
|
@ -29,10 +31,10 @@ fn to_spendable_note(row: &Row) -> Result<SpendableNote, SqliteClientError> {
|
||||||
Diversifier(tmp)
|
Diversifier(tmp)
|
||||||
};
|
};
|
||||||
|
|
||||||
let note_value = Amount::from_i64(row.get(1)?).unwrap();
|
let note_value = Amount::from_i64(row.get(2)?).unwrap();
|
||||||
|
|
||||||
let rseed = {
|
let rseed = {
|
||||||
let rcm_bytes: Vec<_> = row.get(2)?;
|
let rcm_bytes: Vec<_> = row.get(3)?;
|
||||||
|
|
||||||
// We store rcm directly in the data DB, regardless of whether the note
|
// We store rcm directly in the data DB, regardless of whether the note
|
||||||
// used a v1 or v2 note plaintext, so for the purposes of spending let's
|
// used a v1 or v2 note plaintext, so for the purposes of spending let's
|
||||||
|
@ -47,11 +49,12 @@ fn to_spendable_note(row: &Row) -> Result<SpendableNote, SqliteClientError> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let witness = {
|
let witness = {
|
||||||
let d: Vec<_> = row.get(3)?;
|
let d: Vec<_> = row.get(4)?;
|
||||||
IncrementalWitness::read(&d[..])?
|
IncrementalWitness::read(&d[..])?
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(SpendableNote {
|
Ok(SpendableNote {
|
||||||
|
note_id,
|
||||||
diversifier,
|
diversifier,
|
||||||
note_value,
|
note_value,
|
||||||
rseed,
|
rseed,
|
||||||
|
@ -66,23 +69,34 @@ pub fn get_spendable_sapling_notes<P>(
|
||||||
wdb: &WalletDb<P>,
|
wdb: &WalletDb<P>,
|
||||||
account: AccountId,
|
account: AccountId,
|
||||||
anchor_height: BlockHeight,
|
anchor_height: BlockHeight,
|
||||||
) -> Result<Vec<SpendableNote>, SqliteClientError> {
|
exclude: &[NoteId],
|
||||||
|
) -> Result<Vec<SpendableNote<NoteId>>, SqliteClientError> {
|
||||||
let mut stmt_select_notes = wdb.conn.prepare(
|
let mut stmt_select_notes = wdb.conn.prepare(
|
||||||
"SELECT diversifier, value, rcm, witness
|
"SELECT id_note, diversifier, value, rcm, witness
|
||||||
FROM received_notes
|
FROM received_notes
|
||||||
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
||||||
INNER JOIN sapling_witnesses ON sapling_witnesses.note = received_notes.id_note
|
INNER JOIN sapling_witnesses ON sapling_witnesses.note = received_notes.id_note
|
||||||
WHERE account = :account
|
WHERE account = :account
|
||||||
AND spent IS NULL
|
AND spent IS NULL
|
||||||
AND transactions.block <= :anchor_height
|
AND transactions.block <= :anchor_height
|
||||||
AND sapling_witnesses.block = :anchor_height",
|
AND sapling_witnesses.block = :anchor_height
|
||||||
|
AND id_note NOT IN rarray(:exclude)",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Select notes
|
let excluded: Vec<Value> = exclude
|
||||||
|
.iter()
|
||||||
|
.filter_map(|n| match n {
|
||||||
|
NoteId::ReceivedNoteId(i) => Some(Value::from(*i)),
|
||||||
|
NoteId::SentNoteId(_) => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let excluded_ptr = Rc::new(excluded);
|
||||||
|
|
||||||
let notes = stmt_select_notes.query_and_then(
|
let notes = stmt_select_notes.query_and_then(
|
||||||
named_params![
|
named_params![
|
||||||
":account": &u32::from(account),
|
":account": &u32::from(account),
|
||||||
":anchor_height": &u32::from(anchor_height),
|
":anchor_height": &u32::from(anchor_height),
|
||||||
|
":exclude": &excluded_ptr,
|
||||||
],
|
],
|
||||||
to_spendable_note,
|
to_spendable_note,
|
||||||
)?;
|
)?;
|
||||||
|
@ -98,7 +112,8 @@ pub fn select_spendable_sapling_notes<P>(
|
||||||
account: AccountId,
|
account: AccountId,
|
||||||
target_value: Amount,
|
target_value: Amount,
|
||||||
anchor_height: BlockHeight,
|
anchor_height: BlockHeight,
|
||||||
) -> Result<Vec<SpendableNote>, SqliteClientError> {
|
exclude: &[NoteId],
|
||||||
|
) -> Result<Vec<SpendableNote<NoteId>>, SqliteClientError> {
|
||||||
// The goal of this SQL statement is to select the oldest notes until the required
|
// The goal of this SQL statement is to select the oldest notes until the required
|
||||||
// value has been reached, and then fetch the witnesses at the desired height for the
|
// value has been reached, and then fetch the witnesses at the desired height for the
|
||||||
// selected notes. This is achieved in several steps:
|
// selected notes. This is achieved in several steps:
|
||||||
|
@ -125,7 +140,10 @@ pub fn select_spendable_sapling_notes<P>(
|
||||||
(PARTITION BY account, spent ORDER BY id_note) AS so_far
|
(PARTITION BY account, spent ORDER BY id_note) AS so_far
|
||||||
FROM received_notes
|
FROM received_notes
|
||||||
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
||||||
WHERE account = :account AND spent IS NULL AND transactions.block <= :anchor_height
|
WHERE account = :account
|
||||||
|
AND spent IS NULL
|
||||||
|
AND transactions.block <= :anchor_height
|
||||||
|
AND id_note NOT IN rarray(:exclude)
|
||||||
)
|
)
|
||||||
SELECT * FROM eligible WHERE so_far < :target_value
|
SELECT * FROM eligible WHERE so_far < :target_value
|
||||||
UNION
|
UNION
|
||||||
|
@ -134,17 +152,26 @@ pub fn select_spendable_sapling_notes<P>(
|
||||||
SELECT note, witness FROM sapling_witnesses
|
SELECT note, witness FROM sapling_witnesses
|
||||||
WHERE block = :anchor_height
|
WHERE block = :anchor_height
|
||||||
)
|
)
|
||||||
SELECT selected.diversifier, selected.value, selected.rcm, witnesses.witness
|
SELECT selected.id_note, selected.diversifier, selected.value, selected.rcm, witnesses.witness
|
||||||
FROM selected
|
FROM selected
|
||||||
INNER JOIN witnesses ON selected.id_note = witnesses.note",
|
INNER JOIN witnesses ON selected.id_note = witnesses.note",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Select notes
|
let excluded: Vec<Value> = exclude
|
||||||
|
.iter()
|
||||||
|
.filter_map(|n| match n {
|
||||||
|
NoteId::ReceivedNoteId(i) => Some(Value::from(*i)),
|
||||||
|
NoteId::SentNoteId(_) => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let excluded_ptr = Rc::new(excluded);
|
||||||
|
|
||||||
let notes = stmt_select_notes.query_and_then(
|
let notes = stmt_select_notes.query_and_then(
|
||||||
named_params![
|
named_params![
|
||||||
":account": &u32::from(account),
|
":account": &u32::from(account),
|
||||||
":anchor_height": &u32::from(anchor_height),
|
":anchor_height": &u32::from(anchor_height),
|
||||||
":target_value": &i64::from(target_value),
|
":target_value": &i64::from(target_value),
|
||||||
|
":exclude": &excluded_ptr
|
||||||
],
|
],
|
||||||
to_spendable_note,
|
to_spendable_note,
|
||||||
)?;
|
)?;
|
||||||
|
@ -166,17 +193,23 @@ mod tests {
|
||||||
consensus::{BlockHeight, BranchId},
|
consensus::{BlockHeight, BranchId},
|
||||||
legacy::TransparentAddress,
|
legacy::TransparentAddress,
|
||||||
sapling::{note_encryption::try_sapling_output_recovery, prover::TxProver},
|
sapling::{note_encryption::try_sapling_output_recovery, prover::TxProver},
|
||||||
transaction::{components::Amount, Transaction},
|
transaction::{components::Amount, fees::zip317::FeeRule as Zip317FeeRule, Transaction},
|
||||||
zip32::sapling::ExtendedSpendingKey,
|
zip32::sapling::ExtendedSpendingKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
|
address::RecipientAddress,
|
||||||
data_api::{
|
data_api::{
|
||||||
self, chain::scan_cached_blocks, wallet::create_spend_to_address, WalletRead,
|
self,
|
||||||
WalletWrite,
|
chain::scan_cached_blocks,
|
||||||
|
error::Error,
|
||||||
|
wallet::{create_spend_to_address, input_selection::GreedyInputSelector, spend},
|
||||||
|
WalletRead, WalletWrite,
|
||||||
},
|
},
|
||||||
|
fees::{zip317, DustOutputPolicy},
|
||||||
keys::UnifiedSpendingKey,
|
keys::UnifiedSpendingKey,
|
||||||
wallet::OvkPolicy,
|
wallet::OvkPolicy,
|
||||||
|
zip321::{Payment, TransactionRequest},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -220,7 +253,7 @@ mod tests {
|
||||||
|
|
||||||
// Attempting to spend with a USK that is not in the wallet results in an error
|
// Attempting to spend with a USK that is not in the wallet results in an error
|
||||||
let mut db_write = db_data.get_update_ops().unwrap();
|
let mut db_write = db_data.get_update_ops().unwrap();
|
||||||
assert!(matches!(
|
assert_matches!(
|
||||||
create_spend_to_address(
|
create_spend_to_address(
|
||||||
&mut db_write,
|
&mut db_write,
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
|
@ -232,10 +265,8 @@ mod tests {
|
||||||
OvkPolicy::Sender,
|
OvkPolicy::Sender,
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
Err(crate::SqliteClientError::BackendError(
|
Err(data_api::error::Error::KeyNotRecognized)
|
||||||
data_api::error::Error::KeyNotRecognized
|
);
|
||||||
))
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -253,7 +284,7 @@ mod tests {
|
||||||
|
|
||||||
// We cannot do anything if we aren't synchronised
|
// We cannot do anything if we aren't synchronised
|
||||||
let mut db_write = db_data.get_update_ops().unwrap();
|
let mut db_write = db_data.get_update_ops().unwrap();
|
||||||
assert!(matches!(
|
assert_matches!(
|
||||||
create_spend_to_address(
|
create_spend_to_address(
|
||||||
&mut db_write,
|
&mut db_write,
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
|
@ -265,10 +296,8 @@ mod tests {
|
||||||
OvkPolicy::Sender,
|
OvkPolicy::Sender,
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
Err(crate::SqliteClientError::BackendError(
|
Err(data_api::error::Error::ScanRequired)
|
||||||
data_api::error::Error::ScanRequired
|
);
|
||||||
))
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -300,7 +329,7 @@ mod tests {
|
||||||
|
|
||||||
// We cannot spend anything
|
// We cannot spend anything
|
||||||
let mut db_write = db_data.get_update_ops().unwrap();
|
let mut db_write = db_data.get_update_ops().unwrap();
|
||||||
assert!(matches!(
|
assert_matches!(
|
||||||
create_spend_to_address(
|
create_spend_to_address(
|
||||||
&mut db_write,
|
&mut db_write,
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
|
@ -312,14 +341,12 @@ mod tests {
|
||||||
OvkPolicy::Sender,
|
OvkPolicy::Sender,
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
Err(crate::SqliteClientError::BackendError(
|
Err(data_api::error::Error::InsufficientFunds {
|
||||||
data_api::error::Error::InsufficientBalance(
|
available,
|
||||||
available,
|
required
|
||||||
required
|
})
|
||||||
)
|
|
||||||
))
|
|
||||||
if available == Amount::zero() && required == Amount::from_u64(1001).unwrap()
|
if available == Amount::zero() && required == Amount::from_u64(1001).unwrap()
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -384,7 +411,7 @@ mod tests {
|
||||||
// Spend fails because there are insufficient verified notes
|
// Spend fails because there are insufficient verified notes
|
||||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||||
let to = extsk2.default_address().1.into();
|
let to = extsk2.default_address().1.into();
|
||||||
assert!(matches!(
|
assert_matches!(
|
||||||
create_spend_to_address(
|
create_spend_to_address(
|
||||||
&mut db_write,
|
&mut db_write,
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
|
@ -396,15 +423,13 @@ mod tests {
|
||||||
OvkPolicy::Sender,
|
OvkPolicy::Sender,
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
Err(crate::SqliteClientError::BackendError(
|
Err(data_api::error::Error::InsufficientFunds {
|
||||||
data_api::error::Error::InsufficientBalance(
|
available,
|
||||||
available,
|
required
|
||||||
required
|
})
|
||||||
)
|
|
||||||
))
|
|
||||||
if available == Amount::from_u64(50000).unwrap()
|
if available == Amount::from_u64(50000).unwrap()
|
||||||
&& required == Amount::from_u64(71000).unwrap()
|
&& required == Amount::from_u64(71000).unwrap()
|
||||||
));
|
);
|
||||||
|
|
||||||
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second
|
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second
|
||||||
// note is verified
|
// note is verified
|
||||||
|
@ -421,7 +446,7 @@ mod tests {
|
||||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||||
|
|
||||||
// Second spend still fails
|
// Second spend still fails
|
||||||
assert!(matches!(
|
assert_matches!(
|
||||||
create_spend_to_address(
|
create_spend_to_address(
|
||||||
&mut db_write,
|
&mut db_write,
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
|
@ -433,15 +458,13 @@ mod tests {
|
||||||
OvkPolicy::Sender,
|
OvkPolicy::Sender,
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
Err(crate::SqliteClientError::BackendError(
|
Err(data_api::error::Error::InsufficientFunds {
|
||||||
data_api::error::Error::InsufficientBalance(
|
available,
|
||||||
available,
|
required
|
||||||
required
|
})
|
||||||
)
|
|
||||||
))
|
|
||||||
if available == Amount::from_u64(50000).unwrap()
|
if available == Amount::from_u64(50000).unwrap()
|
||||||
&& required == Amount::from_u64(71000).unwrap()
|
&& required == Amount::from_u64(71000).unwrap()
|
||||||
));
|
);
|
||||||
|
|
||||||
// Mine block 11 so that the second note becomes verified
|
// Mine block 11 so that the second note becomes verified
|
||||||
let (cb, _) = fake_compact_block(
|
let (cb, _) = fake_compact_block(
|
||||||
|
@ -455,7 +478,7 @@ mod tests {
|
||||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||||
|
|
||||||
// Second spend should now succeed
|
// Second spend should now succeed
|
||||||
assert!(matches!(
|
assert_matches!(
|
||||||
create_spend_to_address(
|
create_spend_to_address(
|
||||||
&mut db_write,
|
&mut db_write,
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
|
@ -468,7 +491,7 @@ mod tests {
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
Ok(_)
|
Ok(_)
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -504,7 +527,7 @@ mod tests {
|
||||||
// Send some of the funds to another address
|
// Send some of the funds to another address
|
||||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||||
let to = extsk2.default_address().1.into();
|
let to = extsk2.default_address().1.into();
|
||||||
assert!(matches!(
|
assert_matches!(
|
||||||
create_spend_to_address(
|
create_spend_to_address(
|
||||||
&mut db_write,
|
&mut db_write,
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
|
@ -517,10 +540,10 @@ mod tests {
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
Ok(_)
|
Ok(_)
|
||||||
));
|
);
|
||||||
|
|
||||||
// A second spend fails because there are no usable notes
|
// A second spend fails because there are no usable notes
|
||||||
assert!(matches!(
|
assert_matches!(
|
||||||
create_spend_to_address(
|
create_spend_to_address(
|
||||||
&mut db_write,
|
&mut db_write,
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
|
@ -532,14 +555,12 @@ mod tests {
|
||||||
OvkPolicy::Sender,
|
OvkPolicy::Sender,
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
Err(crate::SqliteClientError::BackendError(
|
Err(data_api::error::Error::InsufficientFunds {
|
||||||
data_api::error::Error::InsufficientBalance(
|
available,
|
||||||
available,
|
required
|
||||||
required
|
})
|
||||||
)
|
|
||||||
))
|
|
||||||
if available == Amount::zero() && required == Amount::from_u64(3000).unwrap()
|
if available == Amount::zero() && required == Amount::from_u64(3000).unwrap()
|
||||||
));
|
);
|
||||||
|
|
||||||
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 21 (that don't send us funds)
|
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 21 (that don't send us funds)
|
||||||
// until just before the first transaction expires
|
// until just before the first transaction expires
|
||||||
|
@ -556,7 +577,7 @@ mod tests {
|
||||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||||
|
|
||||||
// Second spend still fails
|
// Second spend still fails
|
||||||
assert!(matches!(
|
assert_matches!(
|
||||||
create_spend_to_address(
|
create_spend_to_address(
|
||||||
&mut db_write,
|
&mut db_write,
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
|
@ -568,14 +589,12 @@ mod tests {
|
||||||
OvkPolicy::Sender,
|
OvkPolicy::Sender,
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
Err(crate::SqliteClientError::BackendError(
|
Err(data_api::error::Error::InsufficientFunds {
|
||||||
data_api::error::Error::InsufficientBalance(
|
available,
|
||||||
available,
|
required
|
||||||
required
|
})
|
||||||
)
|
|
||||||
))
|
|
||||||
if available == Amount::zero() && required == Amount::from_u64(3000).unwrap()
|
if available == Amount::zero() && required == Amount::from_u64(3000).unwrap()
|
||||||
));
|
);
|
||||||
|
|
||||||
// Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires
|
// Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires
|
||||||
let (cb, _) = fake_compact_block(
|
let (cb, _) = fake_compact_block(
|
||||||
|
@ -804,7 +823,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let to = TransparentAddress::PublicKey([7; 20]).into();
|
let to = TransparentAddress::PublicKey([7; 20]).into();
|
||||||
assert!(matches!(
|
assert_matches!(
|
||||||
create_spend_to_address(
|
create_spend_to_address(
|
||||||
&mut db_write,
|
&mut db_write,
|
||||||
&tests::network(),
|
&tests::network(),
|
||||||
|
@ -817,6 +836,115 @@ mod tests {
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
Ok(_)
|
Ok(_)
|
||||||
));
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zip317_spend() {
|
||||||
|
let cache_file = NamedTempFile::new().unwrap();
|
||||||
|
let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap());
|
||||||
|
init_cache_database(&db_cache).unwrap();
|
||||||
|
|
||||||
|
let data_file = NamedTempFile::new().unwrap();
|
||||||
|
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
||||||
|
init_wallet_db(&mut db_data, None).unwrap();
|
||||||
|
|
||||||
|
// Add an account to the wallet
|
||||||
|
let mut ops = db_data.get_update_ops().unwrap();
|
||||||
|
let seed = Secret::new([0u8; 32].to_vec());
|
||||||
|
let (_, usk) = ops.create_account(&seed).unwrap();
|
||||||
|
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||||
|
|
||||||
|
// Add funds to the wallet
|
||||||
|
let (cb, _) = fake_compact_block(
|
||||||
|
sapling_activation_height(),
|
||||||
|
BlockHash([0; 32]),
|
||||||
|
&dfvk,
|
||||||
|
AddressType::Internal,
|
||||||
|
Amount::from_u64(50000).unwrap(),
|
||||||
|
);
|
||||||
|
insert_into_cache(&db_cache, &cb);
|
||||||
|
|
||||||
|
// Add 10 dust notes to the wallet
|
||||||
|
for i in 1..=10 {
|
||||||
|
let (cb, _) = fake_compact_block(
|
||||||
|
sapling_activation_height() + i,
|
||||||
|
cb.hash(),
|
||||||
|
&dfvk,
|
||||||
|
AddressType::DefaultExternal,
|
||||||
|
Amount::from_u64(1000).unwrap(),
|
||||||
|
);
|
||||||
|
insert_into_cache(&db_cache, &cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut db_write = db_data.get_update_ops().unwrap();
|
||||||
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||||
|
|
||||||
|
// Verified balance matches total balance
|
||||||
|
let total = Amount::from_u64(60000).unwrap();
|
||||||
|
let (_, anchor_height) = db_data.get_target_and_anchor_heights(1).unwrap().unwrap();
|
||||||
|
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), total);
|
||||||
|
assert_eq!(
|
||||||
|
get_balance_at(&db_data, AccountId::from(0), anchor_height).unwrap(),
|
||||||
|
total
|
||||||
|
);
|
||||||
|
|
||||||
|
let input_selector = GreedyInputSelector::new(
|
||||||
|
zip317::SingleOutputChangeStrategy::new(Zip317FeeRule::standard()),
|
||||||
|
DustOutputPolicy::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// This first request will fail due to insufficient non-dust funds
|
||||||
|
let req = TransactionRequest::new(vec![Payment {
|
||||||
|
recipient_address: RecipientAddress::Shielded(dfvk.default_address().1),
|
||||||
|
amount: Amount::from_u64(50000).unwrap(),
|
||||||
|
memo: None,
|
||||||
|
label: None,
|
||||||
|
message: None,
|
||||||
|
other_params: vec![],
|
||||||
|
}])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
spend(
|
||||||
|
&mut db_write,
|
||||||
|
&tests::network(),
|
||||||
|
test_prover(),
|
||||||
|
&input_selector,
|
||||||
|
&usk,
|
||||||
|
req,
|
||||||
|
OvkPolicy::Sender,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
Err(Error::InsufficientFunds { available, required })
|
||||||
|
if available == Amount::from_u64(51000).unwrap()
|
||||||
|
&& required == Amount::from_u64(60000).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// This request will succeed, spending a single dust input to pay the 10000
|
||||||
|
// ZAT fee in addition to the 41000 ZAT output to the recipient
|
||||||
|
let req = TransactionRequest::new(vec![Payment {
|
||||||
|
recipient_address: RecipientAddress::Shielded(dfvk.default_address().1),
|
||||||
|
amount: Amount::from_u64(41000).unwrap(),
|
||||||
|
memo: None,
|
||||||
|
label: None,
|
||||||
|
message: None,
|
||||||
|
other_params: vec![],
|
||||||
|
}])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
spend(
|
||||||
|
&mut db_write,
|
||||||
|
&tests::network(),
|
||||||
|
test_prover(),
|
||||||
|
&input_selector,
|
||||||
|
&usk,
|
||||||
|
req,
|
||||||
|
OvkPolicy::Sender,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
Ok(_)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -492,10 +492,10 @@ mod tests {
|
||||||
transaction::{
|
transaction::{
|
||||||
builder::Builder,
|
builder::Builder,
|
||||||
components::{
|
components::{
|
||||||
amount::{Amount, DEFAULT_FEE},
|
amount::Amount,
|
||||||
tze::{Authorized, Bundle, OutPoint, TzeIn, TzeOut},
|
tze::{Authorized, Bundle, OutPoint, TzeIn, TzeOut},
|
||||||
},
|
},
|
||||||
fees::FixedFeeRule,
|
fees::fixed,
|
||||||
Transaction, TransactionData, TxVersion,
|
Transaction, TransactionData, TxVersion,
|
||||||
},
|
},
|
||||||
zip32::ExtendedSpendingKey,
|
zip32::ExtendedSpendingKey,
|
||||||
|
@ -809,7 +809,7 @@ mod tests {
|
||||||
//
|
//
|
||||||
|
|
||||||
let mut rng = OsRng;
|
let mut rng = OsRng;
|
||||||
let fee_rule = FixedFeeRule::new(DEFAULT_FEE);
|
let fee_rule = fixed::FeeRule::standard();
|
||||||
|
|
||||||
// create some inputs to spend
|
// create some inputs to spend
|
||||||
let extsk = ExtendedSpendingKey::master(&[]);
|
let extsk = ExtendedSpendingKey::master(&[]);
|
||||||
|
@ -848,7 +848,7 @@ mod tests {
|
||||||
|
|
||||||
let mut builder_b = demo_builder(tx_height + 1);
|
let mut builder_b = demo_builder(tx_height + 1);
|
||||||
let prevout_a = (OutPoint::new(tx_a.txid(), 0), tze_a.vout[0].clone());
|
let prevout_a = (OutPoint::new(tx_a.txid(), 0), tze_a.vout[0].clone());
|
||||||
let value_xfr = (value - DEFAULT_FEE).unwrap();
|
let value_xfr = (value - fee_rule.fixed_fee()).unwrap();
|
||||||
builder_b
|
builder_b
|
||||||
.demo_transfer_to_close(prevout_a, value_xfr, preimage_1, h2)
|
.demo_transfer_to_close(prevout_a, value_xfr, preimage_1, h2)
|
||||||
.map_err(|e| format!("transfer failure: {:?}", e))
|
.map_err(|e| format!("transfer failure: {:?}", e))
|
||||||
|
@ -874,7 +874,7 @@ mod tests {
|
||||||
builder_c
|
builder_c
|
||||||
.add_transparent_output(
|
.add_transparent_output(
|
||||||
&TransparentAddress::PublicKey([0; 20]),
|
&TransparentAddress::PublicKey([0; 20]),
|
||||||
(value_xfr - DEFAULT_FEE).unwrap(),
|
(value_xfr - fee_rule.fixed_fee()).unwrap(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,12 @@ and this library adheres to Rust's notion of
|
||||||
### Added
|
### Added
|
||||||
- Added in `zcash_primitives::zip32`
|
- Added in `zcash_primitives::zip32`
|
||||||
- An implementation of `TryFrom<DiversifierIndex>` for `u32`
|
- An implementation of `TryFrom<DiversifierIndex>` for `u32`
|
||||||
|
- `zcash_primitives::transaction::components::amount::NonNegativeAmount`
|
||||||
- Added to `zcash_primitives::transaction::builder`
|
- Added to `zcash_primitives::transaction::builder`
|
||||||
- `Error::InsufficientFunds`
|
- `Error::InsufficientFunds`
|
||||||
- `Error::ChangeRequired`
|
- `Error::ChangeRequired`
|
||||||
|
- `Error::Balance`
|
||||||
|
- `Error::Fee`
|
||||||
- `Builder` state accessor methods:
|
- `Builder` state accessor methods:
|
||||||
- `Builder::params()`
|
- `Builder::params()`
|
||||||
- `Builder::target_height()`
|
- `Builder::target_height()`
|
||||||
|
@ -24,6 +27,10 @@ and this library adheres to Rust's notion of
|
||||||
and types related to fee calculations.
|
and types related to fee calculations.
|
||||||
- `FeeRule` a trait that describes how to compute the fee required for a
|
- `FeeRule` a trait that describes how to compute the fee required for a
|
||||||
transaction given inputs and outputs to the transaction.
|
transaction given inputs and outputs to the transaction.
|
||||||
|
- `fixed`, a new module containing an implementation of the existing fixed
|
||||||
|
fee rule.
|
||||||
|
- `zip317`, a new module containing an implementation of the ZIP 317 fee
|
||||||
|
rules.
|
||||||
- Added to `zcash_primitives::transaction::components::sapling::builder`
|
- Added to `zcash_primitives::transaction::components::sapling::builder`
|
||||||
- `SaplingBuilder::{inputs, outputs}`: accessors for Sapling builder state.
|
- `SaplingBuilder::{inputs, outputs}`: accessors for Sapling builder state.
|
||||||
- `zcash_primitives::transaction::components::sapling::fees`
|
- `zcash_primitives::transaction::components::sapling::fees`
|
||||||
|
@ -33,14 +40,22 @@ and this library adheres to Rust's notion of
|
||||||
- `zcash_primitives::sapling::Note::commitment`
|
- `zcash_primitives::sapling::Note::commitment`
|
||||||
- Added to `zcash_primitives::zip32::sapling::DiversifiableFullViewingKey`
|
- Added to `zcash_primitives::zip32::sapling::DiversifiableFullViewingKey`
|
||||||
- `DiversifiableFullViewingKey::{diversified_address, diversified_change_address}`
|
- `DiversifiableFullViewingKey::{diversified_address, diversified_change_address}`
|
||||||
|
- `impl Eq for zcash_primitves::sapling::PaymentAddress`
|
||||||
|
- `impl {PartialOrd, Ord} for zcash_primitves::transaction::components::transparent::OutPoint`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- `zcash_primitives::transaction::builder::Builder::build` now takes a `FeeRule`
|
- `zcash_primitives::transaction::builder::Builder::build` now takes a `FeeRule`
|
||||||
argument which is used to compute the fee for the transaction as part of the
|
argument which is used to compute the fee for the transaction as part of the
|
||||||
build process.
|
build process.
|
||||||
|
- `zcash_primitives::transaction::builder::Builder::value_balance` now
|
||||||
|
returns `Result<Amount, BalanceError>` instead of `Option<Amount>`.
|
||||||
- `zcash_primitives::transaction::builder::Builder::{new, new_with_rng}` no
|
- `zcash_primitives::transaction::builder::Builder::{new, new_with_rng}` no
|
||||||
longer fixes the fee for transactions to 0.00001 ZEC; the builder instead
|
longer fixes the fee for transactions to 0.00001 ZEC; the builder instead
|
||||||
computes the fee using a `FeeRule` implementation at build time.
|
computes the fee using a `FeeRule` implementation at build time.
|
||||||
|
- `zcash_primitives::transaction::builder::Error` now is parameterized by the
|
||||||
|
types that can now be produced by fee calculation.
|
||||||
|
- `zcash_primitives::transaction::components::tze::builder::Builder::value_balance` now
|
||||||
|
returns `Result<Amount, BalanceError>` instead of `Option<Amount>`.
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
- `zcash_primitives::zip32::sapling::to_extended_full_viewing_key` Use
|
- `zcash_primitives::zip32::sapling::to_extended_full_viewing_key` Use
|
||||||
|
@ -55,6 +70,7 @@ and this library adheres to Rust's notion of
|
||||||
- Removed from `zcash_primitives::transaction::builder::Error`
|
- Removed from `zcash_primitives::transaction::builder::Error`
|
||||||
- `Error::ChangeIsNegative`
|
- `Error::ChangeIsNegative`
|
||||||
- `Error::NoChangeAddress`
|
- `Error::NoChangeAddress`
|
||||||
|
- `Error::InvalidAmount` (replaced by `Error::BalanceError`)
|
||||||
- `zcash_primitives::transaction::components::sapling::builder::SaplingBuilder::get_candidate_change_address`
|
- `zcash_primitives::transaction::components::sapling::builder::SaplingBuilder::get_candidate_change_address`
|
||||||
has been removed; change outputs must now be added by the caller.
|
has been removed; change outputs must now be added by the caller.
|
||||||
- The `From<&ExtendedSpendingKey>` instance for `ExtendedFullViewingKey` has been
|
- The `From<&ExtendedSpendingKey>` instance for `ExtendedFullViewingKey` has been
|
||||||
|
|
|
@ -280,6 +280,8 @@ impl PartialEq for PaymentAddress {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Eq for PaymentAddress {}
|
||||||
|
|
||||||
impl PaymentAddress {
|
impl PaymentAddress {
|
||||||
/// Constructs a PaymentAddress from a diversifier and a Jubjub point.
|
/// Constructs a PaymentAddress from a diversifier and a Jubjub point.
|
||||||
///
|
///
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
//! Structs for building transactions.
|
//! Structs for building transactions.
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::convert::Infallible;
|
|
||||||
use std::error;
|
use std::error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
|
@ -20,7 +19,7 @@ use crate::{
|
||||||
sapling::{prover::TxProver, Diversifier, Node, Note, PaymentAddress},
|
sapling::{prover::TxProver, Diversifier, Node, Note, PaymentAddress},
|
||||||
transaction::{
|
transaction::{
|
||||||
components::{
|
components::{
|
||||||
amount::Amount,
|
amount::{Amount, BalanceError},
|
||||||
sapling::{
|
sapling::{
|
||||||
self,
|
self,
|
||||||
builder::{SaplingBuilder, SaplingMetadata},
|
builder::{SaplingBuilder, SaplingMetadata},
|
||||||
|
@ -54,15 +53,17 @@ const DEFAULT_TX_EXPIRY_DELTA: u32 = 20;
|
||||||
|
|
||||||
/// Errors that can occur during transaction construction.
|
/// Errors that can occur during transaction construction.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Error {
|
pub enum Error<FeeError> {
|
||||||
/// Insufficient funds were provided to the transaction builder; the given
|
/// Insufficient funds were provided to the transaction builder; the given
|
||||||
/// additional amount is required in order to construct the transaction.
|
/// additional amount is required in order to construct the transaction.
|
||||||
InsufficientFunds(Amount),
|
InsufficientFunds(Amount),
|
||||||
/// The transaction has inputs in excess of outputs and fees; the user must
|
/// The transaction has inputs in excess of outputs and fees; the user must
|
||||||
/// add a change output.
|
/// add a change output.
|
||||||
ChangeRequired(Amount),
|
ChangeRequired(Amount),
|
||||||
|
/// An error occurred in computing the fees for a transaction.
|
||||||
|
Fee(FeeError),
|
||||||
/// An overflow or underflow occurred when computing value balances
|
/// An overflow or underflow occurred when computing value balances
|
||||||
InvalidAmount,
|
Balance(BalanceError),
|
||||||
/// An error occurred in constructing the transparent parts of a transaction.
|
/// An error occurred in constructing the transparent parts of a transaction.
|
||||||
TransparentBuild(transparent::builder::Error),
|
TransparentBuild(transparent::builder::Error),
|
||||||
/// An error occurred in constructing the Sapling parts of a transaction.
|
/// An error occurred in constructing the Sapling parts of a transaction.
|
||||||
|
@ -72,7 +73,7 @@ pub enum Error {
|
||||||
TzeBuild(tze::builder::Error),
|
TzeBuild(tze::builder::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl<FE: fmt::Display> fmt::Display for Error<FE> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Error::InsufficientFunds(amount) => write!(
|
Error::InsufficientFunds(amount) => write!(
|
||||||
|
@ -85,7 +86,8 @@ impl fmt::Display for Error {
|
||||||
"The transaction requires an additional change output of {:?} zatoshis",
|
"The transaction requires an additional change output of {:?} zatoshis",
|
||||||
amount
|
amount
|
||||||
),
|
),
|
||||||
Error::InvalidAmount => write!(f, "Invalid amount (overflow or underflow)"),
|
Error::Balance(e) => write!(f, "Invalid amount {:?}", e),
|
||||||
|
Error::Fee(e) => write!(f, "An error occurred in fee calculation: {}", e),
|
||||||
Error::TransparentBuild(err) => err.fmt(f),
|
Error::TransparentBuild(err) => err.fmt(f),
|
||||||
Error::SaplingBuild(err) => err.fmt(f),
|
Error::SaplingBuild(err) => err.fmt(f),
|
||||||
#[cfg(feature = "zfuture")]
|
#[cfg(feature = "zfuture")]
|
||||||
|
@ -94,11 +96,11 @@ impl fmt::Display for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl error::Error for Error {}
|
impl<FE: fmt::Debug + fmt::Display> error::Error for Error<FE> {}
|
||||||
|
|
||||||
impl From<Infallible> for Error {
|
impl<FE> From<BalanceError> for Error<FE> {
|
||||||
fn from(_: Infallible) -> Error {
|
fn from(e: BalanceError) -> Self {
|
||||||
unreachable!()
|
Error::Balance(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +173,7 @@ impl<'a, P, R> Builder<'a, P, R> {
|
||||||
|
|
||||||
/// Returns the set of Sapling inputs currently committed to be consumed
|
/// Returns the set of Sapling inputs currently committed to be consumed
|
||||||
/// by the transaction.
|
/// by the transaction.
|
||||||
pub fn sapling_inputs(&self) -> &[impl sapling::fees::InputView] {
|
pub fn sapling_inputs(&self) -> &[impl sapling::fees::InputView<()>] {
|
||||||
self.sapling_builder.inputs()
|
self.sapling_builder.inputs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,10 +241,9 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
|
||||||
diversifier: Diversifier,
|
diversifier: Diversifier,
|
||||||
note: Note,
|
note: Note,
|
||||||
merkle_path: MerklePath<Node>,
|
merkle_path: MerklePath<Node>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), sapling::builder::Error> {
|
||||||
self.sapling_builder
|
self.sapling_builder
|
||||||
.add_spend(&mut self.rng, extsk, diversifier, note, merkle_path)
|
.add_spend(&mut self.rng, extsk, diversifier, note, merkle_path)
|
||||||
.map_err(Error::SaplingBuild)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a Sapling address to send funds to.
|
/// Adds a Sapling address to send funds to.
|
||||||
|
@ -252,10 +253,9 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
|
||||||
to: PaymentAddress,
|
to: PaymentAddress,
|
||||||
value: Amount,
|
value: Amount,
|
||||||
memo: MemoBytes,
|
memo: MemoBytes,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), sapling::builder::Error> {
|
||||||
self.sapling_builder
|
self.sapling_builder
|
||||||
.add_output(&mut self.rng, ovk, to, value, memo)
|
.add_output(&mut self.rng, ovk, to, value, memo)
|
||||||
.map_err(Error::SaplingBuild)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a transparent coin to be spent in this transaction.
|
/// Adds a transparent coin to be spent in this transaction.
|
||||||
|
@ -266,10 +266,8 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
|
||||||
sk: secp256k1::SecretKey,
|
sk: secp256k1::SecretKey,
|
||||||
utxo: transparent::OutPoint,
|
utxo: transparent::OutPoint,
|
||||||
coin: TxOut,
|
coin: TxOut,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), transparent::builder::Error> {
|
||||||
self.transparent_builder
|
self.transparent_builder.add_input(sk, utxo, coin)
|
||||||
.add_input(sk, utxo, coin)
|
|
||||||
.map_err(Error::TransparentBuild)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a transparent address to send funds to.
|
/// Adds a transparent address to send funds to.
|
||||||
|
@ -277,10 +275,8 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &TransparentAddress,
|
to: &TransparentAddress,
|
||||||
value: Amount,
|
value: Amount,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), transparent::builder::Error> {
|
||||||
self.transparent_builder
|
self.transparent_builder.add_output(to, value)
|
||||||
.add_output(to, value)
|
|
||||||
.map_err(Error::TransparentBuild)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the notifier channel, where progress of building the transaction is sent.
|
/// Sets the notifier channel, where progress of building the transaction is sent.
|
||||||
|
@ -294,22 +290,18 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the sum of the transparent, Sapling, and TZE value balances.
|
/// Returns the sum of the transparent, Sapling, and TZE value balances.
|
||||||
fn value_balance(&self) -> Result<Amount, Error> {
|
fn value_balance(&self) -> Result<Amount, BalanceError> {
|
||||||
let value_balances = [
|
let value_balances = [
|
||||||
self.transparent_builder
|
self.transparent_builder.value_balance()?,
|
||||||
.value_balance()
|
|
||||||
.ok_or(Error::InvalidAmount)?,
|
|
||||||
self.sapling_builder.value_balance(),
|
self.sapling_builder.value_balance(),
|
||||||
#[cfg(feature = "zfuture")]
|
#[cfg(feature = "zfuture")]
|
||||||
self.tze_builder
|
self.tze_builder.value_balance()?,
|
||||||
.value_balance()
|
|
||||||
.ok_or(Error::InvalidAmount)?,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
value_balances
|
value_balances
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sum::<Option<_>>()
|
.sum::<Option<_>>()
|
||||||
.ok_or(Error::InvalidAmount)
|
.ok_or(BalanceError::Overflow)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a transaction from the configured spends and outputs.
|
/// Builds a transaction from the configured spends and outputs.
|
||||||
|
@ -320,18 +312,17 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
|
||||||
self,
|
self,
|
||||||
prover: &impl TxProver,
|
prover: &impl TxProver,
|
||||||
fee_rule: &FR,
|
fee_rule: &FR,
|
||||||
) -> Result<(Transaction, SaplingMetadata), Error>
|
) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> {
|
||||||
where
|
let fee = fee_rule
|
||||||
Error: From<FR::Error>,
|
.fee_required(
|
||||||
{
|
&self.params,
|
||||||
let fee = fee_rule.fee_required(
|
self.target_height,
|
||||||
&self.params,
|
self.transparent_builder.inputs(),
|
||||||
self.target_height,
|
self.transparent_builder.outputs(),
|
||||||
self.transparent_builder.inputs(),
|
self.sapling_builder.inputs().len(),
|
||||||
self.transparent_builder.outputs(),
|
self.sapling_builder.outputs().len(),
|
||||||
self.sapling_builder.inputs(),
|
)
|
||||||
self.sapling_builder.outputs(),
|
.map_err(Error::Fee)?;
|
||||||
)?;
|
|
||||||
self.build_internal(prover, fee)
|
self.build_internal(prover, fee)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,28 +335,28 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
|
||||||
self,
|
self,
|
||||||
prover: &impl TxProver,
|
prover: &impl TxProver,
|
||||||
fee_rule: &FR,
|
fee_rule: &FR,
|
||||||
) -> Result<(Transaction, SaplingMetadata), Error>
|
) -> Result<(Transaction, SaplingMetadata), Error<FR::Error>> {
|
||||||
where
|
let fee = fee_rule
|
||||||
Error: From<FR::Error>,
|
.fee_required_zfuture(
|
||||||
{
|
&self.params,
|
||||||
let fee = fee_rule.fee_required_zfuture(
|
self.target_height,
|
||||||
&self.params,
|
self.transparent_builder.inputs(),
|
||||||
self.target_height,
|
self.transparent_builder.outputs(),
|
||||||
self.transparent_builder.inputs(),
|
self.sapling_builder.inputs().len(),
|
||||||
self.transparent_builder.outputs(),
|
self.sapling_builder.outputs().len(),
|
||||||
self.sapling_builder.inputs(),
|
self.tze_builder.inputs(),
|
||||||
self.sapling_builder.outputs(),
|
self.tze_builder.outputs(),
|
||||||
self.tze_builder.inputs(),
|
)
|
||||||
self.tze_builder.outputs(),
|
.map_err(Error::Fee)?;
|
||||||
)?;
|
|
||||||
self.build_internal(prover, fee)
|
self.build_internal(prover, fee)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_internal(
|
fn build_internal<FE>(
|
||||||
self,
|
self,
|
||||||
prover: &impl TxProver,
|
prover: &impl TxProver,
|
||||||
fee: Amount,
|
fee: Amount,
|
||||||
) -> Result<(Transaction, SaplingMetadata), Error> {
|
) -> Result<(Transaction, SaplingMetadata), Error<FE>> {
|
||||||
let consensus_branch_id = BranchId::for_height(&self.params, self.target_height);
|
let consensus_branch_id = BranchId::for_height(&self.params, self.target_height);
|
||||||
|
|
||||||
// determine transaction version
|
// determine transaction version
|
||||||
|
@ -376,7 +367,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
|
||||||
//
|
//
|
||||||
|
|
||||||
// After fees are accounted for, the value balance of the transaction must be zero.
|
// After fees are accounted for, the value balance of the transaction must be zero.
|
||||||
let balance_after_fees = (self.value_balance()? - fee).ok_or(Error::InvalidAmount)?;
|
let balance_after_fees = (self.value_balance()? - fee).ok_or(BalanceError::Underflow)?;
|
||||||
|
|
||||||
match balance_after_fees.cmp(&Amount::zero()) {
|
match balance_after_fees.cmp(&Amount::zero()) {
|
||||||
Ordering::Less => {
|
Ordering::Less => {
|
||||||
|
@ -514,12 +505,13 @@ impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a
|
||||||
#[cfg(any(test, feature = "test-dependencies"))]
|
#[cfg(any(test, feature = "test-dependencies"))]
|
||||||
mod testing {
|
mod testing {
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
use super::{Builder, Error, SaplingMetadata};
|
use super::{Builder, Error, SaplingMetadata};
|
||||||
use crate::{
|
use crate::{
|
||||||
consensus::{self, BlockHeight},
|
consensus::{self, BlockHeight},
|
||||||
sapling::prover::mock::MockTxProver,
|
sapling::prover::mock::MockTxProver,
|
||||||
transaction::{components::amount::DEFAULT_FEE, fees::FixedFeeRule, Transaction},
|
transaction::{fees::fixed, Transaction},
|
||||||
};
|
};
|
||||||
|
|
||||||
impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
|
impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
|
||||||
|
@ -536,8 +528,8 @@ mod testing {
|
||||||
Self::new_internal(params, rng, height)
|
Self::new_internal(params, rng, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error> {
|
pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error<Infallible>> {
|
||||||
self.build(&MockTxProver, &FixedFeeRule::new(DEFAULT_FEE))
|
self.build(&MockTxProver, &fixed::FeeRule::standard())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -599,7 +591,7 @@ mod tests {
|
||||||
Amount::from_i64(-1).unwrap(),
|
Amount::from_i64(-1).unwrap(),
|
||||||
MemoBytes::empty()
|
MemoBytes::empty()
|
||||||
),
|
),
|
||||||
Err(Error::SaplingBuild(build_s::Error::InvalidAmount))
|
Err(build_s::Error::InvalidAmount)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -714,7 +706,7 @@ mod tests {
|
||||||
&TransparentAddress::PublicKey([0; 20]),
|
&TransparentAddress::PublicKey([0; 20]),
|
||||||
Amount::from_i64(-1).unwrap(),
|
Amount::from_i64(-1).unwrap(),
|
||||||
),
|
),
|
||||||
Err(Error::TransparentBuild(build_t::Error::InvalidAmount))
|
Err(build_t::Error::InvalidAmount)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::iter::Sum;
|
use std::iter::Sum;
|
||||||
use std::ops::{Add, AddAssign, Neg, Sub, SubAssign};
|
use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign};
|
||||||
|
|
||||||
use memuse::DynamicUsage;
|
use memuse::DynamicUsage;
|
||||||
use orchard::value as orchard;
|
use orchard::value as orchard;
|
||||||
|
@ -205,6 +205,17 @@ impl Neg for Amount {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Mul<usize> for Amount {
|
||||||
|
type Output = Option<Amount>;
|
||||||
|
|
||||||
|
fn mul(self, rhs: usize) -> Option<Amount> {
|
||||||
|
let rhs: i64 = rhs.try_into().ok()?;
|
||||||
|
self.0
|
||||||
|
.checked_mul(rhs)
|
||||||
|
.and_then(|i| Amount::try_from(i).ok())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<orchard::ValueSum> for Amount {
|
impl TryFrom<orchard::ValueSum> for Amount {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
|
@ -213,6 +224,35 @@ impl TryFrom<orchard::ValueSum> for Amount {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A type-safe representation of some nonnegative amount of Zcash.
|
||||||
|
///
|
||||||
|
/// A NonNegativeAmount can only be constructed from an integer that is within the valid monetary
|
||||||
|
/// range of `{0..MAX_MONEY}` (where `MAX_MONEY` = 21,000,000 × 10⁸ zatoshis).
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)]
|
||||||
|
pub struct NonNegativeAmount(Amount);
|
||||||
|
|
||||||
|
impl NonNegativeAmount {
|
||||||
|
/// Creates a NonNegativeAmount from a u64.
|
||||||
|
///
|
||||||
|
/// Returns an error if the amount is outside the range `{0..MAX_MONEY}`.
|
||||||
|
pub fn from_u64(amount: u64) -> Result<Self, ()> {
|
||||||
|
Amount::from_u64(amount).map(NonNegativeAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a NonNegativeAmount from an i64.
|
||||||
|
///
|
||||||
|
/// Returns an error if the amount is outside the range `{0..MAX_MONEY}`.
|
||||||
|
pub fn from_nonnegative_i64(amount: i64) -> Result<Self, ()> {
|
||||||
|
Amount::from_nonnegative_i64(amount).map(NonNegativeAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NonNegativeAmount> for Amount {
|
||||||
|
fn from(n: NonNegativeAmount) -> Self {
|
||||||
|
n.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A type for balance violations in amount addition and subtraction
|
/// A type for balance violations in amount addition and subtraction
|
||||||
/// (overflow and underflow of allowed ranges)
|
/// (overflow and underflow of allowed ranges)
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
|
@ -69,10 +69,14 @@ pub struct SpendDescriptionInfo {
|
||||||
merkle_path: MerklePath<Node>,
|
merkle_path: MerklePath<Node>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fees::InputView for SpendDescriptionInfo {
|
impl fees::InputView<()> for SpendDescriptionInfo {
|
||||||
|
fn note_id(&self) -> &() {
|
||||||
|
// The builder does not make use of note identifiers, so we can just return the unit value.
|
||||||
|
&()
|
||||||
|
}
|
||||||
|
|
||||||
fn value(&self) -> Amount {
|
fn value(&self) -> Amount {
|
||||||
// An existing note to be spent must have a valid
|
// An existing note to be spent must have a valid amount value.
|
||||||
// amount value.
|
|
||||||
Amount::from_u64(self.note.value).unwrap()
|
Amount::from_u64(self.note.value).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -243,7 +247,7 @@ impl<P> SaplingBuilder<P> {
|
||||||
|
|
||||||
/// Returns the list of Sapling inputs that will be consumed by the transaction being
|
/// Returns the list of Sapling inputs that will be consumed by the transaction being
|
||||||
/// constructed.
|
/// constructed.
|
||||||
pub fn inputs(&self) -> &[impl fees::InputView] {
|
pub fn inputs(&self) -> &[impl fees::InputView<()>] {
|
||||||
&self.spends
|
&self.spends
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,9 @@ use crate::transaction::components::amount::Amount;
|
||||||
|
|
||||||
/// A trait that provides a minimized view of a Sapling input suitable for use in
|
/// A trait that provides a minimized view of a Sapling input suitable for use in
|
||||||
/// fee and change calculation.
|
/// fee and change calculation.
|
||||||
pub trait InputView {
|
pub trait InputView<NoteRef> {
|
||||||
|
/// An identifier for the input being spent.
|
||||||
|
fn note_id(&self) -> &NoteRef;
|
||||||
/// The value of the input being spent.
|
/// The value of the input being spent.
|
||||||
fn value(&self) -> Amount;
|
fn value(&self) -> Amount;
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ impl<A: Authorization> Bundle<A> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct OutPoint {
|
pub struct OutPoint {
|
||||||
hash: [u8; 32],
|
hash: [u8; 32],
|
||||||
n: u32,
|
n: u32,
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
legacy::{Script, TransparentAddress},
|
legacy::{Script, TransparentAddress},
|
||||||
transaction::{
|
transaction::{
|
||||||
components::{
|
components::{
|
||||||
amount::Amount,
|
amount::{Amount, BalanceError},
|
||||||
transparent::{self, fees, Authorization, Authorized, Bundle, TxIn, TxOut},
|
transparent::{self, fees, Authorization, Authorized, Bundle, TxIn, TxOut},
|
||||||
},
|
},
|
||||||
sighash::TransparentAuthorizingContext,
|
sighash::TransparentAuthorizingContext,
|
||||||
|
@ -176,23 +176,26 @@ impl TransparentBuilder {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value_balance(&self) -> Option<Amount> {
|
pub fn value_balance(&self) -> Result<Amount, BalanceError> {
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
let input_sum = self
|
let input_sum = self
|
||||||
.inputs
|
.inputs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|input| input.coin.value)
|
.map(|input| input.coin.value)
|
||||||
.sum::<Option<Amount>>()?;
|
.sum::<Option<Amount>>()
|
||||||
|
.ok_or(BalanceError::Overflow)?;
|
||||||
|
|
||||||
#[cfg(not(feature = "transparent-inputs"))]
|
#[cfg(not(feature = "transparent-inputs"))]
|
||||||
let input_sum = Amount::zero();
|
let input_sum = Amount::zero();
|
||||||
|
|
||||||
input_sum
|
let output_sum = self
|
||||||
- self
|
.vout
|
||||||
.vout
|
.iter()
|
||||||
.iter()
|
.map(|vo| vo.value)
|
||||||
.map(|vo| vo.value)
|
.sum::<Option<Amount>>()
|
||||||
.sum::<Option<Amount>>()?
|
.ok_or(BalanceError::Overflow)?;
|
||||||
|
|
||||||
|
(input_sum - output_sum).ok_or(BalanceError::Underflow)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Option<transparent::Bundle<Unauthorized>> {
|
pub fn build(self) -> Option<transparent::Bundle<Unauthorized>> {
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
transaction::{
|
transaction::{
|
||||||
self as tx,
|
self as tx,
|
||||||
components::{
|
components::{
|
||||||
amount::Amount,
|
amount::{Amount, BalanceError},
|
||||||
tze::{fees, Authorization, Authorized, Bundle, OutPoint, TzeIn, TzeOut},
|
tze::{fees, Authorization, Authorized, Bundle, OutPoint, TzeIn, TzeOut},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -121,16 +121,22 @@ impl<'a, BuildCtx> TzeBuilder<'a, BuildCtx> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value_balance(&self) -> Option<Amount> {
|
pub fn value_balance(&self) -> Result<Amount, BalanceError> {
|
||||||
self.vin
|
let total_in = self
|
||||||
|
.vin
|
||||||
.iter()
|
.iter()
|
||||||
.map(|tzi| tzi.coin.value)
|
.map(|tzi| tzi.coin.value)
|
||||||
.sum::<Option<Amount>>()?
|
.sum::<Option<Amount>>()
|
||||||
- self
|
.ok_or(BalanceError::Overflow)?;
|
||||||
.vout
|
|
||||||
.iter()
|
let total_out = self
|
||||||
.map(|tzo| tzo.value)
|
.vout
|
||||||
.sum::<Option<Amount>>()?
|
.iter()
|
||||||
|
.map(|tzo| tzo.value)
|
||||||
|
.sum::<Option<Amount>>()
|
||||||
|
.ok_or(BalanceError::Overflow)?;
|
||||||
|
|
||||||
|
(total_in - total_out).ok_or(BalanceError::Underflow)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> (Option<Bundle<Unauthorized>>, Vec<TzeSigner<'a, BuildCtx>>) {
|
pub fn build(self) -> (Option<Bundle<Unauthorized>>, Vec<TzeSigner<'a, BuildCtx>>) {
|
||||||
|
|
|
@ -2,14 +2,15 @@
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
consensus::{self, BlockHeight},
|
consensus::{self, BlockHeight},
|
||||||
transaction::components::{
|
transaction::components::{amount::Amount, transparent::fees as transparent},
|
||||||
amount::Amount, sapling::fees as sapling, transparent::fees as transparent,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "zfuture")]
|
#[cfg(feature = "zfuture")]
|
||||||
use crate::transaction::components::tze::fees as tze;
|
use crate::transaction::components::tze::fees as tze;
|
||||||
|
|
||||||
|
pub mod fixed;
|
||||||
|
pub mod zip317;
|
||||||
|
|
||||||
/// A trait that represents the ability to compute the fees that must be paid
|
/// A trait that represents the ability to compute the fees that must be paid
|
||||||
/// by a transaction having a specified set of inputs and outputs.
|
/// by a transaction having a specified set of inputs and outputs.
|
||||||
pub trait FeeRule {
|
pub trait FeeRule {
|
||||||
|
@ -26,8 +27,8 @@ pub trait FeeRule {
|
||||||
target_height: BlockHeight,
|
target_height: BlockHeight,
|
||||||
transparent_inputs: &[impl transparent::InputView],
|
transparent_inputs: &[impl transparent::InputView],
|
||||||
transparent_outputs: &[impl transparent::OutputView],
|
transparent_outputs: &[impl transparent::OutputView],
|
||||||
sapling_inputs: &[impl sapling::InputView],
|
sapling_input_count: usize,
|
||||||
sapling_outputs: &[impl sapling::OutputView],
|
sapling_output_count: usize,
|
||||||
) -> Result<Amount, Self::Error>;
|
) -> Result<Amount, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,55 +48,9 @@ pub trait FutureFeeRule: FeeRule {
|
||||||
target_height: BlockHeight,
|
target_height: BlockHeight,
|
||||||
transparent_inputs: &[impl transparent::InputView],
|
transparent_inputs: &[impl transparent::InputView],
|
||||||
transparent_outputs: &[impl transparent::OutputView],
|
transparent_outputs: &[impl transparent::OutputView],
|
||||||
sapling_inputs: &[impl sapling::InputView],
|
sapling_input_count: usize,
|
||||||
sapling_outputs: &[impl sapling::OutputView],
|
sapling_output_count: usize,
|
||||||
tze_inputs: &[impl tze::InputView],
|
tze_inputs: &[impl tze::InputView],
|
||||||
tze_outputs: &[impl tze::OutputView],
|
tze_outputs: &[impl tze::OutputView],
|
||||||
) -> Result<Amount, Self::Error>;
|
) -> Result<Amount, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A fee rule that always returns a fixed fee, irrespective of the structure of
|
|
||||||
/// the transaction being constructed.
|
|
||||||
pub struct FixedFeeRule {
|
|
||||||
fixed_fee: Amount,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FixedFeeRule {
|
|
||||||
/// Creates a new fixed fee rule with the specified fixed fee.
|
|
||||||
pub fn new(fixed_fee: Amount) -> Self {
|
|
||||||
Self { fixed_fee }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FeeRule for FixedFeeRule {
|
|
||||||
type Error = std::convert::Infallible;
|
|
||||||
|
|
||||||
fn fee_required<P: consensus::Parameters>(
|
|
||||||
&self,
|
|
||||||
_params: &P,
|
|
||||||
_target_height: BlockHeight,
|
|
||||||
_transparent_inputs: &[impl transparent::InputView],
|
|
||||||
_transparent_outputs: &[impl transparent::OutputView],
|
|
||||||
_sapling_inputs: &[impl sapling::InputView],
|
|
||||||
_sapling_outputs: &[impl sapling::OutputView],
|
|
||||||
) -> Result<Amount, Self::Error> {
|
|
||||||
Ok(self.fixed_fee)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "zfuture")]
|
|
||||||
impl FutureFeeRule for FixedFeeRule {
|
|
||||||
fn fee_required_zfuture<P: consensus::Parameters>(
|
|
||||||
&self,
|
|
||||||
_params: &P,
|
|
||||||
_target_height: BlockHeight,
|
|
||||||
_transparent_inputs: &[impl transparent::InputView],
|
|
||||||
_transparent_outputs: &[impl transparent::OutputView],
|
|
||||||
_sapling_inputs: &[impl sapling::InputView],
|
|
||||||
_sapling_outputs: &[impl sapling::OutputView],
|
|
||||||
_tze_inputs: &[impl tze::InputView],
|
|
||||||
_tze_outputs: &[impl tze::OutputView],
|
|
||||||
) -> Result<Amount, Self::Error> {
|
|
||||||
Ok(self.fixed_fee)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
use crate::{
|
||||||
|
consensus::{self, BlockHeight},
|
||||||
|
transaction::components::{
|
||||||
|
amount::{Amount, DEFAULT_FEE},
|
||||||
|
transparent::fees as transparent,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "zfuture")]
|
||||||
|
use crate::transaction::components::tze::fees as tze;
|
||||||
|
|
||||||
|
/// A fee rule that always returns a fixed fee, irrespective of the structure of
|
||||||
|
/// the transaction being constructed.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct FeeRule {
|
||||||
|
fixed_fee: Amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeeRule {
|
||||||
|
/// Creates a new nonstandard fixed fee rule with the specified fixed fee.
|
||||||
|
pub fn non_standard(fixed_fee: Amount) -> Self {
|
||||||
|
Self { fixed_fee }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new fixed fee rule with the standard default fee.
|
||||||
|
pub fn standard() -> Self {
|
||||||
|
Self {
|
||||||
|
fixed_fee: DEFAULT_FEE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the fixed fee amount which which this rule was configured.
|
||||||
|
pub fn fixed_fee(&self) -> Amount {
|
||||||
|
self.fixed_fee
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::FeeRule for FeeRule {
|
||||||
|
type Error = std::convert::Infallible;
|
||||||
|
|
||||||
|
fn fee_required<P: consensus::Parameters>(
|
||||||
|
&self,
|
||||||
|
_params: &P,
|
||||||
|
_target_height: BlockHeight,
|
||||||
|
_transparent_inputs: &[impl transparent::InputView],
|
||||||
|
_transparent_outputs: &[impl transparent::OutputView],
|
||||||
|
_sapling_input_count: usize,
|
||||||
|
_sapling_output_count: usize,
|
||||||
|
) -> Result<Amount, Self::Error> {
|
||||||
|
Ok(self.fixed_fee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "zfuture")]
|
||||||
|
impl super::FutureFeeRule for FeeRule {
|
||||||
|
fn fee_required_zfuture<P: consensus::Parameters>(
|
||||||
|
&self,
|
||||||
|
_params: &P,
|
||||||
|
_target_height: BlockHeight,
|
||||||
|
_transparent_inputs: &[impl transparent::InputView],
|
||||||
|
_transparent_outputs: &[impl transparent::OutputView],
|
||||||
|
_sapling_input_count: usize,
|
||||||
|
_sapling_output_count: usize,
|
||||||
|
_tze_inputs: &[impl tze::InputView],
|
||||||
|
_tze_outputs: &[impl tze::OutputView],
|
||||||
|
) -> Result<Amount, Self::Error> {
|
||||||
|
Ok(self.fixed_fee)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
//! Types related to implementing a [`FeeRule`] provides [ZIP 317] fee calculation.
|
||||||
|
//!
|
||||||
|
//! [`FeeRule`]: crate::transaction::fees::FeeRule
|
||||||
|
//! [ZIP 317]: https//zips.z.cash/zip-0317
|
||||||
|
use core::cmp::max;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
consensus::{self, BlockHeight},
|
||||||
|
legacy::TransparentAddress,
|
||||||
|
transaction::components::{
|
||||||
|
amount::{Amount, BalanceError},
|
||||||
|
transparent::{fees as transparent, OutPoint},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A [`FeeRule`] implementation that implements the [ZIP 317] fee rule.
|
||||||
|
///
|
||||||
|
/// This fee rule supports only P2pkh transparent inputs; an error will be returned if a coin
|
||||||
|
/// containing a non-p2pkh script is provided as an input. This fee rule may slightly overestimate
|
||||||
|
/// fees in case where the user is attempting to spend more than ~150 transparent inputs.
|
||||||
|
///
|
||||||
|
/// [`FeeRule`]: crate::transaction::fees::FeeRule
|
||||||
|
/// [ZIP 317]: https//zips.z.cash/zip-0317
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FeeRule {
|
||||||
|
marginal_fee: Amount,
|
||||||
|
grace_actions: usize,
|
||||||
|
p2pkh_standard_input_size: usize,
|
||||||
|
p2pkh_standard_output_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeeRule {
|
||||||
|
/// Construct a new FeeRule using the standard [ZIP 317] constants.
|
||||||
|
///
|
||||||
|
/// [ZIP 317]: https//zips.z.cash/zip-0317
|
||||||
|
pub fn standard() -> Self {
|
||||||
|
Self {
|
||||||
|
marginal_fee: Amount::from_u64(5000).unwrap(),
|
||||||
|
grace_actions: 2,
|
||||||
|
p2pkh_standard_input_size: 150,
|
||||||
|
p2pkh_standard_output_size: 34,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a new FeeRule instance with the specified parameter values.
|
||||||
|
///
|
||||||
|
/// Returns `None` if either `p2pkh_standard_input_size` or `p2pkh_standard_output_size` are
|
||||||
|
/// zero.
|
||||||
|
pub fn non_standard(
|
||||||
|
marginal_fee: Amount,
|
||||||
|
grace_actions: usize,
|
||||||
|
p2pkh_standard_input_size: usize,
|
||||||
|
p2pkh_standard_output_size: usize,
|
||||||
|
) -> Option<Self> {
|
||||||
|
if p2pkh_standard_input_size == 0 || p2pkh_standard_output_size == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Self {
|
||||||
|
marginal_fee,
|
||||||
|
grace_actions,
|
||||||
|
p2pkh_standard_input_size,
|
||||||
|
p2pkh_standard_output_size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the ZIP 317 marginal fee.
|
||||||
|
pub fn marginal_fee(&self) -> Amount {
|
||||||
|
self.marginal_fee
|
||||||
|
}
|
||||||
|
/// Returns the ZIP 317 number of grace actions
|
||||||
|
pub fn grace_actions(&self) -> usize {
|
||||||
|
self.grace_actions
|
||||||
|
}
|
||||||
|
/// Returns the ZIP 317 standard P2PKH input size
|
||||||
|
pub fn p2pkh_standard_input_size(&self) -> usize {
|
||||||
|
self.p2pkh_standard_input_size
|
||||||
|
}
|
||||||
|
/// Returns the ZIP 317 standard P2PKH output size
|
||||||
|
pub fn p2pkh_standard_output_size(&self) -> usize {
|
||||||
|
self.p2pkh_standard_output_size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors that can occur in ZIP 317 fee computation
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum FeeError {
|
||||||
|
/// An overflow or underflow of amount computation occurred.
|
||||||
|
Balance(BalanceError),
|
||||||
|
/// Transparent inputs provided to the fee calculation included coins that do not pay to
|
||||||
|
/// standard P2pkh scripts.
|
||||||
|
NonP2pkhInputs(Vec<OutPoint>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BalanceError> for FeeError {
|
||||||
|
fn from(err: BalanceError) -> Self {
|
||||||
|
FeeError::Balance(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::FeeRule for FeeRule {
|
||||||
|
type Error = FeeError;
|
||||||
|
|
||||||
|
fn fee_required<P: consensus::Parameters>(
|
||||||
|
&self,
|
||||||
|
_params: &P,
|
||||||
|
_target_height: BlockHeight,
|
||||||
|
transparent_inputs: &[impl transparent::InputView],
|
||||||
|
transparent_outputs: &[impl transparent::OutputView],
|
||||||
|
sapling_input_count: usize,
|
||||||
|
sapling_output_count: usize,
|
||||||
|
) -> Result<Amount, Self::Error> {
|
||||||
|
let non_p2pkh_inputs: Vec<_> = transparent_inputs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t_in| match t_in.coin().script_pubkey.address() {
|
||||||
|
Some(TransparentAddress::PublicKey(_)) => None,
|
||||||
|
_ => Some(t_in.outpoint()),
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !non_p2pkh_inputs.is_empty() {
|
||||||
|
return Err(FeeError::NonP2pkhInputs(non_p2pkh_inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
let t_in_total_size = transparent_inputs.len() * 150;
|
||||||
|
let t_out_total_size = transparent_outputs.len() * 34;
|
||||||
|
|
||||||
|
let ceildiv = |num: usize, den: usize| (num + den - 1) / den;
|
||||||
|
|
||||||
|
let logical_actions = max(
|
||||||
|
ceildiv(t_in_total_size, self.p2pkh_standard_input_size),
|
||||||
|
ceildiv(t_out_total_size, self.p2pkh_standard_output_size),
|
||||||
|
) + max(sapling_input_count, sapling_output_count);
|
||||||
|
|
||||||
|
(self.marginal_fee * max(self.grace_actions, logical_actions))
|
||||||
|
.ok_or_else(|| BalanceError::Overflow.into())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue