Skip to content

Commit

Permalink
[Groestl]: Groestlcoin Rust migration (trustwallet#4271)
Browse files Browse the repository at this point in the history
* feat(groestl): Add Groestlcoin blockchain

* Refactor `tw_hash` by adding `StatefulHasher` and `StaticHasher` for every hashing algo

* feat(utxo): Add `GroestlAddress`

* Add `UtxoContext::TX_HASHER`

* feat(groestl): Fix transaction planning, hashing

* feat(groestl): Add support for custom `Derivation`

* Add address tests
* Add P2WPKH signing test

* feat(groestl): Add compile test

* feat(groestl): Add `tw_groestlcoin` documentation

* feat(groestl): Bridge C++ and Rust implementations

* Add C++ signing test

* feat(groestl): Add Android test

* feat(groestl): Revert `registry.md`

* feat(groestl): Fix Rust tests and fmt
  • Loading branch information
satoshiotomakan authored Feb 18, 2025
1 parent 6cea3f4 commit a1d5706
Show file tree
Hide file tree
Showing 59 changed files with 1,312 additions and 203 deletions.
Original file line number Diff line number Diff line change
@@ -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")
}
}
16 changes: 16 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions rust/chains/tw_bitcoin/src/modules/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ impl<Context: BitcoinSigningContext> BitcoinCompiler<Context> {
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,
Expand All @@ -146,7 +146,7 @@ impl<Context: BitcoinSigningContext> BitcoinCompiler<Context> {
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,
Expand Down
4 changes: 2 additions & 2 deletions rust/chains/tw_bitcoin/src/modules/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl<Context: BitcoinSigningContext> BitcoinSigner<Context> {
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)`.
Expand Down Expand Up @@ -107,7 +107,7 @@ impl<Context: BitcoinSigningContext> BitcoinSigner<Context> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SighashType> {
Expand Down
2 changes: 1 addition & 1 deletion rust/chains/tw_bitcoincash/src/cash_address/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions rust/chains/tw_groestlcoin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" }
9 changes: 9 additions & 0 deletions rust/chains/tw_groestlcoin/src/address/groestlcoin_legacy.rs
Original file line number Diff line number Diff line change
@@ -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<Sha256Ripemd, Groestl512d>;
130 changes: 130 additions & 0 deletions rust/chains/tw_groestlcoin/src/address/mod.rs
Original file line number Diff line number Diff line change
@@ -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<StandardBitcoinPrefix>,
) -> AddressResult<GroestlAddress> {
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<GroestlAddress> {
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<StandardBitcoinPrefix>,
) -> AddressResult<GroestlAddress> {
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<Self, Self::Err> {
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}"),
}
}
}
46 changes: 46 additions & 0 deletions rust/chains/tw_groestlcoin/src/context.rs
Original file line number Diff line number Diff line change
@@ -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<Self::Transaction>;

/// Groestlcoin uses a different hash algorithm.
const TX_HASHER: Hasher = Sha256;

fn addr_to_script_pubkey(
addr: &Self::Address,
prefixes: AddressPrefixes,
) -> SigningResult<Script> {
match addr {
GroestlAddress::Legacy(legacy) => {
legacy.to_script_pubkey(prefixes.p2pkh_prefix, prefixes.p2sh_prefix)
},
GroestlAddress::Segwit(segwit) => segwit.to_script_pubkey(),
}
}
}

impl BitcoinSigningContext for GroestlContext {
type SigningRequestBuilder = StandardSigningRequestBuilder;
type ProtobufBuilder = StandardProtobufBuilder;
type PsbtRequestBuilder = StandardPsbtRequestBuilder;
}
Loading

0 comments on commit a1d5706

Please sign in to comment.