Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: parsing for NakamotoCoinbase tx type #15

Merged
merged 5 commits into from
Dec 8, 2023
Merged
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
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();
});