Skip to content

Commit

Permalink
feat: add support for Blobstream API (celestiaorg#3470)
Browse files Browse the repository at this point in the history
Co-authored-by: rene <[email protected]>
  • Loading branch information
2 people authored and sebasti810 committed Aug 14, 2024
1 parent fd32379 commit 651c95b
Show file tree
Hide file tree
Showing 26 changed files with 1,477 additions and 74 deletions.
39 changes: 21 additions & 18 deletions api/rpc/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/celestiaorg/celestia-node/api/rpc/perms"
"github.com/celestiaorg/celestia-node/nodebuilder/blob"
"github.com/celestiaorg/celestia-node/nodebuilder/blobstream"
"github.com/celestiaorg/celestia-node/nodebuilder/da"
"github.com/celestiaorg/celestia-node/nodebuilder/das"
"github.com/celestiaorg/celestia-node/nodebuilder/fraud"
Expand All @@ -27,15 +28,16 @@ var (
)

type Client struct {
Fraud fraud.API
Header header.API
State state.API
Share share.API
DAS das.API
P2P p2p.API
Node node.API
Blob blob.API
DA da.API
Fraud fraud.API
Header header.API
State state.API
Share share.API
DAS das.API
P2P p2p.API
Node node.API
Blob blob.API
DA da.API
Blobstream blobstream.API

closer multiClientCloser
}
Expand Down Expand Up @@ -116,14 +118,15 @@ func newClient(ctx context.Context, addr string, authHeader http.Header, config
func moduleMap(client *Client) map[string]interface{} {
// TODO: this duplication of strings many times across the codebase can be avoided with issue #1176
return map[string]interface{}{
"share": &client.Share.Internal,
"state": &client.State.Internal,
"header": &client.Header.Internal,
"fraud": &client.Fraud.Internal,
"das": &client.DAS.Internal,
"p2p": &client.P2P.Internal,
"node": &client.Node.Internal,
"blob": &client.Blob.Internal,
"da": &client.DA.Internal,
"share": &client.Share.Internal,
"state": &client.State.Internal,
"header": &client.Header.Internal,
"fraud": &client.Fraud.Internal,
"das": &client.DAS.Internal,
"p2p": &client.P2P.Internal,
"node": &client.Node.Internal,
"blob": &client.Blob.Internal,
"da": &client.DA.Internal,
"blobstream": &client.Blobstream.Internal,
}
}
41 changes: 23 additions & 18 deletions api/rpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"github.com/celestiaorg/celestia-node/nodebuilder"
"github.com/celestiaorg/celestia-node/nodebuilder/blob"
blobMock "github.com/celestiaorg/celestia-node/nodebuilder/blob/mocks"
"github.com/celestiaorg/celestia-node/nodebuilder/blobstream"
blobstreamMock "github.com/celestiaorg/celestia-node/nodebuilder/blobstream/mocks"
"github.com/celestiaorg/celestia-node/nodebuilder/da"
daMock "github.com/celestiaorg/celestia-node/nodebuilder/da/mocks"
"github.com/celestiaorg/celestia-node/nodebuilder/das"
Expand Down Expand Up @@ -90,15 +92,16 @@ func TestRPCCallsUnderlyingNode(t *testing.T) {
// api contains all modules that are made available as the node's
// public API surface
type api struct {
Fraud fraud.Module
Header header.Module
State statemod.Module
Share share.Module
DAS das.Module
Node node.Module
P2P p2p.Module
Blob blob.Module
DA da.Module
Fraud fraud.Module
Header header.Module
State statemod.Module
Share share.Module
DAS das.Module
Node node.Module
P2P p2p.Module
Blob blob.Module
DA da.Module
Blobstream blobstream.Module
}

func TestModulesImplementFullAPI(t *testing.T) {
Expand Down Expand Up @@ -312,6 +315,7 @@ func setupNodeWithAuthedRPC(t *testing.T,
nodeMock.NewMockModule(ctrl),
blobMock.NewMockModule(ctrl),
daMock.NewMockModule(ctrl),
blobstreamMock.NewMockModule(ctrl),
}

// given the behavior of fx.Invoke, this invoke will be called last as it is added at the root
Expand Down Expand Up @@ -342,13 +346,14 @@ func setupNodeWithAuthedRPC(t *testing.T,
}

type mockAPI struct {
State *stateMock.MockModule
Share *shareMock.MockModule
Fraud *fraudMock.MockModule
Header *headerMock.MockModule
Das *dasMock.MockModule
P2P *p2pMock.MockModule
Node *nodeMock.MockModule
Blob *blobMock.MockModule
DA *daMock.MockModule
State *stateMock.MockModule
Share *shareMock.MockModule
Fraud *fraudMock.MockModule
Header *headerMock.MockModule
Das *dasMock.MockModule
P2P *p2pMock.MockModule
Node *nodeMock.MockModule
Blob *blobMock.MockModule
DA *daMock.MockModule
Blobstream *blobstreamMock.MockModule
}
14 changes: 0 additions & 14 deletions blob/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,6 @@ import (

var errEmptyShares = errors.New("empty shares")

// Commitment is a Merkle Root of the subtree built from shares of the Blob.
// It is computed by splitting the blob into shares and building the Merkle subtree to be included
// after Submit.
type Commitment []byte

func (com Commitment) String() string {
return string(com)
}

// Equal ensures that commitments are the same
func (com Commitment) Equal(c Commitment) bool {
return bytes.Equal(com, c)
}

// The Proof is a set of nmt proofs that can be verified only through
// the included method (due to limitation of the nmt https://github.com/celestiaorg/nmt/issues/218).
// Proof proves the WHOLE namespaced data to the row roots.
Expand Down
134 changes: 134 additions & 0 deletions blob/commitment_proof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package blob

import (
"bytes"
"fmt"

coretypes "github.com/tendermint/tendermint/types"

"github.com/celestiaorg/celestia-app/pkg/appconsts"
"github.com/celestiaorg/celestia-app/pkg/shares"
"github.com/celestiaorg/nmt"
"github.com/celestiaorg/nmt/namespace"

"github.com/celestiaorg/celestia-node/share"
)

// Commitment is a Merkle Root of the subtree built from shares of the Blob.
// It is computed by splitting the blob into shares and building the Merkle subtree to be included
// after Submit.
type Commitment []byte

// CommitmentProof is an inclusion proof of a commitment to the data root.
type CommitmentProof struct {
// SubtreeRoots are the subtree roots of the blob's data that are
// used to create the commitment.
SubtreeRoots [][]byte `json:"subtree_roots"`
// SubtreeRootProofs are the NMT proofs for the subtree roots
// to the row roots.
SubtreeRootProofs []*nmt.Proof `json:"subtree_root_proofs"`
// NamespaceID is the namespace id of the commitment being proven. This
// namespace id is used when verifying the proof. If the namespace id doesn't
// match the namespace of the shares, the proof will fail verification.
NamespaceID namespace.ID `json:"namespace_id"`
// RowProof is the proof of the rows containing the blob's data to the
// data root.
RowProof coretypes.RowProof `json:"row_proof"`
NamespaceVersion uint8 `json:"namespace_version"`
}

func (com Commitment) String() string {
return string(com)
}

// Equal ensures that commitments are the same
func (com Commitment) Equal(c Commitment) bool {
return bytes.Equal(com, c)
}

// Validate performs basic validation to the commitment proof.
// Note: it doesn't verify if the proof is valid or not.
// Check Verify() for that.
func (commitmentProof *CommitmentProof) Validate() error {
if len(commitmentProof.SubtreeRoots) < len(commitmentProof.SubtreeRootProofs) {
return fmt.Errorf(
"the number of subtree roots %d should be bigger than the number of subtree root proofs %d",
len(commitmentProof.SubtreeRoots),
len(commitmentProof.SubtreeRootProofs),
)
}
if len(commitmentProof.SubtreeRootProofs) != len(commitmentProof.RowProof.Proofs) {
return fmt.Errorf(
"the number of subtree root proofs %d should be equal to the number of row root proofs %d",
len(commitmentProof.SubtreeRootProofs),
len(commitmentProof.RowProof.Proofs),
)
}
if int(commitmentProof.RowProof.EndRow-commitmentProof.RowProof.StartRow+1) != len(commitmentProof.RowProof.RowRoots) {
return fmt.Errorf(
"the number of rows %d must equal the number of row roots %d",
int(commitmentProof.RowProof.EndRow-commitmentProof.RowProof.StartRow+1),
len(commitmentProof.RowProof.RowRoots),
)
}
if len(commitmentProof.RowProof.Proofs) != len(commitmentProof.RowProof.RowRoots) {
return fmt.Errorf(
"the number of proofs %d must equal the number of row roots %d",
len(commitmentProof.RowProof.Proofs),
len(commitmentProof.RowProof.RowRoots),
)
}
return nil
}

// Verify verifies that a commitment proof is valid, i.e., the subtree roots commit
// to some data that was posted to a square.
// Expects the commitment proof to be properly formulated and validated
// using the Validate() function.
func (commitmentProof *CommitmentProof) Verify(root []byte, subtreeRootThreshold int) (bool, error) {
nmtHasher := nmt.NewNmtHasher(appconsts.NewBaseHashFunc(), share.NamespaceSize, true)

// computes the total number of shares proven.
numberOfShares := 0
for _, proof := range commitmentProof.SubtreeRootProofs {
numberOfShares += proof.End() - proof.Start()
}

// use the computed total number of shares to calculate the subtree roots
// width.
// the subtree roots width is defined in ADR-013:
//
//https://github.com/celestiaorg/celestia-app/blob/main/docs/architecture/adr-013-non-interactive-default-rules-for-zero-padding.md
subtreeRootsWidth := shares.SubTreeWidth(numberOfShares, subtreeRootThreshold)

// verify the proof of the subtree roots
subtreeRootsCursor := 0
for i, subtreeRootProof := range commitmentProof.SubtreeRootProofs {
// calculate the share range that each subtree root commits to.
ranges, err := nmt.ToLeafRanges(subtreeRootProof.Start(), subtreeRootProof.End(), subtreeRootsWidth)
if err != nil {
return false, err
}
valid, err := subtreeRootProof.VerifySubtreeRootInclusion(
nmtHasher,
commitmentProof.SubtreeRoots[subtreeRootsCursor:subtreeRootsCursor+len(ranges)],
subtreeRootsWidth,
commitmentProof.RowProof.RowRoots[i],
)
if err != nil {
return false, err
}
if !valid {
return false,
fmt.Errorf(
"subtree root proof for range [%d, %d) is invalid",
subtreeRootProof.Start(),
subtreeRootProof.End(),
)
}
subtreeRootsCursor += len(ranges)
}

// verify row roots to data root proof
return commitmentProof.RowProof.VerifyProof(root), nil
}
Loading

0 comments on commit 651c95b

Please sign in to comment.