Skip to content

Commit

Permalink
feat: add tenure change payload
Browse files Browse the repository at this point in the history
  • Loading branch information
janniks committed Nov 30, 2023
1 parent 9dc46b3 commit 4e2e406
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/transactions/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export enum PayloadType {
PoisonMicroblock = 0x03,
Coinbase = 0x04,
CoinbaseToAltRecipient = 0x05,
TenureChange = 0x7,
}

/**
Expand Down
92 changes: 89 additions & 3 deletions packages/transactions/src/payload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { concatArray, IntegerType, intToBigInt, intToBytes, writeUInt32BE } from '@stacks/common';
import {
bytesToHex,
concatArray,
hexToBytes,
IntegerType,
intToBigInt,
intToBytes,
writeUInt32BE,
writeUInt8,
} from '@stacks/common';
import { ClarityVersion, COINBASE_BYTES_LENGTH, PayloadType, StacksMessageType } from './constants';

import { BytesReader } from './bytesReader';
Expand All @@ -23,7 +32,8 @@ export type Payload =
| VersionedSmartContractPayload
| PoisonPayload
| CoinbasePayload
| CoinbasePayloadToAltRecipient;
| CoinbasePayloadToAltRecipient
| TenureChangePayload;

export function isTokenTransferPayload(p: Payload): p is TokenTransferPayload {
return p.payloadType === PayloadType.TokenTransfer;
Expand Down Expand Up @@ -56,7 +66,8 @@ export type PayloadInput =
| VersionedSmartContractPayload
| PoisonPayload
| CoinbasePayload
| CoinbasePayloadToAltRecipient;
| CoinbasePayloadToAltRecipient
| TenureChangePayload;

export function createTokenTransferPayload(
recipient: string | PrincipalCV,
Expand Down Expand Up @@ -203,6 +214,52 @@ export function createCoinbasePayload(
};
}

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 interface TenureChangePayload {
readonly type: StacksMessageType.Payload;
readonly payloadType: PayloadType.TenureChange;
/** Stacks block hash (hex string) */
readonly previousTenureEnd: string;
/** Number of blocks produced in the previous tenure */
readonly previousTenureBlocks: number;
/** Cause of change in mining tenure */
readonly cause: TenureChangeCause;
/** The public key hash of the current tenure (hex string) */
readonly publicKeyHash: string;
/** The bitmap of which Stackers signed (hex string) */
readonly signers: string;
/** The Schnorr signature from at least 70% of the Stackers (hex string) */
readonly signature: string;
}

export function createTenureChangePayload(
previousTenureEnd: string,
previousTenureBlocks: number,
cause: TenureChangeCause,
publicKeyHash: string,
signers: string,
signature: string
): TenureChangePayload {
return {
type: StacksMessageType.Payload,
payloadType: PayloadType.TenureChange,
previousTenureEnd,
previousTenureBlocks,
cause,
publicKeyHash,
signers,
signature,
};
}

