Skip to content

Commit daef340

Browse files
feat: update notifications to get milestone by height (#482)
Co-authored-by: David Salami <31099392+Wizdave97@users.noreply.github.com> Co-authored-by: David Salami <wizdave97@gmail.com>
1 parent 0057366 commit daef340

File tree

3 files changed

+220
-42
lines changed

3 files changed

+220
-42
lines changed

modules/consensus/tendermint/prover/src/tests/integration_tests.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ mod tests {
128128
)?;
129129

130130
let (milestone_number, milestone) = client.get_latest_milestone().await?;
131+
trace!("Latest milestone: number {}", milestone_number);
131132

132133
let latest_height = client.latest_height().await?;
133134
let abci_query: AbciQuery =
@@ -143,6 +144,16 @@ mod tests {
143144
return Err("Proof should be present".into());
144145
}
145146

147+
let latest_milestone_at_height =
148+
client.get_latest_milestone_at_height(latest_height).await?;
149+
150+
match latest_milestone_at_height {
151+
Some((number, _)) => {
152+
trace!("Latest milestone at latest height {}: number {}", latest_height, number)
153+
},
154+
None => trace!("No milestone found at height {}", latest_height),
155+
}
156+
146157
Ok(())
147158
}
148159

tesseract/consensus/polygon/src/client.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,144 @@ impl HeimdallClient {
242242
Ok(abci_query)
243243
}
244244

245+
/// Retrieves the milestone count at a specific height using ABCI query.
246+
///
247+
/// This method queries the Heimdall node's ABCI store to get the milestone count
248+
/// at the specified height, allowing milestone updates even when syncing.
249+
///
250+
/// # Arguments
251+
///
252+
/// * `height` - The height at which to query the milestone count
253+
///
254+
/// # Returns
255+
///
256+
/// - `Ok(u64)`: The milestone count at the specified height
257+
/// - `Err(ProverError)`: If the request fails or the response cannot be parsed
258+
///
259+
/// # Errors
260+
///
261+
/// Returns `ProverError` if:
262+
/// - The ABCI query fails
263+
/// - The height conversion fails
264+
/// - The response cannot be deserialized
265+
pub async fn get_milestone_count_at_height(
266+
&self,
267+
height: u64,
268+
) -> Result<Option<u64>, ProverError> {
269+
let key = vec![0x83];
270+
271+
let abci_query: AbciQuery = self
272+
.http_client
273+
.abci_query(
274+
Some("/store/milestone/key".to_string()),
275+
key,
276+
Some(Height::try_from(height).unwrap()),
277+
true,
278+
)
279+
.await
280+
.map_err(|e| ProverError::NetworkError(e.to_string()))?;
281+
282+
let count_bytes = abci_query.value;
283+
if count_bytes.is_empty() {
284+
return Ok(None); // No milestones yet
285+
}
286+
287+
// The count is stored as a u64 in big-endian format
288+
if count_bytes.len() != 8 {
289+
return Err(ProverError::ConversionError(
290+
"Invalid milestone count bytes length".to_string(),
291+
));
292+
}
293+
294+
let mut bytes = [0u8; 8];
295+
bytes.copy_from_slice(&count_bytes[..=7]);
296+
let count = u64::from_be_bytes(bytes);
297+
298+
Ok(Some(count))
299+
}
300+
301+
/// Retrieves the latest milestone at a specific height using ABCI query.
302+
///
303+
/// This method queries the Heimdall node's ABCI store to get the latest milestone
304+
/// at the specified height, allowing milestone updates even when syncing.
305+
///
306+
/// # Arguments
307+
///
308+
/// * `height` - The height at which to query the latest milestone
309+
///
310+
/// # Returns
311+
///
312+
/// - `Ok(Option<(u64, Milestone)>)`: The milestone number and data if available
313+
/// - `Err(ProverError)`: If the request fails or the response cannot be parsed
314+
///
315+
/// # Errors
316+
///
317+
/// Returns `ProverError` if:
318+
/// - The ABCI query fails
319+
/// - The height conversion fails
320+
/// - The response cannot be deserialized
321+
pub async fn get_latest_milestone_at_height(
322+
&self,
323+
height: u64,
324+
) -> Result<Option<(u64, Milestone)>, ProverError> {
325+
let count = self.get_milestone_count_at_height(height).await?;
326+
327+
match count {
328+
Some(count) => {
329+
let milestone = self.get_milestone_at_height(count, height).await?;
330+
Ok(Some((count, milestone)))
331+
},
332+
None => Ok(None),
333+
}
334+
}
335+
336+
/// Retrieves a specific milestone at a specific height using ABCI query.
337+
///
338+
/// This method queries the Heimdall node's ABCI store to get a specific milestone
339+
/// at the specified height.
340+
///
341+
/// # Arguments
342+
///
343+
/// * `milestone_number` - The milestone number to retrieve
344+
/// * `height` - The height at which to query the milestone
345+
///
346+
/// # Returns
347+
///
348+
/// - `Ok(Milestone)`: The milestone data
349+
/// - `Err(ProverError)`: If the request fails or the response cannot be parsed
350+
///
351+
/// # Errors
352+
///
353+
/// Returns `ProverError` if:
354+
/// - The ABCI query fails
355+
/// - The height conversion fails
356+
/// - The response cannot be deserialized
357+
pub async fn get_milestone_at_height(
358+
&self,
359+
milestone_number: u64,
360+
height: u64,
361+
) -> Result<Milestone, ProverError> {
362+
let mut key = vec![0x81];
363+
key.extend_from_slice(&milestone_number.to_be_bytes());
364+
365+
let abci_query: AbciQuery = self
366+
.http_client
367+
.abci_query(
368+
Some("/store/milestone/key".to_string()),
369+
key,
370+
Some(Height::try_from(height).unwrap()),
371+
true,
372+
)
373+
.await
374+
.map_err(|e| ProverError::NetworkError(e.to_string()))?;
375+
376+
let milestone = ismp_polygon::Milestone::proto_decode(&abci_query.value).map_err(|e| {
377+
ProverError::ConversionError(format!("Failed to decode milestone: {}", e))
378+
})?;
379+
380+
Ok(milestone)
381+
}
382+
245383
/// Fetches an Ethereum block header from the execution RPC client and converts it to a
246384
/// CodecHeader.
247385
///

