multi: add static address loop in monitor#1165
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request implements support for static address loop-in swaps within the monitor system. It extends the existing swap type definitions to include a new variant, updates the RPC interface to allow clients to interact with and filter these swaps, and ensures that the internal marshalling and filtering logic correctly processes these transactions. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
|
Looking for ConceptACK/suggestions. Still need to test. |
There was a problem hiding this comment.
Code Review
This pull request introduces support for a new swap type, STATIC_ADDRESS_LOOP_IN, updating the internal swap structures, marshalling logic, filtering, and RPC definitions. The feedback highlights a potential nil pointer dereference in marshallSwap when both HtlcAddressP2TR and HtlcAddressP2WSH are nil, and suggests adding a nil check for HtlcAddressP2WSH to prevent a panic.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
starius
left a comment
There was a problem hiding this comment.
Concept ACK for API shape.
One implementation note. Monitor snapshots and subscribes only to s.swaps, populated from loop.Client.FetchSwaps and live statusChan updates. Static address loop-ins live under staticLoopInManager.GetAllSwaps() and (ListStaticAddressSwaps), so the current changes alone will not make loop monitor see them.
Some bridging is needed to deliver static loop-in updates into monitor's SwapStatus stream: initial snapshot plus live state updates. That also requires an explicit state mapping from static loop-in FSM states to generic SwapState, and filling the generic SwapInfo fields safely, especially HTLC address, amount, label, last hop, timestamps, and costs.
| LOOP_IN = 2; | ||
|
|
||
| // STATIC_ADDRESS_LOOP_IN indicates a static address loop in swap. | ||
| STATIC_ADDRESS_LOOP_IN = 3; |
There was a problem hiding this comment.
This change seems unrelated to the loop monitor integration of static loop-ins. If we need it for completeness, I propose to do it in a separate commit or in a follow-up PR.
There was a problem hiding this comment.
The thing here is, how can we tell the monitor that this is a static loop in swap and that we are sending a new notification type, not the classic swap in or swap out notification?
SwapStatus is currently a flat struct returned in the rpc Monitor, and SwapType is the field that carries this information. Adding this option here was the best design I found so far.
Are you thinking about another option? How would that look?
|
Thanks @GustavoStingelin for putting up this draft PR! I am looking a bit ahead of where we'd update the monitor with the static state, and it should probably be Keeping future swap types in mind maybe it would make sense to send lightweight notifications Maybe we could have a small monitor publisher in each existing and new swap config that does something like: What do you think @GustavoStingelin @starius ? |
I like the idea, but I have two questions:
In that case, we could show the second state twice in the monitor and lose the first state transition from the monitor log. |
|
I am thinking that, in the future, we could have something like this: message MonitorEvent {
string id = 1;
bytes id_bytes = 2;
SwapKind kind = 3;
int64 amount = 4;
int64 initiation_time = 5;
int64 last_update_time = 6;
string label = 7;
// Human friendly normalized lifecycle.
MonitorPhase phase = 8;
// Machine specific exact state.
string state = 9;
oneof details {
LoopInMonitorDetails loop_in = 20;
LoopOutMonitorDetails loop_out = 21;
StaticLoopInMonitorDetails static_loop_in = 22;
// Future swap types can be added here.
}
}Where:
The idea is to separate the high level swap category, the normalized monitor lifecycle, and the exact internal state. That should make the monitor API easier to extend without overloading one field with multiple meanings. |
from
|
I tend to not assemble info in the static address state machine and then send it to the monitor, rather just send a lightweight reference which the monitor can assemble from.
This is a good point! We might cache the swap state at that point in time to not miss anything, but that might defeat I think as long as the data we are returning to the monitor is already in memory we can think about this optimization later. |
Cool, I totally agree. I think doing that broader refactor in this PR would be too much, but I’m also not very happy with my current local code. It has some ugly workarounds to support both state models. So what do you think about shifting to the new monitor approach now? Or would you prefer to take a look at my current code first and decide from there? Also, just to confirm, since we release the client and loopd together, a breaking protobuf API change should not be a problem, right? |
c93d2dd to
db63a8b
Compare
db63a8b to
73bc60a
Compare
|
@claude review |
ReviewOverview: this PR wires static-address loop-in swaps into the generic Potential issues
Nice catches / good practices
Minor
Nothing here looks blocking — mostly scoping/consistency questions and small test/DRY nits. Nice work on the race-condition handling for the snapshot/live-update overlap. |
| SwapHash: swp.SwapHash, | ||
| SwapType: swap.TypeStaticAddressLoopIn, | ||
| StaticAddressLoopInState: swp.GetState(), | ||
| HtlcAddressP2TR: staticAddress, |
There was a problem hiding this comment.
HTLC address is not the same as staticAddress. These are two different addresses and it is very important to separate them. We got issues previously when people sent funds to HTLC addresses by mistake.
HTLC address also exists and is a property of a static loop-in (as well as of other swap types). It should be loaded/computed and put here.
Static address is a long-living address which is reused by (potentially) many deposits and many static loop-ins. Probably we need a new field to pass it here. The new field for the static address will only be filled if the swap is a static loop-in.
There was a problem hiding this comment.
Imo we shouldn't show the htlc address in the static world as this is a backup transaction by the server that is rarely ever shown as we try to avoid publishing it and instead sweep directly.
If we still think this is useful information we could recreate like it is done in loopin.go:
func (l *StaticAddressLoopIn) getHtlc(chainParams *chaincfg.Params)
There was a problem hiding this comment.
We are no longer putting the reusable static address into the HTLC field. Now, we only set HtlcAddressP2WSH to the actual HTLC address.
@hieblmi Yeah, if it is not needed, I agree that we should remove it and simplify this even further. However, I would also prefer not to expose HtlcAddressP2WSH here if it should not be populated. That would bring us back to the broader monitor refactor, though.
If you all agree that it should be left empty for now, I can change it. Otherwise, we can open a follow up issue for a broader refactor. It is up to you.
| case loopin.Failed, loopin.SucceededTransitioningFailed: | ||
| return loopdb.StateFailAbandoned |
There was a problem hiding this comment.
I think loopdb.StateFailAbandoned is a wrong state for Failed and SucceededTransitioningFailed.
@hieblmi What do you think?
There was a problem hiding this comment.
Technically not correct as abandoned is reserved in the old loop world for manual swap abandonment, if a swap is borked e.g., similar to how lnd allows abandoning channels.
@GustavoStingelin do we need the mapping of the static states into the old states?
I think we can just rely on the static swap state and leave the old state empty? So just use
StaticAddressLoopInState: swp.GetState(),
There was a problem hiding this comment.
changed to not map like that, now all use the default enum zero value (StateInitiatedfor old school loop) when it is static
| State: staticAddressLoopInGenericState(swp.GetState()), | ||
| Cost: loopdb.SwapCost{ | ||
| Server: btcutil.Amount( | ||
| staticAddressLoopInSwapServerCost(swp), |
There was a problem hiding this comment.
I would maybe add/keep a comment saying this mirrors ListStaticAddressSwaps: only actual persisted client-visible server cost is reported; onchain/offchain remain zero until static loop-ins persist real fee data.
| case swp := <-s.statusChan: | ||
| s.swapsLock.Lock() | ||
| s.swaps[swp.SwapHash] = swp | ||
| if swp.SwapType != swap.TypeStaticAddressLoopIn { |
There was a problem hiding this comment.
Claude raised an interesting point. We do now have an inconsistency of loop monitor and loop listswaps / loop swapinfo.
Users might see a swap hash in loop monitor and try to check its information in loop listswaps or loop swapinfo and be confused that the swap is missing (because it is a static loop-in).
I propose to add a note on CLI side to loop listswaps output and its docs that it covers only traditional loop-ins and loop-outs, not static loop-ins. We have command loop static listswaps which should be used instead - tell this in the message.
For loop swapinfo <swap> we don't have a direct replacement in the static world. @hieblmi would it make sense to extend loop swapinfo <swap> so it works for static loop-ins as well? Or is it better to add loop static swapinfo <swap> command and in loop swapinfo tell the user to use the new command instead if we detect that the hash is of a static loop-in?
There was a problem hiding this comment.
For
loop swapinfo <swap>we don't have a direct replacement in the static world. @hieblmi would it make sense to extendloop swapinfo <swap>so it works for static loop-ins as well? Or is it better to addloop static swapinfo <swap>command and inloop swapinfotell the user to use the new command instead if we detect that the hash is of a static loop-in?
I think we should either add loop static swapinfo <swap> or extend existing loop static listswaps with a --swap_hash param. In the future the goal is to get rid of loop swapinfo respectively replace it with loop static [swapinfo|listswaps]
There was a problem hiding this comment.
added the CLI observation, I guess we can create some good follow-up issues from this PR, right?
hieblmi
left a comment
There was a problem hiding this comment.
Thanks for the changes so far @GustavoStingelin 🚀 I've left some comments.
Overall I wonder if we could simplify the monitoring approach without having to modify all too much of the existing flows and also be extensible when future swap types need to be monitored.
What about if we seed a staticSwaps map[lntypes.Hash]loop.SwapInfo once from the database during daemon startup, then update it in processStatusUpdates. Monitor could register and snapshot both legacy and static maps under the same lock.
That would eliminate:
- Full database reads on every monitor connection.
- Snapshot/live timestamp deduplication.
- Most of the race-specific logic.
- The need to put static swaps into the legacy s.swaps map or expose them through
ListSwaps.
| return | ||
| } | ||
|
|
||
| err = f.sendUpdate(ctx) |
There was a problem hiding this comment.
Here comes back my initial performance concern, since we don't want to slow further down the state transitions of the state machine.
We should add godocs pointing out that sendUpdate is not supposed to do heavy lifting. If the updates become to heavy(like calculating htlc address from given parameters or something else) we could queue the monitor updates. But for now lgtm.
There was a problem hiding this comment.
added godocs in the sendUpdate function definition
|
Just tested the current set of changes in regtest by submitting a static and legacy swap, looks good: Noticed that the on-chain fee of a static swap is currently |
21fdc46 to
2bb998b
Compare
|
The other RPC call changes are deferred for a future follow-up issue/pr. |
2bb998b to
742eae0
Compare
Implements monitor support for static address loop-in swaps.