Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bitcoin]: Add support for Taproot address generation #4074

Merged
merged 6 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading