Skip to content
Open
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: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
- simulator: simulate a Nova device
- Add API call to fetch multiple xpubs at once
- Add the option for the simulator to write its memory to file.
- Bitcoin: add support for OP_RETURN outputs

### 9.23.1
- EVM: add HyperEVM (HYPE) and SONIC (S) to known networks
Expand Down
1 change: 1 addition & 0 deletions messages/btc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ enum BTCOutputType {
P2WPKH = 3;
P2WSH = 4;
P2TR = 5;
OP_RETURN = 6;
}

message BTCSignOutputRequest {
Expand Down
1 change: 1 addition & 0 deletions py/bitbox02/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

# 7.1.0
- Add `btc_xpubs()` to fetch multiple xpubs at once
- Bitcoin: add support for OP_RETURN outputs

# 7.0.0
- get_info: add optional device initialized boolean to returned tuple
Expand Down
10 changes: 10 additions & 0 deletions py/bitbox02/bitbox02/bitbox02/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,16 @@ def btc_sign(
# Attaching output info supported since v9.22.0.
self._require_atleast(semver.VersionInfo(9, 22, 0))

if any(
map(
lambda output: isinstance(output, BTCOutputExternal)
and output.type == btc.BTCOutputType.OP_RETURN,
outputs,
)
):
# OP_RETURN supported sice v9.24.0
self._require_atleast(semver.VersionInfo(9, 24, 0))

supports_antiklepto = self.version >= semver.VersionInfo(9, 4, 0)

sigs: List[Tuple[int, bytes]] = []
Expand Down
4 changes: 2 additions & 2 deletions py/bitbox02/bitbox02/communication/generated/btc_pb2.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class _BTCOutputTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
P2WPKH: _BTCOutputType.ValueType # 3
P2WSH: _BTCOutputType.ValueType # 4
P2TR: _BTCOutputType.ValueType # 5
OP_RETURN: _BTCOutputType.ValueType # 6

class BTCOutputType(_BTCOutputType, metaclass=_BTCOutputTypeEnumTypeWrapper): ...

Expand All @@ -79,6 +80,7 @@ P2SH: BTCOutputType.ValueType # 2
P2WPKH: BTCOutputType.ValueType # 3
P2WSH: BTCOutputType.ValueType # 4
P2TR: BTCOutputType.ValueType # 5
OP_RETURN: BTCOutputType.ValueType # 6
global___BTCOutputType = BTCOutputType

@typing.final
Expand Down
38 changes: 38 additions & 0 deletions py/send_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,43 @@ def _sign_btc_policy(self) -> None:
for input_index, sig in sigs:
print("Signature for input {}: {}".format(input_index, sig.hex()))

def _sign_btc_op_return(
self,
format_unit: "bitbox02.btc.BTCSignInitRequest.FormatUnit.V" = bitbox02.btc.BTCSignInitRequest.FormatUnit.DEFAULT,
) -> None:
# pylint: disable=no-member
bip44_account: int = 0 + HARDENED
inputs, outputs = _btc_demo_inputs_outputs(bip44_account)
outputs.append(
bitbox02.BTCOutputExternal(
output_type=bitbox02.btc.OP_RETURN,
output_payload=b"hello world",
value=0,
)
)
sigs = self._device.btc_sign(
bitbox02.btc.BTC,
[
bitbox02.btc.BTCScriptConfigWithKeypath(
script_config=bitbox02.btc.BTCScriptConfig(
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH
),
keypath=[84 + HARDENED, 0 + HARDENED, bip44_account],
),
bitbox02.btc.BTCScriptConfigWithKeypath(
script_config=bitbox02.btc.BTCScriptConfig(
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
),
keypath=[49 + HARDENED, 0 + HARDENED, bip44_account],
),
],
inputs=inputs,
outputs=outputs,
format_unit=format_unit,
)
for input_index, sig in sigs:
print("Signature for input {}: {}".format(input_index, sig.hex()))

def _sign_btc_tx_from_raw(self) -> None:
"""
Experiment with testnet transactions.
Expand Down Expand Up @@ -896,6 +933,7 @@ def _sign_btc_tx(self) -> None:
("Taproot inputs", self._sign_btc_taproot_inputs),
("Taproot output", self._sign_btc_taproot_output),
("Policy", self._sign_btc_policy),
("OP_RETURN", self._sign_btc_op_return),
("From testnet tx ID", self._sign_btc_tx_from_raw),
)
choice = ask_user(choices)
Expand Down
153 changes: 117 additions & 36 deletions src/rust/bitbox02-rust/src/hww/api/bitcoin/common.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022-2024 Shift Crypto AG
// Copyright 2022-2025 Shift Crypto AG
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -25,10 +25,11 @@ pub use pb::btc_sign_init_request::FormatUnit;
pub use pb::{BtcCoin, BtcOutputType};

use super::script_configs::{ValidatedScriptConfig, ValidatedScriptConfigWithKeypath};
use super::{multisig, params::Params, script};
use super::{multisig, params::Params};

use sha2::{Digest, Sha256};

use bitcoin::ScriptBuf;
use bitcoin::bech32;
use bitcoin::hashes::Hash;

Expand Down Expand Up @@ -210,7 +211,8 @@ impl Payload {
}
}

/// Converts a payload to an address.
/// Converts a payload to an address. Returns an error for invalid input or if an address does
/// not exist for the output type (e.g. OP_RETURN).
pub fn address(&self, params: &Params) -> Result<String, ()> {
let payload = self.data.as_slice();
match self.output_type {
Expand Down Expand Up @@ -251,54 +253,51 @@ impl Payload {
}
encode_segwit_addr(params.bech32_hrp, 1, payload)
}
BtcOutputType::OpReturn => Err(()),
}
}

/// Computes the pkScript from a pubkey hash or script hash or pubkey, depending on the output type.
/// Computes the pkScript from a pubkey hash or script hash or pubkey, depending on the output
/// type.
pub fn pk_script(&self, params: &Params) -> Result<Vec<u8>, Error> {
let payload = self.data.as_slice();
match self.output_type {
BtcOutputType::Unknown => Err(Error::InvalidInput),
let script = match self.output_type {
BtcOutputType::Unknown => return Err(Error::InvalidInput),
BtcOutputType::P2pkh => {
if payload.len() != HASH160_LEN {
return Err(Error::Generic);
}
let mut result = vec![script::OP_DUP, script::OP_HASH160];
script::push_data(&mut result, payload);
result.extend_from_slice(&[script::OP_EQUALVERIFY, script::OP_CHECKSIG]);
Ok(result)
let pk_hash =
bitcoin::PubkeyHash::from_slice(payload).map_err(|_| Error::Generic)?;

ScriptBuf::new_p2pkh(&pk_hash)
}
BtcOutputType::P2sh => {
if payload.len() != HASH160_LEN {
return Err(Error::Generic);
}
let mut result = vec![script::OP_HASH160];
script::push_data(&mut result, payload);
result.push(script::OP_EQUAL);
Ok(result)
let script_hash =
bitcoin::ScriptHash::from_slice(payload).map_err(|_| Error::Generic)?;
ScriptBuf::new_p2sh(&script_hash)
}
BtcOutputType::P2wpkh | BtcOutputType::P2wsh => {
if (self.output_type == BtcOutputType::P2wpkh && payload.len() != HASH160_LEN)
|| (self.output_type == BtcOutputType::P2wsh && payload.len() != SHA256_LEN)
{
return Err(Error::Generic);
}
let mut result = vec![script::OP_0];
script::push_data(&mut result, payload);
Ok(result)
BtcOutputType::P2wpkh => {
let wpkh = bitcoin::WPubkeyHash::from_slice(payload).map_err(|_| Error::Generic)?;
ScriptBuf::new_p2wpkh(&wpkh)
}
BtcOutputType::P2wsh => {
let wsh = bitcoin::WScriptHash::from_slice(payload).map_err(|_| Error::Generic)?;
ScriptBuf::new_p2wsh(&wsh)
}
BtcOutputType::P2tr => {
if !params.taproot_support {
return Err(Error::InvalidInput);
}
if payload.len() != 32 {
return Err(Error::Generic);
}
let mut result = vec![script::OP_1];
script::push_data(&mut result, payload);
Ok(result)
let tweaked = bitcoin::key::TweakedPublicKey::dangerous_assume_tweaked(
bitcoin::XOnlyPublicKey::from_slice(payload).map_err(|_| Error::Generic)?,
);
ScriptBuf::new_p2tr_tweaked(tweaked)
}
}
BtcOutputType::OpReturn => {
let pushbytes: &bitcoin::script::PushBytes =
payload.try_into().map_err(|_| Error::InvalidInput)?;
ScriptBuf::new_op_return(pushbytes)
}
};
Ok(script.into_bytes())
}
}

Expand Down Expand Up @@ -622,4 +621,86 @@ mod tests {
b"\x25\x0e\xc8\x02\xb6\xd3\xdb\x98\x42\xd1\xbd\xbe\x0e\xe4\x8d\x52\xf9\xa4\xb4\x6e\x60\xcb\xbb\xab\x3b\xcc\x4e\xe9\x15\x73\xfc\xe8"
);
}

#[test]
fn test_pkscript() {
let params = super::super::params::get(pb::BtcCoin::Btc);

let payload = Payload {
data: vec![],
output_type: BtcOutputType::Unknown,
};
assert_eq!(payload.pk_script(params), Err(Error::InvalidInput));

struct Test {
payload: &'static str,
output_type: BtcOutputType,
expected_pkscript: &'static str,
}

let tests = [
Test {
payload: "669c6cb1883c50a1b10c34bd1693c1f34fe3d798",
output_type: BtcOutputType::P2pkh,
expected_pkscript: "76a914669c6cb1883c50a1b10c34bd1693c1f34fe3d79888ac",
},
Test {
payload: "b59e844a19063a882b3c34b64b941a8acdad74ee",
output_type: BtcOutputType::P2sh,
expected_pkscript: "a914b59e844a19063a882b3c34b64b941a8acdad74ee87",
},
Test {
payload: "b7cfb87a9806bb232e64f64e714785bd8366596b",
output_type: BtcOutputType::P2wpkh,
expected_pkscript: "0014b7cfb87a9806bb232e64f64e714785bd8366596b",
},
Test {
payload: "526e8e589b4bf1de80774986d972aed96ae70f17572d35fe89e61e9e88e2dd4a",
output_type: BtcOutputType::P2wsh,
expected_pkscript: "0020526e8e589b4bf1de80774986d972aed96ae70f17572d35fe89e61e9e88e2dd4a",
},
Test {
payload: "a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c",
output_type: BtcOutputType::P2tr,
expected_pkscript: "5120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c",
},
Test {
payload: "aabbcc",
output_type: BtcOutputType::OpReturn,
expected_pkscript: "6a03aabbcc",
},
Test {
payload: "",
output_type: BtcOutputType::OpReturn,
expected_pkscript: "6a00",
},
Test {
// 80 byte payload
payload: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
output_type: BtcOutputType::OpReturn,
expected_pkscript: "6a4c50aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
];

for test in tests {
// OK
let payload = Payload {
data: hex::decode(test.payload).unwrap(),
output_type: test.output_type,
};
assert_eq!(
hex::encode(payload.pk_script(params).unwrap()),
test.expected_pkscript,
);

// Payload of wrong size. Does not apply to OpReturn, almost any size is accepted.
if test.output_type != BtcOutputType::OpReturn {
let payload = Payload {
data: hex::decode(&test.payload[2..]).unwrap(),
output_type: test.output_type,
};
assert_eq!(payload.pk_script(params), Err(Error::Generic));
}
}
}
}
37 changes: 0 additions & 37 deletions src/rust/bitbox02-rust/src/hww/api/bitcoin/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,6 @@

use alloc::vec::Vec;

// https://en.bitcoin.it/wiki/Script
pub const OP_0: u8 = 0;
pub const OP_1: u8 = 0x51;
pub const OP_HASH160: u8 = 0xa9;
pub const OP_DUP: u8 = 0x76;
pub const OP_EQUALVERIFY: u8 = 0x88;
pub const OP_CHECKSIG: u8 = 0xac;
pub const OP_EQUAL: u8 = 0x87;

/// Serialize a number in the VarInt encoding.
/// https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
pub fn serialize_varint(value: u64) -> Vec<u8> {
Expand All @@ -45,12 +36,6 @@ pub fn serialize_varint(value: u64) -> Vec<u8> {
out
}

/// Performs a data push onto `v`: the varint length of data followed by data.
pub fn push_data(v: &mut Vec<u8>, data: &[u8]) {
v.extend_from_slice(&serialize_varint(data.len() as _));
v.extend_from_slice(data);
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -111,26 +96,4 @@ mod tests {
b"\xff\xff\xff\xff\xff\xff\xff\xff\xff"
);
}

#[test]
fn test_push_data() {
assert_eq!(
{
let mut v = Vec::new();
push_data(&mut v, b"");
v
},
vec![0]
);

// Data with length 255.
assert_eq!(
{
let mut v = Vec::new();
push_data(&mut v, b"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
v
},
b"\xfd\xff\x00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_vec(),
);
}
}
Loading