Skip to content
Merged
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
134 changes: 134 additions & 0 deletions pkg/cascade/signature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package cascade

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"

"github.com/LumeraProtocol/supernode/v2/pkg/codec"
"github.com/LumeraProtocol/supernode/v2/pkg/keyring"
"github.com/LumeraProtocol/supernode/v2/pkg/utils"
"github.com/cosmos/btcutil/base58"
cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring"
"lukechampine.com/blake3"
)

// CreateLayoutSignature creates the cascade signature format for a given layout file.
// It returns the signature format and index file IDs needed for CASCADE action.
func CreateLayoutSignature(metadataFile codec.Layout, kr cosmoskeyring.Keyring, userKeyName string, ic uint32, maxFiles uint32) (signatureFormat string, indexFileIDs []string, err error) {
// Step 1: Convert metadata to JSON then base64
me, err := json.Marshal(metadataFile)
if err != nil {
return "", nil, fmt.Errorf("failed to marshal metadata: %w", err)
}
layoutBase64 := base64.StdEncoding.EncodeToString(me)

// Step 2: Sign the layout data
layoutSignature, err := keyring.SignBytes(kr, userKeyName, []byte(layoutBase64))
if err != nil {
return "", nil, fmt.Errorf("failed to sign layout: %w", err)
}
layoutSignatureB64 := base64.StdEncoding.EncodeToString(layoutSignature)

// Step 3: Generate redundant layout file IDs
layoutIDs := GenerateLayoutIDsBatch(layoutBase64, layoutSignatureB64, ic, maxFiles)

// Step 4: Create index file containing layout references
indexFile := map[string]interface{}{
"layout_ids": layoutIDs,
"layout_signature": layoutSignatureB64,
}

// Step 5: Sign the index file
indexFileJSON, err := json.Marshal(indexFile)
if err != nil {
return "", nil, fmt.Errorf("failed to marshal index file: %w", err)
}
indexFileBase64 := base64.StdEncoding.EncodeToString(indexFileJSON)

creatorSignature, err := keyring.SignBytes(kr, userKeyName, []byte(indexFileBase64))
if err != nil {
return "", nil, fmt.Errorf("failed to sign index file: %w", err)
}
creatorSignatureB64 := base64.StdEncoding.EncodeToString(creatorSignature)

// Step 6: Create final signature format
signatureFormat = fmt.Sprintf("%s.%s", indexFileBase64, creatorSignatureB64)

// Step 7: Generate final index file IDs for submission
indexFileIDs = GenerateIndexIDsBatch(signatureFormat, ic, maxFiles)

return signatureFormat, indexFileIDs, nil
}

// GenerateLayoutIDsBatch generates layout IDs using the process:
// combine data -> add counter -> compress -> hash -> Base58 encode
func GenerateLayoutIDsBatch(layoutBase64, layoutSignatureB64 string, ic, maxFiles uint32) []string {
layoutWithSig := fmt.Sprintf("%s.%s", layoutBase64, layoutSignatureB64)
layoutIDs := make([]string, maxFiles)

var buffer bytes.Buffer
buffer.Grow(len(layoutWithSig) + 10)

for i := uint32(0); i < maxFiles; i++ {
// Build unique content with counter
buffer.Reset()
buffer.WriteString(layoutWithSig)
buffer.WriteByte('.')
buffer.WriteString(fmt.Sprintf("%d", ic+i))

// Compress for efficiency
compressedData, err := utils.ZstdCompress(buffer.Bytes())
if err != nil {
continue
}

// Hash for uniqueness
hash, err := utils.Blake3Hash(compressedData)
if err != nil {
continue
}

// Base58 encode for readable ID
layoutIDs[i] = base58.Encode(hash)
}

return layoutIDs
}

// GenerateIndexIDsBatch generates index file IDs using same process as layout IDs
func GenerateIndexIDsBatch(signatureFormat string, ic, maxFiles uint32) []string {
indexFileIDs := make([]string, maxFiles)

var buffer bytes.Buffer
buffer.Grow(len(signatureFormat) + 10)

for i := uint32(0); i < maxFiles; i++ {
buffer.Reset()
buffer.WriteString(signatureFormat)
buffer.WriteByte('.')
buffer.WriteString(fmt.Sprintf("%d", ic+i))

compressedData, err := utils.ZstdCompress(buffer.Bytes())
if err != nil {
continue
}
hash, err := utils.Blake3Hash(compressedData)
if err != nil {
continue
}
indexFileIDs[i] = base58.Encode(hash)
}
return indexFileIDs
}

// ComputeBlake3Hash computes Blake3 hash of the given message
func ComputeBlake3Hash(msg []byte) ([]byte, error) {
hasher := blake3.New(32, nil)
if _, err := io.Copy(hasher, bytes.NewReader(msg)); err != nil {
return nil, err
}
return hasher.Sum(nil), nil
}
9 changes: 7 additions & 2 deletions tests/system/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,10 @@ require (
github.com/LumeraProtocol/lumera v1.7.0
github.com/LumeraProtocol/supernode/v2 v2.0.0-00010101000000-000000000000
github.com/cometbft/cometbft v0.38.17
github.com/cosmos/btcutil v1.0.5
github.com/tidwall/gjson v1.14.2
github.com/tidwall/sjson v1.2.5
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
gopkg.in/yaml.v3 v3.0.1
lukechampine.com/blake3 v1.4.0
)

