verify contract slate commitments

This commit is contained in:
wiesche
2026-09-02 14:03:16 +02:00
parent 893e598512
commit a65ab35310
10 changed files with 693 additions and 61 deletions
+13 -1
View File
@@ -15,7 +15,7 @@
use crate::core::core::FeeFields;
use crate::core::core::{self, amount_to_hr_string};
use crate::core::global;
use crate::libwallet::contract::types::ContractView;
use crate::libwallet::contract::types::{ContractView, OwnCommitmentStatus};
use crate::libwallet::{
AcctPathMapping, Error, OutputCommitMapping, OutputStatus, Slate, TxLogEntry, ViewWallet,
WalletInfo,
@@ -651,6 +651,18 @@ pub fn contract_view(slate: &Slate, view: &ContractView) {
let agreed = format_net_change(view.agreed_net_change);
table.add_row(row![bFC->"Agreed Net Change", bGC->agreed]);
table.add_row(row![bFC->"Executed", bGC->view.is_executed]);
let (unexpected, warning) = match view.own_commitment_status {
OwnCommitmentStatus::UnexpectedInput => ("Input", true),
OwnCommitmentStatus::UnexpectedOutput => ("Output", true),
OwnCommitmentStatus::UnexpectedInputAndOutput => ("Input and output", true),
OwnCommitmentStatus::Clean => ("No", false),
OwnCommitmentStatus::Unknown => ("Unknown", false),
};
if warning {
table.add_row(row![bFC->"Unexpected Own Commitments", bFR->unexpected]);
} else {
table.add_row(row![bFC->"Unexpected Own Commitments", bGC->unexpected]);
}
table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
table.printstd();
+79 -3
View File
@@ -20,8 +20,9 @@ extern crate log;
use grin_wallet_libwallet as libwallet;
use grin_core::consensus;
use grin_core::core::transaction::{Input, OutputFeatures};
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI, OwnCommitmentStatus};
use libwallet::{OutputStatus, Slate, SlateState};
use std::sync::atomic::Ordering;
use std::thread;
@@ -34,11 +35,13 @@ use std::path::PathBuf;
/// contract new with --add-outputs
fn contract_early_lock_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create a single wallet and mine 5 blocks
// create two wallets and mine 5 blocks in each
let (wallets, _chain, stopper, _bh) =
create_wallets(vec![vec![("default", 5)]], test_dir).unwrap();
create_wallets(vec![vec![("default", 5)], vec![("default", 5)]], test_dir).unwrap();
let send_wallet = wallets[0].0.clone();
let send_mask = wallets[0].1.as_ref();
let recv_wallet = wallets[1].0.clone();
let recv_mask = wallets[1].1.as_ref();
// Confirm all our outputs are unspent
wallet::controller::owner_single_use(
@@ -102,6 +105,79 @@ fn contract_early_lock_tx_impl(test_dir: &'static str) -> Result<(), libwallet::
},
)?;
// The receiver signs without seeing the sender's locked commitments.
wallet::controller::owner_single_use(
recv_wallet.clone(),
recv_mask,
PathBuf::from(test_dir),
|api, m| {
slate = api.contract_sign(
m,
&slate,
&ContractSetupArgsAPI {
net_change: Some(80_000_000_000),
..Default::default()
},
)?;
Ok(())
},
)?;
assert_eq!(slate.state, SlateState::Standard2);
// Even a commitment reserved for this contract must not be present before we add it.
wallet::controller::owner_single_use(
send_wallet.clone(),
send_mask,
PathBuf::from(test_dir),
|api, m| {
let (_, outputs) = api.retrieve_outputs(m, false, false, None)?;
let output = outputs
.iter()
.find(|output| output.output.status == OutputStatus::Locked)
.expect("sender has a locked contract input");
let features = if output.output.is_coinbase {
OutputFeatures::Coinbase
} else {
OutputFeatures::Plain
};
let mut changed = slate.clone();
changed.tx = Some(
changed
.tx_or_err()?
.clone()
.with_input(Input::new(features, output.commit)),
);
let view = api.contract_view(m, &changed)?;
assert_eq!(
view.own_commitment_status,
OwnCommitmentStatus::UnexpectedInput
);
let err = api
.contract_sign(m, &changed, &ContractSetupArgsAPI::default())
.unwrap_err();
match err {
libwallet::Error::GenericError(message) => assert_eq!(
message,
"Contract slate contains an unexpected input commitment from this wallet"
),
err => panic!("unexpected error: {}", err),
}
Ok(())
},
)?;
// The sender adds the reserved commitments and completes the contract.
wallet::controller::owner_single_use(
send_wallet.clone(),
send_mask,
PathBuf::from(test_dir),
|api, m| {
slate = api.contract_sign(m, &slate, &ContractSetupArgsAPI::default())?;
Ok(())
},
)?;
assert_eq!(slate.state, SlateState::Standard3);
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
+187 -1
View File
@@ -19,9 +19,10 @@ extern crate log;
use grin_wallet_libwallet as libwallet;
use grin_core::core::transaction::{CommitWrapper, Input, Inputs, OutputFeatures, Transaction};
use impls::test_framework::{self};
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI, OwnCommitmentStatus};
use libwallet::{Slate, SlateState, TxLogEntryType};
use std::sync::atomic::Ordering;
use std::thread;
@@ -78,6 +79,171 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
},
)?;
assert_eq!(slate.state, SlateState::Standard2);
wallet::controller::owner_single_use(
recv_wallet.clone(),
recv_mask,
PathBuf::from(test_dir),
|api, m| {
let view = api.contract_view(m, &slate)?;
assert_eq!(view.own_commitment_status, OwnCommitmentStatus::Clean);
Ok(())
},
)?;
wallet::controller::owner_single_use(
send_wallet.clone(),
send_mask,
PathBuf::from(test_dir),
|api, m| {
let view = api.contract_view(m, &slate)?;
assert_eq!(view.own_commitment_status, OwnCommitmentStatus::Clean);
let mut no_tx = slate.clone();
no_tx.tx = None;
let view = api.contract_view(m, &no_tx)?;
assert_eq!(view.own_commitment_status, OwnCommitmentStatus::Unknown);
Ok(())
},
)?;
// The sender must reject an unused commitment from its wallet if it was inserted by the receiver.
wallet::controller::owner_single_use(
send_wallet.clone(),
send_mask,
PathBuf::from(test_dir),
|api, m| {
let (_, outputs) = api.retrieve_outputs(m, false, false, None)?;
let tx = slate.tx_or_err()?;
let input_commits = Vec::<CommitWrapper>::from(&tx.inputs());
let child_index = {
let mut wallet = api.wallet_inst.lock();
let wallet = wallet.lc_provider()?.wallet_inst()?;
let parent = wallet.parent_key_id();
wallet.current_child_index(&parent)?
};
let mut changed = slate.clone();
changed.tx = Some(
Transaction::new(
Inputs::CommitOnly(input_commits.clone()),
tx.outputs(),
tx.kernels(),
)
.with_offset(tx.offset.clone()),
);
let err = api
.contract_sign(m, &changed, &ContractSetupArgsAPI::default())
.unwrap_err();
match err {
libwallet::Error::GenericError(message) => {
assert_eq!(message, "Contract slate input features are missing")
}
err => panic!("unexpected error: {}", err),
}
let view = api.contract_view(m, &changed)?;
assert_eq!(view.own_commitment_status, OwnCommitmentStatus::Unknown);
let current_child_index = {
let mut wallet = api.wallet_inst.lock();
let wallet = wallet.lc_provider()?.wallet_inst()?;
let parent = wallet.parent_key_id();
wallet.current_child_index(&parent)?
};
assert_eq!(current_child_index, child_index);
let (output, pos) = outputs
.iter()
.find_map(|output| {
let unused = !input_commits
.iter()
.any(|input| input.commitment() == output.commit)
&& !tx
.outputs()
.iter()
.any(|tx_output| tx_output.commitment() == output.commit);
if unused {
chain
.get_output_pos(&output.commit)
.ok()
.map(|pos| (output, pos))
} else {
None
}
})
.expect("sender has an unused confirmed output");
let features = if output.output.is_coinbase {
OutputFeatures::Coinbase
} else {
OutputFeatures::Plain
};
let mut changed = slate.clone();
changed.tx = Some(
changed
.tx_or_err()?
.clone()
.with_input(Input::new(features, output.commit)),
);
let view = api.contract_view(m, &changed)?;
assert_eq!(
view.own_commitment_status,
OwnCommitmentStatus::UnexpectedInput
);
let err = api
.contract_sign(m, &changed, &ContractSetupArgsAPI::default())
.unwrap_err();
match err {
libwallet::Error::GenericError(message) => assert_eq!(
message,
"Contract slate contains an unexpected input commitment from this wallet"
),
err => panic!("unexpected error: {}", err),
}
let mut changed = slate.clone();
changed.tx = Some(
changed
.tx_or_err()?
.clone()
.with_output(chain.get_unspent_output_at(pos).unwrap()),
);
let view = api.contract_view(m, &changed)?;
assert_eq!(
view.own_commitment_status,
OwnCommitmentStatus::UnexpectedOutput
);
let err = api
.contract_sign(m, &changed, &ContractSetupArgsAPI::default())
.unwrap_err();
match err {
libwallet::Error::GenericError(message) => assert_eq!(
message,
"Contract slate contains an unexpected output commitment from this wallet"
),
err => panic!("unexpected error: {}", err),
}
let mut changed = slate.clone();
changed.tx = Some(
changed
.tx_or_err()?
.clone()
.with_input(Input::new(features, output.commit))
.with_output(chain.get_unspent_output_at(pos).unwrap()),
);
let view = api.contract_view(m, &changed)?;
assert_eq!(
view.own_commitment_status,
OwnCommitmentStatus::UnexpectedInputAndOutput
);
let err = api
.contract_sign(m, &changed, &ContractSetupArgsAPI::default())
.unwrap_err();
match err {
libwallet::Error::GenericError(message) => assert_eq!(
message,
"Contract slate contains unexpected input and output commitments from this wallet"
),
err => panic!("unexpected error: {}", err),
}
Ok(())
},
)?;
// Send wallet finalizes and posts
wallet::controller::owner_single_use(
@@ -93,6 +259,26 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
},
)?;
assert_eq!(slate.state, SlateState::Standard3);
wallet::controller::owner_single_use(
recv_wallet.clone(),
recv_mask,
PathBuf::from(test_dir),
|api, m| {
let view = api.contract_view(m, &slate)?;
assert_eq!(view.own_commitment_status, OwnCommitmentStatus::Clean);
Ok(())
},
)?;
wallet::controller::owner_single_use(
send_wallet.clone(),
send_mask,
PathBuf::from(test_dir),
|api, m| {
let view = api.contract_view(m, &slate)?;
assert_eq!(view.own_commitment_status, OwnCommitmentStatus::Unknown);
Ok(())
},
)?;
wallet::controller::owner_single_use(
send_wallet.clone(),