diff --git a/index.ts b/index.ts index cea46e0..052afce 100644 --- a/index.ts +++ b/index.ts @@ -21,7 +21,16 @@ export interface DecodedTxResult { post_conditions: TxPostCondition[]; /** Hex string */ post_conditions_buffer: string; - payload: TxPayloadTokenTransfer | TxPayloadSmartContract | TxPayloadContractCall | TxPayloadPoisonMicroblock | TxPayloadCoinbase | TxPayloadCoinbaseToAltRecipient | TxPayloadVersionedSmartContract | TxPayloadTenureChange; + payload: + | TxPayloadTokenTransfer + | TxPayloadSmartContract + | TxPayloadContractCall + | TxPayloadPoisonMicroblock + | TxPayloadCoinbase + | TxPayloadCoinbaseToAltRecipient + | TxPayloadVersionedSmartContract + | TxPayloadTenureChange + | TxPayloadNakamotoCoinbase; } export enum PostConditionAssetInfoID { @@ -188,6 +197,16 @@ export interface TxPayloadCoinbaseToAltRecipient { recipient: PrincipalStandardData | PrincipalContractData; } +export interface TxPayloadNakamotoCoinbase { + type_id: TxPayloadTypeID.NakamotoCoinbase; + /** Hex string */ + payload_buffer: string; + /** Optional, null if not specified */ + recipient: PrincipalStandardData | PrincipalContractData | null; + /** Hex string */ + vrf_proof: string; +} + export interface TxPayloadVersionedSmartContract { type_id: TxPayloadTypeID.VersionedSmartContract; clarity_version: ClarityVersion; @@ -229,6 +248,7 @@ export enum TxPayloadTypeID { CoinbaseToAltRecipient = 5, VersionedSmartContract = 6, TenureChange = 7, + NakamotoCoinbase = 8, } export enum PostConditionAuthFlag { diff --git a/src/stacks_tx/deserialize.rs b/src/stacks_tx/deserialize.rs index ad669ce..c0e6190 100644 --- a/src/stacks_tx/deserialize.rs +++ b/src/stacks_tx/deserialize.rs @@ -390,6 +390,19 @@ impl TransactionPayload { let payload = TransactionTenureChange::deserialize(fd)?; TransactionPayload::TenureChange(payload) } + x if x == TransactionPayloadID::NakamotoCoinbase as u8 => { + let mut payload_bytes = [0u8; 32]; + fd.read_exact(&mut payload_bytes)?; + let payload = CoinbasePayload(payload_bytes); + + let principal = PrincipalData::deserialize_optional(fd)?; + + let vrf_proof_len: u32 = fd.read_u32::()?; + let mut vrf_proof: Vec = vec![0u8; vrf_proof_len as usize]; + fd.read_exact(&mut vrf_proof)?; + + TransactionPayload::NakamotoCoinbase(payload, principal, VRFProof(vrf_proof)) + } _ => { return Err(format!( "Failed to parse transaction -- unknown payload ID {}", @@ -531,6 +544,21 @@ impl PrincipalData { _ => Err("Bad principal prefix".into()), } } + + pub fn deserialize_optional(fd: &mut Cursor<&[u8]>) -> Result, DeserializeError> { + let mut header = [0]; + fd.read_exact(&mut header)?; + let prefix = + TypePrefix::from_u8(header[0]).ok_or_else(|| "Bad optional PrincipalData prefix")?; + match prefix { + TypePrefix::OptionalNone => Ok(None), + TypePrefix::OptionalSome => { + let principal_data = PrincipalData::deserialize(fd)?; + Ok(Some(principal_data)) + } + _ => Err("Bad optional PrincipalData prefix".into()), + } + } } impl StandardPrincipalData { @@ -663,6 +691,7 @@ pub enum TransactionPayloadID { CoinbaseToAltRecipient = 5, VersionedSmartContract = 6, TenureChange = 7, + NakamotoCoinbase = 8, } pub enum TransactionPayload { @@ -674,10 +703,13 @@ pub enum TransactionPayload { CoinbaseToAltRecipient(CoinbasePayload, PrincipalData), VersionedSmartContract(TransactionSmartContract, ClarityVersion), TenureChange(TransactionTenureChange), + NakamotoCoinbase(CoinbasePayload, Option, VRFProof), } pub struct CoinbasePayload(pub [u8; 32]); +pub struct VRFProof(pub Vec); + pub struct TransactionTenureChange { pub previous_tenure_end: [u8; 32], pub previous_tenure_blocks: u32, @@ -756,7 +788,15 @@ mod tests { #[test] fn test_decode_bug() { - let input = b"0x00000000010400982f3ec112a5f5928a5c96a914bd733793b896a5000000000000053000000000000002290000c85889dad0d5b08a997a93a28a7c93eb22c324e5f8992dc93e37865ef4f3e0d65383beefeffc4871a2facbc4b590ddf887c80de6638ed4e2ec0e633d1e130f230301000000000216982f3ec112a5f5928a5c96a914bd733793b896a51861726b6164696b6f2d676f7665726e616e63652d76332d310770726f706f7365000000060616982f3ec112a5f5928a5c96a914bd733793b896a51d61726b6164696b6f2d7374616b652d706f6f6c2d64696b6f2d76312d32010000000000000000000000000000ef8801000000000000000000000000000003f00e00000028414950313020557064617465204c54567320616e64204c69717569646174696f6e20526174696f730e0000003168747470733a2f2f6769746875622e636f6d2f61726b6164696b6f2d64616f2f61726b6164696b6f2f70756c6c2f3439330b000000010c0000000507616464726573730516982f3ec112a5f5928a5c96a914bd733793b896a50863616e2d6275726e040863616e2d6d696e7404046e616d650d0000002b61697031302d61726b6164696b6f2d7570646174652d74766c2d6c69717569646174696f6e2d726174696f0e7175616c69666965642d6e616d650616982f3ec112a5f5928a5c96a914bd733793b896a52b61697031302d61726b6164696b6f2d7570646174652d74766c2d6c69717569646174696f6e2d726174696f"; + // 07c15258750a06e6ddae0320f978e5d86973933f1803d5bbd35213b54e75d2310f006402e97fca6444b0dc98f6f9a1013c5554975c7ce1c7954135949e6af4b9c56ed9cbf1a61dc83d054fa9cc699c9918af44a9b9ab2e5ccaf9611b86e963f139c49a6c546a8e94d67bb21cda0aa3b05364960e91d4281e7000000015124b91930cea290260f27dd56093f0dbefc4e6c5fa + // pre-payload byte length: 115 + // let input = b"0x00000000010400982f3ec112a5f5928a5c96a914bd733793b896a5000000000000053000000000000002290000c85889dad0d5b08a997a93a28a7c93eb22c324e5f8992dc93e37865ef4f3e0d65383beefeffc4871a2facbc4b590ddf887c80de6638ed4e2ec0e633d1e130f230301000000000216982f3ec112a5f5928a5c96a914bd733793b896a51861726b6164696b6f2d676f7665726e616e63652d76332d310770726f706f7365000000060616982f3ec112a5f5928a5c96a914bd733793b896a51d61726b6164696b6f2d7374616b652d706f6f6c2d64696b6f2d76312d32010000000000000000000000000000ef8801000000000000000000000000000003f00e00000028414950313020557064617465204c54567320616e64204c69717569646174696f6e20526174696f730e0000003168747470733a2f2f6769746875622e636f6d2f61726b6164696b6f2d64616f2f61726b6164696b6f2f70756c6c2f3439330b000000010c0000000507616464726573730516982f3ec112a5f5928a5c96a914bd733793b896a50863616e2d6275726e040863616e2d6d696e7404046e616d650d0000002b61697031302d61726b6164696b6f2d7570646174652d74766c2d6c69717569646174696f6e2d726174696f0e7175616c69666965642d6e616d650616982f3ec112a5f5928a5c96a914bd733793b896a52b61697031302d61726b6164696b6f2d7570646174652d74766c2d6c69717569646174696f6e2d726174696f"; + + // tx prefix (before payload): + // let input = b"00000000010400982f3ec112a5f5928a5c96a914bd733793b896a5000000000000053000000000000002290000c85889dad0d5b08a997a93a28a7c93eb22c324e5f8992dc93e37865ef4f3e0d65383beefeffc4871a2facbc4b590ddf887c80de6638ed4e2ec0e633d1e130f23030100000000"; + + let input = b"80800000000400ad0cc5ca0b4571dd435a9da7e16cbc662716dceb00000000000000010000000000000000000015833671ecd7432e6412423273eebf8a78d973beb08f690e58ba548f67ee26584967a5bc24d44f27ecca18e82a9956181e9d9cef7c67f718b33c5f5d0f82643801020000000008010101010101010101010101010101010101010101010101010101010101010109000000506f77e9a15503066b515060aa438ae3f5bc5207339b8e2933bdeae0891362d8e7ca2e5b047153904272d5f030ddcc83333676df6583394b0852a7e411b7c8d4c973f17fb7687601891ad7ca6707aa8408"; + let bytes = decode_hex(input).unwrap(); let mut cursor = Cursor::new(bytes.as_ref()); let tx = StacksTransaction::deserialize(&mut cursor); diff --git a/src/stacks_tx/neon_encoder.rs b/src/stacks_tx/neon_encoder.rs index 9d231ec..42b564a 100644 --- a/src/stacks_tx/neon_encoder.rs +++ b/src/stacks_tx/neon_encoder.rs @@ -522,6 +522,25 @@ impl NeonJsSerialize for TransactionPayload { tenure_change.neon_js_serialize(cx, obj, extra_ctx)?; } + TransactionPayload::NakamotoCoinbase(ref buf, ref principal, ref vrf_proof) => { + let type_id = cx.number(TransactionPayloadID::NakamotoCoinbase as u8); + obj.set(cx, "type_id", type_id)?; + + let payload_buffer = cx.string(encode_hex(&buf.0)); + obj.set(cx, "payload_buffer", payload_buffer)?; + + if let Some(principal) = principal { + let recipient_obj = cx.empty_object(); + principal.neon_js_serialize(cx, &recipient_obj, extra_ctx)?; + obj.set(cx, "recipient", recipient_obj)?; + } else { + let recipient_obj = cx.null(); + obj.set(cx, "recipient", recipient_obj)?; + } + + let vrf_proof_buffer = cx.string(encode_hex(&vrf_proof.0)); + obj.set(cx, "vrf_proof", vrf_proof_buffer)?; + } } Ok(()) } diff --git a/tests/tx-decode-3.0.test.ts b/tests/tx-decode-3.0.test.ts index f4bb7e8..16ece4a 100644 --- a/tests/tx-decode-3.0.test.ts +++ b/tests/tx-decode-3.0.test.ts @@ -5,6 +5,7 @@ import { PostConditionModeID, TenureChangeCause, TransactionVersion, + TxPayloadNakamotoCoinbase, TxPayloadTypeID, TxPublicKeyEncoding } from '../index.js'; @@ -46,3 +47,43 @@ test('stacks3.0 - decode tx - tenure change', () => { } }); }); + +test('stacks3.0 - decode tx - nakamoto coinbase - no alt recipient', () => { + const tenureChangeTx = '80800000000400ad0cc5ca0b4571dd435a9da7e16cbc662716dceb00000000000000010000000000000000000015833671ecd7432e6412423273eebf8a78d973beb08f690e58ba548f67ee26584967a5bc24d44f27ecca18e82a9956181e9d9cef7c67f718b33c5f5d0f82643801020000000008010101010101010101010101010101010101010101010101010101010101010109000000506f77e9a15503066b515060aa438ae3f5bc5207339b8e2933bdeae0891362d8e7ca2e5b047153904272d5f030ddcc83333676df6583394b0852a7e411b7c8d4c973f17fb7687601891ad7ca6707aa8408'; + const decoded = decodeTransaction(tenureChangeTx); + expect(decoded).toEqual({ + "tx_id": "0xa18614990f3a67b8ab13ec95846aebd409b2ef85017c900840436ac547a537aa", + "version": TransactionVersion.Testnet, + "chain_id": 0x80000000, + "auth": { + "type_id": PostConditionAuthFlag.Standard, + "origin_condition": { + "hash_mode": 0, + "signer": { + "address_version": 26, + "address_hash_bytes": "0xad0cc5ca0b4571dd435a9da7e16cbc662716dceb", + "address": "ST2PGSHEA1D2Q3QA3BAETFRBCQHK2E5PWXECD5E7T" + }, + "nonce": "1", + "tx_fee": "0", + "key_encoding": TxPublicKeyEncoding.Compressed, + "signature": "0x0015833671ecd7432e6412423273eebf8a78d973beb08f690e58ba548f67ee26584967a5bc24d44f27ecca18e82a9956181e9d9cef7c67f718b33c5f5d0f826438" + } + }, + "anchor_mode": AnchorModeID.OnChainOnly, + "post_condition_mode": PostConditionModeID.Deny, + "post_conditions": [], + "post_conditions_buffer": "0x0200000000", + "payload": { + "type_id": TxPayloadTypeID.NakamotoCoinbase, + "payload_buffer": "0x0101010101010101010101010101010101010101010101010101010101010101", + "recipient": null, + "vrf_proof": "0x6f77e9a15503066b515060aa438ae3f5bc5207339b8e2933bdeae0891362d8e7ca2e5b047153904272d5f030ddcc83333676df6583394b0852a7e411b7c8d4c973f17fb7687601891ad7ca6707aa8408" + } + }); + + const payload = decoded.payload as TxPayloadNakamotoCoinbase; + const txType: TxPayloadTypeID.NakamotoCoinbase = payload.type_id; + expect(txType).toEqual(TxPayloadTypeID.NakamotoCoinbase); + expect(payload.recipient).toBeNull(); +});