Skip to content

Commit

Permalink
feat: parsing for NakamotoCoinbase tx type (#15)
Browse files Browse the repository at this point in the history
* feat: parsing for NakamotoCoinbase tx type

* test: assert full tx bytes are read during deserialization

* fix: use latest NakamotoCoinbase wire format (fixed length vrf)

* test: add vectors from stacks-core codebase

* chore: fix test variable name
  • Loading branch information
zone117x authored Dec 8, 2023
1 parent bcce7de commit ee78a5d
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 2 deletions.
22 changes: 21 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -229,6 +248,7 @@ export enum TxPayloadTypeID {
CoinbaseToAltRecipient = 5,
VersionedSmartContract = 6,
TenureChange = 7,
NakamotoCoinbase = 8,
}

export enum PostConditionAuthFlag {
Expand Down
43 changes: 42 additions & 1 deletion src/stacks_tx/deserialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,18 @@ 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 mut vrf_proof: Vec<u8> = vec![0u8; 80];
fd.read_exact(&mut vrf_proof)?;

TransactionPayload::NakamotoCoinbase(payload, principal, VRFProof(vrf_proof))
}
_ => {
return Err(format!(
"Failed to parse transaction -- unknown payload ID {}",
Expand Down Expand Up @@ -531,6 +543,21 @@ impl PrincipalData {
_ => Err("Bad principal prefix".into()),
}
}

pub fn deserialize_optional(fd: &mut Cursor<&[u8]>) -> Result<Option<Self>, 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 {
Expand Down Expand Up @@ -663,6 +690,7 @@ pub enum TransactionPayloadID {
CoinbaseToAltRecipient = 5,
VersionedSmartContract = 6,
TenureChange = 7,
NakamotoCoinbase = 8,
}

pub enum TransactionPayload {
Expand All @@ -674,10 +702,13 @@ pub enum TransactionPayload {
CoinbaseToAltRecipient(CoinbasePayload, PrincipalData),
VersionedSmartContract(TransactionSmartContract, ClarityVersion),
TenureChange(TransactionTenureChange),
NakamotoCoinbase(CoinbasePayload, Option<PrincipalData>, VRFProof),
}

pub struct CoinbasePayload(pub [u8; 32]);

pub struct VRFProof(pub Vec<u8>);

pub struct TransactionTenureChange {
pub previous_tenure_end: [u8; 32],
pub previous_tenure_blocks: u32,
Expand Down Expand Up @@ -756,10 +787,20 @@ 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"80800000000400b40723ab4d7781cf1b45083aa043ce4563006c6100000000000000010000000000000000000158be820619a4838f74e63099bb113fcf7ee13ef3b2bb56728cd19470f9379f05288d4accc987d8dd85de5101776c2ad000784d118e35deb4f02852540bf6dd5f01020000000008010101010101010101010101010101010101010101010101010101010101010109119054d8cfba5f6aebaac75b0f6671a6917211729fa7bafa35ab0ad68fe243cf4169eb339d8a26ee8e036c8380e3afd63da8aca1f9673d19a59ef00bf13e1ba2e540257d0b471fc591a877a90e04e00b";

let bytes = decode_hex(input).unwrap();
let bytes_len = bytes.len();
let mut cursor = Cursor::new(bytes.as_ref());
let tx = StacksTransaction::deserialize(&mut cursor);
assert!(tx.is_ok());
assert_eq!(cursor.position() as usize, bytes_len);
}
}
19 changes: 19 additions & 0 deletions src/stacks_tx/neon_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down
128 changes: 128 additions & 0 deletions tests/tx-decode-3.0.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
decodeTransaction,
PostConditionAuthFlag,
PostConditionModeID,
PrincipalTypeID,
TenureChangeCause,
TransactionVersion,
TxPayloadNakamotoCoinbase,
TxPayloadTypeID,
TxPublicKeyEncoding
} from '../index.js';
Expand Down Expand Up @@ -46,3 +48,129 @@ test('stacks3.0 - decode tx - tenure change', () => {
}
});
});

test('stacks3.0 - decode tx - nakamoto coinbase - no alt recipient (mockamoto vector)', () => {
const nakamotoCoinbaseTx = '80800000000400b40723ab4d7781cf1b45083aa043ce4563006c6100000000000000010000000000000000000158be820619a4838f74e63099bb113fcf7ee13ef3b2bb56728cd19470f9379f05288d4accc987d8dd85de5101776c2ad000784d118e35deb4f02852540bf6dd5f01020000000008010101010101010101010101010101010101010101010101010101010101010109119054d8cfba5f6aebaac75b0f6671a6917211729fa7bafa35ab0ad68fe243cf4169eb339d8a26ee8e036c8380e3afd63da8aca1f9673d19a59ef00bf13e1ba2e540257d0b471fc591a877a90e04e00b';
const decoded = decodeTransaction(nakamotoCoinbaseTx);
expect(decoded).toEqual({
"tx_id": "0x1ecc33bfdd58a94ff97afb6d64a2ebefb0021f22490767e844ebd80285486e16",
"version": TransactionVersion.Testnet,
"chain_id": 0x80000000,
"auth": {
"type_id": PostConditionAuthFlag.Standard,
"origin_condition": {
"hash_mode": 0,
"signer": {
"address_version": 26,
"address_hash_bytes": "0xb40723ab4d7781cf1b45083aa043ce4563006c61",
"address": "ST2T0E8XB9NVR3KRV8M43N823SS2P603CC4Y4DG1V"
},
"nonce": "1",
"tx_fee": "0",
"key_encoding": TxPublicKeyEncoding.Compressed,
"signature": "0x0158be820619a4838f74e63099bb113fcf7ee13ef3b2bb56728cd19470f9379f05288d4accc987d8dd85de5101776c2ad000784d118e35deb4f02852540bf6dd5f"
}
},
"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": "0x119054d8cfba5f6aebaac75b0f6671a6917211729fa7bafa35ab0ad68fe243cf4169eb339d8a26ee8e036c8380e3afd63da8aca1f9673d19a59ef00bf13e1ba2e540257d0b471fc591a877a90e04e00b"
}
});

const payload = decoded.payload as TxPayloadNakamotoCoinbase;
const txType: TxPayloadTypeID.NakamotoCoinbase = payload.type_id;
expect(txType).toEqual(TxPayloadTypeID.NakamotoCoinbase);
expect(payload.recipient).toBeNull();
});

test('stacks3.0 - decode tx - nakamoto coinbase - no alt recipient (stacks-core vector 1)', () => {
const nakamotoCoinbaseTx = '80800000000400b40723ab4d7781cf1b45083aa043ce4563006c6100000000000000010000000000000000000158be820619a4838f74e63099bb113fcf7ee13ef3b2bb56728cd19470f9379f05288d4accc987d8dd85de5101776c2ad000784d118e35deb4f02852540bf6dd5f010200000000081212121212121212121212121212121212121212121212121212121212121212099275df67a68c8745c0ff97b48201ee6db447f7c93b23ae24cdc2400f52fdb08a1a6ac7ec71bf9c9c76e96ee4675ebff60625af28718501047bfd87b810c2d2139b73c23bd69de66360953a642c2a330a';
const decoded = decodeTransaction(nakamotoCoinbaseTx);
expect(decoded).toEqual({
"tx_id": "0x3f23c7c7d865e1ff924950bf03b12eecb949c68f024fcad45b6d8e2420fb77cc",
"version": TransactionVersion.Testnet,
"chain_id": 0x80000000,
"auth": {
"type_id": PostConditionAuthFlag.Standard,
"origin_condition": {
"hash_mode": 0,
"signer": {
"address_version": 26,
"address_hash_bytes": "0xb40723ab4d7781cf1b45083aa043ce4563006c61",
"address": "ST2T0E8XB9NVR3KRV8M43N823SS2P603CC4Y4DG1V"
},
"nonce": "1",
"tx_fee": "0",
"key_encoding": TxPublicKeyEncoding.Compressed,
"signature": "0x0158be820619a4838f74e63099bb113fcf7ee13ef3b2bb56728cd19470f9379f05288d4accc987d8dd85de5101776c2ad000784d118e35deb4f02852540bf6dd5f"
}
},
"anchor_mode": AnchorModeID.OnChainOnly,
"post_condition_mode": PostConditionModeID.Deny,
"post_conditions": [],
"post_conditions_buffer": "0x0200000000",
"payload": {
"type_id": TxPayloadTypeID.NakamotoCoinbase,
"payload_buffer": "0x1212121212121212121212121212121212121212121212121212121212121212",
"recipient": null,
"vrf_proof": "0x9275df67a68c8745c0ff97b48201ee6db447f7c93b23ae24cdc2400f52fdb08a1a6ac7ec71bf9c9c76e96ee4675ebff60625af28718501047bfd87b810c2d2139b73c23bd69de66360953a642c2a330a"
}
});

const payload = decoded.payload as TxPayloadNakamotoCoinbase;
const txType: TxPayloadTypeID.NakamotoCoinbase = payload.type_id;
expect(txType).toEqual(TxPayloadTypeID.NakamotoCoinbase);
expect(payload.recipient).toBeNull();
});

