Skip to content

Commit

Permalink
[ZetaChain]: Deposit BTC and call a smart contract in zEVM (#3739)
Browse files Browse the repository at this point in the history
* feat(zetachain): Add an optional `output_op_return_index` to `SigningInput` and `Plan`

* feat(zetachain): Add a unit test
  • Loading branch information
satoshiotomakan authored Mar 15, 2024
1 parent b6cb1be commit b92d1de
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 10 deletions.
3 changes: 3 additions & 0 deletions src/Bitcoin/SigningInput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
4 changes: 4 additions & 0 deletions src/Bitcoin/SigningInput.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions src/Bitcoin/TransactionBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 11 additions & 4 deletions src/Bitcoin/TransactionBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Transaction, Common::Proto::SigningError>::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()) {
Expand Down
17 changes: 16 additions & 1 deletion src/Bitcoin/TransactionPlan.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
#include "Data.h"
#include "../proto/Bitcoin.pb.h"

#include <optional>

namespace TW::Bitcoin {

using MaybeIndex = std::optional<std::size_t>;

/// Describes a preliminary transaction plan.
struct TransactionPlan {
/// Amount to be received at the other end.
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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<uint32_t>(outputOpReturnIndex.value()));
}
plan.set_error(error);
return plan;
}
Expand Down
17 changes: 12 additions & 5 deletions src/Zen/TransactionBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,30 @@ 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<Transaction, Common::Proto::SigningError>::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()) {
return Result<Transaction, Common::Proto::SigningError>::failure(Common::Proto::Error_invalid_address);
}
tx.outputs.push_back(output.value());
}

return Result<Transaction, Common::Proto::SigningError>(tx);
}

Expand Down
13 changes: 13 additions & 0 deletions src/proto/Bitcoin.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down
76 changes: 76 additions & 0 deletions tests/chains/Bitcoin/TWBitcoinSigningTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
1 change: 1 addition & 0 deletions tests/chains/Bitcoin/TransactionPlanTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit b92d1de

Please sign in to comment.