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] feat: add nakamoto coinbase payload #1601

Merged
merged 1 commit into from
Dec 8, 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
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);
});
Loading