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!: unify the blob.Proof and the CommitmentProof #3821

Open
wants to merge 5 commits into
base: feature_branch_blob_proof
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
174 changes: 127 additions & 47 deletions blob/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"fmt"

"github.com/tendermint/tendermint/crypto/merkle"
"github.com/tendermint/tendermint/pkg/consts"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
coretypes "github.com/tendermint/tendermint/types"

"github.com/celestiaorg/celestia-app/v2/pkg/appconsts"
Expand All @@ -28,78 +26,150 @@ var errEmptyShares = errors.New("empty shares")

// Proof constructs the proof of a blob to the data root.
type Proof struct {
// ShareToRowRootProof the proofs of the shares to the row roots they belong to.
// SubtreeRoots are the subtree roots of the blob's data that are
// used to create the commitment.
SubtreeRoots [][]byte `json:"subtree_roots"`
// SubtreeRootProofs the proofs of the subtree roots to the row roots they belong to.
// If the blob spans across multiple rows, then this will contain multiple proofs.
ShareToRowRootProof []*tmproto.NMTProof
SubtreeRootProofs []*nmt.Proof `json:"share_to_row_root_proofs"`
// RowToDataRootProof the proofs of the row roots containing the blob shares
// to the data root.
RowToDataRootProof coretypes.RowProof
RowToDataRootProof coretypes.RowProof `json:"row_to_data_root_proof"`
}

// namespaceToRowRootProof a proof of a set of namespace shares to the row
// roots they belong to.
type namespaceToRowRootProof []*nmt.Proof

