-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Switch IAVL Store query to use ics proofs #6324
Changes from all commits
5f0a378
3f38699
6eae708
aad1ac7
09519aa
17b6f21
1dae554
056fcfd
1e13c9f
cc9aa0d
28c344c
cf6a061
cf3778d
c715058
9f39846
f59df00
4e7d95e
2a471ec
9d0292a
71664bc
52128a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ import ( | |
"io" | ||
"sync" | ||
|
||
ics23iavl "github.com/confio/ics23-iavl" | ||
ics23 "github.com/confio/ics23/go" | ||
"github.com/pkg/errors" | ||
"github.com/tendermint/iavl" | ||
abci "github.com/tendermint/tendermint/abci/types" | ||
|
@@ -275,31 +277,25 @@ func (st *Store) Query(req abci.RequestQuery) (res abci.ResponseQuery) { | |
break | ||
} | ||
|
||
if req.Prove { | ||
value, proof, err := tree.GetVersionedWithProof(key, res.Height) | ||
if err != nil { | ||
res.Log = err.Error() | ||
break | ||
} | ||
if proof == nil { | ||
// Proof == nil implies that the store is empty. | ||
if value != nil { | ||
panic("unexpected value for an empty proof") | ||
} | ||
} | ||
if value != nil { | ||
// value was found | ||
res.Value = value | ||
res.Proof = &merkle.Proof{Ops: []merkle.ProofOp{iavl.NewValueOp(key, proof).ProofOp()}} | ||
} else { | ||
// value wasn't found | ||
res.Value = nil | ||
res.Proof = &merkle.Proof{Ops: []merkle.ProofOp{iavl.NewAbsenceOp(key, proof).ProofOp()}} | ||
} | ||
} else { | ||
_, res.Value = tree.GetVersioned(key, res.Height) | ||
_, res.Value = tree.GetVersioned(key, res.Height) | ||
if !req.Prove { | ||
break | ||
} | ||
|
||
// Continue to prove existence/absence of value | ||
// Must convert store.Tree to iavl.MutableTree with given version to use in CreateProof | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you take a look at ics23-iavl to see if I can change this requirement (and accept iavl.ImmutableTree instead). I was just gettng it to work, but in this use case referring to an immutable tree seems better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea since you aren't using any Versioned function, you should be able to just swap out This will make the code here cleaner |
||
iTree, err := tree.GetImmutable(res.Height) | ||
if err != nil { | ||
// sanity check: If value for given version was retrieved, immutable tree must also be retrievable | ||
panic(fmt.Sprintf("version exists in store but could not retrieve corresponding versioned tree in store, %s", err.Error())) | ||
} | ||
mtree := &iavl.MutableTree{ | ||
ImmutableTree: iTree, | ||
} | ||
|
||
// get proof from tree and convert to merkle.Proof before adding to result | ||
res.Proof = getProofFromTree(mtree, req.Data, res.Value != nil) | ||
|
||
case "/subspace": | ||
var KVs []types.KVPair | ||
|
||
|
@@ -321,6 +317,34 @@ func (st *Store) Query(req abci.RequestQuery) (res abci.ResponseQuery) { | |
return res | ||
} | ||
|
||
// Takes a MutableTree, a key, and a flag for creating existence or absence proof and returns the | ||
// appropriate merkle.Proof. Since this must be called after querying for the value, this function should never error | ||
// Thus, it will panic on error rather than returning it | ||
func getProofFromTree(tree *iavl.MutableTree, key []byte, exists bool) *merkle.Proof { | ||
var ( | ||
commitmentProof *ics23.CommitmentProof | ||
err error | ||
) | ||
|
||
if exists { | ||
// value was found | ||
commitmentProof, err = ics23iavl.CreateMembershipProof(tree, key) | ||
if err != nil { | ||
// sanity check: If value was found, membership proof must be creatable | ||
panic(fmt.Sprintf("unexpected value for empty proof: %s", err.Error())) | ||
} | ||
} else { | ||
// value wasn't found | ||
commitmentProof, err = ics23iavl.CreateNonMembershipProof(tree, key) | ||
if err != nil { | ||
// sanity check: If value wasn't found, nonmembership proof must be creatable | ||
panic(fmt.Sprintf("unexpected error for nonexistence proof: %s", err.Error())) | ||
} | ||
} | ||
op := types.NewIavlCommitmentOp(key, commitmentProof) | ||
return &merkle.Proof{Ops: []merkle.ProofOp{op.ProofOp()}} | ||
} | ||
|
||
//---------------------------------------- | ||
|
||
// Implements types.Iterator. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package types | ||
|
||
import ( | ||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" | ||
) | ||
|
||
const StoreCodespace = "store" | ||
|
||
var ( | ||
ErrInvalidProof = sdkerrors.Register(StoreCodespace, 2, "invalid proof") | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
package types | ||
|
||
import ( | ||
ics23 "github.com/confio/ics23/go" | ||
"github.com/tendermint/tendermint/crypto/merkle" | ||
|
||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" | ||
) | ||
|
||
const ( | ||
ProofOpIAVLCommitment = "ics23:iavl" | ||
ProofOpSimpleMerkleCommitment = "ics23:simple" | ||
) | ||
|
||
// CommitmentOp implements merkle.ProofOperator by wrapping an ics23 CommitmentProof | ||
// It also contains a Key field to determine which key the proof is proving. | ||
// NOTE: CommitmentProof currently can either be ExistenceProof or NonexistenceProof | ||
// | ||
// Type and Spec are classified by the kind of merkle proof it represents allowing | ||
// the code to be reused by more types. Spec is never on the wire, but mapped from type in the code. | ||
type CommitmentOp struct { | ||
Type string | ||
Spec *ics23.ProofSpec | ||
Key []byte | ||
Proof *ics23.CommitmentProof | ||
} | ||
|
||
var _ merkle.ProofOperator = CommitmentOp{} | ||
|
||
func NewIavlCommitmentOp(key []byte, proof *ics23.CommitmentProof) CommitmentOp { | ||
return CommitmentOp{ | ||
Type: ProofOpIAVLCommitment, | ||
Spec: ics23.IavlSpec, | ||
Key: key, | ||
Proof: proof, | ||
} | ||
} | ||
|
||
func NewSimpleMerkleCommitmentOp(key []byte, proof *ics23.CommitmentProof) CommitmentOp { | ||
return CommitmentOp{ | ||
Type: ProofOpSimpleMerkleCommitment, | ||
Spec: ics23.TendermintSpec, | ||
Key: key, | ||
Proof: proof, | ||
} | ||
} | ||
|
||
// CommitmentOpDecoder takes a merkle.ProofOp and attempts to decode it into a CommitmentOp ProofOperator | ||
// The proofOp.Data is just a marshalled CommitmentProof. The Key of the CommitmentOp is extracted | ||
// from the unmarshalled proof. | ||
func CommitmentOpDecoder(pop merkle.ProofOp) (merkle.ProofOperator, error) { | ||
var spec *ics23.ProofSpec | ||
switch pop.Type { | ||
case ProofOpIAVLCommitment: | ||
spec = ics23.IavlSpec | ||
case ProofOpSimpleMerkleCommitment: | ||
spec = ics23.TendermintSpec | ||
default: | ||
return nil, sdkerrors.Wrapf(ErrInvalidProof, "unexpected ProofOp.Type; got %s, want supported ics23 subtypes 'ProofOpIAVLCommitment' or 'ProofOpSimpleMerkleCommitment'", pop.Type) | ||
} | ||
|
||
proof := &ics23.CommitmentProof{} | ||
err := proof.Unmarshal(pop.Data) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
op := CommitmentOp{ | ||
Type: pop.Type, | ||
Key: pop.Key, | ||
Spec: spec, | ||
Proof: proof, | ||
} | ||
return op, nil | ||
} | ||
|
||
func (op CommitmentOp) GetKey() []byte { | ||
return op.Key | ||
} | ||
|
||
// Run takes in a list of arguments and attempts to run the proof op against these arguments | ||
// Returns the root wrapped in [][]byte if the proof op succeeds with given args. If not, | ||
// it will return an error. | ||
// | ||
// CommitmentOp will accept args of length 1 or length 0 | ||
// If length 1 args is passed in, then CommitmentOp will attempt to prove the existence of the key | ||
// with the value provided by args[0] using the embedded CommitmentProof and return the CommitmentRoot of the proof | ||
// If length 0 args is passed in, then CommitmentOp will attempt to prove the absence of the key | ||
// in the CommitmentOp and return the CommitmentRoot of the proof | ||
func (op CommitmentOp) Run(args [][]byte) ([][]byte, error) { | ||
colin-axner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Only support an existence proof or nonexistence proof (batch proofs currently unsupported) | ||
switch len(args) { | ||
case 0: | ||
// Args are nil, so we verify the absence of the key. | ||
nonexistProof, ok := op.Proof.Proof.(*ics23.CommitmentProof_Nonexist) | ||
if !ok { | ||
return nil, sdkerrors.Wrap(ErrInvalidProof, "proof is not a nonexistence proof and args is nil") | ||
} | ||
|
||
// get root from either left or right existence proof. Note they must have the same root if both exist | ||
// and at least one proof must be non-nil | ||
var ( | ||
root []byte | ||
err error | ||
) | ||
switch { | ||
// check left proof to calculate root | ||
case nonexistProof.Nonexist.Left != nil: | ||
root, err = nonexistProof.Nonexist.Left.Calculate() | ||
if err != nil { | ||
return nil, sdkerrors.Wrap(ErrInvalidProof, "could not calculate root from nonexistence proof") | ||
} | ||
case nonexistProof.Nonexist.Right != nil: | ||
// Left proof is nil, check right proof | ||
root, err = nonexistProof.Nonexist.Right.Calculate() | ||
if err != nil { | ||
return nil, sdkerrors.Wrap(ErrInvalidProof, "could not calculate root from nonexistence proof") | ||
} | ||
default: | ||
// both left and right existence proofs are empty | ||
// this only proves absence against a nil root (empty store) | ||
return [][]byte{nil}, nil | ||
} | ||
|
||
absent := ics23.VerifyNonMembership(op.Spec, root, op.Proof, op.Key) | ||
if !absent { | ||
return nil, sdkerrors.Wrapf(ErrInvalidProof, "proof did not verify absence of key: %s", string(op.Key)) | ||
} | ||
|
||
return [][]byte{root}, nil | ||
|
||
case 1: | ||
// Args is length 1, verify existence of key with value args[0] | ||
existProof, ok := op.Proof.Proof.(*ics23.CommitmentProof_Exist) | ||
if !ok { | ||
return nil, sdkerrors.Wrap(ErrInvalidProof, "proof is not a existence proof and args is length 1") | ||
} | ||
// For subtree verification, we simply calculate the root from the proof and use it to prove | ||
// against the value | ||
root, err := existProof.Exist.Calculate() | ||
if err != nil { | ||
return nil, sdkerrors.Wrap(ErrInvalidProof, "could not calculate root from existence proof") | ||
} | ||
|
||
if !ics23.VerifyMembership(op.Spec, root, op.Proof, op.Key, args[0]) { | ||
return nil, sdkerrors.Wrapf(ErrInvalidProof, "proof did not verify existence of key %s with given value %x", op.Key, args[0]) | ||
} | ||
|
||
return [][]byte{root}, nil | ||
default: | ||
return nil, sdkerrors.Wrapf(ErrInvalidProof, "args must be length 0 or 1, got: %d", len(args)) | ||
} | ||
} | ||
|
||
// ProofOp implements ProofOperator interface and converts a CommitmentOp | ||
// into a merkle.ProofOp format that can later be decoded by CommitmentOpDecoder | ||
// back into a CommitmentOp for proof verification | ||
func (op CommitmentOp) ProofOp() merkle.ProofOp { | ||
fedekunze marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I would opt to return an error here and allow the caller to decide to panic or not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We cannot return an error here, this method is satisfying the ProofOperator interface There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yikes, I would consider revising that interface if possible. Not a good UX IMHO. Ideally, you should always be able to bubble up errors as much as possible and allow callers to handle it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm... yea that would definitely require a separate discussion with TM team and a separate PR since this is a tendermint interface. Don't think a panic is the worst idea here since it should be an invariant that any well-constructed |
||
bz, err := op.Proof.Marshal() | ||
if err != nil { | ||
panic(err.Error()) | ||
} | ||
return merkle.ProofOp{ | ||
Type: op.Type, | ||
Key: op.Key, | ||
Data: bz, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a follow up to this (sometime before tagging the 0.39 sdk), it would be nice to merge this ics23-iavl code into tendermint/iavl, so this can be optimized and integrated in the tendermint codebase.
Do you want to make an issue for that if you agree (I feel odd doing so and cannot label/milestone it)