Skip to content

Commit

Permalink
feat(base_layer): checkpoint signature validation (#4297)
Browse files Browse the repository at this point in the history
Description
---
* New base layer validation of checkpoint signatures: verifies that ALL the signatures present in the checkpoint are valid
* Unified the error type for invalid signatures across all contract output types

Motivation and Context
---
The base layer needs to check if all the signatures in a checkpoint are valid.

This PR is based on previous work on checkpoints (see #4261  and #4285). The checkpoint signature is calculated as:
`e = H_1(signer_public_key || public_nonce || H_2(contract_id||commitment||merkle_root||checkpoint_number))`

But take into account that the `commitment` is still a mock value as of now we still don't have a method for creating a shared value.

How Has This Been Tested?
---
New unit test checks that invalid signatures are detected
  • Loading branch information
mrnaveira committed Jul 12, 2022
1 parent edcdf33 commit 850e78f
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ pub fn validate_signature<B: BlockchainBackend>(
let is_valid_signature = SignerSignature::verify(signature, validator_node_public_key, challenge);
if !is_valid_signature {
return Err(ValidationError::DanLayerError(
DanLayerValidationError::InvalidAcceptanceSignature,
DanLayerValidationError::InvalidSignature,
));
}

Expand Down Expand Up @@ -200,20 +200,24 @@ mod test {
use super::fetch_constitution_commitment;
use crate::{
txn_schema,
validation::dan_validators::test_helpers::{
assert_dan_validator_fail,
assert_dan_validator_success,
create_acceptance_signature,
create_block,
create_contract_acceptance_schema,
create_contract_acceptance_schema_with_signature,
create_contract_constitution,
create_contract_constitution_schema,
create_random_key_pair,
init_test_blockchain,
publish_constitution,
publish_definition,
schema_to_transaction,
validation::dan_validators::{
test_helpers::{
assert_dan_validator_err,
assert_dan_validator_fail,
assert_dan_validator_success,
create_acceptance_signature,
create_block,
create_contract_acceptance_schema,
create_contract_acceptance_schema_with_signature,
create_contract_constitution,
create_contract_constitution_schema,
create_random_key_pair,
init_test_blockchain,
publish_constitution,
publish_definition,
schema_to_transaction,
},
DanLayerValidationError,
},
};

Expand Down Expand Up @@ -378,6 +382,7 @@ mod test {
let (tx, _) = schema_to_transaction(&schema);

// try to validate the acceptance transaction and check that we get the error
assert_dan_validator_fail(&blockchain, &tx, "Invalid acceptance signature");
let err = assert_dan_validator_err(&blockchain, &tx);
assert!(matches!(err, DanLayerValidationError::InvalidSignature { .. }));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use tari_common_types::types::{Commitment, FixedHash};

use super::helpers::{fetch_contract_constitution, get_sidechain_features};
use crate::{
chain_storage::{BlockchainBackend, BlockchainDatabase},
transactions::transaction_components::{
CheckpointChallenge,
CommitteeSignatures,
ContractCheckpoint,
ContractConstitution,
OutputType,
SideChainFeatures,
SignerSignature,
TransactionOutput,
},
validation::{
Expand All @@ -51,6 +55,8 @@ pub fn validate_contract_checkpoint<B: BlockchainBackend>(
let prev_cp = fetch_current_contract_checkpoint(db, contract_id)?;
validate_checkpoint_number(prev_cp.as_ref(), checkpoint)?;

validate_signatures(checkpoint, &contract_id)?;

Ok(())
}

Expand Down Expand Up @@ -92,10 +98,36 @@ fn validate_committee(
Ok(())
}

pub fn validate_signatures(checkpoint: &ContractCheckpoint, contract_id: &FixedHash) -> Result<(), ValidationError> {
let challenge = create_checkpoint_challenge(checkpoint, contract_id);
let signatures = &checkpoint.signatures;

let are_all_signatures_valid = signatures
.into_iter()
.all(|s| SignerSignature::verify(s.signature(), s.signer(), challenge));
if !are_all_signatures_valid {
return Err(ValidationError::DanLayerError(
DanLayerValidationError::InvalidSignature,
));
}

Ok(())
}

pub fn create_checkpoint_challenge(checkpoint: &ContractCheckpoint, contract_id: &FixedHash) -> CheckpointChallenge {
// TODO: update when shared commitment consensus among VNs is implemented
let commitment = Commitment::default();
CheckpointChallenge::new(
contract_id,
&commitment,
&checkpoint.merkle_root,
checkpoint.checkpoint_number,
)
}

#[cfg(test)]
mod test {
use tari_common_types::types::Signature;

use super::create_checkpoint_challenge;
use crate::validation::dan_validators::{
test_helpers::{
assert_dan_validator_err,
Expand Down Expand Up @@ -218,19 +250,48 @@ mod test {
let (mut blockchain, utxos) = init_test_blockchain();

// Publish a new contract specifying a committee with only one member ("alice")
let (_, alice) = create_random_key_pair();
let contract_id = publish_contract(&mut blockchain, &utxos, vec![alice.clone()]);
let alice = create_random_key_pair();
let contract_id = publish_contract(&mut blockchain, &utxos, vec![alice.1.clone()]);

// Create a checkpoint, with a committe that has an extra member ("bob") not present in the constiution
let mut checkpoint = create_contract_checkpoint(0);
let (_, bob) = create_random_key_pair();
checkpoint.signatures =
create_committee_signatures(vec![(alice, Signature::default()), (bob, Signature::default())]);
let bob = create_random_key_pair();
let challenge = create_checkpoint_challenge(&checkpoint, &contract_id);
checkpoint.signatures = create_committee_signatures(vec![alice, bob], challenge.as_ref());
let schema = create_contract_checkpoint_schema(contract_id, utxos[1].clone(), checkpoint);
let (tx, _) = schema_to_transaction(&schema);

// try to validate the acceptance transaction and check that we get the error
let err = assert_dan_validator_err(&blockchain, &tx);
assert!(matches!(err, DanLayerValidationError::InconsistentCommittee { .. }));
}

#[test]
fn it_rejects_checkpoint_with_invalid_signatures() {
// initialise a blockchain with enough funds to spend at contract transactions
let (mut blockchain, utxos) = init_test_blockchain();

// Publish a new contract specifying a committee with two members
let alice = create_random_key_pair();
let mut bob = create_random_key_pair();
let contract_id = publish_contract(&mut blockchain, &utxos, vec![alice.1.clone(), bob.1.clone()]);

// To generate an invalid signature, lets swap bob private key for a random private key but keep the public key
let (altered_private_key, _) = create_random_key_pair();
bob.0 = altered_private_key;

// Create a checkpoint with the altered key pair,
// bob private key is altered compared to the one use in the contract constitution
let mut checkpoint = create_contract_checkpoint(0);
let challenge = create_checkpoint_challenge(&checkpoint, &contract_id);
checkpoint.signatures = create_committee_signatures(vec![alice, bob], challenge.as_ref());

// Create the invalid transaction
let schema = create_contract_checkpoint_schema(contract_id, utxos[1].clone(), checkpoint);
let (tx, _) = schema_to_transaction(&schema);

// try to validate the checkpoint transaction and check that we get the error
let err = assert_dan_validator_err(&blockchain, &tx);
assert!(matches!(err, DanLayerValidationError::InvalidSignature { .. }));
}
}
4 changes: 2 additions & 2 deletions base_layer/core/src/validation/dan_validators/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ pub enum DanLayerValidationError {
UpdatedConstitutionAmendmentMismatch,
#[error("Acceptance window has expired for contract_id ({contract_id})")]
AcceptanceWindowHasExpired { contract_id: FixedHash },
#[error("Invalid acceptance signature")]
InvalidAcceptanceSignature,
#[error("Invalid signature")]
InvalidSignature,
#[error("Proposal acceptance window has expired for contract_id ({contract_id}) and proposal_id ({proposal_id})")]
ProposalAcceptanceWindowHasExpired { contract_id: FixedHash, proposal_id: u64 },
#[error("Checkpoint has non-sequential number. Got: {got}, expected: {expected}")]
Expand Down
14 changes: 9 additions & 5 deletions base_layer/core/src/validation/dan_validators/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

use std::convert::TryInto;

use tari_common_types::types::{Commitment, FixedHash, PublicKey, Signature};
use tari_common_types::types::{Commitment, FixedHash, PrivateKey, PublicKey, Signature};
use tari_crypto::ristretto::{RistrettoPublicKey, RistrettoSecretKey};
use tari_p2p::Network;

Expand Down Expand Up @@ -338,10 +338,14 @@ pub fn assert_dan_validator_success(blockchain: &TestBlockchain, transaction: &T
perform_validation(blockchain, transaction).unwrap()
}

pub fn create_committee_signatures(key_signature_pairs: Vec<(PublicKey, Signature)>) -> CommitteeSignatures {
let signer_signatures: Vec<SignerSignature> = key_signature_pairs
.iter()
.map(|(k, s)| SignerSignature::new(k.clone(), s.clone()))
pub fn create_committee_signatures(keys: Vec<(PrivateKey, PublicKey)>, challenge: &[u8]) -> CommitteeSignatures {
let signer_signatures: Vec<SignerSignature> = keys
.into_iter()
.map(|(pri_k, pub_k)| {
let (nonce, _) = create_random_key_pair();
let signature = Signature::sign(pri_k, nonce, challenge).unwrap();
SignerSignature::new(pub_k, signature)
})
.collect();

CommitteeSignatures::new(signer_signatures.try_into().unwrap())
Expand Down

0 comments on commit 850e78f

Please sign in to comment.