Skip to content

Commit a79249d

Browse files
author
NightCrawler
committed
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 a79249d

16 files changed

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

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)