Skip to content

Commit 0b01acd

Browse files
authored
Merge pull request #512 from bsv-blockchain/fix/uora-anchor-exact-reader
fix(overlay-topics): read the uora anchor output exactly
2 parents 9660166 + 80fbdff commit 0b01acd

11 files changed

Lines changed: 228 additions & 30 deletions

File tree

‎docs/packages/overlays/overlay-topics.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: '@bsv/overlay-topics'
44
kind: package
55
domain: overlays
66
npm: '@bsv/overlay-topics'
7-
version: '1.7.1'
7+
version: '1.7.2'
88
last_updated: '2026-08-27'
99
last_verified: '2026-08-27'
1010
review_cadence_days: 30
@@ -207,3 +207,18 @@ const admittance = await manager.identifyAdmissibleOutputs(beef, [])
207207
- [API reference (TypeDoc)](https://bsv-blockchain.github.io/ts-stack/api/overlay-topics/)
208208
- [Source on GitHub](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/topics)
209209
- [npm](https://www.npmjs.com/package/@bsv/overlay-topics)
210+
211+
## UORA v3 reader compatibility
212+
213+
Version 1.7.2 aligns `readUoraAnchor` and `tm_uora_dpp` with the UORA v3 format:
214+
a 33-byte compressed locking key, eight fields, exactly four `OP_2DROP`
215+
instructions, and printable UTF-8 text without C0/C1 controls or DEL. Valid
216+
anchors keep the same bytes, signing preimage, topic identifier and admission
217+
result. Unicode text remains supported; no new normalization is applied.
218+
219+
Coordinate reader upgrades across nodes serving `tm_uora_dpp`. Audit any
220+
previously indexed nonconforming outputs before rebuilding that topic, because
221+
older readers may have admitted inputs that the format does not permit.
222+
Repository fixtures establish format compatibility; they do not establish an
223+
inventory of every deployed or historical anchor. Other topics, lookup query
224+
shapes and persisted schemas are unchanged.

‎docs/reference/package-api-migrations.md‎

Lines changed: 3 additions & 3 deletions
Large diffs are not rendered by default.

‎docs/reference/stack-facts.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ authorized release action.
6161
| 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) |
6262
| 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) |
6363
| 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) |
64-
| overlays | `@bsv/overlay-topics` | `1.7.1` | node-library | node-esm | node | `>=22` | [packages/overlays/topics](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/topics) |
64+
| overlays | `@bsv/overlay-topics` | `1.7.2` | node-library | node-esm | node | `>=22` | [packages/overlays/topics](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/topics) |
6565
| sdk | `@bsv/sdk` | `2.6.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) |
6666
| 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) |
6767
| 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) |

‎governance/package-release-notes.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@
147147
"name": "@bsv/overlay-topics",
148148
"publishedVersion": "1.6.10",
149149
"releaseType": "minor",
150-
"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.",
151-
"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."
150+
"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.",
151+
"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."
152152
},
153153
{
154154
"name": "@bsv/paymail",

‎governance/repository-health/baselines.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@
321321
"@bsv/overlay": "2.3.1",
322322
"@bsv/overlay-discovery-services": "2.2.1",
323323
"@bsv/overlay-express": "2.6.1",
324-
"@bsv/overlay-topics": "1.7.1",
324+
"@bsv/overlay-topics": "1.7.2",
325325
"@bsv/sdk": "2.6.0",
326326
"@bsv/verifast": "0.3.5",
327327
"@bsv/btms": "1.2.2",

‎packages/overlays/topics/CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ All notable changes to this project will be documented in this file. The format
3838

3939
### Fixed
4040

41-
- (Document bugs that were fixed since the last release.)
41+
- Version 1.7.2 aligns `tm_uora_dpp` with the versioned UORA v3 format: compressed locking keys, exact drop tails, and printable UTF-8 fields. Valid anchors retain their bytes and admission result. The shared reference fixture covers key and tail validation. Coordinate reader upgrades and audit previously indexed nonconforming outputs before rebuilding the topic; this change does not claim a complete inventory of historical anchors.
4242

4343
### Security
4444

‎packages/overlays/topics/README.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,18 @@ ESM consumer.
197197
## License
198198

199199
Open BSV License — see [LICENSE.txt](./LICENSE.txt).
200+
201+
## UORA v3 reader compatibility
202+
203+
Version 1.7.2 aligns `readUoraAnchor` and `tm_uora_dpp` with the UORA v3 format:
204+
a 33-byte compressed locking key, eight fields, exactly four `OP_2DROP`
205+
instructions, and printable UTF-8 text without C0/C1 controls or DEL. Valid
206+
anchors keep the same bytes, signing preimage, topic identifier and admission
207+
result. Unicode text remains supported; no new normalization is applied.
208+
209+
Coordinate reader upgrades across nodes serving `tm_uora_dpp`. Audit any
210+
previously indexed nonconforming outputs before rebuilding that topic, because
211+
older readers may have admitted inputs that the format does not permit.
212+
Repository fixtures establish format compatibility; they do not establish an
213+
inventory of every deployed or historical anchor. Other topics, lookup query
214+
shapes and persisted schemas are unchanged.

‎packages/overlays/topics/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bsv/overlay-topics",
3-
"version": "1.7.1",
3+
"version": "1.7.2",
44
"sideEffects": false,
55
"engines": {
66
"node": ">=22"

‎packages/overlays/topics/src/__tests__/uoradpp.test.ts‎

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,48 @@ describe('the boundary between the subject and the type', () => {
529529
})
530530
})
531531

532+
describe('UORA v3 key and drop-tail validation', () => {
533+
const F = ANCHOR_V3_FIXTURE
534+
const manager = new UoraDppTopicManager()
535+
536+
it('refuses the 65-byte uncompressed locking push', async () => {
537+
const script = LockingScript.fromHex(F.uncompressedKey)
538+
expect(() => readUoraAnchor(script)).toThrow(/locking key push is not 33 bytes/)
539+
const admitted = await manager.identifyAdmissibleOutputs(txWith(script).toBEEF(), [])
540+
expect(admitted.outputsToAdmit).toEqual([])
541+
})
542+
543+
it('refuses a short, mixed or trailing drop tail', async () => {
544+
expect(F.malformedTail).toHaveLength(3)
545+
for (const hex of F.malformedTail) {
546+
const script = LockingScript.fromHex(hex)
547+
expect(() => readUoraAnchor(script)).toThrow(/drop tail/)
548+
const admitted = await manager.identifyAdmissibleOutputs(txWith(script).toBEEF(), [])
549+
expect(admitted.outputsToAdmit).toEqual([])
550+
}
551+
})
552+
553+
it('names what is wrong with a script that is not this shape at all', () => {
554+
const key = { op: 33, data: Utils.toArray(SERVICE_KEY, 'hex') }
555+
const checksig = { op: 0xac }
556+
expect(() => readUoraAnchor(new LockingScript([]))).toThrow(/locking key push is not 33 bytes/)
557+
expect(() => readUoraAnchor(new LockingScript([key, { op: 0x6a }]))).toThrow(/no OP_CHECKSIG/)
558+
expect(() => readUoraAnchor(new LockingScript([key, checksig, { op: 0x6a }]))).toThrow(
559+
/opcode 0x6a where a field should be/
560+
)
561+
// 33 bytes in the compressed shape that are not a point on the curve.
562+
const notAPoint = { op: 33, data: Utils.toArray(`02${'00'.repeat(32)}`, 'hex') }
563+
expect(() => readUoraAnchor(new LockingScript([notAPoint, checksig]))).toThrow(
564+
/not a valid public key/
565+
)
566+
// OP_0 and a small-integer opcode read as the bytes they push, and the tail
567+
// is exact for two fields, so this fails on the count and nothing earlier.
568+
expect(() =>
569+
readUoraAnchor(new LockingScript([key, checksig, { op: 0 }, { op: 0x51 }, { op: 0x6d }]))
570+
).toThrow(/expected 7 fields and a signature, found 2/)
571+
})
572+
})
573+
532574
describe('what readUoraAnchor refuses', () => {
533575
const manager = new UoraDppTopicManager()
534576

@@ -551,6 +593,25 @@ describe('what readUoraAnchor refuses', () => {
551593
)
552594
}
553595

596+
it('preserves printable Unicode and exact field length boundaries', async () => {
597+
const unicode = claim({ subject: '产品/café/🌱', uoraType: 'Origine é' })
598+
const bounded = claim({
599+
attestationId: 'i'.repeat(256),
600+
subject: 's'.repeat(512),
601+
uoraType: 't'.repeat(64)
602+
})
603+
for (const expected of [unicode, bounded]) {
604+
const script = await anchorScript(expected)
605+
const { anchor, fields } = readUoraAnchor(script)
606+
expect(anchor).toMatchObject(expected)
607+
await expect(
608+
assertAnchorSignature(fields, anchor.anchoredBy, anchor.attestationId)
609+
).resolves.toBeUndefined()
610+
const result = await manager.identifyAdmissibleOutputs(txWith(script).toBEEF(), [])
611+
expect(result.outputsToAdmit).toEqual([0])
612+
}
613+
})
614+
554615
it('refuses an output that is not this many fields', async () => {
555616
// Two fields and no signature. The count is checked before anything is
556617
// read, because a short output has no field 6 to attribute it by and
@@ -604,6 +665,25 @@ describe('what readUoraAnchor refuses', () => {
604665
}
605666
})
606667

668+
it('refuses a field carrying a control character', async () => {
669+
// Well-formed UTF-8 that round-trips, and still not text the format admits:
670+
// a C0 control, 0x7F and a C1 control are refused, as the reference reader
671+
// refuses them, so an index never carries a subject or a type it cannot show.
672+
for (const code of [0x07, 0x7f, 0x85]) {
673+
const one = claim()
674+
const script = await scriptWithFields([
675+
UORA_ANCHOR_PREFIX,
676+
one.digest,
677+
one.attestationId,
678+
one.issuer,
679+
one.subject,
680+
`${one.uoraType}${String.fromCharCode(code)}`,
681+
one.anchoredBy
682+
])
683+
expect(() => readUoraAnchor(script)).toThrow(/control character/)
684+
}
685+
})
686+
607687
it('refuses a field that is not valid UTF-8 text', async () => {
608688
// Invalid continuation byte: toUTF8 either throws or produces a string that
609689
// does not re-encode to the same bytes. Either path is "not UTF-8".
@@ -702,9 +782,9 @@ describe('UoraDppLookupService, at its edges', () => {
702782
/valid query is required/
703783
)
704784
// `query` omitted is treated as `{}`, not as a missing question object.
705-
await expect(
706-
service.lookup({ service: 'ls_uora_dpp' } as LookupQuestion)
707-
).rejects.toThrow(/issuer, issuerKey, subject, attestationId or digest/)
785+
await expect(service.lookup({ service: 'ls_uora_dpp' } as LookupQuestion)).rejects.toThrow(
786+
/issuer, issuerKey, subject, attestationId or digest/
787+
)
708788
await expect(
709789
service.lookup({ service: 'ls_uora_dpp', query: { issuer: '' } } as LookupQuestion)
710790
).rejects.toThrow(/issuer, issuerKey, subject, attestationId or digest/)

0 commit comments

Comments
 (0)