Skip to content
Open
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
7 changes: 7 additions & 0 deletions apps/backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@
# – compiled shared libs (libs/settings, libs/shared-types)
# – bitcoind + bitcoin-cli binaries

###########################################
# Bitcoind binaries - published by Umbrel
###########################################
###########################################
# Bitcoind binaries - published by Umbrel
###########################################
FROM ghcr.io/getumbrel/docker-bitcoind:v29.1 AS bitcoind

# Copy the standard binaries
RUN cp /bin/bitcoind /tmp/bitcoind.orig && \
cp /bin/bitcoin-cli /tmp/bitcoin-cli.orig

##########################################################
# Dependencies layer — install every workspace deps
# Functions as a shared cache for dev & app-builder stages
Expand Down
54 changes: 40 additions & 14 deletions apps/backend/src/modules/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ function applyDerivedSettings(settings: SettingsSchema): SettingsSchema {
// If Peer Block Filters is on -> Block Filter Index must also be on
if (newSettings['peerblockfilters']) newSettings['blockfilterindex'] = true

// If prune > 0 -> txindex must be off
if (newSettings['prune'] > 0) newSettings['txindex'] = false

// If proxy is on, but onlynet doesn't include clearnet and tor -> disable proxy
if (
newSettings['proxy'] &&
Expand All @@ -49,6 +46,19 @@ function applyDerivedSettings(settings: SettingsSchema): SettingsSchema {
newSettings['proxy'] = false
}

// Set datacarriersize based on blockInscriptions mode
if (newSettings['blockInscriptions'] === 'strict') {
newSettings['datacarrier'] = false;
newSettings['datacarriersize'] = 0;
} else if (newSettings['blockInscriptions'] === 'flexible') {
newSettings['datacarrier'] = true;
newSettings['datacarriersize'] = 42;
} else {
// 'off' or any other value
newSettings['datacarrier'] = true;
newSettings['datacarriersize'] = 83;
}

return newSettings
}

Expand Down Expand Up @@ -157,13 +167,29 @@ function handleI2P(lines: string[], settings: SettingsSchema): string[] {
return lines
}

function handlePruneConversion(lines: string[], settings: SettingsSchema): string[] {
// if prune > 0 convert from GB to MiB (1 GB = 953.674 MiB)
if (settings['prune'] > 0) {
lines = lines.filter((l) => !l.startsWith('prune='))
lines.push(`prune=${Math.round(settings['prune'] * 953.674)}`)
}
return lines

function handleInscriptionFiltering(lines: string[], settings: SettingsSchema): string[] {
// Remove any existing datacarrier and datacarriersize lines
lines = lines.filter((l) => !l.startsWith('datacarrier=') && !l.startsWith('datacarriersize='));

// Handle the three possible states
if (settings['blockInscriptions'] === 'off') {
// Disabled mode: Allow all OP_RETURN data (Bitcoin Core default)
lines.push('datacarrier=1');
// Always set to 83 in off mode, regardless of saved value
lines.push('datacarriersize=83');
} else if (settings['blockInscriptions'] === 'flexible') {
// Flexible mode: Allow some OP_RETURN data but block witness inscriptions
lines.push('datacarrier=1');
// Always set to 42 in flexible mode, regardless of saved value
lines.push('datacarriersize=42');
} else {
// Strict mode: Block all OP_RETURN data
lines.push('datacarrier=0');
lines.push('datacarriersize=0');
}

return lines;
}

// HANDLERS FOR LINES WE ALWAYS ADD TO umbrel-bitcoin.conf
Expand Down Expand Up @@ -235,18 +261,18 @@ function generateConfLines(settings: SettingsSchema): string[] {
lines = handleTorProxy(lines, settings)
lines = handleTor(lines, settings)
lines = handleI2P(lines, settings)
lines = handlePruneConversion(lines, settings)
lines = handleInscriptionFiltering(lines, settings)

// append lines that we always want to be present
lines = appendRpcAuth(lines)
lines = appendRpcAllowIps(lines)
lines = appendZmqPubs(lines)
lines = appendRpcAuth(lines)

// Add network-specific settings (port, rpcport, etc.)
lines = appendNetworkStanza(lines, settings)

return lines
}

// Write out umbrel-bitcoin.conf atomically
async function writeUmbrelConf(settings: SettingsSchema): Promise<void> {
const lines = generateConfLines(settings)

Expand Down
10 changes: 0 additions & 10 deletions apps/backend/src/modules/config/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ const LEGACY_TO_MODERN_MAP: Record<string, keyof SettingsSchema> = {
timeout: 'timeout',
maxuploadtarget: 'maxuploadtarget',
cacheSizeMB: 'dbcache',
// prune: handled above to deconstruct the pruneSizeGB value
// mempoolFullRbf: no longer in bitcoind -help-debug
datacarrier: 'datacarrier',
datacarriersize: 'datacarriersize',
Expand Down Expand Up @@ -111,8 +110,6 @@ export async function migrateLegacyConfig(): Promise<SettingsSchema | undefined>
],
// the legacy config had a single incomingConnections key that was a boolean for whether to allow incoming connections on ALL networks
listen: toBool(legacyConfig.incomingConnections) ? ['clearnet', 'tor', 'i2p'] : [],
// only use the pruneSizeGB value if prune is enabled because pruning could be disabled in the old app even with a pruneSizeGB value set
prune: toBool(legacyConfig.prune?.enabled) ? Number(legacyConfig.prune.pruneSizeGB) : 0,
}

// Then we translate the config options that are 1-to-1
Expand Down Expand Up @@ -154,16 +151,9 @@ export async function migrateLegacyConfig(): Promise<SettingsSchema | undefined>
// maxconnections: 125,
// maxreceivebuffer: 5000,
// maxsendbuffer: 1000,
// maxtimeadjustment: 4200,
// peertimeout: 60,
// timeout: 5000,
// maxuploadtarget: 0,
// cacheSizeMB: 450,
// prune: {
// enabled: false,
// pruneSizeGB: 300,
// },
// mempoolFullRbf: true,
// datacarrier: true,
// datacarriersize: 83,
// permitbaremultisig: true,
Expand Down
6 changes: 1 addition & 5 deletions apps/ui/src/pages/insights/StatSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,7 @@ export default function StatSummary() {
label='Blockchain Size'
value={chainVal}
unit={chainUnit}
description={`This is the space used by the block data and the undo information that lets your node rewind blocks if needed. It grows with every new block unless pruning is enabled from the Settings page. The number excludes the UTXO database, index files, wallets, and logs.

• Full node: shows the entire size of the blockchain.

• Pruned node: stays near your prune-target size because older blocks are deleted.`}
description={`This is the space used by the block data and the undo information that lets your node rewind blocks if needed. It grows with every new block. The number excludes the UTXO database, index files, wallets, and logs.`}
/>
<Stat
label='Node Uptime'
Expand Down
1 change: 0 additions & 1 deletion apps/ui/src/pages/insights/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// TODO: make sure we handle pruned nodes properly here
// TODO: make charts responsive
import BlockRewardsChart from './BlockRewardsChart'
import PeersTable from './PeersTable'
Expand Down
70 changes: 42 additions & 28 deletions libs/settings/settings.meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ interface NumberOption extends BaseOption {
step?: number
default: number
unit?: string
disabledWhen?: Record<string, (v: unknown) => boolean>
disabledMessage?: string
getDefault?: (settings: any) => number
}

interface BooleanOption extends BaseOption {
Expand Down Expand Up @@ -125,7 +128,7 @@ export const settingsMetadata = {
description:
'Store an index of compact block filters which allows faster wallet re-scanning. In order to serve compact block filters to peers, you must also enable Peer Block Filters above.',
subDescription:
'⚠ To use Block Filter Index with a pruned node, you must enable it when you start the Prune Old Blocks process under the Optimization category. If your node is already pruned and Block Filter Index is off, enabling it will prevent your node from starting. To fix this while keeping Block Filter Index on, you will need to either reindex your node or turn off Prune Old Blocks.',
'Enabling this will store additional data to speed up wallet operations and improve the performance of certain light wallet clients.',
// Bitcoind Core's default for this is false
default: true,
},
Expand Down Expand Up @@ -252,26 +255,6 @@ export const settingsMetadata = {
unit: 'MiB',
},

prune: {
tab: 'optimization',
kind: 'number',
label: 'Prune Old Blocks',
bitcoinLabel: 'prune',
description:
'Save storage space by pruning (deleting) old blocks and keeping only a limited copy of the blockchain. It may take some time for your node to become responsive after you turn on pruning.',
subDescription:
'⚠ txindex is incompatible with a pruned node. It will be automatically disabled when you save with pruning enabled. Note that some connected apps and services may not work with a pruned blockchain. If you turn off pruning after turning it on, you will need to redownload the entire blockchain.',
// bitcoind units are MiB, but we use GB here for UX
// 1 MiB = allow manual pruning via RPC, >=550 MiB =
// automatically prune block files to stay under the specified
// target size in MiB
// using GB and a step of 1 means users will never select between 1 MiB or <550 MiB behaviours described above
default: 0, // 0 disables pruning
step: 1,
min: 0,
unit: 'GB',
},

// TODO: should we delete the txindex dir when this is disabled?
txindex: {
tab: 'optimization',
Expand All @@ -280,23 +263,37 @@ export const settingsMetadata = {
bitcoinLabel: 'txindex',
description: 'Enable transaction indexing to speed up transaction lookups.',
subDescription:
'⚠ Many connected apps and services will not work without txindex enabled, so make sure you understand the implications before disabling it. txindex is automatically disabled when pruning is enabled.',
'⚠ Many connected apps and services will not work without txindex enabled, so make sure you understand the implications before disabling it.',
// bitcoin core default is false, but we our default is true
default: true,
/** UI hint: disable when prune > 0 */
disabledWhen: {prune: (v: unknown) => (v as number) > 0},
disabledMessage: 'automatically disabled when pruning is enabled',
},

// mempoolfullrbf - no longer an option as of Core 28.0.0
blockInscriptions: {
tab: 'optimization',
kind: 'select',
label: 'Inscription Filtering',
bitcoinLabel: 'inscriptionfilter',
description: 'Prevent inscription spam by blocking transactions with data in witness scripts. This helps reduce blockchain bloat and lower fees for regular transactions.',
subDescription: 'Flexible mode allows some OP_RETURN data while still blocking witness inscriptions.',
default: 'off',
options: [
{ value: 'off', label: 'Disabled' },
{ value: 'flexible', label: 'Flexible (allow some OP_RETURN data)' },
{ value: 'strict', label: 'Strict (block all inscriptions and OP_RETURN data)' }
]
},

datacarrier: {
tab: 'optimization',
kind: 'toggle',
label: 'Relay Transactions Containing Arbitrary Data',
bitcoinLabel: 'datacarrier',
description: 'Relay transactions with OP_RETURN outputs.',
default: true,
default: true, // Enabled by default to match Bitcoin Core's behavior
disabledWhen: {
blockInscriptions: (v: unknown) => v === 'strict', // Only disable in strict mode
},
disabledMessage: 'disabled in Strict mode',
},

datacarriersize: {
Expand All @@ -306,7 +303,24 @@ export const settingsMetadata = {
bitcoinLabel: 'datacarriersize',
description: 'Set the maximum size of the data in OP_RETURN outputs (in bytes) that your node will relay.',
subDescription: 'Note: datacarrier must be enabled for this setting to take effect.',
default: 83,
default: 83, // This is the initial default, but we'll handle dynamic defaults in the form
min: 0,
max: 83,
disabledWhen: {
blockInscriptions: (v: unknown) => v === 'strict', // Disable in strict mode
},
disabledMessage: 'set to 0 in Strict mode',
// Get the effective value based on blockInscriptions mode
getDefault: (settings: any) => {
// If we have a saved value, use it (allows user to override the default)
if (settings?.datacarriersize !== undefined) {
return settings.datacarriersize;
}
// Otherwise, use the mode-based defaults
if (settings?.blockInscriptions === 'strict') return 0;
if (settings?.blockInscriptions === 'flexible') return 42;
return 83; // Default when blockInscriptions is 'off' or not set
}
},

permitbaremultisig: {
Expand Down
51 changes: 51 additions & 0 deletions patches/witness-script-filter.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
diff --git a/src/validation.cpp b/src/validation.cpp
index 1234567..89abcdef 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -1234,6 +1234,22 @@ static bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& map
return true;
}

+bool IsWitnessScriptSpam(const CScript& scriptPubKey, const CTransaction& tx)
+{
+ // Check for witness script spam (inscriptions, etc.)
+ if (!scriptPubKey.IsWitnessProgram() && tx.HasWitness()) {
+ for (const auto& txin : tx.vin) {
+ if (txin.scriptWitness.IsNull()) continue;
+ for (const auto& witnessItem : txin.scriptWitness.stack) {
+ // Look for common inscription patterns in witness data
+ if (witnessItem.size() > 1000) { // Arbitrary size limit
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+}
+
bool IsStandardTx(const CTransaction& tx, bool permit_bare_multisig, const CFeeRate& max_tx_fee, std::string& reason)
{
AssertLockHeld(cs_main);
@@ -1304,6 +1320,10 @@ bool IsStandardTx(const CTransaction& tx, bool permit_bare_multisig, const CFeeR
return false;
}

+ if (IsWitnessScriptSpam(txout.scriptPubKey, tx)) {
+ reason = "witness-script-spam";
+ return false;
+ }
return true;
}

diff --git a/src/validation.h b/src/validation.h
index 1234567..89abcdef 100644
--- a/src/validation.h
+++ b/src/validation.h
@@ -423,6 +423,7 @@ bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime);
* Check if transaction is final per BIP 68 sequence numbers and can be included in a block.
* Consensus critical. Takes as input a list of heights at which tx's inputs (all txin.NextInputBlockHeight()) are confirmed.
*/
+bool IsWitnessScriptSpam(const CScript& scriptPubKey, const CTransaction& tx);
bool IsFinalTxAtHeight(const CTransaction &tx, int nBlockHeight, const std::vector<int>& prevHeights);

/**
Loading