feat: Add ics23 proof tools: `ics23-{iavl,tendermint,smt}` (#10802)

Moves the separate repos for ICS23 proof tooling into `store/tools`

I've set `github.com/confio/ics23/go` to version 0.7.0 in anticipation of https://github.com/confio/ics23/pull/61 being merged, as it's a dependency for SMT proofs, so this shouldn't be merged until that version exists.

Closes: https://github.com/cosmos/cosmos-sdk/issues/10801



---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [x] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed 
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
This commit is contained in:
Roy Crihfield 2022-02-22 19:39:08 +08:00 committed by GitHub
parent 81cfc6cc85
commit 56af6a7fbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 3008 additions and 21 deletions

View File

@ -15,23 +15,6 @@ var (
ErrEmptyKeyInData = errors.New("data contains empty key")
)
// TendermintSpec constrains the format from ics23-tendermint (crypto/merkle SimpleProof)
var TendermintSpec = &ics23.ProofSpec{
LeafSpec: &ics23.LeafOp{
Prefix: []byte{0},
Hash: ics23.HashOp_SHA256,
PrehashValue: ics23.HashOp_SHA256,
Length: ics23.LengthOp_VAR_PROTO,
},
InnerSpec: &ics23.InnerSpec{
ChildOrder: []int32{0, 1},
MinPrefixLength: 1,
MaxPrefixLength: 1, // fixed prefix + one child
ChildSize: 32, // (no length byte)
Hash: ics23.HashOp_SHA256,
},
}
/*
CreateMembershipProof will produce a CommitmentProof that the given key (and queries value) exists in the iavl tree.
If the key doesn't exist in the tree, this will return an error.

View File

@ -36,12 +36,12 @@ func TestCreateMembership(t *testing.T) {
}
root := CalcRoot(data)
err = proof.GetExist().Verify(TendermintSpec, root, []byte(key), val)
err = proof.GetExist().Verify(ics23.TendermintSpec, root, []byte(key), val)
if err != nil {
t.Fatalf("Verifying Proof: %+v", err)
}
valid := ics23.VerifyMembership(TendermintSpec, root, proof, []byte(key), val)
valid := ics23.VerifyMembership(ics23.TendermintSpec, root, proof, []byte(key), val)
if !valid {
t.Fatalf("Membership Proof Invalid")
}
@ -77,12 +77,12 @@ func TestCreateNonMembership(t *testing.T) {
}
root := CalcRoot(data)
err = proof.GetNonexist().Verify(TendermintSpec, root, []byte(key))
err = proof.GetNonexist().Verify(ics23.TendermintSpec, root, []byte(key))
if err != nil {
t.Fatalf("Verifying Proof: %+v", err)
}
valid := ics23.VerifyNonMembership(TendermintSpec, root, proof, []byte(key))
valid := ics23.VerifyNonMembership(ics23.TendermintSpec, root, proof, []byte(key))
if !valid {
t.Fatalf("Non Membership Proof Invalid")
}

View File

@ -0,0 +1,44 @@
GO_RUN= go run -mod=readonly
GENDIR ?= ./testdata
.PHONY: testgen testgen-iavl testgen-smt testgen-simple
# make sure we turn on go modules
export GO111MODULE := on
# Usage: GENDIR=/path/to/ics23/testdata/iavl make testgen-iavl
testgen: testgen-iavl testgen-smt testgen-simple
testgen-iavl:
@mkdir -p "$(GENDIR)"
$(GO_RUN) ./iavl/cmd/testgen-iavl exist left 987 > "$(GENDIR)"/exist_left.json
$(GO_RUN) ./iavl/cmd/testgen-iavl exist middle 812 > "$(GENDIR)"/exist_middle.json
$(GO_RUN) ./iavl/cmd/testgen-iavl exist right 1261 > "$(GENDIR)"/exist_right.json
$(GO_RUN) ./iavl/cmd/testgen-iavl nonexist left 813 > "$(GENDIR)"/nonexist_left.json
$(GO_RUN) ./iavl/cmd/testgen-iavl nonexist middle 691 > "$(GENDIR)"/nonexist_middle.json
$(GO_RUN) ./iavl/cmd/testgen-iavl nonexist right 1535 > "$(GENDIR)"/nonexist_right.json
$(GO_RUN) ./iavl/cmd/testgen-iavl batch 1801 20 0 > "$(GENDIR)"/batch_exist.json
$(GO_RUN) ./iavl/cmd/testgen-iavl batch 1807 0 20 > "$(GENDIR)"/batch_nonexist.json
testgen-smt:
@mkdir -p "$(GENDIR)"
$(GO_RUN) ./smt/cmd/testgen-smt exist left 987 > "$(GENDIR)"/exist_left.json
$(GO_RUN) ./smt/cmd/testgen-smt exist middle 812 > "$(GENDIR)"/exist_middle.json
$(GO_RUN) ./smt/cmd/testgen-smt exist right 1261 > "$(GENDIR)"/exist_right.json
$(GO_RUN) ./smt/cmd/testgen-smt nonexist left 813 > "$(GENDIR)"/nonexist_left.json
$(GO_RUN) ./smt/cmd/testgen-smt nonexist middle 691 > "$(GENDIR)"/nonexist_middle.json
$(GO_RUN) ./smt/cmd/testgen-smt nonexist right 1535 > "$(GENDIR)"/nonexist_right.json
$(GO_RUN) ./smt/cmd/testgen-smt batch 1801 20 0 > "$(GENDIR)"/batch_exist.json
$(GO_RUN) ./smt/cmd/testgen-smt batch 1807 0 20 > "$(GENDIR)"/batch_nonexist.json
testgen-simple:
@mkdir -p "$(GENDIR)"
$(GO_RUN) ./tendermint/cmd/testgen-simple exist left 987 > "$(GENDIR)"/exist_left.json
$(GO_RUN) ./tendermint/cmd/testgen-simple exist middle 812 > "$(GENDIR)"/exist_middle.json
$(GO_RUN) ./tendermint/cmd/testgen-simple exist right 1261 > "$(GENDIR)"/exist_right.json
$(GO_RUN) ./tendermint/cmd/testgen-simple nonexist left 813 > "$(GENDIR)"/nonexist_left.json
$(GO_RUN) ./tendermint/cmd/testgen-simple nonexist middle 691 > "$(GENDIR)"/nonexist_middle.json
$(GO_RUN) ./tendermint/cmd/testgen-simple nonexist right 1535 > "$(GENDIR)"/nonexist_right.json
$(GO_RUN) ./tendermint/cmd/testgen-simple batch 1801 20 0 > "$(GENDIR)"/batch_exist.json
$(GO_RUN) ./tendermint/cmd/testgen-simple batch 1807 0 20 > "$(GENDIR)"/batch_nonexist.json

44
store/tools/ics23/go.mod Normal file
View File

@ -0,0 +1,44 @@
module github.com/cosmos/cosmos-sdk/store/tools/ics23
go 1.17
require (
github.com/confio/ics23/go v0.6.7-0.20220201201850-606d5105384e
github.com/cosmos/cosmos-sdk v0.45.0
github.com/cosmos/iavl v0.17.3
github.com/lazyledger/smt v0.2.1-0.20210709230900-03ea40719554
github.com/tendermint/tendermint v0.34.14
github.com/tendermint/tm-db v0.6.4
)
require (
github.com/DataDog/zstd v1.4.5 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/dgraph-io/badger/v2 v2.2007.2 // indirect
github.com/dgraph-io/ristretto v0.0.3 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gogo/protobuf v1.3.3 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.3-0.20201103224600-674baa8c7fc3 // indirect
github.com/google/btree v1.0.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa // indirect
github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca // indirect
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect
go.etcd.io/bbolt v1.3.5 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f // indirect
golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71 // indirect
google.golang.org/grpc v1.42.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
)
replace github.com/cosmos/cosmos-sdk/store/tools/ics23 => ./
replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1

1236
store/tools/ics23/go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
package ics23_tools
import (
"fmt"
"strconv"
tmproofs "github.com/cosmos/cosmos-sdk/store/internal/proofs"
)
func ParseArgs(args []string) (exist bool, loc tmproofs.Where, size int, err error) {
if len(args) != 3 && len(args) != 4 {
err = fmt.Errorf("Insufficient args")
return
}
switch args[1] {
case "exist":
exist = true
case "nonexist":
exist = false
default:
err = fmt.Errorf("Invalid arg: %s", args[1])
return
}
switch args[2] {
case "left":
loc = tmproofs.Left
case "middle":
loc = tmproofs.Middle
case "right":
loc = tmproofs.Right
default:
err = fmt.Errorf("Invalid arg: %s", args[2])
return
}
size = 400
if len(args) == 4 {
size, err = strconv.Atoi(args[3])
}
return
}
func ParseBatchArgs(args []string) (size int, exist int, nonexist int, err error) {
if len(args) != 3 {
err = fmt.Errorf("Insufficient args")
return
}
size, err = strconv.Atoi(args[0])
if err != nil {
return
}
exist, err = strconv.Atoi(args[1])
if err != nil {
return
}
nonexist, err = strconv.Atoi(args[2])
return
}

View File

@ -0,0 +1,18 @@
.PHONY: testgen
GENDIR ?= ./testdata
# make sure we turn on go modules
export GO111MODULE := on
# Usage: GENDIR=../ics23/testdata/iavl make testgen
testgen:
@mkdir -p "$(GENDIR)"
go run -mod=readonly ./cmd/testgen-iavl exist left 987 > "$(GENDIR)"/exist_left.json
go run -mod=readonly ./cmd/testgen-iavl exist middle 812 > "$(GENDIR)"/exist_middle.json
go run -mod=readonly ./cmd/testgen-iavl exist right 1261 > "$(GENDIR)"/exist_right.json
go run -mod=readonly ./cmd/testgen-iavl nonexist left 813 > "$(GENDIR)"/nonexist_left.json
go run -mod=readonly ./cmd/testgen-iavl nonexist middle 691 > "$(GENDIR)"/nonexist_middle.json
go run -mod=readonly ./cmd/testgen-iavl nonexist right 1535 > "$(GENDIR)"/nonexist_right.json
go run -mod=readonly ./cmd/testgen-iavl batch 1801 20 0 > "$(GENDIR)"/batch_exist.json
go run -mod=readonly ./cmd/testgen-iavl batch 1807 0 20 > "$(GENDIR)"/batch_nonexist.json

View File

@ -0,0 +1,44 @@
# Proofs IAVL
This is a demo project to show converting proofs from cosmos/iavl into the format
specified in confio/proofs and validating that they still work.
## Library usage
It exposes a two main functions :
`func CreateMembershipProof(tree *iavl.MutableTree, key []byte) (*proofs.CommitmentProof, error)`
produces a CommitmentProof that the given key exists in the iavl tree (and contains the
current value). This returns an error if the key does not exist in the tree.
`func CreateNonMembershipProof(tree *iavl.MutableTree, key []byte) (*proofs.CommitmentProof, error)`
produces a CommitmentProof that the given key doesn't exist in the iavl tree.
This returns an error if the key does not exist in the tree.
Generalized range proofs are lower in priority, as they are just an optimization of the
two basic proof types, and don't provide any additional capabilities.
We will soon add some `Batch` capabilities that can represent these.
## CLI usage
We also expose a simple script to generate test data for the confio proofs package.
```shell
go install ./cmd/testgen-iavl
testgen-iavl
```
Will output some json data, from a randomly generated merkle tree each time.
```json
{
"existence": "0a146f65436a684273735a34567543774b567a435963121e76616c75655f666f725f6f65436a684273735a34567543774b567a4359631a0d0a0b0801180120012a030002021a2d122b08011204020402201a2120d307032505383dee34ea9eadf7649c31d1ce294b6d62b273d804da478ac161da1a2d122b08011204040802201a2120306b7d51213bd93bac17c5ee3d727ec666300370b19fd55cc13d7341dc589a991a2b12290801122508160220857103d59863ac55d1f34008a681f837c01975a223c0f54883a05a446d49c7c6201a2b1229080112250a2202204498eb5c93e40934bc8bad9626f19e333c1c0be4541b9098f139585c3471bae2201a2d122b080112040e6c02201a212022648db12dbf830485cc41435ecfe37bcac26c6c305ac4304f649977ddc339d51a2c122a0801122610c60102204e0b7996a7104f5b1ac1a2caa0704c4b63f60112e0e13763b2ba03f40a54e845201a2c122a08011226129003022017858e28e0563f7252eaca19acfc1c3828c892e635f76f971b3fbdc9bbd2742e20",
"root": "cea07656c77e8655521f4c904730cf4649242b8e482be786b2b220a15150d5f0"
}
```
`"root"` is the hex-encoded root hash of the merkle tree
`"existence"` is the hex-encoding of the protobuf binary encoding of a `proofs.ExistenceProof` object. This contains a (key, value) pair,
along with all steps to reach the root hash. This provides a non-trivial test case, to ensure client in multiple languages can verify the
protobuf proofs we generate from the iavl tree

View File

@ -0,0 +1,195 @@
package main
import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
ics23 "github.com/confio/ics23/go"
tmproofs "github.com/cosmos/cosmos-sdk/store/internal/proofs"
tools "github.com/cosmos/cosmos-sdk/store/tools/ics23"
iavlproofs "github.com/cosmos/cosmos-sdk/store/tools/ics23/iavl"
"github.com/cosmos/cosmos-sdk/store/tools/ics23/iavl/helpers"
)
/**
testgen-iavl will generate a json struct on stdout (meant to be saved to file for testdata).
this will be an auto-generated existence proof in the form:
{
"root": "<hex encoded root hash of tree>",
"key": "<hex encoded key to prove>",
"value": "<hex encoded value to prove> (empty on non-existence)",
"proof": "<hex encoded protobuf marshaling of a CommitmentProof>"
}
It accepts two or three arguments (optional size: default 400)
testgen-iavl [exist|nonexist] [left|right|middle] <size>
If you make a batch, we have a different format:
{
"root": "<hex encoded root hash of tree>",
"proof": "<hex encoded protobuf marshaling of a CommitmentProof (Compressed Batch)>",
"items": [{
"key": "<hex encoded key to prove>",
"value": "<hex encoded value to prove> (empty on non-existence)",
}, ...]
}
The batch variant accepts 5 arguments:
testgen-iavl [batch] [size] [num exist] [num nonexist]
**/
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: testgen-iavl batch [size] [# exist] [# nonexist]")
fmt.Println(" testgen-iavl [exist|nonexist] [left|right|middle] <size>")
os.Exit(1)
}
if os.Args[1] == "batch" {
err := doBatch(os.Args[2:])
if err != nil {
fmt.Printf("%+v\n", err)
fmt.Println("Usage: testgen-iavl [batch] [size] [# exist] [# nonexist]")
os.Exit(1)
}
return
}
exist, loc, size, err := tools.ParseArgs(os.Args)
if err != nil {
fmt.Printf("%+v\n", err)
fmt.Println("Usage: testgen-iavl [exist|nonexist] [left|right|middle] <size>")
os.Exit(1)
}
tree, allkeys, err := helpers.BuildTree(size)
if err != nil {
fmt.Printf("%+v\n", err)
fmt.Println("Usage: testgen-iavl [exist|nonexist] [left|right|middle] <size>")
os.Exit(1)
}
root := tree.WorkingHash()
var key, value []byte
if exist {
key = helpers.GetKey(allkeys, loc)
_, value = tree.Get(key)
} else {
key = helpers.GetNonKey(allkeys, loc)
}
var proof *ics23.CommitmentProof
if exist {
proof, err = iavlproofs.CreateMembershipProof(tree, key)
} else {
proof, err = iavlproofs.CreateNonMembershipProof(tree, key)
}
if err != nil {
fmt.Printf("Error: create proof: %+v\n", err)
os.Exit(1)
}
binary, err := proof.Marshal()
if err != nil {
fmt.Printf("Error: protobuf marshal: %+v\n", err)
os.Exit(1)
}
res := map[string]interface{}{
"root": hex.EncodeToString(root),
"key": hex.EncodeToString(key),
"value": hex.EncodeToString(value),
"proof": hex.EncodeToString(binary),
}
out, err := json.MarshalIndent(res, "", " ")
if err != nil {
fmt.Printf("Error: json encoding: %+v\n", err)
os.Exit(1)
}
fmt.Println(string(out))
}
type item struct {
Key string `json:"key"`
Value string `json:"value"`
}
func doBatch(args []string) error {
size, exist, nonexist, err := tools.ParseBatchArgs(args)
if err != nil {
return err
}
tree, allkeys, err := helpers.BuildTree(size)
if err != nil {
return err
}
root := tree.WorkingHash()
items := []item{}
proofs := []*ics23.CommitmentProof{}
for i := 0; i < exist; i++ {
key := []byte(helpers.GetKey(allkeys, tmproofs.Middle))
_, value := tree.Get(key)
proof, err := iavlproofs.CreateMembershipProof(tree, key)
if err != nil {
return fmt.Errorf("create proof: %+v", err)
}
proofs = append(proofs, proof)
item := item{
Key: hex.EncodeToString(key),
Value: hex.EncodeToString(value),
}
items = append(items, item)
}
for i := 0; i < nonexist; i++ {
key := []byte(helpers.GetNonKey(allkeys, tmproofs.Middle))
proof, err := iavlproofs.CreateNonMembershipProof(tree, key)
if err != nil {
return fmt.Errorf("create proof: %+v", err)
}
proofs = append(proofs, proof)
item := item{
Key: hex.EncodeToString(key),
}
items = append(items, item)
}
// combine all proofs into batch and compress
proof, err := ics23.CombineProofs(proofs)
if err != nil {
fmt.Printf("Error: combine proofs: %+v\n", err)
os.Exit(1)
}
binary, err := proof.Marshal()
if err != nil {
fmt.Printf("Error: protobuf marshal: %+v\n", err)
os.Exit(1)
}
res := map[string]interface{}{
"root": hex.EncodeToString(root),
"items": items,
"proof": hex.EncodeToString(binary),
}
out, err := json.MarshalIndent(res, "", " ")
if err != nil {
fmt.Printf("Error: json encoding: %+v\n", err)
os.Exit(1)
}
fmt.Println(string(out))
return nil
}

