diff --git a/.gitignore b/.gitignore index 8e5b4964..d62880d2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ measurements.json *.pdf twins.json +fuzz/previous_messages.b64 +fuzz/previous_messages.seed diff --git a/.vscode/dict.txt b/.vscode/dict.txt index e8ec8629..f39df6f5 100644 --- a/.vscode/dict.txt +++ b/.vscode/dict.txt @@ -16,6 +16,7 @@ Debugf durationpb emptypb Erevik +eventloop Fangyu fasthotstuff felixge @@ -23,7 +24,9 @@ Feng ferr fgprof fgprofprofile +Fuzzer gocyclo +gofuzz golangci golint gomock @@ -41,6 +44,7 @@ Infof Jalalzai Jehl Jianyu +keygen kilic latencygen Malkhi @@ -48,6 +52,8 @@ Mathieu Meling mitchellh nolint +Oneof +Oneofs orchestrationpb partitioner Paulo @@ -59,6 +65,7 @@ propsed proto protobuf protoc +protoreflect protostream ptypes QC's @@ -71,6 +78,7 @@ simplehotstuff Sonnino SSWU structs +subconfiguration throughputvslatency timestamppb TMPDIR diff --git a/.vscode/settings.json b/.vscode/settings.json index 49a998a6..1b97c1a2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,3 +18,4 @@ ], "cSpell.enabled": true } + diff --git a/Makefile b/Makefile index 938bb314..8085b96f 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,9 @@ proto_src := internal/proto/clientpb/client.proto \ internal/proto/hotstuffpb/hotstuff.proto \ internal/proto/orchestrationpb/orchestration.proto \ internal/proto/handelpb/handel.proto \ - metrics/types/types.proto + metrics/types/types.proto \ + fuzz/fuzz.proto + proto_go := $(proto_src:%.proto=%.pb.go) gorums_go := internal/proto/clientpb/client_gorums.pb.go \ internal/proto/hotstuffpb/hotstuff_gorums.pb.go \ diff --git a/fuzz/doc.go b/fuzz/doc.go new file mode 100644 index 00000000..19a7eae5 --- /dev/null +++ b/fuzz/doc.go @@ -0,0 +1,2 @@ +// Package fuzz contains fuzzing functions for the fuzzing protobuf/hotstuff messages. +package fuzz diff --git a/fuzz/error.go b/fuzz/error.go new file mode 100644 index 00000000..8798eee0 --- /dev/null +++ b/fuzz/error.go @@ -0,0 +1,192 @@ +package fuzz + +import ( + "fmt" + "reflect" + "sort" + "strconv" + "strings" + "testing" + + "github.com/meling/proto2" +) + +type typeCount map[reflect.Type]int + +func (tc typeCount) Add(typ reflect.Type) { + tc[typ]++ +} + +func (tc typeCount) String(typeTotalCount typeCount) string { + keys := make([]reflect.Type, 0) + for key := range tc { + keys = append(keys, key) + } + + sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() }) + + str := "" + for _, key := range keys { + str += key.String() + ": " + strconv.Itoa(tc[key]) + " / " + strconv.Itoa(typeTotalCount[key]) + "\n" + } + return str +} + +type panicInfo struct { + Err any + StackTrace string + FuzzMsg string + FuzzMsgB64 string + Seed *int64 + LineNum int + TypeCount typeCount +} + +type errorInfo struct { + currentFuzzMsg *FuzzMsg + currentFuzzMsgSeed *int64 + errorCount int + panics map[string]panicInfo + totalScenarios int + failedScenarios int + totalMessages int + failedMessages int + TypePanicCount typeCount + TypeTotalCount typeCount +} + +func (ei *errorInfo) init() { + ei.panics = make(map[string]panicInfo) + ei.TypeTotalCount = make(typeCount) + ei.TypePanicCount = make(typeCount) +} + +func (ei *errorInfo) outputErrors(t *testing.T) { + ei.outputInfo(t) + for _, panicInfo := range ei.panics { + t.Error(panicInfo.Err) + } +} + +func (ei *errorInfo) outputInfo(t *testing.T) { + t.Helper() + b64s := "" + seeds := "" + + fmt.Println() + fmt.Println() + fmt.Println() + + fmt.Println("ERROR INFO") + + keys := make([]string, 0) + for key := range ei.panics { + keys = append(keys, key) + } + + // sorting the keys of the + sort.Strings(keys) + + for i, key := range keys { + panicInfo := ei.panics[key] + b64s += panicInfo.FuzzMsgB64 + "\n" + + if panicInfo.Seed != nil { + seeds += strconv.FormatInt(*panicInfo.Seed, 10) + "\n" + } + + fmt.Println() + fmt.Printf("ERROR NUMBER %d\n", i+1) + // contains error location, err text and recover point + fmt.Println(key) + fmt.Println() + fmt.Println("crash amounts grouped by type:") + fmt.Println(panicInfo.TypeCount.String(ei.TypeTotalCount)) + fmt.Println("- STACK TRACE BEGIN") + fmt.Print(panicInfo.StackTrace) + fmt.Println("- STACK TRACE END") + fmt.Println() + fmt.Println("- FUZZ MESSAGE BEGIN") + fmt.Println(panicInfo.FuzzMsg) + fmt.Println("- FUZZ MESSAGE END") + fmt.Println() + } + + err := saveStringToFile("previous_messages.b64", b64s) + if err != nil { + t.Error(err) + } + + if seeds != "" { + err := saveStringToFile("previous_messages.seed", seeds) + if err != nil { + t.Error(err) + } + } + + fmt.Println() + fmt.Println("SUMMARY") + fmt.Printf("unique errors found: %d\n", len(ei.panics)) + fmt.Printf("%d runs were errors\n", ei.errorCount) + fmt.Printf("%d of %d scenarios failed\n", ei.failedScenarios, ei.totalScenarios) + fmt.Printf("%d of %d messages failed\n", ei.failedMessages, ei.totalMessages) + fmt.Println() + fmt.Println("crash amounts grouped by type:") + fmt.Println(ei.TypePanicCount.String(ei.TypeTotalCount)) +} + +func (ei *errorInfo) addTotal(fuzzMessage *FuzzMsg, seed *int64) { + ei.totalMessages++ + ei.currentFuzzMsg = fuzzMessage + ei.currentFuzzMsgSeed = seed + protoMsg := extractProtoMsg(ei.currentFuzzMsg) + typ := reflect.TypeOf(protoMsg) + ei.TypeTotalCount.Add(typ) +} + +func (ei *errorInfo) addPanic(fullStack string, err2 any, info string) { + simpleStack := simplifyStack(fullStack) + identifier := "error location:\t" + simpleStack + "\nerror info:\t" + fmt.Sprint(err2) + "\nrecovered from:\t" + info + + ei.errorCount++ + + oldPanic, okPanic := ei.panics[identifier] + + b64, err := fuzzMsgToB64(ei.currentFuzzMsg) + if err != nil { + panic(err) + } + + protoMsg := extractProtoMsg(ei.currentFuzzMsg) + fuzzMsgString := proto2.GoStruct(protoMsg) + newLines := strings.Count(fuzzMsgString, "\n") + + newPanic := panicInfo{ + Err: err2, + StackTrace: fullStack, + FuzzMsg: fuzzMsgString, + FuzzMsgB64: b64, + Seed: ei.currentFuzzMsgSeed, + LineNum: newLines, + } + + if okPanic { + newPanic.TypeCount = oldPanic.TypeCount + } else { + newPanic.TypeCount = make(typeCount) + } + + oldLines := oldPanic.LineNum + if !okPanic || newLines < oldLines { + ei.panics[identifier] = newPanic + } + typ := reflect.TypeOf(protoMsg) + ei.panics[identifier].TypeCount.Add(typ) + ei.TypePanicCount.Add(typ) +} + +func simplifyStack(stack string) string { + stackLines := strings.Split(strings.ReplaceAll(stack, "\r\n", "\n"), "\n") + // line 9 tells us where the panic happened, found through testing + return stackLines[8][1:] +} diff --git a/fuzz/extra_test.go b/fuzz/extra_test.go new file mode 100644 index 00000000..760af277 --- /dev/null +++ b/fuzz/extra_test.go @@ -0,0 +1,59 @@ +package fuzz + +// this file includes a couple of extra unnecessary tests +// used for probability tests, profiling and other things + +import ( + "fmt" + "math/rand" + "os" + "testing" +) + +func TestFrequencyErrorFuzz(t *testing.T) { + if os.Getenv("FUZZ") == "" { + t.Skip("Skipping slow test; run with FUZZ=1 to enable") + } + frequency := make(map[string]int, 0) + + f := initFuzz() + for j := 0; j < 1000; j++ { + errInfo := new(errorInfo) + errInfo.init() + + iterations := 1 + for i := 0; i < iterations; i++ { + fuzzMessage := createFuzzMessage(f, nil) + useFuzzMessage(errInfo, fuzzMessage, nil) + } + + for key := range errInfo.panics { + frequency[key]++ + } + } + + sum := 0 + for key, val := range frequency { + sum += val + fmt.Println(key) + fmt.Println(val) + fmt.Println() + } + fmt.Println(sum) +} + +func BenchmarkFuzz(b *testing.B) { + errInfo := new(errorInfo) + errInfo.init() + + f := initFuzz() + + for i := 0; i < b.N; i++ { + seed := rand.Int63() + fuzzMessage := createFuzzMessage(f, &seed) + useFuzzMessage(errInfo, fuzzMessage, &seed) + } + + fmt.Println(b.Elapsed()) + fmt.Println() +} diff --git a/fuzz/fuzz.go b/fuzz/fuzz.go new file mode 100644 index 00000000..0d3dcd7b --- /dev/null +++ b/fuzz/fuzz.go @@ -0,0 +1,73 @@ +package fuzz + +import ( + "math/rand" + + fuzz "github.com/google/gofuzz" + hotstuffpb "github.com/relab/hotstuff/internal/proto/hotstuffpb" +) + +func initFuzz() *fuzz.Fuzzer { + nilChance := 0.1 + + f := fuzz.New().NilChance(nilChance).Funcs( + func(m *FuzzMsg, c fuzz.Continue) { + switch c.Intn(4) { + case 0: + msg := FuzzMsg_ProposeMsg{ + ProposeMsg: &ProposeMsg{}, + } + c.Fuzz(msg.ProposeMsg) + m.Message = &msg + case 1: + msg := FuzzMsg_VoteMsg{ + VoteMsg: &VoteMsg{}, + } + c.Fuzz(msg.VoteMsg) + m.Message = &msg + case 2: + msg := FuzzMsg_TimeoutMsg{ + TimeoutMsg: &TimeoutMsg{}, + } + c.Fuzz(msg.TimeoutMsg) + m.Message = &msg + case 3: + msg := FuzzMsg_NewViewMsg{ + NewViewMsg: &NewViewMsg{}, + } + c.Fuzz(msg.NewViewMsg) + m.Message = &msg + } + }, + func(sig **hotstuffpb.QuorumSignature, c fuzz.Continue) { + if c.Float64() < nilChance { + *sig = nil + return + } + + *sig = new(hotstuffpb.QuorumSignature) + switch c.Intn(2) { + case 0: + ecdsa := new(hotstuffpb.QuorumSignature_ECDSASigs) + c.Fuzz(&ecdsa) + (*sig).Sig = ecdsa + case 1: + bls12 := new(hotstuffpb.QuorumSignature_BLS12Sig) + c.Fuzz(&bls12) + (*sig).Sig = bls12 + } + }, + ) + + return f +} + +func createFuzzMessage(f *fuzz.Fuzzer, seed *int64) *FuzzMsg { + if seed != nil { + f.RandSource(rand.NewSource(*seed)) + } + + newMessage := new(FuzzMsg) + f.Fuzz(newMessage) + return newMessage +} diff --git a/fuzz/fuzz.pb.go b/fuzz/fuzz.pb.go new file mode 100644 index 00000000..959ae8cc --- /dev/null +++ b/fuzz/fuzz.pb.go @@ -0,0 +1,541 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.0 +// protoc v3.21.9 +// source: fuzz/fuzz.proto + +package fuzz + +import ( + hotstuffpb "github.com/relab/hotstuff/internal/proto/hotstuffpb" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type FuzzMsg struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Message: + // + // *FuzzMsg_ProposeMsg + // *FuzzMsg_TimeoutMsg + // *FuzzMsg_VoteMsg + // *FuzzMsg_NewViewMsg + Message isFuzzMsg_Message `protobuf_oneof:"Message"` +} + +func (x *FuzzMsg) Reset() { + *x = FuzzMsg{} + if protoimpl.UnsafeEnabled { + mi := &file_fuzz_fuzz_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FuzzMsg) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FuzzMsg) ProtoMessage() {} + +func (x *FuzzMsg) ProtoReflect() protoreflect.Message { + mi := &file_fuzz_fuzz_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FuzzMsg.ProtoReflect.Descriptor instead. +func (*FuzzMsg) Descriptor() ([]byte, []int) { + return file_fuzz_fuzz_proto_rawDescGZIP(), []int{0} +} + +func (m *FuzzMsg) GetMessage() isFuzzMsg_Message { + if m != nil { + return m.Message + } + return nil +} + +func (x *FuzzMsg) GetProposeMsg() *ProposeMsg { + if x, ok := x.GetMessage().(*FuzzMsg_ProposeMsg); ok { + return x.ProposeMsg + } + return nil +} + +func (x *FuzzMsg) GetTimeoutMsg() *TimeoutMsg { + if x, ok := x.GetMessage().(*FuzzMsg_TimeoutMsg); ok { + return x.TimeoutMsg + } + return nil +} + +func (x *FuzzMsg) GetVoteMsg() *VoteMsg { + if x, ok := x.GetMessage().(*FuzzMsg_VoteMsg); ok { + return x.VoteMsg + } + return nil +} + +func (x *FuzzMsg) GetNewViewMsg() *NewViewMsg { + if x, ok := x.GetMessage().(*FuzzMsg_NewViewMsg); ok { + return x.NewViewMsg + } + return nil +} + +type isFuzzMsg_Message interface { + isFuzzMsg_Message() +} + +type FuzzMsg_ProposeMsg struct { + ProposeMsg *ProposeMsg `protobuf:"bytes,1,opt,name=ProposeMsg,proto3,oneof"` +} + +type FuzzMsg_TimeoutMsg struct { + TimeoutMsg *TimeoutMsg `protobuf:"bytes,2,opt,name=TimeoutMsg,proto3,oneof"` +} + +type FuzzMsg_VoteMsg struct { + VoteMsg *VoteMsg `protobuf:"bytes,3,opt,name=VoteMsg,proto3,oneof"` +} + +type FuzzMsg_NewViewMsg struct { + NewViewMsg *NewViewMsg `protobuf:"bytes,4,opt,name=NewViewMsg,proto3,oneof"` +} + +func (*FuzzMsg_ProposeMsg) isFuzzMsg_Message() {} + +func (*FuzzMsg_TimeoutMsg) isFuzzMsg_Message() {} + +func (*FuzzMsg_VoteMsg) isFuzzMsg_Message() {} + +func (*FuzzMsg_NewViewMsg) isFuzzMsg_Message() {} + +type ProposeMsg struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ID uint32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` + Proposal *hotstuffpb.Proposal `protobuf:"bytes,2,opt,name=Proposal,proto3" json:"Proposal,omitempty"` +} + +func (x *ProposeMsg) Reset() { + *x = ProposeMsg{} + if protoimpl.UnsafeEnabled { + mi := &file_fuzz_fuzz_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProposeMsg) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProposeMsg) ProtoMessage() {} + +func (x *ProposeMsg) ProtoReflect() protoreflect.Message { + mi := &file_fuzz_fuzz_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProposeMsg.ProtoReflect.Descriptor instead. +func (*ProposeMsg) Descriptor() ([]byte, []int) { + return file_fuzz_fuzz_proto_rawDescGZIP(), []int{1} +} + +func (x *ProposeMsg) GetID() uint32 { + if x != nil { + return x.ID + } + return 0 +} + +func (x *ProposeMsg) GetProposal() *hotstuffpb.Proposal { + if x != nil { + return x.Proposal + } + return nil +} + +type TimeoutMsg struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ID uint32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` + TimeoutMsg *hotstuffpb.TimeoutMsg `protobuf:"bytes,2,opt,name=TimeoutMsg,proto3" json:"TimeoutMsg,omitempty"` +} + +func (x *TimeoutMsg) Reset() { + *x = TimeoutMsg{} + if protoimpl.UnsafeEnabled { + mi := &file_fuzz_fuzz_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TimeoutMsg) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TimeoutMsg) ProtoMessage() {} + +func (x *TimeoutMsg) ProtoReflect() protoreflect.Message { + mi := &file_fuzz_fuzz_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TimeoutMsg.ProtoReflect.Descriptor instead. +func (*TimeoutMsg) Descriptor() ([]byte, []int) { + return file_fuzz_fuzz_proto_rawDescGZIP(), []int{2} +} + +func (x *TimeoutMsg) GetID() uint32 { + if x != nil { + return x.ID + } + return 0 +} + +func (x *TimeoutMsg) GetTimeoutMsg() *hotstuffpb.TimeoutMsg { + if x != nil { + return x.TimeoutMsg + } + return nil +} + +type VoteMsg struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ID uint32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` + PartialCert *hotstuffpb.PartialCert `protobuf:"bytes,2,opt,name=PartialCert,proto3" json:"PartialCert,omitempty"` + Deferred bool `protobuf:"varint,3,opt,name=Deferred,proto3" json:"Deferred,omitempty"` +} + +func (x *VoteMsg) Reset() { + *x = VoteMsg{} + if protoimpl.UnsafeEnabled { + mi := &file_fuzz_fuzz_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VoteMsg) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VoteMsg) ProtoMessage() {} + +func (x *VoteMsg) ProtoReflect() protoreflect.Message { + mi := &file_fuzz_fuzz_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VoteMsg.ProtoReflect.Descriptor instead. +func (*VoteMsg) Descriptor() ([]byte, []int) { + return file_fuzz_fuzz_proto_rawDescGZIP(), []int{3} +} + +func (x *VoteMsg) GetID() uint32 { + if x != nil { + return x.ID + } + return 0 +} + +func (x *VoteMsg) GetPartialCert() *hotstuffpb.PartialCert { + if x != nil { + return x.PartialCert + } + return nil +} + +func (x *VoteMsg) GetDeferred() bool { + if x != nil { + return x.Deferred + } + return false +} + +type NewViewMsg struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ID uint32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` + SyncInfo *hotstuffpb.SyncInfo `protobuf:"bytes,2,opt,name=SyncInfo,proto3" json:"SyncInfo,omitempty"` +} + +func (x *NewViewMsg) Reset() { + *x = NewViewMsg{} + if protoimpl.UnsafeEnabled { + mi := &file_fuzz_fuzz_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NewViewMsg) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NewViewMsg) ProtoMessage() {} + +func (x *NewViewMsg) ProtoReflect() protoreflect.Message { + mi := &file_fuzz_fuzz_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NewViewMsg.ProtoReflect.Descriptor instead. +func (*NewViewMsg) Descriptor() ([]byte, []int) { + return file_fuzz_fuzz_proto_rawDescGZIP(), []int{4} +} + +func (x *NewViewMsg) GetID() uint32 { + if x != nil { + return x.ID + } + return 0 +} + +func (x *NewViewMsg) GetSyncInfo() *hotstuffpb.SyncInfo { + if x != nil { + return x.SyncInfo + } + return nil +} + +var File_fuzz_fuzz_proto protoreflect.FileDescriptor + +var file_fuzz_fuzz_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x66, 0x75, 0x7a, 0x7a, 0x2f, 0x66, 0x75, 0x7a, 0x7a, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x04, 0x66, 0x75, 0x7a, 0x7a, 0x1a, 0x28, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x6f, 0x74, 0x73, 0x74, 0x75, 0x66, 0x66, + 0x70, 0x62, 0x2f, 0x68, 0x6f, 0x74, 0x73, 0x74, 0x75, 0x66, 0x66, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0xdb, 0x01, 0x0a, 0x07, 0x46, 0x75, 0x7a, 0x7a, 0x4d, 0x73, 0x67, 0x12, 0x32, 0x0a, + 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x4d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x10, 0x2e, 0x66, 0x75, 0x7a, 0x7a, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, + 0x4d, 0x73, 0x67, 0x48, 0x00, 0x52, 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x4d, 0x73, + 0x67, 0x12, 0x32, 0x0a, 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x67, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x66, 0x75, 0x7a, 0x7a, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x67, 0x48, 0x00, 0x52, 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x6f, + 0x75, 0x74, 0x4d, 0x73, 0x67, 0x12, 0x29, 0x0a, 0x07, 0x56, 0x6f, 0x74, 0x65, 0x4d, 0x73, 0x67, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x66, 0x75, 0x7a, 0x7a, 0x2e, 0x56, 0x6f, + 0x74, 0x65, 0x4d, 0x73, 0x67, 0x48, 0x00, 0x52, 0x07, 0x56, 0x6f, 0x74, 0x65, 0x4d, 0x73, 0x67, + 0x12, 0x32, 0x0a, 0x0a, 0x4e, 0x65, 0x77, 0x56, 0x69, 0x65, 0x77, 0x4d, 0x73, 0x67, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x66, 0x75, 0x7a, 0x7a, 0x2e, 0x4e, 0x65, 0x77, 0x56, + 0x69, 0x65, 0x77, 0x4d, 0x73, 0x67, 0x48, 0x00, 0x52, 0x0a, 0x4e, 0x65, 0x77, 0x56, 0x69, 0x65, + 0x77, 0x4d, 0x73, 0x67, 0x42, 0x09, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x4e, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x4d, 0x73, 0x67, 0x12, 0x0e, 0x0a, + 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x49, 0x44, 0x12, 0x30, 0x0a, + 0x08, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x14, 0x2e, 0x68, 0x6f, 0x74, 0x73, 0x74, 0x75, 0x66, 0x66, 0x70, 0x62, 0x2e, 0x50, 0x72, 0x6f, + 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x22, + 0x54, 0x0a, 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x67, 0x12, 0x0e, 0x0a, + 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x49, 0x44, 0x12, 0x36, 0x0a, + 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x68, 0x6f, 0x74, 0x73, 0x74, 0x75, 0x66, 0x66, 0x70, 0x62, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x67, 0x52, 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x6f, + 0x75, 0x74, 0x4d, 0x73, 0x67, 0x22, 0x70, 0x0a, 0x07, 0x56, 0x6f, 0x74, 0x65, 0x4d, 0x73, 0x67, + 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x49, 0x44, + 0x12, 0x39, 0x0a, 0x0b, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x43, 0x65, 0x72, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x6f, 0x74, 0x73, 0x74, 0x75, 0x66, 0x66, + 0x70, 0x62, 0x2e, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x43, 0x65, 0x72, 0x74, 0x52, 0x0b, + 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x43, 0x65, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x44, + 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x44, + 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x22, 0x4e, 0x0a, 0x0a, 0x4e, 0x65, 0x77, 0x56, 0x69, + 0x65, 0x77, 0x4d, 0x73, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x02, 0x49, 0x44, 0x12, 0x30, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x49, 0x6e, 0x66, + 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x68, 0x6f, 0x74, 0x73, 0x74, 0x75, + 0x66, 0x66, 0x70, 0x62, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x53, + 0x79, 0x6e, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x20, 0x5a, 0x1e, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x65, 0x6c, 0x61, 0x62, 0x2f, 0x68, 0x6f, 0x74, 0x73, + 0x74, 0x75, 0x66, 0x66, 0x2f, 0x66, 0x75, 0x7a, 0x7a, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_fuzz_fuzz_proto_rawDescOnce sync.Once + file_fuzz_fuzz_proto_rawDescData = file_fuzz_fuzz_proto_rawDesc +) + +func file_fuzz_fuzz_proto_rawDescGZIP() []byte { + file_fuzz_fuzz_proto_rawDescOnce.Do(func() { + file_fuzz_fuzz_proto_rawDescData = protoimpl.X.CompressGZIP(file_fuzz_fuzz_proto_rawDescData) + }) + return file_fuzz_fuzz_proto_rawDescData +} + +var file_fuzz_fuzz_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_fuzz_fuzz_proto_goTypes = []interface{}{ + (*FuzzMsg)(nil), // 0: fuzz.FuzzMsg + (*ProposeMsg)(nil), // 1: fuzz.ProposeMsg + (*TimeoutMsg)(nil), // 2: fuzz.TimeoutMsg + (*VoteMsg)(nil), // 3: fuzz.VoteMsg + (*NewViewMsg)(nil), // 4: fuzz.NewViewMsg + (*hotstuffpb.Proposal)(nil), // 5: hotstuffpb.Proposal + (*hotstuffpb.TimeoutMsg)(nil), // 6: hotstuffpb.TimeoutMsg + (*hotstuffpb.PartialCert)(nil), // 7: hotstuffpb.PartialCert + (*hotstuffpb.SyncInfo)(nil), // 8: hotstuffpb.SyncInfo +} +var file_fuzz_fuzz_proto_depIdxs = []int32{ + 1, // 0: fuzz.FuzzMsg.ProposeMsg:type_name -> fuzz.ProposeMsg + 2, // 1: fuzz.FuzzMsg.TimeoutMsg:type_name -> fuzz.TimeoutMsg + 3, // 2: fuzz.FuzzMsg.VoteMsg:type_name -> fuzz.VoteMsg + 4, // 3: fuzz.FuzzMsg.NewViewMsg:type_name -> fuzz.NewViewMsg + 5, // 4: fuzz.ProposeMsg.Proposal:type_name -> hotstuffpb.Proposal + 6, // 5: fuzz.TimeoutMsg.TimeoutMsg:type_name -> hotstuffpb.TimeoutMsg + 7, // 6: fuzz.VoteMsg.PartialCert:type_name -> hotstuffpb.PartialCert + 8, // 7: fuzz.NewViewMsg.SyncInfo:type_name -> hotstuffpb.SyncInfo + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_fuzz_fuzz_proto_init() } +func file_fuzz_fuzz_proto_init() { + if File_fuzz_fuzz_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_fuzz_fuzz_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FuzzMsg); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_fuzz_fuzz_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProposeMsg); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_fuzz_fuzz_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TimeoutMsg); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_fuzz_fuzz_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VoteMsg); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_fuzz_fuzz_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NewViewMsg); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_fuzz_fuzz_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*FuzzMsg_ProposeMsg)(nil), + (*FuzzMsg_TimeoutMsg)(nil), + (*FuzzMsg_VoteMsg)(nil), + (*FuzzMsg_NewViewMsg)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_fuzz_fuzz_proto_rawDesc, + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_fuzz_fuzz_proto_goTypes, + DependencyIndexes: file_fuzz_fuzz_proto_depIdxs, + MessageInfos: file_fuzz_fuzz_proto_msgTypes, + }.Build() + File_fuzz_fuzz_proto = out.File + file_fuzz_fuzz_proto_rawDesc = nil + file_fuzz_fuzz_proto_goTypes = nil + file_fuzz_fuzz_proto_depIdxs = nil +} diff --git a/fuzz/fuzz.proto b/fuzz/fuzz.proto new file mode 100644 index 00000000..58127d18 --- /dev/null +++ b/fuzz/fuzz.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package fuzz; + +import "internal/proto/hotstuffpb/hotstuff.proto"; + +option go_package = "github.com/relab/hotstuff/fuzz"; + +message FuzzMsg { + oneof Message { + ProposeMsg ProposeMsg = 1; + TimeoutMsg TimeoutMsg = 2; + VoteMsg VoteMsg = 3; + NewViewMsg NewViewMsg = 4; + } +} + +message ProposeMsg { + uint32 ID = 1; + hotstuffpb.Proposal Proposal = 2; +} + +message TimeoutMsg { + uint32 ID = 1; + hotstuffpb.TimeoutMsg TimeoutMsg = 2; +} + +message VoteMsg { + uint32 ID = 1; + hotstuffpb.PartialCert PartialCert = 2; + bool Deferred = 3; +} + +message NewViewMsg { + uint32 ID = 1; + hotstuffpb.SyncInfo SyncInfo = 2; +} diff --git a/fuzz/fuzz_test.go b/fuzz/fuzz_test.go new file mode 100644 index 00000000..17901c6e --- /dev/null +++ b/fuzz/fuzz_test.go @@ -0,0 +1,137 @@ +package fuzz + +import ( + "fmt" + "math/rand" + "runtime/debug" + "testing" + + _ "github.com/relab/hotstuff/consensus/chainedhotstuff" +) + +func tryExecuteScenario(errInfo *errorInfo, oldMessage any, newMessage any) { + errInfo.totalScenarios++ + defer func() { + if err := recover(); err != nil { + stack := string(debug.Stack()) + + errInfo.addPanic(stack, err, "TryExecuteScenario") + errInfo.failedScenarios++ + } + }() + + var numNodes uint8 = 4 + + allNodesSet := make(NodeSet) + for i := 1; i <= int(numNodes); i++ { + allNodesSet.Add(uint32(i)) + } + + s := Scenario{} + s = append(s, View{Leader: 1, Partitions: []NodeSet{allNodesSet}}) + s = append(s, View{Leader: 1, Partitions: []NodeSet{allNodesSet}}) + s = append(s, View{Leader: 1, Partitions: []NodeSet{allNodesSet}}) + s = append(s, View{Leader: 1, Partitions: []NodeSet{allNodesSet}}) + + result, err := ExecuteScenario(s, numNodes, 0, 100, "chainedhotstuff", oldMessage, newMessage) + if err != nil { + panic(err) + } + + if !result.Safe { + panic("Expected no safety violations") + } + + if result.Commits != 1 { + panic(fmt.Sprintf("Expected one commit (got %d)", result.Commits)) + } +} + +func fuzzScenario(errInfo *errorInfo, newMessage any) { + tryExecuteScenario(errInfo, 1, newMessage) +} + +func fuzzMsgToMsg(errInfo *errorInfo, fuzzMsg *FuzzMsg) any { + errInfo.totalMessages++ + defer func() { + if err := recover(); err != nil { + stack := string(debug.Stack()) + + errInfo.addPanic(stack, err, "fuzzMsgToMsg") + errInfo.failedMessages++ + } + }() + return fuzzMsgToHotStuffMsg(fuzzMsg) +} + +func useFuzzMessage(errInfo *errorInfo, fuzzMessage *FuzzMsg, seed *int64) { + errInfo.addTotal(fuzzMessage, seed) + newMessage := fuzzMsgToMsg(errInfo, fuzzMessage) + if newMessage != nil { + fuzzScenario(errInfo, newMessage) + } +} + +func ShowProgress(t *testing.T, numIteration int, iterations int, errorCount int) { + t.Helper() + fmt.Printf("running test %4d/%4d %4d errors \r", numIteration, iterations, errorCount) +} + +// the main test +func TestFuzz(t *testing.T) { + errInfo := new(errorInfo) + errInfo.init() + + f := initFuzz() + + iterations := 1000 + + for i := 0; i < iterations; i++ { + ShowProgress(t, i+1, iterations, errInfo.errorCount) + + seed := rand.Int63() + fuzzMessage := createFuzzMessage(f, &seed) + useFuzzMessage(errInfo, fuzzMessage, &seed) + } + + errInfo.outputErrors(t) +} + +// load previously created fuzz messages from a file +// it doesn't work quite right, i blame proto.Marshal() +func TestPreviousFuzz(t *testing.T) { + errInfo := new(errorInfo) + errInfo.init() + + fuzzMsgs, err := loadFuzzMessagesFromFile("previous_messages.b64") + if err != nil { + panic(err) + } + + for _, fuzzMessage := range fuzzMsgs { + useFuzzMessage(errInfo, fuzzMessage, nil) + } + + errInfo.outputErrors(t) +} + +// load previously created fuzz messages from a file +// it recreates the fuzz messages from a 64-bit source +func TestSeedPreviousFuzz(t *testing.T) { + errInfo := new(errorInfo) + errInfo.init() + + seeds, err := loadSeedsFromFile("previous_messages.seed") + if err != nil { + panic(err) + } + + f := initFuzz() + + for _, seed := range seeds { + fuzzMessage := createFuzzMessage(f, &seed) + useFuzzMessage(errInfo, fuzzMessage, nil) + } + + errInfo.outputErrors(t) +} diff --git a/fuzz/network.go b/fuzz/network.go new file mode 100644 index 00000000..a1e5b087 --- /dev/null +++ b/fuzz/network.go @@ -0,0 +1,512 @@ +package fuzz + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "strings" + "time" + + "github.com/relab/hotstuff" + "github.com/relab/hotstuff/blockchain" + "github.com/relab/hotstuff/consensus" + "github.com/relab/hotstuff/crypto" + "github.com/relab/hotstuff/crypto/ecdsa" + "github.com/relab/hotstuff/crypto/keygen" + "github.com/relab/hotstuff/eventloop" + "github.com/relab/hotstuff/logging" + "github.com/relab/hotstuff/modules" + "github.com/relab/hotstuff/synchronizer" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +// NodeID is an ID that is unique to a node in the network. +// The ReplicaID is the ID that the node uses when taking part in the consensus protocol, +// while the NetworkID is used to distinguish nodes on the network. +type NodeID struct { + ReplicaID hotstuff.ID + NetworkID uint32 +} + +func (id NodeID) String() string { + return fmt.Sprintf("r%dn%d", id.ReplicaID, id.NetworkID) +} + +type node struct { + blockChain modules.BlockChain + consensus modules.Consensus + eventLoop *eventloop.EventLoop + leaderRotation modules.LeaderRotation + synchronizer modules.Synchronizer + opts *modules.Options + + id NodeID + executedBlocks []*hotstuff.Block + effectiveView hotstuff.View + log strings.Builder +} + +func (n *node) InitModule(mods *modules.Core) { + mods.Get( + &n.blockChain, + &n.consensus, + &n.eventLoop, + &n.leaderRotation, + &n.synchronizer, + &n.opts, + ) +} + +type pendingMessage struct { + message any + receiver uint32 +} + +// Network is a simulated network that supports twins. +type Network struct { + nodes map[uint32]*node + // Maps a replica ID to a replica and its twins. + replicas map[hotstuff.ID][]*node + // For each view (starting at 1), contains the list of partitions for that view. + views []View + + OldMessage int + NewMessage any + + Messages []any + + MessageCounter int + + // the message types to drop + dropTypes map[reflect.Type]struct{} + + pendingMessages []pendingMessage + + logger logging.Logger + // the destination of the logger + log strings.Builder +} + +// NewSimpleNetwork creates a simple network. +func NewSimpleNetwork() *Network { + return &Network{ + nodes: make(map[uint32]*node), + replicas: make(map[hotstuff.ID][]*node), + dropTypes: make(map[reflect.Type]struct{}), + } +} + +// NewPartitionedNetwork creates a new Network with the specified partitions. +// partitions specifies the network partitions for each view. +func NewPartitionedNetwork(views []View, dropTypes ...any) *Network { + n := &Network{ + nodes: make(map[uint32]*node), + replicas: make(map[hotstuff.ID][]*node), + views: views, + dropTypes: make(map[reflect.Type]struct{}), + } + n.logger = logging.NewWithDest(&n.log, "network") + for _, t := range dropTypes { + n.dropTypes[reflect.TypeOf(t)] = struct{}{} + } + return n +} + +// GetNodeBuilder returns a consensus.Builder instance for a node in the network. +func (n *Network) GetNodeBuilder(id NodeID, pk hotstuff.PrivateKey) modules.Builder { + node := node{ + id: id, + } + n.nodes[id.NetworkID] = &node + n.replicas[id.ReplicaID] = append(n.replicas[id.ReplicaID], &node) + builder := modules.NewBuilder(id.ReplicaID, pk) + // register node as an anonymous module because that allows configuration to obtain it. + builder.Add(&node) + return builder +} + +func (n *Network) createTwinsNodes(nodes []NodeID, _ Scenario, consensusName string) error { + cg := &commandGenerator{} + for _, nodeID := range nodes { + + var err error + pk, err := keygen.GenerateECDSAPrivateKey() + if err != nil { + return err + } + + builder := n.GetNodeBuilder(nodeID, pk) + node := n.nodes[nodeID.NetworkID] + + consensusModule, ok := modules.GetModule[consensus.Rules](consensusName) + if !ok { + return fmt.Errorf("unknown consensus module: '%s'", consensusName) + } + builder.Add( + eventloop.New(100), + blockchain.New(), + consensus.New(consensusModule), + consensus.NewVotingMachine(), + crypto.NewCache(ecdsa.New(), 100), + synchronizer.New(FixedTimeout(0)), + logging.NewWithDest(&node.log, fmt.Sprintf("r%dn%d", nodeID.ReplicaID, nodeID.NetworkID)), + // twins-specific: + &configuration{network: n, node: node}, + &timeoutManager{network: n, node: node, timeout: 5}, + leaderRotation(n.views), + &commandModule{commandGenerator: cg, node: node}, + ) + builder.Options().SetShouldVerifyVotesSync() + builder.Build() + } + return nil +} + +func (n *Network) run(ticks int) { + // kick off the initial proposal(s) + for _, node := range n.nodes { + if node.leaderRotation.GetLeader(1) == node.id.ReplicaID { + node.consensus.Propose(node.synchronizer.(*synchronizer.Synchronizer).SyncInfo()) + } + } + + for tick := 0; tick < ticks; tick++ { + n.tick() + } +} + +// tick performs one tick for each node +func (n *Network) tick() { + for _, msg := range n.pendingMessages { + n.nodes[msg.receiver].eventLoop.AddEvent(msg.message) + } + n.pendingMessages = nil + + for _, node := range n.nodes { + node.eventLoop.AddEvent(tick{}) + // run each event loop as long as it has events + for node.eventLoop.Tick(context.Background()) { + } + } +} + +// shouldDrop decides if the sender should drop the message, based on the current view of the sender and the +// partitions configured for that view. +func (n *Network) shouldDrop(sender, receiver uint32, message any) bool { + node, ok := n.nodes[sender] + if !ok { + panic(fmt.Errorf("node matching sender id %d was not found", sender)) + } + + // Index into viewPartitions. + i := -1 + if node.effectiveView > node.synchronizer.View() { + i += int(node.effectiveView) + } else { + i += int(node.synchronizer.View()) + } + + if i < 0 { + return false + } + + // will default to dropping all messages from views that don't have any specified partitions. + if i >= len(n.views) { + return true + } + + partitions := n.views[i].Partitions + for _, partition := range partitions { + if partition.Contains(sender) && partition.Contains(receiver) { + return false + } + } + + _, ok = n.dropTypes[reflect.TypeOf(message)] + + return ok +} + +func (n *Network) shouldSwap() bool { + return n.OldMessage == n.MessageCounter +} + +// NewConfiguration returns a new Configuration module for this network. +func (n *Network) NewConfiguration() modules.Configuration { + return &configuration{network: n} +} + +type configuration struct { + node *node + network *Network + subConfig hotstuff.IDSet +} + +// alternative way to get a pointer to the node. +func (c *configuration) InitModule(mods *modules.Core) { + if c.node == nil { + mods.TryGet(&c.node) + } +} + +func (c *configuration) broadcastMessage(message any) { + c.network.logger.Infof("broadcasting message") + for id := range c.network.replicas { + if id == c.node.id.ReplicaID { + // do not send message to self or twin + continue + } else if c.subConfig == nil || c.subConfig.Contains(id) { + c.sendMessage(id, message) + } + } +} + +func (c *configuration) sendMessage(id hotstuff.ID, message any) { + nodes, ok := c.network.replicas[id] + if !ok { + panic(fmt.Errorf("attempt to send message to replica %d, but this replica does not exist", id)) + } + + for _, node := range nodes { + + if c.shouldDrop(node.id, message) { + c.network.logger.Infof("node %v -> node %v: DROP %T(%v)", c.node.id, node.id, message, message) + continue + } + + c.network.Messages = append(c.network.Messages, message) + c.network.MessageCounter++ + + if c.network.shouldSwap() { + c.network.logger.Infof("swapping message with fuzz message") + message = c.network.NewMessage + } + + c.network.logger.Infof("node %v -> node %v: SEND %T(%v)", c.node.id, node.id, message, message) + c.network.pendingMessages = append( + c.network.pendingMessages, + pendingMessage{ + receiver: uint32(node.id.NetworkID), + message: message, + }, + ) + } +} + +// shouldDrop checks if a message to the node identified by id should be dropped. +func (c *configuration) shouldDrop(id NodeID, message any) bool { + // retrieve the drop config for this node. + return c.network.shouldDrop(c.node.id.NetworkID, id.NetworkID, message) +} + +// Replicas returns all of the replicas in the configuration. +func (c *configuration) Replicas() map[hotstuff.ID]modules.Replica { + m := make(map[hotstuff.ID]modules.Replica) + for id := range c.network.replicas { + m[id] = &replica{ + config: c, + id: id, + } + } + return m +} + +// Replica returns a replica if present in the configuration. +func (c *configuration) Replica(id hotstuff.ID) (r modules.Replica, ok bool) { + if _, ok = c.network.replicas[id]; ok { + return &replica{ + config: c, + id: id, + }, true + } + return nil, false +} + +// SubConfig returns a subconfiguration containing the replicas specified in the ids slice. +func (c *configuration) SubConfig(ids []hotstuff.ID) (sub modules.Configuration, err error) { + subConfig := hotstuff.NewIDSet() + for _, id := range ids { + subConfig.Add(id) + } + return &configuration{ + node: c.node, + network: c.network, + subConfig: subConfig, + }, nil +} + +// Len returns the number of replicas in the configuration. +func (c *configuration) Len() int { + return len(c.network.replicas) +} + +// QuorumSize returns the size of a quorum. +func (c *configuration) QuorumSize() int { + return hotstuff.QuorumSize(c.Len()) +} + +// Propose sends the block to all replicas in the configuration. +func (c *configuration) Propose(proposal hotstuff.ProposeMsg) { + c.broadcastMessage(proposal) +} + +// Timeout sends the timeout message to all replicas. +func (c *configuration) Timeout(msg hotstuff.TimeoutMsg) { + c.broadcastMessage(msg) +} + +// Fetch requests a block from all the replicas in the configuration. +func (c *configuration) Fetch(_ context.Context, hash hotstuff.Hash) (block *hotstuff.Block, ok bool) { + for _, replica := range c.network.replicas { + for _, node := range replica { + if c.shouldDrop(node.id, hash) { + continue + } + block, ok = node.blockChain.LocalGet(hash) + if ok { + return block, true + } + } + } + return nil, false +} + +type replica struct { + // pointer to the node that wants to contact this replica. + config *configuration + // id of the replica. + id hotstuff.ID +} + +// ID returns the replica's id. +func (r *replica) ID() hotstuff.ID { + return r.config.network.replicas[r.id][0].id.ReplicaID +} + +// PublicKey returns the replica's public key. +func (r *replica) PublicKey() hotstuff.PublicKey { + return r.config.network.replicas[r.id][0].opts.PrivateKey().Public() +} + +// Vote sends the partial certificate to the other replica. +func (r *replica) Vote(cert hotstuff.PartialCert) { + r.config.sendMessage(r.id, hotstuff.VoteMsg{ + ID: r.config.node.opts.ID(), + PartialCert: cert, + }) +} + +// NewView sends the quorum certificate to the other replica. +func (r *replica) NewView(si hotstuff.SyncInfo) { + r.config.sendMessage(r.id, hotstuff.NewViewMsg{ + ID: r.config.node.opts.ID(), + SyncInfo: si, + }) +} + +func (r *replica) Metadata() map[string]string { + return r.config.network.replicas[r.id][0].opts.ConnectionMetadata() +} + +// NodeSet is a set of network ids. +type NodeSet map[uint32]struct{} + +// Add adds a NodeID to the set. +func (s NodeSet) Add(v uint32) { + s[v] = struct{}{} +} + +// Contains returns true if the set contains the NodeID, false otherwise. +func (s NodeSet) Contains(v uint32) bool { + _, ok := s[v] + return ok +} + +// MarshalJSON returns a JSON representation of the node set. +func (s NodeSet) MarshalJSON() ([]byte, error) { + ids := maps.Keys(s) + slices.Sort(ids) + return json.Marshal(ids) +} + +// UnmarshalJSON restores the node set from JSON. +func (s *NodeSet) UnmarshalJSON(data []byte) error { + if *s == nil { + *s = make(NodeSet) + } + var nodes []uint32 + err := json.Unmarshal(data, &nodes) + if err != nil { + return err + } + for _, node := range nodes { + s.Add(node) + } + return nil +} + +type tick struct{} + +type timeoutManager struct { + synchronizer modules.Synchronizer + eventLoop *eventloop.EventLoop + + node *node + network *Network + countdown int + timeout int +} + +func (tm *timeoutManager) advance() { + tm.countdown-- + if tm.countdown == 0 { + view := tm.synchronizer.View() + tm.eventLoop.AddEvent(synchronizer.TimeoutEvent{View: view}) + tm.countdown = tm.timeout + if tm.node.effectiveView <= view { + tm.node.effectiveView = view + 1 + tm.network.logger.Infof("node %v effective view is %d due to timeout", tm.node.id, tm.node.effectiveView) + } + } +} + +func (tm *timeoutManager) viewChange(event synchronizer.ViewChangeEvent) { + tm.countdown = tm.timeout + if event.Timeout { + tm.network.logger.Infof("node %v entered view %d after timeout", tm.node.id, event.View) + } else { + tm.network.logger.Infof("node %v entered view %d after voting", tm.node.id, event.View) + } +} + +// InitModule gives the module a reference to the Modules object. +// It also allows the module to set module options using the OptionsBuilder. +func (tm *timeoutManager) InitModule(mods *modules.Core) { + mods.Get( + &tm.synchronizer, + &tm.eventLoop, + ) + + tm.eventLoop.RegisterObserver(tick{}, func(event any) { + tm.advance() + }) + tm.eventLoop.RegisterObserver(synchronizer.ViewChangeEvent{}, func(event any) { + tm.viewChange(event.(synchronizer.ViewChangeEvent)) + }) +} + +// FixedTimeout returns an ExponentialTimeout with a max exponent of 0. +func FixedTimeout(timeout time.Duration) synchronizer.ViewDuration { + return fixedDuration{timeout} +} + +type fixedDuration struct { + timeout time.Duration +} + +func (d fixedDuration) Duration() time.Duration { return d.timeout } +func (d fixedDuration) ViewStarted() {} +func (d fixedDuration) ViewSucceeded() {} +func (d fixedDuration) ViewTimeout() {} diff --git a/fuzz/scenario.go b/fuzz/scenario.go new file mode 100644 index 00000000..139dc8dd --- /dev/null +++ b/fuzz/scenario.go @@ -0,0 +1,221 @@ +package fuzz + +import ( + "context" + "fmt" + "strconv" + "strings" + "sync" + + "github.com/relab/hotstuff" +) + +// View specifies the leader id and the partition scenario for a single view. +type View struct { + Leader hotstuff.ID `json:"leader"` + Partitions []NodeSet `json:"partitions"` +} + +// Scenario specifies the nodes, partitions and leaders for a twins scenario. +type Scenario []View + +func (s Scenario) String() string { + var sb strings.Builder + for i := 0; i < len(s); i++ { + sb.WriteString(fmt.Sprintf("leader: %d, partitions: ", s[i].Leader)) + for _, partition := range s[i].Partitions { + sb.WriteString("[ ") + for id := range partition { + sb.WriteString(fmt.Sprint(id)) + sb.WriteString(" ") + } + sb.WriteString("] ") + } + sb.WriteString("\n") + } + return sb.String() +} + +// ScenarioResult contains the result and logs from executing a scenario. +type ScenarioResult struct { + Safe bool + Commits int + NetworkLog string + NodeLogs map[NodeID]string + NodeCommits map[NodeID][]*hotstuff.Block + Messages []any + MessageCount int +} + +func assignNodeIDs(numNodes, numTwins uint8) (nodes, twins []NodeID) { + replicaID := hotstuff.ID(1) + networkID := uint32(1) + remainingTwins := numTwins + + // assign IDs to nodes + for i := uint8(0); i < numNodes; i++ { + if remainingTwins > 0 { + twins = append(twins, NodeID{ + ReplicaID: replicaID, + NetworkID: networkID, + }) + networkID++ + twins = append(twins, NodeID{ + ReplicaID: replicaID, + NetworkID: networkID, + }) + remainingTwins-- + } else { + nodes = append(nodes, NodeID{ + ReplicaID: replicaID, + NetworkID: networkID, + }) + } + networkID++ + replicaID++ + } + + return +} + +// ExecuteScenario executes a twins scenario. +func ExecuteScenario(scenario Scenario, numNodes, numTwins uint8, numTicks int, consensusName string, replaceMessage ...any) (result ScenarioResult, err error) { + // Network simulator that blocks proposals, votes, and fetch requests between nodes that are in different partitions. + // Timeout and NewView messages are permitted. + + network := NewPartitionedNetwork(scenario, + hotstuff.ProposeMsg{}, + hotstuff.VoteMsg{}, + hotstuff.Hash{}, + hotstuff.NewViewMsg{}, + hotstuff.TimeoutMsg{}, + ) + + if len(replaceMessage) == 2 { + network.OldMessage = replaceMessage[0].(int) + network.NewMessage = replaceMessage[1] + } + + nodes, twins := assignNodeIDs(numNodes, numTwins) + nodes = append(nodes, twins...) + + err = network.createTwinsNodes(nodes, scenario, consensusName) + if err != nil { + return ScenarioResult{}, err + } + + network.run(numTicks) + + nodeLogs := make(map[NodeID]string) + for _, node := range network.nodes { + nodeLogs[node.id] = node.log.String() + } + + // check if the majority of replicas have committed the same blocks + safe, commits := checkCommits(network) + + return ScenarioResult{ + Safe: safe, + Commits: commits, + NetworkLog: network.log.String(), + NodeLogs: nodeLogs, + NodeCommits: getBlocks(network), + Messages: network.Messages, + MessageCount: network.MessageCounter, + }, nil +} + +func checkCommits(network *Network) (safe bool, commits int) { + i := 0 + for { + noCommits := true + commitCount := make(map[hotstuff.Hash]int) + for _, replica := range network.replicas { + if len(replica) != 1 { + // TODO: should we be skipping replicas with twins? + continue + } + if len(replica[0].executedBlocks) <= i { + continue + } + commitCount[replica[0].executedBlocks[i].Hash()]++ + noCommits = false + } + + if noCommits { + break + } + + // if all correct replicas have executed the same blocks, then there should be only one entry in commitCount + // the number of replicas that committed the block could be smaller, if some correct replicas happened to + // be in a different partition at the time when the test ended. + if len(commitCount) != 1 { + return false, i + } + + i++ + } + return true, i +} + +type leaderRotation []View + +// GetLeader returns the id of the leader in the given view. +func (lr leaderRotation) GetLeader(view hotstuff.View) hotstuff.ID { + // we start at view 1 + v := int(view) - 1 + if v >= 0 && v < len(lr) { + return lr[v].Leader + } + // default to 0 (which is an invalid id) + return 0 +} + +func getBlocks(network *Network) map[NodeID][]*hotstuff.Block { + m := make(map[NodeID][]*hotstuff.Block) + for _, node := range network.nodes { + m[node.id] = node.executedBlocks + } + return m +} + +type commandGenerator struct { + mut sync.Mutex + nextCmd uint64 +} + +func (cg *commandGenerator) next() hotstuff.Command { + cg.mut.Lock() + defer cg.mut.Unlock() + cmd := hotstuff.Command(strconv.FormatUint(cg.nextCmd, 10)) + cg.nextCmd++ + return cmd +} + +type commandModule struct { + commandGenerator *commandGenerator + node *node +} + +// Accept returns true if the replica should accept the command, false otherwise. +func (commandModule) Accept(_ hotstuff.Command) bool { + return true +} + +// Proposed tells the acceptor that the propose phase for the given command succeeded, and it should no longer be +// accepted in the future. +func (commandModule) Proposed(_ hotstuff.Command) {} + +// Get returns the next command to be proposed. +// It may run until the context is cancelled. +// If no command is available, the 'ok' return value should be false. +func (cm commandModule) Get(_ context.Context) (cmd hotstuff.Command, ok bool) { + return cm.commandGenerator.next(), true +} + +// Exec executes the given command. +func (cm commandModule) Exec(block *hotstuff.Block) { + cm.node.executedBlocks = append(cm.node.executedBlocks, block) +} + +func (commandModule) Fork(_ *hotstuff.Block) {} diff --git a/fuzz/serialize.go b/fuzz/serialize.go new file mode 100644 index 00000000..4468c12f --- /dev/null +++ b/fuzz/serialize.go @@ -0,0 +1,81 @@ +package fuzz + +import ( + "encoding/base64" + "os" + "strconv" + "strings" + + "google.golang.org/protobuf/proto" +) + +func fuzzMsgToB64(msg *FuzzMsg) (string, error) { + bytes, err := proto.Marshal(msg) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(bytes), nil +} + +func b64ToFuzzMsg(str string) (*FuzzMsg, error) { + bytes, err := base64.StdEncoding.DecodeString(str) + if err != nil { + return nil, err + } + msg := new(FuzzMsg) + err = proto.Unmarshal(bytes, msg) + if err != nil { + return nil, err + } + return msg, err +} + +func saveStringToFile(filename string, str string) error { + return os.WriteFile(filename, []byte(str), 0o600) +} + +func loadStringFromFile(filename string) (string, error) { + b, err := os.ReadFile(filename) + if err != nil { + return "", err + } + return string(b), nil +} + +func loadFuzzMessagesFromFile(filename string) ([]*FuzzMsg, error) { + str, err := loadStringFromFile(filename) + if err != nil { + return nil, err + } + fuzzMsgs := make([]*FuzzMsg, 0) + for _, b64 := range strings.Split(str, "\n") { + if b64 == "" { + continue + } + fuzzMsg, err := b64ToFuzzMsg(b64) + if err != nil { + return nil, err + } + fuzzMsgs = append(fuzzMsgs, fuzzMsg) + } + return fuzzMsgs, nil +} + +func loadSeedsFromFile(filename string) ([]int64, error) { + str, err := loadStringFromFile(filename) + if err != nil { + return nil, err + } + seeds := make([]int64, 0) + for _, seedString := range strings.Split(str, "\n") { + if seedString == "" { + continue + } + seed, err := strconv.ParseInt(seedString, 10, 64) + if err != nil { + return nil, err + } + seeds = append(seeds, seed) + } + return seeds, nil +} diff --git a/fuzz/types.go b/fuzz/types.go new file mode 100644 index 00000000..25d3f8eb --- /dev/null +++ b/fuzz/types.go @@ -0,0 +1,56 @@ +package fuzz + +import ( + "fmt" + + "github.com/relab/hotstuff" + "github.com/relab/hotstuff/internal/proto/hotstuffpb" + "google.golang.org/protobuf/proto" +) + +// extractProtoMsg extracts the proto message from the FuzzMsg's oneof field. +func extractProtoMsg(fuzzMsg *FuzzMsg) (m proto.Message) { + switch msg := fuzzMsg.Message.(type) { + case *FuzzMsg_ProposeMsg: + return msg.ProposeMsg + case *FuzzMsg_TimeoutMsg: + return msg.TimeoutMsg + case *FuzzMsg_VoteMsg: + return msg.VoteMsg + case *FuzzMsg_NewViewMsg: + return msg.NewViewMsg + default: + panic(fmt.Errorf("unknown message type: %T", msg)) + } +} + +// fuzzMsgToHotStuffMsg returns a hotstuff message extracted from a FuzzMsg encapsulating a proto message. +func fuzzMsgToHotStuffMsg(fuzzMsg *FuzzMsg) any { + switch msg := fuzzMsg.Message.(type) { + case *FuzzMsg_ProposeMsg: + proposeMsg := hotstuffpb.ProposalFromProto(msg.ProposeMsg.Proposal) + proposeMsg.ID = hotstuff.ID(msg.ProposeMsg.ID) + return proposeMsg + + case *FuzzMsg_TimeoutMsg: + timeoutMsg := hotstuffpb.TimeoutMsgFromProto(msg.TimeoutMsg.TimeoutMsg) + timeoutMsg.ID = hotstuff.ID(msg.TimeoutMsg.ID) + return timeoutMsg + + case *FuzzMsg_VoteMsg: + voteMsg := hotstuff.VoteMsg{} + voteMsg.PartialCert = hotstuffpb.PartialCertFromProto(msg.VoteMsg.PartialCert) + voteMsg.ID = hotstuff.ID(msg.VoteMsg.ID) + voteMsg.Deferred = msg.VoteMsg.Deferred + return voteMsg + + case *FuzzMsg_NewViewMsg: + newViewMsg := hotstuff.NewViewMsg{} + newViewMsg.SyncInfo = hotstuffpb.SyncInfoFromProto(msg.NewViewMsg.SyncInfo) + newViewMsg.ID = hotstuff.ID(msg.NewViewMsg.ID) + return newViewMsg + + default: + panic(fmt.Errorf("unknown message type: %T", msg)) + } +} diff --git a/go.mod b/go.mod index ab4d85da..4d109a15 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,10 @@ go 1.20 require ( github.com/felixge/fgprof v0.9.2 github.com/golang/mock v1.6.0 + github.com/google/gofuzz v1.1.0 github.com/kilic/bls12-381 v0.1.1-0.20210208205449-6045b0235e36 github.com/mattn/go-isatty v0.0.14 + github.com/meling/proto2 v0.0.0-20230703190343-27bbb7955179 github.com/mitchellh/go-homedir v1.1.0 github.com/mroth/weightedrand v0.4.1 github.com/relab/gorums v0.7.1-0.20220818130557-8533cb369cd6 @@ -21,7 +23,7 @@ require ( gonum.org/v1/plot v0.11.0 google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f google.golang.org/grpc v1.53.0 - google.golang.org/protobuf v1.28.1 + google.golang.org/protobuf v1.31.0 ) require ( diff --git a/go.sum b/go.sum index 039ea1eb..9ca4c33c 100644 --- a/go.sum +++ b/go.sum @@ -396,6 +396,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -507,6 +508,8 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/meling/proto2 v0.0.0-20230703190343-27bbb7955179 h1:ywfmQjNyaL0LrfjkN0rir8LvRvLThhupx/kXDbpHb7c= +github.com/meling/proto2 v0.0.0-20230703190343-27bbb7955179/go.mod h1:pvWGo3hsDXoNnRuPLpXVOZj4ABerp8yPbFG2EyDeKno= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -1186,8 +1189,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/proto/hotstuffpb/hotstuff.pb.go b/internal/proto/hotstuffpb/hotstuff.pb.go index e1226cab..f452cf83 100644 --- a/internal/proto/hotstuffpb/hotstuff.pb.go +++ b/internal/proto/hotstuffpb/hotstuff.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.21.4 +// protoc-gen-go v1.28.0 +// protoc v4.23.4 // source: internal/proto/hotstuffpb/hotstuff.proto package hotstuffpb @@ -319,6 +319,7 @@ type Signature struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Sig: + // // *Signature_ECDSASig // *Signature_BLS12Sig Sig isSignature_Sig `protobuf_oneof:"Sig"` @@ -556,6 +557,7 @@ type QuorumSignature struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Sig: + // // *QuorumSignature_ECDSASigs // *QuorumSignature_BLS12Sig Sig isQuorumSignature_Sig `protobuf_oneof:"Sig"` diff --git a/internal/proto/hotstuffpb/hotstuff_gorums.pb.go b/internal/proto/hotstuffpb/hotstuff_gorums.pb.go index fc82a827..1e46a1c9 100644 --- a/internal/proto/hotstuffpb/hotstuff_gorums.pb.go +++ b/internal/proto/hotstuffpb/hotstuff_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.7.0-devel -// protoc v3.21.4 +// protoc v4.23.4 // source: internal/proto/hotstuffpb/hotstuff.proto package hotstuffpb @@ -33,8 +33,9 @@ type Configuration struct { // ConfigurationFromRaw returns a new Configuration from the given raw configuration and QuorumSpec. // // This function may for example be used to "clone" a configuration but install a different QuorumSpec: -// cfg1, err := mgr.NewConfiguration(qspec1, opts...) -// cfg2 := ConfigurationFromRaw(cfg1.RawConfig, qspec2) +// +// cfg1, err := mgr.NewConfiguration(qspec1, opts...) +// cfg2 := ConfigurationFromRaw(cfg1.RawConfig, qspec2) func ConfigurationFromRaw(rawCfg gorums.RawConfiguration, qspec QuorumSpec) *Configuration { // return an error if the QuorumSpec interface is not empty and no implementation was provided. var test interface{} = struct{}{}