diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts
index 3e1fe2108..7fa431db6 100644
--- a/backend/src/api/bitcoin/bitcoin-api.ts
+++ b/backend/src/api/bitcoin/bitcoin-api.ts
@@ -323,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard',
'multisig': 'multisig',
+ 'anchor': 'anchor',
'nulldata': 'op_return'
};
diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts
index a5b8af0e2..306179ca5 100644
--- a/backend/src/api/blocks.ts
+++ b/backend/src/api/blocks.ts
@@ -219,10 +219,10 @@ class Blocks {
};
}
- public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
+ public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
return {
id: hash,
- transactions: Common.classifyTransactions(transactions),
+ transactions: Common.classifyTransactions(transactions, height),
};
}
@@ -616,7 +616,7 @@ class Blocks {
// add CPFP
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify
- const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
+ const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
@@ -653,7 +653,7 @@ class Blocks {
}
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
// classify
- const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
+ const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx;
@@ -912,7 +912,7 @@ class Blocks {
}
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
- const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
+ const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
if (Common.indexingEnabled()) {
@@ -1169,7 +1169,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0;
try {
- flags = Common.getTransactionFlags(tx);
+ flags = Common.getTransactionFlags(tx, height);
} catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
}
@@ -1188,7 +1188,7 @@ class Blocks {
} else {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
- summary = this.summarizeBlockTransactions(hash, txs);
+ summary = this.summarizeBlockTransactions(hash, height || 0, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@@ -1324,7 +1324,7 @@ class Blocks {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
- summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
+ summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
summaryVersion = 1;
} else {
// Call Core RPC
diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts
index 13fc86147..d17068a09 100644
--- a/backend/src/api/common.ts
+++ b/backend/src/api/common.ts
@@ -10,7 +10,6 @@ import logger from '../logger';
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
// Bitcoin Core default policy settings
-const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@@ -200,10 +199,13 @@ export class Common {
*
* returns true early if any standardness rule is violated, otherwise false
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
+ *
+ * As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
+ * For now, just pull out individual rules into versioned functions where necessary.
*/
- static isNonStandard(tx: TransactionExtended): boolean {
+ static isNonStandard(tx: TransactionExtended, height?: number): boolean {
// version
- if (tx.version > TX_MAX_STANDARD_VERSION) {
+ if (this.isNonStandardVersion(tx, height)) {
return true;
}
@@ -250,6 +252,8 @@ export class Common {
}
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true;
+ } else if (this.isNonStandardAnchor(tx, height)) {
+ return true;
}
// TODO: bad-witness-nonstandard
}
@@ -335,6 +339,49 @@ export class Common {
return false;
}
+ // Individual versioned standardness rules
+
+ static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
+ 'testnet4': 42_000,
+ 'testnet': 2_900_000,
+ 'signet': 211_000,
+ '': 863_500,
+ };
+ static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
+ let TX_MAX_STANDARD_VERSION = 3;
+ if (
+ height != null
+ && this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
+ && height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
+ ) {
+ // V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
+ TX_MAX_STANDARD_VERSION = 2;
+ }
+
+ if (tx.version > TX_MAX_STANDARD_VERSION) {
+ return true;
+ }
+ return false;
+ }
+
+ static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
+ 'testnet4': 42_000,
+ 'testnet': 2_900_000,
+ 'signet': 211_000,
+ '': 863_500,
+ };
+ static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
+ if (
+ height != null
+ && this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
+ && height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
+ ) {
+ // anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
+ return true;
+ }
+ return false;
+ }
+
static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight;
let hasWitness = false;
@@ -415,7 +462,7 @@ export class Common {
return flags;
}
- static getTransactionFlags(tx: TransactionExtended): number {
+ static getTransactionFlags(tx: TransactionExtended, height?: number): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF)
@@ -548,7 +595,7 @@ export class Common {
if (hasFakePubkey) {
flags |= TransactionFlags.fake_pubkey;
}
-
+
// fast but bad heuristic to detect possible coinjoins
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
@@ -564,17 +611,17 @@ export class Common {
flags |= TransactionFlags.batch_payout;
}
- if (this.isNonStandard(tx)) {
+ if (this.isNonStandard(tx, height)) {
flags |= TransactionFlags.nonstandard;
}
return Number(flags);
}
- static classifyTransaction(tx: TransactionExtended): TransactionClassified {
+ static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
let flags = 0;
try {
- flags = Common.getTransactionFlags(tx);
+ flags = Common.getTransactionFlags(tx, height);
} catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
}
@@ -585,8 +632,8 @@ export class Common {
};
}
- static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
- return txs.map(Common.classifyTransaction);
+ static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
+ return txs.map(tx => Common.classifyTransaction(tx, height));
}
static stripTransaction(tx: TransactionExtended): TransactionStripped {
diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts
index 90100a767..de6c1deb8 100644
--- a/backend/src/repositories/BlocksRepository.ts
+++ b/backend/src/repositories/BlocksRepository.ts
@@ -1106,7 +1106,7 @@ class BlocksRepository {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
- summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
+ summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
summaryVersion = 1;
} else {
// Call Core RPC
diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts
index dd81b9809..ff3c27240 100644
--- a/frontend/src/app/components/address-labels/address-labels.component.ts
+++ b/frontend/src/app/components/address-labels/address-labels.component.ts
@@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
}
handleVin() {
- const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin])
+ const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]);
if (address?.scripts.size) {
const script = address?.scripts.values().next().value;
if (script.template?.label) {
diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts
index 24b5fc1dc..3b0f53e9c 100644
--- a/frontend/src/app/components/tracker/tracker.component.ts
+++ b/frontend/src/app/components/tracker/tracker.component.ts
@@ -747,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
checkAccelerationEligibility() {
if (this.tx) {
- this.tx.flags = getTransactionFlags(this.tx);
+ this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n;
const highSigop = (this.tx.sigops * 20) > this.tx.weight;
this.eligibleForAcceleration = !replaceableInputs && !highSigop;
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts
index 8c0d3b4a9..c80006552 100644
--- a/frontend/src/app/components/transaction/transaction.component.ts
+++ b/frontend/src/app/components/transaction/transaction.component.ts
@@ -901,7 +901,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
- this.tx.flags = getTransactionFlags(this.tx);
+ this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
this.checkAccelerationEligibility();
} else {
diff --git a/frontend/src/app/shared/address-utils.ts b/frontend/src/app/shared/address-utils.ts
index 92646af14..59c85014b 100644
--- a/frontend/src/app/shared/address-utils.ts
+++ b/frontend/src/app/shared/address-utils.ts
@@ -17,6 +17,7 @@ export type AddressType = 'fee'
| 'v0_p2wsh'
| 'v1_p2tr'
| 'confidential'
+ | 'anchor'
| 'unknown'
const ADDRESS_PREFIXES = {
@@ -188,6 +189,12 @@ export class AddressTypeInfo {
const v = vin[0];
this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm));
}
+ } else if (this.type === 'unknown') {
+ for (const v of vin) {
+ if (v.prevout?.scriptpubkey === '51024e73') {
+ this.type = 'anchor';
+ }
+ }
}
// and there's nothing more to learn from processing inputs for other types
}
@@ -197,6 +204,10 @@ export class AddressTypeInfo {
if (!this.scripts.size) {
this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm));
}
+ } else if (this.type === 'unknown') {
+ if (output.scriptpubkey === '51024e73') {
+ this.type = 'anchor';
+ }
}
}
diff --git a/frontend/src/app/shared/components/address-type/address-type.component.html b/frontend/src/app/shared/components/address-type/address-type.component.html
index fe4286689..598c21a6e 100644
--- a/frontend/src/app/shared/components/address-type/address-type.component.html
+++ b/frontend/src/app/shared/components/address-type/address-type.component.html
@@ -20,6 +20,9 @@
@case ('multisig') {
bare multisig
}
+ @case ('anchor') {
+ anchor
+ }
@case (null) {
unknown
}
diff --git a/frontend/src/app/shared/script.utils.ts b/frontend/src/app/shared/script.utils.ts
index 171112dcc..637eede30 100644
--- a/frontend/src/app/shared/script.utils.ts
+++ b/frontend/src/app/shared/script.utils.ts
@@ -166,6 +166,7 @@ export const ScriptTemplates: { [type: string]: (...args: any) => ScriptTemplate
ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }),
ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }),
multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }),
+ anchor: () => ({ type: 'anchor', label: 'anchor' }),
};
export class ScriptInfo {
diff --git a/frontend/src/app/shared/transaction.utils.ts b/frontend/src/app/shared/transaction.utils.ts
index c13616c60..bbf28a250 100644
--- a/frontend/src/app/shared/transaction.utils.ts
+++ b/frontend/src/app/shared/transaction.utils.ts
@@ -2,9 +2,9 @@ import { TransactionFlags } from './filters.utils';
import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils';
import { Transaction } from '../interfaces/electrs.interface';
import { CpfpInfo, RbfInfo, TransactionStripped } from '../interfaces/node-api.interface';
+import { StateService } from '../services/state.service';
// Bitcoin Core default policy settings
-const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@@ -89,10 +89,13 @@ export function isDERSig(w: string): boolean {
*
* returns true early if any standardness rule is violated, otherwise false
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
+ *
+ * As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
+ * For now, just pull out individual rules into versioned functions where necessary.
*/
-export function isNonStandard(tx: Transaction): boolean {
+export function isNonStandard(tx: Transaction, height?: number, network?: string): boolean {
// version
- if (tx.version > TX_MAX_STANDARD_VERSION) {
+ if (isNonStandardVersion(tx, height, network)) {
return true;
}
@@ -139,6 +142,8 @@ export function isNonStandard(tx: Transaction): boolean {
}
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true;
+ } else if (isNonStandardAnchor(tx, height, network)) {
+ return true;
}
// TODO: bad-witness-nonstandard
}
@@ -203,6 +208,51 @@ export function isNonStandard(tx: Transaction): boolean {
return false;
}
+// Individual versioned standardness rules
+
+const V3_STANDARDNESS_ACTIVATION_HEIGHT = {
+ 'testnet4': 42_000,
+ 'testnet': 2_900_000,
+ 'signet': 211_000,
+ '': 863_500,
+};
+function isNonStandardVersion(tx: Transaction, height?: number, network?: string): boolean {
+ let TX_MAX_STANDARD_VERSION = 3;
+ if (
+ height != null
+ && network != null
+ && V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
+ && height <= V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
+ ) {
+ // V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
+ TX_MAX_STANDARD_VERSION = 2;
+ }
+
+ if (tx.version > TX_MAX_STANDARD_VERSION) {
+ return true;
+ }
+ return false;
+}
+
+const ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
+ 'testnet4': 42_000,
+ 'testnet': 2_900_000,
+ 'signet': 211_000,
+ '': 863_500,
+};
+function isNonStandardAnchor(tx: Transaction, height?: number, network?: string): boolean {
+ if (
+ height != null
+ && network != null
+ && ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
+ && height <= ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
+ ) {
+ // anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
+ return true;
+ }
+ return false;
+}
+
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
// followed by a data push between 2 and 40 bytes.
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
@@ -289,7 +339,7 @@ export function isBurnKey(pubkey: string): boolean {
].includes(pubkey);
}
-export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean): bigint {
+export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean, height?: number, network?: string): bigint {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF)
@@ -439,7 +489,7 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
flags |= TransactionFlags.batch_payout;
}
- if (isNonStandard(tx)) {
+ if (isNonStandard(tx, height, network)) {
flags |= TransactionFlags.nonstandard;
}