View File

@ -0,0 +1,98 @@
package iavlproofs
import (
"fmt"
ics23 "github.com/confio/ics23/go"
"github.com/cosmos/iavl"
)
// convertExistenceProof will convert the given proof into a valid
// existence proof, if that's what it is.
//
// This is the simplest case of the range proof and we will focus on
// demoing compatibility here
func convertExistenceProof(p *iavl.RangeProof, key, value []byte) (*ics23.ExistenceProof, error) {
if len(p.Leaves) != 1 {
return nil, fmt.Errorf("Existence proof requires RangeProof to have exactly one leaf")
}
return &ics23.ExistenceProof{
Key: key,
Value: value,
Leaf: convertLeafOp(p.Leaves[0].Version),
Path: convertInnerOps(p.LeftPath),
}, nil
}
func convertLeafOp(version int64) *ics23.LeafOp {
// this is adapted from iavl/proof.go:proofLeafNode.Hash()
prefix := aminoVarInt(0)
prefix = append(prefix, aminoVarInt(1)...)
prefix = append(prefix, aminoVarInt(version)...)
return &ics23.LeafOp{
Hash: ics23.HashOp_SHA256,
PrehashValue: ics23.HashOp_SHA256,
Length: ics23.LengthOp_VAR_PROTO,
Prefix: prefix,
}
}
// we cannot get the proofInnerNode type, so we need to do the whole path in one function
func convertInnerOps(path iavl.PathToLeaf) []*ics23.InnerOp {
steps := make([]*ics23.InnerOp, 0, len(path))
// lengthByte is the length prefix prepended to each of the sha256 sub-hashes
var lengthByte byte = 0x20
// we need to go in reverse order, iavl starts from root to leaf,
// we want to go up from the leaf to the root
for i := len(path) - 1; i >= 0; i-- {
// this is adapted from iavl/proof.go:proofInnerNode.Hash()
prefix := aminoVarInt(int64(path[i].Height))
prefix = append(prefix, aminoVarInt(path[i].Size)...)
prefix = append(prefix, aminoVarInt(path[i].Version)...)
var suffix []byte
if len(path[i].Left) > 0 {
// length prefixed left side
prefix = append(prefix, lengthByte)
prefix = append(prefix, path[i].Left...)
// prepend the length prefix for child
prefix = append(prefix, lengthByte)
} else {
// prepend the length prefix for child
prefix = append(prefix, lengthByte)
// length-prefixed right side
suffix = []byte{lengthByte}
suffix = append(suffix, path[i].Right...)
}
op := &ics23.InnerOp{
Hash: ics23.HashOp_SHA256,
Prefix: prefix,
Suffix: suffix,
}
steps = append(steps, op)
}
return steps
}
func aminoVarInt(orig int64) []byte {
// amino-specific byte swizzling
i := uint64(orig) << 1
if orig < 0 {
i = ^i
}
// avoid multiple allocs for normal case
res := make([]byte, 0, 8)
// standard protobuf encoding
for i >= 1<<7 {
res = append(res, uint8(i&0x7f|0x80))
i >>= 7
}
res = append(res, uint8(i))
return res
}

