From 9b15a113408d0852d4807a0e8c34e8fdc4ff9a76 Mon Sep 17 00:00:00 2001 From: janniks Date: Tue, 5 Dec 2023 16:22:01 +0100 Subject: [PATCH] feat: add nakamoto coinbase payload --- packages/transactions/src/constants.ts | 2 + packages/transactions/src/payload.ts | 71 +++++++++++++++++++-- packages/transactions/src/transaction.ts | 44 ++++++------- packages/transactions/tests/builder.test.ts | 10 +++ 4 files changed, 95 insertions(+), 32 deletions(-) diff --git a/packages/transactions/src/constants.ts b/packages/transactions/src/constants.ts index ae121f752..f64f6d65b 100644 --- a/packages/transactions/src/constants.ts +++ b/packages/transactions/src/constants.ts @@ -12,6 +12,7 @@ export const MAX_STRING_LENGTH_BYTES = 128; export const CLARITY_INT_SIZE = 128; export const CLARITY_INT_BYTE_SIZE = 16; export const COINBASE_BYTES_LENGTH = 32; +export const VRF_PROOF_BYTES_LENGTH = 80; export const RECOVERABLE_ECDSA_SIG_LENGTH_BYTES = 65; export const COMPRESSED_PUBKEY_LENGTH_BYTES = 32; export const UNCOMPRESSED_PUBKEY_LENGTH_BYTES = 64; @@ -57,6 +58,7 @@ export enum PayloadType { Coinbase = 0x04, CoinbaseToAltRecipient = 0x05, TenureChange = 0x7, + NakamotoCoinbase = 0x08, } /** diff --git a/packages/transactions/src/payload.ts b/packages/transactions/src/payload.ts index 9c89e9d3f..b37469678 100644 --- a/packages/transactions/src/payload.ts +++ b/packages/transactions/src/payload.ts @@ -8,12 +8,24 @@ import { writeUInt32BE, writeUInt8, } from '@stacks/common'; -import { ClarityVersion, COINBASE_BYTES_LENGTH, PayloadType, StacksMessageType } from './constants'; - import { BytesReader } from './bytesReader'; -import { ClarityValue, deserializeCV, serializeCV } from './clarity/'; +import { + ClarityType, + ClarityValue, + deserializeCV, + noneCV, + OptionalCV, + serializeCV, +} from './clarity/'; import { PrincipalCV, principalCV } from './clarity/types/principalCV'; import { Address } from './common'; +import { + ClarityVersion, + COINBASE_BYTES_LENGTH, + PayloadType, + StacksMessageType, + VRF_PROOF_BYTES_LENGTH, +} from './constants'; import { createAddress, createLPString, LengthPrefixedString } from './postcondition-types'; import { codeBodyString, @@ -33,6 +45,7 @@ export type Payload = | PoisonPayload | CoinbasePayload | CoinbasePayloadToAltRecipient + | NakamotoCoinbasePayload | TenureChangePayload; export function isTokenTransferPayload(p: Payload): p is TokenTransferPayload { @@ -67,6 +80,7 @@ export type PayloadInput = | PoisonPayload | CoinbasePayload | CoinbasePayloadToAltRecipient + | NakamotoCoinbasePayload | TenureChangePayload; export function createTokenTransferPayload( @@ -214,6 +228,36 @@ export function createCoinbasePayload( }; } +export interface NakamotoCoinbasePayload { + readonly type: StacksMessageType.Payload; + readonly payloadType: PayloadType.NakamotoCoinbase; + readonly coinbaseBytes: Uint8Array; + readonly recipient?: PrincipalCV; + readonly vrfProof: Uint8Array; +} + +export function createNakamotoCoinbasePayload( + coinbaseBytes: Uint8Array, + recipient: OptionalCV, + vrfProof: Uint8Array +): NakamotoCoinbasePayload { + if (coinbaseBytes.byteLength != COINBASE_BYTES_LENGTH) { + throw Error(`Coinbase buffer size must be ${COINBASE_BYTES_LENGTH} bytes`); + } + + if (vrfProof.byteLength != VRF_PROOF_BYTES_LENGTH) { + throw Error(`VRF proof buffer size must be ${VRF_PROOF_BYTES_LENGTH} bytes`); + } + + return { + type: StacksMessageType.Payload, + payloadType: PayloadType.NakamotoCoinbase, + coinbaseBytes, + recipient: recipient.type === ClarityType.OptionalSome ? recipient.value : undefined, + vrfProof, + }; +} + export enum TenureChangeCause { /** A valid winning block-commit */ BlockFound = 0, @@ -300,6 +344,11 @@ export function serializePayload(payload: PayloadInput): Uint8Array { bytesArray.push(payload.coinbaseBytes); bytesArray.push(serializeCV(payload.recipient)); break; + case PayloadType.NakamotoCoinbase: + bytesArray.push(payload.coinbaseBytes); + bytesArray.push(serializeCV(payload.recipient ?? noneCV())); + bytesArray.push(payload.vrfProof); + break; case PayloadType.TenureChange: bytesArray.push(hexToBytes(payload.previousTenureEnd)); bytesArray.push(writeUInt32BE(new Uint8Array(4), payload.previousTenureBlocks)); @@ -359,13 +408,21 @@ export function deserializePayload(bytesReader: BytesReader): Payload { case PayloadType.PoisonMicroblock: // TODO: implement return createPoisonPayload(); - case PayloadType.Coinbase: + case PayloadType.Coinbase: { const coinbaseBytes = bytesReader.readBytes(COINBASE_BYTES_LENGTH); return createCoinbasePayload(coinbaseBytes); - case PayloadType.CoinbaseToAltRecipient: - const coinbaseToAltRecipientBuffer = bytesReader.readBytes(COINBASE_BYTES_LENGTH); + } + case PayloadType.CoinbaseToAltRecipient: { + const coinbaseBytes = bytesReader.readBytes(COINBASE_BYTES_LENGTH); const altRecipient = deserializeCV(bytesReader) as PrincipalCV; - return createCoinbasePayload(coinbaseToAltRecipientBuffer, altRecipient); + return createCoinbasePayload(coinbaseBytes, altRecipient); + } + case PayloadType.NakamotoCoinbase: { + const coinbaseBytes = bytesReader.readBytes(COINBASE_BYTES_LENGTH); + const recipient = deserializeCV(bytesReader) as OptionalCV; + const vrfProof = bytesReader.readBytes(VRF_PROOF_BYTES_LENGTH); + return createNakamotoCoinbasePayload(coinbaseBytes, recipient, vrfProof); + } case PayloadType.TenureChange: const previousTenureEnd = bytesToHex(bytesReader.readBytes(32)); const previousTenureBlocks = bytesReader.readUInt32BE(); diff --git a/packages/transactions/src/transaction.ts b/packages/transactions/src/transaction.ts index 874e485b3..68d1ccbc5 100644 --- a/packages/transactions/src/transaction.ts +++ b/packages/transactions/src/transaction.ts @@ -6,20 +6,6 @@ import { intToBigInt, writeUInt32BE, } from '@stacks/common'; -import { - AnchorMode, - anchorModeFromNameOrValue, - AnchorModeName, - AuthType, - ChainID, - DEFAULT_CHAIN_ID, - PayloadType, - PostConditionMode, - PubKeyEncoding, - StacksMessageType, - TransactionVersion, -} from './constants'; - import { Authorization, deserializeAuthorization, @@ -34,19 +20,26 @@ import { SpendingConditionOpts, verifyOrigin, } from './authorization'; -import { createTransactionAuthField } from './signature'; - -import { cloneDeep, txidFromData } from './utils'; - -import { deserializePayload, Payload, PayloadInput, serializePayload } from './payload'; - -import { createLPList, deserializeLPList, LengthPrefixedList, serializeLPList } from './types'; - -import { isCompressed, StacksPrivateKey, StacksPublicKey } from './keys'; - import { BytesReader } from './bytesReader'; - +import { + AnchorMode, + anchorModeFromNameOrValue, + AnchorModeName, + AuthType, + ChainID, + DEFAULT_CHAIN_ID, + PayloadType, + PostConditionMode, + PubKeyEncoding, + StacksMessageType, + TransactionVersion, +} from './constants'; import { SerializationError, SigningError } from './errors'; +import { isCompressed, StacksPrivateKey, StacksPublicKey } from './keys'; +import { deserializePayload, Payload, PayloadInput, serializePayload } from './payload'; +import { createTransactionAuthField } from './signature'; +import { createLPList, deserializeLPList, LengthPrefixedList, serializeLPList } from './types'; +import { cloneDeep, txidFromData } from './utils'; export class StacksTransaction { version: TransactionVersion; @@ -86,6 +79,7 @@ export class StacksTransaction { switch (payload.payloadType) { case PayloadType.Coinbase: case PayloadType.CoinbaseToAltRecipient: + case PayloadType.NakamotoCoinbase: case PayloadType.PoisonMicroblock: case PayloadType.TenureChange: this.anchorMode = AnchorMode.OnChainOnly; diff --git a/packages/transactions/tests/builder.test.ts b/packages/transactions/tests/builder.test.ts index a7cb2e88f..cf6a35630 100644 --- a/packages/transactions/tests/builder.test.ts +++ b/packages/transactions/tests/builder.test.ts @@ -2186,3 +2186,13 @@ describe('serialize/deserialize tenure change', () => { expect(deserializePayload(reader)).toEqual(payload); }); }); + +describe('serialize/deserialize nakamoto coinbase', () => { + // test vector generated with mockamoto node + const txBytes = + '80800000000400ad0cc5ca0b4571dd435a9da7e16cbc662716dceb00000000000000010000000000000000000015833671ecd7432e6412423273eebf8a78d973beb08f690e58ba548f67ee26584967a5bc24d44f27ecca18e82a9956181e9d9cef7c67f718b33c5f5d0f82643801020000000008010101010101010101010101010101010101010101010101010101010101010109000000506f77e9a15503066b515060aa438ae3f5bc5207339b8e2933bdeae0891362d8e7ca2e5b047153904272d5f030ddcc83333676df6583394b0852a7e411b7c8d4c973f17fb7687601891ad7ca6707aa8408'; + const transaction = deserializeTransaction(txBytes); + + expect(transaction).toBeDefined(); + expect(bytesToHex(transaction.serialize())).toEqual(txBytes); +});