Skip to content

Commit 1d721a4

Browse files
committed
protofsm: add new package for driving generic protocol FSMs
In this PR, we create a new package, `protofsm` which is intended to abstract away from something we've done dozens of time in the daemon: create a new event-drive protocol FSM. One example of this is the co-op close state machine, and also the channel state machine itself. This packages picks out the common themes of: * clear states and transitions between them * calling out to special daemon adapters for I/O such as transaction broadcast or sending a message to a peer * cleaning up after state machine execution * notifying relevant callers of updates to the state machine The goal of this PR, is that devs can now implement a state machine based off of this primary interface: ```go // State defines an abstract state along, namely its state transition function // that takes as input an event and an environment, and returns a state // transition (next state, and set of events to emit). As state can also either // be terminal, or not, a terminal event causes state execution to halt. type State[Event any, Env Environment] interface { // ProcessEvent takes an event and an environment, and returns a new // state transition. This will be iteratively called until either a // terminal state is reached, or no further internal events are // emitted. ProcessEvent(event Event, env Env) (*StateTransition[Event, Env], error) // IsTerminal returns true if this state is terminal, and false otherwise. IsTerminal() bool } ``` With their focus being _only_ on each state transition, rather than all the boiler plate involved (processing new events, advancing to completion, doing I/O, etc, etc). Instead, they just make their states, then create the state machine given the starting state and env. The only other custom component needed is something capable of mapping wire messages or other events from the "outside world" into the domain of the state machine. The set of types is based on a pseudo sum type system wherein you declare an interface, make the sole method private, then create other instances based on that interface. This restricts call sites (must pass in that interface) type, and with some tooling, exhaustive matching can also be enforced via a linter. The best way to get a hang of the pattern proposed here is to check out the tests. They make a mock state machine, and then use the new executor to drive it to completion. You'll also get a view of how the code will actually look, with the focus being on the: input event, current state, and output transition (can also emit events to drive itself forward).
1 parent 6cef367 commit 1d721a4

File tree

4 files changed

+834
-0
lines changed

4 files changed

+834
-0
lines changed

protofsm/daemon_events.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package protofsm
2+
3+
import (
4+
"github.com/btcsuite/btcd/btcec/v2"
5+
"github.com/btcsuite/btcd/wire"
6+
"github.com/lightningnetwork/lnd/fn"
7+
"github.com/lightningnetwork/lnd/lnwire"
8+
)
9+
10+
// DaemonEvent is a special event that can be emmitted by a state transition
11+
// function. A state machine can use this to perform side effects, such as
12+
// sending a message to a peer, or broadcasting a transaction.
13+
type DaemonEvent interface {
14+
daemonSealed()
15+
}
16+
17+
// DaemonEventSet is a set of daemon events that can be emitted by a state
18+
// transition.
19+
type DaemonEventSet []DaemonEvent
20+
21+
// DaemonEvents is a special type constraint that enumerates all the possible
22+
// types of daemon events.
23+
type DaemonEvents interface {
24+
SendMsgEvent[any] | BroadcastTxn
25+
}
26+
27+
// SendPredicate is a function that returns true if the target message should
28+
// sent.
29+
type SendPredicate = func() bool
30+
31+
// SendMsgEvent is a special event that can be emitted by a state transition
32+
// that instructs the daemon to send the contained message to the target peer.
33+
type SendMsgEvent[Event any] struct {
34+
// TargetPeer is the peer to send the message to.
35+
TargetPeer btcec.PublicKey
36+
37+
// Msgs is the set of messages to send to the target peer.
38+
Msgs []lnwire.Message
39+
40+
// SendWhen implements a system for a conditional send once a special
41+
// send predicate has been met.
42+
//
43+
// TODO(roasbeef): contrast with usage of OnCommitFlush, etc
44+
SendWhen fn.Option[SendPredicate]
45+
46+
// PostSendEvent is an optional event that is to be emitted after the
47+
// message has been sent. If a SendWhen is specified, then this will
48+
// only be executed after that returns true to unblock the send.
49+
PostSendEvent fn.Option[Event]
50+
}
51+
52+
// daemonSealed indicates that this struct is a DaemonEvent instance.
53+
func (s *SendMsgEvent[E]) daemonSealed() {}
54+
55+
// BroadcastTxn indicates the target transaction should be broadcast to the
56+
// network.
57+
type BroadcastTxn struct {
58+
// Tx is the transaction to broadcast.
59+
Tx *wire.MsgTx
60+
61+
// Label is an optional label to attach to the transaction.
62+
Label string
63+
}
64+
65+
// daemonSealed indicates that this struct is a DaemonEvent instance.
66+
func (b *BroadcastTxn) daemonSealed() {}

protofsm/log.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package protofsm
2+
3+
import (
4+
"github.com/btcsuite/btclog"
5+
"github.com/lightningnetwork/lnd/build"
6+
)
7+
8+
// log is a logger that is initialized with no output filters. This
9+
// means the package will not perform any logging by default until the caller
10+
// requests it.
11+
var log btclog.Logger
12+
13+
// The default amount of logging is none.
14+
func init() {
15+
UseLogger(build.NewSubLogger("PRCL", nil))
16+
}
17+
18+
// DisableLog disables all library log output. Logging output is disabled
19+
// by default until UseLogger is called.
20+
func DisableLog() {
21+
UseLogger(btclog.Disabled)
22+
}
23+
24+
// UseLogger uses a specified Logger to output package logging info.
25+
// This should be used in preference to SetLogWriter if the caller is also
26+
// using btclog.
27+
func UseLogger(logger btclog.Logger) {
28+
log = logger
29+
}

0 commit comments

Comments
 (0)