require (
Expand All @@ -61,13 +59,15 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/pebble v1.1.5 // indirect
github.com/cockroachdb/redact v1.1.6 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/cometbft/cometbft-db v0.14.1 // indirect
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/cosmos-db v1.1.1 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/cosmos/ics23/go v0.11.0 // indirect
Expand Down Expand Up @@ -170,13 +170,18 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect
google.golang.org/protobuf v1.36.6 // indirect
gotest.tools/v3 v3.5.2 // indirect
lukechampine.com/blake3 v1.4.0 // indirect
nhooyr.io/websocket v1.8.10 // indirect
pgregory.net/rapid v1.2.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

replace (
github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0

// Fix bytedance/sonic compatibility
github.com/bytedance/sonic => github.com/bytedance/sonic v1.12.3
github.com/bytedance/sonic/loader => github.com/bytedance/sonic/loader v0.2.1
// dgrijalva/jwt-go is deprecated and doesn't receive security updates.
// See: https://github.com/cosmos/cosmos-sdk/issues/13134
github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2
Expand Down
10 changes: 5 additions & 5 deletions tests/system/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,10 @@ github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pY
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
Expand All @@ -149,6 +148,7 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
Expand Down
127 changes: 6 additions & 121 deletions tests/system/signature_utils.go
Original file line number Diff line number Diff line change
@@ -1,132 +1,17 @@
package system

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"

"github.com/LumeraProtocol/supernode/v2/pkg/cascade"
"github.com/LumeraProtocol/supernode/v2/pkg/codec"
"github.com/LumeraProtocol/supernode/v2/pkg/keyring"
"github.com/LumeraProtocol/supernode/v2/pkg/utils"
"github.com/cosmos/btcutil/base58"
cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring"
"lukechampine.com/blake3"
)

// createCascadeLayoutSignature is a wrapper for the common cascade signature function
func createCascadeLayoutSignature(metadataFile codec.Layout, kr cosmoskeyring.Keyring, userKeyName string, ic uint32, maxFiles uint32) (signatureFormat string, indexFileIDs []string, err error) {

// Step 1: Convert metadata to JSON then base64
me, err := json.Marshal(metadataFile)
if err != nil {
return "", nil, fmt.Errorf("failed to marshal metadata: %w", err)
}
layoutBase64 := base64.StdEncoding.EncodeToString(me)

// Step 2: Sign the layout data
layoutSignature, err := keyring.SignBytes(kr, userKeyName, []byte(layoutBase64))
if err != nil {
return "", nil, fmt.Errorf("failed to sign layout: %w", err)
}
layoutSignatureB64 := base64.StdEncoding.EncodeToString(layoutSignature)

// Step 3: Generate redundant layout file IDs
layoutIDs := generateLayoutIDsBatch(layoutBase64, layoutSignatureB64, ic, maxFiles)

// Step 4: Create index file containing layout references
indexFile := map[string]interface{}{
"layout_ids": layoutIDs,
"layout_signature": layoutSignatureB64,
}

// Step 5: Sign the index file
indexFileJSON, err := json.Marshal(indexFile)
if err != nil {
return "", nil, fmt.Errorf("failed to marshal index file: %w", err)
}
indexFileBase64 := base64.StdEncoding.EncodeToString(indexFileJSON)

creatorSignature, err := keyring.SignBytes(kr, userKeyName, []byte(indexFileBase64))
if err != nil {
return "", nil, fmt.Errorf("failed to sign index file: %w", err)
}
creatorSignatureB64 := base64.StdEncoding.EncodeToString(creatorSignature)

// Step 6: Create final signature format
signatureFormat = fmt.Sprintf("%s.%s", indexFileBase64, creatorSignatureB64)

// Step 7: Generate final index file IDs for submission
indexFileIDs = generateIndexIDsBatch(signatureFormat, ic, maxFiles)

return signatureFormat, indexFileIDs, nil
}

// Process: combine data -> add counter -> compress -> hash -> Base58 encode
func generateLayoutIDsBatch(layoutBase64, layoutSignatureB64 string, ic, maxFiles uint32) []string {
layoutWithSig := fmt.Sprintf("%s.%s", layoutBase64, layoutSignatureB64)
layoutIDs := make([]string, maxFiles)

var buffer bytes.Buffer
buffer.Grow(len(layoutWithSig) + 10)

for i := uint32(0); i < maxFiles; i++ {
// Build unique content with counter
buffer.Reset()
buffer.WriteString(layoutWithSig)
buffer.WriteByte('.')
buffer.WriteString(fmt.Sprintf("%d", ic+i))

// Compress for efficiency
compressedData, err := utils.ZstdCompress(buffer.Bytes())
if err != nil {
continue
}

// Hash for uniqueness
hash, err := utils.Blake3Hash(compressedData)
if err != nil {
continue
}

// Base58 encode for readable ID
layoutIDs[i] = base58.Encode(hash)
}

return layoutIDs
return cascade.CreateLayoutSignature(metadataFile, kr, userKeyName, ic, maxFiles)
}

// generateIndexIDsBatch generates index file IDs using same process as layout IDs
func generateIndexIDsBatch(signatureFormat string, ic, maxFiles uint32) []string {
indexFileIDs := make([]string, maxFiles)

var buffer bytes.Buffer
buffer.Grow(len(signatureFormat) + 10)

for i := uint32(0); i < maxFiles; i++ {
buffer.Reset()
buffer.WriteString(signatureFormat)
buffer.WriteByte('.')
buffer.WriteString(fmt.Sprintf("%d", ic+i))

compressedData, err := utils.ZstdCompress(buffer.Bytes())
if err != nil {
continue
}
hash, err := utils.Blake3Hash(compressedData)
if err != nil {
continue
}
indexFileIDs[i] = base58.Encode(hash)
}
return indexFileIDs
}

// ComputeBlake3Hash computes Blake3 hash of the given message
// ComputeBlake3Hash is a wrapper for the common Blake3 hash function
func ComputeBlake3Hash(msg []byte) ([]byte, error) {
hasher := blake3.New(32, nil)
if _, err := io.Copy(hasher, bytes.NewReader(msg)); err != nil {
return nil, err
}
return hasher.Sum(nil), nil
}
return cascade.ComputeBlake3Hash(msg)
}