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
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ pub fn new_p2tr_script_path(pubkey: &H264, merkle_root: &H256) -> Script {
}

pub fn new_op_return(data: &[u8]) -> Script {
let mut s = Script::with_capacity(83);
// Capacity: 1 (OP_RETURN) + 3 (max push opcode for 10000 bytes is OP_PUSHDATA2 + 2 bytes) + data.len()
let mut s = Script::with_capacity(4 + data.len());
s.push(OP_RETURN);
s.push_slice(data);
s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ use tw_hash::sha2::sha256;
use tw_hash::{H160, H256};
use tw_keypair::{ecdsa, schnorr};

pub const OP_RETURN_DATA_LIMIT: usize = 80;
/// Maximum length for OP_RETURN data payload.
/// Bitcoin Core v30 increased the default -datacarriersize from 83 to 10000 bytes.
/// This constant represents the maximum payload data size (not including OP_RETURN and push opcodes).
pub const OP_RETURN_DATA_LIMIT: usize = 10000;

pub struct OutputBuilder {
amount: Amount,
Expand Down Expand Up @@ -169,3 +172,65 @@ impl OutputBuilder {
})
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_op_return_small_data() {
// Small data (< 75 bytes) uses direct push
let data = vec![0x42; 50];
let output = OutputBuilder::new(0).op_return(&data).unwrap();
let script = output.script_pubkey.as_slice();
assert_eq!(script[0], 0x6a); // OP_RETURN
assert_eq!(script[1], 50); // direct push length
assert_eq!(script.len(), 2 + 50);
}

#[test]
fn test_op_return_pushdata1() {
// Data > 75 bytes but <= 255 uses OP_PUSHDATA1
let data = vec![0x42; 100];
let output = OutputBuilder::new(0).op_return(&data).unwrap();
let script = output.script_pubkey.as_slice();
assert_eq!(script[0], 0x6a); // OP_RETURN
assert_eq!(script[1], 0x4c); // OP_PUSHDATA1
assert_eq!(script[2], 100); // length
assert_eq!(script.len(), 3 + 100);
}

#[test]
fn test_op_return_pushdata2() {
// Data > 255 bytes uses OP_PUSHDATA2
let data = vec![0x42; 1000];
let output = OutputBuilder::new(0).op_return(&data).unwrap();
let script = output.script_pubkey.as_slice();
assert_eq!(script[0], 0x6a); // OP_RETURN
assert_eq!(script[1], 0x4d); // OP_PUSHDATA2
assert_eq!(script[2], 0xe8); // length low byte (1000 = 0x03e8)
assert_eq!(script[3], 0x03); // length high byte
assert_eq!(script.len(), 4 + 1000);
}

#[test]
fn test_op_return_max_size() {
// Maximum allowed size (10000 bytes - Bitcoin Core v30 default)
let data = vec![0x42; 10000];
let output = OutputBuilder::new(0).op_return(&data).unwrap();
let script = output.script_pubkey.as_slice();
assert_eq!(script[0], 0x6a); // OP_RETURN
assert_eq!(script[1], 0x4d); // OP_PUSHDATA2
assert_eq!(script[2], 0x10); // length low byte (10000 = 0x2710)
assert_eq!(script[3], 0x27); // length high byte
assert_eq!(script.len(), 4 + 10000);
}

#[test]
fn test_op_return_too_large() {
// Data > 10000 bytes should fail
let data = vec![0x42; 10001];
let result = OutputBuilder::new(0).op_return(&data);
assert!(result.is_err());
}
}
19 changes: 13 additions & 6 deletions src/Bitcoin/Script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -333,17 +333,25 @@ Script Script::buildPayToScriptHashReplay(const Data& scriptHash, const Data& bl
}


// Append to the buffer the length for the upcoming data (push). Supported length range: 0-75 bytes
// Append to the buffer the length for the upcoming data (push).
// Supports length ranges: 0-75 (OP_PUSHBYTES_N), 76-255 (OP_PUSHDATA1), 256-65535 (OP_PUSHDATA2).
void pushDataLength(Data& buffer, size_t len) {
assert(len <= 255);
if (len < static_cast<byte>(OP_PUSHDATA1)) {
// up to 75 bytes, simple OP_PUSHBYTES with len
buffer.push_back(static_cast<byte>(len));
return;
}
// 75 < len < 256, OP_PUSHDATA with 1-byte len
buffer.push_back(OP_PUSHDATA1);
buffer.push_back(static_cast<byte>(len));
if (len <= 0xff) {
// 75 < len <= 255, OP_PUSHDATA1 with 1-byte len
buffer.push_back(OP_PUSHDATA1);
buffer.push_back(static_cast<byte>(len));
return;
}
// 256 <= len <= 65535, OP_PUSHDATA2 with 2-byte len (little-endian)
assert(len <= 0xffff);
buffer.push_back(OP_PUSHDATA2);
buffer.push_back(static_cast<byte>(len & 0xff));
buffer.push_back(static_cast<byte>((len >> 8) & 0xff));
}

