Skip to content

Commit 7e5752a

Browse files
authored
feat: support for Stacks 2.1 coinbase-pay-to-alt-recipient txs (#1283)
* feat: support Stacks 2.1 `coinbase-pay-to-alt` tx types * fix: add a `coinbase-to-alt` tx handling in a few more areas * chore: fix coinbase in mempool table schema * chore: update unit tests with new coinbase payload responses * test: add tests for ingesting new coinbase tx payloads and generating expected API responses * docs: better description for coinbase alt recipient
1 parent 4ff1a91 commit 7e5752a

File tree

15 files changed

+375
-13
lines changed

15 files changed

+375
-13
lines changed

docs/entities/transactions/transaction-4-coinbase-metadata.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
"data": {
1818
"type": "string",
1919
"description": "Hex encoded 32-byte scratch space for block leader's use"
20+
},
21+
"alt_recipient": {
22+
"type": "string",
23+
"nullable": true,
24+
"description": "A principal that will receive the miner rewards for this coinbase transaction. Can be either a standard principal or contract principal. Only specified for `coinbase-to-alt-recipient` transaction types, otherwise null."
2025
}
2126
}
2227
}

docs/generated.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,10 @@ export interface CoinbaseTransactionMetadata {
10911091
* Hex encoded 32-byte scratch space for block leader's use
10921092
*/
10931093
data: string;
1094+
/**
1095+
* A principal that will receive the miner rewards for this coinbase transaction. Can be either a standard principal or contract principal. Only specified for `coinbase-to-alt-recipient` transaction types, otherwise null.
1096+
*/
1097+
alt_recipient?: string;
10941098
};
10951099
}
10961100
/**

src/api/controllers/db-controller.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export function getTxTypeString(typeId: DbTxTypeId): Transaction['tx_type'] {
9191
case DbTxTypeId.PoisonMicroblock:
9292
return 'poison_microblock';
9393
case DbTxTypeId.Coinbase:
94+
case DbTxTypeId.CoinbaseToAltRecipient:
9495
return 'coinbase';
9596
default:
9697
throw new Error(`Unexpected DbTxTypeId: ${typeId}`);
@@ -121,7 +122,7 @@ export function getTxTypeId(typeString: Transaction['tx_type']): DbTxTypeId[] {
121122
case 'poison_microblock':
122123
return [DbTxTypeId.PoisonMicroblock];
123124
case 'coinbase':
124-
return [DbTxTypeId.Coinbase];
125+
return [DbTxTypeId.Coinbase, DbTxTypeId.CoinbaseToAltRecipient];
125126
default:
126127
throw new Error(`Unexpected tx type string: ${typeString}`);
127128
}
@@ -534,7 +535,10 @@ async function getRosettaBlockTxFromDataStore(opts: {
534535
let minerRewards: DbMinerReward[] = [],
535536
unlockingEvents: StxUnlockEvent[] = [];
536537

537-
if (opts.tx.type_id === DbTxTypeId.Coinbase) {
538+
if (
539+
opts.tx.type_id === DbTxTypeId.Coinbase ||
540+
opts.tx.type_id === DbTxTypeId.CoinbaseToAltRecipient
541+
) {
538542
minerRewards = await opts.db.getMinersRewardsAtHeight({
539543
blockHeight: opts.block.block_height,
540544
});
@@ -742,6 +746,20 @@ function parseDbTxTypeMetadata(dbTx: DbTx | DbMempoolTx): TransactionMetadata {
742746
tx_type: 'coinbase',
743747
coinbase_payload: {
744748
data: unwrapOptional(dbTx.coinbase_payload, () => 'Unexpected nullish coinbase_payload'),
749+
alt_recipient: null as any,
750+
},
751+
};
752+
return metadata;
753+
}
754+
case DbTxTypeId.CoinbaseToAltRecipient: {
755+
const metadata: CoinbaseTransactionMetadata = {
756+
tx_type: 'coinbase',
757+
coinbase_payload: {
758+
data: unwrapOptional(dbTx.coinbase_payload, () => 'Unexpected nullish coinbase_payload'),
759+
alt_recipient: unwrapOptional(
760+
dbTx.coinbase_alt_recipient,
761+
() => 'Unexpected nullish coinbase_alt_recipient'
762+
),
745763
},
746764
};
747765
return metadata;

src/datastore/common.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ export interface DbTx extends BaseTx {
180180
/** Only valid for `coinbase` tx types. Hex encoded 32-bytes. */
181181
coinbase_payload?: string;
182182

