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

Smart Contract Interpreter v1 (mutable bindings/modules/for loop) #903

Conversation

bchamagne
Copy link
Member

@bchamagne bchamagne commented Feb 16, 2023

Description

I am creating this PR on top of branch #895 .
Here are the features of this new interpreter:

  1. scoped mutable bindings
  2. functions grouped into modules
  3. for loop

I reused as much of the previous interpreter as possible. You'll often see Version0 calls from within Version1.
This PR is so big, the diff will not help you. To review, just open the /archethic/contracts/interpreter folder and read everything from there (you can skip Version0 folder, it is the previous interpreter)

Fixes #894

TODO

  • for loop
  • documentation

Type of change

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

Many unit tests were added.
Tested manually with the transactionBuilder

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

@apoorv-2204
Copy link
Contributor

very complex,
may be the biggest pr of aenode

@bchamagne bchamagne changed the title Smart Contract Interpreter v1 (mutable bindings & modules) Smart Contract Interpreter v1 (mutable bindings/modules/for loop) Feb 17, 2023
@bchamagne
Copy link
Member Author

bchamagne commented Feb 20, 2023

Here's the code proposition to handle the faucet via a smart contract:

@version 1
condition inherit: [
	uco_transfers: true,
	content: true
]

# every hours, send UCOs to the addresses that asked for it
# a chain can do the faucet once every 6 hours
actions triggered_by: interval, at: "0 0 * * *" do
	now = Time.now() # Current time
	faucet_delay = 6 * 60 * 60 # 6 hour delay
	calls = Contract.get_calls() # list of calls since last tick

	# work only if there is at least one call
	if calls != [] do
		# remove the addresses that have waited long enough
		content = ""
		for line in Regex.scan(contract.content, "^(\\w+),(\\d+)$") do
			address = List.at(line, 0)
			at = List.at(line, 1)

			if now - String.to_int(at) <= faucet_delay do
				content = "#{content}#{address},#{at}\n"
			end
		end

		# get the genesis and if allowed, add the transfer
		for call in calls do
			genesis_address = Chain.get_genesis_address(call.address)

			if Regex.extract(content, genesis_address) == "" do
				# transfer 5 uco
				Contract.add_uco_transfer(to: genesis_address, amount: 5)

				# update state
				content = "#{content}#{genesis_address},#{now}\n"
			end
		end

		# update the state
		Contract.set_content(content)
	end
end

Works fine.

FYI, here is the same contract with interpreter version 0

condition inherit: [
	uco_transfers: true,
	content: true	
]

# every minutes, send UCOs to all _allowed_ addresses that asked for it
actions triggered_by: interval, at: "0 * * * *" do
	now = timestamp() # Current time
	faucet_delay = 2 * 60 # 2 min delay
	calls = get_calls() # amount of calls since last tick
	
	# work only if there is 1..* call
	if calls != [] do 
		lines = regex_scan(contract.content, "^(\\w+),(\\d+)$") # Returns a list of [address, timestamp]

		# remove the addresses that have waited long enough
		forbidden_addresses = reduce(lines, "", fn line, acc -> 
			address = at(line, 0)
			faucet_at = at(line, 1)
			
			if now - int(faucet_at) > faucet_delay do 
				acc
			else
				concat(acc, "#{address},#{faucet_at}\n")	
			end
		end)

		# convert to genesis, check if allowed and remove dups
		unique_allowed_genesis_addresses = reduce(calls, [], fn tx, acc ->
			genesis_address = get_genesis_address(tx.address)
			
			if !in?(genesis_address, acc) and regex_extract(forbidden_addresses, genesis_address) == "" do 
				prepend(acc, genesis_address)
			else
				acc
			end
		end)

		# even if there is 0 allowed address, we want to create a new transaction to clear the inputs
					
		# send 5 UCOs to all of them
		transfers = reduce(unique_allowed_genesis_addresses, [], fn address, acc ->
			prepend(acc, [to: address, amount: 500000000])
		end)
	
		# update the content
		updated_content = reduce(unique_allowed_genesis_addresses, forbidden_addresses, fn address, acc ->
			concat(acc, "#{address},#{now}\n")
		end)

		add_uco_transfers(transfers)
		set_content(updated_content)
	end
