diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 9a215a0785..0000000000 --- a/TODO.md +++ /dev/null @@ -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) - diff --git a/client/commands/query/get.go b/client/commands/query/get.go index 91831892d9..574d679bad 100644 --- a/client/commands/query/get.go +++ b/client/commands/query/get.go @@ -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. diff --git a/client/commands/query/query_test.go b/client/commands/query/query_test.go new file mode 100644 index 0000000000..23ec06f424 --- /dev/null +++ b/client/commands/query/query_test.go @@ -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) +} diff --git a/client/commands/query/tx.go b/client/commands/query/tx.go index 873deb8666..7b108b950a 100644 --- a/client/commands/query/tx.go +++ b/client/commands/query/tx.go @@ -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 } diff --git a/glide.lock b/glide.lock index 5d0c74a713..97fee36fd8 100644 --- a/glide.lock +++ b/glide.lock @@ -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 diff --git a/glide.yaml b/glide.yaml index e10ba05f98..9fde2472a1 100644 --- a/glide.yaml +++ b/glide.yaml @@ -29,7 +29,7 @@ import: - certifiers/client - certifiers/files - package: github.com/tendermint/merkleeyes - version: unstable + version: origin/unstable subpackages: - client - iavl diff --git a/modules/ibc/commands/query.go b/modules/ibc/commands/query.go index b320da81ce..e0e3c564c7 100644 --- a/modules/ibc/commands/query.go +++ b/modules/ibc/commands/query.go @@ -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 } diff --git a/state/bonsai.go b/state/bonsai.go index 7eb98568a9..eae0f589fb 100644 --- a/state/bonsai.go +++ b/state/bonsai.go @@ -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