Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 91 additions & 83 deletions rust/main/chains/dymension-kaspa/src/providers/validators.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
use tonic::async_trait;

use hyperlane_core::{
rpc_clients::BlockNumberGetter, ChainCommunicationError, ChainResult, Signature,
SignedCheckpointWithMessageId, H160,
rpc_clients::BlockNumberGetter, ChainCommunicationError, ChainResult, Signable, Signature,
SignedCheckpointWithMessageId, SignedType, H160,
};

use bytes::Bytes;
use eyre::Result;
use reqwest::StatusCode;
use std::str::FromStr;
use tracing::{error, info, warn};
use tracing::{error, info};

use crate::ConnectionConf;
use crate::SignableProgressIndication;
use futures::stream::{FuturesUnordered, StreamExt};
use std::sync::Arc;
use std::time::Instant;
Expand All @@ -23,6 +24,55 @@ use crate::ops::{
};
use kaspa_wallet_pskt::prelude::Bundle;

/// Verifies that a signature was produced by the expected ISM address.
/// Returns true if valid, false otherwise (with error logging).
fn verify_ism_signer<T: Signable>(
index: usize,
host: &str,
expected_address: &str,
signed: &SignedType<T>,
) -> bool {
let expected_h160 = match H160::from_str(expected_address) {
Ok(h) => h,
Err(e) => {
error!(
validator = ?host,
validator_index = index,
expected_address = ?expected_address,
error = ?e,
"kaspa: invalid ISM address format"
);
return false;
}
};

match signed.recover() {
Ok(recovered_signer) => {
if recovered_signer != expected_h160 {
error!(
validator = ?host,
validator_index = index,
expected_signer = ?expected_h160,
actual_signer = ?recovered_signer,
"kaspa: signature verification failed - signer mismatch"
);
false
} else {
true
}
}
Err(e) => {
error!(
validator = ?host,
validator_index = index,
error = ?e,
"kaspa: signature recovery failed"
);
false
}
}
}

