Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/logtrace/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ const (
FieldTaskID = "task_id"
FieldActionID = "action_id"
FieldHashHex = "hash_hex"
FieldExpected = "expected"
FieldActual = "actual"
)
15 changes: 8 additions & 7 deletions supernode/node/action/server/cascade/cascade_action_server.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
68 changes: 27 additions & 41 deletions supernode/services/cascade/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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
}

Expand All @@ -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 {
Expand Down
93 changes: 62 additions & 31 deletions supernode/services/cascade/helper_test.go
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down
16 changes: 2 additions & 14 deletions supernode/services/cascade/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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 {
Expand Down
21 changes: 9 additions & 12 deletions supernode/services/cascade/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -97,28 +100,22 @@ 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
}
logtrace.Info(ctx, "signature has been verified", fields)
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)

Expand Down
Loading
Loading