Skip to content

feat(overlay-topics): add tm_uora_dpp / ls_uora_dpp for attestation anchors - #423

Draft
mo-jaber wants to merge 1 commit into
mainfrom
feat/uora-dpp-topic
Draft

feat(overlay-topics): add tm_uora_dpp / ls_uora_dpp for attestation anchors#423
mo-jaber wants to merge 1 commit into
mainfrom
feat/uora-dpp-topic

Conversation

@mo-jaber

@mo-jaber mo-jaber commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Context, FYI

DPP on BSV: full context, architecture and open decisions

Background on the programme this comes from: why the work exists, how the pieces connect, and what is still undecided. For the overall direction, and optional to comment on.

This PR stands on its own: the overlay is what makes UORA attestations distributed and findable.

Summary

Adds tm_uora_dpp and ls_uora_dpp: admission and lookup for UORA attestation anchors, keyed on the did:key of the party that made the claim.

Draft on purpose. CONTRIBUTING.md asks that a substantial new capability be discussed before implementation, so this is the proposal with working code attached. Close it without hard feelings if the direction is wrong.

The problem

A digital product passport records claims made about one physical object over its life: who manufactured it, who it passed to, what service or recycling happened to it. Each claim has a different author, and the point is that a reader can check one without trusting whoever showed it to them.

The claims cannot go on chain, because they carry personal data and a battery fleet produces one per service event. So only a digest is anchored. But a digest alone cannot be indexed by author: an overlay holding the hash of a document it has never seen knows something existed and cannot say whose claim it was.

So "what has this party attested" has nowhere to be answered. That is what this topic is for.

What it adds

An anchor is a 1-satoshi PushDrop output:

# Field
0 uora-anchor-v2
1 SHA-256 of the attestation's RFC 8785 canonical form
2 attestation id
3 the issuer's did:key
4 subject: the product passport identifier
5 UORA attestation type
6 the anchoring service's identity key
7 signature over 0 to 6, appended by PushDrop.lock

Lookups take issuer, issuerKey, subject, attestationId or digest, narrowed optionally by uoraType or anchoredBy. Answers are the outputs as BEEF, so a caller checks them against the chain rather than trusting the index.

The decision worth reviewing

Field 6 names the anchoring service, and the locking key must be its BRC-42 child at [1, 'uora anchor v2'], key id the attestation id, counterparty anyone. Producing that needs the service's private key.

So every admitted anchor names its author checkably, by anyone holding only the transaction, with nothing configured. A shared node can carry anchors from a deployment it was never told about and still say whose each one is. This follows certmap, which validates against the registry operator its token names. Our first design instead configured each instance with a list of services to accept, which a shared node cannot sensibly be, and which leaves a reader holding only the transaction unable to attribute it at all. anchorServiceKeys survives as an optional narrowing, documented as a preference about what to index rather than a security control.

What it does not prove: that the party in field 3 made the claim. Anyone able to write an anchor can write any DID there. That is settled by the attestation's own signature, off chain. The class docblock and getDocumentation() both say so, because the field invites the opposite reading.

Anchors are leaves. Never spent, nothing retained. Every valid anchor in a transaction is admitted rather than exactly one, so a service can batch without this topic changing.

Compatibility

Additive only. No existing topic, export, type, schema, dependency or behaviour changes.

  • new exports registered in src/index.ts and added to the pack:check allowlist
  • new Mongo collection uoraDppAnchors, created lazily
  • @bsv/sdk usage stays inside the declared ^2.1.6 peer range; no dependency added
  • built on the existing shared/identifyPushDropOutputs helper

Evidence

Gate Result
typecheck clean
lint (oxlint --deny-warnings) clean
format:check clean
pack:check verified
test 39 suites, 411 tests, up from 384

27 new tests, negative cases included: an anchor naming a service its locking key cannot derive from, a field edited after signing, a lifted signature, a non-canonical key, a wrong key id, an Ed25519 did:key, an over-long field. Lookup coverage uses mongodb-memory-server, as the package's existing tests do.

Exercised on mainnet, end to end

