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

Proposal feedback #78

Closed
HarryR opened this issue Jun 15, 2018 · 5 comments
Closed

Proposal feedback #78

HarryR opened this issue Jun 15, 2018 · 5 comments

Comments

@HarryR
Copy link
Contributor

HarryR commented Jun 15, 2018

As per https://github.com/clearmatics/ion/wiki/Ion-Stage-2---Proposal

You have done a very good job of understanding how ethereum block headers and receipts are used and specifying a method of validating receipts, however my criticism is split into three parts:

  1. How do people use this thing, what is the end-result

  2. There is more than one blockchain in the world

  3. Is there a simpler, smaller, cheaper, more portable solution

TL;DR less talky more codey, working code is canon, ideas are worth nothing until they work (unless you talk to the ICO crowd, then worthless ideas are worth everything, regardless of if they work)

Elaborating on 1)

Maybe starting with use cases first?

#77

e.g. if the problem domain isn't very well defined yet, maybe start with some ideal world examples of how a developer would use your technology with minimum friction, and work from there to define the components and make the underlying protocol meet in the middle while making trade-offs where necessary.

Elaborating on 2)

If your customers may ever want to integrate between mainnet Ethereum and a private Quorum network (or even, heaven forbid, heretical software like Parity's Kovan, Neo or Rootstock), then maybe you should consider a simpler more portable solution.

Is there something which works across all of the Turing-complete blockchains which doesn't require parsing the internal structures of each block and event of each chain on each chain? Yes, it's in the git repo...

This proposal like a fragile solution where you're binding the on-chain contracts to the off-chain network protocol of a piece of software you don't really have much control over, which is subject to change outside of your control, in a way which any change will instantly break all of the on-chain components even though the network level components can and do gracefully upgrade without breaking.

e.g. clients on the network can be progressively upgraded without having to touch any of the on-chain components and the network will remain in-sync and compatible all of the way through. As soon as you add parsing of the network-level protocols into the on-chain components this becomes a nightmare of delicate upgrades, blocking components and potential stop-the-world breakage.

Instead of a 'test the alpha, roll-out a limited beta, release then allow each participants at its own pace' it becomes 'ok everybody stop, annnndddd....... upgrade NOW, oh wait... no... everythings still offline while we figure this out because now the on-chain contracts are out of sync with the network protocol' kinda thing. Capiche?

Now, if you wanna go ahead and do that, sure - why not - it would be an interesting proof of concept, but please try and avoid the frailty of one choice from impacting and limiting the rest of the system. Do this by separating concerns and having clean implementation-agnostic interfaces.

Elaborating on 3)

TL;DR yes, it's in the Git repo and you have already proven that it works... why start from scratch with a more convoluted and complex solution that works in less situations.

There are issues with the current proof of concept, some of which have been highlighted already:

  1. The disconnection between authorities of the separate chains and the authorities which are permitted to update the merkle roots.

  2. How do you ensure the proof is only used once?

  3. Pls insert more in comments... I will address them as time permits.

1) Disconnect between chains / authorities

Yes, there should be a separation between those who are authorised to provide proofs that an event occurred, versus those who are authorised to mine blocks for the specific network which the event occurred on.

It should be possible for a single authority to monitor multiple block chains to prove that events have occurred without having to participate in the mining of blocks on each individual network. e.g. the same set of authorities should be able to provide proofs across two networks because each network is an island of its own trust, and the level above it which proves across networks is also its own island of trust.

Why should an implementation of this interoperability solution on a Neo or Parity chain be constrained by whoever operates a specific private and unrelated blockchain when they're trusting that whoever runs the Clearmatics Lithium instance is also verifying the integrity of that chain.

2) Ensure proof only used once

  • That a condition for execution includes usage of proofs only once per function that defines such proof as it's trigger
  • That a contract function that requires a specific proof also requires a specific function to assert this proof

This is where the design hinges on one direction and another, but it's an interesting question. And it also brings into question the validity of the whole system bring proved from the top-down versus the bottom up.

The only design constraint I will push is for the ability of components to be used separately and independently regardless of whether or not they're part of the system you're designing or are relied upon by systems that others design.

I want to create a contract where the same proof can be provided multiple times, on another chain somebody approves me as a valid authority over something, I can then take this proof to the other chain to perform an action on behalf of a user on that chain - I should be able to supply the same proof multiple times because it's still valid.

The other side to that is that proof should only be usable once because a later action may invalidate that proof, and because the systems are separate and unconnected there's no way to know if it has been revoked.

However, this should be a choice of the system designer. They could attach their contract to a 'ValidateProof' interface, or a 'ValidateProofOnce' implementation (for convenience) - or implement their own double-use validation.

Downsides to proposal

  • Requires modifying code to work explicitly with it, e.g. IonCompatibleEvent, rather than relying on Lithium to turn all events into Ion compatible events...

With the lithium implementation as-is the events from any pre-existing contract can be used on other systems without having to modify the whole system to accommodate this new use case. e.g. the rest of the world shouldn't have to re-write their ERC-20 contracts specifically for Clearmatics if somebody using Clearmatics' stuff wants to perform an action when a deposit has provably been made on another chain.

Transaction Overheads with Proposal

However, with the proposed solution there is a problem with the size of data
15kb of data per block header per chain, then an additional Nkb of state per proof is a significant overhead given that all the data is going to be stored on-chain forever - this kind of thing makes it painfully expensive for anybody to use it on the Ethereum main net, like $40+ per block header update and $10+ per proof transaction (ontop of any other logic costs). Given the alternative is probably around 0.5% of the cost, I'd say that's a significant impediment to this proposal being adopted outside of the vapourware sandbox.

Serious question, which can easily quash this argument: if I want to prove that a single event occurred with specific parameters, how much data needs to be transmitted as proof with the transaction?

You cannot use the logsBloom parameter of the block because it can contain false positives (whereas a merkle tree proof cannot, at least without effectively breaking the hash function), and with some luck or malicious brute-force effort I can craft an event which can be submitted as proof and pass the logsBloom filter check.

See §4.4 pg5 of yellowpaper: http://gavwood.com/paper.pdf

The transaction receipt is a tuple of four items comprising the post-transaction state, , the cumulative gas used in the block containing the transaction receipt as of immediately after the transaction has happened, Ru, the set of logs created through execution of the transaction, Rl and the Bloom filter composed from information in those logs, Rb:
R ≡ (Rσ, Ru, Rb, Rl)

But... there's a problem, you have to submit the entire receipt in addition to the receipt proof path as proof of a single event because receiptsRoot contains no sub-trie for logs. That alone is going to be at least 256 bytes just because of the logs Bloom field. To give you a size comparison, the proof for a merkle tree leaf (as per HarryR/solcrypto) with 10k items is 448 bytes + size of leaf (a hash of the elements). With a couple hundred entries it's less than 256 bytes per merkle proof.

You cannot just provide proof that a receipt exists, because that doesn't prove that a specific log entry occurred - so you can't do event triggered things depending on the output of a transaction call.

You could prove that a transaction occurred, but as proof you'd have to supply all details of the transaction to recreate the leaf value, in addition to the trie proof - and then you'd have to impose restrictions like each function call can only result in one possible outcome (rather than multiple, as per the event proof), and that on any error you'd have to revert - and lots of other really tedious and annoying preconditions to writing code which integrates with this.

The solution I proposed has been optimised for size, simplicity of implementation and portability, IMO it's as close to the optimal as is possible - so lets hear the downsides to it, formally in writing, and I will provide well reasoned and cogent responses to them regardless of whether or not they're in favour or against.

