/* * Copyright (C) 2019-2023 Six Clovers, Inc. - All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.sixclovers.external; import static org.testng.Assert.assertNotNull; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import org.apache.commons.codec.DecoderException; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Context; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.SegwitAddress; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Transaction.SigHash; import org.bitcoinj.core.TransactionWitness; import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptError; import org.bitcoinj.script.ScriptException; import org.bitcoinj.script.ScriptPattern; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.web3j.utils.Numeric; import com.algorand.algosdk.util.Encoder; import com.sixclovers.integrations.coin.model.CoinSignaturePayload; import com.sixclovers.integrations.model.HDWallet; public class BitcoinExternalSignatureTest { /* Step 1: Provide the referenceToken and signaturePayloads from the transaction response. */ // An opaque token to be maintained with the signature payload. private final String referenceToken = "PLACEHOLDER - replace with data from a TransactionPayload"; // The list of signature payloads that parties must sign. private final List signaturePayloads = Arrays.asList("PLACEHOLDER", "replace with data from a TransactionPayload"); /* Step 2: Provide the private key to sign transactions. */ // The private key (the seed is held by you in a hot wallet to sign transactions). private final String signerPrivateKey = "PLACEHOLDER - replace with a hex encoded seed"; private NetworkParameters networkParameters; private HDWallet wallet; @BeforeClass public void beforeClass() throws DecoderException { networkParameters = TestNet3Params.get(); wallet = new HDWallet(signerPrivateKey); Context.getOrCreate(networkParameters); } @Test public void testBitcoinExternalSignatureTransaction() { /* Step 3: Decode the signaturePayloads given by the transaction request. */ final List decodedTransactions = decodeSignaturePayloads(signaturePayloads); /* Step 4: Verify and sign each transaction. */ final List signedTransactions = signTransactions(decodedTransactions); /* Step 5: Encode the objects so that we may send them to the Commit Transaction endpoint. */ final List encodedTransactions = encodeTransactions(signedTransactions); assertNotNull(referenceToken); assertNotNull(encodedTransactions); /* Step 6: Submit the values to the Commit Transaction endpoint. */ System.out.println("referenceToken: " + encodeToJson(referenceToken)); System.out.println("signaturePayloads: " + encodeToJson(encodedTransactions)); } private byte[] decodeFromBase64(final String value) { return Encoder.decodeFromBase64(value); } private T decodeFromJson(final String value, final Class clazz) { try { return Encoder.decodeFromJson(value, clazz); } catch (final Exception e) { // We never expect this to happen in live code, but to make test failures obvious, we throw a RuntimeException. // Log.error(e); // return null; throw new RuntimeException(e); } } private List decodeSignaturePayloads(final List payloads) { final List decodedTransactions = new LinkedList<>(); for (final String payload : payloads) { decodedTransactions.add(decodeFromJson(new String(decodeFromBase64(payload)), CoinSignaturePayload.class)); } return decodedTransactions; } private String encodeToBase64(final byte[] bytes) { return Encoder.encodeToBase64(bytes); } private String encodeToJson(final Object object) { try { return Encoder.encodeToJson(object); } catch (final Exception e) { // We never expect this to happen in live code, but to make test failures obvious, we throw a RuntimeException. // Log.error(e); // return null; throw new RuntimeException(e); } } private List encodeTransactions(final List payloads) { final List encodedTransactions = new LinkedList<>(); for (final CoinSignaturePayload payload : payloads) { encodedTransactions.add(encodeToBase64(encodeToJson(payload).getBytes())); } return encodedTransactions; } private void signTransaction(final Transaction transaction, final Address source, final ECKey key) { final Script scriptPubKey = ScriptBuilder.createOutputScript(source); for (int i = 0; i < transaction.getInputs().size(); i++) { if (ScriptPattern.isP2PK(scriptPubKey) || ScriptPattern.isP2PKH(scriptPubKey)) { final Sha256Hash hash = transaction.hashForSignature(i, scriptPubKey, SigHash.ALL, false); final TransactionSignature signature = new TransactionSignature(key.sign(hash), SigHash.ALL, false); final Script script = ScriptPattern.isP2PK(scriptPubKey) ? ScriptBuilder.createInputScript(signature) : ScriptBuilder.createInputScript(signature, key); transaction.getInput(i).setScriptSig(script); transaction.getInput(i).setWitness(null); } else if (ScriptPattern.isP2WPKH(scriptPubKey)) { final Script script = ScriptBuilder.createP2PKHOutputScript(key); final Coin value = transaction.getInput(i).getValue(); final Sha256Hash hash = transaction.hashForWitnessSignature(i, script, value, SigHash.ALL, false); final TransactionSignature signature = new TransactionSignature(key.sign(hash), SigHash.ALL, false); transaction.getInput(i).setScriptSig(ScriptBuilder.createEmpty()); transaction.getInput(i).setWitness(TransactionWitness.redeemP2WPKH(signature, key)); } else { throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Unable to sign this scrptPubKey: " + scriptPubKey); } } } private List signTransactions(final List payloads) { final List signedTransactions = new LinkedList<>(); final SegwitAddress segwitAddress = SegwitAddress.fromKey(networkParameters, wallet.getECKeyFromPrivate()); final Address source = Address.fromString(networkParameters, segwitAddress.toBech32()); for (final CoinSignaturePayload payload : payloads) { if (payload.getFrom().equals(source.toString())) { // Sign the transaction. final Transaction transaction = new Transaction(networkParameters, Numeric.hexStringToByteArray(payload.getTransaction())); signTransaction(transaction, source, wallet.getECKeyFromPrivate()); payload.setTransaction(Numeric.toHexStringNoPrefix(transaction.bitcoinSerialize())); // Store the signed transaction. signedTransactions.add(payload); } else { // Store the same transaction unsigned. It belongs to the same transaction group and will be signed by someone else. signedTransactions.add(payload); } } return signedTransactions; } }