Skip to content

Commit

Permalink
feat: add nakamoto coinbase payload
Browse files Browse the repository at this point in the history
  • Loading branch information
janniks committed Dec 7, 2023
1 parent 1f9bd6e commit b503c97
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 53 deletions.
2 changes: 1 addition & 1 deletion packages/transactions/src/clarity/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/transactions/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,6 +58,7 @@ export enum PayloadType {
Coinbase = 0x04,
CoinbaseToAltRecipient = 0x05,
TenureChange = 0x7,
NakamotoCoinbase = 0x08,
}

/**
Expand Down
72 changes: 65 additions & 7 deletions packages/transactions/src/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,6 +46,7 @@ export type Payload =
| PoisonPayload
| CoinbasePayload
| CoinbasePayloadToAltRecipient
| NakamotoCoinbasePayload
| TenureChangePayload;

export function isTokenTransferPayload(p: Payload): p is TokenTransferPayload {
Expand Down Expand Up @@ -67,6 +81,7 @@ export type PayloadInput =
| PoisonPayload
| CoinbasePayload
| CoinbasePayloadToAltRecipient
| NakamotoCoinbasePayload
| TenureChangePayload;

export function createTokenTransferPayload(
Expand Down Expand Up @@ -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<PrincipalCV>,
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,
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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<PrincipalCV>;
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();
Expand Down
44 changes: 19 additions & 25 deletions packages/transactions/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
42 changes: 26 additions & 16 deletions packages/transactions/tests/builder.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand All @@ -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(
Expand Down Expand Up @@ -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);
});
23 changes: 19 additions & 4 deletions packages/transactions/tests/payload.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { utf8ToBytes } from '@stacks/common';
import { bytesToHex, hexToBytes, utf8ToBytes } from '@stacks/common';
import { BytesReader } from '../src';
import {
contractPrincipalCV,
falseCV,
Expand All @@ -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';

Expand Down Expand Up @@ -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);
});

0 comments on commit b503c97

Please sign in to comment.