Skip to content

Commit efeba07

Browse files
mateeullahmalikNightCrawler
authored andcommitted
lep5: add availability commitment support to supernode and SDK
SDK Client (BuildCascadeMetadataFromFile): - Build Merkle tree from file chunks using chain SVC params - Generate challenge indices and AvailabilityCommitment - Attach commitment to CascadeMetadata at registration Supernode Server (Register): - After data hash verification, verify Merkle root against on-chain commitment - Generate chunk proofs for challenged indices - Pass proofs through SimulateFinalizeAction and FinalizeAction Lumera Client Layer: - Thread ChunkProofs through FinalizeCascadeAction and SimulateFinalizeCascadeAction - Include chunk_proofs in finalize metadata JSON - Update interface, impl, helpers, mocks, test fakes New: pkg/cascadekit/commitment.go - BuildCommitmentFromFile: chunk file, build tree, derive indices - VerifyCommitmentRoot: rebuild tree and verify against on-chain root - GenerateChunkProofs: produce Merkle proofs for challenge indices - SelectChunkSize: adaptive chunk sizing per LEP-5 spec go.mod: enable local lumera replace for PR-103 compatibility
1 parent 5517a57 commit efeba07

16 files changed

Lines changed: 315 additions & 45 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
cosmossdk.io/math v1.5.3
1313
github.com/AlecAivazis/survey/v2 v2.3.7
1414
github.com/DataDog/zstd v1.5.7
15-
github.com/LumeraProtocol/lumera v1.11.0-rc
15+
github.com/LumeraProtocol/lumera v1.11.1-0.20260308102614-4d4f1ce3f65e
1616
github.com/LumeraProtocol/rq-go v0.2.1
1717
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce
1818
github.com/cenkalti/backoff/v4 v4.3.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50
111111
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 h1:ig/FpDD2JofP/NExKQUbn7uOSZzJAQqogfqluZK4ed4=
112112
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
113113
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
114-
github.com/LumeraProtocol/lumera v1.11.0-rc h1:ISJLUhjihuOterLMHpgGWpMZmybR1vmQLNgmSHkc1WA=
115-
github.com/LumeraProtocol/lumera v1.11.0-rc/go.mod h1:p2sZZG3bLzSBdaW883qjuU3DXXY4NJzTTwLywr8uI0w=
114+
github.com/LumeraProtocol/lumera v1.11.1-0.20260308102614-4d4f1ce3f65e h1:acxjs0ki/uNv9+b/x5dcUzGoi+lea4E8QMdJx805svU=
115+
github.com/LumeraProtocol/lumera v1.11.1-0.20260308102614-4d4f1ce3f65e/go.mod h1:p2sZZG3bLzSBdaW883qjuU3DXXY4NJzTTwLywr8uI0w=
116116
github.com/LumeraProtocol/rq-go v0.2.1 h1:8B3UzRChLsGMmvZ+UVbJsJj6JZzL9P9iYxbdUwGsQI4=
117117
github.com/LumeraProtocol/rq-go v0.2.1/go.mod h1:APnKCZRh1Es2Vtrd2w4kCLgAyaL5Bqrkz/BURoRJ+O8=
118118
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=

