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
62 changes: 31 additions & 31 deletions docs/packages/overlays/overlay-topics.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: '@bsv/overlay-topics'
kind: package
domain: overlays
npm: '@bsv/overlay-topics'
version: '1.7.3'
version: '1.8.0'
last_updated: '2026-09-15'
last_verified: '2026-09-15'
review_cadence_days: 30
Expand Down Expand Up @@ -208,36 +208,6 @@ const admittance = await manager.identifyAdmissibleOutputs(beef, [])
- [Source on GitHub](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/topics)
- [npm](https://www.npmjs.com/package/@bsv/overlay-topics)

## Mandala admin chain and spender identity (1.7.3)

Version 1.7.3 closes two admission holes in `tm_mandala`:

- Admin actions are anchored to the chain of spends. A non-genesis action
(`issue`, `reissue`, `unpause`, `unfreeze`, `allowIdentity`, …) must spend a
prior the engine lists in `previousCoins`, and when the optional
`stateStore.isAdminOutpoint(assetId, txid, vout)` is supplied, one recorded
as an admin output of that asset. Re-deriving the lock key from
`details.counterparty` proved nothing, because BRC-42 lets the named
counterparty compute and spend that key itself.
- Spenders are named from the owner bound at admission
(`stateStore.getTokenRow`), not from input linkage. A supplied input linkage
is verified as proof with `verifyInputKeyLinkage` (`prover + L*G`): it must
control the coin being spent and agree with the stored owner, or the
transaction is rejected. Sanctions and access-mode screening therefore run
against the actual spender, and spends of sender-blinded receipts are no
longer refused.

- Every token-shaped output must carry a linkage that verifies to the key it
is locked to. One that is missing, mismatched or unreadable rejects the whole
transaction with `output N: MandalaToken-decodable output with no verified
linkage`. Skipping it left a phantom coin: siblings were admitted, the
admission signed and the transaction broadcast with an unattested token
output inside it.

Operators should supply `isAdminOutpoint` from their lookup store; without it
the prior check still requires the spent input to be one the engine admitted.
Topic and lookup identifiers, persisted schemas and query shapes are unchanged.

## UORA v3 reader compatibility

Version 1.7.2 aligns `readUoraAnchor` and `tm_uora_dpp` with the UORA v3 format:
Expand All @@ -252,3 +222,33 @@ older readers may have admitted inputs that the format does not permit.
Repository fixtures establish format compatibility; they do not establish an
inventory of every deployed or historical anchor. Other topics, lookup query
shapes and persisted schemas are unchanged.

### Mandala admission and the 1.8.0 upgrade

Use the same `MandalaStorageManager` for Mandala admission and lookup. The
reference store now implements `isAdminOutpoint(assetId, txid, outputIndex)`
against admitted admin history. Custom adapters must implement that predicate;
a missing verifier rejects non-genesis admin actions. Its optional TypeScript
member preserves source compatibility, not permission to bypass verification.
Never implement it as a constant `true`.

Registration must omit `assetId` or use an empty string: the registration's own
outpoint defines its asset. Subsequent admin actions must spend a previously
admitted admin output for that same asset. Token spends require a stored owner
row matching the source outpoint, asset and amount. Optional input linkage
corroborates that owner and the source locking key; it cannot replace missing
state. Sender blinding and transfers without input linkage remain supported
when authoritative owner state is present. Linkage arrays require unique,
non-negative integer indices.

Before upgrading an existing Mandala deployment, back up and audit its admin
history and token-owner records. Restore missing rows from verified admission
evidence before historical replay; do not infer authority from a submitted
payload. The engine identifies admissible outputs before sending spend
notifications, so normal admission can read the owner before lookup removes
the spent row. Custom replay adapters must preserve that ordering. These checks
do not retroactively validate old records.

Coordinate the admission and lookup upgrade. Existing valid wire fields and
encodings are unchanged, and no database collection migration is required.
Keep the new admission checks enabled while repairing historical data.
6 changes: 3 additions & 3 deletions docs/reference/package-api-migrations.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/reference/stack-facts.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ authorized release action.
| overlays | `@bsv/overlay` | `2.3.1` | node-library | node-cjs, node-esm | node | `>=22` | [packages/overlays/overlay](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/overlay) |
| overlays | `@bsv/overlay-discovery-services` | `2.2.1` | node-library | node-cjs, node-esm | node | `>=22` | [packages/overlays/overlay-discovery-services](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/overlay-discovery-services) |
| overlays | `@bsv/overlay-express` | `2.6.1` | node-library | node-cjs, node-esm | node | `>=22` | [packages/overlays/overlay-express](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/overlay-express) |
| overlays | `@bsv/overlay-topics` | `1.7.3` | node-library | node-esm | node | `>=22` | [packages/overlays/topics](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/topics) |
| overlays | `@bsv/overlay-topics` | `1.8.0` | node-library | node-esm | node | `>=22` | [packages/overlays/topics](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/topics) |
| sdk | `@bsv/sdk` | `2.7.0` | browser-library | browser-bundler, browser-esm, node-cjs, node-esm, umd-global | browser, node, umd | `>=22` | [packages/sdk](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk) |
| sdk | `@bsv/verifast` | `0.3.5` | wasm-library | browser-bundler, browser-esm, node-cjs, node-esm, umd-global, wasm-worker | browser, node, umd, wasm, worker | `>=22` | [packages/verifast](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/verifast) |
| wallet | `@bsv/btms` | `1.2.2` | node-library | node-cjs, node-esm | node | `>=22` | [packages/wallet/btms](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/btms) |
Expand Down
2 changes: 1 addition & 1 deletion governance/mutation-testing/targets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export function buildMutationTargets(repositoryRoot) {
packageDirectory: 'packages/overlays/topics',
manifest: 'packages/overlays/topics/package.json',
propertyTest: 'packages/overlays/topics/src/mandala/__tests/types.property.test.ts',
mutate: ['src/mandala/types.ts:72-78', 'src/admission/issuerPolicy.ts:36-39'],
mutate: ['src/mandala/types.ts:72-97', 'src/admission/issuerPolicy.ts:36-39'],
...jestTarget('jest.config.js', ['<rootDir>/src/mandala/__tests/types*.test.ts'], {
esm: true
})
Expand Down
4 changes: 2 additions & 2 deletions governance/package-release-notes.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@
"name": "@bsv/overlay-topics",
"publishedVersion": "1.6.10",
"releaseType": "minor",
"summary": "Adds persistent first-writer reservations for UMP presentation and recovery hashes, aborts provisional claims after strict broadcast failure, keeps confirmed owners protected until successor indexing, retries transient initialization, marks one-time legacy bootstrap, and returns the newest bounded legacy candidates for verified lineage or an operator pin. It also retains the collection-index resilience and opt-in repair controls from the prior candidate. Standardizes first-party author metadata on the current BSV Association name. Aligns the UORA v3 reader with the versioned format, including compressed locking keys, exact drop tails, and printable UTF-8 fields. Anchors Mandala admin actions to the chain of spends and names token spenders from the owner bound at admission, verifying any input linkage as proof of control. Rejects any token-shaped output that lacks a verified linkage instead of skipping it.",
"migration": "Existing topic and lookup identifiers remain unchanged. Production UMP overlays must give UMPTopicManager and the UMP lookup service Mongo-backed stores that use the same database, then roll out before updated wallet clients; the no-argument manager is bounded but intended only for isolated single-process use. The reservation and bootstrap-marker collections are additive and initialize from currently indexed UMP UTXOs; take a MongoDB backup before rollout. Legacy ambiguous rows remain visible and can be resolved with WAB pinning rather than deleted. Valid uora-anchor-v3 outputs retain their bytes, signatures, and admission result. Readers now reject nonconforming key, tail, and text shapes. Coordinate reader upgrades across nodes serving tm_uora_dpp and audit any previously indexed nonconforming outputs before rebuilding that topic. Mandala operators should supply the optional stateStore.isAdminOutpoint from their lookup store so admin actions anchor to recorded admin outputs; without it the prior must still be an input the engine admitted. Wallets that reveal input linkages must reveal the spender's own child key (prover + L*G). No wire prefix, export, or persisted schema changes are required."
"summary": "Adds persistent first-writer reservations for UMP presentation and recovery hashes, aborts provisional claims after strict broadcast failure, keeps confirmed owners protected until successor indexing, retries transient initialization, marks one-time legacy bootstrap, and returns the newest bounded legacy candidates for verified lineage or an operator pin. It also retains the collection-index resilience and opt-in repair controls from the prior candidate. Standardizes first-party author metadata on the current BSV Association name. Aligns the UORA v3 reader with the versioned format, including compressed locking keys, exact drop tails, and printable UTF-8 fields. Version 1.8.0 requires verified per-asset admin history and matching stored token ownership for Mandala admission, assigns registrations to their own genesis outpoint, rejects ambiguous linkage indices, and normalizes in-memory sanctions key casing. Retains the complete token-output linkage verification from 1.7.3.",
"migration": "Existing topic and lookup identifiers remain unchanged. Production UMP overlays must give UMPTopicManager and the UMP lookup service Mongo-backed stores that use the same database, then roll out before updated wallet clients; the no-argument manager is bounded but intended only for isolated single-process use. The reservation and bootstrap-marker collections are additive and initialize from currently indexed UMP UTXOs; take a MongoDB backup before rollout. Legacy ambiguous rows remain visible and can be resolved with WAB pinning rather than deleted. Valid uora-anchor-v3 outputs retain their bytes, signatures, and admission result. Readers now reject nonconforming key, tail, and text shapes. Coordinate reader upgrades across nodes serving tm_uora_dpp and audit any previously indexed nonconforming outputs before rebuilding that topic. No wire prefix, export, or persisted schema changes are required. Mandala custom state adapters must implement isAdminOutpoint against admitted per-asset admin history and retain matching owner rows during admission. Registration must omit assetId or use an empty string. Back up and audit historical admin and ownership records before replay; missing records must be restored from verified admission evidence. Valid wire fields and encodings are unchanged, and no collection migration is needed. Wallets that reveal input linkage must reveal the spender's own child key."
},
{
"name": "@bsv/paymail",
Expand Down
2 changes: 1 addition & 1 deletion governance/repository-health/baselines.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@
"@bsv/overlay": "2.3.1",
"@bsv/overlay-discovery-services": "2.2.1",
"@bsv/overlay-express": "2.6.1",
"@bsv/overlay-topics": "1.7.3",
"@bsv/overlay-topics": "1.8.0",
"@bsv/sdk": "2.7.0",
"@bsv/verifast": "0.3.5",
"@bsv/btms": "1.2.2",
Expand Down
11 changes: 11 additions & 0 deletions infra/overlay-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,14 @@ Pull requests and issues are welcome! Please open an issue to discuss any major

## License
[Open BSV License Version 6](./LICENSE.txt)

## Mandala state adapter compatibility

The Mandala manager and lookup share one lazily initialized storage manager.
The admission adapter verifies admin outpoints against that store's per-asset
history, including the asset, transaction ID and output index. This wiring uses
the existing history API so it can compile with the currently locked package
and consume Overlay Topics 1.8.0's stricter admission contract on upgrade.
Before a deployed upgrade, follow the [Mandala migration guide](../../packages/overlays/topics/README.md#mandala-admission-and-the-180-upgrade)
and audit historical admin and owner records. Source publication does not
upgrade a running overlay or its locked dependencies automatically.
2 changes: 1 addition & 1 deletion infra/overlay-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"scripts": {
"build": "tsc",
"lint": "oxlint src --deny-warnings",
"test": "node --import tsx --test src/lifecycle.test.ts",
"test": "node --import tsx --test src/lifecycle.test.ts src/mandalaStateStore.test.ts",
"start": "node --import ./dist/telemetry.js dist/index.js",
"dev": "tsx src/index.ts"
},
Expand Down
8 changes: 3 additions & 5 deletions infra/overlay-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createMandalaStateStore } from './mandalaStateStore.js'
import { WalletAdvertiser } from '@bsv/overlay-discovery-services'
import OverlayExpress from '@bsv/overlay-express'
import {
Expand Down Expand Up @@ -440,18 +441,15 @@ const main = async () => {
}
return mandalaStorage
}
const mandalaStateStore = createMandalaStateStore(requireMandalaStorage)
server.configureTopicManager(
'tm_mandala',
new MandalaTopicManager({
verifierWallet: mandalaWallet,
screeningProvider: new InMemoryScreeningProvider([]),
adminWallet: mandalaWallet,
adminProtocolID: [2, 'mandala admin'] as [2, string],
stateStore: {
getAssetState: async assetId => await requireMandalaStorage().getAssetState(assetId),
getTokenRow: async (txid, outputIndex) =>
await requireMandalaStorage().getTokenRow(txid, outputIndex)
}
stateStore: mandalaStateStore
})
)
server.configureLookupServiceWithMongo('ls_mandala', db => {
Expand Down
36 changes: 36 additions & 0 deletions infra/overlay-server/src/mandalaStateStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { createMandalaStateStore } from './mandalaStateStore.js'

test('resolves the shared store lazily and matches the exact admin history tuple', async () => {
const state = { assetId: 'asset' }
const token = { txid: 'token', outputIndex: 2 }
let ready = false
const store = {
async getAssetState(assetId: string) {
assert.equal(assetId, 'asset')
return state
},
async getTokenRow(txid: string, index: number) {
assert.equal(txid, 'token')
assert.equal(index, 2)
return token
},
async findAdminHistoryByAssetId() {
return [{ assetId: 'asset', txid: 'admin', outputIndex: 1 }]
}
}
type Store = ReturnType<Parameters<typeof createMandalaStateStore>[0]>
const adapter = createMandalaStateStore(() => {
if (!ready) throw new Error('storage unavailable')
return store as unknown as Store
})
await assert.rejects(adapter.isAdminOutpoint('asset', 'admin', 1), /storage unavailable/)
ready = true
assert.equal(await adapter.getAssetState('asset'), state)
assert.equal(await adapter.getTokenRow('token', 2), token)
assert.equal(await adapter.isAdminOutpoint('asset', 'admin', 1), true)
assert.equal(await adapter.isAdminOutpoint('other', 'admin', 1), false)
assert.equal(await adapter.isAdminOutpoint('asset', 'other', 1), false)
assert.equal(await adapter.isAdminOutpoint('asset', 'admin', 0), false)
})
23 changes: 23 additions & 0 deletions infra/overlay-server/src/mandalaStateStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { MandalaStorageManager } from '@bsv/overlay-topics'

type MandalaStore = Pick<
MandalaStorageManager,
'getAssetState' | 'getTokenRow' | 'findAdminHistoryByAssetId'
>

/** Resolve lazily because Mongo lookup configuration initializes the shared store. */
export function createMandalaStateStore(resolve: () => MandalaStore) {
return {
getAssetState: async (assetId: string) => await resolve().getAssetState(assetId),
getTokenRow: async (txid: string, outputIndex: number) =>
await resolve().getTokenRow(txid, outputIndex),
// Use the existing history API to support the currently published store.
isAdminOutpoint: async (assetId: string, txid: string, outputIndex: number) => {
const history = await resolve().findAdminHistoryByAssetId(assetId)
return history.some(
entry =>
entry.assetId === assetId && entry.txid === txid && entry.outputIndex === outputIndex
)
}
}
}
36 changes: 10 additions & 26 deletions packages/overlays/topics/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,33 +53,17 @@ All notable changes to this project will be documented in this file. The format

### Security

- `tm_mandala`: anchor the admin chain to the chain of spends. `verifyAdminOutput`
re-derived the expected lock key from `details.counterparty`, which arrives in
the unauthenticated off-chain payload; by BRC-42 that key belongs to the named
counterparty, who can compute and spend it from their own root key plus the
overlay's public identity key, so any third party could reproduce the expected
`pubKeyHash`. The prior check accepted any input of the same transaction.
Together these admitted forged `unpause`, `unfreeze`, `allowIdentity` and —
because a verified admin output credits authorized issuance — forged `issue`
and `reissue`, an unbounded mint. A non-genesis action must now spend a prior
that the engine lists in `previousCoins` and, when the new optional
`stateStore.isAdminOutpoint` is supplied, one recorded as an admin output of
that asset. Delegation is unaffected: authority passes to whoever the next
admin output is locked to.
- `tm_mandala`: name a spend from the owner bound at admission rather than from
the submitted payload. `verifyKeyLinkage` returns `linkage.counterparty`,
which for an input is whoever *paid* that coin, not the spender, and can
never be checked against the coin being spent. Sanctions and access-mode
screening therefore ran against the wrong party, a submitter could steer
screening by choosing what to reveal, and omitting input linkages skipped
sender screening entirely. Spenders now come from `stateStore.getTokenRow`; a
supplied input linkage is verified as proof via the new
`verifyInputKeyLinkage` (`prover + L*G`), which must control the spent coin
and agree with the stored owner. This also unblocks sender blinding, whose
one-time key is never registry-admitted and would otherwise have caused every
spend of a blinded receipt to be refused.
- Version 1.8.0 requires admitted per-asset admin history for every non-genesis
Mandala action. The reference storage manager provides the verifier; custom
adapters must implement it. Registration uses its own genesis outpoint.
- Token spends require authoritative stored ownership matching the source
outpoint, asset and amount. Optional linkage corroborates the stored owner
and source key. Sender blinding remains supported.
- Reject duplicate or invalid linkage indices and normalize sanctions key
casing. Valid wire fields and encodings are unchanged.
- Back up and audit historical admin and ownership records before replay, and
coordinate admission and lookup upgrades. See the README migration guide.

---

## [1.6.0] - 2026-07-10

Expand Down
Loading
Loading