From: Devrandom Date: Mon, 9 Aug 2021 10:09:39 +0000 (+0200) Subject: Enforce that revocation can only occur after we validated a new commitment X-Git-Tag: v0.0.101~28^2~4 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=241e448d37a9fa6c8ed149b934acefb15b69ee92;p=rust-lightning Enforce that revocation can only occur after we validated a new commitment --- diff --git a/lightning/src/chain/keysinterface.rs b/lightning/src/chain/keysinterface.rs index c2e78a793..ac81e5430 100644 --- a/lightning/src/chain/keysinterface.rs +++ b/lightning/src/chain/keysinterface.rs @@ -212,6 +212,11 @@ pub trait BaseSign { /// Note that the commitment number starts at (1 << 48) - 1 and counts backwards. // TODO: return a Result so we can signal a validation error fn release_commitment_secret(&self, idx: u64) -> [u8; 32]; + /// Validate the counterparty's signatures on the holder commitment transaction and HTLCs. + /// + /// This is required in order for the signer to make sure that releasing a commitment + /// secret won't leave us without a broadcastable holder transaction. + fn validate_holder_commitment(&self, holder_tx: &HolderCommitmentTransaction); /// Gets the holder's channel public keys and basepoints fn pubkeys(&self) -> &ChannelPublicKeys; /// Gets an arbitrary identifier describing the set of keys which are provided back to you in @@ -558,6 +563,9 @@ impl BaseSign for InMemorySigner { chan_utils::build_commitment_secret(&self.commitment_seed, idx) } + fn validate_holder_commitment(&self, _holder_tx: &HolderCommitmentTransaction) { + } + fn pubkeys(&self) -> &ChannelPublicKeys { &self.holder_channel_pubkeys } fn channel_keys_id(&self) -> [u8; 32] { self.channel_keys_id } diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 03968118a..2cd7e3c2f 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1791,6 +1791,8 @@ impl Channel { self.counterparty_funding_pubkey() ); + self.holder_signer.validate_holder_commitment(&holder_commitment_tx); + // Now that we're past error-generating stuff, update our local state: let funding_redeemscript = self.get_funding_redeemscript(); @@ -1865,6 +1867,7 @@ impl Channel { self.counterparty_funding_pubkey() ); + self.holder_signer.validate_holder_commitment(&holder_commitment_tx); let funding_redeemscript = self.get_funding_redeemscript(); let funding_txo = self.get_funding_txo().unwrap(); @@ -2502,6 +2505,7 @@ impl Channel { ); let next_per_commitment_point = self.holder_signer.get_per_commitment_point(self.cur_holder_commitment_transaction_number - 1, &self.secp_ctx); + self.holder_signer.validate_holder_commitment(&holder_commitment_tx); let per_commitment_secret = self.holder_signer.release_commitment_secret(self.cur_holder_commitment_transaction_number + 1); // Update state now that we've passed all the can-fail calls... diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 132893957..739152348 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -1301,6 +1301,9 @@ fn test_fee_spike_violation_fails_htlc() { let chan_lock = nodes[0].node.channel_state.lock().unwrap(); let local_chan = chan_lock.by_id.get(&chan.2).unwrap(); let chan_signer = local_chan.get_signer(); + // Make the signer believe we validated another commitment, so we can release the secret + chan_signer.get_enforcement_state().last_holder_commitment -= 1; + let pubkeys = chan_signer.pubkeys(); (pubkeys.revocation_basepoint, pubkeys.htlc_basepoint, chan_signer.release_commitment_secret(INITIAL_COMMITMENT_NUMBER), @@ -7981,7 +7984,7 @@ fn test_counterparty_raa_skip_no_crash() { // commitment transaction, we would have happily carried on and provided them the next // commitment transaction based on one RAA forward. This would probably eventually have led to // channel closure, but it would not have resulted in funds loss. Still, our - // EnforcingSigner would have paniced as it doesn't like jumps into the future. Here, we + // EnforcingSigner would have panicked as it doesn't like jumps into the future. Here, we // check simply that the channel is closed in response to such an RAA, but don't check whether // we decide to punish our counterparty for revoking their funds (as we don't currently // implement that). @@ -7992,11 +7995,19 @@ fn test_counterparty_raa_skip_no_crash() { let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), InitFeatures::known()).2; let mut guard = nodes[0].node.channel_state.lock().unwrap(); - let keys = &guard.by_id.get_mut(&channel_id).unwrap().get_signer(); + let keys = guard.by_id.get_mut(&channel_id).unwrap().get_signer(); + const INITIAL_COMMITMENT_NUMBER: u64 = (1 << 48) - 1; + + // Make signer believe we got a counterparty signature, so that it allows the revocation + keys.get_enforcement_state().last_holder_commitment -= 1; let per_commitment_secret = keys.release_commitment_secret(INITIAL_COMMITMENT_NUMBER); + // Must revoke without gaps + keys.get_enforcement_state().last_holder_commitment -= 1; keys.release_commitment_secret(INITIAL_COMMITMENT_NUMBER - 1); + + keys.get_enforcement_state().last_holder_commitment -= 1; let next_per_commitment_point = PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&keys.release_commitment_secret(INITIAL_COMMITMENT_NUMBER - 2)).unwrap()); diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index f94909a9a..5b49f43b1 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -194,7 +194,7 @@ pub struct FundingCreated { pub funding_txid: Txid, /// The specific output index funding this channel pub funding_output_index: u16, - /// The signature of the channel initiator (funder) on the funding transaction + /// The signature of the channel initiator (funder) on the initial commitment transaction pub signature: Signature, } @@ -203,7 +203,7 @@ pub struct FundingCreated { pub struct FundingSigned { /// The channel ID pub channel_id: [u8; 32], - /// The signature of the channel acceptor (fundee) on the funding transaction + /// The signature of the channel acceptor (fundee) on the initial commitment transaction pub signature: Signature, } diff --git a/lightning/src/util/enforcing_trait_impls.rs b/lightning/src/util/enforcing_trait_impls.rs index cd3ead649..18ed0ea83 100644 --- a/lightning/src/util/enforcing_trait_impls.rs +++ b/lightning/src/util/enforcing_trait_impls.rs @@ -15,6 +15,7 @@ use io; use prelude::*; use core::cmp; use sync::{Mutex, Arc}; +#[cfg(test)] use sync::MutexGuard; use bitcoin::blockdata::transaction::{Transaction, SigHashType}; use bitcoin::util::bip143; @@ -35,12 +36,17 @@ pub const INITIAL_REVOKED_COMMITMENT_NUMBER: u64 = 1 << 48; /// - When signing, the holder transaction has not been revoked /// - When revoking, the holder transaction has not been signed /// - The holder commitment number is monotonic and without gaps +/// - The revoked holder commitment number is monotonic and without gaps +/// - There is at least one unrevoked holder transaction at all times /// - The counterparty commitment number is monotonic and without gaps /// - The pre-derived keys and pre-built transaction in CommitmentTransaction were correctly built /// /// Eventually we will probably want to expose a variant of this which would essentially /// be what you'd want to run on a hardware wallet. /// +/// Note that counterparty signatures on the holder transaction are not checked, but it should +/// be in a complete implementation. +/// /// Note that before we do so we should ensure its serialization format has backwards- and /// forwards-compatibility prefix/suffixes! #[derive(Clone)] @@ -74,6 +80,11 @@ impl EnforcingSigner { disable_revocation_policy_check } } + + #[cfg(test)] + pub fn get_enforcement_state(&self) -> MutexGuard { + self.state.lock().unwrap() + } } impl BaseSign for EnforcingSigner { @@ -84,12 +95,20 @@ impl BaseSign for EnforcingSigner { fn release_commitment_secret(&self, idx: u64) -> [u8; 32] { { let mut state = self.state.lock().unwrap(); - assert!(idx == state.revoked_commitment || idx == state.revoked_commitment - 1, "can only revoke the current or next unrevoked commitment - trying {}, revoked {}", idx, state.revoked_commitment); - state.revoked_commitment = idx; + assert!(idx == state.last_holder_revoked_commitment || idx == state.last_holder_revoked_commitment - 1, "can only revoke the current or next unrevoked commitment - trying {}, last revoked {}", idx, state.last_holder_revoked_commitment); + assert!(idx > state.last_holder_commitment, "cannot revoke the last holder commitment - attempted to revoke {} last commitment {}", idx, state.last_holder_commitment); + state.last_holder_revoked_commitment = idx; } self.inner.release_commitment_secret(idx) } + fn validate_holder_commitment(&self, holder_tx: &HolderCommitmentTransaction) { + let mut state = self.state.lock().unwrap(); + let idx = holder_tx.commitment_number(); + assert!(idx == state.last_holder_commitment || idx == state.last_holder_commitment - 1, "expecting to validate the current or next holder commitment - trying {}, current {}", idx, state.last_holder_commitment); + state.last_holder_commitment = idx; + } + fn pubkeys(&self) -> &ChannelPublicKeys { self.inner.pubkeys() } fn channel_keys_id(&self) -> [u8; 32] { self.inner.channel_keys_id() } @@ -116,10 +135,10 @@ impl BaseSign for EnforcingSigner { let state = self.state.lock().unwrap(); let commitment_number = trusted_tx.commitment_number(); - if state.revoked_commitment - 1 != commitment_number && state.revoked_commitment - 2 != commitment_number { + if state.last_holder_revoked_commitment - 1 != commitment_number && state.last_holder_revoked_commitment - 2 != commitment_number { if !self.disable_revocation_policy_check { panic!("can only sign the next two unrevoked commitment numbers, revoked={} vs requested={} for {}", - state.revoked_commitment, commitment_number, self.inner.commitment_seed[0]) + state.last_holder_revoked_commitment, commitment_number, self.inner.commitment_seed[0]) } } @@ -212,8 +231,9 @@ pub struct EnforcementState { /// The last counterparty commitment number we signed, backwards counting pub last_counterparty_commitment: u64, /// The last holder commitment number we revoked, backwards counting - pub revoked_commitment: u64, - + pub last_holder_revoked_commitment: u64, + /// The last validated holder commitment number, backwards counting + pub last_holder_commitment: u64, } impl EnforcementState { @@ -221,7 +241,8 @@ impl EnforcementState { pub fn new() -> Self { EnforcementState { last_counterparty_commitment: INITIAL_REVOKED_COMMITMENT_NUMBER, - revoked_commitment: INITIAL_REVOKED_COMMITMENT_NUMBER, + last_holder_revoked_commitment: INITIAL_REVOKED_COMMITMENT_NUMBER, + last_holder_commitment: INITIAL_REVOKED_COMMITMENT_NUMBER, } } }