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
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ under the pre-1.0 conventions in [VERSIONING.md](docs/governance/VERSIONING.md):
against an earlier version keeps its classes and the bytes its signatures
cover; a new one is written against the new version. (#561)

### Added

- **A lost submission handle is found again.** A replayed idempotency key is
answered with the conflict and not the submission, so a node whose first
attempt got through and whose record of the answer was lost, by a crash
between the `POST` and the row write or a failed write, held a `submitted` row
it could never poll. The drain now calls `dpp-core` 0.23.0's
`find_submission`, which reads the dashboard and matches the passport by the
unique product identifier it was submitted under, the one thing the registry
reports each passport against, and answers with the handle and the ruling
together, so a recovered passport is registered in the same pass. It does not
guess: an identifier is not unique to a passport, since a successor can be
submitted under its predecessor's, so more than one submission reporting it is
`Ambiguous`, listed for a person, and a task still being checked reports no
passport, so not finding one while any is on the list is never read as the
registry holding none, and a single match is taken only from a search that
read every task. A row that is not found is backed off and counted as an
attempt, so it reaches the stalled count instead of sorting to the head of the
queue, and so is one whose recovered handle could not be written down.
A finished task's result is kept in a bounded cache once it refuses or names
every registration, because the lookup repeats every pass; one that does not
yet is read again. A task the list shows twice is one candidate. (#484)

### Fixed

- **A draft is published under the operator's primary identifier as it stands
Expand Down Expand Up @@ -108,6 +131,23 @@ under the pre-1.0 conventions in [VERSIONING.md](docs/governance/VERSIONING.md):
fields too, as terms; their values are not served, and the tests that scan the
body for a withheld field now read it without its `@context`. (#561)

- **A submission past the first thousand tasks on the dashboard is found.** The
dashboard's task list was read from the front for twenty pages, so once an
operator had more than a thousand tasks a new submission on an oldest-first
list was never found and was polled forever. The first page says how long the
list is (`totalElements`), and a longer one is now read from both ends,
interleaved, since the order is not recorded. A search that ran out says so
instead of reporting the registry as still working. (#484)

- **An access credential that fails authentication is told which of seven
things to fix:** a token that is not a compact JWS, no key for its `kid`, a
signature that does not verify, a header or signature that cannot be read, a
token not typed as a VC-JWT, a payload that is not a credential, or an issuer
other than its document's. They read the same before, so an issuer whose
tooling had not yet set `typ` was sent to debug a sound signature. The
presenter's own `kid` and issuer are not echoed. A bad signature keeps its old
sentence. (#484)

## [0.15.0] - 2026-11-03

### Breaking
Expand Down
61 changes: 60 additions & 1 deletion crates/dpp-node/src/infra/registry/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,22 @@
//! "registered", which is exactly the ghost-as-real dishonesty the honesty
//! invariant forbids.

use std::collections::HashMap;
use std::sync::Arc;

use dpp_common::event_codes;
use dpp_domain::error::DppError;
use reqwest::Client;
use tokio::sync::RwLock;
use tokio::sync::{Mutex, RwLock};

use super::config::EuRegistrySyncConfig;
use super::dashboard::TaskResult;
use super::token::CachedToken;

/// How many finished tasks' results are kept. A bound, not a tuning knob: it
/// exists so the cache cannot grow with the registry's history.
const FINISHED_RESULTS_KEPT: usize = 2_048;

/// HTTP adapter for the EU Central DPP Registry.
///
/// Implements `RegistrySyncPort` (see the `super::mapping` module) by making REST calls
Expand All @@ -56,6 +62,14 @@ pub struct EuRegistrySync {
pub(super) client: Client,
pub(super) config: EuRegistrySyncConfig,
token_cache: Arc<RwLock<Option<CachedToken>>>,
/// Results of tasks the registry has finished with, by task identifier.
///
/// A finished task's result does not change, so reading one again can only
/// cost a request. Looking for a submission whose handle was lost reads
/// every finished task's result, every pass, until it finds the passport; this
/// is what keeps that to the tasks that are new. Unfinished tasks are never
/// put here, because theirs does change.
finished_results: Mutex<HashMap<String, Arc<TaskResult>>>,
}

impl EuRegistrySync {
Expand All @@ -70,9 +84,54 @@ impl EuRegistrySync {
client,
config,
token_cache: Arc::new(RwLock::new(None)),
finished_results: Mutex::new(HashMap::new()),
})
}

/// The result of a finished task this adapter has already read, if it has.
pub(super) async fn cached_finished_result(&self, task_id: &str) -> Option<Arc<TaskResult>> {
self.finished_results.lock().await.get(task_id).cloned()
}

/// The result of a task the registry has finished with: from the cache when
/// this adapter has read it before, from the registry otherwise.
///
/// Only for a finished task. The caller checks that, because a result read
/// while the task is still being checked is not the result it will have.
pub(super) async fn finished_result(
&self,
results_url: &str,
task_id: &str,
) -> Result<Arc<TaskResult>, DppError> {
if let Some(hit) = self.cached_finished_result(task_id).await {
return Ok(hit);
}
self.read_finished_result(results_url, task_id).await
}

/// Read a finished task's result from the registry, and keep it once it is
/// [complete](TaskResult::is_complete).
pub(super) async fn read_finished_result(
&self,
results_url: &str,
task_id: &str,
) -> Result<Arc<TaskResult>, DppError> {
let result: TaskResult = self
.with_retry(|| self.get_json(results_url, &[], "task result"))
.await
.map_err(RetryableError::into_dpp_error)?;
let result = Arc::new(result);
if !result.is_complete() {
return Ok(result);
}
let mut kept = self.finished_results.lock().await;
if kept.len() >= FINISHED_RESULTS_KEPT {
kept.clear();
}
kept.insert(task_id.to_owned(), Arc::clone(&result));
Ok(result)
}

/// Acquire an OAuth2 access token, using the cache when possible.
pub(super) async fn get_token(&self) -> Result<String, DppError> {
use super::token::TokenResponse;
Expand Down
Loading
Loading