Merge pull request #6537 from mempool/natsoni/document-taptree
Some checks failed
Backend Integration Tests with MariaDB / Backend Integration Tests - node 24.13.0 (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Backend (dev) - node 24.13.0 (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Backend (prod) - node 24.13.0 (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Cache assets for builds (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Frontend (dev) - node 24.13.0 (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Frontend (prod) - node 24.13.0 (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for liquid (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for mempool (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for testnet4 (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Validate generated backend Docker JSON (push) Has been cancelled
Supply Chain Audit / Backend install-script audit (push) Has been cancelled
Supply Chain Audit / Frontend install-script audit (push) Has been cancelled

Add documentation for taproot tree features
This commit is contained in:
mononaut 2026-06-04 10:43:47 +09:00 committed by GitHub
commit b3bfdd8c26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 108 additions and 7 deletions

View file

@ -37,6 +37,7 @@ interface LeafNode {
})
export class TaprootAddressScriptsComponent implements OnChanges {
@Input() address: string;
@Input() network?: string;
@Input() scripts: Map<string, ScriptInfo>;
@Output() tapTreeIncomplete = new EventEmitter<boolean>(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<string, [string, string]>());
treeStructure[0].set(outputKeyNode, [internalKeyNode, merkleRootNode]);
this.depth++;

View file

@ -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 { }

View file

@ -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',

View file

@ -466,6 +466,30 @@
</dl>
</ng-template>
<ng-template type="how-does-the-taproot-tree-work">
<p>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.</p>
<p>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.</p>
<p>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.</p>
<div class="taproot-faq-widget" *ngIf="faqTaprootInfo?.published as taprootInfo">
<app-taproot-address-scripts [address]="faqTaprootAddress" network="mainnet" [scripts]="taprootInfo.scripts"></app-taproot-address-scripts>
</div>
<p><a [href]="('/address/' | relativeUrl:'mainnet') + faqTaprootAddress">View an example on the address page</a>.</p>
</ng-template>
<ng-template type="how-can-i-share-or-verify-taproot-scripts">
<p>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.</p>
<p>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 <fa-icon [icon]="['fas', 'code-fork']" [fixedWidth]="true"></fa-icon> icon to open a form where Taproot data can be entered. The same data can also be shared directly in the URL fragment:</p>
<ul>
<li><a [href]="('/address/' | relativeUrl:'mainnet') + faqTaprootAddress + '#taptree=01c0462060e531bc7b23e145618de9d21a9240e9cf1909a32e77b688f36ec67901500d58ac202edfc0c6e4166b1d5497d9c7a72e7ed4c83fe03596fe5ce5edf7311ddeddf3b1ba529c01c02220ab46f1bd685e9c768cca20e5b9a5972b4e1ebab9afda82012ffd0a09d340eb39ac'"><code>/address/&lt;address&gt;#taptree=&lt;taptree&gt;</code></a></li>
<li><a [href]="('/address/' | relativeUrl:'mainnet') + faqTaprootAddress + '#psbt=cHNidP8BAF4CAAAAAaw0pDKziErd65x14VlMyr0dk40sbfnsamlaosoIMs/uAAAAAAD9////ARwUAAAAAAAAIlEgnvbZpjy+K9OYazdE0ZpDDp7aT7dUorK7Vj6Y0E7uqqkAAAAAAAEAXgIAAAABgLeH7oKnqJhBXimT6kZD0aqj5ruNbph8lB9VAPQr3sIAAAAAAP3///8BvBQAAAAAAAAiUSBJJVwiFU5hOZBwUBPCN2A/wOQ9ck4K4MpzwtCbYVTCpQAAAAABASu8FAAAAAAAACJRIEklXCIVTmE5kHBQE8I3YD/A5D1yTgrgynPC0JthVMKlQhXAitqBVHimnBwQrybUyzcOpT3P3sL9AwCmrjUQQVEzwSbXHdkZm5ahJwmj+qvwMlpwzuB/N8Mh7VP8L72/Mr/zS0cgYOUxvHsj4UVhjenSGpJA6c8ZCaMud7aI827GeQFQDVisIC7fwMbkFmsdVJfZx6cuftTIP+A1lv5c5e33MR3e3fOxulKcwEIVwIragVR4ppwcEK8m1Ms3DqU9z97C/QMApq41EEFRM8EmHxKGqo8QrcKRJ/V/AofE38r3/E00jTURzyXSM2o8QowjIKtG8b1oXpx2jMog5bmllytOHrq5r9qCAS/9CgnTQOs5rMABFyCK2oFUeKacHBCvJtTLNw6lPc/ewv0DAKauNRBBUTPBJgAA'"><code>/address/&lt;address&gt;#psbt=&lt;PSBT&gt;</code></a></li>
<li><a [href]="('/address/' | relativeUrl:'mainnet') + faqTaprootAddress + '#tapleaf=c08ada815478a69c1c10af26d4cb370ea53dcfdec2fd0300a6ae3510415133c126d71dd9199b96a12709a3faabf0325a70cee07f37c321ed53fc2fbdbf32bff34b:2060e531bc7b23e145618de9d21a9240e9cf1909a32e77b688f36ec67901500d58ac202edfc0c6e4166b1d5497d9c7a72e7ed4c83fe03596fe5ce5edf7311ddeddf3b1ba529cc0'"><code>/address/&lt;address&gt;#tapleaf=&lt;control-block&gt;:&lt;script&gt;&lt;leaf-version&gt;</code></a></li>
</ul>
<div class="taproot-faq-widget" *ngIf="faqTaprootInfo?.full as taprootInfo">
<app-taproot-address-scripts [address]="faqTaprootAddress" network="mainnet" [scripts]="taprootInfo.scripts"></app-taproot-address-scripts>
</div>
<p>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.</p>
</ng-template>
<ng-template type="what-are-sigops">
<p>A "sigop" is a way of accounting for the cost of "signature operations" in Bitcoin script, like <code>OP_CHECKSIG</code>, <code>OP_CHECKSIGVERIFY</code>, <code>OP_CHECKMULTISIG</code> and <code>OP_CHECKMULTISIGVERIFY</code></p>
<p>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.</p>

View file

@ -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 {

View file

@ -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<FaqTemplateDirective>;
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;
}
}

View file

@ -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 { }

View file

@ -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 { }