diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/groestlcoin/TestGroestlcoinSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/groestlcoin/TestGroestlcoinSigner.kt new file mode 100644 index 00000000000..64cb6bba4f5 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/groestlcoin/TestGroestlcoinSigner.kt @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.groestlcoin + +import com.google.protobuf.ByteString +import com.trustwallet.core.app.utils.Numeric +import org.junit.Assert.assertEquals +import org.junit.Test + +import wallet.core.java.AnySigner +import wallet.core.jni.BitcoinSigHashType +import wallet.core.jni.CoinType.GROESTLCOIN +import wallet.core.jni.proto.Common.SigningError +import wallet.core.jni.proto.Bitcoin +import wallet.core.jni.proto.Bitcoin.SigningOutput +import wallet.core.jni.proto.BitcoinV2 +import wallet.core.jni.proto.Utxo + +class TestGroestlcoinSigner { + + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testSignV2P2PKH() { + // Successfully broadcasted: https://explorer.zcha.in/transactions/ec9033381c1cc53ada837ef9981c03ead1c7c41700ff3a954389cfaddc949256 + val privateKeyData = Numeric.hexStringToByteArray("dc334e7347f2f9f72fce789b11832bdf78adf0158bc6617e6d2d2a530a0d4bc6") + val dustSatoshis = 546.toLong() + val senderAddress = "grs1qw4teyraux2s77nhjdwh9ar8rl9dt7zww8r6lne" + val toAddress = "31inaRqambLsd9D7Ke4USZmGEVd3PHkh7P" + val changeAddress = "Fj62rBJi8LvbmWu2jzkaUX1NFXLEqDLoZM" + + val txid0 = Numeric.hexStringToByteArray("8f4ecc7844e19aa1d3183e47eee89d795f9e7c875a55ec0203946d6c9eb06895").reversedArray() + val utxo0 = BitcoinV2.Input.newBuilder() + .setOutPoint(Utxo.OutPoint.newBuilder().apply { + hash = ByteString.copyFrom(txid0) + vout = 1 + }) + .setValue(4774) + .setSighashType(BitcoinSigHashType.ALL.value()) + .setReceiverAddress(senderAddress) + + val out0 = BitcoinV2.Output.newBuilder() + .setValue(2500) + .setToAddress(toAddress) + + val changeOut = BitcoinV2.Output.newBuilder() + .setValue(2048) + .setToAddress(changeAddress) + + val builder = BitcoinV2.TransactionBuilder.newBuilder() + .setVersion(BitcoinV2.TransactionVersion.UseDefault) + .addInputs(utxo0) + .addOutputs(out0) + .addOutputs(changeOut) + .setInputSelector(BitcoinV2.InputSelector.UseAll) + .setFixedDustThreshold(dustSatoshis) + val signingInput = BitcoinV2.SigningInput.newBuilder() + .setBuilder(builder) + .addPrivateKeys(ByteString.copyFrom(privateKeyData)) + .setChainInfo(BitcoinV2.ChainInfo.newBuilder().apply { + p2PkhPrefix = 36 + p2ShPrefix = 5 + }) + .build() + + val legacySigningInput = Bitcoin.SigningInput.newBuilder().apply { + signingV2 = signingInput + coinType = GROESTLCOIN.value() + } + + val output = AnySigner.sign(legacySigningInput.build(), GROESTLCOIN, SigningOutput.parser()) + + assertEquals(output.error, SigningError.OK) + assertEquals(output.signingResultV2.errorMessage, "") + assertEquals(output.signingResultV2.error, SigningError.OK) + assertEquals(Numeric.toHexString(output.signingResultV2.encoded.toByteArray()), "0x010000000001019568b09e6c6d940302ec555a877c9e5f799de8ee473e18d3a19ae14478cc4e8f0100000000ffffffff02c40900000000000017a9140055b0c94df477ee6b9f75185dfc9aa8ce2e52e48700080000000000001976a91498af0aaca388a7e1024f505c033626d908e3b54a88ac024830450221009bbd0228dcb7343828633ded99d216555d587b74db40c4a46f560187eca222dd022032364cf6dbf9c0213076beb6b4a20935d4e9c827a551c3f6f8cbb22d8b464467012102e9c9b9b76e982ad8fa9a7f48470eafbeeba9bf6d287579318c517db5157d936e00000000") + assertEquals(Numeric.toHexString(output.signingResultV2.txid.toByteArray()), "0x40b539c578934c9863a93c966e278fbeb3e67b0da4eb9e3030092c1b717e7a64") + } +} diff --git a/rust/Cargo.lock b/rust/Cargo.lock index de309cba3aa..aa28248a982 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1880,6 +1880,7 @@ dependencies = [ "tw_ethereum", "tw_evm", "tw_greenfield", + "tw_groestlcoin", "tw_hash", "tw_internet_computer", "tw_keypair", @@ -1996,6 +1997,21 @@ dependencies = [ "tw_proto", ] +[[package]] +name = "tw_groestlcoin" +version = "0.1.0" +dependencies = [ + "tw_base58_address", + "tw_bitcoin", + "tw_coin_entry", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_memory", + "tw_proto", + "tw_utxo", +] + [[package]] name = "tw_hash" version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 435a6f26615..e23a466ecf4 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -8,6 +8,7 @@ members = [ "chains/tw_cosmos", "chains/tw_ethereum", "chains/tw_greenfield", + "chains/tw_groestlcoin", "chains/tw_internet_computer", "chains/tw_komodo", "chains/tw_native_evmos", diff --git a/rust/chains/tw_bitcoin/src/modules/compiler.rs b/rust/chains/tw_bitcoin/src/modules/compiler.rs index a2ce77d7176..c12ed068257 100644 --- a/rust/chains/tw_bitcoin/src/modules/compiler.rs +++ b/rust/chains/tw_bitcoin/src/modules/compiler.rs @@ -121,7 +121,7 @@ impl BitcoinCompiler { Ok(Proto::SigningOutput { transaction: Context::ProtobufBuilder::tx_to_proto(&signed_tx), encoded: Cow::from(signed_tx.encode_out()), - txid: Cow::from(signed_tx.txid()), + txid: Cow::from(signed_tx.txid(Context::TX_HASHER)), // `vsize` could have been changed after the transaction being signed. vsize: signed_tx.vsize() as u64, weight: signed_tx.weight() as u64, @@ -146,7 +146,7 @@ impl BitcoinCompiler { Ok(Proto::SigningOutput { transaction: Context::ProtobufBuilder::tx_to_proto(&signed_tx), encoded: Cow::from(signed_tx.encode_out()), - txid: Cow::from(signed_tx.txid()), + txid: Cow::from(signed_tx.txid(Context::TX_HASHER)), // `vsize` could have been changed after the transaction being signed. vsize: signed_tx.vsize() as u64, weight: signed_tx.weight() as u64, diff --git a/rust/chains/tw_bitcoin/src/modules/signer.rs b/rust/chains/tw_bitcoin/src/modules/signer.rs index db95ceab71e..1a3a7dd7bdc 100644 --- a/rust/chains/tw_bitcoin/src/modules/signer.rs +++ b/rust/chains/tw_bitcoin/src/modules/signer.rs @@ -70,7 +70,7 @@ impl BitcoinSigner { Ok(Proto::SigningOutput { transaction: Context::ProtobufBuilder::tx_to_proto(&signed_tx), encoded: Cow::from(signed_tx.encode_out()), - txid: Cow::from(signed_tx.txid()), + txid: Cow::from(signed_tx.txid(Context::TX_HASHER)), // `vsize` could have been changed after the transaction being signed. vsize: signed_tx.vsize() as u64, // `fee` should haven't been changed since it's a difference between `sum(inputs)` and `sum(outputs)`. @@ -107,7 +107,7 @@ impl BitcoinSigner { Ok(Proto::SigningOutput { transaction: Context::ProtobufBuilder::tx_to_proto(&signed_tx), encoded: Cow::from(signed_tx.encode_out()), - txid: Cow::from(signed_tx.txid()), + txid: Cow::from(signed_tx.txid(Context::TX_HASHER)), // `vsize` could have been changed after the transaction being signed. vsize: signed_tx.vsize() as u64, fee, diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs index 7158d67f9a2..3879cef4ee9 100644 --- a/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/output_protobuf.rs @@ -7,7 +7,7 @@ use crate::modules::tx_builder::BitcoinChainInfo; use std::marker::PhantomData; use std::str::FromStr; use tw_coin_entry::error::prelude::*; -use tw_hash::hasher::sha256_ripemd; +use tw_hash::ripemd::sha256_ripemd; use tw_hash::sha2::sha256; use tw_hash::{Hash, H256}; use tw_keypair::{ecdsa, schnorr}; diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/public_keys.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/public_keys.rs index 6c822c686b6..94f344fc868 100644 --- a/rust/chains/tw_bitcoin/src/modules/tx_builder/public_keys.rs +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/public_keys.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use tw_coin_entry::error::prelude::*; -use tw_hash::hasher::sha256_ripemd; +use tw_hash::ripemd::sha256_ripemd; use tw_hash::H160; use tw_keypair::ecdsa; use tw_memory::Data; diff --git a/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs b/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs index d3fe32a7e2d..b853e8eb04b 100644 --- a/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs +++ b/rust/chains/tw_bitcoin/src/modules/tx_builder/utxo_protobuf.rs @@ -211,7 +211,8 @@ impl<'a, Context: UtxoContext> UtxoProtobuf<'a, Context> { .prev_index(index) .sequence(sequence) .amount(self.input.value) - .sighash_type(sighash_ty)) + .sighash_type(sighash_ty) + .tx_hasher(Context::TX_HASHER)) } pub fn sighash_ty(&self) -> SigningResult { diff --git a/rust/chains/tw_bitcoincash/src/cash_address/mod.rs b/rust/chains/tw_bitcoincash/src/cash_address/mod.rs index f060724fc40..96bc91e0815 100644 --- a/rust/chains/tw_bitcoincash/src/cash_address/mod.rs +++ b/rust/chains/tw_bitcoincash/src/cash_address/mod.rs @@ -8,7 +8,7 @@ use tw_coin_entry::coin_context::CoinContext; use tw_coin_entry::coin_entry::CoinAddress; use tw_coin_entry::error::prelude::*; use tw_encoding::bech32; -use tw_hash::hasher::sha256_ripemd; +use tw_hash::ripemd::sha256_ripemd; use tw_hash::H160; use tw_keypair::ecdsa; use tw_memory::Data; diff --git a/rust/chains/tw_groestlcoin/Cargo.toml b/rust/chains/tw_groestlcoin/Cargo.toml new file mode 100644 index 00000000000..6b6c8538e66 --- /dev/null +++ b/rust/chains/tw_groestlcoin/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tw_groestlcoin" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_base58_address = { path = "../../tw_base58_address" } +tw_bitcoin = { path = "../../chains/tw_bitcoin" } +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } +tw_utxo = { path = "../../frameworks/tw_utxo" } diff --git a/rust/chains/tw_groestlcoin/src/address/groestlcoin_legacy.rs b/rust/chains/tw_groestlcoin/src/address/groestlcoin_legacy.rs new file mode 100644 index 00000000000..31d53cabf52 --- /dev/null +++ b/rust/chains/tw_groestlcoin/src/address/groestlcoin_legacy.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_hash::groestl::Groestl512d; +use tw_hash::ripemd::Sha256Ripemd; +use tw_utxo::address::legacy::PrefixedBase58Address; + +pub type GroestlLegacyAddress = PrefixedBase58Address; diff --git a/rust/chains/tw_groestlcoin/src/address/mod.rs b/rust/chains/tw_groestlcoin/src/address/mod.rs new file mode 100644 index 00000000000..fef029fd89d --- /dev/null +++ b/rust/chains/tw_groestlcoin/src/address/mod.rs @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_keypair::tw; +use tw_memory::Data; +use tw_utxo::address::derivation::BitcoinDerivation; +use tw_utxo::address::segwit::SegwitAddress; +use tw_utxo::address::standard_bitcoin::StandardBitcoinPrefix; + +pub mod groestlcoin_legacy; +use groestlcoin_legacy::GroestlLegacyAddress; + +pub enum GroestlAddress { + Legacy(GroestlLegacyAddress), + Segwit(SegwitAddress), +} + +impl GroestlAddress { + /// Tries to parse one of the `BitcoinAddress` variants + /// and validates if the result address matches the given `prefix` address or belongs to the `coin` network. + pub fn from_str_with_coin_and_prefix( + coin: &dyn CoinContext, + s: &str, + prefix: Option, + ) -> AddressResult { + match prefix { + Some(StandardBitcoinPrefix::Base58(base58)) => { + GroestlLegacyAddress::from_str_with_coin_and_prefix(coin, s, Some(base58)) + .map(GroestlAddress::Legacy) + }, + Some(StandardBitcoinPrefix::Bech32(bech32)) => { + SegwitAddress::from_str_with_coin_and_prefix(coin, s, Some(bech32)) + .map(GroestlAddress::Segwit) + }, + None => GroestlAddress::from_str_checked(coin, s), + } + } + + /// Tries to parse one of the `BitcoinAddress` variants + /// and validates if the result address belongs to the `coin` network. + pub fn from_str_checked(coin: &dyn CoinContext, s: &str) -> AddressResult { + if let Ok(legacy) = GroestlLegacyAddress::from_str_with_coin_and_prefix(coin, s, None) { + return Ok(GroestlAddress::Legacy(legacy)); + } + + if let Ok(segwit) = SegwitAddress::from_str_with_coin_and_prefix(coin, s, None) { + return Ok(GroestlAddress::Segwit(segwit)); + } + + Err(AddressError::InvalidInput) + } + + /// TrustWallet derivation inherited from: + /// https://github.com/trustwallet/wallet-core/blob/43235636ad1c97e4e13388afd3db3d6f9d09e1ca/src/Groestlcoin/Entry.cpp#L20-L30 + pub fn derive_as_tw( + coin: &dyn CoinContext, + public_key: &tw::PublicKey, + derivation: Derivation, + maybe_prefix: Option, + ) -> AddressResult { + match maybe_prefix { + Some(StandardBitcoinPrefix::Base58(prefix)) => { + return GroestlLegacyAddress::p2pkh_with_coin_and_prefix( + coin, + public_key, + Some(prefix), + ) + .map(GroestlAddress::Legacy); + }, + Some(StandardBitcoinPrefix::Bech32(prefix)) => { + return SegwitAddress::p2wpkh_with_coin_and_prefix(coin, public_key, Some(prefix)) + .map(GroestlAddress::Segwit); + }, + // Derive an address as declared in registry.json. + None => (), + } + + match BitcoinDerivation::tw_derivation(coin, derivation) { + BitcoinDerivation::Legacy => { + GroestlLegacyAddress::p2pkh_with_coin_and_prefix(coin, public_key, None) + .map(GroestlAddress::Legacy) + }, + BitcoinDerivation::Segwit => { + SegwitAddress::p2wpkh_with_coin_and_prefix(coin, public_key, None) + .map(GroestlAddress::Segwit) + }, + BitcoinDerivation::Taproot => Err(AddressError::InvalidRegistry), + } + } +} + +impl CoinAddress for GroestlAddress { + #[inline] + fn data(&self) -> Data { + match self { + GroestlAddress::Legacy(legacy) => legacy.bytes().to_vec(), + GroestlAddress::Segwit(segwit) => segwit.witness_program().to_vec(), + } + } +} + +impl FromStr for GroestlAddress { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + if let Ok(legacy) = GroestlLegacyAddress::from_str(s) { + return Ok(GroestlAddress::Legacy(legacy)); + } + if let Ok(segwit) = SegwitAddress::from_str(s) { + return Ok(GroestlAddress::Segwit(segwit)); + } + Err(AddressError::InvalidInput) + } +} + +impl fmt::Display for GroestlAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GroestlAddress::Legacy(legacy) => write!(f, "{legacy}"), + GroestlAddress::Segwit(segwit) => write!(f, "{segwit}"), + } + } +} diff --git a/rust/chains/tw_groestlcoin/src/context.rs b/rust/chains/tw_groestlcoin/src/context.rs new file mode 100644 index 00000000000..6885fa9db04 --- /dev/null +++ b/rust/chains/tw_groestlcoin/src/context.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::GroestlAddress; +use tw_bitcoin::context::BitcoinSigningContext; +use tw_bitcoin::modules::protobuf_builder::standard_protobuf_builder::StandardProtobufBuilder; +use tw_bitcoin::modules::psbt_request::standard_psbt_request_builder::StandardPsbtRequestBuilder; +use tw_bitcoin::modules::signing_request::standard_signing_request::StandardSigningRequestBuilder; +use tw_coin_entry::error::prelude::SigningResult; +use tw_hash::hasher::Hasher; +use tw_hash::hasher::Hasher::Sha256; +use tw_utxo::context::{AddressPrefixes, UtxoContext}; +use tw_utxo::fee::fee_estimator::StandardFeeEstimator; +use tw_utxo::script::Script; +use tw_utxo::transaction::standard_transaction::Transaction; + +#[derive(Default)] +pub struct GroestlContext; + +impl UtxoContext for GroestlContext { + type Address = GroestlAddress; + type Transaction = Transaction; + type FeeEstimator = StandardFeeEstimator; + + /// Groestlcoin uses a different hash algorithm. + const TX_HASHER: Hasher = Sha256; + + fn addr_to_script_pubkey( + addr: &Self::Address, + prefixes: AddressPrefixes, + ) -> SigningResult