From 3c2be7e4661b4882263b8c2e15550125c3906364 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Wed, 15 Feb 2023 16:02:58 +0100 Subject: [PATCH 01/30] Added functionalities for validating Bitcoin transaction proof --- typescript/src/bitcoin.ts | 6 +++ typescript/src/proof.ts | 100 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 3ef1f7025..0eb6eae6f 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -2,6 +2,7 @@ import bcoin, { TX } from "bcoin" import wif from "wif" import bufio from "bufio" import hash160 from "bcrypto/lib/hash160" +import sha256 from "bcrypto/lib/sha256-browser.js" import { BigNumber } from "ethers" import { Hex } from "./hex" import { BitcoinNetwork, toBcoinNetwork } from "./bitcoin-network" @@ -374,6 +375,11 @@ export function computeHash160(text: string): string { return hash160.digest(Buffer.from(text, "hex")).toString("hex") } +export function computeHash256(text: string): string { + const firstHash = sha256.digest(Buffer.from(text, "hex")) + return sha256.digest(firstHash).toString("hex") +} + /** * Encodes a public key hash into a P2PKH/P2WPKH address. * @param publicKeyHash - public key hash that will be encoded. Must be an diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index b32cd320b..d99dab196 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -4,6 +4,10 @@ import { TransactionMerkleBranch, Client as BitcoinClient, TransactionHash, + decomposeRawTransaction, + RawTransaction, + DecomposedRawTransaction, + computeHash256, } from "./bitcoin" /** @@ -70,3 +74,99 @@ function createMerkleProof(txMerkleBranch: TransactionMerkleBranch): string { }) return proof.toString("hex") } + +export async function validateProof( + transactionHash: TransactionHash, + requiredConfirmations: number, + bitcoinClient: BitcoinClient +) { + const proof = await assembleTransactionProof( + transactionHash, + requiredConfirmations, + bitcoinClient + ) + + // TODO: Write a converter and use it to convert the transaction part of the + // proof to the decomposed transaction data (version, inputs, outputs, locktime). + // Use raw transaction data for now. + const rawTransaction: RawTransaction = await bitcoinClient.getRawTransaction( + transactionHash + ) + + const decomposedRawTransaction: DecomposedRawTransaction = + decomposeRawTransaction(rawTransaction) + + const txBytes: Buffer = Buffer.concat([ + Buffer.from(decomposedRawTransaction.version, "hex"), + Buffer.from(decomposedRawTransaction.inputs, "hex"), + Buffer.from(decomposedRawTransaction.outputs, "hex"), + Buffer.from(decomposedRawTransaction.locktime, "hex"), + ]) + + const txId = computeHash256(txBytes.toString("hex")) + const merkleRoot = extractMerkleRootLE(proof.bitcoinHeaders) + + if (!prove(txId, merkleRoot, proof.merkleProof, proof.txIndexInBlock)) { + throw new Error( + "Tx merkle proof is not valid for provided header and tx hash" + ) + } +} + +export function extractMerkleRootLE(header: string): string { + const headerBytes = Buffer.from(header, "hex") + const merkleRootBytes = headerBytes.slice(36, 68) + return merkleRootBytes.toString("hex") +} + +export function prove( + txId: string, + merkleRoot: string, + intermediateNodes: string, + index: number +): boolean { + // Shortcut the empty-block case + if (txId == merkleRoot && index == 0 && intermediateNodes.length == 0) { + return true + } + + // If the Merkle proof failed, bubble up error + return verifyHash256Merkle(txId, intermediateNodes, merkleRoot, index) +} + +function verifyHash256Merkle( + leaf: string, + tree: string, + root: string, + index: number +): boolean { + // Not an even number of hashes + if (tree.length % 64 !== 0) { + return false + } + + // Should never occur + if (tree.length === 0) { + return false + } + + let idx = index + let current = leaf + + // i moves in increments of 64 + for (let i = 0; i < tree.length; i += 64) { + if (idx % 2 === 1) { + current = hash256MerkleStep(tree.slice(i, i + 64), current) + } else { + current = hash256MerkleStep(current, tree.slice(i, i + 64)) + } + idx = idx >> 1 + } + + return current === root +} + +function hash256MerkleStep(firstHash: string, secondHash: string): string { + // TODO: Make sure the strings are not prepended with `0x` + return computeHash256(firstHash + secondHash) +} From c6a3822b6c490b536ac6bdf3945d68ba4615a8c1 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 16 Feb 2023 17:15:36 +0100 Subject: [PATCH 02/30] Added functionalities for evaluating proof difficulties --- typescript/src/proof.ts | 117 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index d99dab196..86ad9da20 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -9,6 +9,7 @@ import { DecomposedRawTransaction, computeHash256, } from "./bitcoin" +import { BigNumber } from "ethers" /** * Assembles a proof that a given transaction was included in the blockchain and @@ -75,6 +76,8 @@ function createMerkleProof(txMerkleBranch: TransactionMerkleBranch): string { return proof.toString("hex") } +// TODO: Those functions were rewritten from Solidity. +// Refactor all the functions, e.g. represent BitcoinHeaders as structure. export async function validateProof( transactionHash: TransactionHash, requiredConfirmations: number, @@ -111,6 +114,13 @@ export async function validateProof( "Tx merkle proof is not valid for provided header and tx hash" ) } + + // TODO: Replace with real difficulties + evaluateProofDifficulty( + proof.bitcoinHeaders, + BigNumber.from(39156400059293), + BigNumber.from(39350942467772) + ) } export function extractMerkleRootLE(header: string): string { @@ -170,3 +180,110 @@ function hash256MerkleStep(firstHash: string, secondHash: string): string { // TODO: Make sure the strings are not prepended with `0x` return computeHash256(firstHash + secondHash) } + +export function evaluateProofDifficulty( + headers: string, + previousDifficulty: BigNumber, + currentDifficulty: BigNumber +) { + if (headers.length % 160 !== 0) { + throw new Error("Invalid length of the headers chain") + } + + let digest = "" + for (let start = 0; start < headers.length; start += 160) { + if (start !== 0) { + if (!validateHeaderPrevHash(headers, start, digest)) { + throw new Error("Invalid headers chain") + } + } + + const target = extractTargetAt(headers, start) + digest = computeHash256(headers.slice(start, start + 160)) + + const digestAsNumber = digestToBigNumber(digest) + + if (digestAsNumber.gt(target)) { + throw new Error("Insufficient work in a header") + } + + const difficulty = calculateDifficulty(target) + + if (previousDifficulty.eq(1) && currentDifficulty.eq(1)) { + // Special case for Bitcoin Testnet. Do not check block's difficulty + // due to required difficulty falling to `1` for Testnet. + return + } + + if ( + !difficulty.eq(previousDifficulty) && + !difficulty.eq(currentDifficulty) + ) { + throw new Error("Header difficulty not at current or previous difficulty") + } + } +} + +function validateHeaderPrevHash( + headers: string, + at: number, + prevHeaderDigest: string +): boolean { + // Extract prevHash of current header + const prevHash = extractPrevBlockLEAt(headers, at) + + // Compare prevHash of current header to previous header's digest + if (prevHash != prevHeaderDigest) { + return false + } + return true +} + +function extractPrevBlockLEAt(header: string, at: number): string { + return header.slice(8 + at, 8 + 64 + at) +} + +function extractTargetAt(headers: string, at: number): BigNumber { + const mantissa = extractMantissa(headers, at) + const e = parseInt(headers.slice(150 + at, 150 + 2 + at), 16) + const exponent = e - 3 + + return BigNumber.from(mantissa).mul(BigNumber.from(256).pow(exponent)) +} + +function extractMantissa(headers: string, at: number): number { + const mantissaBytes = headers.slice(144 + at, 144 + 6 + at) + const buffer = Buffer.from(mantissaBytes, "hex") + buffer.reverse() + return parseInt(buffer.toString("hex"), 16) +} + +/** + * Reverses the endianness of a hash represented as a hex string and converts + * the has to BigNumber + * @param hexString The hash to reverse + * @returns The reversed hash as a BigNumber + */ +function digestToBigNumber(hexString: string): BigNumber { + if (!hexString.match(/^[0-9a-fA-F]+$/)) { + throw new Error("Input is not a valid hexadecimal string") + } + + const buf = Buffer.from(hexString, "hex") + buf.reverse() + const reversedHex = buf.toString("hex") + + try { + return BigNumber.from("0x" + reversedHex) + } catch (e) { + throw new Error("Error converting hexadecimal string to BigNumber") + } +} + +function calculateDifficulty(_target: BigNumber): BigNumber { + const DIFF1_TARGET = BigNumber.from( + "0x00000000FFFF0000000000000000000000000000000000000000000000000000" + ) + // Difficulty 1 calculated from 0x1d00ffff + return DIFF1_TARGET.div(_target) +} From ac8ff6f89db683f4b0ba7815712892d299ed7e1a Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 16 Feb 2023 17:20:46 +0100 Subject: [PATCH 03/30] Passed previous and current Bitcoin epoch difficulties from outside --- typescript/src/proof.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 86ad9da20..be0c562a9 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -81,6 +81,8 @@ function createMerkleProof(txMerkleBranch: TransactionMerkleBranch): string { export async function validateProof( transactionHash: TransactionHash, requiredConfirmations: number, + previousDifficulty: BigNumber, + currentDifficulty: BigNumber, bitcoinClient: BitcoinClient ) { const proof = await assembleTransactionProof( @@ -115,11 +117,10 @@ export async function validateProof( ) } - // TODO: Replace with real difficulties evaluateProofDifficulty( proof.bitcoinHeaders, - BigNumber.from(39156400059293), - BigNumber.from(39350942467772) + previousDifficulty, + currentDifficulty ) } From 77aab64d9e1aed95c27c36f6f02ea0f157460610 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 16 Feb 2023 17:55:39 +0100 Subject: [PATCH 04/30] Refactored Bitcoin transaction validation --- typescript/src/proof.ts | 189 +++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 98 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index be0c562a9..0e5140432 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -101,36 +101,43 @@ export async function validateProof( const decomposedRawTransaction: DecomposedRawTransaction = decomposeRawTransaction(rawTransaction) - const txBytes: Buffer = Buffer.concat([ + const transactionBytes: Buffer = Buffer.concat([ Buffer.from(decomposedRawTransaction.version, "hex"), Buffer.from(decomposedRawTransaction.inputs, "hex"), Buffer.from(decomposedRawTransaction.outputs, "hex"), Buffer.from(decomposedRawTransaction.locktime, "hex"), ]) - const txId = computeHash256(txBytes.toString("hex")) - const merkleRoot = extractMerkleRootLE(proof.bitcoinHeaders) - - if (!prove(txId, merkleRoot, proof.merkleProof, proof.txIndexInBlock)) { + const transactionHashLE: string = computeHash256( + transactionBytes.toString("hex") + ) + const merkleRoot: string = extractMerkleRootLE(proof.bitcoinHeaders) + + if ( + !validateMerkleTree( + transactionHashLE, + merkleRoot, + proof.merkleProof, + proof.txIndexInBlock + ) + ) { throw new Error( - "Tx merkle proof is not valid for provided header and tx hash" + "Transaction merkle proof is not valid for provided header and transaction hash" ) } - evaluateProofDifficulty( - proof.bitcoinHeaders, - previousDifficulty, - currentDifficulty - ) + const bitcoinHeaders = splitHeaders(proof.bitcoinHeaders) + + validateProofDifficulty(bitcoinHeaders, previousDifficulty, currentDifficulty) } -export function extractMerkleRootLE(header: string): string { - const headerBytes = Buffer.from(header, "hex") - const merkleRootBytes = headerBytes.slice(36, 68) +export function extractMerkleRootLE(headers: string): string { + const headersBytes: Buffer = Buffer.from(headers, "hex") + const merkleRootBytes: Buffer = headersBytes.slice(36, 68) return merkleRootBytes.toString("hex") } -export function prove( +export function validateMerkleTree( txId: string, merkleRoot: string, intermediateNodes: string, @@ -140,12 +147,10 @@ export function prove( if (txId == merkleRoot && index == 0 && intermediateNodes.length == 0) { return true } - - // If the Merkle proof failed, bubble up error - return verifyHash256Merkle(txId, intermediateNodes, merkleRoot, index) + return validateMerkleTreeHashes(txId, intermediateNodes, merkleRoot, index) } -function verifyHash256Merkle( +function validateMerkleTreeHashes( leaf: string, tree: string, root: string, @@ -167,9 +172,9 @@ function verifyHash256Merkle( // i moves in increments of 64 for (let i = 0; i < tree.length; i += 64) { if (idx % 2 === 1) { - current = hash256MerkleStep(tree.slice(i, i + 64), current) + current = computeHash256(tree.slice(i, i + 64) + current) } else { - current = hash256MerkleStep(current, tree.slice(i, i + 64)) + current = computeHash256(current + tree.slice(i, i + 64)) } idx = idx >> 1 } @@ -177,43 +182,82 @@ function verifyHash256Merkle( return current === root } -function hash256MerkleStep(firstHash: string, secondHash: string): string { - // TODO: Make sure the strings are not prepended with `0x` - return computeHash256(firstHash + secondHash) -} - -export function evaluateProofDifficulty( - headers: string, +export function validateProofDifficulty( + serializedHeaders: string[], previousDifficulty: BigNumber, currentDifficulty: BigNumber ) { - if (headers.length % 160 !== 0) { - throw new Error("Invalid length of the headers chain") + const validateHeaderPrevHash = ( + header: string, + prevHeaderDigest: string + ): boolean => { + // Extract prevHash of current header + const prevHash = header.slice(8, 8 + 64) + + // Compare prevHash of current header to previous header's digest + if (prevHash != prevHeaderDigest) { + return false + } + return true } - let digest = "" - for (let start = 0; start < headers.length; start += 160) { - if (start !== 0) { - if (!validateHeaderPrevHash(headers, start, digest)) { + const extractMantissa = (header: string): number => { + const mantissaBytes = header.slice(144, 144 + 6) + const buffer = Buffer.from(mantissaBytes, "hex") + buffer.reverse() + return parseInt(buffer.toString("hex"), 16) + } + + const extractTargetAt = (header: string): BigNumber => { + const mantissa = extractMantissa(header) + const e = parseInt(header.slice(150, 150 + 2), 16) + const exponent = e - 3 + + return BigNumber.from(mantissa).mul(BigNumber.from(256).pow(exponent)) + } + + const digestToBigNumber = (hexString: string): BigNumber => { + const buffer = Buffer.from(hexString, "hex") + buffer.reverse() + const reversedHex = buffer.toString("hex") + return BigNumber.from("0x" + reversedHex) + } + + const calculateDifficulty = (_target: BigNumber): BigNumber => { + const DIFF1_TARGET = BigNumber.from( + "0x00000000FFFF0000000000000000000000000000000000000000000000000000" + ) + // Difficulty 1 calculated from 0x1d00ffff + return DIFF1_TARGET.div(_target) + } + + let previousDigest: string = "" + // for (let start = 0; start < headers.length; start += 160) { + for (let index = 0; index < serializedHeaders.length; index++) { + const currentHeader = serializedHeaders[index] + + if (index !== 0) { + if (!validateHeaderPrevHash(currentHeader, previousDigest)) { throw new Error("Invalid headers chain") } } - const target = extractTargetAt(headers, start) - digest = computeHash256(headers.slice(start, start + 160)) + const target = extractTargetAt(currentHeader) + const digest = computeHash256(currentHeader) - const digestAsNumber = digestToBigNumber(digest) - - if (digestAsNumber.gt(target)) { + if (digestToBigNumber(digest).gt(target)) { throw new Error("Insufficient work in a header") } + // Save the current digest to compare it with the next block header's digest + previousDigest = digest + const difficulty = calculateDifficulty(target) if (previousDifficulty.eq(1) && currentDifficulty.eq(1)) { // Special case for Bitcoin Testnet. Do not check block's difficulty // due to required difficulty falling to `1` for Testnet. - return + continue } if ( @@ -225,66 +269,15 @@ export function evaluateProofDifficulty( } } -function validateHeaderPrevHash( - headers: string, - at: number, - prevHeaderDigest: string -): boolean { - // Extract prevHash of current header - const prevHash = extractPrevBlockLEAt(headers, at) - - // Compare prevHash of current header to previous header's digest - if (prevHash != prevHeaderDigest) { - return false - } - return true -} - -function extractPrevBlockLEAt(header: string, at: number): string { - return header.slice(8 + at, 8 + 64 + at) -} - -function extractTargetAt(headers: string, at: number): BigNumber { - const mantissa = extractMantissa(headers, at) - const e = parseInt(headers.slice(150 + at, 150 + 2 + at), 16) - const exponent = e - 3 - - return BigNumber.from(mantissa).mul(BigNumber.from(256).pow(exponent)) -} - -function extractMantissa(headers: string, at: number): number { - const mantissaBytes = headers.slice(144 + at, 144 + 6 + at) - const buffer = Buffer.from(mantissaBytes, "hex") - buffer.reverse() - return parseInt(buffer.toString("hex"), 16) -} - -/** - * Reverses the endianness of a hash represented as a hex string and converts - * the has to BigNumber - * @param hexString The hash to reverse - * @returns The reversed hash as a BigNumber - */ -function digestToBigNumber(hexString: string): BigNumber { - if (!hexString.match(/^[0-9a-fA-F]+$/)) { - throw new Error("Input is not a valid hexadecimal string") +function splitHeaders(headers: string): string[] { + if (headers.length % 160 !== 0) { + throw new Error("Incorrect length of Bitcoin headers") } - const buf = Buffer.from(hexString, "hex") - buf.reverse() - const reversedHex = buf.toString("hex") - - try { - return BigNumber.from("0x" + reversedHex) - } catch (e) { - throw new Error("Error converting hexadecimal string to BigNumber") + const result = [] + for (let i = 0; i < headers.length; i += 160) { + result.push(headers.substring(i, i + 160)) } -} -function calculateDifficulty(_target: BigNumber): BigNumber { - const DIFF1_TARGET = BigNumber.from( - "0x00000000FFFF0000000000000000000000000000000000000000000000000000" - ) - // Difficulty 1 calculated from 0x1d00ffff - return DIFF1_TARGET.div(_target) + return result } From adb4a27cefe9d408901e5058a7dfee51e29082c2 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Fri, 17 Feb 2023 13:53:11 +0100 Subject: [PATCH 05/30] Added BlockHeader interface --- typescript/src/bitcoin.ts | 55 +++++++++++++++++++++++++++++ typescript/src/proof.ts | 74 ++++++++++++--------------------------- 2 files changed, 78 insertions(+), 51 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 0eb6eae6f..b58f12486 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -173,6 +173,55 @@ export interface TransactionMerkleBranch { position: number } +export interface BlockHeader { + version: number + previousBlockHeaderHash: Hex + merkleRootHash: Hex + time: number + bits: number + nonce: number +} + +// TODO: Add unit tests and descriptions +export function decomposeBlockHeader(blockHeaderStr: string): BlockHeader { + const buffer = Buffer.from(blockHeaderStr, "hex") + const version = buffer.readUInt32LE(0) + const previousBlockHeaderHash = buffer.slice(4, 36) + const merkleRootHash = buffer.slice(36, 68) + const time = Buffer.from(buffer.slice(68, 72)).reverse().readUInt32BE(0) + const bits = Buffer.from(buffer.slice(72, 76)).reverse().readUInt32BE(0) + const nonce = Buffer.from(buffer.slice(76, 80)).reverse().readUInt32BE(0) + + return { + version: version, + previousBlockHeaderHash: Hex.from(previousBlockHeaderHash), + merkleRootHash: Hex.from(merkleRootHash), + time: time, + bits: bits, + nonce: nonce, + } +} + +// TODO: Add unit tests and description. +export function bitsToDifficultyTarget(bits: number): BigNumber { + const exponent = ((bits >>> 24) & 0xff) - 3 + const mantissa = bits & 0x7fffff + + const difficultyTarget = BigNumber.from(mantissa).mul( + BigNumber.from(256).pow(exponent) + ) + return difficultyTarget +} + +// TODO: Add unit tests and description. +export function targetToDifficulty(target: BigNumber): BigNumber { + const DIFF1_TARGET = BigNumber.from( + "0x00000000FFFF0000000000000000000000000000000000000000000000000000" + ) + // Difficulty 1 calculated from 0x1d00ffff + return DIFF1_TARGET.div(target) +} + /** * Represents a Bitcoin client. */ @@ -380,6 +429,12 @@ export function computeHash256(text: string): string { return sha256.digest(firstHash).toString("hex") } +export function hashToBigNumber(hash: string): BigNumber { + return BigNumber.from( + "0x" + Buffer.from(hash, "hex").reverse().toString("hex") + ) +} + /** * Encodes a public key hash into a P2PKH/P2WPKH address. * @param publicKeyHash - public key hash that will be encoded. Must be an diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 0e5140432..d6fea0374 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -8,8 +8,13 @@ import { RawTransaction, DecomposedRawTransaction, computeHash256, + decomposeBlockHeader, + bitsToDifficultyTarget, + targetToDifficulty, + hashToBigNumber, } from "./bitcoin" import { BigNumber } from "ethers" +import { Hex } from "./hex" /** * Assembles a proof that a given transaction was included in the blockchain and @@ -182,77 +187,42 @@ function validateMerkleTreeHashes( return current === root } +// Note that it requires that the headers come from current or previous epoch. +// Validation will fail if the export function validateProofDifficulty( serializedHeaders: string[], previousDifficulty: BigNumber, currentDifficulty: BigNumber ) { - const validateHeaderPrevHash = ( - header: string, - prevHeaderDigest: string - ): boolean => { - // Extract prevHash of current header - const prevHash = header.slice(8, 8 + 64) - - // Compare prevHash of current header to previous header's digest - if (prevHash != prevHeaderDigest) { - return false - } - return true - } - - const extractMantissa = (header: string): number => { - const mantissaBytes = header.slice(144, 144 + 6) - const buffer = Buffer.from(mantissaBytes, "hex") - buffer.reverse() - return parseInt(buffer.toString("hex"), 16) - } - - const extractTargetAt = (header: string): BigNumber => { - const mantissa = extractMantissa(header) - const e = parseInt(header.slice(150, 150 + 2), 16) - const exponent = e - 3 - - return BigNumber.from(mantissa).mul(BigNumber.from(256).pow(exponent)) - } - - const digestToBigNumber = (hexString: string): BigNumber => { - const buffer = Buffer.from(hexString, "hex") - buffer.reverse() - const reversedHex = buffer.toString("hex") - return BigNumber.from("0x" + reversedHex) - } - - const calculateDifficulty = (_target: BigNumber): BigNumber => { - const DIFF1_TARGET = BigNumber.from( - "0x00000000FFFF0000000000000000000000000000000000000000000000000000" - ) - // Difficulty 1 calculated from 0x1d00ffff - return DIFF1_TARGET.div(_target) - } + let previousDigest: Hex = Hex.from("00") - let previousDigest: string = "" - // for (let start = 0; start < headers.length; start += 160) { for (let index = 0; index < serializedHeaders.length; index++) { const currentHeader = serializedHeaders[index] + const blockHeaderDecomposed = decomposeBlockHeader(currentHeader) + // Check if the current block header stores the hash of the previously + // processed block header. Skip the check for the first header. if (index !== 0) { - if (!validateHeaderPrevHash(currentHeader, previousDigest)) { + if ( + !previousDigest.equals(blockHeaderDecomposed.previousBlockHeaderHash) + ) { throw new Error("Invalid headers chain") } } - const target = extractTargetAt(currentHeader) + const target = bitsToDifficultyTarget(blockHeaderDecomposed.bits) const digest = computeHash256(currentHeader) - if (digestToBigNumber(digest).gt(target)) { - throw new Error("Insufficient work in a header") + if (hashToBigNumber(digest).gt(target)) { + throw new Error("Insufficient work in the header") } // Save the current digest to compare it with the next block header's digest - previousDigest = digest + previousDigest = Hex.from(digest) - const difficulty = calculateDifficulty(target) + // Check if the stored block difficulty is equal to previous or current + // difficulties. + const difficulty = targetToDifficulty(target) if (previousDifficulty.eq(1) && currentDifficulty.eq(1)) { // Special case for Bitcoin Testnet. Do not check block's difficulty @@ -260,6 +230,8 @@ export function validateProofDifficulty( continue } + // TODO: For mainnet we could check if there is no more than one switch + // from previous to current difficulties if ( !difficulty.eq(previousDifficulty) && !difficulty.eq(currentDifficulty) From 6ee8f3d743d8f38fcdecce409c83424aa127fa83 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Fri, 17 Feb 2023 14:01:59 +0100 Subject: [PATCH 06/30] Added check for transaction hash --- typescript/src/proof.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index d6fea0374..ff426fdcb 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -116,8 +116,14 @@ export async function validateProof( const transactionHashLE: string = computeHash256( transactionBytes.toString("hex") ) - const merkleRoot: string = extractMerkleRootLE(proof.bitcoinHeaders) + // TODO: Should we recreate transactionHashLE from its components? + // We don't check the components anywhere. + if (!transactionHash.equals(Hex.from(transactionHashLE).reverse())) { + throw new Error("Incorrect transaction hash") + } + + const merkleRoot: string = extractMerkleRootLE(proof.bitcoinHeaders) if ( !validateMerkleTree( transactionHashLE, @@ -132,7 +138,6 @@ export async function validateProof( } const bitcoinHeaders = splitHeaders(proof.bitcoinHeaders) - validateProofDifficulty(bitcoinHeaders, previousDifficulty, currentDifficulty) } From c8ac33d364cdd2ad4a28ad49ae7d90bdb8e77e36 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Fri, 17 Feb 2023 14:51:48 +0100 Subject: [PATCH 07/30] Refactored functionalities --- typescript/src/bitcoin.ts | 69 +++++++++++-- typescript/src/proof.ts | 202 ++++++++++++++++++-------------------- 2 files changed, 160 insertions(+), 111 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index b58f12486..84f9ade36 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -173,18 +173,52 @@ export interface TransactionMerkleBranch { position: number } +/** + * BlockHeader represents the header of a Bitcoin block. For reference, see: + * https://developer.bitcoin.org/reference/block_chain.html#block-headers. + */ export interface BlockHeader { + /** + * The block version number that indicates which set of block validation rules + * to follow. + */ version: number + + /** + * The hash of the previous block's header. + */ previousBlockHeaderHash: Hex + + /** + * The hash derived from the hashes of all transactions included in this block. + */ merkleRootHash: Hex + + /** + * The Unix epoch time when the miner started hashing the header. + */ time: number + + /** + * Bits that determine the target threshold this block's header hash must be + * less than or equal to. + */ bits: number + + /** + * An arbitrary number miners change to modify the header hash in order to + * produce a hash less than or equal to the target threshold. + */ nonce: number } -// TODO: Add unit tests and descriptions -export function decomposeBlockHeader(blockHeaderStr: string): BlockHeader { - const buffer = Buffer.from(blockHeaderStr, "hex") +/** + * Deserializes a block header in the raw representation to BlockHeader. + * @param rawBlockHeader - BlockHeader in the raw format. + * @returns Block header as a BlockHeader. + */ +export function deserializeBlockHeader(rawBlockHeader: string): BlockHeader { + const buffer = Buffer.from(rawBlockHeader, "hex") const version = buffer.readUInt32LE(0) const previousBlockHeaderHash = buffer.slice(4, 36) const merkleRootHash = buffer.slice(36, 68) @@ -202,7 +236,27 @@ export function decomposeBlockHeader(blockHeaderStr: string): BlockHeader { } } -// TODO: Add unit tests and description. +/** + * Serializes a BlockHeader to the raw representation. + * @param blockHeader - block header. + * @returns Serialized block header. + */ +export function serializeBlockHeader(blockHeader: BlockHeader): string { + const buffer = Buffer.alloc(80) + buffer.writeUInt32LE(blockHeader.version, 0) + blockHeader.previousBlockHeaderHash.toBuffer().copy(buffer, 4) + blockHeader.merkleRootHash.toBuffer().copy(buffer, 36) + buffer.writeUInt32LE(blockHeader.time, 68) + buffer.writeUInt32LE(blockHeader.bits, 72) + buffer.writeUInt32LE(blockHeader.nonce, 76) + return buffer.toString("hex") +} + +/** + * Converts a block header's bits into difficulty target. + * @param bits - bits from block header. + * @returns Difficulty target. + */ export function bitsToDifficultyTarget(bits: number): BigNumber { const exponent = ((bits >>> 24) & 0xff) - 3 const mantissa = bits & 0x7fffff @@ -213,12 +267,15 @@ export function bitsToDifficultyTarget(bits: number): BigNumber { return difficultyTarget } -// TODO: Add unit tests and description. +/** + * Converts difficulty target to difficulty. + * @param target - difficulty target. + * @returns Difficulty as a BigNumber. + */ export function targetToDifficulty(target: BigNumber): BigNumber { const DIFF1_TARGET = BigNumber.from( "0x00000000FFFF0000000000000000000000000000000000000000000000000000" ) - // Difficulty 1 calculated from 0x1d00ffff return DIFF1_TARGET.div(target) } diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index ff426fdcb..7f2be1f50 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -4,14 +4,13 @@ import { TransactionMerkleBranch, Client as BitcoinClient, TransactionHash, - decomposeRawTransaction, - RawTransaction, - DecomposedRawTransaction, computeHash256, - decomposeBlockHeader, + deserializeBlockHeader, bitsToDifficultyTarget, targetToDifficulty, hashToBigNumber, + serializeBlockHeader, + BlockHeader, } from "./bitcoin" import { BigNumber } from "ethers" import { Hex } from "./hex" @@ -81,9 +80,9 @@ function createMerkleProof(txMerkleBranch: TransactionMerkleBranch): string { return proof.toString("hex") } -// TODO: Those functions were rewritten from Solidity. -// Refactor all the functions, e.g. represent BitcoinHeaders as structure. -export async function validateProof( +// TODO: Description +// TODO: should we check the transaction itself (inputs, outputs)? +export async function validateTransactionProof( transactionHash: TransactionHash, requiredConfirmations: number, previousDifficulty: BigNumber, @@ -96,140 +95,128 @@ export async function validateProof( bitcoinClient ) - // TODO: Write a converter and use it to convert the transaction part of the - // proof to the decomposed transaction data (version, inputs, outputs, locktime). - // Use raw transaction data for now. - const rawTransaction: RawTransaction = await bitcoinClient.getRawTransaction( - transactionHash - ) - - const decomposedRawTransaction: DecomposedRawTransaction = - decomposeRawTransaction(rawTransaction) - - const transactionBytes: Buffer = Buffer.concat([ - Buffer.from(decomposedRawTransaction.version, "hex"), - Buffer.from(decomposedRawTransaction.inputs, "hex"), - Buffer.from(decomposedRawTransaction.outputs, "hex"), - Buffer.from(decomposedRawTransaction.locktime, "hex"), - ]) + const bitcoinHeaders = splitHeaders(proof.bitcoinHeaders) + const merkleRootHash = bitcoinHeaders[0].merkleRootHash - const transactionHashLE: string = computeHash256( - transactionBytes.toString("hex") + validateMerkleTree( + transactionHash.reverse().toString(), + merkleRootHash.toString(), + proof.merkleProof, + proof.txIndexInBlock ) - // TODO: Should we recreate transactionHashLE from its components? - // We don't check the components anywhere. - if (!transactionHash.equals(Hex.from(transactionHashLE).reverse())) { - throw new Error("Incorrect transaction hash") - } - - const merkleRoot: string = extractMerkleRootLE(proof.bitcoinHeaders) - if ( - !validateMerkleTree( - transactionHashLE, - merkleRoot, - proof.merkleProof, - proof.txIndexInBlock - ) - ) { - throw new Error( - "Transaction merkle proof is not valid for provided header and transaction hash" - ) - } - - const bitcoinHeaders = splitHeaders(proof.bitcoinHeaders) - validateProofDifficulty(bitcoinHeaders, previousDifficulty, currentDifficulty) -} - -export function extractMerkleRootLE(headers: string): string { - const headersBytes: Buffer = Buffer.from(headers, "hex") - const merkleRootBytes: Buffer = headersBytes.slice(36, 68) - return merkleRootBytes.toString("hex") + validateBlockHeadersChain( + bitcoinHeaders, + previousDifficulty, + currentDifficulty + ) } -export function validateMerkleTree( - txId: string, - merkleRoot: string, +function validateMerkleTree( + transactionHash: string, + merkleRootHash: string, intermediateNodes: string, - index: number -): boolean { + transactionIdxInBlock: number +) { // Shortcut the empty-block case - if (txId == merkleRoot && index == 0 && intermediateNodes.length == 0) { - return true + if ( + transactionHash == merkleRootHash && + transactionIdxInBlock == 0 && + intermediateNodes.length == 0 + ) { + return } - return validateMerkleTreeHashes(txId, intermediateNodes, merkleRoot, index) + + validateMerkleTreeHashes( + transactionHash, + intermediateNodes, + merkleRootHash, + transactionIdxInBlock + ) } function validateMerkleTreeHashes( - leaf: string, - tree: string, - root: string, - index: number -): boolean { - // Not an even number of hashes - if (tree.length % 64 !== 0) { - return false - } - - // Should never occur - if (tree.length === 0) { - return false + leafHash: string, + intermediateNodes: string, + merkleRoot: string, + transactionIdxInBlock: number +) { + if (intermediateNodes.length === 0 || intermediateNodes.length % 64 !== 0) { + throw new Error("Invalid merkle tree") } - let idx = index - let current = leaf + let idx = transactionIdxInBlock + let current = leafHash // i moves in increments of 64 - for (let i = 0; i < tree.length; i += 64) { + for (let i = 0; i < intermediateNodes.length; i += 64) { if (idx % 2 === 1) { - current = computeHash256(tree.slice(i, i + 64) + current) + current = computeHash256(intermediateNodes.slice(i, i + 64) + current) } else { - current = computeHash256(current + tree.slice(i, i + 64)) + current = computeHash256(current + intermediateNodes.slice(i, i + 64)) } idx = idx >> 1 } - return current === root + if (current !== merkleRoot) { + throw new Error( + "Transaction merkle proof is not valid for provided header and transaction hash" + ) + } } -// Note that it requires that the headers come from current or previous epoch. -// Validation will fail if the -export function validateProofDifficulty( - serializedHeaders: string[], - previousDifficulty: BigNumber, - currentDifficulty: BigNumber +/** + * Validates a chain of consecutive block headers. It checks if each of the + * block headers has appropriate difficulty, hash of each block is below the + * required target and block headers form a chain. + * @dev The block headers must come form Bitcoin epochs with difficulties + * marked by previous and current difficulties. If a Bitcoin difficulty + * relay is used to provide these values and the relay is up-to-date, only + * the recent block headers will pass validation. Block headers older than + * the current and previous Bitcoin epochs will fail. + * @param blockHeaders - block headers that form the chain. + * @param previousEpochDifficulty - difficulty of the previous Bitcoin epoch. + * @param currentEpochDifficulty - difficulty of the current Bitcoin epoch. + * @returns Empty return. + */ +function validateBlockHeadersChain( + blockHeaders: BlockHeader[], + previousEpochDifficulty: BigNumber, + currentEpochDifficulty: BigNumber ) { - let previousDigest: Hex = Hex.from("00") + let previousBlockHeaderHash: Hex = Hex.from("00") - for (let index = 0; index < serializedHeaders.length; index++) { - const currentHeader = serializedHeaders[index] - const blockHeaderDecomposed = decomposeBlockHeader(currentHeader) + for (let index = 0; index < blockHeaders.length; index++) { + const currentHeader = blockHeaders[index] // Check if the current block header stores the hash of the previously // processed block header. Skip the check for the first header. if (index !== 0) { if ( - !previousDigest.equals(blockHeaderDecomposed.previousBlockHeaderHash) + !previousBlockHeaderHash.equals(currentHeader.previousBlockHeaderHash) ) { throw new Error("Invalid headers chain") } } - const target = bitsToDifficultyTarget(blockHeaderDecomposed.bits) - const digest = computeHash256(currentHeader) + const difficultyTarget = bitsToDifficultyTarget(currentHeader.bits) + const currentBlockHeaderHash = computeHash256( + serializeBlockHeader(currentHeader) + ) - if (hashToBigNumber(digest).gt(target)) { + if (hashToBigNumber(currentBlockHeaderHash).gt(difficultyTarget)) { throw new Error("Insufficient work in the header") } - // Save the current digest to compare it with the next block header's digest - previousDigest = Hex.from(digest) + // Save the current block header hash to compare it with the next block + // header's previous block header hash. + previousBlockHeaderHash = Hex.from(currentBlockHeaderHash) // Check if the stored block difficulty is equal to previous or current // difficulties. - const difficulty = targetToDifficulty(target) + const difficulty = targetToDifficulty(difficultyTarget) - if (previousDifficulty.eq(1) && currentDifficulty.eq(1)) { + if (previousEpochDifficulty.eq(1) && currentEpochDifficulty.eq(1)) { // Special case for Bitcoin Testnet. Do not check block's difficulty // due to required difficulty falling to `1` for Testnet. continue @@ -238,22 +225,27 @@ export function validateProofDifficulty( // TODO: For mainnet we could check if there is no more than one switch // from previous to current difficulties if ( - !difficulty.eq(previousDifficulty) && - !difficulty.eq(currentDifficulty) + !difficulty.eq(previousEpochDifficulty) && + !difficulty.eq(currentEpochDifficulty) ) { throw new Error("Header difficulty not at current or previous difficulty") } } } -function splitHeaders(headers: string): string[] { - if (headers.length % 160 !== 0) { +/** + * Splits Bitcoin block headers in the raw format into an array of BlockHeaders. + * @param blockHeaders - string that contains block headers in the raw format. + * @returns Array of BlockHeader objects. + */ +function splitHeaders(blockHeaders: string): BlockHeader[] { + if (blockHeaders.length % 160 !== 0) { throw new Error("Incorrect length of Bitcoin headers") } - const result = [] - for (let i = 0; i < headers.length; i += 160) { - result.push(headers.substring(i, i + 160)) + const result: BlockHeader[] = [] + for (let i = 0; i < blockHeaders.length; i += 160) { + result.push(deserializeBlockHeader(blockHeaders.substring(i, i + 160))) } return result From a6a2b8dd7b734aa4c7a8e7978cbeba693d0078d7 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Tue, 21 Feb 2023 12:13:58 +0100 Subject: [PATCH 08/30] Added unit tests for Bitcoin-related functions --- typescript/src/bitcoin.ts | 22 +++++-- typescript/src/proof.ts | 5 +- typescript/test/bitcoin.test.ts | 111 ++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 8 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 84f9ade36..4e776cc7b 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -222,9 +222,9 @@ export function deserializeBlockHeader(rawBlockHeader: string): BlockHeader { const version = buffer.readUInt32LE(0) const previousBlockHeaderHash = buffer.slice(4, 36) const merkleRootHash = buffer.slice(36, 68) - const time = Buffer.from(buffer.slice(68, 72)).reverse().readUInt32BE(0) - const bits = Buffer.from(buffer.slice(72, 76)).reverse().readUInt32BE(0) - const nonce = Buffer.from(buffer.slice(76, 80)).reverse().readUInt32BE(0) + const time = buffer.readUInt32LE(68) + const bits = buffer.readUInt32LE(72) + const nonce = buffer.readUInt32LE(76) return { version: version, @@ -259,7 +259,7 @@ export function serializeBlockHeader(blockHeader: BlockHeader): string { */ export function bitsToDifficultyTarget(bits: number): BigNumber { const exponent = ((bits >>> 24) & 0xff) - 3 - const mantissa = bits & 0x7fffff + const mantissa = bits & 0xffffff const difficultyTarget = BigNumber.from(mantissa).mul( BigNumber.from(256).pow(exponent) @@ -274,7 +274,7 @@ export function bitsToDifficultyTarget(bits: number): BigNumber { */ export function targetToDifficulty(target: BigNumber): BigNumber { const DIFF1_TARGET = BigNumber.from( - "0x00000000FFFF0000000000000000000000000000000000000000000000000000" + "0xffff0000000000000000000000000000000000000000000000000000" ) return DIFF1_TARGET.div(target) } @@ -481,12 +481,22 @@ export function computeHash160(text: string): string { return hash160.digest(Buffer.from(text, "hex")).toString("hex") } +/** + * Computes the double SHA256 for the given text. + * @param text - Text the double SHA256 is computed for. + * @returns Hash as a 32-byte un-prefixed hex string. + */ export function computeHash256(text: string): string { const firstHash = sha256.digest(Buffer.from(text, "hex")) return sha256.digest(firstHash).toString("hex") } -export function hashToBigNumber(hash: string): BigNumber { +/** + * Converts a hash in hex string in little endian to a BigNumber. + * @param hash - Hash in hex-string format. + * @returns BigNumber representation of the hash. + */ +export function hashLEToBigNumber(hash: string): BigNumber { return BigNumber.from( "0x" + Buffer.from(hash, "hex").reverse().toString("hex") ) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 7f2be1f50..0ba96fbc9 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -8,7 +8,7 @@ import { deserializeBlockHeader, bitsToDifficultyTarget, targetToDifficulty, - hashToBigNumber, + hashLEToBigNumber, serializeBlockHeader, BlockHeader, } from "./bitcoin" @@ -200,11 +200,12 @@ function validateBlockHeadersChain( } const difficultyTarget = bitsToDifficultyTarget(currentHeader.bits) + const currentBlockHeaderHash = computeHash256( serializeBlockHeader(currentHeader) ) - if (hashToBigNumber(currentBlockHeaderHash).gt(difficultyTarget)) { + if (hashLEToBigNumber(currentBlockHeaderHash).gt(difficultyTarget)) { throw new Error("Insufficient work in the header") } diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index 543095358..657b6a6b0 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -5,9 +5,17 @@ import { decodeBitcoinAddress, isPublicKeyHashLength, locktimeToNumber, + BlockHeader, + serializeBlockHeader, + deserializeBlockHeader, + hashLEToBigNumber, + bitsToDifficultyTarget, + targetToDifficulty, } from "../src/bitcoin" import { calculateDepositRefundLocktime } from "../src/deposit" import { BitcoinNetwork } from "../src/bitcoin-network" +import { Hex } from "../src/hex" +import { BigNumber } from "ethers" describe("Bitcoin", () => { describe("compressPublicKey", () => { @@ -354,4 +362,107 @@ describe("Bitcoin", () => { }) }) }) + + describe("serializeBlockHeader", () => { + it("calculates correct value", () => { + const blockHeader: BlockHeader = { + version: 536870916, + previousBlockHeaderHash: Hex.from( + "a5a3501e6ba1f3e2a1ee5d29327a549524ed33f272dfef300045660000000000" + ), + merkleRootHash: Hex.from( + "e27d241ca36de831ab17e6729056c14a383e7a3f43d56254f846b49649775112" + ), + time: 1641914003, + bits: 436256810, + nonce: 778087099, + } + + const expectedSerializedBlockHeader: string = + "04000020a5a3501e6ba1f3e2a1ee5d29327a549524ed33f272dfef30004566000000" + + "0000e27d241ca36de831ab17e6729056c14a383e7a3f43d56254f846b49649775112" + + "939edd612ac0001abbaa602e" + + expect(serializeBlockHeader(blockHeader)).to.be.equal( + expectedSerializedBlockHeader + ) + }) + }) + + describe("deserializeBlockHeader", () => { + it("calculates correct value", () => { + const rawBlockHeader: string = + "04000020a5a3501e6ba1f3e2a1ee5d29327a549524ed33f272dfef30004566000000" + + "0000e27d241ca36de831ab17e6729056c14a383e7a3f43d56254f846b49649775112" + + "939edd612ac0001abbaa602e" + + const expectedBlockHeader: BlockHeader = { + version: 536870916, + previousBlockHeaderHash: Hex.from( + "a5a3501e6ba1f3e2a1ee5d29327a549524ed33f272dfef300045660000000000" + ), + merkleRootHash: Hex.from( + "e27d241ca36de831ab17e6729056c14a383e7a3f43d56254f846b49649775112" + ), + time: 1641914003, + bits: 436256810, + nonce: 778087099, + } + + expect(deserializeBlockHeader(rawBlockHeader)).to.deep.equal( + expectedBlockHeader + ) + }) + }) + + describe("hashLEToBigNumber", () => { + it("calculates correct value", () => { + const hash = + "31552151fbef8e96a33f979e6253d29edf65ac31b04802319e00000000000000" + const expectedBigNumber = BigNumber.from( + "992983769452983078390935942095592601503357651673709518345521" + ) + expect(hashLEToBigNumber(hash)).to.equal(expectedBigNumber) + }) + }) + + describe("bitsToDifficultyTarget", () => { + it("calculates correct value for random block header bits", () => { + const difficultyBits = 436256810 + const expectedDifficultyTarget = BigNumber.from( + "1206233370197704583969288378458116959663044038027202007138304" + ) + expect(bitsToDifficultyTarget(difficultyBits)).to.equal( + expectedDifficultyTarget + ) + }) + + it("calculates correct value for block header with difficulty of 1", () => { + const difficultyBits = 486604799 + const expectedDifficultyTarget = BigNumber.from( + "26959535291011309493156476344723991336010898738574164086137773096960" + ) + expect(bitsToDifficultyTarget(difficultyBits)).to.equal( + expectedDifficultyTarget + ) + }) + }) + + describe("targetToDifficulty", () => { + it("calculates correct value for random block header bits", () => { + const target = BigNumber.from( + "1206233370197704583969288378458116959663044038027202007138304" + ) + const expectedDifficulty = BigNumber.from("22350181") + expect(targetToDifficulty(target)).to.equal(expectedDifficulty) + }) + + it("calculates correct value for block header with difficulty of 1", () => { + const target = BigNumber.from( + "26959535291011309493156476344723991336010898738574164086137773096960" + ) + const expectedDifficulty = BigNumber.from("1") + expect(targetToDifficulty(target)).to.equal(expectedDifficulty) + }) + }) }) From 170169292116c94c62a42a1e07b9d27b97169a8b Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Wed, 22 Feb 2023 12:39:32 +0100 Subject: [PATCH 09/30] Minor improvements to transaction validation --- typescript/src/proof.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 0ba96fbc9..297d4b55c 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -184,6 +184,7 @@ function validateBlockHeadersChain( previousEpochDifficulty: BigNumber, currentEpochDifficulty: BigNumber ) { + let requireCurrentDifficulty: boolean = false let previousBlockHeaderHash: Hex = Hex.from("00") for (let index = 0; index < blockHeaders.length; index++) { @@ -205,6 +206,7 @@ function validateBlockHeadersChain( serializeBlockHeader(currentHeader) ) + // Ensure the header has sufficient work. if (hashLEToBigNumber(currentBlockHeaderHash).gt(difficultyTarget)) { throw new Error("Insufficient work in the header") } @@ -223,14 +225,25 @@ function validateBlockHeadersChain( continue } - // TODO: For mainnet we could check if there is no more than one switch - // from previous to current difficulties if ( !difficulty.eq(previousEpochDifficulty) && !difficulty.eq(currentEpochDifficulty) ) { - throw new Error("Header difficulty not at current or previous difficulty") + throw new Error( + "Header difficulty not at current or previous Bitcoin difficulty" + ) } + + // Additionally, require the header to be at current difficulty if some + // headers with current difficulty have already been seen. This ensures + // there is at most one switch from previous to current difficulties. + if (requireCurrentDifficulty && !difficulty.eq(currentEpochDifficulty)) { + throw new Error("Header must be at current Bitcoin difficulty") + } + + // If the header is at current difficulty, require the subsequent headers to + // be at current difficulty as well. + requireCurrentDifficulty = difficulty.eq(currentEpochDifficulty) } } From eff639fa1cd3a684e6ef0b5b146f6970884285ef Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Wed, 22 Feb 2023 17:22:17 +0100 Subject: [PATCH 10/30] Added missing docstrings --- typescript/src/proof.ts | 126 ++++++++++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 36 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 297d4b55c..fd66542f2 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -69,7 +69,7 @@ export async function assembleTransactionProof( /** * Create a proof of transaction inclusion in the block by concatenating * 32-byte-long hash values. The values are converted to little endian form. - * @param txMerkleBranch - Branch of a merkle tree leading to a transaction. + * @param txMerkleBranch - Branch of a Merkle tree leading to a transaction. * @returns Transaction inclusion proof in hexadecimal form. */ function createMerkleProof(txMerkleBranch: TransactionMerkleBranch): string { @@ -80,8 +80,22 @@ function createMerkleProof(txMerkleBranch: TransactionMerkleBranch): string { return proof.toString("hex") } -// TODO: Description -// TODO: should we check the transaction itself (inputs, outputs)? +/** + * Proves that a transaction with the given hash is included in the Bitcoin + * blockchain by validating the transaction's inclusion in the Merkle tree and + * verifying that the block containing the transaction has enough confirmations. + * @param transactionHash The hash of the transaction to be validated. + * @param requiredConfirmations The number of confirmations required for the + * transaction to be considered valid. + * @param previousDifficulty The difficulty of the previous Bitcoin epoch. + * @param currentDifficulty The difficulty of the current Bitcoin epoch. + * @param bitcoinClient The client for interacting with the Bitcoin blockchain. + * @throws {Error} If the transaction is not included in the Bitcoin blockchain + * or if the block containing the transaction does not have enough + * confirmations. + * @dev The function should be used within a try-catch block. + * @returns An empty return value. + */ export async function validateTransactionProof( transactionHash: TransactionHash, requiredConfirmations: number, @@ -95,8 +109,8 @@ export async function validateTransactionProof( bitcoinClient ) - const bitcoinHeaders = splitHeaders(proof.bitcoinHeaders) - const merkleRootHash = bitcoinHeaders[0].merkleRootHash + const bitcoinHeaders: BlockHeader[] = splitHeaders(proof.bitcoinHeaders) + const merkleRootHash: Hex = bitcoinHeaders[0].merkleRootHash validateMerkleTree( transactionHash.reverse().toString(), @@ -112,72 +126,112 @@ export async function validateTransactionProof( ) } +/** + * Validates the Merkle tree by checking if the provided transaction hash, + * Merkle root hash, intermediate node hashes, and transaction index parameters + * produce a valid Merkle proof. + * @param transactionHash The hash of the transaction being validated. + * @param merkleRootHash The Merkle root hash that the intermediate node hashes + * should compute to. + * @param intermediateNodeHashes The Merkle tree intermediate node hashes, + * concatenated as a single string. + * @param transactionIndex The index of the transaction being validated within + * the block, used to determine the path to traverse in the Merkle tree. + * @throws {Error} If the Merkle tree is not valid. + * @returns An empty return value. + */ function validateMerkleTree( transactionHash: string, merkleRootHash: string, - intermediateNodes: string, - transactionIdxInBlock: number + intermediateNodeHashes: string, + transactionIndex: number ) { // Shortcut the empty-block case if ( transactionHash == merkleRootHash && - transactionIdxInBlock == 0 && - intermediateNodes.length == 0 + transactionIndex == 0 && + intermediateNodeHashes.length == 0 ) { return } validateMerkleTreeHashes( transactionHash, - intermediateNodes, + intermediateNodeHashes, merkleRootHash, - transactionIdxInBlock + transactionIndex ) } +/** + * Validates the transaction's Merkle proof by traversing the Merkle tree + * starting from the provided transaction hash and using the intermediate node + * hashes to compute the root hash. If the computed root hash does not match the + * merkle root hash, an error is thrown. + * @param transactionHash The hash of the transaction being validated. + * @param intermediateNodesHashes The Merkle tree intermediate nodes hashes, + * concatenated as a single string. + * @param merkleRootHash The Merkle root hash that the intermediate nodes should + * compute to. + * @param transactionIndex The index of the transaction in the block, used + * to determine the path to traverse in the Merkle tree. + * @throws {Error} If the intermediate nodes are of an invalid length or if the + * computed root hash does not match the merkle root hash parameter. + * @returns An empty return value. + */ function validateMerkleTreeHashes( - leafHash: string, - intermediateNodes: string, - merkleRoot: string, - transactionIdxInBlock: number + transactionHash: string, + intermediateNodesHashes: string, + merkleRootHash: string, + transactionIndex: number ) { - if (intermediateNodes.length === 0 || intermediateNodes.length % 64 !== 0) { + if ( + intermediateNodesHashes.length === 0 || + intermediateNodesHashes.length % 64 !== 0 + ) { throw new Error("Invalid merkle tree") } - let idx = transactionIdxInBlock - let current = leafHash + let idx = transactionIndex + let current = transactionHash // i moves in increments of 64 - for (let i = 0; i < intermediateNodes.length; i += 64) { + for (let i = 0; i < intermediateNodesHashes.length; i += 64) { if (idx % 2 === 1) { - current = computeHash256(intermediateNodes.slice(i, i + 64) + current) + current = computeHash256( + intermediateNodesHashes.slice(i, i + 64) + current + ) } else { - current = computeHash256(current + intermediateNodes.slice(i, i + 64)) + current = computeHash256( + current + intermediateNodesHashes.slice(i, i + 64) + ) } idx = idx >> 1 } - if (current !== merkleRoot) { + if (current !== merkleRootHash) { throw new Error( - "Transaction merkle proof is not valid for provided header and transaction hash" + "Transaction Merkle proof is not valid for provided header and transaction hash" ) } } /** - * Validates a chain of consecutive block headers. It checks if each of the - * block headers has appropriate difficulty, hash of each block is below the - * required target and block headers form a chain. - * @dev The block headers must come form Bitcoin epochs with difficulties - * marked by previous and current difficulties. If a Bitcoin difficulty - * relay is used to provide these values and the relay is up-to-date, only - * the recent block headers will pass validation. Block headers older than - * the current and previous Bitcoin epochs will fail. - * @param blockHeaders - block headers that form the chain. - * @param previousEpochDifficulty - difficulty of the previous Bitcoin epoch. - * @param currentEpochDifficulty - difficulty of the current Bitcoin epoch. - * @returns Empty return. + * Validates a chain of consecutive block headers by checking each header's + * difficulty, hash, and continuity with the previous header. This function can + * be used to validate a series of Bitcoin block headers for their validity. + * @param blockHeaders An array of block headers that form the chain to be + * validated. + * @param previousEpochDifficulty The difficulty of the previous Bitcoin epoch. + * @param currentEpochDifficulty The difficulty of the current Bitcoin epoch. + * @dev The block headers must come from Bitcoin epochs with difficulties marked + * by the previous and current difficulties. If a Bitcoin difficulty relay + * is used to provide these values and the relay is up-to-date, only the + * recent block headers will pass validation. Block headers older than the + * current and previous Bitcoin epochs will fail. + * @throws {Error} If any of the block headers are invalid, or if the block + * header chain is not continuous. + * @returns An empty return value. */ function validateBlockHeadersChain( blockHeaders: BlockHeader[], @@ -235,7 +289,7 @@ function validateBlockHeadersChain( } // Additionally, require the header to be at current difficulty if some - // headers with current difficulty have already been seen. This ensures + // headers at current difficulty have already been seen. This ensures // there is at most one switch from previous to current difficulties. if (requireCurrentDifficulty && !difficulty.eq(currentEpochDifficulty)) { throw new Error("Header must be at current Bitcoin difficulty") From 072614a49f7d9c2f96b1d51f6f5f4cff2b8ae64d Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Wed, 22 Feb 2023 18:06:21 +0100 Subject: [PATCH 11/30] Used Hex to represent hex strings --- typescript/src/bitcoin.ts | 11 +++++------ typescript/src/proof.ts | 10 ++++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 4e776cc7b..18a08e86b 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -218,7 +218,7 @@ export interface BlockHeader { * @returns Block header as a BlockHeader. */ export function deserializeBlockHeader(rawBlockHeader: string): BlockHeader { - const buffer = Buffer.from(rawBlockHeader, "hex") + const buffer = Hex.from(rawBlockHeader).toBuffer() const version = buffer.readUInt32LE(0) const previousBlockHeaderHash = buffer.slice(4, 36) const merkleRootHash = buffer.slice(36, 68) @@ -486,9 +486,10 @@ export function computeHash160(text: string): string { * @param text - Text the double SHA256 is computed for. * @returns Hash as a 32-byte un-prefixed hex string. */ -export function computeHash256(text: string): string { +export function computeHash256(text: string): Hex { const firstHash = sha256.digest(Buffer.from(text, "hex")) - return sha256.digest(firstHash).toString("hex") + const secondHash = sha256.digest(firstHash) + return Hex.from(secondHash) } /** @@ -497,9 +498,7 @@ export function computeHash256(text: string): string { * @returns BigNumber representation of the hash. */ export function hashLEToBigNumber(hash: string): BigNumber { - return BigNumber.from( - "0x" + Buffer.from(hash, "hex").reverse().toString("hex") - ) + return BigNumber.from(Hex.from(hash).reverse().toPrefixedString()) } /** diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index fd66542f2..8b18b6d46 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -200,11 +200,11 @@ function validateMerkleTreeHashes( if (idx % 2 === 1) { current = computeHash256( intermediateNodesHashes.slice(i, i + 64) + current - ) + ).toString() } else { current = computeHash256( current + intermediateNodesHashes.slice(i, i + 64) - ) + ).toString() } idx = idx >> 1 } @@ -261,13 +261,15 @@ function validateBlockHeadersChain( ) // Ensure the header has sufficient work. - if (hashLEToBigNumber(currentBlockHeaderHash).gt(difficultyTarget)) { + if ( + hashLEToBigNumber(currentBlockHeaderHash.toString()).gt(difficultyTarget) + ) { throw new Error("Insufficient work in the header") } // Save the current block header hash to compare it with the next block // header's previous block header hash. - previousBlockHeaderHash = Hex.from(currentBlockHeaderHash) + previousBlockHeaderHash = currentBlockHeaderHash // Check if the stored block difficulty is equal to previous or current // difficulties. From a9366b6b8e4fde7a2687a5c0ea10d227fa42e138 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 23 Feb 2023 15:36:51 +0100 Subject: [PATCH 12/30] Added unit tests for validating transaction inclusion proof --- typescript/src/proof.ts | 5 +- typescript/test/data/proof.ts | 245 ++++++++++++++++++++++++++++++++++ typescript/test/proof.test.ts | 133 +++++++++++++++++- 3 files changed, 378 insertions(+), 5 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 8b18b6d46..cf594601d 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -47,7 +47,7 @@ export async function assembleTransactionProof( const headersChain = await bitcoinClient.getHeadersChain( txBlockHeight, - requiredConfirmations + requiredConfirmations - 1 ) const merkleBranch = await bitcoinClient.getTransactionMerkle( @@ -110,6 +110,9 @@ export async function validateTransactionProof( ) const bitcoinHeaders: BlockHeader[] = splitHeaders(proof.bitcoinHeaders) + if (bitcoinHeaders.length != requiredConfirmations) { + throw new Error("Wrong number of confirmations") + } const merkleRootHash: Hex = bitcoinHeaders[0].merkleRootHash validateMerkleTree( diff --git a/typescript/test/data/proof.ts b/typescript/test/data/proof.ts index 6540d709d..97a08f326 100644 --- a/typescript/test/data/proof.ts +++ b/typescript/test/data/proof.ts @@ -336,3 +336,248 @@ export const multipleInputsProofTestData: ProofTestData = { "b58e6cd93b85290a885dd749f4d61c62ed3e031ad9a83746", }, } + +export interface TransactionProofData { + requiredConfirmations: number + bitcoinChainData: { + transaction: Transaction + accumulatedTxConfirmations: number + latestBlockHeight: number + headersChain: string + transactionMerkleBranch: TransactionMerkleBranch + previousDifficulty: BigNumber + currentDifficulty: BigNumber + } +} + +export const transactionConfirmationsInOneEpochData: TransactionProofData = { + requiredConfirmations: 6, + bitcoinChainData: { + transaction: { + transactionHash: TransactionHash.from( + "713525ee9d9ab23433cd6ad470566ba1f47cac2d7f119cc50119128a84d718aa" + ), + inputs: [ + { + transactionHash: TransactionHash.from( + "91b83d443f32d5a1e87a200eac5d3501af0f156bef3a59d5e8805b4679c4a2a5" + ), + outputIndex: 3, + scriptSig: Hex.from( + "473044022008bfea0e9b8e24b0ab04de42db2dd8aea9e6f764f9f94aa88e284d" + + "5c2800706d02200d793f7441ea17802da993914da732e2f4e354e54dd168636b" + + "e73e6b60a39eab012103e356007964fc225a44c38352899c41e6293a97f8d811" + + "5998ae7e97184704c092" + ), + }, + ], + outputs: [ + { + outputIndex: 0, + value: BigNumber.from(5500), + scriptPubKey: Hex.from( + "6a4c5058325b63f33166b9786bdd34b2be8160d5e4fbef9a0a45e773c4201a82" + + "a4b1eb44793a61d19892a7f8aede51b70953a210e9e8dba54375e4a06d95d68f" + + "90aa3c6e8914000bd7e50056000bd775012528" + ), + }, + { + outputIndex: 1, + value: BigNumber.from(48850), + scriptPubKey: Hex.from( + "76a914953490146c3ae270d66e09c4d12df4573d24c75b88ac" + ), + }, + { + outputIndex: 2, + value: BigNumber.from(48850), + scriptPubKey: Hex.from( + "a914352481ec2fecfde0c5cdc635a383c4ac27b9f71e87" + ), + }, + { + outputIndex: 3, + value: BigNumber.from(12614691), + scriptPubKey: Hex.from( + "76a914b00de0cc7b5e518f7d1e43d6e5ecbd52e0cd0c2f88ac" + ), + }, + ], + }, + accumulatedTxConfirmations: 1798, + latestBlockHeight: 777963, + headersChain: + "00e0ff2f5ad9c09e1d8aae777a58bf29c41621eb629032598f7900000000000000000" + + "0004dea17724c3b7e67d4cf1ac41a4c7527b884f7406575eaf5b8efaf2fb12572ecb1" + + "ace86339300717760098100000ff3fd3ab40174610c286e569edd20fa713bd98bab53" + + "bee83050000000000000000002345f5ef807cf75de7b30ccfe493c46c6e07aca044aa" + + "2aa106141637f1bb8500a6ade863393007177fbbd4b300800120646d493817f0ac988" + + "6a0a194ca3a957f70c3eb642ffd05000000000000000000d95674b737f097f042eebe" + + "b970c09b274df7e72a9c202ff2292ed72b056ee90967aee863393007172e2bb92e006" + + "03b27a391d248c258ef628dfb8c710ce44c8017667a07941402000000000000000000" + + "35214e58eb018dea1efa7eaf1b7f19ff2d6f0310c122be6dc8c0258d9524ae9382aee" + + "863393007173e82b2000000002003c7003ff9a79f16d956fc764b43b35080efe3a820" + + "af050000000000000000007808e96809cd46d5898d86faabc8f28a8b6572eb8399796" + + "70b2851d78fc1f75f17b3e86339300717450f17650400e020fb9b6a28bb2e9cea36d3" + + "40588f19ffa4e944b050e73f03000000000000000000bbd7534f2550ee99f31efcd77" + + "564f1b5b3f3966a76847896a8d9f9ee964d670ba2b4e8633930071777b10cfc", + transactionMerkleBranch: { + blockHeight: 776166, + merkle: [ + "f6ce0e34cc5b2a4b8cd4fd02a65d7cf62013206969e8e5cf1df18f994abcf1ff", + "08899ec43299b324583722f3e7d0938446a1f31a6ab34c8e24cb4ea9ba6cd384", + "9677b6075dfa2da8bcc98aa10ae7d30f81e6506215eadd3f3739a5d987e62b35", + "aa6712d8820c06ec8ce99f9c19d580ab54bb45f69b426935153b81e7d412ddba", + "b38be47e1dd9a7324ad81a395a133f26fc88cb736a4998dbba6cbabca10629a8", + "13bdefbf92421aa7861528e16e7046b569d25ee0f4b7649492e42e9ea2331c39", + "df429494c5eef971a7ab80c8a0f7f9cdfa30148afef706f07923bd93d5a7e22a", + "c8a3f1bc73146bd4a1a0e848f2b0b4a21be86e4930f239d856af8e9646014236", + "1f514df87fe2c400e508e01cd8967657ef76db9681f65dc82b0bc6d4004b575f", + "e463950c8efd9114237189f07ddf1cfdb72658bad23bce667c269652bd0ade3c", + "3d7ae6df787807320fdc397a7055e86c932a7c36ab1d1f942b92c53bf2a1d2f9", + ], + position: 17, + }, + previousDifficulty: BigNumber.from(39156400059293), + currentDifficulty: BigNumber.from(39350942467772), + }, +} + +export const transactionConfirmationsInTwoEpochsData: TransactionProofData = { + requiredConfirmations: 6, + bitcoinChainData: { + transaction: { + transactionHash: TransactionHash.from( + "e073636400e132b8c1082133ab2b48866919153998f4f04877b580e9932d5a17" + ), + inputs: [ + { + transactionHash: TransactionHash.from( + "f160a6565d07fd2e8f1d0aaaff538f3150b7f9d2bc64f191076f45c92725b990" + ), + outputIndex: 0, + scriptSig: Hex.from(""), + }, + ], + outputs: [ + { + outputIndex: 0, + value: BigNumber.from(38385795), + scriptPubKey: Hex.from( + "00145ade2be870b440e171644f22973db748a2002305" + ), + }, + { + outputIndex: 1, + value: BigNumber.from(2181468), + scriptPubKey: Hex.from( + "76a914dbdbe7f1c2ba3dfe38c32b9261f5d8fcb36b689788ac" + ), + }, + ], + }, + accumulatedTxConfirmations: 3838, + latestBlockHeight: 777979, + headersChain: + "0040f224871a401b605e02c475e05e147bd418e5e2ae9eb599e200000000000000000" + + "000193dc07aea4388a163ed0e3e5234ef54594cfc046bce727d2d6b3445d3ce0e8c44" + + "0dd663e27c07170c0d54de00e0682c9c27df3b2a1b011753c986c290ce22c60d09a05" + + "3707100000000000000000000ddf3b023ed6368bdac8578bd55d0c3fad7f234ae971b" + + "902b155bee7318bf0919b30dd663e27c0717be025f2b00000020514a9bd87c51caedd" + + "45a20c495f0ba1983b6f3f51639050000000000000000001f4c60a97f4127b4f90fbb" + + "7a6a1041881b10d4f7351340b6770301f62b36725ce10dd66320270717c11c5e7b002" + + "0002043e99cc906d52209796ecb37b252e4514f197d727ea701000000000000000000" + + "274ecaf37779be81c23748d33ef4a0cad36a8abd935a11f0e0a71640c6dd1deaf10dd" + + "66320270717846927aa0000c02090a4a88ab1ad55e235932fe0adc7b4c822b4322f58" + + "9305000000000000000000decc945dc9cdf595715ffeee3bffc0ec0c8c5ff77e43b8e" + + "91213e21a9975c99ddc10d663202707179f93251000203229e618c1eb9274a1acbb74" + + "d44bfe9a4ecfae236ea35e8b0300000000000000000029a9f7b4f6671dec5d6ba05ac" + + "b060fcd2ffc6e46a992189c6f60d770d9c5a5cda31cd66320270717542691a2", + transactionMerkleBranch: { + blockHeight: 774142, + merkle: [ + "e80f706f53d5abd77070ea6c8a60c141748400e09fc9b373d5cdb0129cbce5ec", + "20d22506199cf00caf2e32e240c77a23c226d5a74de4dc9150ccd6f5200b4dd7", + "8b446693fadaae7479725f0e98430c24f8bf8936f5a5cab7c725692cd78e61e3", + "93e61f1ac82cf6a66e321c60410ae4bdfcc0ab45b7efd50353d7b08104758403", + "1dc52561092701978f1e48a10bc4da5464e668f0f4b3a940853c941474ee52de", + "84aca5ec5b339b69a50b93d35c2fd7b146c037842ca76b33cbf835b9e6c86f0c", + "ebcd1bb7039d40ac0d477af58964b4582c6741d1c901ab4a2b0de15e600cba69", + "38d458a70805902a52342cfc552d374bdb217cd389e9550adfc4f86df6fdce82", + "07781ff50552aefea962f0f4972fe882cb38a281ebdd533c2886d5137b80fbeb", + "e7e530e181683d272293f19fe18a33f1dc05eded12ec27945b49311b2e14ee42", + ], + position: 262, + }, + previousDifficulty: BigNumber.from(37590453655497), + currentDifficulty: BigNumber.from(39350942467772), + }, +} + +export const testnetTransactionData: TransactionProofData = { + requiredConfirmations: 6, + bitcoinChainData: { + transaction: { + transactionHash: TransactionHash.from( + "b78636ae08e6c17261a9f3134109c13c2eb69f6df52e591cc0e0780f5ebf6472" + ), + inputs: [ + { + transactionHash: TransactionHash.from( + "b230eb52608287da6320fa0926b3ada60f8979fa662d878494d11909d9841aba" + ), + outputIndex: 1, + scriptSig: Hex.from(""), + }, + ], + outputs: [ + { + outputIndex: 0, + value: BigNumber.from(1342326), + scriptPubKey: Hex.from( + "0014ffadb0a5ab3f58e651383b478acdc7cd0008e351" + ), + }, + { + outputIndex: 1, + value: BigNumber.from(7218758882), + scriptPubKey: Hex.from( + "00143c258d94e7abf4695585911b0420c24c1c78213e" + ), + }, + ], + }, + accumulatedTxConfirmations: 18, + latestBlockHeight: 2421198, + headersChain: + "000000203528cf6e8112d970a1adeb9743937d2e980afb43cb8ce3600100000000000" + + "0007bacd9aa2249c74fdba75dd651a16755e9b4dc3c1953f2baa01d657f317e3eb936" + + "62f763ffff001d7045e837000040207184a40ae97e64b2bce8fed41f967eac210e036" + + "9a66855bd2b37c86200000000fe261c184d19c15c7b66c284d5f65e79595f65d576cc" + + "40f20cccf0fcbae3c063a866f7639cde2c193ed763b904e000209885f5bb4bc96f8ff" + + "ed3bf31c6f526f1f71fc6dd3f9bb0ed0200000000000000720c67b13ee8805763110f" + + "b345cbfb5369836344e6a990e4ac0c363211362b2c6168f7639cde2c19294a1006000" + + "040200aafa9b9e947a9bd6fe2e9f04dece7753863d59b11e5c63b1500000000000000" + + "7a63f980ffc1f993c0d7dbe0670e71be2eeae8710a7906f758d3b400dd6a1e6b3c69f" + + "7639cde2c1940a3735000008020ba335b0d58de55cf227fdd35ba380a4a288d4f7926" + + "8be6a01800000000000000ffdc211cb41a97249e18a54aa4861a77f43093d6716995a" + + "9f659370ee1cf8aea406af7639cde2c19254197450000002069b318d3a7c7c154651f" + + "23ac4c3a51c7ec5158f40a62783c0400000000000000f452ef784d467c9f541331552" + + "32d005bdd0f2d323933646976ef2b7275206d7ff96ef763ffff001db18d224b", + transactionMerkleBranch: { + blockHeight: 2421181, + merkle: [ + "33610df4f460e1338d9f6a055de18d5c694edf590722211b6feeec77a9479846", + "0fd7e0afdde99bdfbfdc0d0e6f5ccda4cd1873eee315bb989622fd58bd5c4446", + "2d4ab6c53cedc1a447e21ad2f38c6d9d0d9c761426975a65f83fe10f12e3c9e0", + "0eebd6daa03f6db4a27541a91bcf86612c97d100bc37c3eb321d64d943adb2a5", + "b25854f31fc046eb0f53cddbf2b6de3d54d52710acd79a796c78c3be235f031a", + "1fc5ab77039f59ac2494791fc05c75fb53e2dacf57a20f67e7d6727b38778825", + "5b0acfdbb89af64a583a88e92252b8634bd4da06ee102ecd34c2662955e9f1c7", + ], + position: 4, + }, + previousDifficulty: BigNumber.from(1), + currentDifficulty: BigNumber.from(1), + }, +} diff --git a/typescript/test/proof.test.ts b/typescript/test/proof.test.ts index cbb8409ca..a8596161b 100644 --- a/typescript/test/proof.test.ts +++ b/typescript/test/proof.test.ts @@ -1,21 +1,25 @@ import { MockBitcoinClient } from "./utils/mock-bitcoin-client" -import { Transaction } from "./bitcoin" +import { Transaction } from "../src/bitcoin" import { singleInputProofTestData, multipleInputsProofTestData, + transactionConfirmationsInOneEpochData, + transactionConfirmationsInTwoEpochsData, + testnetTransactionData, ProofTestData, } from "./data/proof" -import { assembleTransactionProof } from "../src/proof" +import { + assembleTransactionProof, + validateTransactionProof, +} from "../src/proof" import { Proof } from "./bitcoin" import { expect } from "chai" -import bcoin from "bcoin" describe("Proof", () => { describe("assembleTransactionProof", () => { let bitcoinClient: MockBitcoinClient beforeEach(async () => { - bcoin.set("testnet") bitcoinClient = new MockBitcoinClient() }) @@ -106,4 +110,125 @@ describe("Proof", () => { return proof } }) + + describe("validateTransactionProof", () => { + let bitcoinClient: MockBitcoinClient + + beforeEach(async () => { + bitcoinClient = new MockBitcoinClient() + }) + + context("when the transaction is from Bitcoin Mainnet", () => { + context("when the transaction confirmations span only one epoch", () => { + const data = transactionConfirmationsInOneEpochData + + beforeEach(async () => { + const transactions = new Map<string, Transaction>() + const transactionHash = + data.bitcoinChainData.transaction.transactionHash + transactions.set( + transactionHash.toString(), + data.bitcoinChainData.transaction + ) + bitcoinClient.transactions = transactions + bitcoinClient.latestHeight = data.bitcoinChainData.latestBlockHeight + bitcoinClient.headersChain = data.bitcoinChainData.headersChain + bitcoinClient.transactionMerkle = + data.bitcoinChainData.transactionMerkleBranch + const confirmations = new Map<string, number>() + confirmations.set( + transactionHash.toString(), + data.bitcoinChainData.accumulatedTxConfirmations + ) + bitcoinClient.confirmations = confirmations + }) + + it("should not throw", async () => { + expect( + await validateTransactionProof( + data.bitcoinChainData.transaction.transactionHash, + data.requiredConfirmations, + data.bitcoinChainData.previousDifficulty, + data.bitcoinChainData.currentDifficulty, + bitcoinClient + ) + ).not.to.throw + }) + }) + + context("when the transaction confirmations span two epochs", () => { + const data = transactionConfirmationsInTwoEpochsData + + beforeEach(async () => { + const transactions = new Map<string, Transaction>() + const transactionHash = + data.bitcoinChainData.transaction.transactionHash + transactions.set( + transactionHash.toString(), + data.bitcoinChainData.transaction + ) + bitcoinClient.transactions = transactions + bitcoinClient.latestHeight = data.bitcoinChainData.latestBlockHeight + bitcoinClient.headersChain = data.bitcoinChainData.headersChain + bitcoinClient.transactionMerkle = + data.bitcoinChainData.transactionMerkleBranch + const confirmations = new Map<string, number>() + confirmations.set( + transactionHash.toString(), + data.bitcoinChainData.accumulatedTxConfirmations + ) + bitcoinClient.confirmations = confirmations + }) + + it("should not throw", async () => { + expect( + await validateTransactionProof( + data.bitcoinChainData.transaction.transactionHash, + data.requiredConfirmations, + data.bitcoinChainData.previousDifficulty, + data.bitcoinChainData.currentDifficulty, + bitcoinClient + ) + ).not.to.throw + }) + }) + }) + + context("when the transaction is from Bitcoin Testnet", () => { + const data = testnetTransactionData + + beforeEach(async () => { + const transactions = new Map<string, Transaction>() + const transactionHash = + data.bitcoinChainData.transaction.transactionHash + transactions.set( + transactionHash.toString(), + data.bitcoinChainData.transaction + ) + bitcoinClient.transactions = transactions + bitcoinClient.latestHeight = data.bitcoinChainData.latestBlockHeight + bitcoinClient.headersChain = data.bitcoinChainData.headersChain + bitcoinClient.transactionMerkle = + data.bitcoinChainData.transactionMerkleBranch + const confirmations = new Map<string, number>() + confirmations.set( + transactionHash.toString(), + data.bitcoinChainData.accumulatedTxConfirmations + ) + bitcoinClient.confirmations = confirmations + }) + + it("should not throw", async () => { + expect( + await validateTransactionProof( + data.bitcoinChainData.transaction.transactionHash, + data.requiredConfirmations, + data.bitcoinChainData.previousDifficulty, + data.bitcoinChainData.currentDifficulty, + bitcoinClient + ) + ).not.to.throw + }) + }) + }) }) From 9ca9023a9d6fe53289ac9d1378f6a2c6ba242456 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 23 Feb 2023 18:50:54 +0100 Subject: [PATCH 13/30] Refactored unit tests --- typescript/test/proof.test.ts | 136 ++++++++++------------------------ 1 file changed, 40 insertions(+), 96 deletions(-) diff --git a/typescript/test/proof.test.ts b/typescript/test/proof.test.ts index a8596161b..fa33090de 100644 --- a/typescript/test/proof.test.ts +++ b/typescript/test/proof.test.ts @@ -7,6 +7,7 @@ import { transactionConfirmationsInTwoEpochsData, testnetTransactionData, ProofTestData, + TransactionProofData, } from "./data/proof" import { assembleTransactionProof, @@ -14,6 +15,9 @@ import { } from "../src/proof" import { Proof } from "./bitcoin" import { expect } from "chai" +import * as chai from "chai" +import chaiAsPromised from "chai-as-promised" +chai.use(chaiAsPromised) describe("Proof", () => { describe("assembleTransactionProof", () => { @@ -120,115 +124,55 @@ describe("Proof", () => { context("when the transaction is from Bitcoin Mainnet", () => { context("when the transaction confirmations span only one epoch", () => { - const data = transactionConfirmationsInOneEpochData - - beforeEach(async () => { - const transactions = new Map<string, Transaction>() - const transactionHash = - data.bitcoinChainData.transaction.transactionHash - transactions.set( - transactionHash.toString(), - data.bitcoinChainData.transaction - ) - bitcoinClient.transactions = transactions - bitcoinClient.latestHeight = data.bitcoinChainData.latestBlockHeight - bitcoinClient.headersChain = data.bitcoinChainData.headersChain - bitcoinClient.transactionMerkle = - data.bitcoinChainData.transactionMerkleBranch - const confirmations = new Map<string, number>() - confirmations.set( - transactionHash.toString(), - data.bitcoinChainData.accumulatedTxConfirmations - ) - bitcoinClient.confirmations = confirmations - }) - it("should not throw", async () => { - expect( - await validateTransactionProof( - data.bitcoinChainData.transaction.transactionHash, - data.requiredConfirmations, - data.bitcoinChainData.previousDifficulty, - data.bitcoinChainData.currentDifficulty, - bitcoinClient - ) - ).not.to.throw + await expect( + runProofValidationScenario(transactionConfirmationsInOneEpochData) + ).not.to.be.rejected }) }) context("when the transaction confirmations span two epochs", () => { - const data = transactionConfirmationsInTwoEpochsData - - beforeEach(async () => { - const transactions = new Map<string, Transaction>() - const transactionHash = - data.bitcoinChainData.transaction.transactionHash - transactions.set( - transactionHash.toString(), - data.bitcoinChainData.transaction - ) - bitcoinClient.transactions = transactions - bitcoinClient.latestHeight = data.bitcoinChainData.latestBlockHeight - bitcoinClient.headersChain = data.bitcoinChainData.headersChain - bitcoinClient.transactionMerkle = - data.bitcoinChainData.transactionMerkleBranch - const confirmations = new Map<string, number>() - confirmations.set( - transactionHash.toString(), - data.bitcoinChainData.accumulatedTxConfirmations - ) - bitcoinClient.confirmations = confirmations - }) - it("should not throw", async () => { - expect( - await validateTransactionProof( - data.bitcoinChainData.transaction.transactionHash, - data.requiredConfirmations, - data.bitcoinChainData.previousDifficulty, - data.bitcoinChainData.currentDifficulty, - bitcoinClient - ) - ).not.to.throw + await expect( + runProofValidationScenario(transactionConfirmationsInTwoEpochsData) + ).not.to.be.rejected }) }) }) context("when the transaction is from Bitcoin Testnet", () => { - const data = testnetTransactionData - - beforeEach(async () => { - const transactions = new Map<string, Transaction>() - const transactionHash = - data.bitcoinChainData.transaction.transactionHash - transactions.set( - transactionHash.toString(), - data.bitcoinChainData.transaction - ) - bitcoinClient.transactions = transactions - bitcoinClient.latestHeight = data.bitcoinChainData.latestBlockHeight - bitcoinClient.headersChain = data.bitcoinChainData.headersChain - bitcoinClient.transactionMerkle = - data.bitcoinChainData.transactionMerkleBranch - const confirmations = new Map<string, number>() - confirmations.set( - transactionHash.toString(), - data.bitcoinChainData.accumulatedTxConfirmations - ) - bitcoinClient.confirmations = confirmations - }) - it("should not throw", async () => { - expect( - await validateTransactionProof( - data.bitcoinChainData.transaction.transactionHash, - data.requiredConfirmations, - data.bitcoinChainData.previousDifficulty, - data.bitcoinChainData.currentDifficulty, - bitcoinClient - ) - ).not.to.throw + await expect(runProofValidationScenario(testnetTransactionData)).not.to + .be.rejected }) }) + + async function runProofValidationScenario(data: TransactionProofData) { + const transactions = new Map<string, Transaction>() + const transactionHash = data.bitcoinChainData.transaction.transactionHash + transactions.set( + transactionHash.toString(), + data.bitcoinChainData.transaction + ) + bitcoinClient.transactions = transactions + bitcoinClient.latestHeight = data.bitcoinChainData.latestBlockHeight + bitcoinClient.headersChain = data.bitcoinChainData.headersChain + bitcoinClient.transactionMerkle = + data.bitcoinChainData.transactionMerkleBranch + const confirmations = new Map<string, number>() + confirmations.set( + transactionHash.toString(), + data.bitcoinChainData.accumulatedTxConfirmations + ) + bitcoinClient.confirmations = confirmations + + await validateTransactionProof( + data.bitcoinChainData.transaction.transactionHash, + data.requiredConfirmations, + data.bitcoinChainData.previousDifficulty, + data.bitcoinChainData.currentDifficulty, + bitcoinClient + ) + } }) }) From 681ccb6bcd890cde3a6f3bbf0e55a622163bf1ee Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Mon, 27 Feb 2023 15:18:15 +0100 Subject: [PATCH 14/30] Added unit tests for corrupted proof --- typescript/src/proof.ts | 2 +- typescript/test/proof.test.ts | 218 +++++++++++++++++++++++++++++++--- 2 files changed, 205 insertions(+), 15 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index cf594601d..9f9e02158 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -311,7 +311,7 @@ function validateBlockHeadersChain( * @param blockHeaders - string that contains block headers in the raw format. * @returns Array of BlockHeader objects. */ -function splitHeaders(blockHeaders: string): BlockHeader[] { +export function splitHeaders(blockHeaders: string): BlockHeader[] { if (blockHeaders.length % 160 !== 0) { throw new Error("Incorrect length of Bitcoin headers") } diff --git a/typescript/test/proof.test.ts b/typescript/test/proof.test.ts index fa33090de..af39bbf17 100644 --- a/typescript/test/proof.test.ts +++ b/typescript/test/proof.test.ts @@ -1,5 +1,6 @@ import { MockBitcoinClient } from "./utils/mock-bitcoin-client" -import { Transaction } from "../src/bitcoin" +import { serializeBlockHeader, Transaction, BlockHeader } from "../src/bitcoin" +import { Hex } from "../src/hex" import { singleInputProofTestData, multipleInputsProofTestData, @@ -12,6 +13,7 @@ import { import { assembleTransactionProof, validateTransactionProof, + splitHeaders, } from "../src/proof" import { Proof } from "./bitcoin" import { expect } from "chai" @@ -122,29 +124,217 @@ describe("Proof", () => { bitcoinClient = new MockBitcoinClient() }) - context("when the transaction is from Bitcoin Mainnet", () => { - context("when the transaction confirmations span only one epoch", () => { + context("when the transaction proof is correct", () => { + context("when the transaction is from Bitcoin Mainnet", () => { + context( + "when the transaction confirmations span only one epoch", + () => { + it("should not throw", async () => { + await expect( + runProofValidationScenario( + transactionConfirmationsInOneEpochData + ) + ).not.to.be.rejected + }) + } + ) + + context("when the transaction confirmations span two epochs", () => { + it("should not throw", async () => { + await expect( + runProofValidationScenario( + transactionConfirmationsInTwoEpochsData + ) + ).not.to.be.rejected + }) + }) + }) + + context("when the transaction is from Bitcoin Testnet", () => { it("should not throw", async () => { + await expect(runProofValidationScenario(testnetTransactionData)).not + .to.be.rejected + }) + }) + }) + + context("when the transaction proof is incorrect", () => { + context("when the length of headers chain is incorrect", () => { + it("should throw", async () => { + // Corrupt data by adding additional byte to the headers chain. + const corruptedProofData: TransactionProofData = { + ...transactionConfirmationsInOneEpochData, + bitcoinChainData: { + ...transactionConfirmationsInOneEpochData.bitcoinChainData, + headersChain: + transactionConfirmationsInOneEpochData.bitcoinChainData + .headersChain + "ff", + }, + } await expect( - runProofValidationScenario(transactionConfirmationsInOneEpochData) - ).not.to.be.rejected + runProofValidationScenario(corruptedProofData) + ).to.be.rejectedWith("Incorrect length of Bitcoin headers") }) }) - context("when the transaction confirmations span two epochs", () => { - it("should not throw", async () => { + context( + "when the headers chain contains an incorrect number of headers", + () => { + // Corrupt the data by adding additional 80 bytes to the headers chain. + it("should throw", async () => { + const corruptedProofData: TransactionProofData = { + ...transactionConfirmationsInOneEpochData, + bitcoinChainData: { + ...transactionConfirmationsInOneEpochData.bitcoinChainData, + headersChain: + transactionConfirmationsInOneEpochData.bitcoinChainData + .headersChain + "f".repeat(160), + }, + } + await expect( + runProofValidationScenario(corruptedProofData) + ).to.be.rejectedWith("Wrong number of confirmations") + }) + } + ) + + context("when the merkle proof is of incorrect length", () => { + it("should throw", async () => { + // Corrupt the data by adding a byte to the Merkle proof. + const merkle = [ + ...transactionConfirmationsInOneEpochData.bitcoinChainData + .transactionMerkleBranch.merkle, + ] + merkle[merkle.length - 1] += "ff" + + const corruptedProofData: TransactionProofData = { + ...transactionConfirmationsInOneEpochData, + bitcoinChainData: { + ...transactionConfirmationsInOneEpochData.bitcoinChainData, + transactionMerkleBranch: { + ...transactionConfirmationsInOneEpochData.bitcoinChainData + .transactionMerkleBranch, + merkle: merkle, + }, + }, + } + await expect( - runProofValidationScenario(transactionConfirmationsInTwoEpochsData) - ).not.to.be.rejected + runProofValidationScenario(corruptedProofData) + ).to.be.rejectedWith("Invalid merkle tree") }) }) - }) - context("when the transaction is from Bitcoin Testnet", () => { - it("should not throw", async () => { - await expect(runProofValidationScenario(testnetTransactionData)).not.to - .be.rejected + context("when the merkle proof contains incorrect hash", () => { + it("should throw", async () => { + // Corrupt the data by changing a byte of one of the hashes in the + // Merkle proof. + const merkle = [ + ...transactionConfirmationsInOneEpochData.bitcoinChainData + .transactionMerkleBranch.merkle, + ] + + merkle[3] = "ff" + merkle[3].slice(2) + + const corruptedProofData: TransactionProofData = { + ...transactionConfirmationsInOneEpochData, + bitcoinChainData: { + ...transactionConfirmationsInOneEpochData.bitcoinChainData, + transactionMerkleBranch: { + ...transactionConfirmationsInOneEpochData.bitcoinChainData + .transactionMerkleBranch, + merkle: merkle, + }, + }, + } + + await expect( + runProofValidationScenario(corruptedProofData) + ).to.be.rejectedWith( + "Transaction Merkle proof is not valid for provided header and transaction hash" + ) + }) }) + + context("when the block headers do not form a chain", () => { + it("should throw", async () => { + // Corrupt data by modifying previous block header hash of one of the + // headers. + const headers: BlockHeader[] = splitHeaders( + transactionConfirmationsInOneEpochData.bitcoinChainData.headersChain + ) + headers[headers.length - 1].previousBlockHeaderHash = Hex.from( + "ff".repeat(32) + ) + const corruptedHeadersChain: string = headers + .map(serializeBlockHeader) + .join("") + + const corruptedProofData: TransactionProofData = { + ...transactionConfirmationsInOneEpochData, + bitcoinChainData: { + ...transactionConfirmationsInOneEpochData.bitcoinChainData, + headersChain: corruptedHeadersChain, + }, + } + + await expect( + runProofValidationScenario(corruptedProofData) + ).to.be.rejectedWith("Invalid headers chain") + }) + }) + + context("when one of the block headers has insufficient work", () => { + it("should throw", async () => { + // Corrupt data by modifying the nonce of one of the headers, so that + // the resulting hash will be above the required difficulty target. + const headers: BlockHeader[] = splitHeaders( + transactionConfirmationsInOneEpochData.bitcoinChainData.headersChain + ) + headers[headers.length - 1].nonce++ + const corruptedHeadersChain: string = headers + .map(serializeBlockHeader) + .join("") + + const corruptedProofData: TransactionProofData = { + ...transactionConfirmationsInOneEpochData, + bitcoinChainData: { + ...transactionConfirmationsInOneEpochData.bitcoinChainData, + headersChain: corruptedHeadersChain, + }, + } + + await expect( + runProofValidationScenario(corruptedProofData) + ).to.be.rejectedWith("Insufficient work in the header") + }) + }) + + context( + "when some of the block headers are not at current or previous difficulty", + () => { + it("should throw", async () => { + // Corrupt data by setting current difficulty to a different value + // than stored in block headers. + const corruptedProofData: TransactionProofData = { + ...transactionConfirmationsInTwoEpochsData, + bitcoinChainData: { + ...transactionConfirmationsInTwoEpochsData.bitcoinChainData, + currentDifficulty: + transactionConfirmationsInTwoEpochsData.bitcoinChainData.currentDifficulty.add( + 1 + ), + }, + } + + await expect( + runProofValidationScenario(corruptedProofData) + ).to.be.rejectedWith( + "Header difficulty not at current or previous Bitcoin difficulty" + ) + }) + } + ) }) async function runProofValidationScenario(data: TransactionProofData) { From 6478e12a3c3984f156acf1c0ca4b21bbf1f8b34a Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Wed, 1 Mar 2023 10:38:26 +0100 Subject: [PATCH 15/30] Minor improvements --- typescript/src/bitcoin.ts | 34 +++++++++++++++++----------------- typescript/src/proof.ts | 8 +++++++- typescript/test/data/proof.ts | 17 +++++++++++++++++ 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 18a08e86b..7ea56a4e4 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -212,6 +212,22 @@ export interface BlockHeader { nonce: number } +/** + * Serializes a BlockHeader to the raw representation. + * @param blockHeader - block header. + * @returns Serialized block header. + */ +export function serializeBlockHeader(blockHeader: BlockHeader): string { + const buffer = Buffer.alloc(80) + buffer.writeUInt32LE(blockHeader.version, 0) + blockHeader.previousBlockHeaderHash.toBuffer().copy(buffer, 4) + blockHeader.merkleRootHash.toBuffer().copy(buffer, 36) + buffer.writeUInt32LE(blockHeader.time, 68) + buffer.writeUInt32LE(blockHeader.bits, 72) + buffer.writeUInt32LE(blockHeader.nonce, 76) + return buffer.toString("hex") +} + /** * Deserializes a block header in the raw representation to BlockHeader. * @param rawBlockHeader - BlockHeader in the raw format. @@ -236,26 +252,10 @@ export function deserializeBlockHeader(rawBlockHeader: string): BlockHeader { } } -/** - * Serializes a BlockHeader to the raw representation. - * @param blockHeader - block header. - * @returns Serialized block header. - */ -export function serializeBlockHeader(blockHeader: BlockHeader): string { - const buffer = Buffer.alloc(80) - buffer.writeUInt32LE(blockHeader.version, 0) - blockHeader.previousBlockHeaderHash.toBuffer().copy(buffer, 4) - blockHeader.merkleRootHash.toBuffer().copy(buffer, 36) - buffer.writeUInt32LE(blockHeader.time, 68) - buffer.writeUInt32LE(blockHeader.bits, 72) - buffer.writeUInt32LE(blockHeader.nonce, 76) - return buffer.toString("hex") -} - /** * Converts a block header's bits into difficulty target. * @param bits - bits from block header. - * @returns Difficulty target. + * @returns Difficulty target as a BigNumber. */ export function bitsToDifficultyTarget(bits: number): BigNumber { const exponent = ((bits >>> 24) & 0xff) - 3 diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 9f9e02158..2dabbc36d 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -86,7 +86,9 @@ function createMerkleProof(txMerkleBranch: TransactionMerkleBranch): string { * verifying that the block containing the transaction has enough confirmations. * @param transactionHash The hash of the transaction to be validated. * @param requiredConfirmations The number of confirmations required for the - * transaction to be considered valid. + * transaction to be considered valid. The transaction has 1 confirmation + * when it is in the block at the current blockchain tip. Every subsequent + * block added to the blockchain is one additional confirmation. * @param previousDifficulty The difficulty of the previous Bitcoin epoch. * @param currentDifficulty The difficulty of the current Bitcoin epoch. * @param bitcoinClient The client for interacting with the Bitcoin blockchain. @@ -103,6 +105,10 @@ export async function validateTransactionProof( currentDifficulty: BigNumber, bitcoinClient: BitcoinClient ) { + if (requiredConfirmations < 1) { + throw new Error("The number of required confirmations but at least 1") + } + const proof = await assembleTransactionProof( transactionHash, requiredConfirmations, diff --git a/typescript/test/data/proof.ts b/typescript/test/data/proof.ts index 97a08f326..a0acdf37c 100644 --- a/typescript/test/data/proof.ts +++ b/typescript/test/data/proof.ts @@ -337,6 +337,9 @@ export const multipleInputsProofTestData: ProofTestData = { }, } +/** + * Represents a set of data used for given transaction proof validation scenario. + */ export interface TransactionProofData { requiredConfirmations: number bitcoinChainData: { @@ -350,6 +353,11 @@ export interface TransactionProofData { } } +/** + * Test data that is based on a random Bitcoin mainnet transaction with all the + * blocks headers from one difficulty epoch + * https://live.blockcypher.com/btc/tx/713525ee9d9ab23433cd6ad470566ba1f47cac2d7f119cc50119128a84d718aa/ + */ export const transactionConfirmationsInOneEpochData: TransactionProofData = { requiredConfirmations: 6, bitcoinChainData: { @@ -443,6 +451,11 @@ export const transactionConfirmationsInOneEpochData: TransactionProofData = { }, } +/** + * Test data that is based on a random Bitcoin mainnet transaction with the + * blocks headers spanning two difficulty epochs + * https://live.blockcypher.com/btc/tx/e073636400e132b8c1082133ab2b48866919153998f4f04877b580e9932d5a17/ + */ export const transactionConfirmationsInTwoEpochsData: TransactionProofData = { requiredConfirmations: 6, bitcoinChainData: { @@ -514,6 +527,10 @@ export const transactionConfirmationsInTwoEpochsData: TransactionProofData = { }, } +/** + * Test data that is based on a random Bitcoin testnet transaction + * https://live.blockcypher.com/btc-testnet/tx/b78636ae08e6c17261a9f3134109c13c2eb69f6df52e591cc0e0780f5ebf6472/ + */ export const testnetTransactionData: TransactionProofData = { requiredConfirmations: 6, bitcoinChainData: { From c219a2a82183340790f41a29acc2782eba18cb39 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Wed, 1 Mar 2023 11:42:10 +0100 Subject: [PATCH 16/30] Made validateTransactionProof be exported from tbtc-v2 --- typescript/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/typescript/src/index.ts b/typescript/src/index.ts index aacb8abe8..15f686f54 100644 --- a/typescript/src/index.ts +++ b/typescript/src/index.ts @@ -22,6 +22,8 @@ import { getOptimisticMintingRequest, } from "./optimistic-minting" +import { validateTransactionProof } from "./proof" + export const TBTC = { calculateDepositAddress, suggestDepositWallet, @@ -43,6 +45,10 @@ export const OptimisticMinting = { getOptimisticMintingRequest, } +export const Bitcoin = { + validateTransactionProof, +} + export { TransactionHash as BitcoinTransactionHash, Transaction as BitcoinTransaction, From dde5ac56613658b93e2f4f67c0474b5399717ff4 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Wed, 8 Mar 2023 14:31:37 +0100 Subject: [PATCH 17/30] Used Hex to represent block header in the raw format --- typescript/src/bitcoin.ts | 8 ++++---- typescript/src/proof.ts | 6 ++++-- typescript/test/bitcoin.test.ts | 16 +++++++++------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 7ea56a4e4..860dda1f5 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -217,7 +217,7 @@ export interface BlockHeader { * @param blockHeader - block header. * @returns Serialized block header. */ -export function serializeBlockHeader(blockHeader: BlockHeader): string { +export function serializeBlockHeader(blockHeader: BlockHeader): Hex { const buffer = Buffer.alloc(80) buffer.writeUInt32LE(blockHeader.version, 0) blockHeader.previousBlockHeaderHash.toBuffer().copy(buffer, 4) @@ -225,7 +225,7 @@ export function serializeBlockHeader(blockHeader: BlockHeader): string { buffer.writeUInt32LE(blockHeader.time, 68) buffer.writeUInt32LE(blockHeader.bits, 72) buffer.writeUInt32LE(blockHeader.nonce, 76) - return buffer.toString("hex") + return Hex.from(buffer) } /** @@ -233,8 +233,8 @@ export function serializeBlockHeader(blockHeader: BlockHeader): string { * @param rawBlockHeader - BlockHeader in the raw format. * @returns Block header as a BlockHeader. */ -export function deserializeBlockHeader(rawBlockHeader: string): BlockHeader { - const buffer = Hex.from(rawBlockHeader).toBuffer() +export function deserializeBlockHeader(rawBlockHeader: Hex): BlockHeader { + const buffer = rawBlockHeader.toBuffer() const version = buffer.readUInt32LE(0) const previousBlockHeaderHash = buffer.slice(4, 36) const merkleRootHash = buffer.slice(36, 68) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 2dabbc36d..fde8b5a79 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -266,7 +266,7 @@ function validateBlockHeadersChain( const difficultyTarget = bitsToDifficultyTarget(currentHeader.bits) const currentBlockHeaderHash = computeHash256( - serializeBlockHeader(currentHeader) + serializeBlockHeader(currentHeader).toString() ) // Ensure the header has sufficient work. @@ -324,7 +324,9 @@ export function splitHeaders(blockHeaders: string): BlockHeader[] { const result: BlockHeader[] = [] for (let i = 0; i < blockHeaders.length; i += 160) { - result.push(deserializeBlockHeader(blockHeaders.substring(i, i + 160))) + result.push( + deserializeBlockHeader(Hex.from(blockHeaders.substring(i, i + 160))) + ) } return result diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index 657b6a6b0..ccb734774 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -378,12 +378,13 @@ describe("Bitcoin", () => { nonce: 778087099, } - const expectedSerializedBlockHeader: string = + const expectedSerializedBlockHeader = Hex.from( "04000020a5a3501e6ba1f3e2a1ee5d29327a549524ed33f272dfef30004566000000" + - "0000e27d241ca36de831ab17e6729056c14a383e7a3f43d56254f846b49649775112" + - "939edd612ac0001abbaa602e" + "0000e27d241ca36de831ab17e6729056c14a383e7a3f43d56254f846b496497751" + + "12939edd612ac0001abbaa602e" + ) - expect(serializeBlockHeader(blockHeader)).to.be.equal( + expect(serializeBlockHeader(blockHeader)).to.be.deep.equal( expectedSerializedBlockHeader ) }) @@ -391,10 +392,11 @@ describe("Bitcoin", () => { describe("deserializeBlockHeader", () => { it("calculates correct value", () => { - const rawBlockHeader: string = + const rawBlockHeader = Hex.from( "04000020a5a3501e6ba1f3e2a1ee5d29327a549524ed33f272dfef30004566000000" + - "0000e27d241ca36de831ab17e6729056c14a383e7a3f43d56254f846b49649775112" + - "939edd612ac0001abbaa602e" + "0000e27d241ca36de831ab17e6729056c14a383e7a3f43d56254f846b496497751" + + "12939edd612ac0001abbaa602e" + ) const expectedBlockHeader: BlockHeader = { version: 536870916, From 742b44f0937ebedb302335d8e32331a4699f6921 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Wed, 8 Mar 2023 14:54:07 +0100 Subject: [PATCH 18/30] Function rename --- typescript/src/bitcoin.ts | 12 +++++------- typescript/src/proof.ts | 4 ++-- typescript/test/bitcoin.test.ts | 12 ++++-------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 860dda1f5..84450adc7 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -253,18 +253,16 @@ export function deserializeBlockHeader(rawBlockHeader: Hex): BlockHeader { } /** - * Converts a block header's bits into difficulty target. + * Converts a block header's bits into target. * @param bits - bits from block header. - * @returns Difficulty target as a BigNumber. + * @returns Target as a BigNumber. */ -export function bitsToDifficultyTarget(bits: number): BigNumber { +export function bitsToTarget(bits: number): BigNumber { const exponent = ((bits >>> 24) & 0xff) - 3 const mantissa = bits & 0xffffff - const difficultyTarget = BigNumber.from(mantissa).mul( - BigNumber.from(256).pow(exponent) - ) - return difficultyTarget + const target = BigNumber.from(mantissa).mul(BigNumber.from(256).pow(exponent)) + return target } /** diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index fde8b5a79..46c506b90 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -6,7 +6,7 @@ import { TransactionHash, computeHash256, deserializeBlockHeader, - bitsToDifficultyTarget, + bitsToTarget, targetToDifficulty, hashLEToBigNumber, serializeBlockHeader, @@ -263,7 +263,7 @@ function validateBlockHeadersChain( } } - const difficultyTarget = bitsToDifficultyTarget(currentHeader.bits) + const difficultyTarget = bitsToTarget(currentHeader.bits) const currentBlockHeaderHash = computeHash256( serializeBlockHeader(currentHeader).toString() diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index ccb734774..0cf939286 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -9,7 +9,7 @@ import { serializeBlockHeader, deserializeBlockHeader, hashLEToBigNumber, - bitsToDifficultyTarget, + bitsToTarget, targetToDifficulty, } from "../src/bitcoin" import { calculateDepositRefundLocktime } from "../src/deposit" @@ -428,15 +428,13 @@ describe("Bitcoin", () => { }) }) - describe("bitsToDifficultyTarget", () => { + describe("bitsToTarget", () => { it("calculates correct value for random block header bits", () => { const difficultyBits = 436256810 const expectedDifficultyTarget = BigNumber.from( "1206233370197704583969288378458116959663044038027202007138304" ) - expect(bitsToDifficultyTarget(difficultyBits)).to.equal( - expectedDifficultyTarget - ) + expect(bitsToTarget(difficultyBits)).to.equal(expectedDifficultyTarget) }) it("calculates correct value for block header with difficulty of 1", () => { @@ -444,9 +442,7 @@ describe("Bitcoin", () => { const expectedDifficultyTarget = BigNumber.from( "26959535291011309493156476344723991336010898738574164086137773096960" ) - expect(bitsToDifficultyTarget(difficultyBits)).to.equal( - expectedDifficultyTarget - ) + expect(bitsToTarget(difficultyBits)).to.equal(expectedDifficultyTarget) }) }) From c09301bab376c3fba3c6c8670972ea1ad5d27a29 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Wed, 8 Mar 2023 15:00:43 +0100 Subject: [PATCH 19/30] Improved descriptions of BlockHeader's fields --- typescript/src/bitcoin.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index 84450adc7..c8b871a79 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -180,34 +180,37 @@ export interface TransactionMerkleBranch { export interface BlockHeader { /** * The block version number that indicates which set of block validation rules - * to follow. + * to follow. The field is 4-byte long. */ version: number /** - * The hash of the previous block's header. + * The hash of the previous block's header. The field is 32-byte long. */ previousBlockHeaderHash: Hex /** * The hash derived from the hashes of all transactions included in this block. + * The field is 32-byte long. */ merkleRootHash: Hex /** - * The Unix epoch time when the miner started hashing the header. + * The Unix epoch time when the miner started hashing the header. The field is + * 4-byte long. */ time: number /** * Bits that determine the target threshold this block's header hash must be - * less than or equal to. + * less than or equal to. The field is 4-byte long. */ bits: number /** * An arbitrary number miners change to modify the header hash in order to - * produce a hash less than or equal to the target threshold. + * produce a hash less than or equal to the target threshold. The field is + * 4-byte long. */ nonce: number } From c41eb0627aaced1f9014990e4f3425dc35d49590 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Wed, 8 Mar 2023 15:37:07 +0100 Subject: [PATCH 20/30] Added better description for converting bits into target --- typescript/src/bitcoin.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index c8b871a79..ddfde8787 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -261,6 +261,40 @@ export function deserializeBlockHeader(rawBlockHeader: Hex): BlockHeader { * @returns Target as a BigNumber. */ export function bitsToTarget(bits: number): BigNumber { + // A serialized 80-byte block header stores the `bits` value as a 4-byte + // little-endian hexadecimal value in a slot including bytes 73, 74, 75, and + // 76. This function's input argument is expected to be a numerical + // representation of that 4-byte value reverted to the big-endian order. + // For example, if the `bits` little-endian value in the header is + // `0xcb04041b`, it must be reverted to the big-endian form `0x1b0404cb` and + // turned to a decimal number `453248203` in order to be used as this + // function's input. + // + // The `bits` 4-byte big-endian representation is a compact value that works + // like a base-256 version of scientific notation. It encodes the target + // exponent in the first byte and the target mantissa in the last three bytes. + // Referring to the previous example, if `bits = 453248203`, the hexadecimal + // representation is `0x1b0404cb` so the exponent is `0x1b` while the mantissa + // is `0x0404cb`. + // + // To extract the exponent, we need to shift right by 3 bytes (24 bits), + // extract the last byte of the result, and subtract 3 (because of the + // mantissa length): + // - 0x1b0404cb >>> 24 = 0x0000001b + // - 0x0000001b & 0xff = 0x1b + // - 0x1b - 3 = 24 (decimal) + // + // To extract the mantissa, we just need to take the last three bytes: + // - 0x1b0404cb & 0xffffff = 0x0404cb = 263371 (decimal) + // + // The final difficulty can be computed as mantissa * 256^exponent: + // - 263371 * 256^24 = + // 1653206561150525499452195696179626311675293455763937233695932416 (decimal) + // + // Sources: + // - https://developer.bitcoin.org/reference/block_chain.html#target-nbits + // - https://wiki.bitcoinsv.io/index.php/Target + const exponent = ((bits >>> 24) & 0xff) - 3 const mantissa = bits & 0xffffff From 2b0b653d5d4131cc0c7b60bf2a42396a6af2eb0a Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 9 Mar 2023 11:45:16 +0100 Subject: [PATCH 21/30] Used Hex type when handling hashes --- typescript/src/bitcoin.ts | 10 +++++----- typescript/src/proof.ts | 10 ++++------ typescript/test/bitcoin.test.ts | 3 ++- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/typescript/src/bitcoin.ts b/typescript/src/bitcoin.ts index ddfde8787..98b8e0e27 100644 --- a/typescript/src/bitcoin.ts +++ b/typescript/src/bitcoin.ts @@ -521,9 +521,9 @@ export function computeHash160(text: string): string { * @param text - Text the double SHA256 is computed for. * @returns Hash as a 32-byte un-prefixed hex string. */ -export function computeHash256(text: string): Hex { - const firstHash = sha256.digest(Buffer.from(text, "hex")) - const secondHash = sha256.digest(firstHash) +export function computeHash256(text: Hex): Hex { + const firstHash: Buffer = sha256.digest(text.toBuffer()) + const secondHash: Buffer = sha256.digest(firstHash) return Hex.from(secondHash) } @@ -532,8 +532,8 @@ export function computeHash256(text: string): Hex { * @param hash - Hash in hex-string format. * @returns BigNumber representation of the hash. */ -export function hashLEToBigNumber(hash: string): BigNumber { - return BigNumber.from(Hex.from(hash).reverse().toPrefixedString()) +export function hashLEToBigNumber(hash: Hex): BigNumber { + return BigNumber.from(hash.reverse().toPrefixedString()) } /** diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 46c506b90..b9dc67faa 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -208,11 +208,11 @@ function validateMerkleTreeHashes( for (let i = 0; i < intermediateNodesHashes.length; i += 64) { if (idx % 2 === 1) { current = computeHash256( - intermediateNodesHashes.slice(i, i + 64) + current + Hex.from(intermediateNodesHashes.slice(i, i + 64) + current) ).toString() } else { current = computeHash256( - current + intermediateNodesHashes.slice(i, i + 64) + Hex.from(current + intermediateNodesHashes.slice(i, i + 64)) ).toString() } idx = idx >> 1 @@ -266,13 +266,11 @@ function validateBlockHeadersChain( const difficultyTarget = bitsToTarget(currentHeader.bits) const currentBlockHeaderHash = computeHash256( - serializeBlockHeader(currentHeader).toString() + serializeBlockHeader(currentHeader) ) // Ensure the header has sufficient work. - if ( - hashLEToBigNumber(currentBlockHeaderHash.toString()).gt(difficultyTarget) - ) { + if (hashLEToBigNumber(currentBlockHeaderHash).gt(difficultyTarget)) { throw new Error("Insufficient work in the header") } diff --git a/typescript/test/bitcoin.test.ts b/typescript/test/bitcoin.test.ts index 0cf939286..7321c99c7 100644 --- a/typescript/test/bitcoin.test.ts +++ b/typescript/test/bitcoin.test.ts @@ -419,8 +419,9 @@ describe("Bitcoin", () => { describe("hashLEToBigNumber", () => { it("calculates correct value", () => { - const hash = + const hash = Hex.from( "31552151fbef8e96a33f979e6253d29edf65ac31b04802319e00000000000000" + ) const expectedBigNumber = BigNumber.from( "992983769452983078390935942095592601503357651673709518345521" ) From 57c55f4e3fc5cb0446dafeb566e274134ba9d77c Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 9 Mar 2023 13:09:58 +0100 Subject: [PATCH 22/30] Used replaced string with TransactionHash --- typescript/src/proof.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index b9dc67faa..02eccecb7 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -122,7 +122,7 @@ export async function validateTransactionProof( const merkleRootHash: Hex = bitcoinHeaders[0].merkleRootHash validateMerkleTree( - transactionHash.reverse().toString(), + transactionHash, merkleRootHash.toString(), proof.merkleProof, proof.txIndexInBlock @@ -150,14 +150,14 @@ export async function validateTransactionProof( * @returns An empty return value. */ function validateMerkleTree( - transactionHash: string, + transactionHash: TransactionHash, merkleRootHash: string, intermediateNodeHashes: string, transactionIndex: number ) { // Shortcut the empty-block case if ( - transactionHash == merkleRootHash && + transactionHash.reverse().toString() == merkleRootHash && transactionIndex == 0 && intermediateNodeHashes.length == 0 ) { @@ -189,7 +189,7 @@ function validateMerkleTree( * @returns An empty return value. */ function validateMerkleTreeHashes( - transactionHash: string, + transactionHash: TransactionHash, intermediateNodesHashes: string, merkleRootHash: string, transactionIndex: number @@ -202,23 +202,23 @@ function validateMerkleTreeHashes( } let idx = transactionIndex - let current = transactionHash + let current = transactionHash.reverse() // i moves in increments of 64 for (let i = 0; i < intermediateNodesHashes.length; i += 64) { if (idx % 2 === 1) { current = computeHash256( Hex.from(intermediateNodesHashes.slice(i, i + 64) + current) - ).toString() + ) } else { current = computeHash256( Hex.from(current + intermediateNodesHashes.slice(i, i + 64)) - ).toString() + ) } idx = idx >> 1 } - if (current !== merkleRootHash) { + if (current.toString() !== merkleRootHash) { throw new Error( "Transaction Merkle proof is not valid for provided header and transaction hash" ) From 9fea7d9b2acb15fd1b4974b949c6c3c08ef7bda9 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 9 Mar 2023 13:15:15 +0100 Subject: [PATCH 23/30] Used TransactionHash to represent transaction hash --- typescript/src/proof.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 02eccecb7..75f1d444a 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -123,7 +123,7 @@ export async function validateTransactionProof( validateMerkleTree( transactionHash, - merkleRootHash.toString(), + merkleRootHash, proof.merkleProof, proof.txIndexInBlock ) @@ -151,13 +151,13 @@ export async function validateTransactionProof( */ function validateMerkleTree( transactionHash: TransactionHash, - merkleRootHash: string, + merkleRootHash: Hex, intermediateNodeHashes: string, transactionIndex: number ) { // Shortcut the empty-block case if ( - transactionHash.reverse().toString() == merkleRootHash && + transactionHash.reverse().equals(merkleRootHash) && transactionIndex == 0 && intermediateNodeHashes.length == 0 ) { @@ -191,7 +191,7 @@ function validateMerkleTree( function validateMerkleTreeHashes( transactionHash: TransactionHash, intermediateNodesHashes: string, - merkleRootHash: string, + merkleRootHash: Hex, transactionIndex: number ) { if ( @@ -218,7 +218,7 @@ function validateMerkleTreeHashes( idx = idx >> 1 } - if (current.toString() !== merkleRootHash) { + if (!current.equals(merkleRootHash)) { throw new Error( "Transaction Merkle proof is not valid for provided header and transaction hash" ) From 43cc758b27fb9c0c77ee7cb16d91fdd97fe6cde0 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 9 Mar 2023 13:22:00 +0100 Subject: [PATCH 24/30] Reordered arguments --- typescript/src/proof.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 75f1d444a..6d387c232 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -166,8 +166,8 @@ function validateMerkleTree( validateMerkleTreeHashes( transactionHash, - intermediateNodeHashes, merkleRootHash, + intermediateNodeHashes, transactionIndex ) } @@ -178,10 +178,10 @@ function validateMerkleTree( * hashes to compute the root hash. If the computed root hash does not match the * merkle root hash, an error is thrown. * @param transactionHash The hash of the transaction being validated. - * @param intermediateNodesHashes The Merkle tree intermediate nodes hashes, - * concatenated as a single string. * @param merkleRootHash The Merkle root hash that the intermediate nodes should * compute to. + * @param intermediateNodesHashes The Merkle tree intermediate nodes hashes, + * concatenated as a single string. * @param transactionIndex The index of the transaction in the block, used * to determine the path to traverse in the Merkle tree. * @throws {Error} If the intermediate nodes are of an invalid length or if the @@ -190,8 +190,8 @@ function validateMerkleTree( */ function validateMerkleTreeHashes( transactionHash: TransactionHash, - intermediateNodesHashes: string, merkleRootHash: Hex, + intermediateNodesHashes: string, transactionIndex: number ) { if ( From 7fdc5c0618bb05d9e1a915e6fa27210a494efb36 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 9 Mar 2023 15:11:33 +0100 Subject: [PATCH 25/30] Handle intermediate nodes of Merkle tree individually --- typescript/src/proof.ts | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 6d387c232..6f2bf6c67 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -119,12 +119,14 @@ export async function validateTransactionProof( if (bitcoinHeaders.length != requiredConfirmations) { throw new Error("Wrong number of confirmations") } + const merkleRootHash: Hex = bitcoinHeaders[0].merkleRootHash + const intermediateNodesHashes: Hex[] = splitMerkleProof(proof.merkleProof) validateMerkleTree( transactionHash, merkleRootHash, - proof.merkleProof, + intermediateNodesHashes, proof.txIndexInBlock ) @@ -152,7 +154,7 @@ export async function validateTransactionProof( function validateMerkleTree( transactionHash: TransactionHash, merkleRootHash: Hex, - intermediateNodeHashes: string, + intermediateNodeHashes: Hex[], transactionIndex: number ) { // Shortcut the empty-block case @@ -191,28 +193,24 @@ function validateMerkleTree( function validateMerkleTreeHashes( transactionHash: TransactionHash, merkleRootHash: Hex, - intermediateNodesHashes: string, + intermediateNodesHashes: Hex[], transactionIndex: number ) { - if ( - intermediateNodesHashes.length === 0 || - intermediateNodesHashes.length % 64 !== 0 - ) { + if (intermediateNodesHashes.length === 0) { throw new Error("Invalid merkle tree") } let idx = transactionIndex let current = transactionHash.reverse() - // i moves in increments of 64 - for (let i = 0; i < intermediateNodesHashes.length; i += 64) { + for (let i = 0; i < intermediateNodesHashes.length; i++) { if (idx % 2 === 1) { current = computeHash256( - Hex.from(intermediateNodesHashes.slice(i, i + 64) + current) + Hex.from(intermediateNodesHashes[i].toString() + current.toString()) ) } else { current = computeHash256( - Hex.from(current + intermediateNodesHashes.slice(i, i + 64)) + Hex.from(current.toString() + intermediateNodesHashes[i].toString()) ) } idx = idx >> 1 @@ -310,6 +308,25 @@ function validateBlockHeadersChain( } } +/** + * Splits a given Merkle proof string into an array of intermediate node hashes. + * @param merkleProof A string representation of the Merkle proof. + * @returns An array of intermediate node hashes. + * @throws {Error} If the length of the Merkle proof is not a multiple of 64. + */ +export function splitMerkleProof(merkleProof: string): Hex[] { + if (merkleProof.length % 64 != 0) { + throw new Error("Incorrect length of Merkle proof") + } + + const intermediateNodeHashes: Hex[] = [] + for (let i = 0; i < merkleProof.length; i += 64) { + intermediateNodeHashes.push(Hex.from(merkleProof.slice(i, i + 64))) + } + + return intermediateNodeHashes +} + /** * Splits Bitcoin block headers in the raw format into an array of BlockHeaders. * @param blockHeaders - string that contains block headers in the raw format. From 967d705b56566971adb3a61805b6438c102a2195 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 9 Mar 2023 15:27:17 +0100 Subject: [PATCH 26/30] Updated unit tests --- typescript/test/proof.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/typescript/test/proof.test.ts b/typescript/test/proof.test.ts index af39bbf17..68cef604e 100644 --- a/typescript/test/proof.test.ts +++ b/typescript/test/proof.test.ts @@ -219,6 +219,27 @@ describe("Proof", () => { }, } + await expect( + runProofValidationScenario(corruptedProofData) + ).to.be.rejectedWith("Incorrect length of Merkle proof") + }) + }) + + context("when the merkle proof is empty", () => { + it("should throw", async () => { + // Corrupt the data by making the Merkle proof empty. + const corruptedProofData: TransactionProofData = { + ...transactionConfirmationsInOneEpochData, + bitcoinChainData: { + ...transactionConfirmationsInOneEpochData.bitcoinChainData, + transactionMerkleBranch: { + ...transactionConfirmationsInOneEpochData.bitcoinChainData + .transactionMerkleBranch, + merkle: [], + }, + }, + } + await expect( runProofValidationScenario(corruptedProofData) ).to.be.rejectedWith("Invalid merkle tree") From 3488910e67c7c1147fbf8ee0cb34a2c23bd269b9 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Thu, 9 Mar 2023 16:35:35 +0100 Subject: [PATCH 27/30] Added explanatory comment --- typescript/src/proof.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 6f2bf6c67..35681b28d 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -45,6 +45,11 @@ export async function assembleTransactionProof( const latestBlockHeight = await bitcoinClient.latestBlockHeight() const txBlockHeight = latestBlockHeight - confirmations + 1 + // We subtract `1` from `requiredConfirmations` because the header at + // `txBlockHeight` is already included in the headers chain and is considered + // the first confirmation. So we only need to retrieve `requiredConfirmations - 1` + // subsequent block headers to reach the desired number of confirmations for + // the transaction. const headersChain = await bitcoinClient.getHeadersChain( txBlockHeight, requiredConfirmations - 1 From a1e54032d0e30095c9a329f2f6fb93a55f81cd18 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Fri, 10 Mar 2023 11:57:07 +0100 Subject: [PATCH 28/30] Added explanatory comment --- typescript/src/proof.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 35681b28d..a39affd4e 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -162,7 +162,7 @@ function validateMerkleTree( intermediateNodeHashes: Hex[], transactionIndex: number ) { - // Shortcut the empty-block case + // Shortcut for a block that contains only a single transaction (coinbase). if ( transactionHash.reverse().equals(merkleRootHash) && transactionIndex == 0 && From 414374c55462532682449a72045c2c3bc0879273 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Fri, 10 Mar 2023 12:28:45 +0100 Subject: [PATCH 29/30] Updated docstrings --- typescript/src/proof.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index a39affd4e..4cc4fc074 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -126,12 +126,12 @@ export async function validateTransactionProof( } const merkleRootHash: Hex = bitcoinHeaders[0].merkleRootHash - const intermediateNodesHashes: Hex[] = splitMerkleProof(proof.merkleProof) + const intermediateNodeHashes: Hex[] = splitMerkleProof(proof.merkleProof) validateMerkleTree( transactionHash, merkleRootHash, - intermediateNodesHashes, + intermediateNodeHashes, proof.txIndexInBlock ) @@ -149,8 +149,9 @@ export async function validateTransactionProof( * @param transactionHash The hash of the transaction being validated. * @param merkleRootHash The Merkle root hash that the intermediate node hashes * should compute to. - * @param intermediateNodeHashes The Merkle tree intermediate node hashes, - * concatenated as a single string. + * @param intermediateNodeHashes The Merkle tree intermediate node hashes. + * This is a list of hashes the transaction being validated is paired + * with in the Merkle tree. * @param transactionIndex The index of the transaction being validated within * the block, used to determine the path to traverse in the Merkle tree. * @throws {Error} If the Merkle tree is not valid. @@ -187,8 +188,9 @@ function validateMerkleTree( * @param transactionHash The hash of the transaction being validated. * @param merkleRootHash The Merkle root hash that the intermediate nodes should * compute to. - * @param intermediateNodesHashes The Merkle tree intermediate nodes hashes, - * concatenated as a single string. + * @param intermediateNodeHashes The Merkle tree intermediate node hashes. + * This is a list of hashes the transaction being validated is paired + * with in the Merkle tree. * @param transactionIndex The index of the transaction in the block, used * to determine the path to traverse in the Merkle tree. * @throws {Error} If the intermediate nodes are of an invalid length or if the @@ -198,24 +200,24 @@ function validateMerkleTree( function validateMerkleTreeHashes( transactionHash: TransactionHash, merkleRootHash: Hex, - intermediateNodesHashes: Hex[], + intermediateNodeHashes: Hex[], transactionIndex: number ) { - if (intermediateNodesHashes.length === 0) { + if (intermediateNodeHashes.length === 0) { throw new Error("Invalid merkle tree") } let idx = transactionIndex let current = transactionHash.reverse() - for (let i = 0; i < intermediateNodesHashes.length; i++) { + for (let i = 0; i < intermediateNodeHashes.length; i++) { if (idx % 2 === 1) { current = computeHash256( - Hex.from(intermediateNodesHashes[i].toString() + current.toString()) + Hex.from(intermediateNodeHashes[i].toString() + current.toString()) ) } else { current = computeHash256( - Hex.from(current.toString() + intermediateNodesHashes[i].toString()) + Hex.from(current.toString() + intermediateNodeHashes[i].toString()) ) } idx = idx >> 1 From c673dca748ec6ba23bf54b260b4e7cd083e2a2c5 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon <tomasz.slabon@keep.network> Date: Fri, 10 Mar 2023 13:58:44 +0100 Subject: [PATCH 30/30] Added explanation how transaction is validated in Merkle tree --- typescript/src/proof.ts | 56 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/typescript/src/proof.ts b/typescript/src/proof.ts index 4cc4fc074..f07cf8161 100644 --- a/typescript/src/proof.ts +++ b/typescript/src/proof.ts @@ -32,6 +32,7 @@ export async function assembleTransactionProof( const confirmations = await bitcoinClient.getTransactionConfirmations( transactionHash ) + if (confirmations < requiredConfirmations) { throw new Error( "Transaction confirmations number[" + @@ -203,27 +204,70 @@ function validateMerkleTreeHashes( intermediateNodeHashes: Hex[], transactionIndex: number ) { + // To prove the transaction inclusion in a block we only need the hashes that + // form a path from the transaction being validated to the Merkle root hash. + // If the Merkle tree looks like this: + // + // h_01234567 + // / \ + // h_0123 h_4567 + // / \ / \ + // h_01 h_23 h_45 h_67 + // / \ / \ / \ / \ + // h_0 h_1 h_2 h_3 h_4 h_5 h_6 h_7 + // + // and the transaction hash to be validated is h_5 the following data + // will be used: + // - `transactionHash`: h_5 + // - `merkleRootHash`: h_01234567 + // - `intermediateNodeHashes`: [h_4, h_67, h_0123] + // - `transactionIndex`: 5 + // + // The following calculations will be performed: + // - h_4 and h_5 will be hashed to obtain h_45 + // - h_45 and h_67 will be hashed to obtain h_4567 + // - h_0123 will be hashed with h_4567 to obtain h_1234567 (Merkle root hash). + + // Note that when we move up the Merkle tree calculating parent hashes we need + // to join children hashes. The information which child hash should go first + // is obtained from `transactionIndex`. When `transactionIndex` is odd the + // hash taken from `intermediateNodeHashes` must go first. If it is even the + // hash from previous calculation must go first. The `transactionIndex` is + // divided by `2` after every hash calculation. + if (intermediateNodeHashes.length === 0) { throw new Error("Invalid merkle tree") } let idx = transactionIndex - let current = transactionHash.reverse() + let currentHash = transactionHash.reverse() + // Move up the Merkle tree hashing current hash value with hashes taken + // from `intermediateNodeHashes`. Use `idx` to determine the order of joining + // children hashes. for (let i = 0; i < intermediateNodeHashes.length; i++) { if (idx % 2 === 1) { - current = computeHash256( - Hex.from(intermediateNodeHashes[i].toString() + current.toString()) + // If the current value of idx is odd the hash taken from + // `intermediateNodeHashes` goes before the current hash. + currentHash = computeHash256( + Hex.from(intermediateNodeHashes[i].toString() + currentHash.toString()) ) } else { - current = computeHash256( - Hex.from(current.toString() + intermediateNodeHashes[i].toString()) + // If the current value of idx is even the hash taken from the current + // hash goes before the hash taken from `intermediateNodeHashes`. + currentHash = computeHash256( + Hex.from(currentHash.toString() + intermediateNodeHashes[i].toString()) ) } + + // Divide the value of `idx` by `2` when we move one level up the Merkle + // tree. idx = idx >> 1 } - if (!current.equals(merkleRootHash)) { + // Verify we arrived at the same value of Merkle root hash as the one stored + // in the block header. + if (!currentHash.equals(merkleRootHash)) { throw new Error( "Transaction Merkle proof is not valid for provided header and transaction hash" )