TL;DR your solution costs more gas, is more complex on both the client & server side, is much more difficult to write a cleanroom implementation of, requires more data to prove any event happens, is more cumbersome to use from a developers perspective, is inherently tied to a vapourware product that hasn't ever existed and doesn't as easily handle multiple blockchains (both Ethereum and non-Ethereum) etc. etc.

API breakdown

TODO, I may skip this as it feels more complex and limiting, and just go ahead and finish writing the code

@HarryR
Copy link
Contributor Author

HarryR commented Jun 15, 2018

Disregard the above.

The only thing I care about is that there is a consistent interface which allows for multiple solutions to be implemented, and multiple components of each solution to be implemented in parallel.

The way this works is:

  • VerifyProofInterface

You provide a contract which implements VerifyProofInterface as an argument to a contract which needs to verify proofs.

To supply the proof you add a bytes proof to the method which needs the proof.

The method makes a hash of the event log which it expects, e.g. SHA(event_sig, arg1, arg2, arg3) and supplies it along with the provided proof to the VerifyProofInterface contract.

e.g.

contract Test {
   VerifyProofInterface m_prover;
   address m_other_contract;

   constructor (VerifyProofInterface prover, address other_contract) public {
      m_prover = prover;
      m_other_contract =  other_contract;
   }

   function SomethingNeedingProof (address my_address, bytes proof) {
      bytes32 sig = SHA('MyProofEvent(address)');
      if( m_prover.Verify(m_other_contract, sig, my_address) ) {
         // TODO: verify if proof has already been used to trigger this function
         // other_contract emitted event with `sig` with arg of `my_address`
      }
   }
}

