Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Blobstream API #3470

Merged
merged 59 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
dda88ee
feat: add support for the Blobstream API in node
rach-id Jun 4, 2024
41bfd50
chore: add permissions to methods
rach-id Jun 4, 2024
a3a32f3
fix: use the share proofs to generate the commitment proofs
rach-id Jun 5, 2024
e5d2e1d
chore: gofumpt
rach-id Jun 5, 2024
cb6f660
chore: docs
rach-id Jun 5, 2024
8220364
Update nodebuilder/blobstream/service.go
rach-id Jun 5, 2024
4309614
Merge branch 'main' into blobstream--api
rach-id Jun 5, 2024
15b59dd
chore: go mod tidy
rach-id Jun 5, 2024
8231ade
Merge remote-tracking branch 'origin/blobstream--api' into blobstream…
rach-id Jun 5, 2024
d31ee83
chore: lint
rach-id Jun 5, 2024
a784be9
chore: lint
rach-id Jun 5, 2024
4463b4a
chore: gofumpt
rach-id Jun 5, 2024
1953fc5
chore: gofumpt
rach-id Jun 5, 2024
eb0b29e
chore: gofumpt
rach-id Jun 5, 2024
e4c5910
chore: lowercase import
rach-id Jun 7, 2024
f8a3066
chore: internal struct definition inside API
rach-id Jun 7, 2024
a409722
chore: fmt
rach-id Jun 20, 2024
44f17a1
chore: DataCommitment to GetDataCommitment as suggested by @vgonkivs
rach-id Jun 20, 2024
080101a
chore: DataCommitment to GetDataCommitment as suggested by @vgonkivs
rach-id Jun 20, 2024
5fc0203
chore: DataRootInclusionProof to GetDataRootInclusionProof as suggest…
rach-id Jun 20, 2024
350d950
chore: remove todo
rach-id Jun 20, 2024
411fc20
chore: commitment proof pointer receiver
rach-id Jun 20, 2024
9e48e55
fix: subtreeRootProofs instead of SubtreeRoots in error message
rach-id Jun 20, 2024
f24171e
chore: new line
rach-id Jun 20, 2024
9b79dba
chore: revert unnecessary changes
rach-id Jun 20, 2024
7db0ab9
Update nodebuilder/blobstream/service.go
rach-id Jun 20, 2024
02be8ac
chore: unwrap API types
rach-id Jul 3, 2024
68936af
chore: refactor the blobstream API
rach-id Jul 13, 2024
4ce631d
Merge branch 'main' into blobstream--api
rach-id Jul 13, 2024
576c077
chore: imports
rach-id Jul 13, 2024
1f2dba2
fix: wrap the get range return value
rach-id Jul 13, 2024
765240d
chore: lint
rach-id Jul 13, 2024
3b2680b
chore: update nmt dependency
rach-id Jul 16, 2024
8c56c74
chore: use a ErrHeightNegative
rach-id Jul 19, 2024
609e714
chore: rename to data root tuple root
rach-id Jul 19, 2024
4bc4d7d
chore: rename to data root tuple root
rach-id Jul 19, 2024
76a1aaf
chore: un-export data root tuple
rach-id Jul 19, 2024
2c53cb0
docs: document GetDataRootTupleInclusionProof and GetDataRootTupleRoot
rach-id Jul 19, 2024
8bc6a66
chore: use uint64 for heights
rach-id Jul 19, 2024
6917aa2
Update nodebuilder/header/service.go
rach-id Jul 19, 2024
d44ffdb
chore: regenerate api
rach-id Jul 19, 2024
18452f6
fix: remove call to network head
rach-id Jul 19, 2024
6a0b5cf
Merge remote-tracking branch 'origin/blobstream--api' into blobstream…
rach-id Jul 19, 2024
e5f6a05
docs: comment suggestion by @renaynay
rach-id Jul 19, 2024
e5d1719
docs: whitespace as suggested by @renaynay
rach-id Jul 19, 2024
f8a7354
docs: whitespace as suggested by @renaynay
rach-id Jul 19, 2024
9d46758
chore: use GetHeaderRange instead of manually getting the headers
rach-id Jul 19, 2024
b9d7da2
refactor: remove data root tuple type
rach-id Jul 19, 2024
3cfecc2
Merge branch 'main' into blobstream--api
rach-id Jul 19, 2024
8276e5a
chore: bump app version and remove unnecessary checks
rach-id Jul 19, 2024
fbc63b6
Merge remote-tracking branch 'origin/blobstream--api' into blobstream…
rach-id Jul 19, 2024
da27e8a
refactor: reintroduce the blobstream module
rach-id Jul 19, 2024
4b71f5b
fix: correctly register the blobstream module
rach-id Jul 19, 2024
e978706
Update nodebuilder/blobstream/data_root_tuple_root.go
rach-id Jul 22, 2024
644c279
fix: also include the initial header when encoding data root tuples
rach-id Jul 22, 2024
b8d7a44
docs: update to comment to reflect data root tuple blocks limit time
rach-id Jul 22, 2024
dc08265
chore: gofumpt
rach-id Jul 22, 2024
1183e46
chore: add array capacity
rach-id Jul 22, 2024
6384bcf
Merge branch 'main' into blobstream--api
renaynay Jul 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
198 changes: 198 additions & 0 deletions blob/service.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package blob

