Skip to content

Instantly share code, notes, and snippets.

@christroutner
Created May 27, 2021 21:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save christroutner/1e2cee8259cec7e62443e3ebf3b3766a to your computer and use it in GitHub Desktop.
Save christroutner/1e2cee8259cec7e62443e3ebf3b3766a to your computer and use it in GitHub Desktop.
Double Spend
/*
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