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
Empty file modified .gitignore
100644 → 100755
Empty file.
22 changes: 22 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to CLN Plugin",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "${workspaceFolder}"
}
],
"justMyCode": false,
"internalConsoleOptions": "neverOpen"
}
]
}
2 changes: 2 additions & 0 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,5 @@ The plugin accepts the following configuration options:
3. Make your changes
4. Run the test suite to ensure everything works
5. Submit a pull request

Note: If you wish to contribute and you are having issues with the bitcoin rpc server timing out, add this to your `bitcoin.conf` file: `rpcservertimeout=0`.
614 changes: 277 additions & 337 deletions bumpit.py

Large diffs are not rendered by default.

Empty file modified requirements-dev.txt
100644 → 100755
Empty file.
Empty file modified requirements.txt
100644 → 100755
Empty file.
14 changes: 6 additions & 8 deletions test_child_highfee.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from pyln.testing.fixtures import * # noqa: F403
from pyln.testing.utils import sync_blockheight, FUNDAMOUNT, BITCOIND_CONFIG

# import debugpy
# debugpy.listen(("localhost", 5678))

pluginopt = {'plugin': os.path.join(os.path.dirname(__file__), "bumpit.py")}
FUNDAMOUNT = 1000000 # Match the manual test amount of 1M sats

Expand Down Expand Up @@ -84,16 +87,12 @@ def test_child_highfee(node_factory):
txid=funding_txid,
vout=change_output['output'],
amount=target_feerate,
yolo="dryrun"
yolo="yolo"
)
print(f"Result: {result}")

# Handle error responses
if 'code' in result and result['code'] == -32600:
print(f"Error response: {result['message']}")
assert "reserve" in result['message'].lower() or "confirmed" in result['message'].lower(), (
f"Unexpected error: {result['message']}"
)
return
assert 'code' not in result

# Extract plugin results
plugin_parent_fee = result.get('parent_fee', 0)
Expand Down Expand Up @@ -142,4 +141,3 @@ def test_child_highfee(node_factory):
assert abs(plugin_total_feerate - calculated_total_feerate) < 0.01, (
f"Plugin total feerate mismatch: plugin={plugin_total_feerate:.2f}, calculated={calculated_total_feerate:.2f}"
)

28 changes: 19 additions & 9 deletions test_confirmed_bump.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
from pyln.testing.fixtures import * # noqa: F403
from pyln.testing.utils import sync_blockheight, BITCOIND_CONFIG

# import debugpy
# debugpy.listen(("localhost", 5678))

import pytest

pluginopt = {'plugin': os.path.join(os.path.dirname(__file__), "bumpit.py")}
FUNDAMOUNT = 50000 # Channel funding amount in satoshis
INITIAL_FUNDING = 200000 # Initial wallet funding in satoshis
Expand Down Expand Up @@ -40,21 +45,26 @@ def test_confirmed_bump(node_factory):
funds = l1.rpc.listfunds()
outputs = funds.get("outputs", [])
funding_utxo = None

# for txid in outputs:
# if txid["txid"] != funding_txid:
# confirmed_txid = txid

for output in outputs:
if output["txid"] == funding_txid:
funding_utxo = output
break
assert funding_utxo is not None and funding_utxo.get("status") == "confirmed", f"Funding tx {funding_txid} is not confirmed"

# Step 3: Attempt to bump the confirmed funding transaction
result = l1.rpc.bumpchannelopen(
txid=funding_txid, # Use funding_txid instead of wallet_txid
vout=funding_utxo["output"], # Use funding_utxo's vout
amount="3satvb"
)
with pytest.raises(RpcError) as exc_info:
l1.rpc.bumpchannelopen(
txid=funding_txid, # Use funding_txid instead of wallet_txid
vout=funding_utxo["output"], # Use funding_utxo's vout
amount="3satvb"
)