import (
bytes2 "bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"slices"
Expand All @@ -14,7 +16,12 @@ import (
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"

"github.com/celestiaorg/celestia-app/pkg/appconsts"
appns "github.com/celestiaorg/celestia-app/pkg/namespace"
pkgproof "github.com/celestiaorg/celestia-app/pkg/proof"
"github.com/celestiaorg/celestia-app/pkg/shares"
"github.com/celestiaorg/nmt"
"github.com/celestiaorg/rsmt2d"

"github.com/celestiaorg/celestia-node/header"
"github.com/celestiaorg/celestia-node/libs/utils"
Expand Down Expand Up @@ -387,3 +394,194 @@ func (s *Service) getBlobs(
_, _, err = s.retrieve(ctx, header.Height(), namespace, sharesParser)
return blobs, err
}

func (s *Service) GetCommitmentProof(
ctx context.Context,
height uint64,
namespace share.Namespace,
shareCommitment []byte,
) (*CommitmentProof, error) {
log.Debugw("proving share commitment", "height", height, "commitment", shareCommitment, "namespace", namespace)
if height == 0 {
return nil, fmt.Errorf("height cannot be equal to 0")
}

// get the blob to compute the subtree roots
log.Debugw(
"getting the blob",
"height",
height,
"commitment",
shareCommitment,
"namespace",
namespace,
)
blb, err := s.Get(ctx, height, namespace, shareCommitment)
if err != nil {
return nil, err
}

log.Debugw(
"converting the blob to shares",
"height",
height,
"commitment",
shareCommitment,
"namespace",
namespace,
)
blobShares, err := BlobsToShares(blb)
if err != nil {
return nil, err
}
if len(blobShares) == 0 {
return nil, fmt.Errorf("the blob shares for commitment %s are empty", hex.EncodeToString(shareCommitment))
}

// get the extended header
log.Debugw(
"getting the extended header",
"height",
height,
)
extendedHeader, err := s.headerGetter(ctx, height)
if err != nil {
return nil, err
}

log.Debugw("getting eds", "height", height)
eds, err := s.shareGetter.GetEDS(ctx, extendedHeader)
if err != nil {
return nil, err
}

return ProveCommitment(eds, namespace, blobShares)
}

func ProveCommitment(
eds *rsmt2d.ExtendedDataSquare,
namespace share.Namespace,
blobShares []share.Share,
) (*CommitmentProof, error) {
// find the blob shares in the EDS
blobSharesStartIndex := -1
for index, share := range eds.FlattenedODS() {
if bytes2.Equal(share, blobShares[0]) {
blobSharesStartIndex = index
}
}
if blobSharesStartIndex < 0 {
return nil, fmt.Errorf("couldn't find the blob shares in the ODS")
}

nID, err := appns.From(namespace)
if err != nil {
return nil, err
}

log.Debugw(
"generating the blob share proof for commitment",
"start_share",
blobSharesStartIndex,
"end_share",
blobSharesStartIndex+len(blobShares),
)
sharesProof, err := pkgproof.NewShareInclusionProofFromEDS(
eds,
nID,
shares.NewRange(blobSharesStartIndex, blobSharesStartIndex+len(blobShares)),
)
if err != nil {
return nil, err
}

// convert the shares to row root proofs to nmt proofs
nmtProofs := make([]*nmt.Proof, 0)
for _, proof := range sharesProof.ShareProofs {
nmtProof := nmt.NewInclusionProof(int(proof.Start),
int(proof.End),
proof.Nodes,
true)
nmtProofs = append(
nmtProofs,
&nmtProof,
)
}

// compute the subtree roots of the blob shares
log.Debugw("computing the subtree roots")
subtreeRoots := make([][]byte, 0)
dataCursor := 0
for _, proof := range nmtProofs {
// TODO: do we want directly use the default subtree root threshold
// or want to allow specifying which version to use?
ranges, err := nmt.ToLeafRanges(
proof.Start(),
proof.End(),
shares.SubTreeWidth(len(blobShares), appconsts.DefaultSubtreeRootThreshold),
)
if err != nil {
return nil, err
}
roots, err := computeSubtreeRoots(
blobShares[dataCursor:dataCursor+proof.End()-proof.Start()],
ranges,
proof.Start(),
)
if err != nil {
return nil, err
}
subtreeRoots = append(subtreeRoots, roots...)
dataCursor += proof.End() - proof.Start()
}

log.Debugw("successfully proved the share commitment")
commitmentProof := CommitmentProof{
SubtreeRoots: subtreeRoots,
SubtreeRootProofs: nmtProofs,
NamespaceID: namespace.ID(),
RowProof: sharesProof.RowProof,
NamespaceVersion: namespace.Version(),
}
return &commitmentProof, nil
}

// computeSubtreeRoots takes a set of shares and ranges and returns the corresponding subtree roots.
// the offset is the number of shares that are before the subtree roots we're calculating.
func computeSubtreeRoots(shares []share.Share, ranges []nmt.LeafRange, offset int) ([][]byte, error) {
if len(shares) == 0 {
return nil, fmt.Errorf("cannot compute subtree roots for an empty shares list")
}
if len(ranges) == 0 {
return nil, fmt.Errorf("cannot compute subtree roots for an empty ranges list")
}
if offset < 0 {
return nil, fmt.Errorf("the offset %d cannot be stricly negative", offset)
}

// create a tree containing the shares to generate their subtree roots
tree := nmt.New(
appconsts.NewBaseHashFunc(),
nmt.IgnoreMaxNamespace(true),
nmt.NamespaceIDSize(share.NamespaceSize),
)
for _, sh := range shares {
leafData := make([]byte, 0)
leafData = append(append(leafData, share.GetNamespace(sh)...), sh...)
err := tree.Push(leafData)
if err != nil {
return nil, err
}
}

// generate the subtree roots
subtreeRoots := make([][]byte, 0)
for _, rg := range ranges {
root, err := tree.ComputeSubtreeRoot(rg.Start-offset, rg.End-offset)
if err != nil {
return nil, err
}
subtreeRoots = append(subtreeRoots, root)
}
return subtreeRoots, nil
}
Loading