-
Notifications
You must be signed in to change notification settings - Fork 292
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
multi: Add initial committed filter (CF) support #1151
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.
This is just a very quick first pass. In addition to the inline comments, the ProtocolVersion
in wire/protocol.go
needs to be bumped to 6 and a new variable CFilterVersion uint32 = 6
since the wire protocol is being changed.
gcs/gcs.go
Outdated
|
||
"github.com/aead/siphash" | ||
"github.com/decred/dcrd/chaincfg/chainhash" | ||
"github.com/kkdai/bstream" // TODO: can probably remove this dep |
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.
Yes, let's address this TODO and remove this please. I really would prefer to avoid yet another dependency for something this trivial.
blockchain/indexers/cfindex.go
Outdated
@@ -0,0 +1,308 @@ | |||
// Copyright (c) 2017 The btcsuite developers | |||
// Copyright (c) 2017 The Decred developers |
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.
2018
LICENSE
Outdated
Copyright (c) 2013-2016 The btcsuite developers | ||
Copyright (c) 2015-2016 The Decred developers | ||
Copyright (c) 2013-2017 The btcsuite developers | ||
Copyright (c) 2015-2017 The Decred developers |
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.
2018
blockchain/indexers/cfindex.go
Outdated
} | ||
|
||
firstHeader := make([]byte, chainhash.HashSize) | ||
err = dbStoreFilterHeader( |
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.
Please make the arguments match the normal style.
blockchain/indexers/cfindex.go
Outdated
|
||
// storeFilter stores a given filter, and performs the steps needed to | ||
// generate the filter's header. | ||
func storeFilter(dbTx database.Tx, block *dcrutil.Block, f *gcs.Filter, |
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.
Func definitions on one line.
@@ -901,6 +905,14 @@ func loadConfig() (*config, []string, error) { | |||
return nil, nil, err | |||
} | |||
|
|||
// !--nocfilters and --dropcfindex do not mix. |
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.
Not sure why the previous one has an exclamation point. None of the others do. Please remove it and the other one above too.
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 because this is an index enabled by default. The previous check for the addrexistsindex also uses the !, because keeping the index enabled while also dropping the index doesn't make sense.
7c50501
to
c2914b8
Compare
There currently exists a bug where the CF index cannot be dropped. The index dropping code assumes there are no nested buckets in the index and uses a cursor to iterate over the bucket to delete all entries (which itself is a hack to reduce memory usage vs deleting the index bucket itself). |
Plan is to merge this after #1158 is in, which will solve the problem of being unable to drop the CF index. |
wire/msgcfheaders.go
Outdated
// BtcDecode decodes r using the wire protocol encoding into the receiver. | ||
// This is part of the Message interface implementation. | ||
func (msg *MsgCFHeaders) BtcDecode(r io.Reader, pver uint32) error { | ||
// Read stop hash |
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.
All of these BtcDecode
and BtcEncode
functions for the new messages should be checking the protocol version and returning an error if it's before the minimum supported one.
e.g.
if pver < NodeCFVersion {
str := fmt.Sprintf("getcftypes message invalid for protocol "+
"version %d", pver)
return messageError("MsgCFHeaders.BtcDecode", str)
}
...
Then, in the tests for all of these new messages, there should be a cross protocol test function (e.g. TestCFHeadersCrossProtocol
for this case) which ensures attempting to decode a message encoded with the new protocol version fails as expected when attempting to decode it with an older protocol version.
See msgfilteradd.go
and msgfilteradd_test.go
in the upstream btcd code for examples.
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.
There appears to be variation in the meaning of the cross protocol tests. In dcrd's TestSendHeadersCrossProtocol test, the two protocols used both support the message being tested and serialization/deserialization is tested to not error on either. This same test is also present in btcd.
wire/msggetcfilter.go
Outdated
// NewMsgGetCFilter returns a new getcfilter message that conforms to the | ||
// Message interface using the passed parameters and defaults for the remaining | ||
// fields. | ||
func NewMsgGetCFilter(blockHash *chainhash.Hash, |
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.
Func definitions on the same line, please.
wire/msggetcfilter.go
Outdated
if err != nil { | ||
return err | ||
} | ||
return readElement(r, &msg.FilterType) |
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 be cast to (*uint8)(&msg.FilterType)
to avoid invoking the slow reflection path in readElement
. Either that, or readElement
could have *FilterType
added as one of the cases. I personally would just do the cast here though as it's less code.
wire/msggetcfilter.go
Outdated
if err != nil { | ||
return err | ||
} | ||
return writeElement(w, msg.FilterType) |
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 either be return binarySerializer.PutUint8(w, msg.FilterType)
or writeElement
would need to have a FilterType
case added to avoid the slow reflection-based path. I would suggest just using binarySerializer
directly since that matches what is done elsewhere in msgtx.go
.
wire/msggetcfilter.go
Outdated
@@ -0,0 +1,62 @@ | |||
// Copyright (c) 2017 The btcsuite developers | |||
// Use of this source code is governed by an ISC |
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.
Don't forget to add the 2018 Decred copyright when making the modifications.
blockchain/indexers/cfindex.go
Outdated
|
||
// dbFetchFilter retrieves a block's basic or extended filter. A filter's | ||
// absence is not considered an error. | ||
func dbFetchFilter(dbTx database.Tx, key []byte, h *chainhash.Hash) ([]byte, 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.
The error return seems unnecessary since nothing returns one.
blockchain/indexers/cfindex.go
Outdated
func (idx *CFIndex) FilterHeaderByBlockHash(h *chainhash.Hash, filterType wire.FilterType) ([]byte, error) { | ||
var fh []byte | ||
err := idx.db.View(func(dbTx database.Tx) error { | ||
if uint8(filterType) > maxFilterType { |
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 here regarding performing the checkout outside of the db transaction.
gcs/builder/builder.go
Outdated
// Use of this source code is governed by an ISC | ||
// license that can be found in the LICENSE file. | ||
|
||
package builder |
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.
Any reason this is in a separate package and not just the gcs
package? It results in a generic and nondescriptive name as it is in a separate package.
It would be much clearer for the code to read like gcs.BuildExtFilter(...)
instead of builder.BuildExtFilter
.
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 agree with unifying the packages. The builder package came from @Roasbeef's original implementation. Unsure if there was a technical reason for the separation.
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.
There are many other functions exposed by the builder package that would need to be renamed if this change was made. For example, WithKeyPN
and the rest of the With...
functions. Currently these are called as builder.With...
to return a *builder.GCSBuilder
, but these function names wouldn't make much sense if they were qualified with the gcs
name.
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.
After looking into this some more, it appears that the gcs package is intended to be a general implementation of golomb coded sets while builder is specific to building filters from decred blocks. We can and probably also should remove the extra unused builder methods (everything but BuildBasicFilter and BuildExtFilter).
peer/peer.go
Outdated
@@ -147,6 +157,18 @@ type MessageListeners struct { | |||
// message. | |||
OnGetHeaders func(p *Peer, msg *wire.MsgGetHeaders) | |||
|
|||
// OnGetCFilter is invoked when a peer receives a getcfilter decred |
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.
s/decred/wire/
peer/peer.go
Outdated
OnGetCFilter func(p *Peer, msg *wire.MsgGetCFilter) | ||
|
||
// OnGetCFHeaders is invoked when a peer receives a getcfheaders | ||
// decred 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.
s/decred/wire/
peer/peer.go
Outdated
// decred message. | ||
OnGetCFHeaders func(p *Peer, msg *wire.MsgGetCFHeaders) | ||
|
||
// OnGetCFTypes is invoked when a peer receives a getcftypes decred |
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.
s/decred/wire/
gcs/builder/builder.go
Outdated
|
||
// RandomKey is a utility function that returns a cryptographically random | ||
// [gcs.KeySize]byte usable as a key for a GCS filter. | ||
func RandomKey() (key [gcs.KeySize]byte, err 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.
This package is using named returns in various places. Please remove them all.
gcs/builder/builder_test.go
Outdated
0x3b, 0x8b, 0x0e, 0x26, 0x64, 0x8d, 0x4a, 0x15, | ||
0x3b, 0x8b, 0x0e, 0x26, 0x64, 0x8d, 0x4a, 0x15, | ||
}, | ||
|
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.
extra blank line
gcs/builder/builder.go
Outdated
b := WithKeyHash(&blockHash) | ||
|
||
// For each stake transaction, commit the transaction hash. | ||
for _, tx := range block.STransactions { |
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.
Based on the intent described by the comment of the function, this appears to be missing the signature scripts for all of the stake transactions.
In the case of votes and revocations, it's the output of the ticket that is being spent (.TxIn[1]
for votes, and .TxIn[0]
for revocations), and in the case of tickets, it is all inputs.
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 function comment says it includes the witness/sigscripts of regular transactions. No where does it say it commits to the input scripts of stake transactions.
I'm not sure if it would be worthwhile modifying the extended filter to include this data, or if a 3rd filter type should be added specifically for extra commitments for stake transactions.
wire/msgcfheaders.go
Outdated
} | ||
|
||
// Read filter type | ||
err = readElement(r, &msg.FilterType) |
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.
As mentioned elsewhere, this needs to be a cast to avoid the slow path (or change readElement
).
wire/msgcfheaders.go
Outdated
} | ||
|
||
// Write filter type | ||
err = writeElement(w, msg.FilterType) |
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 here regarding use binarySerializer.PutUint8
.
wire/msgcfilter.go
Outdated
return err | ||
} | ||
// Read filter type | ||
err = readElement(r, &msg.FilterType) |
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 here.
wire/msgcfilter.go
Outdated
return err | ||
} | ||
|
||
err = writeElement(w, msg.FilterType) |
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.
And here.
wire/msgcfilter.go
Outdated
if err != nil { | ||
return err | ||
} | ||
// Read filter type |
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.
All the other decode funcs have blank lines between each field, but this one doesn't.
wire/msgcftypes.go
Outdated
if err != nil { | ||
return 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.
The maximum allowed number of filter types is 256 due to it being a uint8
. This needs to ensure the value read does not exceed it. Otherwise, it would be trivial to send a message with a massive count and panic the process remotely.
wire/msgcftypes.go
Outdated
msg.SupportedFilters = make([]FilterType, count) | ||
for i := uint64(0); i < count; i++ { | ||
var filterType uint8 | ||
err = readElement(r, &filterType) |
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 regarding the cast or addition of *FilterType
to readElement
.
Also, to cut down on the number of allocations, please read it directly into the array element.
wire/msgcftypes.go
Outdated
} | ||
|
||
for i := range msg.SupportedFilters { | ||
err = writeElement(w, msg.SupportedFilters[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.
Same regarding binarySerializer.PutUint8
.
wire/msggetcfheaders.go
Outdated
return err | ||
} | ||
|
||
return readElement(r, &msg.FilterType) |
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.
Cast...
wire/msggetcfheaders.go
Outdated
return err | ||
} | ||
|
||
return writeElement(w, msg.FilterType) |
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.
binarySerializer.PutUint8
...
The regular filter has been changed to commit to output scripts themselves instead of all data pushes within the script. This significantly reduces filter bloat at the cost of needing to know the exact script used (instead of, say, a hash160). This introduces a new problem that is specific to Decred. Output scripts of stake transactions include an opcode tag in the scripts to distinguish between a ticket purchase, ticket purchase change, vote coin generation, and revocation coin generation. This now means that SPV wallets that search the filter for address usage must now check 4 (!) possible output scripts instead of 1: P2PKH, OP_SSTXCHANGE P2PKH, OP_SSGEN P2PKH, and OP_SSRTX P2PKH. Obviously this is not ideal, and I'm considering stripping the tag before committing these scripts. |
gcs/gcs.go
Outdated
return false | ||
} | ||
|
||
// MatchAny returns checks whether any []byte value is likely (within collision |
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.
s/returns checks/checks/
gcs/gcs.go
Outdated
return v*f.modulusP + rem, nil | ||
} | ||
|
||
// Hash returns the double-BLAKE256 hash of the filter. |
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 should change this to be a single hash. There is no reason to double hash in BLAKE256 since the hash function isn't vulnerable to length extension attacks which is why double sha256 is used in the upstream code.
gcs/gcs.go
Outdated
copy(filterTip, filterHash[:]) | ||
copy(filterTip[chainhash.HashSize:], prevHeader[:]) | ||
|
||
// The final filter hash is the double-blake256 of the hash computed |
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 here. No need to double hash.
Stake opcode tags are now being stripped before committing the output script. |
cde78ec
to
624ed55
Compare
This change begins the work of bringing committed filters to the network consensus daemon. Committed filters are designed to enable light wallets without many of the privacy issues associated with server-side bloom filtering. The new gcs package provides the primitives for creating and matching against Golomb-coded sets (GCS) filters while the blockcf package provides creation of filters and filter entries for data structures found in blocks. The wire package has been updated to define a new protocol version and service flag for advertising CF support and includes types for the following new messages: cfheaders, cfilter, cftypes, getcfheaders, getcfilter, getcftypes. The peer package and server implementation have been updated to include support for the new protocol version and messages. Filters are created using a collision probability of 2^-20 and are saved to a new optional database index when running with committed filter support enabled (the default). At first startup, if support is not disabled, the index will be created and populated with filters and filter headers for all preexisting blocks, and new filters will be recorded for processed blocks. Multiple filter types are supported. The regular filter commits to output scripts and previous outpoints that any non-voting wallet will require access to. Scripts and previous outpoints that can only be spent by votes and revocations are not committed to the filter. The extended filter is a supplementary filter which commits to all transaction hashes and script data pushes from the input scripts of non-coinbase regular and ticket purchase transactions. Creating these filters is based on the algorithm defined by BIP0158 but is modified to only commit "regular" data in stake transactions to prevent committed filters from being used to create SPV voting wallets.
This change begins the work of bringing committed filters to the
network consensus daemon. Committed filters are designed to enable
light wallets without many of the privacy issues associated with
server-side bloom filtering.
The new gcs package provides the primitives for creating and matching
against Golomb-coded sets (GCS) filters while the blockcf package
provides creation of filters and filter entries for data structures
found in blocks.
The wire package has been updated to define a new protocol version and
service flag for advertising CF support and includes types for the
following new messages: cfheaders, cfilter, cftypes, getcfheaders,
getcfilter, getcftypes. The peer package and server implementation
have been updated to include support for the new protocol version and
messages.
Filters are created using a collision probability of 2^-20 and are
saved to a new optional database index when running with committed
filter support enabled (the default). At first startup, if support is
not disabled, the index will be created and populated with filters and
filter headers for all preexisting blocks, and new filters will be
recorded for processed blocks.
Multiple filter types are supported. The regular filter commits to
output scripts and previous outpoints that any non-voting wallet will
require access to. Scripts and previous outpoints that can only be
spent by votes and revocations are not committed to the filter. The
extended filter is a supplementary filter which commits to all
transaction hashes and script data pushes from the input scripts of
non-coinbase regular and ticket purchase transactions. Creating these
filters is based on the algorithm defined by BIP0158 but is modified
to only commit "regular" data in stake transactions to prevent
committed filters from being used to create SPV voting wallets.