Skip to content

Commit

Permalink
Merge pull request #856 from LedgerHQ/add_taproot
Browse files Browse the repository at this point in the history
Add support of P2TR outputs
  • Loading branch information
viktorb-ledger authored Jun 20, 2022
2 parents 835028c + 528dccb commit 4ab0d73
Show file tree
Hide file tree
Showing 44 changed files with 867 additions and 199 deletions.
5 changes: 3 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,9 @@ jobs:
command: |
sudo apt-get update -q=2
sudo apt-get install -qy doxygen rsync software-properties-common
sudo add-apt-repository --update "deb http://httpredir.debian.org/debian stretch-backports main"
sudo apt-get install -qy -t stretch-backports cmake
wget https://cmake.org/files/v3.16/cmake-3.16.9.tar.gz
tar -xvf cmake-3.16.9.tar.gz
cd cmake-3.16.9 && ./bootstrap && make && sudo make install
- run: cmake -H. -Bbuild
- run:
command: make doc
Expand Down
15 changes: 12 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
## 4.3.0

- Send to Taproot addresses (Bitcoin only):
-- Bech32m encoding
-- Generation of P2TR outputs
-- ALLOW\_P2TR feature flag (off by default) blocks "Send to Taproot"

Side effect: can't serialize native segwit address to base58 and legacy address to bech32.

## 4.1.2

- Fix of Tezos fees calculation formula

## 4.1.1
> Unreleased
Expand All @@ -23,11 +32,11 @@
- Performance improvements
- Tezos: support of all tz adresses (tz1, tz2, tz3) by implementing ED25519, SECP256K1 and P256
- Tezos: support of grouped transaction messages
- Tezos: support of grouped transaction messages
- Tezos: fix smart contracts balance
- Tezos: concurent transactions (optimistic counter)
- Tezos: get current delegate
- Tezos: fees estimation
- Tezos: fees estimation
- Tezos: get token balance

## 4.0.6
Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ include_what_you_use() # add cmake conf option IWYU=ON to activate

# The project version number.
set(VERSION_MAJOR 4 CACHE STRING "Project major version number.")
set(VERSION_MINOR 1 CACHE STRING "Project minor version number.")
set(VERSION_PATCH 15 CACHE STRING "Project patch version number.")
set(VERSION_MINOR 3 CACHE STRING "Project minor version number.")
set(VERSION_PATCH 0 CACHE STRING "Project patch version number.")
mark_as_advanced(VERSION_MAJOR VERSION_MINOR VERSION_PATCH)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY build)
Expand Down
3 changes: 3 additions & 0 deletions core/idl/bitcoin/addresses.djinni
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ BitcoinLikeAddress = interface +c {
# Checks if the given address is a P2WPKH address
# @return True if the keychain engine is P2WPKH
isP2WPKH(): bool;
# Checks if the given address is a P2TR address
# @return True if the keychain engine is P2TR
isP2TR(): bool;
}

# The xPUB definition for Bitcoin.
Expand Down
4 changes: 4 additions & 0 deletions core/idl/wallet/configuration.djinni
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ KeychainEngines = interface +c {
const BIP49_P2SH: string = "BIP49_P2SH";
const BIP173_P2WPKH: string = "BIP173_P2WPKH";
const BIP173_P2WSH: string = "BIP173_P2WSH";
const BIP350_P2TR: string = "BIP350_P2TR";
}

# Class of constants representing the engines to rely on for synchronization.
Expand Down Expand Up @@ -117,6 +118,9 @@ Configuration = interface +c {
# Grace period (in seconds) during which no "missing" transactions on-chain should trigger
# deletion from database
const MEMPOOL_GRACE_PERIOD_SECS: string = "MEMPOOL_GRACE_PERIOD_SECS";

# Allow the generation of the P2TR (Taproot) outputs
const ALLOW_P2TR: string = "ALLOW_P2TR";
}

# Configuration of wallet pools.
Expand Down
6 changes: 6 additions & 0 deletions core/src/api/BitcoinLikeAddress.hpp

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

2 changes: 2 additions & 0 deletions core/src/api/Configuration.cpp

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

