Skip to content

[fix] make cNIGHT grpc utxo queries deterministic#796

Open
lowhung wants to merge 10 commits intoinput-output-hk:mainfrom
lowhung:lowhung/cnight-grpc-endpoint-fixes
Open

[fix] make cNIGHT grpc utxo queries deterministic#796
lowhung wants to merge 10 commits intoinput-output-hk:mainfrom
lowhung:lowhung/cnight-grpc-endpoint-fixes

Conversation

@lowhung
Copy link
Collaborator

@lowhung lowhung commented Mar 18, 2026

Description

This PR moves the remaining gRPC-side compatibility logic for Midnight mainchain reads into Acropolis so a gRPC-backed node can match the existing db-sync-backed node behavior.

On the cNIGHT path, the endpoint now owns the full legacy compatibility contract instead of splitting it across the node client and the server:

  • GetUtxoEvents is anchored to a caller-supplied end_block_hash so event collection is bounded to a chosen Cardano head.
  • Event ordering is deterministic within a transaction by ordering create-like events before spend-like events and then by UTxO id.
  • The request includes the full start_position, and the response includes next_position, so the server owns both truncation and continuation semantics.
  • cNIGHT event timestamps and next_position timestamps are emitted in millis.
  • The endpoint reproduces the legacy transaction-capacity truncation behavior expected by the current node/network.

On the mc-hash path, the endpoint now answers historical stability queries instead of only current tip-relative ones:

  • GetLatestStableBlock and GetStableBlock take stability_offset plus as_of_timestamp_unix_millis.
  • The service evaluates stability against a timestamp window derived from Cardano protocol params.
  • Protocol-param-derived stable-window bounds are cached in midnight_state::State, so the service no longer queries the parameters module on every gRPC request.

Related Issue(s)

Relates to the matching Midnight node client changes in whankinsiv/midnight-node-acropolis#16.

How was this tested?

  • cargo test -p acropolis_module_midnight_state
  • cargo clippy -p acropolis_module_midnight_state -- -D warnings

Checklist

  • My code builds and passes local tests
  • I added/updated tests for my changes, where applicable
  • I updated documentation (if applicable)
  • branch has ≤ 5 commits (honor system)
  • commit messages tell a coherent story
  • branch is up to date with main (rebased on main; fast-forward possible)
  • CI/CD passes on the merged-with-main result

Impact / Side effects

This changes the Acropolis gRPC contract used by the Midnight node client:

  • cNIGHT GetUtxoEvents now requires end_block_hash and start_position, and returns next_position.
  • cNIGHT event timestamps are consistently emitted in millis.
  • mc-hash stable-block requests now require as_of_timestamp_unix_millis and use stability_offset naming.

The intended effect is to make the gRPC-backed node produce the same mainchain-derived inputs as the existing db-sync-backed node while keeping the compatibility logic server-owned.

Reviewer notes / Areas to focus

@lowhung lowhung changed the title [fix] align grpc mc-hash and cNIGHT endpoint semantics [fix] make cNIGHT grpc utxo queries deterministic Mar 18, 2026
}
message UtxoEventsResponse {
repeated UtxoEvent events = 1;
optional CardanoPosition next_position = 2;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed. The db-sync implementation uses the last event.

truncated_utxos
    .last()
    .map_or(start_position.clone(), |u| u.header.tx_position.clone())
    .increment(),

Copy link
Collaborator Author

@lowhung lowhung Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm I wanted to keep next_position in the response. The "legacy compatibility" behaviour here is a paired contract once the server applies the old truncation semantics, it also needs to return the matching next position...otherwise the client has to infer end again, which is the split contract I was hoping to remove. 09c780c.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request now carries the full start_position, so the endpoint can compute the exact fallback instead of making the client infer end again. That keeps truncation and continuation semantics together on the server. Updated in 5454aad

Comment on lines +872 to +903
fn stable_block_window(
protocol_params: &ProtocolParams,
as_of_timestamp_unix_millis: u64,
) -> Result<StableBlockWindow, Status> {
let shelley = protocol_params.shelley.as_ref().ok_or_else(|| {
Status::failed_precondition("latest protocol parameters do not include shelley params")
})?;

let active_slots_coeff_numerator = u128::from(*shelley.active_slots_coeff.numer());
let active_slots_coeff_denominator = u128::from(*shelley.active_slots_coeff.denom());
if active_slots_coeff_numerator == 0 {
return Err(Status::failed_precondition(
"active_slots_coeff numerator must be non-zero",
));
}

let slot_duration_millis = u128::from(shelley.slot_length).saturating_mul(1000);
let min_block_age_millis = rounded_div_u128_to_u64(
slot_duration_millis
.saturating_mul(u128::from(shelley.security_param))
.saturating_mul(active_slots_coeff_denominator),
active_slots_coeff_numerator,
)?;
let max_block_age_millis = min_block_age_millis.saturating_mul(3);

Ok(StableBlockWindow {
min_block_timestamp_unix_millis: as_of_timestamp_unix_millis
.saturating_sub(max_block_age_millis),
max_block_timestamp_unix_millis: as_of_timestamp_unix_millis
.saturating_sub(min_block_age_millis),
})
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think StableBlockWindow should be stored in state instead of having to query each time.

Copy link
Collaborator Author

@lowhung lowhung Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e8a3cb9. I did not store a literal timestamped StableBlockWindow, since that depends on each request’s as_of_timestamp_unix_millis; instead midnight_state::State now caches the derived bounds, and the gRPC service builds the request-specific window from those cached bounds.

@lowhung lowhung marked this pull request as ready for review March 19, 2026 23:16
Copy link
Collaborator

@whankinsiv whankinsiv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants