Created
April 2, 2024 18:47
-
-
Save nkavian/f3586e1f19aa59486dd5f18bc025df7f to your computer and use it in GitHub Desktop.
Revisions
-
nkavian created this gist
Apr 2, 2024 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,181 @@ /* * 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<String> 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<CoinSignaturePayload> decodedTransactions = decodeSignaturePayloads(signaturePayloads); /* Step 4: Verify and sign each transaction. */ final List<CoinSignaturePayload> signedTransactions = signTransactions(decodedTransactions); /* Step 5: Encode the objects so that we may send them to the Commit Transaction endpoint. */ final List<String> 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> T decodeFromJson(final String value, final Class<T> 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<CoinSignaturePayload> decodeSignaturePayloads(final List<String> payloads) { final List<CoinSignaturePayload> 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<String> encodeTransactions(final List<CoinSignaturePayload> payloads) { final List<String> 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<CoinSignaturePayload> signTransactions(final List<CoinSignaturePayload> payloads) { final List<CoinSignaturePayload> 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; } } This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,252 @@ /* * 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 java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Field; import java.util.List; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Message; import org.bitcoinj.core.MessageSerializer; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.ProtocolException; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Transaction.Purpose; import org.bitcoinj.core.TransactionConfidence.Source; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.TransactionWitness; import org.bitcoinj.core.UnsafeByteArrayOutputStream; import org.bitcoinj.core.Utils; import org.bitcoinj.core.VarInt; import org.web3j.utils.Numeric; import com.sixclovers.utility.Log; public class CoinSerializer { private static class TransactionInput extends org.bitcoinj.core.TransactionInput { public TransactionInput( final NetworkParameters params, final Transaction parentTransaction, final byte[] payload, final int offset, final MessageSerializer serializer) throws ProtocolException { super(params, parentTransaction, payload, offset, serializer); } public void setValue(final Coin value) { try { final Field valueField = getClass().getSuperclass().getDeclaredField("value"); valueField.setAccessible(true); valueField.set(this, value); } catch (final NoSuchFieldException | IllegalAccessException e) { Log.error(e); } } } private static final int MESSAGE_LENGTH = 36; public static Transaction decode(final NetworkParameters networkParameters, final String encoded) { try { final Transaction transaction = new Transaction(networkParameters); final byte[] payload = Numeric.hexStringToByteArray(encoded); int cursor = 0; // version transaction.setVersion((int)Utils.readUint32(payload, cursor)); cursor += 4; final VarInt segwitVarInt = readVarInt(payload, cursor, 0); final boolean useSegwit = segwitVarInt.intValue() == 0; if (useSegwit) { cursor += 2; } // txin_count, txins, txin_values cursor = parseInputs(transaction, networkParameters, payload, cursor); // txout_count, txouts cursor = parseOutputs(transaction, networkParameters, payload, cursor); // script_witnisses if (useSegwit) { cursor = parseWitness(transaction, payload, cursor); } // lock_time transaction.setLockTime((int)Utils.readUint32(payload, cursor)); cursor += 4; // source transaction.getConfidence().setSource(Source.values()[(int)Utils.readUint32(payload, cursor)]); cursor += 4; // purpose transaction.setPurpose(Purpose.values()[(int)Utils.readUint32(payload, cursor)]); cursor += 4; return transaction; } catch (final Exception e) { Log.error(e); return null; } } public static String encode(final Transaction transaction) { final boolean useSegwit = transaction.hasWitnesses(); final int length = transaction.getMessageSize(); final ByteArrayOutputStream stream = new UnsafeByteArrayOutputStream(length < 32 ? 32 : length + 32); try { // version Utils.uint32ToByteStreamLE(transaction.getVersion(), stream); if (useSegwit) { stream.write(0); stream.write(1); } // txin_count, txins, txin_values final List<org.bitcoinj.core.TransactionInput> inputs = transaction.getInputs(); stream.write(new VarInt(inputs.size()).encode()); for (final org.bitcoinj.core.TransactionInput in : inputs) { in.bitcoinSerialize(stream); Utils.int64ToByteStreamLE(in.getValue().getValue(), stream); } // txout_count, txouts final List<TransactionOutput> outputs = transaction.getOutputs(); stream.write(new VarInt(outputs.size()).encode()); for (final TransactionOutput out : outputs) { out.bitcoinSerialize(stream); } // script_witnisses if (useSegwit) { for (final org.bitcoinj.core.TransactionInput in : inputs) { final int pushCount = in.getWitness().getPushCount(); stream.write(new VarInt(pushCount).encode()); for (int i = 0; i < pushCount; i++) { final byte[] push = in.getWitness().getPush(i); stream.write(new VarInt(push.length).encode()); stream.write(push); } } } // lock_time Utils.uint32ToByteStreamLE(transaction.getLockTime(), stream); // source final int source = transaction.getConfidence().getSource().ordinal(); Utils.uint32ToByteStreamLE(source, stream); // purpose final int purpose = transaction.getPurpose().ordinal(); Utils.uint32ToByteStreamLE(purpose, stream); return Numeric.toHexStringNoPrefix(stream.toByteArray()); } catch (final IOException e) { // Cannot happen, we are serializing to a memory stream. } return null; } private static int parseInputs(final Transaction transaction, final NetworkParameters networkParameters, final byte[] payload, int cursor) { final VarInt numInputsVarInt = readVarInt(payload, cursor, 0); cursor += numInputsVarInt.getOriginalSizeInBytes(); final int numInputs = numInputsVarInt.intValue(); for (long i = 0; i < numInputs; i++) { final TransactionInput input = new TransactionInput(networkParameters, transaction, payload, cursor, networkParameters.getDefaultSerializer()); final VarInt scriptLenVarInt = readVarInt(payload, cursor, MESSAGE_LENGTH); cursor += MESSAGE_LENGTH + numInputsVarInt.getOriginalSizeInBytes(); final int scriptLen = scriptLenVarInt.intValue(); cursor += scriptLen + 4; final long value = Utils.readInt64(payload, cursor); input.setValue(Coin.ofSat(value)); transaction.addInput(input); cursor += 8; } return cursor; } private static int parseOutputs(final Transaction transaction, final NetworkParameters networkParameters, final byte[] payload, int cursor) { final VarInt numOutputsVarInt = readVarInt(payload, cursor, 0); cursor += numOutputsVarInt.getOriginalSizeInBytes(); final int numOutputs = numOutputsVarInt.intValue(); for (long i = 0; i < numOutputs; i++) { transaction.addOutput(new TransactionOutput(networkParameters, transaction, payload, cursor, networkParameters.getDefaultSerializer())); final VarInt scriptLenVarInt = readVarInt(payload, cursor, 8); cursor += 8 + scriptLenVarInt.getOriginalSizeInBytes(); final int scriptLen = scriptLenVarInt.intValue(); cursor += scriptLen; } return cursor; } private static int parseWitness(final Transaction transaction, final byte[] payload, int cursor) { final int numWitnesses = transaction.getInputs().size(); for (int i = 0; i < numWitnesses; i++) { final VarInt pushCountVarInt = readVarInt(payload, cursor, 0); cursor += pushCountVarInt.getOriginalSizeInBytes(); final int pushCount = pushCountVarInt.intValue(); final TransactionWitness witness = new TransactionWitness(pushCount); transaction.getInput(i).setWitness(witness); for (int y = 0; y < pushCount; y++) { final VarInt pushSizeVarInt = readVarInt(payload, cursor, 0); cursor += pushSizeVarInt.getOriginalSizeInBytes(); final int pushSize = pushSizeVarInt.intValue(); final byte[] push = readBytes(payload, cursor, pushSize); cursor += pushSize; witness.setPush(y, push); } } return cursor; } private static byte[] readBytes(final byte[] payload, final int cursor, final int length) throws ProtocolException { if ((length > Message.MAX_SIZE) || (cursor + length > payload.length)) { throw new ProtocolException("Claimed value length too large: " + length); } try { final byte[] b = new byte[length]; System.arraycopy(payload, cursor, b, 0, length); return b; } catch (final IndexOutOfBoundsException e) { throw new ProtocolException(e); } } private static VarInt readVarInt(final byte[] payload, final int cursor, final int offset) throws ProtocolException { try { return new VarInt(payload, cursor + offset); } catch (final ArrayIndexOutOfBoundsException e) { throw new ProtocolException(e); } } } This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,68 @@ /* * 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 java.math.BigInteger; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; import org.bitcoinj.core.ECKey; import org.web3j.crypto.Bip32ECKeyPair; import org.web3j.crypto.Credentials; import org.web3j.crypto.ECKeyPair; import org.web3j.crypto.Keys; import lombok.Data; @Data public class HDWallet { private final String address; private final Credentials credentials; private final ECKeyPair keyPair; private final String seed; public HDWallet() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException { this(Keys.createEcKeyPair()); } public HDWallet(final String seed) throws DecoderException { this(ECKeyPair.create(new BigInteger(Hex.decodeHex(seed)))); } private HDWallet(final ECKeyPair keyPair) { this.keyPair = keyPair; final Credentials credentials = Credentials.create(keyPair); address = credentials.getAddress(); this.credentials = credentials; seed = Hex.encodeHexString(keyPair.getPrivateKey().toByteArray()); } public HDWallet generateChild(final int... path) { return keyPair instanceof Bip32ECKeyPair ? new HDWallet(Bip32ECKeyPair.deriveKeyPair((Bip32ECKeyPair)keyPair, path)) : null; } public HDWallet generateMaster() throws DecoderException { return new HDWallet(Bip32ECKeyPair.generateKeyPair(Hex.decodeHex(seed))); } public ECKey getECKeyFromPrivate() { return ECKey.fromPrivate(keyPair.getPrivateKey()); } } This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,19 @@ # Bitcoin External Signature Test This gist contains code that can be added to your project and run within a Java 8+ environmnet. The code has 3rd party dependencies named below required to compile the sample code: - Apache Commons Codec - Bitcoinj - Bouncy Castle - Jackson - Encode / Decode JSON - Web3J - HDWallet support In `BitcoinExternalSignatureTest` it defines steps that can also be considered as psuedo code in case you want to build the logic from scratch. These steps are intended to be executed in an automated fashion while securely storing sensitive data: - Step 1: Provide the referenceToken and signaturePayloads from the API transaction response. - Step 2: Provide the private key to sign transactions. - Step 3: Decode the signaturePayloads given by the transaction request. - Step 4: Verify and sign each transaction. - Step 5: Encode the objects so that we may send them to the Commit Transaction endpoint. - Step 6: Submit the values to the Commit Transaction endpoint.