test('stacks3.0 - decode tx - nakamoto coinbase - no alt recipient (stacks-core vector 2)', () => {
const nakamotoCoinbaseTx = '80800000000400b40723ab4d7781cf1b45083aa043ce4563006c6100000000000000010000000000000000000158be820619a4838f74e63099bb113fcf7ee13ef3b2bb56728cd19470f9379f05288d4accc987d8dd85de5101776c2ad000784d118e35deb4f02852540bf6dd5f0102000000000812121212121212121212121212121212121212121212121212121212121212120a0601ffffffffffffffffffffffffffffffffffffffff0c666f6f2d636f6e74726163749275df67a68c8745c0ff97b48201ee6db447f7c93b23ae24cdc2400f52fdb08a1a6ac7ec71bf9c9c76e96ee4675ebff60625af28718501047bfd87b810c2d2139b73c23bd69de66360953a642c2a330a';
const decoded = decodeTransaction(nakamotoCoinbaseTx);
expect(decoded).toEqual({
"tx_id": "0x3448d47b2e2ef6db517963e1d8e7534ba84afccac9b2c79c1dcf32b21f56871a",
"version": TransactionVersion.Testnet,
"chain_id": 0x80000000,
"auth": {
"type_id": PostConditionAuthFlag.Standard,
"origin_condition": {
"hash_mode": 0,
"signer": {
"address_version": 26,
"address_hash_bytes": "0xb40723ab4d7781cf1b45083aa043ce4563006c61",
"address": "ST2T0E8XB9NVR3KRV8M43N823SS2P603CC4Y4DG1V"
},
"nonce": "1",
"tx_fee": "0",
"key_encoding": TxPublicKeyEncoding.Compressed,
"signature": "0x0158be820619a4838f74e63099bb113fcf7ee13ef3b2bb56728cd19470f9379f05288d4accc987d8dd85de5101776c2ad000784d118e35deb4f02852540bf6dd5f"
}
},
"anchor_mode": AnchorModeID.OnChainOnly,
"post_condition_mode": PostConditionModeID.Deny,
"post_conditions": [],
"post_conditions_buffer": "0x0200000000",
"payload": {
"type_id": TxPayloadTypeID.NakamotoCoinbase,
"payload_buffer": "0x1212121212121212121212121212121212121212121212121212121212121212",
"recipient": {
"address": "S13ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZXCFYZCG",
"address_hash_bytes": "0xffffffffffffffffffffffffffffffffffffffff",
"address_version": 1,
"contract_name": "foo-contract",
"type_id": PrincipalTypeID.Contract,
},
"vrf_proof": "0x9275df67a68c8745c0ff97b48201ee6db447f7c93b23ae24cdc2400f52fdb08a1a6ac7ec71bf9c9c76e96ee4675ebff60625af28718501047bfd87b810c2d2139b73c23bd69de66360953a642c2a330a"
}
});

const payload = decoded.payload as TxPayloadNakamotoCoinbase;
const txType: TxPayloadTypeID.NakamotoCoinbase = payload.type_id;
expect(txType).toEqual(TxPayloadTypeID.NakamotoCoinbase);
expect(payload.recipient).not.toBeNull();
});

0 comments on commit ee78a5d

Please sign in to comment.