diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ace70b7d85..72788c0a6b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -20,7 +20,7 @@ jobs: fail-fast: true matrix: testnet: ["dashcore", "rotate"] - timeout-minutes: 25 + timeout-minutes: 30 env: FULLNODE_PUBKEY_KEEP: false CGO_LDFLAGS: "-L/usr/local/lib -ldashbls -lrelic_s -lmimalloc-secure -lgmp" diff --git a/abci/example/kvstore/kvstore.go b/abci/example/kvstore/kvstore.go index 41e9c2e52d..b7504090ba 100644 --- a/abci/example/kvstore/kvstore.go +++ b/abci/example/kvstore/kvstore.go @@ -411,8 +411,8 @@ func (app *Application) FinalizeBlock(_ context.Context, req *abci.RequestFinali if app.shouldCommitVerify { vsu := app.getActiveValidatorSetUpdates() qsd := types.QuorumSignData{ - Block: makeBlockSignItem(req, btcjson.LLMQType_5_60, vsu.QuorumHash), - Extensions: makeVoteExtensionSignItems(req, btcjson.LLMQType_5_60, vsu.QuorumHash), + Block: makeBlockSignItem(req, btcjson.LLMQType_5_60, vsu.QuorumHash), + VoteExtensionSignItems: makeVoteExtensionSignItems(req, btcjson.LLMQType_5_60, vsu.QuorumHash), } err := app.verifyBlockCommit(qsd, req.Commit) if err != nil { diff --git a/abci/example/kvstore/verify.go b/abci/example/kvstore/verify.go index 5e81aa18a1..e52116b5eb 100644 --- a/abci/example/kvstore/verify.go +++ b/abci/example/kvstore/verify.go @@ -9,7 +9,6 @@ import ( abci "github.com/dashpay/tenderdash/abci/types" "github.com/dashpay/tenderdash/crypto/encoding" tmbytes "github.com/dashpay/tenderdash/libs/bytes" - types1 "github.com/dashpay/tenderdash/proto/tendermint/types" "github.com/dashpay/tenderdash/types" ) @@ -23,27 +22,16 @@ func (app *Application) verifyBlockCommit(qsd types.QuorumSignData, commit abci. if err != nil { return err } - return verifier.Verify(pubKey, types.QuorumSigns{ - BlockSign: commit.BlockSignature, - ExtensionSigns: makeThresholdVoteExtensions(commit.ThresholdVoteExtensions), - }) -} -func makeThresholdVoteExtensions(pbVoteExtensions []*types1.VoteExtension) []types.ThresholdExtensionSign { - voteExtensions := types.VoteExtensionsFromProto(pbVoteExtensions) - var thresholdExtensionSigns []types.ThresholdExtensionSign - thresholdVoteExtensions, ok := voteExtensions[types1.VoteExtensionType_THRESHOLD_RECOVER] - if !ok { - return nil + extSigs := make([][]byte, 0, len(commit.ThresholdVoteExtensions)) + for _, ext := range commit.ThresholdVoteExtensions { + extSigs = append(extSigs, ext.Signature) } - thresholdExtensionSigns = make([]types.ThresholdExtensionSign, len(thresholdVoteExtensions)) - for i, voteExtension := range thresholdVoteExtensions { - thresholdExtensionSigns[i] = types.ThresholdExtensionSign{ - Extension: voteExtension.Extension, - ThresholdSignature: voteExtension.Signature, - } - } - return thresholdExtensionSigns + + return verifier.Verify(pubKey, types.QuorumSigns{ + BlockSign: commit.BlockSignature, + VoteExtensionSignatures: extSigs, + }) } func makeBlockSignItem( @@ -67,19 +55,14 @@ func makeVoteExtensionSignItems( req *abci.RequestFinalizeBlock, quorumType btcjson.LLMQType, quorumHash []byte, -) map[types1.VoteExtensionType][]types.SignItem { - items := make(map[types1.VoteExtensionType][]types.SignItem) - reqID := types.VoteExtensionRequestID(req.Height, req.Round) - protoExtensionsMap := types1.VoteExtensionsToMap(req.Commit.ThresholdVoteExtensions) - for t, exts := range protoExtensionsMap { - if items[t] == nil && len(exts) > 0 { - items[t] = make([]types.SignItem, len(exts)) - } - chainID := req.Block.Header.ChainID - for i, ext := range exts { - raw := types.VoteExtensionSignBytes(chainID, req.Height, req.Round, ext) - items[t][i] = types.NewSignItem(quorumType, quorumHash, reqID, raw) - } +) []types.SignItem { + + extensions := types.VoteExtensionsFromProto(req.Commit.ThresholdVoteExtensions...) + chainID := req.Block.Header.ChainID + + items, err := extensions.SignItems(chainID, quorumType, quorumHash, req.Height, req.Round) + if err != nil { + panic(fmt.Errorf("vote extension sign items: %w", err)) } return items } diff --git a/abci/types/types.go b/abci/types/types.go index c1569c67f8..23779998d4 100644 --- a/abci/types/types.go +++ b/abci/types/types.go @@ -370,3 +370,23 @@ func (m *RequestFinalizeBlock) ToCanonicalVote() (types.CanonicalVote, error) { } return cv, nil } + +// Convert to proto.types.VoteExtension. +// Signature field will be nil, as ExtendVoteExtension doesn't have it. +func (m *ExtendVoteExtension) ToVoteExtension() types.VoteExtension { + ve := types.VoteExtension{ + Type: m.Type, + Extension: m.Extension, + } + + // workaround for a bug in gogoproto + if m.XSignRequestId != nil { + src := m.GetSignRequestId() + + ve.XSignRequestId = &types.VoteExtension_SignRequestId{ + SignRequestId: bytes.Clone(src), + } + } + + return ve +} diff --git a/abci/types/types.pb.go b/abci/types/types.pb.go index f0fc0d9704..9d6b484742 100644 --- a/abci/types/types.pb.go +++ b/abci/types/types.pb.go @@ -3067,13 +3067,19 @@ func (m *ResponseProcessProposal) GetValidatorSetUpdate() *ValidatorSetUpdate { return nil } -// Provides a vote extension for signing. Each field is mandatory for filling +// Provides a vote extension for signing. `type` and `extension` fields are mandatory for filling type ExtendVoteExtension struct { - // Vote extension type can be either DEFAULT or THRESHOLD_RECOVER. - // The Tenderdash supports only THRESHOLD_RECOVER at this moment. + // Vote extension type can be either DEFAULT, THRESHOLD_RECOVER or THRESHOLD_RECOVER_RAW. + // The Tenderdash supports only THRESHOLD_RECOVER and THRESHOLD_RECOVER_RAW at this moment. Type types1.VoteExtensionType `protobuf:"varint,1,opt,name=type,proto3,enum=tendermint.types.VoteExtensionType" json:"type,omitempty"` // Deterministic or (Non-Deterministic) extension provided by the sending validator's Application. + // + // For THRESHOLD_RECOVER_RAW, it MUST be 32 bytes. Extension []byte `protobuf:"bytes,2,opt,name=extension,proto3" json:"extension,omitempty"` + // Types that are valid to be assigned to XSignRequestId: + // + // *ExtendVoteExtension_SignRequestId + XSignRequestId isExtendVoteExtension_XSignRequestId `protobuf_oneof:"_sign_request_id"` } func (m *ExtendVoteExtension) Reset() { *m = ExtendVoteExtension{} } @@ -3109,6 +3115,25 @@ func (m *ExtendVoteExtension) XXX_DiscardUnknown() { var xxx_messageInfo_ExtendVoteExtension proto.InternalMessageInfo +type isExtendVoteExtension_XSignRequestId interface { + isExtendVoteExtension_XSignRequestId() + MarshalTo([]byte) (int, error) + Size() int +} + +type ExtendVoteExtension_SignRequestId struct { + SignRequestId []byte `protobuf:"bytes,3,opt,name=sign_request_id,json=signRequestId,proto3,oneof" json:"sign_request_id,omitempty"` +} + +func (*ExtendVoteExtension_SignRequestId) isExtendVoteExtension_XSignRequestId() {} + +func (m *ExtendVoteExtension) GetXSignRequestId() isExtendVoteExtension_XSignRequestId { + if m != nil { + return m.XSignRequestId + } + return nil +} + func (m *ExtendVoteExtension) GetType() types1.VoteExtensionType { if m != nil { return m.Type @@ -3123,6 +3148,20 @@ func (m *ExtendVoteExtension) GetExtension() []byte { return nil } +func (m *ExtendVoteExtension) GetSignRequestId() []byte { + if x, ok := m.GetXSignRequestId().(*ExtendVoteExtension_SignRequestId); ok { + return x.SignRequestId + } + return nil +} + +// XXX_OneofWrappers is for the internal use of the proto package. +func (*ExtendVoteExtension) XXX_OneofWrappers() []interface{} { + return []interface{}{ + (*ExtendVoteExtension_SignRequestId)(nil), + } +} + type ResponseExtendVote struct { VoteExtensions []*ExtendVoteExtension `protobuf:"bytes,1,rep,name=vote_extensions,json=voteExtensions,proto3" json:"vote_extensions,omitempty"` } @@ -4278,62 +4317,62 @@ func init() { func init() { proto.RegisterFile("tendermint/abci/types.proto", fileDescriptor_252557cfdd89a31a) } var fileDescriptor_252557cfdd89a31a = []byte{ - // 3631 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x5b, 0xcd, 0x73, 0x1b, 0xd7, - 0x91, 0xc7, 0xe0, 0x1b, 0x8d, 0xaf, 0xe1, 0x23, 0x25, 0x41, 0x90, 0x44, 0xd2, 0xa3, 0xb5, 0x25, - 0x6b, 0x6d, 0xd2, 0x96, 0xd6, 0x96, 0xbd, 0xf6, 0x6e, 0x15, 0x08, 0x42, 0x0b, 0x4a, 0x14, 0x49, - 0x0f, 0x41, 0xba, 0xbc, 0x5e, 0x7b, 0x6a, 0x08, 0x3c, 0x12, 0x63, 0x01, 0x98, 0xf1, 0xcc, 0x80, - 0x02, 0x7d, 0xdd, 0xf5, 0x1e, 0x7c, 0xf2, 0x3f, 0xe0, 0xdb, 0xee, 0x71, 0x2f, 0x7b, 0x4a, 0xe5, - 0x90, 0x54, 0x6e, 0x4e, 0xe5, 0xe2, 0x63, 0x2e, 0x51, 0x5c, 0xf2, 0x25, 0x95, 0x5b, 0x4e, 0xb9, - 0xa5, 0x52, 0xef, 0x63, 0x3e, 0x81, 0xc1, 0x87, 0xe5, 0xaa, 0x54, 0x6e, 0x78, 0xfd, 0xba, 0x7b, - 0xde, 0x47, 0xbf, 0xee, 0x7e, 0xbf, 0x7e, 0x80, 0x6b, 0x36, 0x1e, 0x74, 0xb0, 0xd9, 0xd7, 0x06, - 0xf6, 0xa6, 0x7a, 0xd2, 0xd6, 0x36, 0xed, 0x0b, 0x03, 0x5b, 0x1b, 0x86, 0xa9, 0xdb, 0x3a, 0x2a, - 0x7b, 0x9d, 0x1b, 0xa4, 0xb3, 0x7a, 0xc3, 0xc7, 0xdd, 0x36, 0x2f, 0x0c, 0x5b, 0xdf, 0x34, 0x4c, - 0x5d, 0x3f, 0x65, 0xfc, 0x55, 0xbf, 0x32, 0xaa, 0x67, 0xb3, 0xa3, 0x5a, 0x5d, 0xde, 0x79, 0x7d, - 0xac, 0xf3, 0xa4, 0xa7, 0xb7, 0x9f, 0x44, 0xf6, 0xfa, 0x06, 0x12, 0xe8, 0xe5, 0xdf, 0x7d, 0x82, - 0x2f, 0x9c, 0xde, 0x1b, 0x63, 0xb2, 0x86, 0x6a, 0xaa, 0x7d, 0xa7, 0x7b, 0xd5, 0xd7, 0x7d, 0x8e, - 0x4d, 0x4b, 0xd3, 0x07, 0x01, 0xe5, 0x6b, 0x67, 0xba, 0x7e, 0xd6, 0xc3, 0x9b, 0xb4, 0x75, 0x32, - 0x3c, 0xdd, 0xb4, 0xb5, 0x3e, 0xb6, 0x6c, 0xb5, 0x6f, 0x70, 0x86, 0x95, 0x33, 0xfd, 0x4c, 0xa7, - 0x3f, 0x37, 0xc9, 0x2f, 0x46, 0x95, 0xbe, 0xcc, 0x41, 0x46, 0xc6, 0x9f, 0x0f, 0xb1, 0x65, 0xa3, - 0xbb, 0x90, 0xc4, 0xed, 0xae, 0x5e, 0x11, 0xd6, 0x85, 0xdb, 0xf9, 0xbb, 0xd7, 0x37, 0x42, 0xeb, - 0xb6, 0xc1, 0xf9, 0x1a, 0xed, 0xae, 0xde, 0x8c, 0xc9, 0x94, 0x17, 0xbd, 0x05, 0xa9, 0xd3, 0xde, - 0xd0, 0xea, 0x56, 0xe2, 0x54, 0xe8, 0x46, 0x94, 0xd0, 0x03, 0xc2, 0xd4, 0x8c, 0xc9, 0x8c, 0x9b, - 0x7c, 0x4a, 0x1b, 0x9c, 0xea, 0x95, 0xc4, 0xf4, 0x4f, 0xed, 0x0c, 0x4e, 0xe9, 0xa7, 0x08, 0x2f, - 0xda, 0x02, 0xd0, 0x06, 0x9a, 0xad, 0xb4, 0xbb, 0xaa, 0x36, 0xa8, 0x24, 0xa9, 0xe4, 0x4b, 0xd1, - 0x92, 0x9a, 0x5d, 0x27, 0x8c, 0xcd, 0x98, 0x9c, 0xd3, 0x9c, 0x06, 0x19, 0xee, 0xe7, 0x43, 0x6c, - 0x5e, 0x54, 0x52, 0xd3, 0x87, 0xfb, 0x01, 0x61, 0x22, 0xc3, 0xa5, 0xdc, 0xe8, 0x7d, 0xc8, 0xb6, - 0xbb, 0xb8, 0xfd, 0x44, 0xb1, 0x47, 0x95, 0x0c, 0x95, 0x5c, 0x8b, 0x92, 0xac, 0x13, 0xbe, 0xd6, - 0xa8, 0x19, 0x93, 0x33, 0x6d, 0xf6, 0x13, 0xed, 0x41, 0xa9, 0xa7, 0x59, 0xb6, 0x62, 0x0d, 0x54, - 0xc3, 0xea, 0xea, 0xb6, 0x55, 0xc9, 0x53, 0x1d, 0x2f, 0x47, 0xe9, 0xd8, 0xd5, 0x2c, 0xfb, 0xd0, - 0x61, 0x6e, 0xc6, 0xe4, 0x62, 0xcf, 0x4f, 0x20, 0xfa, 0xf4, 0xd3, 0x53, 0x6c, 0xba, 0x0a, 0x2b, - 0x85, 0xe9, 0xfa, 0xf6, 0x09, 0xb7, 0x23, 0x4f, 0xf4, 0xe9, 0x7e, 0x02, 0xfa, 0x18, 0x96, 0x7b, - 0xba, 0xda, 0x71, 0xd5, 0x29, 0xed, 0xee, 0x70, 0xf0, 0xa4, 0x52, 0xa4, 0x4a, 0x5f, 0x8d, 0x1c, - 0xa4, 0xae, 0x76, 0x1c, 0x15, 0x75, 0x22, 0xd0, 0x8c, 0xc9, 0x4b, 0xbd, 0x30, 0x11, 0x7d, 0x0a, - 0x2b, 0xaa, 0x61, 0xf4, 0x2e, 0xc2, 0xda, 0x4b, 0x54, 0xfb, 0x9d, 0x28, 0xed, 0x35, 0x22, 0x13, - 0x56, 0x8f, 0xd4, 0x31, 0x2a, 0x6a, 0x81, 0x68, 0x98, 0xd8, 0x50, 0x4d, 0xac, 0x18, 0xa6, 0x6e, - 0xe8, 0x96, 0xda, 0xab, 0x94, 0xa9, 0xee, 0x5b, 0x51, 0xba, 0x0f, 0x18, 0xff, 0x01, 0x67, 0x6f, - 0xc6, 0xe4, 0xb2, 0x11, 0x24, 0x31, 0xad, 0x7a, 0x1b, 0x5b, 0x96, 0xa7, 0x55, 0x9c, 0xa5, 0x95, - 0xf2, 0x07, 0xb5, 0x06, 0x48, 0xa8, 0x01, 0x79, 0x3c, 0x22, 0xe2, 0xca, 0xb9, 0x6e, 0xe3, 0xca, - 0x12, 0x55, 0x28, 0x45, 0x9e, 0x33, 0xca, 0x7a, 0xac, 0xdb, 0xb8, 0x19, 0x93, 0x01, 0xbb, 0x2d, - 0xa4, 0xc2, 0xa5, 0x73, 0x6c, 0x6a, 0xa7, 0x17, 0x54, 0x8d, 0x42, 0x7b, 0x88, 0x3f, 0xa8, 0x20, - 0xaa, 0xf0, 0x1f, 0xa3, 0x14, 0x1e, 0x53, 0x21, 0xa2, 0xa2, 0xe1, 0x88, 0x34, 0x63, 0xf2, 0xf2, - 0xf9, 0x38, 0x99, 0x98, 0xd8, 0xa9, 0x36, 0x50, 0x7b, 0xda, 0x17, 0x58, 0xa1, 0x0e, 0xae, 0xb2, + // 3661 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x5b, 0xcd, 0x73, 0x1b, 0x57, + 0x72, 0xc7, 0xe0, 0x1b, 0x8d, 0xaf, 0xe1, 0x23, 0x25, 0x41, 0x90, 0x44, 0xd2, 0xa3, 0xd8, 0x92, + 0x65, 0x9b, 0xb4, 0xa5, 0xd8, 0xb2, 0x63, 0x27, 0x55, 0x20, 0x08, 0x05, 0x94, 0x28, 0x92, 0x1e, + 0x82, 0x74, 0x39, 0x8e, 0x3d, 0x35, 0x04, 0x1e, 0x89, 0xb1, 0x00, 0xcc, 0x78, 0x66, 0x40, 0x81, + 0xbe, 0x26, 0x4e, 0xa5, 0x7c, 0xf2, 0x3f, 0xe0, 0x5b, 0x72, 0xcc, 0x25, 0xa7, 0x54, 0x0e, 0x49, + 0xed, 0xcd, 0x5b, 0x7b, 0xf1, 0x71, 0x2f, 0xab, 0x75, 0xc9, 0x97, 0xad, 0xbd, 0xed, 0x69, 0x6f, + 0x5b, 0x5b, 0xef, 0x63, 0x3e, 0x81, 0xc1, 0x87, 0xe5, 0xaa, 0xad, 0xbd, 0xe1, 0xf5, 0xeb, 0xee, + 0x79, 0x1f, 0xfd, 0xba, 0xfb, 0xfd, 0xfa, 0x01, 0xae, 0xd9, 0x78, 0xd0, 0xc1, 0x66, 0x5f, 0x1b, + 0xd8, 0x9b, 0xea, 0x49, 0x5b, 0xdb, 0xb4, 0x2f, 0x0c, 0x6c, 0x6d, 0x18, 0xa6, 0x6e, 0xeb, 0xa8, + 0xec, 0x75, 0x6e, 0x90, 0xce, 0xea, 0x0d, 0x1f, 0x77, 0xdb, 0xbc, 0x30, 0x6c, 0x7d, 0xd3, 0x30, + 0x75, 0xfd, 0x94, 0xf1, 0x57, 0xfd, 0xca, 0xa8, 0x9e, 0xcd, 0x8e, 0x6a, 0x75, 0x79, 0xe7, 0xf5, + 0xb1, 0xce, 0x93, 0x9e, 0xde, 0x7e, 0x12, 0xd9, 0xeb, 0x1b, 0x48, 0xa0, 0x97, 0x7f, 0xf7, 0x09, + 0xbe, 0x70, 0x7a, 0x6f, 0x8c, 0xc9, 0x1a, 0xaa, 0xa9, 0xf6, 0x9d, 0xee, 0x55, 0x5f, 0xf7, 0x39, + 0x36, 0x2d, 0x4d, 0x1f, 0x04, 0x94, 0xaf, 0x9d, 0xe9, 0xfa, 0x59, 0x0f, 0x6f, 0xd2, 0xd6, 0xc9, + 0xf0, 0x74, 0xd3, 0xd6, 0xfa, 0xd8, 0xb2, 0xd5, 0xbe, 0xc1, 0x19, 0x56, 0xce, 0xf4, 0x33, 0x9d, + 0xfe, 0xdc, 0x24, 0xbf, 0x18, 0x55, 0xfa, 0x2a, 0x07, 0x19, 0x19, 0x7f, 0x31, 0xc4, 0x96, 0x8d, + 0xee, 0x42, 0x12, 0xb7, 0xbb, 0x7a, 0x45, 0x58, 0x17, 0x6e, 0xe7, 0xef, 0x5e, 0xdf, 0x08, 0xad, + 0xdb, 0x06, 0xe7, 0x6b, 0xb4, 0xbb, 0x7a, 0x33, 0x26, 0x53, 0x5e, 0xf4, 0x36, 0xa4, 0x4e, 0x7b, + 0x43, 0xab, 0x5b, 0x89, 0x53, 0xa1, 0x1b, 0x51, 0x42, 0x0f, 0x08, 0x53, 0x33, 0x26, 0x33, 0x6e, + 0xf2, 0x29, 0x6d, 0x70, 0xaa, 0x57, 0x12, 0xd3, 0x3f, 0xb5, 0x33, 0x38, 0xa5, 0x9f, 0x22, 0xbc, + 0x68, 0x0b, 0x40, 0x1b, 0x68, 0xb6, 0xd2, 0xee, 0xaa, 0xda, 0xa0, 0x92, 0xa4, 0x92, 0x2f, 0x45, + 0x4b, 0x6a, 0x76, 0x9d, 0x30, 0x36, 0x63, 0x72, 0x4e, 0x73, 0x1a, 0x64, 0xb8, 0x5f, 0x0c, 0xb1, + 0x79, 0x51, 0x49, 0x4d, 0x1f, 0xee, 0x87, 0x84, 0x89, 0x0c, 0x97, 0x72, 0xa3, 0x0f, 0x20, 0xdb, + 0xee, 0xe2, 0xf6, 0x13, 0xc5, 0x1e, 0x55, 0x32, 0x54, 0x72, 0x2d, 0x4a, 0xb2, 0x4e, 0xf8, 0x5a, + 0xa3, 0x66, 0x4c, 0xce, 0xb4, 0xd9, 0x4f, 0xb4, 0x07, 0xa5, 0x9e, 0x66, 0xd9, 0x8a, 0x35, 0x50, + 0x0d, 0xab, 0xab, 0xdb, 0x56, 0x25, 0x4f, 0x75, 0xbc, 0x1c, 0xa5, 0x63, 0x57, 0xb3, 0xec, 0x43, + 0x87, 0xb9, 0x19, 0x93, 0x8b, 0x3d, 0x3f, 0x81, 0xe8, 0xd3, 0x4f, 0x4f, 0xb1, 0xe9, 0x2a, 0xac, + 0x14, 0xa6, 0xeb, 0xdb, 0x27, 0xdc, 0x8e, 0x3c, 0xd1, 0xa7, 0xfb, 0x09, 0xe8, 0x13, 0x58, 0xee, + 0xe9, 0x6a, 0xc7, 0x55, 0xa7, 0xb4, 0xbb, 0xc3, 0xc1, 0x93, 0x4a, 0x91, 0x2a, 0x7d, 0x35, 0x72, + 0x90, 0xba, 0xda, 0x71, 0x54, 0xd4, 0x89, 0x40, 0x33, 0x26, 0x2f, 0xf5, 0xc2, 0x44, 0xf4, 0x19, + 0xac, 0xa8, 0x86, 0xd1, 0xbb, 0x08, 0x6b, 0x2f, 0x51, 0xed, 0x77, 0xa2, 0xb4, 0xd7, 0x88, 0x4c, + 0x58, 0x3d, 0x52, 0xc7, 0xa8, 0xa8, 0x05, 0xa2, 0x61, 0x62, 0x43, 0x35, 0xb1, 0x62, 0x98, 0xba, + 0xa1, 0x5b, 0x6a, 0xaf, 0x52, 0xa6, 0xba, 0x6f, 0x45, 0xe9, 0x3e, 0x60, 0xfc, 0x07, 0x9c, 0xbd, + 0x19, 0x93, 0xcb, 0x46, 0x90, 0xc4, 0xb4, 0xea, 0x6d, 0x6c, 0x59, 0x9e, 0x56, 0x71, 0x96, 0x56, + 0xca, 0x1f, 0xd4, 0x1a, 0x20, 0xa1, 0x06, 0xe4, 0xf1, 0x88, 0x88, 0x2b, 0xe7, 0xba, 0x8d, 0x2b, + 0x4b, 0x54, 0xa1, 0x14, 0x79, 0xce, 0x28, 0xeb, 0xb1, 0x6e, 0xe3, 0x66, 0x4c, 0x06, 0xec, 0xb6, + 0x90, 0x0a, 0x97, 0xce, 0xb1, 0xa9, 0x9d, 0x5e, 0x50, 0x35, 0x0a, 0xed, 0x21, 0xfe, 0xa0, 0x82, + 0xa8, 0xc2, 0xd7, 0xa2, 0x14, 0x1e, 0x53, 0x21, 0xa2, 0xa2, 0xe1, 0x88, 0x34, 0x63, 0xf2, 0xf2, + 0xf9, 0x38, 0x99, 0x98, 0xd8, 0xa9, 0x36, 0x50, 0x7b, 0xda, 0x97, 0x58, 0xa1, 0x0e, 0xae, 0xb2, 0x3c, 0xdd, 0xc4, 0x1e, 0x70, 0xee, 0x2d, 0xc2, 0x4c, 0x4c, 0xec, 0xd4, 0x4f, 0xd8, 0xca, 0x40, 0xea, 0x5c, 0xed, 0x0d, 0xf1, 0xc3, 0x64, 0x36, 0x2d, 0x66, 0x1e, 0x26, 0xb3, 0x59, 0x31, 0xf7, 0x30, 0x99, 0xcd, 0x89, 0xf0, 0x30, 0x99, 0x05, 0x31, 0x2f, 0xdd, 0x82, 0xbc, 0xcf, 0xbd, 0xa0, 0x0a, 0x64, 0xfa, 0xd8, 0xb2, 0xd4, 0x33, 0x4c, 0xbd, 0x51, 0x4e, 0x76, 0x9a, 0x52, 0x09, 0x0a, - 0x7e, 0x97, 0x22, 0x7d, 0x2d, 0xb8, 0x92, 0xc4, 0x5b, 0x10, 0x49, 0xee, 0x1e, 0x1d, 0x49, 0xde, + 0x7e, 0x97, 0x22, 0x7d, 0x23, 0xb8, 0x92, 0xc4, 0x5b, 0x10, 0x49, 0xee, 0x1e, 0x1d, 0x49, 0xde, 0x44, 0x37, 0xa1, 0x48, 0xa7, 0xa2, 0x38, 0xfd, 0xc4, 0x65, 0x25, 0xe5, 0x02, 0x25, 0x1e, 0x73, 0xa6, 0x35, 0xc8, 0x1b, 0x77, 0x0d, 0x97, 0x25, 0x41, 0x59, 0xc0, 0xb8, 0x6b, 0x38, 0x0c, 0x2f, - 0x41, 0x81, 0xcc, 0xdb, 0xe5, 0x48, 0xd2, 0x8f, 0xe4, 0x09, 0x8d, 0xb3, 0x48, 0xff, 0x95, 0x00, - 0x31, 0xec, 0x86, 0xd0, 0x3b, 0x90, 0x24, 0x1e, 0x99, 0x3b, 0xd7, 0xea, 0x06, 0x73, 0xd7, 0x1b, - 0x8e, 0xbb, 0xde, 0x68, 0x39, 0xee, 0x7a, 0x2b, 0xfb, 0xed, 0xb3, 0xb5, 0xd8, 0xd7, 0xbf, 0x5f, + 0x41, 0x81, 0xcc, 0xdb, 0xe5, 0x48, 0xd2, 0x8f, 0xe4, 0x09, 0x8d, 0xb3, 0x48, 0xff, 0x9a, 0x00, + 0x31, 0xec, 0x86, 0xd0, 0xbb, 0x90, 0x24, 0x1e, 0x99, 0x3b, 0xd7, 0xea, 0x06, 0x73, 0xd7, 0x1b, + 0x8e, 0xbb, 0xde, 0x68, 0x39, 0xee, 0x7a, 0x2b, 0xfb, 0xdd, 0xb3, 0xb5, 0xd8, 0x37, 0xbf, 0x5d, 0x13, 0x64, 0x2a, 0x81, 0xae, 0x12, 0xe7, 0xa3, 0x6a, 0x03, 0x45, 0xeb, 0xd0, 0x21, 0xe7, 0x88, 0x67, 0x51, 0xb5, 0xc1, 0x4e, 0x07, 0xed, 0x82, 0xd8, 0xd6, 0x07, 0x16, 0x1e, 0x58, 0x43, 0x4b, 0x61, 0xe1, 0x82, 0xbb, 0xd4, 0x80, 0x63, 0x64, 0x71, 0xa2, 0xee, 0x70, 0x1e, 0x50, 0x46, 0xb9, 0xdc, 0x0e, 0x12, 0xd0, 0x1e, 0x14, 0xcf, 0xd5, 0x9e, 0xd6, 0x51, 0x6d, 0xdd, 0x54, 0x2c, 0x6c, 0x73, 0x1f, 0x7b, 0x73, 0x6c, 0xcf, 0x8f, 0x1d, 0xae, 0x43, 0x6c, 0x1f, 0x19, 0x1d, 0xd5, 0xc6, - 0x5b, 0xc9, 0x6f, 0x9f, 0xad, 0x09, 0x72, 0xe1, 0xdc, 0xd7, 0x83, 0x5e, 0x81, 0xb2, 0x6a, 0x18, + 0x5b, 0xc9, 0xef, 0x9e, 0xad, 0x09, 0x72, 0xe1, 0xdc, 0xd7, 0x83, 0x5e, 0x81, 0xb2, 0x6a, 0x18, 0x8a, 0x65, 0xab, 0x36, 0x56, 0x4e, 0x2e, 0x6c, 0x6c, 0x51, 0xb7, 0x5b, 0x90, 0x8b, 0xaa, 0x61, 0x1c, 0x12, 0xea, 0x16, 0x21, 0xa2, 0x97, 0xa1, 0x44, 0x3c, 0xb4, 0xa6, 0xf6, 0x94, 0x2e, 0xd6, 0xce, 0xba, 0x76, 0x25, 0xbd, 0x2e, 0xdc, 0x4e, 0xc8, 0x45, 0x4e, 0x6d, 0x52, 0x22, 0xda, 0x80, @@ -4342,14 +4381,14 @@ var fileDescriptor_252557cfdd89a31a = []byte{ 0x50, 0x90, 0xe9, 0x6f, 0x42, 0x33, 0x54, 0xbb, 0xcb, 0xd7, 0x95, 0xfe, 0x46, 0x97, 0x21, 0xcd, 0x55, 0x27, 0xe8, 0x30, 0x78, 0x0b, 0xad, 0x40, 0xca, 0x30, 0xf5, 0x73, 0x4c, 0x97, 0x25, 0x2b, 0xb3, 0x86, 0x24, 0x43, 0x29, 0xe8, 0xf9, 0x51, 0x09, 0xe2, 0xf6, 0x88, 0x7f, 0x25, 0x6e, 0x8f, - 0xd0, 0x1b, 0x90, 0x24, 0x1b, 0x40, 0xbf, 0x51, 0x9a, 0x10, 0xeb, 0xb8, 0x5c, 0xeb, 0xc2, 0xc0, + 0xd0, 0x9b, 0x90, 0x24, 0x1b, 0x40, 0xbf, 0x51, 0x9a, 0x10, 0xeb, 0xb8, 0x5c, 0xeb, 0xc2, 0xc0, 0x32, 0xe5, 0x94, 0x2e, 0xc3, 0xca, 0xa4, 0x48, 0x20, 0x75, 0x5d, 0x7a, 0xc0, 0xa3, 0xa3, 0xb7, - 0x20, 0xeb, 0x86, 0x02, 0x66, 0x5f, 0x57, 0xc7, 0xbe, 0xe2, 0x30, 0xcb, 0x2e, 0x2b, 0x31, 0x2c, + 0x21, 0xeb, 0x86, 0x02, 0x66, 0x5f, 0x57, 0xc7, 0xbe, 0xe2, 0x30, 0xcb, 0x2e, 0x2b, 0x31, 0x2c, 0xb2, 0x3f, 0x5d, 0x95, 0x87, 0xef, 0x82, 0x9c, 0x51, 0x0d, 0xa3, 0xa9, 0x5a, 0x5d, 0xe9, 0x0c, 0x2a, 0x51, 0x6e, 0xde, 0xb7, 0x3e, 0x02, 0x3d, 0x1d, 0xce, 0xfa, 0xf8, 0x4e, 0x5e, 0x9c, 0xee, 0x89, 0x7b, 0xf2, 0xa8, 0x05, 0x0f, 0x07, 0x4f, 0x88, 0x05, 0x27, 0xd8, 0x87, 0x68, 0x7b, 0xa7, 0x23, 0x75, 0xe0, 0x6a, 0xa4, 0xc7, 0x0f, 0xc8, 0x09, 0x01, 0x39, 0xb2, 0x19, 0x2c, 0x8e, 0xb0, - 0x81, 0xb3, 0x06, 0x19, 0x9a, 0x45, 0xe7, 0x4d, 0x3f, 0x93, 0x93, 0x79, 0x4b, 0xfa, 0x53, 0x12, + 0x81, 0xb3, 0x06, 0x19, 0x9a, 0x45, 0xe7, 0x4d, 0x3f, 0x93, 0x93, 0x79, 0x4b, 0xfa, 0x43, 0x12, 0x2e, 0x4f, 0x76, 0xfe, 0x68, 0x1d, 0x0a, 0x7d, 0x75, 0xa4, 0xd8, 0x23, 0x6e, 0xa1, 0x02, 0xdd, 0x73, 0xe8, 0xab, 0xa3, 0xd6, 0x88, 0x99, 0xa7, 0x08, 0x09, 0x7b, 0x64, 0x55, 0xe2, 0xeb, 0x89, 0xdb, 0x05, 0x99, 0xfc, 0x44, 0x8f, 0x61, 0xa9, 0xa7, 0xb7, 0xd5, 0x9e, 0xd2, 0x53, 0x2d, 0x5b, @@ -4357,155 +4396,157 @@ var fileDescriptor_252557cfdd89a31a = []byte{ 0x90, 0xc4, 0xe4, 0x32, 0x95, 0xdd, 0x55, 0x2d, 0x9b, 0x75, 0xa1, 0x6d, 0xc8, 0xf7, 0x35, 0xeb, 0x04, 0x77, 0xd5, 0x73, 0x4d, 0x37, 0x2b, 0xc9, 0xf5, 0xc4, 0xc4, 0x9c, 0xe8, 0xb1, 0xc7, 0xc3, 0x35, 0xf9, 0xc5, 0x7c, 0xdb, 0x92, 0x0a, 0x98, 0xad, 0xe3, 0x78, 0xd2, 0x0b, 0x3b, 0x9e, 0x37, - 0x60, 0x65, 0x80, 0x47, 0xb6, 0xe2, 0x1e, 0x6a, 0x8b, 0xd9, 0x4a, 0x86, 0x2e, 0x39, 0x22, 0x7d, + 0x61, 0x65, 0x80, 0x47, 0xb6, 0xe2, 0x1e, 0x6a, 0x8b, 0xd9, 0x4a, 0x86, 0x2e, 0x39, 0x22, 0x7d, 0xae, 0x27, 0xb0, 0x88, 0xd9, 0x90, 0x5d, 0x31, 0xf5, 0xe1, 0xa0, 0x53, 0xc9, 0xae, 0x0b, 0xb7, 0x53, 0x32, 0x6b, 0xa0, 0xfb, 0x50, 0xa1, 0x07, 0x96, 0x79, 0x31, 0xe2, 0x6d, 0x71, 0xc7, 0x39, 0xbd, 0x39, 0x6a, 0x29, 0x97, 0x48, 0x3f, 0xf5, 0x93, 0xbb, 0xb4, 0x97, 0x9f, 0xf8, 0x4d, 0x58, 0x61, 0xd1, 0x17, 0x9b, 0x24, 0x0c, 0x93, 0x4d, 0xa2, 0x03, 0x00, 0x3a, 0x80, 0x25, 0xa7, 0xef, - 0xc0, 0xd4, 0x5b, 0x23, 0xfa, 0xfd, 0x37, 0x5c, 0x81, 0x8e, 0x42, 0x4c, 0xdb, 0xb1, 0xc7, 0x3c, + 0xc0, 0xd4, 0x5b, 0x23, 0xfa, 0xfd, 0x37, 0x5d, 0x81, 0x8e, 0x42, 0x4c, 0xdb, 0xb1, 0xc7, 0x3c, 0x35, 0x54, 0xe4, 0xf4, 0xd5, 0x0c, 0xd7, 0x9d, 0xdf, 0xf7, 0x8c, 0xb6, 0x30, 0x9e, 0x12, 0xf2, - 0x2e, 0xcf, 0x75, 0x7a, 0x36, 0xbd, 0x06, 0xf9, 0xcf, 0x87, 0xba, 0x39, 0xec, 0xb3, 0x21, 0x15, - 0xe9, 0x90, 0x80, 0x91, 0xe8, 0x11, 0xfa, 0x65, 0xca, 0x67, 0x73, 0xc1, 0x3c, 0x80, 0x5b, 0x94, - 0xe0, 0x59, 0xd4, 0xa1, 0x6f, 0xe0, 0x7e, 0xa3, 0x8a, 0xcf, 0x6b, 0x54, 0xee, 0xdc, 0xa2, 0xed, - 0x2a, 0xf1, 0xe3, 0xec, 0x0a, 0x41, 0x92, 0xce, 0x30, 0xc9, 0xdc, 0x26, 0xf9, 0x1d, 0x69, 0x6b, - 0xee, 0xfe, 0xa7, 0xfd, 0xfb, 0xef, 0x58, 0x60, 0xe6, 0x27, 0xb3, 0xc0, 0x6c, 0xa4, 0x05, 0xfe, - 0x68, 0x5b, 0x6b, 0xc1, 0xe5, 0x90, 0xa0, 0x32, 0xa4, 0xa1, 0x8d, 0x5a, 0x5b, 0x28, 0xe1, 0x77, - 0x02, 0xaa, 0x4f, 0x91, 0xbc, 0x1c, 0xd0, 0xcb, 0xc2, 0x62, 0xa4, 0x05, 0xe7, 0x17, 0xb5, 0xe0, - 0xc2, 0x3c, 0x16, 0x5c, 0x7c, 0x11, 0x0b, 0x2e, 0x8d, 0x59, 0xf0, 0x11, 0x2c, 0x8d, 0xa5, 0xa2, - 0xae, 0x39, 0x08, 0x13, 0xcd, 0x21, 0x3e, 0xd9, 0x1c, 0x12, 0x3e, 0x73, 0x90, 0xbe, 0x17, 0xa0, - 0x1a, 0x9d, 0x91, 0x4e, 0xfc, 0xc0, 0x9b, 0x70, 0xc9, 0xcb, 0x4c, 0xfc, 0xeb, 0xc8, 0xbc, 0x3f, - 0x72, 0x3b, 0xbd, 0x85, 0x9c, 0x12, 0xc5, 0xd9, 0x98, 0x92, 0x7e, 0x13, 0x7d, 0x0c, 0xe5, 0x60, - 0x2e, 0x4d, 0x52, 0x15, 0x72, 0x5c, 0xfe, 0x61, 0xec, 0xb8, 0x78, 0x6b, 0xe1, 0x8e, 0x59, 0x2e, - 0x9d, 0xfb, 0x9b, 0x96, 0xf4, 0x9b, 0xb8, 0x1b, 0xa9, 0x03, 0x89, 0x31, 0x7a, 0x17, 0xd2, 0xfc, - 0x64, 0x0b, 0xf3, 0x9e, 0x6c, 0x2e, 0x10, 0x3e, 0xcd, 0xf1, 0x17, 0x3b, 0xcd, 0x89, 0x89, 0xdb, - 0x97, 0x9c, 0xbc, 0x54, 0x29, 0xff, 0x52, 0xbd, 0x0e, 0x29, 0x76, 0x23, 0x60, 0x01, 0xe5, 0xca, - 0xf8, 0xb9, 0xa0, 0x53, 0x95, 0x19, 0x17, 0xaa, 0x41, 0x96, 0x65, 0xdd, 0x5a, 0x87, 0x3b, 0x80, - 0xab, 0x11, 0x12, 0x3b, 0xdb, 0x5b, 0xf9, 0xe7, 0xcf, 0xd6, 0x32, 0xbc, 0x21, 0x67, 0xa8, 0xdc, - 0x4e, 0x47, 0xfa, 0x55, 0x0e, 0xb2, 0x32, 0xb6, 0x0c, 0x62, 0xc2, 0x68, 0x0b, 0x72, 0x78, 0xd4, - 0xc6, 0x86, 0xed, 0x64, 0xf8, 0x93, 0x6f, 0x50, 0x8c, 0xbb, 0xe1, 0x70, 0x36, 0x63, 0xb2, 0x27, - 0x86, 0xee, 0x71, 0xa0, 0x23, 0x1a, 0xb3, 0xe0, 0xe2, 0x7e, 0xa4, 0xe3, 0x6d, 0x07, 0xe9, 0x60, - 0x81, 0x7e, 0x35, 0x52, 0x2a, 0x04, 0x75, 0xdc, 0xe3, 0x50, 0x47, 0x72, 0xc6, 0xc7, 0x02, 0x58, - 0x47, 0x3d, 0x80, 0x75, 0xa4, 0x66, 0x4c, 0x33, 0x02, 0xec, 0x78, 0xdb, 0x01, 0x3b, 0xd2, 0x33, - 0x46, 0x1c, 0x42, 0x3b, 0xfe, 0x65, 0x0c, 0xed, 0x58, 0x8f, 0x14, 0x9d, 0x00, 0x77, 0xec, 0x8f, - 0xc1, 0x1d, 0x59, 0xaa, 0xe4, 0x95, 0x48, 0x25, 0x33, 0xf0, 0x8e, 0xfd, 0x31, 0xbc, 0x23, 0x37, - 0x43, 0xe1, 0x0c, 0xc0, 0xe3, 0x3f, 0x26, 0x03, 0x1e, 0x10, 0x09, 0x49, 0xf0, 0x61, 0xce, 0x87, - 0x78, 0x28, 0x11, 0x88, 0x47, 0x3e, 0xf2, 0x76, 0xce, 0xd4, 0xcf, 0x0d, 0x79, 0x1c, 0x4d, 0x80, - 0x3c, 0x58, 0xf2, 0x72, 0x3b, 0x52, 0xf9, 0x1c, 0x98, 0xc7, 0xd1, 0x04, 0xcc, 0xa3, 0x38, 0x53, - 0xed, 0x4c, 0xd0, 0xe3, 0x41, 0x10, 0xf4, 0x28, 0x45, 0xdc, 0x29, 0xbd, 0x23, 0x1b, 0x81, 0x7a, - 0x9c, 0x44, 0xa1, 0x1e, 0x0c, 0xed, 0x79, 0x2d, 0x52, 0xe3, 0x02, 0xb0, 0xc7, 0xfe, 0x18, 0xec, - 0x21, 0xce, 0xb0, 0xb4, 0x39, 0x71, 0x0f, 0xe9, 0x55, 0x12, 0x4b, 0x43, 0x4e, 0x89, 0x38, 0x58, - 0x6c, 0x9a, 0xba, 0xc9, 0x91, 0x0a, 0xd6, 0x90, 0x6e, 0x93, 0x7b, 0xab, 0xe7, 0x80, 0xa6, 0x60, - 0x21, 0x65, 0x28, 0x06, 0x9c, 0x8e, 0xf4, 0x33, 0xc1, 0x93, 0xa5, 0x68, 0x88, 0xff, 0xce, 0x9b, - 0xe3, 0x77, 0xde, 0xd0, 0x3d, 0x2d, 0x17, 0xc8, 0x08, 0xfc, 0x39, 0x07, 0x07, 0x3f, 0x54, 0x2f, - 0xd7, 0xb8, 0x03, 0x4b, 0x34, 0x3b, 0x65, 0x1e, 0x3d, 0x10, 0x34, 0xca, 0xa4, 0x83, 0xad, 0x02, - 0x8b, 0x1e, 0xaf, 0xc3, 0xb2, 0x8f, 0xd7, 0xbd, 0x68, 0x32, 0x04, 0x40, 0x74, 0xb9, 0x6b, 0xfc, - 0xc6, 0xf9, 0x87, 0xb8, 0xb7, 0x42, 0x1e, 0x6a, 0x32, 0x09, 0xe0, 0x10, 0x7e, 0x34, 0xc0, 0x11, - 0x7d, 0xe1, 0x45, 0x1f, 0xc3, 0x4a, 0x00, 0xfb, 0x70, 0x92, 0xbf, 0xc4, 0x62, 0x10, 0x48, 0xcc, - 0x97, 0x8b, 0xb8, 0x3d, 0xe8, 0x13, 0xb8, 0x46, 0xd3, 0xd8, 0x88, 0x04, 0x33, 0x39, 0x5f, 0x82, - 0x79, 0x85, 0xe8, 0xa8, 0x4f, 0x48, 0x32, 0x23, 0x80, 0x91, 0x54, 0x14, 0x30, 0xf2, 0x67, 0xc1, - 0xb3, 0x1b, 0x17, 0x1a, 0x69, 0xeb, 0x1d, 0x66, 0x5f, 0x45, 0x99, 0xfe, 0x26, 0x97, 0x94, 0x9e, - 0x7e, 0xc6, 0x4d, 0x84, 0xfc, 0x24, 0x5c, 0x2e, 0x68, 0x9f, 0xe3, 0x81, 0x6a, 0x05, 0x52, 0xda, - 0xa0, 0x83, 0x47, 0xdc, 0x0a, 0x58, 0x83, 0xc8, 0x3e, 0xc1, 0x17, 0x7c, 0xaf, 0xc9, 0x4f, 0xc2, - 0x47, 0x0f, 0x02, 0x8d, 0x45, 0x05, 0x99, 0x35, 0xd0, 0x3b, 0x90, 0xa3, 0x95, 0x17, 0x45, 0x37, - 0x2c, 0x1e, 0x6a, 0x02, 0x19, 0x11, 0xab, 0x92, 0x6c, 0x1c, 0x10, 0x9e, 0x7d, 0xc3, 0x92, 0xb3, - 0x06, 0xff, 0xe5, 0xcb, 0x59, 0xb2, 0x81, 0x9c, 0xe5, 0x3a, 0xe4, 0xc8, 0xe8, 0x2d, 0x43, 0x6d, - 0x63, 0x1a, 0x26, 0x72, 0xb2, 0x47, 0x90, 0x7e, 0x21, 0x40, 0x39, 0x14, 0xb9, 0x26, 0xce, 0xdd, - 0x39, 0x36, 0xf1, 0x20, 0x54, 0x34, 0x36, 0xfb, 0x1b, 0x00, 0x67, 0xaa, 0xa5, 0x3c, 0x55, 0x07, - 0x36, 0xee, 0xf0, 0x25, 0xc8, 0x9d, 0xa9, 0xd6, 0x87, 0x94, 0x10, 0x1c, 0x4c, 0x2a, 0x34, 0x18, - 0x1f, 0x58, 0x91, 0xf6, 0x83, 0x15, 0xa8, 0x0a, 0x59, 0xc3, 0xd4, 0x74, 0x53, 0xb3, 0x2f, 0xe8, - 0x9a, 0x24, 0x64, 0xb7, 0x2d, 0x1d, 0xc0, 0xa5, 0x89, 0x41, 0x13, 0xdd, 0x87, 0x9c, 0x17, 0x6f, - 0x05, 0x9a, 0x1b, 0x4e, 0xc1, 0x80, 0x3c, 0x5e, 0xb2, 0x24, 0x97, 0x26, 0x86, 0x4d, 0xd4, 0x80, - 0xb4, 0x89, 0xad, 0x61, 0x8f, 0xe5, 0xaa, 0xa5, 0xbb, 0xaf, 0xcf, 0x17, 0x6e, 0x09, 0x75, 0xd8, - 0xb3, 0x65, 0x2e, 0x2c, 0x7d, 0x0a, 0x69, 0x46, 0x41, 0x79, 0xc8, 0x1c, 0xed, 0x3d, 0xda, 0xdb, - 0xff, 0x70, 0x4f, 0x8c, 0x21, 0x80, 0x74, 0xad, 0x5e, 0x6f, 0x1c, 0xb4, 0x44, 0x01, 0xe5, 0x20, - 0x55, 0xdb, 0xda, 0x97, 0x5b, 0x62, 0x9c, 0x90, 0xe5, 0xc6, 0xc3, 0x46, 0xbd, 0x25, 0x26, 0xd0, - 0x12, 0x14, 0xd9, 0x6f, 0xe5, 0xc1, 0xbe, 0xfc, 0xb8, 0xd6, 0x12, 0x93, 0x3e, 0xd2, 0x61, 0x63, - 0x6f, 0xbb, 0x21, 0x8b, 0x29, 0xe9, 0x4d, 0xb8, 0x1a, 0x19, 0xa0, 0x3d, 0x98, 0x48, 0xf0, 0xc1, - 0x44, 0xd2, 0x77, 0x71, 0x72, 0x03, 0x89, 0x8a, 0xba, 0xe8, 0x61, 0x68, 0xe2, 0x77, 0x17, 0x08, - 0xd9, 0xa1, 0xd9, 0xa3, 0x97, 0xa1, 0x64, 0xe2, 0x53, 0x6c, 0xb7, 0xbb, 0x2c, 0x0b, 0x70, 0x70, - 0xa4, 0x22, 0xa7, 0x52, 0x21, 0x8b, 0xb1, 0x7d, 0x86, 0xdb, 0xb6, 0xc2, 0x8c, 0xc0, 0xa2, 0xb7, - 0xf5, 0x1c, 0x61, 0x23, 0xd4, 0x43, 0x46, 0x24, 0x0e, 0x9a, 0x39, 0x12, 0xa6, 0x2a, 0x49, 0x55, - 0x01, 0xf5, 0x0b, 0x94, 0x22, 0x3d, 0x5d, 0x68, 0xb1, 0x73, 0x90, 0x92, 0x1b, 0x2d, 0xf9, 0x23, - 0x31, 0x81, 0x10, 0x94, 0xe8, 0x4f, 0xe5, 0x70, 0xaf, 0x76, 0x70, 0xd8, 0xdc, 0x27, 0x8b, 0xbd, - 0x0c, 0x65, 0x67, 0xb1, 0x1d, 0x62, 0x0a, 0x5d, 0x82, 0xa5, 0xfa, 0xfe, 0xe3, 0x83, 0xdd, 0x46, - 0xab, 0xe1, 0x91, 0xd3, 0xd2, 0xcf, 0x13, 0x70, 0x25, 0x22, 0xd7, 0x40, 0xef, 0x00, 0xd8, 0x23, - 0xc5, 0xc4, 0x6d, 0xdd, 0xec, 0x44, 0x1b, 0x67, 0x6b, 0x24, 0x53, 0x0e, 0x39, 0x67, 0xf3, 0x5f, - 0x53, 0x1d, 0xf6, 0xfb, 0x5c, 0x29, 0x99, 0xac, 0xc5, 0xb1, 0x8d, 0x1b, 0x13, 0x2e, 0x6b, 0xb8, - 0x4d, 0x14, 0xd3, 0x3d, 0xa1, 0x8a, 0x29, 0x3f, 0xfa, 0x08, 0xae, 0x84, 0xe2, 0x0a, 0x77, 0xc6, - 0xd6, 0xa4, 0xc2, 0xe2, 0xe4, 0xf0, 0x72, 0x29, 0x18, 0x5e, 0x98, 0x33, 0xb6, 0xa6, 0x00, 0x09, - 0xa9, 0x17, 0x00, 0x12, 0xa2, 0xe2, 0x53, 0x7a, 0x51, 0x88, 0x7e, 0x42, 0x7c, 0x92, 0xfe, 0x3f, - 0xb0, 0x79, 0xc1, 0xf4, 0x6d, 0x1f, 0xd2, 0x96, 0xad, 0xda, 0x43, 0x8b, 0x1f, 0x86, 0xfb, 0xf3, - 0xe6, 0x82, 0x1b, 0xce, 0x8f, 0x43, 0x2a, 0x2e, 0x73, 0x35, 0x7f, 0x97, 0x7b, 0x1a, 0xb5, 0xfa, - 0xa9, 0x9f, 0x62, 0xf5, 0xdf, 0x82, 0x52, 0x70, 0xa9, 0xa2, 0xcf, 0xae, 0xe7, 0x1d, 0xe3, 0x52, - 0x0f, 0x96, 0x27, 0x40, 0x11, 0xe8, 0x3e, 0xaf, 0x36, 0xb0, 0xdd, 0xba, 0x39, 0x3e, 0xe5, 0x00, - 0xbb, 0x57, 0x74, 0x20, 0xc1, 0xca, 0xcb, 0xa9, 0xd9, 0xc6, 0x78, 0x04, 0xa9, 0x0d, 0x68, 0x3c, - 0x43, 0x9f, 0x04, 0x9b, 0x08, 0x2f, 0x00, 0x9b, 0xfc, 0xaf, 0x00, 0xd7, 0xa6, 0x64, 0xed, 0xe8, - 0x83, 0x90, 0x2d, 0xbe, 0xbb, 0x48, 0xce, 0xbf, 0xc1, 0x68, 0x41, 0x6b, 0x94, 0xee, 0x41, 0xc1, - 0x4f, 0x9f, 0x6f, 0xe9, 0xff, 0xdb, 0x17, 0x33, 0x83, 0xf8, 0x4e, 0x13, 0xd2, 0xf8, 0x1c, 0x0f, - 0xdc, 0x18, 0x7c, 0x79, 0x7c, 0x1d, 0x48, 0xf7, 0x56, 0x85, 0xe4, 0x8a, 0x7f, 0x7c, 0xb6, 0x26, - 0x32, 0xee, 0xd7, 0xf4, 0xbe, 0x66, 0xe3, 0xbe, 0x61, 0x5f, 0xc8, 0x5c, 0x1e, 0xdd, 0x84, 0xa2, - 0x89, 0x6d, 0xe2, 0x42, 0x02, 0xd0, 0x5a, 0x81, 0x11, 0x79, 0x26, 0xf7, 0x6b, 0x01, 0xc0, 0x03, - 0x8c, 0x3c, 0xc0, 0x46, 0xf0, 0x03, 0x36, 0x21, 0x9c, 0x2f, 0x1e, 0xc6, 0xf9, 0xd0, 0x2d, 0x28, - 0xb3, 0x24, 0xdd, 0xd2, 0xce, 0x06, 0xaa, 0x3d, 0x34, 0x31, 0x87, 0x87, 0x4a, 0x94, 0x7c, 0xe8, - 0x50, 0xd1, 0xc7, 0x70, 0xd5, 0xee, 0x9a, 0xd8, 0xea, 0xea, 0xbd, 0x8e, 0x12, 0xde, 0x78, 0x56, - 0xb6, 0x58, 0x9b, 0x61, 0x70, 0xf2, 0x15, 0x57, 0xc3, 0x71, 0x70, 0xf3, 0xbf, 0x80, 0x14, 0x5d, - 0x1b, 0x92, 0x68, 0xb9, 0x16, 0x9c, 0xe3, 0xc6, 0xf9, 0x09, 0x80, 0x6a, 0xdb, 0xa6, 0x76, 0x32, - 0x24, 0xc7, 0x39, 0x3e, 0xfe, 0x29, 0x6f, 0x6d, 0x6b, 0x0e, 0xdf, 0xd6, 0x75, 0xbe, 0xc8, 0x2b, - 0x9e, 0xa8, 0x6f, 0xa1, 0x7d, 0x0a, 0xa5, 0x3d, 0x28, 0x05, 0x65, 0x9d, 0x0c, 0x96, 0x8d, 0x21, - 0x98, 0xc1, 0xb2, 0x8c, 0x98, 0x67, 0xb0, 0x6e, 0xfe, 0x9b, 0x60, 0x45, 0x41, 0xda, 0x90, 0xfe, - 0x22, 0x40, 0xc1, 0xef, 0xa6, 0xe6, 0x4e, 0x32, 0x79, 0xd2, 0x9d, 0x18, 0x4f, 0xba, 0x93, 0x91, - 0x69, 0x67, 0x2a, 0x9c, 0x76, 0x5e, 0x85, 0x2c, 0xe9, 0x1e, 0x5a, 0xb8, 0xc3, 0x2b, 0xa9, 0x99, - 0x33, 0xd5, 0x3a, 0xb2, 0x70, 0xc7, 0x67, 0x9f, 0x99, 0x17, 0xb4, 0xcf, 0x40, 0x6e, 0x9b, 0x0d, - 0x27, 0xda, 0x5f, 0x0a, 0x90, 0x75, 0x27, 0x1f, 0x2c, 0x18, 0x06, 0xf0, 0x45, 0xb6, 0x76, 0xac, - 0x5c, 0xc8, 0xef, 0x0e, 0xac, 0x7c, 0x9a, 0x70, 0xcb, 0xa7, 0xef, 0xb9, 0xd9, 0x58, 0x14, 0x82, - 0xe6, 0x5f, 0x69, 0x07, 0x34, 0xe5, 0xc9, 0xe7, 0xff, 0xf0, 0x71, 0x90, 0x74, 0x02, 0xfd, 0x33, - 0xa4, 0xd5, 0xb6, 0x8b, 0x1b, 0x96, 0x26, 0x00, 0x6a, 0x0e, 0xeb, 0x46, 0x6b, 0x54, 0xa3, 0x9c, - 0x32, 0x97, 0xe0, 0xa3, 0x8a, 0x3b, 0xa3, 0x92, 0x76, 0x89, 0x5e, 0xc6, 0x13, 0xf4, 0x19, 0x25, - 0x80, 0xa3, 0xbd, 0xc7, 0xfb, 0xdb, 0x3b, 0x0f, 0x76, 0x1a, 0xdb, 0x3c, 0xdd, 0xda, 0xde, 0x6e, - 0x6c, 0x8b, 0x71, 0xc2, 0x27, 0x37, 0x1e, 0xef, 0x1f, 0x37, 0xb6, 0xc5, 0x04, 0x69, 0x6c, 0x37, - 0x76, 0x6b, 0x1f, 0x35, 0xb6, 0xc5, 0xa4, 0x54, 0x83, 0x9c, 0x1b, 0x32, 0x68, 0x9d, 0x59, 0x7f, - 0x8a, 0x4d, 0xbe, 0x5a, 0xac, 0x81, 0x56, 0x21, 0x3f, 0x0e, 0x7c, 0x93, 0xdb, 0x13, 0xc3, 0xbb, - 0xa5, 0xff, 0x13, 0xa0, 0xec, 0xea, 0xe0, 0x49, 0xc3, 0x7b, 0x90, 0x31, 0x86, 0x27, 0x8a, 0x63, - 0xc8, 0x21, 0xb8, 0xd8, 0xb9, 0x5c, 0x0d, 0x4f, 0x7a, 0x5a, 0xfb, 0x11, 0xbe, 0xe0, 0x21, 0x2a, - 0x6d, 0x0c, 0x4f, 0x1e, 0x31, 0x7b, 0x67, 0xc3, 0x88, 0x4f, 0x19, 0x46, 0x22, 0x34, 0x0c, 0x74, - 0x0b, 0x0a, 0x03, 0xbd, 0x83, 0x15, 0xb5, 0xd3, 0x31, 0xb1, 0xc5, 0x22, 0x6f, 0x8e, 0x6b, 0xce, - 0x93, 0x9e, 0x1a, 0xeb, 0x90, 0xbe, 0x17, 0x00, 0x8d, 0x87, 0x49, 0x74, 0x08, 0x4b, 0x5e, 0xa4, - 0x75, 0xc2, 0x37, 0xf3, 0xa5, 0xeb, 0xd1, 0x61, 0x36, 0x70, 0x03, 0x17, 0xcf, 0x83, 0x64, 0x92, - 0x92, 0xad, 0x78, 0x7e, 0xcb, 0xa0, 0xf3, 0xa5, 0x8b, 0x12, 0x9f, 0x73, 0x51, 0x62, 0x32, 0x72, - 0xe5, 0xdd, 0x9e, 0xb0, 0x5f, 0x4d, 0x8c, 0xd5, 0x4f, 0x0c, 0xa8, 0xb4, 0xc6, 0xc4, 0xf8, 0x3c, - 0xa3, 0x86, 0x24, 0xbc, 0xc8, 0x90, 0xa4, 0x7b, 0x20, 0x7e, 0xe0, 0x7e, 0x9f, 0x7f, 0x29, 0x34, - 0x4c, 0x61, 0x6c, 0x98, 0xe7, 0x90, 0x25, 0xae, 0x98, 0x46, 0x90, 0x7f, 0x85, 0x9c, 0xbb, 0x7a, - 0xee, 0x53, 0x95, 0xc8, 0x65, 0xe7, 0x23, 0xf1, 0x44, 0xd0, 0x1d, 0x58, 0x22, 0x41, 0xc4, 0xa9, - 0x62, 0x32, 0x0c, 0x2d, 0x4e, 0x5d, 0x63, 0x99, 0x75, 0xec, 0x3a, 0xc0, 0x0f, 0x89, 0xf6, 0x22, - 0xcb, 0x0a, 0x70, 0xe7, 0x6f, 0x31, 0x00, 0x72, 0xe9, 0x0a, 0x41, 0x89, 0x6c, 0x0f, 0x8b, 0x81, - 0xb4, 0x44, 0xfa, 0xcf, 0x38, 0xe4, 0x7d, 0x55, 0x15, 0xf4, 0x4f, 0x81, 0x0c, 0x6b, 0x7d, 0x5a, - 0x05, 0xc6, 0x97, 0x5e, 0x05, 0x26, 0x16, 0x5f, 0x7c, 0x62, 0x51, 0xf5, 0x2c, 0xa7, 0xb8, 0x9a, - 0x5c, 0xb8, 0xb8, 0xfa, 0x1a, 0x20, 0x5b, 0xb7, 0xd5, 0x1e, 0x89, 0xe4, 0xda, 0xe0, 0x4c, 0x61, - 0xa7, 0x9d, 0x45, 0x13, 0x91, 0xf6, 0x1c, 0xd3, 0x8e, 0x03, 0x42, 0x97, 0x7a, 0x90, 0x75, 0x91, - 0x81, 0xc5, 0x5f, 0x80, 0x4c, 0x2a, 0x22, 0x57, 0x21, 0xdb, 0xc7, 0xb6, 0x4a, 0x63, 0x20, 0x43, - 0x8a, 0xdc, 0xf6, 0x9d, 0x77, 0x21, 0xef, 0x7b, 0x16, 0x43, 0xc2, 0xe2, 0x5e, 0xe3, 0x43, 0x31, - 0x56, 0xcd, 0x7c, 0xf5, 0xcd, 0x7a, 0x62, 0x0f, 0x3f, 0x25, 0x9f, 0x92, 0x1b, 0xf5, 0x66, 0xa3, - 0xfe, 0x48, 0x14, 0xaa, 0xf9, 0xaf, 0xbe, 0x59, 0xcf, 0xc8, 0x98, 0x16, 0x20, 0xee, 0x3c, 0x82, - 0x72, 0x68, 0x07, 0x82, 0x0e, 0x1a, 0x41, 0x69, 0xfb, 0xe8, 0x60, 0x77, 0xa7, 0x5e, 0x6b, 0x35, - 0x94, 0xe3, 0xfd, 0x56, 0x43, 0x14, 0xd0, 0x15, 0x58, 0xde, 0xdd, 0xf9, 0xb7, 0x66, 0x4b, 0xa9, - 0xef, 0xee, 0x34, 0xf6, 0x5a, 0x4a, 0xad, 0xd5, 0xaa, 0xd5, 0x1f, 0x89, 0xf1, 0xbb, 0xbf, 0x03, - 0x28, 0xd7, 0xb6, 0xea, 0x3b, 0xe4, 0xa2, 0xaf, 0xb5, 0x55, 0xea, 0xee, 0xeb, 0x90, 0xa4, 0xb8, - 0xec, 0xd4, 0x07, 0xb2, 0xd5, 0xe9, 0x55, 0x25, 0xf4, 0x00, 0x52, 0x14, 0xb2, 0x45, 0xd3, 0x5f, - 0xcc, 0x56, 0x67, 0x94, 0x99, 0xc8, 0x60, 0xe8, 0xb9, 0x99, 0xfa, 0x84, 0xb6, 0x3a, 0xbd, 0xea, - 0x84, 0x76, 0x21, 0xe3, 0xa0, 0x61, 0xb3, 0xde, 0xb5, 0x56, 0x67, 0x96, 0x82, 0xc8, 0xd4, 0x18, - 0xaa, 0x38, 0xfd, 0x75, 0x6d, 0x75, 0x46, 0x3d, 0x0a, 0xc9, 0x90, 0xf3, 0x80, 0xe0, 0xd9, 0x0f, - 0x7d, 0xab, 0x73, 0xd4, 0xc7, 0xd0, 0xa7, 0x50, 0x0c, 0xe2, 0x66, 0xf3, 0xbd, 0xc1, 0xad, 0xce, - 0x59, 0xbb, 0x22, 0xfa, 0x83, 0x20, 0xda, 0x7c, 0x6f, 0x72, 0xab, 0x73, 0x96, 0xb2, 0xd0, 0x67, - 0xb0, 0x34, 0x0e, 0x72, 0xcd, 0xff, 0x44, 0xb7, 0xba, 0x40, 0x71, 0x0b, 0xf5, 0x01, 0x4d, 0x00, - 0xc7, 0x16, 0x78, 0xb1, 0x5b, 0x5d, 0xa4, 0xd6, 0x85, 0x3a, 0x50, 0x0e, 0x03, 0x47, 0xf3, 0xbe, - 0xe0, 0xad, 0xce, 0x5d, 0xf7, 0x62, 0x5f, 0x09, 0x22, 0x1c, 0xf3, 0xbe, 0xe8, 0xad, 0xce, 0x5d, - 0x06, 0x43, 0x47, 0x00, 0xbe, 0x5b, 0xf2, 0x1c, 0x2f, 0x7c, 0xab, 0xf3, 0x14, 0xc4, 0x90, 0x01, - 0xcb, 0x93, 0xae, 0xc5, 0x8b, 0x3c, 0xf8, 0xad, 0x2e, 0x54, 0x27, 0x23, 0xf6, 0x1c, 0xbc, 0xe0, - 0xce, 0xf7, 0x00, 0xb8, 0x3a, 0x67, 0xc1, 0x6c, 0x6b, 0xeb, 0xdb, 0xe7, 0xab, 0xc2, 0x77, 0xcf, - 0x57, 0x85, 0xef, 0x9f, 0xaf, 0x0a, 0x5f, 0xff, 0xb0, 0x1a, 0xfb, 0xee, 0x87, 0xd5, 0xd8, 0x6f, - 0x7f, 0x58, 0x8d, 0xfd, 0xfb, 0xed, 0x33, 0xcd, 0xee, 0x0e, 0x4f, 0x36, 0xda, 0x7a, 0x9f, 0xfe, - 0xff, 0xc2, 0x50, 0x2f, 0x36, 0x99, 0x4e, 0xd2, 0xf2, 0xfd, 0xcb, 0xe3, 0x24, 0x4d, 0x63, 0xdd, - 0xbd, 0xbf, 0x06, 0x00, 0x00, 0xff, 0xff, 0x2b, 0x73, 0xfa, 0xad, 0x05, 0x32, 0x00, 0x00, + 0x2e, 0xcf, 0x75, 0x7a, 0x36, 0xbd, 0x06, 0xf9, 0x2f, 0x86, 0xba, 0x39, 0xec, 0xb3, 0x21, 0x15, + 0xe9, 0x90, 0x80, 0x91, 0xe8, 0x11, 0xfa, 0xff, 0x94, 0xcf, 0xe6, 0x82, 0x79, 0x00, 0xb7, 0x28, + 0xc1, 0xb3, 0xa8, 0x43, 0xdf, 0xc0, 0xfd, 0x46, 0x15, 0x9f, 0xd7, 0xa8, 0xdc, 0xb9, 0x45, 0xdb, + 0x55, 0xe2, 0xa7, 0xd9, 0x15, 0x82, 0x24, 0x9d, 0x61, 0x92, 0xb9, 0x4d, 0xf2, 0x3b, 0xd2, 0xd6, + 0xdc, 0xfd, 0x4f, 0xfb, 0xf7, 0xdf, 0xb1, 0xc0, 0xcc, 0xcf, 0x66, 0x81, 0xd9, 0x48, 0x0b, 0xfc, + 0xc9, 0xb6, 0xd6, 0x82, 0xcb, 0x21, 0x41, 0x65, 0x48, 0x43, 0x1b, 0xb5, 0xb6, 0x50, 0xc2, 0xef, + 0x04, 0x54, 0x9f, 0x22, 0x79, 0x39, 0xa0, 0x97, 0x85, 0xc5, 0x48, 0x0b, 0xce, 0x2f, 0x6a, 0xc1, + 0x85, 0x79, 0x2c, 0xb8, 0xf8, 0x22, 0x16, 0x5c, 0x1a, 0xb3, 0xe0, 0x23, 0x58, 0x1a, 0x4b, 0x45, + 0x5d, 0x73, 0x10, 0x26, 0x9a, 0x43, 0x7c, 0xb2, 0x39, 0x24, 0x7c, 0xe6, 0x20, 0xfd, 0x20, 0x40, + 0x35, 0x3a, 0x23, 0x9d, 0xf8, 0x81, 0xb7, 0xe0, 0x92, 0x97, 0x99, 0xf8, 0xd7, 0x91, 0x79, 0x7f, + 0xe4, 0x76, 0x7a, 0x0b, 0x39, 0x25, 0x8a, 0xb3, 0x31, 0x25, 0xfd, 0x26, 0xfa, 0x18, 0xca, 0xc1, + 0x5c, 0x9a, 0xa4, 0x2a, 0xe4, 0xb8, 0xfc, 0xcd, 0xd8, 0x71, 0xf1, 0xd6, 0xc2, 0x1d, 0xb3, 0x5c, + 0x3a, 0xf7, 0x37, 0x2d, 0xe9, 0x57, 0x71, 0x37, 0x52, 0x07, 0x12, 0x63, 0xf4, 0x1e, 0xa4, 0xf9, + 0xc9, 0x16, 0xe6, 0x3d, 0xd9, 0x5c, 0x20, 0x7c, 0x9a, 0xe3, 0x2f, 0x76, 0x9a, 0x13, 0x13, 0xb7, + 0x2f, 0x39, 0x79, 0xa9, 0x52, 0xfe, 0xa5, 0x7a, 0x03, 0x52, 0xec, 0x46, 0xc0, 0x02, 0xca, 0x95, + 0xf1, 0x73, 0x41, 0xa7, 0x2a, 0x33, 0x2e, 0x54, 0x83, 0x2c, 0xcb, 0xba, 0xb5, 0x0e, 0x77, 0x00, + 0x57, 0x23, 0x24, 0x76, 0xb6, 0xb7, 0xf2, 0xcf, 0x9f, 0xad, 0x65, 0x78, 0x43, 0xce, 0x50, 0xb9, + 0x9d, 0x8e, 0xf4, 0x8b, 0x1c, 0x64, 0x65, 0x6c, 0x19, 0xc4, 0x84, 0xd1, 0x16, 0xe4, 0xf0, 0xa8, + 0x8d, 0x0d, 0xdb, 0xc9, 0xf0, 0x27, 0xdf, 0xa0, 0x18, 0x77, 0xc3, 0xe1, 0x6c, 0xc6, 0x64, 0x4f, + 0x0c, 0xdd, 0xe3, 0x40, 0x47, 0x34, 0x66, 0xc1, 0xc5, 0xfd, 0x48, 0xc7, 0x3b, 0x0e, 0xd2, 0xc1, + 0x02, 0xfd, 0x6a, 0xa4, 0x54, 0x08, 0xea, 0xb8, 0xc7, 0xa1, 0x8e, 0xe4, 0x8c, 0x8f, 0x05, 0xb0, + 0x8e, 0x7a, 0x00, 0xeb, 0x48, 0xcd, 0x98, 0x66, 0x04, 0xd8, 0xf1, 0x8e, 0x03, 0x76, 0xa4, 0x67, + 0x8c, 0x38, 0x84, 0x76, 0xfc, 0xfd, 0x18, 0xda, 0xb1, 0x1e, 0x29, 0x3a, 0x01, 0xee, 0xd8, 0x1f, + 0x83, 0x3b, 0xb2, 0x54, 0xc9, 0x2b, 0x91, 0x4a, 0x66, 0xe0, 0x1d, 0xfb, 0x63, 0x78, 0x47, 0x6e, + 0x86, 0xc2, 0x19, 0x80, 0xc7, 0x3f, 0x4f, 0x06, 0x3c, 0x20, 0x12, 0x92, 0xe0, 0xc3, 0x9c, 0x0f, + 0xf1, 0x50, 0x22, 0x10, 0x8f, 0x7c, 0xe4, 0xed, 0x9c, 0xa9, 0x9f, 0x1b, 0xf2, 0x38, 0x9a, 0x00, + 0x79, 0xb0, 0xe4, 0xe5, 0x76, 0xa4, 0xf2, 0x39, 0x30, 0x8f, 0xa3, 0x09, 0x98, 0x47, 0x71, 0xa6, + 0xda, 0x99, 0xa0, 0xc7, 0x83, 0x20, 0xe8, 0x51, 0x8a, 0xb8, 0x53, 0x7a, 0x47, 0x36, 0x02, 0xf5, + 0x38, 0x89, 0x42, 0x3d, 0x18, 0xda, 0xf3, 0x7a, 0xa4, 0xc6, 0x05, 0x60, 0x8f, 0xfd, 0x31, 0xd8, + 0x43, 0x9c, 0x61, 0x69, 0x73, 0xe2, 0x1e, 0xd2, 0xab, 0x24, 0x96, 0x86, 0x9c, 0x12, 0x71, 0xb0, + 0xd8, 0x34, 0x75, 0x93, 0x23, 0x15, 0xac, 0x21, 0xdd, 0x26, 0xf7, 0x56, 0xcf, 0x01, 0x4d, 0xc1, + 0x42, 0xca, 0x50, 0x0c, 0x38, 0x1d, 0xe9, 0x7f, 0x04, 0x4f, 0x96, 0xa2, 0x21, 0xfe, 0x3b, 0x6f, + 0x8e, 0xdf, 0x79, 0x43, 0xf7, 0xb4, 0x5c, 0x20, 0x23, 0xf0, 0xe7, 0x1c, 0x1c, 0xfc, 0x50, 0xbd, + 0x5c, 0xe3, 0x0e, 0x2c, 0xd1, 0xec, 0x94, 0x79, 0xf4, 0x40, 0xd0, 0x28, 0x93, 0x0e, 0xb6, 0x0a, + 0x2c, 0x7a, 0xbc, 0x01, 0xcb, 0x3e, 0x5e, 0xf7, 0xa2, 0xc9, 0x10, 0x00, 0xd1, 0xe5, 0xae, 0xf1, + 0x1b, 0xe7, 0xef, 0xe2, 0xde, 0x0a, 0x79, 0xa8, 0xc9, 0x24, 0x80, 0x43, 0xf8, 0xc9, 0x00, 0x47, + 0xf4, 0x85, 0x17, 0x7d, 0x02, 0x2b, 0x01, 0xec, 0xc3, 0x49, 0xfe, 0x12, 0x8b, 0x41, 0x20, 0x31, + 0x5f, 0x2e, 0xe2, 0xf6, 0xa0, 0x4f, 0xe1, 0x1a, 0x4d, 0x63, 0x23, 0x12, 0xcc, 0xe4, 0x7c, 0x09, + 0xe6, 0x15, 0xa2, 0xa3, 0x3e, 0x21, 0xc9, 0x8c, 0x00, 0x46, 0x52, 0x51, 0xc0, 0xc8, 0x1f, 0x05, + 0xcf, 0x6e, 0x5c, 0x68, 0xa4, 0xad, 0x77, 0x98, 0x7d, 0x15, 0x65, 0xfa, 0x9b, 0x5c, 0x52, 0x7a, + 0xfa, 0x19, 0x37, 0x11, 0xf2, 0x93, 0x70, 0xb9, 0xa0, 0x7d, 0x8e, 0x07, 0xaa, 0x15, 0x48, 0x69, + 0x83, 0x0e, 0x1e, 0x71, 0x2b, 0x60, 0x0d, 0x22, 0xfb, 0x04, 0x5f, 0xf0, 0xbd, 0x26, 0x3f, 0x09, + 0x1f, 0x3d, 0x08, 0x34, 0x16, 0x15, 0x64, 0xd6, 0x40, 0xef, 0x42, 0x8e, 0x56, 0x5e, 0x14, 0xdd, + 0xb0, 0x78, 0xa8, 0x09, 0x64, 0x44, 0xac, 0x4a, 0xb2, 0x71, 0x40, 0x78, 0xf6, 0x0d, 0x4b, 0xce, + 0x1a, 0xfc, 0x97, 0x2f, 0x67, 0xc9, 0x06, 0x72, 0x96, 0xeb, 0x90, 0x23, 0xa3, 0xb7, 0x0c, 0xb5, + 0x8d, 0x69, 0x98, 0xc8, 0xc9, 0x1e, 0x41, 0xfa, 0x3f, 0x01, 0xca, 0xa1, 0xc8, 0x35, 0x71, 0xee, + 0xce, 0xb1, 0x89, 0x07, 0xa1, 0xa2, 0xb1, 0xd9, 0xdf, 0x00, 0x38, 0x53, 0x2d, 0xe5, 0xa9, 0x3a, + 0xb0, 0x71, 0x87, 0x2f, 0x41, 0xee, 0x4c, 0xb5, 0x3e, 0xa2, 0x84, 0xe0, 0x60, 0x52, 0xa1, 0xc1, + 0xf8, 0xc0, 0x8a, 0xb4, 0x1f, 0xac, 0x40, 0x55, 0xc8, 0x1a, 0xa6, 0xa6, 0x9b, 0x9a, 0x7d, 0x41, + 0xd7, 0x24, 0x21, 0xbb, 0x6d, 0xe9, 0x00, 0x2e, 0x4d, 0x0c, 0x9a, 0xe8, 0x3e, 0xe4, 0xbc, 0x78, + 0x2b, 0xd0, 0xdc, 0x70, 0x0a, 0x06, 0xe4, 0xf1, 0x92, 0x25, 0xb9, 0x34, 0x31, 0x6c, 0xa2, 0x06, + 0xa4, 0x4d, 0x6c, 0x0d, 0x7b, 0x2c, 0x57, 0x2d, 0xdd, 0x7d, 0x63, 0xbe, 0x70, 0x4b, 0xa8, 0xc3, + 0x9e, 0x2d, 0x73, 0x61, 0xe9, 0x33, 0x48, 0x33, 0x0a, 0xca, 0x43, 0xe6, 0x68, 0xef, 0xd1, 0xde, + 0xfe, 0x47, 0x7b, 0x62, 0x0c, 0x01, 0xa4, 0x6b, 0xf5, 0x7a, 0xe3, 0xa0, 0x25, 0x0a, 0x28, 0x07, + 0xa9, 0xda, 0xd6, 0xbe, 0xdc, 0x12, 0xe3, 0x84, 0x2c, 0x37, 0x1e, 0x36, 0xea, 0x2d, 0x31, 0x81, + 0x96, 0xa0, 0xc8, 0x7e, 0x2b, 0x0f, 0xf6, 0xe5, 0xc7, 0xb5, 0x96, 0x98, 0xf4, 0x91, 0x0e, 0x1b, + 0x7b, 0xdb, 0x0d, 0x59, 0x4c, 0x49, 0x6f, 0xc1, 0xd5, 0xc8, 0x00, 0xed, 0xc1, 0x44, 0x82, 0x0f, + 0x26, 0x92, 0xbe, 0x8f, 0x93, 0x1b, 0x48, 0x54, 0xd4, 0x45, 0x0f, 0x43, 0x13, 0xbf, 0xbb, 0x40, + 0xc8, 0x0e, 0xcd, 0x1e, 0xbd, 0x0c, 0x25, 0x13, 0x9f, 0x62, 0xbb, 0xdd, 0x65, 0x59, 0x80, 0x83, + 0x23, 0x15, 0x39, 0x95, 0x0a, 0x59, 0x8c, 0xed, 0x73, 0xdc, 0xb6, 0x15, 0x66, 0x04, 0x16, 0xbd, + 0xad, 0xe7, 0x08, 0x1b, 0xa1, 0x1e, 0x32, 0x22, 0x71, 0xd0, 0xcc, 0x91, 0x30, 0x55, 0x49, 0xaa, + 0x0a, 0xa8, 0x5f, 0xa0, 0x14, 0xe9, 0xe9, 0x42, 0x8b, 0x9d, 0x83, 0x94, 0xdc, 0x68, 0xc9, 0x1f, + 0x8b, 0x09, 0x84, 0xa0, 0x44, 0x7f, 0x2a, 0x87, 0x7b, 0xb5, 0x83, 0xc3, 0xe6, 0x3e, 0x59, 0xec, + 0x65, 0x28, 0x3b, 0x8b, 0xed, 0x10, 0x53, 0xe8, 0x12, 0x2c, 0xd5, 0xf7, 0x1f, 0x1f, 0xec, 0x36, + 0x5a, 0x0d, 0x8f, 0x9c, 0x96, 0xfe, 0x37, 0x01, 0x57, 0x22, 0x72, 0x0d, 0xf4, 0x2e, 0x80, 0x3d, + 0x52, 0x4c, 0xdc, 0xd6, 0xcd, 0x4e, 0xb4, 0x71, 0xb6, 0x46, 0x32, 0xe5, 0x90, 0x73, 0x36, 0xff, + 0x35, 0xd5, 0x61, 0x7f, 0xc0, 0x95, 0x92, 0xc9, 0x5a, 0x1c, 0xdb, 0xb8, 0x31, 0xe1, 0xb2, 0x86, + 0xdb, 0x44, 0x31, 0xdd, 0x13, 0xaa, 0x98, 0xf2, 0xa3, 0x8f, 0xe1, 0x4a, 0x28, 0xae, 0x70, 0x67, + 0x6c, 0x4d, 0x2a, 0x2c, 0x4e, 0x0e, 0x2f, 0x97, 0x82, 0xe1, 0x85, 0x39, 0x63, 0x6b, 0x0a, 0x90, + 0x90, 0x7a, 0x01, 0x20, 0x21, 0x2a, 0x3e, 0xa5, 0x17, 0x85, 0xe8, 0x27, 0xc4, 0x27, 0xe9, 0xbf, + 0x03, 0x9b, 0x17, 0x4c, 0xdf, 0xf6, 0x21, 0x6d, 0xd9, 0xaa, 0x3d, 0xb4, 0xf8, 0x61, 0xb8, 0x3f, + 0x6f, 0x2e, 0xb8, 0xe1, 0xfc, 0x38, 0xa4, 0xe2, 0x32, 0x57, 0xf3, 0x57, 0xb9, 0xa7, 0x51, 0xab, + 0x9f, 0xfa, 0x39, 0x56, 0xff, 0x6d, 0x28, 0x05, 0x97, 0x2a, 0xfa, 0xec, 0x7a, 0xde, 0x31, 0x2e, + 0xfd, 0x97, 0x00, 0xcb, 0x13, 0xb0, 0x08, 0x74, 0x9f, 0x97, 0x1b, 0xd8, 0x76, 0xdd, 0x1c, 0x9f, + 0x73, 0x80, 0xdd, 0xab, 0x3a, 0x90, 0x68, 0xe5, 0x25, 0xd5, 0x6c, 0x67, 0x3c, 0x02, 0x7a, 0x0d, + 0xca, 0x96, 0x76, 0x36, 0x50, 0x4c, 0x06, 0x6b, 0xb8, 0x50, 0x3e, 0xc9, 0x79, 0x49, 0x87, 0x53, + 0xf0, 0xea, 0xfc, 0xbb, 0x20, 0x6c, 0x21, 0x10, 0x95, 0x10, 0xb7, 0xd4, 0x06, 0x34, 0x9e, 0xe3, + 0x4f, 0x02, 0x5e, 0x84, 0x17, 0x00, 0x5e, 0xfe, 0x53, 0x80, 0x6b, 0x53, 0xf2, 0x7e, 0xf4, 0x61, + 0xc8, 0x9a, 0xdf, 0x5b, 0xe4, 0xd6, 0xb0, 0xc1, 0x68, 0x41, 0x7b, 0x96, 0xee, 0x41, 0xc1, 0x4f, + 0x9f, 0x6f, 0xf3, 0xfe, 0xcd, 0x17, 0x75, 0x83, 0x08, 0x51, 0x13, 0xd2, 0xf8, 0x1c, 0x0f, 0xdc, + 0x28, 0x7e, 0x79, 0x7c, 0x1d, 0x48, 0xf7, 0x56, 0x85, 0x64, 0x9b, 0xbf, 0x7f, 0xb6, 0x26, 0x32, + 0xee, 0xd7, 0xf5, 0xbe, 0x66, 0xe3, 0xbe, 0x61, 0x5f, 0xc8, 0x5c, 0x1e, 0xdd, 0x84, 0xa2, 0x89, + 0x6d, 0xe2, 0x84, 0x02, 0xe0, 0x5c, 0x81, 0x11, 0x79, 0x2e, 0xf8, 0x4b, 0x01, 0xc0, 0x83, 0x9c, + 0x3c, 0xc8, 0x47, 0xf0, 0x43, 0x3e, 0x21, 0xa4, 0x30, 0x1e, 0x46, 0x0a, 0xd1, 0x2d, 0x28, 0xb3, + 0x34, 0x9f, 0x6c, 0xba, 0x6a, 0x0f, 0x4d, 0xcc, 0x01, 0xa6, 0x12, 0x25, 0x1f, 0x3a, 0x54, 0xf4, + 0x09, 0x5c, 0xb5, 0xbb, 0x26, 0xb6, 0xba, 0x7a, 0xaf, 0xa3, 0x84, 0x37, 0x9e, 0x15, 0x3e, 0xd6, + 0x66, 0x58, 0xac, 0x7c, 0xc5, 0xd5, 0x70, 0x1c, 0xdc, 0xfc, 0x2f, 0x21, 0x45, 0xd7, 0x86, 0xa4, + 0x6a, 0xee, 0x11, 0xc8, 0x71, 0xeb, 0xfe, 0x14, 0x40, 0xb5, 0x6d, 0x53, 0x3b, 0x19, 0x12, 0x87, + 0x10, 0x1f, 0xff, 0x94, 0xb7, 0xb6, 0x35, 0x87, 0x6f, 0xeb, 0x3a, 0x5f, 0xe4, 0x15, 0x4f, 0xd4, + 0xb7, 0xd0, 0x3e, 0x85, 0xd2, 0x1e, 0x94, 0x82, 0xb2, 0x4e, 0x0e, 0xcc, 0xc6, 0x10, 0xcc, 0x81, + 0x59, 0x4e, 0xcd, 0x73, 0x60, 0x37, 0x83, 0x4e, 0xb0, 0xb2, 0x22, 0x6d, 0x48, 0x7f, 0x12, 0xa0, + 0xe0, 0x77, 0x74, 0x73, 0xa7, 0xa9, 0x3c, 0x6d, 0x4f, 0x8c, 0xa7, 0xed, 0xc9, 0xc8, 0xc4, 0x35, + 0x15, 0x4e, 0x5c, 0xaf, 0x42, 0x96, 0x74, 0x0f, 0x2d, 0xdc, 0xe1, 0xb5, 0xd8, 0xcc, 0x99, 0x6a, + 0x1d, 0x59, 0xb8, 0xe3, 0xb3, 0xcf, 0xcc, 0x0b, 0xda, 0x67, 0x20, 0x3b, 0xce, 0x86, 0x53, 0xf5, + 0xaf, 0x04, 0xc8, 0xba, 0x93, 0x0f, 0x96, 0x1c, 0x03, 0x08, 0x25, 0x5b, 0x3b, 0x56, 0x70, 0xe4, + 0xb7, 0x0f, 0x56, 0x80, 0x4d, 0xb8, 0x05, 0xd8, 0xf7, 0xdd, 0x7c, 0x2e, 0x0a, 0x83, 0xf3, 0xaf, + 0xb4, 0x03, 0xbb, 0xf2, 0xf4, 0xf5, 0x3f, 0xf8, 0x38, 0x48, 0x42, 0x82, 0xfe, 0x0e, 0xd2, 0x6a, + 0xdb, 0x45, 0x1e, 0x4b, 0x13, 0x20, 0x39, 0x87, 0x75, 0xa3, 0x35, 0xaa, 0x51, 0x4e, 0x99, 0x4b, + 0xf0, 0x51, 0xc5, 0x9d, 0x51, 0x49, 0xbb, 0x44, 0x2f, 0xe3, 0x09, 0xfa, 0x8c, 0x12, 0xc0, 0xd1, + 0xde, 0xe3, 0xfd, 0xed, 0x9d, 0x07, 0x3b, 0x8d, 0x6d, 0x9e, 0xb0, 0x6d, 0x6f, 0x37, 0xb6, 0xc5, + 0x38, 0xe1, 0x93, 0x1b, 0x8f, 0xf7, 0x8f, 0x1b, 0xdb, 0x62, 0x82, 0x34, 0xb6, 0x1b, 0xbb, 0xb5, + 0x8f, 0x1b, 0xdb, 0x62, 0x52, 0xaa, 0x41, 0xce, 0x0d, 0x3a, 0xb4, 0x52, 0xad, 0x3f, 0xc5, 0x26, + 0x5f, 0x2d, 0xd6, 0x40, 0xab, 0x90, 0x1f, 0x87, 0xce, 0xc9, 0xfd, 0x8b, 0x21, 0xe6, 0x24, 0xa0, + 0x94, 0x5d, 0x1d, 0x3c, 0xed, 0x78, 0x1f, 0x32, 0xc6, 0xf0, 0x44, 0x71, 0x0c, 0x39, 0x04, 0x38, + 0x3b, 0xd7, 0xb3, 0xe1, 0x49, 0x4f, 0x6b, 0x3f, 0xc2, 0x17, 0x3c, 0xc8, 0xa5, 0x8d, 0xe1, 0xc9, + 0x23, 0x66, 0xef, 0x6c, 0x18, 0xf1, 0x29, 0xc3, 0x48, 0x84, 0x86, 0x81, 0x6e, 0x41, 0x61, 0xa0, + 0x77, 0xb0, 0xa2, 0x76, 0x3a, 0x26, 0xb6, 0x58, 0xec, 0xce, 0x71, 0xcd, 0x79, 0xd2, 0x53, 0x63, + 0x1d, 0xd2, 0x0f, 0x02, 0xa0, 0xf1, 0x40, 0x8b, 0x0e, 0x61, 0xc9, 0x8b, 0xd5, 0x4e, 0x02, 0xc0, + 0x7c, 0xe9, 0x7a, 0x74, 0xa0, 0x0e, 0xdc, 0xe1, 0xc5, 0xf3, 0x20, 0x99, 0x24, 0x75, 0x2b, 0x9e, + 0xdf, 0x32, 0xe8, 0x7c, 0xe9, 0xa2, 0xc4, 0xe7, 0x5c, 0x94, 0x98, 0x8c, 0x5c, 0x79, 0xb7, 0x27, + 0xec, 0x57, 0x13, 0x63, 0x15, 0x18, 0x03, 0x2a, 0xad, 0x31, 0x31, 0x3e, 0xcf, 0xa8, 0x21, 0x09, + 0x2f, 0x32, 0x24, 0xe9, 0x1e, 0x88, 0x1f, 0xba, 0xdf, 0xe7, 0x5f, 0x0a, 0x0d, 0x53, 0x18, 0x1b, + 0xe6, 0x39, 0x64, 0x89, 0x2b, 0xa6, 0x11, 0xe4, 0x1f, 0x20, 0xe7, 0xae, 0x9e, 0xfb, 0xd8, 0x25, + 0x72, 0xd9, 0xf9, 0x48, 0x3c, 0x11, 0x74, 0x07, 0x96, 0x48, 0x10, 0x71, 0xea, 0xa0, 0x0c, 0x85, + 0x8b, 0x53, 0xd7, 0x58, 0x66, 0x1d, 0xbb, 0x0e, 0x74, 0x44, 0xa2, 0xbd, 0xc8, 0xb2, 0x02, 0xdc, + 0xf9, 0x4b, 0x0c, 0x80, 0x5c, 0xdb, 0x42, 0x60, 0x24, 0xdb, 0xc3, 0x62, 0x20, 0x2d, 0x91, 0xfe, + 0x25, 0x0e, 0x79, 0x5f, 0x5d, 0x06, 0xfd, 0x6d, 0x20, 0x45, 0x5b, 0x9f, 0x56, 0xc3, 0xf1, 0xe5, + 0x67, 0x81, 0x89, 0xc5, 0x17, 0x9f, 0x58, 0x54, 0x45, 0xcc, 0x29, 0xcf, 0x26, 0x17, 0x2e, 0xcf, + 0xbe, 0x0e, 0xc8, 0xd6, 0x6d, 0xb5, 0x47, 0x22, 0xb9, 0x36, 0x38, 0x53, 0xd8, 0x69, 0x67, 0xd1, + 0x44, 0xa4, 0x3d, 0xc7, 0xb4, 0xe3, 0x80, 0xd0, 0xa5, 0x1e, 0x64, 0x5d, 0x6c, 0x61, 0xf1, 0x37, + 0x24, 0x93, 0xca, 0xd0, 0x55, 0xc8, 0xf6, 0xb1, 0xad, 0xd2, 0x18, 0xc8, 0xb0, 0x26, 0xb7, 0x7d, + 0xe7, 0x3d, 0xc8, 0xfb, 0x1e, 0xd6, 0x90, 0xb0, 0xb8, 0xd7, 0xf8, 0x48, 0x8c, 0x55, 0x33, 0x5f, + 0x7f, 0xbb, 0x9e, 0xd8, 0xc3, 0x4f, 0xc9, 0xa7, 0xe4, 0x46, 0xbd, 0xd9, 0xa8, 0x3f, 0x12, 0x85, + 0x6a, 0xfe, 0xeb, 0x6f, 0xd7, 0x33, 0x32, 0xa6, 0x25, 0x8c, 0x3b, 0x8f, 0xa0, 0x1c, 0xda, 0x81, + 0xa0, 0x83, 0x46, 0x50, 0xda, 0x3e, 0x3a, 0xd8, 0xdd, 0xa9, 0xd7, 0x5a, 0x0d, 0xe5, 0x78, 0xbf, + 0xd5, 0x10, 0x05, 0x74, 0x05, 0x96, 0x77, 0x77, 0xfe, 0xb1, 0xd9, 0x52, 0xea, 0xbb, 0x3b, 0x8d, + 0xbd, 0x96, 0x52, 0x6b, 0xb5, 0x6a, 0xf5, 0x47, 0x62, 0xfc, 0xee, 0x6f, 0x00, 0xca, 0xb5, 0xad, + 0xfa, 0x4e, 0xcd, 0x30, 0x7a, 0x5a, 0x5b, 0xa5, 0xee, 0xbe, 0x0e, 0x49, 0x8a, 0xec, 0x4e, 0x7d, + 0x62, 0x5b, 0x9d, 0x5e, 0x97, 0x42, 0x0f, 0x20, 0x45, 0x41, 0x5f, 0x34, 0xfd, 0xcd, 0x6d, 0x75, + 0x46, 0xa1, 0x8a, 0x0c, 0x86, 0x9e, 0x9b, 0xa9, 0x8f, 0x70, 0xab, 0xd3, 0xeb, 0x56, 0x68, 0x17, + 0x32, 0x0e, 0x9e, 0x36, 0xeb, 0x65, 0x6c, 0x75, 0x66, 0x31, 0x89, 0x4c, 0x8d, 0xe1, 0x92, 0xd3, + 0xdf, 0xe7, 0x56, 0x67, 0x54, 0xb4, 0x90, 0x0c, 0x39, 0x0f, 0x4a, 0x9e, 0xfd, 0x54, 0xb8, 0x3a, + 0x47, 0x85, 0x0d, 0x7d, 0x06, 0xc5, 0x20, 0xf2, 0x36, 0xdf, 0x2b, 0xde, 0xea, 0x9c, 0xd5, 0x2f, + 0xa2, 0x3f, 0x08, 0xc3, 0xcd, 0xf7, 0xaa, 0xb7, 0x3a, 0x67, 0x31, 0x0c, 0x7d, 0x0e, 0x4b, 0xe3, + 0x30, 0xd9, 0xfc, 0x8f, 0x7c, 0xab, 0x0b, 0x94, 0xc7, 0x50, 0x1f, 0xd0, 0x04, 0x78, 0x6d, 0x81, + 0x37, 0xbf, 0xd5, 0x45, 0xaa, 0x65, 0xa8, 0x03, 0xe5, 0x30, 0xf4, 0x34, 0xef, 0x1b, 0xe0, 0xea, + 0xdc, 0x95, 0x33, 0xf6, 0x95, 0x20, 0x46, 0x32, 0xef, 0x9b, 0xe0, 0xea, 0xdc, 0x85, 0x34, 0x74, + 0x04, 0xe0, 0xbb, 0x25, 0xcf, 0xf1, 0x46, 0xb8, 0x3a, 0x4f, 0x49, 0x0d, 0x19, 0xb0, 0x3c, 0xe9, + 0x5a, 0xbc, 0xc8, 0x93, 0xe1, 0xea, 0x42, 0x95, 0x36, 0x62, 0xcf, 0xc1, 0x0b, 0xee, 0x7c, 0x4f, + 0x88, 0xab, 0x73, 0x96, 0xdc, 0xb6, 0xb6, 0xbe, 0x7b, 0xbe, 0x2a, 0x7c, 0xff, 0x7c, 0x55, 0xf8, + 0xe1, 0xf9, 0xaa, 0xf0, 0xcd, 0x8f, 0xab, 0xb1, 0xef, 0x7f, 0x5c, 0x8d, 0xfd, 0xfa, 0xc7, 0xd5, + 0xd8, 0x3f, 0xdd, 0x3e, 0xd3, 0xec, 0xee, 0xf0, 0x64, 0xa3, 0xad, 0xf7, 0xe9, 0x3f, 0x38, 0x0c, + 0xf5, 0x62, 0x93, 0xe9, 0x24, 0x2d, 0xdf, 0xff, 0x44, 0x4e, 0xd2, 0x34, 0xd6, 0xdd, 0xfb, 0x73, + 0x00, 0x00, 0x00, 0xff, 0xff, 0x2a, 0x14, 0xac, 0xb9, 0x47, 0x32, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -7375,6 +7416,15 @@ func (m *ExtendVoteExtension) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.XSignRequestId != nil { + { + size := m.XSignRequestId.Size() + i -= size + if _, err := m.XSignRequestId.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + } + } if len(m.Extension) > 0 { i -= len(m.Extension) copy(dAtA[i:], m.Extension) @@ -7390,6 +7440,22 @@ func (m *ExtendVoteExtension) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *ExtendVoteExtension_SignRequestId) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ExtendVoteExtension_SignRequestId) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + if m.SignRequestId != nil { + i -= len(m.SignRequestId) + copy(dAtA[i:], m.SignRequestId) + i = encodeVarintTypes(dAtA, i, uint64(len(m.SignRequestId))) + i-- + dAtA[i] = 0x1a + } + return len(dAtA) - i, nil +} func (m *ResponseExtendVote) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -9315,9 +9381,24 @@ func (m *ExtendVoteExtension) Size() (n int) { if l > 0 { n += 1 + l + sovTypes(uint64(l)) } + if m.XSignRequestId != nil { + n += m.XSignRequestId.Size() + } return n } +func (m *ExtendVoteExtension_SignRequestId) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.SignRequestId != nil { + l = len(m.SignRequestId) + n += 1 + l + sovTypes(uint64(l)) + } + return n +} func (m *ResponseExtendVote) Size() (n int) { if m == nil { return 0 @@ -15660,6 +15741,39 @@ func (m *ExtendVoteExtension) Unmarshal(dAtA []byte) error { m.Extension = []byte{} } iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SignRequestId", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthTypes + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + v := make([]byte, postIndex-iNdEx) + copy(v, dAtA[iNdEx:postIndex]) + m.XSignRequestId = &ExtendVoteExtension_SignRequestId{v} + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTypes(dAtA[iNdEx:]) diff --git a/crypto/quorum.go b/crypto/quorum.go deleted file mode 100644 index 70b0753611..0000000000 --- a/crypto/quorum.go +++ /dev/null @@ -1,26 +0,0 @@ -package crypto - -import ( - bls "github.com/dashpay/bls-signatures/go-bindings" - "github.com/dashpay/dashd-go/btcjson" -) - -// SignID returns signing session data that will be signed to get threshold signature share. -// See DIP-0007 -func SignID(llmqType btcjson.LLMQType, quorumHash QuorumHash, requestID []byte, messageHash []byte) []byte { - var blsQuorumHash bls.Hash - copy(blsQuorumHash[:], quorumHash.Bytes()) - - var blsRequestID bls.Hash - copy(blsRequestID[:], requestID) - - var blsMessageHash bls.Hash - copy(blsMessageHash[:], messageHash) - - blsSignHash := bls.BuildSignHash(uint8(llmqType), blsQuorumHash, blsRequestID, blsMessageHash) - - signHash := make([]byte, 32) - copy(signHash, blsSignHash[:]) - - return signHash -} diff --git a/dash/core/mock.go b/dash/core/mock.go index 0cfda7142f..8afd06d93f 100644 --- a/dash/core/mock.go +++ b/dash/core/mock.go @@ -152,13 +152,8 @@ func (mc *MockClient) QuorumSign( if !mc.canSign { return nil, errors.New("dash core mock client not set up for signing") } + signID := types.NewSignItemFromHash(quorumType, quorumHash, requestID, messageHash).SignHash - signID := crypto.SignID( - quorumType, - tmbytes.Reverse(quorumHash), - tmbytes.Reverse(requestID), - tmbytes.Reverse(messageHash), - ) privateKey, err := mc.localPV.GetPrivateKey(context.Background(), quorumHash) if err != nil { panic(err) @@ -190,12 +185,9 @@ func (mc *MockClient) QuorumVerify( if err := quorumType.Validate(); err != nil { return false, err } - signID := crypto.SignID( - quorumType, - tmbytes.Reverse(quorumHash), - tmbytes.Reverse(requestID), - tmbytes.Reverse(messageHash), - ) + + signID := types.NewSignItemFromHash(quorumType, quorumHash, requestID, messageHash).SignHash + thresholdPublicKey, err := mc.localPV.GetThresholdPublicKey(context.Background(), quorumHash) if err != nil { panic(err) diff --git a/go.mod b/go.mod index 80cfbd2544..b607624ccf 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.2 github.com/golang/snappy v0.0.4 // indirect - github.com/golangci/golangci-lint v1.55.0 + github.com/golangci/golangci-lint v1.55.2 github.com/google/btree v1.1.2 // indirect github.com/google/gopacket v1.1.19 github.com/google/orderedcode v0.0.1 @@ -51,7 +51,7 @@ require ( github.com/creachadair/atomicfile v0.2.6 github.com/creachadair/taskgroup v0.3.2 github.com/go-pkgz/jrpc v0.2.0 - github.com/google/go-cmp v0.5.9 + github.com/google/go-cmp v0.6.0 github.com/vektra/mockery/v2 v2.33.2 gotest.tools v2.2.0+incompatible ) @@ -98,7 +98,7 @@ require ( github.com/moby/buildkit v0.10.3 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/nunnatsa/ginkgolinter v0.14.0 // indirect + github.com/nunnatsa/ginkgolinter v0.14.1 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect @@ -138,7 +138,7 @@ require ( github.com/bombsimon/wsl/v3 v3.4.0 // indirect github.com/breml/bidichk v0.2.7 // indirect github.com/breml/errchkjson v0.3.6 // indirect - github.com/butuzov/ireturn v0.2.1 // indirect + github.com/butuzov/ireturn v0.2.2 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect @@ -181,7 +181,7 @@ require ( github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect github.com/golangci/misspell v0.4.1 // indirect - github.com/golangci/revgrep v0.5.0 // indirect + github.com/golangci/revgrep v0.5.2 // indirect github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect @@ -248,7 +248,7 @@ require ( github.com/ryancurrah/gomodguard v1.3.0 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect - github.com/securego/gosec/v2 v2.18.1 // indirect + github.com/securego/gosec/v2 v2.18.2 // indirect github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sivchari/containedctx v1.0.3 // indirect diff --git a/go.sum b/go.sum index fde04e281f..cde3e72466 100644 --- a/go.sum +++ b/go.sum @@ -144,8 +144,8 @@ github.com/bufbuild/buf v1.7.0 h1:uWRjhIXcrWkzIkA5TqXGyJbF51VW54QJsQZ3nwaes5Q= github.com/bufbuild/buf v1.7.0/go.mod h1:Go40fMAF46PnPLC7jJgTQhAI95pmC0+VtxFKVC0qLq0= github.com/bufbuild/connect-go v0.2.0 h1:WuMI/jLiJIhysHWvLWlxRozV67mGjCOUuDSl/lkDVic= github.com/bufbuild/connect-go v0.2.0/go.mod h1:4efZ2eXFENwd4p7tuLaL9m0qtTsCOzuBvrohvRGevDM= -github.com/butuzov/ireturn v0.2.1 h1:w5Ks4tnfeFDZskGJ2x1GAkx5gaQV+kdU3NKNr3NEBzY= -github.com/butuzov/ireturn v0.2.1/go.mod h1:RfGHUvvAuFFxoHKf4Z8Yxuh6OjlCw1KvR2zM1NFHeBk= +github.com/butuzov/ireturn v0.2.2 h1:jWI36dxXwVrI+RnXDwux2IZOewpmfv930OuIRfaBUJ0= +github.com/butuzov/ireturn v0.2.2/go.mod h1:RfGHUvvAuFFxoHKf4Z8Yxuh6OjlCw1KvR2zM1NFHeBk= github.com/butuzov/mirror v1.1.0 h1:ZqX54gBVMXu78QLoiqdwpl2mgmoOJTk7s4p4o+0avZI= github.com/butuzov/mirror v1.1.0/go.mod h1:8Q0BdQU6rC6WILDiBM60DBfvV78OLJmMmixe7GF45AE= github.com/catenacyber/perfsprint v0.2.0 h1:azOocHLscPjqXVJ7Mf14Zjlkn4uNua0+Hcg1wTR6vUo= @@ -410,16 +410,16 @@ github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6 github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ= github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZaA6TlQbiM3J2GCPnkx/bGF6sX/g= github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM= -github.com/golangci/golangci-lint v1.55.0 h1:ePpc6YhM1ZV8kHU8dwmHDHAdeedZHdK8cmTXlkkRdi8= -github.com/golangci/golangci-lint v1.55.0/go.mod h1:Z/OawFQ4yqFo2/plDYlIjoZlJeVYkRcqS9dW55p0FXg= +github.com/golangci/golangci-lint v1.55.2 h1:yllEIsSJ7MtlDBwDJ9IMBkyEUz2fYE0b5B8IUgO1oP8= +github.com/golangci/golangci-lint v1.55.2/go.mod h1:H60CZ0fuqoTwlTvnbyjhpZPWp7KmsjwV2yupIMiMXbM= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= github.com/golangci/misspell v0.4.1 h1:+y73iSicVy2PqyX7kmUefHusENlrP9YwuHZHPLGQj/g= github.com/golangci/misspell v0.4.1/go.mod h1:9mAN1quEo3DlpbaIKKyEvRxK1pwqR9s/Sea1bJCtlNI= -github.com/golangci/revgrep v0.5.0 h1:GGBqHFtFOeHiSUQtFVZXPJtVZYOGB4iVlAjaoFRBQvY= -github.com/golangci/revgrep v0.5.0/go.mod h1:bjAMA+Sh/QUfTDcHzxfyHxr4xKvllVr/0sCv2e7jJHA= +github.com/golangci/revgrep v0.5.2 h1:EndcWoRhcnfj2NHQ+28hyuXpLMF+dQmCN+YaeeIl4FU= +github.com/golangci/revgrep v0.5.2/go.mod h1:bjAMA+Sh/QUfTDcHzxfyHxr4xKvllVr/0sCv2e7jJHA= github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -439,8 +439,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -660,8 +660,8 @@ github.com/nishanths/exhaustive v0.11.0 h1:T3I8nUGhl/Cwu5Z2hfc92l0e04D2GEW6e0l8p github.com/nishanths/exhaustive v0.11.0/go.mod h1:RqwDsZ1xY0dNdqHho2z6X+bgzizwbLYOWnZbbl2wLB4= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.14.0 h1:XQPNmw+kZz5cC/HbFK3mQutpjzAQv1dHregRA+4CGGg= -github.com/nunnatsa/ginkgolinter v0.14.0/go.mod h1:cm2xaqCUCRd7qcP4DqbVvpcyEMkuLM9CF0wY6VASohk= +github.com/nunnatsa/ginkgolinter v0.14.1 h1:khx0CqR5U4ghsscjJ+lZVthp3zjIFytRXPTaQ/TMiyA= +github.com/nunnatsa/ginkgolinter v0.14.1/go.mod h1:nY0pafUSst7v7F637e7fymaMlQqI9c0Wka2fGsDkzWg= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -677,13 +677,13 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA= -github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c= -github.com/onsi/gomega v1.28.0/go.mod h1:A1H2JE76sI14WIP57LMKj7FVfCHx3g3BcZVjJG8bjX8= +github.com/onsi/gomega v1.28.1 h1:MijcGUbfYuznzK/5R4CPNoUP/9Xvuo20sXfEm6XxoTA= +github.com/onsi/gomega v1.28.1/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= @@ -781,8 +781,8 @@ github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84d github.com/sashamelentyev/usestdlibvars v1.24.0 h1:MKNzmXtGh5N0y74Z/CIaJh4GlB364l0K1RUT08WSWAc= github.com/sashamelentyev/usestdlibvars v1.24.0/go.mod h1:9cYkq+gYJ+a5W2RPdhfaSCnTVUC1OQP/bSiiBhq3OZE= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= -github.com/securego/gosec/v2 v2.18.1 h1:xnnehWg7dIW8qrRPGm8ykY21zp2MueKyC99Vlcuj96I= -github.com/securego/gosec/v2 v2.18.1/go.mod h1:ZUTcKD9gAFip1lLGHWCjkoBQJyaEzePTNzjwlL2HHoE= +github.com/securego/gosec/v2 v2.18.2 h1:DkDt3wCiOtAHf1XkiXZBhQ6m6mK/b9T/wD257R3/c+I= +github.com/securego/gosec/v2 v2.18.2/go.mod h1:xUuqSF6i0So56Y2wwohWAmB07EdBkUN6crbLlHwbyJs= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= diff --git a/internal/blocksync/reactor_test.go b/internal/blocksync/reactor_test.go index 2585935952..f29f990b04 100644 --- a/internal/blocksync/reactor_test.go +++ b/internal/blocksync/reactor_test.go @@ -189,7 +189,7 @@ func (rts *reactorTestSuite) addNode( peerEvents := func(ctx context.Context, _ string) *p2p.PeerUpdates { return rts.peerUpdates[nodeID] } reactor := makeReactor(ctx, t, rts.config, proTxHash, nodeID, genDoc, privVal, chCreator, peerEvents) - commit := types.NewCommit(0, 0, types.BlockID{}, nil) + commit := types.NewCommit(0, 0, types.BlockID{}, nil, nil) state, err := reactor.stateStore.Load() require.NoError(t, err) @@ -237,10 +237,11 @@ func makeNextBlock(ctx context.Context, vote.Height, vote.Round, blockID, + vote.VoteExtensions, &types.CommitSigns{ QuorumSigns: types.QuorumSigns{ - BlockSign: vote.BlockSignature, - ExtensionSigns: types.MakeThresholdExtensionSigns(vote.VoteExtensions), + BlockSign: vote.BlockSignature, + VoteExtensionSignatures: vote.VoteExtensions.GetSignatures(), }, QuorumHash: state.Validators.QuorumHash, }, diff --git a/internal/consensus/block_executor.go b/internal/consensus/block_executor.go index c2beffcb46..6a0e768857 100644 --- a/internal/consensus/block_executor.go +++ b/internal/consensus/block_executor.go @@ -40,7 +40,7 @@ func (c *blockExecutor) create(ctx context.Context, rs *cstypes.RoundState, roun case rs.Height == c.committedState.InitialHeight: // We're creating a proposal for the first block. // The commit is empty, but not nil. - commit = types.NewCommit(0, 0, types.BlockID{}, nil) + commit = types.NewCommit(0, 0, types.BlockID{}, nil, nil) case rs.LastCommit != nil: // Make the commit from LastPrecommits commit = rs.LastCommit diff --git a/internal/consensus/block_executor_test.go b/internal/consensus/block_executor_test.go index 86d5a98c9f..9fa00938ee 100644 --- a/internal/consensus/block_executor_test.go +++ b/internal/consensus/block_executor_test.go @@ -20,7 +20,7 @@ import ( "github.com/dashpay/tenderdash/types/mocks" ) -type BockExecutorTestSuite struct { +type BlockExecutorTestSuite struct { suite.Suite blockExec *blockExecutor @@ -28,11 +28,11 @@ type BockExecutorTestSuite struct { mockBlockExec *smmocks.Executor } -func TestBockExecutor(t *testing.T) { - suite.Run(t, new(BockExecutorTestSuite)) +func TestBlockExecutor(t *testing.T) { + suite.Run(t, new(BlockExecutorTestSuite)) } -func (suite *BockExecutorTestSuite) SetupTest() { +func (suite *BlockExecutorTestSuite) SetupTest() { logger := log.NewTestingLogger(suite.T()) suite.mockPrivVal = mocks.NewPrivValidator(suite.T()) suite.mockBlockExec = smmocks.NewExecutor(suite.T()) @@ -47,13 +47,13 @@ func (suite *BockExecutorTestSuite) SetupTest() { } } -func (suite *BockExecutorTestSuite) TestCreate() { +func (suite *BlockExecutorTestSuite) TestCreate() { ctx := context.Background() commitH99R0 := &types.Commit{ Height: 99, Round: 0, } - emptyCommit := types.NewCommit(0, 0, types.BlockID{}, nil) + emptyCommit := types.NewCommit(0, 0, types.BlockID{}, nil, nil) testCases := []struct { round int32 initialHeight int64 @@ -137,7 +137,7 @@ func (suite *BockExecutorTestSuite) TestCreate() { } } -func (suite *BockExecutorTestSuite) TestProcess() { +func (suite *BlockExecutorTestSuite) TestProcess() { ctx := context.Background() const round = int32(0) wantDefaultCRS := sm.CurrentRoundState{ diff --git a/internal/consensus/common_test.go b/internal/consensus/common_test.go index 15a65f6c1c..7766b3a17f 100644 --- a/internal/consensus/common_test.go +++ b/internal/consensus/common_test.go @@ -157,10 +157,7 @@ func signVote( blockID types.BlockID, quorumType btcjson.LLMQType, quorumHash crypto.QuorumHash) *types.Vote { - exts := make(types.VoteExtensions) - if voteType == tmproto.PrecommitType && !blockID.IsNil() { - exts.Add(tmproto.VoteExtensionType_DEFAULT, []byte("extension")) - } + exts := make(types.VoteExtensions, 0) v, err := vs.signVote(ctx, voteType, chainID, blockID, quorumType, quorumHash, exts) require.NoError(t, err, "failed to sign vote") @@ -1109,7 +1106,7 @@ func signDataIsEqual(v1 *types.Vote, v2 *tmproto.Vote) bool { if v1 == nil || v2 == nil { return false } - if v1.VoteExtensions.IsSameWithProto(v2.VoteExtensionsToMap()) { + if v1.VoteExtensions.IsSameWithProto(v2.VoteExtensions) { return false } return v1.Type == v2.Type && diff --git a/internal/consensus/gossiper_test.go b/internal/consensus/gossiper_test.go index 09d0cd9347..7007b31b2c 100644 --- a/internal/consensus/gossiper_test.go +++ b/internal/consensus/gossiper_test.go @@ -603,6 +603,6 @@ func (suite *GossiperSuiteTest) signVote(vote *types.Vote) { err := privVal.SignVote(ctx, factory.DefaultTestChainID, suite.valSet.QuorumType, suite.valSet.QuorumHash, protoVote, nil) suite.Require().NoError(err) vote.BlockSignature = protoVote.BlockSignature - err = vote.VoteExtensions.CopySignsFromProto(protoVote.VoteExtensionsToMap()) + err = vote.VoteExtensions.CopySignsFromProto(protoVote.VoteExtensions) suite.Require().NoError(err) } diff --git a/internal/consensus/msgs_test.go b/internal/consensus/msgs_test.go index 73b5a1dede..85211b2af0 100644 --- a/internal/consensus/msgs_test.go +++ b/internal/consensus/msgs_test.go @@ -384,9 +384,10 @@ func TestConsMsgsVectors(t *testing.T) { Round: 0, Type: tmproto.PrecommitType, BlockID: bi, - VoteExtensions: types.VoteExtensions{ - tmproto.VoteExtensionType_DEFAULT: []types.VoteExtension{{Extension: []byte("extension")}}, - }, + VoteExtensions: types.VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_DEFAULT, + Extension: []byte("extension"), + }), } vpb := v.ToProto() diff --git a/internal/consensus/reactor.go b/internal/consensus/reactor.go index 145c19f7a2..d16e1c3543 100644 --- a/internal/consensus/reactor.go +++ b/internal/consensus/reactor.go @@ -687,6 +687,10 @@ func (r *Reactor) handleVoteMessage(ctx context.Context, envelope *p2p.Envelope, if isValidator { // ignore votes on non-validator nodes; TODO don't even send it vMsg := msgI.(*VoteMessage) + if err := vMsg.Vote.ValidateBasic(); err != nil { + return fmt.Errorf("invalid vote received from %s: %w", envelope.From, err) + } + ps.EnsureVoteBitArrays(height, valSize) ps.EnsureVoteBitArrays(height-1, lastCommitSize) if err := ps.SetHasVote(vMsg.Vote); err != nil { diff --git a/internal/consensus/replay_test.go b/internal/consensus/replay_test.go index d6ef79b79d..517592e04c 100644 --- a/internal/consensus/replay_test.go +++ b/internal/consensus/replay_test.go @@ -1121,10 +1121,11 @@ func makeBlockchainFromWAL(t *testing.T, wal WAL) ([]*types.Block, []*types.Comm if p.Type == tmproto.PrecommitType { thisBlockCommit = types.NewCommit(p.Height, p.Round, p.BlockID, + p.VoteExtensions, &types.CommitSigns{ QuorumSigns: types.QuorumSigns{ - BlockSign: p.BlockSignature, - ExtensionSigns: types.MakeThresholdExtensionSigns(p.VoteExtensions), + BlockSign: p.BlockSignature, + VoteExtensionSignatures: p.VoteExtensions.GetSignatures(), }, QuorumHash: crypto.RandQuorumHash(), }, diff --git a/internal/consensus/state_test.go b/internal/consensus/state_test.go index 7825c0a828..cb90c58b7f 100644 --- a/internal/consensus/state_test.go +++ b/internal/consensus/state_test.go @@ -2141,12 +2141,10 @@ func TestExtendVote(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - voteExtensions := []*abci.ExtendVoteExtension{ - { - Type: tmproto.VoteExtensionType_DEFAULT, - Extension: []byte("extension"), - }, - } + voteExtensions := types.VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("extension"), + }) m := abcimocks.NewApplication(t) m.On("ProcessProposal", mock.Anything, mock.Anything).Return(&abci.ResponseProcessProposal{ @@ -2159,7 +2157,7 @@ func TestExtendVote(t *testing.T) { m.On("ExtendVote", mock.Anything, mock.Anything).Return(&abci.ResponseExtendVote{ VoteExtensions: []*abci.ExtendVoteExtension{ { - Type: tmproto.VoteExtensionType_DEFAULT, + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, Extension: []byte("extension"), }, }, @@ -2200,14 +2198,14 @@ func TestExtendVote(t *testing.T) { assert.Equal(t, req.Round, round) }) m.On("ExtendVote", mock.Anything, reqExtendVoteFunc).Return(&abci.ResponseExtendVote{ - VoteExtensions: voteExtensions, + VoteExtensions: voteExtensions.ToExtendProto(), }, nil) reqVerifyVoteExtFunc := mock.MatchedBy(func(req *abci.RequestVerifyVoteExtension) bool { _, ok := proTxHashMap[types.ProTxHash(req.ValidatorProTxHash).String()] - return assert.Equal(t, req.Hash, blockID.Hash.Bytes()) && - assert.Equal(t, req.Height, height) && - assert.Equal(t, req.Round, round) && - assert.Equal(t, req.VoteExtensions, voteExtensions) && + return assert.Equal(t, blockID.Hash.Bytes(), req.Hash) && + assert.Equal(t, height, req.Height) && + assert.Equal(t, round, req.Round) && + assert.Equal(t, voteExtensions.ToExtendProto(), req.VoteExtensions) && assert.True(t, ok) }) m.On("VerifyVoteExtension", mock.Anything, reqVerifyVoteExtFunc). @@ -2218,7 +2216,7 @@ func TestExtendVote(t *testing.T) { ensurePrevoteMatch(t, voteCh, height, round, blockID.Hash) ensurePrecommit(t, voteCh, height, round) - signAddVotes(ctx, t, cs1, tmproto.PrecommitType, config.ChainID(), blockID, vss[1:]...) + signAddPrecommitsWithExtension(ctx, t, cs1, config.ChainID(), blockID, voteExtensions, vss[1:]...) ensureNewRound(t, newRoundCh, height+1, 0) m.AssertExpectations(t) mock.AssertExpectationsForObjects(t, m) @@ -2302,12 +2300,11 @@ func TestVerifyVoteExtensionNotCalledOnAbsentPrecommit(t *testing.T) { config := configSetup(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() - voteExtensions := []*abci.ExtendVoteExtension{ - { - Type: tmproto.VoteExtensionType_DEFAULT, - Extension: []byte("extension"), - }, - } + voteExtensions := types.VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("extension"), + }) + m := abcimocks.NewApplication(t) m.On("ProcessProposal", mock.Anything, mock.Anything).Return(&abci.ResponseProcessProposal{ AppHash: make([]byte, crypto.DefaultAppHashSize), @@ -2317,7 +2314,7 @@ func TestVerifyVoteExtensionNotCalledOnAbsentPrecommit(t *testing.T) { AppHash: make([]byte, crypto.DefaultAppHashSize), }, nil) m.On("ExtendVote", mock.Anything, mock.Anything).Return(&abci.ResponseExtendVote{ - VoteExtensions: voteExtensions, + VoteExtensions: voteExtensions.ToExtendProto(), }, nil) m.On("FinalizeBlock", mock.Anything, mock.Anything).Return(&abci.ResponseFinalizeBlock{}, nil).Maybe() cs1, vss := makeState(ctx, t, makeStateArgs{config: config, application: m}) @@ -2355,10 +2352,10 @@ func TestVerifyVoteExtensionNotCalledOnAbsentPrecommit(t *testing.T) { }) reqVerifyVoteExtFunc := mock.MatchedBy(func(req *abci.RequestVerifyVoteExtension) bool { _, ok := proTxHashMap[types.ProTxHash(req.ValidatorProTxHash).String()] - return assert.Equal(t, req.Hash, blockID.Hash.Bytes()) && - assert.Equal(t, req.Height, height) && - assert.Equal(t, req.Round, round) && - assert.Equal(t, req.VoteExtensions, voteExtensions) && + return assert.Equal(t, blockID.Hash.Bytes(), req.Hash) && + assert.Equal(t, height, req.Height) && + assert.Equal(t, round, req.Round) && + assert.Equal(t, voteExtensions.ToExtendProto(), req.VoteExtensions) && assert.True(t, ok) }) m.On("VerifyVoteExtension", mock.Anything, reqVerifyVoteExtFunc). @@ -2366,7 +2363,7 @@ func TestVerifyVoteExtensionNotCalledOnAbsentPrecommit(t *testing.T) { Status: abci.ResponseVerifyVoteExtension_ACCEPT, }, nil) - signAddVotes(ctx, t, cs1, tmproto.PrecommitType, config.ChainID(), blockID, vss[2:]...) + signAddPrecommitsWithExtension(ctx, t, cs1, config.ChainID(), blockID, voteExtensions, vss[2:]...) ensureNewRound(t, newRoundCh, height+1, 0) m.AssertExpectations(t) @@ -2381,7 +2378,7 @@ func TestVerifyVoteExtensionNotCalledOnAbsentPrecommit(t *testing.T) { Round: round, VoteExtensions: []*abci.ExtendVoteExtension{ { - Type: tmproto.VoteExtensionType_DEFAULT, + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, Extension: []byte("extension"), }, }, @@ -2392,7 +2389,7 @@ func TestVerifyVoteExtensionNotCalledOnAbsentPrecommit(t *testing.T) { // TestPrepareProposalReceivesVoteExtensions tests that the PrepareProposal method // is called with the vote extensions from the previous height. The test functions // be completing a consensus height with a mock application as the proposer. The -// test then proceeds to fail sever rounds of consensus until the mock application +// test then proceeds to fail several rounds of consensus until the mock application // is the proposer again and ensures that the mock application receives the set of // vote extensions from the previous consensus instance. func TestPrepareProposalReceivesVoteExtensions(t *testing.T) { @@ -2401,36 +2398,56 @@ func TestPrepareProposalReceivesVoteExtensions(t *testing.T) { config := configSetup(t) + voteExtensions := types.VoteExtensionsFromProto( + &tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, + Extension: crypto.Checksum([]byte("extension-raw")), + }, &tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("deterministic"), + }, + ) + m := &abcimocks.Application{} m.On("ExtendVote", mock.Anything, mock.Anything).Return(&abci.ResponseExtendVote{ - VoteExtensions: []*abci.ExtendVoteExtension{ - { - Type: tmproto.VoteExtensionType_DEFAULT, - Extension: []byte("extension"), - }, - { - Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, - Extension: []byte("deterministic"), - }, - }, + VoteExtensions: voteExtensions.ToExtendProto(), }, nil) m.On("ProcessProposal", mock.Anything, mock.Anything).Return(&abci.ResponseProcessProposal{ AppHash: make([]byte, crypto.DefaultAppHashSize), Status: abci.ResponseProcessProposal_ACCEPT, }, nil) - // capture the prepare proposal request. - rpp := &abci.RequestPrepareProposal{} + // matcher for prepare proposal request m.On("PrepareProposal", mock.Anything, mock.MatchedBy(func(r *abci.RequestPrepareProposal) bool { - rpp = r - return true + if r.Height == 1 { + return assert.Empty(t, r.GetLocalLastCommit().ThresholdVoteExtensions, "no vote extensions should be present on the first height") + } + + // at height 2, we expect the vote extensions from the previous height to be present. + extensions := make([][]byte, 0) + for _, ext := range r.GetLocalLastCommit().ThresholdVoteExtensions { + extensions = append(extensions, ext.Extension) + } + return assert.EqualValues(t, 2, r.Height) && + assert.EqualValues(t, 3, r.Round) && + assert.Len(t, r.GetLocalLastCommit().ThresholdVoteExtensions, 2, "expected 2 vote extensions at height %d", r.Height) && + assert.EqualValues(t, voteExtensions.GetExtensions(), extensions) + })).Return(&abci.ResponsePrepareProposal{ AppHash: make([]byte, crypto.DefaultAppHashSize), }, nil) - m.On("PrepareProposal", mock.Anything, mock.Anything).Return(&abci.ResponsePrepareProposal{AppHash: make([]byte, crypto.DefaultAppHashSize)}, nil).Once() m.On("VerifyVoteExtension", mock.Anything, mock.Anything).Return(&abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_ACCEPT}, nil) - m.On("FinalizeBlock", mock.Anything, mock.Anything).Return(&abci.ResponseFinalizeBlock{}, nil) + + // We expect 2 threshold-recovered vote extensions in current Commit + m.On("FinalizeBlock", mock.Anything, mock.MatchedBy(func(r *abci.RequestFinalizeBlock) bool { + assert.Len(t, r.Commit.ThresholdVoteExtensions, 2) + vexts := r.Commit.ThresholdVoteExtensions + + return bytes.Equal(vexts[0].Extension, voteExtensions[0].GetExtension()) && + bytes.Equal(vexts[1].Extension, voteExtensions[1].GetExtension()) + + })).Return(&abci.ResponseFinalizeBlock{}, nil) cs1, vss := makeState(ctx, t, makeStateArgs{config: config, application: m}) stateData := cs1.GetStateData() @@ -2452,17 +2469,7 @@ func TestPrepareProposalReceivesVoteExtensions(t *testing.T) { signAddVotes(ctx, t, cs1, tmproto.PrevoteType, config.ChainID(), blockID, vss[1:]...) // create a precommit for each validator with the associated vote extension. - for _, vs := range vss[1:] { - voteExtensions := types.VoteExtensions{ - tmproto.VoteExtensionType_DEFAULT: []types.VoteExtension{ - {Extension: []byte("extension")}, - }, - tmproto.VoteExtensionType_THRESHOLD_RECOVER: []types.VoteExtension{ - {Extension: []byte("deterministic")}, - }, - } - signAddPrecommitWithExtension(ctx, t, cs1, config.ChainID(), blockID, voteExtensions, vs) - } + signAddPrecommitsWithExtension(ctx, t, cs1, config.ChainID(), blockID, voteExtensions, vss[1:]...) ensurePrevote(t, voteCh, height, round) @@ -2482,9 +2489,7 @@ func TestPrepareProposalReceivesVoteExtensions(t *testing.T) { ensureNewRound(t, newRoundCh, height, round) ensureNewProposal(t, proposalCh, height, round) - // ensure that the proposer received the list of vote extensions from the - // previous height. - require.Len(t, rpp.LocalLastCommit.ThresholdVoteExtensions, 1) + m.AssertExpectations(t) } // 4 vals, 3 Nil Precommits at P0 @@ -3281,18 +3286,28 @@ func subscribe( return ch } -func signAddPrecommitWithExtension(ctx context.Context, +func signAddPrecommitsWithExtension(ctx context.Context, t *testing.T, cs *State, chainID string, blockID types.BlockID, extensions types.VoteExtensions, - stub *validatorStub) { + vss ...*validatorStub) { _, valSet := cs.GetValidatorSet() - v, err := stub.signVote(ctx, tmproto.PrecommitType, chainID, blockID, valSet.QuorumType, - valSet.QuorumHash, extensions) - require.NoError(t, err, "failed to sign vote") - addVotes(cs, v) + votes := make([]*types.Vote, 0, len(vss)) + + for _, vs := range vss { + v, err := vs.signVote(ctx, tmproto.PrecommitType, chainID, blockID, valSet.QuorumType, + valSet.QuorumHash, extensions.Copy()) + require.NoError(t, err, "failed to sign vote") + vs.lastVote = v + votes = append(votes, v) + + protx, _ := vs.GetProTxHash(ctx) + q, _ := vs.GetPubKey(ctx, valSet.QuorumHash) + t.Logf("signAddPrecommitsWithExtension: pubkey: %X, sigs %X, val protxhash(%d): %X\n", q.Bytes(), v.VoteExtensions.GetSignatures(), vs.Index, protx) + } + addVotes(cs, votes...) } // mockProposerApplicationCalls configures mock Application `m` to support calls executed for each round on the proposer. @@ -3321,7 +3336,7 @@ func mockProposerApplicationCalls(t *testing.T, m *abcimocks.Application, round m.On("ExtendVote", mock.Anything, roundMatcher). Return(&abci.ResponseExtendVote{ VoteExtensions: []*abci.ExtendVoteExtension{{ - Type: tmproto.VoteExtensionType_DEFAULT, + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, Extension: []byte("extension"), }}, }, nil).Once() diff --git a/internal/consensus/types/height_vote_set_test.go b/internal/consensus/types/height_vote_set_test.go index 19223279b3..b65ab9ed6a 100644 --- a/internal/consensus/types/height_vote_set_test.go +++ b/internal/consensus/types/height_vote_set_test.go @@ -87,7 +87,7 @@ func makeVoteHR( require.NoError(t, err, "Error signing vote") vote.BlockSignature = v.BlockSignature - err = vote.VoteExtensions.CopySignsFromProto(v.VoteExtensionsToMap()) + err = vote.VoteExtensions.CopySignsFromProto(v.VoteExtensions) require.NoError(t, err) return vote diff --git a/internal/consensus/vote_signer_test.go b/internal/consensus/vote_signer_test.go index c9b2369691..614352da39 100644 --- a/internal/consensus/vote_signer_test.go +++ b/internal/consensus/vote_signer_test.go @@ -39,11 +39,11 @@ func TestVoteSigner_signAddVote(t *testing.T) { PrivValidator: priVals[0], ProTxHash: proTxHash, } - voteExtensions := types.VoteExtensions{ - tmproto.VoteExtensionType_THRESHOLD_RECOVER: []types.VoteExtension{ - {Extension: tmbytes.MustHexDecode("524F1D03D1D81E94A099042736D40BD9681B867321443FF58A4568E274DBD83B")}, - }, - } + voteExtensions := tmproto.VoteExtensions{{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: tmbytes.MustHexDecode("524F1D03D1D81E94A099042736D40BD9681B867321443FF58A4568E274DBD83B"), + }} + conf := configSetup(t) stateData := StateData{ config: conf.Consensus, @@ -103,7 +103,7 @@ func TestVoteSigner_signAddVote(t *testing.T) { { msgType: tmproto.PrecommitType, blockID: blockID, - voteExtensions: voteExtensions, + voteExtensions: types.VoteExtensionsFromProto(voteExtensions...), mockFn: mockFn, wantBlockSign: "9755FA9803D98C344CB16A43B782D2A93ED9A7E7E1C8437482F42781D5EF802EC82442C14C44429737A7355B1F9D87CB139EB2CF193A1CF7C812E38B99221ADF4DAA60CE16550ED6509A9C467A3D4492D77038505235796968465337A1E14B3E", }, @@ -138,11 +138,14 @@ func TestVoteSigner_signAddVote(t *testing.T) { key, err := privVal.GetPubKey(ctx, valSet.QuorumHash) assert.NoError(t, err) + for _, ext := range vote.VoteExtensions { + assert.NotEmpty(t, ext.GetSignature()) + } + key1, err := bls.G1ElementFromBytes(key.Bytes()) assert.NoError(t, err) t.Logf("key: %x", key1.Serialize()) - t.Logf("%+v", vote.VoteExtensions[tmproto.VoteExtensionType_THRESHOLD_RECOVER]) }) } } diff --git a/internal/evidence/pool_test.go b/internal/evidence/pool_test.go index 40f605e444..d243ef8aff 100644 --- a/internal/evidence/pool_test.go +++ b/internal/evidence/pool_test.go @@ -562,6 +562,7 @@ func makeCommit(height int64, quorumHash []byte) *types.Commit { height, 0, types.BlockID{}, + nil, &types.CommitSigns{ QuorumSigns: types.QuorumSigns{ BlockSign: crypto.CRandBytes(types.SignatureSize), diff --git a/internal/state/execution.go b/internal/state/execution.go index e1d7bd220c..463474da59 100644 --- a/internal/state/execution.go +++ b/internal/state/execution.go @@ -615,7 +615,7 @@ func buildLastCommitInfo(block *types.Block, initialHeight int64) abci.CommitInf Round: block.LastCommit.Round, QuorumHash: block.LastCommit.QuorumHash, BlockSignature: block.LastCommit.ThresholdBlockSignature, - ThresholdVoteExtensions: types.ThresholdExtensionSignToProto(block.LastCommit.ThresholdVoteExtensions), + ThresholdVoteExtensions: block.LastCommit.ThresholdVoteExtensions, } } diff --git a/internal/state/execution_test.go b/internal/state/execution_test.go index 41f6673cc7..fe890f927f 100644 --- a/internal/state/execution_test.go +++ b/internal/state/execution_test.go @@ -236,7 +236,7 @@ func TestProcessProposal(t *testing.T) { Signature: make([]byte, bls12381.SignatureSize), } - lastCommit := types.NewCommit(height-1, 0, types.BlockID{}, nil) + lastCommit := types.NewCommit(height-1, 0, types.BlockID{}, nil, nil) block1, err := sf.MakeBlock(state, height, lastCommit, 1) require.NoError(t, err) block1.SetCoreChainLock(&coreChainLockUpdate) @@ -316,7 +316,7 @@ func TestUpdateConsensusParams(t *testing.T) { eventBus, ) - lastCommit := types.NewCommit(height-1, 0, types.BlockID{}, nil) + lastCommit := types.NewCommit(height-1, 0, types.BlockID{}, nil, nil) txResults := factory.ExecTxResults(txs) app.On("PrepareProposal", mock.Anything, mock.Anything).Return(&abci.ResponsePrepareProposal{ @@ -601,7 +601,7 @@ func TestFinalizeBlockValidatorUpdates(t *testing.T) { 1, round, state, - types.NewCommit(state.LastBlockHeight, 0, state.LastBlockID, nil), + types.NewCommit(state.LastBlockHeight, 0, state.LastBlockID, nil, nil), proTxHashes[0], 1, ) diff --git a/internal/state/helpers_test.go b/internal/state/helpers_test.go index aeba7f965b..3c5593a478 100644 --- a/internal/state/helpers_test.go +++ b/internal/state/helpers_test.go @@ -102,6 +102,7 @@ func makeValidCommit( return types.NewCommit( height, 0, blockID, + votes[0].VoteExtensions, &types.CommitSigns{ QuorumSigns: *thresholdSigns, QuorumHash: vals.QuorumHash, diff --git a/internal/state/test/factory/block.go b/internal/state/test/factory/block.go index d6ff4f237a..feff7ec174 100644 --- a/internal/state/test/factory/block.go +++ b/internal/state/test/factory/block.go @@ -84,7 +84,11 @@ func makeBlockAndPartSet( t.Helper() quorumSigns := &types.CommitSigns{QuorumHash: state.LastValidators.QuorumHash} - lastCommit := types.NewCommit(height-1, 0, types.BlockID{}, quorumSigns) + var ve types.VoteExtensions + if lastBlock != nil && lastBlock.LastCommit != nil { + ve = types.VoteExtensionsFromProto(lastBlock.LastCommit.ThresholdVoteExtensions...) + } + lastCommit := types.NewCommit(height-1, 0, types.BlockID{}, ve, quorumSigns) if height > 1 { var err error votes := make([]*types.Vote, len(privVals)) @@ -106,6 +110,7 @@ func makeBlockAndPartSet( lastBlock.Header.Height, 0, lastBlockMeta.BlockID, + types.VoteExtensionsFromProto(lastBlock.LastCommit.ThresholdVoteExtensions...), &types.CommitSigns{ QuorumSigns: *thresholdSigns, QuorumHash: state.LastValidators.QuorumHash, diff --git a/internal/state/validation_test.go b/internal/state/validation_test.go index 281ac4eb7e..d3bab08967 100644 --- a/internal/state/validation_test.go +++ b/internal/state/validation_test.go @@ -78,7 +78,7 @@ func TestValidateBlockHeader(t *testing.T) { }) require.NoError(t, err) - lastCommit := types.NewCommit(0, 0, types.BlockID{}, nil) + lastCommit := types.NewCommit(0, 0, types.BlockID{}, nil, nil) // some bad values wrongHash := crypto.Checksum([]byte("this hash is wrong")) @@ -206,8 +206,8 @@ func TestValidateBlockCommit(t *testing.T) { blockStore, eventBus, ) - lastCommit := types.NewCommit(0, 0, types.BlockID{}, nil) - wrongVoteMessageSignedCommit := types.NewCommit(1, 0, types.BlockID{}, nil) + lastCommit := types.NewCommit(0, 0, types.BlockID{}, nil, nil) + wrongVoteMessageSignedCommit := types.NewCommit(1, 0, types.BlockID{}, nil, nil) badPrivValQuorumHash := crypto.RandQuorumHash() badPrivVal := types.NewMockPVForQuorum(badPrivValQuorumHash) @@ -235,6 +235,7 @@ func TestValidateBlockCommit(t *testing.T) { wrongHeightVote.Height, wrongHeightVote.Round, state.LastBlockID, + wrongHeightVote.VoteExtensions, &types.CommitSigns{ QuorumSigns: *thresholdSigns, QuorumHash: state.Validators.QuorumHash, @@ -328,8 +329,8 @@ func TestValidateBlockCommit(t *testing.T) { require.NoError(t, err, "height %d", height) goodVote.BlockSignature, badVote.BlockSignature = g.BlockSignature, b.BlockSignature - goodVote.VoteExtensions = types.VoteExtensionsFromProto(g.VoteExtensions) - badVote.VoteExtensions = types.VoteExtensionsFromProto(b.VoteExtensions) + goodVote.VoteExtensions = types.VoteExtensionsFromProto(g.VoteExtensions...) + badVote.VoteExtensions = types.VoteExtensionsFromProto(b.VoteExtensions...) thresholdSigns, err := types.NewSignsRecoverer([]*types.Vote{badVote}).Recover() require.NoError(t, err) @@ -338,7 +339,7 @@ func TestValidateBlockCommit(t *testing.T) { QuorumHash: state.Validators.QuorumHash, } wrongVoteMessageSignedCommit = types.NewCommit(goodVote.Height, goodVote.Round, - blockID, quorumSigns) + blockID, goodVote.VoteExtensions, quorumSigns) } } @@ -385,7 +386,7 @@ func TestValidateBlockEvidence(t *testing.T) { blockStore, eventBus, ) - lastCommit := types.NewCommit(0, 0, types.BlockID{}, nil) + lastCommit := types.NewCommit(0, 0, types.BlockID{}, nil, nil) for height := int64(1); height < validationTestsStopHeight; height++ { proposerProTxHash := state.Validators.GetProposer().ProTxHash diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 0c02455064..f7dbbaa844 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -52,7 +52,7 @@ func makeTestCommit(state sm.State, height int64) *types.Commit { _ = privVal.SignVote(context.Background(), "chainID", state.Validators.QuorumType, state.Validators.QuorumHash, g, nil) goodVote.BlockSignature = g.BlockSignature - goodVote.VoteExtensions = types.VoteExtensionsFromProto(g.VoteExtensions) + goodVote.VoteExtensions = types.VoteExtensionsFromProto(g.VoteExtensions...) thresholdSigns, _ := types.NewSignsRecoverer([]*types.Vote{goodVote}).Recover() return types.NewCommit(height, 0, @@ -61,6 +61,7 @@ func makeTestCommit(state sm.State, height int64) *types.Commit { PartSetHeader: types.PartSetHeader{Hash: []byte(""), Total: 2}, StateID: []byte{}, }, + goodVote.VoteExtensions, &types.CommitSigns{ QuorumSigns: *thresholdSigns, QuorumHash: crypto.RandQuorumHash(), diff --git a/internal/test/factory/commit.go b/internal/test/factory/commit.go index 385c8e03d6..d93898552d 100644 --- a/internal/test/factory/commit.go +++ b/internal/test/factory/commit.go @@ -37,10 +37,12 @@ func MakeCommit( return nil, err } vote.BlockSignature = v.BlockSignature - err = vote.VoteExtensions.CopySignsFromProto(v.VoteExtensionsToMap()) + + err = vote.VoteExtensions.CopySignsFromProto(v.VoteExtensions) if err != nil { return nil, err } + if _, err := voteSet.AddVote(vote); err != nil { return nil, err } diff --git a/internal/test/factory/vote.go b/internal/test/factory/vote.go index 40c0a33c40..7a5110858f 100644 --- a/internal/test/factory/vote.go +++ b/internal/test/factory/vote.go @@ -39,8 +39,10 @@ func MakeVote( } v.BlockSignature = vpb.BlockSignature - err = v.VoteExtensions.CopySignsFromProto(vpb.VoteExtensionsToMap()) + + err = v.VoteExtensions.CopySignsFromProto(vpb.VoteExtensions) if err != nil { + return nil, err } return v, nil diff --git a/light/helpers_test.go b/light/helpers_test.go index 4b68cfc953..9ff02e0ca0 100644 --- a/light/helpers_test.go +++ b/light/helpers_test.go @@ -87,7 +87,7 @@ func (pkz privKeys) signHeader(t testing.TB, header *types.Header, valSet *types QuorumSigns: *thresholdSigns, QuorumHash: valSet.QuorumHash, } - return types.NewCommit(header.Height, 1, blockID, quorumSigns) + return types.NewCommit(header.Height, 1, blockID, votes[0].VoteExtensions, quorumSigns) } func makeVote(t testing.TB, header *types.Header, valset *types.ValidatorSet, proTxHash crypto.ProTxHash, diff --git a/node/node_test.go b/node/node_test.go index cba1e308ed..43b526bd76 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -378,7 +378,7 @@ func TestCreateProposalBlock(t *testing.T) { ) proposedAppVersion := uint64(1) - commit := types.NewCommit(height-1, 0, types.BlockID{}, nil) + commit := types.NewCommit(height-1, 0, types.BlockID{}, nil, nil) block, _, err := blockExec.CreateProposalBlock( ctx, height, @@ -466,7 +466,7 @@ func TestMaxTxsProposalBlockSize(t *testing.T) { eventBus, ) - commit := types.NewCommit(height-1, 0, types.BlockID{}, nil) + commit := types.NewCommit(height-1, 0, types.BlockID{}, nil, nil) block, _, err := blockExec.CreateProposalBlock( ctx, height, diff --git a/privval/dash_consensus_key.go b/privval/dash_consensus_key.go index 5eb6a909d3..60a976c8bb 100644 --- a/privval/dash_consensus_key.go +++ b/privval/dash_consensus_key.go @@ -9,7 +9,7 @@ import ( "github.com/dashpay/dashd-go/btcjson" tmcrypto "github.com/dashpay/tenderdash/crypto" - tmbytes "github.com/dashpay/tenderdash/libs/bytes" + "github.com/dashpay/tenderdash/types" ) type dashConsensusPrivateKey struct { @@ -105,12 +105,7 @@ func (pub DashConsensusPublicKey) VerifySignature(msg []byte, sig []byte) bool { return pub.VerifySignatureDigest(hash, sig) } func (pub DashConsensusPublicKey) VerifySignatureDigest(hash []byte, sig []byte) bool { - signID := tmcrypto.SignID( - pub.quorumType, - tmbytes.Reverse(pub.quorumHash), - tmbytes.Reverse(hash[:]), - tmbytes.Reverse(hash[:]), - ) + signID := types.NewSignItemFromHash(pub.quorumType, pub.quorumHash, hash, hash).SignHash return pub.PubKey.VerifySignatureDigest(signID, sig) } diff --git a/privval/dash_core_signer_client.go b/privval/dash_core_signer_client.go index c8bc136bdc..a2f183d169 100644 --- a/privval/dash_core_signer_client.go +++ b/privval/dash_core_signer_client.go @@ -275,10 +275,10 @@ func (sc *DashCoreSignerClient) SignVote( "voteType", protoVote.Type, "quorumType", quorumType, "quorumHash", quorumHash, - "signature", qs.sign, + "signature", hex.EncodeToString(qs.sign), "proTxHash", proTxHash, "coreBlockRequestId", qs.ID, - "coreSignId", tmbytes.Reverse(qs.signHash), + "coreSignId", hex.EncodeToString(tmbytes.Reverse(qs.signHash)), "signItem", quorumSigns, "signResult", qs, ) @@ -314,11 +314,8 @@ func (sc *DashCoreSignerClient) QuorumSign( quorumType btcjson.LLMQType, quorumHash crypto.QuorumHash, ) ([]byte, []byte, error) { - signItem := types.SignItem{ - ReqID: requestIDHash, - ID: types.MakeSignID(msgHash, requestIDHash, quorumType, quorumHash), - Hash: msgHash, - } + signItem := types.NewSignItemFromHash(quorumType, quorumHash, requestIDHash, msgHash) + qs, err := sc.quorumSignAndVerify(ctx, quorumType, quorumHash, signItem) if err != nil { return nil, nil, err @@ -372,22 +369,27 @@ func (sc *DashCoreSignerClient) signVoteExtensions( protoVote *tmproto.Vote, quorumSignData types.QuorumSignData, ) error { + sc.logger.Trace("signing vote extensions", "vote", protoVote) + if protoVote.Type != tmproto.PrecommitType { if len(protoVote.VoteExtensions) > 0 { return errors.New("unexpected vote extension - extensions are only allowed in precommits") } return nil } - for et, extensions := range protoVote.VoteExtensionsToMap() { - for i, ext := range extensions { - signItem := quorumSignData.Extensions[et][i] - resp, err := sc.quorumSignAndVerify(ctx, quorumType, quorumHash, signItem) - if err != nil { - return err - } - ext.Signature = resp.sign + + for i, ext := range quorumSignData.VoteExtensionSignItems { + signItem := ext + resp, err := sc.quorumSignAndVerify(ctx, quorumType, quorumHash, signItem) + if err != nil { + return err } + + protoVote.VoteExtensions[i].Signature = resp.sign } + + sc.logger.Trace("vote extensions signed", "extensions", protoVote.VoteExtensions) + return nil } @@ -404,16 +406,16 @@ func (sc *DashCoreSignerClient) quorumSignAndVerify( sc.logger.Trace("quorum sign result", "sign", hex.EncodeToString(qs.sign), "sign_hash", hex.EncodeToString(qs.signHash), - "req_id", hex.EncodeToString(signItem.ReqID), - "id", hex.EncodeToString(signItem.ID), - "raw", hex.EncodeToString(signItem.Raw), - "hash", hex.EncodeToString(signItem.Hash), + "req_id", hex.EncodeToString(signItem.ID), + "id", hex.EncodeToString(signItem.SignHash), + "raw", hex.EncodeToString(signItem.Msg), + "hash", hex.EncodeToString(signItem.MsgHash), "quorum_sign_result", *qs.QuorumSignResult) pubKey, err := sc.GetPubKey(ctx, quorumHash) if err != nil { return nil, &RemoteSignerError{Code: 500, Description: err.Error()} } - verified := pubKey.VerifySignatureDigest(signItem.ID, qs.sign) + verified := pubKey.VerifySignatureDigest(signItem.SignHash, qs.sign) if !verified { return nil, fmt.Errorf("unable to verify signature with pubkey %s", pubKey.String()) } @@ -425,7 +427,7 @@ func (sc *DashCoreSignerClient) quorumSign( quorumHash crypto.QuorumHash, signItem types.SignItem, ) (*quorumSignResult, error) { - resp, err := sc.dashCoreRPCClient.QuorumSign(quorumType, signItem.ReqID, signItem.Hash, quorumHash) + resp, err := sc.dashCoreRPCClient.QuorumSign(quorumType, signItem.ID, signItem.MsgHash, quorumHash) if err != nil { return nil, &RemoteSignerError{Code: 500, Description: "cannot sign vote: " + err.Error()} } diff --git a/privval/file.go b/privval/file.go index 266bb51b95..29f15b5462 100644 --- a/privval/file.go +++ b/privval/file.go @@ -686,16 +686,14 @@ func (pv *FilePV) signVote( // application may have created a different extension. We therefore always // re-sign the vote extensions of precommits. For prevotes, the extension // signature will always be empty. - extSigns := make(map[tmproto.VoteExtensionType][]tmbytes.HexBytes) + extSigns := make([]tmbytes.HexBytes, 0, len(quorumSigns.VoteExtensionSignItems)) if vote.Type == tmproto.PrecommitType { - for et, signItems := range quorumSigns.Extensions { - for _, signItem := range signItems { - extSig, err := privKey.SignDigest(signItem.ID) - if err != nil { - return err - } - extSigns[et] = append(extSigns[et], extSig) + for _, signItem := range quorumSigns.VoteExtensionSignItems { + extSig, err := privKey.SignDigest(signItem.SignHash) + if err != nil { + return err } + extSigns = append(extSigns, extSig) } } else if len(vote.VoteExtensions) > 0 { return errors.New("unexpected vote extension - extensions are only allowed in precommits") @@ -707,16 +705,16 @@ func (pv *FilePV) signVote( // If they only differ by timestamp, use last timestamp and signature // Otherwise, return error if sameHRS { - if bytes.Equal(quorumSigns.Block.Raw, lss.BlockSignBytes) { + if bytes.Equal(quorumSigns.Block.Msg, lss.BlockSignBytes) { vote.BlockSignature = lss.BlockSignature } else { return errors.New("conflicting data") } - fillProtoVoteExtensionSigns(vote.VoteExtensionsToMap(), extSigns) + fillProtoVoteExtensionSigns(vote.VoteExtensions, extSigns) return nil } - sigBlock, err := privKey.SignDigest(quorumSigns.Block.ID) + sigBlock, err := privKey.SignDigest(quorumSigns.Block.SignHash) if err != nil { return err } @@ -729,13 +727,13 @@ func (pv *FilePV) signVote( // sigBlock, vote) // } - err = pv.saveSigned(height, round, step, quorumSigns.Block.Raw, sigBlock) + err = pv.saveSigned(height, round, step, quorumSigns.Block.Msg, sigBlock) if err != nil { return err } vote.BlockSignature = sigBlock - fillProtoVoteExtensionSigns(vote.VoteExtensionsToMap(), extSigns) + fillProtoVoteExtensionSigns(vote.VoteExtensions, extSigns) return nil } @@ -815,12 +813,12 @@ func (pv *FilePV) saveSigned( } func fillProtoVoteExtensionSigns( - voteExtensions map[tmproto.VoteExtensionType][]*tmproto.VoteExtension, - signs map[tmproto.VoteExtensionType][]tmbytes.HexBytes, + extensions []*tmproto.VoteExtension, + signs []tmbytes.HexBytes, ) { - for et, extensions := range voteExtensions { - for i, ext := range extensions { - ext.Signature = signs[et][i] - } + + for i, ext := range extensions { + ext.Signature = signs[i] } + } diff --git a/privval/file_test.go b/privval/file_test.go index 2a89fa1925..ef51f62482 100644 --- a/privval/file_test.go +++ b/privval/file_test.go @@ -238,7 +238,7 @@ func TestSignVote(t *testing.T) { } for _, c := range cases { - assert.Error(t, privVal.SignVote(ctx, "mychainid", 0, crypto.QuorumHash{}, c.ToProto(), nil), + assert.Error(t, privVal.SignVote(ctx, "mychainid", 0, quorumHash, c.ToProto(), nil), "expected error on signing conflicting vote") } @@ -289,7 +289,7 @@ func TestSignProposal(t *testing.T) { } for _, c := range cases { - _, err = privVal.SignProposal(ctx, "mychainid", 0, crypto.QuorumHash{}, c.ToProto()) + _, err = privVal.SignProposal(ctx, "mychainid", 0, quorumHash, c.ToProto()) assert.Error(t, err, "expected error on signing conflicting proposal") } } @@ -359,9 +359,11 @@ func TestVoteExtensionsAreAlwaysSigned(t *testing.T) { } voteType := tmproto.PrecommitType - exts := types.VoteExtensions{ - tmproto.VoteExtensionType_DEFAULT: []types.VoteExtension{{Extension: []byte("extension")}}, - } + exts := types.VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("extension"), + }) + // We initially sign this vote without an extension vote1 := newVote(proTxHash, 0, height, round, voteType, blockID, exts) vpb1 := vote1.ToProto() @@ -370,16 +372,17 @@ func TestVoteExtensionsAreAlwaysSigned(t *testing.T) { assert.NoError(t, err, "expected no error signing vote") assert.NotNil(t, vpb1.VoteExtensions[0].Signature) - extSignItem1, err := types.MakeVoteExtensionSignItems(chainID, vpb1, quorumType, quorumHash) + extSignItem1, err := types.VoteExtensionsFromProto(vpb1.VoteExtensions...).SignItems(chainID, quorumType, quorumHash, vpb1.Height, vpb1.Round) require.NoError(t, err) - assert.True(t, pubKey.VerifySignatureDigest(extSignItem1[tmproto.VoteExtensionType_DEFAULT][0].ID, vpb1.VoteExtensions[0].Signature)) + assert.True(t, pubKey.VerifySignatureDigest(extSignItem1[0].SignHash, vpb1.VoteExtensions[0].Signature)) // We duplicate this vote precisely, including its timestamp, but change // its extension vote2 := vote1.Copy() - vote2.VoteExtensions = types.VoteExtensions{ - tmproto.VoteExtensionType_DEFAULT: []types.VoteExtension{{Extension: []byte("new extension")}}, - } + vote2.VoteExtensions = types.VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("new extension")}) + vpb2 := vote2.ToProto() err = privVal.SignVote(ctx, chainID, quorumType, quorumHash, vpb2, logger) @@ -389,10 +392,10 @@ func TestVoteExtensionsAreAlwaysSigned(t *testing.T) { // that validates against the vote extension sign bytes with the new // extension, and does not validate against the vote extension sign bytes // with the old extension. - extSignItem2, err := types.MakeVoteExtensionSignItems(chainID, vpb2, quorumType, quorumHash) + extSignItem2, err := types.VoteExtensionsFromProto(vpb2.VoteExtensions...).SignItems(chainID, quorumType, quorumHash, vpb2.Height, vpb2.Round) require.NoError(t, err) - assert.True(t, pubKey.VerifySignatureDigest(extSignItem2[tmproto.VoteExtensionType_DEFAULT][0].ID, vpb2.VoteExtensions[0].Signature)) - assert.False(t, pubKey.VerifySignatureDigest(extSignItem1[tmproto.VoteExtensionType_DEFAULT][0].ID, vpb2.VoteExtensions[0].Signature)) + assert.True(t, pubKey.VerifySignatureDigest(extSignItem2[0].SignHash, vpb2.VoteExtensions[0].Signature)) + assert.False(t, pubKey.VerifySignatureDigest(extSignItem1[0].SignHash, vpb2.VoteExtensions[0].Signature)) vpb2.BlockSignature = nil vpb2.VoteExtensions[0].Signature = nil @@ -400,10 +403,10 @@ func TestVoteExtensionsAreAlwaysSigned(t *testing.T) { err = privVal.SignVote(ctx, chainID, quorumType, quorumHash, vpb2, logger) assert.NoError(t, err, "expected no error signing same vote with manipulated timestamp and vote extension") - extSignItem3, err := types.MakeVoteExtensionSignItems(chainID, vpb2, quorumType, quorumHash) + extSignItem3, err := types.VoteExtensionsFromProto(vpb2.VoteExtensions...).SignItems(chainID, quorumType, quorumHash, vpb2.Height, vpb2.Round) require.NoError(t, err) - assert.True(t, pubKey.VerifySignatureDigest(extSignItem3[tmproto.VoteExtensionType_DEFAULT][0].ID, vpb2.VoteExtensions[0].Signature)) - assert.False(t, pubKey.VerifySignatureDigest(extSignItem1[tmproto.VoteExtensionType_DEFAULT][0].ID, vpb2.VoteExtensions[0].Signature)) + assert.True(t, pubKey.VerifySignatureDigest(extSignItem3[0].SignHash, vpb2.VoteExtensions[0].Signature)) + assert.False(t, pubKey.VerifySignatureDigest(extSignItem1[0].SignHash, vpb2.VoteExtensions[0].Signature)) } func newVote(proTxHash types.ProTxHash, idx int32, height int64, round int32, diff --git a/privval/msgs_test.go b/privval/msgs_test.go index bbb4d5f339..32ba1354e3 100644 --- a/privval/msgs_test.go +++ b/privval/msgs_test.go @@ -44,9 +44,10 @@ func exampleVote() *types.Vote { }, ValidatorProTxHash: crypto.ProTxHashFromSeedBytes([]byte("validator_pro_tx_hash")), ValidatorIndex: 56789, - VoteExtensions: types.VoteExtensions{ - tmproto.VoteExtensionType_DEFAULT: []types.VoteExtension{{Extension: []byte("extension")}}, - }, + VoteExtensions: types.VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("extension"), + }), } } @@ -95,8 +96,8 @@ func TestPrivvalVectors(t *testing.T) { {"pubKey request", &privproto.PubKeyRequest{}, "0a00"}, {"pubKey response", &privproto.PubKeyResponse{PubKey: ppk, Error: nil}, "12340a321a30991a1c4f159f8e4730bf897e97e27c11f27ba0c1337111a3c102e1081a19372832b596623b1a248a0e00b156d80690cf"}, {"pubKey response with error", &privproto.PubKeyResponse{PubKey: cryptoproto.PublicKey{}, Error: remoteError}, "12140a0012100801120c697427732061206572726f72"}, - {"Vote Request", &privproto.SignVoteRequest{Vote: votepb}, "1aaa010aa701080210031802226c0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a1a20b583d49b95a0a5526966b519d8b7bba2aefc800b370a7438c7063728904e58ee2a20959a8f5ef2be68d0ed3a07ed8cff85991ee7995c2ac17030f742c135f9729fbe30d5bb03420b1209657874656e73696f6e"}, - {"Vote Response", &privproto.SignedVoteResponse{Vote: *votepb, Error: nil}, "22aa010aa701080210031802226c0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a1a20b583d49b95a0a5526966b519d8b7bba2aefc800b370a7438c7063728904e58ee2a20959a8f5ef2be68d0ed3a07ed8cff85991ee7995c2ac17030f742c135f9729fbe30d5bb03420b1209657874656e73696f6e"}, + {"Vote Request", &privproto.SignVoteRequest{Vote: votepb}, "1aac010aa901080210031802226c0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a1a20b583d49b95a0a5526966b519d8b7bba2aefc800b370a7438c7063728904e58ee2a20959a8f5ef2be68d0ed3a07ed8cff85991ee7995c2ac17030f742c135f9729fbe30d5bb03420d08011209657874656e73696f6e"}, + {"Vote Response", &privproto.SignedVoteResponse{Vote: *votepb, Error: nil}, "22ac010aa901080210031802226c0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a1a20b583d49b95a0a5526966b519d8b7bba2aefc800b370a7438c7063728904e58ee2a20959a8f5ef2be68d0ed3a07ed8cff85991ee7995c2ac17030f742c135f9729fbe30d5bb03420d08011209657874656e73696f6e"}, {"Vote Response with error", &privproto.SignedVoteResponse{Vote: tmproto.Vote{}, Error: remoteError}, "22180a042202120012100801120c697427732061206572726f72"}, {"Proposal Request", &privproto.SignProposalRequest{Proposal: proposalpb}, "2a700a6e08011003180220022a4a0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a320608f49a8ded053a10697427732061207369676e6174757265"}, {"Proposal Response", &privproto.SignedProposalResponse{Proposal: *proposalpb, Error: nil}, "32700a6e08011003180220022a4a0a208b01023386c371778ecb6368573e539afc3cc860ec3a2f614e54fe5652f4fc80122608c0843d122072db3d959635dff1bb567bedaa70573392c5159666a3f8caf11e413aac52207a320608f49a8ded053a10697427732061207369676e6174757265"}, diff --git a/proto/tendermint/abci/types.proto b/proto/tendermint/abci/types.proto index db1b346831..2ee78dee8c 100644 --- a/proto/tendermint/abci/types.proto +++ b/proto/tendermint/abci/types.proto @@ -660,13 +660,27 @@ message ResponseProcessProposal { ValidatorSetUpdate validator_set_update = 5 [(gogoproto.nullable) = true]; } -// Provides a vote extension for signing. Each field is mandatory for filling +// Provides a vote extension for signing. `type` and `extension` fields are mandatory for filling message ExtendVoteExtension { - // Vote extension type can be either DEFAULT or THRESHOLD_RECOVER. - // The Tenderdash supports only THRESHOLD_RECOVER at this moment. + // Vote extension type can be either DEFAULT, THRESHOLD_RECOVER or THRESHOLD_RECOVER_RAW. + // The Tenderdash supports only THRESHOLD_RECOVER and THRESHOLD_RECOVER_RAW at this moment. tendermint.types.VoteExtensionType type = 1; // Deterministic or (Non-Deterministic) extension provided by the sending validator's Application. + // + // For THRESHOLD_RECOVER_RAW, it MUST be 32 bytes. bytes extension = 2; + // Sign request ID that will be used to sign the vote extensions. + // Only applicable for THRESHOLD_RECOVER_RAW vote extension type. + // + // Tenderdash will use SHA256 checksum of `sign_request_id` when generating quorum signatures of + // THRESHOLD_RECOVER_RAW vote extensions. It MUST NOT be set for any other vote extension types. + + // If not set, Tenderdash will generate it based on height and round. + // + // If set, it SHOULD be unique per voting round, and it MUST start with `dpevote` or `\x06plwdtx` prefix. + // + // Use with caution - it can have severe security consequences. + optional bytes sign_request_id = 3; } message ResponseExtendVote { diff --git a/proto/tendermint/types/dash.go b/proto/tendermint/types/dash.go index 46e0f7f41b..7ac0d9939a 100644 --- a/proto/tendermint/types/dash.go +++ b/proto/tendermint/types/dash.go @@ -1,4 +1,152 @@ package types +import ( + "bytes" + "errors" + fmt "fmt" + + "github.com/dashpay/tenderdash/crypto" + "github.com/dashpay/tenderdash/crypto/bls12381" +) + // VoteExtensions is a container type for grouped vote extensions by type -type VoteExtensions map[VoteExtensionType][]*VoteExtension +type VoteExtensions []*VoteExtension + +var ( + errExtensionNil = errors.New("vote extension is nil") + errExtensionSignEmpty = errors.New("vote extension signature is missing") + errExtensionSignTooBig = fmt.Errorf("vote extension signature is too big (max: %d)", bls12381.SignatureSize) + errExtensionSignRequestIDNotSupported = errors.New("vote extension sign request id is not supported") + errExtensionSignRequestIDWrongPrefix = errors.New("vote extension sign request id must have dpevote or \\x06plwdtx prefix") +) + +// Clone returns a shallow copy of current vote-extension +// +// Clone of nil will panic + +func (v *VoteExtension) Clone() VoteExtension { + if v == nil { + panic("cannot clone nil vote-extension") + } + + ve := VoteExtension{ + Type: v.Type, + Extension: v.Extension, + Signature: v.Signature, + } + + if v.XSignRequestId != nil && v.XSignRequestId.Size() > 0 { + ve.XSignRequestId = &VoteExtension_SignRequestId{ + SignRequestId: v.GetSignRequestId(), + } + } + + return ve +} + +// Copy returns a deep copy of current vote-extension. +func (v *VoteExtension) Copy() VoteExtension { + if v == nil { + panic("cannot copy nil vote-extension") + } + + ve := VoteExtension{ + Type: v.Type, + Extension: bytes.Clone(v.Extension), + Signature: bytes.Clone(v.Signature), + } + + if v.XSignRequestId != nil && v.XSignRequestId.Size() > 0 { + ve.XSignRequestId = &VoteExtension_SignRequestId{ + SignRequestId: bytes.Clone(v.GetSignRequestId()), + } + } + + return ve +} + +func (v *VoteExtension) Equal(other *VoteExtension) bool { + if v == nil || other == nil { + return false + } + + if v.Type != other.Type { + return false + } + + if !bytes.Equal(v.Extension, other.Extension) { + return false + } + + if !bytes.Equal(v.Signature, other.Signature) { + return false + } + + // one of them is nil, but not both + if (v.XSignRequestId != nil) != (other.XSignRequestId != nil) { + return false + } + + if v.XSignRequestId != nil && other.XSignRequestId != nil { + if !bytes.Equal(v.GetSignRequestId(), other.GetSignRequestId()) { + return false + } + } + + return true +} + +// Validate checks the validity of the vote-extension +func (v *VoteExtension) Validate() error { + if v == nil { + return errExtensionNil + } + + if v.Type == VoteExtensionType_DEFAULT { + return fmt.Errorf("vote extension type %s is not supported", v.Type.String()) + } + + if v.Type == VoteExtensionType_THRESHOLD_RECOVER_RAW { + if len(v.Extension) != crypto.HashSize { + return fmt.Errorf("invalid %s vote extension size: got %d, expected %d", + v.Type.String(), len(v.Extension), crypto.HashSize) + } + } + + if len(v.Extension) > 0 && len(v.Signature) == 0 { + return errExtensionSignEmpty + } + if len(v.Signature) > bls12381.SignatureSize { + return errExtensionSignTooBig + } + + if v.XSignRequestId != nil && v.XSignRequestId.Size() > 0 { + if v.Type != VoteExtensionType_THRESHOLD_RECOVER_RAW { + return errExtensionSignRequestIDNotSupported + } + var validPrefixes = []string{"\x06plwdtx", "dpevote"} + requestID := v.GetSignRequestId() + + var validPrefix bool + for _, prefix := range validPrefixes { + if bytes.HasPrefix(requestID, []byte(prefix)) { + validPrefix = true + break + } + } + + if !validPrefix { + return errExtensionSignRequestIDWrongPrefix + } + } + + return nil +} +func (v VoteExtensions) Contains(other VoteExtension) bool { + for _, ext := range v { + if ext.Equal(&other) { + return true + } + } + return false +} diff --git a/proto/tendermint/types/dash.pb.go b/proto/tendermint/types/dash.pb.go index 661526d33a..ad693ce5ab 100644 --- a/proto/tendermint/types/dash.pb.go +++ b/proto/tendermint/types/dash.pb.go @@ -29,18 +29,34 @@ type VoteExtensionType int32 const ( // Unsupported VoteExtensionType_DEFAULT VoteExtensionType = 0 + // Sign canonical form of vote extension and threshold-recover signatures. + // // Deterministic vote extension - each validator in a quorum must provide the same vote extension data. VoteExtensionType_THRESHOLD_RECOVER VoteExtensionType = 1 + // Sign raw form of vote extension and threshold-recover signatures. + // + // Deterministic vote extension - each validator in a quorum must provide the same vote extension data. + // Use with caution - it can have severe security consequences, like replay attacks. + // + // THRESHOLD_RECOVER_RAW alows overriding sign request ID with `sign_request_id` field + // of ExtendVoteExtension.sign_request_id. If sign_request_id is provided, SHA256(sign_request_id) will be used as + // a sign request ID. + // + // It also changes how threshold-recover signatures are generated. Instead of signing canonical form of + // threshold-recover signatures, it signs SHA256 of raw form of the vote extension (`ExtendVoteExtension.extension`). + VoteExtensionType_THRESHOLD_RECOVER_RAW VoteExtensionType = 2 ) var VoteExtensionType_name = map[int32]string{ 0: "DEFAULT", 1: "THRESHOLD_RECOVER", + 2: "THRESHOLD_RECOVER_RAW", } var VoteExtensionType_value = map[string]int32{ - "DEFAULT": 0, - "THRESHOLD_RECOVER": 1, + "DEFAULT": 0, + "THRESHOLD_RECOVER": 1, + "THRESHOLD_RECOVER_RAW": 2, } func (x VoteExtensionType) String() string { @@ -116,6 +132,9 @@ type VoteExtension struct { Type VoteExtensionType `protobuf:"varint,1,opt,name=type,proto3,enum=tendermint.types.VoteExtensionType" json:"type,omitempty"` Extension []byte `protobuf:"bytes,2,opt,name=extension,proto3" json:"extension,omitempty"` Signature []byte `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` + // Types that are valid to be assigned to XSignRequestId: + // *VoteExtension_SignRequestId + XSignRequestId isVoteExtension_XSignRequestId `protobuf_oneof:"_sign_request_id"` } func (m *VoteExtension) Reset() { *m = VoteExtension{} } @@ -151,6 +170,25 @@ func (m *VoteExtension) XXX_DiscardUnknown() { var xxx_messageInfo_VoteExtension proto.InternalMessageInfo +type isVoteExtension_XSignRequestId interface { + isVoteExtension_XSignRequestId() + MarshalTo([]byte) (int, error) + Size() int +} + +type VoteExtension_SignRequestId struct { + SignRequestId []byte `protobuf:"bytes,4,opt,name=sign_request_id,json=signRequestId,proto3,oneof" json:"sign_request_id,omitempty"` +} + +func (*VoteExtension_SignRequestId) isVoteExtension_XSignRequestId() {} + +func (m *VoteExtension) GetXSignRequestId() isVoteExtension_XSignRequestId { + if m != nil { + return m.XSignRequestId + } + return nil +} + func (m *VoteExtension) GetType() VoteExtensionType { if m != nil { return m.Type @@ -172,6 +210,20 @@ func (m *VoteExtension) GetSignature() []byte { return nil } +func (m *VoteExtension) GetSignRequestId() []byte { + if x, ok := m.GetXSignRequestId().(*VoteExtension_SignRequestId); ok { + return x.SignRequestId + } + return nil +} + +// XXX_OneofWrappers is for the internal use of the proto package. +func (*VoteExtension) XXX_OneofWrappers() []interface{} { + return []interface{}{ + (*VoteExtension_SignRequestId)(nil), + } +} + func init() { proto.RegisterEnum("tendermint.types.VoteExtensionType", VoteExtensionType_name, VoteExtensionType_value) proto.RegisterType((*CoreChainLock)(nil), "tendermint.types.CoreChainLock") @@ -181,28 +233,31 @@ func init() { func init() { proto.RegisterFile("tendermint/types/dash.proto", fileDescriptor_098b09a14a95d15e) } var fileDescriptor_098b09a14a95d15e = []byte{ - // 332 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x2e, 0x49, 0xcd, 0x4b, - 0x49, 0x2d, 0xca, 0xcd, 0xcc, 0x2b, 0xd1, 0x2f, 0xa9, 0x2c, 0x48, 0x2d, 0xd6, 0x4f, 0x49, 0x2c, - 0xce, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x40, 0x48, 0xea, 0x81, 0x25, 0xa5, 0x44, - 0xd2, 0xf3, 0xd3, 0xf3, 0xc1, 0x92, 0xfa, 0x20, 0x16, 0x44, 0x9d, 0x52, 0x3b, 0x23, 0x17, 0xaf, - 0x73, 0x7e, 0x51, 0xaa, 0x73, 0x46, 0x62, 0x66, 0x9e, 0x4f, 0x7e, 0x72, 0xb6, 0x90, 0x16, 0x97, - 0x60, 0x72, 0x7e, 0x51, 0x6a, 0x7c, 0x52, 0x4e, 0x7e, 0x72, 0x76, 0x7c, 0x46, 0x6a, 0x66, 0x7a, - 0x46, 0x89, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x6f, 0x10, 0x3f, 0x48, 0xc2, 0x09, 0x24, 0xee, 0x01, - 0x16, 0x16, 0x52, 0xe3, 0xe2, 0x47, 0x56, 0x9b, 0x58, 0x9c, 0x21, 0xc1, 0xa4, 0xc0, 0xa8, 0xc1, - 0x13, 0xc4, 0x8b, 0x50, 0x99, 0x58, 0x9c, 0x21, 0x24, 0xc3, 0xc5, 0x59, 0x9c, 0x99, 0x9e, 0x97, - 0x58, 0x52, 0x5a, 0x94, 0x2a, 0xc1, 0x0c, 0x56, 0x81, 0x10, 0xb0, 0x62, 0x79, 0xb1, 0x40, 0x9e, - 0x51, 0xa9, 0x85, 0x91, 0x8b, 0x37, 0x2c, 0xbf, 0x24, 0xd5, 0xb5, 0xa2, 0x24, 0x35, 0xaf, 0x38, - 0x33, 0x3f, 0x4f, 0xc8, 0x9c, 0x8b, 0x05, 0xe4, 0x74, 0xb0, 0xe5, 0x7c, 0x46, 0xca, 0x7a, 0xe8, - 0x5e, 0xd2, 0x43, 0x51, 0x1e, 0x52, 0x59, 0x90, 0x1a, 0x04, 0xd6, 0x00, 0xb2, 0x2e, 0x15, 0x26, - 0x0c, 0x75, 0x10, 0x42, 0x00, 0xbf, 0x63, 0xb4, 0xcc, 0xb9, 0x04, 0x31, 0x8c, 0x15, 0xe2, 0xe6, - 0x62, 0x77, 0x71, 0x75, 0x73, 0x0c, 0xf5, 0x09, 0x11, 0x60, 0x10, 0x12, 0xe5, 0x12, 0x0c, 0xf1, - 0x08, 0x72, 0x0d, 0xf6, 0xf0, 0xf7, 0x71, 0x89, 0x0f, 0x72, 0x75, 0xf6, 0x0f, 0x73, 0x0d, 0x12, - 0x60, 0x74, 0xf2, 0x3b, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, 0x18, - 0x27, 0x3c, 0x96, 0x63, 0xb8, 0xf0, 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x28, 0x93, 0xf4, - 0xcc, 0x92, 0x8c, 0xd2, 0x24, 0xbd, 0xe4, 0xfc, 0x5c, 0x70, 0x14, 0x15, 0x24, 0x56, 0xea, 0x43, - 0xfc, 0x02, 0xe2, 0xe9, 0x43, 0xe2, 0x04, 0x3d, 0x32, 0x93, 0xd8, 0xc0, 0xe2, 0xc6, 0x80, 0x00, - 0x00, 0x00, 0xff, 0xff, 0x57, 0xe9, 0x12, 0xcd, 0xe7, 0x01, 0x00, 0x00, + // 384 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x92, 0xcf, 0x4a, 0xe3, 0x40, + 0x1c, 0xc7, 0x33, 0xdd, 0xb2, 0xcb, 0xce, 0x6e, 0xb6, 0xe9, 0xb0, 0x85, 0xf8, 0x87, 0x58, 0x2a, + 0x48, 0xa9, 0x90, 0x80, 0x0a, 0x82, 0xb7, 0xfe, 0x89, 0x44, 0x28, 0x16, 0xc6, 0x5a, 0xc1, 0x4b, + 0x48, 0x93, 0x21, 0x09, 0xb5, 0x99, 0x98, 0x4c, 0xc1, 0x3e, 0x81, 0x1e, 0x7d, 0x04, 0x5f, 0x46, + 0xf0, 0xd8, 0xa3, 0x47, 0x69, 0x2f, 0x3e, 0x86, 0xcc, 0x44, 0x89, 0xb6, 0xe0, 0x2d, 0xf3, 0xf9, + 0x7e, 0xc2, 0xef, 0x9b, 0xfc, 0x06, 0x6e, 0x30, 0x12, 0x79, 0x24, 0x19, 0x87, 0x11, 0x33, 0xd8, + 0x34, 0x26, 0xa9, 0xe1, 0x39, 0x69, 0xa0, 0xc7, 0x09, 0x65, 0x14, 0x29, 0x79, 0xa8, 0x8b, 0x70, + 0xfd, 0xbf, 0x4f, 0x7d, 0x2a, 0x42, 0x83, 0x3f, 0x65, 0x5e, 0xed, 0x16, 0x40, 0xb9, 0x4d, 0x13, + 0xd2, 0x0e, 0x9c, 0x30, 0xea, 0x52, 0x77, 0x84, 0x1a, 0xb0, 0xec, 0xd2, 0x84, 0xd8, 0xc3, 0x2b, + 0xea, 0x8e, 0xec, 0x80, 0x84, 0x7e, 0xc0, 0x54, 0x50, 0x05, 0x75, 0x19, 0x97, 0x78, 0xd0, 0xe2, + 0xdc, 0x12, 0x18, 0xed, 0xc0, 0xd2, 0x67, 0xd7, 0x49, 0x03, 0xb5, 0x50, 0x05, 0xf5, 0xbf, 0x58, + 0xce, 0x4d, 0x27, 0x0d, 0xd0, 0x26, 0xfc, 0x9d, 0x86, 0x7e, 0xe4, 0xb0, 0x49, 0x42, 0xd4, 0x1f, + 0xc2, 0xc8, 0xc1, 0x51, 0xf1, 0xf5, 0x61, 0x0b, 0xd4, 0x1e, 0x01, 0x94, 0x07, 0x94, 0x11, 0xf3, + 0x86, 0x91, 0x28, 0x0d, 0x69, 0x84, 0x0e, 0x61, 0x91, 0x57, 0x17, 0xc3, 0xff, 0xed, 0x6d, 0xeb, + 0xcb, 0x9f, 0xa4, 0x7f, 0xd1, 0xfb, 0xd3, 0x98, 0x60, 0xf1, 0x02, 0x1f, 0x47, 0x3e, 0xf0, 0x7b, + 0xa1, 0x1c, 0x7c, 0x5f, 0x06, 0xed, 0xc2, 0x12, 0x3f, 0xd8, 0x09, 0xb9, 0x9e, 0x90, 0x94, 0xd9, + 0xa1, 0xa7, 0x16, 0xb9, 0x63, 0x49, 0x58, 0xe6, 0x01, 0xce, 0xf8, 0x89, 0x77, 0x07, 0x40, 0x0b, + 0x41, 0xc5, 0x5e, 0xb2, 0x1b, 0x18, 0x96, 0x57, 0x7a, 0xa1, 0x3f, 0xf0, 0x57, 0xc7, 0x3c, 0x6e, + 0x9e, 0x77, 0xfb, 0x8a, 0x84, 0x2a, 0xb0, 0xdc, 0xb7, 0xb0, 0x79, 0x66, 0xf5, 0xba, 0x1d, 0x1b, + 0x9b, 0xed, 0xde, 0xc0, 0xc4, 0x0a, 0x40, 0x6b, 0xb0, 0xb2, 0x82, 0x6d, 0xdc, 0xbc, 0x50, 0x0a, + 0xad, 0xd3, 0xa7, 0xb9, 0x06, 0x66, 0x73, 0x0d, 0xbc, 0xcc, 0x35, 0x70, 0xbf, 0xd0, 0xa4, 0xd9, + 0x42, 0x93, 0x9e, 0x17, 0x9a, 0x74, 0x79, 0xe0, 0x87, 0x2c, 0x98, 0x0c, 0x75, 0x97, 0x8e, 0xc5, + 0xfa, 0x63, 0x67, 0x6a, 0x64, 0xff, 0x89, 0x9f, 0x8c, 0x6c, 0xdf, 0xcb, 0x17, 0x65, 0xf8, 0x53, + 0xf0, 0xfd, 0xb7, 0x00, 0x00, 0x00, 0xff, 0xff, 0x1b, 0x70, 0x1d, 0xe4, 0x43, 0x02, 0x00, 0x00, } func (this *CoreChainLock) Equal(that interface{}) bool { @@ -297,6 +352,15 @@ func (m *VoteExtension) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.XSignRequestId != nil { + { + size := m.XSignRequestId.Size() + i -= size + if _, err := m.XSignRequestId.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + } + } if len(m.Signature) > 0 { i -= len(m.Signature) copy(dAtA[i:], m.Signature) @@ -319,6 +383,22 @@ func (m *VoteExtension) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *VoteExtension_SignRequestId) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *VoteExtension_SignRequestId) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + if m.SignRequestId != nil { + i -= len(m.SignRequestId) + copy(dAtA[i:], m.SignRequestId) + i = encodeVarintDash(dAtA, i, uint64(len(m.SignRequestId))) + i-- + dAtA[i] = 0x22 + } + return len(dAtA) - i, nil +} func encodeVarintDash(dAtA []byte, offset int, v uint64) int { offset -= sovDash(v) base := offset @@ -367,6 +447,22 @@ func (m *VoteExtension) Size() (n int) { if l > 0 { n += 1 + l + sovDash(uint64(l)) } + if m.XSignRequestId != nil { + n += m.XSignRequestId.Size() + } + return n +} + +func (m *VoteExtension_SignRequestId) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.SignRequestId != nil { + l = len(m.SignRequestId) + n += 1 + l + sovDash(uint64(l)) + } return n } @@ -629,6 +725,39 @@ func (m *VoteExtension) Unmarshal(dAtA []byte) error { m.Signature = []byte{} } iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SignRequestId", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowDash + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthDash + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthDash + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + v := make([]byte, postIndex-iNdEx) + copy(v, dAtA[iNdEx:postIndex]) + m.XSignRequestId = &VoteExtension_SignRequestId{v} + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipDash(dAtA[iNdEx:]) diff --git a/proto/tendermint/types/dash.proto b/proto/tendermint/types/dash.proto index 237c3ed768..2e42a12fbb 100644 --- a/proto/tendermint/types/dash.proto +++ b/proto/tendermint/types/dash.proto @@ -16,12 +16,36 @@ message CoreChainLock { enum VoteExtensionType { // Unsupported DEFAULT = 0; + // Sign canonical form of vote extension and threshold-recover signatures. + // // Deterministic vote extension - each validator in a quorum must provide the same vote extension data. THRESHOLD_RECOVER = 1; + // Sign raw form of vote extension and threshold-recover signatures. + // + // Deterministic vote extension - each validator in a quorum must provide the same vote extension data. + // Use with caution - it can have severe security consequences, like replay attacks. + // + // THRESHOLD_RECOVER_RAW alows overriding sign request ID with `sign_request_id` field + // of ExtendVoteExtension.sign_request_id. If sign_request_id is provided, SHA256(sign_request_id) will be used as + // a sign request ID. + // + // It also changes how threshold-recover signatures are generated. Instead of signing canonical form of + // threshold-recover signatures, it signs SHA256 of raw form of the vote extension (`ExtendVoteExtension.extension`). + THRESHOLD_RECOVER_RAW = 2; } message VoteExtension { VoteExtensionType type = 1; bytes extension = 2; bytes signature = 3; + // Sign request ID that will be used to sign the vote extensions. + // Tenderdash will use checksum of `sign_request_id` when generating quorum signatures of + // THRESHOLD_RECOVER vote extensions. + + // If not set, Tenderdash will generate it based on height and round. + // + // If set, it SHOULD be unique per voting round, and it MUST start with `dpevote` or `\x06plwdtx` string. + // + // Use with caution - it can have severe security consequences. + optional bytes sign_request_id = 4; } diff --git a/proto/tendermint/types/dash_test.go b/proto/tendermint/types/dash_test.go new file mode 100644 index 0000000000..0191df6952 --- /dev/null +++ b/proto/tendermint/types/dash_test.go @@ -0,0 +1,75 @@ +package types_test + +import ( + "testing" + + "github.com/dashpay/tenderdash/proto/tendermint/types" + "github.com/stretchr/testify/assert" +) + +func TestMarshalVoteExtension(t *testing.T) { + testCases := []struct { + extension types.VoteExtension + expectPanic bool + }{ + { + extension: types.VoteExtension{ + Type: types.VoteExtensionType_THRESHOLD_RECOVER_RAW, + Extension: []byte("threshold"), + XSignRequestId: &types.VoteExtension_SignRequestId{ + SignRequestId: []byte("sign-request-id"), + }}}, + { + extension: types.VoteExtension{ + Type: types.VoteExtensionType_THRESHOLD_RECOVER_RAW, + Extension: []byte("threshold"), + XSignRequestId: nil, + }}, + { + extension: types.VoteExtension{ + Type: types.VoteExtensionType_THRESHOLD_RECOVER_RAW, + Extension: []byte("threshold"), + XSignRequestId: &types.VoteExtension_SignRequestId{nil}, + }}, + // Test below panics because of nil pointer dereference bug in gogoproto + // FIXME: remove expectPanic when we replace gogoproto + { + expectPanic: true, + extension: types.VoteExtension{ + Type: types.VoteExtensionType_THRESHOLD_RECOVER_RAW, + Extension: []byte("threshold"), + XSignRequestId: (*types.VoteExtension_SignRequestId)(nil), + }}, + { + extension: (&types.VoteExtension{ + Type: types.VoteExtensionType_THRESHOLD_RECOVER_RAW, + Extension: []byte("threshold"), + XSignRequestId: (*types.VoteExtension_SignRequestId)(nil), + }).Clone()}, + } + + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + // marshaled, err := protoio.MarshalDelimited(tc) + // assert.NoError(t, err) + // assert.NotEmpty(t, marshaled) + + v := types.Vote{ + Type: types.PrecommitType, + VoteExtensions: []*types.VoteExtension{&tc.extension}, + } + f := func() { + marshaled, err := v.Marshal() + assert.NoError(t, err) + assert.NotEmpty(t, marshaled) + } + + if tc.expectPanic { + assert.Panics(t, f) + } else { + assert.NotPanics(t, f) + } + }) + } + +} diff --git a/proto/tendermint/types/types.go b/proto/tendermint/types/types.go index c32a9a1406..0a27c46b8a 100644 --- a/proto/tendermint/types/types.go +++ b/proto/tendermint/types/types.go @@ -39,14 +39,6 @@ func (m *PartSetHeader) IsZero() bool { return m == nil || len(m.Hash) == 0 } -// VoteExtensionsToMap creates a map where a key is vote-extension type and value is the extensions grouped by type -func (m *Vote) VoteExtensionsToMap() VoteExtensions { - if m == nil { - return nil - } - return VoteExtensionsToMap(m.VoteExtensions) -} - // SignBytes represent data to be signed for the given vote. // It's a 64-byte slice containing concatenation of: // * Checksum of CanonicalVote @@ -171,11 +163,3 @@ func (s StateID) ValidateBasic() error { return nil } - -func VoteExtensionsToMap(voteExtensions []*VoteExtension) VoteExtensions { - res := make(map[VoteExtensionType][]*VoteExtension) - for _, ext := range voteExtensions { - res[ext.Type] = append(res[ext.Type], ext) - } - return res -} diff --git a/spec/abci++/api.md b/spec/abci++/api.md index c059cbfe85..19b0f4945f 100644 --- a/spec/abci++/api.md +++ b/spec/abci++/api.md @@ -155,13 +155,24 @@ ExecTxResult contains results of executing one individual transaction. ### ExtendVoteExtension -Provides a vote extension for signing. Each field is mandatory for filling +Provides a vote extension for signing. `type` and `extension` fields are mandatory for filling | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | -| type | [tendermint.types.VoteExtensionType](#tendermint-types-VoteExtensionType) | | Vote extension type can be either DEFAULT or THRESHOLD_RECOVER. The Tenderdash supports only THRESHOLD_RECOVER at this moment. | -| extension | [bytes](#bytes) | | Deterministic or (Non-Deterministic) extension provided by the sending validator's Application. | +| type | [tendermint.types.VoteExtensionType](#tendermint-types-VoteExtensionType) | | Vote extension type can be either DEFAULT, THRESHOLD_RECOVER or THRESHOLD_RECOVER_RAW. The Tenderdash supports only THRESHOLD_RECOVER and THRESHOLD_RECOVER_RAW at this moment. | +| extension | [bytes](#bytes) | | Deterministic or (Non-Deterministic) extension provided by the sending validator's Application. + +For THRESHOLD_RECOVER_RAW, it MUST be 32 bytes. + +Sign request ID that will be used to sign the vote extensions. Only applicable for THRESHOLD_RECOVER_RAW vote extension type. + +Tenderdash will use SHA256 checksum of `sign_request_id` when generating quorum signatures of THRESHOLD_RECOVER_RAW vote extensions. It MUST NOT be set for any other vote extension types. | +| sign_request_id | [bytes](#bytes) | optional | If not set, Tenderdash will generate it based on height and round. + +If set, it SHOULD be unique per voting round, and it MUST start with `dpevote` or `\x06plwdtx` prefix. + +Use with caution - it can have severe security consequences. | diff --git a/test/e2e/app/app.go b/test/e2e/app/app.go index 027577b30a..1c3bba6b33 100644 --- a/test/e2e/app/app.go +++ b/test/e2e/app/app.go @@ -6,7 +6,7 @@ import ( "encoding/binary" "errors" "fmt" - "math/rand" + "math/big" "strconv" "strings" "time" @@ -17,6 +17,7 @@ import ( "github.com/dashpay/tenderdash/abci/example/code" "github.com/dashpay/tenderdash/abci/example/kvstore" abci "github.com/dashpay/tenderdash/abci/types" + "github.com/dashpay/tenderdash/crypto" "github.com/dashpay/tenderdash/libs/log" types1 "github.com/dashpay/tenderdash/proto/tendermint/types" "github.com/dashpay/tenderdash/types" @@ -85,22 +86,18 @@ func (app *Application) ExtendVote(_ context.Context, req *abci.RequestExtendVot ) return &abci.ResponseExtendVote{}, nil } - ext := make([]byte, binary.MaxVarintLen64) - // We don't care that these values are generated by a weak random number - // generator. It's just for test purposes. - //nolint:gosec // G404: Use of weak random number generator - num := rand.Int63n(voteExtensionMaxVal) - extLen := binary.PutVarint(ext, num) + ext := make([]byte, crypto.DefaultHashSize) + copy(ext, big.NewInt(lastHeight+1).Bytes()) + app.logger.Info("generated vote extension", - "num", num, - "ext", fmt.Sprintf("%x", ext[:extLen]), - "state.Height", lastHeight, + "ext", fmt.Sprintf("%x", ext), + "state.Height", lastHeight+1, ) return &abci.ResponseExtendVote{ VoteExtensions: []*abci.ExtendVoteExtension{ { - Type: types1.VoteExtensionType_DEFAULT, - Extension: ext[:extLen], + Type: types1.VoteExtensionType_THRESHOLD_RECOVER_RAW, + Extension: ext, }, { Type: types1.VoteExtensionType_THRESHOLD_RECOVER, @@ -161,6 +158,14 @@ func (app *Application) FinalizeBlock(ctx context.Context, req *abci.RequestFina app.mu.Lock() defer app.mu.Unlock() + for i, ext := range req.Commit.ThresholdVoteExtensions { + if len(ext.Signature) == 0 { + return &abci.ResponseFinalizeBlock{}, fmt.Errorf("vote extension signature is empty: %+v", ext) + } + + app.logger.Debug("vote extension received in FinalizeBlock", "extension", ext, "i", i) + } + prevState := kvstore.NewKvState(db.NewMemDB(), 0) if err := app.LastCommittedState.Copy(prevState); err != nil { return &abci.ResponseFinalizeBlock{}, err @@ -258,5 +263,14 @@ func verifyTx(tx types.Tx, _ abci.CheckTxType) (abci.ResponseCheckTx, error) { fmt.Errorf("malformed vote extension transaction %X=%X: %w", k, v, err) } } - return abci.ResponseCheckTx{Code: code.CodeTypeOK, GasWanted: 1}, nil + // For TestApp_TxTooBig we need to preserve order of transactions + var priority int64 + // in this case, k is defined as fmt.Sprintf("testapp-big-tx-%v-%08x-%d=", node.Name, session, i) + // but in general, we take last digit as inverse priority + split = bytes.Split(k, []byte{'-'}) + if n, err := strconv.ParseInt(string(split[len(split)-1]), 10, 64); err == nil { + priority = 1000000000 - n + } + + return abci.ResponseCheckTx{Code: code.CodeTypeOK, GasWanted: 1, Priority: priority}, nil } diff --git a/test/e2e/pkg/mockcoreserver/core_server.go b/test/e2e/pkg/mockcoreserver/core_server.go index 4281d30c36..4b6025e125 100644 --- a/test/e2e/pkg/mockcoreserver/core_server.go +++ b/test/e2e/pkg/mockcoreserver/core_server.go @@ -9,8 +9,8 @@ import ( "github.com/dashpay/dashd-go/btcjson" "github.com/dashpay/tenderdash/crypto" - tmbytes "github.com/dashpay/tenderdash/libs/bytes" "github.com/dashpay/tenderdash/privval" + "github.com/dashpay/tenderdash/types" ) // CoreServer is an interface of a mock core-server @@ -93,13 +93,8 @@ func (c *MockCoreServer) QuorumSign(ctx context.Context, cmd btcjson.QuorumCmd) panic(err) } quorumHash := crypto.QuorumHash(quorumHashBytes) + signID := types.NewSignItemFromHash(*cmd.LLMQType, quorumHash, reqID, msgHash).SignHash - signID := crypto.SignID( - *cmd.LLMQType, - tmbytes.Reverse(quorumHash), - tmbytes.Reverse(reqID), - tmbytes.Reverse(msgHash), - ) privateKey, err := c.FilePV.GetPrivateKey(ctx, quorumHash) if err != nil { panic(err) @@ -142,13 +137,8 @@ func (c *MockCoreServer) QuorumVerify(ctx context.Context, cmd btcjson.QuorumCmd if err != nil { panic(err) } + signID := types.NewSignItemFromHash(*cmd.LLMQType, quorumHash, reqID, msgHash).SignHash - signID := crypto.SignID( - *cmd.LLMQType, - tmbytes.Reverse(quorumHash), - tmbytes.Reverse(reqID), - tmbytes.Reverse(msgHash), - ) thresholdPublicKey, err := c.FilePV.GetThresholdPublicKey(ctx, quorumHash) if err != nil { panic(err) diff --git a/test/e2e/runner/test.go b/test/e2e/runner/test.go index c44f58acd4..79886fd3ea 100644 --- a/test/e2e/runner/test.go +++ b/test/e2e/runner/test.go @@ -15,5 +15,5 @@ func Test(ctx context.Context, testnet *e2e.Testnet) error { return err } - return exec.CommandVerbose(ctx, "./build/tests", "-test.count=1", "-test.v", "-test.timeout=5m") + return exec.CommandVerbose(ctx, "./build/tests", "-test.count=1", "-test.v", "-test.timeout=10m") } diff --git a/test/e2e/tests/app_test.go b/test/e2e/tests/app_test.go index 3c5b239681..d302242c1f 100644 --- a/test/e2e/tests/app_test.go +++ b/test/e2e/tests/app_test.go @@ -195,7 +195,7 @@ func TestApp_Tx(t *testing.T) { // when I submit them to the node, // then the first transaction should be committed before the last one. func TestApp_TxTooBig(t *testing.T) { - const timeout = 10 * time.Second + const timeout = 60 * time.Second testNode(t, func(ctx context.Context, t *testing.T, node e2e.Node) { session := rand.Int63() @@ -203,7 +203,10 @@ func TestApp_TxTooBig(t *testing.T) { client, err := node.Client() require.NoError(t, err) - cp, err := client.ConsensusParams(ctx, nil) + // FIXME: ConsensusParams is broken for last height, this is just workaround + status, err := client.Status(ctx) + assert.NoError(t, err) + cp, err := client.ConsensusParams(ctx, &status.SyncInfo.LatestBlockHeight) assert.NoError(t, err) // ensure we have more txs than fits the block @@ -216,7 +219,7 @@ func TestApp_TxTooBig(t *testing.T) { var key string for i := 0; i < numTxs; i++ { - key = fmt.Sprintf("testapp-big-tx-%v-%08x-%06x=", node.Name, session, i) + key = fmt.Sprintf("testapp-big-tx-%v-%08x-%d=", node.Name, session, i) copy(tx, key) payloadOffset := len(tx) - 8 // where we put the `i` into the payload @@ -230,7 +233,6 @@ func TestApp_TxTooBig(t *testing.T) { } _, err = client.BroadcastTxAsync(ctx, tx) - t.Logf("submitted tx %x", tx.Hash()) assert.NoError(t, err, "failed to broadcast tx %06x", i) } @@ -248,14 +250,21 @@ func TestApp_TxTooBig(t *testing.T) { assert.NoError(t, err, "first tx should be committed before second") assert.EqualValues(t, firstTxHash, firstTxResp.Tx.Hash()) - t.Logf("first tx in block %d, last tx in block %d", firstTxResp.Height, lastTxResp.Height) + firstTxBlock, err := client.Header(ctx, &firstTxResp.Height) + assert.NoError(t, err) + lastTxBlock, err := client.Header(ctx, &lastTxResp.Height) + assert.NoError(t, err) + + t.Logf("first tx in block %d, last tx in block %d, time diff %s", + firstTxResp.Height, + lastTxResp.Height, + lastTxBlock.Header.Time.Sub(firstTxBlock.Header.Time).String(), + ) assert.Less(t, firstTxResp.Height, lastTxResp.Height, "first tx should in block before last tx") return true } - // tx2 not there yet - t.Log("last tx not committed within timeout") return false }, timeout, // timeout diff --git a/types/block.go b/types/block.go index df65dc7be3..434c1a3fae 100644 --- a/types/block.go +++ b/types/block.go @@ -294,6 +294,20 @@ func (b *Block) ToProto() (*tmproto.Block, error) { return pb, nil } +func (b *Block) MarshalZerologObject(e *zerolog.Event) { + if b == nil { + e.Bool("nil", true) + return + } + e.Bool("nil", false) + + e.Interface("header", b.Header) + e.Interface("core_chain_lock", b.CoreChainLock) + e.Interface("data", b.Data) + e.Interface("evidence", b.Evidence) + e.Interface("last_commit", b.LastCommit) +} + // FromProto sets a protobuf Block to the given pointer. // It returns an error if the block is invalid. func BlockFromProto(bp *tmproto.Block) (*Block, error) { @@ -737,7 +751,7 @@ type Commit struct { QuorumHash crypto.QuorumHash `json:"quorum_hash"` ThresholdBlockSignature []byte `json:"threshold_block_signature"` // ThresholdVoteExtensions keeps the list of recovered threshold signatures for vote-extensions - ThresholdVoteExtensions []ThresholdExtensionSign `json:"threshold_vote_extensions"` + ThresholdVoteExtensions tmproto.VoteExtensions `json:"threshold_vote_extensions"` // Memoized in first call to corresponding method. // NOTE: can't memoize in constructor because constructor isn't used for @@ -746,12 +760,17 @@ type Commit struct { } // NewCommit returns a new Commit. -func NewCommit(height int64, round int32, blockID BlockID, commitSigns *CommitSigns) *Commit { +func NewCommit(height int64, round int32, blockID BlockID, voteExtensions VoteExtensions, commitSigns *CommitSigns) *Commit { commit := &Commit{ Height: height, Round: round, BlockID: blockID, + ThresholdVoteExtensions: voteExtensions.Filter(func(ext VoteExtensionIf) bool { + _, ok := ext.(ThresholdVoteExtensionIf) + return ok + }).ToProto(), } + if commitSigns != nil { commitSigns.CopyToCommit(commit) } @@ -764,7 +783,7 @@ func (commit *Commit) ToCommitInfo() types.CommitInfo { Round: commit.Round, QuorumHash: commit.QuorumHash, BlockSignature: commit.ThresholdBlockSignature, - ThresholdVoteExtensions: ThresholdExtensionSignToProto(commit.ThresholdVoteExtensions), + ThresholdVoteExtensions: commit.ThresholdVoteExtensions, } } @@ -943,7 +962,7 @@ func (commit *Commit) ToProto() *tmproto.Commit { c.BlockID = commit.BlockID.ToProto() c.ThresholdBlockSignature = commit.ThresholdBlockSignature - c.ThresholdVoteExtensions = ThresholdExtensionSignToProto(commit.ThresholdVoteExtensions) + c.ThresholdVoteExtensions = commit.ThresholdVoteExtensions c.QuorumHash = commit.QuorumHash return c @@ -967,7 +986,7 @@ func CommitFromProto(cp *tmproto.Commit) (*Commit, error) { commit.QuorumHash = cp.QuorumHash commit.ThresholdBlockSignature = cp.ThresholdBlockSignature - commit.ThresholdVoteExtensions = ThresholdExtensionSignFromProto(cp.ThresholdVoteExtensions) + commit.ThresholdVoteExtensions = cp.ThresholdVoteExtensions commit.Height = cp.Height commit.Round = cp.Round @@ -1034,6 +1053,17 @@ func (data *Data) ToProto() tmproto.Data { return *tp } +func (data *Data) MarshalZerologObject(e *zerolog.Event) { + if data == nil { + e.Bool("nil", true) + return + } + e.Bool("nil", false) + + e.Array("txs", data.Txs) + e.Hex("hash", data.Hash()) +} + // DataFromProto takes a protobuf representation of Data & // returns the native type. func DataFromProto(dp *tmproto.Data) (Data, error) { diff --git a/types/block_test.go b/types/block_test.go index f9a4af6489..743237f491 100644 --- a/types/block_test.go +++ b/types/block_test.go @@ -117,7 +117,7 @@ func TestBlockValidateBasic(t *testing.T) { blk.LastCommit = nil }, true}, {"Invalid LastCommit", func(blk *Block) { - blk.LastCommit = NewCommit(-1, 0, *voteSet.maj23, nil) + blk.LastCommit = NewCommit(-1, 0, *voteSet.maj23, nil, nil) }, true}, {"Invalid Evidence", func(blk *Block) { emptyEv := &DuplicateVoteEvidence{} @@ -528,7 +528,7 @@ func TestBlockMaxDataBytes(t *testing.T) { require.NotNil(t, commit) // minBlockSize is minimum correct size of a block - const minBlockSize = 1231 + const minBlockSize = 1370 testCases := []struct { maxBytes int64 diff --git a/types/canonical.go b/types/canonical.go index e4c5d74f51..1bdaf231b1 100644 --- a/types/canonical.go +++ b/types/canonical.go @@ -1,6 +1,7 @@ package types import ( + "fmt" "time" tmtime "github.com/dashpay/tenderdash/libs/time" @@ -31,14 +32,21 @@ func CanonicalizeProposal(chainID string, proposal *tmproto.Proposal) tmproto.Ca // CanonicalizeVoteExtension extracts the vote extension from the given vote // and constructs a CanonicalizeVoteExtension struct, whose representation in // bytes is what is signed in order to produce the vote extension's signature. -func CanonicalizeVoteExtension(chainID string, ext *tmproto.VoteExtension, height int64, round int32) tmproto.CanonicalVoteExtension { - return tmproto.CanonicalVoteExtension{ - Extension: ext.Extension, - Type: ext.Type, - Height: height, - Round: int64(round), - ChainId: chainID, +func CanonicalizeVoteExtension(chainID string, ext *tmproto.VoteExtension, height int64, round int32) (tmproto.CanonicalVoteExtension, error) { + switch ext.Type { + case tmproto.VoteExtensionType_DEFAULT, tmproto.VoteExtensionType_THRESHOLD_RECOVER: + { + canonical := tmproto.CanonicalVoteExtension{ + Extension: ext.Extension, + Type: ext.Type, + Height: height, + Round: int64(round), + ChainId: chainID, + } + return canonical, nil + } } + return tmproto.CanonicalVoteExtension{}, fmt.Errorf("provided vote extension type %s does not have canonical form for signing", ext.Type) } // CanonicalTime can be used to stringify time in a canonical way. diff --git a/types/priv_validator.go b/types/priv_validator.go index 5ed26f95e3..7d3faabfcb 100644 --- a/types/priv_validator.go +++ b/types/priv_validator.go @@ -273,6 +273,7 @@ func (pv *MockPV) SignVote( } vote.BlockSignature = blockSignature + // We only sign vote extensions for precommits if vote.Type != tmproto.PrecommitType { if len(vote.VoteExtensions) > 0 { return errors.New("unexpected vote extension - vote extensions are only allowed in precommits") @@ -280,22 +281,20 @@ func (pv *MockPV) SignVote( return nil } - // We only sign vote extensions for precommits - extSigns, err := MakeVoteExtensionSignItems(useChainID, vote, quorumType, quorumHash) + extensions := VoteExtensionsFromProto(vote.VoteExtensions...) + signItems, err := extensions.SignItems(useChainID, quorumType, quorumHash, vote.Height, vote.Round) if err != nil { return err } - protoExtensionsMap := vote.VoteExtensionsToMap() - for et, signs := range extSigns { - extensions := protoExtensionsMap[et] - for i, sign := range signs { - sign, err := privKey.SignDigest(sign.ID) - if err != nil { - return err - } - extensions[i].Signature = sign + + for i, sign := range signItems { + sig, err := privKey.SignDigest(sign.SignHash) + if err != nil { + return err } + vote.VoteExtensions[i].Signature = sig } + return nil } diff --git a/types/proposal.go b/types/proposal.go index 9525556a0b..7dad0b55c2 100644 --- a/types/proposal.go +++ b/types/proposal.go @@ -9,13 +9,12 @@ import ( "time" "github.com/dashpay/dashd-go/btcjson" - "github.com/rs/zerolog" - "github.com/dashpay/tenderdash/crypto" "github.com/dashpay/tenderdash/internal/libs/protoio" tmbytes "github.com/dashpay/tenderdash/libs/bytes" tmtime "github.com/dashpay/tenderdash/libs/time" tmproto "github.com/dashpay/tenderdash/proto/tendermint/types" + "github.com/rs/zerolog" ) var ( @@ -182,12 +181,7 @@ func ProposalBlockSignID( proposalRequestID := ProposalRequestIDProto(p) - signID := crypto.SignID( - quorumType, - tmbytes.Reverse(quorumHash), - tmbytes.Reverse(proposalRequestID), - tmbytes.Reverse(proposalMessageHash[:]), - ) + signID := NewSignItemFromHash(quorumType, quorumHash, proposalRequestID, proposalMessageHash[:]).SignHash return signID } diff --git a/types/quorum.go b/types/quorum.go index 5e0a5c7e67..367e28f9e2 100644 --- a/types/quorum.go +++ b/types/quorum.go @@ -1,11 +1,11 @@ package types import ( + "bytes" "fmt" "github.com/dashpay/tenderdash/crypto" "github.com/dashpay/tenderdash/libs/log" - tmproto "github.com/dashpay/tenderdash/proto/tendermint/types" ) // CommitSigns is used to combine threshold signatures and quorum-hash that were used @@ -15,94 +15,41 @@ type CommitSigns struct { } // CopyToCommit copies threshold signature to commit +// +// commit.ThresholdVoteExtensions must be initialized func (c *CommitSigns) CopyToCommit(commit *Commit) { commit.QuorumHash = c.QuorumHash commit.ThresholdBlockSignature = c.BlockSign - commit.ThresholdVoteExtensions = c.ExtensionSigns + if len(c.VoteExtensionSignatures) != len(commit.ThresholdVoteExtensions) { + panic(fmt.Sprintf("count of threshold vote extension signatures (%d) doesn't match vote extensions in commit (%d)", + len(commit.ThresholdVoteExtensions), len(c.VoteExtensionSignatures), + )) + } + for i, ext := range c.VoteExtensionSignatures { + commit.ThresholdVoteExtensions[i].Signature = bytes.Clone(ext) + } } // QuorumSigns holds all created signatures, block, state and for each recovered vote-extensions type QuorumSigns struct { - BlockSign []byte - ExtensionSigns []ThresholdExtensionSign + BlockSign []byte + // List of vote extensions signatures. Order matters. + VoteExtensionSignatures [][]byte } -// NewQuorumSignsFromCommit creates and returns QuorumSigns using threshold signatures from a commit +// NewQuorumSignsFromCommit creates and returns QuorumSigns using threshold signatures from a commit. +// +// Note it only uses threshold-revoverable vote extension signatures, as non-threshold signatures are not included in the commit func NewQuorumSignsFromCommit(commit *Commit) QuorumSigns { - return QuorumSigns{ - BlockSign: commit.ThresholdBlockSignature, - ExtensionSigns: commit.ThresholdVoteExtensions, - } -} - -// ThresholdExtensionSign is used for keeping extension and recovered threshold signature -type ThresholdExtensionSign struct { - Extension []byte - ThresholdSignature []byte -} - -// MakeThresholdExtensionSigns creates and returns the list of ThresholdExtensionSign for given VoteExtensions container -func MakeThresholdExtensionSigns(voteExtensions VoteExtensions) []ThresholdExtensionSign { - if voteExtensions == nil { - return nil - } - extensions := voteExtensions[tmproto.VoteExtensionType_THRESHOLD_RECOVER] - if len(extensions) == 0 { - return nil - } - thresholdSigns := make([]ThresholdExtensionSign, len(extensions)) - for i, ext := range extensions { - thresholdSigns[i] = ThresholdExtensionSign{ - Extension: ext.Extension, - ThresholdSignature: ext.Signature, - } - } - return thresholdSigns -} - -// ThresholdExtensionSignFromProto transforms a list of protobuf ThresholdVoteExtension -// into the list of domain ThresholdExtensionSign -func ThresholdExtensionSignFromProto(protoExtensions []*tmproto.VoteExtension) []ThresholdExtensionSign { - if len(protoExtensions) == 0 { - return nil - } - extensions := make([]ThresholdExtensionSign, len(protoExtensions)) - for i, ext := range protoExtensions { - extensions[i] = ThresholdExtensionSign{ - Extension: ext.Extension, - ThresholdSignature: ext.Signature, - } + sigs := make([][]byte, 0, len(commit.ThresholdVoteExtensions)) + for _, ext := range commit.ThresholdVoteExtensions { + sigs = append(sigs, ext.Signature) } - return extensions -} -// ThresholdExtensionSignToProto transforms a list of domain ThresholdExtensionSign -// into the list of protobuf VoteExtension -func ThresholdExtensionSignToProto(extensions []ThresholdExtensionSign) []*tmproto.VoteExtension { - if len(extensions) == 0 { - return nil - } - protoExtensions := make([]*tmproto.VoteExtension, len(extensions)) - for i, ext := range extensions { - protoExtensions[i] = &tmproto.VoteExtension{ - Extension: ext.Extension, - Signature: ext.ThresholdSignature, - } - } - return protoExtensions -} - -// MakeThresholdVoteExtensions creates a list of ThresholdExtensionSign from the list of VoteExtension -// and recovered threshold signatures. The lengths of vote-extensions and threshold signatures must be the same -func MakeThresholdVoteExtensions(extensions []VoteExtension, thresholdSigs [][]byte) []ThresholdExtensionSign { - thresholdExtensions := make([]ThresholdExtensionSign, len(extensions)) - for i, ext := range extensions { - thresholdExtensions[i] = ThresholdExtensionSign{ - Extension: ext.Extension, - ThresholdSignature: thresholdSigs[i], - } + return QuorumSigns{ + BlockSign: commit.ThresholdBlockSignature, + VoteExtensionSignatures: sigs, } - return thresholdExtensions } // QuorumSingsVerifier ... @@ -170,17 +117,18 @@ func (q *QuorumSingsVerifier) verifyBlock(pubKey crypto.PubKey, signs QuorumSign if !q.shouldVerifyBlock { return nil } - if !pubKey.VerifySignatureDigest(q.Block.ID, signs.BlockSign) { + if !pubKey.VerifySignatureDigest(q.Block.SignHash, signs.BlockSign) { return fmt.Errorf( "threshold block signature is invalid: (%X) signID=%X: %w", - q.Block.Raw, - q.Block.ID, + q.Block.Msg, + q.Block.SignHash, ErrVoteInvalidBlockSignature, ) } return nil } +// verify threshold-recoverable vote extensions signatures func (q *QuorumSingsVerifier) verifyVoteExtensions( pubKey crypto.PubKey, signs QuorumSigns, @@ -188,20 +136,27 @@ func (q *QuorumSingsVerifier) verifyVoteExtensions( if !q.shouldVerifyVoteExtensions { return nil } - sings := signs.ExtensionSigns - signItems := q.Extensions[tmproto.VoteExtensionType_THRESHOLD_RECOVER] + + thresholdSigs := signs.VoteExtensionSignatures + signItems := q.VoteExtensionSignItems if len(signItems) == 0 { return nil } - if len(signItems) != len(sings) { - return fmt.Errorf("count of threshold vote extension signatures (%d) doesn't match with recoverable vote extensions (%d)", - len(sings), len(signItems), + if len(signItems) != len(thresholdSigs) { + return fmt.Errorf("count of threshold vote extension signatures (%d) doesn't match recoverable vote extensions (%d)", + len(thresholdSigs), len(signItems), ) } - for i, ext := range sings { - if !pubKey.VerifySignatureDigest(signItems[i].ID, ext.ThresholdSignature) { - return fmt.Errorf("threshold vote-extension signature is invalid (%d) %X", - i, signItems[i].Raw) + + for i, sig := range thresholdSigs { + if !pubKey.VerifySignatureDigest(signItems[i].SignHash, sig) { + return fmt.Errorf("vote-extension %d signature is invalid: raw %X, signature %X, pubkey %X, sigHash: %X", + i, + signItems[i].Msg, + sig, + pubKey.Bytes(), + signItems[i].SignHash, + ) } } return nil diff --git a/types/quorum_sign_data.go b/types/quorum_sign_data.go index 9607131d6e..af7db79477 100644 --- a/types/quorum_sign_data.go +++ b/types/quorum_sign_data.go @@ -1,53 +1,48 @@ package types import ( - "errors" + "bytes" "fmt" + bls "github.com/dashpay/bls-signatures/go-bindings" "github.com/dashpay/dashd-go/btcjson" - "github.com/rs/zerolog" - "github.com/dashpay/tenderdash/crypto" tmbytes "github.com/dashpay/tenderdash/libs/bytes" "github.com/dashpay/tenderdash/proto/tendermint/types" -) - -var ( - errUnexpectedVoteType = errors.New("unexpected vote extension - vote extensions are only allowed in precommits") + "github.com/rs/zerolog" ) // QuorumSignData holds data which is necessary for signing and verification block, state, and each vote-extension in a list type QuorumSignData struct { - Block SignItem - Extensions map[types.VoteExtensionType][]SignItem + Block SignItem + VoteExtensionSignItems []SignItem } -// Verify verifies a quorum signatures: block, state and vote-extensions -func (q QuorumSignData) Verify(pubKey crypto.PubKey, signs QuorumSigns) error { - return NewQuorumSignsVerifier(q).Verify(pubKey, signs) -} - -// SignItem represents quorum sign data, like a request id, message bytes, sha256 hash of message and signID -type SignItem struct { - ReqID []byte // Request ID for quorum signing - ID []byte // Signature ID - Raw []byte // Raw data to be signed - Hash []byte // Checksum of Raw -} +// Signs items inside QuorumSignData using a given private key. +// +// Mainly for testing. +func (q QuorumSignData) SignWithPrivkey(key crypto.PrivKey) (QuorumSigns, error) { + var err error + var signs QuorumSigns + if signs.BlockSign, err = key.SignDigest(q.Block.SignHash); err != nil { + return signs, err + } -// Validate validates prepared data for signing -func (i *SignItem) Validate() error { - if len(i.ReqID) != crypto.DefaultHashSize { - return fmt.Errorf("invalid request ID size: %X", i.ReqID) + signs.VoteExtensionSignatures = make([][]byte, 0, len(q.VoteExtensionSignItems)) + for _, item := range q.VoteExtensionSignItems { + var sign []byte + if sign, err = key.SignDigest(item.SignHash); err != nil { + return signs, err + } + signs.VoteExtensionSignatures = append(signs.VoteExtensionSignatures, sign) } - return nil + + return signs, nil } -func (i SignItem) MarshalZerologObject(e *zerolog.Event) { - e.Hex("signBytes", i.Raw) - e.Hex("signRequestID", i.ReqID) - e.Hex("signID", i.ID) - e.Hex("signHash", i.Hash) +// Verify verifies a quorum signatures: block, state and vote-extensions +func (q QuorumSignData) Verify(pubKey crypto.PubKey, signs QuorumSigns) error { + return NewQuorumSignsVerifier(q).Verify(pubKey, signs) } // MakeQuorumSignsWithVoteSet creates and returns QuorumSignData struct built with a vote-set and an added vote @@ -72,7 +67,12 @@ func MakeQuorumSigns( Block: MakeBlockSignItem(chainID, protoVote, quorumType, quorumHash), } var err error - quorumSign.Extensions, err = MakeVoteExtensionSignItems(chainID, protoVote, quorumType, quorumHash) + quorumSign.VoteExtensionSignItems, err = + VoteExtensionsFromProto(protoVote.VoteExtensions...). + Filter(func(ext VoteExtensionIf) bool { + return ext.IsThresholdRecoverable() + }). + SignItems(chainID, quorumType, quorumHash, protoVote.Height, protoVote.Round) if err != nil { return QuorumSignData{}, err } @@ -94,52 +94,118 @@ func BlockRequestID(height int64, round int32) []byte { return heightRoundRequestID("dpbvote", height, round) } -// MakeVoteExtensionSignItems creates a list SignItem structs for a vote extensions -func MakeVoteExtensionSignItems( - chainID string, - protoVote *types.Vote, - quorumType btcjson.LLMQType, - quorumHash []byte, -) (map[types.VoteExtensionType][]SignItem, error) { - // We only sign vote extensions for precommits - if protoVote.Type != types.PrecommitType { - if len(protoVote.VoteExtensions) > 0 { - return nil, errUnexpectedVoteType - } - return nil, nil +// SignItem represents signing session data (in field SignItem.ID) that will be signed to get threshold signature share. +// Field names are the same as in Dash Core, but the meaning is different. +// See DIP-0007 +type SignItem struct { + LlmqType btcjson.LLMQType // Quorum type for which this sign item is created + ID []byte // Request ID for quorum signing + MsgHash []byte // Checksum of Raw + QuorumHash []byte // Quorum hash for which this sign item is created + + SignHash []byte // Hash of llmqType, quorumHash, id, and msgHash - as provided to crypto sign/verify functions + + Msg []byte // Raw data to be signed, before any transformations; optional +} + +// Validate validates prepared data for signing +func (i *SignItem) Validate() error { + if len(i.ID) != crypto.DefaultHashSize { + return fmt.Errorf("invalid request ID size: %X", i.ID) } - items := make(map[types.VoteExtensionType][]SignItem) - reqID := VoteExtensionRequestID(protoVote.Height, protoVote.Round) - protoExtensionsMap := protoVote.VoteExtensionsToMap() - for t, exts := range protoExtensionsMap { - if items[t] == nil && len(exts) > 0 { - items[t] = make([]SignItem, len(exts)) - } - for i, ext := range exts { - raw := VoteExtensionSignBytes(chainID, protoVote.Height, protoVote.Round, ext) - items[t][i] = NewSignItem(quorumType, quorumHash, reqID, raw) + if len(i.MsgHash) != crypto.DefaultHashSize { + return fmt.Errorf("invalid hash size %d: %X", len(i.MsgHash), i.MsgHash) + } + if len(i.QuorumHash) != crypto.DefaultHashSize { + return fmt.Errorf("invalid quorum hash size %d: %X", len(i.QuorumHash), i.QuorumHash) + } + // Msg is optional + if len(i.Msg) > 0 { + if !bytes.Equal(crypto.Checksum(i.Msg), i.MsgHash) { + return fmt.Errorf("invalid hash %X for raw data: %X", i.MsgHash, i.Msg) } } - return items, nil + return nil +} + +func (i SignItem) MarshalZerologObject(e *zerolog.Event) { + e.Hex("msg", i.Msg) + e.Hex("signRequestID", i.ID) + e.Hex("signID", i.SignHash) + e.Hex("msgHash", i.MsgHash) + e.Hex("quorumHash", i.QuorumHash) + e.Uint8("llmqType", uint8(i.LlmqType)) + } // NewSignItem creates a new instance of SignItem with calculating a hash for a raw and creating signID -func NewSignItem(quorumType btcjson.LLMQType, quorumHash, reqID, raw []byte) SignItem { - msgHash := crypto.Checksum(raw) - return SignItem{ - ReqID: reqID, - ID: MakeSignID(msgHash, reqID, quorumType, quorumHash), - Raw: raw, - Hash: msgHash, +// +// Arguments: +// - quorumType: quorum type +// - quorumHash: quorum hash +// - reqID: sign request ID +// - msg: raw data to be signed; it will be hashed with crypto.Checksum() +func NewSignItem(quorumType btcjson.LLMQType, quorumHash, reqID, msg []byte) SignItem { + msgHash := crypto.Checksum(msg) // FIXME: shouldn't we use sha256(sha256(raw)) here? + item := NewSignItemFromHash(quorumType, quorumHash, reqID, msgHash) + item.Msg = msg + + return item +} + +// Create a new sign item without raw value, using provided hash. +func NewSignItemFromHash(quorumType btcjson.LLMQType, quorumHash, reqID, msgHash []byte) SignItem { + item := SignItem{ + ID: reqID, + MsgHash: msgHash, + LlmqType: quorumType, + QuorumHash: quorumHash, + Msg: nil, // Raw is empty, as we don't have it } + + // By default, reverse fields when calculating SignHash + item.UpdateSignHash(true) + + return item } -// MakeSignID creates singID -func MakeSignID(msgHash, reqID []byte, quorumType btcjson.LLMQType, quorumHash []byte) []byte { - return crypto.SignID( - quorumType, - tmbytes.Reverse(quorumHash), - tmbytes.Reverse(reqID), - tmbytes.Reverse(msgHash), - ) +// UpdateSignHash recalculates signHash field +// If reverse is true, then all []byte elements will be reversed before +// calculating signID +func (i *SignItem) UpdateSignHash(reverse bool) { + if err := i.Validate(); err != nil { + panic("invalid sign item: " + err.Error()) + } + llmqType := i.LlmqType + + quorumHash := i.QuorumHash + requestID := i.ID + messageHash := i.MsgHash + + if reverse { + quorumHash = tmbytes.Reverse(quorumHash) + requestID = tmbytes.Reverse(requestID) + messageHash = tmbytes.Reverse(messageHash) + } + + var blsQuorumHash bls.Hash + copy(blsQuorumHash[:], quorumHash) + + var blsRequestID bls.Hash + copy(blsRequestID[:], requestID) + + var blsMessageHash bls.Hash + copy(blsMessageHash[:], messageHash) + + // fmt.Printf("LlmqType: %x + ", llmqType) + // fmt.Printf("QuorumHash: %x + ", blsQuorumHash) + // fmt.Printf("RequestID: %x + ", blsRequestID) + // fmt.Printf("MsgHash: %x\n", blsMessageHash) + + blsSignHash := bls.BuildSignHash(uint8(llmqType), blsQuorumHash, blsRequestID, blsMessageHash) + + signHash := make([]byte, 32) + copy(signHash, blsSignHash[:]) + + i.SignHash = signHash } diff --git a/types/quorum_sign_data_test.go b/types/quorum_sign_data_test.go index 54467ab895..422c3d96a5 100644 --- a/types/quorum_sign_data_test.go +++ b/types/quorum_sign_data_test.go @@ -1,6 +1,7 @@ package types import ( + "encoding/hex" "fmt" "testing" @@ -8,9 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/dashpay/tenderdash/crypto" tmbytes "github.com/dashpay/tenderdash/libs/bytes" - "github.com/dashpay/tenderdash/libs/log" "github.com/dashpay/tenderdash/proto/tendermint/types" ) @@ -20,8 +19,10 @@ func TestBlockRequestID(t *testing.T) { assert.EqualValues(t, expected, got) } -func TestMakeBlockSignID(t *testing.T) { +func TestMakeBlockSignItem(t *testing.T) { const chainID = "dash-platform" + const quorumType = btcjson.LLMQType_5_60 + testCases := []struct { vote Vote quorumHash []byte @@ -40,88 +41,47 @@ func TestMakeBlockSignID(t *testing.T) { "DA25B746781DDF47B5D736F30B1D9D0CC86981EEC67CBE255265C4361DEF8C2E", "02000000E9030000000000000000000000000000E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B"+ "7852B855E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855646173682D706C6174666F726D", + "6A12D9CF7091D69072E254B297AEF15997093E480FDE295E09A7DE73B31CEEDD", + quorumType, ), wantHash: tmbytes.MustHexDecode("0CA3D5F42BDFED0C4FDE7E6DE0F046CC76CDA6CEE734D65E8B2EE0E375D4C57D"), }, } for i, tc := range testCases { t.Run(fmt.Sprintf("test-case %d", i), func(t *testing.T) { - signItem := MakeBlockSignItem(chainID, tc.vote.ToProto(), btcjson.LLMQType_5_60, tc.quorumHash) - t.Logf("hash %X id %X raw %X reqID %X", signItem.Hash, signItem.ID, signItem.Raw, signItem.ReqID) - require.Equal(t, tc.want, signItem) - require.Equal(t, tc.wantHash, signItem.Hash) + signItem := MakeBlockSignItem(chainID, tc.vote.ToProto(), quorumType, tc.quorumHash) + t.Logf("hash %X id %X raw %X reqID %X", signItem.MsgHash, signItem.SignHash, signItem.Msg, signItem.ID) + require.Equal(t, tc.want, signItem, "Got ID: %X", signItem.SignHash) + require.Equal(t, tc.wantHash, signItem.MsgHash) }) } } -func TestMakeVoteExtensionSignsData(t *testing.T) { - const chainID = "dash-platform" - logger := log.NewTestingLogger(t) - testCases := []struct { - vote Vote - quorumHash []byte - want map[types.VoteExtensionType][]SignItem - wantHash map[types.VoteExtensionType][][]byte - }{ - { - vote: Vote{ - Type: types.PrecommitType, - Height: 1001, - ValidatorProTxHash: tmbytes.MustHexDecode("9CC13F685BC3EA0FCA99B87F42ABCC934C6305AA47F62A32266A2B9D55306B7B"), - VoteExtensions: VoteExtensions{ - types.VoteExtensionType_DEFAULT: []VoteExtension{{Extension: []byte("default")}}, - types.VoteExtensionType_THRESHOLD_RECOVER: []VoteExtension{{Extension: []byte("threshold")}}, - }, - }, - quorumHash: tmbytes.MustHexDecode("6A12D9CF7091D69072E254B297AEF15997093E480FDE295E09A7DE73B31CEEDD"), - want: map[types.VoteExtensionType][]SignItem{ - types.VoteExtensionType_DEFAULT: { - newSignItem( - "FB95F2CA6530F02AC623589D7938643FF22AE79A75DD79AEA1C8871162DE675E", - "533524404D3A905F5AC9A30FCEB5A922EAD96F30DA02F979EE41C4342F540467", - "210A0764656661756C7411E903000000000000220D646173682D706C6174666F726D", - ), - }, - types.VoteExtensionType_THRESHOLD_RECOVER: { - newSignItem( - "fb95f2ca6530f02ac623589d7938643ff22ae79a75dd79aea1c8871162de675e", - "d3b7d53a0f9ca8072d47d6c18e782ee3155ef8dcddb010087030b6cbc63978bc", - "250a097468726573686f6c6411e903000000000000220d646173682d706c6174666f726d2801", - ), - }, - }, - wantHash: map[types.VoteExtensionType][][]byte{ - types.VoteExtensionType_DEFAULT: { - tmbytes.MustHexDecode("61519D79DE4C4D5AC5DD210C1BCE81AA24F76DD5581A24970E60112890C68FB7"), - }, - types.VoteExtensionType_THRESHOLD_RECOVER: { - tmbytes.MustHexDecode("46C72C423B74034E1AF574A99091B017C0698FEAA55C8B188BFD512FCADD3143"), - }, - }, - }, - } - for i, tc := range testCases { - t.Run(fmt.Sprintf("test-case #%d", i), func(t *testing.T) { - signItems, err := MakeVoteExtensionSignItems(chainID, tc.vote.ToProto(), btcjson.LLMQType_5_60, tc.quorumHash) - require.NoError(t, err) - for et, signs := range signItems { - for i, sign := range signs { - assert.Equal(t, tc.wantHash[et][i], sign.Hash, "want %X, actual %X", tc.wantHash[et][i], sign.Hash) - if !assert.Equal(t, tc.want[et][i], sign) { - logger.Error("invalid sign", "sign", sign, "type", et, "i", i) - } - } - } - }) +func newSignItem(reqID, signHash, raw, quorumHash string, quorumType btcjson.LLMQType) SignItem { + item := NewSignItem(quorumType, tmbytes.MustHexDecode(quorumHash), tmbytes.MustHexDecode(reqID), tmbytes.MustHexDecode(raw)) + item.SignHash = tmbytes.MustHexDecode(signHash) + return item +} + +func TestQuorumSignItem(t *testing.T) { + + si := SignItem{ + ID: mustHexDecode("87cda9461081793e7e31ab1def8ffbd453775a0f9987304598398d42a78d68d4"), + MsgHash: mustHexDecode("5ef9b9eecc4df7c5aee677c0a72816f4515999a539003cf4bbb6c15c39634c31"), + LlmqType: 106, + QuorumHash: mustHexDecode("366f07c9b80a2661563a33c09f02156720159b911186b4438ff281e537674771"), } + si.UpdateSignHash(true) + + expectID := tmbytes.Reverse(mustHexDecode("94635358f4c75a1d0b38314619d1c5d9a16f12961b5314d857e04f2eb61d78d2")) + + assert.EqualValues(t, expectID, si.SignHash) } -func newSignItem(reqID, ID, raw string) SignItem { - item := SignItem{ - ReqID: tmbytes.MustHexDecode(reqID), - ID: tmbytes.MustHexDecode(ID), - Raw: tmbytes.MustHexDecode(raw), +func mustHexDecode(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) } - item.Hash = crypto.Checksum(item.Raw) - return item + return b } diff --git a/types/signs_recoverer.go b/types/signs_recoverer.go index d9e7afdfa9..f84e9e1edc 100644 --- a/types/signs_recoverer.go +++ b/types/signs_recoverer.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/dashpay/tenderdash/crypto/bls12381" - tmproto "github.com/dashpay/tenderdash/proto/tendermint/types" + "github.com/dashpay/tenderdash/proto/tendermint/types" ) // SignsRecoverer is used to recover threshold block, state, and vote-extension signatures @@ -13,8 +13,11 @@ type SignsRecoverer struct { blockSigs [][]byte stateSigs [][]byte validatorProTxHashes [][]byte - voteExts [][]byte - voteExtSigs [][][]byte + // List of all vote extensions. Order matters. + voteExtensions VoteExtensions + + // true when the recovery of vote extensions was already executed + voteExtensionsRecovered bool quorumReached bool } @@ -55,11 +58,32 @@ func (v *SignsRecoverer) Recover() (*QuorumSigns, error) { return thresholdSigns, nil } +// Helper function that returns deep copy of recovered vote extensions with signatures from QuorumSigns. +// +// Note that this method doesn't recover threshold signatures. +// It requires to call Recover() method first. +// +// ## Panics +// +// Panics when the count of threshold vote extension signatures in QuorumSigns doesn't match recoverable vote extensions +func (v *SignsRecoverer) GetVoteExtensions(qs QuorumSigns) VoteExtensions { + if len(qs.VoteExtensionSignatures) != len(v.voteExtensions) { + panic(fmt.Sprintf("count of threshold vote extension signatures (%d) doesn't match recoverable vote extensions (%d)", + len(qs.VoteExtensionSignatures), len(v.voteExtensions))) + } + exts := v.voteExtensions.Copy() + for i, ext := range exts { + ext.SetSignature(qs.VoteExtensionSignatures[i]) + } + + return exts +} + func (v *SignsRecoverer) init(votes []*Vote) { v.blockSigs = nil v.stateSigs = nil v.validatorProTxHashes = nil - v.voteExtSigs = nil + for _, vote := range votes { v.addVoteSigs(vote) } @@ -69,19 +93,41 @@ func (v *SignsRecoverer) addVoteSigs(vote *Vote) { if vote == nil { return } + v.blockSigs = append(v.blockSigs, vote.BlockSignature) v.validatorProTxHashes = append(v.validatorProTxHashes, vote.ValidatorProTxHash) - v.addVoteExtensions(vote.VoteExtensions) + v.addVoteExtensionSigs(vote) } -func (v *SignsRecoverer) addVoteExtensions(voteExtensions VoteExtensions) { - extensions := voteExtensions[tmproto.VoteExtensionType_THRESHOLD_RECOVER] - for i, ext := range extensions { - if len(extensions) > len(v.voteExtSigs) { - v.voteExts = append(v.voteExts, ext.Extension) - v.voteExtSigs = append(v.voteExtSigs, nil) +// Add threshold-recovered vote extensions +func (v *SignsRecoverer) addVoteExtensionSigs(vote *Vote) { + if len(vote.VoteExtensions) == 0 { + return + } + + // initialize vote extensions + if v.voteExtensions.IsEmpty() { + v.voteExtensions = vote.VoteExtensions.Copy() + } + + // sanity check; this should be detected on higher layers + if vote.Type != types.PrecommitType || vote.BlockID.IsNil() { + panic(fmt.Sprintf("only non-nil precommits can have vote extensions, got: %s", vote.String())) + } + + if len(vote.VoteExtensions) != len(v.voteExtensions) { + panic(fmt.Sprintf("received vote extensions with different length: current %d, received %d", + len(v.voteExtensions), len(vote.VoteExtensions))) + } + + // append signatures from this vote to each extension + for i, ext := range vote.VoteExtensions { + if recoverable, ok := (v.voteExtensions[i]).(ThresholdVoteExtensionIf); ok { + if err := recoverable.AddThresholdSignature(vote.ValidatorProTxHash, ext.GetSignature()); err != nil { + panic(fmt.Errorf("failed to add vote %s to recover vote extension threshold sig: %w", vote.String(), err)) + } + v.voteExtensions[i] = recoverable } - v.voteExtSigs[i] = append(v.voteExtSigs[i], ext.Signature) } } @@ -94,20 +140,32 @@ func (v *SignsRecoverer) recoverBlockSig(thresholdSigns *QuorumSigns) error { return nil } -func (v *SignsRecoverer) recoverVoteExtensionSigs(thresholdSigns *QuorumSigns) error { +// recoverVoteExtensionSigs recovers threshold signatures for vote-extensions +func (v *SignsRecoverer) recoverVoteExtensionSigs(quorumSigs *QuorumSigns) error { if !v.quorumReached { return nil } - var err error - thresholdSigns.ExtensionSigns = make([]ThresholdExtensionSign, len(v.voteExtSigs)) - for i, sigs := range v.voteExtSigs { - if len(sigs) > 0 { - thresholdSigns.ExtensionSigns[i].Extension = v.voteExts[i] - thresholdSigns.ExtensionSigns[i].ThresholdSignature, err = bls12381.RecoverThresholdSignatureFromShares(sigs, v.validatorProTxHashes) + + if quorumSigs.VoteExtensionSignatures == nil { + quorumSigs.VoteExtensionSignatures = make([][]byte, len(v.voteExtensions)) + } + + if len(v.voteExtensions) != len(quorumSigs.VoteExtensionSignatures) { + return fmt.Errorf("count of threshold vote extension signatures (%d) doesn't match recoverable vote extensions (%d)", + len(quorumSigs.VoteExtensionSignatures), len(v.voteExtensions)) + } + + for i, ext := range v.voteExtensions { + if extension, ok := ext.(ThresholdVoteExtensionIf); ok { + sig, err := extension.ThresholdRecover() if err != nil { - return fmt.Errorf("error recovering threshold vote-extension sig: %w", err) + return fmt.Errorf("error recovering threshold signature for vote extension %d: %w", i, err) } + quorumSigs.VoteExtensionSignatures[i] = sig } } + + v.voteExtensionsRecovered = true + return nil } diff --git a/types/signs_recoverer_test.go b/types/signs_recoverer_test.go index e805df56f8..940197fe79 100644 --- a/types/signs_recoverer_test.go +++ b/types/signs_recoverer_test.go @@ -23,7 +23,8 @@ func TestSigsRecoverer(t *testing.T) { quorumType := crypto.SmallQuorumType() quorumHash := crypto.RandQuorumHash() testCases := []struct { - votes []*Vote + expectInvalidSig bool + votes []*Vote }{ { votes: []*Vote{ @@ -32,8 +33,8 @@ func TestSigsRecoverer(t *testing.T) { Type: tmproto.PrecommitType, BlockID: blockID, VoteExtensions: mockVoteExtensions(t, - tmproto.VoteExtensionType_DEFAULT, "default", tmproto.VoteExtensionType_THRESHOLD_RECOVER, "threshold", + tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, crypto.Checksum([]byte("threshold-raw")), ), }, { @@ -41,7 +42,75 @@ func TestSigsRecoverer(t *testing.T) { Type: tmproto.PrecommitType, BlockID: blockID, VoteExtensions: mockVoteExtensions(t, - tmproto.VoteExtensionType_DEFAULT, "default", + tmproto.VoteExtensionType_THRESHOLD_RECOVER, "threshold", + tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, crypto.Checksum([]byte("threshold-raw")), + ), + }, + }, + }, + { + votes: []*Vote{ + { + ValidatorProTxHash: crypto.RandProTxHash(), + Type: tmproto.PrecommitType, + BlockID: blockID, + VoteExtensions: mockVoteExtensions(t, + tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, crypto.Checksum([]byte("threshold-raw")), + tmproto.VoteExtensionType_THRESHOLD_RECOVER, "threshold", + ), + }, + { + ValidatorProTxHash: crypto.RandProTxHash(), + Type: tmproto.PrecommitType, + BlockID: blockID, + VoteExtensions: mockVoteExtensions(t, + tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, crypto.Checksum([]byte("threshold-raw")), + tmproto.VoteExtensionType_THRESHOLD_RECOVER, "threshold", + ), + }, + }, + }, + { + expectInvalidSig: true, + votes: []*Vote{ + { + ValidatorProTxHash: crypto.RandProTxHash(), + Type: tmproto.PrecommitType, + BlockID: blockID, + VoteExtensions: mockVoteExtensions(t, + tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, crypto.Checksum([]byte("threshold-raw")), + tmproto.VoteExtensionType_THRESHOLD_RECOVER, "threshold", + ), + }, + { + ValidatorProTxHash: crypto.RandProTxHash(), + Type: tmproto.PrecommitType, + BlockID: blockID, + VoteExtensions: mockVoteExtensions(t, + tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, crypto.Checksum([]byte("threshold-raw")), + tmproto.VoteExtensionType_THRESHOLD_RECOVER, "threshold1", + ), + }, + }, + }, + { + expectInvalidSig: true, + votes: []*Vote{ + { + ValidatorProTxHash: crypto.RandProTxHash(), + Type: tmproto.PrecommitType, + BlockID: blockID, + VoteExtensions: mockVoteExtensions(t, + tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, crypto.Checksum([]byte("threshold-raw1")), + tmproto.VoteExtensionType_THRESHOLD_RECOVER, "threshold", + ), + }, + { + ValidatorProTxHash: crypto.RandProTxHash(), + Type: tmproto.PrecommitType, + BlockID: blockID, + VoteExtensions: mockVoteExtensions(t, + tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, crypto.Checksum([]byte("threshold-raw")), tmproto.VoteExtensionType_THRESHOLD_RECOVER, "threshold", ), }, @@ -78,7 +147,11 @@ func TestSigsRecoverer(t *testing.T) { thresholdVoteSigns, err := sr.Recover() require.NoError(t, err) err = quorumSigns.Verify(thresholdPubKey, *thresholdVoteSigns) - require.NoError(t, err) + if tc.expectInvalidSig { + require.Error(t, err) + } else { + require.NoError(t, err) + } }) } } @@ -108,7 +181,7 @@ func TestSigsRecoverer_UsingVoteSet(t *testing.T) { Type: tmproto.PrecommitType, BlockID: blockID, VoteExtensions: mockVoteExtensions(t, - tmproto.VoteExtensionType_DEFAULT, "default", + tmproto.VoteExtensionType_THRESHOLD_RECOVER, "default", tmproto.VoteExtensionType_THRESHOLD_RECOVER, "threshold", ), } @@ -130,18 +203,20 @@ func TestSigsRecoverer_UsingVoteSet(t *testing.T) { // the format of pairs is // 1. the length of pairs must be even // 2. each pair consist of 2 elements: type and extension value -// example: types.VoteExtensionType_DEFAULT, "defailt", types.VoteExtensionType_THRESHOLD_RECOVER, "threshold" +// example: types.VoteExtensionType_THRESHOLD_RECOVER, "defailt", types.VoteExtensionType_THRESHOLD_RECOVER, "threshold" func mockVoteExtensions(t *testing.T, pairs ...interface{}) VoteExtensions { if len(pairs)%2 != 0 { t.Fatalf("the pairs length must be even") } - ve := make(VoteExtensions) + ve := make(VoteExtensions, 0) for i := 0; i < len(pairs); i += 2 { - et, ok := pairs[i].(tmproto.VoteExtensionType) + extensionType, ok := pairs[i].(tmproto.VoteExtensionType) if !ok { t.Fatalf("given unsupported type %T", pairs[i]) } - ext := VoteExtension{} + ext := tmproto.VoteExtension{ + Type: extensionType, + } switch v := pairs[i+1].(type) { case string: ext.Extension = []byte(v) @@ -150,7 +225,8 @@ func mockVoteExtensions(t *testing.T, pairs ...interface{}) VoteExtensions { default: t.Fatalf("given unsupported type %T", pairs[i+1]) } - ve[et] = append(ve[et], ext) + ve.Add(ext) + } return ve } diff --git a/types/test_util.go b/types/test_util.go index 1f0ca2b7f7..0418d1f082 100644 --- a/types/test_util.go +++ b/types/test_util.go @@ -45,10 +45,14 @@ func makeCommit( Round: round, Type: tmproto.PrecommitType, BlockID: blockID, - VoteExtensions: VoteExtensions{ - tmproto.VoteExtensionType_DEFAULT: []VoteExtension{{Extension: []byte("default")}}, - tmproto.VoteExtensionType_THRESHOLD_RECOVER: []VoteExtension{{Extension: []byte("threshold")}}, - }, + VoteExtensions: VoteExtensionsFromProto( + &tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, + Extension: crypto.Checksum([]byte("raw"))}, + &tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("threshold")}, + ), } _, err = signAddVote(ctx, validators[i], vote, voteSet) @@ -68,9 +72,11 @@ func signAddVote(ctx context.Context, privVal PrivValidator, vote *Vote, voteSet if err != nil { return false, err } + err = vote.PopulateSignsFromProto(v) if err != nil { return false, err } + return voteSet.AddVote(vote) } diff --git a/types/tx.go b/types/tx.go index 9080ad564b..ec1c89c7ca 100644 --- a/types/tx.go +++ b/types/tx.go @@ -7,6 +7,8 @@ import ( "fmt" "sort" + "github.com/rs/zerolog" + abci "github.com/dashpay/tenderdash/abci/types" "github.com/dashpay/tenderdash/crypto" "github.com/dashpay/tenderdash/crypto/merkle" @@ -103,6 +105,12 @@ func (txs Txs) ToSliceOfBytes() [][]byte { return txBzs } +func (txs Txs) MarshalZerologArray(e *zerolog.Array) { + for _, tx := range txs { + e.Hex(tx.Hash()) + } +} + // TxRecordSet contains indexes into an underlying set of transactions. // These indexes are useful for validating and working with a list of TxRecords // from the PrepareProposal response. diff --git a/types/validation_test.go b/types/validation_test.go index 8f5809cd44..9d1224c0e9 100644 --- a/types/validation_test.go +++ b/types/validation_test.go @@ -31,21 +31,24 @@ func TestValidatorSet_VerifyCommit_All(t *testing.T) { vote.ValidatorProTxHash = proTxHash v := vote.ToProto() - quorumSigns, err := MakeQuorumSigns(chainID, btcjson.LLMQType_5_60, quorumHash, v) + dataToSign, err := MakeQuorumSigns(chainID, btcjson.LLMQType_5_60, quorumHash, v) require.NoError(t, err) - blockSig, err := privKey.SignDigest(quorumSigns.Block.ID) + sig, err := dataToSign.SignWithPrivkey(privKey) + require.NoError(t, err) + + vote.BlockSignature = sig.BlockSign + err = vote.VoteExtensions.SetSignatures(sig.VoteExtensionSignatures) require.NoError(t, err) - vote.BlockSignature = blockSig commit := NewCommit(vote.Height, vote.Round, vote.BlockID, + vote.VoteExtensions, + &CommitSigns{ - QuorumSigns: QuorumSigns{ - BlockSign: blockSig, - }, - QuorumHash: quorumHash, + QuorumSigns: sig, + QuorumHash: quorumHash, }, ) @@ -83,13 +86,20 @@ func TestValidatorSet_VerifyCommit_All(t *testing.T) { {"wrong height", chainID, vote.BlockID, vote.Height - 1, commit, true}, {"threshold block signature is invalid", chainID, vote.BlockID, vote.Height, - NewCommit(vote.Height, vote.Round, vote.BlockID, &CommitSigns{QuorumHash: quorumHash}), true}, + NewCommit(vote.Height, vote.Round, vote.BlockID, vote.VoteExtensions, + &CommitSigns{ + QuorumHash: quorumHash, + QuorumSigns: QuorumSigns{ + BlockSign: []byte("invalid block signature"), + VoteExtensionSignatures: sig.VoteExtensionSignatures, + }}), true}, {"threshold block signature is invalid", chainID, vote.BlockID, vote.Height, NewCommit(vote.Height, vote.Round, vote.BlockID, + vote.VoteExtensions, &CommitSigns{ QuorumHash: quorumHash, - QuorumSigns: QuorumSigns{BlockSign: vote2.BlockSignature}, + QuorumSigns: QuorumSigns{BlockSign: vote2.BlockSignature, VoteExtensionSignatures: vote2.VoteExtensions.GetSignatures()}, }, ), true}, } @@ -136,11 +146,16 @@ func TestValidatorSet_VerifyCommit_CheckThresholdSignatures(t *testing.T) { assert.Contains(t, err.Error(), "threshold block signature is invalid") } + goodVote := voteSet.GetByIndex(0) recoverer := NewSignsRecoverer(voteSet.votes) thresholdSigns, err := recoverer.Recover() require.NoError(t, err) commit.ThresholdBlockSignature = thresholdSigns.BlockSign - commit.ThresholdVoteExtensions = thresholdSigns.ExtensionSigns + exts := goodVote.VoteExtensions.Copy() + for i, ext := range exts { + ext.SetSignature(thresholdSigns.VoteExtensionSignatures[i]) + } + commit.ThresholdVoteExtensions = exts.ToProto() err = valSet.VerifyCommit(chainID, blockID, h, commit) require.NoError(t, err) } diff --git a/types/vote.go b/types/vote.go index be7988e0f8..4f49f95f53 100644 --- a/types/vote.go +++ b/types/vote.go @@ -11,7 +11,6 @@ import ( "github.com/dashpay/tenderdash/crypto" "github.com/dashpay/tenderdash/crypto/bls12381" - "github.com/dashpay/tenderdash/internal/libs/protoio" tmbytes "github.com/dashpay/tenderdash/libs/bytes" tmcons "github.com/dashpay/tenderdash/proto/tendermint/consensus" tmproto "github.com/dashpay/tenderdash/proto/tendermint/types" @@ -25,12 +24,6 @@ const ( MaxVoteBytesEd25519 int64 = 209 ) -// VoteExtensionTypes is a list of all possible vote-extension types -var VoteExtensionTypes = []tmproto.VoteExtensionType{ - tmproto.VoteExtensionType_DEFAULT, - tmproto.VoteExtensionType_THRESHOLD_RECOVER, -} - func MaxVoteBytesForKeyType(keyType crypto.KeyType) int64 { switch keyType { case crypto.Ed25519: @@ -42,19 +35,20 @@ func MaxVoteBytesForKeyType(keyType crypto.KeyType) int64 { } var ( - ErrVoteUnexpectedStep = errors.New("unexpected step") - ErrVoteInvalidValidatorIndex = errors.New("invalid validator index") - ErrVoteInvalidValidatorAddress = errors.New("invalid validator address") - ErrVoteInvalidSignature = errors.New("invalid signature") - ErrVoteInvalidBlockHash = errors.New("invalid block hash") - ErrVoteNonDeterministicSignature = errors.New("non-deterministic signature") - ErrVoteNil = errors.New("nil vote") - ErrVoteInvalidExtension = errors.New("invalid vote extension") - ErrVoteInvalidValidatorProTxHash = errors.New("invalid validator pro_tx_hash") - ErrVoteInvalidValidatorPubKeySize = errors.New("invalid validator public key size") - ErrVoteInvalidBlockSignature = errors.New("invalid block signature") - ErrVoteInvalidStateSignature = errors.New("invalid state signature") - ErrVoteStateSignatureShouldBeNil = errors.New("state signature when voting for nil block") + ErrVoteUnexpectedStep = errors.New("unexpected step") + ErrVoteInvalidValidatorIndex = errors.New("invalid validator index") + ErrVoteInvalidValidatorAddress = errors.New("invalid validator address") + ErrVoteInvalidSignature = errors.New("invalid signature") + ErrVoteInvalidBlockHash = errors.New("invalid block hash") + ErrVoteNonDeterministicSignature = errors.New("non-deterministic signature") + ErrVoteNil = errors.New("nil vote") + ErrVoteInvalidExtension = errors.New("invalid vote extension") + ErrVoteExtensionTypeWrongForRequestID = errors.New("provided vote extension type does not support sign request ID") + ErrVoteInvalidValidatorProTxHash = errors.New("invalid validator pro_tx_hash") + ErrVoteInvalidValidatorPubKeySize = errors.New("invalid validator public key size") + ErrVoteInvalidBlockSignature = errors.New("invalid block signature") + ErrVoteInvalidStateSignature = errors.New("invalid state signature") + ErrVoteStateSignatureShouldBeNil = errors.New("state signature when voting for nil block") ) type ErrVoteConflictingVotes struct { @@ -108,33 +102,14 @@ func VoteFromProto(pv *tmproto.Vote) (*Vote, error) { ValidatorProTxHash: pv.ValidatorProTxHash, ValidatorIndex: pv.ValidatorIndex, BlockSignature: pv.BlockSignature, - VoteExtensions: VoteExtensionsFromProto(pv.VoteExtensions), + VoteExtensions: VoteExtensionsFromProto(pv.VoteExtensions...), }, nil } -// VoteExtensionSignBytes returns the proto-encoding of the canonicalized vote -// extension for signing. Panics if the marshaling fails. -// -// Similar to VoteSignBytes, the encoded Protobuf message is varint -// length-prefixed for backwards-compatibility with the Amino encoding. -func VoteExtensionSignBytes(chainID string, height int64, round int32, ext *tmproto.VoteExtension) []byte { - pb := CanonicalizeVoteExtension(chainID, ext, height, round) - bz, err := protoio.MarshalDelimited(&pb) - if err != nil { - panic(err) - } - return bz -} - -// VoteExtensionRequestID returns vote extension request ID -func VoteExtensionRequestID(height int64, round int32) []byte { - return heightRoundRequestID("dpevote", height, round) -} - // VoteBlockSignID returns signID that should be signed for the block func VoteBlockSignID(chainID string, vote *tmproto.Vote, quorumType btcjson.LLMQType, quorumHash []byte) []byte { signID := MakeBlockSignItem(chainID, vote, quorumType, quorumHash) - return signID.ID + return signID.SignHash } // Copy creates a deep copy of the vote @@ -236,6 +211,7 @@ func (vote *Vote) Verify( return err } + // TODO check why we don't verify extensions here return vote.verifySign(pubKey, quorumSignData, WithVerifyExtensions(false)) } @@ -281,8 +257,10 @@ func (vote *Vote) verifySign( func (vote *Vote) makeQuorumSigns() QuorumSigns { return QuorumSigns{ - BlockSign: vote.BlockSignature, - ExtensionSigns: MakeThresholdExtensionSigns(vote.VoteExtensions), + BlockSign: vote.BlockSignature, + VoteExtensionSignatures: vote.VoteExtensions.Filter(func(ext VoteExtensionIf) bool { + return ext.IsThresholdRecoverable() + }).GetSignatures(), } } @@ -340,8 +318,7 @@ func (vote *Vote) ValidateBasic() error { } if vote.Type == tmproto.PrecommitType && !vote.BlockID.IsNil() { - err := vote.VoteExtensions.Validate() - if err != nil { + if err := vote.VoteExtensions.Validate(); err != nil { return err } } @@ -403,6 +380,7 @@ func (vote *Vote) MarshalZerologObject(e *zerolog.Event) { e.Str("val_proTxHash", vote.ValidatorProTxHash.ShortString()) e.Int32("val_index", vote.ValidatorIndex) e.Bool("nil", vote.BlockID.IsNil()) + e.Array("extensions", vote.VoteExtensions) } func (vote *Vote) HasVoteMessage() *tmcons.HasVote { diff --git a/types/vote_dash.go b/types/vote_dash.go index aae8f430b8..f2a5b8fb3d 100644 --- a/types/vote_dash.go +++ b/types/vote_dash.go @@ -5,23 +5,11 @@ import tmproto "github.com/dashpay/tenderdash/proto/tendermint/types" // PopulateSignsFromProto updates the signatures of the current Vote with values are taken from the Vote's protobuf func (vote *Vote) PopulateSignsFromProto(pv *tmproto.Vote) error { vote.BlockSignature = pv.BlockSignature - return vote.VoteExtensions.CopySignsFromProto(pv.VoteExtensionsToMap()) + return vote.VoteExtensions.CopySignsFromProto(pv.VoteExtensions) } // PopulateSignsToProto updates the signatures of the given protobuf Vote entity with values are taken from the current Vote's func (vote *Vote) PopulateSignsToProto(pv *tmproto.Vote) error { pv.BlockSignature = vote.BlockSignature - return vote.VoteExtensions.CopySignsToProto(pv.VoteExtensionsToMap()) -} - -// GetVoteExtensionsSigns returns the list of signatures for given vote-extension type -func (vote *Vote) GetVoteExtensionsSigns(extType tmproto.VoteExtensionType) [][]byte { - if vote.VoteExtensions == nil { - return nil - } - sigs := make([][]byte, len(vote.VoteExtensions[extType])) - for i, ext := range vote.VoteExtensions[extType] { - sigs[i] = ext.Signature - } - return sigs + return vote.VoteExtensions.CopySignsToProto(pv.VoteExtensions) } diff --git a/types/vote_extension.go b/types/vote_extension.go index dcce659a33..784da379a5 100644 --- a/types/vote_extension.go +++ b/types/vote_extension.go @@ -2,206 +2,497 @@ package types import ( "bytes" + "crypto/sha256" "errors" "fmt" + "math/big" + + "github.com/dashpay/dashd-go/btcjson" + "github.com/hashicorp/go-multierror" + "github.com/rs/zerolog" abci "github.com/dashpay/tenderdash/abci/types" + "github.com/dashpay/tenderdash/crypto" + "github.com/dashpay/tenderdash/crypto/bls12381" + "github.com/dashpay/tenderdash/internal/libs/protoio" tmbytes "github.com/dashpay/tenderdash/libs/bytes" tmproto "github.com/dashpay/tenderdash/proto/tendermint/types" ) var ( - errExtensionSignEmpty = errors.New("vote extension signature is missing") - errExtensionSignTooBig = fmt.Errorf("vote extension signature is too big (max: %d)", SignatureSize) - errUnableCopySigns = errors.New("unable copy signatures the sizes of extensions are not equal") + errUnableCopySigns = errors.New("unable to copy signatures: the sizes of extensions are not equal") ) // VoteExtensions is a container where the key is vote-extension type and value is a list of VoteExtension -type VoteExtensions map[tmproto.VoteExtensionType][]VoteExtension +type VoteExtensions []VoteExtensionIf // NewVoteExtensionsFromABCIExtended returns vote-extensions container for given ExtendVoteExtension func NewVoteExtensionsFromABCIExtended(exts []*abci.ExtendVoteExtension) VoteExtensions { - voteExtensions := make(VoteExtensions) + voteExtensions := make(VoteExtensions, 0, len(exts)) + for _, ext := range exts { - voteExtensions.Add(ext.Type, ext.Extension) + ve := ext.ToVoteExtension() + voteExtensions.Add(ve) } return voteExtensions } -// Add creates and adds VoteExtension into a container by vote-extension type -func (e VoteExtensions) Add(t tmproto.VoteExtensionType, ext []byte) { - e[t] = append(e[t], VoteExtension{Extension: ext}) +// Add creates and adds protobuf VoteExtension into a container by vote-extension type +func (e *VoteExtensions) Add(ext tmproto.VoteExtension) { + *e = append(*e, VoteExtensionFromProto(ext)) +} + +// MakeVoteExtensionSignItems creates a list SignItem structs for a vote extensions +func (e VoteExtensions) SignItems( + chainID string, + quorumType btcjson.LLMQType, + quorumHash []byte, + height int64, + round int32, +) ([]SignItem, error) { + + items := make([]SignItem, 0, e.Len()) + + for _, ext := range e { + item, err := ext.SignItem(chainID, height, round, quorumType, quorumHash) + if err != nil { + return nil, err + } + + items = append(items, item) + } + + return items, nil +} + +func (e VoteExtensions) GetSignatures() [][]byte { + signatures := make([][]byte, 0, e.Len()) + + for _, ext := range e { + signatures = append(signatures, ext.GetSignature()) + } + + return signatures +} + +func (e VoteExtensions) GetExtensions() [][]byte { + exts := make([][]byte, 0, e.Len()) + + for _, ext := range e { + exts = append(exts, ext.GetExtension()) + } + + return exts } // Validate returns error if an added vote-extension is invalid func (e VoteExtensions) Validate() error { - for _, et := range VoteExtensionTypes { - for _, ext := range e[et] { - err := ext.Validate() - if err != nil { - return err - } + var errs *multierror.Error + + for i, ext := range e { + if err := ext.Validate(); err != nil { + errs = multierror.Append(errs, fmt.Errorf("invalid %s vote extension %d: %w", ext.ToProto().Type, i, err)) } } - return nil + + return errs.ErrorOrNil() } // IsEmpty returns true if a vote-extension container is empty, otherwise false func (e VoteExtensions) IsEmpty() bool { - for _, exts := range e { - if len(exts) > 0 { - return false - } - } - return true + return len(e) == 0 } // ToProto transforms the current state of vote-extension container into VoteExtensions's protobuf func (e VoteExtensions) ToProto() []*tmproto.VoteExtension { - extensions := make([]*tmproto.VoteExtension, 0, e.totalCount()) - for _, t := range VoteExtensionTypes { - for _, ext := range e[t] { - extensions = append(extensions, &tmproto.VoteExtension{ - Type: t, - Extension: ext.Extension, - Signature: ext.Signature, - }) - } + extensions := make([]*tmproto.VoteExtension, 0, e.Len()) + for _, ext := range e { + pbExt := ext.ToProto() + extensions = append(extensions, &pbExt) } + return extensions } // ToExtendProto transforms the current state of vote-extension container into ExtendVoteExtension's protobuf func (e VoteExtensions) ToExtendProto() []*abci.ExtendVoteExtension { - proto := make([]*abci.ExtendVoteExtension, 0, e.totalCount()) - for _, et := range VoteExtensionTypes { - for _, ext := range e[et] { - proto = append(proto, &abci.ExtendVoteExtension{ - Type: et, - Extension: ext.Extension, - }) + proto := make([]*abci.ExtendVoteExtension, 0, e.Len()) + + for _, ext := range e { + pb := ext.ToProto() + eve := &abci.ExtendVoteExtension{ + Type: pb.Type, + Extension: pb.Extension, } + + if pb.XSignRequestId != nil { + if src := pb.GetSignRequestId(); len(src) > 0 { + eve.XSignRequestId = &abci.ExtendVoteExtension_SignRequestId{ + SignRequestId: bytes.Clone(src), + } + } + } + + proto = append(proto, eve) } + return proto } // Fingerprint returns a fingerprint of all vote-extensions in a state of this container func (e VoteExtensions) Fingerprint() []byte { - cnt := 0 - for _, v := range e { - cnt += len(v) - } - l := make([][]byte, 0, cnt) - for _, et := range VoteExtensionTypes { - for _, ext := range e[et] { - l = append(l, ext.Extension) + if e.IsEmpty() { + return tmbytes.Fingerprint(nil) + } + sha := sha256.New() + for _, ext := range e { + pb := ext.ToProto() + // type + extension + if _, err := sha.Write(big.NewInt(int64(pb.Type)).Bytes()); err != nil { + panic(err) + } + if _, err := sha.Write(pb.Extension); err != nil { + panic(err) } } - return tmbytes.Fingerprint(bytes.Join(l, nil)) + return tmbytes.Fingerprint(sha.Sum(nil)) } // IsSameWithProto compares the current state of the vote-extension with the same in VoteExtensions's protobuf // checks only the value of extensions -func (e VoteExtensions) IsSameWithProto(proto tmproto.VoteExtensions) bool { - for t, extensions := range e { - if len(proto[t]) != len(extensions) { +func (e VoteExtensions) IsSameWithProto(right tmproto.VoteExtensions) bool { + if len(e) != len(right) { + return false + } + + for t, ext := range e { + pb := ext.ToProto() + other := right[t] + if !pb.Equal(other) { return false } - for i, ext := range extensions { - if !bytes.Equal(ext.Extension, proto[t][i].Extension) { - return false - } - } } return true } -func (e VoteExtensions) totalCount() int { - cnt := 0 - for _, exts := range e { - cnt += len(exts) +func (e VoteExtensions) Len() int { + return len(e) +} + +// VoteExtensionsFromProto creates VoteExtensions container from VoteExtensions's protobuf +func VoteExtensionsFromProto(pve ...*tmproto.VoteExtension) VoteExtensions { + if len(pve) == 0 { + return nil + } + voteExtensions := make(VoteExtensions, 0, len(pve)) + for _, ext := range pve { + voteExtensions = append(voteExtensions, VoteExtensionFromProto(*ext)) + } + + return voteExtensions +} + +// Copy creates a deep copy of VoteExtensions +func (e VoteExtensions) Copy() VoteExtensions { + if e == nil || e.IsEmpty() { + return nil + } + + copied := make(VoteExtensions, 0, len(e)) + for _, ext := range e { + copied = append(copied, ext.Copy()) + } + + return copied +} + +// Filter returns a new VoteExtensions container with vote-extensions filtered by provided function. +// It does not copy data, just creates a new container with references to the same data +func (e VoteExtensions) Filter(fn func(ext VoteExtensionIf) bool) VoteExtensions { + result := make(VoteExtensions, 0, len(e)) + for _, ext := range e { + if fn(ext) { + result = append(result, ext) + } } - return cnt + + return result[:] } -// VoteExtension represents a vote extension data, with possible types: default or threshold recover -type VoteExtension struct { - Extension []byte `json:"extension"` - Signature tmbytes.HexBytes `json:"signature"` +// CopySignsFromProto copies the signatures from VoteExtensions's protobuf into the current VoteExtension state +func (e VoteExtensions) CopySignsFromProto(src tmproto.VoteExtensions) error { + if len(e) != len(src) { + return errUnableCopySigns + } + + for i, ext := range e { + ext.SetSignature(src[i].Signature) + } + + return nil } -// Validate ... -func (v *VoteExtension) Validate() error { - if len(v.Extension) > 0 && len(v.Signature) == 0 { - return errExtensionSignEmpty +func (e VoteExtensions) SetSignatures(src [][]byte) error { + if len(e) != len(src) { + return errUnableCopySigns } - if len(v.Signature) > SignatureSize { - return errExtensionSignTooBig + + for i, ext := range e { + ext.SetSignature(src[i]) } + return nil } -// Clone returns a copy of current vote-extension -func (v *VoteExtension) Clone() VoteExtension { - return VoteExtension{ - Extension: v.Extension, - Signature: v.Signature, +// CopySignsToProto copies the signatures from the current VoteExtensions into VoteExtension's protobuf +func (e VoteExtensions) CopySignsToProto(dest tmproto.VoteExtensions) error { + if len(e) != len(dest) { + return errUnableCopySigns } + for i, ext := range e { + pb := ext.ToProto() + dest[i].Signature = pb.Signature + } + + return nil } -// VoteExtensionsFromProto creates VoteExtensions container from VoteExtensions's protobuf -func VoteExtensionsFromProto(pve []*tmproto.VoteExtension) VoteExtensions { - if pve == nil { +// Marshal VoteExtensions as zerolog array +func (e VoteExtensions) MarshalZerologArray(a *zerolog.Array) { + for _, ext := range e { + a.Object(ext) + } +} + +type VoteExtensionIf interface { + // Return type of this vote extension + GetType() tmproto.VoteExtensionType + // Return extension bytes + GetExtension() []byte + // Return signature bytes + GetSignature() []byte + // Copy creates a deep copy of VoteExtension + Copy() VoteExtensionIf + // ToProto transforms the current state of vote-extension into VoteExtension's proto-generated object. + // It should prioritize performance and can do a shallow copy of the vote-extension, + // so the returned object should not be modified. + ToProto() tmproto.VoteExtension + SignItem(chainID string, height int64, round int32, quorumType btcjson.LLMQType, quorumHash []byte) (SignItem, error) + IsThresholdRecoverable() bool + // Validate returns error if a vote-extension is invalid. + // It should not modify the state of the vote-extension. + Validate() error + + SetSignature(sig []byte) + + zerolog.LogObjectMarshaler +} + +type ThresholdVoteExtensionIf interface { + VoteExtensionIf + + AddThresholdSignature(validator ProTxHash, sig []byte) error + // Recover threshold signature from collected signatures + // + // Returns recovered signature or error. VoteExtension, including any signature already set, is not modified. + ThresholdRecover() ([]byte, error) +} + +func VoteExtensionFromProto(ve tmproto.VoteExtension) VoteExtensionIf { + switch ve.Type { + case tmproto.VoteExtensionType_DEFAULT: + return &GenericVoteExtension{VoteExtension: ve} + case tmproto.VoteExtensionType_THRESHOLD_RECOVER: + ext := newThresholdVoteExtension(ve) + return &ext + case tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW: + ext := newThresholdVoteExtension(ve) + return &ThresholdRawVoteExtension{ThresholdVoteExtension: ext} + default: + panic(fmt.Errorf("unknown vote extension type: %s", ve.Type.String())) + } +} + +func newThresholdVoteExtension(ve tmproto.VoteExtension) ThresholdVoteExtension { + return ThresholdVoteExtension{GenericVoteExtension: GenericVoteExtension{VoteExtension: ve}} +} + +// VOTE EXTENSION TYPES + +// GenericVoteExtension is a default type of VoteExtension +type GenericVoteExtension struct { + tmproto.VoteExtension +} + +func (e GenericVoteExtension) Copy() VoteExtensionIf { + return &GenericVoteExtension{VoteExtension: e.VoteExtension.Copy()} +} + +func (e GenericVoteExtension) ToProto() tmproto.VoteExtension { + return e.VoteExtension.Clone() +} + +func (e GenericVoteExtension) SignItem(chainID string, height int64, round int32, quorumType btcjson.LLMQType, quorumHash []byte) (SignItem, error) { + requestID, err := voteExtensionRequestID(height, round) + if err != nil { + return SignItem{}, err + } + canonical, err := CanonicalizeVoteExtension(chainID, &e.VoteExtension, height, round) + if err != nil { + panic(err) + } + + signBytes, err := protoio.MarshalDelimited(&canonical) + if err != nil { + panic(err) + } + + si := NewSignItem(quorumType, quorumHash, requestID, signBytes) + // we do not reverse fields when calculating SignHash for vote extensions + // si.UpdateSignHash(false) + return si, nil +} + +func (e GenericVoteExtension) IsThresholdRecoverable() bool { + return false +} + +func (e *GenericVoteExtension) SetSignature(sig []byte) { + e.Signature = sig +} + +func (e GenericVoteExtension) MarshalZerologObject(o *zerolog.Event) { + o.Str("type", e.GetType().String()) + o.Hex("extension", e.GetExtension()) + o.Hex("signature", e.GetSignature()) + o.Hex("sign_request_id", e.GetSignRequestId()) +} + +//nolint:stylecheck // name is the same as in protobuf-generated code +func (e GenericVoteExtension) GetSignRequestId() []byte { + if e.XSignRequestId == nil { return nil } - voteExtensions := make(VoteExtensions) - for _, ext := range pve { - voteExtensions[ext.Type] = append(voteExtensions[ext.Type], VoteExtension{ - Extension: ext.Extension, - Signature: ext.Signature, - }) + id, ok := e.XSignRequestId.(*tmproto.VoteExtension_SignRequestId) + if !ok || id == nil { + return nil } - return voteExtensions + + return id.SignRequestId } -// Copy creates a deep copy of VoteExtensions -func (e VoteExtensions) Copy() VoteExtensions { - copied := make(VoteExtensions, len(e)) - for extType, extensions := range e { - copied[extType] = make([]VoteExtension, len(extensions)) - for k, v := range extensions { - copied[extType][k] = v.Clone() - } +type ThresholdSignature [bls12381.SignatureSize]byte + +// ThresholdVoteExtension is a threshold type of VoteExtension +type ThresholdVoteExtension struct { + GenericVoteExtension + // threshold signatures for this vote extension, collected from validators + thresholdSignatures map[[crypto.ProTxHashSize]byte]ThresholdSignature +} + +func (e ThresholdVoteExtension) Copy() VoteExtensionIf { + return &ThresholdVoteExtension{GenericVoteExtension: GenericVoteExtension{ + VoteExtension: e.VoteExtension.Copy(), + }, } +} - return copied +func (e ThresholdVoteExtension) IsThresholdRecoverable() bool { + return true } -// CopySignsFromProto copies the signatures from VoteExtensions's protobuf into the current VoteExtension state -func (e VoteExtensions) CopySignsFromProto(src tmproto.VoteExtensions) error { - return e.copySigns(src, func(a *tmproto.VoteExtension, b *VoteExtension) { - b.Signature = a.Signature - }) +func (e *ThresholdVoteExtension) AddThresholdSignature(validator ProTxHash, sig []byte) error { + if e.thresholdSignatures == nil { + e.thresholdSignatures = make(map[[crypto.ProTxHashSize]byte]ThresholdSignature) + } + + proTxHash := [crypto.ProTxHashSize]byte(validator) + e.thresholdSignatures[proTxHash] = ThresholdSignature(sig) + + return nil } -// CopySignsToProto copies the signatures from the current VoteExtensions into VoteExtension's protobuf -func (e VoteExtensions) CopySignsToProto(dist tmproto.VoteExtensions) error { - return e.copySigns(dist, func(a *tmproto.VoteExtension, b *VoteExtension) { - a.Signature = b.Signature - }) -} - -func (e VoteExtensions) copySigns( - protoMap tmproto.VoteExtensions, - modifier func(a *tmproto.VoteExtension, b *VoteExtension), -) error { - for t, exts := range e { - if len(exts) != len(protoMap[t]) { - return errUnableCopySigns +// ThresholdRecover recovers threshold signature from collected signatures +func (e *ThresholdVoteExtension) ThresholdRecover() ([]byte, error) { + proTxHashes := make([][]byte, 0, len(e.thresholdSignatures)) + signatures := make([][]byte, 0, len(e.thresholdSignatures)) + + // collect signatures and proTxHashes + for proTxHash, signature := range e.thresholdSignatures { + if len(signature) != bls12381.SignatureSize { + return nil, fmt.Errorf("invalid vote extension signature len from validator %s: got %d, expected %d", + proTxHash, len(signature), bls12381.SignatureSize) } - for i := range exts { - modifier(protoMap[t][i], &exts[i]) + + proTxHashes = append(proTxHashes, bytes.Clone(proTxHash[:])) + signatures = append(signatures, bytes.Clone(signature[:])) + } + + if len(signatures) > 0 { + thresholdSignature, err := bls12381.RecoverThresholdSignatureFromShares(signatures, proTxHashes) + if err != nil { + return nil, fmt.Errorf("error recovering vote extension %s %X threshold signature: %w", + e.GetType().String(), e.GetExtension(), err) } + + return thresholdSignature, nil } - return nil + + return nil, fmt.Errorf("vote extension %s of type %X does not have any signatures for threshold-recovering", + e.GetType().String(), e.GetExtension()) +} + +// ThresholdRawVoteExtension is a threshold raw type of VoteExtension +type ThresholdRawVoteExtension struct { + ThresholdVoteExtension +} + +func (e ThresholdRawVoteExtension) Copy() VoteExtensionIf { + inner := e.ThresholdVoteExtension.Copy().(*ThresholdVoteExtension) + return &ThresholdRawVoteExtension{ThresholdVoteExtension: *inner} +} + +// SignItem creates a SignItem for a threshold raw vote extension +// +// Note: signItem.Msg left empty by purpose, as we don't want hash to be checked in Verify() +func (e ThresholdRawVoteExtension) SignItem(_ string, height int64, round int32, quorumType btcjson.LLMQType, quorumHash []byte) (SignItem, error) { + var signRequestID []byte + var err error + + ext := &e.VoteExtension + + if ext.XSignRequestId != nil && ext.XSignRequestId.Size() > 0 { + receivedReqID := ext.GetSignRequestId() + signRequestID = crypto.Checksum(crypto.Checksum(receivedReqID)) // reverse ext.GetSignRequestId()? + signRequestID = tmbytes.Reverse(signRequestID) + } else { + if signRequestID, err = voteExtensionRequestID(height, round); err != nil { + return SignItem{}, err + } + } + + // ensure Extension is 32 bytes long + if len(ext.Extension) != crypto.DefaultHashSize { + return SignItem{}, fmt.Errorf("invalid vote extension %s %X: extension must be %d bytes long", + ext.Type.String(), ext.Extension, crypto.DefaultHashSize) + } + + // We sign extension as it is, without any hashing, etc. + // However, as it is reversed in SignItem.UpdateSignHash, we need to reverse it also here to undo + // that reversal. + msgHash := tmbytes.Reverse(ext.Extension) + + signItem, err := NewSignItemFromHash(quorumType, quorumHash, signRequestID, msgHash), nil + if err != nil { + return SignItem{}, err + } + // signItem.Msg left empty by purpose, as we don't want hash to be checked in Verify() + + return signItem, nil +} + +// voteExtensionRequestID returns vote extension sign request ID used to generate +// threshold signatures +func voteExtensionRequestID(height int64, round int32) ([]byte, error) { + return heightRoundRequestID("dpevote", height, round), nil } diff --git a/types/vote_extension_test.go b/types/vote_extension_test.go new file mode 100644 index 0000000000..0bf6cdb779 --- /dev/null +++ b/types/vote_extension_test.go @@ -0,0 +1,201 @@ +package types + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" + "testing" + + "github.com/dashpay/dashd-go/btcjson" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dashpay/tenderdash/crypto/bls12381" + tmbytes "github.com/dashpay/tenderdash/libs/bytes" + "github.com/dashpay/tenderdash/libs/log" + tmproto "github.com/dashpay/tenderdash/proto/tendermint/types" +) + +func TestVoteExtensionCopySignsFromProto(t *testing.T) { + src := tmproto.VoteExtensions{ + &tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("threshold"), + Signature: []byte("signature"), + }, + } + + dst := VoteExtensions{&ThresholdVoteExtension{}} + err := dst.CopySignsFromProto(src) + require.NoError(t, err) + assert.EqualValues(t, src[0].GetSignature(), dst[0].GetSignature()) +} + +func TestMakeVoteExtensionsSignItems(t *testing.T) { + const chainID = "dash-platform" + const quorumType = btcjson.LLMQType_5_60 + + logger := log.NewTestingLogger(t) + testCases := []struct { + vote Vote + quorumHash []byte + want []SignItem + wantHash [][]byte + }{ + { + vote: Vote{ + Type: tmproto.PrecommitType, + Height: 1001, + ValidatorProTxHash: tmbytes.MustHexDecode("9CC13F685BC3EA0FCA99B87F42ABCC934C6305AA47F62A32266A2B9D55306B7B"), + VoteExtensions: VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_DEFAULT, + Extension: []byte("default")}, + &tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("threshold")}, + ), + }, + quorumHash: tmbytes.MustHexDecode("6A12D9CF7091D69072E254B297AEF15997093E480FDE295E09A7DE73B31CEEDD"), + want: []SignItem{ + newSignItem( + "FB95F2CA6530F02AC623589D7938643FF22AE79A75DD79AEA1C8871162DE675E", + "533524404D3A905F5AC9A30FCEB5A922EAD96F30DA02F979EE41C4342F540467", + "210A0764656661756C7411E903000000000000220D646173682D706C6174666F726D", + "6A12D9CF7091D69072E254B297AEF15997093E480FDE295E09A7DE73B31CEEDD", + quorumType, + ), + newSignItem( + "fb95f2ca6530f02ac623589d7938643ff22ae79a75dd79aea1c8871162de675e", + "D3B7D53A0F9CA8072D47D6C18E782EE3155EF8DCDDB010087030B6CBC63978BC", + "250a097468726573686f6c6411e903000000000000220d646173682d706c6174666f726d2801", + "6A12D9CF7091D69072E254B297AEF15997093E480FDE295E09A7DE73B31CEEDD", + quorumType, + ), + }, + wantHash: [][]byte{ + tmbytes.MustHexDecode("61519D79DE4C4D5AC5DD210C1BCE81AA24F76DD5581A24970E60112890C68FB7"), + tmbytes.MustHexDecode("46C72C423B74034E1AF574A99091B017C0698FEAA55C8B188BFD512FCADD3143"), + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("test-case #%d", i), func(t *testing.T) { + signItems, err := tc.vote.VoteExtensions.SignItems(chainID, quorumType, tc.quorumHash, tc.vote.Height, tc.vote.Round) + + require.NoError(t, err) + + for i, sign := range signItems { + assert.Equal(t, tc.wantHash[i], sign.MsgHash, "want %X, actual %X", tc.wantHash[i], sign.MsgHash) + if !assert.Equal(t, tc.want[i], sign, "Got ID(%d): %X", i, sign.SignHash) { + logger.Error("invalid sign", "sign", sign, "i", i) + } + } + }) + } +} + +// Test vectors of THRESHOLD_RECOVER_RAW vote extensions hashing, as used by Dash Platform withdrawals mechanism. +// +// Given some vote extension, llmq type, quorum hash and sign request id, sign data should match predefined test vector. +func TestVoteExtensionsRaw_SignDataRawVector_Withdrawals(t *testing.T) { + const chainID = "some-chain" // unused but required for VoteExtension.SignItem + const llmqType = btcjson.LLMQType_TEST_PLATFORM + + testCases := []struct { + llmqType btcjson.LLMQType + quorumHash []byte + requestID []byte + extension []byte + + // optional + quorumSig []byte + quorumPubKey []byte + + expectedMsgHash []byte + expectedSignHash []byte + expectedRequestID []byte + }{ + { // llmqType:106 + // quorumHash:53c006055af6d0ae9aa9627df8615a71c312421a28c4712c8add83c8e1bfdadd + // requestID:922a8fc39b6e265ca761eaaf863387a5e2019f4795a42260805f5562699fd9fa + // messageHash:2a3b788b83a8a3877d618874c0987ce62b43762ea18362cd336f4a79402d25c0 + // ==== signHash:9753911839e0a8304626b95ada276b55a3785bca657294a153bd5d66301756b7 + llmqType: btcjson.LLMQType_TEST_PLATFORM, + + quorumHash: tmbytes.MustHexDecode("53c006055af6d0ae9aa9627df8615a71c312421a28c4712c8add83c8e1bfdadd"), + requestID: binary.LittleEndian.AppendUint64([]byte("\x06plwdtx"), 0), + extension: []byte{192, 37, 45, 64, 121, 74, 111, 51, 205, 98, 131, 161, 46, 118, 67, 43, 230, 124, 152, 192, 116, 136, 97, 125, 135, 163, 168, 131, 139, 120, 59, 42}, + expectedSignHash: tmbytes.Reverse(tmbytes.MustHexDecode("9753911839e0a8304626b95ada276b55a3785bca657294a153bd5d66301756b7")), + expectedRequestID: tmbytes.MustHexDecode("922a8fc39b6e265ca761eaaf863387a5e2019f4795a42260805f5562699fd9fa"), + }, + { // test that requestID is correct for index 102 + quorumHash: tmbytes.MustHexDecode("53c006055af6d0ae9aa9627df8615a71c312421a28c4712c8add83c8e1bfdadd"), + requestID: binary.LittleEndian.AppendUint64([]byte("\x06plwdtx"), 102), + extension: []byte{192, 37, 45, 64, 121, 74, 111, 51, 205, 98, 131, 161, 46, 118, 67, 43, 230, 124, 152, 192, 116, 136, 97, 125, 135, 163, 168, 131, 139, 120, 59, 42}, + expectedRequestID: tmbytes.MustHexDecode("7a1b17a4542f4748c6b91bd46c7daa4f26f77f67cd2d9d405c8d956c77a44764"), + }, + { + // tx = 03000900000190cff4050000000023210375aae0756e8115ea064b46705c7b0a8ffad3d79688d910ef0337239fc + // 1b3760dac000000009101650000000000000070110100db04000031707c372e1a75dab8455659a2c7757842aa80 + // 998eae146847f1372447a5d02585fe5c9e8f7985ac27d41b1ca654e783bd7eaab484882ceae3cb3511acefac0e8 + // 875b691813ec26101c3384a6e506be9133ba977ae12be89ffa1a1105968fc01c7de4e02ac8c689989a5677ceb2e + // 284a57d57f7885c40658096d7c6294f9fa7a + + llmqType: btcjson.LLMQType_TEST, + quorumHash: tmbytes.MustHexDecode("25d0a5472437f1476814ae8e9980aa427875c7a2595645b8da751a2e377c7031"), + requestID: binary.LittleEndian.AppendUint64([]byte("\x06plwdtx"), 101), + extension: tmbytes.MustHexDecode("68CA7F464880F4040AF87DBE79F725C74398C3A2001700D7A0FEDB9417FD622F"), + + // TODO: Uncomment when we have public key + // quorumSig: tmbytes.MustHexDecode("85fe5c9e8f7985ac27d41b1ca654e783bd7eaab484882ceae3cb3511acefac0e8875b691813ec26101c3384a6e506be9133ba977ae12be89ffa1a1105968fc01c7de4e02ac8c689989a5677ceb2e284a57d57f7885c40658096d7c6294f9fa7a"), + // quorumPubKey: tmbytes.MustHexDecode("210375aae0756e8115ea064b46705c7b0a8ffad3d79688d910ef0337239fc1b3760dac"), + + // expectedSignHash: tmbytes.Reverse(tmbytes.MustHexDecode("9753911839e0a8304626b95ada276b55a3785bca657294a153bd5d66301756b7")), + expectedMsgHash: tmbytes.Reverse(tmbytes.MustHexDecode("68ca7f464880f4040af87dbe79f725c74398c3a2001700d7a0fedb9417fd622f")), + expectedRequestID: tmbytes.MustHexDecode("fcc76a643c5c668244fdcef09833955d6f4b803fa6c459f7732983c2332389fd"), + }, + } + + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + + ve := tmproto.VoteExtension{ + Extension: tc.extension, + Signature: []byte{}, + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, + XSignRequestId: &tmproto.VoteExtension_SignRequestId{ + SignRequestId: bytes.Clone(tc.requestID), + }, + } + voteExtension := VoteExtensionFromProto(ve) + signItem, err := voteExtension.SignItem(chainID, 1, 0, llmqType, tc.quorumHash) + require.NoError(t, err) + + // t.Logf("LLMQ type: %s (%d)\n", llmqType.Name(), llmqType) + // t.Logf("extension: %X\n", extension) + // t.Logf("sign requestID: %X\n", requestID) + // t.Logf("quorum hash: %X\n", quorumHash) + + t.Logf("RESULT: sign hash: %X", signItem.SignHash) + if len(tc.expectedSignHash) > 0 { + assert.EqualValues(t, tc.expectedSignHash, signItem.SignHash, "sign hash mismatch") + } + if len(tc.expectedRequestID) > 0 { + t.Logf("requestID: %s", hex.EncodeToString(tc.requestID)) + assert.EqualValues(t, tc.expectedRequestID, signItem.ID, "sign request id mismatch") + } + if len(tc.expectedMsgHash) > 0 { + assert.EqualValues(t, tc.expectedMsgHash, signItem.MsgHash, "msg hash mismatch") + } + + if len(tc.quorumSig) > 0 { + require.Len(t, tc.quorumPubKey, bls12381.PubKeySize) + pubKey := bls12381.PubKey(tc.quorumPubKey) + require.NoError(t, pubKey.Validate(), "invalid public key") + assert.True(t, pubKey.VerifySignatureDigest(signItem.SignHash, tc.quorumSig), "signature verification failed") + } + }) + } + +} diff --git a/types/vote_set.go b/types/vote_set.go index 103e86dcab..b80ce203aa 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -70,8 +70,8 @@ type VoteSet struct { peerMaj23s map[string]maj23Info // Maj23 for each peer // dash fields - thresholdBlockSig []byte // If a 2/3 majority is seen, recover the block sig - thresholdVoteExtSigs []ThresholdExtensionSign // If a 2/3 majority is seen, recover the vote extension sigs + thresholdBlockSig []byte // If a 2/3 majority is seen, recover the block sig + thresholdVoteExtSigs VoteExtensions // If a 2/3 majority is seen, recover the vote extension sigs } type maj23Info struct { @@ -349,10 +349,10 @@ func (voteSet *VoteSet) recoverThresholdSignsAndVerify(blockVotes *blockVotes, q // there is only 1 validator vote := blockVotes.votes[0] voteSet.thresholdBlockSig = vote.BlockSignature - voteSet.thresholdVoteExtSigs = MakeThresholdVoteExtensions( - vote.VoteExtensions[tmproto.VoteExtensionType_THRESHOLD_RECOVER], - vote.GetVoteExtensionsSigns(tmproto.VoteExtensionType_THRESHOLD_RECOVER), - ) + voteSet.thresholdVoteExtSigs = vote.VoteExtensions. + Filter(func(ext VoteExtensionIf) bool { + return ext.IsThresholdRecoverable() + }).Copy() return nil } err := voteSet.recoverThresholdSigns(blockVotes) @@ -376,8 +376,9 @@ func (voteSet *VoteSet) recoverThresholdSigns(blockVotes *blockVotes) error { if err != nil { return err } + voteSet.thresholdBlockSig = thresholdSigns.BlockSign - voteSet.thresholdVoteExtSigs = thresholdSigns.ExtensionSigns + voteSet.thresholdVoteExtSigs = signsRecoverer.GetVoteExtensions(*thresholdSigns) return nil } @@ -735,6 +736,7 @@ func (voteSet *VoteSet) MakeCommit() *Commit { voteSet.GetHeight(), voteSet.GetRound(), *voteSet.maj23, + voteSet.thresholdVoteExtSigs, voteSet.makeCommitSigns(), ) } @@ -742,8 +744,8 @@ func (voteSet *VoteSet) MakeCommit() *Commit { func (voteSet *VoteSet) makeCommitSigns() *CommitSigns { return &CommitSigns{ QuorumSigns: QuorumSigns{ - BlockSign: voteSet.thresholdBlockSig, - ExtensionSigns: voteSet.thresholdVoteExtSigs, + BlockSign: voteSet.thresholdBlockSig, + VoteExtensionSignatures: voteSet.thresholdVoteExtSigs.GetSignatures(), }, QuorumHash: voteSet.valSet.QuorumHash, } @@ -751,8 +753,8 @@ func (voteSet *VoteSet) makeCommitSigns() *CommitSigns { func (voteSet *VoteSet) makeQuorumSigns() QuorumSigns { return QuorumSigns{ - BlockSign: voteSet.thresholdBlockSig, - ExtensionSigns: voteSet.thresholdVoteExtSigs, + BlockSign: voteSet.thresholdBlockSig, + VoteExtensionSignatures: voteSet.thresholdVoteExtSigs.GetSignatures(), } } diff --git a/types/vote_test.go b/types/vote_test.go index 0c05e9d502..1dd1847440 100644 --- a/types/vote_test.go +++ b/types/vote_test.go @@ -21,7 +21,7 @@ import ( const ( //nolint: lll - preCommitTestStr = `Vote{56789:959A8F5EF2BE 12345/02/Precommit(8B01023386C3) 000000000000 000000000000}` + preCommitTestStr = `Vote{56789:959A8F5EF2BE 12345/02/Precommit(8B01023386C3) 000000000000 03962B14DA9F}` //nolint: lll preVoteTestStr = `Vote{56789:959A8F5EF2BE 12345/02/Prevote(8B01023386C3) 000000000000 000000000000}` ) @@ -39,9 +39,11 @@ func examplePrevote(t *testing.T) *Vote { func examplePrecommit(t testing.TB) *Vote { t.Helper() vote := exampleVote(t, byte(tmproto.PrecommitType)) - vote.VoteExtensions = VoteExtensions{ - tmproto.VoteExtensionType_DEFAULT: []VoteExtension{{Signature: []byte("signature")}}, - } + vote.VoteExtensions = VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("extension"), + Signature: make([]byte, SignatureSize), + }) return vote } @@ -179,11 +181,9 @@ func TestVoteSignBytesTestVectors(t *testing.T) { // containing vote extension 5: { "test_chain_id", &Vote{ - Height: 1, - Round: 1, - VoteExtensions: VoteExtensions{ - tmproto.VoteExtensionType_DEFAULT: []VoteExtension{{Extension: []byte("extension")}}, - }, + Height: 1, + Round: 1, + VoteExtensions: VoteExtensionsFromProto(&tmproto.VoteExtension{Extension: []byte("extension")}), }, []byte{ 0x0, 0x0, 0x0, 0x0, //type @@ -331,18 +331,40 @@ func TestVoteExtension(t *testing.T) { expectError bool }{ { - name: "all fields present", - extensions: VoteExtensions{ - tmproto.VoteExtensionType_THRESHOLD_RECOVER: []VoteExtension{{Extension: []byte("extension")}}, - }, + name: "valid THRESHOLD_RECOVER", + extensions: VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("extension")}), + includeSignature: true, + expectError: false, + }, + { + name: "valid THRESHOLD_RECOVER_RAW plwdtx", + extensions: VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, + XSignRequestId: &tmproto.VoteExtension_SignRequestId{ + SignRequestId: []byte("\x06plwdtx"), + }, + Extension: bytes.Repeat([]byte("extensio"), 4)}), // must be 32 bytes + includeSignature: true, + expectError: false, + }, + { + name: "valid THRESHOLD_RECOVER_RAW dpevote", + extensions: VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, + XSignRequestId: &tmproto.VoteExtension_SignRequestId{ + SignRequestId: []byte("dpevote"), + }, + Extension: bytes.Repeat([]byte("extensio"), 4)}), // must be 32 bytes includeSignature: true, expectError: false, }, { name: "no extension signature", - extensions: VoteExtensions{ - tmproto.VoteExtensionType_THRESHOLD_RECOVER: []VoteExtension{{Extension: []byte("extension")}}, - }, + extensions: VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("extension")}), includeSignature: false, expectError: true, }, @@ -380,13 +402,11 @@ func TestVoteExtension(t *testing.T) { require.NoError(t, err) vote.BlockSignature = v.BlockSignature if tc.includeSignature { - protoExtensionsMap := v.VoteExtensionsToMap() - for et, extensions := range protoExtensionsMap { - for i, ext := range extensions { - vote.VoteExtensions[et][i].Signature = ext.Signature - } + for i, ext := range v.VoteExtensions { + vote.VoteExtensions[i].SetSignature(ext.Signature) } } + err = vote.VerifyWithExtension("test_chain_id", btcjson.LLMQType_5_60, quorumHash, pk, proTxHash) if tc.expectError { require.Error(t, err) @@ -470,6 +490,7 @@ func TestVoteString(t *testing.T) { vote: func() *Vote { v := examplePrecommit(t) v.BlockID.Hash = nil + v.VoteExtensions = nil return v }(), expectedResult: nilVoteTestStr, @@ -515,7 +536,11 @@ func TestValidVotes(t *testing.T) { { "good precommit with vote extension", examplePrecommit(t), func(v *Vote) { - v.VoteExtensions[tmproto.VoteExtensionType_DEFAULT][0].Extension = []byte("extension") + v.VoteExtensions[0] = VoteExtensionFromProto(tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Extension: []byte("extension"), + Signature: make([]byte, SignatureSize), + }) }, }, } @@ -579,13 +604,13 @@ func TestInvalidPrevotes(t *testing.T) { { "vote extension present", func(v *Vote) { - v.VoteExtensions = VoteExtensions{tmproto.VoteExtensionType_DEFAULT: []VoteExtension{{Extension: []byte("extension")}}} + v.VoteExtensions = VoteExtensionsFromProto(&tmproto.VoteExtension{Extension: []byte("extension")}) }, }, { "vote extension signature present", func(v *Vote) { - v.VoteExtensions = VoteExtensions{tmproto.VoteExtensionType_DEFAULT: []VoteExtension{{Signature: []byte("signature")}}} + v.VoteExtensions = VoteExtensionsFromProto(&tmproto.VoteExtension{Signature: []byte("signature")}) }, }, } @@ -610,9 +635,7 @@ func TestInvalidPrecommitExtensions(t *testing.T) { }{ { "vote extension present without signature", func(v *Vote) { - v.VoteExtensions = VoteExtensions{ - tmproto.VoteExtensionType_THRESHOLD_RECOVER: {{Extension: []byte("extension")}}, - } + v.VoteExtensions = VoteExtensionsFromProto(&tmproto.VoteExtension{Extension: []byte("extension")}) }, }, // TODO(thane): Re-enable once https://github.com/tendermint/tendermint/issues/8272 is resolved @@ -620,9 +643,9 @@ func TestInvalidPrecommitExtensions(t *testing.T) { { "oversized vote extension signature", func(v *Vote) { - v.VoteExtensions = VoteExtensions{ - tmproto.VoteExtensionType_THRESHOLD_RECOVER: []VoteExtension{{Signature: make([]byte, SignatureSize+1)}}, - } + v.VoteExtensions = VoteExtensionsFromProto(&tmproto.VoteExtension{ + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, + Signature: make([]byte, SignatureSize+1)}) }, }, } @@ -648,11 +671,44 @@ func TestVoteExtensionsSignBytes(t *testing.T) { Signature: []byte{}, Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER, } - actual := VoteExtensionSignBytes("some-chain", 1, 2, &ve) + signItem, err := VoteExtensionFromProto(ve).SignItem("some-chain", 1, 2, btcjson.LLMQType_TEST_PLATFORM, crypto.RandQuorumHash()) + assert.NoError(t, err) + + actual := signItem.Msg + t.Logf("sign bytes: %x", actual) assert.EqualValues(t, expect, actual) } +// TestVoteExtensionsSignBytesRaw checks vote extension sign bytes for a raw vote extension type. +// +// Given some vote extension, SignBytes or THRESHOLD_RECOVER_RAW returns that extension. +func TestVoteExtensionsSignBytesRaw(t *testing.T) { + extension := bytes.Repeat([]byte{1, 2, 3, 4, 5, 6, 7, 8}, 4) + quorumHash := bytes.Repeat([]byte{8, 7, 6, 5, 4, 3, 2, 1}, 4) + expectedSignHash := []byte{0xe, 0x88, 0x8d, 0xa8, 0x97, 0xf1, 0xc0, 0xfd, 0x6a, 0xe8, 0x3b, 0x77, 0x9b, 0x5, 0xdd, + 0x28, 0xc, 0xe2, 0x58, 0xf6, 0x4c, 0x86, 0x1, 0x34, 0xfa, 0x4, 0x27, 0xe1, 0xaa, 0xab, 0x1a, 0xde} + + assert.Len(t, extension, 32) + + ve := tmproto.VoteExtension{ + Extension: extension, + Signature: []byte{}, + Type: tmproto.VoteExtensionType_THRESHOLD_RECOVER_RAW, + XSignRequestId: &tmproto.VoteExtension_SignRequestId{ + SignRequestId: []byte("dpevote-someSignRequestID"), + }, + } + + signItem, err := VoteExtensionFromProto(ve).SignItem("some-chain", 1, 2, btcjson.LLMQType_TEST_PLATFORM, quorumHash) + assert.NoError(t, err) + + actual := signItem.SignHash + + t.Logf("sign hash: %x", actual) + assert.EqualValues(t, expectedSignHash, actual) +} + func TestVoteProtobuf(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -675,22 +731,24 @@ func TestVoteProtobuf(t *testing.T) { {"fail vote validate basic", &Vote{}, true, false}, } for _, tc := range testCases { - protoProposal := tc.vote.ToProto() + t.Run(tc.msg, func(t *testing.T) { + protoProposal := tc.vote.ToProto() - v, err := VoteFromProto(protoProposal) - if tc.convertsOk { - require.NoError(t, err) - } else { - require.Error(t, err) - } + v, err := VoteFromProto(protoProposal) + if tc.convertsOk { + require.NoError(t, err) + } else { + require.Error(t, err) + } - err = v.ValidateBasic() - if tc.passesValidateBasic { - require.NoError(t, err) - require.Equal(t, tc.vote, v, tc.msg) - } else { - require.Error(t, err) - } + err = v.ValidateBasic() + if tc.passesValidateBasic { + require.NoError(t, err) + require.Equal(t, tc.vote, v, tc.msg) + } else { + require.Error(t, err) + } + }) } }