hash (features|commitment) in output mmr (#615)

* experiment with lock_heights on outputs

* playing around with lock_height as part of the switch commitment hash

* cleanup

* include features in the switch commit hash key

* commit

* rebase off master

* commit

* cleanup

* missing docs

* rework coinbase maturity test to build valid tx

* pool and chain tests passing (inputs have switch commitments)

* commit

* cleanup

* check inputs spending coinbase outputs have valid lock_heights

* wip - got it building (tests still failing)

* use zero key for non coinbase switch commit hash

* fees and height wrong order...

* send output lock_height over to wallet via api

* no more header by height index
workaround this for wallet refresh and wallet restore

* refresh heights for unspent wallet outputs where missing

* TODO - might be slow?

* simplify - do not pass around lock_height for non coinbase outputs

* commit

* fix tests after merge

* build input vs coinbase_input
switch commit hash key encodes lock_height
cleanup output by commit index (currently broken...)

* is_unspent and get_unspent cleanup - we have no outputs, only switch_commit_hashes

* separate concept of utxo vs output in the api
utxos come from the sumtrees (and only the sumtrees, limited info)
outputs come from blocks (and we need to look them up via block height)

* cleanup

* better api support for block outputs with range proofs

* basic wallet operations appear to work
restore is not working fully
refresh refreshes heights correctly (at least appears to)

* wallet refresh and wallet restore appear to be working now

* fix core tests

* fix some mine_simple_chain tests

* fixup chain tests

* rework so pool tests pass

* wallet restore now safely habndles duplicate commitments (reused wallet keys)
for coinbase outputs where lock_height is _very_ important

* wip

* validate_coinbase_maturity
got things building
tests are failing

* lite vs full versions of is_unspent

* builds and working locally
zero-conf - what to do here?

* handle zero-conf edge case (use latest block)

* introduce OutputIdentifier, avoid leaking SumCommit everywhere

* fix the bad merge

* pool verifies coinbase maturity via is_matured
this uses sumtree in a consistent way

* cleanup

* add docs, cleanup build warnings

* fix core tests

* fix chain tests

* fix pool tests

* cleanup debug logging that we no longer need

* make out_block optional on an input (only care about it for spending coinbase outputs)

* cleanup

* bump the build
This commit is contained in:
AntiochP
2018-01-16 22:03:40 -05:00
committed by GitHub
parent 7e7c8e157e
commit cbd3b2ff87
31 changed files with 1345 additions and 901 deletions
+119 -43
View File
@@ -19,15 +19,25 @@ use util;
use util::{secp, static_secp_instance};
use std::collections::HashSet;
use core::Committed;
use core::{Input, Output, Proof, SwitchCommitHash, Transaction, TxKernel, COINBASE_KERNEL,
COINBASE_OUTPUT};
use core::{
Committed,
Input,
Output,
OutputIdentifier,
SwitchCommitHash,
Proof,
TxKernel,
Transaction,
COINBASE_KERNEL,
COINBASE_OUTPUT
};
use consensus;
use consensus::{exceeds_weight, reward, MINIMUM_DIFFICULTY, REWARD, VerifySortOrder};
use core::hash::{Hash, Hashed, ZERO_HASH};
use core::target::Difficulty;
use core::transaction;
use ser::{self, read_and_verify_sorted, Readable, Reader, Writeable, WriteableSorted, Writer};
use ser::{self, Readable, Reader, Writeable, Writer, WriteableSorted, read_and_verify_sorted};
use util::kernel_sig_msg;
use util::LOGGER;
use global;
use keychain;
@@ -45,10 +55,7 @@ pub enum Error {
/// Too many inputs, outputs or kernels in the block
WeightExceeded,
/// Kernel not valid due to lock_height exceeding block header height
KernelLockHeight {
/// The lock_height causing this validation error
lock_height: u64,
},
KernelLockHeight(u64),
/// Underlying tx related error
Transaction(transaction::Error),
/// Underlying Secp256k1 error (signature validation or invalid public key typically)
@@ -57,6 +64,15 @@ pub enum Error {
Keychain(keychain::Error),
/// Underlying consensus error (sort order currently)
Consensus(consensus::Error),
/// Coinbase has not yet matured and cannot be spent (1,000 blocks)
ImmatureCoinbase {
/// The height of the block containing the input spending the coinbase output
height: u64,
/// The lock_height needed to be reached for the coinbase output to mature
lock_height: u64,
},
/// Other unspecified error condition
Other(String)
}
impl From<transaction::Error> for Error {
@@ -295,7 +311,12 @@ impl Block {
difficulty: Difficulty,
) -> Result<Block, Error> {
let fees = txs.iter().map(|tx| tx.fee).sum();
let (reward_out, reward_proof) = Block::reward_output(keychain, key_id, fees)?;
let (reward_out, reward_proof) = Block::reward_output(
keychain,
key_id,
fees,
prev.height + 1,
)?;
let block = Block::with_reward(prev, txs, reward_out, reward_proof, difficulty)?;
Ok(block)
}
@@ -454,14 +475,20 @@ impl Block {
/// trees, reward, etc.
///
/// TODO - performs various verification steps - discuss renaming this to "verify"
/// as all the steps within are verify steps.
///
pub fn validate(&self) -> Result<(), Error> {
self.verify_weight()?;
self.verify_sorted()?;
self.verify_coinbase()?;
self.verify_kernels()?;
Ok(())
}
fn verify_weight(&self) -> Result<(), Error> {
if exceeds_weight(self.inputs.len(), self.outputs.len(), self.kernels.len()) {
return Err(Error::WeightExceeded);
}
self.verify_sorted()?;
self.verify_coinbase()?;
self.verify_kernels(false)?;
Ok(())
}
@@ -474,15 +501,16 @@ impl Block {
/// Verifies the sum of input/output commitments match the sum in kernels
/// and that all kernel signatures are valid.
/// TODO - when would we skip_sig? Is this needed or used anywhere?
fn verify_kernels(&self, skip_sig: bool) -> Result<(), Error> {
fn verify_kernels(&self) -> Result<(), Error> {
for k in &self.kernels {
if k.fee & 1 != 0 {
return Err(Error::OddKernelFee);
}
// check we have no kernels with lock_heights greater than current height
// no tx can be included in a block earlier than its lock_height
if k.lock_height > self.header.height {
return Err(Error::KernelLockHeight { lock_height: k.lock_height });
return Err(Error::KernelLockHeight(k.lock_height));
}
}
@@ -504,11 +532,10 @@ impl Block {
}
// verify all signatures with the commitment as pk
if !skip_sig {
for proof in &self.kernels {
proof.verify()?;
}
for proof in &self.kernels {
proof.verify()?;
}
Ok(())
}
@@ -518,19 +545,17 @@ impl Block {
// * That the sum of blinding factors for all coinbase-marked outputs match
// the coinbase-marked kernels.
fn verify_coinbase(&self) -> Result<(), Error> {
let cb_outs = filter_map_vec!(self.outputs, |out| if out.features.contains(
COINBASE_OUTPUT,
)
{
Some(out.commitment())
} else {
None
});
let cb_kerns = filter_map_vec!(self.kernels, |k| if k.features.contains(COINBASE_KERNEL) {
Some(k.excess)
} else {
None
});
let cb_outs = self.outputs
.iter()
.filter(|out| out.features.contains(COINBASE_OUTPUT))
.cloned()
.collect::<Vec<Output>>();
let cb_kerns = self.kernels
.iter()
.filter(|kernel| kernel.features.contains(COINBASE_KERNEL))
.cloned()
.collect::<Vec<TxKernel>>();
let over_commit;
let out_adjust_sum;
@@ -539,8 +564,14 @@ impl Block {
let secp = static_secp_instance();
let secp = secp.lock().unwrap();
over_commit = secp.commit_value(reward(self.total_fees()))?;
out_adjust_sum = secp.commit_sum(cb_outs, vec![over_commit])?;
kerns_sum = secp.commit_sum(cb_kerns, vec![])?;
out_adjust_sum = secp.commit_sum(
cb_outs.iter().map(|x| x.commitment()).collect(),
vec![over_commit],
)?;
kerns_sum = secp.commit_sum(
cb_kerns.iter().map(|x| x.excess).collect(),
vec![],
)?;
}
if kerns_sum != out_adjust_sum {
@@ -549,15 +580,53 @@ impl Block {
Ok(())
}
/// NOTE: this happens during apply_block (not the earlier validate_block)
///
/// Calculate lock_height as block_height + 1,000
/// Confirm height <= lock_height
pub fn verify_coinbase_maturity(
&self,
input: &Input,
height: u64,
) -> Result<(), Error> {
let output = OutputIdentifier::from_input(&input);
// We should only be calling verify_coinbase_maturity
// if the sender claims we are spending a coinbase output
// _and_ that we trust this claim.
// We should have already confirmed the entry from the MMR exists
// and has the expected hash.
assert!(output.features.contains(COINBASE_OUTPUT));
if let Some(_) = self.outputs
.iter()
.find(|x| OutputIdentifier::from_output(&x) == output)
{
let lock_height = self.header.height + global::coinbase_maturity();
if lock_height > height {
Err(Error::ImmatureCoinbase{
height: height,
lock_height: lock_height,
})
} else {
Ok(())
}
} else {
Err(Error::Other(format!("output not found in block")))
}
}
/// Builds the blinded output and related signature proof for the block reward.
pub fn reward_output(
keychain: &keychain::Keychain,
key_id: &keychain::Identifier,
fees: u64,
height: u64,
) -> Result<(Output, TxKernel), keychain::Error> {
let commit = keychain.commit(reward(fees), key_id)?;
let switch_commit = keychain.switch_commit(key_id)?;
let switch_commit_hash = SwitchCommitHash::from_switch_commit(switch_commit);
trace!(
LOGGER,
"Block reward - Pedersen Commit is: {:?}, Switch Commit is: {:?}",
@@ -585,15 +654,22 @@ impl Block {
let out_commit = output.commitment();
let excess = secp.commit_sum(vec![out_commit], vec![over_commit])?;
let msg = util::secp::Message::from_slice(&[0; secp::constants::MESSAGE_SIZE])?;
let sig = keychain.aggsig_sign_from_key_id(&msg, &key_id).unwrap();
// NOTE: Remember we sign the fee *and* the lock_height.
// For a coinbase output the fee is 0 and the lock_height is
// the lock_height of the coinbase output itself,
// not the lock_height of the tx (there is no tx for a coinbase output).
// This output will not be spendable earlier than lock_height (and we sign this here).
let msg = secp::Message::from_slice(&kernel_sig_msg(0, height))?;
let sig = keychain.aggsig_sign_from_key_id(&msg, &key_id)?;
let proof = TxKernel {
features: COINBASE_KERNEL,
excess: excess,
excess_sig: sig,
fee: 0,
lock_height: 0,
// lock_height here is the height of the block (tx should be valid immediately)
// *not* the lock_height of the coinbase output (only spendable 1,000 blocks later)
lock_height: height,
};
Ok((output, proof))
}
@@ -602,6 +678,7 @@ impl Block {
#[cfg(test)]
mod test {
use super::*;
use core::hash::ZERO_HASH;
use core::Transaction;
use core::build::{self, input, output, with_fee};
use core::test::tx2i1o;
@@ -633,7 +710,7 @@ mod test {
key_id2: Identifier,
) -> Transaction {
build::transaction(
vec![input(v, key_id1), output(3, key_id2), with_fee(2)],
vec![input(v, ZERO_HASH, key_id1), output(3, key_id2), with_fee(2)],
&keychain,
).map(|(tx, _)| tx)
.unwrap()
@@ -657,7 +734,7 @@ mod test {
}
let now = Instant::now();
parts.append(&mut vec![input(500000, pks.pop().unwrap()), with_fee(2)]);
parts.append(&mut vec![input(500000, ZERO_HASH, pks.pop().unwrap()), with_fee(2)]);
let mut tx = build::transaction(parts, &keychain)
.map(|(tx, _)| tx)
.unwrap();
@@ -677,7 +754,7 @@ mod test {
let mut btx1 = tx2i1o();
let (mut btx2, _) = build::transaction(
vec![input(7, key_id1), output(5, key_id2.clone()), with_fee(2)],
vec![input(7, ZERO_HASH, key_id1), output(5, key_id2.clone()), with_fee(2)],
&keychain,
).unwrap();
@@ -705,7 +782,7 @@ mod test {
let mut btx1 = tx2i1o();
let (mut btx2, _) = build::transaction(
vec![input(7, key_id1), output(5, key_id2.clone()), with_fee(2)],
vec![input(7, ZERO_HASH, key_id1), output(5, key_id2.clone()), with_fee(2)],
&keychain,
).unwrap();
@@ -767,7 +844,7 @@ mod test {
b.verify_coinbase(),
Err(Error::CoinbaseSumMismatch)
);
assert_eq!(b.verify_kernels(false), Ok(()));
assert_eq!(b.verify_kernels(), Ok(()));
assert_eq!(
b.validate(),
@@ -789,7 +866,6 @@ mod test {
b.verify_coinbase(),
Err(Error::Secp(secp::Error::IncorrectCommitSum))
);
assert_eq!(b.verify_kernels(true), Ok(()));
assert_eq!(
b.validate(),
+50 -9
View File
@@ -27,10 +27,11 @@
use util::{secp, kernel_sig_msg};
use core::{Input, Output, SwitchCommitHash, Transaction, DEFAULT_OUTPUT};
use util::LOGGER;
use core::{Transaction, Input, Output, OutputFeatures, SwitchCommitHash, COINBASE_OUTPUT, DEFAULT_OUTPUT};
use core::hash::Hash;
use keychain;
use keychain::{BlindSum, BlindingFactor, Identifier, Keychain};
use keychain::{Keychain, BlindSum, BlindingFactor, Identifier};
use util::LOGGER;
/// Context information available to transaction combinators.
pub struct Context<'a> {
@@ -43,17 +44,56 @@ pub type Append = for<'a> Fn(&'a mut Context, (Transaction, BlindSum)) -> (Trans
/// Adds an input with the provided value and blinding key to the transaction
/// being built.
pub fn input(value: u64, key_id: Identifier) -> Box<Append> {
fn build_input(
value: u64,
features: OutputFeatures,
out_block: Option<Hash>,
key_id: Identifier,
) -> Box<Append> {
Box::new(move |build, (tx, sum)| -> (Transaction, BlindSum) {
let commit = build.keychain.commit(value, &key_id).unwrap();
(tx.with_input(Input(commit)), sum.sub_key_id(key_id.clone()))
let input = Input::new(
features,
commit,
out_block,
);
(tx.with_input(input), sum.sub_key_id(key_id.clone()))
})
}
/// Adds an input with the provided value and blinding key to the transaction
/// being built.
pub fn input(
value: u64,
out_block: Hash,
key_id: Identifier,
) -> Box<Append> {
debug!(LOGGER, "Building input (spending regular output): {}, {}", value, key_id);
build_input(value, DEFAULT_OUTPUT, Some(out_block), key_id)
}
/// Adds a coinbase input spending a coinbase output.
/// We will use the block hash to verify coinbase maturity.
pub fn coinbase_input(
value: u64,
out_block: Hash,
key_id: Identifier,
) -> Box<Append> {
debug!(LOGGER, "Building input (spending coinbase): {}, {}", value, key_id);
build_input(value, COINBASE_OUTPUT, Some(out_block), key_id)
}
/// Adds an output with the provided value and key identifier from the
/// keychain.
pub fn output(value: u64, key_id: Identifier) -> Box<Append> {
Box::new(move |build, (tx, sum)| -> (Transaction, BlindSum) {
debug!(
LOGGER,
"Building an output: {}, {}",
value,
key_id,
);
let commit = build.keychain.commit(value, &key_id).unwrap();
let switch_commit = build.keychain.switch_commit(&key_id).unwrap();
let switch_commit_hash = SwitchCommitHash::from_switch_commit(switch_commit);
@@ -61,7 +101,7 @@ pub fn output(value: u64, key_id: Identifier) -> Box<Append> {
LOGGER,
"Builder - Pedersen Commit is: {:?}, Switch Commit is: {:?}",
commit,
switch_commit
switch_commit,
);
trace!(
LOGGER,
@@ -145,6 +185,7 @@ pub fn transaction(
#[cfg(test)]
mod test {
use super::*;
use core::hash::ZERO_HASH;
#[test]
fn blind_simple_tx() {
@@ -155,8 +196,8 @@ mod test {
let (tx, _) = transaction(
vec![
input(10, key_id1),
input(11, key_id2),
input(10, ZERO_HASH, key_id1),
input(11, ZERO_HASH, key_id2),
output(20, key_id3),
with_fee(1),
],
@@ -173,7 +214,7 @@ mod test {
let key_id2 = keychain.derive_key_id(2).unwrap();
let (tx, _) = transaction(
vec![input(6, key_id1), output(2, key_id2), with_fee(4)],
vec![input(6, ZERO_HASH, key_id1), output(2, key_id2), with_fee(4)],
&keychain,
).unwrap();
+17
View File
@@ -25,6 +25,7 @@ use blake2::blake2b::Blake2b;
use consensus;
use ser::{self, AsFixedBytes, Error, Readable, Reader, Writeable, Writer};
use util;
use util::LOGGER;
/// A hash consisting of all zeroes, used as a sentinel. No known preimage.
@@ -65,6 +66,22 @@ impl Hash {
pub fn to_vec(&self) -> Vec<u8> {
self.0.to_vec()
}
/// The "zero" hash. No known preimage.
pub fn zero() -> Hash {
ZERO_HASH
}
/// Convert a hash to hex string format.
pub fn to_hex(&self) -> String {
util::to_hex(self.to_vec())
}
/// Convert hex string back to hash.
pub fn from_hex(hex: &str) -> Result<Hash, Error> {
let bytes = util::from_hex(hex.to_string()).unwrap();
Ok(Hash::from_vec(bytes))
}
}
impl ops::Index<usize> for Hash {
+12 -13
View File
@@ -28,7 +28,6 @@ use std::cmp::Ordering;
use std::num::ParseFloatError;
use consensus::GRIN_BASE;
use core::target::Difficulty;
use util::{secp, static_secp_instance};
use util::secp::pedersen::*;
@@ -211,6 +210,7 @@ pub fn amount_to_hr_string(amount: u64) -> String {
#[cfg(test)]
mod test {
use super::*;
use core::target::Difficulty;
use core::hash::ZERO_HASH;
use core::build::{initial_tx, input, output, with_excess, with_fee, with_lock_height};
use core::block::Error::KernelLockHeight;
@@ -248,7 +248,7 @@ mod test {
// blinding should fail as signing with a zero r*G shouldn't work
build::transaction(
vec![
input(10, key_id1.clone()),
input(10, ZERO_HASH, key_id1.clone()),
output(9, key_id1.clone()),
with_fee(1),
],
@@ -260,10 +260,9 @@ mod test {
fn simple_tx_ser() {
let tx = tx2i1o();
let mut vec = Vec::new();
ser::serialize(&mut vec, &tx).expect("serialized failed");
ser::serialize(&mut vec, &tx).expect("serialization failed");
println!("{}", vec.len());
assert!(vec.len() > 5340);
assert!(vec.len() < 5370);
assert!(vec.len() == 5352);
}
#[test]
@@ -304,7 +303,7 @@ mod test {
let (tx, _) = build::transaction(
vec![
input(75, key_id1),
input(75, ZERO_HASH, key_id1),
output(42, key_id2),
output(32, key_id3),
with_fee(1),
@@ -359,7 +358,7 @@ mod test {
{
// Alice gets 2 of her pre-existing outputs to send 5 coins to Bob, they
// become inputs in the new transaction
let (in1, in2) = (input(4, key_id1), input(3, key_id2));
let (in1, in2) = (input(4, ZERO_HASH, key_id1), input(3, ZERO_HASH, key_id2));
// Alice builds her transaction, with change, which also produces the sum
// of blinding factors before they're obscured.
@@ -448,7 +447,7 @@ mod test {
// and that the resulting block is valid
let tx1 = build::transaction(
vec![
input(5, key_id1.clone()),
input(5, ZERO_HASH, key_id1.clone()),
output(3, key_id2.clone()),
with_fee(2),
with_lock_height(1),
@@ -469,7 +468,7 @@ mod test {
// now try adding a timelocked tx where lock height is greater than current block height
let tx1 = build::transaction(
vec![
input(5, key_id1.clone()),
input(5, ZERO_HASH, key_id1.clone()),
output(3, key_id2.clone()),
with_fee(2),
with_lock_height(2),
@@ -486,7 +485,7 @@ mod test {
Difficulty::minimum(),
).unwrap();
match b.validate() {
Err(KernelLockHeight { lock_height: height }) => {
Err(KernelLockHeight(height)) => {
assert_eq!(height, 2);
}
_ => panic!("expecting KernelLockHeight error here"),
@@ -514,8 +513,8 @@ mod test {
build::transaction(
vec![
input(10, key_id1),
input(11, key_id2),
input(10, ZERO_HASH, key_id1),
input(11, ZERO_HASH, key_id2),
output(19, key_id3),
with_fee(2),
],
@@ -531,7 +530,7 @@ mod test {
let key_id2 = keychain.derive_key_id(2).unwrap();
build::transaction(
vec![input(5, key_id1), output(3, key_id2), with_fee(2)],
vec![input(5, ZERO_HASH, key_id1), output(3, key_id2), with_fee(2)],
&keychain,
).map(|(tx, _)| tx)
.unwrap()
+255 -55
View File
@@ -17,20 +17,25 @@ use blake2::blake2b::blake2b;
use util::secp::{self, Message, Signature};
use util::{static_secp_instance, kernel_sig_msg};
use util::secp::pedersen::{Commitment, RangeProof};
use std::cmp::min;
use std::cmp::Ordering;
use std::ops;
use consensus;
use consensus::VerifySortOrder;
use core::Committed;
use core::hash::Hashed;
use core::hash::{Hash, Hashed, ZERO_HASH};
use core::pmmr::Summable;
use keychain::{Identifier, Keychain};
use ser::{self, read_and_verify_sorted, Readable, Reader, Writeable, WriteableSorted, Writer};
use util;
/// The size to use for the stored blake2 hash of a switch_commitment
pub const SWITCH_COMMIT_HASH_SIZE: usize = 20;
/// The size of the secret key used to generate the switch commitment hash (blake2)
pub const SWITCH_COMMIT_KEY_SIZE: usize = 20;
bitflags! {
/// Options for a kernel's structure or use
pub flags KernelFeatures: u8 {
@@ -69,13 +74,16 @@ macro_rules! hashable_ord {
pub enum Error {
/// Transaction fee can't be odd, due to half fee burning
OddFee,
/// Underlying Secp256k1 error (signature validation or invalid public
/// key typically)
/// Underlying Secp256k1 error (signature validation or invalid public key typically)
Secp(secp::Error),
/// Restrict number of incoming inputs
TooManyInputs,
/// Underlying consensus error (currently for sort order)
ConsensusError(consensus::Error),
/// Error originating from an invalid lock-height
LockHeight(u64),
/// Error originating from an invalid switch commitment (coinbase lock_height related)
SwitchCommitment,
}
impl From<secp::Error> for Error {
@@ -149,14 +157,12 @@ impl TxKernel {
/// as a public key and checking the signature verifies with the fee as
/// message.
pub fn verify(&self) -> Result<(), secp::Error> {
let msg = try!(Message::from_slice(
&kernel_sig_msg(self.fee, self.lock_height),
));
let msg = Message::from_slice(&kernel_sig_msg(self.fee, self.lock_height))?;
let secp = static_secp_instance();
let secp = secp.lock().unwrap();
let sig = &self.excess_sig;
let valid = Keychain::aggsig_verify_single_from_commit(&secp, &sig, &msg, &self.excess);
if !valid{
if !valid {
return Err(secp::Error::IncorrectSignature);
}
Ok(())
@@ -172,8 +178,7 @@ pub struct Transaction {
pub outputs: Vec<Output>,
/// Fee paid by the transaction.
pub fee: u64,
/// Transaction is not valid before this block height.
/// It is invalid for this to be less than the lock_height of any UTXO being spent.
/// Transaction is not valid before this chain height.
pub lock_height: u64,
/// The signature proving the excess is a valid public key, which signs
/// the transaction fee.
@@ -184,7 +189,6 @@ pub struct Transaction {
/// write the transaction as binary.
impl Writeable for Transaction {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
println!("Excess sig write: {:?}", self.excess_sig);
ser_multiwrite!(
writer,
[write_u64, self.fee],
@@ -335,7 +339,7 @@ impl Transaction {
// pretend the sum is a public key (which it is, being of the form r.G) and
// verify the transaction sig with it
let valid = Keychain::aggsig_verify_single_from_commit(&secp, &sig, &msg, &rsum);
if !valid{
if !valid {
return Err(secp::Error::IncorrectSignature);
}
Ok(rsum)
@@ -377,10 +381,23 @@ impl Transaction {
}
}
/// A transaction input, mostly a reference to an output being spent by the
/// transaction.
#[derive(Debug, Copy, Clone)]
pub struct Input(pub Commitment);
/// A transaction input.
///
/// Primarily a reference to an output being spent by the transaction.
/// But also information required to verify coinbase maturity through
/// the lock_height hashed in the switch_commit_hash.
#[derive(Debug, Clone, Copy)]
pub struct Input{
/// The features of the output being spent.
/// We will check maturity for coinbase output.
pub features: OutputFeatures,
/// The commit referencing the output being spent.
pub commit: Commitment,
/// The hash of the block the output originated from.
/// Currently we only care about this for coinbase outputs.
/// TODO - include the merkle proof here once we support these.
pub out_block: Option<Hash>,
}
hashable_ord!(Input);
@@ -388,7 +405,14 @@ hashable_ord!(Input);
/// an Input as binary.
impl Writeable for Input {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_fixed_bytes(&self.0)
writer.write_u8(self.features.bits())?;
writer.write_fixed_bytes(&self.commit)?;
if self.features.contains(COINBASE_OUTPUT) {
writer.write_fixed_bytes(&self.out_block.unwrap_or(ZERO_HASH))?;
}
Ok(())
}
}
@@ -396,16 +420,49 @@ impl Writeable for Input {
/// an Input from a binary stream.
impl Readable for Input {
fn read(reader: &mut Reader) -> Result<Input, ser::Error> {
Ok(Input(Commitment::read(reader)?))
let features = OutputFeatures::from_bits(reader.read_u8()?).ok_or(
ser::Error::CorruptedData,
)?;
let commit = Commitment::read(reader)?;
let out_block = if features.contains(COINBASE_OUTPUT) {
Some(Hash::read(reader)?)
} else {
None
};
Ok(Input::new(
features,
commit,
out_block,
))
}
}
/// The input for a transaction, which spends a pre-existing output. The input
/// commitment is a reproduction of the commitment of the output it's spending.
/// The input for a transaction, which spends a pre-existing unspent output.
/// The input commitment is a reproduction of the commitment of the output being spent.
/// Input must also provide the original output features and the hash of the block
/// the output originated from.
impl Input {
/// Extracts the referenced commitment from a transaction output
/// Build a new input from the data required to identify and verify an output beng spent.
pub fn new(
features: OutputFeatures,
commit: Commitment,
out_block: Option<Hash>,
) -> Input {
Input {
features,
commit,
out_block,
}
}
/// The input commitment which _partially_ identifies the output being spent.
/// In the presence of a fork we need additional info to uniquely identify the output.
/// Specifically the block hash (so correctly calculate lock_height for coinbase outputs).
pub fn commitment(&self) -> Commitment {
self.0
self.commit
}
}
@@ -422,15 +479,23 @@ bitflags! {
/// Definition of the switch commitment hash
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct SwitchCommitHash {
/// simple hash
pub hash: [u8; SWITCH_COMMIT_HASH_SIZE],
pub struct SwitchCommitHashKey ([u8; SWITCH_COMMIT_KEY_SIZE]);
impl SwitchCommitHashKey {
/// We use a zero value key for regular transactions.
pub fn zero() -> SwitchCommitHashKey {
SwitchCommitHashKey([0; SWITCH_COMMIT_KEY_SIZE])
}
}
/// Definition of the switch commitment hash
#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct SwitchCommitHash([u8; SWITCH_COMMIT_HASH_SIZE]);
/// Implementation of Writeable for a switch commitment hash
impl Writeable for SwitchCommitHash {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_fixed_bytes(&self.hash)?;
writer.write_fixed_bytes(&self.0)?;
Ok(())
}
}
@@ -444,31 +509,62 @@ impl Readable for SwitchCommitHash {
for i in 0..SWITCH_COMMIT_HASH_SIZE {
c[i] = a[i];
}
Ok(SwitchCommitHash { hash: c })
Ok(SwitchCommitHash(c))
}
}
// As Ref for AsFixedBytes
impl AsRef<[u8]> for SwitchCommitHash {
fn as_ref(&self) -> &[u8] {
&self.hash
&self.0
}
}
impl ::std::fmt::Debug for SwitchCommitHash {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
try!(write!(f, "{}(", stringify!(SwitchCommitHash)));
try!(write!(f, "{}", self.to_hex()));
write!(f, ")")
}
}
impl SwitchCommitHash {
/// Builds a switch commitment hash from a switch commit using blake2
/// Builds a switch commit hash from a switch commit using blake2
pub fn from_switch_commit(switch_commit: Commitment) -> SwitchCommitHash {
let switch_commit_hash = blake2b(SWITCH_COMMIT_HASH_SIZE, &[], &switch_commit.0);
// always use the "zero" key for now
let key = SwitchCommitHashKey::zero();
let switch_commit_hash = blake2b(SWITCH_COMMIT_HASH_SIZE, &key.0, &switch_commit.0);
let switch_commit_hash = switch_commit_hash.as_bytes();
let mut h = [0; SWITCH_COMMIT_HASH_SIZE];
for i in 0..SWITCH_COMMIT_HASH_SIZE {
h[i] = switch_commit_hash[i];
}
SwitchCommitHash { hash: h }
SwitchCommitHash(h)
}
/// Reconstructs a switch commit hash from an array of bytes.
pub fn from_bytes(bytes: &[u8]) -> SwitchCommitHash {
let mut hash = [0; SWITCH_COMMIT_HASH_SIZE];
for i in 0..min(SWITCH_COMMIT_HASH_SIZE, bytes.len()) {
hash[i] = bytes[i];
}
SwitchCommitHash(hash)
}
/// Hex string represenation of a switch commitment hash.
pub fn to_hex(&self) -> String {
util::to_hex(self.0.to_vec())
}
/// Reconstrcuts a switch commit hash from a hex string.
pub fn from_hex(hex: &str) -> Result<SwitchCommitHash, ser::Error> {
let bytes = util::from_hex(hex.to_string())
.map_err(|_| ser::Error::HexError(format!("switch_commit_hash from_hex error")))?;
Ok(SwitchCommitHash::from_bytes(&bytes))
}
/// Build an "zero" switch commitment hash
pub fn zero() -> SwitchCommitHash {
SwitchCommitHash { hash: [0; SWITCH_COMMIT_HASH_SIZE] }
SwitchCommitHash([0; SWITCH_COMMIT_HASH_SIZE])
}
}
@@ -476,18 +572,18 @@ impl SwitchCommitHash {
/// transferred. The commitment is a blinded value for the output while the
/// range proof guarantees the commitment includes a positive value without
/// overflow and the ownership of the private key. The switch commitment hash
/// provides future-proofing against quantum-based attacks, as well as provides
/// provides future-proofing against quantum-based attacks, as well as providing
/// wallet implementations with a way to identify their outputs for wallet
/// reconstruction
/// reconstruction.
///
/// The hash of an output only covers its features, lock_height, commitment,
/// The hash of an output only covers its features, commitment,
/// and switch commitment. The range proof is expected to have its own hash
/// and is stored and committed to separately.
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct Output {
/// Options for an output's structure or use
pub features: OutputFeatures,
/// The homomorphic commitment representing the output's amount
/// The homomorphic commitment representing the output amount
pub commit: Commitment,
/// The switch commitment hash, a 160 bit length blake2 hash of blind*J
pub switch_commit_hash: SwitchCommitHash,
@@ -569,24 +665,127 @@ impl Output {
}
}
/// Wrapper to Output commitments to provide the Summable trait.
#[derive(Clone, Debug)]
pub struct SumCommit {
/// An output_identifier can be build from either an input _or_ and output and
/// contains everything we need to uniquely identify an output being spent.
/// Needed because it is not sufficient to pass a commitment around.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct OutputIdentifier {
/// Output features (coinbase vs. regular transaction output)
/// We need to include this when hashing to ensure coinbase maturity can be enforced.
pub features: OutputFeatures,
/// Output commitment
pub commit: Commitment,
}
impl OutputIdentifier {
/// Build a new output_identifier.
pub fn new(features: OutputFeatures, commit: &Commitment) -> OutputIdentifier {
OutputIdentifier {
features: features.clone(),
commit: commit.clone(),
}
}
/// Build an output_identifier from an existing output.
pub fn from_output(output: &Output) -> OutputIdentifier {
OutputIdentifier {
features: output.features,
commit: output.commit,
}
}
/// Build an output_identifier from an existing input.
pub fn from_input(input: &Input) -> OutputIdentifier {
OutputIdentifier {
features: input.features,
commit: input.commit,
}
}
/// convert an output_identifier to hex string format.
pub fn to_hex(&self) -> String {
format!(
"{:b}{}",
self.features.bits(),
util::to_hex(self.commit.0.to_vec()),
)
}
/// Convert an output_indentifier to a sum_commit representation
/// so we can use it to query the the output MMR
pub fn as_sum_commit(&self) -> SumCommit {
SumCommit::new(self.features, &self.commit)
}
/// Convert a sum_commit back to an output_identifier.
pub fn from_sum_commit(sum_commit: &SumCommit) -> OutputIdentifier {
OutputIdentifier::new(sum_commit.features, &sum_commit.commit)
}
}
impl Writeable for OutputIdentifier {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_u8(self.features.bits())?;
self.commit.write(writer)?;
Ok(())
}
}
impl Readable for OutputIdentifier {
fn read(reader: &mut Reader) -> Result<OutputIdentifier, ser::Error> {
let features = OutputFeatures::from_bits(reader.read_u8()?).ok_or(
ser::Error::CorruptedData,
)?;
Ok(OutputIdentifier {
commit: Commitment::read(reader)?,
features: features,
})
}
}
/// Wrapper to Output commitments to provide the Summable trait.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SumCommit {
/// Output features (coinbase vs. regular transaction output)
/// We need to include this when hashing to ensure coinbase maturity can be enforced.
pub features: OutputFeatures,
/// Output commitment
pub commit: Commitment,
/// The corresponding "switch commit hash"
pub switch_commit_hash: SwitchCommitHash,
}
impl SumCommit {
/// For when we do not care about the switch_commit_hash
/// for example when comparing sum_commit hashes
pub fn from_commit(commit: &Commitment) -> SumCommit {
/// Build a new sum_commit.
pub fn new(features: OutputFeatures, commit: &Commitment) -> SumCommit {
SumCommit {
features: features.clone(),
commit: commit.clone(),
switch_commit_hash: SwitchCommitHash::zero(),
}
}
/// Build a new sum_commit from an existing output.
pub fn from_output(output: &Output) -> SumCommit {
SumCommit {
features: output.features,
commit: output.commit,
}
}
/// Build a new sum_commit from an existing input.
pub fn from_input(input: &Input) -> SumCommit {
SumCommit {
features: input.features,
commit: input.commit,
}
}
/// Convert a sum_commit to hex string.
pub fn to_hex(&self) -> String {
format!(
"{:b}{}",
self.features.bits(),
util::to_hex(self.commit.0.to_vec()),
)
}
}
/// Outputs get summed through their commitments.
@@ -596,33 +795,31 @@ impl Summable for SumCommit {
fn sum(&self) -> SumCommit {
SumCommit {
commit: self.commit.clone(),
switch_commit_hash: self.switch_commit_hash.clone(),
features: self.features.clone(),
}
}
fn sum_len() -> usize {
secp::constants::PEDERSEN_COMMITMENT_SIZE + SWITCH_COMMIT_HASH_SIZE
secp::constants::PEDERSEN_COMMITMENT_SIZE + 1
}
}
impl Writeable for SumCommit {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_u8(self.features.bits())?;
self.commit.write(writer)?;
if writer.serialization_mode() == ser::SerializationMode::Full {
self.switch_commit_hash.write(writer)?;
}
Ok(())
}
}
impl Readable for SumCommit {
fn read(reader: &mut Reader) -> Result<SumCommit, ser::Error> {
let commit = Commitment::read(reader)?;
let switch_commit_hash = SwitchCommitHash::read(reader)?;
let features = OutputFeatures::from_bits(reader.read_u8()?).ok_or(
ser::Error::CorruptedData,
)?;
Ok(SumCommit {
commit: commit,
switch_commit_hash: switch_commit_hash,
commit: Commitment::read(reader)?,
features: features,
})
}
}
@@ -642,7 +839,10 @@ impl ops::Add for SumCommit {
Ok(s) => s,
Err(_) => Commitment::from_vec(vec![1; 33]),
};
SumCommit::from_commit(&sum)
SumCommit::new(
self.features | other.features,
&sum,
)
}
}
+4
View File
@@ -50,6 +50,8 @@ pub enum Error {
TooLargeReadErr,
/// Consensus rule failure (currently sort order)
ConsensusError(consensus::Error),
/// Error from from_hex deserialization
HexError(String),
}
impl From<io::Error> for Error {
@@ -75,6 +77,7 @@ impl fmt::Display for Error {
Error::CorruptedData => f.write_str("corrupted data"),
Error::TooLargeReadErr => f.write_str("too large read"),
Error::ConsensusError(ref e) => write!(f, "consensus error {:?}", e),
Error::HexError(ref e) => write!(f, "hex error {:?}", e),
}
}
}
@@ -97,6 +100,7 @@ impl error::Error for Error {
Error::CorruptedData => "corrupted data",
Error::TooLargeReadErr => "too large read",
Error::ConsensusError(_) => "consensus error (sort order)",
Error::HexError(_) => "hex error",
}
}
}