neo-project / neo

NEO Smart Economy
MIT License
3.47k stars 1.03k forks source link

System.Crypto.CheckData interop #2866

Open roman-khimov opened 1 year ago

roman-khimov commented 1 year ago

Summary or problem description We've got two basic mechanisms to verify some permissions or action validity in a contract:

The first one is the standard thing that works for any Neo account. The second one only works with some single keys (but can handle secp256k1 curve). Usually the second one is used to check some auxiliary non-Neo-related data like cross-chain headers or state data, while regular Neo accounts provide witness (usually, signature(s)) for Neo transactions.

Creating a transaction for Neo blockchain implies answering to several questions:

  1. Who is paying for it?
  2. What nonce/fees/other technical fields are?
  3. What is the script this transaction will execute?

There are cases where we'd like to free the signer from answering to this questions. The first one can be solved with #2577 (and NeoFS sidechain uses it already), but the other two are somewhat more problematic. They assume some knowledge of Neo transaction structure and (!) NeoVM on the signing side. Of course transaction can be prepared elsewhere, but passing some base64 for wallet to sign is not a good idea in general, signer needs to know what exactly he is signing. This problem intersects strongly with neo-project/proposals#68 (and #2835 duplicate of it), but as mentioned there, it's not that easy in Neo context.

So going from an intent of doing something application-specific to transaction requires some effort. Some contracts try dealing with it by using simple signatures they can check with CryptoLib.VerifyWithECDsa. The data to sign can be anything application wants (like serialized NeoFS container structure) and this structure can be as app-friendly as it can be (only containing app-relevant data), but the approach itself inherently means that only simple single-key accounts could be used, application can't rely on Neo account system in this case and can't have multisignature, contract-based or non-standard accounts using this scheme.

Do you have any solution you want to propose? Neo account witness in general is three things: account hash (address), invocation script and verification script (which is omitted for contracts), so verification requires executing some code which usually calls System.Crypto.CheckSig or System.Crypto.CheckMultisig. Both operate with the so-called "script container" (usually a transaction), serialized representation of which should be signed. So to be able to verify something we need to run two VM scripts with some specific script container.

We have an ability to run arbitrary read-only scripts in VM since 3.5.0 (System.Runtime.LoadScript from neo-project/neo#2756). The only thing left to implement is ability to provide some data to check, overriding ScriptContainer for the execution since both System.Crypto.CheckSig and System.Crypto.CheckMultisig should work. The proposal is to implement this in a System.Crypto.CheckData interop.

It should accept:

And it should return a simple boolean true/false result (as verification scripts usually do).

This allows to implement dApp-specific trust schemes that are easy to sign on clients that have absolutely no idea what's going on in the blockchain.

Example 1 NeoFS currently only allows single-key accounts and that's exactly because of the limitation described above. Creating a container via NeoFS SDK doesn't require managing transactions since we want to provide an API to manage data and not dealing with sidechain. But internally it's a sidechain transaction that calls NeoFS container contract. Going via System.Crypto.CheckData would allow to have containers owned by multisignature accounts.

Example 2 Consider neo-project/proposals#152 problem. While I understand people there want a very specific compatible solution, let's imagine how the problem of "allowing a spender to spend x amount of funds on behalf of an owner" could be solved with System.Crypto.CheckData (but notice that this is just an example, not a proposal for a fancy transferFrom).

It can be done with just a single transferFrom(spender Hash160, from Hash160, to Hash160, amount Integer, data Any, token String, invocation ByteArray, verification ByteArray) accepting a JWT-like token:

{
  "iss": "Nhfg3TbpwogLvDGVvAvqyThbsHgoSUKwtn",
  "sub": "NMUedC8TSV2rE17wGguSvPk9XcmHSaT275",
  "exp": 3560000,
  "nbf": 3550000,
  "seq": 42,
  "asset": "NiHURyS83nX2mpxtA7xq84cGxVbHojj5Wc",
  "amount": 100
}

The implementation will:

So we have a scheme that only adds a single method, can work with a signature for a very simple and easy to understand structure and doesn't even require any transactions on the from side.

Neo Version

Where in the software does this update applies to?

roman-khimov commented 1 year ago

This can also help for NeoFS withdrawals. As we've said numerous times, in absence of #1573 we have a huge problem with GAS withdrawals from NeoFS mainnet contract. It requires confirmation from all of the alphabet IR nodes, so at the moment this implies collecting votes in the contract itself and consequently leads to a hefty GAS penalty for doing so. Instead a check can be signed in the sidechain and transferred to mainnet contract to withdraw with a single transaction.

vncoelho commented 1 year ago

Hi @roman-khimov,

perhaps this CheckData interop is a good direction to go.

Is not the current System.Runtime.LoadScript able to handle all this if we use specific calls inside it? If we pass a complex script with all rules.

roman-khimov commented 1 year ago

We can pass invocation/verification scripts (most likely a concatenation of those) into LoadScript now and get a result, but the problem is that Checksig/Checkmultisig calls will check for a signature of script container (transaction) and not some abstract data. Of course we can have arbitrary logic in scripts, but how do we tie this logic to Neo accounts if this logic is not a verification script?

roman-khimov commented 1 month ago

One drawback of this approach is that signature checks are moved to execution phase, so they inherently become sequential (unlike transaction verification that can be performed concurrently). For some applications it's still OK this way, for some performance requirements can be higher.

vncoelho commented 1 month ago

That is something I personally loved on NEO2, the ability of account abstraction was huge. However, @roman-khimov , as you recently mentioned in your comment, make everything on the execution phase is not a good direction.

roman-khimov commented 1 month ago

The only other way here is moving this into transaction. Make it a container for (an array of)

data to check (or a hash of it) address to check this data against invocation script verification script (can be null for contracts)

Either in version 1 or just via some new attribute. This way the check could be performed as a part of a regular transaction verification and other code (like entry script) could access this data from transaction. Then no interop is needed, but this goes deeper at the same time.