mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-10-17 07:11:36 +02:00
derive contract signing state
This commit is contained in:
@@ -20,6 +20,7 @@ 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 grin_core::core::transaction::{CommitWrapper, Input, Inputs, OutputFeatures, Transaction};
|
||||||
|
use grin_util::secp::Signature;
|
||||||
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, OwnCommitmentStatus};
|
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI, OwnCommitmentStatus};
|
||||||
@@ -94,6 +95,30 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
|||||||
signatures: 0,
|
signatures: 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
wallet::controller::owner_single_use(
|
||||||
|
send_wallet.clone(),
|
||||||
|
send_mask,
|
||||||
|
PathBuf::from(test_dir),
|
||||||
|
|api, m| {
|
||||||
|
let progress = common::wallet_progress(api, m)?;
|
||||||
|
let mut invalid = slate.clone();
|
||||||
|
invalid.participant_data[0].part_sig =
|
||||||
|
Some(Signature::from_raw_data(&[0; 64]).unwrap());
|
||||||
|
let setup_args = ContractSetupArgsAPI {
|
||||||
|
selection_args: common::contract_selection_args(),
|
||||||
|
net_change: Some(-5_000_000_000),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let err = {
|
||||||
|
let mut owner = api.wallet_inst.lock();
|
||||||
|
let wallet = owner.lc_provider()?.wallet_inst()?;
|
||||||
|
libwallet::contract::setup(wallet, m, &invalid, &setup_args).unwrap_err()
|
||||||
|
};
|
||||||
|
assert!(matches!(err, libwallet::Error::LibTX(_)));
|
||||||
|
assert_eq!(common::wallet_progress(api, m)?, progress);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
wallet::controller::owner_single_use(
|
wallet::controller::owner_single_use(
|
||||||
recv_wallet.clone(),
|
recv_wallet.clone(),
|
||||||
@@ -192,6 +217,18 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
|||||||
if msg == &format!("Slate with id:{} has already been signed.", incoming.id)
|
if msg == &format!("Slate with id:{} has already been signed.", incoming.id)
|
||||||
));
|
));
|
||||||
assert_eq!(common::wallet_progress(api, m)?, progress);
|
assert_eq!(common::wallet_progress(api, m)?, progress);
|
||||||
|
|
||||||
|
let err = {
|
||||||
|
let mut owner = api.wallet_inst.lock();
|
||||||
|
let wallet = owner.lc_provider()?.wallet_inst()?;
|
||||||
|
libwallet::contract::setup(wallet, m, &slate, &args).unwrap_err()
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
libwallet::Error::GenericError(ref msg)
|
||||||
|
if msg == &format!("Slate with id:{} has already been signed.", slate.id)
|
||||||
|
));
|
||||||
|
assert_eq!(common::wallet_progress(api, m)?, progress);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -65,16 +65,12 @@ Separating side effects until the 'save_step' part would make these functions mu
|
|||||||
- Decide whether `contract_accounts_switch.rs` is a legitimate scenario, or whether
|
- Decide whether `contract_accounts_switch.rs` is a legitimate scenario, or whether
|
||||||
signing under a different account than setup should error.
|
signing under a different account than setup should error.
|
||||||
|
|
||||||
#### save_step
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// - is_signed should be derived from the slate
|
|
||||||
// - Consider taking ownership of Context here. It should not be used after this is called.
|
|
||||||
|
|
||||||
### Side-effects
|
### Side-effects
|
||||||
|
|
||||||
#### Setup
|
#### Setup
|
||||||
|
// Persisting setup::compute() requires verify_not_signed first
|
||||||
// Side-effects:
|
// Side-effects:
|
||||||
|
// - contract_utils::verify_not_signed -> tx_log_iter
|
||||||
// - height = w.w2n_client().get_chain_tip()?.0;
|
// - height = w.w2n_client().get_chain_tip()?.0;
|
||||||
// - maybe_context = w.get_private_context(keychain_mask, sl.id.as_bytes())
|
// - maybe_context = w.get_private_context(keychain_mask, sl.id.as_bytes())
|
||||||
// - create_contract_ctx -> updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?;
|
// - create_contract_ctx -> updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?;
|
||||||
|
|||||||
@@ -39,17 +39,10 @@ where
|
|||||||
K: Keychain,
|
K: Keychain,
|
||||||
{
|
{
|
||||||
// Compute state for 'new'
|
// Compute state for 'new'
|
||||||
let (slate, mut context) = compute(w, keychain_mask, setup_args, slate_id)?;
|
let (slate, context) = compute(w, keychain_mask, setup_args, slate_id)?;
|
||||||
|
|
||||||
// Atomically commit state
|
// Atomically commit state
|
||||||
contract::utils::save_step(
|
contract::utils::save_step(w, keychain_mask, &slate, context, setup_args.add_outputs)?;
|
||||||
w,
|
|
||||||
keychain_mask,
|
|
||||||
&slate,
|
|
||||||
&mut context,
|
|
||||||
setup_args.add_outputs,
|
|
||||||
false,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(slate)
|
Ok(slate)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,18 +35,12 @@ where
|
|||||||
C: NodeClient,
|
C: NodeClient,
|
||||||
K: Keychain,
|
K: Keychain,
|
||||||
{
|
{
|
||||||
|
contract::utils::verify_not_signed(w, slate.id)?;
|
||||||
// Compute state for 'setup'
|
// Compute state for 'setup'
|
||||||
let (slate, mut context) = compute(w, keychain_mask, slate, setup_args)?;
|
let (slate, context) = compute(w, keychain_mask, slate, setup_args)?;
|
||||||
|
|
||||||
// Atomically commit state
|
// Atomically commit state
|
||||||
contract::utils::save_step(
|
contract::utils::save_step(w, keychain_mask, &slate, context, setup_args.add_outputs)?;
|
||||||
w,
|
|
||||||
keychain_mask,
|
|
||||||
&slate,
|
|
||||||
&mut context,
|
|
||||||
setup_args.add_outputs,
|
|
||||||
false,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(slate)
|
Ok(slate)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ where
|
|||||||
.map(|context| !context.has_inputs_or_outputs())
|
.map(|context| !context.has_inputs_or_outputs())
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
// Compute state for 'sign'
|
// Compute state for 'sign'
|
||||||
let (sl, mut context) = compute(
|
let (sl, context) = compute(
|
||||||
w,
|
w,
|
||||||
keychain_mask,
|
keychain_mask,
|
||||||
slate,
|
slate,
|
||||||
@@ -79,7 +79,7 @@ where
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
// 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, context, will_add_outputs)?;
|
||||||
|
|
||||||
Ok(sl)
|
Ok(sl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -465,9 +465,8 @@ pub(super) fn transition_state(slate: &mut Slate) -> Result<(), Error> {
|
|||||||
|
|
||||||
/// Add partial signature to the slate.
|
/// Add partial signature to the slate.
|
||||||
// Nonce reuse is prevented by forgetting the signing context after a signed step:
|
// Nonce reuse is prevented by forgetting the signing context after a signed step:
|
||||||
// save_step deletes the private context (which holds sec_key/sec_nonce) once is_signed
|
// save_step deletes sec_key/sec_nonce after signing, except for step2 contract view
|
||||||
// (except the deliberately-retained step2 context used for safe cancel). The context is
|
// Contexts are keyed by slate.id so a nonce is only used for one message
|
||||||
// keyed by slate.id, so a given nonce is only ever used for one message.
|
|
||||||
pub fn sign<C, K>(
|
pub fn sign<C, K>(
|
||||||
w: &mut WalletBackend<C, K>,
|
w: &mut WalletBackend<C, K>,
|
||||||
keychain_mask: Option<&SecretKey>,
|
keychain_mask: Option<&SecretKey>,
|
||||||
@@ -490,6 +489,8 @@ where
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let keychain = w.keychain(keychain_mask)?;
|
let keychain = w.keychain(keychain_mask)?;
|
||||||
|
// Do not let fill_round_2 skip a missing participant
|
||||||
|
slate.find_index_matching_context(&keychain, context)?;
|
||||||
slate.fill_round_2(&keychain, &context.sec_key, &context.sec_nonce)?;
|
slate.fill_round_2(&keychain, &context.sec_key, &context.sec_nonce)?;
|
||||||
debug!(
|
debug!(
|
||||||
"contract::sign => signed for slate fees: {}",
|
"contract::sign => signed for slate fees: {}",
|
||||||
|
|||||||
@@ -60,12 +60,14 @@ pub fn create_tx_log_entry(
|
|||||||
Ok(t)
|
Ok(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates TxLogEntry for a contract with information available in the 'sign' step
|
/// Update TxLogEntry with data from the sign step
|
||||||
|
/// `participant_index` is our entry in `slate.participant_data`
|
||||||
pub fn update_tx_log_entry<C, K>(
|
pub fn update_tx_log_entry<C, K>(
|
||||||
wallet: &mut WalletBackend<C, K>,
|
wallet: &mut WalletBackend<C, K>,
|
||||||
keychain_mask: Option<&SecretKey>,
|
keychain: &K,
|
||||||
slate: &Slate,
|
slate: &Slate,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
|
participant_index: usize,
|
||||||
tx_log_entry: &mut TxLogEntry,
|
tx_log_entry: &mut TxLogEntry,
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
@@ -73,7 +75,6 @@ where
|
|||||||
K: Keychain,
|
K: Keychain,
|
||||||
{
|
{
|
||||||
// This is expected to be called when we are signing the contract and have already contributed inputs & outputs
|
// This is expected to be called when we are signing the contract and have already contributed inputs & outputs
|
||||||
let keychain = wallet.keychain(keychain_mask)?;
|
|
||||||
let parent_key_id = context.parent_key_id.clone();
|
let parent_key_id = context.parent_key_id.clone();
|
||||||
let current_height = wallet.w2n_client().get_chain_tip()?.0;
|
let current_height = wallet.w2n_client().get_chain_tip()?.0;
|
||||||
// We have already contributed inputs and outputs so we know how much of each we contribute
|
// We have already contributed inputs and outputs so we know how much of each we contribute
|
||||||
@@ -93,7 +94,7 @@ where
|
|||||||
// note we only use a single path for now
|
// note we only use a single path for now
|
||||||
let sender_address_path = 0u32;
|
let sender_address_path = 0u32;
|
||||||
let sender_key = address::address_from_derivation_path(
|
let sender_key = address::address_from_derivation_path(
|
||||||
&keychain,
|
keychain,
|
||||||
&parent_key_id,
|
&parent_key_id,
|
||||||
sender_address_path,
|
sender_address_path,
|
||||||
)?;
|
)?;
|
||||||
@@ -102,14 +103,20 @@ where
|
|||||||
// We're looking for the OTHER party here, the recipient. The 'xor 1' pairing
|
// We're looking for the OTHER party here, the recipient. The 'xor 1' pairing
|
||||||
// only holds for a two-party slate, so check that before indexing: a slate
|
// only holds for a two-party slate, so check that before indexing: a slate
|
||||||
// with a shorter participant list would otherwise panic here.
|
// with a shorter participant list would otherwise panic here.
|
||||||
let sender_index = slate.find_index_matching_context(&keychain, context)?;
|
|
||||||
if slate.participant_data.len() != 2 {
|
if slate.participant_data.len() != 2 {
|
||||||
return Err(Error::GenericError(format!(
|
return Err(Error::GenericError(format!(
|
||||||
"Expected 2 participants for a payment proof, found {}",
|
"Expected 2 participants for a payment proof, found {}",
|
||||||
slate.participant_data.len()
|
slate.participant_data.len()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let recipient_index = sender_index ^ 1;
|
let sender = slate
|
||||||
|
.participant_data
|
||||||
|
.get(participant_index)
|
||||||
|
.ok_or(Error::ContextToIndex)?;
|
||||||
|
let recipient = slate
|
||||||
|
.participant_data
|
||||||
|
.get(participant_index ^ 1)
|
||||||
|
.ok_or(Error::ContextToIndex)?;
|
||||||
|
|
||||||
tx_log_entry.payment_proof = Some(StoredProofInfo {
|
tx_log_entry.payment_proof = Some(StoredProofInfo {
|
||||||
receiver_address: p.receiver_address,
|
receiver_address: p.receiver_address,
|
||||||
@@ -120,14 +127,12 @@ where
|
|||||||
// Filled as separate steps for now; could be merged into a general case
|
// Filled as separate steps for now; could be merged into a general case
|
||||||
// once we know which nonces here belong to the recipient.
|
// once we know which nonces here belong to the recipient.
|
||||||
proof_type: Some(p.proof_type.as_u8()),
|
proof_type: Some(p.proof_type.as_u8()),
|
||||||
receiver_public_nonce: Some(slate.participant_data[recipient_index].public_nonce),
|
receiver_public_nonce: Some(recipient.public_nonce),
|
||||||
receiver_public_excess: Some(
|
receiver_public_excess: Some(recipient.public_blind_excess),
|
||||||
slate.participant_data[recipient_index].public_blind_excess,
|
|
||||||
),
|
|
||||||
timestamp: Some(timestamp),
|
timestamp: Some(timestamp),
|
||||||
memo: p.memo.clone(),
|
memo: p.memo.clone(),
|
||||||
promise_signature: p.promise_signature,
|
promise_signature: p.promise_signature,
|
||||||
sender_part_sig: slate.participant_data[sender_index].part_sig,
|
sender_part_sig: sender.part_sig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,15 +183,15 @@ pub fn get_net_change(
|
|||||||
Ok(expected_net_change.unwrap())
|
Ok(expected_net_change.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Atomically locks the inputs and saves the changes of Context, TxLogEntry and OutputData.
|
/// Lock inputs and store the Context, TxLogEntry and OutputData atomically
|
||||||
/// Additionally, the transaction is saved in a file in case we signed it.
|
/// Consumes the context and derives the signed state from our participant data
|
||||||
|
/// Signed transactions are written outside the database batch
|
||||||
pub fn save_step<C, K>(
|
pub fn save_step<C, K>(
|
||||||
w: &mut WalletBackend<C, K>,
|
w: &mut WalletBackend<C, K>,
|
||||||
keychain_mask: Option<&SecretKey>,
|
keychain_mask: Option<&SecretKey>,
|
||||||
slate: &Slate,
|
slate: &Slate,
|
||||||
context: &mut Context,
|
mut context: Context,
|
||||||
step_added_outputs: bool,
|
step_added_outputs: bool,
|
||||||
is_signed: bool,
|
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
C: NodeClient,
|
C: NodeClient,
|
||||||
@@ -198,9 +203,16 @@ where
|
|||||||
);
|
);
|
||||||
// Phase 1 - precompute the data needed for atomic update
|
// Phase 1 - precompute the data needed for atomic update
|
||||||
let parent_key_id = &context.parent_key_id;
|
let parent_key_id = &context.parent_key_id;
|
||||||
|
let keychain = w.keychain(keychain_mask)?;
|
||||||
|
let participant_index = slate.find_index_matching_context(&keychain, &context)?;
|
||||||
|
let is_signed = slate.participant_data[participant_index].is_complete();
|
||||||
|
if is_signed {
|
||||||
|
// Verify part_sig before using it as signed state
|
||||||
|
slate.verify_part_sigs(keychain.secp())?;
|
||||||
|
}
|
||||||
let current_height = w.w2n_client().get_chain_tip()?.0;
|
let current_height = w.w2n_client().get_chain_tip()?.0;
|
||||||
// We are at step2 if we don't have context.log_id and we have signed the slate
|
// We are at step2 if we don't have context.log_id and we have signed the slate
|
||||||
let is_step2 = !context.log_id.is_some() && is_signed;
|
let is_step2 = context.log_id.is_none() && is_signed;
|
||||||
|
|
||||||
let mut tx_log_entry = {
|
let mut tx_log_entry = {
|
||||||
if context.log_id.is_none() {
|
if context.log_id.is_none() {
|
||||||
@@ -215,7 +227,14 @@ where
|
|||||||
|
|
||||||
// Update TxLogEntry if we have signed the contract (we have data about the kernel)
|
// Update TxLogEntry if we have signed the contract (we have data about the kernel)
|
||||||
if is_signed {
|
if is_signed {
|
||||||
update_tx_log_entry(w, keychain_mask, &slate, &context, &mut tx_log_entry)?;
|
update_tx_log_entry(
|
||||||
|
w,
|
||||||
|
&keychain,
|
||||||
|
slate,
|
||||||
|
&context,
|
||||||
|
participant_index,
|
||||||
|
&mut tx_log_entry,
|
||||||
|
)?;
|
||||||
// Record where the transaction is stored, as internal::selection does for a
|
// Record where the transaction is stored, as internal::selection does for a
|
||||||
// standard send. store_tx below writes it under this name, and 'txs' reads the
|
// standard send. store_tx below writes it under this name, and 'txs' reads the
|
||||||
// field to report whether the transaction data is held.
|
// field to report whether the transaction data is held.
|
||||||
@@ -252,7 +271,7 @@ where
|
|||||||
let mut batch = w.batch(keychain_mask)?;
|
let mut batch = w.batch(keychain_mask)?;
|
||||||
|
|
||||||
// Update TxLogEntry
|
// Update TxLogEntry
|
||||||
if !context.log_id.is_some() {
|
if context.log_id.is_none() {
|
||||||
// If we just created the TxLogEntry, we have to assign it an id
|
// If we just created the TxLogEntry, we have to assign it an id
|
||||||
let log_id = batch.next_tx_log_id(&parent_key_id)?;
|
let log_id = batch.next_tx_log_id(&parent_key_id)?;
|
||||||
tx_log_entry.id = log_id;
|
tx_log_entry.id = log_id;
|
||||||
@@ -279,8 +298,7 @@ where
|
|||||||
if is_signed && !is_step2 {
|
if is_signed && !is_step2 {
|
||||||
// NOTE: We MUST forget the context when we sign. Ideally, these two would be atomic or perhaps
|
// NOTE: We MUST forget the context when we sign. Ideally, these two would be atomic or perhaps
|
||||||
// when we call slate::sigadd_partial_signaturen we could swap the secret key with a temporary one just to be safe.
|
// when we call slate::sigadd_partial_signaturen we could swap the secret key with a temporary one just to be safe.
|
||||||
// The reason we don't delete if we are at step2 is because in case we want to do safe cancel,
|
// Keep the step2 context for contract view
|
||||||
// we need to know which inputs are in the context to know which input we have to double-spend.
|
|
||||||
batch.delete_private_context(slate.id.as_bytes())?;
|
batch.delete_private_context(slate.id.as_bytes())?;
|
||||||
} else {
|
} else {
|
||||||
batch.save_private_context(slate.id.as_bytes(), &context)?;
|
batch.save_private_context(slate.id.as_bytes(), &context)?;
|
||||||
@@ -288,10 +306,7 @@ where
|
|||||||
|
|
||||||
batch.commit()?;
|
batch.commit()?;
|
||||||
|
|
||||||
// Defense in depth: once we have signed (and are past the step2 context that is kept
|
// Confirm sec_key/sec_nonce are gone after signing, except for step2
|
||||||
// deliberately for safe cancel), the signing context holding sec_key/sec_nonce must be
|
|
||||||
// gone so the nonce can never be reused. It is deleted in the is_signed && !is_step2
|
|
||||||
// branch above; verify the deletion actually took effect.
|
|
||||||
if is_signed && !is_step2 {
|
if is_signed && !is_step2 {
|
||||||
match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
|
match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
|
||||||
Err(Error::NotFoundErr(_)) => {}
|
Err(Error::NotFoundErr(_)) => {}
|
||||||
|
|||||||
@@ -704,7 +704,7 @@ impl Slate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Verifies all of the partial signatures in the Slate are valid
|
/// Verifies all of the partial signatures in the Slate are valid
|
||||||
fn verify_part_sigs(&self, secp: &secp::Secp256k1) -> Result<(), Error> {
|
pub(crate) fn verify_part_sigs(&self, secp: &secp::Secp256k1) -> Result<(), Error> {
|
||||||
// collect public nonces
|
// collect public nonces
|
||||||
for p in self.participant_data.iter() {
|
for p in self.participant_data.iter() {
|
||||||
if p.is_complete() {
|
if p.is_complete() {
|
||||||
|
|||||||
Reference in New Issue
Block a user