-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
peer: more interfaces, refactor unit tests #5067
peer: more interfaces, refactor unit tests #5067
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice job on this refactor! main comment is re: whether or not we can reuse the mock connection in wtmock
, would be great if we can start to unify all of our mocks to reduce code duplication
d0924dc
to
c9f2a36
Compare
c9f2a36
to
ef842ec
Compare
@cfromknecht fixed |
ef842ec
to
ebb2ad5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great work! Def will make our testing more maintainable!
Still reading the relevant code to understand the change. One question is, should the interfaces live in peer
or should they live in their "original" package, such as MessageSwitch
in htlcswitch
or in peer
? If all the interfaces live in peer
, it has the advantage that the interfaces are abstracted as exactly what the peer
needs, which imo is also the downside as it might not fit the abstraction needed by other packages.
peer/brontide.go
Outdated
|
||
// MakeChannelSwitch initializes a ChannelSwitch given a raw *htlcswitch.Switch | ||
// pointer. | ||
func MakeChannelSwitch(innerSwitch *htlcswitch.Switch) *ChannelSwitch { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: use NewChannelSwitch
since it's commonly used style in this repo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
// MessageSwitch is an interface that manages setup, retrieval, and shutdown of | ||
// MessageLink implementations. | ||
type MessageSwitch interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like htlcswitch.Switch
already satisfied most of the functions here. I wonder about the necessity to create the channelSwitch
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation is to make peer
not rely on htlcswitch.Switch
, so instead use a ChannelSwitch
that can be replaced with a mock. Otherwise you need to provide a *htlcswitch.Switch
when testing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
channelSwitch needs to wrap GetLink
to return MessageLink
as *htlcswitch.Switch
returns ChannelLink
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you could get over *htlcswitch.Switch
since channelSwitch
needs it. This design pattern is weird. You have a concrete type defined in another package, and you have an interface defined in this package, then you create a concrete struct that implements the interface, which in the end is the concrete type living in another package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to be able to replace *htlcswitch.Switch
in peer tests (which IMO is worth it, as it does not belong in the peer unit tests):
- Need an interface to pass into the peer config instead of the raw Switch pointer.
- The interface should also live in the peer package.
- So, the Switch now has to comply with this peer-pkg interface.
- Peer calls the interface's
GetLink
which returnsChannelLink, error
- To implement this interface in unit tests, and have a non-nil
ChannelLink
** returned onGetLink
, we must implement this interface in the peer. ChannelLink
interface has this methodHandleSwitchPacket(*htlcPacket) error
which uses a private*htlcPacket
argument.
- To implement this interface in unit tests, and have a non-nil
- So there are two options:
- Change the interface
GetLink
to not returnChannelLink, error
- Change
ChannelLink
methods to not have*htlcPacket
- Change the interface
So IMO this is a workaround until we can cleanup the htlcswitch package. One thing would be to remove *htlcPacket
in public methods. And another would be to have GetLink
return a concrete type so that it can fit into any interface because we don't have interfaces narrowing ChannelLink
-> MessageLink
. This last change would allow us to remove the channelSwitch
type.
** I have a test-case that requires a non-nil link to be returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a blocker for your ACK? I can figure out the best step to avoid this "type confusion"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to be able to replace *htlcswitch.Switch in peer tests (which IMO is worth it, as it does not belong in the peer unit tests):
I agree we should do this. And what confused me here is that, the change made in current commits do not acheive this goal? You have a MessageSwitch
interface defined, which is good. But the concrete implementation still requires a *htlcswitch.Switch
. The test below uses NewChannelSwitch
, which takes a *htlcswitch.Switch
.
The interface should also live in the peer package.
So, the Switch now has to comply with this peer-pkg interface.
This is probably my main blocker because, imo, packages should be independent in the sense of actual implementations of interfaces. peer
should not care about the actual implementation of an interface defined by switch
, and vice versa. peer
should use the interfaces like its an API, only cares about what arguments to use and what values are returned. This way we could provide a proper abstraction for each package.
Peer calls the interface's GetLink which returns ChannelLink, error
To implement this interface in unit tests, and have a non-nil ChannelLink** returned on GetLink, we must implement this interface in the peer.
ChannelLink interface has this method HandleSwitchPacket(*htlcPacket) error which uses a private *htlcPacket argument.
You could also make *htlcPacket
an interface so we don't need the concrete type? Just like you've mentioned,
One thing would be to remove *htlcPacket in public methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests do not use NewChannelSwitch
, they use the mockMessageSwitch
? The peer interface will always be a subset of functionality provided by the Switch
and only cares about the functions used in the peer
package so I think this is achieved? I think why I didn't change *htlcPacket
was because it was a pretty large change that touched the unit tests of the htlcswitch
package
peer/brontide.go
Outdated
// ChannelSwitch is an implementation of the MessageSwitch interface that wraps | ||
// htlcswitch.Switch. | ||
type ChannelSwitch struct { | ||
innerSwitch *htlcswitch.Switch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still reading through the commits, but I think the innerSwitch
and *htlcswitch.Switch
are the same thing in terms of structure right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need the innerSwitch
variable at all? We could just embed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
embedded
return s.innerSwitch.GetLink(cid) | ||
} | ||
|
||
// InitLink initializes a ChannelLink in the Switch. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MessageLink
or ChannelLink
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is using the underlying htlcswitch method AddLink
and NewChannelLink
, so I'd say a ChannelLink
@@ -88,3 +90,22 @@ type MessageSwitch interface { | |||
// ChannelID. | |||
RemoveLink(lnwire.ChannelID) | |||
} | |||
|
|||
// ChannelGraph is an interface that abstracts the network graph. | |||
type ChannelGraph interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could change the names to something else as a) the same names are confusing and b) the name here means an interface but in channeldb
it's a concrete type, which is more confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do this in netann
and autopilot
for ChannelGraph interface
. We should be using package interfaces in most places to reduce dependencies on other packages, so IMO similar naming across packages shouldn't matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice changes that'll make testing much simper! 🥇 Added a few comments and suggestions but overall the PR is almost ready.
|
||
// MessageLink is an interface that contains some functionality from a | ||
// htlcswitch.ChannelLink. | ||
type MessageLink interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be more idiomatic Go, maybe we could name this interface ChannelUpdateHandler
. Since it's handling channel updates. wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer to keep it as MessageLink
peer/brontide.go
Outdated
|
||
// MakeChannelSwitch initializes a ChannelSwitch given a raw *htlcswitch.Switch | ||
// pointer. | ||
func MakeChannelSwitch(innerSwitch *htlcswitch.Switch) *ChannelSwitch { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
peer/brontide.go
Outdated
// ChannelSwitch is an implementation of the MessageSwitch interface that wraps | ||
// htlcswitch.Switch. | ||
type ChannelSwitch struct { | ||
innerSwitch *htlcswitch.Switch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need the innerSwitch
variable at all? We could just embed.
peer/brontide_test.go
Outdated
// Check that mockConn sent out the expected message. | ||
msg, err := getMessage(mockConn) | ||
require.NoError(t, err) | ||
require.Equal(t, msg, test.expectedInit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the recommended order is reqire.Equal(t, expected, got)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed in the commit it's introduced, a later commit removes it
func (p *MockPeer) ReadNextBody(_ []byte) ([]byte, error) { | ||
select { | ||
case msg := <-p.IncomingMsgs: | ||
return msg, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: new line before the next case
branch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
watchtower/wtmock/peer.go
Outdated
select { | ||
case msg := <-p.IncomingMsgs: | ||
return msg, nil | ||
case <-time.After(5 * time.Second): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: could make this a const
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
made const
lntest/mock/walletcontroller.go
Outdated
if addrType == lnwallet.WitnessPubKey { | ||
witProgram := btcutil.Hash160(key) | ||
|
||
addr, _ := btcutil.NewAddressWitnessPubKeyHash( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why swallow the error
if we might as well just return it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
returning error
peer/brontide_test.go
Outdated
// Alice should reply with an Init message, we discard it. | ||
msg, err := getMessage(aliceConn) | ||
require.NoError(t, err) | ||
_, ok := msg.(*lnwire.Init) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: instead of these casts, we could use reqire.IsType
to make the tests more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed only for the Init
messages, other message types are cast and then the fields are used
@yyforyongyu Interfaces live in the peer package so testing the peer is easier, if there are global interfaces they could live in another package. But interface use across packages will just lead to import cycles and not very good separation of what a specific package needs (usually a subset of functionality) |
Another issue with having inter-package dependencies is that it makes it impossible to fuzz or unit test a specific package without including all of its dependencies. So this PR removes that friction by not having to instantiate a whole |
ebb2ad5
to
e8ab671
Compare
Otherwise, the Peer will send Shutdown messages with incorrect DeliveryAddress.
This commit removes the references to internal Brontide members and instead creates *Brontide and only accesses public members. This allows for proper testing of the Brontide API and allows us to send messages as a normal peer would. This also paves the way for moving the peer tests to the peer_test package instead of residing in the peer package.
e8ab671
to
8390f7f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still going through the last commit.
Interfaces live in the peer package so testing the peer is easier
Could you elaborate? As long as it's an interface, I don't think it matters because it's an abstraction, and you really shouldn't test abstractions in unit tests (that's integration test or functional test are for).
But interface use across packages will just lead to import cycles and not very good separation of what a specific package needs (usually a subset of functionality)
It depends. go
's io
package is a good example, it provides a reader/writer interface then the package bytes
implements it in its buffer
struct. In our case, interface living in htlcswitch
means it provides the abstractions that can be used by other packages without caring about the implementation details. This is also why we want to use mock
here, as we don't want to re-implement the logic in our unit tests.
Another issue with having inter-package dependencies is that it makes it impossible to fuzz or unit test a specific package without including all of its dependencies. So this PR removes that friction by not having to instantiate a whole *htlcswitch.Switch when unit testing, among other out-of-package structs
I think that's why we use mock.
|
||
// MessageSwitch is an interface that manages setup, retrieval, and shutdown of | ||
// MessageLink implementations. | ||
type MessageSwitch interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you could get over *htlcswitch.Switch
since channelSwitch
needs it. This design pattern is weird. You have a concrete type defined in another package, and you have an interface defined in this package, then you create a concrete struct that implements the interface, which in the end is the concrete type living in another package.
// Sphinx is an interface that abstracts the decryption of onion blobs. | ||
type Sphinx interface { | ||
// DecodeHopIterators batch decodes HTLC onion blobs. | ||
DecodeHopIterators([]byte, []hop.DecodeHopIteratorRequest) ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually don't add args to interfaces
I don't think that's the case, curious where did you get that?
The abstractions are used so we don't have to make concrete objects that are irrelevant for testing the peer.
If htlcswitch exports a |
Mock does not solve this. The peer requires concrete structs which should not be the case if we want to unit/fuzz test this. The way to solve this is with interfaces. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Written below my personal opinions, non-blocking,
The abstractions are used so we don't have to make concrete objects that are irrelevant for testing the peer.
Agreed. And I like this new approach of interfacing wherever possible. The issue is, who is reponsible for providing and maintaining the abstractions? Why is peer
defining the abstractions while the acutal implementaions are living across various packages? This also means that peer
is not pure in the sense that it needs to know all the implementation details of other packages?
If htlcswitch exports a Switch interface or ChannelLink interface that is then used by the peer package, then peer imports htlcswitch. If htlcswitch then imports peer package (or a package that uses peer indirectly) to make use of some interface living in the peer, then we have an import cycle/
If we have this issue we should fix it. Also I think we should forbid it rather than allowing it to happen. Plus I don't think it's a better solution this way. If the interface is exported by switch
, then used by peer
, then imported again by switch
, the switch
should not import from peer
but use its own defined interfaces.
|
||
respDeliveryScript := shutdownMsg.Address | ||
// Create a test channel between Alice and Bob. | ||
aliceChan, bobLnChan, err := createTestChannels(noUpdate, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also pass the ctx
to function createTestChannels
to avoid the long function signature.
alicePath, err := ioutil.TempDir("", "alicedb") | ||
require.NoError(t, err) | ||
|
||
aliceDb, err := channeldb.Open(alicePath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we want to deprecate the Open
function @bhandras ?
t: t, | ||
writtenMessages: make(chan []byte, expectedMessages), | ||
// pushMessage pushes an lnwire.Message to the MockPeer. | ||
func pushMessage(p *wtmock.MockPeer, msg lnwire.Message) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could change this into assertPushMessage
and use the require
to check the error here, which has the benefit of reducing half of the lines.
// slice we expect. | ||
func (m *mockMessageConn) assertWrite(expected []byte) { | ||
// getMessage retrieves a message from the MockPeer. | ||
func getMessage(p *wtmock.MockPeer) (lnwire.Message, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, this could also be an assertion to reduce roughly two thrids of the lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Future tests may not know the message coming across, so I can change the pushMessage but not the getMessage
if !bytes.Equal(test.expectedScript, shutdownMsg.Address) { | ||
// Check that the Shutdown message includes the | ||
// expected delivery script. | ||
if !bytes.Equal( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use require
instead?
|
||
// MessageSwitch is an interface that manages setup, retrieval, and shutdown of | ||
// MessageLink implementations. | ||
type MessageSwitch interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to be able to replace *htlcswitch.Switch in peer tests (which IMO is worth it, as it does not belong in the peer unit tests):
I agree we should do this. And what confused me here is that, the change made in current commits do not acheive this goal? You have a MessageSwitch
interface defined, which is good. But the concrete implementation still requires a *htlcswitch.Switch
. The test below uses NewChannelSwitch
, which takes a *htlcswitch.Switch
.
The interface should also live in the peer package.
So, the Switch now has to comply with this peer-pkg interface.
This is probably my main blocker because, imo, packages should be independent in the sense of actual implementations of interfaces. peer
should not care about the actual implementation of an interface defined by switch
, and vice versa. peer
should use the interfaces like its an API, only cares about what arguments to use and what values are returned. This way we could provide a proper abstraction for each package.
Peer calls the interface's GetLink which returns ChannelLink, error
To implement this interface in unit tests, and have a non-nil ChannelLink** returned on GetLink, we must implement this interface in the peer.
ChannelLink interface has this method HandleSwitchPacket(*htlcPacket) error which uses a private *htlcPacket argument.
You could also make *htlcPacket
an interface so we don't need the concrete type? Just like you've mentioned,
One thing would be to remove *htlcPacket in public methods.
So I think this is mainly where we disagree -- the peer only cares about the functions and return values it uses. It does not use every method of the |
If the peer is importing an interface defined by the switch, and the switch imports a different interface from the peer (or an indirect package that also imports the peer package), we get this problem. This is the problem with importing from other packages that we want to solve. This is why each package needs its own interfaces that are not used by other packages. |
Like I said this shouldn't happen in the first place. And imo we should NEVER import like how you've described above. Having an import cycle is bad, allowing it to happen is worse. If we do have this issue then there's something wrong with the code structure, and we should fix that rather than enabling it to happen.
The peer not only holds interfaces like I think the effective go has made it clear, and it has a nice example of how
|
The peer package only implements what it uses, it does not implement all methods of the switch -- if the peer already used a function A from the switch package, changing the method would require a change in the peer. Likewise if the peer uses a peer-specific interface which contains the function A, and the switch no longer complies with the interface, a change in the peer is also necessary. So I'll just give some examples to show my point:
Anecdotally, our code structure in lnd allows this to happen because we are importing interfaces across packages instead of having either:
|
Going to open another pull that refactors the peer/switch interaction specifically and then base this PR on that one. Should clean some stuff up. |
Cool this is good to know. As mentioned above this is non-blocking and it's only personal opinions. I don't think there's a hard rule for deciding where the interfaces should live, only recommendations from practices like the effect go. THB it's really case-specific.
Unrelated to this PR, after reading the codebase these days I'm starting to have a vague idea of how the different layers are defined and interacted. Still learning, but will try to draft out the overall structure someday. |
It's good to have discussions about this as it forces me to defend my position and if I'm wrong, that's good as then I can re-evaluate my position. One goal I have for the codebase is to put global mocks in the |
Is this ready for another round? |
No this is pretty low prio for me atm |
!lightninglabs-deploy mute 2022-Feb-01 |
@cfromknecht: review reminder |
!lightninglabs-deploy mute 2022-March-01 |
By adding more interfaces, we reduce dependencies and can test the
peer
package without accessing internal members. Only the last commit is diff-heavy.