Objective
After the funding flow, we need a scenario for fuzzing normal channel operations. So the main objective of this plan is that we can complete the commitment dance fully, and the design should be extensible enough to support future extensions. That way, once we have more advanced mutators, cases like multiple HTLC adds or many commitment dance operations can happen correctly, helping test deeper code paths and edge cases.
Smite APIs
BOLT messages
Commitment sign/verify APIs
Currently, smite only supports the initial funding flow, so we need to extend it with HTLC support. The main changes are:
- Add
Htlc struct and in-flight HTLC tracking to CommitmentState.
- Extend channel/holder config with HTLC basepoints and signing keys.
- Extend commitment signing/verification to include HTLC second-stage signatures.
- Introduce a
PendingUpdate enum covering the commitment-affecting updates (AddHtlc(Htlc), FulfillHtlc(id), FailHtlc(id), UpdateFee(feerate_per_kw)).
- Update commitment advancement (
CommitmentState::advance(updates)) and fee accounting to handle HTLC outputs.
Variables and VariableType
VariableType |
Payload |
HtlcId |
u64 |
PaymentHash |
[u8; 32] |
OnionRoutingPacket |
[u8; 1366] |
Htlc |
smite::channel_tx::Htlc |
RevokeAndAck |
smite::bolt::RevokeAndAck |
Operations
-
CreateHtlc: We need this operation so that we can tie the same HTLC to BuildUpdateAddHtlc and AdvanceCommitmentStateAddHtlc. Otherwise, there is a chance that we send one HTLC but use a different HTLC in the commitment state, which would only result in invalid signatures. So, if we want to test that mismatch case, we can omit this operation, otherwise, it makes sense to keep it.
- Input types:
HtlcId, Amount,PaymentHash, BlockHeight
- Output type:
Htlc
-
BuildUpdateAddHtlc:
- Input types:
ChannelId, Htlc, OnionRoutingPacket
- Output type:
Message
-
AdvanceCommitmentState{Kind}: One op per update kind, each producing the next CommitmentState from the previous one plus the kind-specific payload. This can be implemented in two ways. One approach is the commitment chaining approach (currently used), where each new commitment state is derived from the previous one. This ensures the commitment number and balances on both sides remain correct, and also allows exercising cases with multiple commitments once more interesting mutators are added. The tradeoff is that both SpliceMutator and GeneratorInsertionMutator must update the commitment state indices if instructions are inserted in between, otherwise, these mutators become ineffective for this flow. Another approach is to build the next commitment state directly from raw inputs. However, this has a major caveat around balance handling: if the balances are not correct, the commitment dance will not complete successfully.
AdvanceCommitmentStateAddHtlc
- Input types:
CommitmentState, Htlc, Point, Point (local and remote per-commitment points)
- Output type:
CommitmentState
AdvanceCommitmentStateFailHtlc
- Input types:
CommitmentState, HtlcId, Point, Point
- Output type:
CommitmentState
AdvanceCommitmentStateFulfillHtlc
- Input types:
CommitmentState, HtlcId, Point, Point
- Output type:
CommitmentState
AdvanceCommitmentStateUpdateFee
- Input types:
CommitmentState, FeeratePerKw, Point, Point
- Output type:
CommitmentState
-
BuildCommitmentSigned:
- Input types:
ChannelId, ChannelConfig, CommitmentState, HolderIdentity
- Output type:
Message
-
RecvRevokeAndAck: We can also verify here that the received PerCommitmentSecret actually derives the peer's per_commitment_point stored in the old CommitmentState. For that, we would either need to carry old CommitmentState as an input or simply pass the expected Point directly for verification.
- Input types:
None
- Output type:
RevokeAndAck
-
ExtractRevokeAndAck:
- Input types:
RevokeAndAck
- Output type:
RevokeAndAckField { PerCommitmentSecret: PrivateKey, NextPerCommitmentPoint: Point}
-
RecvCommitmentSigned: Here, we can simply verify the signature and avoid extracting any fields. In the future, if we add cases like force-close fuzzing, we can instead output CommitmentSigned and introduce a separate operation to extract its fields.
- Input types:
ChannelConfig, CommitmentState, HolderIdentity
- Output type:
None
-
BuildRevokeAndAck:
- Input types:
ChannelId, PrivateKey, Point
- Output type:
Message
-
RecvUpdateFailMalformedHtlc: Until we send a valid OnionRoutingPacket or HtlcId, we are mostly going to get this. Here, we do not care about any other received message fields anyway, because we already know we sent something invalid. So we simply accept the failure and extract the HtlcId in order to remove it from the commitment state.
- Input types:
None
- Output type:
HtlcId
-
RecvUpdateFailHtlc: Mostly same as RecvUpdateFailMalformedHtlc
- Input types:
None
- Output type:
HtlcId
-
RecvUpdateFulfillHtlc: Though we are not going to get this right now, maybe we could defer it until we add support for valid OnionRoutingPackets? Also, right now we ignore the received payment_preimage, but we could add support for it later. We can retrieve the HTLC from the commitment state and verify the preimage against its payment hash, or perhaps simply query the output of CreateHtlc.
- Input types:
None
- Output type:
HtlcId
-
BuildUpdateFee:
- Input types:
ChannelId, FeeratePerKw
- Output type:
Message
Builder
generate_fresh arms for HtlcId, PaymentHash, and OnionRoutingPacket, the other variable types should be generated from composed inputs.
Note that for the MVP we send arbitrary OnionRoutingPackets, but later we will need to add support for constructing valid onions as well, and only mutate them through mutators for invalid cases.
Generators
Currently, until we have SpliceMutator or GeneratorInsertionMutator, we cannot realistically have many standalone generators for things like UpdateAddHtlcGenerator, CommitmentSignedGenerator, or UpdateFeeGenerator. So until then, we likely need a single generator such as CommitmentDanceGenerator that exercises the full commitment dance end-to-end, either with update_add_htlc or perhaps with update_fee as well.
Mutators
-
OperationParamMutator: Most of the operations defined above are built from composed inputs, so they will not be op-param mutable.
-
SpliceMutator and GeneratorInsertionMutator: Now, the current design of CommitmentState is such that we need a previous commitment state in order to create a valid commitment signature. For mutators like these, if another set of valid instructions is inserted at some point, then the commitment state references in the instructions need to be updated accordingly so that the current commitment state always references the closest previous commitment state. Otherwise, these mutators will become ineffective for normal channel operations. The same applies to BuildCommitmentSigned as well. In short, whichever operations take CommitmentState as an input need to be updated to reference the latest commitment state known to them.
Note: We also need to think about how to ensure that the commitment number is updated only once during an entire commitment dance, especially if a generator is inserted before the commitment dance. The same concern applies within a single dance when multiple updates are batched together, since each AdvanceCommitmentState{Kind} currently bumps the number.
InstructionDeleteMutator: Currently, I have not wired the commitment dance for this yet, but this also needs to be handled, otherwise, deleting certain operations may cause timeouts. I think this can be updated accordingly once we standardize the approach and properly integrate these mutators.
SnapshotSetup
Add PostChannelOpenSetup alongside PostInitSetup. It drives the full funding flow procedurally and snapshots with a channel already open, so CommitmentDanceGenerator can exercise the HTLC adds, commitment dance, update fees. State is written into ProgramContext. For this I have 2 ideas on which we can discuss which one to proceed forward with:
- One approach is to add the channel open state through
ProgramContext, where we store discrete values and load them via Load*FromContext operations. In that case, the ProgramContext would look like:
struct ProgramContext {
...
channel_id: Option<ChannelId>,
feerate_per_kw: Option<u32>,
channel_type: Option<Vec<u8>>,
funding_satoshis: Option<u64>,
local_config: Option<ChannelPartyConfig>,
remote_config: Option<ChannelPartyConfig>,
initial_per_commitment_privkey: Option<PrivateKey>, // open_channel <-> accept_channel
next_per_commitment_privkey: Option<PrivateKey>, // channel_ready <-> channel_ready
local_funding_privkey: Option<Secret>,
local_htlc_basepoint_privkey: Option<Secret>,
// from open_channel <-> accept_channel
local_initial_per_commitment_point: Option<PublicKey>, // #0
remote_initial_per_commitment_point: Option<PublicKey>, // #0
// from channel_ready <-> channel_ready
local_next_per_commitment_point: Option<PublicKey>, // #1
remote_next_per_commitment_point: Option<PublicKey>, // #1
funding_outpoint: Option<OutPoint>,
}
Then, ChannelConfig, CommitmentState, HolderIdentity can be constructed from those loaded inputs. The main benefit of this approach is that, since the program loads discrete values directly, mutators can modify individual fields before constructing ChannelConfig, CommitmentState, HolderIdentity. However, I think mutating most of these fields would mainly just result in invalid signatures being sent to the peer, so I am not sure this approach provides much additional value beyond exercising incorrect-signature cases. Because of that, I am thinking about another approach instead.
- Another approach is to directly export
ChannelConfig, CommitmentState and HolderIdentity through ProgramContext, and load them in the executor using LoadChannelConfigFromContext and LoadCommitmentStateFromContext operations. In that case, the ProgramContext would look like:
struct ProgramContext {
...
channel_id: Option<ChannelId>,
channel_config: Option<ChannelConfig>,
commitment_state: Option<CommitmentState>,
initial_per_commitment_privkey: Option<PrivateKey>, // #0
next_per_commitment_privkey: Option<PrivateKey>, // #1
local_per_commitment_point: Option<PublicKey>, // #1
remote_per_commitment_point: Option<PublicKey>, // #1
holder_identity: Option<HolderIdentity>,
}
The tradeoff of this approach is that we lose the ability to mutate individual fields inside ChannelConfig, CommitmentState, HolderIdentity. However, as mentioned above, mutating those fields would mostly just result in invalid signatures being sent, without providing much additional coverage. I think mutations are more valuable when applied to HTLC-related messages and flows, which this approach would still allow.
NOTE: One point to discuss here is that, with the current design, CommitmentDanceGenerator can also run in the PostInitSetup state. We need to ensure that it only runs in the PostChannelOpenSetup state, since only that setup contains valid inputs in ProgramContext for the Load*FromContext operations. Otherwise, those operations may fail in the PostInitSetup state.
Example generated program
# Channel state from ProgramContext
v0 = LoadChannelIdFromContext()
v1 = LoadChannelConfigFromContext()
v2 = LoadCommitmentStateFromContext()
v3 = LoadInitialPerCommitmentPrivkeyFromContext()
v4 = LoadNextPerCommitmentPrivkeyFromContext()
v5 = LoadLocalPerCommitmentPointFromContext()
v6 = LoadRemotePerCommitmentPointFromContext()
v7 = LoadHolderIdentityFromContext()
# HTLC primitives
v8 = LoadHtlcId(0)
v9 = LoadAmount(50_000_000)
v10 = LoadPaymentHash(0xAA..AA)
v11 = LoadBlockHeight(1_000)
v12 = CreateHtlc(v8, v9, v10, v11)
v13 = LoadOnionRoutingPacket(0x00..00)
# Send update_add_htlc (ignoring TLV fields for now)
v14 = BuildUpdateAddHtlc(v0, v12, v13)
SendMessage(v14)
# Advance state with the AddHtlc update
v15 = AdvanceCommitmentStateAddHtlc(v2, v12, v5, v6)
# Sign and send commitment_signed
v16 = BuildCommitmentSigned(v0, v1, v15, v7)
SendMessage(v16)
# Receive peer's revoke_and_ack and their commitment_signed
v17 = RecvRevokeAndAck()
v18 = ExtractNextPerCommitmentPoint(v17)
RecvCommitmentSigned(v1, v15, v7)
# Our next per-commitment point
v19 = LoadPrivateKey(0x02)
v20 = DerivePoint(v19)
# Send our revoke_and_ack
v21 = BuildRevokeAndAck(v0, v3, v20)
SendMessage(v21)
# Receive the failure and extract the failed HtlcId
v22 = RecvUpdateFailMalformedHtlc()
# Advance state to remove the failed HTLC
v23 = AdvanceCommitmentStateFailHtlc(v15, v22, v20, v18)
# Receive target's commitment_signed
RecvCommitmentSigned(v1, v23, v7)
# Our next per-commitment point
v24 = LoadPrivateKey(0x03)
v25 = DerivePoint(v24)
# Send our revoke_and_ack
v26 = BuildRevokeAndAck(v0, v4, v25)
SendMessage(v26)
# Sign and send commitment_signed
v27 = BuildCommitmentSigned(v0, v1, v23, v7)
SendMessage(v27)
# Receive target's final revoke_and_ack
v28 = RecvRevokeAndAck()
Note
Please note that this design is currently more of an MVP, since there are many aspects of normal channel operations still left to cover.
Some assumptions in the current design:
-
In PostChannelOpenSetup, we assume that we are always the channel opener. If we later support the target as the channel opener, we will need to carry a local_side field in ProgramContext. Similarly, Htlc will also need a Side field since the struct should work for both sides. For now, we can keep an offerer field in Htlc and always set it to Side::Opener.
-
Since we currently only exercise the case where we send HTLCs, there is no need for operations like BuildUpdateFulfillHtlc, BuildUpdateFailHtlc etc yet. However, if we later support receiving HTLCs from the target, we can add these operations as well.
Discussion
Multiple pending updates within one commitment dance: Currently, we only support a single pending update between CommitmentSigned messages, which makes it easier to construct and exchange the commitment dance. However, supporting N pending updates may expose additional bugs and edge cases. We need to consider how to evolve the design to support this. In such a case, AdvanceCommitmentStateAddHtlc would need to accept multiple commitment updates between BuildCommitmentSigned calls. Another extension is supporting different pending updates on local and remote sides (which may come later when we receive add Htlc). This would require handling asymmetric pending updates on both sides.
Objective
After the funding flow, we need a scenario for fuzzing normal channel operations. So the main objective of this plan is that we can complete the commitment dance fully, and the design should be extensible enough to support future extensions. That way, once we have more advanced mutators, cases like multiple HTLC adds or many commitment dance operations can happen correctly, helping test deeper code paths and edge cases.
Smite APIs
BOLT messages
update_fulfill_htlcupdate_fail_htlcupdate_fail_malformed_htlcupdate_add_htlccommitment_signedrevoke_and_ackupdate_feeCommitment sign/verify APIs
Currently, smite only supports the initial funding flow, so we need to extend it with HTLC support. The main changes are:
Htlcstruct and in-flight HTLC tracking toCommitmentState.PendingUpdateenum covering the commitment-affecting updates (AddHtlc(Htlc),FulfillHtlc(id),FailHtlc(id),UpdateFee(feerate_per_kw)).CommitmentState::advance(updates)) and fee accounting to handle HTLC outputs.Variables and VariableType
VariableTypeHtlcIdu64PaymentHash[u8; 32]OnionRoutingPacket[u8; 1366]Htlcsmite::channel_tx::HtlcRevokeAndAcksmite::bolt::RevokeAndAckOperations
CreateHtlc: We need this operation so that we can tie the same HTLC toBuildUpdateAddHtlcandAdvanceCommitmentStateAddHtlc. Otherwise, there is a chance that we send one HTLC but use a different HTLC in the commitment state, which would only result in invalid signatures. So, if we want to test that mismatch case, we can omit this operation, otherwise, it makes sense to keep it.HtlcId,Amount,PaymentHash,BlockHeightHtlcBuildUpdateAddHtlc:ChannelId,Htlc,OnionRoutingPacketMessageAdvanceCommitmentState{Kind}: One op per update kind, each producing the nextCommitmentStatefrom the previous one plus the kind-specific payload. This can be implemented in two ways. One approach is the commitment chaining approach (currently used), where each new commitment state is derived from the previous one. This ensures the commitment number and balances on both sides remain correct, and also allows exercising cases with multiple commitments once more interesting mutators are added. The tradeoff is that bothSpliceMutatorandGeneratorInsertionMutatormust update the commitment state indices if instructions are inserted in between, otherwise, these mutators become ineffective for this flow. Another approach is to build the next commitment state directly from raw inputs. However, this has a major caveat around balance handling: if the balances are not correct, the commitment dance will not complete successfully.AdvanceCommitmentStateAddHtlcCommitmentState,Htlc,Point,Point(local and remote per-commitment points)CommitmentStateAdvanceCommitmentStateFailHtlcCommitmentState,HtlcId,Point,PointCommitmentStateAdvanceCommitmentStateFulfillHtlcCommitmentState,HtlcId,Point,PointCommitmentStateAdvanceCommitmentStateUpdateFeeCommitmentState,FeeratePerKw,Point,PointCommitmentStateBuildCommitmentSigned:ChannelId,ChannelConfig,CommitmentState,HolderIdentityMessageRecvRevokeAndAck: We can also verify here that the receivedPerCommitmentSecretactually derives the peer'sper_commitment_pointstored in the oldCommitmentState. For that, we would either need to carry oldCommitmentStateas an input or simply pass the expectedPointdirectly for verification.NoneRevokeAndAckExtractRevokeAndAck:RevokeAndAckRevokeAndAckField { PerCommitmentSecret: PrivateKey, NextPerCommitmentPoint: Point}RecvCommitmentSigned: Here, we can simply verify the signature and avoid extracting any fields. In the future, if we add cases like force-close fuzzing, we can instead outputCommitmentSignedand introduce a separate operation to extract its fields.ChannelConfig,CommitmentState,HolderIdentityNoneBuildRevokeAndAck:ChannelId,PrivateKey,PointMessageRecvUpdateFailMalformedHtlc: Until we send a validOnionRoutingPacketorHtlcId, we are mostly going to get this. Here, we do not care about any other received message fields anyway, because we already know we sent something invalid. So we simply accept the failure and extract theHtlcIdin order to remove it from the commitment state.NoneHtlcIdRecvUpdateFailHtlc: Mostly same asRecvUpdateFailMalformedHtlcNoneHtlcIdRecvUpdateFulfillHtlc: Though we are not going to get this right now, maybe we could defer it until we add support for validOnionRoutingPackets? Also, right now we ignore the receivedpayment_preimage, but we could add support for it later. We can retrieve the HTLC from the commitment state and verify the preimage against its payment hash, or perhaps simply query the output ofCreateHtlc.NoneHtlcIdBuildUpdateFee:ChannelId,FeeratePerKwMessageBuilder
generate_fresharms forHtlcId,PaymentHash, andOnionRoutingPacket, the other variable types should be generated from composed inputs.Note that for the MVP we send arbitrary
OnionRoutingPackets, but later we will need to add support for constructing valid onions as well, and only mutate them through mutators for invalid cases.Generators
Currently, until we have
SpliceMutatororGeneratorInsertionMutator, we cannot realistically have many standalone generators for things likeUpdateAddHtlcGenerator,CommitmentSignedGenerator, orUpdateFeeGenerator. So until then, we likely need a single generator such asCommitmentDanceGeneratorthat exercises the full commitment dance end-to-end, either withupdate_add_htlcor perhaps withupdate_feeas well.Mutators
OperationParamMutator: Most of the operations defined above are built from composed inputs, so they will not be op-param mutable.SpliceMutatorandGeneratorInsertionMutator: Now, the current design ofCommitmentStateis such that we need a previous commitment state in order to create a valid commitment signature. For mutators like these, if another set of valid instructions is inserted at some point, then the commitment state references in the instructions need to be updated accordingly so that the current commitment state always references the closest previous commitment state. Otherwise, these mutators will become ineffective for normal channel operations. The same applies toBuildCommitmentSignedas well. In short, whichever operations takeCommitmentStateas an input need to be updated to reference the latest commitment state known to them.Note: We also need to think about how to ensure that the commitment number is updated only once during an entire commitment dance, especially if a generator is inserted before the commitment dance. The same concern applies within a single dance when multiple updates are batched together, since each
AdvanceCommitmentState{Kind}currently bumps the number.InstructionDeleteMutator: Currently, I have not wired the commitment dance for this yet, but this also needs to be handled, otherwise, deleting certain operations may cause timeouts. I think this can be updated accordingly once we standardize the approach and properly integrate these mutators.SnapshotSetup
Add
PostChannelOpenSetupalongsidePostInitSetup. It drives the full funding flow procedurally and snapshots with a channel already open, soCommitmentDanceGeneratorcan exercise the HTLC adds, commitment dance, update fees. State is written intoProgramContext. For this I have 2 ideas on which we can discuss which one to proceed forward with:ProgramContext, where we store discrete values and load them viaLoad*FromContextoperations. In that case, theProgramContextwould look like:Then,
ChannelConfig,CommitmentState,HolderIdentitycan be constructed from those loaded inputs. The main benefit of this approach is that, since the program loads discrete values directly, mutators can modify individual fields before constructingChannelConfig,CommitmentState,HolderIdentity. However, I think mutating most of these fields would mainly just result in invalid signatures being sent to the peer, so I am not sure this approach provides much additional value beyond exercising incorrect-signature cases. Because of that, I am thinking about another approach instead.ChannelConfig,CommitmentStateandHolderIdentitythroughProgramContext, and load them in the executor usingLoadChannelConfigFromContextandLoadCommitmentStateFromContextoperations. In that case, theProgramContextwould look like:The tradeoff of this approach is that we lose the ability to mutate individual fields inside
ChannelConfig,CommitmentState,HolderIdentity. However, as mentioned above, mutating those fields would mostly just result in invalid signatures being sent, without providing much additional coverage. I think mutations are more valuable when applied to HTLC-related messages and flows, which this approach would still allow.NOTE: One point to discuss here is that, with the current design,
CommitmentDanceGeneratorcan also run in thePostInitSetupstate. We need to ensure that it only runs in thePostChannelOpenSetupstate, since only that setup contains valid inputs inProgramContextfor theLoad*FromContextoperations. Otherwise, those operations may fail in thePostInitSetupstate.Example generated program
Note
Please note that this design is currently more of an MVP, since there are many aspects of normal channel operations still left to cover.
Some assumptions in the current design:
In
PostChannelOpenSetup, we assume that we are always the channel opener. If we later support the target as the channel opener, we will need to carry alocal_sidefield inProgramContext. Similarly,Htlcwill also need aSidefield since the struct should work for both sides. For now, we can keep anoffererfield inHtlcand always set it toSide::Opener.Since we currently only exercise the case where we send HTLCs, there is no need for operations like
BuildUpdateFulfillHtlc,BuildUpdateFailHtlcetc yet. However, if we later support receiving HTLCs from the target, we can add these operations as well.Discussion
Multiple pending updates within one commitment dance: Currently, we only support a single pending update between
CommitmentSignedmessages, which makes it easier to construct and exchange the commitment dance. However, supporting N pending updates may expose additional bugs and edge cases. We need to consider how to evolve the design to support this. In such a case,AdvanceCommitmentStateAddHtlcwould need to accept multiple commitment updates betweenBuildCommitmentSignedcalls. Another extension is supporting different pending updates on local and remote sides (which may come later when we receive add Htlc). This would require handling asymmetric pending updates on both sides.