# Step 4: Assert the outcome
assert "code" in result and result["code"] == -32600, f"Expected error code -32600, got {result}"
assert "message" in result, f"Expected error message, got {result}"
assert "confirmed" in result["message"].lower(), f"Expected 'confirmed' in error, got {result['message']}"
print(f"Success: Cannot bump confirmed transaction: {result['message']}")
assert exc_info.type is RpcError
assert exc_info.value.error["message"] == "Error while processing bumpchannelopen: Transaction is already confirmed and cannot be bumped"
print(f"Success: Cannot bump confirmed transaction: {exc_info.value.error["message"]}")
32 changes: 25 additions & 7 deletions test_emergency_reserve.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import os
import re
from pyln.client import RpcError
from pyln.testing.fixtures import * # noqa: F403
from pyln.testing.utils import sync_blockheight, BITCOIND_CONFIG

# import debugpy
# debugpy.listen(("localhost", 5678))

pluginopt = {'plugin': os.path.join(os.path.dirname(__file__), "bumpit.py")}
FUNDAMOUNT = 74000
INITIAL_FUNDING = 100000
Expand Down Expand Up @@ -55,12 +60,25 @@ def test_emergency_reserve(node_factory):
# Bump using change UTXO
utxo = available_utxos[0]
print(f"Paying CPFP with: txid={utxo['txid']}, vout={utxo['output']}, amount={utxo['amount_msat']/1000} sats")
result = l1.rpc.bumpchannelopen(txid=utxo["txid"], vout=utxo["output"], amount="4satvb")

with pytest.raises(RpcError) as exc_info:
result = l1.rpc.bumpchannelopen(txid=utxo["txid"], vout=utxo["output"], amount="4satvb")

# Sanity check to make sure we are not spending our emergency reserve
leftover_emergencyreserve = change_utxo['amount_msat'] // 1000 - int(result['child_fee'])
assert leftover_emergencyreserve < 25000
assert "code" in result and result["code"] == -32600, f"Expected reserve error, got {result}"
assert "reserve" in result["message"].lower(), f"Expected reserve message, got {result['message']}"
print(f"Success: Reserve protected: {result['message']}")

# leftover_emergencyreserve = change_utxo['amount_msat'] // 1000 - int(result['child_fee'])
# assert leftover_emergencyreserve < 25000
# assert "code" in result and result["code"] == -32600, f"Expected reserve error, got {result}"
# assert "reserve" in result["message"].lower(), f"Expected reserve message, got {result['message']}"
# print(f"Success: Reserve protected: {result['message']}")



# total_unreserved_sats = sum(utxo["amount_msat"] // 1000 for utxo in available_utxos)

# Step 4: Assert the outcome
assert exc_info.type is RpcError
message = exc_info.value.error["message"]
match = re.match(r"Bump would leave (\d+) sats, below 25000 sat emergency reserve\.", message)
# assert match, f"Unexpected error message format: {message}"
# would_leave = int(match.group(1))
# assert would_leave < EMERGENCY_RESERVE
36 changes: 25 additions & 11 deletions test_emergency_reserve_fee_arg.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import os
import re
from pyln.client import RpcError
from pyln.testing.fixtures import * # noqa: F403
from pyln.testing.utils import sync_blockheight, BITCOIND_CONFIG

# import debugpy
# debugpy.listen(("localhost", 5678))

pluginopt = {'plugin': os.path.join(os.path.dirname(__file__), "bumpit.py")}
FUNDAMOUNT = 74000
INITIAL_FUNDING = 100000
EMERGENCY_RESERVE = 25000

