RGB-Tools / RGB-Tools/rgb-lightning-node

RGB wallet state is mutated before the peer input that triggered it is validated

Open
#160 0 comments 0 reactions 0 assignees View on GitHub

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:

  1. _accept_transfer goes online and calls wallet.accept_transfer(funding_txid, funding_vout = 1, consignment_endpoint, STATIC_BLINDING) — a wallet mutation driven by data fetched from the endpoint the peer named;
  2. the consignment is serialised and written to the KV store twice (under the funding txid and under the temporary channel ID);
  3. only then remote_rgb_assignments.len() != 1 is checked and the channel closed if it fails;
  4. the assignment is matched with _ => unreachable!("unsupported schema");
  5. AssetSchema::from_schema_id(consignment.schema_id()).unwrap() and consignment.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_created whose 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_channel with push_asset_amount above the consignment's assignment amount → underflow in handle_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 (wrapped remote_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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.