View File

@ -0,0 +1,30 @@
package iavlproofs
import (
"bytes"
"testing"
tmproofs "github.com/cosmos/cosmos-sdk/store/internal/proofs"
"github.com/cosmos/cosmos-sdk/store/tools/ics23/iavl/helpers"
)
func TestConvertExistence(t *testing.T) {
proof, err := helpers.GenerateIavlResult(200, tmproofs.Middle)
if err != nil {
t.Fatal(err)
}
converted, err := convertExistenceProof(proof.Proof, proof.Key, proof.Value)
if err != nil {
t.Fatal(err)
}
calc, err := converted.Calculate()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(calc, proof.RootHash) {
t.Errorf("Calculated: %X\nExpected: %X", calc, proof.RootHash)
}
}

View File

@ -0,0 +1,94 @@
package iavlproofs
import (
"fmt"
ics23 "github.com/confio/ics23/go"
"github.com/cosmos/iavl"
)
// IavlSpec constrains the format from ics23-iavl (iavl merkle ics23)
var IavlSpec = &ics23.ProofSpec{
LeafSpec: &ics23.LeafOp{
Prefix: []byte{0},
Hash: ics23.HashOp_SHA256,
PrehashValue: ics23.HashOp_SHA256,
Length: ics23.LengthOp_VAR_PROTO,
},
InnerSpec: &ics23.InnerSpec{
ChildOrder: []int32{0, 1},
MinPrefixLength: 4,
MaxPrefixLength: 12,
ChildSize: 33, // (with length byte)
Hash: ics23.HashOp_SHA256,
},
}
/*
CreateMembershipProof will produce a CommitmentProof that the given key (and queries value) exists in the iavl tree.
If the key doesn't exist in the tree, this will return an error.
*/
func CreateMembershipProof(tree *iavl.MutableTree, key []byte) (*ics23.CommitmentProof, error) {
exist, err := createExistenceProof(tree, key)
if err != nil {
return nil, err
}
proof := &ics23.CommitmentProof{
Proof: &ics23.CommitmentProof_Exist{
Exist: exist,
},
}
return proof, nil
}
/*
CreateNonMembershipProof will produce a CommitmentProof that the given key doesn't exist in the iavl tree.
If the key exists in the tree, this will return an error.
*/
func CreateNonMembershipProof(tree *iavl.MutableTree, key []byte) (*ics23.CommitmentProof, error) {
// idx is one node right of what we want....
idx, val := tree.Get(key)
if val != nil {
return nil, fmt.Errorf("Cannot create NonExistanceProof when Key in State")
}
var err error
nonexist := &ics23.NonExistenceProof{
Key: key,
}
if idx >= 1 {
leftkey, _ := tree.GetByIndex(idx - 1)
nonexist.Left, err = createExistenceProof(tree, leftkey)
if err != nil {
return nil, err
}
}
// this will be nil if nothing right of the queried key
rightkey, _ := tree.GetByIndex(idx)
if rightkey != nil {
nonexist.Right, err = createExistenceProof(tree, rightkey)
if err != nil {
return nil, err
}
}
proof := &ics23.CommitmentProof{
Proof: &ics23.CommitmentProof_Nonexist{
Nonexist: nonexist,
},
}
return proof, nil
}
func createExistenceProof(tree *iavl.MutableTree, key []byte) (*ics23.ExistenceProof, error) {
value, proof, err := tree.GetWithProof(key)
if err != nil {
return nil, err
}
if value == nil {
return nil, fmt.Errorf("Cannot create ExistanceProof when Key not in State")
}
return convertExistenceProof(proof, key, value)
}

View File

@ -0,0 +1,80 @@
package iavlproofs
import (
"testing"
ics23 "github.com/confio/ics23/go"
tmproofs "github.com/cosmos/cosmos-sdk/store/internal/proofs"
"github.com/cosmos/cosmos-sdk/store/tools/ics23/iavl/helpers"
)
func TestCreateMembership(t *testing.T) {
cases := map[string]struct {
size int
loc tmproofs.Where
}{
"small left": {size: 100, loc: tmproofs.Left},
"small middle": {size: 100, loc: tmproofs.Middle},
"small right": {size: 100, loc: tmproofs.Right},
"big left": {size: 5431, loc: tmproofs.Left},
"big middle": {size: 5431, loc: tmproofs.Middle},
"big right": {size: 5431, loc: tmproofs.Right},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
tree, allkeys, err := helpers.BuildTree(tc.size)
if err != nil {
t.Fatalf("Creating tree: %+v", err)
}
key := helpers.GetKey(allkeys, tc.loc)
_, val := tree.Get(key)
proof, err := CreateMembershipProof(tree, key)
if err != nil {
t.Fatalf("Creating Proof: %+v", err)
}
root := tree.WorkingHash()
valid := ics23.VerifyMembership(IavlSpec, root, proof, key, val)
if !valid {
t.Fatalf("Membership Proof Invalid")
}
})
}
}
func TestCreateNonMembership(t *testing.T) {
cases := map[string]struct {
size int
loc tmproofs.Where
}{
"small left": {size: 100, loc: tmproofs.Left},
"small middle": {size: 100, loc: tmproofs.Middle},
"small right": {size: 100, loc: tmproofs.Right},
"big left": {size: 5431, loc: tmproofs.Left},
"big middle": {size: 5431, loc: tmproofs.Middle},
"big right": {size: 5431, loc: tmproofs.Right},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
tree, allkeys, err := helpers.BuildTree(tc.size)
if err != nil {
t.Fatalf("Creating tree: %+v", err)
}
key := helpers.GetNonKey(allkeys, tc.loc)
proof, err := CreateNonMembershipProof(tree, key)
if err != nil {
t.Fatalf("Creating Proof: %+v", err)
}
root := tree.WorkingHash()
valid := ics23.VerifyNonMembership(IavlSpec, root, proof, key)
if !valid {
t.Fatalf("Non Membership Proof Invalid")
}
})
}
}

View File

@ -0,0 +1,105 @@
/*
Package helpers contains functions to build sample data for tests/testgen
In it's own package to avoid poluting the godoc for ics23-iavl
*/
package helpers
import (
"bytes"
"fmt"
"sort"
"github.com/cosmos/iavl"
"github.com/tendermint/tendermint/libs/rand"
db "github.com/tendermint/tm-db"
tmproofs "github.com/cosmos/cosmos-sdk/store/internal/proofs"
)
// IavlResult is the result of one match
type IavlResult struct {
Key []byte
Value []byte
Proof *iavl.RangeProof
RootHash []byte
}
// GenerateIavlResult makes a tree of size and returns a range proof for one random element
//
// returns a range proof and the root hash of the tree
func GenerateIavlResult(size int, loc tmproofs.Where) (*IavlResult, error) {
tree, allkeys, err := BuildTree(size)
if err != nil {
return nil, err
}
key := GetKey(allkeys, loc)
value, proof, err := tree.GetWithProof(key)
if err != nil {
return nil, err
}
if value == nil {
return nil, fmt.Errorf("GetWithProof returned nil value")
}
if len(proof.Leaves) != 1 {
return nil, fmt.Errorf("GetWithProof returned %d leaves", len(proof.Leaves))
}
root := tree.WorkingHash()
res := &IavlResult{
Key: key,
Value: value,
Proof: proof,
RootHash: root,
}
return res, nil
}
// GetKey this returns a key, on Left/Right/Middle
func GetKey(allkeys [][]byte, loc tmproofs.Where) []byte {
if loc == tmproofs.Left {
return allkeys[0]
}
if loc == tmproofs.Right {
return allkeys[len(allkeys)-1]
}
// select a random index between 1 and allkeys-2
idx := rand.Int()%(len(allkeys)-2) + 1
return allkeys[idx]
}
// GetNonKey returns a missing key - Left of all, Right of all, or in the Middle
func GetNonKey(allkeys [][]byte, loc tmproofs.Where) []byte {
if loc == tmproofs.Left {
return []byte{0, 0, 0, 1}
}
if loc == tmproofs.Right {
return []byte{0xff, 0xff, 0xff, 0xff}
}
// otherwise, next to an existing key (copy before mod)
key := append([]byte{}, GetKey(allkeys, loc)...)
key[len(key)-2] = 255
key[len(key)-1] = 255
return key
}
// BuildTree creates random key/values and stores in tree
// returns a list of all keys in sorted order
func BuildTree(size int) (tree *iavl.MutableTree, keys [][]byte, err error) {
tree, err = iavl.NewMutableTree(db.NewMemDB(), 0)
// insert lots of info and store the bytes
keys = make([][]byte, size)
for i := 0; i < size; i++ {
key := rand.Str(20)
value := "value_for_" + key
tree.Set([]byte(key), []byte(value))
keys[i] = []byte(key)
}
sort.Slice(keys, func(i, j int) bool {
return bytes.Compare(keys[i], keys[j]) < 0
})
return tree, keys, nil
}

