Skip to content
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

Merged
merged 10 commits into from
Dec 6, 2022
Merged

Fetch tspends in SPV mode #2194

merged 10 commits into from
Dec 6, 2022

Conversation

bgptr
Copy link
Contributor

@bgptr bgptr commented Dec 1, 2022

This fetches tspends via the p2p network in SPV mode using a getinitstate message and adds them to the tspends cache.

This fetches tspends via the p2p network in SPV mode using a `getinitstate` message and adds them to the tspend cache.
p2p/peering.go Outdated Show resolved Hide resolved
wallet/wallet.go Show resolved Hide resolved
@jrick
Copy link
Member

jrick commented Dec 1, 2022

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.

Copy link
Member

@matheusd matheusd left a 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.
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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).

Copy link
Member

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])
Copy link
Member

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)
Copy link
Member

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)
@bgptr bgptr requested a review from jrick December 2, 2022 13:59
wallet/wallet.go Outdated
@@ -232,6 +232,14 @@ func (w *Wallet) VotingEnabled() bool {
return enabled
}

// IsTSpendCached returns wheater the given hash is already cached
Copy link
Member

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
Comment on lines 1142 to 1143
rp.Disconnect(err)
rp.requestedInitStateMu.Unlock()
Copy link
Member

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
Comment on lines 1746 to 1749
go func() {
<-stalled.C
rp.deleteRequestedInitState()
}()
Copy link
Member

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()
Copy link
Member

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()
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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
Comment on lines 919 to 921
if s.checkTSpend(ctx, tx) {
s.wallet.AddTSpend(*tx)
}
Copy link
Member

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
@jrick jrick merged commit eab54c3 into decred:master Dec 6, 2022
@bgptr bgptr deleted the tspend-spv branch December 6, 2022 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants