From bf6e06a41da3af29c312d7a783ff3c6e698a868c Mon Sep 17 00:00:00 2001 From: Fangyu Gai Date: Mon, 28 Oct 2024 19:52:28 +0800 Subject: [PATCH 1/5] create gov prop --- proto/babylon/finality/v1/gov.proto | 25 ++ x/finality/gov.go | 29 ++ x/finality/keeper/gov.go | 11 + x/finality/types/errors.go | 2 + x/finality/types/gov.go | 81 +++++ x/finality/types/gov.pb.go | 456 ++++++++++++++++++++++++++++ 6 files changed, 604 insertions(+) create mode 100644 proto/babylon/finality/v1/gov.proto create mode 100644 x/finality/gov.go create mode 100644 x/finality/keeper/gov.go create mode 100644 x/finality/types/gov.go create mode 100644 x/finality/types/gov.pb.go diff --git a/proto/babylon/finality/v1/gov.proto b/proto/babylon/finality/v1/gov.proto new file mode 100644 index 000000000..ae71ea589 --- /dev/null +++ b/proto/babylon/finality/v1/gov.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; +package babylon.finality.v1; + +import "cosmos_proto/cosmos.proto"; +import "amino/amino.proto"; +import "gogoproto/gogo.proto"; + +option go_package = "github.com/babylonlabs-io/babylon/x/finality/types"; + +// ResumeFinalityProposal is a gov Content type for resuming finality +// in case of finality is halting by jailing a list of sluggish finality +// providers from the halting height +message ResumeFinalityProposal { + option (gogoproto.goproto_getters) = false; + option (gogoproto.goproto_stringer) = false; + option (cosmos_proto.implements_interface) = "cosmos.gov.v1beta1.Content"; + + string title = 1; + string description = 2; + // fp_pks is a list of finality provider public keys to jail + // the public key follows encoding in BIP-340 spec + repeated bytes fp_pks = 3 [ (gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340PubKey" ]; + // halting_height is the height where the finality halting begins + uint32 halting_height = 4; +} diff --git a/x/finality/gov.go b/x/finality/gov.go new file mode 100644 index 000000000..43dc13bc1 --- /dev/null +++ b/x/finality/gov.go @@ -0,0 +1,29 @@ +package finality + +import ( + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + + "github.com/babylonlabs-io/babylon/x/finality/keeper" + "github.com/babylonlabs-io/babylon/x/finality/types" +) + +// NewResumeFinalityProposalHandler is a handler for governance proposal on resume finality. +func NewResumeFinalityProposalHandler(k keeper.Keeper) govtypesv1.Handler { + return func(ctx sdk.Context, content govtypesv1.Content) error { + switch c := content.(type) { + case *types.ResumeFinalityProposal: + return handleResumeFinalityProposal(ctx, k, c) + + default: + return errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized resume finality proposal content type: %T", c) + } + } +} + +// handleResumeFinalityProposal is a handler for jail finality provider proposals +func handleResumeFinalityProposal(ctx sdk.Context, k keeper.Keeper, p *types.ResumeFinalityProposal) error { + return k.JailFinalityProvidersFromHeight(ctx, p.FpPks, p.HaltingHeight) +} diff --git a/x/finality/keeper/gov.go b/x/finality/keeper/gov.go new file mode 100644 index 000000000..34fc3e106 --- /dev/null +++ b/x/finality/keeper/gov.go @@ -0,0 +1,11 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + bbntypes "github.com/babylonlabs-io/babylon/types" +) + +func (k Keeper) JailFinalityProvidersFromHeight(ctx sdk.Context, fps []bbntypes.BIP340PubKey, height uint32) error { + return nil +} diff --git a/x/finality/types/errors.go b/x/finality/types/errors.go index 1b026b6dc..e70c948b6 100644 --- a/x/finality/types/errors.go +++ b/x/finality/types/errors.go @@ -22,4 +22,6 @@ var ( ErrVotingPowerTableNotUpdated = errorsmod.Register(ModuleName, 1113, "voting power table has not been updated") ErrBTCStakingNotActivated = errorsmod.Register(ModuleName, 1114, "the BTC staking protocol is not activated yet") ErrFinalityNotActivated = errorsmod.Register(ModuleName, 1115, "finality is not active yet") + ErrEmptyProposalFinalityProviders = errorsmod.Register(ModuleName, 1116, "the finality provider list in the ResumeFinality is empty") + ErrEmptyProposalHaltingHeight = errorsmod.Register(ModuleName, 1117, "the halting height in the ResumeFinality is empty") ) diff --git a/x/finality/types/gov.go b/x/finality/types/gov.go new file mode 100644 index 000000000..12808b52a --- /dev/null +++ b/x/finality/types/gov.go @@ -0,0 +1,81 @@ +package types + +import ( + "fmt" + "strings" + + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + + "github.com/babylonlabs-io/babylon/types" +) + +const ( + ProposalResumeFinality = "ResumeFinality" +) + +// Init registers proposals to update and replace pool incentives. +func init() { + govtypesv1.RegisterProposalType(ProposalResumeFinality) +} + +var ( + _ govtypesv1.Content = &ResumeFinalityProposal{} +) + +// NewResumeFinalityProposal returns a new instance of a resume finality proposal struct. +func NewResumeFinalityProposal(title, description string, fps []types.BIP340PubKey, haltingHeight uint32) govtypesv1.Content { + return &ResumeFinalityProposal{ + Title: title, + Description: description, + FpPks: fps, + HaltingHeight: haltingHeight, + } +} + +// GetTitle gets the title of the proposal +func (p *ResumeFinalityProposal) GetTitle() string { return p.Title } + +// GetDescription gets the description of the proposal +func (p *ResumeFinalityProposal) GetDescription() string { return p.Description } + +// ProposalRoute returns the router key for the proposal +func (p *ResumeFinalityProposal) ProposalRoute() string { return RouterKey } + +// ProposalType returns the type of the proposal +func (p *ResumeFinalityProposal) ProposalType() string { + return ProposalResumeFinality +} + +// ValidateBasic validates a governance proposal's abstract and basic contents +func (p *ResumeFinalityProposal) ValidateBasic() error { + err := govtypesv1.ValidateAbstract(p) + if err != nil { + return err + } + if len(p.FpPks) == 0 { + return ErrEmptyProposalFinalityProviders + } + + if p.HaltingHeight == 0 { + return ErrEmptyProposalHaltingHeight + } + + return nil +} + +// String returns a string containing the jail finality providers proposal. +func (p *ResumeFinalityProposal) String() string { + fpsStr := fmt.Sprintln("Finality providers to jail:") + for i, pk := range p.FpPks { + fpsStr = fpsStr + fmt.Sprintf("%d. %s\n", i+1, pk.MarshalHex()) + } + + var b strings.Builder + b.WriteString(fmt.Sprintf(`Resume Finality Proposal: + Title: %s + Description: %s + Halting height: %d + %s +`, p.Title, p.Description, p.HaltingHeight, fpsStr)) + return b.String() +} diff --git a/x/finality/types/gov.pb.go b/x/finality/types/gov.pb.go new file mode 100644 index 000000000..5e502f8ec --- /dev/null +++ b/x/finality/types/gov.pb.go @@ -0,0 +1,456 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: babylon/finality/v1/gov.proto + +package types + +import ( + fmt "fmt" + github_com_babylonlabs_io_babylon_types "github.com/babylonlabs-io/babylon/types" + _ "github.com/cosmos/cosmos-proto" + _ "github.com/cosmos/cosmos-sdk/types/tx/amino" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// ResumeFinalityProposal is a gov Content type for resuming finality +// in case of finality is halting by jailing a list of sluggish finality +// providers from the halting height +type ResumeFinalityProposal struct { + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // fp_pks is a list of finality provider public keys to jail + // the public key follows encoding in BIP-340 spec + FpPks []github_com_babylonlabs_io_babylon_types.BIP340PubKey `protobuf:"bytes,3,rep,name=fp_pks,json=fpPks,proto3,customtype=github.com/babylonlabs-io/babylon/types.BIP340PubKey" json:"fp_pks,omitempty"` + // halting_height is the height where the finality halting begins + HaltingHeight uint32 `protobuf:"varint,4,opt,name=halting_height,json=haltingHeight,proto3" json:"halting_height,omitempty"` +} + +func (m *ResumeFinalityProposal) Reset() { *m = ResumeFinalityProposal{} } +func (*ResumeFinalityProposal) ProtoMessage() {} +func (*ResumeFinalityProposal) Descriptor() ([]byte, []int) { + return fileDescriptor_8c9af01aea56f783, []int{0} +} +func (m *ResumeFinalityProposal) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ResumeFinalityProposal) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ResumeFinalityProposal.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ResumeFinalityProposal) XXX_Merge(src proto.Message) { + xxx_messageInfo_ResumeFinalityProposal.Merge(m, src) +} +func (m *ResumeFinalityProposal) XXX_Size() int { + return m.Size() +} +func (m *ResumeFinalityProposal) XXX_DiscardUnknown() { + xxx_messageInfo_ResumeFinalityProposal.DiscardUnknown(m) +} + +var xxx_messageInfo_ResumeFinalityProposal proto.InternalMessageInfo + +func init() { + proto.RegisterType((*ResumeFinalityProposal)(nil), "babylon.finality.v1.ResumeFinalityProposal") +} + +func init() { proto.RegisterFile("babylon/finality/v1/gov.proto", fileDescriptor_8c9af01aea56f783) } + +var fileDescriptor_8c9af01aea56f783 = []byte{ + // 342 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4d, 0x4a, 0x4c, 0xaa, + 0xcc, 0xc9, 0xcf, 0xd3, 0x4f, 0xcb, 0xcc, 0x4b, 0xcc, 0xc9, 0x2c, 0xa9, 0xd4, 0x2f, 0x33, 0xd4, + 0x4f, 0xcf, 0x2f, 0xd3, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x86, 0x4a, 0xeb, 0xc1, 0xa4, + 0xf5, 0xca, 0x0c, 0xa5, 0x24, 0x93, 0xf3, 0x8b, 0x73, 0xf3, 0x8b, 0xe3, 0xc1, 0x4a, 0xf4, 0x21, + 0x1c, 0x88, 0x7a, 0x29, 0xc1, 0xc4, 0xdc, 0xcc, 0xbc, 0x7c, 0x7d, 0x30, 0x09, 0x15, 0x12, 0x49, + 0xcf, 0x4f, 0xcf, 0x87, 0x28, 0x05, 0xb1, 0x20, 0xa2, 0x4a, 0x1f, 0x18, 0xb9, 0xc4, 0x82, 0x52, + 0x8b, 0x4b, 0x73, 0x53, 0xdd, 0xa0, 0x26, 0x07, 0x14, 0xe5, 0x17, 0xe4, 0x17, 0x27, 0xe6, 0x08, + 0x89, 0x70, 0xb1, 0x96, 0x64, 0x96, 0xe4, 0xa4, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06, 0x41, + 0x38, 0x42, 0x0a, 0x5c, 0xdc, 0x29, 0xa9, 0xc5, 0xc9, 0x45, 0x99, 0x05, 0x25, 0x99, 0xf9, 0x79, + 0x12, 0x4c, 0x60, 0x39, 0x64, 0x21, 0x21, 0x7f, 0x2e, 0xb6, 0xb4, 0x82, 0xf8, 0x82, 0xec, 0x62, + 0x09, 0x66, 0x05, 0x66, 0x0d, 0x1e, 0x27, 0x8b, 0x5b, 0xf7, 0xe4, 0x4d, 0xd2, 0x33, 0x4b, 0x32, + 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0xa1, 0x5e, 0xc9, 0x49, 0x4c, 0x2a, 0xd6, 0xcd, 0xcc, + 0x87, 0x71, 0xf5, 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0xf5, 0x9c, 0x3c, 0x03, 0x8c, 0x4d, 0x0c, 0x02, + 0x4a, 0x93, 0xbc, 0x53, 0x2b, 0x83, 0x58, 0xd3, 0x0a, 0x02, 0xb2, 0x8b, 0x85, 0x54, 0xb9, 0xf8, + 0x32, 0x12, 0x73, 0x4a, 0x32, 0xf3, 0xd2, 0xe3, 0x33, 0x52, 0x33, 0xd3, 0x33, 0x4a, 0x24, 0x58, + 0x14, 0x18, 0x35, 0x78, 0x83, 0x78, 0xa1, 0xa2, 0x1e, 0x60, 0x41, 0x2b, 0xb5, 0x8e, 0x05, 0xf2, + 0x0c, 0x33, 0x16, 0xc8, 0x33, 0x9c, 0xda, 0xa2, 0x2b, 0x05, 0x0d, 0x0d, 0x50, 0x08, 0x96, 0x19, + 0x26, 0xa5, 0x96, 0x24, 0x1a, 0xea, 0x39, 0xe7, 0xe7, 0x95, 0xa4, 0xe6, 0x95, 0x38, 0xf9, 0x9c, + 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, 0x8c, 0x13, 0x1e, 0xcb, 0x31, + 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c, 0x43, 0x94, 0x11, 0x61, 0x57, 0x56, 0x20, 0x22, + 0x08, 0xec, 0xe0, 0x24, 0x36, 0x70, 0x38, 0x1a, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x0e, 0x7a, + 0xed, 0x7d, 0xc1, 0x01, 0x00, 0x00, +} + +func (m *ResumeFinalityProposal) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ResumeFinalityProposal) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ResumeFinalityProposal) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.HaltingHeight != 0 { + i = encodeVarintGov(dAtA, i, uint64(m.HaltingHeight)) + i-- + dAtA[i] = 0x20 + } + if len(m.FpPks) > 0 { + for iNdEx := len(m.FpPks) - 1; iNdEx >= 0; iNdEx-- { + { + size := m.FpPks[iNdEx].Size() + i -= size + if _, err := m.FpPks[iNdEx].MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintGov(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.Description) > 0 { + i -= len(m.Description) + copy(dAtA[i:], m.Description) + i = encodeVarintGov(dAtA, i, uint64(len(m.Description))) + i-- + dAtA[i] = 0x12 + } + if len(m.Title) > 0 { + i -= len(m.Title) + copy(dAtA[i:], m.Title) + i = encodeVarintGov(dAtA, i, uint64(len(m.Title))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintGov(dAtA []byte, offset int, v uint64) int { + offset -= sovGov(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *ResumeFinalityProposal) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Title) + if l > 0 { + n += 1 + l + sovGov(uint64(l)) + } + l = len(m.Description) + if l > 0 { + n += 1 + l + sovGov(uint64(l)) + } + if len(m.FpPks) > 0 { + for _, e := range m.FpPks { + l = e.Size() + n += 1 + l + sovGov(uint64(l)) + } + } + if m.HaltingHeight != 0 { + n += 1 + sovGov(uint64(m.HaltingHeight)) + } + return n +} + +func sovGov(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozGov(x uint64) (n int) { + return sovGov(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *ResumeFinalityProposal) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ResumeFinalityProposal: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ResumeFinalityProposal: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Title", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGov + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGov + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Title = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Description", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGov + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGov + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Description = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field FpPks", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthGov + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthGov + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var v github_com_babylonlabs_io_babylon_types.BIP340PubKey + m.FpPks = append(m.FpPks, v) + if err := m.FpPks[len(m.FpPks)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field HaltingHeight", wireType) + } + m.HaltingHeight = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGov + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.HaltingHeight |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipGov(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGov + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipGov(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGov + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGov + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGov + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthGov + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupGov + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthGov + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthGov = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowGov = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupGov = fmt.Errorf("proto: unexpected end of group") +) From 88d6aec6535a87ceb3f34645fe68c7c58da37ef2 Mon Sep 17 00:00:00 2001 From: Fangyu Gai Date: Tue, 29 Oct 2024 22:43:19 +0800 Subject: [PATCH 2/5] add tests --- x/btcstaking/keeper/genesis.go | 3 +- x/finality/gov.go | 2 +- x/finality/keeper/gov.go | 74 ++++++++++++++++++++++- x/finality/keeper/gov_test.go | 106 +++++++++++++++++++++++++++++++++ x/finality/keeper/tallying.go | 7 ++- 5 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 x/finality/keeper/gov_test.go diff --git a/x/btcstaking/keeper/genesis.go b/x/btcstaking/keeper/genesis.go index faebc7ee2..df60c060a 100644 --- a/x/btcstaking/keeper/genesis.go +++ b/x/btcstaking/keeper/genesis.go @@ -5,9 +5,10 @@ import ( "fmt" "math" + sdk "github.com/cosmos/cosmos-sdk/types" + bbn "github.com/babylonlabs-io/babylon/types" "github.com/babylonlabs-io/babylon/x/btcstaking/types" - sdk "github.com/cosmos/cosmos-sdk/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/finality/gov.go b/x/finality/gov.go index 43dc13bc1..e753e548d 100644 --- a/x/finality/gov.go +++ b/x/finality/gov.go @@ -25,5 +25,5 @@ func NewResumeFinalityProposalHandler(k keeper.Keeper) govtypesv1.Handler { // handleResumeFinalityProposal is a handler for jail finality provider proposals func handleResumeFinalityProposal(ctx sdk.Context, k keeper.Keeper, p *types.ResumeFinalityProposal) error { - return k.JailFinalityProvidersFromHeight(ctx, p.FpPks, p.HaltingHeight) + return k.HandleResumeFinalityProposal(ctx, p) } diff --git a/x/finality/keeper/gov.go b/x/finality/keeper/gov.go index 34fc3e106..734c9fc77 100644 --- a/x/finality/keeper/gov.go +++ b/x/finality/keeper/gov.go @@ -1,11 +1,81 @@ package keeper import ( + "errors" + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" - bbntypes "github.com/babylonlabs-io/babylon/types" + bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" + "github.com/babylonlabs-io/babylon/x/finality/types" ) -func (k Keeper) JailFinalityProvidersFromHeight(ctx sdk.Context, fps []bbntypes.BIP340PubKey, height uint32) error { +// HandleResumeFinalityProposal handles the resume finality proposal in the following steps: +// 1. check the validity of the proposal +// 2. jail the finality providers from the list and adjust the voting power cache from the +// halting height to the current height +// 3. tally blocks to ensure finality is resumed +func (k Keeper) HandleResumeFinalityProposal(ctx sdk.Context, p *types.ResumeFinalityProposal) error { + // a valid proposal should be + // 1. the halting height along with some parameterized future heights should be indeed non-finalized + // 2. all the fps from the proposal should have missed the vote for the halting height + // TODO introduce a parameter to define the finality has been halting for at least some heights + + params := k.GetParams(ctx) + currentHeight := ctx.HeaderInfo().Height + currentTime := ctx.HeaderInfo().Time + + // jail the given finality providers + for _, fpPk := range p.FpPks { + fpHex := fpPk.MarshalHex() + voters := k.GetVoters(ctx, uint64(p.HaltingHeight)) + _, voted := voters[fpPk.MarshalHex()] + if voted { + // all the given finality providers should not have voted for the halting height + return fmt.Errorf("the finality provider has voted for height %d", p.HaltingHeight) + } + + err := k.jailSluggishFinalityProvider(ctx, &fpPk) + if err != nil && !errors.Is(err, bstypes.ErrFpAlreadyJailed) { + return fmt.Errorf("failed to jail the finality provider: %w", err) + } + + // update signing info + signInfo, err := k.FinalityProviderSigningTracker.Get(ctx, fpPk.MustMarshal()) + if err != nil { + return fmt.Errorf("the signing info is not created: %w", err) + } + signInfo.JailedUntil = currentTime.Add(params.JailDuration) + signInfo.MissedBlocksCounter = 0 + if err := k.DeleteMissedBlockBitmap(ctx, &fpPk); err != nil { + return fmt.Errorf("failed to remove the missed block bit map: %w", err) + } + err = k.FinalityProviderSigningTracker.Set(ctx, fpPk.MustMarshal(), signInfo) + k.Logger(ctx).Info( + "finality provider is jailed", + "height", p.HaltingHeight, + "public_key", fpHex, + ) + } + + // set the all the given finality providers voting power to 0 + for h := uint64(p.HaltingHeight); h <= uint64(currentHeight); h++ { + distCache := k.GetVotingPowerDistCache(ctx, h) + activeFps := distCache.GetActiveFinalityProviderSet() + for _, fpToJail := range p.FpPks { + if fp, exists := activeFps[fpToJail.MarshalHex()]; exists { + fp.IsJailed = true + k.SetVotingPower(ctx, fpToJail, h, 0) + } + } + + distCache.ApplyActiveFinalityProviders(params.MaxActiveFinalityProviders) + + // set the voting power distribution cache of the current height + k.SetVotingPowerDistCache(ctx, h, distCache) + } + + k.TallyBlocks(ctx) + return nil } diff --git a/x/finality/keeper/gov_test.go b/x/finality/keeper/gov_test.go new file mode 100644 index 000000000..0cb471c2a --- /dev/null +++ b/x/finality/keeper/gov_test.go @@ -0,0 +1,106 @@ +package keeper_test + +import ( + "math/rand" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "github.com/babylonlabs-io/babylon/testutil/datagen" + keepertest "github.com/babylonlabs-io/babylon/testutil/keeper" + bbntypes "github.com/babylonlabs-io/babylon/types" + "github.com/babylonlabs-io/babylon/x/finality/keeper" + "github.com/babylonlabs-io/babylon/x/finality/types" +) + +func TestHandleResumeFinalityProposal(t *testing.T) { + r := rand.New(rand.NewSource(time.Now().Unix())) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + bsKeeper := types.NewMockBTCStakingKeeper(ctrl) + iKeeper := types.NewMockIncentiveKeeper(ctrl) + cKeeper := types.NewMockCheckpointingKeeper(ctrl) + fKeeper, ctx := keepertest.FinalityKeeper(t, bsKeeper, iKeeper, cKeeper) + + haltingHeight := uint64(100) + currentHeight := uint64(110) + + activeFpNum := 3 + activeFpPks := generateNFpPks(t, r, activeFpNum) + setupActiveFps(t, activeFpPks, haltingHeight, fKeeper, ctx) + // set voting power table for each height, only the first fp votes + votedFpPk := activeFpPks[0] + for h := haltingHeight; h <= currentHeight; h++ { + fKeeper.SetBlock(ctx, &types.IndexedBlock{ + Height: h, + AppHash: datagen.GenRandomByteArray(r, 32), + Finalized: false, + }) + dc := types.NewVotingPowerDistCache() + for i := 0; i < activeFpNum; i++ { + fKeeper.SetVotingPower(ctx, activeFpPks[i].MustMarshal(), h, 1) + dc.AddFinalityProviderDistInfo(&types.FinalityProviderDistInfo{ + BtcPk: &activeFpPks[i], + TotalBondedSat: 1, + IsTimestamped: true, + }) + } + dc.ApplyActiveFinalityProviders(uint32(activeFpNum)) + votedSig, err := bbntypes.NewSchnorrEOTSSig(datagen.GenRandomByteArray(r, 32)) + require.NoError(t, err) + fKeeper.SetSig(ctx, h, &votedFpPk, votedSig) + fKeeper.SetVotingPowerDistCache(ctx, h, dc) + } + + // tally blocks and none of them should be finalised + iKeeper.EXPECT().RewardBTCStaking(gomock.Any(), gomock.Any(), gomock.Any()).Return().AnyTimes() + ctx = datagen.WithCtxHeight(ctx, currentHeight) + fKeeper.TallyBlocks(ctx) + for i := haltingHeight; i < currentHeight; i++ { + ib, err := fKeeper.GetBlock(ctx, i) + require.NoError(t, err) + require.False(t, ib.Finalized) + } + + // create a resume finality proposal to jail the last fp + bsKeeper.EXPECT().JailFinalityProvider(ctx, gomock.Any()).Return(nil).AnyTimes() + resumeProposal := &types.ResumeFinalityProposal{ + FpPks: activeFpPks[1:], + HaltingHeight: uint32(haltingHeight), + } + err := fKeeper.HandleResumeFinalityProposal(ctx, resumeProposal) + require.NoError(t, err) + + for i := haltingHeight; i < currentHeight; i++ { + ib, err := fKeeper.GetBlock(ctx, i) + require.NoError(t, err) + require.True(t, ib.Finalized) + } +} + +func generateNFpPks(t *testing.T, r *rand.Rand, n int) []bbntypes.BIP340PubKey { + fpPks := make([]bbntypes.BIP340PubKey, 0) + for i := 0; i < n; i++ { + fpPk, err := datagen.GenRandomBIP340PubKey(r) + require.NoError(t, err) + fpPks = append(fpPks, *fpPk) + } + + return fpPks +} + +func setupActiveFps(t *testing.T, fpPks []bbntypes.BIP340PubKey, height uint64, fKeeper *keeper.Keeper, ctx sdk.Context) { + for _, fpPk := range fpPks { + signingInfo := types.NewFinalityProviderSigningInfo( + &fpPk, + int64(height), + 0, + ) + err := fKeeper.FinalityProviderSigningTracker.Set(ctx, fpPk, signingInfo) + require.NoError(t, err) + } +} diff --git a/x/finality/keeper/tallying.go b/x/finality/keeper/tallying.go index 53051d29d..3e8bc4661 100644 --- a/x/finality/keeper/tallying.go +++ b/x/finality/keeper/tallying.go @@ -109,7 +109,12 @@ func tally(fpSet map[string]uint64, voterBTCPKs map[string]struct{}) bool { votedPower += power } } - return votedPower*3 > totalPower*2 + + if votedPower*3 > totalPower*2 { + return true + } + + return false } // setNextHeightToFinalize sets the next height to finalise as the given height From 5d37132cebc3bbea4a1ca335cb8c32f4c392c1b9 Mon Sep 17 00:00:00 2001 From: Fangyu Gai Date: Thu, 31 Oct 2024 17:55:12 +0800 Subject: [PATCH 3/5] add cli --- app/app.go | 2 + client/parsers.go | 70 ++++++++++++++++++++ x/finality/client/cli/gov.go | 93 +++++++++++++++++++++++++++ x/finality/client/proposal_handler.go | 11 ++++ 4 files changed, 176 insertions(+) create mode 100644 client/parsers.go create mode 100644 x/finality/client/cli/gov.go create mode 100644 x/finality/client/proposal_handler.go diff --git a/app/app.go b/app/app.go index e22ac0155..a72b77d0c 100644 --- a/app/app.go +++ b/app/app.go @@ -94,6 +94,7 @@ import ( "github.com/babylonlabs-io/babylon/app/ante" "github.com/babylonlabs-io/babylon/app/upgrades" bbn "github.com/babylonlabs-io/babylon/types" + finalityclient "github.com/babylonlabs-io/babylon/x/finality/client" appkeepers "github.com/babylonlabs-io/babylon/app/keepers" appparams "github.com/babylonlabs-io/babylon/app/params" @@ -327,6 +328,7 @@ func NewBabylonApp( govtypes.ModuleName: gov.NewAppModuleBasic( []govclient.ProposalHandler{ paramsclient.ProposalHandler, + finalityclient.ResumeFinalityHandler, }, ), }) diff --git a/client/parsers.go b/client/parsers.go new file mode 100644 index 000000000..8cb205850 --- /dev/null +++ b/client/parsers.go @@ -0,0 +1,70 @@ +package client + +import ( + "github.com/cosmos/cosmos-sdk/client" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + "github.com/spf13/cobra" +) + +// adapted from +// https://github.com/osmosis-labs/osmosis/blob/2e85d1ee3e15e3f74898395d37b455af48649268/osmoutils/osmocli/parsers.go + +var DefaultGovAuthority = sdk.AccAddress(address.Module("gov")) + +const ( + FlagIsExpedited = "is-expedited" + FlagAuthority = "authority" +) + +func GetProposalInfo(cmd *cobra.Command) (client.Context, string, string, sdk.Coins, bool, sdk.AccAddress, error) { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return client.Context{}, "", "", nil, false, nil, err + } + + proposalTitle, err := cmd.Flags().GetString(cli.FlagTitle) + if err != nil { + return clientCtx, proposalTitle, "", nil, false, nil, err + } + + summary, err := cmd.Flags().GetString(cli.FlagSummary) + if err != nil { + return client.Context{}, proposalTitle, summary, nil, false, nil, err + } + + depositArg, err := cmd.Flags().GetString(cli.FlagDeposit) + if err != nil { + return client.Context{}, proposalTitle, summary, nil, false, nil, err + } + + deposit, err := sdk.ParseCoinsNormalized(depositArg) + if err != nil { + return client.Context{}, proposalTitle, summary, deposit, false, nil, err + } + + isExpedited, err := cmd.Flags().GetBool(FlagIsExpedited) + if err != nil { + return client.Context{}, proposalTitle, summary, deposit, false, nil, err + } + + authorityString, err := cmd.Flags().GetString(FlagAuthority) + if err != nil { + return client.Context{}, proposalTitle, summary, deposit, false, nil, err + } + authority, err := sdk.AccAddressFromBech32(authorityString) + if err != nil { + return client.Context{}, proposalTitle, summary, deposit, false, nil, err + } + + return clientCtx, proposalTitle, summary, deposit, isExpedited, authority, nil +} + +func AddCommonProposalFlags(cmd *cobra.Command) { + cmd.Flags().String(cli.FlagTitle, "", "Title of proposal") + cmd.Flags().String(cli.FlagSummary, "", "Summary of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "Deposit of proposal") + cmd.Flags().Bool(FlagIsExpedited, false, "Whether the proposal is expedited") + cmd.Flags().String(FlagAuthority, DefaultGovAuthority.String(), "The address of the governance account. Default is the sdk gov module account") +} diff --git a/x/finality/client/cli/gov.go b/x/finality/client/cli/gov.go new file mode 100644 index 000000000..7362d29b5 --- /dev/null +++ b/x/finality/client/cli/gov.go @@ -0,0 +1,93 @@ +package cli + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + + tmjson "github.com/cometbft/cometbft/libs/json" + "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/spf13/cobra" + + bbncli "github.com/babylonlabs-io/babylon/client" + bbntypes "github.com/babylonlabs-io/babylon/types" + "github.com/babylonlabs-io/babylon/x/finality/types" +) + +type FinalityProviderPks struct { + FinalityProviders []string `json:"finality-providers"` +} + +func NewCmdSubmitResumeFinalityProposal() *cobra.Command { + cmd := &cobra.Command{ + Use: "resume-finality [fps-to-jail.json] [halting-height]", + Args: cobra.ExactArgs(2), + Short: "Submit a resume finality proposal", + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, proposalTitle, summary, deposit, isExpedited, authority, err := bbncli.GetProposalInfo(cmd) + if err != nil { + return err + } + + fps, err := loadFpsFromFile(args[0]) + if err != nil { + return fmt.Errorf("cannot load finality providers from %s: %w", args[0], err) + } + + haltingHeight, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid halting-height %s: %w", args[1], err) + } + + content := types.NewResumeFinalityProposal(proposalTitle, summary, fps, uint32(haltingHeight)) + + contentMsg, err := v1.NewLegacyContent(content, authority.String()) + if err != nil { + return err + } + + msg := v1.NewMsgExecLegacyContent(contentMsg.Content, authority.String()) + + proposalMsg, err := v1.NewMsgSubmitProposal([]sdk.Msg{msg}, deposit, clientCtx.GetFromAddress().String(), "", proposalTitle, summary, isExpedited) + if err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), proposalMsg) + }, + } + + bbncli.AddCommonProposalFlags(cmd) + + return cmd +} + +func loadFpsFromFile(filePath string) ([]bbntypes.BIP340PubKey, error) { + bz, err := os.ReadFile(filepath.Clean(filePath)) + if err != nil { + return nil, err + } + fps := new(FinalityProviderPks) + err = tmjson.Unmarshal(bz, fps) + if err != nil { + return nil, err + } + + if len(fps.FinalityProviders) == 0 { + return nil, fmt.Errorf("empty finality providers") + } + + fpPks := make([]bbntypes.BIP340PubKey, len(fps.FinalityProviders)) + for i, pkStr := range fps.FinalityProviders { + pk, err := bbntypes.NewBIP340PubKeyFromHex(pkStr) + if err != nil { + return nil, fmt.Errorf("invalid finality provider public key %s: %w", pkStr, err) + } + fpPks[i] = *pk + } + + return fpPks, nil +} diff --git a/x/finality/client/proposal_handler.go b/x/finality/client/proposal_handler.go new file mode 100644 index 000000000..700d86320 --- /dev/null +++ b/x/finality/client/proposal_handler.go @@ -0,0 +1,11 @@ +package client + +import ( + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + + "github.com/babylonlabs-io/babylon/x/finality/client/cli" +) + +var ( + ResumeFinalityHandler = govclient.NewProposalHandler(cli.NewCmdSubmitResumeFinalityProposal) +) From 05b7ca73e06c5c14a46d7e573038e056650d3a4c Mon Sep 17 00:00:00 2001 From: Fangyu Gai Date: Thu, 31 Oct 2024 18:13:28 +0800 Subject: [PATCH 4/5] fix lint --- x/finality/keeper/gov.go | 14 +++++++++----- x/finality/keeper/tallying.go | 6 +----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/x/finality/keeper/gov.go b/x/finality/keeper/gov.go index 734c9fc77..417b43491 100644 --- a/x/finality/keeper/gov.go +++ b/x/finality/keeper/gov.go @@ -32,27 +32,31 @@ func (k Keeper) HandleResumeFinalityProposal(ctx sdk.Context, p *types.ResumeFin _, voted := voters[fpPk.MarshalHex()] if voted { // all the given finality providers should not have voted for the halting height - return fmt.Errorf("the finality provider has voted for height %d", p.HaltingHeight) + return fmt.Errorf("the finality provider %s has voted for height %d", fpHex, p.HaltingHeight) } err := k.jailSluggishFinalityProvider(ctx, &fpPk) if err != nil && !errors.Is(err, bstypes.ErrFpAlreadyJailed) { - return fmt.Errorf("failed to jail the finality provider: %w", err) + return fmt.Errorf("failed to jail the finality provider %s: %w", fpHex, err) } // update signing info signInfo, err := k.FinalityProviderSigningTracker.Get(ctx, fpPk.MustMarshal()) if err != nil { - return fmt.Errorf("the signing info is not created: %w", err) + return fmt.Errorf("the signing info of finality provider %s is not created: %w", fpHex, err) } signInfo.JailedUntil = currentTime.Add(params.JailDuration) signInfo.MissedBlocksCounter = 0 if err := k.DeleteMissedBlockBitmap(ctx, &fpPk); err != nil { - return fmt.Errorf("failed to remove the missed block bit map: %w", err) + return fmt.Errorf("failed to remove the missed block bit map for finality provider %s: %w", fpHex, err) } err = k.FinalityProviderSigningTracker.Set(ctx, fpPk.MustMarshal(), signInfo) + if err != nil { + return fmt.Errorf("failed to set the signing info for finality provider %s: %w", fpHex, err) + } + k.Logger(ctx).Info( - "finality provider is jailed", + "finality provider is jailed in the proposal", "height", p.HaltingHeight, "public_key", fpHex, ) diff --git a/x/finality/keeper/tallying.go b/x/finality/keeper/tallying.go index 3e8bc4661..d5cb60d09 100644 --- a/x/finality/keeper/tallying.go +++ b/x/finality/keeper/tallying.go @@ -110,11 +110,7 @@ func tally(fpSet map[string]uint64, voterBTCPKs map[string]struct{}) bool { } } - if votedPower*3 > totalPower*2 { - return true - } - - return false + return votedPower*3 > totalPower*2 } // setNextHeightToFinalize sets the next height to finalise as the given height From b3cf34a31ebdec6f35280929e5ceadb9e4281d99 Mon Sep 17 00:00:00 2001 From: Fangyu Gai Date: Thu, 31 Oct 2024 18:31:46 +0800 Subject: [PATCH 5/5] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57950f0f8..c7d6242f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## Unreleased +### Improvements + +* [#242](https://github.com/babylonlabs-io/babylon/pull/242) Add ResumeFinalityProposal and handler + ## v0.15.0 ### State Machine Breaking