Skip to content

changes for mainnet; to schnorr for signing, to spaceHash #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
13 changes: 3 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@

Fabric is a trustless, distributed DNS resolver built on top of [hyperdht](https://github.com/holepunchto/hyperdht), extending its capabilities to allow publishing signed zone files using [spaces](https://spacesprotocol.org) as keys authenticated by Bitcoin. [Spaces](https://spacesprotocol.org) are sovereign Bitcoin identities and serve as a trust anchor, while Fabric DHT enables publishing records off-chain without adding any unnecessary on-chain bloat.

**Note:** Fabric currently defaults to Bitcoin testnet4 since spaces are not yet on Bitcoin mainnet.

## Prerequisites
To use `fabric` and `beam`, you need to:

- Run Bitcoin Core on testnet4
- Run Bitcoin Core on mainnet
- Install and sync spaces

You may use [this guide](https://docs.spacesprotocol.org/getting-started/installation) to set these up.
Expand All @@ -36,9 +34,6 @@ beam @onion TXT

Space @now also has TXT records published.

**Note**: `beam` will automatically connect to a locally run spaces node using its default port for `testnet4` to verify answers from the DHT.


## How to publish records for a Space?

1. Create a zone file (e.g., example.zone) with an SOA record and the records you want to publish:
Expand All @@ -55,13 +50,13 @@ Space @now also has TXT records published.
2. Find the space's private key using `space-cli`

```shell
space-cli --chain testnet4 exportwallet | grep '"spaces_descriptor"' | sed -E 's/.*(tprv[^\/]*).*/\1/'
space-cli --chain mainnet exportwallet | grep '"descriptor"' | sed -E 's/.*(xprv[^\/]*).*/\1/'
```

it should look something like this:

```
tprv8ZgxMBicQKsPeUUxV746bQ9JmsytoyEeioAd9b962bQxcq7PfK8vRbFkSR7JD7ySoBoyswHX5vQvnhS95dHKUxW2maG2Tt7bJcCHsY66gNF
xprv8ZgxMBicQKsPeUUxV746bQ9JmsytoyEeioAd9b962bQxcq7PfK8vRbFkSR7JD7ySoBoyswHX5vQvnhS95dHKUxW2maG2Tt7bJcCHsY66gNF
```


Expand All @@ -83,8 +78,6 @@ You can either:

Run a node if you want to publish your own zones and also contribute to the network. Specify a reachable ip/port:

**Note**: Fabric will automatically connect to a locally run spaces node using its default port for testnet4.

```
fabric --host <ip-address> --port <public-port>
```
Expand Down
2 changes: 1 addition & 1 deletion bin/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function defineMainOptions() {
.version('0.1.0')
.option('--seeds <nodes...>', 'Connect to the following bootstrap nodes')
.option('--peers <nodes...>', 'Include the following known peers')
.option('--chain <chain>', 'Bitcoin network', 'testnet4')
.option('--chain <chain>', 'Bitcoin network', 'mainnet')
.option('--spaces-rpc-url <url>', 'Specify a spaces rpc url (default based on chain)');
}

Expand Down
12 changes: 9 additions & 3 deletions constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import * as crypto from 'hypercore-crypto';
import { BootstrapNode, BootstrapNodes } from './types';

export const BOOTSTRAP_NODES = [
'107.152.45.120@testnet4.fabric.buffrr.dev:22253',
]
export const BOOTSTRAP_NODES = {
mainnet: [
{ host: '44.209.201.250', port: 40357 }
],
testnet4: [
{ host: '107.152.45.120', port: 22253 }
]
};

// Extend the existing COMMANDS from hyperdht/lib/constants
export const COMMANDS = {
Expand Down
4 changes: 3 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import b4a from 'b4a';
import {Spaces} from './spaces';
import * as m from './messages';
import {DHT} from "dht-rpc";
import { BootstrapNode, BootstrapNodes } from './types';

const defaultMaxSize = 32768;
const defaultMaxAge = 48 * 60 * 60 * 1000; // 48 hours
Expand Down Expand Up @@ -52,7 +53,8 @@ export class Fabric extends HyperDHT {
public spaces: Spaces;

constructor(opts: FabricOptions = {}) {
opts.bootstrap = opts.bootstrap || BOOTSTRAP_NODES
const chain = (opts.spaces?.resolver?.chain || 'mainnet') as keyof BootstrapNodes;
opts.bootstrap = opts.bootstrap || BOOTSTRAP_NODES[chain].map((n: BootstrapNode) => `${n.host}:${n.port}`);
super(opts);
this.once('persistent', () => {
this._zones = new Cache(opts.zones || {
Expand Down
48 changes: 25 additions & 23 deletions schnorr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,32 @@ interface TweakedKeyPair {

// Finds the tweaked private key from an extended private key matching a given address
export function findTweakedPair(xprv: string, derivedAddress: string): TweakedKeyPair | null {
const path = `${prefix}'/86'/1'/0'/0/*`;
const network = xprv.startsWith('tprv') ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;
const node: BIP32Interface = bip32.fromBase58(xprv, network);
const network = xprv.startsWith('tprv') ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;
const path = `86'/0'/0'/0/*`;

const rootNode: BIP32Interface = bip32.fromBase58(xprv, network);
const pathComponents = path.split('/').filter(component => component !== '*');
const baseNode: BIP32Interface = pathComponents.reduce((acc: BIP32Interface, component: string) => {
const isHardened = component.endsWith('\'');
const index = parseInt(component, 10);

const pathComponents = path.split('/').filter(component => component !== '*');
const baseNode: BIP32Interface = pathComponents.reduce((acc: BIP32Interface, component: string) => {
const isHardened = component.endsWith('\'');
const index = parseInt(component, 10);
return isHardened ? acc.deriveHardened(index) : acc.derive(index);
}, node);
return isHardened ? acc.deriveHardened(index) : acc.derive(index);
}, rootNode);

const maxDerivations = 1000;
for (let i = 0; i < maxDerivations; i++) {
const child = baseNode.derive(i);
const pubkey = child.publicKey.slice(1, 33);
const {address} = bitcoin.payments.p2tr({
internalPubkey: pubkey,
network,
});
if (!address) continue;
const MAX_DERIVATIONS = 1000;
for (let i = 0; i < MAX_DERIVATIONS; i++) {
const child = baseNode.derive(i);
const pubkey = child.publicKey.slice(1, 33);
const {address} = bitcoin.payments.p2tr({
internalPubkey: pubkey,
network,
});
if (!address) continue;
const tweakHash = bitcoin.crypto.taggedHash('TapTweak', pubkey);
const tweakedPriv = child.tweak(tweakHash) as BIP32Interface;
const tweakedPriv = child.tweak(tweakHash) as BIP32Interface;

if (address === derivedAddress || spaceAddress(address) === derivedAddress) {
return {
return {
privateKey: tweakedPriv.privateKey!,
publicKey: tweakedPriv.publicKey!.slice(1),
};
Expand All @@ -65,10 +66,11 @@ export function scriptPubKeyToAddress(scriptPubKey: string, network: string = 'm
// Adjusts address prefix for the space network
function spaceAddress(address: string): string {
const decoded = bech32m.decode(address);
if (decoded.prefix !== 'tb' && decoded.prefix !== 'bc') {
throw new Error('Invalid address prefix');
// Handle bc -> bcs for spaces
if (decoded.prefix === 'bc') {
return bech32m.encode('bcs', decoded.words);
}
return bech32m.encode(`${decoded.prefix}s`, decoded.words);
return address;
}

// Schnorr signing
Expand Down
2 changes: 1 addition & 1 deletion spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class Resolver {
rpcUrl: URL;

constructor(opts: ResolverOptions = {}) {
this.chain = opts.chain || 'testnet4';
this.chain = opts.chain || 'mainnet';
this.rpcUrl = new URL(opts.rpcUrl || `http://localhost:${this.default_rpc_port()}`);
}

Expand Down
9 changes: 9 additions & 0 deletions types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface BootstrapNode {
host: string;
port: number;
}

export interface BootstrapNodes {
mainnet: BootstrapNode[];
testnet4: BootstrapNode[];
}
9 changes: 4 additions & 5 deletions utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import crypto from 'crypto';

export function spaceHash(spaceName: string): Buffer {
const byteArray = Buffer.from(spaceName, 'utf8');
const label = spaceName.startsWith('@') ? spaceName.slice(1) : spaceName;
const byteArray = Buffer.from(label, 'utf8');
const lengthPrefix = Buffer.from([byteArray.length]);
const lengthPrefixedByteArray = Buffer.concat([lengthPrefix, byteArray]);
const finalByteArray = Buffer.concat([lengthPrefixedByteArray, Buffer.from([0])]);

const finalByteArray = Buffer.concat([lengthPrefix, byteArray]);
const base = Buffer.from(crypto.createHash('sha256').update(finalByteArray).digest('hex'), 'hex');
base[0] &= 0x7f;
return base;
}
}