export function serializePayload(payload: PayloadInput): Uint8Array {
const bytesArray = [];
bytesArray.push(payload.payloadType);
Expand Down Expand Up @@ -243,6 +300,17 @@ export function serializePayload(payload: PayloadInput): Uint8Array {
bytesArray.push(payload.coinbaseBytes);
bytesArray.push(serializeCV(payload.recipient));
break;
case PayloadType.TenureChange:
bytesArray.push(hexToBytes(payload.previousTenureEnd));
bytesArray.push(writeUInt32BE(new Uint8Array(4), payload.previousTenureBlocks));
bytesArray.push(writeUInt8(new Uint8Array(1), payload.cause));
bytesArray.push(hexToBytes(payload.publicKeyHash));
const signers = hexToBytes(payload.signers);
bytesArray.push(writeUInt32BE(new Uint8Array(4), signers.byteLength)); // signers length
bytesArray.push(signers);

bytesArray.push(hexToBytes(payload.signature));
break;
}

return concatArray(bytesArray);
Expand Down Expand Up @@ -298,5 +366,23 @@ export function deserializePayload(bytesReader: BytesReader): Payload {
const coinbaseToAltRecipientBuffer = bytesReader.readBytes(COINBASE_BYTES_LENGTH);
const altRecipient = deserializeCV(bytesReader) as PrincipalCV;
return createCoinbasePayload(coinbaseToAltRecipientBuffer, altRecipient);
case PayloadType.TenureChange:
const previousTenureEnd = bytesToHex(bytesReader.readBytes(32));
const previousTenureBlocks = bytesReader.readUInt32BE();
const cause = bytesReader.readUInt8Enum(TenureChangeCause, n => {
throw new Error(`Cannot recognize TenureChangeCause: ${n}`);
});
const publicKeyHash = bytesToHex(bytesReader.readBytes(20));
const signersLength = bytesReader.readUInt32BE();
const signers = bytesToHex(bytesReader.readBytes(signersLength));
const signature = bytesToHex(bytesReader.readBytes(65));
return createTenureChangePayload(
previousTenureEnd,
previousTenureBlocks,
cause,
publicKeyHash,
signers,
signature
);
}
}
7 changes: 3 additions & 4 deletions packages/transactions/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,16 @@ export class StacksTransaction {
switch (payload.payloadType) {
case PayloadType.Coinbase:
case PayloadType.CoinbaseToAltRecipient:
case PayloadType.PoisonMicroblock: {
case PayloadType.PoisonMicroblock:
case PayloadType.TenureChange:
this.anchorMode = AnchorMode.OnChainOnly;
break;
}
case PayloadType.ContractCall:
case PayloadType.SmartContract:
case PayloadType.VersionedSmartContract:
case PayloadType.TokenTransfer: {
case PayloadType.TokenTransfer:
this.anchorMode = AnchorMode.Any;
break;
}
}
}
}
Expand Down
46 changes: 44 additions & 2 deletions packages/transactions/tests/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,19 @@ import {
pubKeyfromPrivKey,
publicKeyToString,
} from '../src/keys';
import { createTokenTransferPayload, serializePayload, TokenTransferPayload } from '../src/payload';
import {
createTenureChangePayload,
createTokenTransferPayload,
deserializePayload,
serializePayload,
TenureChangeCause,
TokenTransferPayload,
} from '../src/payload';
import { createAssetInfo } from '../src/postcondition-types';
import { createTransactionAuthField } from '../src/signature';
import { TransactionSigner } from '../src/signer';
import { deserializeTransaction, StacksTransaction } from '../src/transaction';
import { cloneDeep } from '../src/utils';
import { cloneDeep, randomBytes } from '../src/utils';

function setSignature(
unsignedTransaction: StacksTransaction,
Expand Down Expand Up @@ -2144,3 +2151,38 @@ test('Get contract map entry - no match', async () => {
expect(result).toEqual(mockResult);
expect(result.type).toBe(ClarityType.OptionalNone);
});

describe('serialize/deserialize tenure change', () => {
test('transaction', () => {
// test vector generated with mockamoto node
const txBytes =
'808000000004000f873150e9790e305b701aa8c7b3bcff9e31a5f9000000000000000000000000000000000001d367da530b92f4984f537f0b903c330eb5158262afa08d67cbbdea6c8e2ecae06008248ac147fc34101d3cc207b1b3e386e0f53732b5548bd5abe1570c2271340302000000000755c9861be5cff984a20ce6d99d4aa65941412889bdc665094136429b84f8c2ee00000001000000000000000000000000000000000000000000000000000279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980000000000000000000000000000000000000000000000000000000000000000';
const transaction = deserializeTransaction(txBytes);

expect(transaction).toBeDefined();
expect(bytesToHex(transaction.serialize())).toEqual(txBytes);
});

test('payload', () => {
const previousTenureEnd = bytesToHex(randomBytes(32));
const previousTenureBlocks = 100;
const cause = TenureChangeCause.NullMiner;
const publicKeyHash = bytesToHex(randomBytes(20));
const signers = bytesToHex(randomBytes(21));
const signature = bytesToHex(randomBytes(65));

const payload = createTenureChangePayload(
previousTenureEnd,
previousTenureBlocks,
cause,
publicKeyHash,
signers,
signature
);

const serialized = serializePayload(payload);
const reader = new BytesReader(serialized);

expect(deserializePayload(reader)).toEqual(payload);
});
});

0 comments on commit 4e2e406

Please sign in to comment.