def test_emergency_reserve_fee_boundary(node_factory):
def test_emergency_reserve_fee_arg(node_factory):
opts = {
'bump_brpc_user': BITCOIND_CONFIG["rpcuser"],
'bump_brpc_pass': BITCOIND_CONFIG["rpcpassword"],
Expand Down Expand Up @@ -56,17 +61,26 @@ def test_emergency_reserve_fee_boundary(node_factory):
utxo = available_utxos[0]
fixed_fee = int(current_unreserved - 24999) # Fee to leave exactly 24,999 sats
print(f"Paying CPFP with: txid={utxo['txid']}, vout={utxo['output']}, amount={utxo['amount_msat']/1000} sats, fixed fee={fixed_fee} sats")
result = l1.rpc.bumpchannelopen(txid=utxo["txid"], vout=utxo["output"], amount=f"{fixed_fee}sats")

with pytest.raises(RpcError) as exc_info:
result = l1.rpc.bumpchannelopen(txid=utxo["txid"], vout=utxo["output"], amount=f"{fixed_fee}sats")

# Sanity check to make sure we are not spending our emergency reserve
leftover_emergencyreserve = change_utxo['amount_msat'] // 1000 - int(result['child_fee'])
assert leftover_emergencyreserve == 24999, f"Expected 24,999 sats left, got {leftover_emergencyreserve}"
assert "code" in result and result["code"] == -32600, f"Expected reserve error, got {result}"
assert "reserve" in result["message"].lower(), f"Expected reserve message, got {result['message']}"
print(f"Success: Reserve protected with fixed fee: {result['message']}")
# leftover_emergencyreserve = change_utxo['amount_msat'] // 1000 - int(result['child_fee'])
# assert leftover_emergencyreserve == 24999, f"Expected 24,999 sats left, got {leftover_emergencyreserve}"
# assert "code" in result and result["code"] == -32600, f"Expected reserve error, got {result}"
# assert "reserve" in result["message"].lower(), f"Expected reserve message, got {result['message']}"
# print(f"Success: Reserve protected with fixed fee: {result['message']}")

# Clean up: Unreserve inputs if reserved
if "unreserve_inputs_command" in result:
l1.rpc.unreserveinputs(result["unreserve_inputs_command"].split()[-1])
print("Unreserved inputs after test")

# if "unreserve_inputs_command" in result:
# l1.rpc.unreserveinputs(result["unreserve_inputs_command"].split()[-1])
# print("Unreserved inputs after test")




# Step 4: Assert the outcome
assert exc_info.type is RpcError
message = exc_info.value.error["message"]
match = re.match(r"Bump would leave (\d+) sats, below 25000 sat emergency reserve\.", message)
61 changes: 41 additions & 20 deletions test_invalidinputs.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pyln.testing.fixtures import * # noqa: F403
from pyln.testing.utils import sync_blockheight, BITCOIND_CONFIG

# import debugpy
# debugpy.listen(("localhost", 5678))

pluginopt = {'plugin': os.path.join(os.path.dirname(__file__), "bumpit.py")}
FUNDAMOUNT = 500000 # Match emergency_reserve for consistency

Expand All @@ -29,26 +32,44 @@ def test_invalidinputs(node_factory):
bitcoind.generate_block(1) # Confirm funding tx
sync_blockheight(bitcoind, [l1, l2])

vout = 0

# Test invalid txid
invalid_txid = "0000000000000000000000000000000000000000000000000000000000000000"
result = l1.rpc.bumpchannelopen(
txid=invalid_txid,
vout=0, # Valid vout index, but txid is invalid
amount="3satvb"
)
assert "code" in result and result["code"] == -32600, f"Expected error code -32600, got {result}"
assert "message" in result, f"Expected error message, got {result}"
print(f"Expected error (invalid txid): {result['message']}")
assert "not found" in result["message"].lower() or "invalid" in result["message"].lower(), f"Expected invalid txid error, got {result['message']}"
with pytest.raises(RpcError) as exc_info:
invalid_txid = "0000000000000000000000000000000000000000000000000000000000000000"
l1.rpc.bumpchannelopen(
txid=invalid_txid,
vout=vout, # Valid vout index, but txid is invalid
amount="3satvb"
)

# assert "code" in result and result["code"] == -32600, f"Expected error code -32600, got {result}"
# assert "message" in result, f"Expected error message, got {result}"
# print(f"Expected error (invalid txid): {result['message']}")
# assert "not found" in result["message"].lower() or "invalid" in result["message"].lower(), f"Expected invalid txid error, got {result['message']}"

# Step 4: Assert the outcome
assert exc_info.type is RpcError
assert exc_info.value.error["message"] == f"Error while processing bumpchannelopen: UTXO {invalid_txid}:{vout} not found in available UTXOs"
# print(f"Success: Cannot bump confirmed transaction: {exc_info.value.error["message"]}")

vout2 = 999

# Test invalid vout
result = l1.rpc.bumpchannelopen(
txid=funding_txid,
vout=999, # Invalid vout
amount="3satvb"
)
assert "code" in result and result["code"] == -32600, f"Expected error code -32600, got {result}"
assert "message" in result, f"Expected error message, got {result}"
print(f"Expected error (invalid vout): {result['message']}")
assert "not found" in result["message"].lower() or "invalid" in result["message"].lower(), f"Expected invalid vout error, got {result['message']}"

with pytest.raises(RpcError) as exc_info:
l1.rpc.bumpchannelopen(
txid=funding_txid,
vout=vout2, # Invalid vout
amount="3satvb"
)

# assert "code" in result and result["code"] == -32600, f"Expected error code -32600, got {result}"
# assert "message" in result, f"Expected error message, got {result}"
# print(f"Expected error (invalid vout): {result['message']}")
# assert "not found" in result["message"].lower() or "invalid" in result["message"].lower(), f"Expected invalid vout error, got {result['message']}"


# Step 4: Assert the outcome
assert exc_info.type is RpcError
assert exc_info.value.error["message"] == f"Error while processing bumpchannelopen: UTXO {funding_txid}:{vout2} not found in available UTXOs"
# print(f"Success: Cannot bump confirmed transaction: {exc_info.value.error["message"]}")
32 changes: 11 additions & 21 deletions test_parent_highfee.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -53,37 +53,27 @@ def test_parent_highfee(node_factory):
target_feerate = int(target_feerate_suffix[:-5])

# Handle error responses
if 'code' in result and result['code'] == -32600:
print(f"Error response: {result['message']}")
assert "reserve" in result['message'].lower() or "confirmed" in result['message'].lower(), (
f"Unexpected error: {result['message']}"
)
return
assert 'code' not in result

# Print plugin output
print("\nPlugin response:")
print(f" Message: {result.get('message', 'N/A')}")
print(f" Parent details:")
print(f" Fee: {result.get('parent_fee', 0)} sats")
print(f" Vsize: {result.get('parent_vsize', 0)} vB")
# print(f" Vsize: {result.get('parent_vsize', 0)} vB")
print(f" Feerate: {result.get('parent_feerate', 0):.2f} sat/vB")
print(f" Child details:")
print(f" Fee: {result.get('child_fee', 0)} sats")
print(f" Vsize: {result.get('child_vsize', 0)} vB")
print(f" Feerate: {result.get('child_feerate', 0):.2f} sat/vB")
print(f" Total details:")
print(f" Fees: {result.get('total_fees', 0)} sats")
print(f" Vsizes: {result.get('total_vsizes', 0)} vB")
print(f" Feerate: {result.get('total_feerate', 0):.2f} sat/vB")
# print(f" Child details:")
# print(f" Fee: {result.get('child_fee', 0)} sats")
# print(f" Vsize: {result.get('child_vsize', 0)} vB")
# print(f" Feerate: {result.get('child_feerate', 0):.2f} sat/vB")
# print(f" Total details:")
# print(f" Fees: {result.get('total_fees', 0)} sats")
# print(f" Vsizes: {result.get('total_vsizes', 0)} vB")
# print(f" Feerate: {result.get('total_feerate', 0):.2f} sat/vB")
print(f" Desired total feerate: {result.get('desired_total_feerate', 'N/A')}")

# Verify the plugin skipped CPFP
assert "No CPFP needed" in result['message'], f"Expected 'No CPFP needed' in message, got: {result['message']}"
assert result['parent_feerate'] > target_feerate, (
f"Parent feerate ({result['parent_feerate']:.2f}) should exceed target ({target_feerate})"
)
assert result['child_fee'] == 0, f"Expected child_fee=0, got: {result['child_fee']}"
assert result['child_vsize'] == 0, f"Expected child_vsize=0, got: {result['child_vsize']}"
assert result['child_feerate'] == 0, f"Expected child_feerate=0, got: {result['child_feerate']}"
assert result['total_fees'] == result['parent_fee'], f"Expected total_fees=parent_fee, got: {result['total_fees']} vs {result['parent_fee']}"

assert "No CPFP needed" in result['message'], f"Expected 'No CPFP needed' in message, got: {result['message']}"
Empty file modified test_parent_lowfee.py
100644 → 100755
Empty file.
Loading