diff --git a/zebra-chain/src/block/tests.rs b/zebra-chain/src/block/tests.rs index faa67082..2d4c376e 100644 --- a/zebra-chain/src/block/tests.rs +++ b/zebra-chain/src/block/tests.rs @@ -6,7 +6,7 @@ use crate::merkle_tree::MerkleTreeRootHash; use crate::serialization::{ sha256d, SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize, }; -use crate::types::LockTime; +use crate::transaction::LockTime; use crate::Network; use crate::test::generate; diff --git a/zebra-chain/src/test/generate.rs b/zebra-chain/src/test/generate.rs index af8f30e0..1dbfd19a 100644 --- a/zebra-chain/src/test/generate.rs +++ b/zebra-chain/src/test/generate.rs @@ -5,8 +5,7 @@ use std::sync::Arc; use crate::{ block::{Block, BlockHeader, MAX_BLOCK_BYTES}, serialization::{ZcashDeserialize, ZcashSerialize}, - transaction::{Transaction, TransparentInput, TransparentOutput}, - types::LockTime, + transaction::{LockTime, Transaction, TransparentInput, TransparentOutput}, }; /// Generate a block header diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index cd953e76..9d3ce064 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; mod hash; mod joinsplit; +mod lock_time; mod serialize; mod shielded_data; mod transparent; @@ -13,12 +14,13 @@ mod tests; pub use hash::TransactionHash; pub use joinsplit::{JoinSplit, JoinSplitData}; +pub use lock_time::LockTime; pub use shielded_data::{Output, ShieldedData, Spend}; pub use transparent::{CoinbaseData, OutPoint, TransparentInput, TransparentOutput}; use crate::amount::Amount; use crate::proofs::{Bctv14Proof, Groth16Proof}; -use crate::types::{BlockHeight, LockTime}; +use crate::types::BlockHeight; /// A Zcash transaction. /// diff --git a/zebra-chain/src/transaction/lock_time.rs b/zebra-chain/src/transaction/lock_time.rs new file mode 100644 index 00000000..9eff8e42 --- /dev/null +++ b/zebra-chain/src/transaction/lock_time.rs @@ -0,0 +1,84 @@ +use std::io; + +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use chrono::{DateTime, TimeZone, Utc}; + +use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}; +use crate::types::BlockHeight; + +/// A Bitcoin-style `locktime`, representing either a block height or an epoch +/// time. +/// +/// # Invariants +/// +/// Users should not construct a `LockTime` with: +/// - a `BlockHeight` greater than MAX_BLOCK_HEIGHT, +/// - a timestamp before 6 November 1985 +/// (Unix timestamp less than MIN_LOCK_TIMESTAMP), or +/// - a timestamp after 5 February 2106 +/// (Unix timestamp greater than MAX_LOCK_TIMESTAMP). +#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum LockTime { + /// Unlock at a particular block height. + Height(BlockHeight), + /// Unlock at a particular time. + Time(DateTime), +} + +impl LockTime { + /// The minimum LockTime::Time, as a timestamp in seconds. + /// + /// Users should not construct lock times less than `MIN_TIMESTAMP`. + pub const MIN_TIMESTAMP: i64 = 500_000_000; + + /// The maximum LockTime::Time, as a timestamp in seconds. + /// + /// Users should not construct lock times greater than `MAX_TIMESTAMP`. + /// LockTime is u32 in the spec, so times are limited to u32::MAX. + pub const MAX_TIMESTAMP: i64 = u32::MAX as i64; + + /// Returns the minimum LockTime::Time, as a LockTime. + /// + /// Users should not construct lock times less than `min_lock_timestamp`. + // + // When `Utc.timestamp` stabilises as a const function, we can make this a + // const function. + pub fn min_lock_time() -> LockTime { + LockTime::Time(Utc.timestamp(Self::MIN_TIMESTAMP, 0)) + } + + /// Returns the maximum LockTime::Time, as a LockTime. + /// + /// Users should not construct lock times greater than `max_lock_timestamp`. + // + // When `Utc.timestamp` stabilises as a const function, we can make this a + // const function. + pub fn max_lock_time() -> LockTime { + LockTime::Time(Utc.timestamp(Self::MAX_TIMESTAMP, 0)) + } +} + +impl ZcashSerialize for LockTime { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + // This implementation does not check the invariants on `LockTime` so that the + // serialization is fallible only if the underlying writer is. This ensures that + // we can always compute a hash of a transaction object. + use LockTime::*; + match self { + Height(BlockHeight(n)) => writer.write_u32::(*n)?, + Time(t) => writer.write_u32::(t.timestamp() as u32)?, + } + Ok(()) + } +} + +impl ZcashDeserialize for LockTime { + fn zcash_deserialize(mut reader: R) -> Result { + let n = reader.read_u32::()?; + if n <= BlockHeight::MAX.0 { + Ok(LockTime::Height(BlockHeight(n))) + } else { + Ok(LockTime::Time(Utc.timestamp(n as i64, 0))) + } + } +} diff --git a/zebra-chain/src/transaction/tests/arbitrary.rs b/zebra-chain/src/transaction/tests/arbitrary.rs index f01c848c..6471ca26 100644 --- a/zebra-chain/src/transaction/tests/arbitrary.rs +++ b/zebra-chain/src/transaction/tests/arbitrary.rs @@ -1,3 +1,4 @@ +use chrono::{TimeZone, Utc}; use futures::future::Either; use proptest::{arbitrary::any, array, collection::vec, option, prelude::*}; @@ -7,11 +8,11 @@ use crate::{ notes::{sapling, sprout}, proofs::{Bctv14Proof, Groth16Proof, ZkSnarkProof}, transaction::{ - CoinbaseData, JoinSplit, JoinSplitData, OutPoint, Output, ShieldedData, Spend, Transaction, - TransparentInput, TransparentOutput, + CoinbaseData, JoinSplit, JoinSplitData, LockTime, OutPoint, Output, ShieldedData, Spend, + Transaction, TransparentInput, TransparentOutput, }, treestate::{self, note_commitment_tree::SaplingNoteTreeRootHash}, - types::{BlockHeight, LockTime, Script}, + types::{BlockHeight, Script}, }; impl Transaction { @@ -100,6 +101,22 @@ impl Transaction { } } +impl Arbitrary for LockTime { + type Parameters = (); + + fn arbitrary_with(_args: ()) -> Self::Strategy { + prop_oneof![ + (BlockHeight::MIN.0..=BlockHeight::MAX.0) + .prop_map(|n| LockTime::Height(BlockHeight(n))), + (LockTime::MIN_TIMESTAMP..=LockTime::MAX_TIMESTAMP) + .prop_map(|n| { LockTime::Time(Utc.timestamp(n as i64, 0)) }) + ] + .boxed() + } + + type Strategy = BoxedStrategy; +} + impl Arbitrary for JoinSplit

