From d2323075abd06d60bc71cbd101063db6d2c9248e Mon Sep 17 00:00:00 2001 From: Matee Ullah Malik Date: Mon, 20 Oct 2025 15:49:42 +0500 Subject: [PATCH] Enhancements in cascadekit, supenrode --- p2p/kademlia/version_gate.go | 10 +- pkg/cascadekit/cascadekit_test.go | 66 +++++++ pkg/cascadekit/doc.go | 2 +- pkg/cascadekit/hash.go | 15 +- pkg/cascadekit/ids.go | 124 ++++++------- pkg/cascadekit/index.go | 25 ++- pkg/cascadekit/index_parse.go | 4 +- pkg/cascadekit/keyring_signatures.go | 14 ++ pkg/cascadekit/metadata.go | 4 +- pkg/cascadekit/parsers.go | 2 +- .../{highlevel.go => request_builder.go} | 13 +- pkg/cascadekit/rqid.go | 56 ++---- pkg/cascadekit/signatures.go | 42 +++-- pkg/cascadekit/verify.go | 8 + pkg/codec/codec.go | 16 +- pkg/codec/codec_default_test.go | 8 +- pkg/codec/raptorq.go | 26 +-- sdk/action/client.go | 26 ++- sdk/helpers/github_helper.go | 5 + sdk/task/helpers.go | 6 +- supernode/adaptors/rq.go | 15 +- supernode/cascade/config.go | 9 - supernode/cascade/helper.go | 88 ++++----- supernode/cascade/register.go | 169 ++++++++++-------- supernode/cascade/service.go | 23 ++- supernode/cmd/start.go | 2 +- tests/system/e2e_cascade_test.go | 76 +++++--- tests/system/go.mod | 4 +- tests/system/go.sum | 5 +- 29 files changed, 483 insertions(+), 380 deletions(-) create mode 100644 pkg/cascadekit/cascadekit_test.go create mode 100644 pkg/cascadekit/keyring_signatures.go rename pkg/cascadekit/{highlevel.go => request_builder.go} (53%) delete mode 100644 supernode/cascade/config.go diff --git a/p2p/kademlia/version_gate.go b/p2p/kademlia/version_gate.go index e9b70239..74c7dc77 100644 --- a/p2p/kademlia/version_gate.go +++ b/p2p/kademlia/version_gate.go @@ -1,6 +1,9 @@ package kademlia -import "strings" +import ( + "os" + "strings" +) var requiredVer string @@ -18,6 +21,11 @@ func requiredVersion() string { // Policy: required and peer must both be non-empty and exactly equal. func versionMismatch(peerVersion string) (required string, mismatch bool) { required = requiredVersion() + // Bypass strict gating during integration tests. + // Tests set os.Setenv("INTEGRATION_TEST", "true"). + if os.Getenv("INTEGRATION_TEST") == "true" { + return required, false + } peer := strings.TrimSpace(peerVersion) if required == "" || peer == "" || peer != required { return required, true diff --git a/pkg/cascadekit/cascadekit_test.go b/pkg/cascadekit/cascadekit_test.go new file mode 100644 index 00000000..d3299705 --- /dev/null +++ b/pkg/cascadekit/cascadekit_test.go @@ -0,0 +1,66 @@ +package cascadekit + +import ( + "encoding/base64" + "testing" + + "github.com/LumeraProtocol/supernode/v2/pkg/codec" + "github.com/klauspost/compress/zstd" +) + +func TestExtractIndexAndCreatorSig_Strict(t *testing.T) { + // too few parts + if _, _, err := ExtractIndexAndCreatorSig("abc"); err == nil { + t.Fatalf("expected error for single segment") + } + // too many parts + if _, _, err := ExtractIndexAndCreatorSig("a.b.c"); err == nil { + t.Fatalf("expected error for three segments") + } + // exactly two parts + a, b, err := ExtractIndexAndCreatorSig("a.b") + if err != nil || a != "a" || b != "b" { + t.Fatalf("unexpected result: a=%q b=%q err=%v", a, b, err) + } +} + +func TestParseCompressedIndexFile_Strict(t *testing.T) { + idx := IndexFile{LayoutIDs: []string{"L1", "L2"}, LayoutSignature: base64.StdEncoding.EncodeToString([]byte("sig"))} + idxB64, err := EncodeIndexB64(idx) + if err != nil { + t.Fatalf("encode index: %v", err) + } + payload := []byte(idxB64 + "." + base64.StdEncoding.EncodeToString([]byte("sig2")) + ".0") + + enc, _ := zstd.NewWriter(nil) + defer enc.Close() + compressed := enc.EncodeAll(payload, nil) + + got, err := ParseCompressedIndexFile(compressed) + if err != nil { + t.Fatalf("parse compressed index: %v", err) + } + if got.LayoutSignature != idx.LayoutSignature || len(got.LayoutIDs) != 2 { + t.Fatalf("unexpected index decoded: %+v", got) + } + + // malformed: only two segments + compressedBad := enc.EncodeAll([]byte("a.b"), nil) + if _, err := ParseCompressedIndexFile(compressedBad); err == nil { + t.Fatalf("expected error for two segments") + } + // malformed: four segments + compressedBad4 := enc.EncodeAll([]byte("a.b.c.d"), nil) + if _, err := ParseCompressedIndexFile(compressedBad4); err == nil { + t.Fatalf("expected error for four segments") + } +} + +func TestVerifySingleBlock(t *testing.T) { + if err := VerifySingleBlock(codec.Layout{Blocks: []codec.Block{{}}}); err != nil { + t.Fatalf("unexpected error for single block: %v", err) + } + if err := VerifySingleBlock(codec.Layout{Blocks: []codec.Block{{}, {}}}); err == nil { + t.Fatalf("expected error for multi-block layout") + } +} diff --git a/pkg/cascadekit/doc.go b/pkg/cascadekit/doc.go index 5fa61f7b..326ed87c 100644 --- a/pkg/cascadekit/doc.go +++ b/pkg/cascadekit/doc.go @@ -5,7 +5,7 @@ // Scope: // - Build and sign layout metadata (RaptorQ layout) and index files // - Generate redundant metadata files and index files + their IDs -// - Extract and decode index payloads from the on-chain signatures string +// - Extract and decode index payloads from the on-chain index signature format string // - Compute data hashes for request metadata // - Verify single-block layout consistency (explicit error if more than 1 block) // diff --git a/pkg/cascadekit/hash.go b/pkg/cascadekit/hash.go index 55288123..811f32cf 100644 --- a/pkg/cascadekit/hash.go +++ b/pkg/cascadekit/hash.go @@ -1,26 +1,15 @@ package cascadekit import ( - "bytes" "encoding/base64" - "io" - "lukechampine.com/blake3" + "github.com/LumeraProtocol/supernode/v2/pkg/utils" ) -// ComputeBlake3Hash computes a 32-byte Blake3 hash of the given data. -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 -} - // ComputeBlake3DataHashB64 computes a Blake3 hash of the input and // returns it as a base64-encoded string. func ComputeBlake3DataHashB64(data []byte) (string, error) { - h, err := ComputeBlake3Hash(data) + h, err := utils.Blake3Hash(data) if err != nil { return "", err } diff --git a/pkg/cascadekit/ids.go b/pkg/cascadekit/ids.go index 5c2b404d..bd9540c9 100644 --- a/pkg/cascadekit/ids.go +++ b/pkg/cascadekit/ids.go @@ -2,96 +2,65 @@ package cascadekit import ( "bytes" - "fmt" "strconv" "github.com/LumeraProtocol/supernode/v2/pkg/errors" "github.com/LumeraProtocol/supernode/v2/pkg/utils" "github.com/cosmos/btcutil/base58" + "github.com/klauspost/compress/zstd" ) // GenerateLayoutIDs computes IDs for redundant layout files (not the final index IDs). -// The ID is base58(blake3(zstd(layout_b64.layout_sig_b64.counter))). -func GenerateLayoutIDs(layoutB64, layoutSigB64 string, ic, max uint32) []string { - layoutWithSig := fmt.Sprintf("%s.%s", layoutB64, layoutSigB64) - layoutIDs := make([]string, max) - - var buffer bytes.Buffer - buffer.Grow(len(layoutWithSig) + 10) - - for i := uint32(0); i < max; i++ { - buffer.Reset() - buffer.WriteString(layoutWithSig) - 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 - } - - layoutIDs[i] = base58.Encode(hash) - } - - return layoutIDs +// The ID is base58(blake3(zstd(layout_signature_format.counter))). +// layoutSignatureFormat must be: base64(JSON(layout)).layout_signature_base64 +func GenerateLayoutIDs(layoutSignatureFormat string, ic, max uint32) ([]string, error) { + return generateIDs([]byte(layoutSignatureFormat), ic, max) } -// GenerateIndexIDs computes IDs for index files from the full signatures string. -func GenerateIndexIDs(signatures string, ic, max uint32) []string { - indexFileIDs := make([]string, max) - - var buffer bytes.Buffer - buffer.Grow(len(signatures) + 10) - - for i := uint32(0); i < max; i++ { - buffer.Reset() - buffer.WriteString(signatures) - 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 +// GenerateIndexIDs computes IDs for index files from the full index signature format string. +func GenerateIndexIDs(indexSignatureFormat string, ic, max uint32) ([]string, error) { + return generateIDs([]byte(indexSignatureFormat), ic, max) } // getIDFiles generates ID files by appending a '.' and counter, compressing, // and returning both IDs and compressed payloads. -func getIDFiles(file []byte, ic uint32, max uint32) (ids []string, files [][]byte, err error) { +// generateIDFiles builds compressed ID files from a base payload and returns +// both their content-addressed IDs and the compressed files themselves. +// For each counter in [ic..ic+max-1], the payload is: +// +// base + '.' + counter +// +// then zstd-compressed; the ID is base58(blake3(compressed)). +func generateIDFiles(base []byte, ic uint32, max uint32) (ids []string, files [][]byte, err error) { idFiles := make([][]byte, 0, max) ids = make([]string, 0, max) var buffer bytes.Buffer + // Reuse a single zstd encoder across iterations + enc, zerr := zstd.NewWriter(nil) + if zerr != nil { + return ids, idFiles, errors.Errorf("compress identifiers file: %w", zerr) + } + defer enc.Close() + for i := uint32(0); i < max; i++ { buffer.Reset() counter := ic + i - buffer.Write(file) + buffer.Write(base) buffer.WriteByte(SeparatorByte) - buffer.WriteString(strconv.Itoa(int(counter))) + // Append counter efficiently without intermediate string + var tmp [20]byte + cnt := strconv.AppendUint(tmp[:0], uint64(counter), 10) + buffer.Write(cnt) - compressedData, err := utils.ZstdCompress(buffer.Bytes()) - if err != nil { - return ids, idFiles, errors.Errorf("compress identifiers file: %w", err) - } + compressedData := enc.EncodeAll(buffer.Bytes(), nil) idFiles = append(idFiles, compressedData) hash, err := utils.Blake3Hash(compressedData) if err != nil { - return ids, idFiles, errors.Errorf("sha3-256-hash error getting an id file: %w", err) + return ids, idFiles, errors.Errorf("blake3 hash error getting an id file: %w", err) } ids = append(ids, base58.Encode(hash)) @@ -99,3 +68,36 @@ func getIDFiles(file []byte, ic uint32, max uint32) (ids []string, files [][]byt return ids, idFiles, nil } + +// generateIDs computes base58(blake3(zstd(base + '.' + counter))) for counters ic..ic+max-1. +// It reuses a single zstd encoder and avoids per-iteration heap churn. +func generateIDs(base []byte, ic, max uint32) ([]string, error) { + ids := make([]string, max) + + var buffer bytes.Buffer + // Reserve base length + dot + up to 10 digits + buffer.Grow(len(base) + 12) + + enc, err := zstd.NewWriter(nil) + if err != nil { + return nil, errors.Errorf("zstd encoder init: %w", err) + } + defer enc.Close() + + for i := uint32(0); i < max; i++ { + buffer.Reset() + buffer.Write(base) + buffer.WriteByte(SeparatorByte) + var tmp [20]byte + cnt := strconv.AppendUint(tmp[:0], uint64(ic+i), 10) + buffer.Write(cnt) + + compressed := enc.EncodeAll(buffer.Bytes(), nil) + h, err := utils.Blake3Hash(compressed) + if err != nil { + return nil, errors.Errorf("blake3 hash (i=%d): %w", i, err) + } + ids[i] = base58.Encode(h) + } + return ids, nil +} diff --git a/pkg/cascadekit/index.go b/pkg/cascadekit/index.go index e0cb3dce..456b365f 100644 --- a/pkg/cascadekit/index.go +++ b/pkg/cascadekit/index.go @@ -24,13 +24,13 @@ func BuildIndex(layoutIDs []string, layoutSigB64 string) IndexFile { return IndexFile{LayoutIDs: layoutIDs, LayoutSignature: layoutSigB64} } -// EncodeIndexB64 marshals an index file and returns both the raw JSON and base64. -func EncodeIndexB64(idx IndexFile) (b64 string, raw []byte, err error) { - raw, err = json.Marshal(idx) +// EncodeIndexB64 marshals an index file and returns its base64-encoded JSON. +func EncodeIndexB64(idx IndexFile) (string, error) { + raw, err := json.Marshal(idx) if err != nil { - return "", nil, errors.Errorf("marshal index file: %w", err) + return "", errors.Errorf("marshal index file: %w", err) } - return base64.StdEncoding.EncodeToString(raw), raw, nil + return base64.StdEncoding.EncodeToString(raw), nil } // DecodeIndexB64 decodes base64(JSON(IndexFile)). @@ -46,17 +46,12 @@ func DecodeIndexB64(data string) (IndexFile, error) { return indexFile, nil } -// ExtractIndexAndCreatorSig splits a signatures string formatted as: +// ExtractIndexAndCreatorSig splits a signature-format string formatted as: // Base64(index_json).Base64(creator_signature) -func ExtractIndexAndCreatorSig(signatures string) (indexB64 string, creatorSigB64 string, err error) { - parts := strings.Split(signatures, ".") - if len(parts) < 2 { - return "", "", errors.New("invalid signatures format") +func ExtractIndexAndCreatorSig(indexSignatureFormat string) (indexB64 string, creatorSigB64 string, err error) { + parts := strings.Split(indexSignatureFormat, ".") + if len(parts) != 2 { + return "", "", errors.New("invalid index signature format: expected 2 segments (index_b64.creator_sig_b64)") } return parts[0], parts[1], nil } - -// MakeSignatureFormat composes the final signatures string. -func MakeSignatureFormat(indexB64, creatorSigB64 string) string { - return indexB64 + "." + creatorSigB64 -} diff --git a/pkg/cascadekit/index_parse.go b/pkg/cascadekit/index_parse.go index 0fbf3dca..342728d6 100644 --- a/pkg/cascadekit/index_parse.go +++ b/pkg/cascadekit/index_parse.go @@ -15,8 +15,8 @@ func ParseCompressedIndexFile(data []byte) (IndexFile, error) { return IndexFile{}, errors.Errorf("decompress index file: %w", err) } parts := bytes.Split(decompressed, []byte{SeparatorByte}) - if len(parts) < 2 { - return IndexFile{}, errors.New("invalid index file format") + if len(parts) != 3 { + return IndexFile{}, errors.New("invalid index file format: expected 3 parts (index_b64.creator_sig_b64.counter)") } return DecodeIndexB64(string(parts[0])) } diff --git a/pkg/cascadekit/keyring_signatures.go b/pkg/cascadekit/keyring_signatures.go new file mode 100644 index 00000000..968af4b5 --- /dev/null +++ b/pkg/cascadekit/keyring_signatures.go @@ -0,0 +1,14 @@ +package cascadekit + +import ( + "github.com/LumeraProtocol/supernode/v2/pkg/codec" + keyringpkg "github.com/LumeraProtocol/supernode/v2/pkg/keyring" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" +) + +// CreateSignaturesWithKeyring signs layout and index using a Cosmos keyring. +// These helpers centralize keyring-backed signing for clarity. +func CreateSignaturesWithKeyring(layout codec.Layout, kr cosmoskeyring.Keyring, keyName string, ic, max uint32) (string, []string, error) { + signer := func(msg []byte) ([]byte, error) { return keyringpkg.SignBytes(kr, keyName, msg) } + return CreateSignatures(layout, signer, ic, max) +} diff --git a/pkg/cascadekit/metadata.go b/pkg/cascadekit/metadata.go index 534ef793..a77ddfd4 100644 --- a/pkg/cascadekit/metadata.go +++ b/pkg/cascadekit/metadata.go @@ -6,12 +6,12 @@ import ( // NewCascadeMetadata creates a types.CascadeMetadata for RequestAction. // The keeper will populate rq_ids_max; rq_ids_ids is for FinalizeAction only. -func NewCascadeMetadata(dataHashB64, fileName string, rqIdsIc uint64, signatures string, public bool) actiontypes.CascadeMetadata { +func NewCascadeMetadata(dataHashB64, fileName string, rqIdsIc uint64, indexSignatureFormat string, public bool) actiontypes.CascadeMetadata { return actiontypes.CascadeMetadata{ DataHash: dataHashB64, FileName: fileName, RqIdsIc: rqIdsIc, - Signatures: signatures, + Signatures: indexSignatureFormat, Public: public, } } diff --git a/pkg/cascadekit/parsers.go b/pkg/cascadekit/parsers.go index be950e4f..eb90dde0 100644 --- a/pkg/cascadekit/parsers.go +++ b/pkg/cascadekit/parsers.go @@ -2,11 +2,11 @@ package cascadekit import ( "bytes" + "encoding/json" "github.com/LumeraProtocol/supernode/v2/pkg/codec" "github.com/LumeraProtocol/supernode/v2/pkg/errors" "github.com/LumeraProtocol/supernode/v2/pkg/utils" - json "github.com/json-iterator/go" ) // ParseRQMetadataFile parses a compressed rq metadata file into layout, signature and counter. diff --git a/pkg/cascadekit/highlevel.go b/pkg/cascadekit/request_builder.go similarity index 53% rename from pkg/cascadekit/highlevel.go rename to pkg/cascadekit/request_builder.go index 16c0072d..695e2fdf 100644 --- a/pkg/cascadekit/highlevel.go +++ b/pkg/cascadekit/request_builder.go @@ -3,28 +3,21 @@ package cascadekit import ( actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" "github.com/LumeraProtocol/supernode/v2/pkg/codec" - keyringpkg "github.com/LumeraProtocol/supernode/v2/pkg/keyring" cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" ) -// CreateSignaturesWithKeyring signs layout and index using a Cosmos keyring. -func CreateSignaturesWithKeyring(layout codec.Layout, kr cosmoskeyring.Keyring, keyName string, ic, max uint32) (string, []string, error) { - signer := func(msg []byte) ([]byte, error) { return keyringpkg.SignBytes(kr, keyName, msg) } - return CreateSignatures(layout, signer, ic, max) -} - // BuildCascadeRequest builds a Cascade request metadata from layout and file bytes. -// It computes blake3(data) base64, creates the signatures string and index IDs, +// It computes blake3(data) base64, creates the index signature format and index IDs, // and returns a CascadeMetadata ready for RequestAction. func BuildCascadeRequest(layout codec.Layout, fileBytes []byte, fileName string, kr cosmoskeyring.Keyring, keyName string, ic, max uint32, public bool) (actiontypes.CascadeMetadata, []string, error) { dataHashB64, err := ComputeBlake3DataHashB64(fileBytes) if err != nil { return actiontypes.CascadeMetadata{}, nil, err } - signatures, indexIDs, err := CreateSignaturesWithKeyring(layout, kr, keyName, ic, max) + indexSignatureFormat, indexIDs, err := CreateSignaturesWithKeyring(layout, kr, keyName, ic, max) if err != nil { return actiontypes.CascadeMetadata{}, nil, err } - meta := NewCascadeMetadata(dataHashB64, fileName, uint64(ic), signatures, public) + meta := NewCascadeMetadata(dataHashB64, fileName, uint64(ic), indexSignatureFormat, public) return meta, indexIDs, nil } diff --git a/pkg/cascadekit/rqid.go b/pkg/cascadekit/rqid.go index 3a05eb94..8f6a85aa 100644 --- a/pkg/cascadekit/rqid.go +++ b/pkg/cascadekit/rqid.go @@ -1,63 +1,27 @@ package cascadekit import ( - "context" - "encoding/json" - - "github.com/LumeraProtocol/supernode/v2/pkg/codec" "github.com/LumeraProtocol/supernode/v2/pkg/errors" - "github.com/LumeraProtocol/supernode/v2/pkg/utils" ) -// GenRQIdentifiersFilesResponse groups the generated files and their IDs. -type GenRQIdentifiersFilesResponse struct { - // IDs of the Redundant Metadata Files -- len(RQIDs) == len(RedundantMetadataFiles) - RQIDs []string - // RedundantMetadataFiles is a list of redundant files generated from the Metadata file - RedundantMetadataFiles [][]byte -} - -// GenerateLayoutFiles builds redundant metadata files from layout and signature. +// GenerateLayoutFilesFromB64 builds redundant metadata files using a precomputed +// base64(JSON(layout)) and the layout signature, avoiding an extra JSON marshal. // The content is: base64(JSON(layout)).layout_signature -func GenerateLayoutFiles(ctx context.Context, layout codec.Layout, layoutSigB64 string, ic uint32, max uint32) (GenRQIdentifiersFilesResponse, error) { - // Validate single-block to match package invariant - if len(layout.Blocks) != 1 { - return GenRQIdentifiersFilesResponse{}, errors.New("layout must contain exactly one block") - } - - metadataFile, err := jsonMarshal(layout) - if err != nil { - return GenRQIdentifiersFilesResponse{}, errors.Errorf("marshal layout: %w", err) - } - b64Encoded := utils.B64Encode(metadataFile) - - // Compose: base64(JSON(layout)).layout_signature - enc := make([]byte, 0, len(b64Encoded)+1+len(layoutSigB64)) - enc = append(enc, b64Encoded...) +func GenerateLayoutFilesFromB64(layoutB64 []byte, layoutSigB64 string, ic uint32, max uint32) (ids []string, files [][]byte, err error) { + enc := make([]byte, 0, len(layoutB64)+1+len(layoutSigB64)) + enc = append(enc, layoutB64...) enc = append(enc, SeparatorByte) enc = append(enc, []byte(layoutSigB64)...) - - ids, files, err := getIDFiles(enc, ic, max) - if err != nil { - return GenRQIdentifiersFilesResponse{}, errors.Errorf("get ID Files: %w", err) - } - - return GenRQIdentifiersFilesResponse{ - RedundantMetadataFiles: files, - RQIDs: ids, - }, nil + return generateIDFiles(enc, ic, max) } -// GenerateIndexFiles generates index files and their IDs from the full signatures format. -func GenerateIndexFiles(ctx context.Context, signaturesFormat string, ic uint32, max uint32) (indexIDs []string, indexFiles [][]byte, err error) { - // Use the full signatures format that matches what was sent during RequestAction +// GenerateIndexFiles generates index files and their IDs from the full index signature format. +func GenerateIndexFiles(indexSignatureFormat string, ic uint32, max uint32) (indexIDs []string, indexFiles [][]byte, err error) { + // Use the full index signature format that matches what was sent during RequestAction // The chain expects this exact format for ID generation - indexIDs, indexFiles, err = getIDFiles([]byte(signaturesFormat), ic, max) + indexIDs, indexFiles, err = generateIDFiles([]byte(indexSignatureFormat), ic, max) if err != nil { return nil, nil, errors.Errorf("get index ID files: %w", err) } return indexIDs, indexFiles, nil } - -// jsonMarshal marshals a value to JSON. -func jsonMarshal(v interface{}) ([]byte, error) { return json.Marshal(v) } diff --git a/pkg/cascadekit/signatures.go b/pkg/cascadekit/signatures.go index 0c71e492..b8a02da9 100644 --- a/pkg/cascadekit/signatures.go +++ b/pkg/cascadekit/signatures.go @@ -33,35 +33,53 @@ func SignLayoutB64(layout codec.Layout, signer Signer) (layoutB64 string, layout return layoutB64, layoutSigB64, nil } -// CreateSignatures reproduces the cascade signature format and index IDs: +// SignIndexB64 marshals the index to JSON, base64-encodes it, and signs the +// base64 payload, returning both the index base64 and creator-signature base64. +func SignIndexB64(idx IndexFile, signer Signer) (indexB64 string, creatorSigB64 string, err error) { + raw, err := json.Marshal(idx) + if err != nil { + return "", "", errors.Errorf("marshal index file: %w", err) + } + indexB64 = base64.StdEncoding.EncodeToString(raw) + + sig, err := signer([]byte(indexB64)) + if err != nil { + return "", "", errors.Errorf("sign index: %w", err) + } + creatorSigB64 = base64.StdEncoding.EncodeToString(sig) + return indexB64, creatorSigB64, nil +} + +// CreateSignatures produces the index signature format and index IDs: // // Base64(index_json).Base64(creator_signature) // // It validates the layout has exactly one block. -func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (signatures string, indexIDs []string, err error) { +func CreateSignatures(layout codec.Layout, signer Signer, ic, max uint32) (indexSignatureFormat string, indexIDs []string, err error) { layoutB64, layoutSigB64, err := SignLayoutB64(layout, signer) if err != nil { return "", nil, err } // Generate layout IDs (not returned; used to populate the index file) - layoutIDs := GenerateLayoutIDs(layoutB64, layoutSigB64, ic, max) + layoutSignatureFormat := layoutB64 + "." + layoutSigB64 + layoutIDs, err := GenerateLayoutIDs(layoutSignatureFormat, ic, max) + if err != nil { + return "", nil, err + } // Build and sign the index file idx := BuildIndex(layoutIDs, layoutSigB64) - indexB64, _, err := EncodeIndexB64(idx) + indexB64, creatorSigB64, err := SignIndexB64(idx, signer) if err != nil { return "", nil, err } + indexSignatureFormat = fmt.Sprintf("%s.%s", indexB64, creatorSigB64) - creatorSig, err := signer([]byte(indexB64)) + // Generate the index IDs (these are the RQIDs sent to chain) + indexIDs, err = GenerateIndexIDs(indexSignatureFormat, ic, max) if err != nil { - return "", nil, errors.Errorf("sign index: %w", err) + return "", nil, err } - creatorSigB64 := base64.StdEncoding.EncodeToString(creatorSig) - signatures = fmt.Sprintf("%s.%s", indexB64, creatorSigB64) - - // Generate the index IDs (these are the RQIDs sent to chain) - indexIDs = GenerateIndexIDs(signatures, ic, max) - return signatures, indexIDs, nil + return indexSignatureFormat, indexIDs, nil } diff --git a/pkg/cascadekit/verify.go b/pkg/cascadekit/verify.go index 5c4ff8a4..74331dde 100644 --- a/pkg/cascadekit/verify.go +++ b/pkg/cascadekit/verify.go @@ -20,3 +20,11 @@ func VerifySingleBlockIDs(ticket, local codec.Layout) error { } return nil } + +// VerifySingleBlock checks that a layout contains exactly one block. +func VerifySingleBlock(layout codec.Layout) error { + if len(layout.Blocks) != 1 { + return errors.New("layout must contain exactly one block") + } + return nil +} diff --git a/pkg/codec/codec.go b/pkg/codec/codec.go index e9a88a1f..73c31a2a 100644 --- a/pkg/codec/codec.go +++ b/pkg/codec/codec.go @@ -4,9 +4,10 @@ import ( "context" ) -// EncodeResponse represents the response of the encode request. +// EncodeResponse represents the response of the encode request. +// Layout contains the single-block layout produced by the encoder. type EncodeResponse struct { - Metadata Layout + Layout Layout SymbolsDir string } @@ -30,13 +31,20 @@ type EncodeRequest struct { Path string DataSize int } +type CreateMetadataRequest struct { + Path string +} + +// CreateMetadataResponse returns the Layout. +type CreateMetadataResponse struct { + Layout Layout +} // RaptorQ contains methods for request services from RaptorQ service. type Codec interface { // Encode a file Encode(ctx context.Context, req EncodeRequest) (EncodeResponse, error) Decode(ctx context.Context, req DecodeRequest) (DecodeResponse, error) - // CreateMetadata builds the single-block layout metadata for the given file // without generating RaptorQ symbols. - CreateMetadata(ctx context.Context, path string) (Layout, error) + CreateMetadata(ctx context.Context, req CreateMetadataRequest) (CreateMetadataResponse, error) } diff --git a/pkg/codec/codec_default_test.go b/pkg/codec/codec_default_test.go index a55e605d..79b97bd1 100644 --- a/pkg/codec/codec_default_test.go +++ b/pkg/codec/codec_default_test.go @@ -34,7 +34,7 @@ func TestEncode_ToDirA(t *testing.T) { t.Logf("encoded to: %s", resp.SymbolsDir) // Log theoretical minimum percentage of symbols needed per block - for _, b := range resp.Metadata.Blocks { + for _, b := range resp.Layout.Blocks { s := int64(rqSymbolSize) if s <= 0 { s = 65535 @@ -131,15 +131,15 @@ func TestCreateMetadata_SaveToFile(t *testing.T) { c := NewRaptorQCodec(BaseDir) // Create metadata using the codec and write it next to the input file. - layout, err := c.CreateMetadata(ctx, InputPath) + resp, err := c.CreateMetadata(ctx, CreateMetadataRequest{Path: InputPath}) if err != nil { t.Fatalf("create metadata: %v", err) } - data, err := json.MarshalIndent(layout, "", " ") + data, err := json.MarshalIndent(resp.Layout, "", " ") if err != nil { t.Fatalf("marshal metadata: %v", err) } - outPath := " . " + ".layout.json" + outPath := InputPath + ".layout.json" if err := os.WriteFile(outPath, data, 0o644); err != nil { t.Fatalf("write output: %v", err) } diff --git a/pkg/codec/raptorq.go b/pkg/codec/raptorq.go index d2761bd9..487f92d8 100644 --- a/pkg/codec/raptorq.go +++ b/pkg/codec/raptorq.go @@ -78,13 +78,13 @@ func (rq *raptorQ) Encode(ctx context.Context, req EncodeRequest) (EncodeRespons } var encodeResp EncodeResponse - if err := json.Unmarshal(layoutData, &encodeResp.Metadata); err != nil { + if err := json.Unmarshal(layoutData, &encodeResp.Layout); err != nil { return EncodeResponse{}, fmt.Errorf("unmarshal layout: %w", err) } encodeResp.SymbolsDir = symbolsDir // Enforce single-block output; abort if multiple blocks are produced - if n := len(encodeResp.Metadata.Blocks); n != 1 { + if n := len(encodeResp.Layout.Blocks); n != 1 { return EncodeResponse{}, fmt.Errorf("raptorq encode produced %d blocks; single-block layout is required", n) } logtrace.Info(ctx, "rq: encode ok", logtrace.Fields{"symbols_dir": encodeResp.SymbolsDir}) @@ -92,21 +92,21 @@ func (rq *raptorQ) Encode(ctx context.Context, req EncodeRequest) (EncodeRespons } // CreateMetadata builds only the layout metadata for the given file without generating symbols. -func (rq *raptorQ) CreateMetadata(ctx context.Context, path string) (Layout, error) { +func (rq *raptorQ) CreateMetadata(ctx context.Context, req CreateMetadataRequest) (CreateMetadataResponse, error) { // Populate fields; include data-size by stat-ing the file to preserve existing log fields fields := logtrace.Fields{ logtrace.FieldMethod: "CreateMetadata", logtrace.FieldModule: "rq", - "path": path, + "path": req.Path, } - if fi, err := os.Stat(path); err == nil { + if fi, err := os.Stat(req.Path); err == nil { fields["data-size"] = int(fi.Size()) } logtrace.Info(ctx, "rq: create-metadata start", fields) processor, err := raptorq.NewRaptorQProcessor(rqSymbolSize, rqRedundancyFactor, rqMaxMemoryMB, rqConcurrency) if err != nil { - return Layout{}, fmt.Errorf("create RaptorQ processor: %w", err) + return CreateMetadataResponse{}, fmt.Errorf("create RaptorQ processor: %w", err) } defer processor.Free() logtrace.Debug(ctx, "RaptorQ processor created", fields) @@ -122,33 +122,33 @@ func (rq *raptorQ) CreateMetadata(ctx context.Context, path string) (Layout, err tmpDir, err := os.MkdirTemp(base, "rq_meta_*") if err != nil { fields[logtrace.FieldError] = err.Error() - return Layout{}, fmt.Errorf("mkdir temp dir: %w", err) + return CreateMetadataResponse{}, fmt.Errorf("mkdir temp dir: %w", err) } defer os.RemoveAll(tmpDir) layoutPath := filepath.Join(tmpDir, "layout.json") // Use rq-go's metadata-only creation; no symbols are produced here. - resp, err := processor.CreateMetadata(path, layoutPath, blockSize) + resp, err := processor.CreateMetadata(req.Path, layoutPath, blockSize) if err != nil { fields[logtrace.FieldError] = err.Error() - return Layout{}, fmt.Errorf("raptorq create metadata: %w", err) + return CreateMetadataResponse{}, fmt.Errorf("raptorq create metadata: %w", err) } layoutData, err := os.ReadFile(resp.LayoutFilePath) if err != nil { fields[logtrace.FieldError] = err.Error() - return Layout{}, fmt.Errorf("read layout %s: %w", resp.LayoutFilePath, err) + return CreateMetadataResponse{}, fmt.Errorf("read layout %s: %w", resp.LayoutFilePath, err) } var layout Layout if err := json.Unmarshal(layoutData, &layout); err != nil { - return Layout{}, fmt.Errorf("unmarshal layout: %w", err) + return CreateMetadataResponse{}, fmt.Errorf("unmarshal layout: %w", err) } // Enforce single-block output; abort if multiple blocks are produced if n := len(layout.Blocks); n != 1 { - return Layout{}, fmt.Errorf("raptorq metadata produced %d blocks; single-block layout is required", n) + return CreateMetadataResponse{}, fmt.Errorf("raptorq metadata produced %d blocks; single-block layout is required", n) } logtrace.Info(ctx, "rq: create-metadata ok", logtrace.Fields{"blocks": len(layout.Blocks)}) - return layout, nil + return CreateMetadataResponse{Layout: layout}, nil } diff --git a/sdk/action/client.go b/sdk/action/client.go index 82ffa052..af7bca07 100644 --- a/sdk/action/client.go +++ b/sdk/action/client.go @@ -23,6 +23,7 @@ import ( "github.com/LumeraProtocol/supernode/v2/pkg/cascadekit" "github.com/LumeraProtocol/supernode/v2/pkg/codec" keyringpkg "github.com/LumeraProtocol/supernode/v2/pkg/keyring" + "github.com/LumeraProtocol/supernode/v2/pkg/utils" "github.com/cosmos/cosmos-sdk/crypto/keyring" ) @@ -249,17 +250,14 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath if err != nil { return actiontypes.CascadeMetadata{}, "", "", fmt.Errorf("stat file: %w", err) } - data, err := os.ReadFile(filePath) - if err != nil { - return actiontypes.CascadeMetadata{}, "", "", fmt.Errorf("read file: %w", err) - } // Build layout metadata only (no symbols). Supernodes will create symbols. rq := codec.NewRaptorQCodec("") - layout, err := rq.CreateMetadata(ctx, filePath) + metaResp, err := rq.CreateMetadata(ctx, codec.CreateMetadataRequest{Path: filePath}) if err != nil { return actiontypes.CascadeMetadata{}, "", "", fmt.Errorf("raptorq create metadata: %w", err) } + layout := metaResp.Layout // Derive `max` from chain params, then create signatures and index IDs paramsResp, err := c.lumeraClient.GetActionParams(ctx) @@ -277,22 +275,24 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath // Pick a random initial counter in [1,100] rnd, _ := crand.Int(crand.Reader, big.NewInt(100)) ic := uint32(rnd.Int64() + 1) // 1..100 - signatures, _, err := cascadekit.CreateSignaturesWithKeyring(layout, c.keyring, c.config.Account.KeyName, ic, max) + // Create signatures from the layout struct + indexSignatureFormat, _, err := cascadekit.CreateSignaturesWithKeyring(layout, c.keyring, c.config.Account.KeyName, ic, max) if err != nil { return actiontypes.CascadeMetadata{}, "", "", fmt.Errorf("create signatures: %w", err) } - // Compute data hash (blake3) as base64 - dataHashB64, err := cascadekit.ComputeBlake3DataHashB64(data) + // Compute data hash (blake3) as base64 using a streaming file hash to avoid loading entire file + h, err := utils.ComputeHashOfFile(filePath) if err != nil { return actiontypes.CascadeMetadata{}, "", "", fmt.Errorf("hash data: %w", err) } + dataHashB64 := base64.StdEncoding.EncodeToString(h) // Derive file name from path fileName := filepath.Base(filePath) // Build metadata proto - meta := cascadekit.NewCascadeMetadata(dataHashB64, fileName, uint64(ic), signatures, public) + meta := cascadekit.NewCascadeMetadata(dataHashB64, fileName, uint64(ic), indexSignatureFormat, public) // Fetch params (already fetched) to get denom and expiration duration denom := paramsResp.Params.BaseActionFee.Denom @@ -316,15 +316,11 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath // GenerateStartCascadeSignatureFromFile computes blake3(file) and signs it with the configured key. // Returns base64-encoded signature suitable for StartCascade. func (c *ClientImpl) GenerateStartCascadeSignatureFromFile(ctx context.Context, filePath string) (string, error) { - data, err := os.ReadFile(filePath) - if err != nil { - return "", fmt.Errorf("read file: %w", err) - } - hash, err := cascadekit.ComputeBlake3Hash(data) + h, err := utils.ComputeHashOfFile(filePath) if err != nil { return "", fmt.Errorf("blake3: %w", err) } - sig, err := keyringpkg.SignBytes(c.keyring, c.config.Account.KeyName, hash) + sig, err := keyringpkg.SignBytes(c.keyring, c.config.Account.KeyName, h) if err != nil { return "", fmt.Errorf("sign hash: %w", err) } diff --git a/sdk/helpers/github_helper.go b/sdk/helpers/github_helper.go index edf5eefc..0c028c55 100644 --- a/sdk/helpers/github_helper.go +++ b/sdk/helpers/github_helper.go @@ -1,6 +1,7 @@ package helpers import ( + "os" "strings" "sync" @@ -16,6 +17,10 @@ var ( // The value is fetched once per process and cached. If lookup fails, it returns // an empty string so callers can gracefully skip strict version gating. func ResolveRequiredSupernodeVersion() string { + // Bypass strict version gating during integration tests. + if os.Getenv("INTEGRATION_TEST") == "true" { + return "" + } requiredVersionOnce.Do(func() { client := gh.NewClient("LumeraProtocol/supernode") if client != nil { diff --git a/sdk/task/helpers.go b/sdk/task/helpers.go index 2d2b7391..1612f12d 100644 --- a/sdk/task/helpers.go +++ b/sdk/task/helpers.go @@ -10,7 +10,7 @@ import ( "sort" "strings" - "github.com/LumeraProtocol/supernode/v2/pkg/cascadekit" + "github.com/LumeraProtocol/supernode/v2/pkg/utils" "github.com/LumeraProtocol/supernode/v2/sdk/adapters/lumera" ) @@ -145,7 +145,7 @@ func orderSupernodesByDeterministicDistance(seed string, sns lumera.Supernodes) return sns } // Precompute seed hash (blake3) - seedHash, err := cascadekit.ComputeBlake3Hash([]byte(seed)) + seedHash, err := utils.Blake3Hash([]byte(seed)) if err != nil { return sns } @@ -160,7 +160,7 @@ func orderSupernodesByDeterministicDistance(seed string, sns lumera.Supernodes) if id == "" { id = sn.GrpcEndpoint } - nHash, err := cascadekit.ComputeBlake3Hash([]byte(id)) + nHash, err := utils.Blake3Hash([]byte(id)) if err != nil { nd = append(nd, nodeDist{sn: sn, distance: new(big.Int).SetInt64(0)}) continue diff --git a/supernode/adaptors/rq.go b/supernode/adaptors/rq.go index 5586edf8..b8efa1dd 100644 --- a/supernode/adaptors/rq.go +++ b/supernode/adaptors/rq.go @@ -2,19 +2,20 @@ package adaptors import ( "context" + "os" "github.com/LumeraProtocol/supernode/v2/pkg/codec" ) // CodecService wraps codec operations used by cascade type CodecService interface { - EncodeInput(ctx context.Context, actionID string, path string) (EncodeResult, error) + EncodeInput(ctx context.Context, actionID string, filePath string) (EncodeResult, error) Decode(ctx context.Context, req DecodeRequest) (DecodeResult, error) } type EncodeResult struct { SymbolsDir string - Metadata codec.Layout + Layout codec.Layout } type DecodeRequest struct { @@ -32,12 +33,16 @@ type codecImpl struct{ codec codec.Codec } func NewCodecService(c codec.Codec) CodecService { return &codecImpl{codec: c} } -func (c *codecImpl) EncodeInput(ctx context.Context, actionID, path string) (EncodeResult, error) { - res, err := c.codec.Encode(ctx, codec.EncodeRequest{TaskID: actionID, Path: path}) +func (c *codecImpl) EncodeInput(ctx context.Context, actionID, filePath string) (EncodeResult, error) { + var size int + if fi, err := os.Stat(filePath); err == nil { + size = int(fi.Size()) + } + res, err := c.codec.Encode(ctx, codec.EncodeRequest{TaskID: actionID, Path: filePath, DataSize: size}) if err != nil { return EncodeResult{}, err } - return EncodeResult{SymbolsDir: res.SymbolsDir, Metadata: res.Metadata}, nil + return EncodeResult{SymbolsDir: res.SymbolsDir, Layout: res.Layout}, nil } func (c *codecImpl) Decode(ctx context.Context, req DecodeRequest) (DecodeResult, error) { diff --git a/supernode/cascade/config.go b/supernode/cascade/config.go deleted file mode 100644 index bb32ca13..00000000 --- a/supernode/cascade/config.go +++ /dev/null @@ -1,9 +0,0 @@ -package cascade - -// Config contains settings for the cascade service -type Config struct { - // SupernodeAccountAddress is the on-chain account address of this supernode. - SupernodeAccountAddress string `mapstructure:"-" json:"-"` - - RqFilesDir string `mapstructure:"rq_files_dir" json:"rq_files_dir,omitempty"` -} diff --git a/supernode/cascade/helper.go b/supernode/cascade/helper.go index 491e2174..a2006354 100644 --- a/supernode/cascade/helper.go +++ b/supernode/cascade/helper.go @@ -48,95 +48,96 @@ func (task *CascadeRegistrationTask) ensureIsTopSupernode(ctx context.Context, b return task.wrapErr(ctx, "failed to get top SNs", err, f) } logtrace.Info(ctx, "register: top-supernodes fetch ok", f) - if !supernode.Exists(top.Supernodes, task.config.SupernodeAccountAddress) { + if !supernode.Exists(top.Supernodes, task.SupernodeAccountAddress) { addresses := make([]string, len(top.Supernodes)) for i, sn := range top.Supernodes { addresses[i] = sn.SupernodeAccount } - logtrace.Debug(ctx, "Supernode not in top list", logtrace.Fields{"currentAddress": task.config.SupernodeAccountAddress, "topSupernodes": addresses}) - return task.wrapErr(ctx, "current supernode does not exist in the top SNs list", errors.Errorf("current address: %s, top supernodes: %v", task.config.SupernodeAccountAddress, addresses), f) + logtrace.Debug(ctx, "Supernode not in top list", logtrace.Fields{"currentAddress": task.SupernodeAccountAddress, "topSupernodes": addresses}) + return task.wrapErr(ctx, "current supernode does not exist in the top SNs list", errors.Errorf("current address: %s, top supernodes: %v", task.SupernodeAccountAddress, addresses), f) } logtrace.Info(ctx, "register: top-supernode verified", f) return nil } -func (task *CascadeRegistrationTask) encodeInput(ctx context.Context, actionID string, path string, f logtrace.Fields) (*adaptors.EncodeResult, error) { +func (task *CascadeRegistrationTask) encodeInput(ctx context.Context, actionID string, filePath string, f logtrace.Fields) (*adaptors.EncodeResult, error) { if f == nil { f = logtrace.Fields{} } f[logtrace.FieldActionID] = actionID - f["input_path"] = path + f["file_path"] = filePath logtrace.Info(ctx, "register: encode input start", f) - resp, err := task.RQ.EncodeInput(ctx, actionID, path) + res, err := task.RQ.EncodeInput(ctx, actionID, filePath) if err != nil { return nil, task.wrapErr(ctx, "failed to encode data", err, f) } // Enrich fields with result for subsequent logs - f["symbols_dir"] = resp.SymbolsDir + f["symbols_dir"] = res.SymbolsDir logtrace.Info(ctx, "register: encode input ok", f) - return &resp, nil + return &res, nil } -func (task *CascadeRegistrationTask) verifySignatureAndDecodeLayout(ctx context.Context, encoded string, creator string, encodedMeta codec.Layout, f logtrace.Fields) (codec.Layout, string, error) { - if f == nil { - f = logtrace.Fields{} - } - f[logtrace.FieldCreator] = creator - logtrace.Info(ctx, "register: verify+decode layout start", f) - indexFileB64, creatorSig, err := cascadekit.ExtractIndexAndCreatorSig(encoded) +// ValidateIndexAndLayout verifies: +// - creator signature over the index payload (index_b64) +// - layout signature over base64(JSON(layout)) +// Returns the decoded index and layoutB64. No logging here; callers handle it. +func (task *CascadeRegistrationTask) validateIndexAndLayout(ctx context.Context, creator string, indexSignatureFormat string, layout codec.Layout) (cascadekit.IndexFile, []byte, error) { + // Extract and verify creator signature on index + indexB64, creatorSigB64, err := cascadekit.ExtractIndexAndCreatorSig(indexSignatureFormat) if err != nil { - return codec.Layout{}, "", task.wrapErr(ctx, "failed to extract index file and creator signature", err, f) + return cascadekit.IndexFile{}, nil, err } - logtrace.Info(ctx, "register: index+creatorSig extracted", f) - creatorSigBytes, err := base64.StdEncoding.DecodeString(creatorSig) + creatorSig, err := base64.StdEncoding.DecodeString(creatorSigB64) if err != nil { - return codec.Layout{}, "", task.wrapErr(ctx, "failed to decode creator signature from base64", err, f) + return cascadekit.IndexFile{}, nil, err } - if err := task.LumeraClient.Verify(ctx, creator, []byte(indexFileB64), creatorSigBytes); err != nil { - return codec.Layout{}, "", task.wrapErr(ctx, "failed to verify creator signature", err, f) + if err := task.LumeraClient.Verify(ctx, creator, []byte(indexB64), creatorSig); err != nil { + return cascadekit.IndexFile{}, nil, err } - logtrace.Info(ctx, "register: creator signature verified", f) - indexFile, err := cascadekit.DecodeIndexB64(indexFileB64) + // Decode index + indexFile, err := cascadekit.DecodeIndexB64(indexB64) if err != nil { - return codec.Layout{}, "", task.wrapErr(ctx, "failed to decode index file", err, f) + return cascadekit.IndexFile{}, nil, err } - _ = indexFile // keep for potential future detail logs - layoutSigBytes, err := base64.StdEncoding.DecodeString(indexFile.LayoutSignature) + // Build layoutB64 and verify single-block + signature + layoutB64, err := cascadekit.LayoutB64(layout) if err != nil { - return codec.Layout{}, "", task.wrapErr(ctx, "failed to decode layout signature from base64", err, f) + return cascadekit.IndexFile{}, nil, err } - layoutB64, err := cascadekit.LayoutB64(encodedMeta) + if err := cascadekit.VerifySingleBlock(layout); err != nil { + return cascadekit.IndexFile{}, nil, err + } + layoutSig, err := base64.StdEncoding.DecodeString(indexFile.LayoutSignature) if err != nil { - return codec.Layout{}, "", task.wrapErr(ctx, "failed to build layout base64", err, f) + return cascadekit.IndexFile{}, nil, err } - if err := task.LumeraClient.Verify(ctx, creator, layoutB64, layoutSigBytes); err != nil { - return codec.Layout{}, "", task.wrapErr(ctx, "failed to verify layout signature", err, f) + if err := task.LumeraClient.Verify(ctx, creator, layoutB64, layoutSig); err != nil { + return cascadekit.IndexFile{}, nil, err } - logtrace.Info(ctx, "register: layout signature verified", f) - logtrace.Info(ctx, "register: verify+decode layout ok", f) - return encodedMeta, indexFile.LayoutSignature, nil + return indexFile, layoutB64, nil } -func (task *CascadeRegistrationTask) generateRQIDFiles(ctx context.Context, meta actiontypes.CascadeMetadata, sig string, encodedMeta codec.Layout, f logtrace.Fields) (cascadekit.GenRQIdentifiersFilesResponse, error) { +func (task *CascadeRegistrationTask) generateRQIDFiles(ctx context.Context, meta actiontypes.CascadeMetadata, layoutSigB64 string, layoutB64 []byte, f logtrace.Fields) ([]string, [][]byte, error) { if f == nil { f = logtrace.Fields{} } f["rq_ic"] = uint32(meta.RqIdsIc) f["rq_max"] = uint32(meta.RqIdsMax) logtrace.Info(ctx, "register: rqid files generation start", f) - layoutRes, err := cascadekit.GenerateLayoutFiles(ctx, encodedMeta, sig, uint32(meta.RqIdsIc), uint32(meta.RqIdsMax)) + + layoutIDs, layoutFiles, err := cascadekit.GenerateLayoutFilesFromB64(layoutB64, layoutSigB64, uint32(meta.RqIdsIc), uint32(meta.RqIdsMax)) if err != nil { - return cascadekit.GenRQIdentifiersFilesResponse{}, task.wrapErr(ctx, "failed to generate layout files", err, f) + return nil, nil, task.wrapErr(ctx, "failed to generate layout files", err, f) } - logtrace.Info(ctx, "register: layout files generated", logtrace.Fields{"count": len(layoutRes.RedundantMetadataFiles)}) - indexIDs, indexFiles, err := cascadekit.GenerateIndexFiles(ctx, meta.Signatures, uint32(meta.RqIdsIc), uint32(meta.RqIdsMax)) + logtrace.Info(ctx, "register: layout files generated", logtrace.Fields{"count": len(layoutFiles), "layout_ids": len(layoutIDs)}) + indexIDs, indexFiles, err := cascadekit.GenerateIndexFiles(meta.Signatures, uint32(meta.RqIdsIc), uint32(meta.RqIdsMax)) if err != nil { - return cascadekit.GenRQIdentifiersFilesResponse{}, task.wrapErr(ctx, "failed to generate index files", err, f) + return nil, nil, task.wrapErr(ctx, "failed to generate index files", err, f) } - allFiles := append(layoutRes.RedundantMetadataFiles, indexFiles...) + allFiles := append(layoutFiles, indexFiles...) logtrace.Info(ctx, "register: index files generated", logtrace.Fields{"count": len(indexFiles), "rqids": len(indexIDs)}) logtrace.Info(ctx, "register: rqid files generation ok", logtrace.Fields{"total_files": len(allFiles)}) - return cascadekit.GenRQIdentifiersFilesResponse{RQIDs: indexIDs, RedundantMetadataFiles: allFiles}, nil + return indexIDs, allFiles, nil } func (task *CascadeRegistrationTask) storeArtefacts(ctx context.Context, actionID string, idFiles [][]byte, symbolsDir string, f logtrace.Fields) error { @@ -182,7 +183,8 @@ func (task *CascadeRegistrationTask) verifyActionFee(ctx context.Context, action } fields["data_bytes"] = dataSize logtrace.Info(ctx, "register: verify action fee start", fields) - dataSizeInKBs := dataSize / 1024 + // Round up to the nearest KB to avoid underestimating required fee + dataSizeInKBs := (dataSize + 1023) / 1024 fee, err := task.LumeraClient.GetActionFee(ctx, strconv.Itoa(dataSizeInKBs)) if err != nil { return task.wrapErr(ctx, "failed to get action fee", err, fields) diff --git a/supernode/cascade/register.go b/supernode/cascade/register.go index 2fe2623d..a9b44117 100644 --- a/supernode/cascade/register.go +++ b/supernode/cascade/register.go @@ -25,125 +25,138 @@ type RegisterResponse struct { } func (task *CascadeRegistrationTask) Register( - ctx context.Context, - req *RegisterRequest, - send func(resp *RegisterResponse) error, + ctx context.Context, + req *RegisterRequest, + send func(resp *RegisterResponse) error, ) (err error) { - if req != nil && req.ActionID != "" { - ctx = logtrace.CtxWithCorrelationID(ctx, req.ActionID) - ctx = logtrace.CtxWithOrigin(ctx, "first_pass") - task.taskID = req.TaskID - } - - fields := logtrace.Fields{logtrace.FieldMethod: "Register", logtrace.FieldRequest: req} - logtrace.Info(ctx, "register: request", fields) - defer func() { - if req != nil && req.FilePath != "" { - if remErr := os.RemoveAll(req.FilePath); remErr != nil { - logtrace.Warn(ctx, "Failed to remove uploaded file", fields) - } else { - logtrace.Debug(ctx, "Uploaded file cleaned up", fields) - } - } - }() - - action, err := task.fetchAction(ctx, req.ActionID, fields) - if err != nil { - return err - } - fields[logtrace.FieldBlockHeight] = action.BlockHeight + // Step 1: Correlate context and capture task identity + if req != nil && req.ActionID != "" { + ctx = logtrace.CtxWithCorrelationID(ctx, req.ActionID) + ctx = logtrace.CtxWithOrigin(ctx, "first_pass") + task.taskID = req.TaskID + } + + // Step 2: Log request and ensure uploaded file cleanup + fields := logtrace.Fields{logtrace.FieldMethod: "Register", logtrace.FieldRequest: req} + logtrace.Info(ctx, "register: request", fields) + defer func() { + if req != nil && req.FilePath != "" { + if remErr := os.RemoveAll(req.FilePath); remErr != nil { + logtrace.Warn(ctx, "Failed to remove uploaded file", fields) + } else { + logtrace.Debug(ctx, "Uploaded file cleaned up", fields) + } + } + }() + + // Step 3: Fetch the action details + action, err := task.fetchAction(ctx, req.ActionID, fields) + if err != nil { + return err + } + fields[logtrace.FieldBlockHeight] = action.BlockHeight fields[logtrace.FieldCreator] = action.Creator fields[logtrace.FieldStatus] = action.State fields[logtrace.FieldPrice] = action.Price logtrace.Info(ctx, "register: action fetched", fields) task.streamEvent(SupernodeEventTypeActionRetrieved, "Action retrieved", "", send) - if err := task.verifyActionFee(ctx, action, req.DataSize, fields); err != nil { - return err - } + // Step 4: Verify action fee based on data size (rounded up to KB) + if err := task.verifyActionFee(ctx, action, req.DataSize, fields); err != nil { + return err + } logtrace.Info(ctx, "register: fee verified", fields) task.streamEvent(SupernodeEventTypeActionFeeVerified, "Action fee verified", "", send) - fields[logtrace.FieldSupernodeState] = task.config.SupernodeAccountAddress - if err := task.ensureIsTopSupernode(ctx, uint64(action.BlockHeight), fields); err != nil { - return err - } + // Step 5: Ensure this node is eligible (top supernode for block) + fields[logtrace.FieldSupernodeState] = task.SupernodeAccountAddress + if err := task.ensureIsTopSupernode(ctx, uint64(action.BlockHeight), fields); err != nil { + return err + } logtrace.Info(ctx, "register: top supernode confirmed", fields) task.streamEvent(SupernodeEventTypeTopSupernodeCheckPassed, "Top supernode eligibility confirmed", "", send) - cascadeMeta, err := cascadekit.UnmarshalCascadeMetadata(action.Metadata) - if err != nil { - return task.wrapErr(ctx, "failed to unmarshal cascade metadata", err, fields) - } + // Step 6: Decode Cascade metadata from the action + cascadeMeta, err := cascadekit.UnmarshalCascadeMetadata(action.Metadata) + if err != nil { + return task.wrapErr(ctx, "failed to unmarshal cascade metadata", err, fields) + } logtrace.Info(ctx, "register: metadata decoded", fields) task.streamEvent(SupernodeEventTypeMetadataDecoded, "Cascade metadata decoded", "", send) - if err := cascadekit.VerifyB64DataHash(req.DataHash, cascadeMeta.DataHash); err != nil { - return err - } + // Step 7: Verify request-provided data hash matches metadata + if err := cascadekit.VerifyB64DataHash(req.DataHash, cascadeMeta.DataHash); err != nil { + return err + } logtrace.Debug(ctx, "request data-hash has been matched with the action data-hash", fields) logtrace.Info(ctx, "register: data hash matched", fields) task.streamEvent(SupernodeEventTypeDataHashVerified, "Data hash verified", "", send) - encResp, err := task.encodeInput(ctx, req.ActionID, req.FilePath, fields) - if err != nil { - return err - } - fields["symbols_dir"] = encResp.SymbolsDir + // Step 8: Encode input using the RQ codec to produce layout and symbols + encodeResult, err := task.encodeInput(ctx, req.ActionID, req.FilePath, fields) + if err != nil { + return err + } + fields["symbols_dir"] = encodeResult.SymbolsDir logtrace.Info(ctx, "register: input encoded", fields) task.streamEvent(SupernodeEventTypeInputEncoded, "Input encoded", "", send) - layout, signature, err := task.verifySignatureAndDecodeLayout(ctx, cascadeMeta.Signatures, action.Creator, encResp.Metadata, fields) - if err != nil { - return err - } - logtrace.Info(ctx, "register: signature verified", fields) - task.streamEvent(SupernodeEventTypeSignatureVerified, "Signature verified", "", send) - - rqidResp, err := task.generateRQIDFiles(ctx, cascadeMeta, signature, encResp.Metadata, fields) - if err != nil { - return err - } + // Step 9: Verify index and layout signatures; produce layoutB64 + logtrace.Info(ctx, "register: verify+decode layout start", fields) + indexFile, layoutB64, vErr := task.validateIndexAndLayout(ctx, action.Creator, cascadeMeta.Signatures, encodeResult.Layout) + if vErr != nil { + return task.wrapErr(ctx, "signature or index validation failed", vErr, fields) + } + layoutSignatureB64 := indexFile.LayoutSignature + logtrace.Info(ctx, "register: signature verified", fields) + task.streamEvent(SupernodeEventTypeSignatureVerified, "Signature verified", "", send) + + // Step 10: Generate RQID files (layout and index) and compute IDs + rqIDs, idFiles, err := task.generateRQIDFiles(ctx, cascadeMeta, layoutSignatureB64, layoutB64, fields) + if err != nil { + return err + } // Calculate combined size of all index and layout files totalSize := 0 - for _, file := range rqidResp.RedundantMetadataFiles { + for _, file := range idFiles { totalSize += len(file) } - fields["id_files_count"] = len(rqidResp.RedundantMetadataFiles) + fields["id_files_count"] = len(idFiles) + fields["rqids_count"] = len(rqIDs) fields["combined_files_size_bytes"] = totalSize fields["combined_files_size_kb"] = float64(totalSize) / 1024 fields["combined_files_size_mb"] = float64(totalSize) / (1024 * 1024) logtrace.Info(ctx, "register: rqid files generated", fields) task.streamEvent(SupernodeEventTypeRQIDsGenerated, "RQID files generated", "", send) - if err := cascadekit.VerifySingleBlockIDs(layout, encResp.Metadata); err != nil { - return task.wrapErr(ctx, "failed to verify IDs", err, fields) - } logtrace.Info(ctx, "register: rqids validated", fields) task.streamEvent(SupernodeEventTypeRqIDsVerified, "RQIDs verified", "", send) - if _, err := task.LumeraClient.SimulateFinalizeAction(ctx, action.ActionID, rqidResp.RQIDs); err != nil { - fields[logtrace.FieldError] = err.Error() - logtrace.Info(ctx, "register: finalize simulation failed", fields) - task.streamEvent(SupernodeEventTypeFinalizeSimulationFailed, "Finalize simulation failed", "", send) - return task.wrapErr(ctx, "finalize action simulation failed", err, fields) - } + // Step 11: Simulate finalize to ensure the tx will succeed + if _, err := task.LumeraClient.SimulateFinalizeAction(ctx, action.ActionID, rqIDs); err != nil { + fields[logtrace.FieldError] = err.Error() + logtrace.Info(ctx, "register: finalize simulation failed", fields) + task.streamEvent(SupernodeEventTypeFinalizeSimulationFailed, "Finalize simulation failed", "", send) + return task.wrapErr(ctx, "finalize action simulation failed", err, fields) + } logtrace.Info(ctx, "register: finalize simulation passed", fields) task.streamEvent(SupernodeEventTypeFinalizeSimulated, "Finalize simulation passed", "", send) - if err := task.storeArtefacts(ctx, action.ActionID, rqidResp.RedundantMetadataFiles, encResp.SymbolsDir, fields); err != nil { - return err - } - task.emitArtefactsStored(ctx, fields, encResp.Metadata, send) - - resp, err := task.LumeraClient.FinalizeAction(ctx, action.ActionID, rqidResp.RQIDs) - if err != nil { - fields[logtrace.FieldError] = err.Error() - logtrace.Info(ctx, "register: finalize action error", fields) - return task.wrapErr(ctx, "failed to finalize action", err, fields) - } + // Step 12: Store artefacts to the network store + if err := task.storeArtefacts(ctx, action.ActionID, idFiles, encodeResult.SymbolsDir, fields); err != nil { + return err + } + task.emitArtefactsStored(ctx, fields, encodeResult.Layout, send) + + // Step 13: Finalize the action on-chain + resp, err := task.LumeraClient.FinalizeAction(ctx, action.ActionID, rqIDs) + if err != nil { + fields[logtrace.FieldError] = err.Error() + logtrace.Info(ctx, "register: finalize action error", fields) + return task.wrapErr(ctx, "failed to finalize action", err, fields) + } txHash := resp.TxResponse.TxHash fields[logtrace.FieldTxHash] = txHash logtrace.Info(ctx, "register: action finalized", fields) diff --git a/supernode/cascade/service.go b/supernode/cascade/service.go index 374a9389..29b047bd 100644 --- a/supernode/cascade/service.go +++ b/supernode/cascade/service.go @@ -11,12 +11,11 @@ import ( ) type CascadeService struct { - config *Config - - LumeraClient adaptors.LumeraClient - P2P adaptors.P2PService - RQ adaptors.CodecService - P2PClient p2p.Client + LumeraClient adaptors.LumeraClient + P2P adaptors.P2PService + RQ adaptors.CodecService + P2PClient p2p.Client + SupernodeAccountAddress string } // Compile-time checks to ensure CascadeService implements required interfaces @@ -32,12 +31,12 @@ func (service *CascadeService) NewCascadeRegistrationTask() CascadeTask { func (service *CascadeService) Run(ctx context.Context) error { <-ctx.Done(); return nil } // NewCascadeService returns a new CascadeService instance -func NewCascadeService(config *Config, lumera lumera.Client, p2pClient p2p.Client, codec codec.Codec, rqstore rqstore.Store) *CascadeService { +func NewCascadeService(supernodeAccountAddress string, lumera lumera.Client, p2pClient p2p.Client, codec codec.Codec, rqstore rqstore.Store) *CascadeService { return &CascadeService{ - config: config, - LumeraClient: adaptors.NewLumeraClient(lumera), - P2P: adaptors.NewP2PService(p2pClient, rqstore), - RQ: adaptors.NewCodecService(codec), - P2PClient: p2pClient, + LumeraClient: adaptors.NewLumeraClient(lumera), + P2P: adaptors.NewP2PService(p2pClient, rqstore), + RQ: adaptors.NewCodecService(codec), + P2PClient: p2pClient, + SupernodeAccountAddress: supernodeAccountAddress, } } diff --git a/supernode/cmd/start.go b/supernode/cmd/start.go index b96bc7d5..44722f24 100644 --- a/supernode/cmd/start.go +++ b/supernode/cmd/start.go @@ -115,7 +115,7 @@ The supernode will connect to the Lumera network and begin participating in the // Configure cascade service cService := cascadeService.NewCascadeService( - &cascadeService.Config{SupernodeAccountAddress: appConfig.SupernodeConfig.Identity, RqFilesDir: appConfig.GetRaptorQFilesDir()}, + appConfig.SupernodeConfig.Identity, lumeraClient, p2pService, codec.NewRaptorQCodec(appConfig.GetRaptorQFilesDir()), diff --git a/tests/system/e2e_cascade_test.go b/tests/system/e2e_cascade_test.go index 2db7ad09..b9af06d2 100644 --- a/tests/system/e2e_cascade_test.go +++ b/tests/system/e2e_cascade_test.go @@ -297,7 +297,7 @@ func TestCascadeE2E(t *testing.T) { t.Logf("Requesting cascade action with metadata: %s", metadata) t.Logf("Action type: %s, Price: %s, Expiration: %s", actionType, autoPrice, expirationTime) - response, err := lumeraClinet.ActionMsg().RequestAction(ctx, actionType, metadata, autoPrice, expirationTime) + response, _ := lumeraClinet.ActionMsg().RequestAction(ctx, actionType, metadata, autoPrice, expirationTime) txresp := response.TxResponse @@ -356,26 +356,38 @@ func TestCascadeE2E(t *testing.T) { // Step 9: Subscribe to all events and extract tx hash // --------------------------------------- - // Channel to receive the transaction hash - txHashCh := make(chan string, 1) - completionCh := make(chan bool, 1) - - // Subscribe to ALL events - err = actionClient.SubscribeToAllEvents(context.Background(), func(ctx context.Context, e event.Event) { - // Only capture TxhasReceived events - if e.Type == event.SDKTaskTxHashReceived { - if txHash, ok := e.Data[event.KeyTxHash].(string); ok && txHash != "" { - // Send the hash to our channel - txHashCh <- txHash - } - } - - // Also monitor for task completion - if e.Type == event.SDKTaskCompleted { - completionCh <- true - } - }) - require.NoError(t, err, "Failed to subscribe to events") + // Channels to receive async signals + txHashCh := make(chan string, 1) + completionCh := make(chan bool, 1) + errCh := make(chan string, 1) + + // Subscribe to ALL events (non-blocking sends to avoid handler stalls) + err = actionClient.SubscribeToAllEvents(context.Background(), func(ctx context.Context, e event.Event) { + // Log every event for debugging and capture key ones + t.Logf("SDK event: type=%s data=%v", e.Type, e.Data) + // Only capture TxhasReceived events + if e.Type == event.SDKTaskTxHashReceived { + if txHash, ok := e.Data[event.KeyTxHash].(string); ok && txHash != "" { + // Non-blocking send; drop if buffer full + select { case txHashCh <- txHash: default: } + } + } + + // Also monitor for task completion + if e.Type == event.SDKTaskCompleted { + // Non-blocking send; drop if buffer full + select { case completionCh <- true: default: } + } + // Capture task failures and propagate error message to main goroutine + if e.Type == event.SDKTaskFailed { + if msg, ok := e.Data[event.KeyError].(string); ok && msg != "" { + select { case errCh <- msg: default: } + } else { + select { case errCh <- "task failed (no error message)" : default: } + } + } + }) + require.NoError(t, err, "Failed to subscribe to events") // Start cascade operation @@ -390,8 +402,26 @@ func TestCascadeE2E(t *testing.T) { require.NoError(t, err, "Failed to start cascade operation") t.Logf("Cascade operation started with task ID: %s", taskID) - recievedhash := <-txHashCh - <-completionCh + // Wait for both tx-hash and completion with a timeout + var recievedhash string + done := false + timeout := time.After(2 * time.Minute) +waitLoop: + for { + if recievedhash != "" && done { + break waitLoop + } + select { + case h := <-txHashCh: + if recievedhash == "" { recievedhash = h } + case <-completionCh: + done = true + case emsg := <-errCh: + t.Fatalf("cascade task reported failure: %s", emsg) + case <-timeout: + t.Fatalf("timeout waiting for events; recievedhash=%q done=%v", recievedhash, done) + } + } t.Logf("Received transaction hash: %s", recievedhash) diff --git a/tests/system/go.mod b/tests/system/go.mod index 99bb1df9..052a1b76 100644 --- a/tests/system/go.mod +++ b/tests/system/go.mod @@ -104,6 +104,7 @@ require ( github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect @@ -118,7 +119,6 @@ require ( github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -128,8 +128,6 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/highwayhash v1.0.3 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect diff --git a/tests/system/go.sum b/tests/system/go.sum index 5737b819..1ac6ecda 100644 --- a/tests/system/go.sum +++ b/tests/system/go.sum @@ -427,6 +427,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= @@ -505,7 +507,6 @@ github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -579,11 +580,9 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=