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

Allow contracts to load script at runtime dynamically #2756

Merged
merged 12 commits into from
Sep 16, 2022

Conversation

erikzhang
Copy link
Member

No description provided.

@roman-khimov
Copy link
Contributor

Can we at least postpone it after 3.3.0?

@erikzhang
Copy link
Member Author

Can we at least postpone it after 3.3.0?

Sure.

@erikzhang
Copy link
Member Author

3.3.0 is released. Now we can review this.

@roman-khimov
Copy link
Contributor

I wouldn't do this at all. Because:

Is there any real problem that requires this as a part of solution?

@erikzhang
Copy link
Member Author

I can do it even today.

public void Eval(byte[] script)
{
    ByteString nef = BuildNef(script);
    string manifest = BuildManifest(script);
    var contract = ContractManagement.Deploy(nef, manifest);
    Contract.Call(contract.Hash, "main", CallFlags.All);
}

So it at least doesn't bring new problems.

Is there any real problem that requires this as a part of solution?

I'm trying to build a dapp with permission management. Usually, when we need to set an administrator, we must explicitly tell the contract which one our administrator account is. But if our administrator account is not fixed, such as consensus address or committee address, there is no way to express it by a fixed address. With dynamic script loading, we can tell the contract the logic of "how to get the admin address". This is equivalent to a contract that can store a piece of logic, not just data. I think it's an improvement, and I didn't expect any security issues with this. If there is, we can limit it, but at least we should discuss it.

@roman-khimov
Copy link
Contributor

I can do it even today.

This contract will stay on-chain and the invocation will cost you quite some GAS. Real dynamic code is a different problem to me.

With dynamic script loading, we can tell the contract the logic of "how to get the admin address"

What prevents this logic to be implemented as a simple contract method? Of a contract that can be updated, if need be.

@erikzhang
Copy link
Member Author

What prevents this logic to be implemented as a simple contract method?

Because I am a user, not the contract owner. My plan is to allow the users to store logic in a contract.

@erikzhang
Copy link
Member Author

Imagine a contract with many users, and each user stores data in the contract. Each user can designate administrators for their data. And allow administrators not to be fixed, but to pass through a logic. An example of a non-fixed administrator is a consensus address or committee address.

@shargon
Copy link
Member

shargon commented May 31, 2022

I can do it even today.

@erikzhang Is right, we can do it right now...

@roman-khimov
Copy link
Contributor

An example of a non-fixed administrator is a consensus address or committee address.

We don't store it explicitly, but this can be done (and there can be an event with it like in current #2764), it all boils down to an address anyway. And it's contract-derived, following well-known update logic. Users that really want this can deploy regular contracts and use their addresses.

@erikzhang Is right, we can do it right now...

The contract deployed can be destroyed in the same transaction, making it somewhat ephemeral, that's true (but even in this case there will be some events generated). This loophole can be easily closed though.

@erikzhang
Copy link
Member Author

erikzhang commented Jun 1, 2022

We don't store it explicitly, but this can be done (and there can be an event with it like in current #2764), it all boils down to an address anyway.

But how do you tell the contract that "I want the committee address to be the administrator of my data"? And how do you specify arbitrary logic? For example, "the multi-sig address of the top 10 richest neo holders". Don't forget that I am not the owner of the contract, I'm just a user.

With this update, I can tell the contract that, "I can't tell you exactly which address to be the administrator, but I can tell the logic of how to obtain it."

@roman-khimov
Copy link
Contributor

One can deploy contract V and use V as the administrator address in contract A (I suppose A is just doing CheckWitness(V)). V address won't change over time, but the logic implemented in verify can do anything needed, get something from invocation scripts, get data from other contracts, CheckWitness(), etc.

@erikzhang
Copy link
Member Author

Yes, it works. But deploying a contract is a bit expensive, and usually the logic of getting an address is very simple logic, and it's not worth deploying a contract for this.

@erikzhang
Copy link
Member Author

And, by contrast, specifying a contract V is actually more dangerous. For example, if I specify that V is ContractManagement and the method to execute is Destroy.
But if I specify a piece of dynamic code, it will not introduce any danger. Because of the dynamic code to call ContractManagement.Destroy nothing happens.

@devhawk
Copy link
Contributor

devhawk commented Jun 2, 2022

What's the use case for this feature? Regardless of what does or does not work today, what is the problem this is intended to solve? How would we see developers using this feature?

Note, I'm with @roman-khimov - this seems very dangerous.

@erikzhang
Copy link
Member Author

this seems very dangerous.

The script is loaded in a different context, so it's not dangerous. This is the same as calling a contract, but saves the cost of deploying a contract.

@devhawk
Copy link
Contributor

devhawk commented Jun 2, 2022

The script is loaded in a different context, so it's not dangerous. This is the same as calling a contract, but saves the cost of deploying a contract.

I'm not sure that's enough protection.

Can you provide an example of how this might be used? Without a use case, I don't see why we would add this

Also, would a dynamically loaded script be able to store data and raise notifications? Data seems like it would be an issue since there is no entry in the contract table.

@erikzhang
Copy link
Member Author

Suppose there is a DAO contract that allows users to create an organization and set up administrative accounts according to the organization's rules. Since users can create arbitrary rules for the organization, the contract cannot know the specific content of the rules in advance, so these rules must be described in dynamic scripts.

@vncoelho
Copy link
Member

This looks a good feature, in fact, we have been always supporting this like the opeval.

