From 63784893118b428345c0ac8de31ba18f536c4df7 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Tue, 26 May 2026 11:15:00 -0700 Subject: [PATCH 1/3] fix(core): validate StorageProof structure in isValidSignedTransaction isValidSignedTransaction previously only verified that a transaction could be unmarshaled as protobuf. Add structural field validation for StorageProof txs (non-empty ProverAddresses, Address, and Height) so malformed transactions are rejected at both CheckTx (mempool) and ProcessProposal (block validation) through a single code path. Co-Authored-By: Claude Opus 4.7 --- pkg/core/server/abci.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pkg/core/server/abci.go b/pkg/core/server/abci.go index 3c2a0c22..92052c28 100644 --- a/pkg/core/server/abci.go +++ b/pkg/core/server/abci.go @@ -239,13 +239,12 @@ func (s *Server) Query(ctx context.Context, req *abcitypes.QueryRequest) (*abcit } func (s *Server) CheckTx(_ context.Context, check *abcitypes.CheckTxRequest) (*abcitypes.CheckTxResponse, error) { - // check if protobuf event _, err := s.isValidSignedTransaction(check.Tx) - if err == nil { - return &abcitypes.CheckTxResponse{Code: abcitypes.CodeTypeOK}, nil + if err != nil { + return &abcitypes.CheckTxResponse{Code: 1}, nil } - return &abcitypes.CheckTxResponse{Code: 1}, nil + return &abcitypes.CheckTxResponse{Code: abcitypes.CodeTypeOK}, nil } func (s *Server) InitChain(_ context.Context, chain *abcitypes.InitChainRequest) (*abcitypes.InitChainResponse, error) { @@ -911,10 +910,24 @@ func (s *Server) commitInProgressTx(ctx context.Context) error { func (s *Server) isValidSignedTransaction(tx []byte) (*v1.SignedTransaction, error) { var msg v1.SignedTransaction - err := proto.Unmarshal(tx, &msg) - if err != nil { + if err := proto.Unmarshal(tx, &msg); err != nil { return nil, err } + + switch msg.Transaction.(type) { + case *v1.SignedTransaction_StorageProof: + sp := msg.GetStorageProof() + if len(sp.ProverAddresses) == 0 { + return nil, fmt.Errorf("storage proof has no prover addresses") + } + if sp.Address == "" { + return nil, fmt.Errorf("storage proof has no prover address") + } + if sp.Height == 0 { + return nil, fmt.Errorf("storage proof has no height") + } + } + return &msg, nil } From 531e51678eacdf0c09303d61cead0e955b027492 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Tue, 26 May 2026 12:42:03 -0700 Subject: [PATCH 2/3] fix(core): validate transaction structure in isValidSignedTransaction isValidSignedTransaction previously only verified protobuf unmarshal. Add structural field checks so malformed transactions are rejected at both CheckTx (mempool) and ProcessProposal (block validation): - Reject transactions with no body (nil oneof) - StorageProof: require ProverAddresses, Address, Height - StorageProofVerification: require Height, Proof - Attestation: require Signatures, body (registration or deregistration) Co-Authored-By: Claude Opus 4.7 --- pkg/core/server/abci.go | 20 ++++ pkg/core/server/abci_test.go | 186 +++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 pkg/core/server/abci_test.go diff --git a/pkg/core/server/abci.go b/pkg/core/server/abci.go index 92052c28..86597eaf 100644 --- a/pkg/core/server/abci.go +++ b/pkg/core/server/abci.go @@ -914,6 +914,10 @@ func (s *Server) isValidSignedTransaction(tx []byte) (*v1.SignedTransaction, err return nil, err } + if msg.Transaction == nil { + return nil, fmt.Errorf("transaction has no body") + } + switch msg.Transaction.(type) { case *v1.SignedTransaction_StorageProof: sp := msg.GetStorageProof() @@ -926,6 +930,22 @@ func (s *Server) isValidSignedTransaction(tx []byte) (*v1.SignedTransaction, err if sp.Height == 0 { return nil, fmt.Errorf("storage proof has no height") } + case *v1.SignedTransaction_StorageProofVerification: + spv := msg.GetStorageProofVerification() + if spv.Height == 0 { + return nil, fmt.Errorf("storage proof verification has no height") + } + if len(spv.Proof) == 0 { + return nil, fmt.Errorf("storage proof verification has no proof") + } + case *v1.SignedTransaction_Attestation: + att := msg.GetAttestation() + if len(att.Signatures) == 0 { + return nil, fmt.Errorf("attestation has no signatures") + } + if att.GetValidatorRegistration() == nil && att.GetValidatorDeregistration() == nil { + return nil, fmt.Errorf("attestation has no body") + } } return &msg, nil diff --git a/pkg/core/server/abci_test.go b/pkg/core/server/abci_test.go new file mode 100644 index 00000000..145656ec --- /dev/null +++ b/pkg/core/server/abci_test.go @@ -0,0 +1,186 @@ +package server + +import ( + "testing" + + v1 "github.com/OpenAudio/go-openaudio/pkg/api/core/v1" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func marshalSignedTx(t *testing.T, tx *v1.SignedTransaction) []byte { + t.Helper() + b, err := proto.Marshal(tx) + require.NoError(t, err) + return b +} + +func TestIsValidSignedTransaction_EmptyBody(t *testing.T) { + s := &Server{} + + t.Run("nil transaction body", func(t *testing.T) { + tx := marshalSignedTx(t, &v1.SignedTransaction{}) + _, err := s.isValidSignedTransaction(tx) + require.ErrorContains(t, err, "no body") + }) + + t.Run("invalid protobuf", func(t *testing.T) { + _, err := s.isValidSignedTransaction([]byte("not protobuf")) + require.Error(t, err) + }) +} + +func TestIsValidSignedTransaction_StorageProof(t *testing.T) { + s := &Server{} + + marshal := func(t *testing.T, sp *v1.StorageProof) []byte { + t.Helper() + return marshalSignedTx(t, &v1.SignedTransaction{ + Transaction: &v1.SignedTransaction_StorageProof{StorageProof: sp}, + }) + } + + t.Run("valid", func(t *testing.T) { + tx := marshal(t, &v1.StorageProof{ + Height: 100, + Address: "ABCD1234", + ProverAddresses: []string{"ADDR1", "ADDR2"}, + Cid: "QmTest", + ProofSignature: []byte("sig"), + }) + msg, err := s.isValidSignedTransaction(tx) + require.NoError(t, err) + require.Len(t, msg.GetStorageProof().ProverAddresses, 2) + }) + + t.Run("empty prover addresses", func(t *testing.T) { + tx := marshal(t, &v1.StorageProof{Height: 100, Address: "ABCD1234"}) + _, err := s.isValidSignedTransaction(tx) + require.ErrorContains(t, err, "no prover addresses") + }) + + t.Run("nil prover addresses after proto roundtrip", func(t *testing.T) { + tx := marshal(t, &v1.StorageProof{Height: 100, Address: "ABCD1234", ProverAddresses: nil}) + _, err := s.isValidSignedTransaction(tx) + require.ErrorContains(t, err, "no prover addresses") + }) + + t.Run("empty slice prover addresses after proto roundtrip", func(t *testing.T) { + tx := marshal(t, &v1.StorageProof{Height: 100, Address: "ABCD1234", ProverAddresses: []string{}}) + _, err := s.isValidSignedTransaction(tx) + require.ErrorContains(t, err, "no prover addresses") + }) + + t.Run("missing address", func(t *testing.T) { + tx := marshal(t, &v1.StorageProof{Height: 100, ProverAddresses: []string{"ADDR1"}}) + _, err := s.isValidSignedTransaction(tx) + require.ErrorContains(t, err, "no prover address") + }) + + t.Run("zero height", func(t *testing.T) { + tx := marshal(t, &v1.StorageProof{Address: "ABCD1234", ProverAddresses: []string{"ADDR1"}}) + _, err := s.isValidSignedTransaction(tx) + require.ErrorContains(t, err, "no height") + }) +} + +func TestIsValidSignedTransaction_StorageProofVerification(t *testing.T) { + s := &Server{} + + marshal := func(t *testing.T, spv *v1.StorageProofVerification) []byte { + t.Helper() + return marshalSignedTx(t, &v1.SignedTransaction{ + Transaction: &v1.SignedTransaction_StorageProofVerification{StorageProofVerification: spv}, + }) + } + + t.Run("valid", func(t *testing.T) { + tx := marshal(t, &v1.StorageProofVerification{Height: 100, Proof: []byte("proof-data")}) + msg, err := s.isValidSignedTransaction(tx) + require.NoError(t, err) + require.Equal(t, int64(100), msg.GetStorageProofVerification().Height) + }) + + t.Run("zero height", func(t *testing.T) { + tx := marshal(t, &v1.StorageProofVerification{Proof: []byte("proof-data")}) + _, err := s.isValidSignedTransaction(tx) + require.ErrorContains(t, err, "no height") + }) + + t.Run("empty proof", func(t *testing.T) { + tx := marshal(t, &v1.StorageProofVerification{Height: 100}) + _, err := s.isValidSignedTransaction(tx) + require.ErrorContains(t, err, "no proof") + }) +} + +func TestIsValidSignedTransaction_Attestation(t *testing.T) { + s := &Server{} + + marshal := func(t *testing.T, att *v1.Attestation) []byte { + t.Helper() + return marshalSignedTx(t, &v1.SignedTransaction{ + Transaction: &v1.SignedTransaction_Attestation{Attestation: att}, + }) + } + + t.Run("valid registration", func(t *testing.T) { + tx := marshal(t, &v1.Attestation{ + Signatures: []string{"sig1", "sig2"}, + Body: &v1.Attestation_ValidatorRegistration{ + ValidatorRegistration: &v1.ValidatorRegistration{}, + }, + }) + _, err := s.isValidSignedTransaction(tx) + require.NoError(t, err) + }) + + t.Run("valid deregistration", func(t *testing.T) { + tx := marshal(t, &v1.Attestation{ + Signatures: []string{"sig1"}, + Body: &v1.Attestation_ValidatorDeregistration{ + ValidatorDeregistration: &v1.ValidatorDeregistration{}, + }, + }) + _, err := s.isValidSignedTransaction(tx) + require.NoError(t, err) + }) + + t.Run("no signatures", func(t *testing.T) { + tx := marshal(t, &v1.Attestation{ + Body: &v1.Attestation_ValidatorRegistration{ + ValidatorRegistration: &v1.ValidatorRegistration{}, + }, + }) + _, err := s.isValidSignedTransaction(tx) + require.ErrorContains(t, err, "no signatures") + }) + + t.Run("no body", func(t *testing.T) { + tx := marshal(t, &v1.Attestation{ + Signatures: []string{"sig1"}, + }) + _, err := s.isValidSignedTransaction(tx) + require.ErrorContains(t, err, "no body") + }) +} + +func TestIsValidSignedTransaction_OtherTypes(t *testing.T) { + s := &Server{} + + t.Run("plays passes through", func(t *testing.T) { + tx := marshalSignedTx(t, &v1.SignedTransaction{ + Transaction: &v1.SignedTransaction_Plays{Plays: &v1.TrackPlays{}}, + }) + _, err := s.isValidSignedTransaction(tx) + require.NoError(t, err) + }) + + t.Run("manage entity passes through", func(t *testing.T) { + tx := marshalSignedTx(t, &v1.SignedTransaction{ + Transaction: &v1.SignedTransaction_ManageEntity{ManageEntity: &v1.ManageEntityLegacy{}}, + }) + _, err := s.isValidSignedTransaction(tx) + require.NoError(t, err) + }) +} From e7764fe86a27a6151421e099bb090c7ebcabd994 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Tue, 26 May 2026 16:06:03 -0700 Subject: [PATCH 3/3] fix(core): allow bootstrap attestations without peer signatures --- pkg/core/server/abci.go | 3 --- pkg/core/server/abci_test.go | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/core/server/abci.go b/pkg/core/server/abci.go index 86597eaf..b8d61c14 100644 --- a/pkg/core/server/abci.go +++ b/pkg/core/server/abci.go @@ -940,9 +940,6 @@ func (s *Server) isValidSignedTransaction(tx []byte) (*v1.SignedTransaction, err } case *v1.SignedTransaction_Attestation: att := msg.GetAttestation() - if len(att.Signatures) == 0 { - return nil, fmt.Errorf("attestation has no signatures") - } if att.GetValidatorRegistration() == nil && att.GetValidatorDeregistration() == nil { return nil, fmt.Errorf("attestation has no body") } diff --git a/pkg/core/server/abci_test.go b/pkg/core/server/abci_test.go index 145656ec..476c1d4b 100644 --- a/pkg/core/server/abci_test.go +++ b/pkg/core/server/abci_test.go @@ -146,14 +146,14 @@ func TestIsValidSignedTransaction_Attestation(t *testing.T) { require.NoError(t, err) }) - t.Run("no signatures", func(t *testing.T) { + t.Run("empty signatures pass structural validation", func(t *testing.T) { tx := marshal(t, &v1.Attestation{ Body: &v1.Attestation_ValidatorRegistration{ ValidatorRegistration: &v1.ValidatorRegistration{}, }, }) _, err := s.isValidSignedTransaction(tx) - require.ErrorContains(t, err, "no signatures") + require.NoError(t, err) }) t.Run("no body", func(t *testing.T) {