Skip to content
This repository has been archived by the owner on Mar 28, 2023. It is now read-only.

Use Electrs API instead of Electrum #136

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"bcrypto": "git+https://github.com/bcoin-org/bcrypto.git#semver:~5.3.0",
"bufio": "^1.0.6",
"electrum-client-js": "git+https://github.com/keep-network/electrum-client-js.git#v0.1.1",
"node-fetch": "^2.6.1",
"p-wait-for": "^3.1.0",
"web3-utils": "^1.2.8"
},
Expand Down
54 changes: 4 additions & 50 deletions src/BitcoinHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,10 @@ const BitcoinHelpers = {
throw new Error("Electrum client not configured.")
}

const electrumClient = new ElectrumClient(BitcoinHelpers.electrumConfig)
const electrumClient = new ElectrumClient(
BitcoinHelpers.electrumConfig,
BitcoinHelpers.electrsConfig
)

await electrumClient.connect()

Expand Down Expand Up @@ -661,26 +664,6 @@ const BitcoinHelpers = {

return transaction.toRaw().toString("hex")
},
/**
* Finds all transactions containing unspent outputs received
* by the `bitcoinAddress`.
*
* @param {string} bitcoinAddress Bitcoin address to check.
*
* @return {Promise<TransactionInBlock[]>} A promise to an array of
* transactions with accompanying information about the output
* position and value pointed at the specified receiver script.
* Resolves with an empty array if no such transactions exist.
*/
findAllUnspent: async function(bitcoinAddress) {
return await BitcoinHelpers.withElectrumClient(async electrumClient => {
const script = BitcoinHelpers.Address.toScript(bitcoinAddress)
return BitcoinHelpers.Transaction.findAllUnspentWithClient(
electrumClient,
script
)
})
},
/**
* Gets the confirmed balance of the `bitcoinAddress`.
*
Expand Down Expand Up @@ -798,35 +781,6 @@ const BitcoinHelpers = {
})

return transactions.length > 0 ? transactions[0] : null
},
/**
* Finds all transactions to the given `receiverScript` using the
* given `electrumClient`.
*
* @param {ElectrumClient} electrumClient An already-initialized Electrum client.
* @param {string} receiverScript A receiver script.
*
* @return {Promise<TransactionInBlock[]>} A promise to an array of
* transactions with accompanying information about the output
* position and value pointed at the specified receiver script.
* Resolves with an empty array if no such transactions exist.
*/
findAllUnspentWithClient: async function(electrumClient, receiverScript) {
const unspentTransactions = await electrumClient.getUnspentToScript(
receiverScript
)

const result = []

for (const tx of unspentTransactions.reverse()) {
result.push({
transactionID: tx.tx_hash,
outputPosition: tx.tx_pos,
value: tx.value
})
}

return result
}
}
}
Expand Down
43 changes: 0 additions & 43 deletions src/lib/BitcoinSPV.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
// JS implementation of merkle.py script from [summa-tx/bitcoin-spv] repository.
//
// [summa-tx/bitcoin-spv]: https://github.com/summa-tx/bitcoin-spv/
import Hash256 from "bcrypto/lib/hash256-browser.js"
import BcryptoMerkle from "bcrypto/lib/merkle.js"
const { deriveRoot } = BcryptoMerkle

/** @typedef { import("./ElectrumClient.js").default } ElectrumClient */