pkg/cascadekit/commitment.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package cascadekit
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
8+
actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types"
9+
"github.com/LumeraProtocol/lumera/x/action/v1/merkle"
10+
"lukechampine.com/blake3"
11+
)
12+
13+
const (
14+
// DefaultChunkSize is the default chunk size for LEP-5 commitment (256 KiB).
15+
DefaultChunkSize = 262144
16+
// MinChunkSize is the minimum allowed chunk size.
17+
MinChunkSize = 1
18+
// MaxChunkSize is the maximum allowed chunk size.
19+
MaxChunkSize = 262144
20+
// MinTotalSize is the minimum file size for LEP-5 commitment.
21+
MinTotalSize = 4
22+
// CommitmentType is the commitment type constant for LEP-5.
23+
CommitmentType = "lep5/chunk-merkle/v1"
24+
)
25+
26+
// SelectChunkSize returns the optimal chunk size for a given file size and
27+
// minimum chunk count. It starts at DefaultChunkSize and halves until the
28+
// file produces at least minChunks chunks.
29+
func SelectChunkSize(fileSize int64, minChunks uint32) uint32 {
30+
s := uint32(DefaultChunkSize)
31+
for numChunks(fileSize, s) < minChunks && s > MinChunkSize {
32+
s /= 2
33+
}
34+
return s
35+
}
36+
37+
func numChunks(fileSize int64, chunkSize uint32) uint32 {
38+
n := uint32(fileSize / int64(chunkSize))
39+
if fileSize%int64(chunkSize) != 0 {
40+
n++
41+
}
42+
return n
43+
}
44+
45+
// ChunkFile reads a file and returns its chunks using the given chunk size.
46+
func ChunkFile(path string, chunkSize uint32) ([][]byte, error) {
47+
f, err := os.Open(path)
48+
if err != nil {
49+
return nil, fmt.Errorf("open file: %w", err)
50+
}
51+
defer f.Close()
52+
53+
fi, err := f.Stat()
54+
if err != nil {
55+
return nil, fmt.Errorf("stat file: %w", err)
56+
}
57+
58+
totalSize := fi.Size()
59+
n := numChunks(totalSize, chunkSize)
60+
chunks := make([][]byte, 0, n)
61+
62+
buf := make([]byte, chunkSize)
63+
for {
64+
nr, err := io.ReadFull(f, buf)
65+
if nr > 0 {
66+
chunk := make([]byte, nr)
67+
copy(chunk, buf[:nr])
68+
chunks = append(chunks, chunk)
69+
}
70+
if err == io.EOF || err == io.ErrUnexpectedEOF {
71+
break
72+
}
73+
if err != nil {
74+
return nil, fmt.Errorf("read chunk: %w", err)
75+
}
76+
}
77+
return chunks, nil
78+
}
79+
80+
// BuildCommitmentFromFile constructs an AvailabilityCommitment for a file.
81+
// It chunks the file, builds a Merkle tree, and generates challenge indices.
82+
// challengeCount and minChunks are the SVC parameters from the chain.
83+
func BuildCommitmentFromFile(filePath string, challengeCount, minChunks uint32) (*actiontypes.AvailabilityCommitment, *merkle.Tree, error) {
84+
fi, err := os.Stat(filePath)
85+
if err != nil {
86+
return nil, nil, fmt.Errorf("stat file: %w", err)
87+
}
88+
totalSize := fi.Size()
89+
if totalSize < MinTotalSize {
90+
return nil, nil, fmt.Errorf("file too small: %d bytes (minimum %d)", totalSize, MinTotalSize)
91+
}
92+
93+
chunkSize := SelectChunkSize(totalSize, minChunks)
94+
nc := numChunks(totalSize, chunkSize)
95+
if nc < minChunks {
96+
return nil, nil, fmt.Errorf("file produces %d chunks, need at least %d", nc, minChunks)
97+
}
98+
99+
chunks, err := ChunkFile(filePath, chunkSize)
100+
if err != nil {
101+
return nil, nil, err
102+
}
103+
104+
tree, err := merkle.BuildTree(chunks)
105+
if err != nil {
106+
return nil, nil, fmt.Errorf("build merkle tree: %w", err)
107+
}
108+
109+
// Generate challenge indices — simple deterministic selection using tree root as entropy.
110+
m := challengeCount
111+
if m > nc {
112+
m = nc
113+
}
114+
indices := deriveSimpleIndices(tree.Root[:], nc, m)
115+
116+
commitment := &actiontypes.AvailabilityCommitment{
117+
CommitmentType: CommitmentType,
118+
HashAlgo: actiontypes.HashAlgo_HASH_ALGO_BLAKE3,
119+
ChunkSize: chunkSize,
120+
TotalSize: uint64(totalSize),
121+
NumChunks: nc,
122+
Root: tree.Root[:],
123+
ChallengeIndices: indices,
124+
}
125+
126+
return commitment, tree, nil
127+
}
128+
129+
// GenerateChunkProofs produces Merkle proofs for the challenge indices in the commitment.
130+
func GenerateChunkProofs(tree *merkle.Tree, indices []uint32) ([]*actiontypes.ChunkProof, error) {
131+
proofs := make([]*actiontypes.ChunkProof, len(indices))
132+
for i, idx := range indices {
133+
p, err := tree.GenerateProof(int(idx))
134+
if err != nil {
135+
return nil, fmt.Errorf("generate proof for chunk %d: %w", idx, err)
136+
}
137+
138+
pathHashes := make([][]byte, len(p.PathHashes))
139+
for j, h := range p.PathHashes {
140+
pathHashes[j] = h[:]
141+
}
142+
143+
proofs[i] = &actiontypes.ChunkProof{
144+
ChunkIndex: p.ChunkIndex,
145+
LeafHash: p.LeafHash[:],
146+
PathHashes: pathHashes,
147+
PathDirections: p.PathDirections,
148+
}
149+
}
150+
return proofs, nil
151+
}
152+
153+
// VerifyCommitmentRoot rebuilds the Merkle tree from a file and checks it matches the on-chain root.
154+
func VerifyCommitmentRoot(filePath string, commitment *actiontypes.AvailabilityCommitment) (*merkle.Tree, error) {
155+
if commitment == nil {
156+
return nil, nil // pre-LEP-5 action, nothing to verify
157+
}
158+
159+
chunks, err := ChunkFile(filePath, commitment.ChunkSize)
160+
if err != nil {
161+
return nil, fmt.Errorf("chunk file for verification: %w", err)
162+
}
163+
164+
if uint32(len(chunks)) != commitment.NumChunks {
165+
return nil, fmt.Errorf("chunk count mismatch: got %d, expected %d", len(chunks), commitment.NumChunks)
166+
}
167+
168+
tree, err := merkle.BuildTree(chunks)
169+
if err != nil {
170+
return nil, fmt.Errorf("build merkle tree for verification: %w", err)
171+
}
172+
173+
if tree.Root != [merkle.HashSize]byte(commitment.Root) {
174+
return nil, fmt.Errorf("merkle root mismatch: computed %x, expected %x", tree.Root[:], commitment.Root)
175+
}
176+
177+
return tree, nil
178+
}
179+
180+
// deriveSimpleIndices generates m unique indices in [0, numChunks) using BLAKE3(root || counter).
181+
func deriveSimpleIndices(root []byte, numChunks, m uint32) []uint32 {
182+
if numChunks == 0 || m == 0 {
183+
return nil
184+
}
185+
186+
indices := make([]uint32, 0, m)
187+
used := make(map[uint32]struct{}, m)
188+
counter := uint32(0)
189+
190+
for uint32(len(indices)) < m {
191+
// BLAKE3(root || uint32be(counter))
192+
buf := make([]byte, len(root)+4)
193+
copy(buf, root)
194+
buf[len(root)] = byte(counter >> 24)
195+
buf[len(root)+1] = byte(counter >> 16)
196+
buf[len(root)+2] = byte(counter >> 8)
197+
buf[len(root)+3] = byte(counter)
198+
199+
h := blake3.Sum256(buf)
200+
// Use first 8 bytes as uint64 mod numChunks
201+
val := uint64(h[0])<<56 | uint64(h[1])<<48 | uint64(h[2])<<40 | uint64(h[3])<<32 |
202+
uint64(h[4])<<24 | uint64(h[5])<<16 | uint64(h[6])<<8 | uint64(h[7])
203+
idx := uint32(val % uint64(numChunks))
204+
205+
if _, exists := used[idx]; !exists {
206+
used[idx] = struct{}{}
207+
indices = append(indices, idx)
208+
}
209+
counter++
210+
}
211+
return indices
212+
}