Am thinking about this, as need to account for which network / source the proof is proven on, however something I remember is that Lithium includes the contract address as part of the event log hash, and because contract addresses should be globally unique (well, there's one edge case...) then the contract address of origin should be sufficient without needing to specify the whole network etc.

After I'm done writing HTLC stuff I will come back to this and do more work on it.

The SomethingNeedingProof function verifies if the proof has previously been used, I think you should be able to provide the same proof to the function multiple times and it should work out what to do with that otherwise you lose out on a big chunk of interesting use cases by adding artificial constraints.

TL;DR need simple examples, which don't require constructing a plethora of different classes or single-use instances of things just to trigger an action, and the components should be easily interchangeable without having to modify function signatures (hence, using bytes proof) along with helper functions for that specific implementation.

@HarryR
Copy link
Contributor Author

HarryR commented Jun 15, 2018

One more thing.

It definitely is worth trying to get the trie proof for a receipt in a block, as a proof of concept this should be the first step - write some client-side code to construct the arguments to a receipt verifier function. e.g. the receipt trie proof.

Then a piece of solidity code to verify if a specific log even exists within the receipt (now you have test data)

Then a piece of solidity code to verify the receipt trie proof exists within a specific receipt root.

This was the biggest blocker for me as I had problems with all of the Ethereum client-side libraries when trying to get then to spit out receipt trie proofs. Some more work has been done on it recently to make it easier, but the problem still persists about the amount of data required to verify if a single log event exists within a receipt without passing lots of data...

After that the rest is just regular software design and avoiding foot-gun design mistakes by using good principals like the separation of concerns, principle of least privilege etc.

@HarryR HarryR closed this as completed Jun 20, 2018
@Shirikatsu
Copy link
Contributor

Just got back from a trip. Thanks again for the input.

A lot of what you mentioned has been part of the thought process of what was output. The gas costs of the amount of data is prohibitively high but fortunately we have the leisure of it not being much of a constraint due to our private network setting.

Your Lithium solution does, as you say, simplify the mechanism and generalising between different networks but does so by off-chain construction of events. If we're thinking trustless, how can the events generated by an off-chain service ever be surely correct? Who would provide this service?

Anyways, we're currently looking into generating trie proofs as a priority since any event consumption mechanism would require this.

In your example Test, is the prover only verifying the proof for the SomethingNeedingProof function and only expecting events emitted from m_other_contract from the other chain? Doesn't this mean that every function that consumes an event will have its own prover unless the consumed event is identical?

@HarryR
Copy link
Contributor Author

HarryR commented Jun 27, 2018

Please see:

They both seem to do what you need, but I like Cosmos more.

If we're thinking trustless, how can the events generated by an off-chain service ever be surely correct? Who would provide this service?

Given access to the same information it's possible to re-create the root hashes, e.g. with the previous state and all of the transactions, after running the same algorithm, you can prove the new state is what it is, but doing that on-chain isn't possible without having every other blockchain recursively executing inside each and every other etc. The other option would be a negative proof, where you prove that something which exists in the merkle root is logically impossible (as Polkadot and TrueBit are aiming to do) - which more realistic and less complex, but still requires you to have all of the logic of the blockchain running recursively inside the blockchain etc.

With authorities and a majority vote then the authenticity of the collective consensus can be verified anywhere, as long as you:

  • verify the initial state matches the initial state of every other instance you're relying on before you agree to participate on-chain
  • trust that the on-chain contracts will execute predictably, wherever they are
  • trust that the authorities / validators will always act honestly and independently (and not collude etc.)
  • trust that a majority of the authorities signing keys will never be compromised at any given point in time (which could cause a fork, if used maliciously/correctly)

If you trust something, then you're relying on that trust in lieu of being able to trustlessly verify something which you have no direct access to. But you can trustlessly verify that whatever you trust is trustable (e.g. the Valset thing).

See: https://github.com/cosmos/peggy/blob/master/ethereum-contracts/contracts/Valset.sol - My Hydrogen idea thing was very similar to that Valset contract, but with some neat extra bits for explicitly replicating across only the chains that the authorities want it replicated to so that only a single root state is needed from which every other instance of it can be verified etc. etc.

Doesn't this mean that every function that consumes an event will have its own prover unless the consumed event is identical?

That would be a problem because it would result in loads of duplication? The prover is just an interface to verify user-supplied proofs against the expected event using on-chain data, rather than being bound to any specific type of event.

The prover needs to allow you to prove any event or transaction occurred based on whatever the contract needs proving, e.g. using abi.encodeWithSelector, and it needs to be usable by every contract that needs proof from that source. The contract requesting proof would construct the arguments that it needs proof of (e.g. based on its own internal state), then supply them along with the user-supplied proof.

require( prover.Verify(abi.encodeWithSelector("MyEvent(address, address,address)", this.owner, msg.sender), this.other_contract, in_proof ) );

Verifies that the user-supplied proof is valid for the event with parameters from the contract was emitted by the other contract.

Ideally a single prover could allow you to prove events from multiple networks, rather than having one prover for Network A and another one for Network B, thus would be providing a consistent state across multiple chains etc. without having to use separate instances of contracts, e.g. 'SwapWithNetworkA' and 'SwapWithNetworkB' (although I guess that's not a problem, as long as it works).

@HarryR
Copy link
Contributor Author

HarryR commented Jun 27, 2018

There's another use case which I think needs to be considered, which is part of my 'if only we can get the rest of the world to switch all of their systems over to our one and ours alone at the same time' antithetical argument, which is that at some point you're going to be relying on data from external non-blockchain 'traditional, trusted' systems which needs to be proved on-chain.

e.g. if I want to prove that I have deposited money into an account, to virtualise it so it's usable on-chain, then the proof I provide on-chain by necessity isn't going to be based on an Ethereum block header, receipt and log, unless I trust that all of the information about transfers into that account are replicated faithfully onto an Ethereum based block chain by one or more trusted parties.

I am thinking about an on-ramp for 'traditional, trusted' systems - where multiple observers (acting as a consensus) independently verify events which cannot be proven on-chain - and publish that information in a way which can be consumed on-chain. e.g. the intersection between the real world and blockchain.

What matters is, on-chain, you can trustlessly prove there was a consensus.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants