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:
parent
81cfc6cc85
commit
56af6a7fbb
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue