Merge pull request #218 from tendermint/feature/client-proofs

Support non-existence proof
This commit is contained in:
Alexis Sellier 2017-08-18 13:06:08 +02:00 committed by GitHub
commit 9c7b1e023f
8 changed files with 223 additions and 74 deletions

21
TODO.md
View File

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

View File

@ -11,6 +11,7 @@ import (
wire "github.com/tendermint/go-wire"
"github.com/tendermint/go-wire/data"
lc "github.com/tendermint/light-client"
"github.com/tendermint/light-client/certifiers"
"github.com/tendermint/light-client/proofs"
"github.com/tendermint/merkleeyes/iavl"
"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)
return data.Bytes(resp.Value), resp.Height, err
}
val, h, _, err := GetWithProof(key)
val, h, _, _, err := GetWithProof(key)
return val, h, err
}
// GetWithProof returns the values stored under a given key at the named
// height as in Get. Additionally, it will return a validated merkle
// proof for the key-value pair if it exists, and all checks pass.
func GetWithProof(key []byte) (data.Bytes, uint64, *iavl.KeyExistsProof, error) {
node := commands.GetNode()
func GetWithProof(key []byte) (data.Bytes, uint64,
*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()
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 {
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
// Validators and will fail on querying tendermint for non-current height.

View File

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

View File

@ -46,7 +46,12 @@ func txQueryCmd(cmd *cobra.Command, args []string) error {
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 {
return err
}

9
glide.lock generated
View File

@ -1,5 +1,5 @@
hash: 2848c30b31fb205f846dd7dfca14ebed8a3249cbc5aaa759066b2bab3e4bbf42
updated: 2017-08-04T15:38:45.048261895+02:00
hash: 246a02006fc46d91294fba71971c51477241b0ace2989df04d728ae6d09f1013
updated: 2017-08-09T17:20:03.756445376+02:00
imports:
- name: github.com/bgentry/speakeasy
version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd
@ -135,14 +135,14 @@ imports:
- data
- data/base58
- name: github.com/tendermint/light-client
version: fcf4e411583135a1900157b8b0274c41e20ea3a1
version: b2afece9635d11e77dd404019b9cf3885d34f4e5
subpackages:
- certifiers
- certifiers/client
- certifiers/files
- proofs
- name: github.com/tendermint/merkleeyes
version: 44c4c64c731db5be4261ff3971b01b7e19729419
version: 25b700b87a45619cb6bd85330ab4a81b7b7fbb0d
subpackages:
- client
- iavl
@ -166,6 +166,7 @@ imports:
- rpc/lib/client
- rpc/lib/server
- rpc/lib/types
- rpc/test
- state
- state/txindex
- state/txindex/kv

View File

@ -29,7 +29,7 @@ import:
- certifiers/client
- certifiers/files
- package: github.com/tendermint/merkleeyes
version: unstable
version: origin/unstable
subpackages:
- client
- iavl

View File

@ -197,7 +197,7 @@ func packetQueryCmd(cmd *cobra.Command, args []string) error {
}
// output queue, create a post packet
bs, height, proof, err := query.GetWithProof(key)
bs, height, proof, _, err := query.GetWithProof(key)
if err != nil {
return err
}

View File

@ -15,6 +15,10 @@ type Bonsai struct {
Tree *iavl.IAVLTree
}
func (b *Bonsai) String() string {
return "Bonsai{" + b.Tree.String() + "}"
}
var _ SimpleDB = &Bonsai{}
// NewBonsai wraps a merkle tree and tags it to track children