Step Result
Attestation signed under a per-brand did:key did:key:zQ3shjnZjhHL3fcgraoPJhZUFQ4y6TBPg7K4iVyChLskjMc2Y
Anchored on mainnet uora-anchor-v2, digest 27ba6868…ab4f
Announced over BRC-22 to a node running this topic admitted
Asked ls_uora_dpp what that DID has attested one output, as BEEF
Other selectors subject, digest and anchoredBy each return it; a wrong uoraType and an unrelated DID return nothing
Re-verified from a public block explorer verified, and the locking key derives from field 6

Anchor: a66141b1c2386bf53e00b441d2cdcaf18948a575fb68e11a175093c89f6476da:0

The last row touches none of the issuing infrastructure, and it found a defect no unit test had: the writer's verify command had never learned to print the v2 attribution.

Status

This supports a demonstration programme and the data specification behind it is not final. That is the honest reason for the draft: better to answer the questions below now than after a release.

Exactly one anchor exists in this format on mainnet, kept to one on purpose. Changing the layout today costs a paragraph rather than a migration.

Questions

  1. Should this use @bsv/did instead of hand-rolling did:key? The reader needs about fifteen lines of multibase encoding. I wrote them locally, then found @bsv/did is a sibling workspace package exporting exactly this. The argument against is that it pulls qrcode, and an overlay index has no business shipping a QR library. Your call.

  2. Topic naming. tm_uora_dpp has an underscore where the convention here is unbroken (tm_supplychain, tm_walletconfig). It sits beside a tm_dpp the programme already runs, and the plan below names it, so a rename is @sirdeggen's call rather than mine.

  3. Does the anchor format belong in specs/ eventually? Not proposed now. It should earn that by being used.

Provenance

Implements a deliverable named in a BSV Association programme plan of 12 June 2026, "DPP on BSV, Plan and Next Steps", by Darren Kellenschwiler (@sirdeggen), whose resolver and protocol layer specifies "a new tm_uora_dpp topic manager + DID-keyed lookup in ts-stack packages/overlays/topics, rolled into our existing overlay infra, mainnet; attestations anchored as signed BSV transactions".

The writer lives in a separate BSV Association service. Its format document is implementable without access to either repository, and the two implementations are pinned by a fixture committed verbatim in both, since neither can import the other.

No overlapping work: searches for uora, dpp and product passport return nothing, and no open PR touches packages/overlays/topics.

…nchors

Admission and lookup for UORA attestation anchors in the `uora-anchor-v2`
format, keyed on the `did:key` of the party that made the claim.

A UORA attestation is a claim one party makes about one product: who made
it, who it passed to, what happened to it. The claims never go on chain,
both because they can carry personal data and because a digest is 32 bytes
whatever the claim's size, which is what keeps per-event anchoring
affordable at fleet scale. The anchor carries the digest plus the issuer,
subject, type and anchoring service in the clear, so an index has something
to key on.

What admission proves: the seven fields were sealed together by the key
locking the output, and that key is the BRC-42 child of the anchoring
service named in field 6 under counterparty `anyone`. Producing an output
satisfying both needs that service's private key, so every admitted anchor
names its author checkably, by anyone holding the transaction, with nothing
configured. That last part is why the service key is a field rather than a
constructor argument: a shared node serving several deployments would
otherwise need amending whenever one was added, and a reader holding only
the transaction could not attribute it at all. `anchorServiceKeys` narrows
what an instance carries and is documented as a preference about what to
index, not a boundary.

What it does not prove: that the party in field 3 made the claim. That is a
did:key copied onto the chain as given, and anyone able to write an anchor
can write any DID into it. What a claim is worth is settled by the
attestation's own signature, off chain. Both the topic manager docblock and
`getDocumentation()` say so, because the field invites the opposite reading.

Anchors are leaves: never spent, no predecessor, no transition rules,
nothing retained. Every valid anchor in a transaction is admitted rather
than exactly one, which leaves a service free to batch without this topic
changing.

Additive only. No existing topic, export, schema, dependency or behaviour
changes. Built on the existing `shared/identifyPushDropOutputs` helper and
follows `certmap`'s pattern of validating an output against an operator the
token itself names.

27 tests: 411 in the package, up from 384. Two worth reading are `refuses an
anchor naming a service its locking key cannot come from`, which is the
format's whole claim, and `does not reuse a fields array`, which pins that
PushDrop.lock appends the signature to the array it is handed.
@sonarqubecloud

sonarqubecloud Bot commented Aug 1, 2026

Copy link
Copy Markdown

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