183+
/** Only valid for `coinbase-to-alt-recipient` tx types. Either a standard principal or contract principal. */
184+
coinbase_alt_recipient?: string;
185+
183186
event_count: number;
184187

185188
execution_cost_read_count: number;
@@ -249,6 +252,9 @@ export interface DbMempoolTx extends BaseTx {
249252

250253
/** Only valid for `coinbase` tx types. Hex encoded 32-bytes. */
251254
coinbase_payload?: string;
255+
256+
/** Only valid for `coinbase-to-alt-recipient` tx types. Either a standard principal or contract principal. */
257+
coinbase_alt_recipient?: string;
252258
}
253259

254260
export interface DbSmartContract {
@@ -694,6 +700,9 @@ export interface MempoolTxQueryResult {
694700
// `coinbase` tx types
695701
coinbase_payload?: string;
696702

703+
/** Only valid for `coinbase-to-alt-recipient` tx types. Either a standard principal or contract principal. */
704+
coinbase_alt_recipient?: string;
705+
697706
// sending abi in case tx is contract call
698707
abi: unknown | null;
699708
}
@@ -752,6 +761,9 @@ export interface TxQueryResult {
752761
// `coinbase` tx types
753762
coinbase_payload?: string;
754763

764+
// `coinbase-to-alt-recipient` tx types
765+
coinbase_alt_recipient?: string;
766+
755767
// events count
756768
event_count: number;
757769

@@ -903,6 +915,7 @@ export interface TxInsertValues {
903915
poison_microblock_header_1: PgBytea | null;
904916
poison_microblock_header_2: PgBytea | null;
905917
coinbase_payload: PgBytea | null;
918+
coinbase_alt_recipient: string | null;
906919
raw_result: PgBytea;
907920
event_count: number;
908921
execution_cost_read_count: number;
@@ -941,6 +954,7 @@ export interface MempoolTxInsertValues {
941954
poison_microblock_header_1: PgBytea | null;
942955
poison_microblock_header_2: PgBytea | null;
943956
coinbase_payload: PgBytea | null;
957+
coinbase_alt_recipient: string | null;
944958
}
945959

946960
export interface BlockInsertValues {

src/datastore/helpers.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export const TX_COLUMNS = [
8383
'poison_microblock_header_1',
8484
'poison_microblock_header_2',
8585
'coinbase_payload',
86+
'coinbase_alt_recipient',
8687
'raw_result',
8788
'event_count',
8889
'execution_cost_read_count',
@@ -121,6 +122,7 @@ export const MEMPOOL_TX_COLUMNS = [
121122
'poison_microblock_header_1',
122123
'poison_microblock_header_2',
123124
'coinbase_payload',
125+
'coinbase_alt_recipient',
124126
];
125127

126128
export const BLOCK_COLUMNS = [
@@ -313,6 +315,9 @@ function parseTxTypeSpecificQueryResult(
313315
target.poison_microblock_header_2 = result.poison_microblock_header_2;
314316
} else if (target.type_id === DbTxTypeId.Coinbase) {
315317
target.coinbase_payload = result.coinbase_payload;
318+
} else if (target.type_id === DbTxTypeId.CoinbaseToAltRecipient) {
319+
target.coinbase_payload = result.coinbase_payload;
320+
target.coinbase_alt_recipient = result.coinbase_alt_recipient;
316321
} else {
317322
throw new Error(`Received unexpected tx type_id from db query: ${target.type_id}`);
318323
}
@@ -731,6 +736,15 @@ function extractTransactionPayload(txData: DecodedTxResult, dbTx: DbTx | DbMempo
731736
dbTx.coinbase_payload = txData.payload.payload_buffer;
732737
break;
733738
}
739+
case TxPayloadTypeID.CoinbaseToAltRecipient: {
740+
dbTx.coinbase_payload = txData.payload.payload_buffer;
741+
if (txData.payload.recipient.type_id === PrincipalTypeID.Standard) {
742+
dbTx.coinbase_alt_recipient = txData.payload.recipient.address;
743+
} else {
744+
dbTx.coinbase_alt_recipient = `${txData.payload.recipient.address}.${txData.payload.recipient.contract_name}`;
745+
}
746+
break;
747+
}
734748
default:
735749
throw new Error(`Unexpected transaction type ID: ${JSON.stringify(txData.payload)}`);
736750
}

src/datastore/pg-store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3686,7 +3686,7 @@ export class PgStore {
36863686
SELECT tx_id
36873687
FROM txs
36883688
WHERE microblock_canonical = true AND canonical = true
3689-
AND block_height = ${block.block_height} AND type_id = ${DbTxTypeId.Coinbase}
3689+
AND block_height = ${block.block_height} AND (type_id = ${DbTxTypeId.Coinbase} OR type_id = ${DbTxTypeId.CoinbaseToAltRecipient})
36903690
LIMIT 1
36913691
`;
36923692

src/datastore/pg-write-store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,7 @@ export class PgWriteStore extends PgStore {
11641164
poison_microblock_header_1: tx.poison_microblock_header_1 ?? null,
11651165
poison_microblock_header_2: tx.poison_microblock_header_2 ?? null,
11661166
coinbase_payload: tx.coinbase_payload ?? null,
1167+
coinbase_alt_recipient: tx.coinbase_alt_recipient ?? null,
11671168
raw_result: tx.raw_result,
11681169
event_count: tx.event_count,
11691170
execution_cost_read_count: tx.execution_cost_read_count,
@@ -1213,6 +1214,7 @@ export class PgWriteStore extends PgStore {
12131214
poison_microblock_header_1: tx.poison_microblock_header_1 ?? null,
12141215
poison_microblock_header_2: tx.poison_microblock_header_2 ?? null,
12151216
coinbase_payload: tx.coinbase_payload ?? null,
1217+
coinbase_alt_recipient: tx.coinbase_alt_recipient ?? null,
12161218
};
12171219
const result = await sql`
12181220
INSERT INTO mempool_txs ${sql(values)}

src/event-stream/reader.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,18 @@ export function parseMessageTransaction(
298298
case TxPayloadTypeID.Coinbase: {
299299
break;
300300
}
301+
case TxPayloadTypeID.CoinbaseToAltRecipient: {
302+
if (payload.recipient.type_id === PrincipalTypeID.Standard) {
303+
logger.verbose(
304+
`Coinbase to alt recipient, standard principal: ${payload.recipient.address}`
305+
);
306+
} else {
307+
logger.verbose(
308+
`Coinbase to alt recipient, contract principal: ${payload.recipient.address}.${payload.recipient.contract_name}`
309+
);
310+
}
311+
break;
312+
}
301313
case TxPayloadTypeID.SmartContract: {
302314
logger.verbose(
303315
`Smart contract deployed: ${parsedTx.sender_address}.${payload.contract_name}`

src/migrations/1584619633448_txs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
160160

161161
// `coinbase` tx types
162162
coinbase_payload: 'bytea',
163+
164+
// `coinbase-pay-to-alt` tx types
165+
coinbase_alt_recipient: 'string',
163166
});
164167

165168
pgm.createIndex('txs', 'tx_id', { method: 'hash' });
@@ -170,6 +173,7 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
170173
pgm.createIndex('txs', 'smart_contract_contract_id', { method: 'hash' });
171174
pgm.createIndex('txs', 'sponsor_address', { method: 'hash' });
172175
pgm.createIndex('txs', 'token_transfer_recipient_address', { method: 'hash' });
176+
pgm.createIndex('txs', 'coinbase_alt_recipient');
173177
pgm.createIndex('txs', 'type_id');
174178
pgm.createIndex('txs', [{ name: 'tx_index', sort: 'DESC' }]);
175179
pgm.createIndex('txs', [
@@ -203,4 +207,8 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
203207
pgm.addConstraint('txs', 'valid_coinbase', `CHECK (type_id != 4 OR (
204208
NOT (coinbase_payload) IS NULL
205209
))`);
210+
211+
pgm.addConstraint('txs', 'valid_coinbase-pay-to-alt', `CHECK (type_id != 5 OR (
212+
NOT (coinbase_payload, coinbase_alt_recipient) IS NULL
213+
))`);
206214
}

src/migrations/1591291822107_mempool_txs.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
9393

9494
// `coinbase` tx types
9595
coinbase_payload: 'bytea',
96+
97+
// `coinbase-pay-to-alt` tx types
98+
coinbase_alt_recipient: 'string',
9699
});
97100

98101
pgm.createIndex('mempool_txs', 'tx_id', { method: 'hash' });
@@ -129,4 +132,8 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
129132
pgm.addConstraint('mempool_txs', 'valid_coinbase', `CHECK (type_id != 4 OR (
130133
NOT (coinbase_payload) IS NULL
131134
))`);
135+
136+
pgm.addConstraint('mempool_txs', 'valid_coinbase-pay-to-alt', `CHECK (type_id != 5 OR (
137+
NOT (coinbase_payload, coinbase_alt_recipient) IS NULL
138+
))`);
132139
}

0 commit comments

Comments
 (0)