From b503c970cc2d8b60b892fdc0b0b0bbce1d2f9d41 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 --- .../transactions/src/clarity/serialize.ts | 2 +- packages/transactions/src/constants.ts | 2 + packages/transactions/src/payload.ts | 72 +++++++++++++++++-- packages/transactions/src/transaction.ts | 44 +++++------- packages/transactions/tests/builder.test.ts | 42 ++++++----- packages/transactions/tests/payload.test.ts | 23 ++++-- 6 files changed, 132 insertions(+), 53 deletions(-) diff --git a/packages/transactions/src/clarity/serialize.ts b/packages/transactions/src/clarity/serialize.ts index dca638a3e..397f457f8 100644 --- a/packages/transactions/src/clarity/serialize.ts +++ b/packages/transactions/src/clarity/serialize.ts @@ -135,7 +135,7 @@ function serializeStringUtf8CV(cv: StringUtf8CV) { * Serializes clarity value to Uint8Array * * @param {ClarityValue} value to be converted to bytes - ** + * * @returns {Uint8Array} returns the bytes * * @example 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..85f8f5f06 100644 --- a/packages/transactions/src/payload.ts +++ b/packages/transactions/src/payload.ts @@ -8,12 +8,25 @@ 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, + someCV, +} 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 +46,7 @@ export type Payload = | PoisonPayload | CoinbasePayload | CoinbasePayloadToAltRecipient + | NakamotoCoinbasePayload | TenureChangePayload; export function isTokenTransferPayload(p: Payload): p is TokenTransferPayload { @@ -67,6 +81,7 @@ export type PayloadInput = | PoisonPayload | CoinbasePayload | CoinbasePayloadToAltRecipient + | NakamotoCoinbasePayload | TenureChangePayload; export function createTokenTransferPayload( @@ -214,6 +229,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 +345,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 ? someCV(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 +409,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..d2be4ece0 100644 --- a/packages/transactions/tests/builder.test.ts +++ b/packages/transactions/tests/builder.test.ts @@ -1,24 +1,28 @@ import { bytesToHex, utf8ToBytes } from '@stacks/common'; import { - createApiKeyMiddleware, - createFetchFn, StacksMainnet, StacksTestnet, + createApiKeyMiddleware, + createFetchFn, } from '@stacks/network'; import * as fs from 'fs'; import fetchMock from 'jest-fetch-mock'; import { + MultiSigSpendingCondition, + SingleSigSpendingCondition, + SponsoredAuthorization, + StandardAuthorization, createSingleSigSpendingCondition, createSponsoredAuth, emptyMessageSignature, isSingleSig, - MultiSigSpendingCondition, nextSignature, - SingleSigSpendingCondition, - SponsoredAuthorization, - StandardAuthorization, } from '../src/authorization'; import { + SignedTokenTransferOptions, + TxBroadcastResult, + TxBroadcastResultOk, + TxBroadcastResultRejected, broadcastTransaction, callReadOnlyFunction, estimateTransaction, @@ -31,28 +35,24 @@ import { makeContractFungiblePostCondition, makeContractNonFungiblePostCondition, makeContractSTXPostCondition, + makeSTXTokenTransfer, makeStandardFungiblePostCondition, makeStandardNonFungiblePostCondition, makeStandardSTXPostCondition, - makeSTXTokenTransfer, makeUnsignedContractCall, makeUnsignedContractDeploy, makeUnsignedSTXTokenTransfer, - SignedTokenTransferOptions, sponsorTransaction, - TxBroadcastResult, - TxBroadcastResultOk, - TxBroadcastResultRejected, } from '../src/builders'; import { BytesReader } from '../src/bytesReader'; import { + ClarityType, + UIntCV, bufferCV, bufferCVFromString, - ClarityType, noneCV, serializeCV, standardPrincipalCV, - UIntCV, uintCV, } from '../src/clarity'; import { principalCV } from '../src/clarity/types/principalCV'; @@ -79,17 +79,17 @@ import { publicKeyToString, } from '../src/keys'; import { + TenureChangeCause, + TokenTransferPayload, 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 { StacksTransaction, deserializeTransaction } from '../src/transaction'; import { cloneDeep, randomBytes } from '../src/utils'; function setSignature( @@ -2186,3 +2186,13 @@ describe('serialize/deserialize tenure change', () => { expect(deserializePayload(reader)).toEqual(payload); }); }); + +test('serialize/deserialize nakamoto coinbase transaction', () => { + // test vector generated based on https://github.com/stacks-network/stacks-core/tree/396b34ba414220834de7ff96a890d55458ded51b + const txBytes = + '00000000000400143e543243dfcd8c02a12ad7ea371bd07bc91df900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010200000000081212121212121212121212121212121212121212121212121212121212121212099275df67a68c8745c0ff97b48201ee6db447f7c93b23ae24cdc2400f52fdb08a1a6ac7ec71bf9c9c76e96ee4675ebff60625af28718501047bfd87b810c2d2139b73c23bd69de66360953a642c2a330a'; + const transaction = deserializeTransaction(txBytes); + + expect(transaction).toBeDefined(); + expect(bytesToHex(transaction.serialize())).toEqual(txBytes); +}); diff --git a/packages/transactions/tests/payload.test.ts b/packages/transactions/tests/payload.test.ts index 4a20178e8..27719565e 100644 --- a/packages/transactions/tests/payload.test.ts +++ b/packages/transactions/tests/payload.test.ts @@ -1,4 +1,5 @@ -import { utf8ToBytes } from '@stacks/common'; +import { bytesToHex, hexToBytes, utf8ToBytes } from '@stacks/common'; +import { BytesReader } from '../src'; import { contractPrincipalCV, falseCV, @@ -11,13 +12,15 @@ import { CoinbasePayload, CoinbasePayloadToAltRecipient, ContractCallPayload, + SmartContractPayload, + TokenTransferPayload, + VersionedSmartContractPayload, createCoinbasePayload, createContractCallPayload, createSmartContractPayload, createTokenTransferPayload, - SmartContractPayload, - TokenTransferPayload, - VersionedSmartContractPayload, + deserializePayload, + serializePayload, } from '../src/payload'; import { serializeDeserialize } from './macros'; @@ -185,3 +188,15 @@ test('Coinbase to contract principal recipient payload serialization and deseria expect(deserialized.coinbaseBytes).toEqual(coinbaseBuffer); expect(deserialized.recipient).toEqual(contractRecipient); }); + +test.each([ + // test vector taken from https://github.com/stacks-network/stacks-core/blob/396b34ba414220834de7ff96a890d55458ded51b/stackslib/src/chainstate/stacks/transaction.rs#L2003-L2122 + '081212121212121212121212121212121212121212121212121212121212121212099275df67a68c8745c0ff97b48201ee6db447f7c93b23ae24cdc2400f52fdb08a1a6ac7ec71bf9c9c76e96ee4675ebff60625af28718501047bfd87b810c2d2139b73c23bd69de66360953a642c2a330a', + // test vector taken from https://github.com/stacks-network/stacks-core/blob/396b34ba414220834de7ff96a890d55458ded51b/stackslib/src/chainstate/stacks/transaction.rs#L2143-L2301 + '0812121212121212121212121212121212121212121212121212121212121212120a0601ffffffffffffffffffffffffffffffffffffffff0c666f6f2d636f6e74726163749275df67a68c8745c0ff97b48201ee6db447f7c93b23ae24cdc2400f52fdb08a1a6ac7ec71bf9c9c76e96ee4675ebff60625af28718501047bfd87b810c2d2139b73c23bd69de66360953a642c2a330a', +])('deserialize/serialize nakamoto coinbase payload', payloadBytes => { + const payload = deserializePayload(new BytesReader(hexToBytes(payloadBytes))); + + expect(payload).toBeDefined(); + expect(bytesToHex(serializePayload(payload))).toEqual(payloadBytes); +});