Script Script::buildPayToV0WitnessProgram(const Data& program) {
Expand Down Expand Up @@ -386,7 +394,6 @@ Script Script::buildOpReturnScript(const Data& data) {
script.bytes.push_back(OP_RETURN);
pushDataLength(script.bytes, data.size());
script.bytes.insert(script.bytes.end(), data.begin(), data.begin() + data.size());
assert(script.bytes.size() <= 83); // max script length, must always hold
return script;
}

Expand Down
8 changes: 5 additions & 3 deletions src/Bitcoin/Script.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ namespace TW::Bitcoin {

class Script {
public:
// Maximum length for OP_RETURN data
static const size_t MaxOpReturnLength = 80;
// Maximum length for OP_RETURN data.
// Bitcoin Core v30 increased the default -datacarriersize from 83 to 10000 bytes.
// This constant represents the maximum payload data size (not including OP_RETURN and push opcodes).
static const size_t MaxOpReturnLength = 10000;

/// Script raw bytes.
Data bytes;
Expand Down Expand Up @@ -122,7 +124,7 @@ class Script {
/// Builds a V1 pay-to-witness-program script, P2TR (from a 32-byte Schnorr public key).
static Script buildPayToV1WitnessProgram(const Data& publicKey);

/// Builds an OP_RETURN script with given data. Returns empty script on error, if data is too long (>80).
/// Builds an OP_RETURN script with given data. Returns empty script on error, if data is too long (>10000).
static Script buildOpReturnScript(const Data& data);

/// Builds a appropriate lock script for the given
Expand Down
60 changes: 54 additions & 6 deletions tests/chains/Bitcoin/BitcoinScriptTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -356,20 +356,68 @@ TEST(BitcoinScript, OpReturn) {
data.push_back(0xab);
Script script = Script::buildOpReturnScript(data);
EXPECT_EQ(script.bytes.size(), 3 + data.size());
EXPECT_EQ(hex(script.bytes),
EXPECT_EQ(hex(script.bytes),
"6a4c50"
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ab");
}
{
// >80 bytes, fails
EXPECT_EQ(hex(Script::buildOpReturnScript(Data(81)).bytes), "");
EXPECT_EQ(hex(Script::buildOpReturnScript(Data(255)).bytes), "");
// 100 bytes - uses OP_PUSHDATA1 (was previously rejected with 80-byte limit)
Data data = Data(100, 0x42);
Script script = Script::buildOpReturnScript(data);
EXPECT_EQ(script.bytes.size(), 3 + data.size()); // OP_RETURN + OP_PUSHDATA1 + 1-byte len + data
EXPECT_EQ(script.bytes[0], 0x6a); // OP_RETURN
EXPECT_EQ(script.bytes[1], 0x4c); // OP_PUSHDATA1
EXPECT_EQ(script.bytes[2], 100); // length
}
{
// 255 bytes - max for OP_PUSHDATA1
Data data = Data(255, 0x42);
Script script = Script::buildOpReturnScript(data);
EXPECT_EQ(script.bytes.size(), 3 + data.size()); // OP_RETURN + OP_PUSHDATA1 + 1-byte len + data
EXPECT_EQ(script.bytes[0], 0x6a); // OP_RETURN
EXPECT_EQ(script.bytes[1], 0x4c); // OP_PUSHDATA1
EXPECT_EQ(script.bytes[2], 255); // length
}
{
// 256 bytes - requires OP_PUSHDATA2
Data data = Data(256, 0x42);
Script script = Script::buildOpReturnScript(data);
EXPECT_EQ(script.bytes.size(), 4 + data.size()); // OP_RETURN + OP_PUSHDATA2 + 2-byte len + data
EXPECT_EQ(script.bytes[0], 0x6a); // OP_RETURN
EXPECT_EQ(script.bytes[1], 0x4d); // OP_PUSHDATA2
EXPECT_EQ(script.bytes[2], 0x00); // length low byte (256 = 0x0100)
EXPECT_EQ(script.bytes[3], 0x01); // length high byte
}
{
// 1000 bytes - uses OP_PUSHDATA2
Data data = Data(1000, 0x42);
Script script = Script::buildOpReturnScript(data);
EXPECT_EQ(script.bytes.size(), 4 + data.size()); // OP_RETURN + OP_PUSHDATA2 + 2-byte len + data
EXPECT_EQ(script.bytes[0], 0x6a); // OP_RETURN
EXPECT_EQ(script.bytes[1], 0x4d); // OP_PUSHDATA2
EXPECT_EQ(script.bytes[2], 0xe8); // length low byte (1000 = 0x03e8)
EXPECT_EQ(script.bytes[3], 0x03); // length high byte
}
{
// 10000 bytes - max allowed (Bitcoin Core v30 default -datacarriersize)
Data data = Data(10000, 0x42);
Script script = Script::buildOpReturnScript(data);
EXPECT_EQ(script.bytes.size(), 4 + data.size()); // OP_RETURN + OP_PUSHDATA2 + 2-byte len + data
EXPECT_EQ(script.bytes[0], 0x6a); // OP_RETURN
EXPECT_EQ(script.bytes[1], 0x4d); // OP_PUSHDATA2
EXPECT_EQ(script.bytes[2], 0x10); // length low byte (10000 = 0x2710)
EXPECT_EQ(script.bytes[3], 0x27); // length high byte
}
{
// >10000 bytes, fails
EXPECT_EQ(hex(Script::buildOpReturnScript(Data(10001)).bytes), "");
EXPECT_EQ(hex(Script::buildOpReturnScript(Data(20000)).bytes), "");
}
}

TEST(BitcoinScript, OpReturnTooLong) {
// too long, truncated
Data data = Data(89);
// too long (>10000 bytes), returns empty script
Data data = Data(10001);
data.push_back(0xab);
Script script = Script::buildOpReturnScript(data);
EXPECT_EQ(hex(script.bytes), "");
Expand Down