@erikzhang
Copy link
Member Author

Go?

@vncoelho
Copy link
Member

I am double checking with @igormcoelho some details, soon we will test here.

shargon
shargon previously approved these changes Jul 19, 2022
@roman-khimov
Copy link
Contributor

But deploying a contract is a bit expensive

BTW, its cost is a feature to me. This PR allows to deploy some cheap shim contract that will accept new code as data, store it as data and invoke as needed. Any updates to this code will also be silent (unlike regular contract updates). So it will make #2788 problem much worse.

Dynamic code won't be able to use CheckWitness normally as well because in general we don't know the hash of the dynamic script in advance.

And how about loading the entry script once again as dynamic one to try having some fun with CalledByEntry checks or some other contract-specific thing (the result of this is totally dependent on the entry script contents which can be anything)? For example, I deploy a contract and ask someone to send 1 GAS to this contract's address. The entry script is rather trivial and will invoke GAS contract that then will invoke onNEP17Payment method of my contract. In this handler I'll load the entry script as dynamic once again, it'll call GAS contract and I think engine.CallingScriptHash == engine.EntryScriptHash will work for it to satisfy CheckWitness, so the contract will get one more GAS. Rinse, repeat and drain user's balance completely. Sure, this loophole can be closed, but how many of them we don't know yet?

I don't think anything changed on my side, this feature will bring more problems than it'll solve. Also, from a pure development cycle perspective we're supposed to be rolling 3.4.0 ASAP now (#2787) and merging this right before the release is also questionable to me.

Copy link
Member

@shargon shargon left a comment

Choose a reason for hiding this comment

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

Tested @superboyiii ?

@roman-khimov
Copy link
Contributor

And how about loading the entry script once again as dynamic one to try having some fun with CalledByEntry checks or some other contract-specific thing (the result of this is totally dependent on the entry script contents which can be anything)?

Fixed.

I wanted to suggest a script hash check to forbid (re)loading the entry script completely, but then I thought that dynamic code can add NOP to any kind of entry script which of course will invalidate old CheckWitness check, but still will allow to try some luck with various other scopes that might be used. Dynamic code can also call getTransactionSigners() and try poking at them (calling transfer(signer, me...) on various contracts). It's no different from regular contracts wrt this and proper scoping solves it, but dynamic code makes these tricks a bit easier to perform. And when any bad behaviour is found we have PolicyContract.BlockAccount for regular contracts while we have nothing for dynamic code (and probably can't have anything).

Some brave souls may also try invoking dynamic code in the verification context. Or maybe not brave, but just not careful enough to see that it happens in some contract internals. This might also be an interesting experience, although not very likely to happen.

Just some random thoughts, sorry for the noise.

@shargon shargon mentioned this pull request Jul 22, 2022
@erikzhang
Copy link
Member Author

erikzhang commented Jul 23, 2022

And when any bad behaviour is found we have PolicyContract.BlockAccount for regular contracts while we have nothing for dynamic code (and probably can't have anything).

@roman-khimov You convinced me. So how about only allowing dynamic scripts to have the CallFlags.ReadOnly permission?

@vncoelho
Copy link
Member

@erikzhang,sounds good, then, it really comes as we previously pushed in the past.
It is nice that you emphasized that and motivated the change, Roman.

@igormcoelho has highlighted it again as well.

Just to emphasize that, in theory, lambda functions are typically pure. This means that, if we want to play safe, it could make sense to disable storage reads and writes from the scope of a dynamic contract (this has the effect of being pure and only depends on its parameter). It could also be forbidden to process notifications, or even launch notifications... we can also limit the number of outputs (to zero or one), preventing collateral effects and so on... sorry if this has been discussed already, I took a quick read on the thread. 

@roman-khimov
Copy link
Contributor

So how about only allowing dynamic scripts to have the CallFlags.ReadOnly permission?

That will greatly reduce the attack surface, so it's an improvement for sure (that also allows to simplify some of the implementation details).

But I'm still not convinced we need it. It's a cool feature, no doubt about it, but:

  • we know it can be dangerous
  • I'm not sure we fully understand all of the potential consequences
    Even with CallFlags.ReadOnly dynamic code can THROW or ABORT. And it can do so based on current state and things like System.Runtime.GetNotifications. Can this be leveraged in some way? I don't know.
  • the scenario with dynamic address outlined above seems to be very specific and not something that will be used a lot

So to me it's a choice between a number of not completely understood risks and a problem that affects something like three users and can be solved in other way. To me it's not worth doing. We need more use cases with wider potential audience and we need more of methodical research on this topic before accepting this.

@erikzhang
Copy link
Member Author

We can merge #2796 first.

# Conflicts:
# src/Neo/Network/P2P/Payloads/Conditions/CalledByEntryCondition.cs
# src/Neo/SmartContract/ExecutionContextState.cs
@erikzhang
Copy link
Member Author

So how about only allowing dynamic scripts to have the CallFlags.ReadOnly permission?

Done. I don't think there is any risk now.

@superboyiii superboyiii mentioned this pull request Sep 5, 2022
@ProDog
Copy link
Contributor

ProDog commented Sep 5, 2022

Tested OK.
Neo.SmartContract.Framework needs to be updated.

@erikzhang
Copy link
Member Author

If there are no objections, I merge it.

@vncoelho
Copy link
Member

From our side we are in agreement.

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

Successfully merging this pull request may close these issues.

None yet

7 participants