RGB-Tools / RGB-Tools/rgb-lightning-node
RGB wallet state is mutated before the peer input that triggered it is validated
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 50
- Forks
- 48
- Avg merge
- 8h 52m
- Merged PRs (30d)
- 2
Description
Summary
During channel funding, RGB and wallet side effects run before the peer message that triggered them is verified. A peer can send a well-formed but invalid funding_created / funding_signed — invalid signature, invalid consignment shape, or an out-of-range asset push — and the node will have already imported a consignment, consumed a fascia into the RGB wallet and written transfer metadata by the time the check fails. The channel is then closed, but there is no rollback for the RGB effects: the residue stays on disk and in the wallet with no owner.
Some of these late checks are also written as unreachable!() or unchecked subtraction, so the peer input can panic the message handler instead of producing a clean channel close.
Details
1. The commitment is colored — and the RGB wallet mutated — before the signature is verified
check_counterparty_commitment_signature builds the commitment transaction, and if the channel is colored calls:
color_commitment(&self.context(), &self.funding(), &mut commitment_data.tx, false)
.expect("successful commitment coloring");
…
secp_check!(self.context().secp_ctx.verify_ecdsa(&sighash, sig, …), "Invalid … signature from peer");
color_commitment is not a pure computation: it colors the PSBT, then runs wallet.consume_fascia(fascia, Some(WitnessOrd::Ignored)).unwrap() and kv_store.write_rgb_transfer_info(&txid, &transfer_info). So the wallet mutation and the transfer-info write happen, and only afterwards is the counterparty signature checked.
Both funding paths reach this: the acceptor via funding_created → initial_commitment_signed, and the initiator via funding_signed → initial_commitment_signed. On failure the inbound path clears funding_outpoint and the channel is closed — nothing undoes the fascia consumption or the transfer-info record. Every unwrap/expect inside color_commitment also turns an rgb-lib or KV error on peer-triggered input into a panic in the message handler.
2. The consignment is fetched, imported and stored before its contents are checked
In internal_funding_created, handle_funding(...) runs before inbound_chan.funding_created(msg, …). Inside handle_funding:
_accept_transfergoes online and callswallet.accept_transfer(funding_txid, funding_vout = 1, consignment_endpoint, STATIC_BLINDING)— a wallet mutation driven by data fetched from the endpoint the peer named;- the consignment is serialised and written to the KV store twice (under the funding txid and under the temporary channel ID);
- only then
remote_rgb_assignments.len() != 1is checked and the channel closed if it fails; - the assignment is matched with
_ => unreachable!("unsupported schema"); AssetSchema::from_schema_id(consignment.schema_id()).unwrap()andconsignment.save(…).expect(…)can panic on peer-supplied data.
A consignment with zero or multiple assignments therefore leaves both KV records and the wallet import behind before the rejection, and an assignment variant outside the expected set aborts the process.
3. The peer-supplied asset push amount is never validated
push_asset_amount is a TLV field on open_channel (msgs.rs), stored on the inbound channel with no bounds check — push_msat is validated against the channel value, push_asset_amount is not. handle_funding then computes:
let push_amount = push_asset_amount.unwrap_or(0);
… remote_rgb_amount: channel_rgb_amount - push_amount
If the peer sends push_asset_amount greater than the amount actually assigned in the consignment, this subtraction underflows: a panic in debug builds, a wrapped value close to u64::MAX written into rgb_channel_info in release builds. That record is what every later commitment coloring and the close/sweep path read.
How it can be triggered
- Connect as a peer, open a colored channel, send a
funding_createdwhose signature is well-formed but does not match the commitment → the node imports/consumes RGB state, then rejects the signature and closes. - Send a consignment with zero or two assignments → wallet import and both KV writes happen, then the channel is closed.
- Send
open_channelwithpush_asset_amountabove the consignment's assignment amount → underflow inhandle_funding.
Peer authentication (BOLT 8) does not help here: any peer that can open a channel can reach all three.
Impact
- Irreversible RGB wallet mutations and metadata caused by a message that is subsequently rejected; the residue belongs to no live channel and is not cleaned up.
- A corrupted
rgb_channel_info(wrappedremote_rgb_amount) that later coloring and the sweeper consume. - Remotely triggerable panic in the peer-message handler (
unreachable!,unwrap, arithmetic overflow).
The mechanism is confirmed in source; the exact end-state after a real funded exchange should still be captured by a test.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with internal_funding_created, handle_funding, check_counterparty_commitment_signature, color_commitment, and the push_asset_amount parsing in msgs.rs. Trace the funding_created and funding_signed paths, then add a test covering invalid signatures, malformed consignments, and excessive asset pushes. Done means rejected peer input leaves no RGB wallet or KV residue, avoids corrupted amounts, and produces a clean channel close rather than a panic.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100