Skip to content

Commit

Permalink
feat: support for TenureChange transaction types (#14)
Browse files Browse the repository at this point in the history
* feat: support for TenureChange transaction types

* feat: types for TenureChange tx

* test: deserialize TenureChange tx

* fix: update to latest TenureChange serialization format

* chore: fix js tests
  • Loading branch information
zone117x authored Nov 30, 2023
1 parent 4e3cdb5 commit bcce7de
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 7 deletions.
28 changes: 27 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface DecodedTxResult {
post_conditions: TxPostCondition[];
/** Hex string */
post_conditions_buffer: string;
payload: TxPayloadTokenTransfer | TxPayloadSmartContract | TxPayloadContractCall | TxPayloadPoisonMicroblock | TxPayloadCoinbase | TxPayloadCoinbaseToAltRecipient | TxPayloadVersionedSmartContract;
payload: TxPayloadTokenTransfer | TxPayloadSmartContract | TxPayloadContractCall | TxPayloadPoisonMicroblock | TxPayloadCoinbase | TxPayloadCoinbaseToAltRecipient | TxPayloadVersionedSmartContract | TxPayloadTenureChange;
}

export enum PostConditionAssetInfoID {
Expand Down Expand Up @@ -195,6 +195,31 @@ export interface TxPayloadVersionedSmartContract {
code_body: string;
}

export interface TxPayloadTenureChange {
type_id: TxPayloadTypeID.TenureChange;
/** (Hex string) Stacks Block hash */
previous_tenure_end: string;
/** The number of blocks produced in the previous tenure */
previous_tenure_blocks: number;
/** Cause of change in mining tenure. Depending on cause, tenure can be ended or extended. */
cause: TenureChangeCause;
/** (Hex string) The ECDSA public key hash of the current tenure */
pubkey_hash: string;
/** (Hex string) A Schnorr signature from at least 70% of the Stackers */
signature: string;
/** (Hex string) A bitmap of which Stackers signed */
signers: string;
}

export enum TenureChangeCause {
/** A valid winning block-commit */
BlockFound = 0,
/** No winning block-commits */
NoBlockFound = 1,
/** A "null miner" won the block-commit */
NullMiner = 2,
}

export enum TxPayloadTypeID {
TokenTransfer = 0,
SmartContract = 1,
Expand All @@ -203,6 +228,7 @@ export enum TxPayloadTypeID {
Coinbase = 4,
CoinbaseToAltRecipient = 5,
VersionedSmartContract = 6,
TenureChange = 7,
}

export enum PostConditionAuthFlag {
Expand Down
82 changes: 77 additions & 5 deletions src/stacks_tx/deserialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,13 +378,18 @@ impl TransactionPayload {
}
x if x == TransactionPayloadID::VersionedSmartContract as u8 => {
let clarity_version_u8 = fd.read_u8()?;
let clarity_version = ClarityVersion::from_u8(clarity_version_u8).ok_or(format!(
"Failed to parse smart contract Clarity version: unknown value {}",
clarity_version_u8
))?;
let clarity_version =
ClarityVersion::from_u8(clarity_version_u8).ok_or(format!(
"Failed to parse smart contract Clarity version: unknown value {}",
clarity_version_u8
))?;
let payload = TransactionSmartContract::deserialize(fd)?;
TransactionPayload::VersionedSmartContract(payload, clarity_version)
}
x if x == TransactionPayloadID::TenureChange as u8 => {
let payload = TransactionTenureChange::deserialize(fd)?;
TransactionPayload::TenureChange(payload)
}
_ => {
return Err(format!(
"Failed to parse transaction -- unknown payload ID {}",
Expand Down Expand Up @@ -428,6 +433,41 @@ impl TransactionSmartContract {
}
}

impl TransactionTenureChange {
pub fn deserialize(fd: &mut Cursor<&[u8]>) -> Result<Self, DeserializeError> {
let mut previous_tenure_end = [0u8; 32];
fd.read_exact(&mut previous_tenure_end)?;

let previous_tenure_blocks = fd.read_u32::<BigEndian>()?;

let cause_u8: u8 = fd.read_u8()?;
let cause = TenureChangeCause::from_u8(cause_u8).ok_or(format!(
"Failed to parse transaction: invalid tenure change cause {}",
cause_u8
))?;

let mut pubkey_hash = [0u8; 20];
fd.read_exact(&mut pubkey_hash)?;

let signers_len: u32 = fd.read_u32::<BigEndian>()?;
let mut signers: Vec<u8> = vec![0u8; signers_len as usize];
fd.read_exact(&mut signers)?;

// ThresholdSignature { R: [0u8; 33], z: [0u8; 32] }
let mut signature = [0u8; 65];
fd.read_exact(&mut signature)?;

Ok(TransactionTenureChange {
previous_tenure_end,
previous_tenure_blocks,
cause,
pubkey_hash,
signers,
signature,
})
}
}

impl StacksMicroblockHeader {
pub fn deserialize(fd: &mut Cursor<&[u8]>) -> Result<Self, DeserializeError> {
let cursor_pos = fd.position() as usize;
Expand Down Expand Up @@ -622,6 +662,7 @@ pub enum TransactionPayloadID {
Coinbase = 4,
CoinbaseToAltRecipient = 5,
VersionedSmartContract = 6,
TenureChange = 7,
}

pub enum TransactionPayload {
Expand All @@ -631,11 +672,42 @@ pub enum TransactionPayload {
PoisonMicroblock(StacksMicroblockHeader, StacksMicroblockHeader),
Coinbase(CoinbasePayload),
CoinbaseToAltRecipient(CoinbasePayload, PrincipalData),
VersionedSmartContract(TransactionSmartContract, ClarityVersion)
VersionedSmartContract(TransactionSmartContract, ClarityVersion),
TenureChange(TransactionTenureChange),
}

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

pub struct TransactionTenureChange {
pub previous_tenure_end: [u8; 32],
pub previous_tenure_blocks: u32,
pub cause: TenureChangeCause,
pub pubkey_hash: [u8; 20],
pub signers: Vec<u8>,
pub signature: [u8; 65],
}

#[repr(u8)]
#[derive(PartialEq, Copy, Clone)]
pub enum TenureChangeCause {
BlockFound = 0,
NoBlockFound = 1,
NullMiner = 2,
}

impl TenureChangeCause {
pub fn from_u8(n: u8) -> Option<TenureChangeCause> {
match n {
x if x == TenureChangeCause::BlockFound as u8 => Some(TenureChangeCause::BlockFound),
x if x == TenureChangeCause::NoBlockFound as u8 => {
Some(TenureChangeCause::NoBlockFound)
}
x if x == TenureChangeCause::NullMiner as u8 => Some(TenureChangeCause::NullMiner),
_ => None,
}
}
}

pub struct TransactionSmartContract {
pub name: ClarityName,
pub code_body: StacksString,
Expand Down
37 changes: 36 additions & 1 deletion src/stacks_tx/neon_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use super::deserialize::{
StacksTransaction, StandardPrincipalData, TransactionAuth, TransactionAuthField,
TransactionAuthFieldID, TransactionAuthFlags, TransactionContractCall, TransactionPayload,
TransactionPayloadID, TransactionPublicKeyEncoding, TransactionSmartContract,
TransactionSpendingCondition, TransactionVersion,
TransactionSpendingCondition, TransactionTenureChange, TransactionVersion,
};

struct TxSerializationContext {
Expand Down Expand Up @@ -516,6 +516,12 @@ impl NeonJsSerialize for TransactionPayload {

smart_contract.neon_js_serialize(cx, obj, extra_ctx)?;
}
TransactionPayload::TenureChange(ref tenure_change) => {
let type_id = cx.number(TransactionPayloadID::TenureChange as u8);
obj.set(cx, "type_id", type_id)?;

tenure_change.neon_js_serialize(cx, obj, extra_ctx)?;
}
}
Ok(())
}
Expand Down Expand Up @@ -629,6 +635,35 @@ impl NeonJsSerialize for TransactionSmartContract {
}
}

impl NeonJsSerialize for TransactionTenureChange {
fn neon_js_serialize(
&self,
cx: &mut FunctionContext,
obj: &Handle<JsObject>,
_extra_ctx: &(),
) -> NeonResult<()> {
let previous_tenure_end = cx.string(encode_hex(&self.previous_tenure_end));
obj.set(cx, "previous_tenure_end", previous_tenure_end)?;

let previous_tenure_blocks = cx.number(self.previous_tenure_blocks);
obj.set(cx, "previous_tenure_blocks", previous_tenure_blocks)?;

let cause = cx.number(self.cause as u8);
obj.set(cx, "cause", cause)?;

let pubkey_hash = cx.string(encode_hex(&self.pubkey_hash));
obj.set(cx, "pubkey_hash", pubkey_hash)?;

let signature = cx.string(encode_hex(&self.signature));
obj.set(cx, "signature", signature)?;

let signers = cx.string(encode_hex(&self.signers));
obj.set(cx, "signers", signers)?;

Ok(())
}
}

impl NeonJsSerialize for StacksMicroblockHeader {
fn neon_js_serialize(
&self,
Expand Down
48 changes: 48 additions & 0 deletions tests/tx-decode-3.0.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
AnchorModeID,
decodeTransaction,
PostConditionAuthFlag,
PostConditionModeID,
TenureChangeCause,
TransactionVersion,
TxPayloadTypeID,
TxPublicKeyEncoding
} from '../index.js';

test('stacks3.0 - decode tx - tenure change', () => {
const tenureChangeTx = '808000000004000f873150e9790e305b701aa8c7b3bcff9e31a5f9000000000000000000000000000000000001d367da530b92f4984f537f0b903c330eb5158262afa08d67cbbdea6c8e2ecae06008248ac147fc34101d3cc207b1b3e386e0f53732b5548bd5abe1570c2271340302000000000755c9861be5cff984a20ce6d99d4aa65941412889bdc665094136429b84f8c2ee00000001000000000000000000000000000000000000000000000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980000000000000000000000000000000000000000000000000000000000000000';
const decoded = decodeTransaction(tenureChangeTx);
expect(decoded).toEqual({
"tx_id": "0xc00148be5e8edb457d1bd1ae7ae5fdc2b74b64455f714d512e717deddeedf069",
"version": TransactionVersion.Testnet,
"chain_id": 0x80000000,
"auth": {
"type_id": PostConditionAuthFlag.Standard,
"origin_condition": {
"hash_mode": 0,
"signer": {
"address_version": 26,
"address_hash_bytes": "0x0f873150e9790e305b701aa8c7b3bcff9e31a5f9",
"address": "ST7RECAGX5WGWC2VE0DAHHXKQKZSWCD5Z4JRG6SR"
},
"nonce": "0",
"tx_fee": "0",
"key_encoding": TxPublicKeyEncoding.Compressed,
"signature": "0x01d367da530b92f4984f537f0b903c330eb5158262afa08d67cbbdea6c8e2ecae06008248ac147fc34101d3cc207b1b3e386e0f53732b5548bd5abe1570c227134"
}
},
"anchor_mode": AnchorModeID.Any,
"post_condition_mode": PostConditionModeID.Deny,
"post_conditions": [],
"post_conditions_buffer": "0x0200000000",
"payload": {
"type_id": TxPayloadTypeID.TenureChange,
"previous_tenure_end": "0x55c9861be5cff984a20ce6d99d4aa65941412889bdc665094136429b84f8c2ee",
"previous_tenure_blocks": 1,
"cause": TenureChangeCause.BlockFound,
"pubkey_hash": "0x0000000000000000000000000000000000000000",
"signature": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980000000000000000000000000000000000000000000000000000000000000000",
"signers": "0x"
}
});
});

0 comments on commit bcce7de

Please sign in to comment.