Skip to content

Commit

Permalink
Merge pull request cosmos#57 from roysc/add-smt-spec
Browse files Browse the repository at this point in the history
Add `SmtSpec` for SMT proofs
  • Loading branch information
ethanfrey authored Feb 1, 2022
2 parents 02465ea + 01524ae commit 606d510
Show file tree
Hide file tree
Showing 15 changed files with 466 additions and 73 deletions.
50 changes: 38 additions & 12 deletions go/proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ var TendermintSpec = &ProofSpec{
},
}

// SmtSpec constrains the format for SMT proofs (as implemented by github.com/celestiaorg/smt)
var SmtSpec = &ProofSpec{
LeafSpec: &LeafOp{
Hash: HashOp_SHA256,
PrehashKey: HashOp_NO_HASH,
PrehashValue: HashOp_SHA256,
Length: LengthOp_NO_PREFIX,
Prefix: []byte{0},
},
InnerSpec: &InnerSpec{
ChildOrder: []int32{0, 1},
ChildSize: 32,
MinPrefixLength: 1,
MaxPrefixLength: 1,
EmptyChild: make([]byte, 32),
Hash: HashOp_SHA256,
},
MaxDepth: 256,
}

// Calculate determines the root hash that matches a given Commitment proof
// by type switching and calculating root based on proof type
// NOTE: Calculate will return the first calculated root in the proof,
Expand Down Expand Up @@ -216,7 +236,7 @@ func IsLeftMost(spec *InnerSpec, path []*InnerOp) bool {

// ensure every step has a prefix and suffix defined to be leftmost, unless it is a placeholder node
for _, step := range path {
if !hasPadding(step, minPrefix, maxPrefix, suffix) && !leftBranchesAreEmpty(spec, step, 0) {
if !hasPadding(step, minPrefix, maxPrefix, suffix) && !leftBranchesAreEmpty(spec, step) {
return false
}
}
Expand All @@ -230,7 +250,7 @@ func IsRightMost(spec *InnerSpec, path []*InnerOp) bool {

// ensure every step has a prefix and suffix defined to be rightmost, unless it is a placeholder node
for _, step := range path {
if !hasPadding(step, minPrefix, maxPrefix, suffix) && !rightBranchesAreEmpty(spec, step, int32(last)) {
if !hasPadding(step, minPrefix, maxPrefix, suffix) && !rightBranchesAreEmpty(spec, step) {
return false
}
}
Expand Down Expand Up @@ -310,12 +330,15 @@ func getPadding(spec *InnerSpec, branch int32) (minPrefix, maxPrefix, suffix int
return
}

// leftBranchesAreEmpty returns true if the padding bytes correspond to all empty children
// on the left side of this branch, ie. it's a valid placeholder on a leftmost path
func leftBranchesAreEmpty(spec *InnerSpec, op *InnerOp, branch int32) bool {
idx := getPosition(spec.ChildOrder, branch)
// leftBranchesAreEmpty returns true if the padding bytes correspond to all empty siblings
// on the left side of a branch, ie. it's a valid placeholder on a leftmost path
func leftBranchesAreEmpty(spec *InnerSpec, op *InnerOp) bool {
idx, err := orderFromPadding(spec, op)
if err != nil {
return false
}
// count branches to left of this
leftBranches := idx
leftBranches := int(idx)
if leftBranches == 0 {
return false
}
Expand All @@ -333,12 +356,15 @@ func leftBranchesAreEmpty(spec *InnerSpec, op *InnerOp, branch int32) bool {
return true
}

// rightBranchesAreEmpty returns true if the padding bytes correspond to all empty children
// on the right side of this branch, ie. it's a valid placeholder on a rightmost path
func rightBranchesAreEmpty(spec *InnerSpec, op *InnerOp, branch int32) bool {
idx := getPosition(spec.ChildOrder, branch)
// rightBranchesAreEmpty returns true if the padding bytes correspond to all empty siblings
// on the right side of a branch, ie. it's a valid placeholder on a rightmost path
func rightBranchesAreEmpty(spec *InnerSpec, op *InnerOp) bool {
idx, err := orderFromPadding(spec, op)
if err != nil {
return false
}
// count branches to right of this one
rightBranches := len(spec.ChildOrder) - 1 - idx
rightBranches := len(spec.ChildOrder) - 1 - int(idx)
if rightBranches == 0 {
return false
}
Expand Down
8 changes: 2 additions & 6 deletions go/proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,10 @@ func TestEmptyBranch(t *testing.T) {
if err := tc.Op.CheckAgainstSpec(tc.Spec); err != nil {
t.Errorf("Invalid InnerOp: %v", err)
}
order, err := orderFromPadding(tc.Spec.InnerSpec, tc.Op)
if err != nil {
t.Errorf("Cannot get orderFromPadding: %v", err)
}
if leftBranchesAreEmpty(tc.Spec.InnerSpec, tc.Op, order) != tc.IsLeft {
if leftBranchesAreEmpty(tc.Spec.InnerSpec, tc.Op) != tc.IsLeft {
t.Errorf("Expected leftBranchesAreEmpty to be %t but it wasn't", tc.IsLeft)
}
if rightBranchesAreEmpty(tc.Spec.InnerSpec, tc.Op, order) != tc.IsRight {
if rightBranchesAreEmpty(tc.Spec.InnerSpec, tc.Op) != tc.IsRight {
t.Errorf("Expected rightBranchesAreEmpty to be %t but it wasn't", tc.IsRight)
}
})
Expand Down
34 changes: 34 additions & 0 deletions go/vectors_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type TestVectorsStruct struct {
func VectorsTestData() []TestVectorsStruct {
iavl := filepath.Join("..", "testdata", "iavl")
tendermint := filepath.Join("..", "testdata", "tendermint")
smt := filepath.Join("..", "testdata", "smt")
cases := []TestVectorsStruct{
{Dir: iavl, Filename: "exist_left.json", Spec: IavlSpec},
{Dir: iavl, Filename: "exist_right.json", Spec: IavlSpec},
Expand All @@ -45,6 +46,12 @@ func VectorsTestData() []TestVectorsStruct {
{Dir: tendermint, Filename: "nonexist_left.json", Spec: TendermintSpec},
{Dir: tendermint, Filename: "nonexist_right.json", Spec: TendermintSpec},
{Dir: tendermint, Filename: "nonexist_middle.json", Spec: TendermintSpec},
{Dir: smt, Filename: "exist_left.json", Spec: SmtSpec},
{Dir: smt, Filename: "exist_right.json", Spec: SmtSpec},
{Dir: smt, Filename: "exist_middle.json", Spec: SmtSpec},
{Dir: smt, Filename: "nonexist_left.json", Spec: SmtSpec},
{Dir: smt, Filename: "nonexist_right.json", Spec: SmtSpec},
{Dir: smt, Filename: "nonexist_middle.json", Spec: SmtSpec},
}
return cases
}
Expand All @@ -69,6 +76,7 @@ type BatchVectorData struct {
func BatchVectorsTestData(t *testing.T) map[string]BatchVectorData {
iavl := filepath.Join("..", "testdata", "iavl")
tendermint := filepath.Join("..", "testdata", "tendermint")
smt := filepath.Join("..", "testdata", "smt")
// Note that each item has a different commitment root,
// so maybe not ideal (cannot check multiple entries)
batchIAVL, refsIAVL := buildBatch(t, iavl, []string{
Expand All @@ -87,10 +95,22 @@ func BatchVectorsTestData(t *testing.T) map[string]BatchVectorData {
"nonexist_right.json",
"nonexist_middle.json",
})
batchSMT, refsSMT := buildBatch(t, smt, []string{
"exist_left.json",
"exist_right.json",
"exist_middle.json",
"nonexist_left.json",
"nonexist_right.json",
"nonexist_middle.json",
})

batchTMExist, refsTMExist := loadBatch(t, tendermint, "batch_exist.json")
batchTMNonexist, refsTMNonexist := loadBatch(t, tendermint, "batch_nonexist.json")
batchIAVLExist, refsIAVLExist := loadBatch(t, iavl, "batch_exist.json")
batchIAVLNonexist, refsIAVLNonexist := loadBatch(t, iavl, "batch_nonexist.json")
batchSMTexist, refsSMTexist := loadBatch(t, smt, "batch_exist.json")
batchSMTnonexist, refsSMTnonexist := loadBatch(t, smt, "batch_nonexist.json")

return map[string]BatchVectorData{
"iavl 0": {Spec: IavlSpec, Proof: batchIAVL, Ref: refsIAVL[0]},
"iavl 1": {Spec: IavlSpec, Proof: batchIAVL, Ref: refsIAVL[1]},
Expand All @@ -114,18 +134,32 @@ func BatchVectorsTestData(t *testing.T) map[string]BatchVectorData {
"tm invalid 2": {Spec: TendermintSpec, Proof: refsTML, Ref: refsIAVL[0], Invalid: true},
"tm batch exist": {Spec: TendermintSpec, Proof: batchTMExist, Ref: refsTMExist[10]},
"tm batch nonexist": {Spec: TendermintSpec, Proof: batchTMNonexist, Ref: refsTMNonexist[3]},
"smt 0": {Spec: SmtSpec, Proof: batchSMT, Ref: refsSMT[0]},
"smt 1": {Spec: SmtSpec, Proof: batchSMT, Ref: refsSMT[1]},
"smt 2": {Spec: SmtSpec, Proof: batchSMT, Ref: refsSMT[2]},
"smt 3": {Spec: SmtSpec, Proof: batchSMT, Ref: refsSMT[3]},
"smt 4": {Spec: SmtSpec, Proof: batchSMT, Ref: refsSMT[4]},
"smt 5": {Spec: SmtSpec, Proof: batchSMT, Ref: refsSMT[5]},
// Note this spec only differs for non-existence proofs
"smt invalid 1": {Spec: IavlSpec, Proof: batchSMT, Ref: refsSMT[4], Invalid: true},
"smt invalid 2": {Spec: SmtSpec, Proof: batchSMT, Ref: refsIAVL[0], Invalid: true},
"smt batch exist": {Spec: SmtSpec, Proof: batchSMTexist, Ref: refsSMTexist[10]},
"smt batch nonexist": {Spec: SmtSpec, Proof: batchSMTnonexist, Ref: refsSMTnonexist[3]},
}
}

func DecompressBatchVectorsTestData(t *testing.T) map[string]*CommitmentProof {
iavl := filepath.Join("..", "testdata", "iavl")
tendermint := filepath.Join("..", "testdata", "tendermint")
smt := filepath.Join("..", "testdata", "smt")
// note that these batches are already compressed
batchIAVL, _ := loadBatch(t, iavl, "batch_exist.json")
batchTM, _ := loadBatch(t, tendermint, "batch_nonexist.json")
batchSMT, _ := loadBatch(t, smt, "batch_nonexist.json")
return map[string]*CommitmentProof{
"iavl": batchIAVL,
"tendermint": batchTM,
"smt": batchSMT,
}
}

Expand Down
19 changes: 19 additions & 0 deletions js/src/proofs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ export const tendermintSpec: ics23.IProofSpec = {
},
};

export const smtSpec: ics23.IProofSpec = {
leafSpec: {
hash: ics23.HashOp.SHA256,
prehashKey: ics23.HashOp.NO_HASH,
prehashValue: ics23.HashOp.SHA256,
length: ics23.LengthOp.NO_PREFIX,
prefix: Uint8Array.from([0]),
},
innerSpec: {
childOrder: [0, 1],
childSize: 32,
minPrefixLength: 1,
maxPrefixLength: 1,
emptyChild: new Uint8Array(32),
hash: ics23.HashOp.SHA256
},
maxDepth: 256,
};

export type CommitmentRoot = Uint8Array;

// verifyExistence will throw an error if the proof doesn't link key, value -> root
Expand Down
128 changes: 128 additions & 0 deletions rust/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,30 @@ pub fn tendermint_spec() -> ics23::ProofSpec {
}
}

pub fn smt_spec() -> ics23::ProofSpec {
let leaf = ics23::LeafOp {
hash: ics23::HashOp::Sha256.into(),
prehash_key: 0,
prehash_value: ics23::HashOp::Sha256.into(),
length: 0,
prefix: vec![0_u8],
};
let inner = ics23::InnerSpec {
child_order: vec![0, 1],
min_prefix_length: 1,
max_prefix_length: 1,
child_size: 32,
empty_child: vec![0; 32],
hash: ics23::HashOp::Sha256.into(),
};
ics23::ProofSpec {
leaf_spec: Some(leaf),
inner_spec: Some(inner),
min_depth: 0,
max_depth: 0,
}
}

#[cfg(feature = "std")]
#[cfg(test)]
mod tests {
Expand Down Expand Up @@ -359,6 +383,48 @@ mod tests {
verify_test_vector("../testdata/tendermint/nonexist_middle.json", &spec)
}

#[test]
#[cfg(feature = "std")]
fn test_vector_smt_left() -> Result<()> {
let spec = smt_spec();
verify_test_vector("../testdata/smt/exist_left.json", &spec)
}

#[test]
#[cfg(feature = "std")]
fn test_vector_smt_right() -> Result<()> {
let spec = smt_spec();
verify_test_vector("../testdata/smt/exist_right.json", &spec)
}

#[test]
#[cfg(feature = "std")]
fn test_vector_smt_middle() -> Result<()> {
let spec = smt_spec();
verify_test_vector("../testdata/smt/exist_middle.json", &spec)
}

#[test]
#[cfg(feature = "std")]
fn test_vector_smt_left_non() -> Result<()> {
let spec = smt_spec();
verify_test_vector("../testdata/smt/nonexist_left.json", &spec)
}

#[test]
#[cfg(feature = "std")]
fn test_vector_smt_right_non() -> Result<()> {
let spec = smt_spec();
verify_test_vector("../testdata/smt/nonexist_right.json", &spec)
}

#[test]
#[cfg(feature = "std")]
fn test_vector_smt_middle_non() -> Result<()> {
let spec = smt_spec();
verify_test_vector("../testdata/smt/nonexist_middle.json", &spec)
}

#[cfg(feature = "std")]
fn load_batch(files: &[&str]) -> Result<(ics23::CommitmentProof, Vec<RefData>)> {
let mut entries = Vec::new();
Expand Down Expand Up @@ -505,4 +571,66 @@ mod tests {
])?;
verify_batch(&spec, &proof, &data[5])
}

#[test]
#[cfg(feature = "std")]
fn test_vector_smt_batch_exist() -> Result<()> {
let spec = smt_spec();
let (proof, data) = load_batch(&[
"../testdata/smt/exist_left.json",
"../testdata/smt/exist_right.json",
"../testdata/smt/exist_middle.json",
"../testdata/smt/nonexist_left.json",
"../testdata/smt/nonexist_right.json",
"../testdata/smt/nonexist_middle.json",
])?;
verify_batch(&spec, &proof, &data[0])
}

#[test]
#[cfg(feature = "std")]
fn compressed_smt_batch_exist() -> Result<()> {
let spec = smt_spec();
let (proof, data) = load_batch(&[
"../testdata/smt/exist_left.json",
"../testdata/smt/exist_right.json",
"../testdata/smt/exist_middle.json",
"../testdata/smt/nonexist_left.json",
"../testdata/smt/nonexist_right.json",
"../testdata/smt/nonexist_middle.json",
])?;
let comp = compress(&proof)?;
verify_batch(&spec, &comp, &data[0])
}

#[test]
#[cfg(feature = "std")]
fn test_vector_smt_batch_nonexist() -> Result<()> {
let spec = smt_spec();
let (proof, data) = load_batch(&[
"../testdata/smt/exist_left.json",
"../testdata/smt/exist_right.json",
"../testdata/smt/exist_middle.json",
"../testdata/smt/nonexist_left.json",
"../testdata/smt/nonexist_right.json",
"../testdata/smt/nonexist_middle.json",
])?;
verify_batch(&spec, &proof, &data[4])
}

#[test]
#[cfg(feature = "std")]
fn compressed_smt_batch_nonexist() -> Result<()> {
let spec = smt_spec();
let (proof, data) = load_batch(&[
"../testdata/smt/exist_left.json",
"../testdata/smt/exist_right.json",
"../testdata/smt/exist_middle.json",
"../testdata/smt/nonexist_left.json",
"../testdata/smt/nonexist_right.json",
"../testdata/smt/nonexist_middle.json",
])?;
let comp = compress(&proof)?;
verify_batch(&spec, &comp, &data[4])
}
}
2 changes: 1 addition & 1 deletion rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod verify;

pub use crate::ics23::*;
pub use api::{
iavl_spec, tendermint_spec, verify_batch_membership, verify_batch_non_membership,
iavl_spec, smt_spec, tendermint_spec, verify_batch_membership, verify_batch_non_membership,
verify_membership, verify_non_membership,
};
pub use compress::{compress, decompress, is_compressed};
Expand Down
Loading

0 comments on commit 606d510

Please sign in to comment.