View File

@ -0,0 +1,24 @@
.PHONY: build test testgen
GENDIR ?= ./testdata
# make sure we turn on go modules
export GO111MODULE := on
build:
go build -mod=readonly ./cmd/testgen-smt
test:
go test -mod=readonly .
# Usage: GENDIR=../ics23/testdata/smt make testgen
testgen:
@mkdir -p "$(GENDIR)"
go run -mod=readonly ./cmd/testgen-smt exist left 987 > "$(GENDIR)"/exist_left.json
go run -mod=readonly ./cmd/testgen-smt exist middle 812 > "$(GENDIR)"/exist_middle.json
go run -mod=readonly ./cmd/testgen-smt exist right 1261 > "$(GENDIR)"/exist_right.json
go run -mod=readonly ./cmd/testgen-smt nonexist left 813 > "$(GENDIR)"/nonexist_left.json
go run -mod=readonly ./cmd/testgen-smt nonexist middle 691 > "$(GENDIR)"/nonexist_middle.json
go run -mod=readonly ./cmd/testgen-smt nonexist right 1535 > "$(GENDIR)"/nonexist_right.json
go run -mod=readonly ./cmd/testgen-smt batch 1801 20 0 > "$(GENDIR)"/batch_exist.json
go run -mod=readonly ./cmd/testgen-smt batch 1807 0 20 > "$(GENDIR)"/batch_nonexist.json

View File

@ -0,0 +1,39 @@
# Proofs SMT
This project demonstrates the generation and validation of ICS-23 proofs for a sparse Merkle tree (SMT) as implemented by [Celestia](https://github.com/celestiaorg/smt).
## Library usage
It exposes a two main functions :
`func CreateMembershipProof(tree *smt.SparseMerkleTree, key []byte) (*ics23.CommitmentProof, error)`
produces a CommitmentProof that the given key exists in the SMT (and contains the current value). This returns an error if the key does not exist in the tree.
`func CreateNonMembershipProof(tree *smt.SparseMerkleTree, key []byte, preimages PreimageMap) (*ics23.CommitmentProof, error)`
produces a CommitmentProof that the given key doesn't exist in the SMT. This returns an error if the key does not exist in the tree.
This relies on an auxiliary `PreimageMap` object which provides access to the preimages of all keys in the tree based on their (hashed) path ordering.
## CLI usage
We also expose a simple script to generate test data for the confio proofs package.
```shell
go install ./cmd/testgen-smt
testgen-smt exist left 10
```
Will output some json data, from a randomly generated Merkle tree each time.
```json
{
"key": "574f516c4364415274743845444d397347484937",
"proof": "0a9d010a2024910c64b5b74b6b72e6b9d3310a1d0bd599032e05e8abc43112d194e1a78f30121e76616c75655f666f725f574f516c4364415274743845444d3973474849371a07080118012a0100222708011201011a20b51557119b6985d54a48a4510e528d5f929f0b1c8b57914bb6cd8f9eab035d75222708011201011a20fff8248ca9e98cbb05c81612d38e74780b2c02d9c88ee628cfbdb8ca44769a63",
"root": "f69ef3599b7f0471b61735490636608a8ff43a327b2b5a3a5528ca7f7059ffa5",
"value": "76616c75655f666f725f574f516c4364415274743845444d397347484937"
}
```
`"root"` is the hex-encoded root hash of the Merkle tree.
`"proof"` is the hex-encoding of the protobuf binary encoding of a `proofs.ExistenceProof` object. This contains a (key, value) pair, along with all steps to reach the root hash. This provides a non-trivial test case, to ensure clients in multiple languages can verify the protobuf proofs we generate from the SMT.

View File

@ -0,0 +1,214 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
ics23 "github.com/confio/ics23/go"
tmproofs "github.com/cosmos/cosmos-sdk/store/internal/proofs"
tools "github.com/cosmos/cosmos-sdk/store/tools/ics23"
smtproofs "github.com/cosmos/cosmos-sdk/store/tools/ics23/smt"
"github.com/cosmos/cosmos-sdk/store/tools/ics23/smt/helpers"
)
/**
testgen-smt will generate a json struct on stdout (meant to be saved to file for testdata).
this will be an auto-generated existence proof in the form:
{
"root": "<hex encoded root hash of tree>",
"key": "<hex encoded key to prove>",
"value": "<hex encoded value to prove> (empty on non-existence)",
"proof": "<hex encoded protobuf marshaling of a CommitmentProof>"
}
It accepts two or three arguments (optional size: default 400)
testgen-smt [exist|nonexist] [left|right|middle] <size>
If you make a batch, we have a different format:
{
"root": "<hex encoded root hash of tree>",
"proof": "<hex encoded protobuf marshaling of a CommitmentProof (Compressed Batch)>",
"items": [{
"key": "<hex encoded key to prove>",
"value": "<hex encoded value to prove> (empty on non-existence)",
}, ...]
}
The batch variant accepts 5 arguments:
testgen-smt [batch] [size] [num exist] [num nonexist]
**/
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: testgen-smt batch [size] [# exist] [# nonexist]")
fmt.Println(" testgen-smt [exist|nonexist] [left|right|middle] <size>")
os.Exit(1)
}
if os.Args[1] == "batch" {
size, exist, nonexist, err := tools.ParseBatchArgs(os.Args[2:])
if err != nil {
fmt.Printf("%+v\n", err)
fmt.Println("Usage: testgen-smt batch [size] [# exist] [# nonexist]")
os.Exit(1)
}
err = doBatch(size, exist, nonexist)
if err != nil {
fmt.Printf("Error: %+v\n", err)
os.Exit(1)
}
return
}
exist, loc, size, err := tools.ParseArgs(os.Args)
if err != nil {
fmt.Printf("%+v\n", err)
fmt.Println("Usage: testgen-smt [exist|nonexist] [left|right|middle] <size>")
os.Exit(1)
}
err = doSingle(exist, loc, size)
if err != nil {
fmt.Printf("Error: %+v\n", err)
os.Exit(1)
}
}
func doSingle(exist bool, loc tmproofs.Where, size int) error {
tree, preim, err := helpers.BuildTree(size)
if err != nil {
return err
}
root := tree.Root()
var key, value []byte
if exist {
key = preim.GetKey(loc)
value, err = tree.Get(key)
if err != nil {
return fmt.Errorf("get key: %w", err)
}
} else {
key = preim.GetNonKey(loc)
}
var proof *ics23.CommitmentProof
if exist {
proof, err = smtproofs.CreateMembershipProof(tree, key)
} else {
proof, err = smtproofs.CreateNonMembershipProof(tree, key, preim)
}
if err != nil {
return fmt.Errorf("create proof: %w", err)
}
binary, err := proof.Marshal()
if err != nil {
return fmt.Errorf("protobuf marshal: %w", err)
}
path := sha256.Sum256(key)
res := map[string]interface{}{
"root": hex.EncodeToString(root),
"key": hex.EncodeToString(path[:]),
"value": hex.EncodeToString(value),
"proof": hex.EncodeToString(binary),
}
out, err := json.MarshalIndent(res, "", " ")
if err != nil {
return fmt.Errorf("json encoding: %w", err)
}
fmt.Println(string(out))
return nil
}
type item struct {
Key string `json:"key"`
Value string `json:"value"`
}
func pickWhere(i int) tmproofs.Where {
if i > 2 {
return tmproofs.Middle
}
return tmproofs.Where(i)
}
func doBatch(size, exist, nonexist int) error {
tree, preim, err := helpers.BuildTree(size)
if err != nil {
return err
}
root := tree.Root()
items := []item{}
proofs := []*ics23.CommitmentProof{}
for i := 0; i < exist; i++ {
where := pickWhere(i)
key := []byte(preim.GetKey(where))
value, err := tree.Get(key)
if err != nil {
return fmt.Errorf("get key: %w", err)
}
proof, err := smtproofs.CreateMembershipProof(tree, key)
if err != nil {
return fmt.Errorf("create proof: %w", err)
}
proofs = append(proofs, proof)
path := sha256.Sum256(key)
item := item{
Key: hex.EncodeToString(path[:]),
Value: hex.EncodeToString(value),
}
items = append(items, item)
}
for i := 0; i < nonexist; i++ {
where := pickWhere(i)
key := []byte(preim.GetNonKey(where))
proof, err := smtproofs.CreateNonMembershipProof(tree, key, preim)
if err != nil {
return fmt.Errorf("create proof: %w", err)
}
proofs = append(proofs, proof)
path := sha256.Sum256(key)
item := item{
Key: hex.EncodeToString(path[:]),
}
items = append(items, item)
}
// combine all proofs into batch and compress
proof, err := ics23.CombineProofs(proofs)
if err != nil {
return fmt.Errorf("combine proofs: %w", err)
}
binary, err := proof.Marshal()
if err != nil {
return fmt.Errorf("protobuf marshal: %w", err)
}
res := map[string]interface{}{
"root": hex.EncodeToString(root),
"items": items,
"proof": hex.EncodeToString(binary),
}
out, err := json.MarshalIndent(res, "", " ")
if err != nil {
return fmt.Errorf("json encoding: %w", err)
}
fmt.Println(string(out))
return nil
}