#[derive(Clone)]
pub struct ValidatorsClient {
pub conf: ConnectionConf,
Expand Down Expand Up @@ -230,9 +280,7 @@ impl ValidatorsClient {
) -> ChainResult<Vec<SignedCheckpointWithMessageId>> {
let threshold = self.multisig_threshold_hub_ism();
let client = self.http_client.clone();
// Use ISM validators for deposit signatures
let hosts = self.hosts_ism();
// Extract ISM addresses from ISM validators for signature verification
let expected_addresses: Vec<String> = self
.validators_ism()
.iter()
Expand All @@ -241,62 +289,15 @@ impl ValidatorsClient {
let metrics = self.metrics.clone();
let fxg = fxg.clone();

// Only validate signatures if ISM addresses are configured (non-empty)
let has_ism_addresses = expected_addresses.iter().any(|a| !a.is_empty());
let validator = if !has_ism_addresses {
None
} else {
Some(
move |index: usize,
host: &String,
signed_checkpoint: &SignedCheckpointWithMessageId| {
if let Some(expected) = expected_addresses.get(index) {
if expected.is_empty() {
return true;
}
match H160::from_str(expected) {
Ok(expected_h160) => match signed_checkpoint.recover() {
Ok(recovered_signer) => {
if recovered_signer != expected_h160 {
error!(
validator = ?host,
validator_index = index,
expected_signer = ?expected_h160,
actual_signer = ?recovered_signer,
"kaspa: signature verification failed - signer mismatch"
);
false
} else {
true
}
}
Err(e) => {
error!(
validator = ?host,
validator_index = index,
error = ?e,
"kaspa: signature recovery failed"
);
false
}
},
Err(e) => {
error!(
validator = ?host,
validator_index = index,
expected_address = ?expected,
error = ?e,
"kaspa: invalid ISM address format"
);
false
}
}
} else {
true
}
},
)
};
let validator =
move |index: usize,
host: &String,
signed_checkpoint: &SignedCheckpointWithMessageId| {
expected_addresses
.get(index)
.map(|expected| verify_ism_signer(index, host, expected, signed_checkpoint))
.unwrap_or(true)
};

let indexed_sigs = Self::collect_with_threshold(
hosts,
Expand All @@ -308,12 +309,11 @@ impl ValidatorsClient {
let fxg = fxg.clone();
Box::pin(async move { request_validate_new_deposits(&client, host, &fxg).await })
},
validator,
Some(validator),
)
.await?;

// Extract signatures and sort by recovered signer address (lexicographic order required by Hub ISM)
// Recovery should not fail here since validation already verified each signature
// Sort by recovered signer address (lexicographic order required by Hub ISM)
let mut sigs: Vec<_> = indexed_sigs.into_iter().map(|(_, sig)| sig).collect();
sigs.sort_by_cached_key(|sig| {
sig.recover()
Expand All @@ -330,28 +330,29 @@ impl ValidatorsClient {
) -> ChainResult<Vec<Signature>> {
let threshold = self.multisig_threshold_hub_ism();
let client = self.http_client.clone();
// Use ISM validators for confirmation signatures
let hosts = self.hosts_ism();
let expected_addresses: Vec<String> = self
.validators_ism()
.iter()
.map(|v| v.ism_address.clone())
.collect();
let metrics = self.metrics.clone();
let fxg = fxg.clone();

// Get ISM addresses for sorting from ISM validators
let ism_addresses: Vec<H160> = self
.validators_ism()
.iter()
.enumerate()
.map(|(idx, v)| {
H160::from_str(&v.ism_address).unwrap_or_else(|e| {
warn!(
validator_index = idx,
ism_address = %v.ism_address,
error = ?e,
"kaspa: ISM address parse failed, using default for sorting"
);
H160::default()
})
// Capture progress_indication for signature verification
let progress_indication = fxg.progress_indication.clone();

let validator = move |index: usize, host: &String, signature: &Signature| {
expected_addresses.get(index).map_or(true, |expected| {
// Construct SignedType to enable signer recovery
let signable = SignableProgressIndication::new(progress_indication.clone());
let signed = SignedType {
value: signable,
signature: *signature,
};
verify_ism_signer(index, host, expected, &signed)
})
.collect();
};

let indexed_sigs = Self::collect_with_threshold(
hosts,
Expand All @@ -365,11 +366,18 @@ impl ValidatorsClient {
async move { request_validate_new_confirmation(&client, host, &fxg).await },
)
},
None::<fn(usize, &String, &Signature) -> bool>,
Some(validator),
)
.await?;

// Pair signatures with ISM addresses and sort by ISM address (lexicographic order required by Hub ISM)
// Get ISM addresses for sorting
let ism_addresses: Vec<H160> = self
.validators_ism()
.iter()
.map(|v| H160::from_str(&v.ism_address).expect("ISM address must be valid"))
.collect();

// Sort by ISM address (lexicographic order required by Hub ISM)
let mut sigs_with_addr: Vec<_> = indexed_sigs
.into_iter()
.map(|(idx, sig)| {
Expand Down
16 changes: 11 additions & 5 deletions rust/main/chains/dymension-kaspa/src/validator/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,14 @@ pub struct SignableProgressIndication {
progress_indication: ProgressIndication,
}

impl SignableProgressIndication {
pub fn new(progress_indication: ProgressIndication) -> Self {
Self {
progress_indication,
}
}
}

impl Serialize for SignableProgressIndication {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
Expand Down Expand Up @@ -519,13 +527,11 @@ async fn respond_validate_confirmed_withdrawals<
info!("validator: confirmed withdrawal is valid");
}

let progress_indication = &conf_fxg.progress_indication;

let sig = res
.must_signing()
.sign_with_fallback(SignableProgressIndication {
progress_indication: progress_indication.clone(),
})
.sign_with_fallback(SignableProgressIndication::new(
conf_fxg.progress_indication.clone(),
))
.await
.map_err(AppError)?;

Expand Down
9 changes: 5 additions & 4 deletions rust/main/chains/dymension-kaspa/src/validator/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ pub fn check_migration_lock(
return Ok(());
}

let expected_escrow = fs::read_to_string(&lock_path).map_err(|e| MigrationLockError::ReadError {
path: lock_path.display().to_string(),
reason: e.to_string(),
})?;
let expected_escrow =
fs::read_to_string(&lock_path).map_err(|e| MigrationLockError::ReadError {
path: lock_path.display().to_string(),
reason: e.to_string(),
})?;

let expected_escrow = expected_escrow.trim();

Expand Down
27 changes: 22 additions & 5 deletions rust/main/hyperlane-base/src/kas_hack/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,20 @@ where
let tx_ids = execute_or_detect_migration(provider, &target_addr, &new_escrow).await;

// Step 2: Wait for TX confirmation then sync hub
info!(delay_secs = SYNC_DELAY_SECS, "Waiting for TX confirmation before hub sync");
info!(
delay_secs = SYNC_DELAY_SECS,
"Waiting for TX confirmation before hub sync"
);
tokio::time::sleep(Duration::from_secs(SYNC_DELAY_SECS)).await;

sync_hub(provider, hub_mailbox, &old_escrow, &new_escrow, &format_signatures).await;
sync_hub(
provider,
hub_mailbox,
&old_escrow,
&new_escrow,
&format_signatures,
)
.await;

Ok(tx_ids)
}
Expand Down Expand Up @@ -84,14 +94,21 @@ async fn sync_hub<F>(
old_escrow: &str,
new_escrow: &str,
format_signatures: &F,
)
where
) where
F: Fn(&mut Vec<Signature>) -> ChainResult<Vec<u8>>,
{
let mut attempt: u64 = 0;
loop {
attempt += 1;
match ensure_hub_synced(provider, hub_mailbox, old_escrow, new_escrow, format_signatures).await {
match ensure_hub_synced(
provider,
hub_mailbox,
old_escrow,
new_escrow,
format_signatures,
)
.await
{
Ok(_) => {
info!("Post-migration hub sync completed");
return;
Expand Down
Loading