From 742dcba484afb40a7d0fe80b4440ec748ef7361d Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Mon, 22 Dec 2025 12:11:06 +0100 Subject: [PATCH 1/2] Introduce envelope for headers on DA to fail fast on unauthorized content --- block/internal/submitting/da_submitter.go | 23 ++- .../da_submitter_integration_test.go | 2 +- .../internal/submitting/da_submitter_test.go | 7 +- block/internal/submitting/submitter.go | 4 +- block/internal/submitting/submitter_test.go | 2 +- block/internal/syncing/da_retriever.go | 64 +++++++- .../syncing/da_retriever_strict_test.go | 97 ++++++++++++ .../evm/types/pb/execution/evm/v1/state.pb.go | 2 +- proto/evnode/v1/evnode.proto | 11 ++ types/pb/evnode/v1/evnode.pb.go | 148 +++++++++++++----- types/serialization.go | 82 ++++++++++ 11 files changed, 390 insertions(+), 52 deletions(-) create mode 100644 block/internal/syncing/da_retriever_strict_test.go diff --git a/block/internal/submitting/da_submitter.go b/block/internal/submitting/da_submitter.go index 020f4b02de..59e802ba32 100644 --- a/block/internal/submitting/da_submitter.go +++ b/block/internal/submitting/da_submitter.go @@ -8,7 +8,6 @@ import ( "time" "github.com/rs/zerolog" - "google.golang.org/protobuf/proto" "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" @@ -161,7 +160,7 @@ func (s *DASubmitter) recordFailure(reason common.DASubmitterFailureReason) { } // SubmitHeaders submits pending headers to DA layer -func (s *DASubmitter) SubmitHeaders(ctx context.Context, cache cache.Manager) error { +func (s *DASubmitter) SubmitHeaders(ctx context.Context, cache cache.Manager, signer signer.Signer) error { headers, err := cache.GetPendingHeaders(ctx) if err != nil { return fmt.Errorf("failed to get pending headers: %w", err) @@ -171,15 +170,29 @@ func (s *DASubmitter) SubmitHeaders(ctx context.Context, cache cache.Manager) er return nil } + if signer == nil { + return fmt.Errorf("signer is nil") + } + s.logger.Info().Int("count", len(headers)).Msg("submitting headers to DA") return submitToDA(s, ctx, headers, func(header *types.SignedHeader) ([]byte, error) { - headerPb, err := header.ToProto() + // A. Marshal the inner SignedHeader content to bytes (canonical representation for signing) + // This effectively signs "Fields 1-3" of the intended DAHeaderEnvelope. + contentBytes, err := header.MarshalBinary() if err != nil { - return nil, fmt.Errorf("failed to convert header to proto: %w", err) + return nil, fmt.Errorf("failed to marshal signed header for envelope signing: %w", err) } - return proto.Marshal(headerPb) + + // B. Sign the contentBytes with the envelope signer (aggregator) + envelopeSignature, err := signer.Sign(contentBytes) + if err != nil { + return nil, fmt.Errorf("failed to sign envelope: %w", err) + } + + // C. Create the envelope and marshal it + return header.MarshalDAEnvelope(envelopeSignature) }, func(submitted []*types.SignedHeader, res *datypes.ResultSubmit) { for _, header := range submitted { diff --git a/block/internal/submitting/da_submitter_integration_test.go b/block/internal/submitting/da_submitter_integration_test.go index 8f03ff7c7f..ddab9a7be6 100644 --- a/block/internal/submitting/da_submitter_integration_test.go +++ b/block/internal/submitting/da_submitter_integration_test.go @@ -99,7 +99,7 @@ func TestDASubmitter_SubmitHeadersAndData_MarksInclusionAndUpdatesLastSubmitted( daSubmitter := NewDASubmitter(client, cfg, gen, common.DefaultBlockOptions(), common.NopMetrics(), zerolog.Nop()) // Submit headers and data - require.NoError(t, daSubmitter.SubmitHeaders(context.Background(), cm)) + require.NoError(t, daSubmitter.SubmitHeaders(context.Background(), cm, n)) require.NoError(t, daSubmitter.SubmitData(context.Background(), cm, n, gen)) // After submission, inclusion markers should be set diff --git a/block/internal/submitting/da_submitter_test.go b/block/internal/submitting/da_submitter_test.go index a34589c222..7b0d182aa1 100644 --- a/block/internal/submitting/da_submitter_test.go +++ b/block/internal/submitting/da_submitter_test.go @@ -213,7 +213,7 @@ func TestDASubmitter_SubmitHeaders_Success(t *testing.T) { require.NoError(t, batch2.Commit()) // Submit headers - err = submitter.SubmitHeaders(ctx, cm) + err = submitter.SubmitHeaders(ctx, cm, signer) require.NoError(t, err) // Verify headers are marked as DA included @@ -229,8 +229,11 @@ func TestDASubmitter_SubmitHeaders_NoPendingHeaders(t *testing.T) { submitter, _, cm, mockDA, _ := setupDASubmitterTest(t) ctx := context.Background() + // Create test signer + _, _, signer := createTestSigner(t) + // Submit headers when none are pending - err := submitter.SubmitHeaders(ctx, cm) + err := submitter.SubmitHeaders(ctx, cm, signer) require.NoError(t, err) // Should succeed with no action mockDA.AssertNotCalled(t, "Submit", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) } diff --git a/block/internal/submitting/submitter.go b/block/internal/submitting/submitter.go index 7340cfbd6e..b17d179fd9 100644 --- a/block/internal/submitting/submitter.go +++ b/block/internal/submitting/submitter.go @@ -25,7 +25,7 @@ import ( // daSubmitterAPI defines minimal methods needed by Submitter for DA submissions. type daSubmitterAPI interface { - SubmitHeaders(ctx context.Context, cache cache.Manager) error + SubmitHeaders(ctx context.Context, cache cache.Manager, signer signer.Signer) error SubmitData(ctx context.Context, cache cache.Manager, signer signer.Signer, genesis genesis.Genesis) error } @@ -158,7 +158,7 @@ func (s *Submitter) daSubmissionLoop() { s.logger.Debug().Time("t", time.Now()).Uint64("headers", headersNb).Msg("Header submission completed") s.headerSubmissionMtx.Unlock() }() - if err := s.daSubmitter.SubmitHeaders(s.ctx, s.cache); err != nil { + if err := s.daSubmitter.SubmitHeaders(s.ctx, s.cache, s.signer); err != nil { // Check for unrecoverable errors that indicate a critical issue if errors.Is(err, common.ErrOversizedItem) { s.logger.Error().Err(err). diff --git a/block/internal/submitting/submitter_test.go b/block/internal/submitting/submitter_test.go index 269aedd0e0..f07cc64126 100644 --- a/block/internal/submitting/submitter_test.go +++ b/block/internal/submitting/submitter_test.go @@ -399,7 +399,7 @@ type fakeDASubmitter struct { chData chan struct{} } -func (f *fakeDASubmitter) SubmitHeaders(ctx context.Context, _ cache.Manager) error { +func (f *fakeDASubmitter) SubmitHeaders(ctx context.Context, _ cache.Manager, _ signer.Signer) error { select { case f.chHdr <- struct{}{}: default: diff --git a/block/internal/syncing/da_retriever.go b/block/internal/syncing/da_retriever.go index ec29af45b5..770c6e0b58 100644 --- a/block/internal/syncing/da_retriever.go +++ b/block/internal/syncing/da_retriever.go @@ -34,6 +34,10 @@ type daRetriever struct { // on restart, will be refetch as da height is updated by syncer pendingHeaders map[uint64]*types.SignedHeader pendingData map[uint64]*types.Data + + // strictMode indicates if the node has seen a valid DAHeaderEnvelope + // and should now reject all legacy/unsigned headers. + strictMode bool } // NewDARetriever creates a new DA retriever @@ -50,6 +54,7 @@ func NewDARetriever( logger: logger.With().Str("component", "da_retriever").Logger(), pendingHeaders: make(map[uint64]*types.SignedHeader), pendingData: make(map[uint64]*types.Data), + strictMode: false, } } @@ -228,15 +233,64 @@ func (r *daRetriever) processBlobs(ctx context.Context, blobs [][]byte, daHeight // tryDecodeHeader attempts to decode a blob as a header func (r *daRetriever) tryDecodeHeader(bz []byte, daHeight uint64) *types.SignedHeader { header := new(types.SignedHeader) - var headerPb pb.SignedHeader - if err := proto.Unmarshal(bz, &headerPb); err != nil { - return nil - } + isValidEnvelope := false + + // Attempt to unmarshal as DAHeaderEnvelope and get the envelope signature + if envelopeSignature, err := header.UnmarshalDAEnvelope(bz); err != nil { + // If in strict mode, we REQUIRE an envelope. + if r.strictMode { + r.logger.Warn().Err(err).Msg("strict mode is enabled, rejecting non-envelope blob") + return nil + } - if err := header.FromProto(&headerPb); err != nil { + // Fallback for backward compatibility (only if NOT in strict mode) + r.logger.Debug().Msg("trying legacy decoding") + var headerPb pb.SignedHeader + if errLegacy := proto.Unmarshal(bz, &headerPb); errLegacy != nil { + return nil + } + if errLegacy := header.FromProto(&headerPb); errLegacy != nil { + return nil + } + } else { + // We have a structurally valid envelope (or at least it parsed) + if len(envelopeSignature) > 0 { + if header.Signer.PubKey == nil { + r.logger.Debug().Msg("header signer has no pubkey, cannot verify envelope") + return nil + } + payload, err := header.MarshalBinary() + if err != nil { + r.logger.Debug().Err(err).Msg("failed to marshal header for verification") + return nil + } + if valid, err := header.Signer.PubKey.Verify(payload, envelopeSignature); err != nil || !valid { + r.logger.Info().Err(err).Msg("DA envelope signature verification failed") + return nil + } + r.logger.Debug().Uint64("height", header.Height()).Msg("DA envelope signature verified") + isValidEnvelope = true + } else { + // No signature in envelope? Treat as legacy or invalid. + if r.strictMode { + r.logger.Warn().Msg("strict mode is enabled, rejecting envelope without signature") + return nil + } + } + } + if r.strictMode && !isValidEnvelope { + r.logger.Warn().Msg("strict mode: rejecting block that is not a fully valid envelope") return nil } + // Mode Switch Logic + if isValidEnvelope && !r.strictMode { + r.logger.Info().Uint64("height", header.Height()).Msg("valid DA envelope detected, switching to STRICT MODE") + r.strictMode = true + } + + // Legacy blob support implies: strictMode == false AND (!isValidEnvelope). + // We fall through here. // Basic validation if err := header.Header.ValidateBasic(); err != nil { diff --git a/block/internal/syncing/da_retriever_strict_test.go b/block/internal/syncing/da_retriever_strict_test.go new file mode 100644 index 0000000000..af1046dba8 --- /dev/null +++ b/block/internal/syncing/da_retriever_strict_test.go @@ -0,0 +1,97 @@ +package syncing + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/evstack/ev-node/pkg/config" + "github.com/evstack/ev-node/pkg/genesis" + "github.com/evstack/ev-node/types" +) + +func TestDARetriever_StrictEnvelopeMode_Switch(t *testing.T) { + // Setup keys + addr, pub, signer := buildSyncTestSigner(t) + gen := genesis.Genesis{ChainID: "tchain", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: addr} + + r := newTestDARetriever(t, nil, config.DefaultConfig(), gen) + + // 1. Create a Legacy Header (SignedHeader marshaled directly) + // This simulates old blobs on the network before the upgrade. + legacyHeader := &types.SignedHeader{ + Header: types.Header{ + BaseHeader: types.BaseHeader{ChainID: gen.ChainID, Height: 1, Time: uint64(time.Now().UnixNano())}, + ProposerAddress: addr, + }, + Signer: types.Signer{PubKey: pub, Address: addr}, + } + // Sign it + bz, err := types.DefaultAggregatorNodeSignatureBytesProvider(&legacyHeader.Header) + require.NoError(t, err) + sig, err := signer.Sign(bz) + require.NoError(t, err) + legacyHeader.Signature = sig + + legacyBlob, err := legacyHeader.MarshalBinary() + require.NoError(t, err) + + // 2. Create an Envelope Header (DAHeaderEnvelope) + // This simulates a new blob after upgrade. + envelopeHeader := &types.SignedHeader{ + Header: types.Header{ + BaseHeader: types.BaseHeader{ChainID: gen.ChainID, Height: 2, Time: uint64(time.Now().UnixNano())}, + ProposerAddress: addr, + }, + Signer: types.Signer{PubKey: pub, Address: addr}, + } + // Sign content + bz2, err := types.DefaultAggregatorNodeSignatureBytesProvider(&envelopeHeader.Header) + require.NoError(t, err) + sig2, err := signer.Sign(bz2) + require.NoError(t, err) + envelopeHeader.Signature = sig2 + + // Create Envelope + // We need to sign the envelope itself. + // The `SubmitHeaders` logic wraps it. We emulate it here using `MarshalDAEnvelope`. + // First get canonical content bytes (fields 1-3) + contentBytes, err := envelopeHeader.MarshalBinary() + require.NoError(t, err) + // Sign envelope + envSig, err := signer.Sign(contentBytes) + require.NoError(t, err) + // Marshal to envelope + envelopeBlob, err := envelopeHeader.MarshalDAEnvelope(envSig) + require.NoError(t, err) + + // --- Test Scenario --- + + // A. Initial State: StrictMode is false. Legacy blob should be accepted. + assert.False(t, r.strictMode) + + decodedLegacy := r.tryDecodeHeader(legacyBlob, 100) + require.NotNil(t, decodedLegacy) + assert.Equal(t, uint64(1), decodedLegacy.Height()) + + // StrictMode should still be false because it was a legacy blob + assert.False(t, r.strictMode) + + // B. Receiving Envelope: Should be accepted and Switch StrictMode to true. + decodedEnvelope := r.tryDecodeHeader(envelopeBlob, 101) + require.NotNil(t, decodedEnvelope) + assert.Equal(t, uint64(2), decodedEnvelope.Height()) + + assert.True(t, r.strictMode, "retriever should have switched to strict mode") + + // C. Receiving Legacy again: Should be REJECTED now. + // We reuse the same legacyBlob (or a new one, doesn't matter, structure is legacy). + decodedLegacyAgain := r.tryDecodeHeader(legacyBlob, 102) + assert.Nil(t, decodedLegacyAgain, "legacy blob should be rejected in strict mode") + + // D. Receiving Envelope again: Should still be accepted. + decodedEnvelopeAgain := r.tryDecodeHeader(envelopeBlob, 103) + require.NotNil(t, decodedEnvelopeAgain) +} diff --git a/execution/evm/types/pb/execution/evm/v1/state.pb.go b/execution/evm/types/pb/execution/evm/v1/state.pb.go index dc2f3ce06d..b37717de63 100644 --- a/execution/evm/types/pb/execution/evm/v1/state.pb.go +++ b/execution/evm/types/pb/execution/evm/v1/state.pb.go @@ -160,7 +160,7 @@ const file_execution_evm_v1_state_proto_rawDesc = "" + "\atx_hash\x18\x06 \x01(\fR\x06txHash\x12\x1c\n" + "\ttimestamp\x18\a \x01(\x03R\ttimestamp\x12\x14\n" + "\x05stage\x18\b \x01(\tR\x05stage\x12&\n" + - "\x0fupdated_at_unix\x18\t \x01(\x03R\rupdatedAtUnixB6Z4github.com/evstack/ev-node/execution/evm/types/pb/v1b\x06proto3" + "\x0fupdated_at_unix\x18\t \x01(\x03R\rupdatedAtUnixBDZBgithub.com/evstack/ev-node/execution/evm/types/pb/execution/evm/v1b\x06proto3" var ( file_execution_evm_v1_state_proto_rawDescOnce sync.Once diff --git a/proto/evnode/v1/evnode.proto b/proto/evnode/v1/evnode.proto index 1cb3e23ea1..bd23d920eb 100644 --- a/proto/evnode/v1/evnode.proto +++ b/proto/evnode/v1/evnode.proto @@ -47,6 +47,17 @@ message SignedHeader { Header header = 1; bytes signature = 2; Signer signer = 3; + // Reserved for DAHeaderEnvelope envelope_signature + reserved 4; +} + +// DAHeaderEnvelope is a wrapper around SignedHeader for DA submission. +// It is binary compatible with SignedHeader (fields 1-3) but adds an envelope signature. +message DAHeaderEnvelope { + Header header = 1; + bytes signature = 2; + Signer signer = 3; + bytes envelope_signature = 4; } // Signer is a signer of a block in the blockchain. diff --git a/types/pb/evnode/v1/evnode.pb.go b/types/pb/evnode/v1/evnode.pb.go index 86d9c4c572..072af753d4 100644 --- a/types/pb/evnode/v1/evnode.pb.go +++ b/types/pb/evnode/v1/evnode.pb.go @@ -261,6 +261,76 @@ func (x *SignedHeader) GetSigner() *Signer { return nil } +// DAHeaderEnvelope is a wrapper around SignedHeader for DA submission. +// It is binary compatible with SignedHeader (fields 1-3) but adds an envelope signature. +type DAHeaderEnvelope struct { + state protoimpl.MessageState `protogen:"open.v1"` + Header *Header `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` + Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` + Signer *Signer `protobuf:"bytes,3,opt,name=signer,proto3" json:"signer,omitempty"` + EnvelopeSignature []byte `protobuf:"bytes,4,opt,name=envelope_signature,json=envelopeSignature,proto3" json:"envelope_signature,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DAHeaderEnvelope) Reset() { + *x = DAHeaderEnvelope{} + mi := &file_evnode_v1_evnode_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DAHeaderEnvelope) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DAHeaderEnvelope) ProtoMessage() {} + +func (x *DAHeaderEnvelope) ProtoReflect() protoreflect.Message { + mi := &file_evnode_v1_evnode_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DAHeaderEnvelope.ProtoReflect.Descriptor instead. +func (*DAHeaderEnvelope) Descriptor() ([]byte, []int) { + return file_evnode_v1_evnode_proto_rawDescGZIP(), []int{3} +} + +func (x *DAHeaderEnvelope) GetHeader() *Header { + if x != nil { + return x.Header + } + return nil +} + +func (x *DAHeaderEnvelope) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +func (x *DAHeaderEnvelope) GetSigner() *Signer { + if x != nil { + return x.Signer + } + return nil +} + +func (x *DAHeaderEnvelope) GetEnvelopeSignature() []byte { + if x != nil { + return x.EnvelopeSignature + } + return nil +} + // Signer is a signer of a block in the blockchain. type Signer struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -274,7 +344,7 @@ type Signer struct { func (x *Signer) Reset() { *x = Signer{} - mi := &file_evnode_v1_evnode_proto_msgTypes[3] + mi := &file_evnode_v1_evnode_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -286,7 +356,7 @@ func (x *Signer) String() string { func (*Signer) ProtoMessage() {} func (x *Signer) ProtoReflect() protoreflect.Message { - mi := &file_evnode_v1_evnode_proto_msgTypes[3] + mi := &file_evnode_v1_evnode_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -299,7 +369,7 @@ func (x *Signer) ProtoReflect() protoreflect.Message { // Deprecated: Use Signer.ProtoReflect.Descriptor instead. func (*Signer) Descriptor() ([]byte, []int) { - return file_evnode_v1_evnode_proto_rawDescGZIP(), []int{3} + return file_evnode_v1_evnode_proto_rawDescGZIP(), []int{4} } func (x *Signer) GetAddress() []byte { @@ -333,7 +403,7 @@ type Metadata struct { func (x *Metadata) Reset() { *x = Metadata{} - mi := &file_evnode_v1_evnode_proto_msgTypes[4] + mi := &file_evnode_v1_evnode_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -345,7 +415,7 @@ func (x *Metadata) String() string { func (*Metadata) ProtoMessage() {} func (x *Metadata) ProtoReflect() protoreflect.Message { - mi := &file_evnode_v1_evnode_proto_msgTypes[4] + mi := &file_evnode_v1_evnode_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -358,7 +428,7 @@ func (x *Metadata) ProtoReflect() protoreflect.Message { // Deprecated: Use Metadata.ProtoReflect.Descriptor instead. func (*Metadata) Descriptor() ([]byte, []int) { - return file_evnode_v1_evnode_proto_rawDescGZIP(), []int{4} + return file_evnode_v1_evnode_proto_rawDescGZIP(), []int{5} } func (x *Metadata) GetChainId() string { @@ -400,7 +470,7 @@ type Data struct { func (x *Data) Reset() { *x = Data{} - mi := &file_evnode_v1_evnode_proto_msgTypes[5] + mi := &file_evnode_v1_evnode_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -412,7 +482,7 @@ func (x *Data) String() string { func (*Data) ProtoMessage() {} func (x *Data) ProtoReflect() protoreflect.Message { - mi := &file_evnode_v1_evnode_proto_msgTypes[5] + mi := &file_evnode_v1_evnode_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -425,7 +495,7 @@ func (x *Data) ProtoReflect() protoreflect.Message { // Deprecated: Use Data.ProtoReflect.Descriptor instead. func (*Data) Descriptor() ([]byte, []int) { - return file_evnode_v1_evnode_proto_rawDescGZIP(), []int{5} + return file_evnode_v1_evnode_proto_rawDescGZIP(), []int{6} } func (x *Data) GetMetadata() *Metadata { @@ -454,7 +524,7 @@ type SignedData struct { func (x *SignedData) Reset() { *x = SignedData{} - mi := &file_evnode_v1_evnode_proto_msgTypes[6] + mi := &file_evnode_v1_evnode_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -466,7 +536,7 @@ func (x *SignedData) String() string { func (*SignedData) ProtoMessage() {} func (x *SignedData) ProtoReflect() protoreflect.Message { - mi := &file_evnode_v1_evnode_proto_msgTypes[6] + mi := &file_evnode_v1_evnode_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -479,7 +549,7 @@ func (x *SignedData) ProtoReflect() protoreflect.Message { // Deprecated: Use SignedData.ProtoReflect.Descriptor instead. func (*SignedData) Descriptor() ([]byte, []int) { - return file_evnode_v1_evnode_proto_rawDescGZIP(), []int{6} + return file_evnode_v1_evnode_proto_rawDescGZIP(), []int{7} } func (x *SignedData) GetData() *Data { @@ -522,7 +592,7 @@ type Vote struct { func (x *Vote) Reset() { *x = Vote{} - mi := &file_evnode_v1_evnode_proto_msgTypes[7] + mi := &file_evnode_v1_evnode_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -534,7 +604,7 @@ func (x *Vote) String() string { func (*Vote) ProtoMessage() {} func (x *Vote) ProtoReflect() protoreflect.Message { - mi := &file_evnode_v1_evnode_proto_msgTypes[7] + mi := &file_evnode_v1_evnode_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -547,7 +617,7 @@ func (x *Vote) ProtoReflect() protoreflect.Message { // Deprecated: Use Vote.ProtoReflect.Descriptor instead. func (*Vote) Descriptor() ([]byte, []int) { - return file_evnode_v1_evnode_proto_rawDescGZIP(), []int{7} + return file_evnode_v1_evnode_proto_rawDescGZIP(), []int{8} } func (x *Vote) GetChainId() string { @@ -604,11 +674,16 @@ const file_evnode_v1_evnode_proto_rawDesc = "" + " \x01(\fR\x0fproposerAddress\x12%\n" + "\x0evalidator_hash\x18\v \x01(\fR\rvalidatorHash\x12\x19\n" + "\bchain_id\x18\f \x01(\tR\achainIdJ\x04\b\x05\x10\x06J\x04\b\a\x10\bJ\x04\b\t\x10\n" + - "\"\x82\x01\n" + + "\"\x88\x01\n" + "\fSignedHeader\x12)\n" + "\x06header\x18\x01 \x01(\v2\x11.evnode.v1.HeaderR\x06header\x12\x1c\n" + "\tsignature\x18\x02 \x01(\fR\tsignature\x12)\n" + - "\x06signer\x18\x03 \x01(\v2\x11.evnode.v1.SignerR\x06signer\";\n" + + "\x06signer\x18\x03 \x01(\v2\x11.evnode.v1.SignerR\x06signerJ\x04\b\x04\x10\x05\"\xb5\x01\n" + + "\x10DAHeaderEnvelope\x12)\n" + + "\x06header\x18\x01 \x01(\v2\x11.evnode.v1.HeaderR\x06header\x12\x1c\n" + + "\tsignature\x18\x02 \x01(\fR\tsignature\x12)\n" + + "\x06signer\x18\x03 \x01(\v2\x11.evnode.v1.SignerR\x06signer\x12-\n" + + "\x12envelope_signature\x18\x04 \x01(\fR\x11envelopeSignature\";\n" + "\x06Signer\x12\x18\n" + "\aaddress\x18\x01 \x01(\fR\aaddress\x12\x17\n" + "\apub_key\x18\x02 \x01(\fR\x06pubKey\"w\n" + @@ -644,31 +719,34 @@ func file_evnode_v1_evnode_proto_rawDescGZIP() []byte { return file_evnode_v1_evnode_proto_rawDescData } -var file_evnode_v1_evnode_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_evnode_v1_evnode_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_evnode_v1_evnode_proto_goTypes = []any{ (*Version)(nil), // 0: evnode.v1.Version (*Header)(nil), // 1: evnode.v1.Header (*SignedHeader)(nil), // 2: evnode.v1.SignedHeader - (*Signer)(nil), // 3: evnode.v1.Signer - (*Metadata)(nil), // 4: evnode.v1.Metadata - (*Data)(nil), // 5: evnode.v1.Data - (*SignedData)(nil), // 6: evnode.v1.SignedData - (*Vote)(nil), // 7: evnode.v1.Vote - (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*DAHeaderEnvelope)(nil), // 3: evnode.v1.DAHeaderEnvelope + (*Signer)(nil), // 4: evnode.v1.Signer + (*Metadata)(nil), // 5: evnode.v1.Metadata + (*Data)(nil), // 6: evnode.v1.Data + (*SignedData)(nil), // 7: evnode.v1.SignedData + (*Vote)(nil), // 8: evnode.v1.Vote + (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp } var file_evnode_v1_evnode_proto_depIdxs = []int32{ 0, // 0: evnode.v1.Header.version:type_name -> evnode.v1.Version 1, // 1: evnode.v1.SignedHeader.header:type_name -> evnode.v1.Header - 3, // 2: evnode.v1.SignedHeader.signer:type_name -> evnode.v1.Signer - 4, // 3: evnode.v1.Data.metadata:type_name -> evnode.v1.Metadata - 5, // 4: evnode.v1.SignedData.data:type_name -> evnode.v1.Data - 3, // 5: evnode.v1.SignedData.signer:type_name -> evnode.v1.Signer - 8, // 6: evnode.v1.Vote.timestamp:type_name -> google.protobuf.Timestamp - 7, // [7:7] is the sub-list for method output_type - 7, // [7:7] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 4, // 2: evnode.v1.SignedHeader.signer:type_name -> evnode.v1.Signer + 1, // 3: evnode.v1.DAHeaderEnvelope.header:type_name -> evnode.v1.Header + 4, // 4: evnode.v1.DAHeaderEnvelope.signer:type_name -> evnode.v1.Signer + 5, // 5: evnode.v1.Data.metadata:type_name -> evnode.v1.Metadata + 6, // 6: evnode.v1.SignedData.data:type_name -> evnode.v1.Data + 4, // 7: evnode.v1.SignedData.signer:type_name -> evnode.v1.Signer + 9, // 8: evnode.v1.Vote.timestamp:type_name -> google.protobuf.Timestamp + 9, // [9:9] is the sub-list for method output_type + 9, // [9:9] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_evnode_v1_evnode_proto_init() } @@ -682,7 +760,7 @@ func file_evnode_v1_evnode_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_evnode_v1_evnode_proto_rawDesc), len(file_evnode_v1_evnode_proto_rawDesc)), NumEnums: 0, - NumMessages: 8, + NumMessages: 9, NumExtensions: 0, NumServices: 0, }, diff --git a/types/serialization.go b/types/serialization.go index c2cc8a0dd4..0ee1ac87fd 100644 --- a/types/serialization.go +++ b/types/serialization.go @@ -146,6 +146,88 @@ func (sh *SignedHeader) UnmarshalBinary(data []byte) error { return nil } +// ToDAEnvelopeProto converts SignedHeader into DAHeaderEnvelope protobuf representation. +func (sh *SignedHeader) ToDAEnvelopeProto(envelopeSignature []byte) (*pb.DAHeaderEnvelope, error) { + if sh.Signer.PubKey == nil { + return &pb.DAHeaderEnvelope{ + Header: sh.Header.ToProto(), + Signature: sh.Signature[:], + Signer: &pb.Signer{}, + EnvelopeSignature: envelopeSignature, + }, nil + } + + pubKey, err := crypto.MarshalPublicKey(sh.Signer.PubKey) + if err != nil { + return nil, err + } + return &pb.DAHeaderEnvelope{ + Header: sh.Header.ToProto(), + Signature: sh.Signature[:], + Signer: &pb.Signer{ + Address: sh.Signer.Address, + PubKey: pubKey, + }, + EnvelopeSignature: envelopeSignature, + }, nil +} + +// FromDAEnvelopeProto fills SignedHeader with data from DAHeaderEnvelope protobuf representation. +// It assumes the envelope signature has already been extracted/verified if needed. +func (sh *SignedHeader) FromDAEnvelopeProto(envelope *pb.DAHeaderEnvelope) error { + if envelope == nil { + return errors.New("da header envelope is nil") + } + if envelope.Header == nil { + return errors.New("da header envelope's Header is nil") + } + if err := sh.Header.FromProto(envelope.Header); err != nil { + return err + } + if envelope.Signature != nil { + sh.Signature = make([]byte, len(envelope.Signature)) + copy(sh.Signature, envelope.Signature) + } else { + sh.Signature = nil + } + if envelope.Signer != nil && len(envelope.Signer.PubKey) > 0 { + pubKey, err := crypto.UnmarshalPublicKey(envelope.Signer.PubKey) + if err != nil { + return err + } + sh.Signer = Signer{ + Address: append([]byte(nil), envelope.Signer.Address...), + PubKey: pubKey, + } + } else { + sh.Signer = Signer{} + } + return nil +} + +// MarshalDAEnvelope encodes SignedHeader into DAHeaderEnvelope binary form. +func (sh *SignedHeader) MarshalDAEnvelope(envelopeSignature []byte) ([]byte, error) { + envelope, err := sh.ToDAEnvelopeProto(envelopeSignature) + if err != nil { + return nil, err + } + return proto.Marshal(envelope) +} + +// UnmarshalDAEnvelope decodes binary form of DAHeaderEnvelope into object and returns the envelope signature. +func (sh *SignedHeader) UnmarshalDAEnvelope(data []byte) ([]byte, error) { + var envelope pb.DAHeaderEnvelope + err := proto.Unmarshal(data, &envelope) + if err != nil { + return nil, err + } + err = sh.FromDAEnvelopeProto(&envelope) + if err != nil { + return nil, err + } + return envelope.EnvelopeSignature, nil +} + // ToProto converts Header into protobuf representation and returns it. func (h *Header) ToProto() *pb.Header { pHeader := &pb.Header{ From 0b927a34db061c67656eebdf5a516bd245dc99e4 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Mon, 22 Dec 2025 13:21:32 +0100 Subject: [PATCH 2/2] Review feedback --- block/internal/syncing/da_retriever.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/block/internal/syncing/da_retriever.go b/block/internal/syncing/da_retriever.go index 770c6e0b58..1307b39681 100644 --- a/block/internal/syncing/da_retriever.go +++ b/block/internal/syncing/da_retriever.go @@ -271,12 +271,6 @@ func (r *daRetriever) tryDecodeHeader(bz []byte, daHeight uint64) *types.SignedH } r.logger.Debug().Uint64("height", header.Height()).Msg("DA envelope signature verified") isValidEnvelope = true - } else { - // No signature in envelope? Treat as legacy or invalid. - if r.strictMode { - r.logger.Warn().Msg("strict mode is enabled, rejecting envelope without signature") - return nil - } } } if r.strictMode && !isValidEnvelope {