View File

@ -0,0 +1,126 @@
package smtproofs
import (
"crypto/sha256"
"fmt"
ics23 "github.com/confio/ics23/go"
"github.com/lazyledger/smt"
)
// PreimageMap represents an interface for accessing hashed tree paths and retrieving their
// corresponding preimages.
type PreimageMap interface {
// KeyFor returns the preimage (key) for given path index.
KeyFor(int) []byte
// FindPath returns the ordered index of a given path, and whether it's contained in the tree.
// If not found, the returned index is where the path would be inserted.
FindPath([32]byte) (int, bool)
// Len returns the number of mapped paths.
Len() int
}
// CreateMembershipProof will produce a CommitmentProof that the given key (and queries value) exists in the SMT.
// If the key doesn't exist in the tree, this will return an error.
func CreateMembershipProof(tree *smt.SparseMerkleTree, key []byte) (*ics23.CommitmentProof, error) {
exist, err := createExistenceProof(tree, key)
if err != nil {
return nil, err
}
proof := &ics23.CommitmentProof{
Proof: &ics23.CommitmentProof_Exist{
Exist: exist,
},
}
return proof, nil
}
func createExistenceProof(tree *smt.SparseMerkleTree, key []byte) (*ics23.ExistenceProof, error) {
has, err := tree.Has(key)
if err != nil {
return nil, err
}
if !has {
return nil, fmt.Errorf("Cannot create ExistenceProof when key not in state")
}
value, err := tree.Get(key)
if err != nil {
return nil, err
}
proof, err := tree.Prove(key)
if err != nil {
return nil, err
}
path := sha256.Sum256(key)
return &ics23.ExistenceProof{
Key: path[:],
Value: value,
Leaf: ics23.SmtSpec.LeafSpec,
Path: convertInnerOps(path[:], proof.SideNodes),
}, nil
}
// CreateNonMembershipProof will produce a CommitmentProof that the given key doesn't exist in the SMT.
// If the key exists in the tree, this will return an error.
func CreateNonMembershipProof(tree *smt.SparseMerkleTree, key []byte, preimages PreimageMap) (*ics23.CommitmentProof, error) {
path := sha256.Sum256(key)
has, err := tree.Has(key)
if err != nil {
return nil, err
}
if has {
return nil, fmt.Errorf("Cannot create NonExistenceProof when key in state")
}
nonexist := &ics23.NonExistenceProof{
Key: path[:],
}
ix, found := preimages.FindPath(path)
if found {
return nil, fmt.Errorf("Found index for key not in state")
}
if ix > 0 {
nonexist.Left, err = createExistenceProof(tree, preimages.KeyFor(ix-1))
if err != nil {
return nil, err
}
}
if ix < preimages.Len() {
nonexist.Right, err = createExistenceProof(tree, preimages.KeyFor(ix))
if err != nil {
return nil, err
}
}
return &ics23.CommitmentProof{
Proof: &ics23.CommitmentProof_Nonexist{
nonexist,
},
}, nil
}
func convertInnerOps(path []byte, sideNodes [][]byte) []*ics23.InnerOp {
depth := len(sideNodes)
inners := make([]*ics23.InnerOp, 0, depth)
for i := 0; i < len(sideNodes); i++ {
op := &ics23.InnerOp{
Hash: ics23.HashOp_SHA256,
Prefix: []byte{1},
}
if getBitAtFromMSB(path[:], depth-1-i) == 1 {
// right child is on path
op.Prefix = append(op.Prefix, sideNodes[i]...)
} else {
op.Suffix = sideNodes[i]
}
inners = append(inners, op)
}
return inners
}
// getBitAtFromMSB gets the bit at an offset from the most significant bit
// Copied from github.com/celestiaorg/smt
func getBitAtFromMSB(data []byte, position int) int {
if int(data[position/8])&(1<<(8-1-uint(position)%8)) > 0 {
return 1
}
return 0
}

View File

@ -0,0 +1,82 @@
package smtproofs
import (
"crypto/sha256"
"testing"
ics23 "github.com/confio/ics23/go"
tmproofs "github.com/cosmos/cosmos-sdk/store/internal/proofs"
"github.com/cosmos/cosmos-sdk/store/tools/ics23/smt/helpers"
)
var numKeys = 50
var cases = map[string]struct {
size int
loc tmproofs.Where
}{
"tiny left": {size: 10, loc: tmproofs.Left},
"tiny middle": {size: 10, loc: tmproofs.Middle},
"tiny right": {size: 10, loc: tmproofs.Right},
"small left": {size: 100, loc: tmproofs.Left},
"small middle": {size: 100, loc: tmproofs.Middle},
"small right": {size: 100, loc: tmproofs.Right},
"big left": {size: 5431, loc: tmproofs.Left},
"big middle": {size: 5431, loc: tmproofs.Middle},
"big right": {size: 5431, loc: tmproofs.Right},
}
func TestCreateMembership(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
tree, preim, err := helpers.BuildTree(tc.size)
if err != nil {
t.Fatalf("Creating tree: %+v", err)
}
for i := 0; i < numKeys; i++ {
key := preim.GetKey(tc.loc)
val, err := tree.Get(key)
if err != nil {
t.Fatalf("Getting key: %+v", err)
}
proof, err := CreateMembershipProof(tree, key)
if err != nil {
t.Fatalf("Creating proof: %+v", err)
}
root := tree.Root()
path := sha256.Sum256(key)
valid := ics23.VerifyMembership(ics23.SmtSpec, root, proof, path[:], val)
if !valid {
t.Fatalf("Membership proof invalid")
}
}
})
}
}
func TestCreateNonMembership(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
tree, preim, err := helpers.BuildTree(tc.size)
if err != nil {
t.Fatalf("Creating tree: %+v", err)
}
for i := 0; i < numKeys; i++ {
key := preim.GetNonKey(tc.loc)
proof, err := CreateNonMembershipProof(tree, key, preim)
if err != nil {
t.Fatalf("Creating proof: %+v", err)
}
root := tree.Root()
path := sha256.Sum256(key)
valid := ics23.VerifyNonMembership(ics23.SmtSpec, root, proof, path[:])
if !valid {
t.Fatalf("Non-membership proof invalid")
}
}
})
}
}