{ type Parameters = (); diff --git a/zebra-chain/src/transaction/tests/prop.rs b/zebra-chain/src/transaction/tests/prop.rs index ac9e9f6c..2f944e7d 100644 --- a/zebra-chain/src/transaction/tests/prop.rs +++ b/zebra-chain/src/transaction/tests/prop.rs @@ -1,11 +1,11 @@ use proptest::prelude::*; +use std::io::Cursor; use super::super::*; -use crate::serialization::{ZcashDeserializeInto, ZcashSerialize}; +use crate::serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize}; proptest! { - #[test] fn transaction_roundtrip(tx in any::()) { let data = tx.zcash_serialize_to_vec().expect("tx should serialize"); @@ -13,4 +13,15 @@ proptest! { prop_assert_eq![tx, tx2]; } + + #[test] + fn locktime_roundtrip(locktime in any::()) { + let mut bytes = Cursor::new(Vec::new()); + locktime.zcash_serialize(&mut bytes)?; + + bytes.set_position(0); + let other_locktime = LockTime::zcash_deserialize(&mut bytes)?; + + prop_assert_eq![locktime, other_locktime]; + } } diff --git a/zebra-chain/src/types.rs b/zebra-chain/src/types.rs index b6a18418..bd7dc2fe 100644 --- a/zebra-chain/src/types.rs +++ b/zebra-chain/src/types.rs @@ -3,8 +3,6 @@ use crate::serialization::{ ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashSerialize, }; -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use chrono::{DateTime, TimeZone, Utc}; use std::{ fmt, io::{self, Read}, @@ -46,25 +44,6 @@ impl Arbitrary for BlockHeight { type Strategy = BoxedStrategy; } -/// A Bitcoin-style `locktime`, representing either a block height or an epoch -/// time. -/// -/// # Invariants -/// -/// Users should not construct a `LockTime` with: -/// - a `BlockHeight` greater than MAX_BLOCK_HEIGHT, -/// - a timestamp before 6 November 1985 -/// (Unix timestamp less than MIN_LOCK_TIMESTAMP), or -/// - a timestamp after 5 February 2106 -/// (Unix timestamp greater than MAX_LOCK_TIMESTAMP). -#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum LockTime { - /// Unlock at a particular block height. - Height(BlockHeight), - /// Unlock at a particular time. - Time(DateTime), -} - impl BlockHeight { /// The minimum BlockHeight. /// @@ -87,81 +66,6 @@ impl BlockHeight { pub const MAX_AS_U32: u32 = Self::MAX.0; } -impl LockTime { - /// The minimum LockTime::Time, as a timestamp in seconds. - /// - /// Users should not construct lock times less than `MIN_TIMESTAMP`. - pub const MIN_TIMESTAMP: i64 = 500_000_000; - - /// The maximum LockTime::Time, as a timestamp in seconds. - /// - /// Users should not construct lock times greater than `MAX_TIMESTAMP`. - /// LockTime is u32 in the spec, so times are limited to u32::MAX. - pub const MAX_TIMESTAMP: i64 = u32::MAX as i64; - - /// Returns the minimum LockTime::Time, as a LockTime. - /// - /// Users should not construct lock times less than `min_lock_timestamp`. - // - // When `Utc.timestamp` stabilises as a const function, we can make this a - // const function. - pub fn min_lock_time() -> LockTime { - LockTime::Time(Utc.timestamp(Self::MIN_TIMESTAMP, 0)) - } - - /// Returns the maximum LockTime::Time, as a LockTime. - /// - /// Users should not construct lock times greater than `max_lock_timestamp`. - // - // When `Utc.timestamp` stabilises as a const function, we can make this a - // const function. - pub fn max_lock_time() -> LockTime { - LockTime::Time(Utc.timestamp(Self::MAX_TIMESTAMP, 0)) - } -} - -impl ZcashSerialize for LockTime { - fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { - // This implementation does not check the invariants on `LockTime` so that the - // serialization is fallible only if the underlying writer is. This ensures that - // we can always compute a hash of a transaction object. - use LockTime::*; - match self { - Height(BlockHeight(n)) => writer.write_u32::(*n)?, - Time(t) => writer.write_u32::(t.timestamp() as u32)?, - } - Ok(()) - } -} - -impl ZcashDeserialize for LockTime { - fn zcash_deserialize(mut reader: R) -> Result { - let n = reader.read_u32::()?; - if n <= BlockHeight::MAX.0 { - Ok(LockTime::Height(BlockHeight(n))) - } else { - Ok(LockTime::Time(Utc.timestamp(n as i64, 0))) - } - } -} - -#[cfg(test)] -impl Arbitrary for LockTime { - type Parameters = (); - - fn arbitrary_with(_args: ()) -> Self::Strategy { - prop_oneof![ - (BlockHeight::MIN.0..=BlockHeight::MAX.0) - .prop_map(|n| LockTime::Height(BlockHeight(n))), - (LockTime::MIN_TIMESTAMP..=LockTime::MAX_TIMESTAMP) - .prop_map(|n| { LockTime::Time(Utc.timestamp(n as i64, 0)) }) - ] - .boxed() - } - - type Strategy = BoxedStrategy; -} - /// A sequence of message authentication tags ... /// /// binding h_sig to each a_sk of the JoinSplit description, computed as @@ -223,21 +127,10 @@ mod proptests { use proptest::prelude::*; - use super::{LockTime, Script}; + use super::*; use crate::serialization::{ZcashDeserialize, ZcashSerialize}; proptest! { - #[test] - fn locktime_roundtrip(locktime in any::()) { - let mut bytes = Cursor::new(Vec::new()); - locktime.zcash_serialize(&mut bytes)?; - - bytes.set_position(0); - let other_locktime = LockTime::zcash_deserialize(&mut bytes)?; - - prop_assert_eq![locktime, other_locktime]; - } - #[test] fn script_roundtrip(script in any::