pkg/cascadekit/metadata.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ import (
66

77
// NewCascadeMetadata creates a types.CascadeMetadata for RequestAction.
88
// The keeper will populate rq_ids_max; rq_ids_ids is for FinalizeAction only.
9-
func NewCascadeMetadata(dataHashB64, fileName string, rqIdsIc uint64, indexSignatureFormat string, public bool) actiontypes.CascadeMetadata {
10-
return actiontypes.CascadeMetadata{
9+
// commitment may be nil for pre-LEP-5 actions.
10+
func NewCascadeMetadata(dataHashB64, fileName string, rqIdsIc uint64, indexSignatureFormat string, public bool, commitment *actiontypes.AvailabilityCommitment) actiontypes.CascadeMetadata {
11+
meta := actiontypes.CascadeMetadata{
1112
DataHash: dataHashB64,
1213
FileName: fileName,
1314
RqIdsIc: rqIdsIc,
1415
Signatures: indexSignatureFormat,
1516
Public: public,
1617
}
18+
if commitment != nil {
19+
meta.AvailabilityCommitment = commitment
20+
}
21+
return meta
1722
}

pkg/cascadekit/request_builder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ func BuildCascadeRequest(layout codec.Layout, fileBytes []byte, fileName string,
1818
if err != nil {
1919
return actiontypes.CascadeMetadata{}, nil, err
2020
}
21-
meta := NewCascadeMetadata(dataHashB64, fileName, uint64(ic), indexSignatureFormat, public)
21+
meta := NewCascadeMetadata(dataHashB64, fileName, uint64(ic), indexSignatureFormat, public, nil)
2222
return meta, indexIDs, nil
2323
}

pkg/lumera/modules/action_msg/action_msg_mock.go

Lines changed: 9 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/lumera/modules/action_msg/helpers.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ func createRequestActionMessage(creator, actionType, metadata, price, expiration
7474
}
7575
}
7676

77-
func createFinalizeActionMessage(creator, actionId string, rqIdsIds []string) (*actiontypes.MsgFinalizeAction, error) {
77+
func createFinalizeActionMessage(creator, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*actiontypes.MsgFinalizeAction, error) {
7878
cascadeMeta := actiontypes.CascadeMetadata{
79-
RqIdsIds: rqIdsIds,
79+
RqIdsIds: rqIdsIds,
80+
ChunkProofs: chunkProofs,
8081
}
8182

8283
metadataBytes, err := json.Marshal(&cascadeMeta)

pkg/lumera/modules/action_msg/impl.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func (m *module) RequestAction(ctx context.Context, actionType, metadata, price,
5959
})
6060
}
6161

62-
func (m *module) FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*sdktx.BroadcastTxResponse, error) {
62+
func (m *module) FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.BroadcastTxResponse, error) {
6363
if err := validateFinalizeActionParams(actionId, rqIdsIds); err != nil {
6464
return nil, err
6565
}
@@ -68,7 +68,7 @@ func (m *module) FinalizeCascadeAction(ctx context.Context, actionId string, rqI
6868
defer m.mu.Unlock()
6969

7070
return m.txHelper.ExecuteTransaction(ctx, func(creator string) (types.Msg, error) {
71-
return createFinalizeActionMessage(creator, actionId, rqIdsIds)
71+
return createFinalizeActionMessage(creator, actionId, rqIdsIds, chunkProofs)
7272
})
7373
}
7474

@@ -86,7 +86,7 @@ func (m *module) SetTxHelperConfig(config *txmod.TxHelperConfig) {
8686
// SimulateFinalizeCascadeAction builds the finalize message and performs a simulation
8787
// without broadcasting the transaction. This is useful to ensure the transaction
8888
// would pass ante/ValidateBasic before doing irreversible work.
89-
func (m *module) SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*sdktx.SimulateResponse, error) {
89+
func (m *module) SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.SimulateResponse, error) {
9090
if err := validateFinalizeActionParams(actionId, rqIdsIds); err != nil {
9191
return nil, err
9292
}
@@ -105,7 +105,7 @@ func (m *module) SimulateFinalizeCascadeAction(ctx context.Context, actionId str
105105
}
106106

107107
// Build the finalize message
108-
msg, err := createFinalizeActionMessage(creator, actionId, rqIdsIds)
108+
msg, err := createFinalizeActionMessage(creator, actionId, rqIdsIds, chunkProofs)
109109
if err != nil {
110110
return nil, err
111111
}

pkg/lumera/modules/action_msg/interface.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package action_msg
44
import (
55
"context"
66

7+
actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types"
78
"github.com/LumeraProtocol/supernode/v2/pkg/lumera/modules/auth"
89
"github.com/LumeraProtocol/supernode/v2/pkg/lumera/modules/tx"
910
"github.com/cosmos/cosmos-sdk/crypto/keyring"
@@ -12,11 +13,12 @@ import (
1213
)
1314

1415
type Module interface {
15-
// FinalizeCascadeAction finalizes a CASCADE action with the given parameters
16+
// RequestAction submits a new action request
1617
RequestAction(ctx context.Context, actionType, metadata, price, expirationTime, fileSizeKbs string) (*sdktx.BroadcastTxResponse, error)
17-
FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*sdktx.BroadcastTxResponse, error)
18+
// FinalizeCascadeAction finalizes a CASCADE action with rqIDs and optional LEP-5 chunk proofs
19+
FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.BroadcastTxResponse, error)
1820
// SimulateFinalizeCascadeAction simulates the finalize action (no broadcast)
19-
SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*sdktx.SimulateResponse, error)
21+
SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.SimulateResponse, error)
2022
}
2123

2224
func NewModule(conn *grpc.ClientConn, authmod auth.Module, txmodule tx.Module, kr keyring.Keyring, keyName string, chainID string) (Module, error) {

0 commit comments

Comments
 (0)