Skip to content

Commit

Permalink
SFT-UNKN: Support OP_RETURN.
Browse files Browse the repository at this point in the history
  • Loading branch information
jeandudey committed Feb 28, 2025
1 parent a5077c9 commit d738a93
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 6 deletions.
1 change: 1 addition & 0 deletions psbt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ bitflags = { workspace = true }
either = { workspace = true }
embedded-io = { workspace = true }
env_logger = { workspace = true, optional = true }
faster-hex = { workspace = true }
foundation-bip32 = { workspace = true }
heapless = { workspace = true }
log = { workspace = true }
Expand Down
103 changes: 102 additions & 1 deletion psbt/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-or-later

use bech32::{hrp, primitives::segwit::MAX_STRING_LENGTH, segwit, Hrp};
use core::fmt;
use core::{fmt, str};
use faster_hex::hex_encode;
use heapless::{String, Vec};
use tinyvec::SliceVec;

Expand Down Expand Up @@ -32,6 +33,7 @@ pub enum AddressType {
P2PKH,
P2SH,
P2PK,
Return,
}

impl AddressType {
Expand Down Expand Up @@ -89,6 +91,41 @@ fn render_base58_address(
Ok(())
}

/// Render a string truncating it if it does not fit in the result.
///
/// Places an ellipsis at the end if it does not fit.
fn render_truncated<const N: usize>(s: &str, result: &mut String<N>) {
// Easy case, string fits.
if s.len() <= result.capacity().saturating_sub(result.len()) {
result
.push_str(s)
.expect("s length should be less than the capacity");
return;
}

for c in s.chars() {
let mut buf = [0; 4];
let encoded = c.encode_utf8(&mut buf);
if encoded.len() > result.capacity().saturating_sub(result.len()) {
break;
}

result.push_str(encoded).expect("capacity should be enough");
}

// Reserve capacity for the ellipsis.
let remaining = result.capacity().saturating_sub(result.len());
if remaining < 3 {
for _ in remaining..3 {
result.pop();
}
}

result
.push_str("...")
.expect("ellipsis length should have been reserved");
}

/// Render a Bitcoin address as text.
///
/// The result is stored in `s`.
Expand Down Expand Up @@ -141,6 +178,23 @@ pub fn render(
}
// Maybe render the public key as hex.
AddressType::P2PK => return Err(RenderAddressError::Unimplemented),
// OP_RETURN, display message if encoded as UTF-8 or just the
// hexadecimal bytes.
AddressType::Return => {
const REMAINING_LENGTH: usize = MAX_STRING_LENGTH - "OP_RETURN:".len();

s.push_str("OP_RETURN:").expect("should have enough space");

match str::from_utf8(data) {
Ok(message) => render_truncated(message, s),
Err(_) => {
let mut buf = [0; REMAINING_LENGTH];
let hex = hex_encode(&data[..REMAINING_LENGTH / 2], &mut buf)
.expect("length of data should fit in buf");
render_truncated(hex, s);
}
}
}
};

Ok(())
Expand All @@ -149,6 +203,7 @@ pub fn render(
#[cfg(test)]
mod tests {
use super::*;
use heapless::String;

#[test]
fn network_bech32_hrp() {
Expand All @@ -165,6 +220,27 @@ mod tests {
assert!(testnet_hrp.is_valid_on_signet());
}

#[test]
fn render_truncated_cases() {
let mut result: String<4> = String::new();

// should fit and contents should append
result.push('a').unwrap();
render_truncated("bcd", &mut result);
assert_eq!(result, "abcd");
result.clear();

// should fit
render_truncated("abcd", &mut result);
assert_eq!(result, "abcd");
result.clear();

// should truncate
render_truncated("abcde", &mut result);
assert_eq!(result, "a...");
result.clear();
}

#[test]
fn render_invalid_p2wpkh() {
let mut s = String::new();
Expand All @@ -190,4 +266,29 @@ mod tests {
Err(RenderAddressError::InvalidAddressData)
);
}

#[test]
fn render_op_return() {
const DATA0: &[u8] = &[
0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21,
];

let mut s = String::new();
render(Network::Mainnet, AddressType::Return, &DATA0, &mut s).unwrap();
assert_eq!(s, "OP_RETURN:Hello, World!");

const DATA1: &[u8] = &[
0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x48,
0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x48, 0x65,
0x6C, 0x6C, 0x6F, 0x2C, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x48, 0x65, 0x6C,
0x6C, 0x6F, 0x2C, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x48, 0x65, 0x6C, 0x6C,
0x6F, 0x2C, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x48, 0x65, 0x6C, 0x6C, 0x6F,
0x2C, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C,
0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21,
];

let mut s = String::new();
render(Network::Mainnet, AddressType::Return, &DATA1, &mut s).unwrap();
assert_eq!(s, "OP_RETURN:Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World...");
}
}
16 changes: 12 additions & 4 deletions psbt/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ impl<I> Transaction<I> {
}

/// A transaction input.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Input<I> {
pub previous_output: OutputPoint,
pub script_sig: I,
pub sequence: u32,
}

/// A transaction output.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Output<I> {
/// Number of satoshis this output is worth.
pub value: i64,
Expand All @@ -93,12 +93,20 @@ where
///
/// So, written for performance, not readability. That is also the reason
/// of the nested ifs.
pub fn address(&self) -> Option<(AddressType, Vec<u8, 35>)> {
pub fn address(&self) -> Option<(AddressType, Vec<u8, 90>)> {
let len = self.script_pubkey.input_len();
let mut iter = self.script_pubkey.iter_elements();
let b0 = iter.next();
let b1 = iter.next();

// OP_RETURN.
if b0 == Some(0x6A) {
return Some((
AddressType::Return,
self.script_pubkey.slice(1..).iter_elements().collect::<_>(),
));
}

// P2WPKH (BIP-0141).
//
// 0x0014 and the rest is the RIPEMD-160 hash of the public key.
Expand Down Expand Up @@ -190,7 +198,7 @@ where
}

/// Points to the output of a transaction.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct OutputPoint {
/// The transaction ID of the transaction holding the output to spend.
pub hash: Txid,
Expand Down
2 changes: 1 addition & 1 deletion psbt/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ pub struct OutputDetails {
/// Address type.
pub address_type: AddressType,
/// Address data.
pub data: Vec<u8, 35>,
pub data: Vec<u8, 90>,
}

/// Validate the output.
Expand Down
20 changes: 20 additions & 0 deletions psbt/tests/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,23 @@ fn test_output_p2wsh() {
_ => panic!(),
}
}

#[test]
fn test_output_return() {
const SCRIPT_PUBKEY: &[u8] = &[
0x6a, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21,
];
const DATA: &[u8] = &[
0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2C, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21,
];

let output = Output {
value: 0,
script_pubkey: SCRIPT_PUBKEY,
};

match output.address() {
Some((AddressType::Return, data)) => assert_eq!(data, DATA),
_ => panic!(),
}
}

0 comments on commit d738a93

Please sign in to comment.