diff --git a/go.mod b/go.mod index f11b6800..dc9fd337 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( cosmossdk.io/math v1.5.3 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/DataDog/zstd v1.5.7 - github.com/LumeraProtocol/lumera v1.11.0-rc + github.com/LumeraProtocol/lumera v1.11.1-0.20260308102614-4d4f1ce3f65e github.com/LumeraProtocol/rq-go v0.2.1 github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce github.com/cenkalti/backoff/v4 v4.3.0 diff --git a/go.sum b/go.sum index a6c9eaa4..da19cc97 100644 --- a/go.sum +++ b/go.sum @@ -111,8 +111,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 h1:ig/FpDD2JofP/NExKQUbn7uOSZzJAQqogfqluZK4ed4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/LumeraProtocol/lumera v1.11.0-rc h1:ISJLUhjihuOterLMHpgGWpMZmybR1vmQLNgmSHkc1WA= -github.com/LumeraProtocol/lumera v1.11.0-rc/go.mod h1:p2sZZG3bLzSBdaW883qjuU3DXXY4NJzTTwLywr8uI0w= +github.com/LumeraProtocol/lumera v1.11.1-0.20260308102614-4d4f1ce3f65e h1:acxjs0ki/uNv9+b/x5dcUzGoi+lea4E8QMdJx805svU= +github.com/LumeraProtocol/lumera v1.11.1-0.20260308102614-4d4f1ce3f65e/go.mod h1:p2sZZG3bLzSBdaW883qjuU3DXXY4NJzTTwLywr8uI0w= github.com/LumeraProtocol/rq-go v0.2.1 h1:8B3UzRChLsGMmvZ+UVbJsJj6JZzL9P9iYxbdUwGsQI4= github.com/LumeraProtocol/rq-go v0.2.1/go.mod h1:APnKCZRh1Es2Vtrd2w4kCLgAyaL5Bqrkz/BURoRJ+O8= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= diff --git a/pkg/cascadekit/commitment.go b/pkg/cascadekit/commitment.go new file mode 100644 index 00000000..49736683 --- /dev/null +++ b/pkg/cascadekit/commitment.go @@ -0,0 +1,212 @@ +package cascadekit + +import ( + "fmt" + "io" + "os" + + actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" + "github.com/LumeraProtocol/lumera/x/action/v1/merkle" + "lukechampine.com/blake3" +) + +const ( + // DefaultChunkSize is the default chunk size for LEP-5 commitment (256 KiB). + DefaultChunkSize = 262144 + // MinChunkSize is the minimum allowed chunk size. + MinChunkSize = 1 + // MaxChunkSize is the maximum allowed chunk size. + MaxChunkSize = 262144 + // MinTotalSize is the minimum file size for LEP-5 commitment. + MinTotalSize = 4 + // CommitmentType is the commitment type constant for LEP-5. + CommitmentType = "lep5/chunk-merkle/v1" +) + +// SelectChunkSize returns the optimal chunk size for a given file size and +// minimum chunk count. It starts at DefaultChunkSize and halves until the +// file produces at least minChunks chunks. +func SelectChunkSize(fileSize int64, minChunks uint32) uint32 { + s := uint32(DefaultChunkSize) + for numChunks(fileSize, s) < minChunks && s > MinChunkSize { + s /= 2 + } + return s +} + +func numChunks(fileSize int64, chunkSize uint32) uint32 { + n := uint32(fileSize / int64(chunkSize)) + if fileSize%int64(chunkSize) != 0 { + n++ + } + return n +} + +// ChunkFile reads a file and returns its chunks using the given chunk size. +func ChunkFile(path string, chunkSize uint32) ([][]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("open file: %w", err) + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return nil, fmt.Errorf("stat file: %w", err) + } + + totalSize := fi.Size() + n := numChunks(totalSize, chunkSize) + chunks := make([][]byte, 0, n) + + buf := make([]byte, chunkSize) + for { + nr, err := io.ReadFull(f, buf) + if nr > 0 { + chunk := make([]byte, nr) + copy(chunk, buf[:nr]) + chunks = append(chunks, chunk) + } + if err == io.EOF || err == io.ErrUnexpectedEOF { + break + } + if err != nil { + return nil, fmt.Errorf("read chunk: %w", err) + } + } + return chunks, nil +} + +// BuildCommitmentFromFile constructs an AvailabilityCommitment for a file. +// It chunks the file, builds a Merkle tree, and generates challenge indices. +// challengeCount and minChunks are the SVC parameters from the chain. +func BuildCommitmentFromFile(filePath string, challengeCount, minChunks uint32) (*actiontypes.AvailabilityCommitment, *merkle.Tree, error) { + fi, err := os.Stat(filePath) + if err != nil { + return nil, nil, fmt.Errorf("stat file: %w", err) + } + totalSize := fi.Size() + if totalSize < MinTotalSize { + return nil, nil, fmt.Errorf("file too small: %d bytes (minimum %d)", totalSize, MinTotalSize) + } + + chunkSize := SelectChunkSize(totalSize, minChunks) + nc := numChunks(totalSize, chunkSize) + if nc < minChunks { + return nil, nil, fmt.Errorf("file produces %d chunks, need at least %d", nc, minChunks) + } + + chunks, err := ChunkFile(filePath, chunkSize) + if err != nil { + return nil, nil, err + } + + tree, err := merkle.BuildTree(chunks) + if err != nil { + return nil, nil, fmt.Errorf("build merkle tree: %w", err) + } + + // Generate challenge indices — simple deterministic selection using tree root as entropy. + m := challengeCount + if m > nc { + m = nc + } + indices := deriveSimpleIndices(tree.Root[:], nc, m) + + commitment := &actiontypes.AvailabilityCommitment{ + CommitmentType: CommitmentType, + HashAlgo: actiontypes.HashAlgo_HASH_ALGO_BLAKE3, + ChunkSize: chunkSize, + TotalSize: uint64(totalSize), + NumChunks: nc, + Root: tree.Root[:], + ChallengeIndices: indices, + } + + return commitment, tree, nil +} + +// GenerateChunkProofs produces Merkle proofs for the challenge indices in the commitment. +func GenerateChunkProofs(tree *merkle.Tree, indices []uint32) ([]*actiontypes.ChunkProof, error) { + proofs := make([]*actiontypes.ChunkProof, len(indices)) + for i, idx := range indices { + p, err := tree.GenerateProof(int(idx)) + if err != nil { + return nil, fmt.Errorf("generate proof for chunk %d: %w", idx, err) + } + + pathHashes := make([][]byte, len(p.PathHashes)) + for j, h := range p.PathHashes { + pathHashes[j] = h[:] + } + + proofs[i] = &actiontypes.ChunkProof{ + ChunkIndex: p.ChunkIndex, + LeafHash: p.LeafHash[:], + PathHashes: pathHashes, + PathDirections: p.PathDirections, + } + } + return proofs, nil +} + +// VerifyCommitmentRoot rebuilds the Merkle tree from a file and checks it matches the on-chain root. +func VerifyCommitmentRoot(filePath string, commitment *actiontypes.AvailabilityCommitment) (*merkle.Tree, error) { + if commitment == nil { + return nil, nil // pre-LEP-5 action, nothing to verify + } + + chunks, err := ChunkFile(filePath, commitment.ChunkSize) + if err != nil { + return nil, fmt.Errorf("chunk file for verification: %w", err) + } + + if uint32(len(chunks)) != commitment.NumChunks { + return nil, fmt.Errorf("chunk count mismatch: got %d, expected %d", len(chunks), commitment.NumChunks) + } + + tree, err := merkle.BuildTree(chunks) + if err != nil { + return nil, fmt.Errorf("build merkle tree for verification: %w", err) + } + + if tree.Root != [merkle.HashSize]byte(commitment.Root) { + return nil, fmt.Errorf("merkle root mismatch: computed %x, expected %x", tree.Root[:], commitment.Root) + } + + return tree, nil +} + +// deriveSimpleIndices generates m unique indices in [0, numChunks) using BLAKE3(root || counter). +func deriveSimpleIndices(root []byte, numChunks, m uint32) []uint32 { + if numChunks == 0 || m == 0 { + return nil + } + + indices := make([]uint32, 0, m) + used := make(map[uint32]struct{}, m) + counter := uint32(0) + + for uint32(len(indices)) < m { + // BLAKE3(root || uint32be(counter)) + buf := make([]byte, len(root)+4) + copy(buf, root) + buf[len(root)] = byte(counter >> 24) + buf[len(root)+1] = byte(counter >> 16) + buf[len(root)+2] = byte(counter >> 8) + buf[len(root)+3] = byte(counter) + + h := blake3.Sum256(buf) + // Use first 8 bytes as uint64 mod numChunks + val := uint64(h[0])<<56 | uint64(h[1])<<48 | uint64(h[2])<<40 | uint64(h[3])<<32 | + uint64(h[4])<<24 | uint64(h[5])<<16 | uint64(h[6])<<8 | uint64(h[7]) + idx := uint32(val % uint64(numChunks)) + + if _, exists := used[idx]; !exists { + used[idx] = struct{}{} + indices = append(indices, idx) + } + counter++ + } + return indices +} diff --git a/pkg/cascadekit/metadata.go b/pkg/cascadekit/metadata.go index a77ddfd4..9124dea0 100644 --- a/pkg/cascadekit/metadata.go +++ b/pkg/cascadekit/metadata.go @@ -6,12 +6,17 @@ 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, indexSignatureFormat string, public bool) actiontypes.CascadeMetadata { - return actiontypes.CascadeMetadata{ +// commitment may be nil for pre-LEP-5 actions. +func NewCascadeMetadata(dataHashB64, fileName string, rqIdsIc uint64, indexSignatureFormat string, public bool, commitment *actiontypes.AvailabilityCommitment) actiontypes.CascadeMetadata { + meta := actiontypes.CascadeMetadata{ DataHash: dataHashB64, FileName: fileName, RqIdsIc: rqIdsIc, Signatures: indexSignatureFormat, Public: public, } + if commitment != nil { + meta.AvailabilityCommitment = commitment + } + return meta } diff --git a/pkg/cascadekit/request_builder.go b/pkg/cascadekit/request_builder.go index 695e2fdf..2d4bcbde 100644 --- a/pkg/cascadekit/request_builder.go +++ b/pkg/cascadekit/request_builder.go @@ -18,6 +18,6 @@ func BuildCascadeRequest(layout codec.Layout, fileBytes []byte, fileName string, if err != nil { return actiontypes.CascadeMetadata{}, nil, err } - meta := NewCascadeMetadata(dataHashB64, fileName, uint64(ic), indexSignatureFormat, public) + meta := NewCascadeMetadata(dataHashB64, fileName, uint64(ic), indexSignatureFormat, public, nil) return meta, indexIDs, nil } diff --git a/pkg/lumera/modules/action_msg/action_msg_mock.go b/pkg/lumera/modules/action_msg/action_msg_mock.go index b8a3f46e..528243bf 100644 --- a/pkg/lumera/modules/action_msg/action_msg_mock.go +++ b/pkg/lumera/modules/action_msg/action_msg_mock.go @@ -13,6 +13,7 @@ import ( context "context" reflect "reflect" + actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" tx "github.com/cosmos/cosmos-sdk/types/tx" gomock "go.uber.org/mock/gomock" ) @@ -42,18 +43,18 @@ func (m *MockModule) EXPECT() *MockModuleMockRecorder { } // FinalizeCascadeAction mocks base method. -func (m *MockModule) FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*tx.BroadcastTxResponse, error) { +func (m *MockModule) FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*tx.BroadcastTxResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FinalizeCascadeAction", ctx, actionId, rqIdsIds) + ret := m.ctrl.Call(m, "FinalizeCascadeAction", ctx, actionId, rqIdsIds, chunkProofs) ret0, _ := ret[0].(*tx.BroadcastTxResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // FinalizeCascadeAction indicates an expected call of FinalizeCascadeAction. -func (mr *MockModuleMockRecorder) FinalizeCascadeAction(ctx, actionId, rqIdsIds any) *gomock.Call { +func (mr *MockModuleMockRecorder) FinalizeCascadeAction(ctx, actionId, rqIdsIds, chunkProofs any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FinalizeCascadeAction", reflect.TypeOf((*MockModule)(nil).FinalizeCascadeAction), ctx, actionId, rqIdsIds) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FinalizeCascadeAction", reflect.TypeOf((*MockModule)(nil).FinalizeCascadeAction), ctx, actionId, rqIdsIds, chunkProofs) } // RequestAction mocks base method. @@ -72,16 +73,16 @@ func (mr *MockModuleMockRecorder) RequestAction(ctx, actionType, metadata, price } // SimulateFinalizeCascadeAction mocks base method. -func (m *MockModule) SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*tx.SimulateResponse, error) { +func (m *MockModule) SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*tx.SimulateResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SimulateFinalizeCascadeAction", ctx, actionId, rqIdsIds) + ret := m.ctrl.Call(m, "SimulateFinalizeCascadeAction", ctx, actionId, rqIdsIds, chunkProofs) ret0, _ := ret[0].(*tx.SimulateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // SimulateFinalizeCascadeAction indicates an expected call of SimulateFinalizeCascadeAction. -func (mr *MockModuleMockRecorder) SimulateFinalizeCascadeAction(ctx, actionId, rqIdsIds any) *gomock.Call { +func (mr *MockModuleMockRecorder) SimulateFinalizeCascadeAction(ctx, actionId, rqIdsIds, chunkProofs any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SimulateFinalizeCascadeAction", reflect.TypeOf((*MockModule)(nil).SimulateFinalizeCascadeAction), ctx, actionId, rqIdsIds) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SimulateFinalizeCascadeAction", reflect.TypeOf((*MockModule)(nil).SimulateFinalizeCascadeAction), ctx, actionId, rqIdsIds, chunkProofs) } diff --git a/pkg/lumera/modules/action_msg/helpers.go b/pkg/lumera/modules/action_msg/helpers.go index 0ba722e0..c4451e59 100644 --- a/pkg/lumera/modules/action_msg/helpers.go +++ b/pkg/lumera/modules/action_msg/helpers.go @@ -74,9 +74,10 @@ func createRequestActionMessage(creator, actionType, metadata, price, expiration } } -func createFinalizeActionMessage(creator, actionId string, rqIdsIds []string) (*actiontypes.MsgFinalizeAction, error) { +func createFinalizeActionMessage(creator, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*actiontypes.MsgFinalizeAction, error) { cascadeMeta := actiontypes.CascadeMetadata{ - RqIdsIds: rqIdsIds, + RqIdsIds: rqIdsIds, + ChunkProofs: chunkProofs, } metadataBytes, err := json.Marshal(&cascadeMeta) diff --git a/pkg/lumera/modules/action_msg/impl.go b/pkg/lumera/modules/action_msg/impl.go index 8e282225..8ed4aca5 100644 --- a/pkg/lumera/modules/action_msg/impl.go +++ b/pkg/lumera/modules/action_msg/impl.go @@ -59,7 +59,7 @@ func (m *module) RequestAction(ctx context.Context, actionType, metadata, price, }) } -func (m *module) FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*sdktx.BroadcastTxResponse, error) { +func (m *module) FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.BroadcastTxResponse, error) { if err := validateFinalizeActionParams(actionId, rqIdsIds); err != nil { return nil, err } @@ -68,7 +68,7 @@ func (m *module) FinalizeCascadeAction(ctx context.Context, actionId string, rqI defer m.mu.Unlock() return m.txHelper.ExecuteTransaction(ctx, func(creator string) (types.Msg, error) { - return createFinalizeActionMessage(creator, actionId, rqIdsIds) + return createFinalizeActionMessage(creator, actionId, rqIdsIds, chunkProofs) }) } @@ -86,7 +86,7 @@ func (m *module) SetTxHelperConfig(config *txmod.TxHelperConfig) { // SimulateFinalizeCascadeAction builds the finalize message and performs a simulation // without broadcasting the transaction. This is useful to ensure the transaction // would pass ante/ValidateBasic before doing irreversible work. -func (m *module) SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*sdktx.SimulateResponse, error) { +func (m *module) SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.SimulateResponse, error) { if err := validateFinalizeActionParams(actionId, rqIdsIds); err != nil { return nil, err } @@ -105,7 +105,7 @@ func (m *module) SimulateFinalizeCascadeAction(ctx context.Context, actionId str } // Build the finalize message - msg, err := createFinalizeActionMessage(creator, actionId, rqIdsIds) + msg, err := createFinalizeActionMessage(creator, actionId, rqIdsIds, chunkProofs) if err != nil { return nil, err } diff --git a/pkg/lumera/modules/action_msg/interface.go b/pkg/lumera/modules/action_msg/interface.go index e273b4fd..7ebea89e 100644 --- a/pkg/lumera/modules/action_msg/interface.go +++ b/pkg/lumera/modules/action_msg/interface.go @@ -4,6 +4,7 @@ package action_msg import ( "context" + actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" "github.com/LumeraProtocol/supernode/v2/pkg/lumera/modules/auth" "github.com/LumeraProtocol/supernode/v2/pkg/lumera/modules/tx" "github.com/cosmos/cosmos-sdk/crypto/keyring" @@ -12,11 +13,12 @@ import ( ) type Module interface { - // FinalizeCascadeAction finalizes a CASCADE action with the given parameters + // RequestAction submits a new action request RequestAction(ctx context.Context, actionType, metadata, price, expirationTime, fileSizeKbs string) (*sdktx.BroadcastTxResponse, error) - FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*sdktx.BroadcastTxResponse, error) + // FinalizeCascadeAction finalizes a CASCADE action with rqIDs and optional LEP-5 chunk proofs + FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.BroadcastTxResponse, error) // SimulateFinalizeCascadeAction simulates the finalize action (no broadcast) - SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*sdktx.SimulateResponse, error) + SimulateFinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.SimulateResponse, error) } func NewModule(conn *grpc.ClientConn, authmod auth.Module, txmodule tx.Module, kr keyring.Keyring, keyName string, chainID string) (Module, error) { diff --git a/pkg/testutil/lumera.go b/pkg/testutil/lumera.go index d7bd1212..c6eecb6b 100644 --- a/pkg/testutil/lumera.go +++ b/pkg/testutil/lumera.go @@ -3,7 +3,7 @@ package testutil import ( "context" - "github.com/LumeraProtocol/lumera/x/action/v1/types" + actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" audittypes "github.com/LumeraProtocol/lumera/x/audit/v1/types" supernodeTypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types" "github.com/LumeraProtocol/supernode/v2/pkg/lumera" @@ -158,16 +158,16 @@ func (m *MockAuthModule) Verify(ctx context.Context, accAddress string, data, si // MockActionModule implements the action.Module interface for testing type MockActionModule struct{} -func (m *MockActionModule) GetAction(ctx context.Context, actionID string) (*types.QueryGetActionResponse, error) { - return &types.QueryGetActionResponse{}, nil +func (m *MockActionModule) GetAction(ctx context.Context, actionID string) (*actiontypes.QueryGetActionResponse, error) { + return &actiontypes.QueryGetActionResponse{}, nil } -func (m *MockActionModule) GetActionFee(ctx context.Context, dataSize string) (*types.QueryGetActionFeeResponse, error) { - return &types.QueryGetActionFeeResponse{}, nil +func (m *MockActionModule) GetActionFee(ctx context.Context, dataSize string) (*actiontypes.QueryGetActionFeeResponse, error) { + return &actiontypes.QueryGetActionFeeResponse{}, nil } -func (m *MockActionModule) GetParams(ctx context.Context) (*types.QueryParamsResponse, error) { - return &types.QueryParamsResponse{}, nil +func (m *MockActionModule) GetParams(ctx context.Context) (*actiontypes.QueryParamsResponse, error) { + return &actiontypes.QueryParamsResponse{}, nil } // MockActionMsgModule implements the action_msg.Module interface for testing @@ -180,13 +180,13 @@ func (m *MockActionMsgModule) RequestAction(ctx context.Context, actionType, met } // FinalizeCascadeAction implements the required method from action_msg.Module interface -func (m *MockActionMsgModule) FinalizeCascadeAction(ctx context.Context, actionId string, signatures []string) (*sdktx.BroadcastTxResponse, error) { +func (m *MockActionMsgModule) FinalizeCascadeAction(ctx context.Context, actionId string, signatures []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.BroadcastTxResponse, error) { // Mock implementation returns success with empty result return &sdktx.BroadcastTxResponse{}, nil } // SimulateFinalizeCascadeAction mocks simulation of finalize action. -func (m *MockActionMsgModule) SimulateFinalizeCascadeAction(ctx context.Context, actionId string, signatures []string) (*sdktx.SimulateResponse, error) { +func (m *MockActionMsgModule) SimulateFinalizeCascadeAction(ctx context.Context, actionId string, signatures []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.SimulateResponse, error) { // Mock implementation returns empty simulation response return &sdktx.SimulateResponse{}, nil } diff --git a/sdk/action/client.go b/sdk/action/client.go index d286d25c..abda9098 100644 --- a/sdk/action/client.go +++ b/sdk/action/client.go @@ -330,8 +330,28 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath // Derive file name from path fileName := filepath.Base(filePath) + // LEP-5: Build availability commitment (Merkle root + challenge indices) + challengeCount := uint32(paramsResp.Params.SvcChallengeCount) + if challengeCount == 0 { + challengeCount = 8 // default + } + minChunks := uint32(paramsResp.Params.SvcMinChunksForChallenge) + if minChunks == 0 { + minChunks = 4 // default + } + // LEP-5: Build availability commitment. Files below MinTotalSize (4 bytes) + // are too small for meaningful storage verification — skip commitment for them. + var commitment *actiontypes.AvailabilityCommitment + if fi.Size() >= cascadekit.MinTotalSize { + var err2 error + commitment, _, err2 = cascadekit.BuildCommitmentFromFile(filePath, challengeCount, minChunks) + if err2 != nil { + return actiontypes.CascadeMetadata{}, "", "", fmt.Errorf("build availability commitment: %w", err2) + } + } + // Build metadata proto - meta := cascadekit.NewCascadeMetadata(dataHashB64, fileName, uint64(ic), indexSignatureFormat, public) + meta := cascadekit.NewCascadeMetadata(dataHashB64, fileName, uint64(ic), indexSignatureFormat, public, commitment) // Fetch params (already fetched) to get denom and expiration duration denom := paramsResp.Params.BaseActionFee.Denom diff --git a/supernode/adaptors/lumera.go b/supernode/adaptors/lumera.go index aa093d36..9553ca92 100644 --- a/supernode/adaptors/lumera.go +++ b/supernode/adaptors/lumera.go @@ -15,8 +15,8 @@ type LumeraClient interface { ListSupernodes(ctx context.Context) (*sntypes.QueryListSuperNodesResponse, error) Verify(ctx context.Context, address string, msg []byte, sig []byte) error GetActionFee(ctx context.Context, dataSizeKB string) (*actiontypes.QueryGetActionFeeResponse, error) - SimulateFinalizeAction(ctx context.Context, actionID string, rqids []string) (*sdktx.SimulateResponse, error) - FinalizeAction(ctx context.Context, actionID string, rqids []string) (*sdktx.BroadcastTxResponse, error) + SimulateFinalizeAction(ctx context.Context, actionID string, rqids []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.SimulateResponse, error) + FinalizeAction(ctx context.Context, actionID string, rqids []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.BroadcastTxResponse, error) } type lumeraImpl struct{ c lumera.Client } @@ -45,10 +45,10 @@ func (l *lumeraImpl) GetActionFee(ctx context.Context, dataSizeKB string) (*acti return l.c.Action().GetActionFee(ctx, dataSizeKB) } -func (l *lumeraImpl) SimulateFinalizeAction(ctx context.Context, actionID string, rqids []string) (*sdktx.SimulateResponse, error) { - return l.c.ActionMsg().SimulateFinalizeCascadeAction(ctx, actionID, rqids) +func (l *lumeraImpl) SimulateFinalizeAction(ctx context.Context, actionID string, rqids []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.SimulateResponse, error) { + return l.c.ActionMsg().SimulateFinalizeCascadeAction(ctx, actionID, rqids, chunkProofs) } -func (l *lumeraImpl) FinalizeAction(ctx context.Context, actionID string, rqids []string) (*sdktx.BroadcastTxResponse, error) { - return l.c.ActionMsg().FinalizeCascadeAction(ctx, actionID, rqids) +func (l *lumeraImpl) FinalizeAction(ctx context.Context, actionID string, rqids []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.BroadcastTxResponse, error) { + return l.c.ActionMsg().FinalizeCascadeAction(ctx, actionID, rqids, chunkProofs) } diff --git a/supernode/cascade/events.go b/supernode/cascade/events.go index f1314a1a..3f549257 100644 --- a/supernode/cascade/events.go +++ b/supernode/cascade/events.go @@ -22,4 +22,7 @@ const ( SupernodeEventTypeNetworkRetrieveStarted SupernodeEventType = 15 SupernodeEventTypeDecodeCompleted SupernodeEventType = 16 SupernodeEventTypeServeReady SupernodeEventType = 17 + // LEP-5 events + SupernodeEventTypeMerkleRootVerified SupernodeEventType = 18 + SupernodeEventTypeChunkProofsGenerated SupernodeEventType = 19 ) diff --git a/supernode/cascade/ica_verify_test.go b/supernode/cascade/ica_verify_test.go index f404ad69..3a22a5be 100644 --- a/supernode/cascade/ica_verify_test.go +++ b/supernode/cascade/ica_verify_test.go @@ -43,11 +43,11 @@ func (f *fakeCascadeLumeraClient) GetActionFee(ctx context.Context, dataSizeKB s return nil, nil } -func (f *fakeCascadeLumeraClient) SimulateFinalizeAction(ctx context.Context, actionID string, rqids []string) (*sdktx.SimulateResponse, error) { +func (f *fakeCascadeLumeraClient) SimulateFinalizeAction(ctx context.Context, actionID string, rqids []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.SimulateResponse, error) { return nil, nil } -func (f *fakeCascadeLumeraClient) FinalizeAction(ctx context.Context, actionID string, rqids []string) (*sdktx.BroadcastTxResponse, error) { +func (f *fakeCascadeLumeraClient) FinalizeAction(ctx context.Context, actionID string, rqids []string, chunkProofs []*actiontypes.ChunkProof) (*sdktx.BroadcastTxResponse, error) { return nil, nil } diff --git a/supernode/cascade/register.go b/supernode/cascade/register.go index 3591ab9c..f5be5d35 100644 --- a/supernode/cascade/register.go +++ b/supernode/cascade/register.go @@ -4,6 +4,7 @@ import ( "context" "os" + actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" "github.com/LumeraProtocol/supernode/v2/pkg/cascadekit" "github.com/LumeraProtocol/supernode/v2/pkg/logtrace" ) @@ -102,6 +103,31 @@ func (task *CascadeRegistrationTask) Register( return err } + // Step 7b (LEP-5): Verify Merkle root against on-chain commitment and build tree + var chunkProofs []*actiontypes.ChunkProof + if cascadeMeta.AvailabilityCommitment != nil { + tree, err := cascadekit.VerifyCommitmentRoot(req.FilePath, cascadeMeta.AvailabilityCommitment) + if err != nil { + return task.wrapErr(ctx, "LEP-5 commitment root verification failed", err, fields) + } + logtrace.Info(ctx, "register: LEP-5 merkle root verified", fields) + if err := task.streamEvent(ctx, SupernodeEventTypeMerkleRootVerified, "Merkle root verified", "", send); err != nil { + return err + } + + // Generate chunk proofs for the challenge indices + chunkProofs, err = cascadekit.GenerateChunkProofs(tree, cascadeMeta.AvailabilityCommitment.ChallengeIndices) + if err != nil { + return task.wrapErr(ctx, "LEP-5 chunk proof generation failed", err, fields) + } + logtrace.Info(ctx, "register: LEP-5 chunk proofs generated", logtrace.WithFields(fields, logtrace.Fields{ + "proof_count": len(chunkProofs), + })) + if err := task.streamEvent(ctx, SupernodeEventTypeChunkProofsGenerated, "Chunk proofs generated", "", send); err != nil { + return err + } + } + // 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 { @@ -153,7 +179,7 @@ func (task *CascadeRegistrationTask) Register( } // Step 11: Simulate finalize to ensure the tx will succeed - if _, err := task.LumeraClient.SimulateFinalizeAction(ctx, action.ActionID, rqIDs); err != nil { + if _, err := task.LumeraClient.SimulateFinalizeAction(ctx, action.ActionID, rqIDs, chunkProofs); err != nil { fields[logtrace.FieldError] = err.Error() logtrace.Info(ctx, "register: finalize simulation failed", fields) if err := task.streamEvent(ctx, SupernodeEventTypeFinalizeSimulationFailed, "Finalize simulation failed", "", send); err != nil { @@ -175,7 +201,7 @@ func (task *CascadeRegistrationTask) Register( } // Step 13: Finalize the action on-chain - resp, err := task.LumeraClient.FinalizeAction(ctx, action.ActionID, rqIDs) + resp, err := task.LumeraClient.FinalizeAction(ctx, action.ActionID, rqIDs, chunkProofs) if err != nil { fields[logtrace.FieldError] = err.Error() logtrace.Info(ctx, "register: finalize action error", fields) diff --git a/supernode/cascade/stream_send_error_test.go b/supernode/cascade/stream_send_error_test.go index 0a8aede4..711cc220 100644 --- a/supernode/cascade/stream_send_error_test.go +++ b/supernode/cascade/stream_send_error_test.go @@ -35,11 +35,11 @@ func (s *stubLumeraClient) GetActionFee(context.Context, string) (*actiontypes.Q panic("unexpected call") } -func (s *stubLumeraClient) SimulateFinalizeAction(context.Context, string, []string) (*sdktx.SimulateResponse, error) { +func (s *stubLumeraClient) SimulateFinalizeAction(context.Context, string, []string, []*actiontypes.ChunkProof) (*sdktx.SimulateResponse, error) { panic("unexpected call") } -func (s *stubLumeraClient) FinalizeAction(context.Context, string, []string) (*sdktx.BroadcastTxResponse, error) { +func (s *stubLumeraClient) FinalizeAction(context.Context, string, []string, []*actiontypes.ChunkProof) (*sdktx.BroadcastTxResponse, error) { panic("unexpected call") }