diff --git a/controller/tests/common/mod.rs b/controller/tests/common/mod.rs index 8d56436..435f2e0 100644 --- a/controller/tests/common/mod.rs +++ b/controller/tests/common/mod.rs @@ -22,7 +22,7 @@ use grin_core as core; use grin_keychain as keychain; use grin_util as util; -use self::core::core::{FeeFields, KernelFeatures}; +use self::core::core::{FeeFields, Inputs, KernelFeatures, OutputFeatures}; use self::core::global; use self::core::global::ChainTypes; use self::keychain::{ExtKeychain, Keychain}; @@ -102,6 +102,7 @@ pub struct ExpectedContractSlate { pub amount: u64, pub fee: u64, pub inputs: usize, + pub coinbase_inputs: usize, pub outputs: usize, pub kernels: usize, pub num_participants: u8, @@ -120,8 +121,20 @@ pub fn assert_basic_contract_slate(slate: &libwallet::Slate, expected: ExpectedC assert!(slate.kernel_features_args.is_none()); assert!(slate.payment_proof.is_none()); assert_eq!(slate.num_participants, expected.num_participants); - assert_eq!(tx.inputs().len(), expected.inputs); + let inputs = tx.inputs(); + assert_eq!(inputs.len(), expected.inputs); + match inputs { + Inputs::FeaturesAndCommit(inputs) => assert_eq!( + inputs + .iter() + .filter(|input| input.features == OutputFeatures::Coinbase) + .count(), + expected.coinbase_inputs + ), + Inputs::CommitOnly(_) => panic!("contract inputs must include their features"), + } assert_eq!(tx.outputs().len(), expected.outputs); + assert!(tx.outputs().iter().all(|output| output.is_plain())); assert_eq!(tx.kernels().len(), expected.kernels); assert_eq!(slate.participant_data.len(), expected.participant_data); assert_eq!( diff --git a/controller/tests/contract_rsr.rs b/controller/tests/contract_rsr.rs index 9cdf996..b3a8127 100644 --- a/controller/tests/contract_rsr.rs +++ b/controller/tests/contract_rsr.rs @@ -111,6 +111,7 @@ fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> amount: 5_000_000_000, fee: participant_fee, inputs: 0, + coinbase_inputs: 0, outputs: 0, kernels: 0, num_participants: 2, @@ -153,6 +154,7 @@ fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> amount: 5_000_000_000, fee: 2 * participant_fee, inputs: 1, + coinbase_inputs: 1, outputs: 1, kernels: 1, num_participants: 2, @@ -182,6 +184,7 @@ fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> amount: 5_000_000_000, fee: 2 * participant_fee, inputs: 2, + coinbase_inputs: 2, outputs: 2, kernels: 1, num_participants: 2, diff --git a/controller/tests/contract_self_spend_custom.rs b/controller/tests/contract_self_spend_custom.rs index 92b0aa6..add3ac4 100644 --- a/controller/tests/contract_self_spend_custom.rs +++ b/controller/tests/contract_self_spend_custom.rs @@ -20,6 +20,7 @@ extern crate log; use grin_wallet_libwallet as libwallet; use grin_core::consensus; +use grin_core::core::transaction::CommitWrapper; use impls::test_framework::{self}; use libwallet::contract::my_fee_contribution; use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs}; @@ -43,6 +44,7 @@ fn contract_self_spend_custom_tx_impl(test_dir: &'static str) -> Result<(), libw let mut use_inputs = String::from(""); + let mut plain_input = None; wallet::controller::owner_single_use( send_wallet.clone(), send_mask, @@ -166,6 +168,15 @@ fn contract_self_spend_custom_tx_impl(test_dir: &'static str) -> Result<(), libw // Assert expected outputs were created // 88, 35, 3, 0.2, 15 and a change output assert_eq!(commits[10].output.value, 88 * consensus::GRIN_BASE); + assert!(!commits[10].output.is_coinbase); + plain_input = Some(( + commits[10] + .output + .commit + .clone() + .expect("plain output has no commitment"), + commits[10].commit, + )); assert_eq!(commits[11].output.value, 35 * consensus::GRIN_BASE); assert_eq!(commits[12].output.value, 3 * consensus::GRIN_BASE); assert_eq!( @@ -184,6 +195,50 @@ fn contract_self_spend_custom_tx_impl(test_dir: &'static str) -> Result<(), libw }, )?; + let (plain_input, plain_commitment) = plain_input.expect("plain output was not found"); + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + slate = api.contract_new( + m, + &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(0), + num_participants: 1, + selection_args: OutputSelectionArgs { + minimum_confirmations: Some(1), + use_inputs: Some(plain_input), + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + }, + )?; + slate = api.contract_sign(m, &slate, &ContractSetupArgsAPI::default())?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard2); + common::assert_basic_contract_slate( + &slate, + common::ExpectedContractSlate { + amount: 0, + fee: my_fee_contribution(1, 1, 1, 1)?.fee(), + inputs: 1, + coinbase_inputs: 0, + outputs: 1, + kernels: 1, + num_participants: 1, + participant_data: 1, + signatures: 1, + }, + ); + let inputs = Vec::::from(&slate.tx_or_err()?.inputs()); + assert_eq!(inputs[0].commitment(), plain_commitment); + // let logging finish stopper.store(false, Ordering::Relaxed); thread::sleep(Duration::from_millis(200)); diff --git a/controller/tests/contract_srs.rs b/controller/tests/contract_srs.rs index 4811108..374cd27 100644 --- a/controller/tests/contract_srs.rs +++ b/controller/tests/contract_srs.rs @@ -86,6 +86,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> amount: 5_000_000_000, fee: participant_fee, inputs: 0, + coinbase_inputs: 0, outputs: 0, kernels: 0, num_participants: 2, @@ -201,6 +202,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> amount: 5_000_000_000, fee: 2 * participant_fee, inputs: 1, + coinbase_inputs: 1, outputs: 1, kernels: 1, num_participants: 2, @@ -394,6 +396,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> amount: 5_000_000_000, fee: 2 * participant_fee, inputs: 2, + coinbase_inputs: 2, outputs: 2, kernels: 1, num_participants: 2, diff --git a/libwallet/src/contract/actions/README.md b/libwallet/src/contract/actions/README.md index ec0a7f2..cfa6f59 100644 --- a/libwallet/src/contract/actions/README.md +++ b/libwallet/src/contract/actions/README.md @@ -39,8 +39,6 @@ Separating side effects until the 'save_step' part would make these functions mu - Reserve funds across open late-locked contracts, or make the possible signing failure clearer to callers. - - Handle the coinbase flows. Coinbase inputs are built into the slate, but nothing else - is exercised. - Keep contracts at two participants until the known multi-party attack is addressed. - For a payjoin, `--use-inputs any` picks an input for us; naming the commitment would be clearer, for the API as much as the CLI. diff --git a/libwallet/src/contract/slate.rs b/libwallet/src/contract/slate.rs index 4c1e7d8..e7b3216 100644 --- a/libwallet/src/contract/slate.rs +++ b/libwallet/src/contract/slate.rs @@ -643,14 +643,24 @@ mod tests { assert!(check_identifiers(&[], &[own_input], "input").is_err()); assert!(check_identifiers(&[own_input], &[own_output], "output").is_err()); assert!(check_identifiers(&[own_input, own_input], &[own_input], "input").is_err()); - assert!(check_identifiers( - &[identifier(1, OutputFeatures::Coinbase)], - &[own_input], - "input" - ) - .is_err()); assert!(cached_commitment("01").is_none()); assert!(cached_commitment("not hex").is_none()); assert!(cached_commitment(&"00".repeat(PEDERSEN_COMMITMENT_SIZE)).is_some()); } + + #[test] + fn coinbase_features() { + let commit = Commitment::from_vec(vec![1]); + let plain = OutputIdentifier::new(OutputFeatures::Plain, &commit); + let coinbase = OutputIdentifier::new(OutputFeatures::Coinbase, &commit); + + for (actual, expected) in [(plain, coinbase), (coinbase, plain)] { + let err = check_identifiers(&[actual], &[expected], "input").unwrap_err(); + assert!(matches!( + err, + Error::GenericError(ref message) + if message == "Contract slate changed or duplicated one of our input commitments" + )); + } + } }