end

@bchamagne
Copy link
Member Author

I will wait for feedback before working on documentation

@samuelmanzanera
Copy link
Member

Except few comments, really great work 🚀

@bchamagne
Copy link
Member Author

FYI, there's a way to replace Process.get/1and Process.put/2 by using macro hygiene instead.

var!(:toto, :context1)
var!(:toto, :context2)

Since they are in a different context, here the 2 toto are different and there's no risk of collision.
https://elixir-lang.org/getting-started/meta/macros.html#macro-hygiene

The issue with this is that the context is an atom. Which means we'd have to do some tricky atom manipulation for scopes.

@samuelmanzanera
Copy link
Member

FYI, there's a way to replace Process.get/1and Process.put/2 by using macro hygiene instead.

var!(:toto, :context1) var!(:toto, :context2)

Since they are in a different context, here the 2 toto are different and there's no risk of collision. https://elixir-lang.org/getting-started/meta/macros.html#macro-hygiene

The issue with this is that the context is an atom. Which means we'd have to do some tricky atom manipulation for scopes.

No worry because the Process directory is by process as long as the execution of a contract is isolated into a single one, no risk of collision.

end

{new_node, acc}
# new_node =
Copy link
Member Author

Choose a reason for hiding this comment

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

I disabled Contract.get_calls() from the condition blocks for now, because it'd require a quorum and not a read from DB. (because the condition inherit validator is not the worker).

@samuelmanzanera samuelmanzanera merged commit 7e92083 into archethic-foundation:894-sc-versioning Mar 8, 2023
bchamagne added a commit to bchamagne/archethic-node that referenced this pull request Mar 9, 2023
…ation#903)

* Add modules for library and transaction statements
* Enable loops
* Improve testing
* Improve some error messages
* Enable ranges
* Handle map[field] syntax
* Allow code blocks in the condition interpreter
* Support several scopes
* Fix double execution of contract
Neylix pushed a commit that referenced this pull request Mar 14, 2023
* Add modules for library and transaction statements
* Enable loops
* Improve testing
* Improve some error messages
* Enable ranges
* Handle map[field] syntax
* Allow code blocks in the condition interpreter
* Support several scopes
* Fix double execution of contract
@Bantarus
Copy link

Bantarus commented Apr 23, 2023

Hello @bchamagne , @Neylix , @samuelmanzanera ,

Bravo for the work on the interpreted language !
I hope it is not too late but here my feedbacks on the language form .

Before i say anything , you can take into account i'm not familiar with writting language interpreter with elixir so take what is coming considering i'm a noobie here.

  • I would go with a syntax even more similar to Javascript/typescript ( like replacing "do .. end" bloc with bracket {} ) since it is the most used language in web, to facilitate the onboarding of web developers.
  • It would be nice to see a structured language like in oriented progamming language , could you provide an example if i have multiples actions ? Can i call an action ( without trigger ) within another action ? I understand that the action is our function paradigm.
  • We could use the condition as a rooting feature like in web api to execute a certain portion/action of the code in the smart contract, not only an absolute condition at the smart contract level.

Here an exemple to illustrate my words.

@version 1

action processFaucetRequests {
  let now = Time.now() // Current time
  let faucet_delay = 6 * 60 * 60 // 6 hour delay
  let calls = Contract.get_calls() // list of calls since last tick

  // work only if there is at least one call
  if calls != [] {
    // remove the addresses that have waited long enough
    let content = ""
    for line in Regex.scan(contract.content, "^(\\w+),(\\d+)$") {
      let address = List.at(line, 0)
      let at = List.at(line, 1)

      if now - String.to_int(at) <= faucet_delay {
        content = "#{content}#{address},#{at}\n"
      }
    }

    // get the genesis and if allowed, add the transfer
    for call in calls {
      let genesis_address = Chain.get_genesis_address(call.address)

      if Regex.extract(content, genesis_address) == "" {
        // transfer 5 uco
        Contract.add_uco_transfer(to: genesis_address, amount: 5)

        // update state
        content = "#{content}#{genesis_address},#{now}\n"
      }
    }

    // update the state
    Contract.set_content(content)
  }
}

contract FaucetContract {
  triggers {
    interval("0 0 * * *") {
      processFaucetRequests()
    }
  }

  actions {
    processFaucetRequests {
      condition inherit {
        uco_transfers: true,
        content: true
      }
    }
  }
}

Also the trigger could be a decorator of an action like this "@trigger_datetime("2023-05-01T00:00:00Z")".
Voila c'est tout ^^.

Bonne journée :)

@bchamagne
Copy link
Member Author

Hey @Bantarus , thank you for your feedback, I appreciate.
Here's my thoughts:

The language use an intermediary AST which is elixir's. This helped us tremendously because we did not have to create an entire interpreter. Therefore we are limitated by what is considered "valid elixir": we cannot change the do...end syntax by curly braces.

I think the named action block, without trigger, is a really nice way to have functions in the language. We'll definitely keep that in mind.

What you propose is very similar to something I drafted a few months ago:

contract([
	condition_inherit: fn out_tx ->
		 assert Regex.match?(out_tx.content, ".*")
	end,
	triggers: [
		[
			trigger: "0 0 0 * * * *",
			code: fn contract ->
				Contract.set_content "hello"
			end,
			condition: []
	    ],
		[
			trigger: "transaction",
			conditions: [
				transaction: fn in_tx ->
					assert List.first(in_tx.uco_transfers).amount > 0
				end
			],
			code: fn contract, in_tx ->
				Contract.set_type "transfer"
				Contract.add_uco_transfer to: "000030831178cd6a49fe446778455a7a980729a293bfa16b0a1d2743935db210da76", amount: 1337 
			end
	    ]	
	]
])

Anyway, it's too big of a breaking change for now. Maybe in a later version?
Cheers!

@samuelmanzanera
Copy link
Member

samuelmanzanera commented May 2, 2023

Hello. Thanks for the contribution.
I just wanted to mention some motivation about this syntax and the smart contract language regarding the points you mentioned. Hopefully this would help to understand the choices we made.

I would go with a syntax even more similar to Javascript/typescript ( like replacing "do .. end" bloc with bracket {} ) since it is the most used language in web, to facilitate the onboarding of web developers.

As mentioned @bchamagne, we are using Elixir to build the interpreted language, hence it's a subset of the Elixir language. But even without it, we designed the language to be as a domain specific language, close to a specification, while allowing developers to leverage a programmatic approach. In that direction, the language is then designed to be easy to read and understand for non-developers if needed.
Hence, the do...end statement helps to understand what happens inside a block like an algorithm handwritten and can be "read". Another assumption is because the Elixir syntax sugar (do..end) is very close to Ruby which is one of the most used language worldwide and being known to be simple to learn and read.

It would be nice to see a structured language like in oriented progamming language , could you provide an example if i have multiples actions ? Can i call an action ( without trigger ) within another action ? I understand that the action is our function paradigm.

The purpose of this language is not to be a generic programming language but a dedicated one for the Archethic's interpreter.
The action for me is not really a function paradigm but more a trigger code. The reason is to avoid some security flows known in Solidity being a Turing complete language. Since the beginning, we didn't want to make a Turing complete language. We have to see the action block as a code which is triggered by an even in the network, like a serverless function/lambda like the AWS Cloud.
However, we are still thinking of a way to branch different actions block based on a name. Hence, the clients and dApps could target a specific action block like a function name.

@Bantarus
Copy link

Hello @samuelmanzanera ,

Thanks you for the detailed answer.
C'est clair comme de l'eau de roche :)

Keep up the incredible work !

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

5 participants