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"
     )