Created
May 27, 2021 21:56
-
-
Save christroutner/1e2cee8259cec7e62443e3ebf3b3766a to your computer and use it in GitHub Desktop.
Double Spend
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Double spend 1000 satoshis. | |
This script requires access to two different instances of bch-api, using two | |
different BCHN full nodes. A single full node will prevent you from broadcasting | |
an intentional double spend. | |
*/ | |
// Set NETWORK to either testnet or mainnet | |
const NETWORK = "mainnet"; | |
const ADDR1 = "bitcoincash:qr867ds8wap86de2wgjsm2x6hlqef485fsdhe0lx40"; | |
const ADDR2 = "bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d"; | |
// set satoshi amount to send | |
const SATOSHIS_TO_SEND = 1000; | |
// REST API servers. | |
const BCHN_MAINNET = "https://bchn.fullstack.cash/v4/"; | |
// const ABC_MAINNET = 'https://abc.fullstack.cash/v4/' | |
const TESTNET3 = "https://testnet3.fullstack.cash/v4/"; | |
// bch-js-examples require code from the main bch-js repo | |
const BCHJS = require("@psf/bch-js"); | |
// Instantiate bch-js based on the network. | |
let bchjs; | |
if (NETWORK === "mainnet") bchjs = new BCHJS({ restURL: BCHN_MAINNET }); | |
else bchjs = new BCHJS({ restURL: TESTNET3 }); | |
// Open the wallet generated with create-wallet. | |
try { | |
var walletInfo = require("./wallet.json"); | |
} catch (err) { | |
console.log( | |
"Could not open wallet.json. Generate a wallet with create-wallet first." | |
); | |
process.exit(0); | |
} | |
const SEND_ADDR = walletInfo.cashAddress; | |
const SEND_MNEMONIC = walletInfo.mnemonic; | |
async function sendBch() { | |
try { | |
// Get the balance of the sending address. | |
const balance = await getBCHBalance(SEND_ADDR, false); | |
console.log(`balance: ${JSON.stringify(balance, null, 2)}`); | |
console.log(`Balance of sending address ${SEND_ADDR} is ${balance} BCH.`); | |
// Exit if the balance is zero. | |
if (balance <= 0.0) { | |
console.log("Balance of sending address is zero. Exiting."); | |
process.exit(0); | |
} | |
// Convert to a legacy address (needed to build transactions). | |
const SEND_ADDR_LEGACY = bchjs.Address.toLegacyAddress(SEND_ADDR); | |
console.log(`Sender Legacy Address: ${SEND_ADDR_LEGACY}`); | |
// Get UTXOs held by the address. | |
// https://developer.bitcoin.com/mastering-bitcoin-cash/4-transactions/ | |
const utxos = await bchjs.Electrumx.utxo(SEND_ADDR); | |
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`); | |
if (utxos.utxos.length === 0) throw new Error("No UTXOs found."); | |
// console.log(`u: ${JSON.stringify(u, null, 2)}` | |
const utxo = await findBiggestUtxo(utxos.utxos); | |
console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`); | |
// Essential variables of a transaction. | |
const satoshisToSend = SATOSHIS_TO_SEND; | |
const originalAmount = utxo.value; | |
const vout = utxo.tx_pos; | |
const txid = utxo.tx_hash; | |
const txObj = { | |
originalAmount, | |
vout, | |
txid | |
}; | |
const hex1 = await getTx1(txObj); | |
console.log(`hex1: `, hex1); | |
const hex2 = await getTx2(txObj); | |
// Broadcast transation to the network | |
const txid1 = await bchjs.RawTransactions.sendRawTransaction([hex1]); | |
console.log(`txid1: ${txid1}`); | |
// connect to my local node. | |
const bchjs2 = new BCHJS({ restURL: "http://192.168.0.36:3000/v4/" }); | |
const txid2 = await bchjs2.RawTransactions.sendRawTransaction([hex2]); | |
console.log(`txid2: ${txid2}`); | |
// // import from util.js file | |
// const util = require('../util.js') | |
// console.log(`Transaction ID: ${txidStr}`) | |
// console.log('Check the status of your transaction on this block explorer:') | |
// util.transactionStatus(txidStr, NETWORK) | |
} catch (err) { | |
console.log("error: ", err); | |
} | |
} | |
sendBch(); | |
// Generate the first transaction for a double spend. | |
async function getTx1(txObj) { | |
try { | |
const { originalAmount, vout, txid } = txObj; | |
// instance of transaction builder | |
const transactionBuilder = new bchjs.TransactionBuilder(); | |
// add input with txid and index of vout | |
transactionBuilder.addInput(txid, vout); | |
// get byte count to calculate fee. paying 1.2 sat/byte | |
const byteCount = bchjs.BitcoinCash.getByteCount( | |
{ P2PKH: 1 }, | |
{ P2PKH: 2 } | |
); | |
console.log(`Transaction byte count: ${byteCount}`); | |
const satoshisPerByte = 1.2; | |
const txFee = Math.floor(satoshisPerByte * byteCount); | |
console.log(`Transaction fee: ${txFee}`); | |
// amount to send back to the sending address. | |
// It's the original amount - 1 sat/byte for tx size | |
const remainder = originalAmount - SATOSHIS_TO_SEND - txFee; | |
if (remainder < 0) { | |
throw new Error("Not enough BCH to complete transaction!"); | |
} | |
// add output w/ address and amount to send | |
transactionBuilder.addOutput(ADDR1, SATOSHIS_TO_SEND); | |
transactionBuilder.addOutput(SEND_ADDR, remainder); | |
// Generate a change address from a Mnemonic of a private key. | |
const change = await changeAddrFromMnemonic(SEND_MNEMONIC); | |
// Generate a keypair from the change address. | |
const keyPair = bchjs.HDNode.toKeyPair(change); | |
// Sign the transaction with the HD node. | |
let redeemScript; | |
transactionBuilder.sign( | |
0, | |
keyPair, | |
redeemScript, | |
transactionBuilder.hashTypes.SIGHASH_ALL, | |
originalAmount | |
); | |
// build tx | |
const tx = transactionBuilder.build(); | |
// output rawhex | |
const hex = tx.toHex(); | |
// console.log(`TX hex: ${hex}`); | |
// console.log(" "); | |
return hex; | |
} catch (err) { | |
console.error("Error in getTx1()"); | |
throw err; | |
} | |
} | |
// Generate the second transaction for a double spend. | |
async function getTx2(txObj) { | |
try { | |
const { originalAmount, vout, txid } = txObj; | |
// instance of transaction builder | |
const transactionBuilder = new bchjs.TransactionBuilder(); | |
// add input with txid and index of vout | |
transactionBuilder.addInput(txid, vout); | |
// get byte count to calculate fee. paying 1.2 sat/byte | |
const byteCount = bchjs.BitcoinCash.getByteCount( | |
{ P2PKH: 1 }, | |
{ P2PKH: 2 } | |
); | |
console.log(`Transaction byte count: ${byteCount}`); | |
const satoshisPerByte = 1.2; | |
const txFee = Math.floor(satoshisPerByte * byteCount); | |
console.log(`Transaction fee: ${txFee}`); | |
// amount to send back to the sending address. | |
// It's the original amount - 1 sat/byte for tx size | |
const remainder = originalAmount - SATOSHIS_TO_SEND - txFee; | |
if (remainder < 0) { | |
throw new Error("Not enough BCH to complete transaction!"); | |
} | |
// add output w/ address and amount to send | |
transactionBuilder.addOutput(ADDR2, SATOSHIS_TO_SEND); | |
transactionBuilder.addOutput(SEND_ADDR, remainder); | |
// Generate a change address from a Mnemonic of a private key. | |
const change = await changeAddrFromMnemonic(SEND_MNEMONIC); | |
// Generate a keypair from the change address. | |
const keyPair = bchjs.HDNode.toKeyPair(change); | |
// Sign the transaction with the HD node. | |
let redeemScript; | |
transactionBuilder.sign( | |
0, | |
keyPair, | |
redeemScript, | |
transactionBuilder.hashTypes.SIGHASH_ALL, | |
originalAmount | |
); | |
// build tx | |
const tx = transactionBuilder.build(); | |
// output rawhex | |
const hex = tx.toHex(); | |
// console.log(`TX hex: ${hex}`); | |
// console.log(" "); | |
return hex; | |
} catch (err) { | |
console.error("Error in getTx2()"); | |
throw err; | |
} | |
} | |
// Generate a change address from a Mnemonic of a private key. | |
async function changeAddrFromMnemonic(mnemonic) { | |
// root seed buffer | |
const rootSeed = await bchjs.Mnemonic.toSeed(mnemonic); | |
// master HDNode | |
let masterHDNode; | |
if (NETWORK === "mainnet") masterHDNode = bchjs.HDNode.fromSeed(rootSeed); | |
else masterHDNode = bchjs.HDNode.fromSeed(rootSeed, "testnet"); | |
// HDNode of BIP44 account | |
const account = bchjs.HDNode.derivePath(masterHDNode, "m/44'/145'/0'"); | |
// derive the first external change address HDNode which is going to spend utxo | |
const change = bchjs.HDNode.derivePath(account, "0/0"); | |
return change; | |
} | |
// Get the balance in BCH of a BCH address. | |
async function getBCHBalance(addr, verbose) { | |
try { | |
const result = await bchjs.Electrumx.balance(addr); | |
if (verbose) console.log(result); | |
// The total balance is the sum of the confirmed and unconfirmed balances. | |
const satBalance = | |
Number(result.balance.confirmed) + Number(result.balance.unconfirmed); | |
// Convert the satoshi balance to a BCH balance | |
const bchBalance = bchjs.BitcoinCash.toBitcoinCash(satBalance); | |
return bchBalance; | |
} catch (err) { | |
console.error("Error in getBCHBalance: ", err); | |
console.log(`addr: ${addr}`); | |
throw err; | |
} | |
} | |
// Returns the utxo with the biggest balance from an array of utxos. | |
async function findBiggestUtxo(utxos) { | |
let largestAmount = 0; | |
let largestIndex = 0; | |
for (var i = 0; i < utxos.length; i++) { | |
const thisUtxo = utxos[i]; | |
// console.log(`thisUTXO: ${JSON.stringify(thisUtxo, null, 2)}`); | |
// Validate the UTXO data with the full node. | |
const txout = await bchjs.Blockchain.getTxOut( | |
thisUtxo.tx_hash, | |
thisUtxo.tx_pos | |
); | |
if (txout === null) { | |
// If the UTXO has already been spent, the full node will respond with null. | |
console.log( | |
"Stale UTXO found. You may need to wait for the indexer to catch up." | |
); | |
continue; | |
} | |
if (thisUtxo.value > largestAmount) { | |
largestAmount = thisUtxo.value; | |
largestIndex = i; | |
} | |
} | |
return utxos[largestIndex]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment