Syntax for the upcoming features (factories, code introspection)

We are making a design decision now and we need your voice.

Since we are introducing contract factories we need to agree on the new syntax on the Sophia side of the new features. The time is up as the opcodes are already in the testing phase.

Just to remind: we are introducing 3 new primitives:

  • BYTECODE_HASH – takes a contract and returns the hash of its bytecode. Fairly simple.
  • CREATE – takes a contract bytecode (new thing), deploys it and returns contract as a callable value.
  • CLONE – just like create but takes an existing contract and clones it. It shall have a separate state and balance that are set up from scratch (no state cloning). It is more efficient than CREATE since we can share code references and therefore the gas price is significantly lower.

Code introspection

Here the situation seems to be obvious. There will be a new function Chain.bytecode_hash : contract => option(hash) that takes a contract and returns hash of it. Will fallback to None on failure.

One of the ideas brought by @cytadela8 is if we should charge gas for the introspected bytecode size as one could deploy a big contract and spam this operation (making the execution costly). On the other hand the VM could just cache the results of it.

Contract cloning

For CLONE my idea is to add a new function Chain.clone with a named argument contract/clonee (to be discussed), optional protection annotation and all the arguments that its init is going to take. It returns a thing of the same type as the cloned contract. Example:

contract Clonee =
  type state = int
  entrypoint init : int => state

contract Main =
  entrypoint duplicate(c : Clonee) : Clonee =
    Chain.clone(contract = c, protected = false, 2137)

I was thinking about the OOP syntax c.clone(21237), but this would collide with a potential clone entrypoint of c. Another possibility is to introduce new keyword for this: new c(2137), but not sure how intuitive would it be.

Question to you: the current remote calls support limitation of gas for the execution. Do you want to have this option for cloning as well? Case is that hypothetically init can take a lot of resources. This is an important thing to agree on if we want to have this, because this affects the behavior of the VM and the arities of FATE opcodes.

Arbitrary contract creation

This will have the biggest impact on the syntax and interface. At the current state Sophia allows only one contract definition and multiple contract declarations (for the purpose of remote calls). Moreover there is an assumption that the latest toplevel entity is the main contract. While it was quite obvious back then which contract was the “main” one, as it had to be also the only one that contains entrypoints’ definitions not only type declarations, now there would be some ambiguity if we do it in the most naive way:

contract FungusToken =
  entrypoint init() = ...

contract FungibleToken =
  entrypoint init() = ...

contract FunnyToken =
  entrypoint init() = ...

…which one of these three is the main one and which are the deployable side contracts? The answer is known to devs with some insight – the last one as stated – but this is very misleading and I believe could be used to tricky frauds.

My proposal is to get rid of that “last contract” assertion and change the syntax to the following:

// Main contract
contract Main = ...

// Contract declaration for remote calls and cloning
contract type RemoteContract = ... 

// Contract definition for deployment,
// but also remote calls and cloning
contract schema DeployableContract = ...

Note that the position of the main contract is now arbitrary and there are syntactic changes:

  • new schema keyword that is used to indicate a independent contract definition that is used only as a type declaration and a base for the new “create” feature
  • requirement to add type between contract keyword and contract’s name in contract type declaration

While I am pretty convinced to the type keyword, I have doubts about schema. How would you describe it? I decided not to change the main contract definition as it would break a lot of existing contracts. Now only those using remote calls are affected to a little extent.

Next thing is the syntax for contract deploy. Some ideas for that:

  • let c = Chain.create(SomeContract, arg1, arg2)
  • let c = SomeContract.create(arg1, arg2)
  • let c = SomeContract(arg1, arg2)
  • let c = new SomeContract(arg1, arg2)
  • let c : SomeContract = Chain.create(arg1, arg2) // thanks Ulf!

The create word is also a matter of discussion – what would you say for deploy?

Personally I like the new keyword, despite Java PTSD.

Gas

Proposed gas prices:

  • CREATE – 10000
  • CLONE – 1000
  • BYTECODE_HASH – 100

btw

Btw, CLONE and CREATE have optional value named argument just like remote calls.

Related PRs

Summary

I need your opinions on the following dillemas:

  • Do we want optional gas limitation on CREATE and CLONE?
  • How do you see the syntax for contract cloning?
  • How do you see the syntax for contract creation?
  • What do you think about the proposed syntax for contract declarations? Asking especially about the schema keyword.
  • Do we want to have some special gas treatment for BYTECODE_HASH?

Stay safe and well typed!

CC @contributor

10 Likes

Personally I would go for new keyword. Also adding some syntax sugger to deploy each contract only once would be good (something like first call new MyChild deploys, any subsequent clones)

1 Like

I agree, new keyword sounds goof

1 Like
  • Gas limits make sense. I see no reason why not.
  • Cloning syntax looks good
  • For creation I would suggest
    let c : SomeContract = Chain.create(arg1, arg2)
    
    This doesn’t introduce any new syntax and harmonizes with Chain.clone.
  • Not sure about the schema question. Maybe child contract for the to-be-created contract and main contract for the main one?
8 Likes

Hey @radrow.chain, awesome work ! A few things to address:

  1. What is technically the difference between CLONE and CREATE? I guess if CREATE deploys fully new bytecode and that’s it, why not just always use CLONE, which just calls the existing code with its own state ? E.g. :
    => I have another contract definition in my contract, I deploy a new instance of it by making the new one call that bytecode but have an own state.

  2. In regards of cloning: What about the cloning by address ? Essentially as you proposed, but the argument would be an address then, not a contract code.

  3. Let’s all please stick to the Chain namespace, say no to OOP kids.
    My favourites here are

  • let c = Chain.deploy(SomeContract, arg1, arg2)
  • let c = Chain.create(SomeContract, arg1, arg2)

Opcode-wise the create would be to chose, but devs always speak of deploying a contract, creating is used in the context of writing code.

  1. For the “last contract” assertion, I would propose utilizing existing vocabulary in the blockchain space:
// Contract declaration for remote calls 
contract interface RemoteContract = ...  %% only entrypoints definitions to follow..

// Contract declaration for deployment/cloning (technically done the same way, right?)
// and calling
contract type SomeContract = .... %% fully blown code here
  1. I have a feeling we need some named parameters like solution for the CLONE/CREATE (Although i liked your simple one more) , because we also need to be able to send value, which is of course to be taken from Call.caller.

  2. Can we please get Call.data, too ? Being able to hash it allows for very very smooth multi-sig mechanisms.

Looking forward to these awesome new features !

1 Like
  1. The difference is that CLONE reuses already deployed bytecode and the main contract does not need to know its definition at all – it only requires to know its interface. The main benefit is that we save storage on the chain and we can redeploy more arbitrary contracts.
  2. Maybe I explained it inaccurately, but it actually works how you described it. Saying that clone takes a “contract” I meant an address of a live contract wrapped by Sophia contract type (like RemoteContract).
  3. I am not aware of the conventions, but in my eyes entrypoints’ definitions shouldn’t count into types?
  4. This is mentioned already in the “BTW” section
  5. Could you write some external proposal for this with some argumentation? We can do it, but this is a separate discussion

Okay, added CLONE_G instruction that asserts gas. For CREATE I don’t think this is a necessity, as the control flow doesn’t escape the contract’s code, so there is no place for a surprise. Thanks!

Is it needed to add the contract interface? Can’t it be imported?

contract Clonee =
  type state = int
  entrypoint init : int => state

contract Main =
  entrypoint duplicate(c : Clonee) : Clonee =
    Chain.clone(contract = c, protected = false, 2137)

e.g.

import Clonee from "./contracts/Clonee.aes"

contract Main =
  entrypoint duplicate(c : Clonee) : Clonee =
    Chain.clone(contract = c, protected = false, 2137)

Same question with new?

1 Like

Apparently we don’t support any reasonable way of “importing” things (see this and this). Regular include (which blatantly pastes the source in place) would do the work though.

2 Likes

You meant 5. :wink:

4 was about the fact that we also need to be able to send along value !
Chain.clone(contract = c, protected = false, value = Call.value, 2137)

ah by the way, what stands protected for ?

Here is the proposal: Let ̶c̶̶a̶̶l̶̶l̶̶.̶̶c̶̶a̶̶l̶̶l̶̶e̶̶r̶ Call.data (typo, thanks @hanssv.chain) be a reference to the (encoded/decoded?) calldata bytes. So by hashing it you would be able to check whether two calls had the same intention to do the same thing(s).

2 Likes

Call.caller already means something else though…

1 Like

Uhm, yes. The forum has renumerated my list. As I said, there will be an optional value named argument for both of them. Regarding protected - this should work the same as in remote calls

2 Likes

Is adding Call.data consensus breaking?

Good question, is the call data available “as such” in the contract execution @hanssv.chain ?

1 Like

It is consensus breaking as we alter the VM. Also afair it is not available there, but one could just add it to the engine state for the purpose of it.

1 Like

Next proposition: main contract for main contract and contract for side contract defs. However, when there is only one contract defined in the top level, the main keyword can be omitted.

3 Likes

that’s actually a good idea.

1 Like

contract for contracts, and interface for contract interfaces perhaps?

1 Like

well, the interface is only for the interfaces, a.k.a. only function heads & no code bodies.

1 Like

Ah, yes, sloppy writing by me - we need three different things and I think we got sensible suggestions.

2 Likes