-
Notifications
You must be signed in to change notification settings - Fork 5.8k
BIP: User-Defined Dynamic Relay Policy Scripts #1985
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
``` | ||
BIP: ? | ||
Layer: Peer Services | ||
Title: User-Defined Dynamic Relay Policy Scripts | ||
Author: Aiden McClelland <me+bip@drbonez.dev> | ||
Status: Draft | ||
Type: Standards Track | ||
Created: 2025-09-23 | ||
License: BSD-2-Clause | ||
``` | ||
|
||
## Abstract | ||
|
||
This BIP proposes a standardized mechanism to validate transactions for mempool acceptance using external scripts. Nodes load a directory of JavaScript (ES2020-compatible) scripts that define acceptance rules. Each submitted transaction (or transaction package) is passed through the configured scripts; if all scripts return successfully, the transaction is accepted into the mempool. If any script throws an error, the transaction is rejected. This framework allows flexible policy development without requiring changes to the node implementation. | ||
|
||
## Copyright | ||
|
||
This BIP is licensed under the BSD 2-clause license. | ||
|
||
## Motivation | ||
|
||
At present, mempool acceptance policy is enforced solely by Bitcoin node software and configurations provided by the software. This approach creates limitations and friction to the addition and removal of policies. A standardized interface for externally provided policy scripts provides a number of benefits: | ||
|
||
- Allows users to eliminate or extend mempool policies without requiring node implementation developers to make code changes. | ||
- Defines a standardized execution environment so scripts can be shared across different implementations. | ||
- Provides a safer, more modular way to test policies. | ||
|
||
## Rationale | ||
|
||
JavaScript is a very well defined and widely understood scripting language with many available runtimes. It allows scripts to be portable across platform and architecture, and runtimes are well optimized such that significant performance penalties will not be incurred. This was somewhat inspired by a similar project using Lua presented at BTC++ by @jasonfoura. | ||
|
||
## Specification | ||
|
||
1. **Script Directory** | ||
- A node MAY specify a directory path containing `.js` files implementing policy functions. | ||
- Files can be nested arbitrarily in subdirectories. | ||
- Files beginning with a number (e.g. 0100-min-relay-fee.js) will be interpreted as policy scripts and executed automatically by the node software on transaction validation. | ||
- Filenames beginning with a `0` will be processed _before_ consensus validation. All other files will be processed _after_. | ||
- Files will be processed in ascending order according to the prefixed number. | ||
- Files that do not begin with a number, but end with a `js`, `mjs`, or `json` extension (e.g. helpers.js) will not be executed but can be imported into other files as ESModules. | ||
- All other files will be ignored. | ||
- JavaScript files in this directory are loaded at startup and compiled in an embedded JavaScript runtime (e.g., QuickJS / Duktape / V8 wrapper). | ||
2. **Execution Model** | ||
- For every package of transactions submitted for mempool acceptance: | ||
- Individual transactions are treated as a package of length 1. | ||
- The package transactions are provided as an array of well‑defined JS objects with canonical fields, attached to the global object. | ||
- These fields may be computed dynamically using custom getters and memoized up to the implementation's discretion. | ||
- If all scripts complete without exception AND consensus validation succeeds, the package is accepted into the mempool and relayed. | ||
- If any script throws an error, the transaction is rejected with error code `script-policy-validation-failed` and a message referencing the failed script. | ||
3. **Sandboxing** | ||
- Scripts MUST be executed in a restricted runtime environment (no filesystem or network access). | ||
- Available globals are limited to the transaction context and a standard library of pure functions. | ||
4. **Error Handling** | ||
- Syntax errors at startup cause the node to fail script loading but do not prevent ordinary operation. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That means a misconfigured node could silently revert to a “no policy” state? I think there should be a strict mode (perhaps default) in which startup aborts if the configured scripts cannot be parsed, validated, or sandboxed successfully. |
||
- To reduce DoS vulnerabilities, the JavaScript Runtime should have limited memory, and validation should be subject to a timeout. If either of these limits are exceeded, the transaction should be rejected with error code `script-policy-validation-failed`. | ||
|
||
### Type Definitions | ||
|
||
To encourage consistency across implementations, transaction data available to scripts should conform to a well-defined type. A proposal for this type is specified below as a TypeScript definition file: | ||
|
||
```typescript | ||
export type Global = typeof globalThis & { | ||
package: Transaction[]; | ||
mempool: MempoolMetadata; | ||
}; | ||
|
||
export type Transaction = { | ||
version: number; | ||
nLockTime: number; | ||
vin: TxIn[]; | ||
vout: TxOut[]; | ||
size: number; | ||
weight: number; | ||
fee: bigint; | ||
blockheight: number | null; // null if in mempool / earlier in package | ||
conflicts: MempoolTransaction[]; | ||
}; | ||
|
||
export type TxIn = { | ||
prevout: OutPoint; | ||
scriptSig: Script; | ||
nSequence: number; | ||
scriptWitness: ScriptWitness; | ||
}; | ||
|
||
export type OutPoint = { | ||
hash: string; // hex txid | ||
n: number; | ||
tx: Transaction; // getter required | ||
vout: TxOut; // getter required | ||
}; | ||
|
||
export type Script = { | ||
hex: string; | ||
ops: Op[]; // getter recommended | ||
kind: | ||
| "nonstandard" | ||
| "anchor" | ||
| "pubkey" | ||
| "pubkeyhash" | ||
| "scripthash" | ||
| "multisig" | ||
| "nulldata" | ||
| "witness-v0-scripthash" | ||
| "witness-v0-keyhash" | ||
| "witness-v1-taproot" | ||
| "witness-unknown"; // getter recommended | ||
}; | ||
|
||
export type Op = { | ||
code: number; | ||
data: string | null; // hex, null if not pushdata | ||
}; | ||
|
||
export type ScriptWitness = { | ||
stack: string[]; // hex | ||
}; | ||
|
||
export type TxOut = { | ||
nValue: bigint; | ||
scriptPubkey: Script; | ||
}; | ||
|
||
export type MempoolTransaction = Transaction & { | ||
blockheight: null; | ||
evict(): void; | ||
}; | ||
|
||
export type MempoolMetadata = { | ||
size: bigint; | ||
minFeerate: MempoolTransaction; | ||
}; | ||
``` | ||
|
||
## Backwards Compatibility | ||
|
||
The proposal does not alter consensus rules. Nodes that do not implement the BIP operate without any changes. Transactions rejected locally because of policy scripts may still propagate elsewhere. | ||
|
||
All existing mempool/relay policies can be re-implemented as a default set of scripts bundled with the node implementation. Existing configuration values can be converted to a `config.json` imported by files in the directory. | ||
|
||
## Security | ||
|
||
- **Determinism**: Scripts should execute deterministically and quickly. Resource exhaustion attacks should be mitigated by memory limits and timeouts. | ||
- **Sandboxing**: No network, filesystem, or process access is permitted. | ||
- **Policy Divergence**: Differing script sets may fragment mempools across nodes. This is already the case for other local policy settings. | ||
|
||
## Reference Implementation | ||
|
||
WIP: https://github.com/dr-bonez/bitcoin/tree/feature/js-mempool-policy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have concerns about embedding JavaScript directly in node implementations.
While the abstract’s goal of allowing flexible and externalized policy is good, mandating JS (and a specific runtime) substantially raises the attack surface both through the complexity of the embedded interpreter and through the broader API exposure inside the node process.
Instead of binding the proposal to a particular scripting engine, it might be safer and more modular to run policies in a local helper daemon that communicates with the node over an RPC interface (e.g. JSON‑RPC or protobuf).
Advantages of that architecture:
The validation engine remains language‑agnostic; policy developers can use any language.
Crashes in the policy engine can’t bring down the node; it can be rate‑limited or restarted independently.
Security review is simpler.
In short, it would be more future‑proof to define an interface and message schema, not a language.