Compare commits
11 Commits
main
...
pallet-exo
| Author | SHA1 | Date | |
|---|---|---|---|
| 653bf08486 | |||
| 1dc61fb17d | |||
| d488bf9a43 | |||
| 74d74f4037 | |||
| 7ad0159517 | |||
| c884ec7b77 | |||
| e3a81e3902 | |||
| f3e7122d43 | |||
| 84b6dfad9f | |||
| 8abd114a5b | |||
| 6b0033425c |
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ghost-exodus"
|
||||
version = "0.0.17"
|
||||
version = "0.0.26"
|
||||
description = "Threshold signature generation with DKG included"
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
@ -47,11 +47,13 @@ mod benchmarks {
|
||||
prepare_pallet::<T>(1);
|
||||
|
||||
dkg_state.next_phase();
|
||||
let dkg_index = dkg_state.get_dkg_index();
|
||||
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
|
||||
|
||||
let round0_package = PackageContext::default()
|
||||
.with_network_curve(NetworkCurve::Secp256k1)
|
||||
.with_authority_index(0)
|
||||
.with_dkg_index(dkg_index)
|
||||
.with_dkg_round0(dummy_hash);
|
||||
|
||||
let dkg_package = DkgPackage::Round0(round0_package);
|
||||
@ -83,10 +85,10 @@ mod benchmarks {
|
||||
dkg_state.next_phase();
|
||||
|
||||
dkg_state.insert_index(authority_index);
|
||||
let dkg_index = dkg_state.get_dkg_index();
|
||||
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
|
||||
|
||||
let mut rng = Pallet::<T>::get_offchain_rng(authority_index);
|
||||
|
||||
let mut rng = ChaCha20Rng::seed_from_u64(694201337);
|
||||
let (_, public_package) = network_curve.dkg_part1(
|
||||
authority_index,
|
||||
max_signers,
|
||||
@ -100,6 +102,7 @@ mod benchmarks {
|
||||
let round1_package = PackageContext::default()
|
||||
.with_network_curve(NetworkCurve::Secp256k1)
|
||||
.with_authority_index(0)
|
||||
.with_dkg_index(dkg_index)
|
||||
.with_dkg_round1(public_package)
|
||||
.expect("Round1 package should be valid");
|
||||
|
||||
@ -131,6 +134,8 @@ mod benchmarks {
|
||||
dkg_state.next_phase();
|
||||
dkg_state.next_phase();
|
||||
|
||||
let dkg_index = dkg_state.get_dkg_index();
|
||||
|
||||
(0..max_signers).for_each(|i| {
|
||||
dkg_state.insert_index(i);
|
||||
});
|
||||
@ -165,6 +170,7 @@ mod benchmarks {
|
||||
let round2_package = PackageContext::default()
|
||||
.with_network_curve(NetworkCurve::Secp256k1)
|
||||
.with_authority_index(authority_index)
|
||||
.with_dkg_index(dkg_index)
|
||||
.with_dkg_round2(encryption_bundle)
|
||||
.expect("Round2 package should be valid");
|
||||
|
||||
@ -197,6 +203,8 @@ mod benchmarks {
|
||||
dkg_state.next_phase();
|
||||
dkg_state.next_phase();
|
||||
|
||||
let dkg_index = dkg_state.get_dkg_index();
|
||||
|
||||
(0..max_signers).for_each(|i| {
|
||||
dkg_state.insert_index(i);
|
||||
});
|
||||
@ -217,6 +225,7 @@ mod benchmarks {
|
||||
let round3_package = PackageContext::default()
|
||||
.with_network_curve(NetworkCurve::Secp256k1)
|
||||
.with_authority_index(authority_index)
|
||||
.with_dkg_index(dkg_index)
|
||||
.with_dkg_round3(accused_indexes)
|
||||
.expect("Round3 package should be valid");
|
||||
|
||||
@ -253,6 +262,8 @@ mod benchmarks {
|
||||
dkg_state.next_phase();
|
||||
dkg_state.next_phase();
|
||||
|
||||
let dkg_index = dkg_state.get_dkg_index();
|
||||
|
||||
(0..max_signers).for_each(|i| {
|
||||
dkg_state.insert_index(i);
|
||||
});
|
||||
@ -287,6 +298,7 @@ mod benchmarks {
|
||||
let round4_package = PackageContext::default()
|
||||
.with_network_curve(NetworkCurve::Secp256k1)
|
||||
.with_authority_index(authority_index)
|
||||
.with_dkg_index(dkg_index)
|
||||
.with_dkg_round4(justification_bundle)
|
||||
.expect("Round4 package should be valid");
|
||||
|
||||
@ -332,11 +344,17 @@ mod benchmarks {
|
||||
|
||||
let dummy_merkle_root = ExodusHash::repeat_byte(69);
|
||||
let dummy_verifying_key = vec![69u8; network_curve.element_bytes_len()];
|
||||
let dummy_key_members = vec![15];
|
||||
|
||||
let round5_package = PackageContext::default()
|
||||
.with_network_curve(NetworkCurve::Secp256k1)
|
||||
.with_authority_index(authority_index)
|
||||
.with_dkg_round5(dummy_verifying_key.clone(), dummy_merkle_root)
|
||||
.with_dkg_index(dkg_index)
|
||||
.with_dkg_round5(
|
||||
dummy_verifying_key.clone(),
|
||||
dummy_key_members.clone(),
|
||||
dummy_merkle_root,
|
||||
)
|
||||
.expect("Round5 package should be valid");
|
||||
|
||||
let dkg_package = DkgPackage::Round5(round5_package);
|
||||
@ -354,20 +372,22 @@ mod benchmarks {
|
||||
).expect("Bounded verifying key should be correct");
|
||||
|
||||
let metadata_hashes = [
|
||||
SubstrateBlake2Hasher::hash(dummy_merkle_root.as_ref()),
|
||||
SubstrateBlake2Hasher::hash(bounded_verifying_key.as_ref()),
|
||||
dummy_merkle_root.as_ref(),
|
||||
bounded_verifying_key.as_ref(),
|
||||
dummy_key_members.as_ref(),
|
||||
];
|
||||
let verifying_hash = sequential_hash::<SubstrateBlake2Hasher, _>(metadata_hashes);
|
||||
let verifying_hash = cumulative_hash::<SubstrateBlake2Hasher, _>(metadata_hashes);
|
||||
|
||||
let verification_key = (network_curve, dkg_index, verifying_hash);
|
||||
let participants = Verifications::<T>::get(verification_key);
|
||||
assert!(participants.contains(authority_index));
|
||||
|
||||
let consensus = VerifyingKeyConsensus::<T>::get(&network_curve, dkg_index);
|
||||
assert!(consensus.participants.contains(authority_index));
|
||||
assert_eq!(consensus.participants.count_ones::<AuthIndex>(), 1);
|
||||
assert_eq!(consensus.merkle_root, dummy_merkle_root);
|
||||
assert_eq!(consensus.element_bytes, bounded_verifying_key);
|
||||
assert!(consensus.state.participants.contains(authority_index));
|
||||
assert_eq!(consensus.state.participants.count_ones::<AuthIndex>(), 1);
|
||||
assert_eq!(consensus.state.merkle_root, dummy_merkle_root);
|
||||
assert_eq!(consensus.state.element_bytes, bounded_verifying_key);
|
||||
assert_eq!(consensus.members.as_ref(), dummy_key_members);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -434,13 +454,14 @@ mod benchmarks {
|
||||
});
|
||||
|
||||
VerifyingKeyConsensus::<T>::mutate(&network_curve, &dkg_index, |consensus| {
|
||||
consensus.participants.insert(authority_index);
|
||||
consensus.merkle_root = dummy_merkle_root;
|
||||
consensus.state.participants.insert(authority_index);
|
||||
consensus.state.merkle_root = dummy_merkle_root;
|
||||
});
|
||||
|
||||
let round6_package = PackageContext::default()
|
||||
.with_network_curve(NetworkCurve::Secp256k1)
|
||||
.with_authority_index(authority_index)
|
||||
.with_dkg_index(dkg_index)
|
||||
.with_dkg_round6(dummy_verifying_share.clone(), dummy_merkle_proof)
|
||||
.expect("Round6 package should be valid");
|
||||
|
||||
@ -485,7 +506,14 @@ mod benchmarks {
|
||||
let exodus_session = CurrentExodus::<T>::get();
|
||||
|
||||
let dummy_address = EvmAddress::repeat_byte(69);
|
||||
let dummy_nonce = vec![0u8; network_curve.element_bytes_len()];
|
||||
|
||||
// generator point for secp256k1
|
||||
let dummy_nonce = [
|
||||
0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55,
|
||||
0xa0, 0x62, 0x95, 0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc,
|
||||
0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16,
|
||||
0xf8, 0x17, 0x98
|
||||
];
|
||||
|
||||
let create_bond = GovernanceAction::Create {
|
||||
market: [69, 420, 1337, 0],
|
||||
@ -503,6 +531,8 @@ mod benchmarks {
|
||||
|
||||
let authorities = QualificationAuthorities::<T>::get(&network_curve);
|
||||
let active_dkg: ActivatedState<T> = (&dkg_state).into();
|
||||
let active_dkg_index = active_dkg.get_dkg_index();
|
||||
|
||||
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
|
||||
ActiveDkgAuthorities::<T>::insert(&network_curve, active_dkg);
|
||||
ActiveAuthorities::<T>::insert(&network_curve, authorities);
|
||||
@ -511,10 +541,11 @@ mod benchmarks {
|
||||
let nonce_package = PackageContext::default()
|
||||
.with_network_curve(NetworkCurve::Secp256k1)
|
||||
.with_authority_index(authority_index)
|
||||
.with_dkg_index(active_dkg_index)
|
||||
.with_exodus_nonce_commitment(
|
||||
exodus_session,
|
||||
dummy_nonce.clone(),
|
||||
dummy_nonce.clone(),
|
||||
dummy_nonce.to_vec(),
|
||||
dummy_nonce.to_vec(),
|
||||
)
|
||||
.expect("Nonce commitment package should be valid");
|
||||
|
||||
@ -540,8 +571,8 @@ mod benchmarks {
|
||||
authority_index,
|
||||
).expect("Nonces should be registered during nonce commitment");
|
||||
|
||||
assert_eq!(registered_nonces.hiding, dummy_nonce.clone());
|
||||
assert_eq!(registered_nonces.binding, dummy_nonce);
|
||||
assert_eq!(registered_nonces.hiding.as_ref(), dummy_nonce);
|
||||
assert_eq!(registered_nonces.binding.as_ref(), dummy_nonce);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -581,6 +612,8 @@ mod benchmarks {
|
||||
|
||||
let authorities = QualificationAuthorities::<T>::get(&network_curve);
|
||||
let active_dkg: ActivatedState<T> = (&dkg_state).into();
|
||||
let active_dkg_index = active_dkg.get_dkg_index();
|
||||
|
||||
QualificationDkgState::<T>::insert(&network_curve, dkg_state);
|
||||
ActiveDkgAuthorities::<T>::insert(&network_curve, active_dkg);
|
||||
ActiveAuthorities::<T>::insert(&network_curve, authorities);
|
||||
@ -597,6 +630,7 @@ mod benchmarks {
|
||||
let group_package = PackageContext::default()
|
||||
.with_network_curve(NetworkCurve::Secp256k1)
|
||||
.with_authority_index(authority_index)
|
||||
.with_dkg_index(active_dkg_index)
|
||||
.with_exodus_group_commitment(
|
||||
exodus_session,
|
||||
dummy_group_commitment.clone(),
|
||||
@ -763,6 +797,7 @@ mod benchmarks {
|
||||
let share_package = PackageContext::default()
|
||||
.with_network_curve(NetworkCurve::Secp256k1)
|
||||
.with_authority_index(authority_index)
|
||||
.with_dkg_index(active_dkg_index)
|
||||
.with_exodus_signature_share(
|
||||
exodus_session,
|
||||
dummy_signature_share.clone(),
|
||||
|
||||
@ -186,6 +186,19 @@ impl FlexibleRoundOptimizedSchnorrThresholdSignature<AuthIndex, ExodusError> for
|
||||
})
|
||||
}
|
||||
|
||||
fn is_nonce_valid_point(&self, nonce_bytes: &[u8]) -> Result<(), ExodusError> {
|
||||
with_ciphersuite!(self, |f| {
|
||||
type InnerGroup = <Ciphersuite as frost_core::Ciphersuite>::Group;
|
||||
|
||||
let serialized_nonce = <InnerGroup as frost_core::Group>::Serialization::try_from(nonce_bytes)
|
||||
.map_err(|_| ExodusError::DeserializationError)?;
|
||||
|
||||
<InnerGroup as frost_core::Group>::deserialize(&serialized_nonce)
|
||||
.map_err(|_| ExodusError::DeserializationError)
|
||||
.map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
fn is_signature_valid(
|
||||
&self,
|
||||
verifying_key_bytes: &[u8],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -603,7 +603,27 @@ fn run_succesfull_dkg_session_partially(
|
||||
assert_eq!(prev_dkg_index + 1, dkg_index);
|
||||
|
||||
ext.execute_with(|| {
|
||||
let dummy_oversized_bitmap = ParticipantsBitmap::<Runtime>::all_ones(1337);
|
||||
let max_authorities_len = raw_authorities.len();
|
||||
|
||||
let max_authority_index = raw_authorities.iter().max().cloned().unwrap_or_default() as usize;
|
||||
let padded_participants = max_authority_index.saturating_add(1);
|
||||
let bitmask_len = padded_participants.div_ceil(8);
|
||||
|
||||
let mut key_members_raw = vec![0u8; bitmask_len];
|
||||
|
||||
for &authority in raw_authorities.iter() {
|
||||
let global_index = authority as usize;
|
||||
let byte_index = global_index >> 3;
|
||||
let bit_index = global_index & 7;
|
||||
if let Some(byte) = key_members_raw.get_mut(byte_index) {
|
||||
*byte |= 1 << bit_index;
|
||||
}
|
||||
}
|
||||
|
||||
let dummy_key_members: BoundedVec<u8, crate::MaxAuthoritiesBitmaskSize<Runtime>> =
|
||||
key_members_raw.try_into().unwrap();
|
||||
|
||||
let dummy_oversized_bitmap = ParticipantsBitmap::<Runtime>::all_ones(max_authorities_len);
|
||||
let mut map: BitmapByAuthority<Runtime> = Default::default();
|
||||
|
||||
for authority in raw_authorities.iter() {
|
||||
@ -625,10 +645,11 @@ fn run_succesfull_dkg_session_partially(
|
||||
vec![69u8; ELEMENT_MAX_BYTES as usize],
|
||||
).unwrap();
|
||||
|
||||
let dummy_consensus_state = ConsensusState::new(
|
||||
let dummy_consensus_state = QualifiedConsensusState::new(
|
||||
dummy_verifying_key,
|
||||
dummy_oversized_bitmap,
|
||||
DUMMY_HASH,
|
||||
dummy_key_members,
|
||||
);
|
||||
|
||||
VerifyingKeyConsensus::<Runtime>::insert(curve, prev_dkg_index, dummy_consensus_state);
|
||||
@ -1021,6 +1042,149 @@ fn run_dkg_session(
|
||||
assert_ne!(prev_active_authorities, active_authorities);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dkg_robustness_against_malformed_scale_slots() {
|
||||
let (mut ext, tx_pool_state) = new_test_ext();
|
||||
let curve = NetworkCurve::Secp256k1;
|
||||
|
||||
let honest_validators = vec![0, 1, 2, 4, 5, 6, 7, 8, 9];
|
||||
let malicious_validator = 3;
|
||||
let all_validators = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
let authorities: Vec<UintAuthorityId> = all_validators.iter().map(|&id| id.into()).collect();
|
||||
ext.execute_with(|| Exodus::start_dkg_qualification(authorities, NetworkCurve::iter()));
|
||||
|
||||
run_to_next_round(&mut ext, curve);
|
||||
for &authority in &all_validators {
|
||||
assert_ok!(execute_round0_happy_path(&mut ext, &tx_pool_state, curve, authority));
|
||||
}
|
||||
run_to_next_round(&mut ext, curve);
|
||||
for &authority in &all_validators {
|
||||
assert_ok!(execute_round1_happy_path(&mut ext, &tx_pool_state, curve, authority));
|
||||
}
|
||||
|
||||
run_to_next_round(&mut ext, curve);
|
||||
|
||||
for &authority in &honest_validators {
|
||||
assert_ok!(execute_round2_happy_path(&mut ext, &tx_pool_state, curve, authority));
|
||||
}
|
||||
|
||||
ext.execute_with(|| {
|
||||
let dkg_index = QualificationDkgState::<Runtime>::get(curve).get_dkg_index();
|
||||
|
||||
let padded_participants = 10usize;
|
||||
let bitmask_len = padded_participants.div_ceil(8);
|
||||
|
||||
let mut perfect_bitmask = vec![0u8; bitmask_len];
|
||||
for &recipient in &honest_validators {
|
||||
let idx = recipient as usize;
|
||||
perfect_bitmask[idx >> 3] |= 1 << (idx & 7);
|
||||
}
|
||||
|
||||
let expected_blob_len = round2_encrypted_package_size(
|
||||
padded_participants,
|
||||
curve.scalar_bytes_len(),
|
||||
curve.header_bytes_len(),
|
||||
);
|
||||
|
||||
let malformed_blob = vec![0xAAu8; expected_blob_len];
|
||||
|
||||
let dummy_encryption_bundle = BundledPackages {
|
||||
bitmask: perfect_bitmask,
|
||||
blob: malformed_blob,
|
||||
};
|
||||
|
||||
let dummy_context = PackageContext::default()
|
||||
.with_authority_index(malicious_validator as AuthIndex)
|
||||
.with_network_curve(curve)
|
||||
.with_dkg_index(dkg_index)
|
||||
.with_dkg_round2(dummy_encryption_bundle)
|
||||
.unwrap();
|
||||
|
||||
let dummy_package = DkgPackage::Round2(dummy_context);
|
||||
|
||||
let malicious_signer: UintAuthorityId = malicious_validator.into();
|
||||
let dummy_signature = malicious_signer.sign(&dummy_package.encode()).unwrap();
|
||||
|
||||
assert_ok!(Exodus::register_encrypted_round2_packages(
|
||||
frame_system::RawOrigin::None.into(),
|
||||
dummy_package,
|
||||
dummy_signature
|
||||
));
|
||||
});
|
||||
|
||||
run_to_next_round(&mut ext, curve);
|
||||
|
||||
for &authority in &honest_validators {
|
||||
assert_ok!(execute_round3_happy_path(&mut ext, &tx_pool_state, curve, authority));
|
||||
}
|
||||
|
||||
ext.execute_with(|| {
|
||||
let complaints = Complaints::<Runtime>::get(curve);
|
||||
|
||||
for &accuser in &honest_validators {
|
||||
let accuser_bitmap = complaints.get(&(accuser as AuthIndex)).unwrap();
|
||||
assert!(accuser_bitmap.contains(malicious_validator as AuthIndex));
|
||||
}
|
||||
});
|
||||
|
||||
run_to_next_round(&mut ext, curve);
|
||||
|
||||
for &authority in &honest_validators {
|
||||
assert_ok!(execute_round4_happy_path(&mut ext, &tx_pool_state, curve, authority));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exodus_dkg_distributed_synced_exclusion() {
|
||||
let (mut ext, tx_pool_state) = new_test_ext();
|
||||
let curve = NetworkCurve::Secp256k1;
|
||||
|
||||
let honest_validators = vec![0, 1, 2, 4, 5, 6, 7, 8, 9];
|
||||
let malicious_validator = 3;
|
||||
let all_validators = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
let authorities: Vec<UintAuthorityId> = all_validators.iter().map(|&id| id.into()).collect();
|
||||
ext.execute_with(|| Exodus::start_dkg_qualification(authorities, NetworkCurve::iter()));
|
||||
|
||||
run_to_next_round(&mut ext, curve);
|
||||
for &authority in &all_validators {
|
||||
assert_ok!(execute_round0_happy_path(&mut ext, &tx_pool_state, curve, authority));
|
||||
}
|
||||
|
||||
run_to_next_round(&mut ext, curve);
|
||||
for &authority in &all_validators {
|
||||
assert_ok!(execute_round1_happy_path(&mut ext, &tx_pool_state, curve, authority));
|
||||
}
|
||||
|
||||
let remaining_rounds = [
|
||||
execute_round2_happy_path,
|
||||
execute_round3_happy_path,
|
||||
execute_round4_happy_path,
|
||||
execute_round5_happy_path,
|
||||
execute_round6_happy_path,
|
||||
];
|
||||
|
||||
for dkg_round in remaining_rounds {
|
||||
run_to_next_round(&mut ext, curve);
|
||||
for &authority in &honest_validators {
|
||||
assert_ok!(dkg_round(&mut ext, &tx_pool_state, curve, authority));
|
||||
}
|
||||
}
|
||||
|
||||
run_to_next_round(&mut ext, curve);
|
||||
|
||||
ext.execute_with(|| {
|
||||
let state = QualificationDkgState::<Runtime>::get(curve);
|
||||
assert_eq!(state.get_phase(), DkgPhase::Round7);
|
||||
assert!(!state.contains_index(malicious_validator));
|
||||
|
||||
for &authority in &honest_validators {
|
||||
assert!(state.contains_index(authority));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_validators_can_execute_initial_dkg() {
|
||||
let curve = NetworkCurve::Secp256k1;
|
||||
@ -1136,7 +1300,7 @@ fn test_all_validators_can_execute_dkg() {
|
||||
ExodusRequests::<Runtime>::count()
|
||||
});
|
||||
|
||||
assert_eq!(rotations_count, 3);
|
||||
assert_eq!(rotations_count, 2);
|
||||
assert_eq!(bridges_count, 2);
|
||||
assert_eq!(governances_count, 15);
|
||||
assert_eq!(pending_exodus_requests, 0);
|
||||
|
||||
@ -6,6 +6,30 @@ use frame_support::{BoundedVec, traits::ConstU32};
|
||||
|
||||
use crate::{ExodusHash, ELEMENT_MAX_BYTES};
|
||||
|
||||
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
|
||||
pub struct QualifiedConsensusState<Bitmap, Bitmask> {
|
||||
pub state: ConsensusState<Bitmap>,
|
||||
pub members: Bitmask,
|
||||
}
|
||||
|
||||
impl<Bitmap, Bitmask> QualifiedConsensusState<Bitmap, Bitmask> {
|
||||
pub fn new(
|
||||
element_bytes: BoundedVec<u8, ConstU32<ELEMENT_MAX_BYTES>>,
|
||||
participants: Bitmap,
|
||||
merkle_root: ExodusHash,
|
||||
members: Bitmask,
|
||||
) -> Self {
|
||||
Self {
|
||||
members,
|
||||
state: ConsensusState::new(
|
||||
element_bytes,
|
||||
participants,
|
||||
merkle_root,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
|
||||
pub struct ConsensusState<Bitmap> {
|
||||
pub element_bytes: BoundedVec<u8, ConstU32<ELEMENT_MAX_BYTES>>,
|
||||
|
||||
@ -387,7 +387,7 @@ where
|
||||
sp_runtime::Rounding::Down
|
||||
)?;
|
||||
|
||||
let bounty_uint32 = scaled_bounty as u32;
|
||||
let bounty_uint32 = scaled_bounty.min(u32::MAX as u128) as u32;
|
||||
buffer[28..32].copy_from_slice(&bounty_uint32.to_be_bytes());
|
||||
|
||||
let mut amount_buffer = [0u8; 32];
|
||||
|
||||
@ -8,7 +8,7 @@ use sp_runtime::{
|
||||
};
|
||||
|
||||
use super::{DkgPackage, ExodusPackage, ExodusOk, PackageContext};
|
||||
use crate::{AuthIndex, ExodusSession, ExodusHash, ExodusError};
|
||||
use crate::{AuthIndex, DkgIndex, ExodusSession, ExodusHash, ExodusError};
|
||||
|
||||
fn create_storage_key(prefix: &[u8], index: &[u8], network: &[u8]) -> Vec<u8> {
|
||||
let mut key = crate::DB_STORAGE_PREFIX.to_vec();
|
||||
@ -75,7 +75,7 @@ impl<ExodusInnerType> ExodusStorageWrapper<ExodusInnerType> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Encode)]
|
||||
pub struct ExodusStorage<BlockNumber, NetworkCurve>
|
||||
where
|
||||
BlockNumber: Default,
|
||||
@ -156,6 +156,7 @@ where
|
||||
pub fn prepare_signed_call_with_context<T, A, F1, F2, F3>(
|
||||
&self,
|
||||
authority_key: &A,
|
||||
current_dkg_index: DkgIndex,
|
||||
exodus_records: &mut ExodusRecords<B>,
|
||||
make_data: F1,
|
||||
submit_call: F2,
|
||||
@ -170,7 +171,8 @@ where
|
||||
{
|
||||
let context = PackageContext::default()
|
||||
.with_authority_index(self.authority_index)
|
||||
.with_network_curve(self.network_curve);
|
||||
.with_network_curve(self.network_curve)
|
||||
.with_dkg_index(current_dkg_index);
|
||||
|
||||
let package = make_data(context)?;
|
||||
|
||||
@ -365,7 +367,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
#[derive(Default, Encode, Clone, Copy)]
|
||||
pub struct DkgStorage<BlockNumber, NetworkCurve>
|
||||
where
|
||||
BlockNumber: Copy + Clone,
|
||||
@ -454,6 +456,7 @@ where
|
||||
pub fn prepare_signed_call_with_context<T, A, F1, F2>(
|
||||
&self,
|
||||
authority_key: &A,
|
||||
current_dkg_index: DkgIndex,
|
||||
dkg_record: &mut DkgRecord<B>,
|
||||
make_data: F1,
|
||||
submit_call: F2,
|
||||
@ -466,7 +469,8 @@ where
|
||||
{
|
||||
let context = PackageContext::default()
|
||||
.with_authority_index(self.authority_index)
|
||||
.with_network_curve(self.network_curve);
|
||||
.with_network_curve(self.network_curve)
|
||||
.with_dkg_index(current_dkg_index);
|
||||
|
||||
let package = make_data(context)?;
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ use sp_runtime::RuntimeDebug;
|
||||
use codec::{Encode, Decode};
|
||||
|
||||
use frame_support::{BoundedVec, traits::ConstU32};
|
||||
use crate::{AuthIndex, ExodusSession, ExodusHash, ExodusError};
|
||||
use crate::{AuthIndex, DkgIndex, ExodusSession, ExodusHash, ExodusError};
|
||||
|
||||
pub trait PackageMetadata<NetworkCurve>
|
||||
where
|
||||
@ -18,6 +18,9 @@ where
|
||||
fn get_network_curve(&self) -> NetworkCurve {
|
||||
self.context().get_network_curve()
|
||||
}
|
||||
fn get_dkg_index(&self) -> DkgIndex {
|
||||
self.context().get_dkg_index()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, RuntimeDebug, TypeInfo, PartialEq, Eq)]
|
||||
@ -72,7 +75,7 @@ where
|
||||
Round2(Round2Package<NetworkCurve, T>),
|
||||
Round3(Round3Package<NetworkCurve, T>),
|
||||
Round4(Round4Package<NetworkCurve, T>),
|
||||
Round5(Round5Package<NetworkCurve>),
|
||||
Round5(Round5Package<NetworkCurve, T>),
|
||||
Round6(Round6Package<NetworkCurve, T>),
|
||||
}
|
||||
|
||||
@ -98,10 +101,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(Default, RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
pub struct PackageContext<NetworkCurve> {
|
||||
authority_index: AuthIndex,
|
||||
network_curve: NetworkCurve
|
||||
network_curve: NetworkCurve,
|
||||
dkg_index: DkgIndex,
|
||||
}
|
||||
|
||||
impl<NetworkCurve> PackageContext<NetworkCurve>
|
||||
@ -116,6 +120,10 @@ where
|
||||
self.network_curve
|
||||
}
|
||||
|
||||
pub fn get_dkg_index(&self) -> DkgIndex {
|
||||
self.dkg_index
|
||||
}
|
||||
|
||||
pub fn with_network_curve(mut self, network_curve: NetworkCurve) -> Self {
|
||||
self.network_curve = network_curve;
|
||||
self
|
||||
@ -125,15 +133,20 @@ where
|
||||
self.authority_index = authority_index;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_dkg_index(mut self, dkg_index: DkgIndex) -> Self {
|
||||
self.dkg_index = dkg_index;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
pub struct BundledPackages {
|
||||
pub blob: Vec<u8>,
|
||||
pub bitmask: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct EncryptionBundledPackages<T>
|
||||
where
|
||||
@ -167,7 +180,7 @@ impl<T: crate::pallet::Config> TryFrom<BundledPackages> for EncryptionBundledPac
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct JustificationsBundledPackages<T>
|
||||
where
|
||||
@ -201,7 +214,7 @@ impl<T: crate::pallet::Config> TryFrom<BundledPackages> for JustificationsBundle
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct ExodusNonceCommitment<NetworkCurve> {
|
||||
context: PackageContext<NetworkCurve>,
|
||||
@ -210,7 +223,7 @@ pub struct ExodusNonceCommitment<NetworkCurve> {
|
||||
pub session: ExodusSession,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
pub struct ExodusGroupCommitment<NetworkCurve> {
|
||||
context: PackageContext<NetworkCurve>,
|
||||
pub group_commitment: BoundedVec<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>,
|
||||
@ -218,7 +231,7 @@ pub struct ExodusGroupCommitment<NetworkCurve> {
|
||||
pub session: ExodusSession,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct ExodusSignatureShare<NetworkCurve, T>
|
||||
where
|
||||
@ -231,13 +244,13 @@ where
|
||||
pub session: ExodusSession,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
pub struct Round0Package<NetworkCurve> {
|
||||
context: PackageContext<NetworkCurve>,
|
||||
pub package_hash: ExodusHash,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Round1Package<NetworkCurve, T>
|
||||
where
|
||||
@ -247,7 +260,7 @@ where
|
||||
pub package: BoundedVec<u8, crate::Round1MaxBytes<T>>,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Round2Package<NetworkCurve, T>
|
||||
where
|
||||
@ -257,7 +270,7 @@ where
|
||||
pub bundle: EncryptionBundledPackages<T>,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Round3Package<NetworkCurve, T>
|
||||
where
|
||||
@ -267,7 +280,7 @@ where
|
||||
pub indices: BoundedVec<u8, crate::AccusedIndicesMaxBytes<T>>,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Round4Package<NetworkCurve, T>
|
||||
where
|
||||
@ -277,14 +290,19 @@ where
|
||||
pub bundle: JustificationsBundledPackages<T>,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
pub struct Round5Package<NetworkCurve> {
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Round5Package<NetworkCurve, T>
|
||||
where
|
||||
T: crate::pallet::Config,
|
||||
{
|
||||
context: PackageContext<NetworkCurve>,
|
||||
pub key_members: BoundedVec<u8, crate::MaxAuthoritiesBitmaskSize<T>>,
|
||||
pub verifying_key: BoundedVec<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>,
|
||||
pub merkle_root: ExodusHash,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)]
|
||||
#[derive(RuntimeDebug, Clone, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Round6Package<NetworkCurve, T>
|
||||
where
|
||||
@ -404,16 +422,20 @@ impl<NetworkCurve> PackageContext<NetworkCurve> {
|
||||
Ok(Round4Package { context: self, bundle })
|
||||
}
|
||||
|
||||
pub fn with_dkg_round5(
|
||||
pub fn with_dkg_round5<T: crate::pallet::Config>(
|
||||
self,
|
||||
verifying_key_vec: Vec<u8>,
|
||||
key_members_vec: Vec<u8>,
|
||||
merkle_root: ExodusHash,
|
||||
) -> Result<Round5Package<NetworkCurve>, ExodusError> {
|
||||
) -> Result<Round5Package<NetworkCurve, T>, ExodusError> {
|
||||
let key_members = BoundedVec::<u8, crate::MaxAuthoritiesBitmaskSize<T>>::try_from(key_members_vec)
|
||||
.map_err(|_| ExodusError::DeserializationError)?;
|
||||
|
||||
let verifying_key =
|
||||
BoundedVec::<u8, ConstU32<{ crate::ELEMENT_MAX_BYTES }>>::try_from(verifying_key_vec)
|
||||
.map_err(|_| ExodusError::DeserializationError)?;
|
||||
|
||||
Ok(Round5Package { context: self, verifying_key, merkle_root })
|
||||
Ok(Round5Package { context: self, key_members, verifying_key, merkle_root })
|
||||
}
|
||||
|
||||
pub fn with_dkg_round6<T: crate::pallet::Config>(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ghost-helpers"
|
||||
version = "0.0.11"
|
||||
version = "0.0.13"
|
||||
description = "Cryptographic utility suite for custom runtimes: optimized Bitmaps, UTXO parsing, Merkle Tree proofs, and Hash Chain components."
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
@ -3,6 +3,8 @@ use sp_std::vec::Vec;
|
||||
|
||||
const MAX_INPUTS: usize = 3;
|
||||
const MAX_OUTPUTS: usize = 4;
|
||||
const MAX_FLAGS_LEN: usize = 8;
|
||||
const MAX_HASHES_LEN: usize = 16;
|
||||
const MAX_SCRIPT_LEN: usize = 512;
|
||||
pub const MAX_TX_LEN: usize = 4096;
|
||||
|
||||
@ -27,20 +29,17 @@ pub struct Transaction<H: GhostHasher> {
|
||||
lock_time: u32,
|
||||
}
|
||||
|
||||
pub struct Header<H: GhostHasher> {
|
||||
pub merkle_root: H::Hash,
|
||||
pub tx_count: u32,
|
||||
pub tree_hashes: Vec<H::Hash>,
|
||||
pub flags: Vec<u8>,
|
||||
}
|
||||
pub struct Header<H: GhostHasher>(sp_std::marker::PhantomData<H>);
|
||||
|
||||
impl<H: GhostHasher> Header<H> {
|
||||
fn calc_tree_width(&self, h: u32) -> u32 {
|
||||
(self.tx_count + (1 << h) - 1) >> h
|
||||
fn calc_tree_width(tx_count: u32, h: u32) -> u32 {
|
||||
(tx_count + (1 << h) - 1) >> h
|
||||
}
|
||||
|
||||
fn traverse(
|
||||
&self,
|
||||
tx_count: u32,
|
||||
flags: &[u8],
|
||||
tree_hashes: &[H::Hash],
|
||||
height: u32,
|
||||
pos: u32,
|
||||
bits_used: &mut u32,
|
||||
@ -51,19 +50,19 @@ impl<H: GhostHasher> Header<H> {
|
||||
let byte_idx = (*bits_used / 8) as usize;
|
||||
let bit_idx = (*bits_used % 8) as u8;
|
||||
|
||||
if byte_idx >= self.flags.len() {
|
||||
if byte_idx >= flags.len() {
|
||||
return Err(ParseError::InvalidVarint(VarintError::BufferTooShort));
|
||||
}
|
||||
|
||||
let parent_of_match = ((self.flags[byte_idx] >> bit_idx) & 1) == 1;
|
||||
let parent_of_match = ((flags[byte_idx] >> bit_idx) & 1) == 1;
|
||||
*bits_used += 1;
|
||||
|
||||
if height == 0 || !parent_of_match {
|
||||
if *hash_used as usize >= self.tree_hashes.len() {
|
||||
if *hash_used as usize >= tree_hashes.len() {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
let curr_hash = self.tree_hashes[*hash_used as usize];
|
||||
let curr_hash = tree_hashes[*hash_used as usize];
|
||||
*hash_used += 1;
|
||||
|
||||
if height == 0 && parent_of_match && curr_hash == target_txid {
|
||||
@ -73,7 +72,10 @@ impl<H: GhostHasher> Header<H> {
|
||||
return Ok(curr_hash);
|
||||
}
|
||||
|
||||
let left = self.traverse(
|
||||
let left = Self::traverse(
|
||||
tx_count,
|
||||
flags,
|
||||
tree_hashes,
|
||||
height - 1,
|
||||
pos * 2,
|
||||
bits_used,
|
||||
@ -82,8 +84,11 @@ impl<H: GhostHasher> Header<H> {
|
||||
target_txid,
|
||||
)?;
|
||||
|
||||
let right = if pos * 2 + 1 < self.calc_tree_width(height - 1) {
|
||||
let right = self.traverse(
|
||||
let right = if pos * 2 + 1 < Self::calc_tree_width(tx_count, height - 1) {
|
||||
let right = Self::traverse(
|
||||
tx_count,
|
||||
flags,
|
||||
tree_hashes,
|
||||
height - 1,
|
||||
pos * 2 + 1,
|
||||
bits_used,
|
||||
@ -109,17 +114,83 @@ impl<H: GhostHasher> Header<H> {
|
||||
Ok(current_node_hash)
|
||||
}
|
||||
|
||||
pub fn extract_proof(&self, target_txid: H::Hash) -> Result<H::Hash, ParseError> {
|
||||
pub fn verify_utxo_merkle_proof(
|
||||
txproof_bytes: &[u8],
|
||||
target_txid: H::Hash,
|
||||
) -> Result<(), ParseError> {
|
||||
if txproof_bytes.len() < 85 {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
let header_bytes = &txproof_bytes[..80];
|
||||
let mut bytes = &txproof_bytes[80..];
|
||||
|
||||
let mut merkle_root = [0u8; 32];
|
||||
merkle_root.copy_from_slice(&header_bytes[36..68]);
|
||||
|
||||
if bytes.len() < 4 {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
let tx_count = u32::from_le_bytes(
|
||||
bytes[..4]
|
||||
.try_into()
|
||||
.map_err(|_| ParseError::TypeConversionFailed)?,
|
||||
);
|
||||
bytes = &bytes[4..];
|
||||
let hash_count = read_varint(&mut bytes)?;
|
||||
|
||||
if hash_count > MAX_HASHES_LEN {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
let hash_count_bytes = hash_count
|
||||
.checked_mul(32)
|
||||
.ok_or(ParseError::TypeConversionFailed)?;
|
||||
|
||||
if bytes.len() < hash_count_bytes {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
let tree_hashes_bytes = &bytes[..hash_count_bytes];
|
||||
bytes = &bytes[hash_count_bytes..];
|
||||
|
||||
let tree_hashes: Vec<H::Hash> = tree_hashes_bytes
|
||||
.chunks_exact(32)
|
||||
.map(|chunk| H::from_slice(chunk))
|
||||
.collect();
|
||||
|
||||
let flags_count = read_varint(&mut bytes)?;
|
||||
|
||||
if flags_count > MAX_FLAGS_LEN {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
if bytes.len() < flags_count {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
let flags = bytes[..flags_count].to_vec();
|
||||
bytes = &bytes[flags_count..];
|
||||
|
||||
if !bytes.is_empty() {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
let mut bits_used = 0u32;
|
||||
let mut hash_used = 0u32;
|
||||
|
||||
let mut matched_index = None;
|
||||
let mut max_height = 0;
|
||||
while (1 << max_height) < self.tx_count {
|
||||
|
||||
while (1 << max_height) < tx_count {
|
||||
max_height += 1;
|
||||
}
|
||||
|
||||
let computed_root = self.traverse(
|
||||
let computed_root = Self::traverse(
|
||||
tx_count,
|
||||
&flags,
|
||||
&tree_hashes,
|
||||
max_height,
|
||||
0,
|
||||
&mut bits_used,
|
||||
@ -128,19 +199,23 @@ impl<H: GhostHasher> Header<H> {
|
||||
target_txid,
|
||||
)?;
|
||||
|
||||
if hash_used as usize != self.tree_hashes.len() {
|
||||
if hash_used as usize != tree_hashes.len() {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
if (bits_used + 7) / 8 != self.flags.len() as u32 {
|
||||
if (bits_used + 7) / 8 != flags.len() as u32 {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
if matched_index.is_some() {
|
||||
Ok(computed_root)
|
||||
} else {
|
||||
Err(ParseError::TransactionNotFoundInProof)
|
||||
if matched_index.is_none() {
|
||||
return Err(ParseError::TransactionNotFoundInProof);
|
||||
}
|
||||
|
||||
if computed_root != H::from_slice(&merkle_root) {
|
||||
return Err(ParseError::InvalidMerkleRoot);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -357,6 +432,9 @@ fn read_varint(bytes: &mut &[u8]) -> Result<usize, VarintError> {
|
||||
.try_into()
|
||||
.map_err(|_| VarintError::TypeConversionFailed)?,
|
||||
);
|
||||
if val < 0xfd {
|
||||
return Err(VarintError::TypeConversionFailed);
|
||||
}
|
||||
*bytes = &bytes[2..];
|
||||
Ok(val as usize)
|
||||
}
|
||||
@ -369,6 +447,9 @@ fn read_varint(bytes: &mut &[u8]) -> Result<usize, VarintError> {
|
||||
.try_into()
|
||||
.map_err(|_| VarintError::TypeConversionFailed)?,
|
||||
);
|
||||
if val < 0x10000 {
|
||||
return Err(VarintError::TypeConversionFailed);
|
||||
}
|
||||
*bytes = &bytes[4..];
|
||||
Ok(val as usize)
|
||||
}
|
||||
@ -381,6 +462,9 @@ fn read_varint(bytes: &mut &[u8]) -> Result<usize, VarintError> {
|
||||
.try_into()
|
||||
.map_err(|_| VarintError::TypeConversionFailed)?,
|
||||
);
|
||||
if val < 0x100000000 {
|
||||
return Err(VarintError::TypeConversionFailed);
|
||||
}
|
||||
*bytes = &bytes[8..];
|
||||
Ok(val as usize)
|
||||
}
|
||||
@ -392,6 +476,7 @@ fn read_varint(bytes: &mut &[u8]) -> Result<usize, VarintError> {
|
||||
pub enum ParseError {
|
||||
InvalidLength,
|
||||
ExceededMaxLimits,
|
||||
InvalidMerkleRoot,
|
||||
TypeConversionFailed,
|
||||
TransactionNotFoundInProof,
|
||||
InvalidVarint(VarintError),
|
||||
@ -525,60 +610,3 @@ impl<'a, H: GhostHasher> TryFrom<&'a [u8]> for Transaction<H> {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, H: GhostHasher> TryFrom<&'a [u8]> for Header<H> {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(mut bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
if bytes.len() < 85 {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
let header_bytes = &bytes[..80];
|
||||
bytes = &bytes[80..];
|
||||
|
||||
let mut merkle_root = [0u8; 32];
|
||||
merkle_root.copy_from_slice(&header_bytes[36..68]);
|
||||
|
||||
if bytes.len() < 4 {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
let tx_count = u32::from_le_bytes(
|
||||
bytes[..4]
|
||||
.try_into()
|
||||
.map_err(|_| ParseError::TypeConversionFailed)?,
|
||||
);
|
||||
bytes = &bytes[4..];
|
||||
|
||||
let hash_count = read_varint(&mut bytes)?;
|
||||
if bytes.len() < hash_count * 32 {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
let tree_hashes_bytes = &bytes[..hash_count * 32];
|
||||
bytes = &bytes[hash_count * 32..];
|
||||
|
||||
let tree_hashes: Vec<H::Hash> = tree_hashes_bytes
|
||||
.chunks_exact(32)
|
||||
.map(|chunk| H::from_slice(chunk))
|
||||
.collect();
|
||||
|
||||
let flags_count = read_varint(&mut bytes)?;
|
||||
if bytes.len() < flags_count {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
let flags = bytes[..flags_count].to_vec();
|
||||
bytes = &bytes[flags_count..];
|
||||
|
||||
if !bytes.is_empty() {
|
||||
return Err(ParseError::InvalidLength);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
merkle_root: H::from_slice(&merkle_root),
|
||||
tx_count,
|
||||
tree_hashes,
|
||||
flags,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ghost-traits"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
description = "Shared traits including `GhostHasher`, `NetworkDataBasicHandler`, `BoundedBTreeMap`, `MerkleTree` and more."
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
@ -33,6 +33,7 @@ pub trait FlexibleRoundOptimizedSchnorrThresholdSignature<A, E>: MerkleTreeBuild
|
||||
vk: &[u8],
|
||||
m: &[u8],
|
||||
) -> Result<(), E>;
|
||||
fn is_nonce_valid_point(&self, b: &[u8]) -> Result<(), E>;
|
||||
fn is_signature_valid(&self, vk: &[u8], r: &[u8], z: &[u8], m: &[u8]) -> Result<(), E>;
|
||||
fn accumulate_signature_scalar(&self, ss: &[u8], sh: &[u8]) -> Result<Vec<u8>, E>;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ghost-weaver"
|
||||
version = "0.0.14"
|
||||
version = "0.0.16"
|
||||
description = "Weaving a secure cryptographic tapestry across different external chains."
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
@ -801,6 +801,116 @@ fn should_successfully_pull_utxo_thread_and_mint_currency() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_successfully_pull_utxo_thread_and_reject_nominal_varint_encoding() {
|
||||
let pair = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed");
|
||||
let incoming_share = 0;
|
||||
|
||||
let network_id = 69;
|
||||
let network_data = NetworkDataBuilder::default()
|
||||
.with_network_type(NetworkType::Utxo)
|
||||
.with_incoming_share(incoming_share)
|
||||
.with_gatekeeper(
|
||||
// Transaction: https://blockstream.info/tx/4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e
|
||||
hex::decode("4635b1794a22bfbe6c5c5eba17b693f4aaf0e348")
|
||||
.expect("Bytes32 from UTXO should be valid"),
|
||||
)
|
||||
.build();
|
||||
|
||||
let authorities = vec![pair.clone()];
|
||||
let networks = vec![(network_id, network_data, Default::default())];
|
||||
|
||||
// Real world tx at block #481838
|
||||
// Block https://blockstream.info/block/0000000000000000011bdccb39e19e942a60edffdccd9779653c5b46b337904a
|
||||
// Transaction: https://blockstream.info/tx/4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e
|
||||
|
||||
let payload = (
|
||||
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "gettxoutproof", "params": [["4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e"], "0000000000000000011bdccb39e19e942a60edffdccd9779653c5b46b337904a"]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
|
||||
hex::decode("00000020375a14cfcf5730daa8ff8d7a5bd2cf59fba797e289c2e0000000000000000000529cd017146361b57e6b7353b16d8d38d32f92ee842039dd779d35bfbfa2f3e53f649e59e93c01187900ea9fb40600000cfbd7b718845d123b1ae1a7fd3bce9c97acba097dc912c4ac67a920395db07ba4eef685960869ba70e0ca38890565c97dc11f1d7ae538e7b509069f81e38eff1a2e4a6b66fc4233d7d0580af4d195c7f0b274849f19d405dfc6b930bb9edd3f4a4c1f1e9fa3a336c50e8ea16039e4db4bef7ff5946e3a019937bdf626c2336d0e3926a83a575218f84e6dc55ffb9d0997a41e4b4d5425a85f22160c9f52362a95021a573ff96bb429bd80a8ec9dcb899971553efe91b6a0fe57c4694c3e8da8dc7f1babd514f2746da1ed06f08664baed853c334e0a3917d0c859b19ee14f448f77e624822070db3b606d5092027ca9ecf7e0243e6c151081021ebb3ddcdd28c1b8f9fdcffebd1962590477675f3d29d52602a1e7e941f772a3bbff42cf2eb6e74a1124bf424f767e294ae6253a21602c7c6b0381743dd6dcd57b025983e0e34375b24f67ac8beb00713d2f70594bbf797d5d94c061cb9965e6458f2f017f983d7ce37361fa13fda372f595dc1f22895ab93339457c72c480fed1c4d1164a242303ff3600").expect("Bytes32 from UTXO should be valid"),
|
||||
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getrawtransaction", "params": ["4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
|
||||
hex::decode("010000000299a43fae581f80baf44e021ddf7b11995f184db282d6e48c22248b9fe79c43bb010000008b483045022100e9db01784dcd879c65d66127c2d52a4f9244321d15796893d6dccadaa1146e3d02203d9bb80f8df8b66692d3d9517f7cbb898d7cc40a30b175432322e1956c9b973a0141048097e4f8bb8ee104d5c2ece11d42f7e4719df8ddba459ecadecaba87d233c81a3c14830371392dd8ed030ef970450c0b7af48d9db4ba20c046896481602eb9dbffffffffb3fd05bc6000a5a0384e9a613d452e9307568df0a964907068e00403a0a0e09e020000006a47304402205bb1cbad4b29421648c1e22cac32a56aa06c933fe655559823fffb71ffebbc0b022021648d970560aa5b2031560b52a0fd06305995a58a457663324c18c1a9b0bd0b8121020d6e50b2660af27933c42bc1395fe93df90ffac5e2a989f6a134919fb8cf8075ffffffff030000000000000000296a2769642b5bb666ac1657090d02985f0cdb41436fe2d5a83a3acb1995d963a698e7e619e927bdcc5e7c150000000000001976a914c1c69d91a5719a23ba2094d89a4df3eadfa4aef788acefea2309000000001976a9144635b1794a22bfbe6c5c5eba17b693f4aaf0e34888ac00000000").expect("Bytes32 from UTXO should be valid"),
|
||||
vec![
|
||||
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["000000000000000000e7da64065cc23d5c5a5aa402f088aaaf37c5fe1c3b6b92", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
|
||||
hex::decode("000000204a9037b3465b3c657997cddcffed602a949ee139cbdc1b010000000000000000669c3e2472384bc95993f154aada38d6b2113d8d32069e32e2724ba40915a1802a669e59e93c011801c8c735").expect("Bytes32 from UTXO should be valid"),
|
||||
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["00000000000000000063da6d9362c0ac3b87877d1c5ad7afd5bd706571d5b393", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
|
||||
hex::decode("02000020926b3b1cfec537afaa88f002a45a5a5c3dc25c0664dae7000000000000000000af0f270bf3f868d47089f395197809c360f1506636c960d96afed68bbd72d0b4496d9e59e93c01183ac94b67").expect("Bytes32 from UTXO should be valid"),
|
||||
// curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["000000000000000000ec6063eb743366264368be7ff5021727682cb518963026", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com
|
||||
hex::decode("0000002093b3d5716570bdd5afd75a1c7d87873bacc062936dda630000000000000000009ab437e7c66b10a59b67127f290d24f381aba4e03d5e16c062a356eea4d608dea56d9e59e93c0118264f310e").expect("Bytes32 from UTXO should be valid"),
|
||||
],
|
||||
);
|
||||
|
||||
let encodings = vec![
|
||||
vec![0x02], // canonical
|
||||
vec![0xFD, 0x02, 0x00], // non-minimal 3 bytes
|
||||
vec![0xFE, 0x02, 0x00, 0x00, 0x00], // non-minimal 5 bytes
|
||||
vec![0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // non-minimal 9 bytes
|
||||
];
|
||||
|
||||
new_test_ext(authorities, networks).execute_with(|| {
|
||||
let session = 0;
|
||||
let block_hash =
|
||||
hex::decode("000000000000000000ec6063eb743366264368be7ff5021727682cb518963026")
|
||||
.expect("Bitcoin Root hex should be valid");
|
||||
|
||||
let expected_root = H256::from_slice(&block_hash);
|
||||
LoomStates::<TestRuntime>::insert(&network_id, &session, expected_root);
|
||||
|
||||
let expected_receiver_bytes = hex::decode("1657090d02985f0cdb41436fe2d5a83a3acb1995d963a698e7e619e927bdcc5e").unwrap();
|
||||
let mut receiver_raw = [0u8; 32];
|
||||
receiver_raw.copy_from_slice(&expected_receiver_bytes);
|
||||
|
||||
let receiver_account = sp_runtime::AccountId32::from(receiver_raw);
|
||||
let transfer_amount: u64 = 153348847;
|
||||
let mut expected_pure_amount: u64 = 0;
|
||||
|
||||
for (idx, enc) in encodings.iter().enumerate() {
|
||||
let mut variant = Vec::with_capacity(payload.1.len() + enc.len() - 1);
|
||||
variant.extend_from_slice(&payload.1[0..4]);
|
||||
variant.extend_from_slice(&enc);
|
||||
variant.extend_from_slice(&payload.1[5..]);
|
||||
|
||||
let utxo_thread_proof = UtxoThreadProof::new(payload.0.clone(), variant, payload.2.clone());
|
||||
let thread_info = ThreadProof::UtxoThreadProof(utxo_thread_proof);
|
||||
let pulled_thread_key = thread_info.get_unique_key(session, network_id);
|
||||
|
||||
assert_eq!(Balances::free_balance(&receiver_account), expected_pure_amount);
|
||||
assert!(!PulledThreads::<TestRuntime>::contains_key(
|
||||
&pulled_thread_key
|
||||
));
|
||||
|
||||
let pull_result = GhostWeaver::pull_thread(
|
||||
RuntimeOrigin::none(),
|
||||
network_id,
|
||||
session,
|
||||
thread_info
|
||||
);
|
||||
|
||||
if idx == 0 {
|
||||
assert!(pull_result.is_ok());
|
||||
assert!(PulledThreads::<TestRuntime>::contains_key(&pulled_thread_key));
|
||||
|
||||
let curve_share = Perbill::from_parts(incoming_share).mul_ceil(transfer_amount);
|
||||
expected_pure_amount += transfer_amount.saturating_sub(curve_share);
|
||||
|
||||
assert_eq!(Balances::free_balance(&receiver_account), expected_pure_amount);
|
||||
|
||||
System::assert_last_event(RuntimeEvent::GhostWeaver(Event::ThreadPulled {
|
||||
pulled_thread_key,
|
||||
network_id,
|
||||
receiver: receiver_account.clone(),
|
||||
amount: transfer_amount,
|
||||
}));
|
||||
} else {
|
||||
assert!(pull_result.is_err());
|
||||
assert!(!PulledThreads::<TestRuntime>::contains_key(&pulled_thread_key));
|
||||
assert_eq!(Balances::free_balance(&receiver_account), expected_pure_amount);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(Balances::free_balance(&receiver_account), transfer_amount);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_initialize_should_finalize_session_when_timeout_is_reached() {
|
||||
let pair1 = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed");
|
||||
|
||||
@ -483,7 +483,7 @@ impl UtxoThreadProof {
|
||||
|
||||
pub fn verify_thread_proof<Balance>(
|
||||
&self,
|
||||
root_hash: H256,
|
||||
block_hash: H256,
|
||||
gatekeeper_ref: &[u8],
|
||||
) -> Option<(AccountId32, Balance)>
|
||||
where
|
||||
@ -498,7 +498,7 @@ impl UtxoThreadProof {
|
||||
let is_chain_valid = verify_hash_ancestry::<UtxoSha2Hasher, _>(
|
||||
header_bytes,
|
||||
self.ancestry_headers.iter(),
|
||||
root_hash,
|
||||
block_hash,
|
||||
);
|
||||
|
||||
if !is_chain_valid { return None; }
|
||||
@ -507,13 +507,11 @@ impl UtxoThreadProof {
|
||||
self.getrawtransaction_bytes.as_ref()
|
||||
).ok()?;
|
||||
|
||||
let header = Header::<UtxoSha2Hasher>::try_from(
|
||||
self.gettxproof_bytes.as_ref(),
|
||||
).ok()?;
|
||||
|
||||
let txid_preimage = transaction.compute_txid()?;
|
||||
let root_hash = header.extract_proof(txid_preimage).ok()?;
|
||||
if root_hash != header.merkle_root { return None; }
|
||||
Header::<UtxoSha2Hasher>::verify_utxo_merkle_proof(
|
||||
self.gettxproof_bytes.as_ref(),
|
||||
txid_preimage,
|
||||
).ok()?;
|
||||
|
||||
#[cfg(not(any(test, feature = "runtime-benchmarks")))]
|
||||
let destination_script = Transaction::<UtxoSha2Hasher>::convert_into_p2tr(gatekeeper_ref)?;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user