From 6c085c3952b4888150461c8ded3739f735573a59 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Tue, 16 Jun 2026 10:17:52 +0200 Subject: [PATCH 1/7] fix: use attester consensus keys for light blocks --- modules/network/keeper/grpc_query.go | 31 + modules/network/types/query.pb.go | 709 ++++++++++++++++++-- modules/network/types/query.pb.gw.go | 65 ++ modules/proto/evabci/network/v1/query.proto | 19 + pkg/rpc/core/attester_light.go | 104 +++ pkg/rpc/core/blocks.go | 68 +- pkg/rpc/core/blocks_test.go | 168 +++++ pkg/rpc/core/consensus.go | 145 +--- pkg/rpc/core/status.go | 10 + pkg/rpc/core/utils.go | 10 + server/attester_cmd.go | 12 +- server/attester_cmd_test.go | 86 ++- tests/integration/gm_gaia_health_test.go | 69 +- 13 files changed, 1215 insertions(+), 281 deletions(-) create mode 100644 pkg/rpc/core/attester_light.go diff --git a/modules/network/keeper/grpc_query.go b/modules/network/keeper/grpc_query.go index 3c9f91e4..c0b807b9 100644 --- a/modules/network/keeper/grpc_query.go +++ b/modules/network/keeper/grpc_query.go @@ -257,3 +257,34 @@ func (q *queryServer) AttesterInfo(c context.Context, req *types.QueryAttesterIn AttesterInfo: attesterInfo, }, nil } + +// Attesters queries the registered attester set. +func (q *queryServer) Attesters(c context.Context, req *types.QueryAttestersRequest) (*types.QueryAttestersResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + + ctx := sdk.UnwrapSDKContext(c) + + attesterAddresses, err := q.keeper.GetAllAttesters(ctx) + if err != nil { + return nil, fmt.Errorf("get all attesters: %w", err) + } + + attesters := make([]*types.RegisteredAttester, 0, len(attesterAddresses)) + for _, address := range attesterAddresses { + attesterInfo, err := q.keeper.GetAttesterInfo(ctx, address) + if err != nil { + return nil, fmt.Errorf("get attester info for %s: %w", address, err) + } + + attesters = append(attesters, &types.RegisteredAttester{ + ValidatorAddress: address, + AttesterInfo: attesterInfo, + }) + } + + return &types.QueryAttestersResponse{ + Attesters: attesters, + }, nil +} diff --git a/modules/network/types/query.pb.go b/modules/network/types/query.pb.go index 3b389163..ac67472f 100644 --- a/modules/network/types/query.pb.go +++ b/modules/network/types/query.pb.go @@ -852,6 +852,141 @@ func (m *QueryAttesterInfoResponse) GetAttesterInfo() *AttesterInfo { return nil } +// QueryAttestersRequest is the request type for the Query/Attesters RPC method. +type QueryAttestersRequest struct { +} + +func (m *QueryAttestersRequest) Reset() { *m = QueryAttestersRequest{} } +func (m *QueryAttestersRequest) String() string { return proto.CompactTextString(m) } +func (*QueryAttestersRequest) ProtoMessage() {} +func (*QueryAttestersRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_faab6bfc228a74e1, []int{17} +} +func (m *QueryAttestersRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryAttestersRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryAttestersRequest.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 *QueryAttestersRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryAttestersRequest.Merge(m, src) +} +func (m *QueryAttestersRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryAttestersRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryAttestersRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryAttestersRequest proto.InternalMessageInfo + +// RegisteredAttester contains the attester consensus address and metadata. +type RegisteredAttester struct { + ValidatorAddress string `protobuf:"bytes,1,opt,name=validator_address,json=validatorAddress,proto3" json:"validator_address,omitempty"` + AttesterInfo *AttesterInfo `protobuf:"bytes,2,opt,name=attester_info,json=attesterInfo,proto3" json:"attester_info,omitempty"` +} + +func (m *RegisteredAttester) Reset() { *m = RegisteredAttester{} } +func (m *RegisteredAttester) String() string { return proto.CompactTextString(m) } +func (*RegisteredAttester) ProtoMessage() {} +func (*RegisteredAttester) Descriptor() ([]byte, []int) { + return fileDescriptor_faab6bfc228a74e1, []int{18} +} +func (m *RegisteredAttester) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *RegisteredAttester) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_RegisteredAttester.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 *RegisteredAttester) XXX_Merge(src proto.Message) { + xxx_messageInfo_RegisteredAttester.Merge(m, src) +} +func (m *RegisteredAttester) XXX_Size() int { + return m.Size() +} +func (m *RegisteredAttester) XXX_DiscardUnknown() { + xxx_messageInfo_RegisteredAttester.DiscardUnknown(m) +} + +var xxx_messageInfo_RegisteredAttester proto.InternalMessageInfo + +func (m *RegisteredAttester) GetValidatorAddress() string { + if m != nil { + return m.ValidatorAddress + } + return "" +} + +func (m *RegisteredAttester) GetAttesterInfo() *AttesterInfo { + if m != nil { + return m.AttesterInfo + } + return nil +} + +// QueryAttestersResponse is the response type for the Query/Attesters RPC method. +type QueryAttestersResponse struct { + Attesters []*RegisteredAttester `protobuf:"bytes,1,rep,name=attesters,proto3" json:"attesters,omitempty"` +} + +func (m *QueryAttestersResponse) Reset() { *m = QueryAttestersResponse{} } +func (m *QueryAttestersResponse) String() string { return proto.CompactTextString(m) } +func (*QueryAttestersResponse) ProtoMessage() {} +func (*QueryAttestersResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_faab6bfc228a74e1, []int{19} +} +func (m *QueryAttestersResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryAttestersResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryAttestersResponse.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 *QueryAttestersResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryAttestersResponse.Merge(m, src) +} +func (m *QueryAttestersResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryAttestersResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryAttestersResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryAttestersResponse proto.InternalMessageInfo + +func (m *QueryAttestersResponse) GetAttesters() []*RegisteredAttester { + if m != nil { + return m.Attesters + } + return nil +} + func init() { proto.RegisterType((*QueryParamsRequest)(nil), "evabci.network.v1.QueryParamsRequest") proto.RegisterType((*QueryParamsResponse)(nil), "evabci.network.v1.QueryParamsResponse") @@ -870,77 +1005,86 @@ func init() { proto.RegisterType((*QueryLastAttestedHeightResponse)(nil), "evabci.network.v1.QueryLastAttestedHeightResponse") proto.RegisterType((*QueryAttesterInfoRequest)(nil), "evabci.network.v1.QueryAttesterInfoRequest") proto.RegisterType((*QueryAttesterInfoResponse)(nil), "evabci.network.v1.QueryAttesterInfoResponse") + proto.RegisterType((*QueryAttestersRequest)(nil), "evabci.network.v1.QueryAttestersRequest") + proto.RegisterType((*RegisteredAttester)(nil), "evabci.network.v1.RegisteredAttester") + proto.RegisterType((*QueryAttestersResponse)(nil), "evabci.network.v1.QueryAttestersResponse") } func init() { proto.RegisterFile("evabci/network/v1/query.proto", fileDescriptor_faab6bfc228a74e1) } var fileDescriptor_faab6bfc228a74e1 = []byte{ - // 1040 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xcf, 0x6f, 0x1b, 0x45, - 0x14, 0xce, 0x36, 0x89, 0x21, 0x2f, 0xa1, 0x6d, 0xa6, 0x21, 0x24, 0x6e, 0xe3, 0x38, 0x0b, 0xb4, - 0x6e, 0x8b, 0x3d, 0x75, 0x50, 0x1b, 0x2a, 0xf5, 0xd2, 0xf0, 0xb3, 0x08, 0xa1, 0xb2, 0x91, 0x7a, - 0xe0, 0x80, 0x35, 0xf6, 0x8e, 0x37, 0xab, 0xda, 0x3b, 0x9b, 0x9d, 0xb1, 0x4b, 0x89, 0x72, 0xe1, - 0x08, 0x17, 0x24, 0xfe, 0x00, 0x04, 0x37, 0x04, 0x7f, 0x02, 0xe2, 0xdc, 0x63, 0x25, 0x2e, 0x9c, - 0x10, 0x4a, 0xf8, 0x43, 0xd0, 0xbe, 0x19, 0xaf, 0xd7, 0xf1, 0xae, 0x9d, 0x9c, 0x92, 0x7d, 0xef, - 0xfb, 0xde, 0xfb, 0xe6, 0xcd, 0xcc, 0x37, 0x86, 0x0d, 0xde, 0x67, 0xcd, 0x96, 0x4f, 0x03, 0xae, - 0x9e, 0x89, 0xe8, 0x29, 0xed, 0xd7, 0xe9, 0x41, 0x8f, 0x47, 0xcf, 0x6b, 0x61, 0x24, 0x94, 0x20, - 0xcb, 0x3a, 0x5d, 0x33, 0xe9, 0x5a, 0xbf, 0x5e, 0x5c, 0xf1, 0x84, 0x27, 0x30, 0x4b, 0xe3, 0xff, - 0x34, 0xb0, 0x78, 0xcd, 0x13, 0xc2, 0xeb, 0x70, 0xca, 0x42, 0x9f, 0xb2, 0x20, 0x10, 0x8a, 0x29, - 0x5f, 0x04, 0xd2, 0x64, 0x6f, 0xb5, 0x84, 0xec, 0x0a, 0x49, 0x9b, 0x4c, 0x72, 0x5d, 0x9f, 0xf6, - 0xeb, 0x4d, 0xae, 0x58, 0x9d, 0x86, 0xcc, 0xf3, 0x03, 0x04, 0x1b, 0x6c, 0x86, 0x22, 0xf5, 0x3c, - 0xe4, 0x83, 0x52, 0xe5, 0xf1, 0x34, 0x53, 0x8a, 0x4b, 0xc5, 0x23, 0x8d, 0xb0, 0x57, 0x80, 0x7c, - 0x11, 0xb7, 0x78, 0xcc, 0x22, 0xd6, 0x95, 0x0e, 0x3f, 0xe8, 0x71, 0xa9, 0xec, 0xcf, 0xe1, 0xca, - 0x48, 0x54, 0x86, 0x22, 0x90, 0x9c, 0xec, 0x40, 0x21, 0xc4, 0xc8, 0x9a, 0x55, 0xb6, 0x2a, 0x8b, - 0xdb, 0xeb, 0xb5, 0xb1, 0x15, 0xd7, 0x34, 0x65, 0x77, 0xee, 0xc5, 0x3f, 0x9b, 0x33, 0x8e, 0x81, - 0xdb, 0x3b, 0xb0, 0x81, 0xf5, 0x1e, 0x62, 0x73, 0x5c, 0xc0, 0xae, 0xaf, 0xba, 0x2c, 0x34, 0x0d, - 0xc9, 0x2a, 0x14, 0xf6, 0xb9, 0xef, 0xed, 0x2b, 0xac, 0x3c, 0xeb, 0x98, 0x2f, 0xfb, 0x2b, 0x28, - 0xe5, 0x11, 0x8d, 0xa6, 0x07, 0x50, 0x68, 0x62, 0xc4, 0x68, 0x7a, 0x2b, 0x43, 0xd3, 0x38, 0xdb, - 0x70, 0xec, 0x2a, 0xbc, 0x8e, 0xf5, 0x3f, 0x0c, 0x45, 0x6b, 0xff, 0x51, 0xd0, 0x16, 0x03, 0x41, - 0x2b, 0x30, 0xcf, 0xe3, 0x18, 0x56, 0x9d, 0x73, 0xf4, 0x87, 0xfd, 0xfd, 0x05, 0x58, 0x3d, 0x8d, - 0x37, 0x3a, 0x32, 0x09, 0x64, 0x0b, 0x96, 0xa4, 0x62, 0x91, 0x6a, 0x98, 0xd5, 0x5d, 0xc0, 0xd5, - 0x2d, 0x62, 0xec, 0x13, 0x0c, 0x91, 0x0d, 0x00, 0x1e, 0xb8, 0x03, 0xc0, 0x2c, 0x02, 0x16, 0x78, - 0xe0, 0x9a, 0x74, 0x1d, 0x56, 0x42, 0x16, 0x29, 0xbf, 0xe5, 0x87, 0xb8, 0x80, 0x86, 0x59, 0xed, - 0x5c, 0xd9, 0xaa, 0x2c, 0x39, 0x57, 0x46, 0x72, 0x7a, 0x71, 0xe4, 0x36, 0x2c, 0xb3, 0x96, 0xf2, - 0xfb, 0xbc, 0xd1, 0x67, 0x1d, 0xdf, 0x65, 0x4a, 0x44, 0x72, 0x6d, 0x1e, 0x65, 0x5d, 0xd6, 0x89, - 0x27, 0x49, 0x9c, 0xdc, 0x87, 0xb5, 0x54, 0x8d, 0xc0, 0x4b, 0x73, 0x0a, 0xc8, 0x79, 0x63, 0x24, - 0x3f, 0xa4, 0xda, 0xf7, 0xa0, 0x88, 0xc3, 0x48, 0x42, 0x8f, 0x02, 0x97, 0x7f, 0x3d, 0x98, 0xe0, - 0x1a, 0xbc, 0xc2, 0x5c, 0x37, 0xe2, 0x52, 0x9f, 0x96, 0x05, 0x67, 0xf0, 0x69, 0x3f, 0x81, 0xab, - 0x99, 0xbc, 0xe4, 0x94, 0xcd, 0xfb, 0x71, 0xc0, 0x6c, 0xe8, 0x56, 0xc6, 0x86, 0x9e, 0x62, 0x6a, - 0xbc, 0xfd, 0x00, 0x6c, 0xac, 0xbb, 0x27, 0xda, 0xea, 0x7d, 0x11, 0xb4, 0xfd, 0xa8, 0x8b, 0x63, - 0xd9, 0x53, 0x4c, 0xf5, 0xe4, 0xb4, 0xa3, 0xf6, 0x87, 0x05, 0x6f, 0x4e, 0xa4, 0x1b, 0x79, 0xb7, - 0x60, 0xd9, 0x97, 0x0d, 0x29, 0xda, 0xaa, 0xd1, 0xd2, 0x28, 0xee, 0x62, 0xa9, 0x57, 0x9d, 0x4b, - 0xbe, 0x4c, 0x91, 0xb9, 0x4b, 0x36, 0x61, 0xb1, 0x2f, 0x14, 0x77, 0x1b, 0xa1, 0x78, 0xc6, 0x23, - 0xdc, 0xfd, 0x39, 0x07, 0x30, 0xf4, 0x38, 0x8e, 0xc4, 0x00, 0x25, 0x14, 0xeb, 0x18, 0xc0, 0xac, - 0x06, 0x60, 0x48, 0x03, 0x6e, 0xc0, 0xa5, 0x83, 0x9e, 0x88, 0x7a, 0xdd, 0x46, 0x3b, 0x8a, 0xf7, - 0x4e, 0x04, 0xb8, 0xf3, 0x0b, 0xce, 0x45, 0x1d, 0xfe, 0xc8, 0x44, 0xed, 0xf7, 0x46, 0x6e, 0x0a, - 0x8f, 0xf6, 0x7c, 0x2f, 0x60, 0xaa, 0x17, 0x71, 0x39, 0xfd, 0x8e, 0x2d, 0x8f, 0x91, 0xe2, 0x33, - 0x94, 0x1c, 0x84, 0xc6, 0xe8, 0x3e, 0x5e, 0x4e, 0x12, 0x0f, 0x75, 0x9c, 0x5c, 0x83, 0x05, 0x39, - 0x60, 0xe2, 0x22, 0x97, 0x9c, 0x61, 0xc0, 0xf6, 0x60, 0x33, 0x57, 0x99, 0x99, 0xe9, 0x07, 0x00, - 0x09, 0x3e, 0x6e, 0x33, 0x3b, 0xf1, 0x22, 0xa7, 0x4a, 0x38, 0x29, 0x9e, 0x5d, 0x36, 0x23, 0xf8, - 0x8c, 0x49, 0x65, 0x90, 0xe6, 0x16, 0x0d, 0x7c, 0xed, 0xbe, 0x91, 0x92, 0x85, 0x30, 0x52, 0xf2, - 0xa6, 0xf4, 0x31, 0xac, 0x8d, 0xac, 0x22, 0x6d, 0x16, 0xe7, 0x19, 0x96, 0xcd, 0x60, 0x3d, 0xa3, - 0x50, 0x32, 0x88, 0xd7, 0x06, 0x06, 0xdd, 0xf0, 0x83, 0xb6, 0x30, 0x77, 0x60, 0x73, 0xc2, 0x2c, - 0x90, 0xbf, 0xc4, 0x52, 0x5f, 0xdb, 0x3f, 0x03, 0xcc, 0x63, 0x0f, 0xf2, 0x0d, 0x14, 0xb4, 0x21, - 0x93, 0xb7, 0x33, 0x4a, 0x8c, 0x3b, 0x7f, 0xf1, 0xfa, 0x34, 0x98, 0x16, 0x6a, 0x6f, 0x7d, 0xfb, - 0xd7, 0x7f, 0x3f, 0x5e, 0xb8, 0x4a, 0xd6, 0xe9, 0xf8, 0x13, 0xa3, 0x4d, 0x9f, 0xfc, 0x6a, 0x0d, - 0x0e, 0x56, 0xda, 0x9c, 0xee, 0xe4, 0x35, 0xc8, 0x7b, 0x1b, 0x8a, 0xf5, 0x73, 0x30, 0x8c, 0x3a, - 0x8a, 0xea, 0x6e, 0x92, 0x1b, 0x34, 0xef, 0x01, 0x44, 0x16, 0x3d, 0xd4, 0x9b, 0x7b, 0x44, 0xbe, - 0xb3, 0x60, 0x21, 0xf1, 0x74, 0x52, 0xc9, 0xeb, 0x78, 0xfa, 0x99, 0x28, 0xde, 0x3c, 0x03, 0xd2, - 0x68, 0xaa, 0xa0, 0x26, 0x9b, 0x94, 0x33, 0x34, 0xe1, 0x63, 0x41, 0x0f, 0xf1, 0xcf, 0x11, 0xf9, - 0xc9, 0x82, 0x8b, 0xa3, 0x0e, 0x47, 0xaa, 0x79, 0x7d, 0x32, 0xbd, 0xb7, 0x58, 0x3b, 0x2b, 0xdc, - 0x68, 0xab, 0xa1, 0xb6, 0x0a, 0xb9, 0x9e, 0xa1, 0x2d, 0x39, 0xc0, 0xf4, 0xd0, 0x1c, 0xed, 0x23, - 0xf2, 0xa7, 0x05, 0xab, 0xd9, 0x36, 0x49, 0xee, 0xe6, 0xb5, 0x9e, 0xe8, 0xca, 0xc5, 0x7b, 0xe7, - 0xa5, 0x19, 0xe5, 0x77, 0x51, 0x39, 0x25, 0xd5, 0x0c, 0xe5, 0xb1, 0x47, 0x57, 0x5b, 0x29, 0xee, - 0x70, 0xbf, 0x7f, 0xb3, 0x80, 0x8c, 0xfb, 0x11, 0x99, 0x72, 0xd4, 0x32, 0x5c, 0xb5, 0xb8, 0x7d, - 0x1e, 0xca, 0x19, 0xc6, 0x3d, 0xf4, 0xb3, 0xa1, 0xda, 0xdf, 0x2d, 0x20, 0xe3, 0x96, 0x95, 0xaf, - 0x36, 0xd7, 0x00, 0xf3, 0xd5, 0xe6, 0x3b, 0xe2, 0xc4, 0xcb, 0xd4, 0x61, 0x52, 0x55, 0x8d, 0xf7, - 0xb8, 0x55, 0xad, 0x97, 0xfc, 0x62, 0xc1, 0x52, 0xda, 0x9d, 0xc8, 0xed, 0x69, 0x33, 0x4a, 0x5f, - 0xa9, 0x77, 0xce, 0x06, 0x36, 0xe2, 0x76, 0x50, 0x5c, 0x9d, 0x50, 0x9a, 0xff, 0x53, 0x97, 0x1e, - 0x8e, 0xb9, 0xf3, 0xd1, 0xee, 0xa7, 0x2f, 0x8e, 0x4b, 0xd6, 0xcb, 0xe3, 0x92, 0xf5, 0xef, 0x71, - 0xc9, 0xfa, 0xe1, 0xa4, 0x34, 0xf3, 0xf2, 0xa4, 0x34, 0xf3, 0xf7, 0x49, 0x69, 0xe6, 0xcb, 0x3b, - 0x9e, 0xaf, 0xf6, 0x7b, 0xcd, 0x5a, 0x4b, 0x74, 0x29, 0xef, 0x4b, 0xc5, 0x5a, 0x4f, 0x29, 0xef, - 0x57, 0xb1, 0x7a, 0x57, 0xb8, 0xbd, 0x0e, 0x97, 0x49, 0x17, 0xfc, 0xb1, 0xdd, 0x2c, 0xe0, 0x6f, - 0xe9, 0x77, 0xff, 0x0f, 0x00, 0x00, 0xff, 0xff, 0x3d, 0xe0, 0x17, 0xeb, 0x20, 0x0c, 0x00, 0x00, + // 1121 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x57, 0xcd, 0x6e, 0x1c, 0x45, + 0x10, 0xf6, 0xf8, 0x67, 0x61, 0xcb, 0x26, 0x89, 0x3b, 0xc6, 0xb1, 0x37, 0xf6, 0xda, 0x1e, 0x42, + 0xe2, 0x24, 0xec, 0x76, 0xd6, 0x28, 0x31, 0x91, 0x72, 0x89, 0xc3, 0x5f, 0x10, 0x42, 0x61, 0x2c, + 0xe5, 0x80, 0x04, 0xab, 0xde, 0x9d, 0xde, 0xf1, 0x28, 0xde, 0xe9, 0xf1, 0x74, 0xef, 0x86, 0x60, + 0xf9, 0xc2, 0x01, 0x24, 0xb8, 0x20, 0xf1, 0x00, 0x48, 0xdc, 0xf8, 0x79, 0x04, 0xc4, 0x39, 0xc7, + 0x48, 0x5c, 0x38, 0x21, 0x64, 0xf3, 0x20, 0x68, 0x6a, 0x7a, 0x66, 0x67, 0x3d, 0x33, 0x6b, 0x3b, + 0xa7, 0x64, 0xaa, 0xbe, 0xaf, 0xfa, 0xab, 0xea, 0xae, 0x2a, 0x2f, 0x2c, 0xf3, 0x3e, 0x6b, 0xb5, + 0x5d, 0xea, 0x71, 0xf5, 0x54, 0x04, 0x4f, 0x68, 0xbf, 0x41, 0xf7, 0x7a, 0x3c, 0x78, 0x56, 0xf7, + 0x03, 0xa1, 0x04, 0x99, 0x8d, 0xdc, 0x75, 0xed, 0xae, 0xf7, 0x1b, 0x95, 0x39, 0x47, 0x38, 0x02, + 0xbd, 0x34, 0xfc, 0x5f, 0x04, 0xac, 0x2c, 0x39, 0x42, 0x38, 0xbb, 0x9c, 0x32, 0xdf, 0xa5, 0xcc, + 0xf3, 0x84, 0x62, 0xca, 0x15, 0x9e, 0xd4, 0xde, 0x1b, 0x6d, 0x21, 0xbb, 0x42, 0xd2, 0x16, 0x93, + 0x3c, 0x8a, 0x4f, 0xfb, 0x8d, 0x16, 0x57, 0xac, 0x41, 0x7d, 0xe6, 0xb8, 0x1e, 0x82, 0x35, 0x36, + 0x47, 0x91, 0x7a, 0xe6, 0xf3, 0x38, 0xd4, 0x6a, 0xd6, 0xcd, 0x94, 0xe2, 0x52, 0xf1, 0x20, 0x42, + 0x98, 0x73, 0x40, 0x3e, 0x0d, 0x8f, 0x78, 0xc4, 0x02, 0xd6, 0x95, 0x16, 0xdf, 0xeb, 0x71, 0xa9, + 0xcc, 0x4f, 0xe0, 0xe2, 0x90, 0x55, 0xfa, 0xc2, 0x93, 0x9c, 0x6c, 0x42, 0xc9, 0x47, 0xcb, 0x82, + 0xb1, 0x6a, 0xac, 0x4f, 0x6f, 0x2c, 0xd6, 0x33, 0x19, 0xd7, 0x23, 0xca, 0xd6, 0xe4, 0xf3, 0x7f, + 0x56, 0xc6, 0x2c, 0x0d, 0x37, 0x37, 0x61, 0x19, 0xe3, 0xdd, 0xc7, 0xc3, 0x31, 0x81, 0x2d, 0x57, + 0x75, 0x99, 0xaf, 0x0f, 0x24, 0xf3, 0x50, 0xda, 0xe1, 0xae, 0xb3, 0xa3, 0x30, 0xf2, 0x84, 0xa5, + 0xbf, 0xcc, 0x2f, 0xa0, 0x5a, 0x44, 0xd4, 0x9a, 0xee, 0x41, 0xa9, 0x85, 0x16, 0xad, 0xe9, 0x4a, + 0x8e, 0xa6, 0x2c, 0x5b, 0x73, 0xcc, 0x1a, 0xbc, 0x8e, 0xf1, 0xdf, 0xf3, 0x45, 0x7b, 0xe7, 0xa1, + 0xd7, 0x11, 0xb1, 0xa0, 0x39, 0x98, 0xe2, 0xa1, 0x0d, 0xa3, 0x4e, 0x5a, 0xd1, 0x87, 0xf9, 0xfd, + 0x38, 0xcc, 0x1f, 0xc7, 0x6b, 0x1d, 0xb9, 0x04, 0xb2, 0x06, 0x33, 0x52, 0xb1, 0x40, 0x35, 0x75, + 0x76, 0xe3, 0x98, 0xdd, 0x34, 0xda, 0x3e, 0x44, 0x13, 0x59, 0x06, 0xe0, 0x9e, 0x1d, 0x03, 0x26, + 0x10, 0x50, 0xe6, 0x9e, 0xad, 0xdd, 0x0d, 0x98, 0xf3, 0x59, 0xa0, 0xdc, 0xb6, 0xeb, 0x63, 0x02, + 0x4d, 0x9d, 0xed, 0xe4, 0xaa, 0xb1, 0x3e, 0x63, 0x5d, 0x1c, 0xf2, 0x45, 0xc9, 0x91, 0x9b, 0x30, + 0xcb, 0xda, 0xca, 0xed, 0xf3, 0x66, 0x9f, 0xed, 0xba, 0x36, 0x53, 0x22, 0x90, 0x0b, 0x53, 0x28, + 0xeb, 0x42, 0xe4, 0x78, 0x9c, 0xd8, 0xc9, 0x5d, 0x58, 0x48, 0xc5, 0xf0, 0x9c, 0x34, 0xa7, 0x84, + 0x9c, 0x4b, 0x43, 0xfe, 0x01, 0xd5, 0xbc, 0x03, 0x15, 0x2c, 0x46, 0x62, 0x7a, 0xe8, 0xd9, 0xfc, + 0xcb, 0xb8, 0x82, 0x0b, 0xf0, 0x0a, 0xb3, 0xed, 0x80, 0xcb, 0xe8, 0xb5, 0x94, 0xad, 0xf8, 0xd3, + 0x7c, 0x0c, 0x97, 0x73, 0x79, 0xc9, 0x2b, 0x9b, 0x72, 0x43, 0x83, 0xbe, 0xd0, 0xb5, 0x9c, 0x0b, + 0x3d, 0xc6, 0x8c, 0xf0, 0xe6, 0x3d, 0x30, 0x31, 0xee, 0xb6, 0xe8, 0xa8, 0x07, 0xc2, 0xeb, 0xb8, + 0x41, 0x17, 0xcb, 0xb2, 0xad, 0x98, 0xea, 0xc9, 0x93, 0x9e, 0xda, 0x1f, 0x06, 0xbc, 0x31, 0x92, + 0xae, 0xe5, 0xdd, 0x80, 0x59, 0x57, 0x36, 0xa5, 0xe8, 0xa8, 0x66, 0x3b, 0x42, 0x71, 0x1b, 0x43, + 0xbd, 0x6a, 0x9d, 0x77, 0x65, 0x8a, 0xcc, 0x6d, 0xb2, 0x02, 0xd3, 0x7d, 0xa1, 0xb8, 0xdd, 0xf4, + 0xc5, 0x53, 0x1e, 0xe0, 0xed, 0x4f, 0x5a, 0x80, 0xa6, 0x47, 0xa1, 0x25, 0x04, 0x28, 0xa1, 0xd8, + 0xae, 0x06, 0x4c, 0x44, 0x00, 0x34, 0x45, 0x80, 0x6b, 0x70, 0x7e, 0xaf, 0x27, 0x82, 0x5e, 0xb7, + 0xd9, 0x09, 0xc2, 0xbb, 0x13, 0x1e, 0xde, 0x7c, 0xd9, 0x3a, 0x17, 0x99, 0xdf, 0xd7, 0x56, 0xf3, + 0x9d, 0xa1, 0x4e, 0xe1, 0xc1, 0xb6, 0xeb, 0x78, 0x4c, 0xf5, 0x02, 0x2e, 0x4f, 0xee, 0xb1, 0xd9, + 0x0c, 0x29, 0x7c, 0x43, 0xc9, 0x43, 0x68, 0x0e, 0xdf, 0xe3, 0x85, 0xc4, 0x71, 0x3f, 0xb2, 0x93, + 0x25, 0x28, 0xcb, 0x98, 0x89, 0x49, 0xce, 0x58, 0x03, 0x83, 0xe9, 0xc0, 0x4a, 0xa1, 0x32, 0x5d, + 0xd3, 0x77, 0x01, 0x12, 0x7c, 0x78, 0xcc, 0xc4, 0xc8, 0x46, 0x4e, 0x85, 0xb0, 0x52, 0x3c, 0x73, + 0x55, 0x97, 0xe0, 0x63, 0x26, 0x95, 0x46, 0xea, 0x2e, 0x8a, 0xe7, 0xda, 0x5d, 0x2d, 0x25, 0x0f, + 0xa1, 0xa5, 0x14, 0x55, 0xe9, 0x03, 0x58, 0x18, 0xca, 0x22, 0x3d, 0x2c, 0xce, 0x52, 0x2c, 0x93, + 0xc1, 0x62, 0x4e, 0xa0, 0xa4, 0x10, 0xaf, 0xc5, 0x03, 0xba, 0xe9, 0x7a, 0x1d, 0xa1, 0x7b, 0x60, + 0x65, 0x44, 0x2d, 0x90, 0x3f, 0xc3, 0x52, 0x5f, 0xe6, 0x25, 0x3d, 0xd5, 0x62, 0x48, 0x32, 0xd7, + 0xbf, 0x35, 0x80, 0x58, 0xdc, 0x71, 0x43, 0x23, 0xb7, 0x63, 0xf7, 0xd9, 0x2e, 0x3b, 0x23, 0x71, + 0xfc, 0x65, 0x24, 0x7e, 0xae, 0x07, 0x69, 0x4a, 0xa2, 0x2e, 0xc1, 0x03, 0x28, 0xc7, 0xc8, 0xf8, + 0x29, 0xbc, 0x99, 0x13, 0x3b, 0x9b, 0x86, 0x35, 0xe0, 0x6d, 0xfc, 0x3a, 0x0d, 0x53, 0x18, 0x9f, + 0x7c, 0x05, 0xa5, 0x68, 0x25, 0x91, 0xbc, 0x28, 0xd9, 0xdd, 0x57, 0xb9, 0x7a, 0x12, 0x2c, 0xd2, + 0x69, 0xae, 0x7d, 0xfd, 0xd7, 0x7f, 0x3f, 0x8e, 0x5f, 0x26, 0x8b, 0x34, 0xbb, 0x64, 0xa3, 0xb5, + 0x47, 0x7e, 0x31, 0xe2, 0xd6, 0x4a, 0x8f, 0xe7, 0x5b, 0x45, 0x07, 0x14, 0x6d, 0xc7, 0x4a, 0xe3, + 0x0c, 0x0c, 0xad, 0x8e, 0xa2, 0xba, 0xeb, 0xe4, 0x1a, 0x2d, 0xfa, 0x13, 0x00, 0x59, 0x74, 0x3f, + 0x7a, 0xde, 0x07, 0xe4, 0x3b, 0x03, 0xca, 0xc9, 0x56, 0x23, 0xeb, 0x45, 0x27, 0x1e, 0x5f, 0x94, + 0x95, 0xeb, 0xa7, 0x40, 0x6a, 0x4d, 0xeb, 0xa8, 0xc9, 0x24, 0xab, 0x39, 0x9a, 0x70, 0x5d, 0xd2, + 0x7d, 0xfc, 0xe7, 0x80, 0xfc, 0x64, 0xc0, 0xb9, 0xe1, 0x19, 0x4f, 0x6a, 0x45, 0xe7, 0xe4, 0x6e, + 0x9f, 0x4a, 0xfd, 0xb4, 0x70, 0xad, 0xad, 0x8e, 0xda, 0xd6, 0xc9, 0xd5, 0x1c, 0x6d, 0x49, 0x0b, + 0xd0, 0x7d, 0xdd, 0x1c, 0x07, 0xe4, 0x4f, 0x03, 0xe6, 0xf3, 0x17, 0x05, 0xb9, 0x5d, 0x74, 0xf4, + 0xc8, 0xbd, 0x54, 0xb9, 0x73, 0x56, 0x9a, 0x56, 0x7e, 0x1b, 0x95, 0x53, 0x52, 0xcb, 0x51, 0x1e, + 0x6e, 0xa9, 0x5a, 0x3b, 0xc5, 0x1d, 0xdc, 0xf7, 0x6f, 0x06, 0x90, 0xec, 0x44, 0x26, 0x27, 0x3c, + 0xb5, 0x9c, 0xbd, 0x52, 0xd9, 0x38, 0x0b, 0xe5, 0x14, 0xe5, 0x1e, 0x4c, 0xf4, 0x81, 0xda, 0xdf, + 0x0d, 0x20, 0xd9, 0xa1, 0x5d, 0xac, 0xb6, 0x70, 0x05, 0x14, 0xab, 0x2d, 0xde, 0x09, 0x23, 0x9b, + 0x69, 0x97, 0x49, 0x55, 0xd3, 0x83, 0xc7, 0xae, 0x45, 0x7a, 0xc9, 0xcf, 0x06, 0xcc, 0xa4, 0x87, + 0x1f, 0xb9, 0x79, 0x52, 0x8d, 0xd2, 0x2d, 0xf5, 0xd6, 0xe9, 0xc0, 0x5a, 0xdc, 0x26, 0x8a, 0x6b, + 0x10, 0x4a, 0x8b, 0xff, 0xd8, 0xa7, 0xfb, 0x99, 0xf9, 0x7e, 0x40, 0xbe, 0x31, 0xa0, 0x9c, 0x8c, + 0xdf, 0xe2, 0x8e, 0x3f, 0xbe, 0x44, 0x8a, 0x3b, 0x3e, 0x33, 0xcb, 0xcd, 0x2b, 0xa8, 0xad, 0x4a, + 0x96, 0x46, 0x68, 0x93, 0x5b, 0x1f, 0x3d, 0x3f, 0xac, 0x1a, 0x2f, 0x0e, 0xab, 0xc6, 0xbf, 0x87, + 0x55, 0xe3, 0x87, 0xa3, 0xea, 0xd8, 0x8b, 0xa3, 0xea, 0xd8, 0xdf, 0x47, 0xd5, 0xb1, 0xcf, 0x6e, + 0x39, 0xae, 0xda, 0xe9, 0xb5, 0xea, 0x6d, 0xd1, 0xa5, 0xbc, 0x2f, 0x15, 0x6b, 0x3f, 0xa1, 0xbc, + 0x5f, 0xc3, 0x50, 0x5d, 0x61, 0xf7, 0x76, 0xb9, 0x4c, 0x42, 0xe2, 0xef, 0x9e, 0x56, 0x09, 0x7f, + 0xd6, 0xbc, 0xfd, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xde, 0x08, 0x2f, 0xcf, 0xab, 0x0d, 0x00, + 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -971,6 +1115,8 @@ type QueryClient interface { LastAttestedHeight(ctx context.Context, in *QueryLastAttestedHeightRequest, opts ...grpc.CallOption) (*QueryLastAttestedHeightResponse, error) // AttesterInfo queries the attester information including public key AttesterInfo(ctx context.Context, in *QueryAttesterInfoRequest, opts ...grpc.CallOption) (*QueryAttesterInfoResponse, error) + // Attesters queries the registered attester set. + Attesters(ctx context.Context, in *QueryAttestersRequest, opts ...grpc.CallOption) (*QueryAttestersResponse, error) } type queryClient struct { @@ -1053,6 +1199,15 @@ func (c *queryClient) AttesterInfo(ctx context.Context, in *QueryAttesterInfoReq return out, nil } +func (c *queryClient) Attesters(ctx context.Context, in *QueryAttestersRequest, opts ...grpc.CallOption) (*QueryAttestersResponse, error) { + out := new(QueryAttestersResponse) + err := c.cc.Invoke(ctx, "/evabci.network.v1.Query/Attesters", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // QueryServer is the server API for Query service. type QueryServer interface { // Params queries the module parameters @@ -1071,6 +1226,8 @@ type QueryServer interface { LastAttestedHeight(context.Context, *QueryLastAttestedHeightRequest) (*QueryLastAttestedHeightResponse, error) // AttesterInfo queries the attester information including public key AttesterInfo(context.Context, *QueryAttesterInfoRequest) (*QueryAttesterInfoResponse, error) + // Attesters queries the registered attester set. + Attesters(context.Context, *QueryAttestersRequest) (*QueryAttestersResponse, error) } // UnimplementedQueryServer can be embedded to have forward compatible implementations. @@ -1101,6 +1258,9 @@ func (*UnimplementedQueryServer) LastAttestedHeight(ctx context.Context, req *Qu func (*UnimplementedQueryServer) AttesterInfo(ctx context.Context, req *QueryAttesterInfoRequest) (*QueryAttesterInfoResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AttesterInfo not implemented") } +func (*UnimplementedQueryServer) Attesters(ctx context.Context, req *QueryAttestersRequest) (*QueryAttestersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Attesters not implemented") +} func RegisterQueryServer(s grpc1.Server, srv QueryServer) { s.RegisterService(&_Query_serviceDesc, srv) @@ -1250,6 +1410,24 @@ func _Query_AttesterInfo_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _Query_Attesters_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryAttestersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).Attesters(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/evabci.network.v1.Query/Attesters", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).Attesters(ctx, req.(*QueryAttestersRequest)) + } + return interceptor(ctx, in, info, handler) +} + var Query_serviceDesc = _Query_serviceDesc var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "evabci.network.v1.Query", @@ -1287,6 +1465,10 @@ var _Query_serviceDesc = grpc.ServiceDesc{ MethodName: "AttesterInfo", Handler: _Query_AttesterInfo_Handler, }, + { + MethodName: "Attesters", + Handler: _Query_Attesters_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "evabci/network/v1/query.proto", @@ -1855,6 +2037,108 @@ func (m *QueryAttesterInfoResponse) MarshalToSizedBuffer(dAtA []byte) (int, erro return len(dAtA) - i, nil } +func (m *QueryAttestersRequest) 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 *QueryAttestersRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryAttestersRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func (m *RegisteredAttester) 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 *RegisteredAttester) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *RegisteredAttester) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.AttesterInfo != nil { + { + size, err := m.AttesterInfo.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if len(m.ValidatorAddress) > 0 { + i -= len(m.ValidatorAddress) + copy(dAtA[i:], m.ValidatorAddress) + i = encodeVarintQuery(dAtA, i, uint64(len(m.ValidatorAddress))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *QueryAttestersResponse) 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 *QueryAttestersResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryAttestersResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Attesters) > 0 { + for iNdEx := len(m.Attesters) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Attesters[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + func encodeVarintQuery(dAtA []byte, offset int, v uint64) int { offset -= sovQuery(v) base := offset @@ -2102,6 +2386,47 @@ func (m *QueryAttesterInfoResponse) Size() (n int) { return n } +func (m *QueryAttestersRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *RegisteredAttester) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.ValidatorAddress) + if l > 0 { + n += 1 + l + sovQuery(uint64(l)) + } + if m.AttesterInfo != nil { + l = m.AttesterInfo.Size() + n += 1 + l + sovQuery(uint64(l)) + } + return n +} + +func (m *QueryAttestersResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Attesters) > 0 { + for _, e := range m.Attesters { + l = e.Size() + n += 1 + l + sovQuery(uint64(l)) + } + } + return n +} + func sovQuery(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -3577,6 +3902,258 @@ func (m *QueryAttesterInfoResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *QueryAttestersRequest) 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 ErrIntOverflowQuery + } + 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: QueryAttestersRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryAttestersRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RegisteredAttester) 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 ErrIntOverflowQuery + } + 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: RegisteredAttester: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RegisteredAttester: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + 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 ErrInvalidLengthQuery + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ValidatorAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AttesterInfo", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.AttesterInfo == nil { + m.AttesterInfo = &AttesterInfo{} + } + if err := m.AttesterInfo.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryAttestersResponse) 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 ErrIntOverflowQuery + } + 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: QueryAttestersResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryAttestersResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Attesters", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Attesters = append(m.Attesters, &RegisteredAttester{}) + if err := m.Attesters[len(m.Attesters)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipQuery(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/modules/network/types/query.pb.gw.go b/modules/network/types/query.pb.gw.go index 1dc93cde..6bf492fb 100644 --- a/modules/network/types/query.pb.gw.go +++ b/modules/network/types/query.pb.gw.go @@ -393,6 +393,24 @@ func local_request_Query_AttesterInfo_0(ctx context.Context, marshaler runtime.M } +func request_Query_Attesters_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryAttestersRequest + var metadata runtime.ServerMetadata + + msg, err := client.Attesters(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Query_Attesters_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryAttestersRequest + var metadata runtime.ServerMetadata + + msg, err := server.Attesters(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterQueryHandlerServer registers the http handlers for service Query to "mux". // UnaryRPC :call QueryServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -583,6 +601,29 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv }) + mux.Handle("GET", pattern_Query_Attesters_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Query_Attesters_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_Attesters_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -784,6 +825,26 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) + mux.Handle("GET", pattern_Query_Attesters_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Query_Attesters_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_Attesters_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -803,6 +864,8 @@ var ( pattern_Query_LastAttestedHeight_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"evabci", "network", "v1", "last-attested-height"}, "", runtime.AssumeColonVerbOpt(false))) pattern_Query_AttesterInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"evabci", "network", "v1", "attester", "validator_address"}, "", runtime.AssumeColonVerbOpt(false))) + + pattern_Query_Attesters_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"evabci", "network", "v1", "attesters"}, "", runtime.AssumeColonVerbOpt(false))) ) var ( @@ -821,4 +884,6 @@ var ( forward_Query_LastAttestedHeight_0 = runtime.ForwardResponseMessage forward_Query_AttesterInfo_0 = runtime.ForwardResponseMessage + + forward_Query_Attesters_0 = runtime.ForwardResponseMessage ) diff --git a/modules/proto/evabci/network/v1/query.proto b/modules/proto/evabci/network/v1/query.proto index 366c39bb..bb780362 100644 --- a/modules/proto/evabci/network/v1/query.proto +++ b/modules/proto/evabci/network/v1/query.proto @@ -51,6 +51,11 @@ service Query { rpc AttesterInfo(QueryAttesterInfoRequest) returns (QueryAttesterInfoResponse) { option (google.api.http).get = "/evabci/network/v1/attester/{validator_address}"; } + + // Attesters queries the registered attester set. + rpc Attesters(QueryAttestersRequest) returns (QueryAttestersResponse) { + option (google.api.http).get = "/evabci/network/v1/attesters"; + } } // QueryParamsRequest is the request type for the Query/Params RPC method. @@ -143,3 +148,17 @@ message QueryAttesterInfoRequest { message QueryAttesterInfoResponse { AttesterInfo attester_info = 1; } + +// QueryAttestersRequest is the request type for the Query/Attesters RPC method. +message QueryAttestersRequest {} + +// RegisteredAttester contains the attester consensus address and metadata. +message RegisteredAttester { + string validator_address = 1; + AttesterInfo attester_info = 2; +} + +// QueryAttestersResponse is the response type for the Query/Attesters RPC method. +message QueryAttestersResponse { + repeated RegisteredAttester attesters = 1; +} diff --git a/pkg/rpc/core/attester_light.go b/pkg/rpc/core/attester_light.go new file mode 100644 index 00000000..92a6d527 --- /dev/null +++ b/pkg/rpc/core/attester_light.go @@ -0,0 +1,104 @@ +package core + +import ( + "bytes" + "context" + "errors" + "fmt" + + abci "github.com/cometbft/cometbft/abci/types" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/cosmos/cosmos-sdk/crypto/codec" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/gogoproto/proto" + + networktypes "github.com/evstack/ev-abci/modules/network/types" +) + +var errNoRegisteredAttesters = errors.New("attester mode: no registered attesters") + +func getAttesterValidatorSet(ctx context.Context) (*cmttypes.ValidatorSet, error) { + reqBytes, err := proto.Marshal(&networktypes.QueryAttestersRequest{}) + if err != nil { + return nil, fmt.Errorf("marshal attesters request: %w", err) + } + + result, err := env.Adapter.App.Query(ctx, &abci.RequestQuery{ + Path: "/evabci.network.v1.Query/Attesters", + Data: reqBytes, + }) + if err != nil { + return nil, fmt.Errorf("query attesters: %w", err) + } + if result.Code != 0 { + return nil, fmt.Errorf("attesters query failed: code %d, log: %s", result.Code, result.Log) + } + + var response networktypes.QueryAttestersResponse + if err := proto.Unmarshal(result.Value, &response); err != nil { + return nil, fmt.Errorf("unmarshal attesters response: %w", err) + } + if len(response.Attesters) == 0 { + return nil, errNoRegisteredAttesters + } + + validators := make([]*cmttypes.Validator, 0, len(response.Attesters)) + for _, attester := range response.Attesters { + validator, err := cometValidatorFromAttester(attester) + if err != nil { + return nil, err + } + validators = append(validators, validator) + } + + return cmttypes.NewValidatorSet(validators), nil +} + +func cometValidatorFromAttester(attester *networktypes.RegisteredAttester) (*cmttypes.Validator, error) { + if attester == nil || attester.AttesterInfo == nil { + return nil, fmt.Errorf("registered attester is missing attester info") + } + if attester.AttesterInfo.Pubkey == nil { + return nil, fmt.Errorf("registered attester %s is missing consensus pubkey", attester.ValidatorAddress) + } + + var actualPubKey cryptotypes.PubKey + if err := networktypes.ModuleCdc.InterfaceRegistry().UnpackAny(attester.AttesterInfo.Pubkey, &actualPubKey); err != nil { + return nil, fmt.Errorf("unpack attester pubkey for %s: %w", attester.ValidatorAddress, err) + } + + cmtPubKey, err := codec.ToCmtPubKeyInterface(actualPubKey) + if err != nil { + return nil, fmt.Errorf("convert attester pubkey for %s: %w", attester.ValidatorAddress, err) + } + + if attester.ValidatorAddress != "" { + expectedAddress, err := sdk.ValAddressFromBech32(attester.ValidatorAddress) + if err != nil { + return nil, fmt.Errorf("decode attester validator address %s: %w", attester.ValidatorAddress, err) + } + if !bytes.Equal(expectedAddress, cmtPubKey.Address().Bytes()) { + return nil, fmt.Errorf("attester %s pubkey derives consensus address %X", attester.ValidatorAddress, cmtPubKey.Address().Bytes()) + } + } + + return cmttypes.NewValidator(cmtPubKey, 1), nil +} + +func applyAttesterValidatorSetToHeader(ctx context.Context, header *cmttypes.Header) error { + validatorSet, err := getAttesterValidatorSet(ctx) + if err != nil { + return err + } + if validatorSet.Proposer == nil { + return fmt.Errorf("attester validator set has no proposer") + } + + validatorHash := validatorSet.Hash() + header.ValidatorsHash = validatorHash + header.NextValidatorsHash = validatorHash + header.ProposerAddress = validatorSet.Proposer.Address + + return nil +} diff --git a/pkg/rpc/core/blocks.go b/pkg/rpc/core/blocks.go index 73921e5d..2e6d86c4 100644 --- a/pkg/rpc/core/blocks.go +++ b/pkg/rpc/core/blocks.go @@ -15,7 +15,6 @@ import ( ctypes "github.com/cometbft/cometbft/rpc/core/types" rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" cmttypes "github.com/cometbft/cometbft/types" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/gogoproto/proto" storepkg "github.com/evstack/ev-node/pkg/store" @@ -147,6 +146,12 @@ func Block(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultBlock, error) Block: block, }, nil } + if env.AttesterMode { + return &ctypes.ResultBlock{ + BlockID: blockMeta.BlockID, + Block: block, + }, nil + } // Use the same BlockID that the sequencer uses from store // The sequencer uses store.GetBlockID() which has the original PartSetHeader @@ -226,7 +231,7 @@ func Commit(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultCommit, erro abciHeader := blockMeta.Header // get current commit (use attester signatures if in attester mode) - commit, err := getCommitForHeight(ctx.Context(), height) + commit, err := getCommitForHeight(ctx.Context(), height, &blockMeta.BlockID) if err != nil { return nil, fmt.Errorf("failed to get commit for height %d: %w", height, err) } @@ -336,7 +341,7 @@ func BlockchainInfo(ctx *rpctypes.Context, minHeight, maxHeight int64) (*ctypes. // getCommitForHeight returns commit info for a specific height, // using attester signatures if in attester mode, otherwise sequencer signatures -func getCommitForHeight(ctx context.Context, height uint64) (*cmttypes.Commit, error) { +func getCommitForHeight(ctx context.Context, height uint64, attesterBlockID *cmttypes.BlockID) (*cmttypes.Commit, error) { // Debug: Log attester mode status env.Logger.Info("getCommitForHeight called", "height", height, @@ -349,9 +354,24 @@ func getCommitForHeight(ctx context.Context, height uint64) (*cmttypes.Commit, e } // In attester mode, try to construct commit from attester signatures - blockID, err := env.Adapter.Store.GetBlockID(ctx, height) + var blockID cmttypes.BlockID + if attesterBlockID != nil { + blockID = *attesterBlockID + } else { + blockMeta, _ := getBlockMeta(ctx, height) + if blockMeta == nil { + return nil, fmt.Errorf("attester mode: block metadata not found for height %d", height) + } + blockID = blockMeta.BlockID + } + + validatorSet, err := getAttesterValidatorSet(ctx) if err != nil { - return nil, fmt.Errorf("get block ID for height %d: %w", height, err) + if errors.Is(err, errNoRegisteredAttesters) { + env.Logger.Info("No attesters registered; returning sequencer commit", "height", height) + return env.Adapter.GetLastCommit(ctx, height+1) + } + return nil, fmt.Errorf("attester mode: failed to get attester validator set: %w", err) } // Query attester signatures from the network module @@ -363,8 +383,7 @@ func getCommitForHeight(ctx context.Context, height uint64) (*cmttypes.Commit, e return nil, fmt.Errorf("attester mode: failed to get attester signatures for height %d: %w", height, err) } - // Build commit with attester signatures - commitSigs := make([]cmttypes.CommitSig, 0, len(signatures)) + signaturesByAddress := make(map[string]cmttypes.CommitSig, len(signatures)) for validatorAddr, signature := range signatures { // Parse the signature bytes (they should be marshaled cmtproto.Vote) var vote cmtproto.Vote @@ -373,33 +392,50 @@ func getCommitForHeight(ctx context.Context, height uint64) (*cmttypes.Commit, e "validator", validatorAddr, "error", err) continue } - - // Decode bech32 validator address to get 20-byte address - valAddrBytes, err := sdk.ValAddressFromBech32(validatorAddr) + voteBlockID, err := cmttypes.BlockIDFromProto(&vote.BlockID) if err != nil { - env.Logger.Error("failed to decode validator address", + env.Logger.Error("failed to decode attester vote block ID", "validator", validatorAddr, "error", err) continue } + if !voteBlockID.Equals(blockID) { + env.Logger.Error("attester vote signed a different block ID", + "validator", validatorAddr, + "expected", blockID.String(), + "got", voteBlockID.String()) + continue + } - commitSigs = append(commitSigs, cmttypes.CommitSig{ + validatorAddress := cmttypes.Address(vote.ValidatorAddress) + signaturesByAddress[string(validatorAddress)] = cmttypes.CommitSig{ BlockIDFlag: cmttypes.BlockIDFlagCommit, - ValidatorAddress: cmttypes.Address(valAddrBytes), + ValidatorAddress: validatorAddress, Timestamp: vote.Timestamp, Signature: vote.Signature, - }) + } } // If no valid attester signatures, return error instead of fallback - if len(commitSigs) == 0 { + if len(signaturesByAddress) == 0 { env.Logger.Error("no attester signatures found for block", "height", height) return nil, fmt.Errorf("attester mode: no attester signatures found for height %d - block not attested", height) } + commitSigs := make([]cmttypes.CommitSig, 0, len(validatorSet.Validators)) + for _, validator := range validatorSet.Validators { + if signature, ok := signaturesByAddress[string(validator.Address)]; ok { + commitSigs = append(commitSigs, signature) + continue + } + commitSigs = append(commitSigs, cmttypes.CommitSig{ + BlockIDFlag: cmttypes.BlockIDFlagAbsent, + }) + } + return &cmttypes.Commit{ Height: int64(height), Round: 0, - BlockID: *blockID, + BlockID: blockID, Signatures: commitSigs, }, nil } diff --git a/pkg/rpc/core/blocks_test.go b/pkg/rpc/core/blocks_test.go index 9059b851..7bb12083 100644 --- a/pkg/rpc/core/blocks_test.go +++ b/pkg/rpc/core/blocks_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + abci "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/crypto/ed25519" cmtlog "github.com/cometbft/cometbft/libs/log" "github.com/cometbft/cometbft/libs/math" @@ -13,6 +14,10 @@ import ( ctypes "github.com/cometbft/cometbft/rpc/core/types" rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" cmttypes "github.com/cometbft/cometbft/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/gogoproto/proto" ds "github.com/ipfs/go-datastore" "github.com/libp2p/go-libp2p/core/crypto" "github.com/stretchr/testify/mock" @@ -21,6 +26,7 @@ import ( rollkitmocks "github.com/evstack/ev-node/test/mocks" "github.com/evstack/ev-node/types" + networktypes "github.com/evstack/ev-abci/modules/network/types" "github.com/evstack/ev-abci/pkg/adapter" execstore "github.com/evstack/ev-abci/pkg/store" ) @@ -260,6 +266,82 @@ func TestCommit_VerifyCometBFTLightClientCompatibility_MultipleBlocks(t *testing } } +func TestCommit_AttesterModeUsesAttesterValidatorSetForHeader(t *testing.T) { + require := require.New(t) + + sequencerPrivKey := ed25519.GenPrivKey() + sequencerPubKey := sequencerPrivKey.PubKey() + sequencerLibP2PPrivKey, err := crypto.UnmarshalEd25519PrivateKey(sequencerPrivKey.Bytes()) + require.NoError(err) + sequencerLibP2PPubKey := sequencerLibP2PPrivKey.GetPublic() + sequencerAddress := sequencerPubKey.Address().Bytes()[:20] + + attesterPrivKey := ed25519.GenPrivKey() + attesterPubKey := attesterPrivKey.PubKey().(ed25519.PubKey) + attesterAddress := attesterPubKey.Address() + attesterValAddress := sdk.ValAddress(attesterAddress).String() + + mockSigner := &MockSigner{} + mockSigner.On("GetPublic").Return(sequencerLibP2PPubKey, nil) + mockSigner.On("GetAddress").Return(sequencerAddress, nil) + + env = setupTestEnvironment(mockSigner) + env.AttesterMode = true + defer func() { + env.AttesterMode = false + }() + + chainID := "test-chain" + blockHeight := uint64(1) + now := time.Now() + + sequencerValidatorHash, err := adapter.ValidatorHasherProvider()(sequencerAddress, sequencerLibP2PPubKey) + require.NoError(err) + blockData, rollkitHeader := createTestBlock(blockHeight, chainID, now, sequencerAddress, sequencerValidatorHash, 1) + + lastCommit, err := env.Adapter.GetLastCommit(context.Background(), blockHeight) + require.NoError(err) + abciHeader, err := adapter.ToABCIHeader(rollkitHeader, lastCommit) + require.NoError(err) + + attesterValidatorSet := cmttypes.NewValidatorSet([]*cmttypes.Validator{ + cmttypes.NewValidator(attesterPubKey, 1), + }) + abciHeader.ValidatorsHash = attesterValidatorSet.Hash() + abciHeader.NextValidatorsHash = attesterValidatorSet.Hash() + abciHeader.ProposerAddress = attesterValidatorSet.Proposer.Address + + abciBlock, err := adapter.ToABCIBlock(abciHeader, lastCommit, blockData) + require.NoError(err) + blockParts, err := abciBlock.MakePartSet(cmttypes.BlockPartSizeBytes) + require.NoError(err) + blockID := &cmttypes.BlockID{ + Hash: abciHeader.Hash(), + PartSetHeader: blockParts.Header(), + } + require.NoError(env.Adapter.Store.SaveBlockID(context.Background(), blockHeight, blockID)) + + voteBytes := signAttesterVote(t, chainID, blockHeight, rollkitHeader.Time(), blockID, attesterPrivKey) + mockAttesterNetworkQueries(t, env.Adapter.App.(*MockApp), attesterValAddress, attesterPubKey, voteBytes) + + mockBlock(blockHeight, rollkitHeader, blockData, make([]byte, 64), sequencerLibP2PPubKey, sequencerAddress) + + commitResult := callCommitRPC(t, blockHeight) + + require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.SignedHeader.Header.ValidatorsHash)) + require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.SignedHeader.Header.NextValidatorsHash)) + require.Equal(attesterAddress.Bytes(), []byte(commitResult.SignedHeader.Header.ProposerAddress)) + require.Equal(commitResult.SignedHeader.Header.Hash(), commitResult.SignedHeader.Commit.BlockID.Hash) + require.Len(commitResult.SignedHeader.Commit.Signatures, 1) + require.Equal(attesterAddress.Bytes(), []byte(commitResult.SignedHeader.Commit.Signatures[0].ValidatorAddress)) + require.NoError(attesterValidatorSet.VerifyCommitLight( + chainID, + commitResult.SignedHeader.Commit.BlockID, + int64(blockHeight), + commitResult.SignedHeader.Commit, + )) +} + func createTestBlock(height uint64, chainID string, baseTime time.Time, validatorAddress []byte, validatorHash []byte, offset int) (*types.Data, types.Header) { blockTime := uint64(baseTime.UnixNano() + int64(offset-1)*int64(time.Second)) @@ -288,6 +370,92 @@ func createTestBlock(height uint64, chainID string, baseTime time.Time, validato return blockData, rollkitHeader } +func signAttesterVote( + t *testing.T, + chainID string, + height uint64, + timestamp time.Time, + blockID *cmttypes.BlockID, + privKey ed25519.PrivKey, +) []byte { + t.Helper() + + vote := cmtproto.Vote{ + Type: cmtproto.PrecommitType, + Height: int64(height), + Round: 0, + BlockID: blockID.ToProto(), + Timestamp: timestamp, + ValidatorAddress: privKey.PubKey().Address(), + ValidatorIndex: 0, + } + signature, err := privKey.Sign(cmttypes.VoteSignBytes(chainID, &vote)) + require.NoError(t, err) + vote.Signature = signature + + voteBytes, err := proto.Marshal(&vote) + require.NoError(t, err) + return voteBytes +} + +func mockAttesterNetworkQueries( + t *testing.T, + mockApp *MockApp, + attesterValAddress string, + attesterPubKey ed25519.PubKey, + voteBytes []byte, +) { + t.Helper() + + sdkPubKey, err := cryptocodec.FromCmtPubKeyInterface(attesterPubKey) + require.NoError(t, err) + pubKeyAny, err := codectypes.NewAnyWithValue(sdkPubKey) + require.NoError(t, err) + + signaturesResponse, err := proto.Marshal(&networktypes.QueryAttesterSignaturesResponse{ + Signatures: []*networktypes.AttesterSignature{ + { + ValidatorAddress: attesterValAddress, + Signature: voteBytes, + }, + }, + }) + require.NoError(t, err) + + attesterInfo := &networktypes.AttesterInfo{ + Authority: "operator", + Pubkey: pubKeyAny, + JoinedHeight: 1, + } + + attesterInfoResponse, err := proto.Marshal(&networktypes.QueryAttesterInfoResponse{ + AttesterInfo: attesterInfo, + }) + require.NoError(t, err) + + attestersResponse, err := proto.Marshal(&networktypes.QueryAttestersResponse{ + Attesters: []*networktypes.RegisteredAttester{ + { + ValidatorAddress: attesterValAddress, + AttesterInfo: attesterInfo, + }, + }, + }) + require.NoError(t, err) + + mockApp.On("Query", mock.Anything, mock.MatchedBy(func(req *abci.RequestQuery) bool { + return req.Path == "/evabci.network.v1.Query/AttesterSignatures" + })).Return(&abci.ResponseQuery{Code: 0, Value: signaturesResponse}, nil).Maybe() + + mockApp.On("Query", mock.Anything, mock.MatchedBy(func(req *abci.RequestQuery) bool { + return req.Path == "/evabci.network.v1.Query/AttesterInfo" + })).Return(&abci.ResponseQuery{Code: 0, Value: attesterInfoResponse}, nil).Maybe() + + mockApp.On("Query", mock.Anything, mock.MatchedBy(func(req *abci.RequestQuery) bool { + return req.Path == "/evabci.network.v1.Query/Attesters" + })).Return(&abci.ResponseQuery{Code: 0, Value: attestersResponse}, nil).Maybe() +} + func signBlockWithBlockID(t *testing.T, header types.Header, privKey crypto.PrivKey, blockID *cmttypes.BlockID) []byte { // create vote bytes using the createVote helper from adapter vote := cmtproto.Vote{ diff --git a/pkg/rpc/core/consensus.go b/pkg/rpc/core/consensus.go index ff0ee28a..5bb68d9a 100644 --- a/pkg/rpc/core/consensus.go +++ b/pkg/rpc/core/consensus.go @@ -1,20 +1,12 @@ package core import ( - "context" + "errors" "fmt" - abci "github.com/cometbft/cometbft/abci/types" - cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" coretypes "github.com/cometbft/cometbft/rpc/core/types" rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" cmttypes "github.com/cometbft/cometbft/types" - "github.com/cosmos/cosmos-sdk/crypto/codec" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/gogoproto/proto" - - networktypes "github.com/evstack/ev-abci/modules/network/types" ) // Validators gets the validator set at the given block height. @@ -34,32 +26,19 @@ func Validators(ctx *rpctypes.Context, heightPtr *int64, _, _ *int) (*coretypes. if env.AttesterMode { env.Logger.Info("Validators endpoint in attester mode - returning active attesters", "height", height) - // Get attester signatures for this height to determine active attesters - signatures, err := getAttesterSignatures(ctx.Context(), int64(height)) - if err != nil { - env.Logger.Error("failed to get attester signatures", "height", height, "error", err) - // Fallback to genesis validator if no attester signatures available - return getGenesisValidatorSet(height) - } - - // If no attester signatures for this height, fallback to genesis validator - if len(signatures) == 0 { - env.Logger.Info("no attester signatures found for height, using genesis validator", "height", height) - return getGenesisValidatorSet(height) - } - - // Convert attester signatures to validator set - validators, err := buildValidatorSetFromAttesters(signatures, height) + validatorSet, err := getAttesterValidatorSet(ctx.Context()) if err != nil { - env.Logger.Error("failed to build validator set from attesters", "error", err) - return getGenesisValidatorSet(height) + if errors.Is(err, errNoRegisteredAttesters) { + return getGenesisValidatorSet(height) + } + return nil, fmt.Errorf("get attester validator set: %w", err) } return &coretypes.ResultValidators{ BlockHeight: int64(height), //nolint:gosec - Validators: validators, - Count: len(validators), - Total: len(validators), + Validators: validatorSet.Validators, + Count: len(validatorSet.Validators), + Total: len(validatorSet.Validators), }, nil } @@ -147,109 +126,3 @@ func ConsensusParams(ctx *rpctypes.Context, heightPtr *int64) (*coretypes.Result }, }, nil } - -// buildValidatorSetFromAttesters builds a CometBFT validator set from attester signatures -// using stored attester information including public keys -func buildValidatorSetFromAttesters(signatures map[string][]byte, height uint64) ([]*cmttypes.Validator, error) { - ctx := context.Background() - validators := make([]*cmttypes.Validator, 0, len(signatures)) - - for validatorAddr, signature := range signatures { - // Parse the signature bytes (they should be marshaled cmtproto.Vote) - var vote cmtproto.Vote - if err := proto.Unmarshal(signature, &vote); err != nil { - env.Logger.Error("failed to unmarshal attester vote", - "validator", validatorAddr, "error", err) - continue - } - - // Use the validator address from the vote for the consensus address - consensusAddr := cmttypes.Address(vote.ValidatorAddress) - if len(vote.ValidatorAddress) != 20 { - // Fallback: try to derive from the bech32 address - valAddrBytes, err := sdk.ValAddressFromBech32(validatorAddr) - if err != nil { - env.Logger.Error("failed to decode validator address", - "validator", validatorAddr, "error", err) - continue - } - // Use first 20 bytes as consensus address - consensusAddr = cmttypes.Address(valAddrBytes[:20]) - } - - // Query the network module for attester information via ABCI - attesterInfo, err := getAttesterInfoByAddress(ctx, validatorAddr) - if err != nil { - env.Logger.Error("failed to get attester info", - "validator", validatorAddr, "error", err) - continue - } - - // Unpack the Any type to get the actual public key using network module codec - var actualPubKey cryptotypes.PubKey - if err := networktypes.ModuleCdc.InterfaceRegistry().UnpackAny(attesterInfo.Pubkey, &actualPubKey); err != nil { - env.Logger.Error("failed to unpack public key from Any type", - "validator", validatorAddr, "error", err) - continue - } - - // Convert Cosmos SDK PubKey to CometBFT PubKey using standard codec - cmtPubKey, err := codec.ToCmtPubKeyInterface(actualPubKey) - if err != nil { - env.Logger.Error("failed to convert public key to CometBFT format", - "validator", validatorAddr, "error", err) - continue - } - - env.Logger.Info("creating validator entry for attester", - "validator", validatorAddr, "address", consensusAddr.String()) - - validators = append(validators, &cmttypes.Validator{ - Address: consensusAddr, - PubKey: cmtPubKey, - VotingPower: 1, // Set uniform voting power for attesters - ProposerPriority: 0, // Set to 0 for all attesters - }) - } - - if len(validators) == 0 { - return nil, fmt.Errorf("no valid attester validators found") - } - - env.Logger.Info("Built validator set from attesters", - "count", len(validators), "height", height) - - return validators, nil -} - -// getAttesterInfoByAddress queries the network module for attester information via ABCI -func getAttesterInfoByAddress(ctx context.Context, validatorAddr string) (*networktypes.AttesterInfo, error) { - // Create properly marshaled query request - queryReq := &networktypes.QueryAttesterInfoRequest{ - ValidatorAddress: validatorAddr, - } - - queryReqBytes, err := proto.Marshal(queryReq) - if err != nil { - return nil, fmt.Errorf("marshal query request: %w", err) - } - - result, err := env.Adapter.App.Query(ctx, &abci.RequestQuery{ - Path: "/evabci.network.v1.Query/AttesterInfo", - Data: queryReqBytes, // Properly marshaled protobuf request - }) - if err != nil { - return nil, fmt.Errorf("query attester info: %w", err) - } - - if result.Code != 0 { - return nil, fmt.Errorf("attester info not found: code %d, log: %s", result.Code, result.Log) - } - - var queryResp networktypes.QueryAttesterInfoResponse - if err := proto.Unmarshal(result.Value, &queryResp); err != nil { - return nil, fmt.Errorf("unmarshal attester info response: %w", err) - } - - return queryResp.AttesterInfo, nil -} diff --git a/pkg/rpc/core/status.go b/pkg/rpc/core/status.go index e630a1ad..cb2ed803 100644 --- a/pkg/rpc/core/status.go +++ b/pkg/rpc/core/status.go @@ -1,6 +1,7 @@ package core import ( + "errors" "fmt" "time" @@ -98,6 +99,15 @@ func Status(ctx *rpctypes.Context) (*ctypes.ResultStatus, error) { PubKey: genesisValidator.PubKey, VotingPower: int64(1), } + if env.AttesterMode { + attesterValidatorSet, err := getAttesterValidatorSet(unwrappedCtx) + if err != nil && !errors.Is(err, errNoRegisteredAttesters) { + return nil, fmt.Errorf("failed to get attester validator set: %w", err) + } + if err == nil && len(attesterValidatorSet.Validators) > 0 { + validator = *attesterValidatorSet.Validators[0] + } + } state, err := env.Adapter.RollkitStore.GetState(unwrappedCtx) if err != nil { diff --git a/pkg/rpc/core/utils.go b/pkg/rpc/core/utils.go index 6129ba7b..5e6886e7 100644 --- a/pkg/rpc/core/utils.go +++ b/pkg/rpc/core/utils.go @@ -56,6 +56,16 @@ func getBlockMeta(ctx context.Context, n uint64) (*cmttypes.BlockMeta, *cmttypes env.Logger.Error("Failed to convert header to ABCI format", "height", n, "err", err) return nil, nil } + if env.AttesterMode { + if err := applyAttesterValidatorSetToHeader(ctx, &abciHeader); err != nil { + if errors.Is(err, errNoRegisteredAttesters) { + env.Logger.Debug("No attesters registered; serving sequencer header", "height", n) + } else { + env.Logger.Error("Failed to apply attester validator set to header", "height", n, "err", err) + return nil, nil + } + } + } abciBlock, err := adapter.ToABCIBlock(abciHeader, lastCommit, data) if err != nil { diff --git a/server/attester_cmd.go b/server/attester_cmd.go index 7fcfc227..ff15d0e3 100644 --- a/server/attester_cmd.go +++ b/server/attester_cmd.go @@ -675,18 +675,12 @@ func submitAttestation( return fmt.Errorf("signing payload: %w", err) } - validatorAddr := pv.Key.Address - - fmt.Printf("šŸ” DEBUG ValidatorAddr used in vote: %X\n", validatorAddr) - fmt.Printf("šŸ” DEBUG pv.GetAddress(): %X\n", pv.GetAddress()) - fmt.Printf("šŸ” DEBUG pubKey.Address(): %X\n", pv.Key.PubKey.Address()) - attesterVote := &cmtproto.Vote{ Type: cmtproto.PrecommitType, - ValidatorAddress: validatorAddr, + ValidatorAddress: pv.Key.Address, Height: height, Round: 0, - BlockID: cmtproto.BlockID{Hash: header.Hash(), PartSetHeader: cmtproto.PartSetHeader{}}, + BlockID: blockID, Timestamp: header.Time(), Signature: signature, } @@ -806,7 +800,7 @@ func getEvolveHeader(node string, height int64) (*evolvetypes.Header, error) { } func getOriginalBlockID(ctx context.Context, node string, height int64) (cmtproto.BlockID, error) { - if height <= 1 { + if height <= 0 { return cmtproto.BlockID{}, nil } diff --git a/server/attester_cmd_test.go b/server/attester_cmd_test.go index 6bf0421b..f9fbaf7a 100644 --- a/server/attester_cmd_test.go +++ b/server/attester_cmd_test.go @@ -2,6 +2,8 @@ package server import ( "context" + "net/http" + "net/http/httptest" "testing" "github.com/stretchr/testify/assert" @@ -48,41 +50,53 @@ func TestPrivateKeyFromMnemonic(t *testing.T) { } func TestGetOriginalBlockID(t *testing.T) { - tests := []struct { - name string - height int64 - expectEmpty bool - }{ - { - name: "height 0", - height: 0, - expectEmpty: true, - }, - { - name: "height 1", - height: 1, - expectEmpty: true, - }, - { - name: "height 2 - requires RPC call", - height: 2, - expectEmpty: false, - }, - } + t.Run("height 0", func(t *testing.T) { + blockID, err := getOriginalBlockID(context.Background(), "tcp://localhost:26657", 0) + require.NoError(t, err) + assert.Empty(t, blockID.Hash) + assert.Empty(t, blockID.PartSetHeader.Hash) + assert.Equal(t, uint32(0), blockID.PartSetHeader.Total) + }) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.expectEmpty { - blockID, err := getOriginalBlockID(context.Background(), "tcp://localhost:26657", tt.height) - require.NoError(t, err) - assert.Empty(t, blockID.Hash) - assert.Empty(t, blockID.PartSetHeader.Hash) - assert.Equal(t, uint32(0), blockID.PartSetHeader.Total) - } else { - // For height > 1, we would need a running node to test - // This test would fail without a node, so we skip the actual call - t.Skip("Requires running node for integration test") - } - }) - } + t.Run("height 1 reads block ID from RPC", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/block", r.URL.Path) + require.Equal(t, "1", r.URL.Query().Get("height")) + _, _ = w.Write([]byte(`{ + "result": { + "block_id": { + "hash": "1111111111111111111111111111111111111111111111111111111111111111", + "parts": { + "hash": "2222222222222222222222222222222222222222222222222222222222222222", + "total": 3 + } + }, + "block": { + "header": { + "height": "1", + "time": "2026-06-16T00:00:00Z", + "chain_id": "test" + } + } + } + }`)) + })) + defer server.Close() + + blockID, err := getOriginalBlockID(context.Background(), server.URL, 1) + require.NoError(t, err) + assert.Equal(t, []byte{ + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + }, blockID.Hash) + assert.Equal(t, []byte{ + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + }, blockID.PartSetHeader.Hash) + assert.Equal(t, uint32(3), blockID.PartSetHeader.Total) + }) } diff --git a/tests/integration/gm_gaia_health_test.go b/tests/integration/gm_gaia_health_test.go index 243ab1e5..e490cf84 100644 --- a/tests/integration/gm_gaia_health_test.go +++ b/tests/integration/gm_gaia_health_test.go @@ -1,9 +1,12 @@ package integration_test import ( + "bytes" "context" "encoding/json" "fmt" + "os" + "path/filepath" "testing" "time" @@ -16,6 +19,7 @@ import ( "github.com/celestiaorg/tastora/framework/testutil/sdkacc" "github.com/celestiaorg/tastora/framework/testutil/wait" "github.com/celestiaorg/tastora/framework/types" + cmprivval "github.com/cometbft/cometbft/privval" "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module/testutil" @@ -29,6 +33,14 @@ import ( "github.com/stretchr/testify/require" ) +func TestNewDistinctAttesterPrivValidatorFilesUsesDifferentKey(t *testing.T) { + sequencerKeyJSON := []byte(`{"address":"sequencer"}`) + + attesterKeyJSON, _ := newDistinctAttesterPrivValidatorFiles(t, sequencerKeyJSON) + + require.False(t, bytes.Equal(sequencerKeyJSON, attesterKeyJSON)) +} + // TestAttesterSystem is an empty test case using the DockerIntegrationTestSuite func (s *DockerIntegrationTestSuite) TestAttesterSystem() { ctx, cancel := context.WithCancel(context.Background()) @@ -80,10 +92,10 @@ func (s *DockerIntegrationTestSuite) TestAttesterSystem() { s.Require().NotNil(validatorKey, "validator key not found in keyring") - validatorArmoredKey, err := kr.ExportPrivKeyArmor("validator", "") - s.Require().NoError(err, "failed to export validator private key") + operatorArmoredKey, err := kr.ExportPrivKeyArmor("validator", "") + s.Require().NoError(err, "failed to export operator private key") - attesterConfig, attesterNode := s.getAttester(ctx, gmChain, validatorArmoredKey) + attesterConfig, attesterNode := s.getAttester(ctx, gmChain, operatorArmoredKey) s.T().Logf("Initializing attester node %s", attesterNode.Name()) err = attesterNode.Init(ctx, attesterConfig.ChainID, attesterConfig.GMNodeURL) @@ -115,13 +127,13 @@ func (s *DockerIntegrationTestSuite) TestAttesterSystem() { s.testIBCTransfers(ctx, s.celestiaChain, gmChain, channel, hermes) } -func (s *DockerIntegrationTestSuite) getAttester(ctx context.Context, gmChain *cosmos.Chain, validatorArmoredKey string) (AttesterConfig, *Attester) { +func (s *DockerIntegrationTestSuite) getAttester(ctx context.Context, gmChain *cosmos.Chain, operatorArmoredKey string) (AttesterConfig, *Attester) { // Create attester configuration attesterConfig := DefaultAttesterConfig() // Set armored key (required) - require.NotEmpty(s.T(), validatorArmoredKey, "validator armored key is required") - attesterConfig.PrivKeyArmor = validatorArmoredKey + require.NotEmpty(s.T(), operatorArmoredKey, "operator armored key is required") + attesterConfig.PrivKeyArmor = operatorArmoredKey // Get the internal network addresses for the GM chain gmNodes := gmChain.GetNodes() @@ -131,12 +143,9 @@ func (s *DockerIntegrationTestSuite) getAttester(ctx context.Context, gmChain *c gmNodeInfo, err := gmNode.GetNetworkInfo(ctx) require.NoError(s.T(), err) - privValidatorKeyJSON, err := gmNode.ReadFile(ctx, "config/priv_validator_key.json") + sequencerPrivValidatorKeyJSON, err := gmNode.ReadFile(ctx, "config/priv_validator_key.json") require.NoError(s.T(), err, "unable to read priv_validator_key.json from GM node") - privValidatorStateJSON, err := gmNode.ReadFile(ctx, "data/priv_validator_state.json") - require.NoError(s.T(), err, "unable to read priv_validator_state.json from GM node") - // Derive attester account address from armored key attesterAccAddr, err := deriveAttesterAccountFromArmor(attesterConfig.PrivKeyArmor) require.NoError(s.T(), err, "failed to derive attester account address from armored key") @@ -156,30 +165,34 @@ func (s *DockerIntegrationTestSuite) getAttester(ctx context.Context, gmChain *c // Create and start the attester attesterNode, err := NewAttester(ctx, s.dockerClient, s.T().Name(), s.networkID, 0, s.logger) require.NoError(s.T(), err) + + attesterPrivValidatorKeyJSON, attesterPrivValidatorStateJSON := newDistinctAttesterPrivValidatorFiles(s.T(), sequencerPrivValidatorKeyJSON) + require.False(s.T(), bytes.Equal(sequencerPrivValidatorKeyJSON, attesterPrivValidatorKeyJSON), "attester consensus key must differ from sequencer consensus key") + require.NoError(s.T(), attesterNode.WriteFile( ctx, "config/priv_validator_key.json", - privValidatorKeyJSON, + attesterPrivValidatorKeyJSON, )) require.NoError(s.T(), attesterNode.WriteFile( ctx, "data/priv_validator_state.json", - privValidatorStateJSON, + attesterPrivValidatorStateJSON, )) - // Verify validator key can be imported (demonstration) - s.T().Log("Setting up attester keyring with validator key...") + // Verify operator key can be imported (demonstration) + s.T().Log("Setting up attester keyring with operator key...") // Create an in-memory keyring for the attester // Include transfer module so MsgTransfer is registered in the interface registry testEncCfg := testutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, bank.AppModuleBasic{}, ibctransfer.AppModuleBasic{}) attesterKeyring := keyring.NewInMemory(testEncCfg.Codec) - // Import the validator key into the attester keyring - err = attesterKeyring.ImportPrivKey("validator", validatorArmoredKey, "") - require.NoError(s.T(), err, "failed to import validator key into attester keyring") + // Import the operator key into the attester keyring + err = attesterKeyring.ImportPrivKey("operator", operatorArmoredKey, "") + require.NoError(s.T(), err, "failed to import operator key into attester keyring") - s.T().Log("Validator key imported successfully into attester keyring") + s.T().Log("Operator key imported successfully into attester keyring") // List keys in attester keyring to verify attesterKeys, err := attesterKeyring.List() @@ -220,6 +233,26 @@ func deriveAttesterAccountFromArmor(armoredKey string) (sdk.AccAddress, error) { return keyAddr, nil } +func newDistinctAttesterPrivValidatorFiles(t testing.TB, sequencerKeyJSON []byte) ([]byte, []byte) { + t.Helper() + + tmpDir := t.TempDir() + keyPath := filepath.Join(tmpDir, "priv_validator_key.json") + statePath := filepath.Join(tmpDir, "priv_validator_state.json") + + pv := cmprivval.GenFilePV(keyPath, statePath) + pv.Save() + + attesterKeyJSON, err := os.ReadFile(keyPath) + require.NoError(t, err) + require.False(t, bytes.Equal(sequencerKeyJSON, attesterKeyJSON), "generated attester key unexpectedly matched sequencer key") + + attesterStateJSON, err := os.ReadFile(statePath) + require.NoError(t, err) + + return attesterKeyJSON, attesterStateJSON +} + func (s *DockerIntegrationTestSuite) getGmChain(ctx context.Context) *cosmos.Chain { daAddress, authToken, _, err := s.getDANetworkParams(ctx) require.NoError(s.T(), err) From 841363e59da2573409c23ee2b508643c7ade82f2 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Tue, 16 Jun 2026 10:45:44 +0200 Subject: [PATCH 2/7] fix: align attester light client RPC data --- modules/network/keeper/grpc_query_test.go | 89 +++++++++++++ pkg/rpc/core/attester_light_test.go | 74 +++++++++++ pkg/rpc/core/blocks.go | 58 ++------- pkg/rpc/core/blocks_test.go | 61 +++++++++ pkg/rpc/core/utils.go | 51 ++++++-- server/attester_cmd.go | 150 +++++++++++----------- server/attester_cmd_test.go | 52 ++++++++ 7 files changed, 396 insertions(+), 139 deletions(-) create mode 100644 modules/network/keeper/grpc_query_test.go create mode 100644 pkg/rpc/core/attester_light_test.go diff --git a/modules/network/keeper/grpc_query_test.go b/modules/network/keeper/grpc_query_test.go new file mode 100644 index 00000000..5dc12c2f --- /dev/null +++ b/modules/network/keeper/grpc_query_test.go @@ -0,0 +1,89 @@ +package keeper + +import ( + "testing" + + "github.com/cometbft/cometbft/crypto/ed25519" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/evstack/ev-abci/modules/network/types" +) + +func TestAttestersQueryReturnsRegisteredAttesterInfo(t *testing.T) { + newQueryServer := func(t *testing.T) (types.QueryServer, Keeper, sdk.Context) { + t.Helper() + sk := NewMockStakingKeeper() + _, keeper, ctx := newTestServer(t, &sk) + return NewQueryServer(keeper), keeper, ctx + } + + t.Run("nil request", func(t *testing.T) { + queryServer, _, ctx := newQueryServer(t) + + resp, err := queryServer.Attesters(ctx, nil) + + require.Nil(t, resp) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument, status.Code(err)) + }) + + t.Run("empty set", func(t *testing.T) { + queryServer, _, ctx := newQueryServer(t) + + resp, err := queryServer.Attesters(ctx, &types.QueryAttestersRequest{}) + + require.NoError(t, err) + require.NotNil(t, resp) + require.Empty(t, resp.Attesters) + }) + + t.Run("joined attester with pubkey", func(t *testing.T) { + queryServer, keeper, ctx := newQueryServer(t) + pubKeyAny := newAttesterPubKeyAny(t) + consensusAddress := sdk.ValAddress(ed25519.GenPrivKey().PubKey().Address()).String() + attesterInfo := &types.AttesterInfo{ + Authority: sdk.AccAddress("operator").String(), + Pubkey: pubKeyAny, + JoinedHeight: ctx.BlockHeight(), + } + require.NoError(t, keeper.SetAttesterSetMember(ctx, consensusAddress)) + require.NoError(t, keeper.SetAttesterInfo(ctx, consensusAddress, attesterInfo)) + + resp, err := queryServer.Attesters(ctx, &types.QueryAttestersRequest{}) + + require.NoError(t, err) + require.Len(t, resp.Attesters, 1) + require.Equal(t, consensusAddress, resp.Attesters[0].ValidatorAddress) + require.Equal(t, attesterInfo.Authority, resp.Attesters[0].AttesterInfo.Authority) + require.Equal(t, attesterInfo.JoinedHeight, resp.Attesters[0].AttesterInfo.JoinedHeight) + require.Equal(t, pubKeyAny.TypeUrl, resp.Attesters[0].AttesterInfo.Pubkey.TypeUrl) + require.Equal(t, pubKeyAny.Value, resp.Attesters[0].AttesterInfo.Pubkey.Value) + }) + + t.Run("missing attester info", func(t *testing.T) { + queryServer, keeper, ctx := newQueryServer(t) + consensusAddress := sdk.ValAddress(ed25519.GenPrivKey().PubKey().Address()).String() + require.NoError(t, keeper.SetAttesterSetMember(ctx, consensusAddress)) + + resp, err := queryServer.Attesters(ctx, &types.QueryAttestersRequest{}) + + require.Nil(t, resp) + require.Error(t, err) + }) +} + +func newAttesterPubKeyAny(t *testing.T) *codectypes.Any { + t.Helper() + + sdkPubKey, err := cryptocodec.FromCmtPubKeyInterface(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + pubKeyAny, err := codectypes.NewAnyWithValue(sdkPubKey) + require.NoError(t, err) + return pubKeyAny +} diff --git a/pkg/rpc/core/attester_light_test.go b/pkg/rpc/core/attester_light_test.go new file mode 100644 index 00000000..54124548 --- /dev/null +++ b/pkg/rpc/core/attester_light_test.go @@ -0,0 +1,74 @@ +package core + +import ( + "strings" + "testing" + + "github.com/cometbft/cometbft/crypto/ed25519" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + networktypes "github.com/evstack/ev-abci/modules/network/types" +) + +func TestCometValidatorFromAttesterValidation(t *testing.T) { + validPubKeyAny := newCoreAttesterPubKeyAny(t, ed25519.GenPrivKey().PubKey().(ed25519.PubKey)) + + tests := map[string]struct { + attester *networktypes.RegisteredAttester + errContains string + }{ + "nil info": { + attester: &networktypes.RegisteredAttester{}, + errContains: "missing attester info", + }, + "nil pubkey": { + attester: &networktypes.RegisteredAttester{ + ValidatorAddress: sdk.ValAddress(ed25519.GenPrivKey().PubKey().Address()).String(), + AttesterInfo: &networktypes.AttesterInfo{}, + }, + errContains: "missing consensus pubkey", + }, + "malformed bech32 address": { + attester: &networktypes.RegisteredAttester{ + ValidatorAddress: "not-bech32", + AttesterInfo: &networktypes.AttesterInfo{ + Pubkey: validPubKeyAny, + }, + }, + errContains: "decode attester validator address", + }, + "pubkey address mismatch": { + attester: &networktypes.RegisteredAttester{ + ValidatorAddress: sdk.ValAddress(ed25519.GenPrivKey().PubKey().Address()).String(), + AttesterInfo: &networktypes.AttesterInfo{ + Pubkey: validPubKeyAny, + }, + }, + errContains: "pubkey derives consensus address", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + validator, err := cometValidatorFromAttester(test.attester) + + require.Nil(t, validator) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), test.errContains), err.Error()) + }) + } +} + +func newCoreAttesterPubKeyAny(t *testing.T, pubKey ed25519.PubKey) *codectypes.Any { + t.Helper() + + sdkPubKey, err := cryptocodec.FromCmtPubKeyInterface(pubKey) + require.NoError(t, err) + + pubKeyAny, err := codectypes.NewAnyWithValue(sdkPubKey) + require.NoError(t, err) + return pubKeyAny +} diff --git a/pkg/rpc/core/blocks.go b/pkg/rpc/core/blocks.go index 2e6d86c4..ec469867 100644 --- a/pkg/rpc/core/blocks.go +++ b/pkg/rpc/core/blocks.go @@ -21,7 +21,6 @@ import ( rlktypes "github.com/evstack/ev-node/types" networktypes "github.com/evstack/ev-abci/modules/network/types" - "github.com/evstack/ev-abci/pkg/adapter" ) // BlockSearch searches for a paginated set of blocks matching PreBlock and @@ -76,32 +75,14 @@ func BlockSearch( return nil, err } - lastCommit, err := env.Adapter.GetLastCommit(wrappedCtx, uint64(results[i])) - if err != nil { - return nil, fmt.Errorf("failed to get last commit for block %d: %w", results[i], err) - } - - abciHeader, err := adapter.ToABCIHeader(header.Header, lastCommit) - if err != nil { - return nil, fmt.Errorf("failed to convert header to ABCI format: %w", err) - } - - abciBlock, err := adapter.ToABCIBlock(abciHeader, lastCommit, data) + abciBlock, blockID, err := buildABCIBlock(wrappedCtx, header, data) if err != nil { return nil, err } - blockParts, err := abciBlock.MakePartSet(cmttypes.BlockPartSizeBytes) - if err != nil { - return nil, fmt.Errorf("make part set: %w", err) - } - blocks = append(blocks, &ctypes.ResultBlock{ - Block: abciBlock, - BlockID: cmttypes.BlockID{ - Hash: abciHeader.Hash(), - PartSetHeader: blockParts.Header(), - }, + Block: abciBlock, + BlockID: blockID, }) } @@ -186,32 +167,14 @@ func BlockByHash(ctx *rpctypes.Context, hash []byte) (*ctypes.ResultBlock, error return nil, err } - lastCommit, err := env.Adapter.GetLastCommit(ctx.Context(), header.Height()) - if err != nil { - return nil, fmt.Errorf("failed to get last commit for block %d: %w", header.Height(), err) - } - - abciHeader, err := adapter.ToABCIHeader(header.Header, lastCommit) - if err != nil { - return nil, fmt.Errorf("failed to convert header to ABCI format: %w", err) - } - - abciBlock, err := adapter.ToABCIBlock(abciHeader, lastCommit, data) + abciBlock, blockID, err := buildABCIBlock(ctx.Context(), header, data) if err != nil { return nil, err } - blockParts, err := abciBlock.MakePartSet(cmttypes.BlockPartSizeBytes) - if err != nil { - return nil, fmt.Errorf("make part set: %w", err) - } - return &ctypes.ResultBlock{ - BlockID: cmttypes.BlockID{ - Hash: abciHeader.Hash(), - PartSetHeader: blockParts.Header(), - }, - Block: abciBlock, + BlockID: blockID, + Block: abciBlock, }, nil } @@ -291,14 +254,9 @@ func HeaderByHash(ctx *rpctypes.Context, hash cmbytes.HexBytes) (*ctypes.ResultH return nil, err } - lastCommit, err := env.Adapter.GetLastCommit(ctx.Context(), header.Height()) + abciHeader, _, err := buildABCIHeader(ctx.Context(), header) if err != nil { - return nil, fmt.Errorf("failed to get last commit for block %d: %w", header.Height(), err) - } - - abciHeader, err := adapter.ToABCIHeader(header.Header, lastCommit) - if err != nil { - return nil, fmt.Errorf("failed to convert header to ABCI format: %w", err) + return nil, err } return &ctypes.ResultHeader{Header: &abciHeader}, nil diff --git a/pkg/rpc/core/blocks_test.go b/pkg/rpc/core/blocks_test.go index 7bb12083..0ef5ce4c 100644 --- a/pkg/rpc/core/blocks_test.go +++ b/pkg/rpc/core/blocks_test.go @@ -342,6 +342,67 @@ func TestCommit_AttesterModeUsesAttesterValidatorSetForHeader(t *testing.T) { )) } +func TestBlockByHash_AttesterModeUsesAttesterValidatorSetForHeader(t *testing.T) { + require := require.New(t) + + sequencerPrivKey := ed25519.GenPrivKey() + sequencerPubKey := sequencerPrivKey.PubKey() + sequencerLibP2PPrivKey, err := crypto.UnmarshalEd25519PrivateKey(sequencerPrivKey.Bytes()) + require.NoError(err) + sequencerLibP2PPubKey := sequencerLibP2PPrivKey.GetPublic() + sequencerAddress := sequencerPubKey.Address().Bytes()[:20] + + attesterPrivKey := ed25519.GenPrivKey() + attesterPubKey := attesterPrivKey.PubKey().(ed25519.PubKey) + attesterAddress := attesterPubKey.Address() + attesterValAddress := sdk.ValAddress(attesterAddress).String() + + mockSigner := &MockSigner{} + mockSigner.On("GetPublic").Return(sequencerLibP2PPubKey, nil) + mockSigner.On("GetAddress").Return(sequencerAddress, nil) + + env = setupTestEnvironment(mockSigner) + env.AttesterMode = true + defer func() { + env.AttesterMode = false + }() + + chainID := "test-chain" + blockHeight := uint64(1) + now := time.Now() + + sequencerValidatorHash, err := adapter.ValidatorHasherProvider()(sequencerAddress, sequencerLibP2PPubKey) + require.NoError(err) + blockData, rollkitHeader := createTestBlock(blockHeight, chainID, now, sequencerAddress, sequencerValidatorHash, 1) + + attesterValidatorSet := cmttypes.NewValidatorSet([]*cmttypes.Validator{ + cmttypes.NewValidator(attesterPubKey, 1), + }) + mockAttesterNetworkQueries(t, env.Adapter.App.(*MockApp), attesterValAddress, attesterPubKey, nil) + + env.Adapter.RollkitStore.(*rollkitmocks.MockStore).On( + "GetBlockByHash", + mock.Anything, + mock.MatchedBy(func(hash types.Hash) bool { + return string(hash) == string(rollkitHeader.Hash()) + }), + ).Return(&types.SignedHeader{ + Header: rollkitHeader, + Signer: types.Signer{ + PubKey: sequencerLibP2PPubKey, + Address: sequencerAddress, + }, + }, blockData, nil) + + result, err := BlockByHash(&rpctypes.Context{}, rollkitHeader.Hash()) + + require.NoError(err) + require.NotNil(result) + require.Equal(attesterValidatorSet.Hash(), []byte(result.Block.ValidatorsHash)) + require.Equal(attesterValidatorSet.Hash(), []byte(result.Block.NextValidatorsHash)) + require.Equal(attesterAddress.Bytes(), []byte(result.Block.ProposerAddress)) +} + func createTestBlock(height uint64, chainID string, baseTime time.Time, validatorAddress []byte, validatorHash []byte, offset int) (*types.Data, types.Header) { blockTime := uint64(baseTime.UnixNano() + int64(offset-1)*int64(time.Second)) diff --git a/pkg/rpc/core/utils.go b/pkg/rpc/core/utils.go index 5e6886e7..36d6d17d 100644 --- a/pkg/rpc/core/utils.go +++ b/pkg/rpc/core/utils.go @@ -7,6 +7,7 @@ import ( "fmt" cmttypes "github.com/cometbft/cometbft/types" + rlktypes "github.com/evstack/ev-node/types" "github.com/evstack/ev-abci/pkg/adapter" ) @@ -45,41 +46,65 @@ func getBlockMeta(ctx context.Context, n uint64) (*cmttypes.BlockMeta, *cmttypes return nil, nil } - lastCommit, err := env.Adapter.GetLastCommit(ctx, n) + abciBlock, _, err := buildABCIBlock(ctx, header, data) if err != nil { - env.Logger.Error("Failed to get last commit in getBlockMeta", "height", n, "err", err) + env.Logger.Error("Failed to convert block to ABCI format", "height", n, "err", err) return nil, nil } - abciHeader, err := adapter.ToABCIHeader(header.Header, lastCommit) + abciBlockMeta, err := adapter.ToABCIBlockMeta(abciBlock) if err != nil { - env.Logger.Error("Failed to convert header to ABCI format", "height", n, "err", err) + env.Logger.Error("Failed to convert block to ABCI block meta", "height", n, "err", err) return nil, nil } + + return abciBlockMeta, abciBlock +} + +func buildABCIHeader(ctx context.Context, header *rlktypes.SignedHeader) (cmttypes.Header, *cmttypes.Commit, error) { + lastCommit, err := env.Adapter.GetLastCommit(ctx, header.Height()) + if err != nil { + return cmttypes.Header{}, nil, fmt.Errorf("get last commit for block %d: %w", header.Height(), err) + } + + abciHeader, err := adapter.ToABCIHeader(header.Header, lastCommit) + if err != nil { + return cmttypes.Header{}, nil, fmt.Errorf("convert header to ABCI format: %w", err) + } + if env.AttesterMode { if err := applyAttesterValidatorSetToHeader(ctx, &abciHeader); err != nil { if errors.Is(err, errNoRegisteredAttesters) { - env.Logger.Debug("No attesters registered; serving sequencer header", "height", n) + env.Logger.Debug("No attesters registered; serving sequencer header", "height", header.Height()) } else { - env.Logger.Error("Failed to apply attester validator set to header", "height", n, "err", err) - return nil, nil + return cmttypes.Header{}, nil, fmt.Errorf("apply attester validator set to header: %w", err) } } } + return abciHeader, lastCommit, nil +} + +func buildABCIBlock(ctx context.Context, header *rlktypes.SignedHeader, data *rlktypes.Data) (*cmttypes.Block, cmttypes.BlockID, error) { + abciHeader, lastCommit, err := buildABCIHeader(ctx, header) + if err != nil { + return nil, cmttypes.BlockID{}, err + } + abciBlock, err := adapter.ToABCIBlock(abciHeader, lastCommit, data) if err != nil { - env.Logger.Error("Failed to convert block to ABCI format", "height", n, "err", err) - return nil, nil + return nil, cmttypes.BlockID{}, err } - abciBlockMeta, err := adapter.ToABCIBlockMeta(abciBlock) + blockParts, err := abciBlock.MakePartSet(cmttypes.BlockPartSizeBytes) if err != nil { - env.Logger.Error("Failed to convert block to ABCI block meta", "height", n, "err", err) - return nil, nil + return nil, cmttypes.BlockID{}, fmt.Errorf("make part set: %w", err) } - return abciBlockMeta, abciBlock + return abciBlock, cmttypes.BlockID{ + Hash: abciHeader.Hash(), + PartSetHeader: blockParts.Header(), + }, nil } func filterMinMax(base, height, mini, maxi, limit int64) (int64, int64, error) { diff --git a/server/attester_cmd.go b/server/attester_cmd.go index ff15d0e3..e1a77218 100644 --- a/server/attester_cmd.go +++ b/server/attester_cmd.go @@ -648,14 +648,9 @@ func submitAttestation( pv *pvm.FilePV, clientCtx client.Context, ) error { - header, err := getEvolveHeader(config.Node, height) + header, blockID, err := getEvolveBlock(ctx, config.Node, height) if err != nil { - return fmt.Errorf("getting Evolve header: %w", err) - } - - blockID, err := getOriginalBlockID(ctx, config.Node, height) - if err != nil { - return fmt.Errorf("getting original block ID: %w", err) + return fmt.Errorf("getting Evolve block: %w", err) } vote := cmtproto.Vote{ @@ -709,56 +704,91 @@ func submitAttestation( return nil } -func getEvolveHeader(node string, height int64) (*evolvetypes.Header, error) { +type evolveBlockResponse struct { + Result struct { + BlockID struct { + Hash string `json:"hash"` + Parts struct { + Hash string `json:"hash"` + Total uint32 `json:"total"` + } `json:"parts"` + } `json:"block_id"` + Block struct { + Header struct { + Version struct { + Block string `json:"block"` + App string `json:"app"` + } `json:"version"` + Height string `json:"height"` + Time string `json:"time"` + LastBlockID struct { + Hash string `json:"hash"` + } `json:"last_block_id"` + LastCommitHash string `json:"last_commit_hash"` + DataHash string `json:"data_hash"` + ValidatorsHash string `json:"validators_hash"` + NextValidatorsHash string `json:"next_validators_hash"` + ConsensusHash string `json:"consensus_hash"` + AppHash string `json:"app_hash"` + LastResultsHash string `json:"last_results_hash"` + EvidenceHash string `json:"evidence_hash"` + ProposerAddress string `json:"proposer_address"` + ChainID string `json:"chain_id"` + } `json:"header"` + } `json:"block"` + } `json:"result"` +} + +func getEvolveBlock(ctx context.Context, node string, height int64) (*evolvetypes.Header, cmtproto.BlockID, error) { parsed, err := url.Parse(node) if err != nil { - return nil, fmt.Errorf("parse node URL: %w", err) + return nil, cmtproto.BlockID{}, fmt.Errorf("parse node URL: %w", err) } httpClient := &http.Client{ Timeout: 10 * time.Second, } - resp, err := httpClient.Get(fmt.Sprintf("http://%s/block?height=%d", parsed.Host, height)) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://%s/block?height=%d", parsed.Host, height), nil) + if err != nil { + return nil, cmtproto.BlockID{}, fmt.Errorf("creating block request: %w", err) + } + + resp, err := httpClient.Do(req) if err != nil { - return nil, fmt.Errorf("querying block: %w", err) + return nil, cmtproto.BlockID{}, fmt.Errorf("querying block: %w", err) } defer func() { _ = resp.Body.Close() }() - var blockResponse struct { - Result struct { - Block struct { - Header struct { - Version struct { - Block string `json:"block"` - App string `json:"app"` - } `json:"version"` - Height string `json:"height"` - Time string `json:"time"` - LastBlockID struct { - Hash string `json:"hash"` - } `json:"last_block_id"` - LastCommitHash string `json:"last_commit_hash"` - DataHash string `json:"data_hash"` - ValidatorsHash string `json:"validators_hash"` - NextValidatorsHash string `json:"next_validators_hash"` - ConsensusHash string `json:"consensus_hash"` - AppHash string `json:"app_hash"` - LastResultsHash string `json:"last_results_hash"` - EvidenceHash string `json:"evidence_hash"` - ProposerAddress string `json:"proposer_address"` - ChainID string `json:"chain_id"` - } `json:"header"` - } `json:"block"` - } `json:"result"` + var blockResponse evolveBlockResponse + if err := json.NewDecoder(resp.Body).Decode(&blockResponse); err != nil { + return nil, cmtproto.BlockID{}, fmt.Errorf("decoding response: %w", err) } - if err := json.NewDecoder(resp.Body).Decode(&blockResponse); err != nil { - return nil, fmt.Errorf("decoding response: %w", err) + header, err := evolveHeaderFromResponse(blockResponse) + if err != nil { + return nil, cmtproto.BlockID{}, err + } + + blockID, err := blockIDFromResponse(blockResponse) + if err != nil { + return nil, cmtproto.BlockID{}, err } + return header, blockID, nil +} + +func getEvolveHeader(node string, height int64) (*evolvetypes.Header, error) { + header, _, err := getEvolveBlock(context.Background(), node, height) + if err != nil { + return nil, err + } + return header, nil +} + +func evolveHeaderFromResponse(blockResponse evolveBlockResponse) (*evolvetypes.Header, error) { header := blockResponse.Result.Block.Header heightUint, err := strconv.ParseUint(header.Height, 10, 64) @@ -804,46 +834,14 @@ func getOriginalBlockID(ctx context.Context, node string, height int64) (cmtprot return cmtproto.BlockID{}, nil } - parsed, err := url.Parse(node) + _, blockID, err := getEvolveBlock(ctx, node, height) if err != nil { - return cmtproto.BlockID{}, fmt.Errorf("parse node URL: %w", err) - } - - httpClient := &http.Client{ - Timeout: 10 * time.Second, - } - - resp, err := httpClient.Get(fmt.Sprintf("http://%s/block?height=%d", parsed.Host, height)) - if err != nil { - return cmtproto.BlockID{}, fmt.Errorf("querying block: %w", err) - } - defer func() { - _ = resp.Body.Close() - }() - - var blockResponse struct { - Result struct { - BlockID struct { - Hash string `json:"hash"` - Parts struct { - Hash string `json:"hash"` - Total uint32 `json:"total"` - } `json:"parts"` - } `json:"block_id"` - Block struct { - Header struct { - Height string `json:"height"` - Time string `json:"time"` - ChainID string `json:"chain_id"` - } `json:"header"` - } `json:"block"` - } `json:"result"` - } - - if err := json.NewDecoder(resp.Body).Decode(&blockResponse); err != nil { - return cmtproto.BlockID{}, fmt.Errorf("decoding response: %w", err) + return cmtproto.BlockID{}, err } + return blockID, nil +} +func blockIDFromResponse(blockResponse evolveBlockResponse) (cmtproto.BlockID, error) { blockIDHash, err := hex.DecodeString(blockResponse.Result.BlockID.Hash) if err != nil { return cmtproto.BlockID{}, fmt.Errorf("decoding block ID hash: %w", err) diff --git a/server/attester_cmd_test.go b/server/attester_cmd_test.go index f9fbaf7a..0da5afbe 100644 --- a/server/attester_cmd_test.go +++ b/server/attester_cmd_test.go @@ -100,3 +100,55 @@ func TestGetOriginalBlockID(t *testing.T) { assert.Equal(t, uint32(3), blockID.PartSetHeader.Total) }) } + +func TestGetEvolveBlockReadsHeaderAndBlockIDFromSingleRPCResponse(t *testing.T) { + requests := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requests++ + require.Equal(t, "/block", r.URL.Path) + require.Equal(t, "7", r.URL.Query().Get("height")) + _, _ = w.Write([]byte(`{ + "result": { + "block_id": { + "hash": "1111111111111111111111111111111111111111111111111111111111111111", + "parts": { + "hash": "2222222222222222222222222222222222222222222222222222222222222222", + "total": 3 + } + }, + "block": { + "header": { + "version": { + "block": "1", + "app": "5" + }, + "height": "7", + "time": "2026-06-16T00:00:00Z", + "last_block_id": { + "hash": "3333333333333333333333333333333333333333333333333333333333333333" + }, + "data_hash": "4444444444444444444444444444444444444444444444444444444444444444", + "validators_hash": "5555555555555555555555555555555555555555555555555555555555555555", + "app_hash": "6666666666666666666666666666666666666666666666666666666666666666", + "proposer_address": "7777777777777777777777777777777777777777", + "chain_id": "test" + } + } + } + }`)) + })) + defer server.Close() + + header, blockID, err := getEvolveBlock(context.Background(), server.URL, 7) + require.NoError(t, err) + require.Equal(t, 1, requests) + require.Equal(t, uint64(7), header.Height()) + require.Equal(t, "test", header.ChainID()) + require.Equal(t, []byte{ + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + }, blockID.Hash) + require.Equal(t, uint32(3), blockID.PartSetHeader.Total) +} From 5e34142234c3834d19a8485e3dc7a4f3d3c6c6ae Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Tue, 16 Jun 2026 13:52:24 +0200 Subject: [PATCH 3/7] fix: address attester RPC CI lint --- pkg/rpc/core/blocks_test.go | 16 ++++++++-------- pkg/rpc/core/utils.go | 1 + server/attester_cmd.go | 8 -------- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/pkg/rpc/core/blocks_test.go b/pkg/rpc/core/blocks_test.go index 0ef5ce4c..5ce4ed86 100644 --- a/pkg/rpc/core/blocks_test.go +++ b/pkg/rpc/core/blocks_test.go @@ -328,17 +328,17 @@ func TestCommit_AttesterModeUsesAttesterValidatorSetForHeader(t *testing.T) { commitResult := callCommitRPC(t, blockHeight) - require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.SignedHeader.Header.ValidatorsHash)) - require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.SignedHeader.Header.NextValidatorsHash)) - require.Equal(attesterAddress.Bytes(), []byte(commitResult.SignedHeader.Header.ProposerAddress)) - require.Equal(commitResult.SignedHeader.Header.Hash(), commitResult.SignedHeader.Commit.BlockID.Hash) - require.Len(commitResult.SignedHeader.Commit.Signatures, 1) - require.Equal(attesterAddress.Bytes(), []byte(commitResult.SignedHeader.Commit.Signatures[0].ValidatorAddress)) + require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.ValidatorsHash)) + require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.NextValidatorsHash)) + require.Equal(attesterAddress.Bytes(), []byte(commitResult.ProposerAddress)) + require.Equal(commitResult.Hash(), commitResult.Commit.BlockID.Hash) + require.Len(commitResult.Commit.Signatures, 1) + require.Equal(attesterAddress.Bytes(), []byte(commitResult.Commit.Signatures[0].ValidatorAddress)) require.NoError(attesterValidatorSet.VerifyCommitLight( chainID, - commitResult.SignedHeader.Commit.BlockID, + commitResult.Commit.BlockID, int64(blockHeight), - commitResult.SignedHeader.Commit, + commitResult.Commit, )) } diff --git a/pkg/rpc/core/utils.go b/pkg/rpc/core/utils.go index 36d6d17d..b17ef06e 100644 --- a/pkg/rpc/core/utils.go +++ b/pkg/rpc/core/utils.go @@ -7,6 +7,7 @@ import ( "fmt" cmttypes "github.com/cometbft/cometbft/types" + rlktypes "github.com/evstack/ev-node/types" "github.com/evstack/ev-abci/pkg/adapter" diff --git a/server/attester_cmd.go b/server/attester_cmd.go index e1a77218..95a8a66b 100644 --- a/server/attester_cmd.go +++ b/server/attester_cmd.go @@ -780,14 +780,6 @@ func getEvolveBlock(ctx context.Context, node string, height int64) (*evolvetype return header, blockID, nil } -func getEvolveHeader(node string, height int64) (*evolvetypes.Header, error) { - header, _, err := getEvolveBlock(context.Background(), node, height) - if err != nil { - return nil, err - } - return header, nil -} - func evolveHeaderFromResponse(blockResponse evolveBlockResponse) (*evolvetypes.Header, error) { header := blockResponse.Result.Block.Header From 04d6e4a43ba74f42b07bc9e1806199b10a398605 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Tue, 16 Jun 2026 17:18:16 +0200 Subject: [PATCH 4/7] test: cover multi-attester quorum and commits --- modules/network/keeper/msg_server_test.go | 77 ++++++++ pkg/rpc/core/blocks_test.go | 205 ++++++++++++++++++---- 2 files changed, 252 insertions(+), 30 deletions(-) diff --git a/modules/network/keeper/msg_server_test.go b/modules/network/keeper/msg_server_test.go index a4419612..73727fcf 100644 --- a/modules/network/keeper/msg_server_test.go +++ b/modules/network/keeper/msg_server_test.go @@ -342,6 +342,83 @@ func TestAttest(t *testing.T) { } } +func TestAttestWithMultipleAttestersTracksQuorumAndSignatures(t *testing.T) { + sk := NewMockStakingKeeper() + server, keeper, ctx := newTestServer(t, &sk) + require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) + + attesters := []struct { + authority string + consensusAddress string + vote []byte + }{ + { + authority: sdk.AccAddress("operator1").String(), + consensusAddress: sdk.ValAddress("attester1").String(), + vote: bytes.Repeat([]byte{0x01}, MinVoteLen), + }, + { + authority: sdk.AccAddress("operator2").String(), + consensusAddress: sdk.ValAddress("attester2").String(), + vote: bytes.Repeat([]byte{0x02}, MinVoteLen), + }, + { + authority: sdk.AccAddress("operator3").String(), + consensusAddress: sdk.ValAddress("attester3").String(), + vote: bytes.Repeat([]byte{0x03}, MinVoteLen), + }, + } + for _, attester := range attesters { + _, err := server.JoinAttesterSet(ctx, &types.MsgJoinAttesterSet{ + Authority: attester.authority, + ConsensusAddress: attester.consensusAddress, + }) + require.NoError(t, err) + } + require.NoError(t, keeper.BuildValidatorIndexMap(ctx)) + + height := int64(10) + for i, attester := range attesters { + rsp, err := server.Attest(ctx, &types.MsgAttest{ + Authority: attester.authority, + ConsensusAddress: attester.consensusAddress, + Height: height, + Vote: attester.vote, + }) + require.NoError(t, err) + require.NotNil(t, rsp) + + bitmap, err := keeper.GetAttestationBitmap(ctx, height) + require.NoError(t, err) + votedPower, err := keeper.CalculateVotedPower(ctx, bitmap) + require.NoError(t, err) + require.Equal(t, uint64(i+1), votedPower) + + totalPower, err := keeper.GetTotalPower(ctx) + require.NoError(t, err) + require.Equal(t, uint64(3), totalPower) + + quorumReached, err := keeper.CheckQuorum(ctx, votedPower, totalPower) + require.NoError(t, err) + require.Equal(t, i >= 1, quorumReached) + + lastAttestedHeight, err := keeper.GetLastAttestedHeight(ctx) + require.NoError(t, err) + if i == 0 { + require.Zero(t, lastAttestedHeight) + } else { + require.Equal(t, height, lastAttestedHeight) + } + + signatures, err := keeper.GetAllSignaturesForHeight(ctx, height) + require.NoError(t, err) + require.Len(t, signatures, i+1) + for _, signedAttester := range attesters[:i+1] { + require.Equal(t, signedAttester.vote, signatures[signedAttester.consensusAddress]) + } + } +} + func newTestServer(t *testing.T, sk *MockStakingKeeper) (msgServer, Keeper, sdk.Context) { t.Helper() cdc := moduletestutil.MakeTestEncodingConfig().Codec diff --git a/pkg/rpc/core/blocks_test.go b/pkg/rpc/core/blocks_test.go index 5ce4ed86..808644db 100644 --- a/pkg/rpc/core/blocks_test.go +++ b/pkg/rpc/core/blocks_test.go @@ -342,6 +342,115 @@ func TestCommit_AttesterModeUsesAttesterValidatorSetForHeader(t *testing.T) { )) } +func TestCommit_AttesterModeBuildsMultiAttesterCommitWithAbsentSignatures(t *testing.T) { + require := require.New(t) + + sequencerPrivKey := ed25519.GenPrivKey() + sequencerPubKey := sequencerPrivKey.PubKey() + sequencerLibP2PPrivKey, err := crypto.UnmarshalEd25519PrivateKey(sequencerPrivKey.Bytes()) + require.NoError(err) + sequencerLibP2PPubKey := sequencerLibP2PPrivKey.GetPublic() + sequencerAddress := sequencerPubKey.Address().Bytes()[:20] + + mockSigner := &MockSigner{} + mockSigner.On("GetPublic").Return(sequencerLibP2PPubKey, nil) + mockSigner.On("GetAddress").Return(sequencerAddress, nil) + + env = setupTestEnvironment(mockSigner) + env.AttesterMode = true + defer func() { + env.AttesterMode = false + }() + + chainID := "test-chain" + blockHeight := uint64(1) + now := time.Now() + + sequencerValidatorHash, err := adapter.ValidatorHasherProvider()(sequencerAddress, sequencerLibP2PPubKey) + require.NoError(err) + blockData, rollkitHeader := createTestBlock(blockHeight, chainID, now, sequencerAddress, sequencerValidatorHash, 1) + + lastCommit, err := env.Adapter.GetLastCommit(context.Background(), blockHeight) + require.NoError(err) + abciHeader, err := adapter.ToABCIHeader(rollkitHeader, lastCommit) + require.NoError(err) + + attesterPrivKeysByAddress := make(map[string]ed25519.PrivKey) + attesterValidators := make([]*cmttypes.Validator, 0, 4) + for range 4 { + privKey := ed25519.GenPrivKey() + pubKey := privKey.PubKey().(ed25519.PubKey) + attesterPrivKeysByAddress[string(pubKey.Address())] = privKey + attesterValidators = append(attesterValidators, cmttypes.NewValidator(pubKey, 1)) + } + attesterValidatorSet := cmttypes.NewValidatorSet(attesterValidators) + abciHeader.ValidatorsHash = attesterValidatorSet.Hash() + abciHeader.NextValidatorsHash = attesterValidatorSet.Hash() + abciHeader.ProposerAddress = attesterValidatorSet.Proposer.Address + + abciBlock, err := adapter.ToABCIBlock(abciHeader, lastCommit, blockData) + require.NoError(err) + blockParts, err := abciBlock.MakePartSet(cmttypes.BlockPartSizeBytes) + require.NoError(err) + blockID := &cmttypes.BlockID{ + Hash: abciHeader.Hash(), + PartSetHeader: blockParts.Header(), + } + require.NoError(env.Adapter.Store.SaveBlockID(context.Background(), blockHeight, blockID)) + + attesterQueries := make([]mockAttesterNetworkQuery, 0, len(attesterValidatorSet.Validators)) + for validatorIndex, validator := range attesterValidatorSet.Validators { + privKey := attesterPrivKeysByAddress[string(validator.Address)] + pubKey := privKey.PubKey().(ed25519.PubKey) + query := mockAttesterNetworkQuery{ + attesterValAddress: sdk.ValAddress(validator.Address).String(), + attesterPubKey: pubKey, + } + if validatorIndex < 3 { + query.voteBytes = signAttesterVoteWithIndex( + t, + chainID, + blockHeight, + rollkitHeader.Time(), + blockID, + int32(validatorIndex), + privKey, + ) + } + attesterQueries = append(attesterQueries, query) + } + mockAttesterNetworkQueriesForSet(t, env.Adapter.App.(*MockApp), attesterQueries) + + mockBlock(blockHeight, rollkitHeader, blockData, make([]byte, 64), sequencerLibP2PPubKey, sequencerAddress) + + commitResult := callCommitRPC(t, blockHeight) + + require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.ValidatorsHash)) + require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.NextValidatorsHash)) + require.Len(commitResult.Commit.Signatures, 4) + + commitSignatures := 0 + absentSignatures := 0 + for i, signature := range commitResult.Commit.Signatures { + if i < 3 { + require.Equal(cmttypes.BlockIDFlagCommit, signature.BlockIDFlag) + require.Equal(attesterValidatorSet.Validators[i].Address.Bytes(), []byte(signature.ValidatorAddress)) + commitSignatures++ + continue + } + require.Equal(cmttypes.BlockIDFlagAbsent, signature.BlockIDFlag) + absentSignatures++ + } + require.Equal(3, commitSignatures) + require.Equal(1, absentSignatures) + require.NoError(attesterValidatorSet.VerifyCommitLight( + chainID, + commitResult.Commit.BlockID, + int64(blockHeight), + commitResult.Commit, + )) +} + func TestBlockByHash_AttesterModeUsesAttesterValidatorSetForHeader(t *testing.T) { require := require.New(t) @@ -440,6 +549,19 @@ func signAttesterVote( privKey ed25519.PrivKey, ) []byte { t.Helper() + return signAttesterVoteWithIndex(t, chainID, height, timestamp, blockID, 0, privKey) +} + +func signAttesterVoteWithIndex( + t *testing.T, + chainID string, + height uint64, + timestamp time.Time, + blockID *cmttypes.BlockID, + validatorIndex int32, + privKey ed25519.PrivKey, +) []byte { + t.Helper() vote := cmtproto.Vote{ Type: cmtproto.PrecommitType, @@ -448,7 +570,7 @@ func signAttesterVote( BlockID: blockID.ToProto(), Timestamp: timestamp, ValidatorAddress: privKey.PubKey().Address(), - ValidatorIndex: 0, + ValidatorIndex: validatorIndex, } signature, err := privKey.Sign(cmttypes.VoteSignBytes(chainID, &vote)) require.NoError(t, err) @@ -459,6 +581,12 @@ func signAttesterVote( return voteBytes } +type mockAttesterNetworkQuery struct { + attesterValAddress string + attesterPubKey ed25519.PubKey + voteBytes []byte +} + func mockAttesterNetworkQueries( t *testing.T, mockApp *MockApp, @@ -467,40 +595,61 @@ func mockAttesterNetworkQueries( voteBytes []byte, ) { t.Helper() - - sdkPubKey, err := cryptocodec.FromCmtPubKeyInterface(attesterPubKey) - require.NoError(t, err) - pubKeyAny, err := codectypes.NewAnyWithValue(sdkPubKey) - require.NoError(t, err) - - signaturesResponse, err := proto.Marshal(&networktypes.QueryAttesterSignaturesResponse{ - Signatures: []*networktypes.AttesterSignature{ - { - ValidatorAddress: attesterValAddress, - Signature: voteBytes, - }, + mockAttesterNetworkQueriesForSet(t, mockApp, []mockAttesterNetworkQuery{ + { + attesterValAddress: attesterValAddress, + attesterPubKey: attesterPubKey, + voteBytes: voteBytes, }, }) - require.NoError(t, err) +} - attesterInfo := &networktypes.AttesterInfo{ - Authority: "operator", - Pubkey: pubKeyAny, - JoinedHeight: 1, +func mockAttesterNetworkQueriesForSet(t *testing.T, mockApp *MockApp, attesters []mockAttesterNetworkQuery) { + t.Helper() + + signatures := make([]*networktypes.AttesterSignature, 0, len(attesters)) + registeredAttesters := make([]*networktypes.RegisteredAttester, 0, len(attesters)) + for i, attester := range attesters { + sdkPubKey, err := cryptocodec.FromCmtPubKeyInterface(attester.attesterPubKey) + require.NoError(t, err) + pubKeyAny, err := codectypes.NewAnyWithValue(sdkPubKey) + require.NoError(t, err) + + attesterInfo := &networktypes.AttesterInfo{ + Authority: "operator", + Pubkey: pubKeyAny, + JoinedHeight: 1, + } + + if len(attester.voteBytes) > 0 { + signatures = append(signatures, &networktypes.AttesterSignature{ + ValidatorAddress: attester.attesterValAddress, + Signature: attester.voteBytes, + }) + } + + registeredAttesters = append(registeredAttesters, &networktypes.RegisteredAttester{ + ValidatorAddress: attester.attesterValAddress, + AttesterInfo: attesterInfo, + }) + if i == 0 { + attesterInfoResponse, err := proto.Marshal(&networktypes.QueryAttesterInfoResponse{ + AttesterInfo: attesterInfo, + }) + require.NoError(t, err) + mockApp.On("Query", mock.Anything, mock.MatchedBy(func(req *abci.RequestQuery) bool { + return req.Path == "/evabci.network.v1.Query/AttesterInfo" + })).Return(&abci.ResponseQuery{Code: 0, Value: attesterInfoResponse}, nil).Maybe() + } } - attesterInfoResponse, err := proto.Marshal(&networktypes.QueryAttesterInfoResponse{ - AttesterInfo: attesterInfo, + signaturesResponse, err := proto.Marshal(&networktypes.QueryAttesterSignaturesResponse{ + Signatures: signatures, }) require.NoError(t, err) attestersResponse, err := proto.Marshal(&networktypes.QueryAttestersResponse{ - Attesters: []*networktypes.RegisteredAttester{ - { - ValidatorAddress: attesterValAddress, - AttesterInfo: attesterInfo, - }, - }, + Attesters: registeredAttesters, }) require.NoError(t, err) @@ -508,10 +657,6 @@ func mockAttesterNetworkQueries( return req.Path == "/evabci.network.v1.Query/AttesterSignatures" })).Return(&abci.ResponseQuery{Code: 0, Value: signaturesResponse}, nil).Maybe() - mockApp.On("Query", mock.Anything, mock.MatchedBy(func(req *abci.RequestQuery) bool { - return req.Path == "/evabci.network.v1.Query/AttesterInfo" - })).Return(&abci.ResponseQuery{Code: 0, Value: attesterInfoResponse}, nil).Maybe() - mockApp.On("Query", mock.Anything, mock.MatchedBy(func(req *abci.RequestQuery) bool { return req.Path == "/evabci.network.v1.Query/Attesters" })).Return(&abci.ResponseQuery{Code: 0, Value: attestersResponse}, nil).Maybe() From 5e0654a8d855cf849e2dd7dfa67fa9b8ecb12e42 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Wed, 17 Jun 2026 17:18:45 +0200 Subject: [PATCH 5/7] test: cover four-attester IBC quorum --- modules/network/keeper/keeper.go | 4 +- modules/network/keeper/msg_server_test.go | 11 +- pkg/rpc/core/blocks_test.go | 213 +++++++++++------- pkg/rpc/core/status.go | 10 +- pkg/rpc/core/utils.go | 6 +- pkg/rpc/core/utils_test.go | 42 ++++ server/attester_cmd.go | 194 ++++------------ server/attester_cmd_test.go | 26 +++ .../patches/app-wiring/patch-app-wiring.sh | 27 ++- tests/integration/gm_gaia_health_test.go | 209 ++++++++++++++--- 10 files changed, 469 insertions(+), 273 deletions(-) diff --git a/modules/network/keeper/keeper.go b/modules/network/keeper/keeper.go index 60494d03..5137e75b 100644 --- a/modules/network/keeper/keeper.go +++ b/modules/network/keeper/keeper.go @@ -282,8 +282,8 @@ func (k Keeper) CheckQuorum(ctx sdk.Context, votedPower, totalPower uint64) (boo return false, fmt.Errorf("invalid quorum fraction: %w", err) } - requiredPower := math.LegacyNewDec(int64(totalPower)).Mul(quorumFrac).TruncateInt().Uint64() - return votedPower >= requiredPower, nil + requiredPower := math.LegacyNewDec(int64(totalPower)).Mul(quorumFrac) + return math.LegacyNewDec(int64(votedPower)).GT(requiredPower), nil } // IsSoftConfirmed checks if a block at a given height is soft-confirmed diff --git a/modules/network/keeper/msg_server_test.go b/modules/network/keeper/msg_server_test.go index 73727fcf..3d5e26ae 100644 --- a/modules/network/keeper/msg_server_test.go +++ b/modules/network/keeper/msg_server_test.go @@ -367,6 +367,11 @@ func TestAttestWithMultipleAttestersTracksQuorumAndSignatures(t *testing.T) { consensusAddress: sdk.ValAddress("attester3").String(), vote: bytes.Repeat([]byte{0x03}, MinVoteLen), }, + { + authority: sdk.AccAddress("operator4").String(), + consensusAddress: sdk.ValAddress("attester4").String(), + vote: bytes.Repeat([]byte{0x04}, MinVoteLen), + }, } for _, attester := range attesters { _, err := server.JoinAttesterSet(ctx, &types.MsgJoinAttesterSet{ @@ -396,15 +401,15 @@ func TestAttestWithMultipleAttestersTracksQuorumAndSignatures(t *testing.T) { totalPower, err := keeper.GetTotalPower(ctx) require.NoError(t, err) - require.Equal(t, uint64(3), totalPower) + require.Equal(t, uint64(4), totalPower) quorumReached, err := keeper.CheckQuorum(ctx, votedPower, totalPower) require.NoError(t, err) - require.Equal(t, i >= 1, quorumReached) + require.Equal(t, i >= 2, quorumReached) lastAttestedHeight, err := keeper.GetLastAttestedHeight(ctx) require.NoError(t, err) - if i == 0 { + if i < 2 { require.Zero(t, lastAttestedHeight) } else { require.Equal(t, height, lastAttestedHeight) diff --git a/pkg/rpc/core/blocks_test.go b/pkg/rpc/core/blocks_test.go index 808644db..9416006f 100644 --- a/pkg/rpc/core/blocks_test.go +++ b/pkg/rpc/core/blocks_test.go @@ -345,88 +345,11 @@ func TestCommit_AttesterModeUsesAttesterValidatorSetForHeader(t *testing.T) { func TestCommit_AttesterModeBuildsMultiAttesterCommitWithAbsentSignatures(t *testing.T) { require := require.New(t) - sequencerPrivKey := ed25519.GenPrivKey() - sequencerPubKey := sequencerPrivKey.PubKey() - sequencerLibP2PPrivKey, err := crypto.UnmarshalEd25519PrivateKey(sequencerPrivKey.Bytes()) - require.NoError(err) - sequencerLibP2PPubKey := sequencerLibP2PPrivKey.GetPublic() - sequencerAddress := sequencerPubKey.Address().Bytes()[:20] - - mockSigner := &MockSigner{} - mockSigner.On("GetPublic").Return(sequencerLibP2PPubKey, nil) - mockSigner.On("GetAddress").Return(sequencerAddress, nil) - - env = setupTestEnvironment(mockSigner) - env.AttesterMode = true - defer func() { - env.AttesterMode = false - }() - - chainID := "test-chain" - blockHeight := uint64(1) - now := time.Now() - - sequencerValidatorHash, err := adapter.ValidatorHasherProvider()(sequencerAddress, sequencerLibP2PPubKey) - require.NoError(err) - blockData, rollkitHeader := createTestBlock(blockHeight, chainID, now, sequencerAddress, sequencerValidatorHash, 1) - - lastCommit, err := env.Adapter.GetLastCommit(context.Background(), blockHeight) - require.NoError(err) - abciHeader, err := adapter.ToABCIHeader(rollkitHeader, lastCommit) - require.NoError(err) + fixture := setupFourAttesterCommitTest(t, 3) + commitResult := fixture.commitResult - attesterPrivKeysByAddress := make(map[string]ed25519.PrivKey) - attesterValidators := make([]*cmttypes.Validator, 0, 4) - for range 4 { - privKey := ed25519.GenPrivKey() - pubKey := privKey.PubKey().(ed25519.PubKey) - attesterPrivKeysByAddress[string(pubKey.Address())] = privKey - attesterValidators = append(attesterValidators, cmttypes.NewValidator(pubKey, 1)) - } - attesterValidatorSet := cmttypes.NewValidatorSet(attesterValidators) - abciHeader.ValidatorsHash = attesterValidatorSet.Hash() - abciHeader.NextValidatorsHash = attesterValidatorSet.Hash() - abciHeader.ProposerAddress = attesterValidatorSet.Proposer.Address - - abciBlock, err := adapter.ToABCIBlock(abciHeader, lastCommit, blockData) - require.NoError(err) - blockParts, err := abciBlock.MakePartSet(cmttypes.BlockPartSizeBytes) - require.NoError(err) - blockID := &cmttypes.BlockID{ - Hash: abciHeader.Hash(), - PartSetHeader: blockParts.Header(), - } - require.NoError(env.Adapter.Store.SaveBlockID(context.Background(), blockHeight, blockID)) - - attesterQueries := make([]mockAttesterNetworkQuery, 0, len(attesterValidatorSet.Validators)) - for validatorIndex, validator := range attesterValidatorSet.Validators { - privKey := attesterPrivKeysByAddress[string(validator.Address)] - pubKey := privKey.PubKey().(ed25519.PubKey) - query := mockAttesterNetworkQuery{ - attesterValAddress: sdk.ValAddress(validator.Address).String(), - attesterPubKey: pubKey, - } - if validatorIndex < 3 { - query.voteBytes = signAttesterVoteWithIndex( - t, - chainID, - blockHeight, - rollkitHeader.Time(), - blockID, - int32(validatorIndex), - privKey, - ) - } - attesterQueries = append(attesterQueries, query) - } - mockAttesterNetworkQueriesForSet(t, env.Adapter.App.(*MockApp), attesterQueries) - - mockBlock(blockHeight, rollkitHeader, blockData, make([]byte, 64), sequencerLibP2PPubKey, sequencerAddress) - - commitResult := callCommitRPC(t, blockHeight) - - require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.ValidatorsHash)) - require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.NextValidatorsHash)) + require.Equal(fixture.attesterValidatorSet.Hash(), []byte(commitResult.ValidatorsHash)) + require.Equal(fixture.attesterValidatorSet.Hash(), []byte(commitResult.NextValidatorsHash)) require.Len(commitResult.Commit.Signatures, 4) commitSignatures := 0 @@ -434,7 +357,7 @@ func TestCommit_AttesterModeBuildsMultiAttesterCommitWithAbsentSignatures(t *tes for i, signature := range commitResult.Commit.Signatures { if i < 3 { require.Equal(cmttypes.BlockIDFlagCommit, signature.BlockIDFlag) - require.Equal(attesterValidatorSet.Validators[i].Address.Bytes(), []byte(signature.ValidatorAddress)) + require.Equal(fixture.attesterValidatorSet.Validators[i].Address.Bytes(), []byte(signature.ValidatorAddress)) commitSignatures++ continue } @@ -443,10 +366,33 @@ func TestCommit_AttesterModeBuildsMultiAttesterCommitWithAbsentSignatures(t *tes } require.Equal(3, commitSignatures) require.Equal(1, absentSignatures) - require.NoError(attesterValidatorSet.VerifyCommitLight( - chainID, + require.NoError(fixture.attesterValidatorSet.VerifyCommitLight( + fixture.chainID, commitResult.Commit.BlockID, - int64(blockHeight), + int64(fixture.blockHeight), + commitResult.Commit, + )) +} + +func TestCommit_AttesterModeWithTwoOfFourSignaturesIsNotIBCVerifiable(t *testing.T) { + require := require.New(t) + + fixture := setupFourAttesterCommitTest(t, 2) + commitResult := fixture.commitResult + + require.Len(commitResult.Commit.Signatures, 4) + for i, signature := range commitResult.Commit.Signatures { + if i < 2 { + require.Equal(cmttypes.BlockIDFlagCommit, signature.BlockIDFlag) + require.Equal(fixture.attesterValidatorSet.Validators[i].Address.Bytes(), []byte(signature.ValidatorAddress)) + continue + } + require.Equal(cmttypes.BlockIDFlagAbsent, signature.BlockIDFlag) + } + require.Error(fixture.attesterValidatorSet.VerifyCommitLight( + fixture.chainID, + commitResult.Commit.BlockID, + int64(fixture.blockHeight), commitResult.Commit, )) } @@ -540,6 +486,103 @@ func createTestBlock(height uint64, chainID string, baseTime time.Time, validato return blockData, rollkitHeader } +type fourAttesterCommitFixture struct { + chainID string + blockHeight uint64 + attesterValidatorSet *cmttypes.ValidatorSet + commitResult *ctypes.ResultCommit +} + +func setupFourAttesterCommitTest(t *testing.T, signedAttesters int) fourAttesterCommitFixture { + t.Helper() + require := require.New(t) + + sequencerPrivKey := ed25519.GenPrivKey() + sequencerPubKey := sequencerPrivKey.PubKey() + sequencerLibP2PPrivKey, err := crypto.UnmarshalEd25519PrivateKey(sequencerPrivKey.Bytes()) + require.NoError(err) + sequencerLibP2PPubKey := sequencerLibP2PPrivKey.GetPublic() + sequencerAddress := sequencerPubKey.Address().Bytes()[:20] + + mockSigner := &MockSigner{} + mockSigner.On("GetPublic").Return(sequencerLibP2PPubKey, nil) + mockSigner.On("GetAddress").Return(sequencerAddress, nil) + + env = setupTestEnvironment(mockSigner) + env.AttesterMode = true + t.Cleanup(func() { + env.AttesterMode = false + }) + + chainID := "test-chain" + blockHeight := uint64(1) + now := time.Now() + + sequencerValidatorHash, err := adapter.ValidatorHasherProvider()(sequencerAddress, sequencerLibP2PPubKey) + require.NoError(err) + blockData, rollkitHeader := createTestBlock(blockHeight, chainID, now, sequencerAddress, sequencerValidatorHash, 1) + + lastCommit, err := env.Adapter.GetLastCommit(context.Background(), blockHeight) + require.NoError(err) + abciHeader, err := adapter.ToABCIHeader(rollkitHeader, lastCommit) + require.NoError(err) + + attesterPrivKeysByAddress := make(map[string]ed25519.PrivKey) + attesterValidators := make([]*cmttypes.Validator, 0, 4) + for range 4 { + privKey := ed25519.GenPrivKey() + pubKey := privKey.PubKey().(ed25519.PubKey) + attesterPrivKeysByAddress[string(pubKey.Address())] = privKey + attesterValidators = append(attesterValidators, cmttypes.NewValidator(pubKey, 1)) + } + attesterValidatorSet := cmttypes.NewValidatorSet(attesterValidators) + abciHeader.ValidatorsHash = attesterValidatorSet.Hash() + abciHeader.NextValidatorsHash = attesterValidatorSet.Hash() + abciHeader.ProposerAddress = attesterValidatorSet.Proposer.Address + + abciBlock, err := adapter.ToABCIBlock(abciHeader, lastCommit, blockData) + require.NoError(err) + blockParts, err := abciBlock.MakePartSet(cmttypes.BlockPartSizeBytes) + require.NoError(err) + blockID := &cmttypes.BlockID{ + Hash: abciHeader.Hash(), + PartSetHeader: blockParts.Header(), + } + require.NoError(env.Adapter.Store.SaveBlockID(context.Background(), blockHeight, blockID)) + + attesterQueries := make([]mockAttesterNetworkQuery, 0, len(attesterValidatorSet.Validators)) + for validatorIndex, validator := range attesterValidatorSet.Validators { + privKey := attesterPrivKeysByAddress[string(validator.Address)] + pubKey := privKey.PubKey().(ed25519.PubKey) + query := mockAttesterNetworkQuery{ + attesterValAddress: sdk.ValAddress(validator.Address).String(), + attesterPubKey: pubKey, + } + if validatorIndex < signedAttesters { + query.voteBytes = signAttesterVoteWithIndex( + t, + chainID, + blockHeight, + rollkitHeader.Time(), + blockID, + int32(validatorIndex), + privKey, + ) + } + attesterQueries = append(attesterQueries, query) + } + mockAttesterNetworkQueriesForSet(t, env.Adapter.App.(*MockApp), attesterQueries) + + mockBlock(blockHeight, rollkitHeader, blockData, make([]byte, 64), sequencerLibP2PPubKey, sequencerAddress) + + return fourAttesterCommitFixture{ + chainID: chainID, + blockHeight: blockHeight, + attesterValidatorSet: attesterValidatorSet, + commitResult: callCommitRPC(t, blockHeight), + } +} + func signAttesterVote( t *testing.T, chainID string, diff --git a/pkg/rpc/core/status.go b/pkg/rpc/core/status.go index cb2ed803..c70e2c24 100644 --- a/pkg/rpc/core/status.go +++ b/pkg/rpc/core/status.go @@ -1,6 +1,7 @@ package core import ( + "context" "errors" "fmt" "time" @@ -172,6 +173,10 @@ func isAttesterNode() bool { // getLastAttestedHeight returns the highest block height attested by this validator func getLastAttestedHeight(ctx *rpctypes.Context) (uint64, error) { + return getLastAttestedHeightFromContext(ctx.Context()) +} + +func getLastAttestedHeightFromContext(ctx context.Context) (uint64, error) { // Create protobuf request for LastAttestedHeight query (O(1) operation) req := &networktypes.QueryLastAttestedHeightRequest{} @@ -182,7 +187,7 @@ func getLastAttestedHeight(ctx *rpctypes.Context) (uint64, error) { } // Use ABCI Query with the new endpoint - res, err := env.Adapter.App.Query(ctx.Context(), &abci.RequestQuery{ + res, err := env.Adapter.App.Query(ctx, &abci.RequestQuery{ Path: "/evabci.network.v1.Query/LastAttestedHeight", Data: reqBytes, }) @@ -199,6 +204,9 @@ func getLastAttestedHeight(ctx *rpctypes.Context) (uint64, error) { if err := response.Unmarshal(res.Value); err != nil { return 0, fmt.Errorf("failed to unmarshal response: %w", err) } + if response.Height < 0 { + return 0, fmt.Errorf("last attested height cannot be negative: %d", response.Height) + } return uint64(response.Height), nil } diff --git a/pkg/rpc/core/utils.go b/pkg/rpc/core/utils.go index b17ef06e..e489562a 100644 --- a/pkg/rpc/core/utils.go +++ b/pkg/rpc/core/utils.go @@ -24,7 +24,11 @@ func normalizeHeight(ctx context.Context, height *int64) (uint64, error) { if height == nil || *height < 0 { // Handle negative heights if they have special meaning // (e.g., -1 for latest) - heightValue, err = env.Adapter.RollkitStore.Height(ctx) + if env != nil && env.AttesterMode { + heightValue, err = getLastAttestedHeightFromContext(ctx) + } else { + heightValue, err = env.Adapter.RollkitStore.Height(ctx) + } if err != nil { return 0, fmt.Errorf("failed to get current height: %w", err) } diff --git a/pkg/rpc/core/utils_test.go b/pkg/rpc/core/utils_test.go index 81946547..f608048d 100644 --- a/pkg/rpc/core/utils_test.go +++ b/pkg/rpc/core/utils_test.go @@ -1,14 +1,24 @@ package core import ( + "context" "crypto/sha256" "encoding/hex" "fmt" "strings" "testing" + abci "github.com/cometbft/cometbft/abci/types" + cmtlog "github.com/cometbft/cometbft/libs/log" + "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + rollkitmocks "github.com/evstack/ev-node/test/mocks" + + networktypes "github.com/evstack/ev-abci/modules/network/types" + "github.com/evstack/ev-abci/pkg/adapter" ) func TestTruncateNodeID(t *testing.T) { @@ -58,3 +68,35 @@ func TestTruncateNodeID(t *testing.T) { assert.Equal(t, exactLengthNodeIDStr, truncatedNodeID, "Truncated Node ID should be the same as input for exact length") }) } + +func TestNormalizeHeightAttesterModeUsesLastAttestedHeight(t *testing.T) { + originalEnv := env + t.Cleanup(func() { env = originalEnv }) + + mockStore := new(rollkitmocks.MockStore) + mockApp := new(MockApp) + env = &Environment{ + Adapter: &adapter.Adapter{ + RollkitStore: mockStore, + App: mockApp, + }, + AttesterMode: true, + Logger: cmtlog.NewNopLogger(), + } + + queryResponse, err := proto.Marshal(&networktypes.QueryLastAttestedHeightResponse{ + Height: 14, + }) + require.NoError(t, err) + + mockApp.On("Query", mock.Anything, mock.MatchedBy(func(req *abci.RequestQuery) bool { + return req.Path == "/evabci.network.v1.Query/LastAttestedHeight" + })).Return(&abci.ResponseQuery{Code: 0, Value: queryResponse}, nil).Once() + + height, err := normalizeHeight(context.Background(), nil) + + require.NoError(t, err) + require.Equal(t, uint64(14), height) + mockStore.AssertNotCalled(t, "Height", mock.Anything) + mockApp.AssertExpectations(t) +} diff --git a/server/attester_cmd.go b/server/attester_cmd.go index 95a8a66b..8d6ac3b4 100644 --- a/server/attester_cmd.go +++ b/server/attester_cmd.go @@ -1,12 +1,10 @@ package server import ( - "bytes" "context" "encoding/hex" "encoding/json" "fmt" - "io" "net/http" "net/url" "os" @@ -252,110 +250,20 @@ func pullBlocksAndAttest( Timeout: 10 * time.Second, } - resp, err := httpClient.Get(fmt.Sprintf("http://%s/status", parsed.Host)) + currentHeight, err := queryLatestProductionHeight(ctx, httpClient, parsed.Host) if err != nil { - return fmt.Errorf("error querying status: %v", err) - } - - var statusResponse struct { - Result struct { - SyncInfo struct { - LatestBlockHeight string `json:"latest_block_height"` - } `json:"sync_info"` - } `json:"result"` - } - - if err := json.NewDecoder(resp.Body).Decode(&statusResponse); err != nil { - _ = resp.Body.Close() - return fmt.Errorf("error parsing status response: %v", err) - } - _ = resp.Body.Close() - - currentHeight, err := strconv.ParseInt(statusResponse.Result.SyncInfo.LatestBlockHeight, 10, 64) - if err != nil { - return fmt.Errorf("error parsing current height: %v", err) + return fmt.Errorf("query current block height: %w", err) } fmt.Printf("šŸ“Š Current blockchain height: %d\n", currentHeight) - fmt.Printf("šŸ“ Attesting blocks 1-%d...\n", currentHeight) - - failedBlocks := make(map[int64]int) - maxRetries := 3 - - for height := int64(1); height <= currentHeight; height++ { - select { - case <-ctx.Done(): - return nil - default: - if height%10 == 0 || height == 1 || height == currentHeight { - fmt.Printf("šŸ“¦ Attesting blocks... %d/%d\n", height, currentHeight) - } - - err = submitAttestation(ctx, config, height, valAddr, senderKey, pv, clientCtx) - if err != nil { - fmt.Printf("āš ļø Error attesting block %d: %v\n", height, err) - failedBlocks[height] = 1 - continue - } - - time.Sleep(time.Millisecond * 100) - } - } - - if len(failedBlocks) > 0 { - fmt.Printf("\nšŸ”„ Retrying %d failed blocks...\n", len(failedBlocks)) - for retryRound := 1; retryRound <= maxRetries && len(failedBlocks) > 0; retryRound++ { - fmt.Printf(" Round %d/%d - %d blocks remaining\n", retryRound, maxRetries, len(failedBlocks)) - - blocksToRetry := make([]int64, 0, len(failedBlocks)) - for height := range failedBlocks { - blocksToRetry = append(blocksToRetry, height) - } - - for i := 0; i < len(blocksToRetry); i++ { - for j := i + 1; j < len(blocksToRetry); j++ { - if blocksToRetry[i] > blocksToRetry[j] { - blocksToRetry[i], blocksToRetry[j] = blocksToRetry[j], blocksToRetry[i] - } - } - } - - for _, height := range blocksToRetry { - select { - case <-ctx.Done(): - return nil - default: - fmt.Printf(" šŸ”„ Retrying block %d (attempt %d)...\n", height, failedBlocks[height]+1) - err = submitAttestation(ctx, config, height, valAddr, senderKey, pv, clientCtx) - if err != nil { - failedBlocks[height]++ - if failedBlocks[height] >= maxRetries { - fmt.Printf(" āŒ Block %d failed after %d attempts\n", height, maxRetries) - delete(failedBlocks, height) - } - } else { - fmt.Printf(" āœ… Block %d attested successfully\n", height) - delete(failedBlocks, height) - } - - time.Sleep(time.Millisecond * 300) - } - } - - if len(failedBlocks) > 0 { - time.Sleep(time.Second * 2) - } - } - - if len(failedBlocks) > 0 { - fmt.Printf("\nāŒ Failed to attest %d blocks after all retries\n", len(failedBlocks)) - for height := range failedBlocks { - fmt.Printf(" - Block %d\n", height) - } + if currentHeight > 0 { + fmt.Printf("šŸ“ Attesting latest block %d...\n", currentHeight) + if err := submitAttestation(ctx, config, currentHeight, valAddr, senderKey, pv, clientCtx); err != nil { + fmt.Printf("āš ļø Error attesting latest block %d: %v\n", currentHeight, err) } } - fmt.Printf("āœ… Finished historical blocks. Watching for new blocks...\n") + fmt.Printf("āœ… Finished startup attestation. Watching for new blocks...\n") lastAttested := currentHeight @@ -364,58 +272,21 @@ func pullBlocksAndAttest( case <-ctx.Done(): return nil default: - resp, err := httpClient.Get(fmt.Sprintf("http://%s/block", parsed.Host)) + height, err := queryLatestProductionHeight(ctx, httpClient, parsed.Host) if err != nil { - fmt.Printf("Error querying block: %v\n", err) - time.Sleep(time.Second / 10) - continue - } - - var blockResponse struct { - Result struct { - Block struct { - Header struct { - Height string `json:"height"` - AppHash string `json:"app_hash"` - } `json:"header"` - } `json:"block"` - } `json:"result"` - } - - var buf bytes.Buffer - if err := json.NewDecoder(io.TeeReader(resp.Body, &buf)).Decode(&blockResponse); err != nil { - fmt.Printf("Error parsing response: %v: %s\n", err, buf.String()) - _ = resp.Body.Close() - time.Sleep(time.Second / 10) - continue - } - _ = resp.Body.Close() - - heightStr := blockResponse.Result.Block.Header.Height - if heightStr == "" { - if config.Verbose { - fmt.Println("Height field is empty in response, retrying...") - } - time.Sleep(time.Second / 10) - continue - } - height, err := strconv.ParseInt(heightStr, 10, 64) - if err != nil { - fmt.Printf("Error parsing height: %v\n", err) + fmt.Printf("Error querying latest block height: %v\n", err) time.Sleep(time.Second / 10) continue } if height > lastAttested { - for missedHeight := lastAttested + 1; missedHeight <= height; missedHeight++ { - fmt.Printf("šŸ“¦ New block %d - attesting...\n", missedHeight) - - err = submitAttestation(ctx, config, missedHeight, valAddr, senderKey, pv, clientCtx) - if err != nil { - fmt.Printf("āš ļø Error submitting attestation for block %d: %v\n", missedHeight, err) - continue - } - fmt.Printf("āœ… Attested block %d\n", missedHeight) + fmt.Printf("šŸ“¦ New block %d - attesting...\n", height) + + err = submitAttestation(ctx, config, height, valAddr, senderKey, pv, clientCtx) + if err != nil { + fmt.Printf("āš ļø Error submitting attestation for block %d: %v\n", height, err) + } else { + fmt.Printf("āœ… Attested block %d\n", height) } lastAttested = height @@ -426,6 +297,39 @@ func pullBlocksAndAttest( } } +func queryLatestProductionHeight(ctx context.Context, httpClient *http.Client, rpcHost string) (int64, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://%s/blockchain", rpcHost), nil) + if err != nil { + return 0, fmt.Errorf("creating blockchain request: %w", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return 0, fmt.Errorf("querying blockchain: %w", err) + } + defer func() { + _ = resp.Body.Close() + }() + + var blockchainResponse struct { + Result struct { + LastHeight string `json:"last_height"` + } `json:"result"` + } + if err := json.NewDecoder(resp.Body).Decode(&blockchainResponse); err != nil { + return 0, fmt.Errorf("decoding blockchain response: %w", err) + } + if blockchainResponse.Result.LastHeight == "" { + return 0, nil + } + + height, err := strconv.ParseInt(blockchainResponse.Result.LastHeight, 10, 64) + if err != nil { + return 0, fmt.Errorf("parsing latest block height: %w", err) + } + return height, nil +} + var accSeq uint64 = 0 func broadcastTx( diff --git a/server/attester_cmd_test.go b/server/attester_cmd_test.go index 0da5afbe..3820fd07 100644 --- a/server/attester_cmd_test.go +++ b/server/attester_cmd_test.go @@ -4,7 +4,9 @@ import ( "context" "net/http" "net/http/httptest" + "net/url" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -49,6 +51,30 @@ func TestPrivateKeyFromMnemonic(t *testing.T) { } } +func TestQueryLatestProductionHeightUsesBlockchainEndpoint(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/blockchain", r.URL.Path) + _, _ = w.Write([]byte(`{ + "result": { + "last_height": "42" + } + }`)) + })) + defer server.Close() + + parsed, err := url.Parse(server.URL) + require.NoError(t, err) + + height, err := queryLatestProductionHeight( + context.Background(), + &http.Client{Timeout: time.Second}, + parsed.Host, + ) + + require.NoError(t, err) + require.Equal(t, int64(42), height) +} + func TestGetOriginalBlockID(t *testing.T) { t.Run("height 0", func(t *testing.T) { blockID, err := getOriginalBlockID(context.Background(), "tcp://localhost:26657", 0) diff --git a/tests/integration/docker/patches/app-wiring/patch-app-wiring.sh b/tests/integration/docker/patches/app-wiring/patch-app-wiring.sh index 3235d06f..cfe6e768 100755 --- a/tests/integration/docker/patches/app-wiring/patch-app-wiring.sh +++ b/tests/integration/docker/patches/app-wiring/patch-app-wiring.sh @@ -161,10 +161,19 @@ add_import "$APP_GO" $'\t_ "github.com/evstack/ev-abci/modules/network" // impor # Add keeper field if ! grep -q "NetworkKeeper networkkeeper.Keeper" "$APP_GO"; then echo "[patch-app-wiring] Adding NetworkKeeper field" - awk '/# stargate\/app\/keeperDeclaration/ { - print "\tNetworkKeeper networkkeeper.Keeper" - } - { print }' "$APP_GO" > "${APP_GO}.tmp" && mv "${APP_GO}.tmp" "$APP_GO" + if grep -q "# stargate/app/keeperDeclaration" "$APP_GO"; then + awk '/# stargate\/app\/keeperDeclaration/ { + print "\tNetworkKeeper networkkeeper.Keeper" + } + { print }' "$APP_GO" > "${APP_GO}.tmp" && mv "${APP_GO}.tmp" "$APP_GO" + else + awk '/^[[:space:]]*ParamsKeeper[[:space:]]+paramskeeper\.Keeper/ { + print + print "\tNetworkKeeper networkkeeper.Keeper" + next + } + { print }' "$APP_GO" > "${APP_GO}.tmp" && mv "${APP_GO}.tmp" "$APP_GO" + fi fi # Add to depinject.Inject @@ -204,6 +213,16 @@ if ! grep -q "Name:.*networktypes.ModuleName" "$APP_CONFIG_GO"; then errors=$((errors + 1)) fi +if ! grep -q "NetworkKeeper networkkeeper.Keeper" "$APP_GO"; then + echo "[patch-app-wiring] ERROR: NetworkKeeper field missing in app.go!" >&2 + errors=$((errors + 1)) +fi + +if ! grep -q "&app.NetworkKeeper" "$APP_GO"; then + echo "[patch-app-wiring] ERROR: NetworkKeeper not injected in app.go!" >&2 + errors=$((errors + 1)) +fi + # Check each list for list in BeginBlockers EndBlockers InitGenesis; do if ! awk -v list="$list" ' diff --git a/tests/integration/gm_gaia_health_test.go b/tests/integration/gm_gaia_health_test.go index e490cf84..9e2c5716 100644 --- a/tests/integration/gm_gaia_health_test.go +++ b/tests/integration/gm_gaia_health_test.go @@ -5,8 +5,10 @@ import ( "context" "encoding/json" "fmt" + "net/http" "os" "path/filepath" + "strconv" "testing" "time" @@ -20,6 +22,7 @@ import ( "github.com/celestiaorg/tastora/framework/testutil/wait" "github.com/celestiaorg/tastora/framework/types" cmprivval "github.com/cometbft/cometbft/privval" + "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module/testutil" @@ -72,39 +75,22 @@ func (s *DockerIntegrationTestSuite) TestAttesterSystem() { }) s.Require().NoError(err) - kr, err := gmChain.GetNodes()[0].GetKeyring() - require.NoError(s.T(), err) - - keys, err := kr.List() - require.NoError(s.T(), err) - s.T().Logf("Available keys in keyring: %d", len(keys)) + const attesterCount = 4 + operatorArmoredKeys := s.createAttesterOperatorArmors(ctx, gmChain, attesterCount) + for i, operatorArmoredKey := range operatorArmoredKeys { + attesterConfig, attesterNode := s.getAttester(ctx, gmChain, operatorArmoredKey, i) - // Log all available keys and find validator key - var validatorKey *keyring.Record - for i, key := range keys { - keyAddr, _ := key.GetAddress() - s.T().Logf("Key %d: Name=%s, Address=%s", i, key.Name, keyAddr.String()) + s.T().Logf("Initializing attester node %s", attesterNode.Name()) + err = attesterNode.Init(ctx, attesterConfig.ChainID, attesterConfig.GMNodeURL) + require.NoError(s.T(), err) - if key.Name == "validator" { - validatorKey = key - } + s.T().Logf("Starting attester node %s", attesterNode.Name()) + err = attesterNode.Start(ctx, attesterConfig) + require.NoError(s.T(), err) + s.T().Logf("Attester node %s started successfully", attesterNode.Name()) } - - s.Require().NotNil(validatorKey, "validator key not found in keyring") - - operatorArmoredKey, err := kr.ExportPrivKeyArmor("validator", "") - s.Require().NoError(err, "failed to export operator private key") - - attesterConfig, attesterNode := s.getAttester(ctx, gmChain, operatorArmoredKey) - - s.T().Logf("Initializing attester node %s", attesterNode.Name()) - err = attesterNode.Init(ctx, attesterConfig.ChainID, attesterConfig.GMNodeURL) - require.NoError(s.T(), err) - - s.T().Logf("Starting attester node %s", attesterNode.Name()) - err = attesterNode.Start(ctx, attesterConfig) - require.NoError(s.T(), err) - s.T().Log("Attester node started successfully") + s.waitForRegisteredAttesters(ctx, gmChain, attesterCount) + s.waitForLastAttestedHeight(ctx, gmChain) hermes, err := relayer.NewHermes(ctx, s.dockerClient, s.T().Name(), s.networkID, 0, s.logger) require.NoError(s.T(), err, "failed to create hermes relayer") @@ -127,7 +113,40 @@ func (s *DockerIntegrationTestSuite) TestAttesterSystem() { s.testIBCTransfers(ctx, s.celestiaChain, gmChain, channel, hermes) } -func (s *DockerIntegrationTestSuite) getAttester(ctx context.Context, gmChain *cosmos.Chain, operatorArmoredKey string) (AttesterConfig, *Attester) { +func (s *DockerIntegrationTestSuite) createAttesterOperatorArmors(ctx context.Context, gmChain *cosmos.Chain, count int) []string { + kr, err := gmChain.GetNodes()[0].GetKeyring() + require.NoError(s.T(), err) + + keys, err := kr.List() + require.NoError(s.T(), err) + s.T().Logf("Available keys in keyring before creating attesters: %d", len(keys)) + + armors := make([]string, 0, count) + for i := range count { + keyName := fmt.Sprintf("attester-operator-%d", i) + record, _, err := kr.NewMnemonic( + keyName, + keyring.English, + sdk.FullFundraiserPath, + keyring.DefaultBIP39Passphrase, + hd.Secp256k1, + ) + require.NoError(s.T(), err, "failed to create attester operator key %s", keyName) + + operatorAddr, err := record.GetAddress() + require.NoError(s.T(), err) + + armor, err := kr.ExportPrivKeyArmor(keyName, "") + require.NoError(s.T(), err, "failed to export attester operator key %s", keyName) + + s.T().Logf("Created attester operator key %s with address %s", keyName, operatorAddr.String()) + armors = append(armors, armor) + } + + return armors +} + +func (s *DockerIntegrationTestSuite) getAttester(ctx context.Context, gmChain *cosmos.Chain, operatorArmoredKey string, index int) (AttesterConfig, *Attester) { // Create attester configuration attesterConfig := DefaultAttesterConfig() @@ -163,7 +182,7 @@ func (s *DockerIntegrationTestSuite) getAttester(ctx context.Context, gmChain *c attesterConfig.GMNodeURL = fmt.Sprintf("tcp://%s:26657", gmNodeInfo.Internal.Hostname) // Create and start the attester - attesterNode, err := NewAttester(ctx, s.dockerClient, s.T().Name(), s.networkID, 0, s.logger) + attesterNode, err := NewAttester(ctx, s.dockerClient, s.T().Name(), s.networkID, index, s.logger) require.NoError(s.T(), err) attesterPrivValidatorKeyJSON, attesterPrivValidatorStateJSON := newDistinctAttesterPrivValidatorFiles(s.T(), sequencerPrivValidatorKeyJSON) @@ -207,6 +226,132 @@ func (s *DockerIntegrationTestSuite) getAttester(ctx context.Context, gmChain *c return attesterConfig, attesterNode } +func (s *DockerIntegrationTestSuite) waitForRegisteredAttesters(ctx context.Context, gmChain *cosmos.Chain, expected int) { + gmNode := gmChain.GetNodes()[0] + networkInfo, err := gmNode.GetNetworkInfo(ctx) + require.NoError(s.T(), err) + + endpoint := fmt.Sprintf("http://%s/evabci/network/v1/attesters", networkInfo.External.APIAddress()) + httpClient := &http.Client{Timeout: 5 * time.Second} + lastCount := 0 + + err = wait.ForCondition(ctx, 2*time.Minute, 2*time.Second, func() (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return false, err + } + + resp, err := httpClient.Do(req) + if err != nil { + return false, nil + } + defer func() { + _ = resp.Body.Close() + }() + if resp.StatusCode != http.StatusOK { + return false, nil + } + + var attestersResponse struct { + Attesters []json.RawMessage `json:"attesters"` + } + if err := json.NewDecoder(resp.Body).Decode(&attestersResponse); err != nil { + return false, nil + } + + lastCount = len(attestersResponse.Attesters) + return lastCount >= expected, nil + }) + require.NoError(s.T(), err, "expected at least %d registered attesters, got %d", expected, lastCount) + s.T().Logf("Registered attester set reached %d entries", lastCount) +} + +func (s *DockerIntegrationTestSuite) waitForLastAttestedHeight(ctx context.Context, gmChain *cosmos.Chain) int64 { + const maxAttestedHeightLag = int64(5) + + gmNode := gmChain.GetNodes()[0] + networkInfo, err := gmNode.GetNetworkInfo(ctx) + require.NoError(s.T(), err) + + attestedHeightEndpoint := fmt.Sprintf("http://%s/evabci/network/v1/last-attested-height", networkInfo.External.APIAddress()) + productionHeightEndpoint := fmt.Sprintf("http://%s/blockchain", networkInfo.External.RPCAddress()) + httpClient := &http.Client{Timeout: 5 * time.Second} + lastHeight := int64(0) + productionHeight := int64(0) + + err = wait.ForCondition(ctx, 3*time.Minute, 2*time.Second, func() (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, attestedHeightEndpoint, nil) + if err != nil { + return false, err + } + + resp, err := httpClient.Do(req) + if err != nil { + return false, nil + } + defer func() { + _ = resp.Body.Close() + }() + if resp.StatusCode != http.StatusOK { + return false, nil + } + + var lastAttestedHeightResponse struct { + Height string `json:"height"` + } + if err := json.NewDecoder(resp.Body).Decode(&lastAttestedHeightResponse); err != nil { + return false, nil + } + + lastHeight, err = strconv.ParseInt(lastAttestedHeightResponse.Height, 10, 64) + if err != nil { + return false, nil + } + + req, err = http.NewRequestWithContext(ctx, http.MethodGet, productionHeightEndpoint, nil) + if err != nil { + return false, err + } + + resp, err = httpClient.Do(req) + if err != nil { + return false, nil + } + defer func() { + _ = resp.Body.Close() + }() + if resp.StatusCode != http.StatusOK { + return false, nil + } + + var blockchainResponse struct { + Result struct { + LastHeight string `json:"last_height"` + } `json:"result"` + } + if err := json.NewDecoder(resp.Body).Decode(&blockchainResponse); err != nil { + return false, nil + } + + productionHeight, err = strconv.ParseInt(blockchainResponse.Result.LastHeight, 10, 64) + if err != nil { + return false, nil + } + + lag := productionHeight - lastHeight + return lastHeight > 0 && lag >= 0 && lag <= maxAttestedHeightLag, nil + }) + require.NoError( + s.T(), + err, + "expected attester quorum to catch up near production height, last attested %d, production %d", + lastHeight, + productionHeight, + ) + s.T().Logf("Last attested height reached %d with production height %d", lastHeight, productionHeight) + return lastHeight +} + func deriveAttesterAccountFromArmor(armoredKey string) (sdk.AccAddress, error) { // Create a temporary in-memory keyring for importing testEncCfg := testutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, bank.AppModuleBasic{}) From b98f792ec50354b960c5aa62c216aa8bc004fac9 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Fri, 19 Jun 2026 10:22:10 +0200 Subject: [PATCH 6/7] fix: propagate attester signature query errors --- pkg/rpc/core/blocks.go | 6 ++--- pkg/rpc/core/blocks_test.go | 54 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/pkg/rpc/core/blocks.go b/pkg/rpc/core/blocks.go index ec469867..2bd5a4f2 100644 --- a/pkg/rpc/core/blocks.go +++ b/pkg/rpc/core/blocks.go @@ -414,13 +414,11 @@ func getAttesterSignatures(ctx context.Context, height int64) (map[string][]byte Data: signaturesReq, }) if err != nil { - env.Logger.Debug("AttesterSignatures query failed", "error", err) - return make(map[string][]byte), nil + return nil, fmt.Errorf("query attester signatures: %w", err) } if result.Code != 0 { - env.Logger.Info("AttesterSignatures not found", "height", height, "code", result.Code) - return make(map[string][]byte), nil + return nil, fmt.Errorf("attester signatures query failed: code %d, log: %s", result.Code, result.Log) } var signaturesResp networktypes.QueryAttesterSignaturesResponse diff --git a/pkg/rpc/core/blocks_test.go b/pkg/rpc/core/blocks_test.go index 9416006f..2191ebd0 100644 --- a/pkg/rpc/core/blocks_test.go +++ b/pkg/rpc/core/blocks_test.go @@ -2,6 +2,7 @@ package core import ( "context" + "errors" "testing" "time" @@ -397,6 +398,59 @@ func TestCommit_AttesterModeWithTwoOfFourSignaturesIsNotIBCVerifiable(t *testing )) } +func TestGetAttesterSignaturesReturnsQueryError(t *testing.T) { + require := require.New(t) + + mockApp := new(MockApp) + previousEnv := env + t.Cleanup(func() { + env = previousEnv + mockApp.AssertExpectations(t) + }) + + env = &Environment{ + Adapter: &adapter.Adapter{App: mockApp}, + Logger: cmtlog.NewNopLogger(), + } + + mockApp.On("Query", mock.Anything, mock.MatchedBy(func(req *abci.RequestQuery) bool { + return req.Path == "/evabci.network.v1.Query/AttesterSignatures" + })).Return(nil, errors.New("query unavailable")).Once() + + signatures, err := getAttesterSignatures(context.Background(), 10) + + require.Nil(signatures) + require.ErrorContains(err, "query attester signatures") + require.ErrorContains(err, "query unavailable") +} + +func TestGetAttesterSignaturesReturnsNonOKQueryCode(t *testing.T) { + require := require.New(t) + + mockApp := new(MockApp) + previousEnv := env + t.Cleanup(func() { + env = previousEnv + mockApp.AssertExpectations(t) + }) + + env = &Environment{ + Adapter: &adapter.Adapter{App: mockApp}, + Logger: cmtlog.NewNopLogger(), + } + + mockApp.On("Query", mock.Anything, mock.MatchedBy(func(req *abci.RequestQuery) bool { + return req.Path == "/evabci.network.v1.Query/AttesterSignatures" + })).Return(&abci.ResponseQuery{Code: 7, Log: "signature store unavailable"}, nil).Once() + + signatures, err := getAttesterSignatures(context.Background(), 10) + + require.Nil(signatures) + require.ErrorContains(err, "attester signatures query failed") + require.ErrorContains(err, "code 7") + require.ErrorContains(err, "signature store unavailable") +} + func TestBlockByHash_AttesterModeUsesAttesterValidatorSetForHeader(t *testing.T) { require := require.New(t) From e673bc09eeeaab99572df8c17e59b42122fbe64d Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Fri, 19 Jun 2026 11:25:51 +0200 Subject: [PATCH 7/7] fix: use one attester vote address source --- server/attester_cmd.go | 64 +++++++++++++++++++++++-------------- server/attester_cmd_test.go | 41 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/server/attester_cmd.go b/server/attester_cmd.go index 8d6ac3b4..da2117e4 100644 --- a/server/attester_cmd.go +++ b/server/attester_cmd.go @@ -557,55 +557,71 @@ func submitAttestation( return fmt.Errorf("getting Evolve block: %w", err) } + voteBytes, err := buildAttesterVoteBytes(config.ChainID, height, blockID, header.Time(), pv) + if err != nil { + return err + } + + authorityAddr := sdk.AccAddress(senderKey.PubKey().Address()).String() + msg := networktypes.NewMsgAttest( + authorityAddr, + valAddr.String(), + height, + voteBytes, + ) + + txHash, err := broadcastTx(ctx, config, msg, senderKey, clientCtx) + if err != nil { + return fmt.Errorf("broadcast attest tx: %w", err) + } + + if config.Verbose { + fmt.Printf("Attestation submitted for block %d with hash: %s\n", height, txHash) + } + return nil +} + +func buildAttesterVoteBytes( + chainID string, + height int64, + blockID cmtproto.BlockID, + timestamp time.Time, + pv *pvm.FilePV, +) ([]byte, error) { + validatorAddress := pv.Key.Address vote := cmtproto.Vote{ Type: cmtproto.PrecommitType, Height: height, BlockID: blockID, Round: 0, - Timestamp: header.Time(), - ValidatorAddress: pv.Key.PrivKey.PubKey().Address(), + Timestamp: timestamp, + ValidatorAddress: validatorAddress, ValidatorIndex: 0, } - signBytes := cmttypes.VoteSignBytes(config.ChainID, &vote) + signBytes := cmttypes.VoteSignBytes(chainID, &vote) signature, err := pv.Key.PrivKey.Sign(signBytes) if err != nil { - return fmt.Errorf("signing payload: %w", err) + return nil, fmt.Errorf("signing payload: %w", err) } attesterVote := &cmtproto.Vote{ Type: cmtproto.PrecommitType, - ValidatorAddress: pv.Key.Address, + ValidatorAddress: validatorAddress, Height: height, Round: 0, BlockID: blockID, - Timestamp: header.Time(), + Timestamp: timestamp, Signature: signature, } voteBytes, err := proto.Marshal(attesterVote) if err != nil { - return fmt.Errorf("marshal vote: %w", err) - } - - authorityAddr := sdk.AccAddress(senderKey.PubKey().Address()).String() - msg := networktypes.NewMsgAttest( - authorityAddr, - valAddr.String(), - height, - voteBytes, - ) - - txHash, err := broadcastTx(ctx, config, msg, senderKey, clientCtx) - if err != nil { - return fmt.Errorf("broadcast attest tx: %w", err) + return nil, fmt.Errorf("marshal vote: %w", err) } - if config.Verbose { - fmt.Printf("Attestation submitted for block %d with hash: %s\n", height, txHash) - } - return nil + return voteBytes, nil } type evolveBlockResponse struct { diff --git a/server/attester_cmd_test.go b/server/attester_cmd_test.go index 3820fd07..bbc26bea 100644 --- a/server/attester_cmd_test.go +++ b/server/attester_cmd_test.go @@ -8,6 +8,11 @@ import ( "testing" "time" + "github.com/cometbft/cometbft/crypto/ed25519" + pvm "github.com/cometbft/cometbft/privval" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -178,3 +183,39 @@ func TestGetEvolveBlockReadsHeaderAndBlockIDFromSingleRPCResponse(t *testing.T) }, blockID.Hash) require.Equal(t, uint32(3), blockID.PartSetHeader.Total) } + +func TestBuildAttesterVoteBytesSignsSerializedValidatorAddress(t *testing.T) { + privKey := ed25519.GenPrivKey() + validatorAddress := cmttypes.Address(bytesOf(0xAB, 20)) + pv := &pvm.FilePV{ + Key: pvm.FilePVKey{ + Address: validatorAddress, + PubKey: privKey.PubKey(), + PrivKey: privKey, + }, + } + blockID := cmtproto.BlockID{ + Hash: bytesOf(0xCD, 32), + PartSetHeader: cmtproto.PartSetHeader{ + Total: 1, + Hash: bytesOf(0xEF, 32), + }, + } + timestamp := time.Date(2026, 6, 19, 12, 0, 0, 0, time.UTC) + + voteBytes, err := buildAttesterVoteBytes("test-chain", 7, blockID, timestamp, pv) + require.NoError(t, err) + + var vote cmtproto.Vote + require.NoError(t, proto.Unmarshal(voteBytes, &vote)) + require.Equal(t, validatorAddress, cmttypes.Address(vote.ValidatorAddress)) + require.True(t, privKey.PubKey().VerifySignature(cmttypes.VoteSignBytes("test-chain", &vote), vote.Signature)) +} + +func bytesOf(value byte, length int) []byte { + bytes := make([]byte, length) + for i := range bytes { + bytes[i] = value + } + return bytes +}