Repo: LayerTwo-Labs/coinshift-rs
File: lib/state/two_way_peg_data.rs:122, in connect_withdrawal_bundle_submitted
Summary
The assert requires a bundle's WithdrawalBundleSubmitted event to arrive exactly one sidechain block after the block that created the bundle. That only holds if the M3 reaches the mainchain in the immediately following block, which a sidechain operator generally cannot arrange.
M3 is a coinbase message. The block producer writes it from its own enforcer's database — BroadcastWithdrawalBundle is a local gRPC call that never propagates. Unlike M8, which is an ordinary mainchain transaction that any miner's enforcer picks up from the mempool, an M3 reaches the chain only when the sidechain operator themself mines a mainchain block. For an operator who is not the dominant miner, that is an arbitrary number of blocks later, and the assert fires.
Impact
The assert panics a tokio-rt-worker. The process does not exit, so systemd still reports the unit active, but the net task is gone. From that point the node:
- has zero peers, permanently
- never connects another tip — every subsequent BMM ends in
net task error: Send new tip ready failed: send failed because receiver is gone
- keeps broadcasting M8 bids that keep winning on L1, producing mainchain commitments for sidechain blocks it then drops
There is no crash, no restart, and no unhealthy status. The node looks fine and is silently dead.
Observed on eCash alphanet, slot 255
- bundle created at sidechain height 11
- M3 mined into mainchain block 996594 (2026-08-30 04:52 UTC), m6id
f3c3a7d7819bf66d68ccaf56b4b51ae82d98a0113df5d730389d7fada4399785
- by then the sidechain was at height 25, so
block_height - 1 = 24 ≠ 11
Aug 30 05:22:04 coinshift_app: thread 'tokio-rt-worker' panicked at lib/state/two_way_peg_data.rs:122:9:
assertion `left == right` failed
left: 11
right: 24
Aug 30 05:38:21 ERROR coinshift::node::net_task: lib/node/net_task.rs:1405:
Net task finished (before or during send) - channel receiver is gone.
Aug 30 05:38:21 ERROR coinshift_app::app: app/app.rs:1212:
net task error: Send new tip ready failed: send failed because receiver is gone
The node then sat at height 25 for two days while its bidder kept winning slot-255 BMM in nearly every mainchain block from 996566 through 996672 — roughly 100 commitments for blocks that never connected.
Reproduction
- Run a sidechain node and create a withdrawal bundle.
- Do not mine the mainchain block immediately following the sidechain block that created it.
- Mine a later mainchain block carrying the M3.
- The net task panics and the node is stuck at that height for the life of the process.
Suggested fix
bundle_block_height is not read anywhere else in the function — every line after the assert uses block_height. Downgrading it to a warning is enough, and it also removes an underflow panic when block_height == 0:
if bundle_block_height != block_height.saturating_sub(1) {
tracing::warn!(
%bundle_block_height,
%block_height,
%m6id,
"Withdrawal bundle submitted later than the block after the one that created it"
);
}
If the one-block relationship is genuinely meant to be enforced, it should be a validation error returned to the caller rather than an assert on a worker thread. A panic there takes networking down for the rest of the process's life with no signal to the operator.
Version
main at 1b8d1d6 still carries the assert at line 122. Seen on 8dd148c (coinshift-rs 0.14.0-alpha) running against a bip300301_enforcer on eCash alphanet. Thunder and Truthcoin carry no equivalent assert in their two_way_peg_data.rs.
Repo: LayerTwo-Labs/coinshift-rs
File:
lib/state/two_way_peg_data.rs:122, inconnect_withdrawal_bundle_submittedSummary
The assert requires a bundle's
WithdrawalBundleSubmittedevent to arrive exactly one sidechain block after the block that created the bundle. That only holds if the M3 reaches the mainchain in the immediately following block, which a sidechain operator generally cannot arrange.M3 is a coinbase message. The block producer writes it from its own enforcer's database —
BroadcastWithdrawalBundleis a local gRPC call that never propagates. Unlike M8, which is an ordinary mainchain transaction that any miner's enforcer picks up from the mempool, an M3 reaches the chain only when the sidechain operator themself mines a mainchain block. For an operator who is not the dominant miner, that is an arbitrary number of blocks later, and the assert fires.Impact
The assert panics a
tokio-rt-worker. The process does not exit, so systemd still reports the unitactive, but the net task is gone. From that point the node:net task error: Send new tip ready failed: send failed because receiver is goneThere is no crash, no restart, and no unhealthy status. The node looks fine and is silently dead.
Observed on eCash alphanet, slot 255
f3c3a7d7819bf66d68ccaf56b4b51ae82d98a0113df5d730389d7fada4399785block_height - 1= 24 ≠ 11The node then sat at height 25 for two days while its bidder kept winning slot-255 BMM in nearly every mainchain block from 996566 through 996672 — roughly 100 commitments for blocks that never connected.
Reproduction
Suggested fix
bundle_block_heightis not read anywhere else in the function — every line after the assert usesblock_height. Downgrading it to a warning is enough, and it also removes an underflow panic whenblock_height == 0:If the one-block relationship is genuinely meant to be enforced, it should be a validation error returned to the caller rather than an assert on a worker thread. A panic there takes networking down for the rest of the process's life with no signal to the operator.
Version
mainat1b8d1d6still carries the assert at line 122. Seen on8dd148c(coinshift-rs 0.14.0-alpha) running against abip300301_enforceron eCash alphanet. Thunder and Truthcoin carry no equivalent assert in theirtwo_way_peg_data.rs.