package org.consenlabs.tokencore.wallet.transaction; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.DumpedPrivateKey; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.UnsafeByteArrayOutputStream; import org.bitcoinj.core.Utils; import org.bitcoinj.core.VarInt; import org.bitcoinj.crypto.ChildNumber; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.HDKeyDerivation; import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.consenlabs.tokencore.foundation.crypto.Hash; import org.consenlabs.tokencore.foundation.utils.ByteUtil; import org.consenlabs.tokencore.foundation.utils.NumericUtil; import org.consenlabs.tokencore.wallet.Wallet; import org.consenlabs.tokencore.wallet.address.SegWitBitcoinAddressCreator; import org.consenlabs.tokencore.wallet.model.Messages; import org.consenlabs.tokencore.wallet.model.Metadata; import org.consenlabs.tokencore.wallet.model.TokenException; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class BitcoinTransaction implements TransactionSigner { private String to; private long amount; private List<UTXO> outputs; private String memo; private long fee; private int changeIdx; private long locktime = 0; private Address changeAddress; private NetworkParameters network; private List<BigInteger> prvKeys; // 2730 sat private final static long DUST_THRESHOLD = 2730; public BitcoinTransaction(String to, int changeIdx, long amount, long fee, ArrayList<UTXO> outputs) { this.to = to; this.amount = amount; this.fee = fee; this.outputs = outputs; this.changeIdx = changeIdx; if (amount < DUST_THRESHOLD) { throw new TokenException(Messages.AMOUNT_LESS_THAN_MINIMUM); } } @Override public String toString() { return "BitcoinTransaction{" + "to='" + to + '\'' + ", amount=" + amount + ", outputs=" + outputs + ", memo='" + memo + '\'' + ", fee=" + fee + ", changeIdx=" + changeIdx + '}'; } public String getTo() { return to; } public void setTo(String to) { this.to = to; } public long getAmount() { return amount; } public void setAmount(long amount) { this.amount = amount; } public List<UTXO> getOutputs() { return outputs; } public void setOutputs(List<UTXO> outputs) { this.outputs = outputs; } public String getMemo() { return memo; } public void setMemo(String memo) { this.memo = memo; } public long getFee() { return fee; } public void setFee(long fee) { this.fee = fee; } public int getChangeIdx() { return changeIdx; } public void setChangeIdx(int changeIdx) { this.changeIdx = changeIdx; } public static class UTXO { private String txHash; private int vout; private long amount; private String address; private String scriptPubKey; private String derivedPath; private long sequence = 4294967295L; @Override public String toString() { return "UTXO{" + "txHash='" + txHash + '\'' + ", vout=" + vout + ", amount=" + amount + ", address='" + address + '\'' + ", scriptPubKey='" + scriptPubKey + '\'' + ", derivedPath='" + derivedPath + '\'' + ", sequence=" + sequence + '}'; } public UTXO(String txHash, int vout, long amount, String address, String scriptPubKey, String derivedPath) { this.txHash = txHash; this.vout = vout; this.amount = amount; this.address = address; this.scriptPubKey = scriptPubKey; this.derivedPath = derivedPath; } public UTXO(String txHash, int vout, long amount, String address, String scriptPubKey, String derivedPath, long sequence) { this.txHash = txHash; this.vout = vout; this.amount = amount; this.address = address; this.scriptPubKey = scriptPubKey; this.derivedPath = derivedPath; this.sequence = sequence; } public int getVout() { return vout; } public void setVout(int vout) { this.vout = vout; } public long getAmount() { return amount; } public void setAmount(long amount) { this.amount = amount; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getTxHash() { return txHash; } public void setTxHash(String txHash) { this.txHash = txHash; } public String getScriptPubKey() { return scriptPubKey; } public void setScriptPubKey(String scriptPubKey) { this.scriptPubKey = scriptPubKey; } public String getDerivedPath() { return derivedPath; } public void setDerivedPath(String derivedPath) { this.derivedPath = derivedPath; } public long getSequence() { return sequence; } public void setSequence(long sequence) { this.sequence = sequence; } } public TxSignResult signTransaction(String chainID, String password, Wallet wallet) { collectPrvKeysAndAddress(Metadata.NONE, password, wallet); Transaction tran = new Transaction(network); long totalAmount = 0L; for (UTXO output : getOutputs()) { totalAmount += output.getAmount(); } if (totalAmount < getAmount()) { throw new TokenException(Messages.INSUFFICIENT_FUNDS); } //add send to output tran.addOutput(Coin.valueOf(getAmount()), Address.fromBase58(network, getTo())); //add change output long changeAmount = totalAmount - (getAmount() + getFee()); if (changeAmount >= DUST_THRESHOLD) { tran.addOutput(Coin.valueOf(changeAmount), changeAddress); } for (UTXO output : getOutputs()) { tran.addInput(Sha256Hash.wrap(output.getTxHash()), output.getVout(), new Script(NumericUtil.hexToBytes(output.getScriptPubKey()))); } for (int i = 0; i < getOutputs().size(); i++) { UTXO output = getOutputs().get(i); BigInteger privateKey = wallet.getMetadata().getSource().equals(Metadata.FROM_WIF) ? prvKeys.get(0) : prvKeys.get(i); ECKey ecKey; if (output.getAddress().equals(ECKey.fromPrivate(privateKey).toAddress(network).toBase58())) { ecKey = ECKey.fromPrivate(privateKey); } else if (output.getAddress().equals(ECKey.fromPrivate(privateKey, false).toAddress(network).toBase58())) { ecKey = ECKey.fromPrivate(privateKey, false); } else { throw new TokenException(Messages.CAN_NOT_FOUND_PRIVATE_KEY); } TransactionInput transactionInput = tran.getInput(i); Script scriptPubKey = ScriptBuilder.createOutputScript(Address.fromBase58(network, output.getAddress())); Sha256Hash hash = tran.hashForSignature(i, scriptPubKey, Transaction.SigHash.ALL, false); ECKey.ECDSASignature ecSig = ecKey.sign(hash); TransactionSignature txSig = new TransactionSignature(ecSig, Transaction.SigHash.ALL, false); if (scriptPubKey.isSentToRawPubKey()) { transactionInput.setScriptSig(ScriptBuilder.createInputScript(txSig)); } else { if (!scriptPubKey.isSentToAddress()) { throw new TokenException(Messages.UNSUPPORT_SEND_TARGET); } transactionInput.setScriptSig(ScriptBuilder.createInputScript(txSig, ecKey)); } } String signedHex = NumericUtil.bytesToHex(tran.bitcoinSerialize()); String txHash = NumericUtil.beBigEndianHex(Hash.sha256(Hash.sha256(signedHex))); return new TxSignResult(signedHex, txHash); } public TxSignResult signSegWitTransaction(String chainId, String password, Wallet wallet) { collectPrvKeysAndAddress(Metadata.P2WPKH, password, wallet); long totalAmount = 0L; boolean hasChange = false; for (UTXO output : getOutputs()) { totalAmount += output.getAmount(); } if (totalAmount < getAmount()) { throw new TokenException(Messages.INSUFFICIENT_FUNDS); } long changeAmount = totalAmount - (getAmount() + getFee()); Address toAddress = Address.fromBase58(network, to); byte[] targetScriptPubKey; if (toAddress.isP2SHAddress()) { targetScriptPubKey = ScriptBuilder.createP2SHOutputScript(toAddress.getHash160()).getProgram(); } else { targetScriptPubKey = ScriptBuilder.createOutputScript(toAddress).getProgram(); } byte[] changeScriptPubKey = ScriptBuilder.createP2SHOutputScript(changeAddress.getHash160()).getProgram(); byte[] hashPrevouts; byte[] hashOutputs; byte[] hashSequence; try { // calc hash prevouts UnsafeByteArrayOutputStream stream = new UnsafeByteArrayOutputStream(); for (UTXO utxo : getOutputs()) { TransactionOutPoint outPoint = new TransactionOutPoint(this.network, utxo.vout, Sha256Hash.wrap(utxo.txHash)); outPoint.bitcoinSerialize(stream); } hashPrevouts = Sha256Hash.hashTwice(stream.toByteArray()); // calc hash outputs stream = new UnsafeByteArrayOutputStream(); TransactionOutput targetOutput = new TransactionOutput(this.network, null, Coin.valueOf(amount), toAddress); targetOutput.bitcoinSerialize(stream); if (changeAmount >= DUST_THRESHOLD) { hasChange = true; TransactionOutput changeOutput = new TransactionOutput(this.network, null, Coin.valueOf(changeAmount), changeAddress); changeOutput.bitcoinSerialize(stream); } // // Utils.uint64ToByteStreamLE(BigInteger.valueOf(amount), stream); // stream.write(new VarInt(targetScriptPubKey.length).encode()); // stream.write(targetScriptPubKey); // Utils.uint64ToByteStreamLE(BigInteger.valueOf(changeAmount), stream); // stream.write(new VarInt(changeScriptPubKey.length).encode()); // stream.write(changeScriptPubKey); hashOutputs = Sha256Hash.hashTwice(stream.toByteArray()); // calc hash sequence stream = new UnsafeByteArrayOutputStream(); for (UTXO utxo : getOutputs()) { Utils.uint32ToByteStreamLE(utxo.getSequence(), stream); } hashSequence = Sha256Hash.hashTwice(stream.toByteArray()); // calc witnesses and redemScripts List<byte[]> witnesses = new ArrayList<>(); List<String> redeemScripts = new ArrayList<>(); for (int i = 0; i < getOutputs().size(); i++) { UTXO utxo = getOutputs().get(i); BigInteger prvKey = Metadata.FROM_WIF.equals(wallet.getMetadata().getSource()) ? prvKeys.get(0) : prvKeys.get(i); ECKey key = ECKey.fromPrivate(prvKey, true); String redeemScript = String.format("0014%s", NumericUtil.bytesToHex(key.getPubKeyHash())); redeemScripts.add(redeemScript); // calc outpoint stream = new UnsafeByteArrayOutputStream(); TransactionOutPoint txOutPoint = new TransactionOutPoint(this.network, utxo.vout, Sha256Hash.wrap(utxo.txHash)); txOutPoint.bitcoinSerialize(stream); byte[] outpoint = stream.toByteArray(); // calc scriptCode byte[] scriptCode = NumericUtil.hexToBytes(String.format("0x1976a914%s88ac", NumericUtil.bytesToHex(key.getPubKeyHash()))); // before sign stream = new UnsafeByteArrayOutputStream(); Utils.uint32ToByteStreamLE(2L, stream); stream.write(hashPrevouts); stream.write(hashSequence); stream.write(outpoint); stream.write(scriptCode); Utils.uint64ToByteStreamLE(BigInteger.valueOf(utxo.getAmount()), stream); Utils.uint32ToByteStreamLE(utxo.getSequence(), stream); stream.write(hashOutputs); Utils.uint32ToByteStreamLE(locktime, stream); // hashType 1 = all Utils.uint32ToByteStreamLE(1L, stream); byte[] hashPreimage = stream.toByteArray(); byte[] sigHash = Sha256Hash.hashTwice(hashPreimage); ECKey.ECDSASignature signature = key.sign(Sha256Hash.wrap(sigHash)); byte hashType = 0x01; // witnesses byte[] sig = ByteUtil.concat(signature.encodeToDER(), new byte[]{hashType}); witnesses.add(sig); } // the second stream is used to calc the traditional txhash UnsafeByteArrayOutputStream[] serialStreams = new UnsafeByteArrayOutputStream[]{ new UnsafeByteArrayOutputStream(), new UnsafeByteArrayOutputStream() }; for (int idx = 0; idx < 2; idx++) { stream = serialStreams[idx]; Utils.uint32ToByteStreamLE(2L, stream); // version if (idx == 0) { stream.write(0x00); // maker stream.write(0x01); // flag } // inputs stream.write(new VarInt(getOutputs().size()).encode()); for (int i = 0; i < getOutputs().size(); i++) { UTXO utxo = getOutputs().get(i); stream.write(NumericUtil.reverseBytes(NumericUtil.hexToBytes(utxo.txHash))); Utils.uint32ToByteStreamLE(utxo.getVout(), stream); // the length of byte array that follows, and this length is used by OP_PUSHDATA1 stream.write(0x17); // the length of byte array that follows, and this length is used by cutting array stream.write(0x16); stream.write(NumericUtil.hexToBytes(redeemScripts.get(i))); Utils.uint32ToByteStreamLE(utxo.getSequence(), stream); } // outputs // outputs size int outputSize = hasChange ? 2 : 1; stream.write(new VarInt(outputSize).encode()); Utils.uint64ToByteStreamLE(BigInteger.valueOf(amount), stream); stream.write(new VarInt(targetScriptPubKey.length).encode()); stream.write(targetScriptPubKey); if (hasChange) { Utils.uint64ToByteStreamLE(BigInteger.valueOf(changeAmount), stream); stream.write(new VarInt(changeScriptPubKey.length).encode()); stream.write(changeScriptPubKey); } // the first stream is used to calc the segwit hash if (idx == 0) { for (int i = 0; i < witnesses.size(); i++) { BigInteger prvKey = Metadata.FROM_WIF.equals(wallet.getMetadata().getSource()) ? prvKeys.get(0) : prvKeys.get(i); ECKey ecKey = ECKey.fromPrivate(prvKey); byte[] wit = witnesses.get(i); stream.write(new VarInt(2).encode()); stream.write(new VarInt(wit.length).encode()); stream.write(wit); stream.write(new VarInt(ecKey.getPubKey().length).encode()); stream.write(ecKey.getPubKey()); } } Utils.uint32ToByteStreamLE(locktime, stream); } byte[] signed = serialStreams[0].toByteArray(); String signedHex = NumericUtil.bytesToHex(signed); String wtxID = NumericUtil.bytesToHex(Sha256Hash.hashTwice(signed)); wtxID = NumericUtil.beBigEndianHex(wtxID); String txHash = NumericUtil.bytesToHex(Sha256Hash.hashTwice(serialStreams[1].toByteArray())); txHash = NumericUtil.beBigEndianHex(txHash); return new TxSignResult(signedHex, txHash, wtxID); } catch (IOException ex) { throw new TokenException("OutputStream error"); } } private void collectPrvKeysAndAddress(String segWit, String password, Wallet wallet) { this.network = wallet.getMetadata().isMainNet() ? MainNetParams.get() : TestNet3Params.get(); if (wallet.getMetadata().getSource().equals(Metadata.FROM_WIF)) { changeAddress = Address.fromBase58(network, wallet.getAddress()); BigInteger prvKey = DumpedPrivateKey.fromBase58(network, wallet.exportPrivateKey(password)).getKey().getPrivKey(); prvKeys = Collections.singletonList(prvKey); } else { prvKeys = new ArrayList<>(getOutputs().size()); String xprv = new String(wallet.decryptMainKey(password), Charset.forName("UTF-8")); DeterministicKey xprvKey = DeterministicKey.deserializeB58(xprv, network); DeterministicKey changeKey = HDKeyDerivation.deriveChildKey(xprvKey, ChildNumber.ONE); DeterministicKey indexKey = HDKeyDerivation.deriveChildKey(changeKey, new ChildNumber(getChangeIdx(), false)); if (Metadata.P2WPKH.equals(segWit)) { changeAddress = new SegWitBitcoinAddressCreator(network).fromPrivateKey(indexKey); } else { changeAddress = indexKey.toAddress(network); } for (UTXO output : getOutputs()) { String derivedPath = output.getDerivedPath().trim(); String[] pathIdxs = derivedPath.replace('/', ' ').split(" "); int accountIdx = Integer.parseInt(pathIdxs[0]); int changeIdx = Integer.parseInt(pathIdxs[1]); DeterministicKey accountKey = HDKeyDerivation.deriveChildKey(xprvKey, new ChildNumber(accountIdx, false)); DeterministicKey externalChangeKey = HDKeyDerivation.deriveChildKey(accountKey, new ChildNumber(changeIdx, false)); prvKeys.add(externalChangeKey.getPrivKey()); } } } }