mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-10-17 15:21:39 +02:00
verify contract slate commitments
This commit is contained in:
@@ -15,7 +15,7 @@
|
|||||||
use crate::core::core::FeeFields;
|
use crate::core::core::FeeFields;
|
||||||
use crate::core::core::{self, amount_to_hr_string};
|
use crate::core::core::{self, amount_to_hr_string};
|
||||||
use crate::core::global;
|
use crate::core::global;
|
||||||
use crate::libwallet::contract::types::ContractView;
|
use crate::libwallet::contract::types::{ContractView, OwnCommitmentStatus};
|
||||||
use crate::libwallet::{
|
use crate::libwallet::{
|
||||||
AcctPathMapping, Error, OutputCommitMapping, OutputStatus, Slate, TxLogEntry, ViewWallet,
|
AcctPathMapping, Error, OutputCommitMapping, OutputStatus, Slate, TxLogEntry, ViewWallet,
|
||||||
WalletInfo,
|
WalletInfo,
|
||||||
@@ -651,6 +651,18 @@ pub fn contract_view(slate: &Slate, view: &ContractView) {
|
|||||||
let agreed = format_net_change(view.agreed_net_change);
|
let agreed = format_net_change(view.agreed_net_change);
|
||||||
table.add_row(row![bFC->"Agreed Net Change", bGC->agreed]);
|
table.add_row(row![bFC->"Agreed Net Change", bGC->agreed]);
|
||||||
table.add_row(row![bFC->"Executed", bGC->view.is_executed]);
|
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.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||||
table.printstd();
|
table.printstd();
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ extern crate log;
|
|||||||
use grin_wallet_libwallet as libwallet;
|
use grin_wallet_libwallet as libwallet;
|
||||||
|
|
||||||
use grin_core::consensus;
|
use grin_core::consensus;
|
||||||
|
use grin_core::core::transaction::{Input, OutputFeatures};
|
||||||
use libwallet::contract::my_fee_contribution;
|
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 libwallet::{OutputStatus, Slate, SlateState};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
@@ -34,11 +35,13 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
/// contract new with --add-outputs
|
/// contract new with --add-outputs
|
||||||
fn contract_early_lock_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
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) =
|
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_wallet = wallets[0].0.clone();
|
||||||
let send_mask = wallets[0].1.as_ref();
|
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
|
// Confirm all our outputs are unspent
|
||||||
wallet::controller::owner_single_use(
|
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
|
// let logging finish
|
||||||
stopper.store(false, Ordering::Relaxed);
|
stopper.store(false, Ordering::Relaxed);
|
||||||
thread::sleep(Duration::from_millis(200));
|
thread::sleep(Duration::from_millis(200));
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ extern crate log;
|
|||||||
|
|
||||||
use grin_wallet_libwallet as libwallet;
|
use grin_wallet_libwallet as libwallet;
|
||||||
|
|
||||||
|
use grin_core::core::transaction::{CommitWrapper, Input, Inputs, OutputFeatures, Transaction};
|
||||||
use impls::test_framework::{self};
|
use impls::test_framework::{self};
|
||||||
use libwallet::contract::my_fee_contribution;
|
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 libwallet::{Slate, SlateState, TxLogEntryType};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::thread;
|
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);
|
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
|
// Send wallet finalizes and posts
|
||||||
wallet::controller::owner_single_use(
|
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);
|
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(
|
wallet::controller::owner_single_use(
|
||||||
send_wallet.clone(),
|
send_wallet.clone(),
|
||||||
|
|||||||
+5
-3
@@ -40,8 +40,11 @@ transaction has spent the available outputs first.
|
|||||||
## View and revoke
|
## View and revoke
|
||||||
|
|
||||||
`view` reads a slate or Slatepack and shows the participants, signatures, suggested or
|
`view` reads a slate or Slatepack and shows the participants, signatures, suggested or
|
||||||
agreed amount change, and whether the local transaction is confirmed. It does not show all
|
agreed amount change, whether the local transaction is confirmed, and whether it contains
|
||||||
inputs, outputs and fees, and it cannot find a contract by id.
|
unexpected inputs or outputs from this wallet. This check is reported as unknown after
|
||||||
|
the private context has been removed, when input features are missing, or when the slate
|
||||||
|
does not contain a transaction. `view` does not show all inputs, outputs and fees, and it
|
||||||
|
cannot find a contract by id.
|
||||||
|
|
||||||
`revoke` cancels the local transaction. When the wallet added an input, it returns a
|
`revoke` cancels the local transaction. When the wallet added an input, it returns a
|
||||||
self-spend of that input. The caller still has to post it, and either transaction can win
|
self-spend of that input. The caller still has to post it, and either transaction can win
|
||||||
@@ -53,7 +56,6 @@ account selected with `--account`.
|
|||||||
## Current limitations
|
## Current limitations
|
||||||
|
|
||||||
* Only one or two participants are supported
|
* Only one or two participants are supported
|
||||||
* A returned slate is not yet checked against every input and output saved during setup
|
|
||||||
* Early payment proofs are only implemented for contracts and are only available through
|
* Early payment proofs are only implemented for contracts and are only available through
|
||||||
the API. Only invoice proofs are supported and they require the experimental Slate V5
|
the API. Only invoice proofs are supported and they require the experimental Slate V5
|
||||||
format. The full proposal is described in
|
format. The full proposal is described in
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ only added to the slate when signing, so the counterparty does not see them earl
|
|||||||
Ideally we'd also separate side effects out of these functions e.g. computing the current_height
|
Ideally we'd also separate side effects out of these functions e.g. computing the current_height
|
||||||
or refreshing the outputs with updater::refresh_outputs(...). The current_height could be
|
or refreshing the outputs with updater::refresh_outputs(...). The current_height could be
|
||||||
communicated through a &ChainState parameter which would collect these values before the call.
|
communicated through a &ChainState parameter which would collect these values before the call.
|
||||||
Additionally, we could fetch the existing Context before the call to avoid doing db fetch.
|
Setup still reads an existing Context again; passing it through would avoid that DB read.
|
||||||
Separating side effects until the 'save_step' part would make these functions much easier to test.
|
Separating side effects until the 'save_step' part would make these functions much easier to test.
|
||||||
|
|
||||||
#### TODOs
|
#### TODOs
|
||||||
@@ -41,8 +41,6 @@ Separating side effects until the 'save_step' part would make these functions mu
|
|||||||
only `--add-outputs` locks during `new`.
|
only `--add-outputs` locks during `new`.
|
||||||
- Reserve funds across open late-locked contracts, or make the possible signing failure
|
- Reserve funds across open late-locked contracts, or make the possible signing failure
|
||||||
clearer to callers.
|
clearer to callers.
|
||||||
- Check the commitments we contributed are the ones that come back, so a counterparty
|
|
||||||
can't return a slate carrying inputs or outputs of ours that we didn't put there.
|
|
||||||
- Handle the coinbase flows. Coinbase inputs are built into the slate, but nothing else
|
- Handle the coinbase flows. Coinbase inputs are built into the slate, but nothing else
|
||||||
is exercised.
|
is exercised.
|
||||||
- For a payjoin, `--use-inputs any` picks an input for us; naming the commitment would be
|
- For a payjoin, `--use-inputs any` picks an input for us; naming the commitment would be
|
||||||
@@ -77,7 +75,7 @@ Separating side effects until the 'save_step' part would make these functions mu
|
|||||||
- signing the same slate twice, which `verify_not_signed` guards
|
- signing the same slate twice, which `verify_not_signed` guards
|
||||||
- zero-value outputs, and `--make-outputs` beyond what is available
|
- zero-value outputs, and `--make-outputs` beyond what is available
|
||||||
- a receiver with no spendable input, through lack of funds or of confirmations
|
- a receiver with no spendable input, through lack of funds or of confirmations
|
||||||
- invalid or inconsistent slates, including changed commitments and unexpected states
|
- invalid or inconsistent slates and unexpected states
|
||||||
- the CLI conflict between `--no-payjoin` and `--use-inputs`
|
- the CLI conflict between `--no-payjoin` and `--use-inputs`
|
||||||
- a committed fee that no longer covers the selected inputs and outputs
|
- a committed fee that no longer covers the selected inputs and outputs
|
||||||
- slate contents at each step, not only the end state
|
- slate contents at each step, not only the end state
|
||||||
@@ -102,6 +100,8 @@ Separating side effects until the 'save_step' part would make these functions mu
|
|||||||
|
|
||||||
#### Sign
|
#### Sign
|
||||||
// Side-effects:
|
// Side-effects:
|
||||||
// - contract_utils::check_already_signed -> tx_log_iter
|
// - contract_utils::verify_not_signed -> tx_log_iter
|
||||||
// - contract_utils::get_net_change -> context and net_change
|
// - sign -> w.get_private_context(...)
|
||||||
|
// - verify_incoming_own_commitments -> w.iter()
|
||||||
|
// - verify_own_commitments -> w.get(...)
|
||||||
// - everything from 'setup'
|
// - everything from 'setup'
|
||||||
|
|||||||
@@ -37,13 +37,23 @@ where
|
|||||||
{
|
{
|
||||||
// Compute if we will add outputs at this step. Only a missing context means we
|
// Compute if we will add outputs at this step. Only a missing context means we
|
||||||
// still need to add them; propagate any other error.
|
// still need to add them; propagate any other error.
|
||||||
let will_add_outputs = match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
|
let existing_context = match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
|
||||||
Ok(ctx) => ctx.get_inputs().len() + ctx.get_outputs().len() == 0,
|
Ok(context) => Some(context),
|
||||||
Err(Error::NotFoundErr(_)) => true,
|
Err(Error::NotFoundErr(_)) => None,
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
};
|
};
|
||||||
|
let will_add_outputs = existing_context
|
||||||
|
.as_ref()
|
||||||
|
.map(|context| context.get_inputs().len() + context.get_outputs().len() == 0)
|
||||||
|
.unwrap_or(true);
|
||||||
// Compute state for 'sign'
|
// Compute state for 'sign'
|
||||||
let (sl, mut context) = compute(w, keychain_mask, slate, setup_args)?;
|
let (sl, mut context) = compute(
|
||||||
|
w,
|
||||||
|
keychain_mask,
|
||||||
|
slate,
|
||||||
|
setup_args,
|
||||||
|
existing_context.as_ref(),
|
||||||
|
)?;
|
||||||
|
|
||||||
// Atomically commit state
|
// Atomically commit state
|
||||||
contract::utils::save_step(w, keychain_mask, &sl, &mut context, will_add_outputs, true)?;
|
contract::utils::save_step(w, keychain_mask, &sl, &mut context, will_add_outputs, true)?;
|
||||||
@@ -57,6 +67,7 @@ pub fn compute<C, K>(
|
|||||||
keychain_mask: Option<&SecretKey>,
|
keychain_mask: Option<&SecretKey>,
|
||||||
slate: &Slate,
|
slate: &Slate,
|
||||||
setup_args: &ContractSetupArgsAPI,
|
setup_args: &ContractSetupArgsAPI,
|
||||||
|
existing_context: Option<&Context>,
|
||||||
) -> Result<(Slate, Context), Error>
|
) -> Result<(Slate, Context), Error>
|
||||||
where
|
where
|
||||||
C: NodeClient,
|
C: NodeClient,
|
||||||
@@ -67,18 +78,20 @@ where
|
|||||||
|
|
||||||
// Ensure net_change has been provided
|
// Ensure net_change has been provided
|
||||||
let expected_net_change =
|
let expected_net_change =
|
||||||
contract::utils::get_net_change(w, keychain_mask, &sl.id, setup_args.net_change)?;
|
contract::utils::get_net_change(existing_context, setup_args.net_change)?;
|
||||||
|
|
||||||
// Define the values that must be provided in the setup phase at the sign step
|
// Define the values that must be provided in the setup phase at the sign step
|
||||||
let mut setup_args = setup_args.clone();
|
let mut setup_args = setup_args.clone();
|
||||||
setup_args.net_change = Some(expected_net_change);
|
setup_args.net_change = Some(expected_net_change);
|
||||||
setup_args.num_participants = sl.num_participants;
|
setup_args.num_participants = sl.num_participants;
|
||||||
setup_args.add_outputs = true; // we add outputs to the Context in case we haven't done that yet
|
setup_args.add_outputs = true; // we add outputs to the Context in case we haven't done that yet
|
||||||
|
contract::slate::verify_incoming_own_commitments(w, keychain_mask, &sl)?;
|
||||||
|
|
||||||
// Ensure Setup phase is done and that inputs/outputs have been added to the Context
|
// Ensure Setup phase is done and that inputs/outputs have been added to the Context
|
||||||
let (mut sl, mut context) = setup::compute(w, keychain_mask, &mut sl, &setup_args)?;
|
let (mut sl, mut context) = setup::compute(w, keychain_mask, &mut sl, &setup_args)?;
|
||||||
// Add outputs to the slate, verify the payment proof and sign the slate
|
// Add outputs to the slate, verify the payment proof and sign the slate
|
||||||
contract::slate::add_outputs(w, keychain_mask, &mut sl, &context)?;
|
contract::slate::add_outputs(w, keychain_mask, &mut sl, &context)?;
|
||||||
|
contract::slate::verify_own_commitments(w, keychain_mask, &sl, &context)?;
|
||||||
contract::slate::verify_invoice_promise(&sl, &w.keychain(keychain_mask)?, &context)?;
|
contract::slate::verify_invoice_promise(&sl, &w.keychain(keychain_mask)?, &context)?;
|
||||||
|
|
||||||
contract::slate::sign(w, keychain_mask, &mut sl, &mut context)?;
|
contract::slate::sign(w, keychain_mask, &mut sl, &mut context)?;
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
//! Implementation of contract view
|
//! Implementation of contract view
|
||||||
|
|
||||||
use crate::backend::WalletBackend;
|
use crate::backend::WalletBackend;
|
||||||
use crate::contract::types::ContractView;
|
use crate::contract;
|
||||||
|
use crate::contract::types::{ContractView, OwnCommitmentStatus};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::grin_keychain::Keychain;
|
use crate::grin_keychain::Keychain;
|
||||||
use crate::grin_util::secp::key::SecretKey;
|
use crate::grin_util::secp::key::SecretKey;
|
||||||
@@ -63,13 +64,32 @@ where
|
|||||||
slate.num_participants
|
slate.num_participants
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
let context = match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
|
||||||
|
Ok(context) => Some(context),
|
||||||
|
Err(Error::NotFoundErr(_)) => None,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
let suggested_net_change = suggested_net_change(&slate.state, slate.amount)?;
|
let suggested_net_change = suggested_net_change(&slate.state, slate.amount)?;
|
||||||
// A contract is executed once the transaction it produced has confirmed. That is
|
// A contract is executed once the transaction it produced has confirmed. That is
|
||||||
// recorded on our own tx log entry for this slate, so no chain lookup is needed; a
|
// recorded on our own tx log entry for this slate, so no chain lookup is needed; a
|
||||||
// slate we have never signed simply has no entry and is not executed.
|
// slate we have never signed simply has no entry and is not executed.
|
||||||
let is_executed = updater::retrieve_txs(w, None, Some(slate.id), None, None, false)?
|
let txs = updater::retrieve_txs(w, None, Some(slate.id), None, None, false)?;
|
||||||
|
let is_executed = txs.iter().any(|tx| tx.confirmed);
|
||||||
|
let has_signed = txs
|
||||||
.iter()
|
.iter()
|
||||||
.any(|tx| tx.confirmed);
|
.any(|tx| contract::utils::is_signed_tx(tx, slate.id));
|
||||||
|
// Once we have signed, the context identifies the commitments we added. If it has
|
||||||
|
// already been removed, the slate no longer carries enough information to do that.
|
||||||
|
let own_commitment_status = if slate.tx.is_none() || (context.is_none() && !txs.is_empty()) {
|
||||||
|
OwnCommitmentStatus::Unknown
|
||||||
|
} else {
|
||||||
|
contract::slate::own_commitment_status(
|
||||||
|
w,
|
||||||
|
keychain_mask,
|
||||||
|
slate,
|
||||||
|
if has_signed { context.as_ref() } else { None },
|
||||||
|
)?
|
||||||
|
};
|
||||||
// Count signatures present (a participant is "complete" once it has a partial sig).
|
// Count signatures present (a participant is "complete" once it has a partial sig).
|
||||||
let num_sigs = slate
|
let num_sigs = slate
|
||||||
.participant_data
|
.participant_data
|
||||||
@@ -79,11 +99,10 @@ where
|
|||||||
.count();
|
.count();
|
||||||
|
|
||||||
// If we have a local context for this slate we've agreed on a net change; surface it.
|
// If we have a local context for this slate we've agreed on a net change; surface it.
|
||||||
let agreed_net_change = match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
|
let agreed_net_change = context
|
||||||
Ok(ctx) => ctx.setup_args.as_ref().and_then(|args| args.net_change),
|
.as_ref()
|
||||||
Err(Error::NotFoundErr(_)) => None,
|
.and_then(|context| context.setup_args.as_ref())
|
||||||
Err(e) => return Err(e),
|
.and_then(|args| args.net_change);
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Maybe we can know if the slate was meant for us if it was encrypted for us.
|
// TODO: Maybe we can know if the slate was meant for us if it was encrypted for us.
|
||||||
// A possible issue is that one can encrypt the same slate for 10 people.
|
// A possible issue is that one can encrypt the same slate for 10 people.
|
||||||
@@ -93,6 +112,7 @@ where
|
|||||||
agreed_net_change,
|
agreed_net_change,
|
||||||
num_sigs: num_sigs as u8,
|
num_sigs: num_sigs as u8,
|
||||||
is_executed: is_executed,
|
is_executed: is_executed,
|
||||||
|
own_commitment_status,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
Ok(ct_view)
|
Ok(ct_view)
|
||||||
|
|||||||
@@ -15,16 +15,21 @@
|
|||||||
//! Contract functions on the Slate
|
//! Contract functions on the Slate
|
||||||
|
|
||||||
use crate::backend::WalletBackend;
|
use crate::backend::WalletBackend;
|
||||||
|
use crate::grin_core::core::transaction::{Inputs, OutputFeatures, OutputIdentifier};
|
||||||
use crate::grin_core::libtx::build;
|
use crate::grin_core::libtx::build;
|
||||||
use crate::grin_core::libtx::proof::ProofBuilder;
|
use crate::grin_core::libtx::proof::ProofBuilder;
|
||||||
use crate::grin_keychain::Keychain;
|
use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType};
|
||||||
|
use crate::grin_util::from_hex;
|
||||||
|
use crate::grin_util::secp::constants::PEDERSEN_COMMITMENT_SIZE;
|
||||||
use crate::grin_util::secp::key::{PublicKey, SecretKey};
|
use crate::grin_util::secp::key::{PublicKey, SecretKey};
|
||||||
|
use crate::grin_util::secp::pedersen::Commitment;
|
||||||
use crate::slate::{PaymentProofType, Slate, SlateState};
|
use crate::slate::{PaymentProofType, Slate, SlateState};
|
||||||
use crate::types::{Context, NodeClient};
|
use crate::types::{Context, NodeClient, OutputData};
|
||||||
use crate::util::OnionV3Address;
|
use crate::util::OnionV3Address;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use super::types::ProofArgs;
|
use super::types::{OwnCommitmentStatus, ProofArgs};
|
||||||
use crate::contract::proofs::InvoiceProof;
|
use crate::contract::proofs::InvoiceProof;
|
||||||
|
|
||||||
/// Add payment proof data to slate, noop for sender
|
/// Add payment proof data to slate, noop for sender
|
||||||
@@ -121,6 +126,243 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn derive_commitment<K>(keychain: &K, id: &Identifier, amount: u64) -> Result<Commitment, Error>
|
||||||
|
where
|
||||||
|
K: Keychain,
|
||||||
|
{
|
||||||
|
keychain
|
||||||
|
.commit(amount, id, SwitchCommitmentType::Regular)
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cached_commitment(commit: &str) -> Option<Commitment> {
|
||||||
|
from_hex(commit)
|
||||||
|
.ok()
|
||||||
|
.filter(|bytes| bytes.len() == PEDERSEN_COMMITMENT_SIZE)
|
||||||
|
.map(Commitment::from_vec)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stored_commitment<K>(keychain: &K, output: &OutputData) -> Result<Commitment, Error>
|
||||||
|
where
|
||||||
|
K: Keychain,
|
||||||
|
{
|
||||||
|
// Commitments written by the wallet are used directly. Rebuild malformed cache entries.
|
||||||
|
if let Some(commit) = output.commit.as_deref().and_then(cached_commitment) {
|
||||||
|
return Ok(commit);
|
||||||
|
}
|
||||||
|
derive_commitment(keychain, &output.key_id, output.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_identifier<K>(
|
||||||
|
keychain: &K,
|
||||||
|
id: &Identifier,
|
||||||
|
amount: u64,
|
||||||
|
features: OutputFeatures,
|
||||||
|
) -> Result<OutputIdentifier, Error>
|
||||||
|
where
|
||||||
|
K: Keychain,
|
||||||
|
{
|
||||||
|
Ok(OutputIdentifier::new(
|
||||||
|
features,
|
||||||
|
&derive_commitment(keychain, id, amount)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_identifiers(
|
||||||
|
actual: &[OutputIdentifier],
|
||||||
|
expected: &[OutputIdentifier],
|
||||||
|
kind: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
for expected in expected {
|
||||||
|
let mut matches = actual
|
||||||
|
.iter()
|
||||||
|
.filter(|actual| actual.commitment() == expected.commitment());
|
||||||
|
match matches.next() {
|
||||||
|
None => {
|
||||||
|
return Err(Error::GenericError(format!(
|
||||||
|
"Contract slate is missing one of our {} commitments",
|
||||||
|
kind
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Some(actual) if actual != expected || matches.next().is_some() => {
|
||||||
|
return Err(Error::GenericError(format!(
|
||||||
|
"Contract slate changed or duplicated one of our {} commitments",
|
||||||
|
kind
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Some(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_identifiers<C, K>(
|
||||||
|
w: &WalletBackend<C, K>,
|
||||||
|
keychain: &K,
|
||||||
|
context: &Context,
|
||||||
|
) -> Result<(Vec<OutputIdentifier>, Vec<OutputIdentifier>), Error>
|
||||||
|
where
|
||||||
|
C: NodeClient,
|
||||||
|
K: Keychain,
|
||||||
|
{
|
||||||
|
let expected_inputs = context
|
||||||
|
.input_ids
|
||||||
|
.iter()
|
||||||
|
.map(|(id, mmr_index, amount)| {
|
||||||
|
let output = w.get(id, mmr_index)?;
|
||||||
|
derive_identifier(
|
||||||
|
keychain,
|
||||||
|
id,
|
||||||
|
*amount,
|
||||||
|
if output.is_coinbase {
|
||||||
|
OutputFeatures::Coinbase
|
||||||
|
} else {
|
||||||
|
OutputFeatures::Plain
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
let expected_outputs = context
|
||||||
|
.output_ids
|
||||||
|
.iter()
|
||||||
|
.map(|(id, _, amount)| derive_identifier(keychain, id, *amount, OutputFeatures::Plain))
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
Ok((expected_inputs, expected_outputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
const MISSING_INPUT_FEATURES: &str = "Contract slate input features are missing";
|
||||||
|
|
||||||
|
/// Compare a slate with commitments owned by this wallet. A signed context
|
||||||
|
/// identifies commitments that this wallet has already added.
|
||||||
|
pub(super) fn own_commitment_status<C, K>(
|
||||||
|
w: &WalletBackend<C, K>,
|
||||||
|
keychain_mask: Option<&SecretKey>,
|
||||||
|
slate: &Slate,
|
||||||
|
signed_context: Option<&Context>,
|
||||||
|
) -> Result<OwnCommitmentStatus, Error>
|
||||||
|
where
|
||||||
|
C: NodeClient,
|
||||||
|
K: Keychain,
|
||||||
|
{
|
||||||
|
let keychain = w.keychain(keychain_mask)?;
|
||||||
|
let tx = slate.tx_or_err()?;
|
||||||
|
let inputs = match tx.inputs() {
|
||||||
|
// Unknown only means missing input features here. View also uses Unknown when
|
||||||
|
// the private context has already been removed.
|
||||||
|
Inputs::CommitOnly(_) => return Ok(OwnCommitmentStatus::Unknown),
|
||||||
|
Inputs::FeaturesAndCommit(inputs) => inputs,
|
||||||
|
};
|
||||||
|
let (expected_inputs, expected_outputs) = match signed_context {
|
||||||
|
Some(context) => {
|
||||||
|
let (inputs, outputs) = expected_identifiers(w, &keychain, context)?;
|
||||||
|
(
|
||||||
|
inputs
|
||||||
|
.into_iter()
|
||||||
|
.map(|input| input.commitment())
|
||||||
|
.collect::<BTreeSet<_>>(),
|
||||||
|
outputs
|
||||||
|
.into_iter()
|
||||||
|
.map(|output| output.commitment())
|
||||||
|
.collect::<BTreeSet<_>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => (BTreeSet::new(), BTreeSet::new()),
|
||||||
|
};
|
||||||
|
let incoming_inputs = inputs
|
||||||
|
.into_iter()
|
||||||
|
.map(|input| input.commitment())
|
||||||
|
.filter(|commit| !expected_inputs.contains(commit))
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let incoming_outputs = tx
|
||||||
|
.outputs()
|
||||||
|
.iter()
|
||||||
|
.map(|output| output.commitment())
|
||||||
|
.filter(|commit| !expected_outputs.contains(commit))
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
if incoming_inputs.is_empty() && incoming_outputs.is_empty() {
|
||||||
|
return Ok(OwnCommitmentStatus::Clean);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut unexpected_input = false;
|
||||||
|
let mut unexpected_output = false;
|
||||||
|
for output in w.iter()? {
|
||||||
|
let commit = stored_commitment(&keychain, &output)?;
|
||||||
|
unexpected_input |= incoming_inputs.contains(&commit);
|
||||||
|
unexpected_output |= incoming_outputs.contains(&commit);
|
||||||
|
if (unexpected_input || incoming_inputs.is_empty())
|
||||||
|
&& (unexpected_output || incoming_outputs.is_empty())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(match (unexpected_input, unexpected_output) {
|
||||||
|
(true, true) => OwnCommitmentStatus::UnexpectedInputAndOutput,
|
||||||
|
(true, false) => OwnCommitmentStatus::UnexpectedInput,
|
||||||
|
(false, true) => OwnCommitmentStatus::UnexpectedOutput,
|
||||||
|
(false, false) => OwnCommitmentStatus::Clean,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reject commitments from this wallet before contract setup reserves any keys.
|
||||||
|
pub(super) fn verify_incoming_own_commitments<C, K>(
|
||||||
|
w: &WalletBackend<C, K>,
|
||||||
|
keychain_mask: Option<&SecretKey>,
|
||||||
|
slate: &Slate,
|
||||||
|
) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
C: NodeClient,
|
||||||
|
K: Keychain,
|
||||||
|
{
|
||||||
|
let found = match own_commitment_status(w, keychain_mask, slate, None)? {
|
||||||
|
OwnCommitmentStatus::UnexpectedInput => "an unexpected input commitment",
|
||||||
|
OwnCommitmentStatus::UnexpectedOutput => "an unexpected output commitment",
|
||||||
|
OwnCommitmentStatus::UnexpectedInputAndOutput => "unexpected input and output commitments",
|
||||||
|
OwnCommitmentStatus::Unknown => {
|
||||||
|
return Err(Error::GenericError(MISSING_INPUT_FEATURES.to_string()))
|
||||||
|
}
|
||||||
|
OwnCommitmentStatus::Clean => return Ok(()),
|
||||||
|
};
|
||||||
|
Err(Error::GenericError(format!(
|
||||||
|
"Contract slate contains {} from this wallet",
|
||||||
|
found
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check that the slate still contains the commitments selected for this contract.
|
||||||
|
pub(super) fn verify_own_commitments<C, K>(
|
||||||
|
w: &WalletBackend<C, K>,
|
||||||
|
keychain_mask: Option<&SecretKey>,
|
||||||
|
slate: &Slate,
|
||||||
|
context: &Context,
|
||||||
|
) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
C: NodeClient,
|
||||||
|
K: Keychain,
|
||||||
|
{
|
||||||
|
let keychain = w.keychain(keychain_mask)?;
|
||||||
|
let (expected_inputs, expected_outputs) = expected_identifiers(w, &keychain, context)?;
|
||||||
|
let tx = slate.tx_or_err()?;
|
||||||
|
match tx.inputs() {
|
||||||
|
Inputs::CommitOnly(_) => {
|
||||||
|
return Err(Error::GenericError(MISSING_INPUT_FEATURES.to_string()))
|
||||||
|
}
|
||||||
|
Inputs::FeaturesAndCommit(inputs) => {
|
||||||
|
let inputs = inputs
|
||||||
|
.iter()
|
||||||
|
.map(OutputIdentifier::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
check_identifiers(&inputs, &expected_inputs, "input")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let actual_outputs = tx
|
||||||
|
.outputs()
|
||||||
|
.iter()
|
||||||
|
.map(|output| output.identifier())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
check_identifiers(&actual_outputs, &expected_outputs, "output")
|
||||||
|
}
|
||||||
|
|
||||||
/// Contribute inputs to slate
|
/// Contribute inputs to slate
|
||||||
fn add_inputs_to_slate<C, K>(
|
fn add_inputs_to_slate<C, K>(
|
||||||
w: &mut WalletBackend<C, K>,
|
w: &mut WalletBackend<C, K>,
|
||||||
@@ -388,4 +630,26 @@ mod tests {
|
|||||||
assert_eq!(slate.state, to);
|
assert_eq!(slate.state, to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn checks_own_commitments() {
|
||||||
|
let identifier =
|
||||||
|
|value, features| OutputIdentifier::new(features, &Commitment::from_vec(vec![value]));
|
||||||
|
let own_input = identifier(1, OutputFeatures::Plain);
|
||||||
|
let own_output = identifier(2, OutputFeatures::Plain);
|
||||||
|
|
||||||
|
check_identifiers(&[own_input], &[own_input], "input").unwrap();
|
||||||
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,29 @@ pub struct ContractView {
|
|||||||
pub num_sigs: u8,
|
pub num_sigs: u8,
|
||||||
/// Has the contract been executed on chain
|
/// Has the contract been executed on chain
|
||||||
pub is_executed: bool,
|
pub is_executed: bool,
|
||||||
|
/// Whether the slate contains an unexpected commitment from this wallet.
|
||||||
|
/// This is unknown when the private context is no longer available, input
|
||||||
|
/// features are missing, or the slate contains no transaction.
|
||||||
|
#[serde(default)]
|
||||||
|
pub own_commitment_status: OwnCommitmentStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of comparing a contract slate with commitments owned by this wallet
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum OwnCommitmentStatus {
|
||||||
|
/// No unexpected wallet commitment was found
|
||||||
|
Clean,
|
||||||
|
/// The slate contains an unexpected input from this wallet
|
||||||
|
UnexpectedInput,
|
||||||
|
/// The slate contains an unexpected output from this wallet
|
||||||
|
UnexpectedOutput,
|
||||||
|
/// The slate contains an unexpected input and output from this wallet
|
||||||
|
UnexpectedInputAndOutput,
|
||||||
|
/// The comparison could not be made
|
||||||
|
#[default]
|
||||||
|
#[serde(other)]
|
||||||
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ContractView {
|
impl Default for ContractView {
|
||||||
@@ -189,6 +212,7 @@ impl Default for ContractView {
|
|||||||
agreed_net_change: None,
|
agreed_net_change: None,
|
||||||
num_sigs: 0,
|
num_sigs: 0,
|
||||||
is_executed: false,
|
is_executed: false,
|
||||||
|
own_commitment_status: OwnCommitmentStatus::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,3 +225,43 @@ pub struct ContractRevokeArgsAPI {
|
|||||||
/// Account containing the transaction, or the active account when omitted
|
/// Account containing the transaction, or the active account when omitted
|
||||||
pub src_acct_name: Option<String>,
|
pub src_acct_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn own_commitment_status_json() {
|
||||||
|
for (status, value) in [
|
||||||
|
(OwnCommitmentStatus::Clean, "clean"),
|
||||||
|
(OwnCommitmentStatus::UnexpectedInput, "unexpected_input"),
|
||||||
|
(OwnCommitmentStatus::UnexpectedOutput, "unexpected_output"),
|
||||||
|
(
|
||||||
|
OwnCommitmentStatus::UnexpectedInputAndOutput,
|
||||||
|
"unexpected_input_and_output",
|
||||||
|
),
|
||||||
|
(OwnCommitmentStatus::Unknown, "unknown"),
|
||||||
|
] {
|
||||||
|
let json = format!("\"{}\"", value);
|
||||||
|
assert_eq!(serde_json::to_string(&status).unwrap(), json);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<OwnCommitmentStatus>(&json).unwrap(),
|
||||||
|
status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<OwnCommitmentStatus>("\"future_status\"").unwrap(),
|
||||||
|
OwnCommitmentStatus::Unknown
|
||||||
|
);
|
||||||
|
|
||||||
|
let view: ContractView = serde_json::from_value(serde_json::json!({
|
||||||
|
"num_participants": 2,
|
||||||
|
"suggested_net_change": null,
|
||||||
|
"agreed_net_change": null,
|
||||||
|
"num_sigs": 0,
|
||||||
|
"is_executed": false
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(view.own_commitment_status, OwnCommitmentStatus::Unknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -136,41 +136,31 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get net_change value. This is obtained either from the Context.net_change or the setup_args.net_change
|
/// Get net_change value. This is obtained either from the Context.net_change or the setup_args.net_change
|
||||||
pub fn get_net_change<C, K>(
|
pub fn get_net_change(
|
||||||
w: &mut WalletBackend<C, K>,
|
context: Option<&Context>,
|
||||||
keychain_mask: Option<&SecretKey>,
|
|
||||||
slate_id: &Uuid,
|
|
||||||
setup_args_net_change: Option<i64>,
|
setup_args_net_change: Option<i64>,
|
||||||
) -> Result<i64, Error>
|
) -> Result<i64, Error> {
|
||||||
where
|
|
||||||
C: NodeClient,
|
|
||||||
K: Keychain,
|
|
||||||
{
|
|
||||||
let mut expected_net_change: Option<i64> = setup_args_net_change;
|
let mut expected_net_change: Option<i64> = setup_args_net_change;
|
||||||
match w.get_private_context(keychain_mask, slate_id.as_bytes()) {
|
if let Some(context) = context {
|
||||||
Ok(ctx) => {
|
debug!("contract::sign => context found");
|
||||||
debug!("contract::sign => context found");
|
// We have a context so we must have agreed on a certain net_change value in Context.net_change.
|
||||||
// We have a context so we must have agreed on a certain net_change value in Context.net_change.
|
// If we have both Context.net_change and setup_args.net_change, then they must be equal.
|
||||||
// If we have both Context.net_change and setup_args.net_change, then they must be equal.
|
let ctx_net_change = context.get_net_change()?;
|
||||||
let ctx_net_change = ctx.get_net_change()?;
|
match expected_net_change {
|
||||||
match expected_net_change {
|
Some(args_net_change) => {
|
||||||
Some(args_net_change) => {
|
if ctx_net_change != args_net_change {
|
||||||
if ctx_net_change != args_net_change {
|
return Err(Error::GenericError(format!(
|
||||||
return Err(Error::GenericError(format!(
|
"Expected net change mismatch! Context.net_change: {}, setup_args.net_change: {}",
|
||||||
"Expected net change mismatch! Context.net_change: {}, setup_args.net_change: {}",
|
ctx_net_change, args_net_change
|
||||||
ctx_net_change, args_net_change
|
)));
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => (),
|
|
||||||
}
|
}
|
||||||
expected_net_change = Some(ctx_net_change);
|
None => (),
|
||||||
}
|
}
|
||||||
Err(Error::NotFoundErr(_)) => {
|
expected_net_change = Some(ctx_net_change);
|
||||||
debug!("contract::utils::get_net_change => context not found")
|
} else {
|
||||||
}
|
debug!("contract::utils::get_net_change => context not found")
|
||||||
Err(e) => return Err(e),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Fail if net_change was not passed to setup_args and was also not present in the context.
|
// Fail if net_change was not passed to setup_args and was also not present in the context.
|
||||||
// This means it has not been explicitly agreed on and we require the user to pass it.
|
// This means it has not been explicitly agreed on and we require the user to pass it.
|
||||||
@@ -357,6 +347,11 @@ pub fn my_fee_contribution(
|
|||||||
Ok(my_fee_fields)
|
Ok(my_fee_fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a transaction log entry shows that this wallet signed the slate.
|
||||||
|
pub(super) fn is_signed_tx(tx: &TxLogEntry, slate_id: Uuid) -> bool {
|
||||||
|
tx.tx_slate_id == Some(slate_id) && tx.kernel_excess.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns an error if the slate has already been signed (in our local database). Even if the
|
/// Returns an error if the slate has already been signed (in our local database). Even if the
|
||||||
/// result is Ok, it's still possible it was signed but we don't have the data about it locally.
|
/// result is Ok, it's still possible it was signed but we don't have the data about it locally.
|
||||||
pub fn verify_not_signed<C, K>(w: &mut WalletBackend<C, K>, slate_id: Uuid) -> Result<(), Error>
|
pub fn verify_not_signed<C, K>(w: &mut WalletBackend<C, K>, slate_id: Uuid) -> Result<(), Error>
|
||||||
@@ -368,7 +363,7 @@ where
|
|||||||
// we have already signed this slate.
|
// we have already signed this slate.
|
||||||
for tx in w.tx_log_iter()? {
|
for tx in w.tx_log_iter()? {
|
||||||
let tx = tx?;
|
let tx = tx?;
|
||||||
if tx.tx_slate_id == Some(slate_id) && tx.kernel_excess.is_some() {
|
if is_signed_tx(&tx, slate_id) {
|
||||||
debug!("contract::utils::verify_not_signed => The slate has already been signed.");
|
debug!("contract::utils::verify_not_signed => The slate has already been signed.");
|
||||||
return Err(Error::GenericError(format!(
|
return Err(Error::GenericError(format!(
|
||||||
"Slate with id:{} has already been signed.",
|
"Slate with id:{} has already been signed.",
|
||||||
|
|||||||
Reference in New Issue
Block a user