3 changes: 3 additions & 0 deletions core/src/api/Configuration.hpp

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

2 changes: 2 additions & 0 deletions core/src/api/KeychainEngines.cpp

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

2 changes: 2 additions & 0 deletions core/src/api/KeychainEngines.hpp

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

106 changes: 79 additions & 27 deletions core/src/bitcoin/BitcoinLikeAddress.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include <crypto/HashAlgorithm.h>
#include <crypto/HASH160.hpp>
#include <wallet/bitcoin/scripts/operators.h>
#include <wallet/currencies.hpp>

namespace ledger {
namespace core {
Expand All @@ -62,8 +63,8 @@ namespace ledger {
return getVersionFromKeychainEngine(_keychainEngine, _params);
}

std::vector<uint8_t> BitcoinLikeAddress::getVersionFromKeychainEngine(const std::string &keychainEngine,
const api::BitcoinLikeNetworkParameters &params) const {
std::vector<uint8_t> BitcoinLikeAddress::getVersionFromKeychainEngine(const std::string &keychainEngine,
const api::BitcoinLikeNetworkParameters &params) {

if (keychainEngine == api::KeychainEngines::BIP32_P2PKH) {
return params.P2PKHVersion;
Expand All @@ -75,6 +76,9 @@ namespace ledger {
} else if (keychainEngine == api::KeychainEngines::BIP173_P2WSH) {
auto bech32Params = Bech32Parameters::getBech32Params(params.Identifier);
return bech32Params.P2WSHVersion;
} else if (keychainEngine == api::KeychainEngines::BIP350_P2TR) {
auto bech32Params = Bech32Parameters::getBech32Params(params.Identifier);
return bech32Params.P2TRVersion;
}
throw make_exception(api::ErrorCode::INVALID_ARGUMENT, "Invalid Keychain Engine: ", keychainEngine);
};
Expand All @@ -88,22 +92,11 @@ namespace ledger {
}

std::string BitcoinLikeAddress::toBase58() {
auto config = std::make_shared<DynamicObject>();
config->putString("networkIdentifier", _params.Identifier);
return Base58::encodeWithChecksum(vector::concat(getVersionFromKeychainEngine(_keychainEngine, _params), _hash160), config);
}

std::string toBech32Helper(const std::string &keychainEngine,
const std::vector<uint8_t> &hash160,
const api::BitcoinLikeNetworkParameters &params) {

auto bech32 = Bech32Factory::newBech32Instance(params.Identifier).getValue();
auto witnessVersion = (keychainEngine == api::KeychainEngines::BIP173_P2WPKH) ? bech32->getBech32Params().P2WPKHVersion : bech32->getBech32Params().P2WSHVersion;
return bech32->encode(hash160, witnessVersion);
return toBase58Impl();
}

std::string BitcoinLikeAddress::toBech32() {
return toBech32Helper(_keychainEngine, _hash160, _params);
return toBech32Impl();
}

bool BitcoinLikeAddress::isP2SH() {
Expand All @@ -122,21 +115,41 @@ namespace ledger {
return _keychainEngine == api::KeychainEngines::BIP173_P2WPKH;
}

bool BitcoinLikeAddress::isP2TR() {
return _keychainEngine == api::KeychainEngines::BIP350_P2TR;
}

std::experimental::optional<std::string> BitcoinLikeAddress::getDerivationPath() {
return _derivationPath.toOptional();
}

std::string BitcoinLikeAddress::toBase58() const {
bool BitcoinLikeAddress::isKeychainEngineBech32Compatible() const {
return _keychainEngine == api::KeychainEngines::BIP173_P2WPKH
|| _keychainEngine == api::KeychainEngines::BIP173_P2WSH
|| _keychainEngine == api::KeychainEngines::BIP350_P2TR;
}

std::string BitcoinLikeAddress::toBase58Impl() const {
if (_keychainEngine != api::KeychainEngines::BIP32_P2PKH && _keychainEngine != api::KeychainEngines::BIP49_P2SH) {
throw Exception(api::ErrorCode::INVALID_BASE58_FORMAT, "Base58 format only available for api::KeychainEngines::BIP32_P2PKH and api::KeychainEngines::BIP49_P2SH");
throw Exception(api::ErrorCode::INVALID_BASE58_FORMAT,
"Base58 format only available for api::KeychainEngines::BIP32_P2PKH and api::KeychainEngines::BIP49_P2SH");
}

auto config = std::make_shared<DynamicObject>();
config->putString("networkIdentifier", _params.Identifier);
return Base58::encodeWithChecksum(vector::concat(getVersionFromKeychainEngine(_keychainEngine, _params), _hash160), config);
}

std::string BitcoinLikeAddress::toBech32() const {
return toBech32Helper(_keychainEngine, _hash160, _params);
std::string BitcoinLikeAddress::toBech32Impl() const {
if (!isKeychainEngineBech32Compatible()) {
throw Exception(api::ErrorCode::INVALID_BECH32_FORMAT,
"Bech32 format only available for api::KeychainEngines::BIP173_P2WPKH/P2WSH and api::KeychainEngines::BIP350_P2TR");
}

auto bech32 = Bech32Factory::newBech32Instance(_params.Identifier).getValue();
std::vector<uint8_t> witnessVersion
= BitcoinLikeAddress::getVersionFromKeychainEngine(_keychainEngine, _params);
return bech32->encode(_hash160, witnessVersion);
}

std::shared_ptr<ledger::core::AbstractAddress>
Expand All @@ -156,10 +169,14 @@ namespace ledger {
}

std::string BitcoinLikeAddress::getStringAddress() const {
if (_keychainEngine == api::KeychainEngines::BIP173_P2WPKH || _keychainEngine == api::KeychainEngines::BIP173_P2WSH) {
return toBech32();
if (isKeychainEngineBech32Compatible()) {
return toBech32Impl();
}
return toBase58();
return toBase58Impl();
}

const std::string& BitcoinLikeAddress::getKeychainEngine() const {
return _keychainEngine;
}

std::shared_ptr<BitcoinLikeAddress> BitcoinLikeAddress::fromBase58(const std::string &address,
Expand Down Expand Up @@ -191,10 +208,44 @@ namespace ledger {
std::shared_ptr<BitcoinLikeAddress> BitcoinLikeAddress::fromBech32(const std::string& address,
const api::Currency& currency,
const Option<std::string>& derivationPath) {
auto& params = currency.bitcoinLikeNetworkParameters.value();
auto bech32 = Bech32Factory::newBech32Instance(params.Identifier).getValue();
auto decoded = bech32->decode(address);
auto keychainEngine = (decoded.second.size() == 32 || decoded.first == bech32->getBech32Params().P2WSHVersion) ? api::KeychainEngines::BIP173_P2WSH : api::KeychainEngines::BIP173_P2WPKH;
const auto& params = currency.bitcoinLikeNetworkParameters.value();
const auto bech32 = Bech32Factory::newBech32Instance(params.Identifier).getValue();
const auto decoded = bech32->decode(address);
std::string keychainEngine;
const auto bech32_params = bech32->getBech32Params();
const size_t payload_sz = decoded.second.size();
if (bech32_params.P2WSHVersion == bech32_params.P2WPKHVersion) {
// Bitcoin-like case (both are equal to zero) => detect keychain engine by length
if (decoded.first == bech32_params.P2WSHVersion) {
if (payload_sz == 32) {
keychainEngine = api::KeychainEngines::BIP173_P2WSH;
} else if (payload_sz == 20) {
keychainEngine = api::KeychainEngines::BIP173_P2WPKH;
} else {
throw Exception(api::ErrorCode::INVALID_BECH32_FORMAT, "Unexpected wintess program length");
}
} else if (decoded.first == bech32_params.P2TRVersion && payload_sz == 32) {
if (currency.name != currencies::BITCOIN.name &&
currency.name != currencies::BITCOIN_TESTNET.name &&
currency.name != currencies::BITCOIN_REGTEST.name) {
throw make_exception(api::ErrorCode::UNSUPPORTED_OPERATION, "Can't create Taproot address ({}) in currency {}", address, currency.name);
}
keychainEngine = api::KeychainEngines::BIP350_P2TR;
} else {
throw Exception(api::ErrorCode::INVALID_BECH32_FORMAT, "Unknown form of Bech32 format (Bitcoin like)");
}
} else {
// Bitcoin Cash like case (P2WPKH is 0 and P2WSH is 8), length of hash can be different
// https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md
if (decoded.first == bech32_params.P2WSHVersion && (payload_sz == 32 || payload_sz == 20)) {
keychainEngine = api::KeychainEngines::BIP173_P2WSH;
} else if (decoded.first == bech32_params.P2WPKHVersion && payload_sz == 20) {
keychainEngine = api::KeychainEngines::BIP173_P2WPKH;
} else {
throw Exception(api::ErrorCode::INVALID_BECH32_FORMAT, "Unknown form of Bech32 format (BCH)");
}
}

return std::make_shared<ledger::core::BitcoinLikeAddress>(currency,
decoded.second,
keychainEngine,
Expand All @@ -210,8 +261,8 @@ namespace ledger {
} else if (keychainEngine == api::KeychainEngines::BIP49_P2SH || keychainEngine == api::KeychainEngines::BIP173_P2WSH) {
auto hash160 = fromPublicKeyToHash160(pubKey->derivePublicKey(derivationPath), pubKey->deriveHash160(derivationPath), currency, keychainEngine);
return BitcoinLikeAddress(currency, hash160, keychainEngine).toString();

}
// keychainEngine == BIP350_P2TR: we don't create Taproot addresses from extended pubkey.
throw make_exception(api::ErrorCode::INVALID_ARGUMENT, "Invalid Keychain Engine: ", keychainEngine);
}

Expand All @@ -237,6 +288,7 @@ namespace ledger {
witnessScript.push_back(btccore::OP_CHECKSIG);
return SHA256::bytesToBytesHash(witnessScript);
}
// keychainEngine == BIP350_P2TR: we don't create a hash160 from the Taproot public key
throw make_exception(api::ErrorCode::INVALID_ARGUMENT, "Invalid Keychain Engine: ", keychainEngine);
}

Expand Down
16 changes: 11 additions & 5 deletions core/src/bitcoin/BitcoinLikeAddress.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,22 @@ namespace ledger {
const std::string &keychainEngine,
const Option<std::string>& derivationPath = Option<std::string>());
virtual std::vector<uint8_t> getVersion() override;
std::vector<uint8_t> getVersionFromKeychainEngine(const std::string &keychainEngine,
const api::BitcoinLikeNetworkParameters &params) const;
virtual std::vector<uint8_t> getHash160() override;
virtual api::BitcoinLikeNetworkParameters getNetworkParameters() override;
virtual std::string toBase58() override;
virtual std::string toBech32() override;
std::string toBase58() const;
std::string toBech32() const;
virtual bool isP2SH() override;
virtual bool isP2PKH() override;
virtual bool isP2WSH() override;
virtual bool isP2WPKH() override;
virtual bool isP2TR() override;
virtual optional<std::string> getDerivationPath() override;

std::string toString() override;
std::string getStringAddress() const;

const std::string& getKeychainEngine() const;

static std::shared_ptr<AbstractAddress> parse(const std::string& address, const api::Currency& currency,
const Option<std::string>& derivationPath = Option<std::string>());
static std::shared_ptr<BitcoinLikeAddress> fromBase58(const std::string& address,
Expand All @@ -87,8 +86,15 @@ namespace ledger {
const api::Currency &currency,
const std::string &keychainEngine);


private:
std::string toBase58Impl() const;
std::string toBech32Impl() const;

bool isKeychainEngineBech32Compatible() const;

static std::vector<uint8_t> getVersionFromKeychainEngine(const std::string &keychainEngine,
const api::BitcoinLikeNetworkParameters &params);

const std::vector<uint8_t> _hash160;
const api::BitcoinLikeNetworkParameters _params;
const Option<std::string> _derivationPath;
Expand Down
Loading

0 comments on commit 4ab0d73

Please sign in to comment.