Skip to content
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

WIP: Fix ICS23-Proofs #6178

Closed
wants to merge 9 commits into from
Closed

WIP: Fix ICS23-Proofs #6178

wants to merge 9 commits into from

Conversation

AdityaSripal
Copy link
Member

@AdityaSripal AdityaSripal commented May 8, 2020

Description

closes #5842, closes #5082 and closes #6091

@auto-comment
Copy link

auto-comment bot commented May 8, 2020

👋 Thanks for creating a PR!

Before we can merge this PR, please make sure that all the following items have been
checked off. If any of the checklist items are not applicable, please leave them but
write a little note why.


For contributor use:

  • Targeted PR against correct branch (see CONTRIBUTING.md)
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Code follows the module structure standards.
  • Wrote unit and integration tests
  • Updated relevant documentation (docs/) or specification (x/<module>/spec/)
  • Added relevant godoc comments.
  • Added a relevant changelog entry to the Unreleased section in CHANGELOG.md
  • Re-reviewed Files changed in the Github PR explorer

For admin use:

  • Added appropriate labels to PR (ex. WIP, R4R, docs, etc)
  • Reviewers assigned
  • Squashed all commits, uses message "Merge pull request #XYZ: [title]" (coding standards)

Thank you for your contribution to the Cosmos-SDK! 🚀

}

runtime := rootmulti.DefaultProofRuntime()
return runtime.VerifyValue(proof.Proof, root.GetHash(), path.String(), value)
if !ics23.VerifyMembership(proof.Spec, root.GetHash(), proof.Proof, []byte(path.String()), value) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the key in the confio VerifyMembership function the path? Or is it the last element of the path?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume it's the full path

Copy link
Collaborator

@fedekunze fedekunze left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you pull master and update the proto files?

@cwgoes
Copy link
Contributor

cwgoes commented May 11, 2020

The ProofSpec should be fixed when a client (e.g. for a Tendermint chain) is created, then used for all future proof verifications performed with that client.

@AdityaSripal
Copy link
Member Author

The ProofSpec should be fixed when a client (e.g. for a Tendermint chain) is created, then used for all future proof verifications performed with that client.

I imagine we cannot assume that all Tendermint clients are SDK chains. Thus we cannot make the IAVLProofSpec a standard for tendermint clients. Will the proof spec have to be negotiated on the connection handshake?

@cwgoes
Copy link
Contributor

cwgoes commented May 17, 2020

The ProofSpec should be fixed when a client (e.g. for a Tendermint chain) is created, then used for all future proof verifications performed with that client.

I imagine we cannot assume that all Tendermint clients are SDK chains. Thus we cannot make the IAVLProofSpec a standard for tendermint clients. Will the proof spec have to be negotiated on the connection handshake?

No, I mean that it is a parameter when a client is created, like the trusting period.

I suppose it could also be per-connection, but it will make the API a bit more complicated - is there likely to be a case where a single Tendermint consensus instance wants to use multiple proof formats for different connections?

x/ibc/07-tendermint/types/client_state.go Outdated Show resolved Hide resolved
x/ibc/07-tendermint/types/msgs.go Outdated Show resolved Hide resolved
// CONTRACT: provided path string MUST be a well formated path. See ICS24 for
// reference.
func ApplyPrefix(prefix exported.Prefix, path string) (MerklePath, error) {
err := host.DefaultPathValidator(path)
func ApplyPrefix(prefix exported.Prefix, path exported.Path) (exported.Path, error) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApplyPrefix has changed to use the prefix to prepend a merkle path for the next tree in a chained proof. This corresponds with the current usage. Since the prefix ibc would be the path for the next tree in the chained multistore proof proving that the iavl roothash for ibc substore is included in multistore hash

@@ -158,6 +177,8 @@ func (cs ClientState) VerifyClientConsensusState(
return err
}

// TODO: Inject proof specs of clientstate into proof or modify verify membership function to take proof specs
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @ethanfrey mentioned, the proof spec needs to be fixed by the client not passed in by the user. I can either inject the clientstate proofspecs into the MerkleProof struct (replacing whatever user passed in). Or i can remove specs from MerkleProof and pass it into the VerifyMembership function.

The latter requires a change in ICS spec

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should change ICS 23 to not put ProofSpec in the proofs

If this requires a spec change as well, we can do that

}

