Merge pull request #218 from tendermint/feature/client-proofs
Support non-existence proof
This commit is contained in:
commit
9c7b1e023f
21
TODO.md
21
TODO.md
|
@ -1,21 +0,0 @@
|
||||||
Alexis:
|
|
||||||
|
|
||||||
* merkle - proof (non-existence - maybe range)
|
|
||||||
* intro to light-client and proofs
|
|
||||||
light-client proofs:
|
|
||||||
* make this sensible -> very tied to merkle proofs and API
|
|
||||||
* support new proof types
|
|
||||||
|
|
||||||
* expose more proof types in basecoin.Query
|
|
||||||
|
|
||||||
|
|
||||||
* merkle - api cleanup (also Bonsai)
|
|
||||||
* later: C bindings (to Bonsai?)
|
|
||||||
|
|
||||||
|
|
||||||
* crypto-ledger (while ethan gone)
|
|
||||||
|
|
||||||
light-client provider:
|
|
||||||
* caching checkpoint on Verify
|
|
||||||
* cleanup (trim old node)
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
wire "github.com/tendermint/go-wire"
|
wire "github.com/tendermint/go-wire"
|
||||||
"github.com/tendermint/go-wire/data"
|
"github.com/tendermint/go-wire/data"
|
||||||
lc "github.com/tendermint/light-client"
|
lc "github.com/tendermint/light-client"
|
||||||
|
"github.com/tendermint/light-client/certifiers"
|
||||||
"github.com/tendermint/light-client/proofs"
|
"github.com/tendermint/light-client/proofs"
|
||||||
"github.com/tendermint/merkleeyes/iavl"
|
"github.com/tendermint/merkleeyes/iavl"
|
||||||
"github.com/tendermint/tendermint/rpc/client"
|
"github.com/tendermint/tendermint/rpc/client"
|
||||||
|
@ -51,65 +52,93 @@ func Get(key []byte, prove bool) (data.Bytes, uint64, error) {
|
||||||
resp, err := node.ABCIQuery("/key", key, false)
|
resp, err := node.ABCIQuery("/key", key, false)
|
||||||
return data.Bytes(resp.Value), resp.Height, err
|
return data.Bytes(resp.Value), resp.Height, err
|
||||||
}
|
}
|
||||||
val, h, _, err := GetWithProof(key)
|
val, h, _, _, err := GetWithProof(key)
|
||||||
return val, h, err
|
return val, h, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWithProof returns the values stored under a given key at the named
|
// GetWithProof returns the values stored under a given key at the named
|
||||||
// height as in Get. Additionally, it will return a validated merkle
|
// height as in Get. Additionally, it will return a validated merkle
|
||||||
// proof for the key-value pair if it exists, and all checks pass.
|
// proof for the key-value pair if it exists, and all checks pass.
|
||||||
func GetWithProof(key []byte) (data.Bytes, uint64, *iavl.KeyExistsProof, error) {
|
func GetWithProof(key []byte) (data.Bytes, uint64,
|
||||||
node := commands.GetNode()
|
*iavl.KeyExistsProof, *iavl.KeyNotExistsProof, error) {
|
||||||
|
|
||||||
resp, err := node.ABCIQuery("/key", key, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, nil, err
|
|
||||||
}
|
|
||||||
ph := int(resp.Height)
|
|
||||||
|
|
||||||
// make sure the proof is the proper height
|
|
||||||
if !resp.Code.IsOK() {
|
|
||||||
return nil, 0, nil, errors.Errorf("Query error %d: %s", resp.Code, resp.Code.String())
|
|
||||||
}
|
|
||||||
// TODO: Handle null proofs
|
|
||||||
if len(resp.Key) == 0 || len(resp.Value) == 0 || len(resp.Proof) == 0 {
|
|
||||||
return nil, 0, nil, lc.ErrNoData()
|
|
||||||
}
|
|
||||||
if ph != 0 && ph != int(resp.Height) {
|
|
||||||
return nil, 0, nil, lc.ErrHeightMismatch(ph, int(resp.Height))
|
|
||||||
}
|
|
||||||
|
|
||||||
check, err := GetCertifiedCheckpoint(ph)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
proof := new(iavl.KeyExistsProof)
|
|
||||||
err = wire.ReadBinaryBytes(resp.Proof, &proof)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate the proof against the certified header to ensure data integrity
|
|
||||||
err = proof.Verify(resp.Key, resp.Value, check.Header.AppHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.Bytes(resp.Value), resp.Height, proof, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCertifiedCheckpoint gets the signed header for a given height
|
|
||||||
// and certifies it. Returns error if unable to get a proven header.
|
|
||||||
func GetCertifiedCheckpoint(h int) (empty lc.Checkpoint, err error) {
|
|
||||||
// here is the certifier, root of all trust
|
|
||||||
node := commands.GetNode()
|
node := commands.GetNode()
|
||||||
cert, err := commands.GetCertifier()
|
cert, err := commands.GetCertifier()
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, nil, nil, err
|
||||||
|
}
|
||||||
|
return getWithProof(key, node, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWithProof(key []byte, node client.Client, cert certifiers.Certifier) (
|
||||||
|
val data.Bytes, height uint64, eproof *iavl.KeyExistsProof, neproof *iavl.KeyNotExistsProof, err error) {
|
||||||
|
|
||||||
|
resp, err := node.ABCIQuery("/key", key, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the checkpoint for this height
|
// make sure the proof is the proper height
|
||||||
|
if !resp.Code.IsOK() {
|
||||||
|
err = errors.Errorf("Query error %d: %s", resp.Code, resp.Code.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(resp.Key) == 0 || len(resp.Proof) == 0 {
|
||||||
|
err = lc.ErrNoData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.Height == 0 {
|
||||||
|
err = errors.New("Height returned is zero")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
check, err := getCertifiedCheckpoint(int(resp.Height), node, cert)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Value) > 0 {
|
||||||
|
// The key was found, construct a proof of existence.
|
||||||
|
eproof = new(iavl.KeyExistsProof)
|
||||||
|
err = wire.ReadBinaryBytes(resp.Proof, &eproof)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "Error reading proof")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the proof against the certified header to ensure data integrity.
|
||||||
|
err = eproof.Verify(resp.Key, resp.Value, check.Header.AppHash)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "Couldn't verify proof")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val = data.Bytes(resp.Value)
|
||||||
|
} else {
|
||||||
|
// The key wasn't found, construct a proof of non-existence.
|
||||||
|
neproof = new(iavl.KeyNotExistsProof)
|
||||||
|
err = wire.ReadBinaryBytes(resp.Proof, &neproof)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "Error reading proof")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the proof against the certified header to ensure data integrity.
|
||||||
|
err = neproof.Verify(resp.Key, check.Header.AppHash)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "Couldn't verify proof")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = lc.ErrNoData()
|
||||||
|
}
|
||||||
|
|
||||||
|
height = resp.Height
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCertifiedCheckpoint gets the signed header for a given height
|
||||||
|
// and certifies it. Returns error if unable to get a proven header.
|
||||||
|
func getCertifiedCheckpoint(h int, node client.Client,
|
||||||
|
cert certifiers.Certifier) (empty lc.Checkpoint, err error) {
|
||||||
|
|
||||||
// FIXME: cannot use cert.GetByHeight for now, as it also requires
|
// FIXME: cannot use cert.GetByHeight for now, as it also requires
|
||||||
// Validators and will fail on querying tendermint for non-current height.
|
// Validators and will fail on querying tendermint for non-current height.
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/tendermint/go-wire"
|
||||||
|
lc "github.com/tendermint/light-client"
|
||||||
|
"github.com/tendermint/light-client/certifiers"
|
||||||
|
certclient "github.com/tendermint/light-client/certifiers/client"
|
||||||
|
nm "github.com/tendermint/tendermint/node"
|
||||||
|
"github.com/tendermint/tendermint/rpc/client"
|
||||||
|
rpctest "github.com/tendermint/tendermint/rpc/test"
|
||||||
|
"github.com/tendermint/tendermint/types"
|
||||||
|
"github.com/tendermint/tmlibs/log"
|
||||||
|
|
||||||
|
"github.com/tendermint/basecoin/app"
|
||||||
|
"github.com/tendermint/basecoin/modules/eyes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var node *nm.Node
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
logger := log.TestingLogger()
|
||||||
|
store, err := app.NewStore("", 0, logger)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
app := app.NewBasecoin(eyes.NewHandler(), store, logger)
|
||||||
|
node = rpctest.StartTendermint(app)
|
||||||
|
|
||||||
|
code := m.Run()
|
||||||
|
|
||||||
|
node.Stop()
|
||||||
|
node.Wait()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppProofs(t *testing.T) {
|
||||||
|
assert, require := assert.New(t), require.New(t)
|
||||||
|
|
||||||
|
cl := client.NewLocal(node)
|
||||||
|
client.WaitForHeight(cl, 1, nil)
|
||||||
|
|
||||||
|
k := []byte("my-key")
|
||||||
|
v := []byte("my-value")
|
||||||
|
|
||||||
|
tx := eyes.SetTx{Key: k, Value: v}.Wrap()
|
||||||
|
btx := wire.BinaryBytes(tx)
|
||||||
|
br, err := cl.BroadcastTxCommit(btx)
|
||||||
|
require.NoError(err, "%+v", err)
|
||||||
|
require.EqualValues(0, br.CheckTx.Code, "%#v", br.CheckTx)
|
||||||
|
require.EqualValues(0, br.DeliverTx.Code)
|
||||||
|
|
||||||
|
// This sets up our trust on the node based on some past point.
|
||||||
|
source := certclient.New(cl)
|
||||||
|
seed, err := source.GetByHeight(br.Height - 2)
|
||||||
|
require.NoError(err, "%+v", err)
|
||||||
|
cert := certifiers.NewStatic("my-chain", seed.Validators)
|
||||||
|
|
||||||
|
latest, err := source.GetLatestCommit()
|
||||||
|
require.NoError(err, "%+v", err)
|
||||||
|
rootHash := latest.Header.AppHash
|
||||||
|
|
||||||
|
// Test existing key.
|
||||||
|
var data eyes.Data
|
||||||
|
|
||||||
|
bs, height, proofExists, _, err := getWithProof(k, cl, cert)
|
||||||
|
require.NoError(err, "%+v", err)
|
||||||
|
require.NotNil(proofExists)
|
||||||
|
require.True(height >= uint64(latest.Header.Height))
|
||||||
|
|
||||||
|
err = wire.ReadBinaryBytes(bs, &data)
|
||||||
|
require.NoError(err, "%+v", err)
|
||||||
|
assert.EqualValues(v, data.Value)
|
||||||
|
err = proofExists.Verify(k, bs, rootHash)
|
||||||
|
assert.NoError(err, "%+v", err)
|
||||||
|
|
||||||
|
// Test non-existing key.
|
||||||
|
missing := []byte("my-missing-key")
|
||||||
|
bs, _, proofExists, proofNotExists, err := getWithProof(missing, cl, cert)
|
||||||
|
require.True(lc.IsNoDataErr(err))
|
||||||
|
require.Nil(bs)
|
||||||
|
require.Nil(proofExists)
|
||||||
|
require.NotNil(proofNotExists)
|
||||||
|
err = proofNotExists.Verify(missing, rootHash)
|
||||||
|
assert.NoError(err, "%+v", err)
|
||||||
|
err = proofNotExists.Verify(k, rootHash)
|
||||||
|
assert.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxProofs(t *testing.T) {
|
||||||
|
assert, require := assert.New(t), require.New(t)
|
||||||
|
|
||||||
|
cl := client.NewLocal(node)
|
||||||
|
client.WaitForHeight(cl, 1, nil)
|
||||||
|
|
||||||
|
tx := eyes.SetTx{Key: []byte("key-a"), Value: []byte("value-a")}.Wrap()
|
||||||
|
|
||||||
|
btx := types.Tx(wire.BinaryBytes(tx))
|
||||||
|
br, err := cl.BroadcastTxCommit(btx)
|
||||||
|
require.NoError(err, "%+v", err)
|
||||||
|
require.EqualValues(0, br.CheckTx.Code, "%#v", br.CheckTx)
|
||||||
|
require.EqualValues(0, br.DeliverTx.Code)
|
||||||
|
|
||||||
|
source := certclient.New(cl)
|
||||||
|
seed, err := source.GetByHeight(br.Height - 2)
|
||||||
|
require.NoError(err, "%+v", err)
|
||||||
|
cert := certifiers.NewStatic("my-chain", seed.Validators)
|
||||||
|
|
||||||
|
// First let's make sure a bogus transaction hash returns a valid non-existence proof.
|
||||||
|
key := types.Tx([]byte("bogus")).Hash()
|
||||||
|
bs, _, proofExists, proofNotExists, err := getWithProof(key, cl, cert)
|
||||||
|
assert.Nil(bs, "value should be nil")
|
||||||
|
require.True(lc.IsNoDataErr(err), "error should signal 'no data'")
|
||||||
|
assert.Nil(proofExists, "existence proof should be nil")
|
||||||
|
require.NotNil(proofNotExists, "non-existence proof shouldn't be nil")
|
||||||
|
err = proofNotExists.Verify(key, proofNotExists.RootHash)
|
||||||
|
require.NoError(err, "%+v", err)
|
||||||
|
|
||||||
|
// Now let's check with the real tx hash.
|
||||||
|
key = btx.Hash()
|
||||||
|
res, err := cl.Tx(key, true)
|
||||||
|
require.NoError(err, "%+v", err)
|
||||||
|
require.NotNil(res)
|
||||||
|
err = res.Proof.Validate(key)
|
||||||
|
assert.NoError(err, "%+v", err)
|
||||||
|
}
|
|
@ -46,7 +46,12 @@ func txQueryCmd(cmd *cobra.Command, args []string) error {
|
||||||
return showTx(res.Height, res.Tx)
|
return showTx(res.Height, res.Tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
check, err := GetCertifiedCheckpoint(res.Height)
|
cert, err := commands.GetCertifier()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
check, err := getCertifiedCheckpoint(res.Height, node, cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
hash: 2848c30b31fb205f846dd7dfca14ebed8a3249cbc5aaa759066b2bab3e4bbf42
|
hash: 246a02006fc46d91294fba71971c51477241b0ace2989df04d728ae6d09f1013
|
||||||
updated: 2017-08-04T15:38:45.048261895+02:00
|
updated: 2017-08-09T17:20:03.756445376+02:00
|
||||||
imports:
|
imports:
|
||||||
- name: github.com/bgentry/speakeasy
|
- name: github.com/bgentry/speakeasy
|
||||||
version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd
|
version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd
|
||||||
|
@ -135,14 +135,14 @@ imports:
|
||||||
- data
|
- data
|
||||||
- data/base58
|
- data/base58
|
||||||
- name: github.com/tendermint/light-client
|
- name: github.com/tendermint/light-client
|
||||||
version: fcf4e411583135a1900157b8b0274c41e20ea3a1
|
version: b2afece9635d11e77dd404019b9cf3885d34f4e5
|
||||||
subpackages:
|
subpackages:
|
||||||
- certifiers
|
- certifiers
|
||||||
- certifiers/client
|
- certifiers/client
|
||||||
- certifiers/files
|
- certifiers/files
|
||||||
- proofs
|
- proofs
|
||||||
- name: github.com/tendermint/merkleeyes
|
- name: github.com/tendermint/merkleeyes
|
||||||
version: 44c4c64c731db5be4261ff3971b01b7e19729419
|
version: 25b700b87a45619cb6bd85330ab4a81b7b7fbb0d
|
||||||
subpackages:
|
subpackages:
|
||||||
- client
|
- client
|
||||||
- iavl
|
- iavl
|
||||||
|
@ -166,6 +166,7 @@ imports:
|
||||||
- rpc/lib/client
|
- rpc/lib/client
|
||||||
- rpc/lib/server
|
- rpc/lib/server
|
||||||
- rpc/lib/types
|
- rpc/lib/types
|
||||||
|
- rpc/test
|
||||||
- state
|
- state
|
||||||
- state/txindex
|
- state/txindex
|
||||||
- state/txindex/kv
|
- state/txindex/kv
|
||||||
|
|
|
@ -29,7 +29,7 @@ import:
|
||||||
- certifiers/client
|
- certifiers/client
|
||||||
- certifiers/files
|
- certifiers/files
|
||||||
- package: github.com/tendermint/merkleeyes
|
- package: github.com/tendermint/merkleeyes
|
||||||
version: unstable
|
version: origin/unstable
|
||||||
subpackages:
|
subpackages:
|
||||||
- client
|
- client
|
||||||
- iavl
|
- iavl
|
||||||
|
|
|
@ -197,7 +197,7 @@ func packetQueryCmd(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// output queue, create a post packet
|
// output queue, create a post packet
|
||||||
bs, height, proof, err := query.GetWithProof(key)
|
bs, height, proof, _, err := query.GetWithProof(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ type Bonsai struct {
|
||||||
Tree *iavl.IAVLTree
|
Tree *iavl.IAVLTree
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bonsai) String() string {
|
||||||
|
return "Bonsai{" + b.Tree.String() + "}"
|
||||||
|
}
|
||||||
|
|
||||||
var _ SimpleDB = &Bonsai{}
|
var _ SimpleDB = &Bonsai{}
|
||||||
|
|
||||||
// NewBonsai wraps a merkle tree and tags it to track children
|
// NewBonsai wraps a merkle tree and tags it to track children
|
||||||
|
|
Loading…
Reference in New Issue