-
Notifications
You must be signed in to change notification settings - Fork 154
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
Fetch tspends in SPV mode #2194
Conversation
This fetches tspends via the p2p network in SPV mode using a `getinitstate` message and adds them to the tspend cache.
Also, tspend transactions that are published after the initstate is handled need to be added to the wallet as well. There needs to be tspend handling added to the spv.Syncer.handleTxInvs method. |
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.
Tested current version on simnet and it worked as intended (tspend arrived to the wallet in spv mode).
p2p/peering.go
Outdated
@@ -1678,3 +1695,16 @@ func (rp *RemotePeer) SendHeadersSent() bool { | |||
rp.requestedHeadersMu.Unlock() | |||
return sent | |||
} | |||
|
|||
// GetInitState attempts to get initial state by sending a NewMsgGetInitState message. |
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.
[...] by sending a GetInitState message.
NewMsgGetInitState
is the name of the func, the message itself is GetInitState
.
p2p/peering.go
Outdated
func (rp *RemotePeer) GetInitState(ctx context.Context) error { | ||
const opf = "remotepeer(%v).GetInitState" | ||
msg := wire.NewMsgGetInitState() | ||
msg.AddTypes(wire.InitStateHeadBlocks, wire.InitStateHeadBlockVotes, wire.InitStateTSpends) |
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.
You can probably just request wire.InitStateTSpends
and not the other types here, because they are not used for anything in spv mode.
p2p/peering.go
Outdated
@@ -771,6 +773,8 @@ func (rp *RemotePeer) readMessages(ctx context.Context) error { | |||
rp.receivedGetMiningState(ctx) | |||
case *wire.MsgGetInitState: | |||
rp.receivedGetInitState(ctx) | |||
case *wire.MsgInitState: | |||
rp.lp.receivedInitState <- newInMsg(rp, msg) |
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.
You need to select across ctx.Done() as well, otherwise this goroutine may leak.
I suggest following the same pattern as the other handlers, introducing a rp.receivedInitState()
func and handling everything there.
Another thing you might wanna consider is adding checks to ensure the MsgInitState
was in fact received as a response to a previous GetInitState
(and not just arbitrarily sent by the remote peer multiple times).
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 see jrick already commented something similar while I was writing this)
spv/sync.go
Outdated
for i, tx := range prevTxs { | ||
if tx != nil { | ||
tspendTxs = append(tspendTxs, tx) | ||
unseenTSpends = append(unseenTSpends, prevUnseen[i]) |
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.
It doesn't look like you're using the unseenTSpends
slice after this point, only tspendTxs
, so it seems unnecessary to also recreate this slice.
spv/sync.go
Outdated
tspendTxs, unseenTSpends = tspendTxs[:0], unseenTSpends[:0] | ||
for i, tx := range prevTxs { | ||
if tx != nil { | ||
tspendTxs = append(tspendTxs, tx) |
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.
You need to double check the received transaction is actually a tspend by calling stake.IsTSpend
, and ideally also check the signature is valid for tspend keys for the current network and that it has not expired.
- instroduce MaskInitState and mask MsgInitState typed messages with it - add comment to IsTSpendCached public function - reword comment (NewMsgGetInitState -> GetInitState) - just request wire.InitStateTSpends type - remove unused unseenTSpends - double check the received transaction is actually a tspend and that it has not expired. - handle incoming tspends in handleTxInvs
…ous GetInitState (and not just arbitrarily sent by the remote peer multiple times)
wallet/wallet.go
Outdated
@@ -232,6 +232,14 @@ func (w *Wallet) VotingEnabled() bool { | |||
return enabled | |||
} | |||
|
|||
// IsTSpendCached returns wheater the given hash is already cached |
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.
wheater => whether
End sentence with a period.
p2p/peering.go
Outdated
rp.Disconnect(err) | ||
rp.requestedInitStateMu.Unlock() |
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.
Swap these two lines, so that you do the Disconnect() after releasing the mutex.
There's no obvious race doing the current way, but in general you should try to do as little as possible inside a critical section (that is, while having a locked mutex), for safety and future proofing.
In fact, you can rewrite the use of the requestedInitStateMu
mutex in this function to keep it really short:
rp.requestedInitStateMu.Lock()
c := rp.requestedInitState
rp.requestedInitState = nil
rp.requestedInitStateMu.Unlock()
if c == nil {
// error out
}
Writing unconditionally to the field (rp.requestedInitState = nil
) is fine because if it's already nil, it will remain so and if it's non-nil, c
will have the channel for the later code to act upon. This makes the time spent in the critical section the smallest possible and makes it easier to deduce there will be no deadlocks possible (because there are no function calls that could themselves trigger a deadlock).
p2p/peering.go
Outdated
go func() { | ||
<-stalled.C | ||
rp.deleteRequestedInitState() | ||
}() |
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.
It's not clear to me why you're doing the deleteRequestedInitState
inside a goroutine.
If the context is done, you can just call the deleteRequestedInitState()
directly (because no reply is expected anymore). Doing after waiting for stalled.C to trigger is just leaking this goroutine for potentially 30 seconds.
p2p/peering.go
Outdated
rp.Disconnect(err) | ||
return nil, err | ||
case <-rp.errc: | ||
stalled.Stop() |
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 potentially leaks timer resources forever (check the doc for the Stop() function).
You need to call this like the doc explains:
if !stalled.Stop() {
<-stalled.C
}
p2p/peering.go
Outdated
case out <- &msgAck{msg, nil}: | ||
out = nil | ||
case msg := <-c: | ||
stalled.Stop() |
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.
Same as above
spv/sync.go
Outdated
@@ -1410,6 +1527,11 @@ func (s *Syncer) startupSync(ctx context.Context, rp *p2p.RemotePeer) error { | |||
} | |||
} | |||
|
|||
err = s.GetInitState(ctx, rp) |
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 should only be called if rp.Pver() >= wire.InitStateVersion
. The current peer code has a minimum supported version that is lower than the one that introduced the GetInitState message, so we could be connecting to a peer that does not support it.
p2p/peering.go
Outdated
func (rp *RemotePeer) GetInitState(ctx context.Context) (*wire.MsgInitState, error) { | ||
const opf = "remotepeer(%v).GetInitState" | ||
msg := wire.NewMsgGetInitState() | ||
msg.AddTypes(wire.InitStateTSpends) |
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.
let's either move these type constants into the method parameters, so it can be used to query anything (we might want to fetch other mempool txs using this, too), or take the *wire.MsgGetInitState as a parameter directly.
spv/sync.go
Outdated
sigHash, err := txscript.CalcSignatureHash(nil, | ||
txscript.SigHashAll, msgTx, 0, nil) | ||
if err != nil { | ||
return fmt.Errorf("CalcSignatureHash: %w", err) |
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 decred.org/dcrwallet/v3/errors
spv/sync.go
Outdated
if s.checkTSpend(ctx, tx) { | ||
s.wallet.AddTSpend(*tx) | ||
} |
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.
Was doing another functional test for this case and I realized this section is only called if the tspend has an output relevant to the wallet.
This needs to be moved into a loop of its own, before filtering for relevant transactions, otherwise the wallet will not know about the tspend to set a voting policy for.
check tspends before filtering for relevant txs
This fetches tspends via the p2p network in SPV mode using a
getinitstate
message and adds them to the tspends cache.