// Verify takes a blob and a data root and verifies if the
// provided blob was committed to the given data root.
func (p *Proof) Verify(blob *Blob, dataRoot []byte) (bool, error) {
blobCommitment, err := inclusion.CreateCommitment(
ToAppBlobs(blob)[0],
merkle.HashFromByteSlices,
appconsts.DefaultSubtreeRootThreshold,
)
if err != nil {
return false, err
// 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

// Verify takes a data root and verifies if the
// provided proof's subtree roots were committed to the given data root.
func (p *Proof) Verify(dataRoot []byte, subtreeRootThreshold int) (bool, error) {
if len(dataRoot) == 0 {
return false, errors.New("root must be non-empty")
}

if subtreeRootThreshold <= 0 {
return false, errors.New("subtreeRootThreshold must be > 0")
}
if !blob.Commitment.Equal(blobCommitment) {

// this check is < instead of != because we can have two subtree roots
// at the same height, depending on the subtree root threshold,
// and they can be used to create the above inner node without needing a proof inner node.
if len(p.SubtreeRoots) < len(p.SubtreeRootProofs) {
return false, fmt.Errorf(
"%w: generated commitment does not match the provided blob commitment",
ErrMismatchCommitment,
"the number of subtree roots %d should be bigger than the number of subtree root proofs %d",
len(p.SubtreeRoots),
len(p.SubtreeRootProofs),
)
}
rawShares, err := BlobsToShares(blob)
if err != nil {
return false, err

// for each row, one or more subtree roots' inclusion is verified against
// their corresponding row root. then, these row roots' inclusion is verified
// against the data root. so their number should be the same.
if len(p.SubtreeRootProofs) != len(p.RowToDataRootProof.Proofs) {
return false, fmt.Errorf(
"the number of subtree root proofs %d should be equal to the number of row root proofs %d",
len(p.SubtreeRootProofs),
len(p.RowToDataRootProof.Proofs),
)
}

// the row root proofs' ranges are defined as [startRow, endRow].
if int(p.RowToDataRootProof.EndRow-p.RowToDataRootProof.StartRow+1) != len(p.RowToDataRootProof.RowRoots) {
return false, fmt.Errorf(
"the number of rows %d must equal the number of row roots %d",
int(p.RowToDataRootProof.EndRow-p.RowToDataRootProof.StartRow+1),
len(p.RowToDataRootProof.RowRoots),
)
}
if len(p.RowToDataRootProof.Proofs) != len(p.RowToDataRootProof.RowRoots) {
return false, fmt.Errorf(
"the number of proofs %d must equal the number of row roots %d",
len(p.RowToDataRootProof.Proofs),
len(p.RowToDataRootProof.RowRoots),
)
}
return p.VerifyShares(rawShares, blob.namespace, dataRoot)
}

// VerifyShares takes a set of shares, a namespace and a data root, and verifies if the
// provided shares are committed to by the data root.
func (p *Proof) VerifyShares(rawShares [][]byte, namespace share.Namespace, dataRoot []byte) (bool, error) {
// verify the row proof
// verify the inclusion of the rows to the data root
if err := p.RowToDataRootProof.Validate(dataRoot); err != nil {
return false, fmt.Errorf("%w: invalid row root to data root proof", err)
return false, err
}

// computes the total number of shares proven given that each subtree root
// references a specific set of leaves.
numberOfShares := 0
for _, proof := range p.SubtreeRootProofs {
numberOfShares += proof.End() - proof.Start()
}

// verify the share proof
ns := append([]byte{namespace.Version()}, namespace.ID()...)
cursor := int32(0)
for i, proof := range p.ShareToRowRootProof {
sharesUsed := proof.End - proof.Start
if len(rawShares) < int(sharesUsed+cursor) {
return false, fmt.Errorf("%w: invalid number of shares", ErrInvalidProof)
// 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 := inclusion.SubTreeWidth(numberOfShares, subtreeRootThreshold)

nmtHasher := nmt.NewNmtHasher(appconsts.NewBaseHashFunc(), share.NamespaceSize, true)
// verify the proof of the subtree roots
subtreeRootsCursor := 0
for i, subtreeRootProof := range p.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
}
nmtProof := nmt.NewInclusionProof(
int(proof.Start),
int(proof.End),
proof.Nodes,
true,
)
valid := nmtProof.VerifyInclusion(
consts.NewBaseHashFunc(),
ns,
rawShares[cursor:sharesUsed+cursor],

if len(p.SubtreeRoots) < subtreeRootsCursor {
return false, fmt.Errorf("len(commitmentProof.SubtreeRoots)=%d < subtreeRootsCursor=%d",
len(p.SubtreeRoots), subtreeRootsCursor)
}
if len(p.SubtreeRoots) < subtreeRootsCursor+len(ranges) {
return false, fmt.Errorf("len(commitmentProof.SubtreeRoots)=%d < subtreeRootsCursor+len(ranges)=%d",
len(p.SubtreeRoots), subtreeRootsCursor+len(ranges))
}
valid, err := subtreeRootProof.VerifySubtreeRootInclusion(
nmtHasher,
p.SubtreeRoots[subtreeRootsCursor:subtreeRootsCursor+len(ranges)],
subtreeRootsWidth,
p.RowToDataRootProof.RowRoots[i],
)
if err != nil {
return false, err
}
if !valid {
return false, ErrInvalidProof
return false,
fmt.Errorf(
"subtree root proof for range [%d, %d) is invalid",
subtreeRootProof.Start(),
subtreeRootProof.End(),
)
}
cursor += sharesUsed
subtreeRootsCursor += len(ranges)
}

return true, nil
}

// GenerateCommitment generates the share commitment corresponding
// to the proof's subtree roots
func (p *Proof) GenerateCommitment() []byte {
return merkle.HashFromByteSlices(p.SubtreeRoots)
}

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)
}

// Blob represents any application-specific binary data that anyone can submit to Celestia.
type Blob struct {
*blob.Blob `json:"blob"`
Expand Down Expand Up @@ -224,6 +294,16 @@ func (b *Blob) UnmarshalJSON(data []byte) error {
return nil
}

func (b *Blob) ComputeSubtreeRoots() ([][]byte, error) {
gsBlob := blob.Blob{
NamespaceId: b.NamespaceId,
Data: b.Data,
ShareVersion: b.ShareVersion,
NamespaceVersion: b.NamespaceVersion,
}
return inclusion.GenerateSubtreeRoots(&gsBlob, appconsts.SubtreeRootThreshold(appVersion))
}

// proveRowRootsToDataRoot creates a set of binary merkle proofs for all the
// roots defined by the range [start, end).
func proveRowRootsToDataRoot(roots [][]byte, start, end int) []*merkle.Proof {
Expand Down
8 changes: 4 additions & 4 deletions blob/blob_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ func FuzzProofEqual(f *testing.F) {
}

type verifyCorpus struct {
CP *CommitmentProof `json:"commitment_proof"`
Root []byte `json:"root"`
SThreshold int `json:"sub_threshold"`
Proof *Proof `json:"proof"`
Root []byte `json:"root"`
SThreshold int `json:"sub_threshold"`
}

func FuzzCommitmentProofVerify(f *testing.F) {
Expand Down Expand Up @@ -83,7 +83,7 @@ func FuzzCommitmentProofVerify(f *testing.F) {
if err := json.Unmarshal(valueJSON, val); err != nil {
return
}
commitProof := val.CP
commitProof := val.Proof
if commitProof == nil {
return
}
Expand Down
Loading