Skip to content

Commit

Permalink
fixup
Browse files Browse the repository at this point in the history
  • Loading branch information
notTanveer committed Oct 11, 2024
1 parent a62dcc1 commit b73c239
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 112 deletions.
2 changes: 1 addition & 1 deletion config/e2e.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ app:
providerType: BITCOIN_CORE_RPC
bitcoinCore:
protocol: http
rpcHost: 127.0.0.1
rpcHost: localhost
rpcPass: password
rpcUser: polaruser
rpcPort: 18443
2 changes: 1 addition & 1 deletion e2e/helpers/api.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class ApiHelper {
this.baseUrl = `http://localhost:${config.app.port}`;
}

async get<TResponseData = any>(path: string, params?: any) {
async get<TResponseData = any>(path: string, params?: AxiosRequestConfig) {
return this.makeRequest<TResponseData>({
method: 'get',
url: `${this.baseUrl}${path}`,
Expand Down
32 changes: 21 additions & 11 deletions e2e/helpers/common.helper.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Payment, Transaction } from 'bitcoinjs-lib';
import { computeScantweak } from '@/indexer/indexer.service';
import { IndexerService } from '@/indexer/indexer.service';
import { SATS_PER_BTC } from '@/common/constants';
import * as currency from 'currency.js';
import { varIntSize } from '@/common/common';

export function generateScantweak(
transaction: Transaction,
outputs: Payment[],
indexerService: IndexerService,
): string {
const txid = transaction.getId();

Expand All @@ -30,18 +32,11 @@ export function generateScantweak(
value: Number(output.value),
}));

const scantweak = computeScantweak(txid, txin, txout)[0];
const scantweak = indexerService.computeScantweak(txid, txin, txout)[0];

return scantweak.toString('hex');
}

export const varIntSize = (value: number): number => {
if (value < 0xfd) return 1;
else if (value <= 0xffff) return 3;
else if (value <= 0xffffffff) return 5;
else return 9;
};

export const readVarInt = (data: Buffer, cursor: number): number => {
const firstByte = data.readUInt8(cursor);
if (firstByte < 0xfd) {
Expand All @@ -55,9 +50,24 @@ export const readVarInt = (data: Buffer, cursor: number): number => {
}
};

export const parseSilentBlock = (data: Buffer) => {
export interface SilentBlockTransaction {
txid: string;
outputs: {
value: number;
pubkey: string;
vout: number;
}[];
scanTweak: string;
}

export interface SilentBlock {
type: number;
transactions: SilentBlockTransaction[];
}

export const parseSilentBlock = (data: Buffer): SilentBlock => {
const type = data.readUInt8(0);
const transactions = [];
const transactions: SilentBlockTransaction[] = [];

let cursor = 1;
const count = readVarInt(data, cursor);
Expand Down
18 changes: 9 additions & 9 deletions e2e/helpers/rpc.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class BitcoinRPCUtil {
}

async getBlockHeight(): Promise<number> {
return await this.request({
return this.request({
data: {
jsonrpc: '1.0',
id: 'silent_payment_indexer',
Expand All @@ -60,7 +60,7 @@ export class BitcoinRPCUtil {
}

async createWallet(walletName: string): Promise<any> {
return await this.request({
return this.request({
data: {
method: 'createwallet',
params: [walletName],
Expand All @@ -71,7 +71,7 @@ export class BitcoinRPCUtil {
}

async loadWallet(walletName: string): Promise<any> {
return await this.request({
return this.request({
data: {
method: 'loadwallet',
params: [walletName],
Expand All @@ -82,7 +82,7 @@ export class BitcoinRPCUtil {
}

async getNewAddress(): Promise<string> {
return await this.request({
return this.request({
data: {
method: 'getnewaddress',
params: [],
Expand All @@ -93,7 +93,7 @@ export class BitcoinRPCUtil {
}

async mineToAddress(numBlocks: number, address: string): Promise<any> {
return await this.request({
return this.request({
data: {
method: 'generatetoaddress',
params: [numBlocks, address],
Expand All @@ -102,7 +102,7 @@ export class BitcoinRPCUtil {
}

async sendToAddress(address: string, amount: number): Promise<string> {
return await this.request({
return this.request({
data: {
method: 'sendtoaddress',
params: [address, amount],
Expand All @@ -111,7 +111,7 @@ export class BitcoinRPCUtil {
}

async sendRawTransaction(rawTx: string): Promise<any> {
return await this.request({
return this.request({
data: {
method: 'sendrawtransaction',
params: [rawTx],
Expand All @@ -120,7 +120,7 @@ export class BitcoinRPCUtil {
}

async getTxOut(txid: string, vout: number): Promise<PartialUtxo> {
return await this.request({
return this.request({
data: {
method: 'gettxout',
params: [txid, vout],
Expand All @@ -129,7 +129,7 @@ export class BitcoinRPCUtil {
}

async getRawTransaction(txid: string): Promise<any> {
return await this.request({
return this.request({
data: {
method: 'getrawtransaction',
params: [txid],
Expand Down
15 changes: 13 additions & 2 deletions e2e/helpers/wallet.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,16 @@ export class WalletHelper {
}
}

const toXOnly = (pubKey) =>
pubKey.length === 32 ? pubKey : pubKey.slice(1, 33);
const toXOnly = (pubKey) => {
if (pubKey.length === 32) {
return pubKey;
}

if (pubKey.length === 33 && (pubKey[0] === 0x02 || pubKey[0] === 0x03)) {
return pubKey.slice(1, 33);
}

throw new Error(
'Invalid public key format. Expected compressed public key or x-only key.',
);
};
11 changes: 9 additions & 2 deletions e2e/indexer.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { parseSilentBlock } from '@e2e/helpers/common.helper';
import { Payment } from 'bitcoinjs-lib';
import { generateScantweak } from '@e2e/helpers/common.helper';
import { convertToSatoshi } from '@e2e/helpers/common.helper';
import { IndexerService } from '@/indexer/indexer.service';

describe('Silent Pay Indexer Tests', () => {
let walletHelper: WalletHelper;
let bitcoinRPCUtil: BitcoinRPCUtil;
let apiHelper: ApiHelper;
let indexerService: IndexerService;
let initialAddress: string;
let p2wkhOutputs: Payment[];
let taprootOutput: Payment;
Expand Down Expand Up @@ -54,6 +56,8 @@ describe('Silent Pay Indexer Tests', () => {
}
}
}

await new Promise((resolve) => setTimeout(resolve, 15000));
});

it('should ensure that the correct silent block is fetched', async () => {
Expand All @@ -67,7 +71,6 @@ describe('Silent Pay Indexer Tests', () => {
await bitcoinRPCUtil.mineToAddress(1, initialAddress)
)[0];

await new Promise((resolve) => setTimeout(resolve, 30000));
const response = await apiHelper.get(
`/silent-block/hash/${blockHash}`,
{
Expand All @@ -91,7 +94,11 @@ describe('Silent Pay Indexer Tests', () => {

expect(output.pubkey).toEqual(taprootPubKeyBuffer);

const scantweak = generateScantweak(transaction, p2wkhOutputs);
const scantweak = generateScantweak(
transaction,
p2wkhOutputs,
indexerService,
);
expect(foundTx.scanTweak).toEqual(scantweak);
});
});
156 changes: 74 additions & 82 deletions src/indexer/indexer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,87 +20,6 @@ export type TransactionOutput = {
value: number;
};

export function computeScantweak(
txid: string,
vin: TransactionInput[],
vout: TransactionOutput[],
): [Buffer, TransactionOutputEntity[]] | undefined {
const eligibleOutputPubKeys: TransactionOutputEntity[] = [];

// verify if the transaction contains at least one BIP341 P2TR output
// this output could be a potential silent payment
let n = 0;
for (const output of vout) {
if (isP2TR(output.scriptPubKey)) {
eligibleOutputPubKeys.push({
pubKey: output.scriptPubKey.substring(4),
value: output.value,
vout: n,
});
}
n++;
}

if (eligibleOutputPubKeys.length === 0) return undefined;

// verify that the transaction does not spend an output with SegWit version > 1
// this would make the transaction ineligible for silent payment v0
for (const input of vin) {
// grab the first op code of the prevOutScript
const firstOpCode = parseInt(input.prevOutScript.slice(0, 2), 16);

// if the first op code is in the range OP_2-OP_16 (0x52-0x60)
// then the transaction is ineligible
if (0x52 <= firstOpCode && firstOpCode <= 0x60) return undefined;
}

// extract the input public keys from the transaction
const pubKeys: Buffer[] = [];
for (const input of vin) {
const pubKey = extractPubKeyFromScript(
Buffer.from(input.prevOutScript, 'hex'),
Buffer.from(input.scriptSig, 'hex'),
input.witness?.map((w) => Buffer.from(w, 'hex')),
);
if (pubKey) pubKeys.push(pubKey);
}

if (pubKeys.length === 0) return undefined;

const smallestOutpoint = getSmallestOutpoint(vin);
const sumOfPublicKeys = Buffer.from(publicKeyCombine(pubKeys, true));

const inputHash = createTaggedHash(
'BIP0352/Inputs',
Buffer.concat([smallestOutpoint, sumOfPublicKeys]),
);

// A * inputHash
const scanTweak = Buffer.from(
publicKeyTweakMul(sumOfPublicKeys, inputHash, true),
);

return [scanTweak, eligibleOutputPubKeys];
}

function isP2TR(spk: string): boolean {
if (spk.match(/^5120[0-9a-fA-F]{64}$/)) return true;
}

function getSmallestOutpoint(vins: TransactionInput[]): Buffer {
const outpoints = vins.map((vin) => {
const n = Buffer.alloc(4);
n.writeUInt32LE(vin.vout);
return Buffer.concat([Buffer.from(vin.txid, 'hex').reverse(), n]);
});

let smallest = outpoints[0];
for (const outpoint of outpoints) {
if (outpoint.compare(smallest) < 0) smallest = outpoint;
}
return smallest;
}

@Injectable()
export class IndexerService {
constructor(private readonly transactionsService: TransactionsService) {}
Expand All @@ -112,7 +31,7 @@ export class IndexerService {
blockHeight: number,
blockHash: string,
) {
const scanResult = computeScantweak(txid, vin, vout);
const scanResult = this.computeScantweak(txid, vin, vout);
if (scanResult) {
const [scanTweak, eligibleOutputPubKeys] = scanResult;
const transaction = new Transaction();
Expand All @@ -126,4 +45,77 @@ export class IndexerService {
await this.transactionsService.saveTransaction(transaction);
}
}

public computeScantweak(
txid: string,
vin: TransactionInput[],
vout: TransactionOutput[],
): [Buffer, TransactionOutputEntity[]] | undefined {
const eligibleOutputPubKeys: TransactionOutputEntity[] = [];

// verify if the transaction contains at least one BIP341 P2TR output
let n = 0;
for (const output of vout) {
if (this.isP2TR(output.scriptPubKey)) {
eligibleOutputPubKeys.push({
pubKey: output.scriptPubKey.substring(4),
value: output.value,
vout: n,
});
}
n++;
}

if (eligibleOutputPubKeys.length === 0) return undefined;

// verify that the transaction does not spend an output with SegWit version > 1
for (const input of vin) {
const firstOpCode = parseInt(input.prevOutScript.slice(0, 2), 16);
if (0x52 <= firstOpCode && firstOpCode <= 0x60) return undefined;
}

const pubKeys: Buffer[] = [];
for (const input of vin) {
const pubKey = extractPubKeyFromScript(
Buffer.from(input.prevOutScript, 'hex'),
Buffer.from(input.scriptSig, 'hex'),
input.witness?.map((w) => Buffer.from(w, 'hex')),
);
if (pubKey) pubKeys.push(pubKey);
}

if (pubKeys.length === 0) return undefined;

const smallestOutpoint = this.getSmallestOutpoint(vin);
const sumOfPublicKeys = Buffer.from(publicKeyCombine(pubKeys, true));

const inputHash = createTaggedHash(
'BIP0352/Inputs',
Buffer.concat([smallestOutpoint, sumOfPublicKeys]),
);

const scanTweak = Buffer.from(
publicKeyTweakMul(sumOfPublicKeys, inputHash, true),
);

return [scanTweak, eligibleOutputPubKeys];
}

private isP2TR(spk: string): boolean {
return !!spk.match(/^5120[0-9a-fA-F]{64}$/);
}

private getSmallestOutpoint(vins: TransactionInput[]): Buffer {
const outpoints = vins.map((vin) => {
const n = Buffer.alloc(4);
n.writeUInt32LE(vin.vout);
return Buffer.concat([Buffer.from(vin.txid, 'hex').reverse(), n]);
});

let smallest = outpoints[0];
for (const outpoint of outpoints) {
if (outpoint.compare(smallest) < 0) smallest = outpoint;
}
return smallest;
}
}
Loading

0 comments on commit b73c239

Please sign in to comment.