/**
Expand Down Expand Up @@ -103,43 +99,4 @@ export class BitcoinSPV {

return { proof: proof.toString("hex"), position: merkle.pos }
}

/**
* Verifies merkle proof of transaction inclusion in the block. It expects proof
* as a concatenation of 32-byte values in a hexadecimal form. The proof should
* include the merkle tree branches, with transaction hash merkle tree root omitted.
* @param {string} proofHex hexadecimal representation of the proof
* @param {string} txHash Transaction hash.
* @param {number} index is transaction index in the block (1-indexed)
* @param {number} blockHeight Height of the block where transaction was confirmed.
* @return {Promise<boolean>} true if verification passed, else false
*/
async verifyMerkleProof(proofHex, txHash, index, blockHeight) {
const proof = Buffer.from(proofHex, "hex")

// Retrieve merkle tree root.
const actualRoot = await this.client
.getMerkleRoot(blockHeight)
.catch(err => {
throw new Error(`failed to get merkle root: [${err}]`)
})

// Extract tree branches
const branches = []
for (let i = 0; i < Math.floor(proof.length / 32); i++) {
const branch = proof.slice(i * 32, (i + 1) * 32)
branches.push(branch)
}

// Derive expected root from branches and transaction.
const txHashBuffer = Buffer.from(txHash, "hex").reverse()
const expectedRoot = deriveRoot(Hash256, txHashBuffer, branches, index)

// Validate if calculated root is equal to the one returned from client.
if (actualRoot.equals(expectedRoot)) {
return true
} else {
return false
}
}
}
117 changes: 55 additions & 62 deletions src/lib/ElectrumClient.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fetch from "node-fetch"
import ElectrumClient from "electrum-client-js"
import sha256 from "bcrypto/lib/sha256-browser.js"
const { digest } = sha256
Expand Down Expand Up @@ -63,20 +64,26 @@ export default class Client {
/**
* Initializes Electrum Client instance with provided configuration.
* @param {Config} config Electrum client connection configuration.
* @param {string} apiUrl Url to the electrs server
*/
constructor(config) {
constructor(config, apiUrl) {
// TODO: config will be removed once all ported
this.electrumClient = new ElectrumClient(
config.server,
config.port,
config.protocol,
config.options
)

this.apiUrl = apiUrl
// TODO: Check connectivity here
}

/**
* Establish connection with the server.
*/
async connect() {
// TODO: Remove when done with electrum client
console.log("Connecting to electrum server...")

await this.electrumClient.connect("tbtc", "1.4.2").catch(err => {
Expand All @@ -88,6 +95,7 @@ export default class Client {
* Disconnect from the server.
*/
async close() {
// TODO: Remove when done with electrum client
console.log("Closing connection to electrum server...")
this.electrumClient.close()
}
Expand All @@ -106,17 +114,59 @@ export default class Client {
return header.height
}

/**
* Get details of the transaction.
* @param {string} txHash Hash of a transaction.
* @return {Promise<TransactionData>} Transaction details.
*/
// async getTransaction(txHash) {
// const tx = await this.electrumClient
// .blockchain_transaction_get(txHash, true)
// .catch(err => {
// throw new Error(`failed to get transaction ${txHash}: [${err}]`)
// })

// return tx
// }

/**
* Get details of the transaction.
* @param {string} txHash Hash of a transaction.
* @return {Promise<TransactionData>} Transaction details.
*/
async getTransaction(txHash) {
const tx = await this.electrumClient
.blockchain_transaction_get(txHash, true)
.catch(err => {
throw new Error(`failed to get transaction ${txHash}: [${err}]`)
const getTxUrl = `${this.apiUrl}/tx/${txHash}`
const tx = await fetch(getTxUrl).then(resp => {
if (!resp.ok) {
throw new Error(`failed to get transaction ${txHash} at ${getTxUrl}`)
}
return resp.json()
})

// append hex data to transaction
const getTxRawUrl = `${this.apiUrl}/tx/${txHash}/hex`
tx.hex = await fetch(getTxRawUrl).then(resp => {
if (!resp.ok) {
throw new Error(
`failed to get hex transaction ${txHash} at ${getTxRawUrl}`
)
}
return resp.text()
})

// append confirmations
if (tx.status.confirmed) {
const heightUrl = `${this.apiUrl}/blocks/tip/height`
const height = await fetch(heightUrl).then(resp => {
if (!resp.ok) {
throw new Error(`failed to get blockchain height at ${heightUrl}`)
}
return resp.text()
})
tx.confirmations = parseInt(height) - tx.status.block_height + 1
} else {
tx.confirmations = 0
}

return tx
}
Expand Down Expand Up @@ -150,24 +200,6 @@ export default class Client {
* @property {number} value The value of the unspent output in satoshis.
*/

/**
* Get unspent outputs sent to a script.
* @param {string} script ScriptPubKey in a hexadecimal format.
* @return {Promise<UnspentOutputData[]>} List of unspent outputs. It includes
* transactions in the mempool.
*/
async getUnspentToScript(script) {
const scriptHash = Client.scriptToHash(script)

const listUnspent = await this.electrumClient
.blockchain_scripthash_listunspent(scriptHash)
.catch(err => {
throw new Error(JSON.stringify(err))
})

return listUnspent
}

/**
* Get balance of a script.
*
Expand Down Expand Up @@ -350,21 +382,6 @@ export default class Client {
})
}

/**
* Get merkle root hash for block.
* @param {number} blockHeight Block height.
* @return {Promise<Buffer>} Merkle root hash.
*/
async getMerkleRoot(blockHeight) {
const header = await this.electrumClient
.blockchain_block_header(blockHeight)
.catch(err => {
throw new Error(`failed to get block header: [${err}]`)
})

return Buffer.from(header, "hex").slice(36, 68)
}

/**
* Get concatenated chunk of block headers built on a starting block.
* @param {number} blockHeight Starting block height.
Expand Down Expand Up @@ -412,30 +429,6 @@ export default class Client {
}))
}

/**
* Finds index of output in a transaction for a given address.
* @param {string} txHash Hash of a transaction.
* @param {string} address Bitcoin address for the output.
* @return {Promise<number>} Index of output in the transaction (0-indexed).
*/
async findOutputForAddress(txHash, address) {
const tx = await this.getTransaction(txHash).catch(err => {
throw new Error(`failed to get transaction: [${err}]`)
})

const outputs = tx.vout

for (let index = 0; index < outputs.length; index++) {
for (const a of outputs[index].scriptPubKey.addresses) {
if (a == address) {
return index
}
}
}

throw new Error(`output for address ${address} not found`)
}

/**
* Gets a history of all transactions the script is involved in.
* @param {string} script The script in raw hexadecimal format.
Expand Down
22 changes: 5 additions & 17 deletions test/BitcoinSPVTest.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BitcoinSPV } from "../src/lib/BitcoinSPV.js"
import ElectrumClient from "../src/lib/ElectrumClient.js"
import { electrumConfig } from "./config/network.js"
import { electrsConfig, electrumConfig } from "./config/network.js"
import { readFileSync } from "fs"
import { assert } from "chai"

Expand All @@ -12,7 +12,10 @@ describe("BitcoinSPV", async () => {
before(async () => {
const txData = readFileSync("./test/data/tx.json", "utf8")
tx = JSON.parse(txData)
electrumClient = new ElectrumClient(electrumConfig["testnet"])
electrumClient = new ElectrumClient(
electrumConfig["testnet"],
electrsConfig["testnet"]
)
bitcoinSPV = new BitcoinSPV(electrumClient)
await electrumClient.connect()
})
Expand All @@ -37,21 +40,6 @@ describe("BitcoinSPV", async () => {
assert.deepEqual(result, expectedResult)
})

it("verifyMerkleProof", async () => {
const proofHex = tx.merkleProof
const index = tx.indexInBlock
const txHash = tx.hash
const blockHeight = tx.blockHeight
const result = await bitcoinSPV.verifyMerkleProof(
proofHex,
txHash,
index,
blockHeight
)

assert.isTrue(result)
})

it("getMerkleProofInfo", async () => {
const expectedResult = tx.merkleProof
const expectedPosition = tx.indexInBlock
Expand Down
Loading