From 52541326b8480b006a2529b293b49a06a47e25f9 Mon Sep 17 00:00:00 2001 From: Matee ullah Malik Date: Wed, 4 Jun 2025 19:51:41 +0500 Subject: [PATCH] Refactor signature verification and data hash handling --- pkg/logtrace/fields.go | 2 + .../server/cascade/cascade_action_server.go | 15 +-- supernode/services/cascade/helper.go | 68 ++++++-------- supernode/services/cascade/helper_test.go | 93 ++++++++++++------- supernode/services/cascade/metadata.go | 16 +--- supernode/services/cascade/register.go | 21 ++--- supernode/services/cascade/register_test.go | 11 +-- 7 files changed, 111 insertions(+), 115 deletions(-) diff --git a/pkg/logtrace/fields.go b/pkg/logtrace/fields.go index d8a38b8a..16e2696c 100644 --- a/pkg/logtrace/fields.go +++ b/pkg/logtrace/fields.go @@ -19,4 +19,6 @@ const ( FieldTaskID = "task_id" FieldActionID = "action_id" FieldHashHex = "hash_hex" + FieldExpected = "expected" + FieldActual = "actual" ) diff --git a/supernode/node/action/server/cascade/cascade_action_server.go b/supernode/node/action/server/cascade/cascade_action_server.go index ad77b421..50bacbc9 100644 --- a/supernode/node/action/server/cascade/cascade_action_server.go +++ b/supernode/node/action/server/cascade/cascade_action_server.go @@ -1,15 +1,16 @@ package cascade import ( - "encoding/hex" + "encoding/base64" "fmt" - "github.com/LumeraProtocol/supernode/pkg/errors" - "google.golang.org/grpc" "io" - "lukechampine.com/blake3" "os" "path/filepath" + "github.com/LumeraProtocol/supernode/pkg/errors" + "google.golang.org/grpc" + "lukechampine.com/blake3" + pb "github.com/LumeraProtocol/supernode/gen/supernode/action/cascade" "github.com/LumeraProtocol/supernode/pkg/logtrace" cascadeService "github.com/LumeraProtocol/supernode/supernode/services/cascade" @@ -117,8 +118,8 @@ func (server *ActionServer) Register(stream pb.CascadeService_RegisterServer) er logtrace.Info(ctx, "metadata received from action-sdk", fields) hash := hasher.Sum(nil) - hashHex := hex.EncodeToString(hash) - fields[logtrace.FieldHashHex] = hashHex + + b64Hash := base64.StdEncoding.EncodeToString(hash) logtrace.Info(ctx, "final BLAKE3 hash generated", fields) targetPath, err := replaceTempDirWithTaskDir(metadata.GetTaskId(), tempFilePath, tempFile) @@ -133,7 +134,7 @@ func (server *ActionServer) Register(stream pb.CascadeService_RegisterServer) er err = task.Register(ctx, &cascadeService.RegisterRequest{ TaskID: metadata.TaskId, ActionID: metadata.ActionId, - DataHash: hash, + DataHash: b64Hash, DataSize: totalSize, FilePath: targetPath, }, func(resp *cascadeService.RegisterResponse) error { diff --git a/supernode/services/cascade/helper.go b/supernode/services/cascade/helper.go index 864c30f5..8eaa1cef 100644 --- a/supernode/services/cascade/helper.go +++ b/supernode/services/cascade/helper.go @@ -87,49 +87,52 @@ func (task *CascadeRegistrationTask) encodeInput(ctx context.Context, path strin return &resp, nil } -func (task *CascadeRegistrationTask) verifySignatureAndDecodeLayout(ctx context.Context, encoded string, creator string, - encodedMeta codec.Layout, f logtrace.Fields) (codec.Layout, string, error) { +func (task *CascadeRegistrationTask) verifySignatures(ctx context.Context, signaturePayload string, layout codec.Layout, creator string, f logtrace.Fields) error { - file, sig, err := extractSignatureAndFirstPart(encoded) + data, signature, err := extractSignatureAndFirstPart(signaturePayload) if err != nil { - return codec.Layout{}, "", task.wrapErr(ctx, "failed to extract signature and first part", err, f) + return task.wrapErr(ctx, "failed to extract signature and first part", err, f) } - logtrace.Info(ctx, "signature and first part have been extracted", f) - // Decode the base64-encoded signature - sigBytes, err := base64.StdEncoding.DecodeString(sig) + signatureBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { - return codec.Layout{}, "", task.wrapErr(ctx, "failed to decode signature from base64", err, f) + return task.wrapErr(ctx, "failed to decode base64 signature", err, f) } - // Log the verification attempt for the node creator - logtrace.Info(ctx, "verifying signature from node creator", logtrace.Fields{ - "creator": creator, - "taskID": task.ID(), - }) + // Calculate the hash of the layout we created and verify it against the provided hash + layoutJson, err := json.Marshal(layout) + if err != nil { + return task.wrapErr(ctx, "failed to marshal layout", err, f) + } - // Pass the decoded signature bytes for verification - if err := task.LumeraClient.Verify(ctx, creator, []byte(file), sigBytes); err != nil { - return codec.Layout{}, "", task.wrapErr(ctx, "failed to verify node creator signature", err, f) + layoutHash, err := utils.Blake3Hash(layoutJson) + if err != nil { + return task.wrapErr(ctx, "failed to calculate layout hash", err, f) } + b64LayoutHash := base64.StdEncoding.EncodeToString(layoutHash) - logtrace.Info(ctx, "node creator signature successfully verified", f) + // First Check + if b64LayoutHash != data { + return task.wrapErr(ctx, "layout hash mismatch", errors.Errorf("expected %s, got %s", b64LayoutHash, data), f) + } - layout, err := decodeMetadataFile(file) - if err != nil { - return codec.Layout{}, "", task.wrapErr(ctx, "failed to decode metadata file", err, f) + // Second Check + if err := task.LumeraClient.Verify(ctx, creator, layoutHash, signatureBytes); err != nil { + return task.wrapErr(ctx, "failed to verify node creator signature", err, f) } - return layout, sig, nil + logtrace.Info(ctx, "node creator signature successfully verified", f) + + return nil } -func (task *CascadeRegistrationTask) generateRQIDFiles(ctx context.Context, meta actiontypes.CascadeMetadata, - sig, creator string, encodedMeta codec.Layout, f logtrace.Fields) (GenRQIdentifiersFilesResponse, error) { +func (task *CascadeRegistrationTask) generateRQIDFiles(ctx context.Context, meta actiontypes.CascadeMetadata, creator string, encodedMeta codec.Layout, f logtrace.Fields) (GenRQIdentifiersFilesResponse, error) { res, err := GenRQIdentifiersFiles(ctx, GenRQIdentifiersFilesRequest{ Metadata: encodedMeta, CreatorSNAddress: creator, RqMax: uint32(meta.RqIdsMax), - Signature: sig, + Signature: meta.Signatures, // Since these are already verified, we can use them directly IC: uint32(meta.RqIdsIc), }) if err != nil { @@ -139,7 +142,6 @@ func (task *CascadeRegistrationTask) generateRQIDFiles(ctx context.Context, meta logtrace.Info(ctx, "rq symbols, rq-ids and rqid-files have been generated", f) return res, nil } - func (task *CascadeRegistrationTask) storeArtefacts(ctx context.Context, actionID string, idFiles [][]byte, symbolsDir string, f logtrace.Fields) error { return task.P2P.StoreArtefacts(ctx, adaptors.StoreArtefactsRequest{ IDFiles: idFiles, @@ -159,14 +161,12 @@ func (task *CascadeRegistrationTask) wrapErr(ctx context.Context, msg string, er } // extractSignatureAndFirstPart extracts the signature and first part from the encoded data -// data is expected to be in format: b64(JSON(Layout)).Signature func extractSignatureAndFirstPart(data string) (encodedMetadata string, signature string, err error) { parts := strings.Split(data, ".") if len(parts) < 2 { return "", "", errors.New("invalid data format") } - // The first part is the base64 encoded data return parts[0], parts[1], nil } @@ -185,20 +185,6 @@ func decodeMetadataFile(data string) (layout codec.Layout, err error) { return layout, nil } -func verifyIDs(ticketMetadata, metadata codec.Layout) error { - // Verify that the symbol identifiers match between versions - if err := utils.EqualStrList(ticketMetadata.Blocks[0].Symbols, metadata.Blocks[0].Symbols); err != nil { - return errors.Errorf("symbol identifiers don't match: %w", err) - } - - // Verify that the block hashes match - if ticketMetadata.Blocks[0].Hash != metadata.Blocks[0].Hash { - return errors.New("block hashes don't match") - } - - return nil -} - // verifyActionFee checks if the action fee is sufficient for the given data size // It fetches action parameters, calculates the required fee, and compares it with the action price func (task *CascadeRegistrationTask) verifyActionFee(ctx context.Context, action *actiontypes.Action, dataSize int, fields logtrace.Fields) error { diff --git a/supernode/services/cascade/helper_test.go b/supernode/services/cascade/helper_test.go index 1f93fac7..515974c3 100644 --- a/supernode/services/cascade/helper_test.go +++ b/supernode/services/cascade/helper_test.go @@ -1,12 +1,14 @@ package cascade import ( + "encoding/base64" "encoding/json" "testing" "github.com/LumeraProtocol/supernode/pkg/codec" "github.com/LumeraProtocol/supernode/pkg/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_extractSignatureAndFirstPart(t *testing.T) { @@ -66,49 +68,78 @@ func Test_decodeMetadataFile(t *testing.T) { } } -func Test_verifyIDs(t *testing.T) { +func Test_verifyLayoutHash(t *testing.T) { + // Create a sample layout + layout := codec.Layout{ + Blocks: []codec.Block{ + { + BlockID: 1, + Hash: "sample_hash", + Symbols: []string{"symbol1", "symbol2"}, + }, + }, + } + + // Marshal to JSON and calculate the Blake3 hash + jsonBytes, err := json.Marshal(layout) + require.NoError(t, err) + + blake3Hash, err := utils.Blake3Hash(jsonBytes) + require.NoError(t, err) + + validB64Hash := base64.StdEncoding.EncodeToString(blake3Hash) + tests := []struct { - name string - ticket codec.Layout - metadata codec.Layout - expectErr string + name string + b64EncodedHash string + metadata codec.Layout + expectErr bool + expectedErrSubstr string }{ { - name: "success match", - ticket: codec.Layout{Blocks: []codec.Block{ - {Symbols: []string{"A"}, Hash: "abc"}, - }}, - metadata: codec.Layout{Blocks: []codec.Block{ - {Symbols: []string{"A"}, Hash: "abc"}, - }}, + name: "valid hash match", + b64EncodedHash: validB64Hash, + metadata: layout, + expectErr: false, + }, + { + name: "hash mismatch", + b64EncodedHash: "invalid_hash_that_wont_match", + metadata: layout, + expectErr: true, + expectedErrSubstr: "layout hash mismatch", }, { - name: "symbol mismatch", - ticket: codec.Layout{Blocks: []codec.Block{ - {Symbols: []string{"A"}}, - }}, - metadata: codec.Layout{Blocks: []codec.Block{ - {Symbols: []string{"B"}}, - }}, - expectErr: "symbol identifiers don't match", + name: "different metadata", + b64EncodedHash: validB64Hash, + metadata: codec.Layout{ + Blocks: []codec.Block{ + { + BlockID: 2, + Hash: "different_hash", + Symbols: []string{"different_symbol"}, + }, + }, + }, + expectErr: true, + expectedErrSubstr: "layout hash mismatch", }, { - name: "hash mismatch", - ticket: codec.Layout{Blocks: []codec.Block{ - {Symbols: []string{"A"}, Hash: "a"}, - }}, - metadata: codec.Layout{Blocks: []codec.Block{ - {Symbols: []string{"A"}, Hash: "b"}, - }}, - expectErr: "block hashes don't match", + name: "empty hash", + b64EncodedHash: "", + metadata: layout, + expectErr: true, + expectedErrSubstr: "layout hash mismatch", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := verifyIDs(tt.ticket, tt.metadata) - if tt.expectErr != "" { - assert.ErrorContains(t, err, tt.expectErr) + if tt.expectErr { + assert.Error(t, err) + if tt.expectedErrSubstr != "" { + assert.Contains(t, err.Error(), tt.expectedErrSubstr) + } } else { assert.NoError(t, err) } diff --git a/supernode/services/cascade/metadata.go b/supernode/services/cascade/metadata.go index 3add83cd..f041c558 100644 --- a/supernode/services/cascade/metadata.go +++ b/supernode/services/cascade/metadata.go @@ -11,7 +11,6 @@ import ( "github.com/LumeraProtocol/supernode/pkg/errors" "github.com/LumeraProtocol/supernode/pkg/utils" "github.com/cosmos/btcutil/base58" - json "github.com/json-iterator/go" ) const ( @@ -33,21 +32,10 @@ type GenRQIdentifiersFilesResponse struct { RedundantMetadataFiles [][]byte } -// GenRQIdentifiersFiles generates Redundant Metadata Files and IDs func GenRQIdentifiersFiles(ctx context.Context, req GenRQIdentifiersFilesRequest) (resp GenRQIdentifiersFilesResponse, err error) { - metadataFile, err := json.Marshal(req.Metadata) - if err != nil { - return resp, errors.Errorf("marshal rqID file: %w", err) - } - b64EncodedMetadataFile := utils.B64Encode(metadataFile) - - // Create the RQID file by combining the encoded file with the signature - var buffer bytes.Buffer - buffer.Write(b64EncodedMetadataFile) - buffer.WriteByte(SeparatorByte) - buffer.Write([]byte(req.Signature)) - encMetadataFileWithSignature := buffer.Bytes() + // Since verifySignatures confirms that the signature is valid, we can use the signature from ticket directly + encMetadataFileWithSignature := []byte(req.Signature) // Generate the specified number of variant IDs rqIdIds, rqIDsFiles, err := GetIDFiles(ctx, encMetadataFileWithSignature, req.IC, req.RqMax) if err != nil { diff --git a/supernode/services/cascade/register.go b/supernode/services/cascade/register.go index a0cccba5..b6bd4517 100644 --- a/supernode/services/cascade/register.go +++ b/supernode/services/cascade/register.go @@ -11,7 +11,7 @@ import ( type RegisterRequest struct { TaskID string ActionID string - DataHash []byte + DataHash string // base64 encoded blake3 hash of the data DataSize int FilePath string } @@ -83,8 +83,11 @@ func (task *CascadeRegistrationTask) Register( task.streamEvent(SupernodeEventTypeMetadataDecoded, "cascade metadata has been decoded", "", send) /* 5. Verify data hash --------------------------------------------------------- */ - if err := task.verifyDataHash(ctx, req.DataHash, cascadeMeta.DataHash, fields); err != nil { - return err + if req.DataHash != cascadeMeta.DataHash { + return task.wrapErr(ctx, "data hash mismatch", nil, logtrace.Fields{ + logtrace.FieldExpected: req.DataHash, + logtrace.FieldActual: cascadeMeta.DataHash, + }) } logtrace.Info(ctx, "data-hash has been verified", fields) task.streamEvent(SupernodeEventTypeDataHashVerified, "data-hash has been verified", "", send) @@ -97,10 +100,8 @@ func (task *CascadeRegistrationTask) Register( logtrace.Info(ctx, "input-data has been encoded", fields) task.streamEvent(SupernodeEventTypeInputEncoded, "input data has been encoded", "", send) - /* 7. Signature verification + layout decode ---------------------------------- */ - layout, signature, err := task.verifySignatureAndDecodeLayout( - ctx, cascadeMeta.Signatures, action.Creator, encResp.Metadata, fields, - ) + /* 7. Signature verification */ + err = task.verifySignatures(ctx, cascadeMeta.Signatures, encResp.Metadata, action.Creator, fields) if err != nil { return err } @@ -108,17 +109,13 @@ func (task *CascadeRegistrationTask) Register( task.streamEvent(SupernodeEventTypeSignatureVerified, "signature has been verified", "", send) /* 8. Generate RQ-ID files ----------------------------------------------------- */ - rqidResp, err := task.generateRQIDFiles(ctx, cascadeMeta, signature, action.Creator, encResp.Metadata, fields) + rqidResp, err := task.generateRQIDFiles(ctx, cascadeMeta, action.Creator, encResp.Metadata, fields) if err != nil { return err } logtrace.Info(ctx, "rq-id files have been generated", fields) task.streamEvent(SupernodeEventTypeRQIDsGenerated, "rq-id files have been generated", "", send) - /* 9. Consistency checks ------------------------------------------------------- */ - if err := verifyIDs(layout, encResp.Metadata); err != nil { - return task.wrapErr(ctx, "failed to verify IDs", err, fields) - } logtrace.Info(ctx, "rq-ids have been verified", fields) task.streamEvent(SupernodeEventTypeRqIDsVerified, "rq-ids have been verified", "", send) diff --git a/supernode/services/cascade/register_test.go b/supernode/services/cascade/register_test.go index 2fe64359..98e14019 100644 --- a/supernode/services/cascade/register_test.go +++ b/supernode/services/cascade/register_test.go @@ -3,7 +3,6 @@ package cascade_test import ( "context" "encoding/base64" - "encoding/hex" "os" "testing" @@ -229,7 +228,7 @@ func TestCascadeRegistrationTask_Register(t *testing.T) { req := &cascade.RegisterRequest{ TaskID: "task1", ActionID: "action123", - DataHash: rawHash, + DataHash: base64.StdEncoding.EncodeToString(rawHash), DataSize: 10240, FilePath: tmpFile.Name(), } @@ -287,11 +286,3 @@ func blake3HashRawAndBase64(t *testing.T, path string) ([]byte, string) { b64 := base64.StdEncoding.EncodeToString(raw) return raw, b64 } - -func decodeHexOrDie(hexStr string) []byte { - bz, err := hex.DecodeString(hexStr) - if err != nil { - panic(err) - } - return bz -}