tesseract/consensus/polygon/src/notification.rs

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub async fn consensus_notification(
2626
let consensus_state: ConsensusState =
2727
ConsensusState::decode(&mut &consensus_state_serialized[..])?;
2828

29-
let trusted_state: TrustedState = consensus_state.tendermint_state.into();
29+
let trusted_state: TrustedState = consensus_state.clone().tendermint_state.into();
3030

3131
let untrusted_header = client.prover.signed_header(latest_height).await?;
3232

@@ -42,46 +42,9 @@ pub async fn consensus_notification(
4242
true,
4343
);
4444

45-
let (mut milestone_number, mut milestone) = client.prover.get_latest_milestone().await?;
46-
let query_height = untrusted_header.header.height.value() - 1;
47-
let mut milestone_proof =
48-
client.prover.get_milestone_proof(milestone_number, query_height).await?;
49-
50-
if milestone_proof.value.is_empty() {
51-
milestone_number -= 1;
52-
milestone_proof = client.prover.get_milestone_proof(milestone_number, query_height).await?;
53-
54-
milestone = ismp_polygon::Milestone::proto_decode(&milestone_proof.value)
55-
.map_err(|e| anyhow::anyhow!("failed to decode milestone: {}", e))?;
56-
}
57-
58-
let maybe_milestone_update = if milestone.end_block > consensus_state.last_finalized_block {
59-
let evm_header = client
60-
.prover
61-
.fetch_header(milestone.end_block)
62-
.await?
63-
.ok_or_else(|| anyhow::anyhow!("EVM header not found"))?;
64-
65-
let merkle_proof = milestone_proof
66-
.clone()
67-
.proof
68-
.map(|p| convert_tm_to_ics_merkle_proof::<ICS23HostFunctions>(&p))
69-
.transpose()
70-
.map_err(|_| anyhow::anyhow!("bad client state proof"))?
71-
.ok_or_else(|| anyhow::anyhow!("proof not found"))?;
72-
73-
let proof = CommitmentProofBytes::try_from(merkle_proof)
74-
.map_err(|e| anyhow::anyhow!("bad client state proof: {}", e))?;
75-
76-
Some(ismp_polygon::MilestoneUpdate {
77-
evm_header,
78-
milestone_number,
79-
ics23_state_proof: proof.into(),
80-
milestone,
81-
})
82-
} else {
83-
None
84-
};
45+
let maybe_milestone_update =
46+
build_milestone_update(client, untrusted_header.header.height.value(), &consensus_state)
47+
.await?;
8548

8649
match validator_set_hash_match.is_ok() && next_validator_set_hash_match.is_ok() {
8750
true => {
@@ -138,6 +101,16 @@ pub async fn consensus_notification(
138101
let matched_height = height;
139102
let matched_header = matched_header.expect("Header must be present if found");
140103
let next_validators = client.prover.next_validators(matched_height).await?;
104+
105+
// Also attempt to construct a milestone update corresponding to the matched header
106+
// height
107+
let maybe_milestone_update = build_milestone_update(
108+
client,
109+
matched_header.header.height.value(),
110+
&consensus_state,
111+
)
112+
.await?;
113+
141114
return Ok(Some(PolygonConsensusUpdate {
142115
tendermint_proof: CodecConsensusProof::from(&ConsensusProof::new(
143116
matched_header.clone(),
@@ -147,7 +120,7 @@ pub async fn consensus_notification(
147120
Some(next_validators)
148121
},
149122
)),
150-
milestone_update: None,
123+
milestone_update: maybe_milestone_update,
151124
}));
152125
} else {
153126
log::error!(target: "tesseract", "Fatal error, failed to find any header that matches onchain validator set");
@@ -158,6 +131,62 @@ pub async fn consensus_notification(
158131
Ok(None)
159132
}
160133

134+
async fn build_milestone_update(
135+
client: &PolygonPosHost,
136+
reference_height: u64,
137+
consensus_state: &ConsensusState,
138+
) -> anyhow::Result<Option<ismp_polygon::MilestoneUpdate>> {
139+
let query_height = reference_height.saturating_sub(1);
140+
let latest_milestone_at_height =
141+
client.prover.get_latest_milestone_at_height(query_height).await?;
142+
143+
let (milestone_number, milestone) = match latest_milestone_at_height {
144+
Some((number, milestone)) => (number, milestone),
145+
None => {
146+
log::warn!(
147+
target: "tesseract",
148+
"No milestone found at height {}, falling back to current latest",
149+
reference_height
150+
);
151+
return Ok(None);
152+
},
153+
};
154+
155+
let milestone_proof = client.prover.get_milestone_proof(milestone_number, query_height).await?;
156+
157+
if milestone_proof.value.is_empty() {
158+
return Ok(None);
159+
}
160+
161+
if milestone.end_block > consensus_state.last_finalized_block {
162+
let evm_header = client
163+
.prover
164+
.fetch_header(milestone.end_block)
165+
.await?
166+
.ok_or_else(|| anyhow::anyhow!("EVM header not found"))?;
167+
168+
let merkle_proof = milestone_proof
169+
.clone()
170+
.proof
171+
.map(|p| convert_tm_to_ics_merkle_proof::<ICS23HostFunctions>(&p))
172+
.transpose()
173+
.map_err(|_| anyhow::anyhow!("bad client state proof"))?
174+
.ok_or_else(|| anyhow::anyhow!("proof not found"))?;
175+
176+
let proof = CommitmentProofBytes::try_from(merkle_proof)
177+
.map_err(|e| anyhow::anyhow!("bad client state proof: {}", e))?;
178+
179+
Ok(Some(ismp_polygon::MilestoneUpdate {
180+
evm_header,
181+
milestone_number,
182+
ics23_state_proof: proof.into(),
183+
milestone,
184+
}))
185+
} else {
186+
Ok(None)
187+
}
188+
}
189+
161190
pub fn convert_tm_to_ics_merkle_proof<H>(
162191
tm_proof: &ProofOps,
163192
) -> Result<MerkleProof, anyhow::Error> {

0 commit comments

Comments
 (0)