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

[Nakamoto] Add tenure change tx #1585

Merged
merged 2 commits into from
Dec 1, 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
19 changes: 12 additions & 7 deletions packages/common/src/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export function readUInt16BE(source: Uint8Array, offset: number): number {
}

/** @ignore */
export function writeUInt16BE(source: Uint8Array, value: number, offset: number): void {
source[offset + 0] = value >>> 8;
source[offset + 1] = value >>> 0;
export function writeUInt16BE(destination: Uint8Array, value: number, offset = 0): Uint8Array {
destination[offset + 0] = value >>> 8;
destination[offset + 1] = value >>> 0;
return destination;
}

// The following methods are based on `microsoft/vscode` implementation
Expand All @@ -49,8 +50,9 @@ export function readUInt8(source: Uint8Array, offset: number): number {
}

/** @ignore */
export function writeUInt8(destination: Uint8Array, value: number, offset: number): void {
export function writeUInt8(destination: Uint8Array, value: number, offset = 0): Uint8Array {
destination[offset] = value;
return destination;
}

/** @ignore */
Expand All @@ -59,10 +61,11 @@ export function readUInt16LE(source: Uint8Array, offset: number): number {
}

/** @ignore */
export function writeUInt16LE(destination: Uint8Array, value: number, offset: number): void {
export function writeUInt16LE(destination: Uint8Array, value: number, offset = 0): Uint8Array {
destination[offset + 0] = value & 0b1111_1111;
value >>>= 8;
destination[offset + 1] = value & 0b1111_1111;
return destination;
}

/** @ignore */
Expand All @@ -76,14 +79,15 @@ export function readUInt32BE(source: Uint8Array, offset: number): number {
}

/** @ignore */
export function writeUInt32BE(destination: Uint8Array, value: number, offset: number): void {
export function writeUInt32BE(destination: Uint8Array, value: number, offset = 0): Uint8Array {
destination[offset + 3] = value;
value >>>= 8;
destination[offset + 2] = value;
value >>>= 8;
destination[offset + 1] = value;
value >>>= 8;
destination[offset] = value;
return destination;
}

/** @ignore */
Expand All @@ -97,12 +101,13 @@ export function readUInt32LE(source: Uint8Array, offset: number): number {
}

/** @ignore */
export function writeUInt32LE(destination: Uint8Array, value: number, offset: number): void {
export function writeUInt32LE(destination: Uint8Array, value: number, offset = 0): Uint8Array {
destination[offset + 0] = value & 0b1111_1111;
value >>>= 8;
destination[offset + 1] = value & 0b1111_1111;
value >>>= 8;
destination[offset + 2] = value & 0b1111_1111;
value >>>= 8;
destination[offset + 3] = value & 0b1111_1111;
return destination;
}
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);
});
});
Loading