View File

@ -0,0 +1,131 @@
/*
Package helpers contains functions to build sample data for tests/testgen
In it's own package to avoid poluting the godoc for ics23-smt
*/
package helpers
import (
"bytes"
"crypto/sha256"
"math/rand"
"sort"
"github.com/lazyledger/smt"
tmproofs "github.com/cosmos/cosmos-sdk/store/internal/proofs"
)
// PreimageMap maps each tree path back to its preimage
// needed because SparseMerkleTree methods take preimage as arg and hash internally
type PreimageMap struct {
paths []preimageMapping
keys [][]byte
// known non-keys at left and rightmost positions
nonKeys []preimageMapping
}
type preimageMapping struct {
path [32]byte
keyIdx int // index of preimage in keys list
}
// BuildTree creates random key/values and stores in tree
// returns a list of all keys in sorted order
func BuildTree(size int) (*smt.SparseMerkleTree, *PreimageMap, error) {
nodes, values := smt.NewSimpleMap(), smt.NewSimpleMap()
tree := smt.NewSparseMerkleTree(nodes, values, sha256.New())
// insert lots of info and store the bytes
keys := make([][]byte, size+2)
for i := 0; i < len(keys); i++ {
key := randStr(20)
value := "value_for_" + key
_, err := tree.Update([]byte(key), []byte(value))
if err != nil {
return nil, nil, err
}
keys[i] = []byte(key)
}
var paths []preimageMapping
for i, key := range keys {
paths = append(paths, preimageMapping{sha256.Sum256(key), i})
}
sort.Slice(paths, func(i, j int) bool {
return bytes.Compare(paths[i].path[:], paths[j].path[:]) < 0
})
// now, find the edge paths and remove them from the tree
leftmost, rightmost := paths[0], paths[len(paths)-1]
_, err := tree.Delete(keys[leftmost.keyIdx])
if err != nil {
return nil, nil, err
}
_, err = tree.Delete(keys[rightmost.keyIdx])
if err != nil {
return nil, nil, err
}
pim := PreimageMap{
keys: keys,
paths: paths[1 : len(paths)-1],
nonKeys: []preimageMapping{leftmost, rightmost},
}
return tree, &pim, nil
}
// FindPath returns the closest index to path in paths, and whether it's a match.
// If not found, the returned index is where the path would be.
func (pim PreimageMap) FindPath(path [32]byte) (int, bool) {
var mid int
from, to := 0, len(pim.paths)-1
for from <= to {
mid = (from + to) / 2
switch bytes.Compare(pim.paths[mid].path[:], path[:]) {
case -1:
from = mid + 1
case 1:
to = mid - 1
default:
return mid, true
}
}
return from, false
}
// Len returns the number of mapped paths.
func (pim PreimageMap) Len() int { return len(pim.paths) }
// KeyFor returns the preimage (key) for given path index.
func (pim PreimageMap) KeyFor(pathIx int) []byte {
return pim.keys[pim.paths[pathIx].keyIdx]
}
// GetKey this returns a key, on Left/Right/Middle
func (pim PreimageMap) GetKey(loc tmproofs.Where) []byte {
if loc == tmproofs.Left {
return pim.KeyFor(0)
}
if loc == tmproofs.Right {
return pim.KeyFor(len(pim.paths) - 1)
}
// select a random index between 1 and len-2
idx := rand.Int()%(len(pim.paths)-2) + 1
return pim.KeyFor(idx)
}
// GetNonKey returns a missing key - Left of all, Right of all, or in the Middle
func (pim PreimageMap) GetNonKey(loc tmproofs.Where) []byte {
if loc == tmproofs.Left {
return pim.keys[pim.nonKeys[0].keyIdx]
}
if loc == tmproofs.Right {
return pim.keys[pim.nonKeys[1].keyIdx]
}
// otherwise, next to an existing key (copy before mod)
key := append([]byte{}, pim.GetKey(tmproofs.Middle)...)
key[len(key)-2] = 255
key[len(key)-1] = 255
return key
}

View File

@ -0,0 +1,34 @@
package helpers
import (
crand "crypto/rand"
"encoding/binary"
"fmt"
rand "math/rand"
)
const (
strChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
)
func init() {
rand.Seed(42)
// rand.Seed(crandSeed())
}
func randStr(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = strChars[rand.Intn(len(strChars))]
}
return string(b)
}
func crandSeed() int64 {
var seed int64
err := binary.Read(crand.Reader, binary.BigEndian, &seed)
if err != nil {
panic(fmt.Sprintf("could not read random seed from crypto/rand: %v", err))
}
return seed
}

View File

@ -0,0 +1,24 @@
.PHONY: build test testgen
GENDIR ?= ./testdata
# make sure we turn on go modules
export GO111MODULE := on
build:
go build -mod=readonly ./cmd/testgen-simple
test:
go test -mod=readonly .
testgen:
# Usage: GENDIR=../ics23/testdata/tendermint make testgen
@mkdir -p "$(GENDIR)"
go run -mod=readonly ./cmd/testgen-simple exist left 987 > "$(GENDIR)"/exist_left.json
go run -mod=readonly ./cmd/testgen-simple exist middle 812 > "$(GENDIR)"/exist_middle.json
go run -mod=readonly ./cmd/testgen-simple exist right 1261 > "$(GENDIR)"/exist_right.json
go run -mod=readonly ./cmd/testgen-simple nonexist left 813 > "$(GENDIR)"/nonexist_left.json
go run -mod=readonly ./cmd/testgen-simple nonexist middle 691 > "$(GENDIR)"/nonexist_middle.json
go run -mod=readonly ./cmd/testgen-simple nonexist right 1535 > "$(GENDIR)"/nonexist_right.json
go run -mod=readonly ./cmd/testgen-simple batch 1801 20 0 > "$(GENDIR)"/batch_exist.json
go run -mod=readonly ./cmd/testgen-simple batch 1807 0 20 > "$(GENDIR)"/batch_nonexist.json

View File