// VerifyNonMembership verifies the absence of a merkle proof against the given root and path.
// VerifyNonMembership verifies a chained proof where the absence of a given path is proven
// at the lowest subtree and then each subtree's inclusion is proved up to the final root.
func (proof MerkleProof) VerifyNonMembership(root exported.Root, path exported.Path) error {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently VerifyNonMembership will verify absence at the smallest subtree, and then prove inclusion of all subtree upto the root.

Thus in the multistore example, we can prove absence of a value in the ibc store, but we can't use this to prove that storeABC does not exist in multistore

That seems sufficient for all ibc usecases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, let's just make sure we panic if we ever try to check a proof of absence of storeABC

// It demonstrates membership or non-membership for an element or set of elements,
// verifiable in conjunction with a known commitment root. Proofs should be
// succinct.
type MerkleProof struct {
Proof *merkle.Proof `json:"proof" yaml:"proof"`
Proofs []*ics23.CommitmentProof `json:"proofs" yaml:"proofs"`
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the SDK's MerkleProof to be a list of CommitmentProof and specs that are chained from smallest subtree to the top tree, each subroot serving as the value for the next proof.

This allows clients to define arbitrary chaining of proofs with different proofspecs in their state whether they are TM/SDK chains or not. Also doesn't require putting this chaining logic in confio package

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this approach.

Comment on lines +202 to +208
existProof, ok := p.Proof.(*ics23.CommitmentProof_Exist)
if !ok {
return sdkerrors.Wrap(ErrInvalidProof, "proof is not an existence proof")
}
// For subtree verification, we simply calculate the root from the proof and use it to prove
// against the value
subroot, err := existProof.Exist.Calculate()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to make Calculate a method on CommitmentProof so I don't have to do this casting when I need to chain the proofs. cc: @ethanfrey

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, if it is available on all variants of the CommitmentProof, that we then expose it on the larger container and dispatch it to the subtypes as needed? I will check if they all have the same method signature (eg. with Batch proofs), but if so, then I can add.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, checking the repo now. I find these examples of calculate:

ExistenceProof: https://github.com/confio/ics23/blob/master/go/proof.go#L72
Non-Existence Proof has none, but I could add one (just checking either of the included proofs)
Batch Proof has none either, and I guess we would have to check all included proofs and then ensure they are the same?
Compressed Batch Proof also has none, and this is even more expensive to implement - first decompression, then the batch proof algorithm.

What would make more sense is just a helper function, as you require existence. (Or do you want to extend this to non-existence also). Something like:

func CalculateExistProofRoot(p *ics23.CommitmentProof) ([]byte, error) {
                existProof, ok := p.Proof.(*ics23.CommitmentProof_Exist)
		if !ok {
			return nil, sdkerrors.Wrap(ErrInvalidProof, "proof is not an existence proof")
		}
		// For subtree verification, we simply calculate the root from the proof and use it to prove
		// against the value
		subroot, err := existProof.Exist.Calculate()
                // anything else to do here?
                return subroot, err
}

This could fit just as well in this module as in ics23

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'll have to get the root for all the proof types because the different proof types show up at the lowest subtree (above that it's all just existence proofs). For example, the VerifyNonMembership proves nonexistence at the lowest subtree, and then proves existence of each subroot up to the final root. Similarly for batch proofs.

I don't need the batch proofs to check that all included proofs commit to the same root. Just return the root of the first proof. I'll then just use the batch-verify functions you have and pass in that first root. This will verify that every proof in my batch proof commits to the same root.

Currently writing up the batchverify functions in this module, and i think it'll be clear why it's better to have Calculate method in ics23 even if it involves an annoying amount of calls to get to the first embedded ExistenceProof in all of these proof types and calculate root on that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, happy for a pr on ics23 that adds that, or I can take a look next week myself

Copy link
Member Author

@AdityaSripal AdityaSripal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a lot of this code can get pushed into a confio Calculate method on CommitmentProof, then the code here becomes much more readable

Could do the type switch and calculate for each proof type in here with a helper function as well, but seems cleaner to have it in confio/ics23

cc: @ethanfrey

Comment on lines +330 to +339
bproof, ok := p.Proof.(*ics23.CommitmentProof_Batch)
if ok {
if len(bproof.Batch.GetEntries()) == 0 || bproof.Batch.GetEntries()[0] == nil {
return sdkerrors.Wrap(ErrInvalidProof, "batch proof has empty entry")
}
bexist := bproof.Batch.GetEntries()[0].GetExist()
if bexist == nil {
return sdkerrors.Wrap(ErrInvalidProof, "batch proof is not an existence proof")
}
subroot, err = bexist.Calculate()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BatchProof root calculation

}
subroot, err = bexist.Calculate()
} else {
return sdkerrors.Wrap(ErrInvalidProof, "not a batch proof, compressed batch proof currently unimplemented")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not doing compressed batch proof calculation manually here but similar principle

Comment on lines +405 to +416
bproof, ok := p.Proof.(*ics23.CommitmentProof_Batch)
if ok {
if len(bproof.Batch.GetEntries()) == 0 || bproof.Batch.GetEntries()[0] == nil {
return sdkerrors.Wrap(ErrInvalidProof, "batch proof has empty entry")
}
bnonexist := bproof.Batch.GetEntries()[0].GetNonexist()
if bnonexist == nil {
return sdkerrors.Wrap(ErrInvalidProof, "batch proof is not a nonexistence proof")
}
subroot, err = bnonexist.Left.Calculate()
} else {
return sdkerrors.Wrap(ErrInvalidProof, "not a batch proof, compressed batch proof currently unimplemented")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto on nonexist batch proof

Copy link
Contributor

@ethanfrey ethanfrey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a lot going on here, which could be done much better in a series of much smaller PRs.

  1. Replacing the CommitInfo proof step from the whole (amino encoded?) struct to merkle.SimpleProof. That code looks pretty good and could be pulled out in a small PR and merged already.
  2. Changing the hashing algorithm for merkleMap and merkle.SimpleMap. This seems to only be desired for some optimizations and should go through an ADR independent of the rest of this PR.
  3. Adding a new op type and runtime decoder for ics23.CommitmentProof (or multiple ops for different subtypes?). Most of the verify logic you write in ics-23 can go in there, completely not breaking. This also allows all the ics code that refers to merkle.Proof to keep doing so.
  4. Update the general query proof to return two ics23.ExistenceProof (wrapped in merkle.ProofOp) rather than ProofOpMultiStore and iavl.ProofOpIAVLValue. Pass those anywhere ics code is expecting a proof.
  5. Create func ICSProofRuntime() (prt *merkle.ProofRuntime) that only has support for ics23 types and see if this works. If not, hunt down where the other types are leaking through.
  6. Ensure all ibc code (including relayer on a branch with this code) is working with this - ideally before merging each PR to help Jack keep his sanity.

This should involve next to no changes in the ibc/02, 03, 07 or 23. We have a nice abstraction there already, eg: return runtime.VerifyValue(proof.Proof, root.GetHash(), path.String(), value). Let's use it and just fit ics23 under the entire merkle.ProofOp machinery. I believe this was the approach discussed. Live side by side, don't make them exclusive.

@@ -37,4 +37,6 @@ require (

replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.2-alpha.regen.1

replace github.com/tendermint/tendermint => github.com/AdityaSripal/tendermint v1.2.3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just looked at this. Essentially changing the hashing algorithm for SimpleMerkleProof (which breaks any external tooling that uses it). Why was that done? And is there any goal to move that into master??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove this replace. Step by step. No need to require merging a breaking change into tendermint core to get this working. If you think it is a nice optimiziation, it may be, and can be another PR process there, but it feels odd to pull it into this PR.

Copy link
Member Author

@AdityaSripal AdityaSripal Jun 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't an optimization. The way simple proofs were being verified didn't line up with the way the stores were getting hashed. There was an extra rounds of hashing happening for leaf values in proof verification that wasn't happening with the simple hash. Thus the proof did not commit to the multistore hash, when just using SimpleHash and SimpleProof without any changes.

After tracking the issue down, I found that removing these extra layers of hashing made the proof verification line up with the multistore commit. Haven't found alternative solutions that also work, so I left this for now and moved on with rest of the code. Will look into this further and try to understand issue fully, because I too do not want to change tendermint if I don't have to.

If it's possible to have an SDK only solution, I will use it. If not, this may be necessary. I haven't figured out if it is necessary yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we still hashing somewhere? Maybe this was accidental, or maybe there was a reason for it - can we ask the Tendermint team (are they using this code anywhere)?

If they're not using this code anywhere, maybe we should move it out of Tendermint altogether.

@@ -30,13 +29,9 @@ func newMerkleMap() *merkleMap {
func (sm *merkleMap) set(key string, value []byte) {
sm.sorted = false

// The value is hashed, so you can check for equality with a cached value (say)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, this is the same change as in tendermint. Again, why?

(Also this breaks ics23-tendermint, which needs a PR to update the format and regenerate the proofs)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/confio/ics23-tendermint/pulls if you really want to make this.

But there should be no real reason to mix it in this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have this code in two places in the first place?

// XXX: This should be managed by the rootMultiStore which may want to register
// more proof ops?
func DefaultProofRuntime() (prt *merkle.ProofRuntime) {
prt = merkle.NewProofRuntime()
prt.RegisterOpDecoder(merkle.ProofOpSimpleValue, merkle.SimpleValueOpDecoder)
prt.RegisterOpDecoder(iavl.ProofOpIAVLValue, iavl.ValueOpDecoder)
prt.RegisterOpDecoder(iavl.ProofOpIAVLAbsence, iavl.AbsenceOpDecoder)
prt.RegisterOpDecoder(ProofOpMultiStore, MultiStoreProofOpDecoder)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can drop this one (:+1:) you an also add another proof op that returns ics23 format, no?

@@ -714,3 +727,13 @@ func SimpleHashFromMap(m map[string][]byte) []byte {

return mm.hash()
}

func SimpleProofOpsFromMap(m map[string][]byte) map[string]merkle.SimpleValueOp {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to convert all the merkle.SimpleProofs into merkle.SimpleValueOp when we just use one of them.

Unless you actually use the multi-return value elsewhere (where this code would make sense), why not just:

// in real code handle the missing case better
func SimpleProofOpFromMap(m map[string][]byte, key string) merkle.SimpleValueOp {
    _, proofs, _ := merkle.SimpleProofsFromMap(m)
    return merkle.NewSimpleValueOp([]byte(key), proofs[key])
}

@@ -609,7 +621,8 @@ func (si storeInfo) Hash() []byte {
panic(err)
}

return hasher.Sum(nil)
hash := hasher.Sum(nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed reverting this change during debugging

if err != nil {
return MerklePath{}, err
}

if prefix == nil || prefix.IsEmpty() {
return MerklePath{}, errors.New("prefix can't be empty")
}
return NewMerklePath([]string{string(prefix.Bytes()), path}), nil
mpath, ok := path.(MerklePath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you require the path to be a MerklePath here (runtime) but allow any exported.Path interface at compile time (function argument) you open up to annoying runtime failures. Why not just require the concrete type. Especially the return type is often much better as a struct (general Golang rule - accept interfaces, return structs).

I would just export MerklePath.

if err != nil {
return sdkerrors.Wrap(ErrInvalidProof, err.Error())
}
if i != len(proof.Proofs)-1 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this code, I think it would make more sense going from top-to-bottom, so we don't have to pre-calculate the expected root each time.

Or is the embedded key-value not passed along with the proof.

I mean, I first check there is a valid proof from header.AppHash to ("ibc", <sub-hash>).
If true, then I check if there is a valid proof from <sub-hash> to (<packet-key>, <packet-value>).

I assume that <sub-hash>, <packet-key> and <packet-value> are sent along with the message. If not, then my inversion doesn't work. Can you point me to all info we have in the calling function (or is this interface fixed). This function does take root hash, all keys and packet-value, so I do see why you have to go reverse... just seems a bit odd

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subhash would not be sent with the message

}

runtime := rootmulti.DefaultProofRuntime()
return runtime.VerifyAbsence(proof.Proof, root.GetHash(), path.String())
mpath, ok := path.(MerklePath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again you only invite user error when you accept an interface and immediately error if the user didn't pass in one implementation.

@AdityaSripal
Copy link
Member Author

Working on making store.Query use ics23 proofs in a separate branch

@cwgoes
Copy link
Contributor

cwgoes commented Jun 2, 2020

I think there is a lot going on here, which could be done much better in a series of much smaller PRs.

I do think this would be easier to review as a set of smaller PRs, do you think that's feasible @AdityaSripal? It would also simplify the current merge conflicts.

Copy link
Contributor

@cwgoes cwgoes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth running through the proof-chaining logic in a synchronous discussion.

@AdityaSripal
Copy link
Member Author

Superseded by #6323 #6324 #6374 #6390

@AdityaSripal AdityaSripal deleted the aditya/ics23-proofs branch June 15, 2020 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants