Skip to content

Commit 1640748

Browse files
authoredOct 25, 2024
[Bitcoin]: Add support for Taproot address generation (#4074)
* [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
1 parent 8c605ed commit 1640748

File tree

28 files changed

+341
-45
lines changed

28 files changed

+341
-45
lines changed
 

‎.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ codegen-v2/bindings/
3838
src/Generated/*.cpp
3939
include/TrustWalletCore/TWHRP.h
4040
include/TrustWalletCore/TW*Proto.h
41-
include/TrustWalletCore/TWDerivation.h
4241
include/TrustWalletCore/TWEthereumChainID.h
4342

4443
# Wasm

‎android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/TestCoinType.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class TestCoinType {
5454
assertEquals(res, "m/84'/0'/0'/0/0")
5555
res = CoinType.createFromValue(CoinType.BITCOIN.value()).derivationPathWithDerivation(Derivation.BITCOINLEGACY).toString()
5656
assertEquals(res, "m/44'/0'/0'/0/0")
57+
res = CoinType.createFromValue(CoinType.BITCOIN.value()).derivationPathWithDerivation(Derivation.BITCOINTAPROOT).toString()
58+
assertEquals(res, "m/86'/0'/0'/0/0")
5759
res = CoinType.createFromValue(CoinType.SOLANA.value()).derivationPathWithDerivation(Derivation.SOLANASOLANA).toString()
5860
assertEquals(res, "m/44'/501'/0'/0'")
5961
}

‎android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestAnyAddress.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class TestAnyAddress {
5858

5959
val address2 = AnyAddress(pubkey, coin, Derivation.BITCOINLEGACY)
6060
assertEquals(address2.description(), "1JvRfEQFv5q5qy9uTSAezH7kVQf4hqnHXx")
61+
62+
val address3 = AnyAddress(pubkey, coin, Derivation.BITCOINTAPROOT)
63+
assertEquals(address3.description(), "bc1pnncpg8s7gu7t6xmmzxqarcj8ydthmaz8gr4m76eephjfprs53maswgel0w")
6164
}
6265

6366
@Test

‎android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestHDWallet.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ class TestHDWallet {
121121

122122
val key3 = wallet.getKeyDerivation(coin, Derivation.BITCOINTESTNET)
123123
assertEquals(key3.data().toHex(), "0xca5845e1b43e3adf577b7f110b60596479425695005a594c88f9901c3afe864f")
124+
125+
val key4 = wallet.getKeyDerivation(coin, Derivation.BITCOINTAPROOT)
126+
assertEquals(key4.data().toHex(), "0xa2c4d6df786f118f20330affd65d248ffdc0750ae9cbc729d27c640302afd030")
124127
}
125128

126129
@Test
@@ -145,6 +148,9 @@ class TestHDWallet {
145148

146149
val address3 = wallet.getAddressDerivation(coin, Derivation.BITCOINTESTNET)
147150
assertEquals(address3, "tb1qwgpxgwn33z3ke9s7q65l976pseh4edrzfmyvl0")
151+
152+
val address4 = wallet.getAddressDerivation(coin, Derivation.BITCOINTAPROOT)
153+
assertEquals(address4, "bc1pgqks0cynn93ymve4x0jq3u7hne77908nlysp289hc44yc4cmy0hslyckrz")
148154
}
149155

150156
@Test

‎codegen/bin/coins

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ require 'erb'
44
require 'fileutils'
55
require 'json'
66

7+
CurrentDir = File.dirname(__FILE__)
8+
$LOAD_PATH.unshift(File.join(CurrentDir, '..', 'lib'))
9+
require 'derivation'
10+
711
# Transforms a coin name to a C++ name
812
def self.format_name(n)
913
formatted = n
@@ -18,24 +22,10 @@ def self.coin_name(coin)
1822
coin['displayName'] || coin['name']
1923
end
2024

21-
def self.derivation_path(coin)
22-
coin['derivation'][0]['path']
23-
end
24-
2525
def self.camel_case(id)
2626
id[0].upcase + id[1..].downcase
2727
end
2828

29-
def self.derivation_name(deriv)
30-
return "" if deriv['name'].nil?
31-
deriv['name'].downcase
32-
end
33-
34-
def self.derivation_enum_name(deriv, coin)
35-
return "TWDerivationDefault" if deriv['name'].nil?
36-
"TWDerivation" + format_name(coin['name']) + camel_case(deriv['name'])
37-
end
38-
3929
def self.coin_img(coin)
4030
"<img src=\"https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/#{coin}/info/logo.png\" width=\"32\" />"
4131
end
@@ -55,14 +45,16 @@ coins = JSON.parse(json_string).sort_by { |x| x['coinId'] }
5545
enum_count = 0
5646

5747
erbs = [
58-
{'template' => 'TWDerivation.h.erb', 'folder' => 'include/TrustWalletCore', 'file' => 'TWDerivation.h'},
5948
{'template' => 'CoinInfoData.cpp.erb', 'folder' => 'src/Generated', 'file' => 'CoinInfoData.cpp'},
6049
{'template' => 'registry.md.erb', 'folder' => 'docs', 'file' => 'registry.md'},
6150
{'template' => 'hrp.cpp.erb', 'folder' => 'src/Generated', 'file' => 'TWHRP.cpp'},
6251
{'template' => 'hrp.h.erb', 'folder' => 'include/TrustWalletCore', 'file' => 'TWHRP.h'},
6352
{'template' => 'TWEthereumChainID.h.erb', 'folder' => 'include/TrustWalletCore', 'file' => 'TWEthereumChainID.h'}
6453
]
6554

55+
# Update coins derivations if changed.
56+
update_derivation_enum(coins)
57+
6658
FileUtils.mkdir_p File.join('src', 'Generated')
6759
erbs.each do |erb|
6860
path = File.expand_path(erb['template'], File.join(File.dirname(__FILE__), '..', 'lib', 'templates'))

‎codegen/lib/coin_skeleton_gen.rb

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require 'entity_decl'
88
require 'code_generator'
99
require 'coin_test_gen'
10+
require 'file_editor'
1011

1112
# Coin template generation
1213

@@ -70,26 +71,6 @@ def insert_coin_entry(coin)
7071
insert_target_line(target_file, target_line, " // end_of_coin_dipatcher_switch_marker_do_not_modify\n")
7172
end
7273

73-
def self.insert_target_line(target_file, target_line, original_line)
74-
lines = File.readlines(target_file)
75-
index = lines.index(target_line)
76-
if !index.nil?
77-
puts "Line is already present, file: #{target_file} line: #{target_line}"
78-
return true
79-
end
80-
index = lines.index(original_line)
81-
if index.nil?
82-
puts "WARNING: Could not find line! file: #{target_file} line: #{original_line}"
83-
return false
84-
end
85-
lines.insert(index, target_line)
86-
File.open(target_file, "w+") do |f|
87-
f.puts(lines)
88-
end
89-
puts "Updated file: #{target_file} new line: #{target_line}"
90-
return true
91-
end
92-
9374
def generate_blockchain_files(coin)
9475
name = format_name(coin)
9576

‎codegen/lib/derivation.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# frozen_string_literal: true
2+
3+
require 'file_editor'
4+
5+
$derivation_file = "include/TrustWalletCore/TWDerivation.h"
6+
$derivation_file_rust = "rust/tw_coin_registry/src/tw_derivation.rs"
7+
8+
# Returns a derivation name if specified.
9+
def derivation_name(deriv)
10+
return "" if deriv['name'].nil?
11+
deriv['name'].downcase
12+
end
13+
14+
# Returns a string of `<Coin><Derivation>` if derivation's name is specified, otherwise returns `Default`.
15+
def derivation_enum_name_no_prefix(deriv, coin)
16+
return "Default" if deriv['name'].nil?
17+
format_name(coin['name']) + camel_case(deriv['name'])
18+
end
19+
20+
# Returns a string of `TWDerivation<Coin><Derivation>` if derivation's name is specified, otherwise returns `TWDerivationDefault`.
21+
def derivation_enum_name(deriv, coin)
22+
return "TWDerivation" + derivation_enum_name_no_prefix(deriv, coin)
23+
end
24+
25+
# Returns a derivation path.
26+
def derivation_path(coin)
27+
coin['derivation'][0]['path']
28+
end
29+
30+
# Get the last `TWDerivation` enum variant ID.
31+
def get_last_derivation(file_path)
32+
last_derivation_id = nil
33+
34+
File.open(file_path, "r") do |file|
35+
file.each_line do |line|
36+
# Match lines that define a TWDerivation enum value
37+
if line =~ /TWDerivation\w+\s*=\s*(\d+),/
38+
last_derivation_id = $1.to_i
39+
end
40+
end
41+
end
42+
43+
last_derivation_id
44+
end
45+
46+
# Returns whether the TWDerivation enum contains the given `derivation` variant.
47+
def find_derivation(file_path, derivation)
48+
File.open(file_path, "r") do |file|
49+
file.each_line do |line|
50+
return true if line.include?(derivation)
51+
end
52+
end
53+
return false
54+
end
55+
56+
# Insert a new `TWDerivation<X> = N,` to the end of the enum.
57+
def insert_derivation(file_path, derivation, derivation_id)
58+
target_line = " #{derivation} = #{derivation_id},"
59+
insert_target_line(file_path, target_line, " // end_of_derivation_enum - USED TO GENERATE CODE\n")
60+
end
61+
62+
# Update TWDerivation enum variants if new derivation appeared.
63+
def update_derivation_enum(coins)
64+
coins.each do |coin|
65+
coin['derivation'].each_with_index do |deriv, index|
66+
deriv_name = derivation_enum_name(deriv, coin)
67+
if !find_derivation($derivation_file, deriv_name)
68+
new_derivation_id = get_last_derivation($derivation_file) + 1
69+
insert_derivation($derivation_file, deriv_name, new_derivation_id)
70+
71+
rust_deriv_name = derivation_enum_name_no_prefix(deriv, coin)
72+
insert_derivation($derivation_file_rust, rust_deriv_name, new_derivation_id)
73+
end
74+
end
75+
end
76+
end

‎codegen/lib/file_editor.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
def insert_target_line(target_file, target_line, original_line)
2+
lines = File.readlines(target_file)
3+
index = lines.index(target_line)
4+
if !index.nil?
5+
puts "Line is already present, file: #{target_file} line: #{target_line}"
6+
return true
7+
end
8+
index = lines.index(original_line)
9+
if index.nil?
10+
puts "WARNING: Could not find line! file: #{target_file} line: #{original_line}"
11+
return false
12+
end
13+
lines.insert(index, target_line)
14+
File.open(target_file, "w+") do |f|
15+
f.puts(lines)
16+
end
17+
puts "Updated file: #{target_file} new line: #{target_line}"
18+
return true
19+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//
3+
// Copyright © 2017 Trust Wallet.
4+
//
5+
// This is a GENERATED FILE from \registry.json, changes made here WILL BE LOST.
6+
//
7+
8+
#pragma once
9+
10+
#include "TWBase.h"
11+
12+
TW_EXTERN_C_BEGIN
13+
14+
/// Non-default coin address derivation names (default, unnamed derivations are not included).
15+
/// Note the enum variant must be sync with `TWDerivation` enum in Rust:
16+
/// https://github.com/trustwallet/wallet-core/blob/master/rust/tw_coin_registry/src/tw_derivation.rs
17+
TW_EXPORT_ENUM()
18+
enum TWDerivation {
19+
TWDerivationDefault = 0, // default, for any coin
20+
TWDerivationCustom = 1, // custom, for any coin
21+
TWDerivationBitcoinSegwit = 2,
22+
TWDerivationBitcoinLegacy = 3,
23+
TWDerivationBitcoinTestnet = 4,
24+
TWDerivationLitecoinLegacy = 5,
25+
TWDerivationSolanaSolana = 6,
26+
TWDerivationStratisSegwit = 7,
27+
TWDerivationBitcoinTaproot = 8,
28+
// end_of_derivation_enum - USED TO GENERATE CODE
29+
};
30+
31+
TW_EXTERN_C_END

‎include/TrustWalletCore/TWPurpose.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ enum TWPurpose {
1818
TWPurposeBIP44 = 44,
1919
TWPurposeBIP49 = 49, // Derivation scheme for P2WPKH-nested-in-P2SH
2020
TWPurposeBIP84 = 84, // Derivation scheme for P2WPKH
21+
TWPurposeBIP86 = 86, // Derivation scheme for P2TR
2122
TWPurposeBIP1852 = 1852, // Derivation scheme used by Cardano-Shelley
2223
};
2324

‎registry.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
"path": "m/84'/1'/0'/0/0",
2525
"xpub": "zpub",
2626
"xprv": "zprv"
27+
},
28+
{
29+
"name": "taproot",
30+
"path": "m/86'/0'/0'/0/0",
31+
"xpub": "zpub",
32+
"xprv": "zprv"
2733
}
2834
],
2935
"curve": "secp256k1",

‎rust/frameworks/tw_utxo/src/address/derivation.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ use tw_coin_entry::coin_context::CoinContext;
66
use tw_coin_entry::derivation::{ChildIndex, Derivation};
77

88
pub const SEGWIT_DERIVATION_PATH_TYPE: ChildIndex = ChildIndex::Hardened(84);
9+
pub const TAPROOT_DERIVATION_PATH_TYPE: ChildIndex = ChildIndex::Hardened(86);
910

1011
pub enum BitcoinDerivation {
1112
Legacy,
1213
Segwit,
14+
Taproot,
1315
}
1416

1517
impl BitcoinDerivation {
@@ -23,6 +25,7 @@ impl BitcoinDerivation {
2325
Derivation::Default | Derivation::Testnet => (),
2426
Derivation::Segwit => return BitcoinDerivation::Segwit,
2527
Derivation::Legacy => return BitcoinDerivation::Legacy,
28+
Derivation::Taproot => return BitcoinDerivation::Taproot,
2629
}
2730

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

3336
match default_derivation.name {
3437
Derivation::Segwit => BitcoinDerivation::Segwit,
38+
Derivation::Taproot => BitcoinDerivation::Taproot,
3539
Derivation::Default if derivation_path_type == Some(SEGWIT_DERIVATION_PATH_TYPE) => {
3640
BitcoinDerivation::Segwit
3741
},
42+
Derivation::Default if derivation_path_type == Some(TAPROOT_DERIVATION_PATH_TYPE) => {
43+
BitcoinDerivation::Taproot
44+
},
3845
Derivation::Default | Derivation::Legacy | Derivation::Testnet => {
3946
BitcoinDerivation::Legacy
4047
},
@@ -49,4 +56,11 @@ impl BitcoinDerivation {
4956
|| der.path.path().first().copied() == Some(SEGWIT_DERIVATION_PATH_TYPE)
5057
})
5158
}
59+
60+
pub fn tw_supports_taproot(coin: &dyn CoinContext) -> bool {
61+
coin.derivations().iter().any(|der| {
62+
der.name == Derivation::Taproot
63+
|| der.path.path().first().copied() == Some(TAPROOT_DERIVATION_PATH_TYPE)
64+
})
65+
}
5266
}

‎rust/frameworks/tw_utxo/src/address/standard_bitcoin.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,10 @@ impl StandardBitcoinAddress {
8282
if let Ok(segwit) = SegwitAddress::from_str_with_coin_and_prefix(coin, s, None) {
8383
return Ok(StandardBitcoinAddress::Segwit(segwit));
8484
}
85+
}
8586

86-
// TODO use `BitcoinDerivation::tw_supports_taproot` based on `registry.json`.
87+
// Try to parse a Taproot address if the coin supports it.
88+
if BitcoinDerivation::tw_supports_taproot(coin) {
8789
if let Ok(taproot) = TaprootAddress::from_str_with_coin_and_prefix(coin, s, None) {
8890
return Ok(StandardBitcoinAddress::Taproot(taproot));
8991
}
@@ -127,6 +129,11 @@ impl StandardBitcoinAddress {
127129
SegwitAddress::p2wpkh_with_coin_and_prefix(coin, public_key, None)
128130
.map(StandardBitcoinAddress::Segwit)
129131
},
132+
BitcoinDerivation::Taproot => {
133+
let no_merkle_root = None;
134+
TaprootAddress::p2tr_with_coin_and_prefix(coin, public_key, None, no_merkle_root)
135+
.map(StandardBitcoinAddress::Taproot)
136+
},
130137
}
131138
}
132139
}

‎rust/tw_any_coin/src/test_utils/address_utils.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ impl WithDestructor for TWAnyAddress {
3030
}
3131

3232
pub fn test_address_derive(coin: CoinType, private_key: &str, address: &str) {
33+
test_address_derive_with_derivation(coin, private_key, address, TWDerivation::Default)
34+
}
35+
36+
pub fn test_address_derive_with_derivation(
37+
coin: CoinType,
38+
private_key: &str,
39+
address: &str,
40+
derivation: TWDerivation,
41+
) {
3342
let coin_item = get_coin_item(coin).unwrap();
3443

3544
let private_key = TWPrivateKeyHelper::with_hex(private_key);
@@ -41,7 +50,7 @@ pub fn test_address_derive(coin: CoinType, private_key: &str, address: &str) {
4150
tw_any_address_create_with_public_key_derivation(
4251
public_key.ptr(),
4352
coin as u32,
44-
TWDerivation::Default as u32,
53+
derivation as u32,
4554
)
4655
});
4756

‎rust/tw_coin_entry/src/derivation.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub enum Derivation {
2323
Segwit,
2424
Legacy,
2525
Testnet,
26+
Taproot,
2627
/// Default derivation.
2728
#[default]
2829
#[serde(other)]

0 commit comments

Comments
 (0)