@ -0,0 +1,39 @@
# Proofs Tendermint
This is an adapter library to convert the `SimpleProof` from
[tendermint/crypto/merkle](https://github.com/tendermint/tendermint/tree/master/crypto/merkle)
into the standard confio/proofs format.
As non-existence proofs depend on ordered keys, and all proofs require the key-value pair
to be encoded in a predictable format in the leaves, we will only support proofs generated
from `SimpleProofsFromMap`, which handles the key-value pairs for leafs in a standard format.
## Library usage
It exposes a top-level function `func ConvertSimpleProof(p *merkle.SimpleProof, key, value []byte) (*proofs.ExistenceProof, error)`
that can convert from `merkle.SimpleProof` with the KVPair encoding for leafs, into confio/proof protobuf objects.
It currently only works for existence proofs. We plan to soon support non-existence proofs.
## CLI usage
We also expose a simple script to generate test data for the confio proofs package.
```shell
make testgen
```
Will output some json data, from a randomly generated merkle tree each time.
```json
{
"existence": "0a146f65436a684273735a34567543774b567a435963121e76616c75655f666f725f6f65436a684273735a34567543774b567a4359631a0d0a0b0801180120012a030002021a2d122b08011204020402201a2120d307032505383dee34ea9eadf7649c31d1ce294b6d62b273d804da478ac161da1a2d122b08011204040802201a2120306b7d51213bd93bac17c5ee3d727ec666300370b19fd55cc13d7341dc589a991a2b12290801122508160220857103d59863ac55d1f34008a681f837c01975a223c0f54883a05a446d49c7c6201a2b1229080112250a2202204498eb5c93e40934bc8bad9626f19e333c1c0be4541b9098f139585c3471bae2201a2d122b080112040e6c02201a212022648db12dbf830485cc41435ecfe37bcac26c6c305ac4304f649977ddc339d51a2c122a0801122610c60102204e0b7996a7104f5b1ac1a2caa0704c4b63f60112e0e13763b2ba03f40a54e845201a2c122a08011226129003022017858e28e0563f7252eaca19acfc1c3828c892e635f76f971b3fbdc9bbd2742e20",
"root": "cea07656c77e8655521f4c904730cf4649242b8e482be786b2b220a15150d5f0"
}
```
`"root"` is the hex-encoded root hash of the merkle tree
`"existence"` is the hex-encoding of the protobuf binary encoding of a `proofs.ExistenceProof` object. This contains a (key, value) pair,
along with all steps to reach the root hash. This provides a non-trivial test case, to ensure client in multiple languages can verify the
protobuf proofs we generate from the iavl tree

View File

@ -0,0 +1,241 @@
package main
import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strconv"
tmproofs "github.com/cosmos/cosmos-sdk/store/internal/proofs"
ics23 "github.com/confio/ics23/go"
)
/**
testgen-simple will generate a json struct on stdout (meant to be saved to file for testdata).
this will be an auto-generated existence proof in the form:
{
"root": "<hex encoded root hash of tree>",
"key": "<hex encoded key to prove>",
"value": "<hex encoded value to prove> (empty on non-existence)",
"proof": "<hex encoded protobuf marshaling of a CommitmentProof>"
}
It accepts two or three arguments (optional size: default 400)
testgen-simple [exist|nonexist] [left|right|middle] <size>
If you make a batch, we have a different format:
{
"root": "<hex encoded root hash of tree>",
"proof": "<hex encoded protobuf marshaling of a CommitmentProof (Compressed Batch)>",
"items": [{
"key": "<hex encoded key to prove>",
"value": "<hex encoded value to prove> (empty on non-existence)",
}, ...]
}
The batch variant accepts 5 arguments:
testgen-simple [batch] [size] [num exist] [num nonexist]
**/
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: testgen-simple batch [size] [# exist] [# nonexist]")
fmt.Println(" testgen-simple [exist|nonexist] [left|right|middle] <size>")
os.Exit(1)
}
if os.Args[1] == "batch" {
err := doBatch(os.Args[2:])
if err != nil {
fmt.Printf("%+v\n", err)
fmt.Println("Usage: testgen-simple [batch] [size] [# exist] [# nonexist]")
os.Exit(1)
}
return
}
exist, loc, size, err := parseArgs(os.Args)
if err != nil {
fmt.Printf("%+v\n", err)
fmt.Println("Usage: testgen-simple [exist|nonexist] [left|right|middle] <size>")
os.Exit(1)
}
data := tmproofs.BuildMap(size)
allkeys := tmproofs.SortedKeys(data)
root := tmproofs.CalcRoot(data)
var key, value []byte
if exist {
key = []byte(tmproofs.GetKey(allkeys, loc))
value = data[string(key)]
} else {
key = []byte(tmproofs.GetNonKey(allkeys, loc))
}
var proof *ics23.CommitmentProof
if exist {
proof, err = tmproofs.CreateMembershipProof(data, key)
} else {
proof, err = tmproofs.CreateNonMembershipProof(data, key)
}
if err != nil {
fmt.Printf("Error: create proof: %+v\n", err)
os.Exit(1)
}
binary, err := proof.Marshal()
if err != nil {
fmt.Printf("Error: protobuf marshal: %+v\n", err)
os.Exit(1)
}
res := map[string]interface{}{
"root": hex.EncodeToString(root),
"key": hex.EncodeToString(key),
"value": hex.EncodeToString(value),
"proof": hex.EncodeToString(binary),
}
out, err := json.MarshalIndent(res, "", " ")
if err != nil {
fmt.Printf("Error: json encoding: %+v\n", err)
os.Exit(1)
}
fmt.Println(string(out))
}
func parseArgs(args []string) (exist bool, loc tmproofs.Where, size int, err error) {
if len(args) != 3 && len(args) != 4 {
err = fmt.Errorf("Insufficient args")
return
}
switch args[1] {
case "exist":
exist = true
case "nonexist":
exist = false
default:
err = fmt.Errorf("Invalid arg: %s", args[1])
return
}
switch args[2] {
case "left":
loc = tmproofs.Left
case "middle":
loc = tmproofs.Middle
case "right":
loc = tmproofs.Right
default:
err = fmt.Errorf("Invalid arg: %s", args[2])
return
}
size = 400
if len(args) == 4 {
size, err = strconv.Atoi(args[3])
}
return
}
type item struct {
Key string `json:"key"`
Value string `json:"value"`
}
func doBatch(args []string) error {
size, exist, nonexist, err := parseBatchArgs(args)
if err != nil {
return err
}
data := tmproofs.BuildMap(size)
allkeys := tmproofs.SortedKeys(data)
root := tmproofs.CalcRoot(data)
items := []item{}
proofs := []*ics23.CommitmentProof{}
for i := 0; i < exist; i++ {
key := []byte(tmproofs.GetKey(allkeys, tmproofs.Middle))
value := data[string(key)]
proof, err := tmproofs.CreateMembershipProof(data, key)
if err != nil {
return fmt.Errorf("create proof: %+v", err)
}
proofs = append(proofs, proof)
item := item{
Key: hex.EncodeToString(key),
Value: hex.EncodeToString(value),
}
items = append(items, item)
}
for i := 0; i < nonexist; i++ {
key := []byte(tmproofs.GetNonKey(allkeys, tmproofs.Middle))
proof, err := tmproofs.CreateNonMembershipProof(data, key)
if err != nil {
return fmt.Errorf("create proof: %+v", err)
}
proofs = append(proofs, proof)
item := item{
Key: hex.EncodeToString(key),
}
items = append(items, item)
}
// combine all proofs into batch and compress
proof, err := ics23.CombineProofs(proofs)
if err != nil {
fmt.Printf("Error: combine proofs: %+v\n", err)
os.Exit(1)
}
binary, err := proof.Marshal()
if err != nil {
fmt.Printf("Error: protobuf marshal: %+v\n", err)
os.Exit(1)
}
res := map[string]interface{}{
"root": hex.EncodeToString(root),
"items": items,
"proof": hex.EncodeToString(binary),
}
out, err := json.MarshalIndent(res, "", " ")
if err != nil {
fmt.Printf("Error: json encoding: %+v\n", err)
os.Exit(1)
}
fmt.Println(string(out))
return nil
}
func parseBatchArgs(args []string) (size int, exist int, nonexist int, err error) {
if len(args) != 3 {
err = fmt.Errorf("Insufficient args")
return
}
size, err = strconv.Atoi(args[0])
if err != nil {
return
}
exist, err = strconv.Atoi(args[1])
if err != nil {
return
}
nonexist, err = strconv.Atoi(args[2])
return
}