55
66use super :: envelope:: build_inscription_script;
77use crate :: error:: WasmUtxoError ;
8+ use miniscript:: bitcoin:: consensus:: Encodable ;
9+ use miniscript:: bitcoin:: hashes:: sha256;
810use miniscript:: bitcoin:: hashes:: Hash ;
911use miniscript:: bitcoin:: key:: UntweakedKeypair ;
10- use miniscript:: bitcoin:: psbt:: Psbt ;
11- use miniscript:: bitcoin:: secp256k1:: { Secp256k1 , SecretKey , XOnlyPublicKey } ;
12+ use miniscript:: bitcoin:: secp256k1:: { PublicKey , Secp256k1 , SecretKey , XOnlyPublicKey } ;
1213use miniscript:: bitcoin:: sighash:: { Prevouts , SighashCache } ;
1314use miniscript:: bitcoin:: taproot:: { ControlBlock , LeafVersion , TapLeafHash , TaprootBuilder } ;
1415use miniscript:: bitcoin:: { ScriptBuf , Transaction , TxOut , Witness } ;
1516
17+ /// NUMS point (Nothing Up My Sleeve) - a secp256k1 x coordinate with unknown discrete logarithm.
18+ /// Equal to SHA256(uncompressedDER(SECP256K1_GENERATOR_POINT)).
19+ /// Used as internal key when key-path spending is disabled.
20+ /// This matches utxo-lib's implementation for compatibility.
21+ fn nums_point ( ) -> XOnlyPublicKey {
22+ let secp = Secp256k1 :: new ( ) ;
23+ // Generator point G is the public key for secret key = 1
24+ let one = SecretKey :: from_slice ( & [
25+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
26+ 0 , 1 ,
27+ ] )
28+ . expect ( "valid secret key" ) ;
29+ let generator = PublicKey :: from_secret_key ( & secp, & one) ;
30+ // Uncompressed format: 04 || x || y (65 bytes)
31+ let uncompressed = generator. serialize_uncompressed ( ) ;
32+ // SHA256 hash of uncompressed generator point
33+ let hash = sha256:: Hash :: hash ( & uncompressed) ;
34+ XOnlyPublicKey :: from_slice ( hash. as_ref ( ) ) . expect ( "valid x-only pubkey" )
35+ }
36+
1637/// Taproot leaf script data needed for spending
1738#[ derive( Debug , Clone ) ]
1839pub struct TapLeafScript {
@@ -33,32 +54,32 @@ pub struct InscriptionRevealData {
3354/// Create inscription reveal data including the commit output script and tap leaf script
3455///
3556/// # Arguments
36- /// * `internal_key ` - The x-only public key (32 bytes)
57+ /// * `script_pubkey ` - The x-only public key for the OP_CHECKSIG in the inscription script
3758/// * `content_type` - MIME type of the inscription
3859/// * `data` - The inscription data
3960///
4061/// # Returns
4162/// `InscriptionRevealData` containing the commit output script, estimated vsize, and tap leaf script
4263pub fn create_inscription_reveal_data (
43- internal_key : & XOnlyPublicKey ,
64+ script_pubkey : & XOnlyPublicKey ,
4465 content_type : & str ,
4566 data : & [ u8 ] ,
4667) -> Result < InscriptionRevealData , WasmUtxoError > {
4768 let secp = Secp256k1 :: new ( ) ;
4869
49- // Build the inscription script
50- let script = build_inscription_script ( internal_key , content_type, data) ;
70+ // Build the inscription script (pubkey is used for OP_CHECKSIG inside the script)
71+ let script = build_inscription_script ( script_pubkey , content_type, data) ;
5172
5273 // Create taproot tree with the inscription script as the only leaf
5374 let builder = TaprootBuilder :: new ( )
5475 . add_leaf ( 0 , script. clone ( ) )
5576 . map_err ( |e| WasmUtxoError :: new ( & format ! ( "Failed to build taproot tree: {:?}" , e) ) ) ?;
5677
57- // Finalize the taproot spend info
58- // Use an unspendable internal key (all zeros XOR'd with script root)
59- // For simplicity, we use the provided internal_key
78+ // Use NUMS point as internal key (disables key-path spending)
79+ // This matches utxo-lib's behavior for compatibility
80+ let internal_key = nums_point ( ) ;
6081 let spend_info = builder
61- . finalize ( & secp, * internal_key)
82+ . finalize ( & secp, internal_key)
6283 . map_err ( |e| WasmUtxoError :: new ( & format ! ( "Failed to finalize taproot: {:?}" , e) ) ) ?;
6384
6485 // Get the output script (network-agnostic)
@@ -94,15 +115,15 @@ pub fn create_inscription_reveal_data(
94115/// * `output_value_sats` - Value in satoshis for the inscription output
95116///
96117/// # Returns
97- /// A signed PSBT containing the reveal transaction
118+ /// The signed reveal transaction as bytes (ready to broadcast)
98119pub fn sign_reveal_transaction (
99120 private_key : & SecretKey ,
100121 tap_leaf_script : & TapLeafScript ,
101122 commit_tx : & Transaction ,
102123 commit_output_script : & [ u8 ] ,
103124 recipient_output_script : & [ u8 ] ,
104125 output_value_sats : u64 ,
105- ) -> Result < Psbt , WasmUtxoError > {
126+ ) -> Result < Vec < u8 > , WasmUtxoError > {
106127 let secp = Secp256k1 :: new ( ) ;
107128
108129 // Convert output scripts
@@ -187,14 +208,13 @@ pub fn sign_reveal_transaction(
187208 witness. push ( control_block. serialize ( ) ) ;
188209 reveal_tx. input [ 0 ] . witness = witness;
189210
190- // Create PSBT from finalized transaction
191- let psbt = Psbt :: from_unsigned_tx ( reveal_tx. clone ( ) )
192- . map_err ( |e| WasmUtxoError :: new ( & format ! ( "Failed to create PSBT: {}" , e) ) ) ?;
211+ // Serialize the signed transaction
212+ let mut tx_bytes = Vec :: new ( ) ;
213+ reveal_tx
214+ . consensus_encode ( & mut tx_bytes)
215+ . map_err ( |e| WasmUtxoError :: new ( & format ! ( "Failed to serialize transaction: {}" , e) ) ) ?;
193216
194- // Note: The PSBT is created from the signed transaction for compatibility
195- // with the expected return type. In practice, this is already finalized.
196-
197- Ok ( psbt)
217+ Ok ( tx_bytes)
198218}
199219
200220/// Estimate the virtual size of a reveal transaction
@@ -224,6 +244,7 @@ fn estimate_reveal_vsize(script: &ScriptBuf, control_block: &ControlBlock) -> us
224244#[ cfg( test) ]
225245mod tests {
226246 use super :: * ;
247+ use miniscript:: bitcoin:: hashes:: hex:: FromHex ;
227248
228249 fn test_keypair ( ) -> ( SecretKey , XOnlyPublicKey ) {
229250 let secp = Secp256k1 :: new ( ) ;
@@ -247,4 +268,123 @@ mod tests {
247268 assert ! ( !data. tap_leaf_script. script. is_empty( ) ) ;
248269 assert ! ( !data. tap_leaf_script. control_block. is_empty( ) ) ;
249270 }
271+
272+ /// Test with the same x-only pubkey as utxo-ord test
273+ /// Expected output script: 5120dc8b12eec336e7215fd1213acf66fb0d5dd962813c0616988a12c08493831109
274+ /// Expected address: tb1pmj939mkrxmnjzh73yyav7ehmp4wajc5p8srpdxy2ztqgfyurzyys4sg9zx
275+ #[ test]
276+ fn test_utxo_ord_fixture_short_data ( ) {
277+ // Same x-only pubkey as utxo-ord test
278+ let xonly_hex = "af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3" ;
279+ let xonly_bytes = Vec :: < u8 > :: from_hex ( xonly_hex) . unwrap ( ) ;
280+ let pubkey = XOnlyPublicKey :: from_slice ( & xonly_bytes) . unwrap ( ) ;
281+
282+ let inscription_data = b"Never Gonna Give You Up" ;
283+ let result = create_inscription_reveal_data ( & pubkey, "text/plain" , inscription_data) ;
284+
285+ assert ! ( result. is_ok( ) ) ;
286+ let data = result. unwrap ( ) ;
287+
288+ // Log the actual output for debugging
289+ let output_hex = hex:: encode ( & data. output_script ) ;
290+ println ! ( "X-only pubkey: {}" , xonly_hex) ;
291+ println ! (
292+ "Inscription data: {:?}" ,
293+ String :: from_utf8_lossy( inscription_data)
294+ ) ;
295+ println ! ( "Output script (actual): {}" , output_hex) ;
296+ println ! ( "Output script (expected): 5120dc8b12eec336e7215fd1213acf66fb0d5dd962813c0616988a12c08493831109" ) ;
297+
298+ // Log the tap leaf script for debugging
299+ println ! (
300+ "Tap leaf script hex: {}" ,
301+ hex:: encode( & data. tap_leaf_script. script)
302+ ) ;
303+ println ! (
304+ "Control block hex: {}" ,
305+ hex:: encode( & data. tap_leaf_script. control_block)
306+ ) ;
307+
308+ // Basic structure checks
309+ assert_eq ! ( data. output_script. len( ) , 34 ) ;
310+ assert_eq ! ( data. output_script[ 0 ] , 0x51 ) ; // OP_1
311+ assert_eq ! ( data. output_script[ 1 ] , 0x20 ) ; // PUSH32
312+
313+ // Assert byte-exact match with utxo-ord
314+ let expected_hex = "5120dc8b12eec336e7215fd1213acf66fb0d5dd962813c0616988a12c08493831109" ;
315+ assert_eq ! (
316+ output_hex, expected_hex,
317+ "Output script should match utxo-ord fixture"
318+ ) ;
319+ }
320+
321+ /// Test with large data (>520 bytes) - same as utxo-ord test
322+ /// Expected output script: 5120ec90ba87f3e7c5462eb2173afdc50e00cea6fc69166677171d70f45dfb3a31b8
323+ /// Expected address: tb1pajgt4plnulz5vt4jzua0m3gwqr82dlrfzen8w9cawr69m7e6xxuq7dzypl
324+ #[ test]
325+ fn test_utxo_ord_fixture_large_data ( ) {
326+ let xonly_hex = "af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3" ;
327+ let xonly_bytes = Vec :: < u8 > :: from_hex ( xonly_hex) . unwrap ( ) ;
328+ let pubkey = XOnlyPublicKey :: from_slice ( & xonly_bytes) . unwrap ( ) ;
329+
330+ // "Never Gonna Let You Down" repeated 100 times
331+ let base = b"Never Gonna Let You Down" ;
332+ let inscription_data: Vec < u8 > = base
333+ . iter ( )
334+ . cycle ( )
335+ . take ( base. len ( ) * 100 )
336+ . copied ( )
337+ . collect ( ) ;
338+
339+ let result = create_inscription_reveal_data ( & pubkey, "text/plain" , & inscription_data) ;
340+
341+ assert ! ( result. is_ok( ) ) ;
342+ let data = result. unwrap ( ) ;
343+
344+ let output_hex = hex:: encode ( & data. output_script ) ;
345+ println ! ( "Output script (actual): {}" , output_hex) ;
346+ println ! ( "Output script (expected): 5120ec90ba87f3e7c5462eb2173afdc50e00cea6fc69166677171d70f45dfb3a31b8" ) ;
347+
348+ assert_eq ! ( data. output_script. len( ) , 34 ) ;
349+ assert_eq ! ( data. output_script[ 0 ] , 0x51 ) ;
350+ assert_eq ! ( data. output_script[ 1 ] , 0x20 ) ;
351+
352+ // Assert byte-exact match with utxo-ord
353+ let expected_hex = "5120ec90ba87f3e7c5462eb2173afdc50e00cea6fc69166677171d70f45dfb3a31b8" ;
354+ assert_eq ! (
355+ output_hex, expected_hex,
356+ "Output script should match utxo-ord fixture"
357+ ) ;
358+ }
359+
360+ /// Debug test to understand taproot key tweaking
361+ #[ test]
362+ fn test_taproot_tweak_details ( ) {
363+ let xonly_hex = "af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3" ;
364+ let xonly_bytes = Vec :: < u8 > :: from_hex ( xonly_hex) . unwrap ( ) ;
365+ let internal_key = XOnlyPublicKey :: from_slice ( & xonly_bytes) . unwrap ( ) ;
366+
367+ println ! ( "Internal key: {}" , internal_key) ;
368+
369+ let secp = Secp256k1 :: new ( ) ;
370+ let script =
371+ build_inscription_script ( & internal_key, "text/plain" , b"Never Gonna Give You Up" ) ;
372+
373+ println ! ( "Inscription script hex: {}" , hex:: encode( script. as_bytes( ) ) ) ;
374+ println ! ( "Inscription script len: {}" , script. len( ) ) ;
375+
376+ // Build taproot tree
377+ let builder = TaprootBuilder :: new ( ) . add_leaf ( 0 , script. clone ( ) ) . unwrap ( ) ;
378+
379+ let spend_info = builder. finalize ( & secp, internal_key) . unwrap ( ) ;
380+
381+ println ! ( "Output key (tweaked): {}" , spend_info. output_key( ) ) ;
382+ println ! (
383+ "Merkle root: {:?}" ,
384+ spend_info. merkle_root( ) . map( |r| r. to_string( ) )
385+ ) ;
386+
387+ let output_script = ScriptBuf :: new_p2tr_tweaked ( spend_info. output_key ( ) ) ;
388+ println ! ( "Output script: {}" , hex:: encode( output_script. as_bytes( ) ) ) ;
389+ }
250390}
0 commit comments