From c614d97f8129f14e5a7fe93bd19d9bab307f9a4c Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 1 Jun 2026 17:57:26 +0200 Subject: [PATCH] Add documentation for taproot tree features --- .../taproot-address-scripts.component.ts | 3 +- .../taproot-address-scripts.module.ts | 24 +++++++++++++++ .../src/app/docs/api-docs/api-docs-data.ts | 14 +++++++++ .../app/docs/api-docs/api-docs.component.html | 24 +++++++++++++++ .../app/docs/api-docs/api-docs.component.scss | 6 ++++ .../app/docs/api-docs/api-docs.component.ts | 30 +++++++++++++++++++ frontend/src/app/docs/docs.module.ts | 6 ++++ frontend/src/app/graphs/graphs.module.ts | 8 ++--- 8 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 frontend/src/app/components/taproot-address-scripts/taproot-address-scripts.module.ts diff --git a/frontend/src/app/components/taproot-address-scripts/taproot-address-scripts.component.ts b/frontend/src/app/components/taproot-address-scripts/taproot-address-scripts.component.ts index 3b2f9bf0d..48f2d72c4 100644 --- a/frontend/src/app/components/taproot-address-scripts/taproot-address-scripts.component.ts +++ b/frontend/src/app/components/taproot-address-scripts/taproot-address-scripts.component.ts @@ -37,6 +37,7 @@ interface LeafNode { }) export class TaprootAddressScriptsComponent implements OnChanges { @Input() address: string; + @Input() network?: string; @Input() scripts: Map; @Output() tapTreeIncomplete = new EventEmitter(true); @@ -117,7 +118,7 @@ export class TaprootAddressScriptsComponent implements OnChanges { // Expand the tree to include public key, internal key and merkle root const internalKeyNode = scripts[0].taprootInfo.scriptPath.internalKey; const merkleRootNode = treeStructure.length > 0 ? treeStructure[0].keys().next().value : leaves.keys().next().value; - const outputKeyNode = addressToScriptPubKey(this.address, this.stateService.network || '').scriptPubKey.slice(4); + const outputKeyNode = addressToScriptPubKey(this.address, this.network || this.stateService.network || '').scriptPubKey.slice(4); treeStructure.unshift(new Map()); treeStructure[0].set(outputKeyNode, [internalKeyNode, merkleRootNode]); this.depth++; diff --git a/frontend/src/app/components/taproot-address-scripts/taproot-address-scripts.module.ts b/frontend/src/app/components/taproot-address-scripts/taproot-address-scripts.module.ts new file mode 100644 index 000000000..54de1d962 --- /dev/null +++ b/frontend/src/app/components/taproot-address-scripts/taproot-address-scripts.module.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NgxEchartsModule } from 'ngx-echarts'; +import { SharedModule } from '@app/shared/shared.module'; +import { AsmStylerPipe } from '@app/shared/pipes/asm-styler/asm-styler.pipe'; +import { TaprootAddressScriptsComponent } from '@components/taproot-address-scripts/taproot-address-scripts.component'; + +@NgModule({ + declarations: [ + TaprootAddressScriptsComponent, + ], + imports: [ + CommonModule, + SharedModule, + NgxEchartsModule, + ], + exports: [ + TaprootAddressScriptsComponent, + ], + providers: [ + AsmStylerPipe, + ], +}) +export class TaprootAddressScriptsModule { } diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index ef4f848af..48af8f6d7 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -12924,6 +12924,20 @@ export const faqData = [ fragment: 'why-do-the-projected-block-fee-ranges-overlap', title: 'Why do the projected block fee ranges overlap?', }, + { + type: 'endpoint', + category: 'advanced', + showConditions: bitcoinNetworks, + fragment: 'how-does-the-taproot-tree-work', + title: 'How does the Taproot Tree work?', + }, + { + type: 'endpoint', + category: 'advanced', + showConditions: bitcoinNetworks, + fragment: 'how-can-i-share-or-verify-taproot-scripts', + title: 'How can I share or verify Taproot scripts?', + }, { type: 'category', category: 'self-hosting', diff --git a/frontend/src/app/docs/api-docs/api-docs.component.html b/frontend/src/app/docs/api-docs/api-docs.component.html index fbe467a4d..d22322fa2 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.html +++ b/frontend/src/app/docs/api-docs/api-docs.component.html @@ -466,6 +466,30 @@ + +

Taproot outputs can commit to many spending scripts. Each script is a leaf in a Merkle tree, called the Taproot Tree, and the Taproot output key commits to the root of that tree.

+

This makes Taproot addresses more private: an external observer does not know all script paths, because scripts are only revealed if coins are spent using them.

+

The Taproot Tree widget shows the script paths already known from on-chain spends, and highlights branches where additional spending scripts may exist but have not been published.

+
+ +
+

View an example on the address page.

+
+ + +

A third party may need to verify unpublished script paths for a Taproot address. One option is to reveal each script by spending coins through every path on-chain, but this hurts privacy and may not be possible if a script is timelocked, for example.

+

A more private option is to share the Taproot data directly with the third party using the Taproot Tree widget. On the address page, use the icon to open a form where Taproot data can be entered. The same data can also be shared directly in the URL fragment:

+ +
+ +
+

Cryptographic checks on the provided data are done in the browser so the verifier can independently confirm that the provided scripts commit to the Taproot output key for the address.

+
+

A "sigop" is a way of accounting for the cost of "signature operations" in Bitcoin script, like OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY

These signature operations incur different costs depending on whether they are single or multi-sig operations, and on where they appear in a Bitcoin transaction.

diff --git a/frontend/src/app/docs/api-docs/api-docs.component.scss b/frontend/src/app/docs/api-docs/api-docs.component.scss index d2a6564f2..706147a8a 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.scss +++ b/frontend/src/app/docs/api-docs/api-docs.component.scss @@ -288,6 +288,12 @@ h3 { } } +.taproot-faq-widget { + background-color: var(--bg); + margin: 1rem 0; + padding: 16px; +} + :host-context(.ltr-layout) { .blockchain-wrapper.time-ltr .blocks-wrapper, .blockchain-wrapper .blocks-wrapper { diff --git a/frontend/src/app/docs/api-docs/api-docs.component.ts b/frontend/src/app/docs/api-docs/api-docs.component.ts index 4a6486a33..b38455029 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.ts +++ b/frontend/src/app/docs/api-docs/api-docs.component.ts @@ -5,6 +5,12 @@ import { tap, takeUntil } from 'rxjs/operators'; import { ActivatedRoute } from '@angular/router'; import { faqData, restApiDocsData, wsApiDocsData, electrumApiDocsData } from '@app/docs/api-docs/api-docs-data'; import { FaqTemplateDirective } from '@app/docs/faq-template/faq-template.component'; +import { AddressTypeInfo } from '@app/shared/address-utils'; +import { convertTextToBuffer, extractTapLeaves, fillTapTree, TapLeaf } from '@app/shared/transaction.utils'; + +const FAQ_TAPROOT_ADDRESS = 'bc1pfyj4cgs4fesnnyrs2qfuydmq8lqwg0tjfc9wpjnnctgfkc25c2jshnmyl4'; +const FAQ_TAPROOT_TAPTREE = '01c0462060e531bc7b23e145618de9d21a9240e9cf1909a32e77b688f36ec67901500d58ac202edfc0c6e4166b1d5497d9c7a72e7ed4c83fe03596fe5ce5edf7311ddeddf3b1ba529c01c02220ab46f1bd685e9c768cca20e5b9a5972b4e1ebab9afda82012ffd0a09d340eb39ac'; +const FAQ_TAPROOT_INTERNAL_KEY = '8ada815478a69c1c10af26d4cb370ea53dcfdec2fd0300a6ae3510415133c126'; @Component({ selector: 'app-api-docs', @@ -37,6 +43,8 @@ export class ApiDocsComponent implements OnInit, AfterViewInit { timeLtrSubscription: Subscription; timeLtr: boolean = this.stateService.timeLtr.value; isMempoolSpaceBuild = this.stateService.isMempoolSpaceBuild; + faqTaprootAddress = FAQ_TAPROOT_ADDRESS; + faqTaprootInfo = this.buildFaqTaprootInfo(); @ViewChildren(FaqTemplateDirective) faqTemplates: QueryList; dict = {}; @@ -238,5 +246,27 @@ export class ApiDocsComponent implements OnInit, AfterViewInit { return `${wsHostname}${curlNetwork}/api/v1/ws`; } + buildFaqTaprootInfo(): { published: AddressTypeInfo, full: AddressTypeInfo } | null { + try { + const leaves = extractTapLeaves(undefined, [], convertTextToBuffer(FAQ_TAPROOT_TAPTREE), convertTextToBuffer(FAQ_TAPROOT_INTERNAL_KEY)); + const publishedLeaf = leaves[1]; + if (!publishedLeaf) { + return null; + } + return { + published: this.fillFaqTaprootInfo([publishedLeaf]), + full: this.fillFaqTaprootInfo(leaves), + }; + } catch (error) { + console.warn('Failed to build Taproot FAQ example', error); + return null; + } + } + + fillFaqTaprootInfo(leaves: TapLeaf[]): AddressTypeInfo { + const taprootInfo = new AddressTypeInfo('mainnet', FAQ_TAPROOT_ADDRESS); + fillTapTree(taprootInfo, leaves); + return taprootInfo; + } } diff --git a/frontend/src/app/docs/docs.module.ts b/frontend/src/app/docs/docs.module.ts index 7f94135aa..99092702d 100644 --- a/frontend/src/app/docs/docs.module.ts +++ b/frontend/src/app/docs/docs.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { NgxEchartsModule } from 'ngx-echarts'; import { SharedModule } from '@app/shared/shared.module'; import { ApiDocsComponent } from '@app/docs/api-docs/api-docs.component'; import { DocsComponent } from '@app/docs/docs/docs.component'; @@ -7,6 +8,7 @@ import { ApiDocsNavComponent } from '@app/docs/api-docs/api-docs-nav.component'; import { CodeTemplateComponent } from '@app/docs/code-template/code-template.component'; import { DocsRoutingModule } from '@app/docs/docs.routing.module'; import { FaqTemplateDirective } from '@app/docs/faq-template/faq-template.component'; +import { TaprootAddressScriptsModule } from '@components/taproot-address-scripts/taproot-address-scripts.module'; @NgModule({ declarations: [ ApiDocsComponent, @@ -18,7 +20,11 @@ import { FaqTemplateDirective } from '@app/docs/faq-template/faq-template.compon imports: [ CommonModule, SharedModule, + TaprootAddressScriptsModule, DocsRoutingModule, + NgxEchartsModule.forRoot({ + echarts: () => import('@app/graphs/echarts').then(m => m.echarts), + }), ] }) export class DocsModule { } diff --git a/frontend/src/app/graphs/graphs.module.ts b/frontend/src/app/graphs/graphs.module.ts index 4fc9781d3..5994a1775 100644 --- a/frontend/src/app/graphs/graphs.module.ts +++ b/frontend/src/app/graphs/graphs.module.ts @@ -45,9 +45,8 @@ import { TreasuriesVerifyProgressComponent } from '@components/treasuries/verify import { UtxoGraphComponent } from '@components/utxo-graph/utxo-graph.component'; import { ActiveAccelerationBox } from '@components/acceleration/active-acceleration-box/active-acceleration-box.component'; import { AddressesTreemap } from '@components/addresses-treemap/addresses-treemap.component'; -import { TaprootAddressScriptsComponent } from '@components/taproot-address-scripts/taproot-address-scripts.component'; +import { TaprootAddressScriptsModule } from '@components/taproot-address-scripts/taproot-address-scripts.module'; import { CommonModule } from '@angular/common'; -import { AsmStylerPipe } from '@app/shared/pipes/asm-styler/asm-styler.pipe'; @NgModule({ declarations: [ @@ -94,11 +93,11 @@ import { AsmStylerPipe } from '@app/shared/pipes/asm-styler/asm-styler.pipe'; UtxoGraphComponent, ActiveAccelerationBox, AddressesTreemap, - TaprootAddressScriptsComponent, ], imports: [ CommonModule, SharedModule, + TaprootAddressScriptsModule, GraphsRoutingModule, NgxEchartsModule.forRoot({ echarts: () => import('@app/graphs/echarts').then(m => m.echarts), @@ -108,8 +107,5 @@ import { AsmStylerPipe } from '@app/shared/pipes/asm-styler/asm-styler.pipe'; NgxEchartsModule, ActiveAccelerationBox, ], - providers: [ - AsmStylerPipe - ] }) export class GraphsModule { }