Skip to content

Commit

Permalink
[Bitcoin]: Add support for Taproot address generation (#4074)
Browse files Browse the repository at this point in the history
* [Bitcoin]: Add taproot derivation

* Refactor codegen tool to generate newly added derivations
* Add `TWDerivation.h` to the git index

* [Bitcoin]: Generate Rust enum variants as well

* [Bitcoin]: Handle `TWDerivationBitcoinTaproot` enum variant

* Add missing `file_editor.rb` file

* [Bitcoin]: Forward deriveAddress to Rust if derivation is Taproot

* [Bitcoin]: Add Android tests

* [Bitcoin]: Add taproot derivation iOS tests
  • Loading branch information
satoshiotomakan authored Oct 25, 2024
1 parent 8c605ed commit 1640748
Show file tree
Hide file tree
Showing 28 changed files with 341 additions and 45 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ codegen-v2/bindings/
src/Generated/*.cpp
include/TrustWalletCore/TWHRP.h
include/TrustWalletCore/TW*Proto.h
include/TrustWalletCore/TWDerivation.h
include/TrustWalletCore/TWEthereumChainID.h

# Wasm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class TestCoinType {
assertEquals(res, "m/84'/0'/0'/0/0")
res = CoinType.createFromValue(CoinType.BITCOIN.value()).derivationPathWithDerivation(Derivation.BITCOINLEGACY).toString()
assertEquals(res, "m/44'/0'/0'/0/0")
res = CoinType.createFromValue(CoinType.BITCOIN.value()).derivationPathWithDerivation(Derivation.BITCOINTAPROOT).toString()
assertEquals(res, "m/86'/0'/0'/0/0")
res = CoinType.createFromValue(CoinType.SOLANA.value()).derivationPathWithDerivation(Derivation.SOLANASOLANA).toString()
assertEquals(res, "m/44'/501'/0'/0'")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ class TestAnyAddress {

val address2 = AnyAddress(pubkey, coin, Derivation.BITCOINLEGACY)
assertEquals(address2.description(), "1JvRfEQFv5q5qy9uTSAezH7kVQf4hqnHXx")

val address3 = AnyAddress(pubkey, coin, Derivation.BITCOINTAPROOT)
assertEquals(address3.description(), "bc1pnncpg8s7gu7t6xmmzxqarcj8ydthmaz8gr4m76eephjfprs53maswgel0w")
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ class TestHDWallet {

val key3 = wallet.getKeyDerivation(coin, Derivation.BITCOINTESTNET)
assertEquals(key3.data().toHex(), "0xca5845e1b43e3adf577b7f110b60596479425695005a594c88f9901c3afe864f")

val key4 = wallet.getKeyDerivation(coin, Derivation.BITCOINTAPROOT)
assertEquals(key4.data().toHex(), "0xa2c4d6df786f118f20330affd65d248ffdc0750ae9cbc729d27c640302afd030")
}

@Test
Expand All @@ -145,6 +148,9 @@ class TestHDWallet {

val address3 = wallet.getAddressDerivation(coin, Derivation.BITCOINTESTNET)
assertEquals(address3, "tb1qwgpxgwn33z3ke9s7q65l976pseh4edrzfmyvl0")

val address4 = wallet.getAddressDerivation(coin, Derivation.BITCOINTAPROOT)
assertEquals(address4, "bc1pgqks0cynn93ymve4x0jq3u7hne77908nlysp289hc44yc4cmy0hslyckrz")
}

@Test
Expand Down
22 changes: 7 additions & 15 deletions codegen/bin/coins
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ require 'erb'
require 'fileutils'
require 'json'

CurrentDir = File.dirname(__FILE__)
$LOAD_PATH.unshift(File.join(CurrentDir, '..', 'lib'))
require 'derivation'

# Transforms a coin name to a C++ name
def self.format_name(n)
formatted = n
Expand All @@ -18,24 +22,10 @@ def self.coin_name(coin)
coin['displayName'] || coin['name']
end

def self.derivation_path(coin)
coin['derivation'][0]['path']
end

def self.camel_case(id)
id[0].upcase + id[1..].downcase
end

def self.derivation_name(deriv)
return "" if deriv['name'].nil?
deriv['name'].downcase
end

def self.derivation_enum_name(deriv, coin)
return "TWDerivationDefault" if deriv['name'].nil?
"TWDerivation" + format_name(coin['name']) + camel_case(deriv['name'])
end

def self.coin_img(coin)
"<img src=\"https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/#{coin}/info/logo.png\" width=\"32\" />"
end
Expand All @@ -55,14 +45,16 @@ coins = JSON.parse(json_string).sort_by { |x| x['coinId'] }
enum_count = 0

erbs = [
{'template' => 'TWDerivation.h.erb', 'folder' => 'include/TrustWalletCore', 'file' => 'TWDerivation.h'},
{'template' => 'CoinInfoData.cpp.erb', 'folder' => 'src/Generated', 'file' => 'CoinInfoData.cpp'},
{'template' => 'registry.md.erb', 'folder' => 'docs', 'file' => 'registry.md'},
{'template' => 'hrp.cpp.erb', 'folder' => 'src/Generated', 'file' => 'TWHRP.cpp'},
{'template' => 'hrp.h.erb', 'folder' => 'include/TrustWalletCore', 'file' => 'TWHRP.h'},
{'template' => 'TWEthereumChainID.h.erb', 'folder' => 'include/TrustWalletCore', 'file' => 'TWEthereumChainID.h'}
]

# Update coins derivations if changed.
update_derivation_enum(coins)

FileUtils.mkdir_p File.join('src', 'Generated')
erbs.each do |erb|
path = File.expand_path(erb['template'], File.join(File.dirname(__FILE__), '..', 'lib', 'templates'))
Expand Down
21 changes: 1 addition & 20 deletions codegen/lib/coin_skeleton_gen.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require 'entity_decl'
require 'code_generator'
require 'coin_test_gen'
require 'file_editor'

# Coin template generation

Expand Down Expand Up @@ -70,26 +71,6 @@ def insert_coin_entry(coin)
insert_target_line(target_file, target_line, " // end_of_coin_dipatcher_switch_marker_do_not_modify\n")
end

def self.insert_target_line(target_file, target_line, original_line)
lines = File.readlines(target_file)
index = lines.index(target_line)
if !index.nil?
puts "Line is already present, file: #{target_file} line: #{target_line}"
return true
end
index = lines.index(original_line)
if index.nil?
puts "WARNING: Could not find line! file: #{target_file} line: #{original_line}"
return false
end
lines.insert(index, target_line)
File.open(target_file, "w+") do |f|
f.puts(lines)
end
puts "Updated file: #{target_file} new line: #{target_line}"
return true
end

def generate_blockchain_files(coin)
name = format_name(coin)

Expand Down
76 changes: 76 additions & 0 deletions codegen/lib/derivation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

require 'file_editor'

$derivation_file = "include/TrustWalletCore/TWDerivation.h"
$derivation_file_rust = "rust/tw_coin_registry/src/tw_derivation.rs"

# Returns a derivation name if specified.
def derivation_name(deriv)
return "" if deriv['name'].nil?
deriv['name'].downcase
end

# Returns a string of `<Coin><Derivation>` if derivation's name is specified, otherwise returns `Default`.
def derivation_enum_name_no_prefix(deriv, coin)
return "Default" if deriv['name'].nil?
format_name(coin['name']) + camel_case(deriv['name'])
end

# Returns a string of `TWDerivation<Coin><Derivation>` if derivation's name is specified, otherwise returns `TWDerivationDefault`.
def derivation_enum_name(deriv, coin)
return "TWDerivation" + derivation_enum_name_no_prefix(deriv, coin)
end

# Returns a derivation path.
def derivation_path(coin)
coin['derivation'][0]['path']
end

# Get the last `TWDerivation` enum variant ID.
def get_last_derivation(file_path)
last_derivation_id = nil

File.open(file_path, "r") do |file|
file.each_line do |line|
# Match lines that define a TWDerivation enum value
if line =~ /TWDerivation\w+\s*=\s*(\d+),/
last_derivation_id = $1.to_i
end
end
end

last_derivation_id
end

# Returns whether the TWDerivation enum contains the given `derivation` variant.
def find_derivation(file_path, derivation)
File.open(file_path, "r") do |file|
file.each_line do |line|
return true if line.include?(derivation)
end
end
return false
end

# Insert a new `TWDerivation<X> = N,` to the end of the enum.
def insert_derivation(file_path, derivation, derivation_id)
target_line = " #{derivation} = #{derivation_id},"
insert_target_line(file_path, target_line, " // end_of_derivation_enum - USED TO GENERATE CODE\n")
end

# Update TWDerivation enum variants if new derivation appeared.
def update_derivation_enum(coins)
coins.each do |coin|
coin['derivation'].each_with_index do |deriv, index|
deriv_name = derivation_enum_name(deriv, coin)
if !find_derivation($derivation_file, deriv_name)
new_derivation_id = get_last_derivation($derivation_file) + 1
insert_derivation($derivation_file, deriv_name, new_derivation_id)

rust_deriv_name = derivation_enum_name_no_prefix(deriv, coin)
insert_derivation($derivation_file_rust, rust_deriv_name, new_derivation_id)
end
end
end
end
19 changes: 19 additions & 0 deletions codegen/lib/file_editor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
def insert_target_line(target_file, target_line, original_line)
lines = File.readlines(target_file)
index = lines.index(target_line)
if !index.nil?
puts "Line is already present, file: #{target_file} line: #{target_line}"
return true
end
index = lines.index(original_line)
if index.nil?
puts "WARNING: Could not find line! file: #{target_file} line: #{original_line}"
return false
end
lines.insert(index, target_line)
File.open(target_file, "w+") do |f|
f.puts(lines)
end
puts "Updated file: #{target_file} new line: #{target_line}"
return true
end
31 changes: 31 additions & 0 deletions include/TrustWalletCore/TWDerivation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.
//
// This is a GENERATED FILE from \registry.json, changes made here WILL BE LOST.
//

#pragma once

#include "TWBase.h"

TW_EXTERN_C_BEGIN

/// Non-default coin address derivation names (default, unnamed derivations are not included).
/// Note the enum variant must be sync with `TWDerivation` enum in Rust:
/// https://github.com/trustwallet/wallet-core/blob/master/rust/tw_coin_registry/src/tw_derivation.rs
TW_EXPORT_ENUM()
enum TWDerivation {
TWDerivationDefault = 0, // default, for any coin
TWDerivationCustom = 1, // custom, for any coin
TWDerivationBitcoinSegwit = 2,
TWDerivationBitcoinLegacy = 3,
TWDerivationBitcoinTestnet = 4,
TWDerivationLitecoinLegacy = 5,
TWDerivationSolanaSolana = 6,
TWDerivationStratisSegwit = 7,
TWDerivationBitcoinTaproot = 8,
// end_of_derivation_enum - USED TO GENERATE CODE
};

TW_EXTERN_C_END
1 change: 1 addition & 0 deletions include/TrustWalletCore/TWPurpose.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ enum TWPurpose {
TWPurposeBIP44 = 44,
TWPurposeBIP49 = 49, // Derivation scheme for P2WPKH-nested-in-P2SH
TWPurposeBIP84 = 84, // Derivation scheme for P2WPKH
TWPurposeBIP86 = 86, // Derivation scheme for P2TR
TWPurposeBIP1852 = 1852, // Derivation scheme used by Cardano-Shelley
};

Expand Down
6 changes: 6 additions & 0 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
"path": "m/84'/1'/0'/0/0",
"xpub": "zpub",
"xprv": "zprv"
},
{
"name": "taproot",
"path": "m/86'/0'/0'/0/0",
"xpub": "zpub",
"xprv": "zprv"
}
],
"curve": "secp256k1",
Expand Down
14 changes: 14 additions & 0 deletions rust/frameworks/tw_utxo/src/address/derivation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ use tw_coin_entry::coin_context::CoinContext;
use tw_coin_entry::derivation::{ChildIndex, Derivation};

pub const SEGWIT_DERIVATION_PATH_TYPE: ChildIndex = ChildIndex::Hardened(84);
pub const TAPROOT_DERIVATION_PATH_TYPE: ChildIndex = ChildIndex::Hardened(86);

pub enum BitcoinDerivation {
Legacy,
Segwit,
Taproot,
}

impl BitcoinDerivation {
Expand All @@ -23,6 +25,7 @@ impl BitcoinDerivation {
Derivation::Default | Derivation::Testnet => (),
Derivation::Segwit => return BitcoinDerivation::Segwit,
Derivation::Legacy => return BitcoinDerivation::Legacy,
Derivation::Taproot => return BitcoinDerivation::Taproot,
}

let Some(default_derivation) = coin.derivations().first() else {
Expand All @@ -32,9 +35,13 @@ impl BitcoinDerivation {

match default_derivation.name {
Derivation::Segwit => BitcoinDerivation::Segwit,
Derivation::Taproot => BitcoinDerivation::Taproot,
Derivation::Default if derivation_path_type == Some(SEGWIT_DERIVATION_PATH_TYPE) => {
BitcoinDerivation::Segwit
},
Derivation::Default if derivation_path_type == Some(TAPROOT_DERIVATION_PATH_TYPE) => {
BitcoinDerivation::Taproot
},
Derivation::Default | Derivation::Legacy | Derivation::Testnet => {
BitcoinDerivation::Legacy
},
Expand All @@ -49,4 +56,11 @@ impl BitcoinDerivation {
|| der.path.path().first().copied() == Some(SEGWIT_DERIVATION_PATH_TYPE)
})
}

pub fn tw_supports_taproot(coin: &dyn CoinContext) -> bool {
coin.derivations().iter().any(|der| {
der.name == Derivation::Taproot
|| der.path.path().first().copied() == Some(TAPROOT_DERIVATION_PATH_TYPE)
})
}
}
9 changes: 8 additions & 1 deletion rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ impl StandardBitcoinAddress {
if let Ok(segwit) = SegwitAddress::from_str_with_coin_and_prefix(coin, s, None) {
return Ok(StandardBitcoinAddress::Segwit(segwit));
}
}

// TODO use `BitcoinDerivation::tw_supports_taproot` based on `registry.json`.
// Try to parse a Taproot address if the coin supports it.
if BitcoinDerivation::tw_supports_taproot(coin) {
if let Ok(taproot) = TaprootAddress::from_str_with_coin_and_prefix(coin, s, None) {
return Ok(StandardBitcoinAddress::Taproot(taproot));
}
Expand Down Expand Up @@ -127,6 +129,11 @@ impl StandardBitcoinAddress {
SegwitAddress::p2wpkh_with_coin_and_prefix(coin, public_key, None)
.map(StandardBitcoinAddress::Segwit)
},
BitcoinDerivation::Taproot => {
let no_merkle_root = None;
TaprootAddress::p2tr_with_coin_and_prefix(coin, public_key, None, no_merkle_root)
.map(StandardBitcoinAddress::Taproot)
},
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion rust/tw_any_coin/src/test_utils/address_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ impl WithDestructor for TWAnyAddress {
}

pub fn test_address_derive(coin: CoinType, private_key: &str, address: &str) {
test_address_derive_with_derivation(coin, private_key, address, TWDerivation::Default)
}

pub fn test_address_derive_with_derivation(
coin: CoinType,
private_key: &str,
address: &str,
derivation: TWDerivation,
) {
let coin_item = get_coin_item(coin).unwrap();

let private_key = TWPrivateKeyHelper::with_hex(private_key);
Expand All @@ -41,7 +50,7 @@ pub fn test_address_derive(coin: CoinType, private_key: &str, address: &str) {
tw_any_address_create_with_public_key_derivation(
public_key.ptr(),
coin as u32,
TWDerivation::Default as u32,
derivation as u32,
)
});

Expand Down
1 change: 1 addition & 0 deletions rust/tw_coin_entry/src/derivation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub enum Derivation {
Segwit,
Legacy,
Testnet,
Taproot,
/// Default derivation.
#[default]
#[serde(other)]
Expand Down
Loading

0 comments on commit 1640748

Please sign in to comment.