diff --git a/src/Bitcoin/SigningInput.cpp b/src/Bitcoin/SigningInput.cpp index 3367652dd4a..bd7c86f7378 100644 --- a/src/Bitcoin/SigningInput.cpp +++ b/src/Bitcoin/SigningInput.cpp @@ -33,6 +33,9 @@ SigningInput::SigningInput(const Proto::SigningInput& input) { plan = TransactionPlan(input.plan()); } outputOpReturn = data(input.output_op_return()); + if (input.has_output_op_return_index()) { + outputOpReturnIndex = input.output_op_return_index().index(); + } lockTime = input.lock_time(); time = input.time(); diff --git a/src/Bitcoin/SigningInput.h b/src/Bitcoin/SigningInput.h index a2b40419323..86ced0f1122 100644 --- a/src/Bitcoin/SigningInput.h +++ b/src/Bitcoin/SigningInput.h @@ -64,6 +64,10 @@ class SigningInput { Data outputOpReturn; + // Optional index of the OP_RETURN output in the transaction. + // If not set, OP_RETURN output will be pushed as the latest output. + MaybeIndex outputOpReturnIndex; + uint32_t lockTime = 0; uint32_t time = 0; diff --git a/src/Bitcoin/TransactionBuilder.cpp b/src/Bitcoin/TransactionBuilder.cpp index b343b2bd924..beb0a7d682f 100644 --- a/src/Bitcoin/TransactionBuilder.cpp +++ b/src/Bitcoin/TransactionBuilder.cpp @@ -85,6 +85,7 @@ TransactionPlan TransactionBuilder::plan(const SigningInput& input) { if (input.outputOpReturn.size() > 0) { plan.outputOpReturn = input.outputOpReturn; } + plan.outputOpReturnIndex = input.outputOpReturnIndex; bool maxAmount = input.useMaxAmount; Amount totalAmount = input.amount + input.extraOutputsAmount; diff --git a/src/Bitcoin/TransactionBuilder.h b/src/Bitcoin/TransactionBuilder.h index 06e876c2e22..667fbe190c7 100644 --- a/src/Bitcoin/TransactionBuilder.h +++ b/src/Bitcoin/TransactionBuilder.h @@ -48,15 +48,22 @@ class TransactionBuilder { } // Optional OP_RETURN output - if (plan.outputOpReturn.size() > 0) { + if (!plan.outputOpReturn.empty()) { auto lockingScriptOpReturn = Script::buildOpReturnScript(plan.outputOpReturn); - if (lockingScriptOpReturn.bytes.size() == 0) { + if (lockingScriptOpReturn.bytes.empty()) { return Result::failure(Common::Proto::Error_invalid_memo); } - tx.outputs.emplace_back(0, lockingScriptOpReturn); + + auto emplace_at = tx.outputs.end(); + if (plan.outputOpReturnIndex.has_value()) { + emplace_at = tx.outputs.begin(); + std::advance(emplace_at, plan.outputOpReturnIndex.value()); + } + int64_t amount = 0; + tx.outputs.emplace(emplace_at, amount, lockingScriptOpReturn); } - // extra outputs + // extra outputs (always in the end of the outputs list) for (auto& o : input.extraOutputs) { auto output = prepareOutputWithScript(o.first, o.second, input.coinType); if (!output.has_value()) { diff --git a/src/Bitcoin/TransactionPlan.h b/src/Bitcoin/TransactionPlan.h index ddfc2dc4810..5124a301ab1 100644 --- a/src/Bitcoin/TransactionPlan.h +++ b/src/Bitcoin/TransactionPlan.h @@ -9,8 +9,12 @@ #include "Data.h" #include "../proto/Bitcoin.pb.h" +#include + namespace TW::Bitcoin { +using MaybeIndex = std::optional; + /// Describes a preliminary transaction plan. struct TransactionPlan { /// Amount to be received at the other end. @@ -39,6 +43,10 @@ struct TransactionPlan { Data outputOpReturn; + // Optional index of the OP_RETURN output in the transaction. + // If not set, OP_RETURN output will be pushed as the latest output. + MaybeIndex outputOpReturnIndex; + Common::Proto::SigningError error = Common::Proto::SigningError::OK; TransactionPlan() = default; @@ -54,7 +62,11 @@ struct TransactionPlan { , preBlockHeight(plan.preblockheight()) , outputOpReturn(plan.output_op_return().begin(), plan.output_op_return().end()) , error(plan.error()) - {} + { + if (plan.has_output_op_return_index()) { + outputOpReturnIndex = plan.output_op_return_index().index(); + } + } Proto::TransactionPlan proto() const { auto plan = Proto::TransactionPlan(); @@ -69,6 +81,9 @@ struct TransactionPlan { plan.set_preblockhash(preBlockHash.data(), preBlockHash.size()); plan.set_preblockheight(preBlockHeight); plan.set_output_op_return(outputOpReturn.data(), outputOpReturn.size()); + if (outputOpReturnIndex.has_value()) { + plan.mutable_output_op_return_index()->set_index(static_cast(outputOpReturnIndex.value())); + } plan.set_error(error); return plan; } diff --git a/src/Zen/TransactionBuilder.h b/src/Zen/TransactionBuilder.h index b3a657f2e6c..7abd9d51cec 100644 --- a/src/Zen/TransactionBuilder.h +++ b/src/Zen/TransactionBuilder.h @@ -55,15 +55,22 @@ struct TransactionBuilder { } // Optional OP_RETURN output - if (plan.outputOpReturn.size() > 0) { + if (!plan.outputOpReturn.empty()) { auto lockingScriptOpReturn = Bitcoin::Script::buildOpReturnScript(plan.outputOpReturn); - if (lockingScriptOpReturn.bytes.size() == 0) { + if (lockingScriptOpReturn.bytes.empty()) { return Result::failure(Common::Proto::Error_invalid_memo); } - tx.outputs.push_back(Bitcoin::TransactionOutput(0, lockingScriptOpReturn)); + + auto emplace_at = tx.outputs.end(); + if (plan.outputOpReturnIndex.has_value()) { + emplace_at = tx.outputs.begin(); + std::advance(emplace_at, plan.outputOpReturnIndex.value()); + } + const int64_t amount = 0; + tx.outputs.emplace(emplace_at, amount, lockingScriptOpReturn); } - // extra outputs + // extra outputs (always in the end of the outputs list) for (auto& o : input.extraOutputs) { auto output = prepareOutputWithScript(o.first, o.second, input.coinType, blockHash, blockHeight); if (!output.has_value()) { @@ -71,7 +78,7 @@ struct TransactionBuilder { } tx.outputs.push_back(output.value()); } - + return Result(tx); } diff --git a/src/proto/Bitcoin.proto b/src/proto/Bitcoin.proto index 0add08247a7..7e525a0a880 100644 --- a/src/proto/Bitcoin.proto +++ b/src/proto/Bitcoin.proto @@ -95,6 +95,11 @@ message OutputAddress { int64 amount = 2; } +// Optional index of a corresponding output in the transaction. +message OutputIndex { + uint32 index = 1; +} + // Input data necessary to create a signed transaction. message SigningInput { // Hash type to use when signing. @@ -142,6 +147,10 @@ message SigningInput { // Optional zero-amount, OP_RETURN output bytes output_op_return = 13; + // Optional index of the OP_RETURN output in the transaction. + // If not set, OP_RETURN output will be pushed as the latest output. + OutputIndex output_op_return_index = 26; + // Optional additional destination addresses, additional to first to_address output repeated OutputAddress extra_outputs = 14; @@ -199,6 +208,10 @@ message TransactionPlan { // Optional zero-amount, OP_RETURN output bytes output_op_return = 8; + // Optional index of the OP_RETURN output in the transaction. + // If not set, OP_RETURN output will be pushed as the latest output. + OutputIndex output_op_return_index = 14; + // zen & bitcoin diamond preblockhash bytes preblockhash = 9; diff --git a/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp b/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp index d3e64144553..fa2fb84e5b2 100644 --- a/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp +++ b/tests/chains/Bitcoin/TWBitcoinSigningTests.cpp @@ -885,6 +885,82 @@ TEST(BitcoinSigning, SignPlanTransactionNotSufficientAfterDustFiltering) { EXPECT_EQ(output.error(), Common::Proto::Error_not_enough_utxos); } +// Deposit 0.0001 BTC from bc1q2sphzvc2uqmxqte2w9dd4gzy4sy9vvfv0me9ke to 0xa8491D40d4F71A752cA41DA0516AEd80c33a1B56 on ZETA mainnet. +// https://www.zetachain.com/docs/developers/omnichain/bitcoin/#example-1-deposit-btc-into-an-account-in-zevm +TEST(BitcoinSigning, SignDepositBtcToZetaChain) { + const auto myPrivateKey = PrivateKey(parse_hex("428d66be0b5a620f126a00fa67637222ce3dc9badfe5c605189520760810cfac")); + auto myPublicKey = myPrivateKey.getPublicKey(TWPublicKeyTypeSECP256k1); + auto utxoPubkeyHash = Hash::ripemd(Hash::sha256(myPublicKey.bytes)); + auto redeemScript = Script::buildPayToWitnessPublicKeyHash(utxoPubkeyHash); + + const auto ownAddress = "bc1q2sphzvc2uqmxqte2w9dd4gzy4sy9vvfv0me9ke"; + const auto ownZetaEvmAddress = parse_hex("a8491D40d4F71A752cA41DA0516AEd80c33a1B56"); + // https://www.zetachain.com/docs/reference/glossary/#tss + const auto zetaTssAddress = "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y"; + + auto utxoHash0 = + parse_hex("17a6adb5db1e33c87467a58aa31cddbb3800052315015cf3cf1c2b0119310e20"); + std::reverse(utxoHash0.begin(), utxoHash0.end()); + auto utxoAmount0 = 20000; + auto utxoOutputIndex0 = 0; + + const auto sendAmount = 10000; + const auto dustAmount = 546; + + Proto::SigningInput signingInput; + signingInput.set_coin_type(TWCoinTypeBitcoin); + signingInput.set_hash_type(TWBitcoinSigHashTypeAll); + signingInput.set_byte_fee(15); + signingInput.set_amount(sendAmount); + signingInput.set_to_address(zetaTssAddress); + signingInput.set_change_address(ownAddress); + signingInput.set_fixed_dust_threshold(dustAmount); + signingInput.set_output_op_return(ownZetaEvmAddress.data(), ownZetaEvmAddress.size()); + // OP_RETURN must be the second output before the change. + signingInput.mutable_output_op_return_index()->set_index(1); + + signingInput.add_private_key(myPrivateKey.bytes.data(), myPrivateKey.bytes.size()); + + // Add UTXO 0 + auto utxo0 = signingInput.add_utxo(); + utxo0->set_script(redeemScript.bytes.data(), redeemScript.bytes.size()); + utxo0->set_amount(utxoAmount0); + utxo0->mutable_out_point()->set_hash( + std::string(utxoHash0.begin(), utxoHash0.end())); + utxo0->mutable_out_point()->set_index(utxoOutputIndex0); + utxo0->mutable_out_point()->set_sequence(UINT32_MAX); + + Proto::SigningOutput output; + ANY_SIGN(signingInput, TWCoinTypeBitcoin); + EXPECT_EQ(output.error(), Common::Proto::SigningError::OK); + + EXPECT_EQ(output.transaction().outputs_size(), 3); + // P2WPKH to the TSS address. + EXPECT_EQ(output.transaction().outputs(0).value(), sendAmount); + // OP_RETURN + EXPECT_EQ(output.transaction().outputs(1).value(), 0); + // Transaction fee. + EXPECT_EQ(output.transaction().outputs(2).value(), 7420); + + // Successfully broadcasted: + // https://mempool.space/tx/2b871b6c1112ad0a777f6db1f7a7709154c4d9af8e771ba4eca148915f830e9d + // https://explorer.zetachain.com/cc/tx/0x269e319478f8849247abb28b33a7b8e0a849dab4551bab328bf58bf67b02a807 + const auto expectedTx = "01000000000101200e3119012b1ccff35c011523050038bbdd1ca38aa56774c8331edbb5ada6170000000000ffffffff031027000000000000160014daaae0d3de9d8fdee31661e61aea828b59be78640000000000000000166a14a8491d40d4f71a752ca41da0516aed80c33a1b56fc1c000000000000160014540371330ae036602f2a715adaa044ac0856312c02483045022100e29731f7474f9103c6df3434c8c62a540a21ad0e10e23df343b1e81e4b26110602202d37fb4fee5341a41f9e4e65ba2d3e0d2309425ea9806d94eb268efe6f21007001210369cdaf80b4a5fdad91e9face90e848225512884ec2e3ed572ca11dc68e75054700000000"; + + EXPECT_EQ(hex(output.encoded()), expectedTx); + + Proto::TransactionPlan plan; + ANY_PLAN(signingInput, plan, TWCoinTypeBitcoin); + EXPECT_EQ(plan.error(), Common::Proto::SigningError::OK); + EXPECT_TRUE(plan.has_output_op_return_index()); + EXPECT_EQ(plan.output_op_return_index().index(), 1); + + *signingInput.mutable_plan() = plan; + ANY_SIGN(signingInput, TWCoinTypeBitcoin); + // The result has to be the same as signing without transaction planning. + EXPECT_EQ(hex(output.encoded()), expectedTx); +} + TEST(BitcoinSigning, SignP2PKH) { auto input = buildInputP2PKH(); diff --git a/tests/chains/Bitcoin/TransactionPlanTests.cpp b/tests/chains/Bitcoin/TransactionPlanTests.cpp index 5a4d6597f74..357d4784979 100644 --- a/tests/chains/Bitcoin/TransactionPlanTests.cpp +++ b/tests/chains/Bitcoin/TransactionPlanTests.cpp @@ -715,6 +715,7 @@ TEST(TransactionPlan, OpReturn) { EXPECT_TRUE(verifyPlan(txPlan, {342101}, 300000, 205 * byteFee)); EXPECT_EQ(txPlan.outputOpReturn.size(), 59ul); EXPECT_EQ(hex(txPlan.outputOpReturn), "535741503a54484f522e52554e453a74686f72317470657263616d6b6b7865633071306a6b366c74646e6c7176737732396775617038776d636c3a"); + EXPECT_FALSE(txPlan.outputOpReturnIndex.has_value()); auto& feeCalculator = getFeeCalculator(TWCoinTypeBitcoin); EXPECT_EQ(feeCalculator.calculate(1, 2, byteFee), 174 * byteFee);