├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── precompiled │ │ ├── __init__.py │ │ ├── updated_submission.py │ │ └── compiled_token.py │ ├── test_sys_contracts │ │ ├── __init__.py │ │ ├── bad_lint.s.py │ │ ├── good_lint.s.py │ │ ├── module4.py │ │ ├── module6.py │ │ ├── module7.py │ │ ├── module8.py │ │ ├── compile_this.s.py │ │ ├── module5.py │ │ ├── module2.py │ │ ├── module3.py │ │ ├── module1.py │ │ ├── module_func.py │ │ └── currency.s.py │ ├── contracts │ │ ├── thistest2.py │ │ ├── proxythis.py │ │ ├── exception.s.py │ │ ├── currency.s.py │ │ └── submission.s.py │ ├── loop_client_test.sh │ ├── test_stdlib_hashing.py │ ├── test_driver_tombstones.py │ ├── test_client_keys_prefix.py │ ├── test_state_management.py │ ├── test_context_data_struct.py │ ├── test_module.py │ ├── test_client.py │ ├── test_revert_on_exception.py │ ├── test_runtime.py │ ├── test_new_driver.py │ ├── test_datetime.py │ └── test_imports_stdlib.py ├── security │ ├── __init__.py │ └── contracts │ │ ├── call_infinate_loop.s.py │ │ ├── constructor_infinate_loop.s.py │ │ ├── import_hash_from_contract.s.py │ │ ├── infinate_loop.s.py │ │ ├── hack_tokens.s.py │ │ ├── builtin_hack_token.s.py │ │ ├── get_set_driver.py │ │ ├── get_set_driver_2.py │ │ ├── con_inf_writes.s.py │ │ ├── double_spend_gas_attack.s.py │ │ ├── submission.s.py │ │ └── erc20_clone.s.py ├── integration │ ├── __init__.py │ ├── test_contracts │ │ ├── client.py │ │ ├── __init__.py │ │ ├── child_test.s.py │ │ ├── contracting.s.py │ │ ├── import_this.s.py │ │ ├── i_use_env.s.py │ │ ├── pass_hash.s.py │ │ ├── dynamic_import.s.py │ │ ├── parent_test.s.py │ │ ├── bad_time.s.py │ │ ├── import_test.s.py │ │ ├── importing_that.s.py │ │ ├── inf_loop.s.py │ │ ├── hashing_works.s.py │ │ ├── construct_function_works.s.py │ │ ├── orm_variable_contract.s.py │ │ ├── orm_hash_contract.s.py │ │ ├── builtin_lib.s.py │ │ ├── json_tests.s.py │ │ ├── mathtime.s.py │ │ ├── time_storage.s.py │ │ ├── owner_stuff.s.py │ │ ├── modules │ │ │ ├── module4.s.py │ │ │ ├── module6.s.py │ │ │ ├── module7.s.py │ │ │ ├── module8.s.py │ │ │ ├── module5.s.py │ │ │ ├── module2.s.py │ │ │ ├── module3.s.py │ │ │ ├── dynamic_import.s.py │ │ │ ├── all_in_one.s.py │ │ │ └── module1.s.py │ │ ├── thing.s.py │ │ ├── orm_no_contract_access.s.py │ │ ├── con_pass_hash.s.py │ │ ├── orm_foreign_hash_contract.s.py │ │ ├── orm_foreign_key_contract.s.py │ │ ├── time.s.py │ │ ├── constructor_args_contract.s.py │ │ ├── dater.py │ │ ├── private_methods.s.py │ │ ├── dynamic_import.py │ │ ├── bastardcoin.s.py │ │ ├── exception.py │ │ ├── foreign_thing.s.py │ │ ├── currency.s.py │ │ ├── leaky.s.py │ │ ├── submission.s.py │ │ ├── float_issue.s.py │ │ ├── stubucks.s.py │ │ ├── erc20_clone.s.py │ │ ├── tejastokens.s.py │ │ ├── dynamic_importing.s.py │ │ └── atomic_swaps.s.py │ ├── test_complex_object_setting.py │ ├── test_datetime_contracts.py │ ├── test_constructor_args.py │ ├── test_run_private_function.py │ ├── test_executor_transaction_writes.py │ ├── test_memory_clean_up_after_execution.py │ ├── test_builtins_locked_off.py │ ├── test_senecaCompiler_integration.py │ ├── test_seneca_client_randoms.py │ ├── test_rich_ctx_calling.py │ └── test_stamp_deduction.py └── performance │ ├── __init__.py │ ├── test_contracts │ ├── __init__.py │ ├── modules │ │ ├── module4.s.py │ │ ├── module6.s.py │ │ ├── module7.s.py │ │ ├── module8.s.py │ │ ├── module5.s.py │ │ ├── module2.s.py │ │ ├── module3.s.py │ │ ├── dynamic_import.s.py │ │ ├── all_in_one.s.py │ │ └── module1.s.py │ ├── submission.s.py │ └── erc20_clone.s.py │ ├── prof_transfer.py │ └── test_transfer.py ├── src └── contracting │ ├── __init__.py │ ├── contracts │ ├── __init__.py │ ├── thistest2.py │ ├── proxythis.py │ └── submission.s.py │ ├── execution │ ├── __init__.py │ ├── runtime.py │ ├── module.py │ └── tracer.py │ ├── stdlib │ ├── __init__.py │ ├── bridge │ │ ├── __init__.py │ │ ├── crypto.py │ │ ├── hashing.py │ │ ├── access.py │ │ ├── orm.py │ │ ├── imports.py │ │ ├── random.py │ │ └── decimal.py │ └── env.py │ ├── storage │ ├── __init__.py │ ├── contract.py │ ├── hdf5.py │ └── encoder.py │ ├── compilation │ ├── __init__.py │ ├── parser.py │ ├── whitelists.py │ └── compiler.py │ └── constants.py ├── .github ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ └── publish.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── LICENSE ├── .gitignore ├── pyproject.toml ├── release.sh ├── examples └── 01 A very simple Counter contract.ipynb └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/contracting/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/security/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/performance/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/contracting/contracts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/contracting/execution/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/contracting/stdlib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/contracting/storage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/precompiled/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/contracting/compilation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/contracting/stdlib/bridge/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/client.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/bad_lint.s.py: -------------------------------------------------------------------------------- 1 | def no_exports(): 2 | return 'hahaha' -------------------------------------------------------------------------------- /tests/integration/test_contracts/child_test.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def get_value(): 3 | return 'good' -------------------------------------------------------------------------------- /tests/integration/test_contracts/contracting.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def hello(): 3 | return 'hello' -------------------------------------------------------------------------------- /tests/integration/test_contracts/import_this.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def howdy(): 3 | return 12345 -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/good_lint.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def exports(): 3 | return 'huehuehue' -------------------------------------------------------------------------------- /tests/integration/test_contracts/i_use_env.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def env_var(): 3 | return this_is_a_passed_in_variable -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/module4.py: -------------------------------------------------------------------------------- 1 | print('{} called from {}, signed by {}'.format(ctx.this, ctx.caller, ctx.signer)) -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/module6.py: -------------------------------------------------------------------------------- 1 | print('{} called from {}, signed by {}'.format(ctx.this, ctx.caller, ctx.signer)) -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/module7.py: -------------------------------------------------------------------------------- 1 | print('{} called from {}, signed by {}'.format(ctx.this, ctx.caller, ctx.signer)) -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/module8.py: -------------------------------------------------------------------------------- 1 | print('{} called from {}, signed by {}'.format(ctx.this, ctx.caller, ctx.signer)) -------------------------------------------------------------------------------- /tests/integration/test_contracts/pass_hash.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def store_on_behalf(H: Any, k: Any, v: Any): 3 | H[k] = v 4 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/dynamic_import.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def import_thing(name: str): 3 | return importlib.import_module(name) -------------------------------------------------------------------------------- /tests/security/contracts/call_infinate_loop.s.py: -------------------------------------------------------------------------------- 1 | import con_infinate_loop 2 | 3 | @export 4 | def call(): 5 | con_infinate_loop.loop() -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/compile_this.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def good_function(): 3 | return 5 4 | 5 | def another_function(): 6 | return 100 -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/module5.py: -------------------------------------------------------------------------------- 1 | import module8 2 | 3 | print('{} called from {}, signed by {}'.format(ctx.this, ctx.caller, ctx.signer)) -------------------------------------------------------------------------------- /tests/integration/test_contracts/parent_test.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def get_val_from_child(s: str): 3 | m = importlib.import_module(s) 4 | return m.get_value() 5 | -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/module2.py: -------------------------------------------------------------------------------- 1 | import module4 2 | import module5 3 | 4 | print('{} called from {}, signed by {}'.format(ctx.this, ctx.caller, ctx.signer)) -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/module3.py: -------------------------------------------------------------------------------- 1 | import module6 2 | import module7 3 | 4 | print('{} called from {}, signed by {}'.format(ctx.this, ctx.caller, ctx.signer)) -------------------------------------------------------------------------------- /tests/integration/test_contracts/bad_time.s.py: -------------------------------------------------------------------------------- 1 | old_time = time.datetime(2019, 1, 1) 2 | 3 | @export 4 | def ha(): 5 | old_time._datetime = None 6 | return old_time 7 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/import_test.s.py: -------------------------------------------------------------------------------- 1 | import contracting 2 | 3 | @export 4 | def woo(): 5 | importlib.import_module('contracting') 6 | return contracting 7 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/importing_that.s.py: -------------------------------------------------------------------------------- 1 | import con_import_this 2 | 3 | @export 4 | def test(): 5 | a = con_import_this.howdy() 6 | a -= 1000 7 | return a -------------------------------------------------------------------------------- /tests/integration/test_contracts/inf_loop.s.py: -------------------------------------------------------------------------------- 1 | @construct 2 | def seed(): 3 | i = 0 4 | while True: 5 | i += 1 6 | 7 | @export 8 | def dummy(): 9 | return 0 -------------------------------------------------------------------------------- /tests/security/contracts/constructor_infinate_loop.s.py: -------------------------------------------------------------------------------- 1 | @construct 2 | def seed(): 3 | i = 0 4 | while True: 5 | i += 1 6 | 7 | @export 8 | def dummy(): 9 | return 0 -------------------------------------------------------------------------------- /tests/integration/test_contracts/hashing_works.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def t_sha3(s: str): 3 | return hashlib.sha3(s) 4 | 5 | @export 6 | def t_sha256(s: str): 7 | return hashlib.sha256(s) 8 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/construct_function_works.s.py: -------------------------------------------------------------------------------- 1 | v = Variable() 2 | 3 | @export 4 | def get(): 5 | return v.get() 6 | 7 | @construct 8 | def seed(): 9 | v.set(42) 10 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/orm_variable_contract.s.py: -------------------------------------------------------------------------------- 1 | v = Variable() 2 | 3 | @export 4 | def set_v(i: int): 5 | v.set(i) 6 | 7 | @export 8 | def get_v(): 9 | return v.get() 10 | -------------------------------------------------------------------------------- /tests/security/contracts/import_hash_from_contract.s.py: -------------------------------------------------------------------------------- 1 | import erc20 2 | 3 | @construct 4 | def seed(): 5 | erc20.balances['stu'] = 999999999999 6 | 7 | @export 8 | def dummy(): 9 | return 0 -------------------------------------------------------------------------------- /tests/integration/test_contracts/orm_hash_contract.s.py: -------------------------------------------------------------------------------- 1 | h = Hash() 2 | 3 | @export 4 | def set_h(k: str, v: int): 5 | h[k] = v 6 | 7 | @export 8 | def get_h(k: str): 9 | return h[k] 10 | -------------------------------------------------------------------------------- /tests/security/contracts/infinate_loop.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def loop(): 3 | i = 0 4 | while True: 5 | i += 1 6 | 7 | @export 8 | def eat_stamps(): 9 | while True: 10 | pass -------------------------------------------------------------------------------- /tests/integration/test_contracts/builtin_lib.s.py: -------------------------------------------------------------------------------- 1 | import token 2 | 3 | @export 4 | def hahaha(): 5 | print('I work, fool!') 6 | 7 | @export 8 | def return_token(): 9 | return vars(token) 10 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/json_tests.s.py: -------------------------------------------------------------------------------- 1 | v = Variable() 2 | 3 | @construct 4 | def seed(): 5 | v.set([1, 2, 3, 4, 5, 6, 7, 8]) 6 | 7 | @export 8 | def get_some(): 9 | return v.get()[0:4] 10 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/mathtime.s.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | pi = Variable() 4 | 5 | @construct 6 | def seed(): 7 | pi.set(math.pi) 8 | 9 | 10 | @export 11 | def get_pi(): 12 | return pi.get() -------------------------------------------------------------------------------- /tests/integration/test_contracts/time_storage.s.py: -------------------------------------------------------------------------------- 1 | time = Variable() 2 | 3 | @construct 4 | def seed(): 5 | time.set(datetime.datetime(2019, 1, 1)) 6 | 7 | @export 8 | def get(): 9 | return time.get() 10 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/owner_stuff.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def get_owner(s: str): 3 | m = importlib.import_module(s) 4 | return importlib.owner_of(m) 5 | 6 | @export 7 | def owner_of_this(): 8 | return ctx.owner 9 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/modules/module4.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def get_context(): 3 | return { 4 | 'owner': ctx.owner, 5 | 'this': ctx.this, 6 | 'signer': ctx.signer, 7 | 'caller': ctx.caller 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/modules/module6.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def get_context(): 3 | return { 4 | 'owner': ctx.owner, 5 | 'this': ctx.this, 6 | 'signer': ctx.signer, 7 | 'caller': ctx.caller 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/modules/module7.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def get_context(): 3 | return { 4 | 'owner': ctx.owner, 5 | 'this': ctx.this, 6 | 'signer': ctx.signer, 7 | 'caller': ctx.caller 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/modules/module8.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def get_context(): 3 | return { 4 | 'owner': ctx.owner, 5 | 'this': ctx.this, 6 | 'signer': ctx.signer, 7 | 'caller': ctx.caller 8 | } 9 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/modules/module4.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def get_context(): 3 | return { 4 | 'owner': ctx.owner, 5 | 'this': ctx.this, 6 | 'signer': ctx.signer, 7 | 'caller': ctx.caller 8 | } 9 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/modules/module6.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def get_context(): 3 | return { 4 | 'owner': ctx.owner, 5 | 'this': ctx.this, 6 | 'signer': ctx.signer, 7 | 'caller': ctx.caller 8 | } 9 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/modules/module7.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def get_context(): 3 | return { 4 | 'owner': ctx.owner, 5 | 'this': ctx.this, 6 | 'signer': ctx.signer, 7 | 'caller': ctx.caller 8 | } 9 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/modules/module8.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def get_context(): 3 | return { 4 | 'owner': ctx.owner, 5 | 'this': ctx.this, 6 | 'signer': ctx.signer, 7 | 'caller': ctx.caller 8 | } 9 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/thing.s.py: -------------------------------------------------------------------------------- 1 | H = Hash() 2 | V = Variable() 3 | 4 | @construct 5 | def seed(): 6 | H['hello'] = 'there' 7 | H['something'] = 'else' 8 | V.set('hi') 9 | 10 | @export 11 | def nop(): 12 | pass 13 | -------------------------------------------------------------------------------- /tests/security/contracts/hack_tokens.s.py: -------------------------------------------------------------------------------- 1 | balances = Hash(default_value=0) 2 | 3 | @construct 4 | def seed(): 5 | balances.key = 'con_erc20.balances' 6 | balances['stu'] = 99999999999999999 7 | 8 | @export 9 | def blah(): 10 | return 1 -------------------------------------------------------------------------------- /tests/integration/test_contracts/orm_no_contract_access.s.py: -------------------------------------------------------------------------------- 1 | c = __Contract() 2 | 3 | @export 4 | def set_c(): 5 | code = ''' 6 | @export 7 | def a(): 8 | print('gottem') 9 | ''' 10 | c.submit(name='baloney', code=code, author='sys') 11 | -------------------------------------------------------------------------------- /tests/unit/contracts/thistest2.py: -------------------------------------------------------------------------------- 1 | @export 2 | def exported(): 3 | return ctx.this, ctx.caller 4 | 5 | @export 6 | def getthis(): 7 | return ctx.this, ctx.caller 8 | 9 | @export 10 | def nested_exported(): 11 | return exported() 12 | -------------------------------------------------------------------------------- /src/contracting/contracts/thistest2.py: -------------------------------------------------------------------------------- 1 | @export 2 | def exported(): 3 | return ctx.this, ctx.caller 4 | 5 | @export 6 | def getthis(): 7 | return ctx.this, ctx.caller 8 | 9 | @export 10 | def nested_exported(): 11 | return exported() 12 | -------------------------------------------------------------------------------- /tests/security/contracts/builtin_hack_token.s.py: -------------------------------------------------------------------------------- 1 | balances = Hash(default_value=0) 2 | 3 | @construct 4 | def seed(): 5 | setattr(balances, '_key', 'erc20.balances') 6 | balances['stu'] = 99999999999999999 7 | 8 | @export 9 | def blah(): 10 | return 1 -------------------------------------------------------------------------------- /tests/integration/test_contracts/con_pass_hash.s.py: -------------------------------------------------------------------------------- 1 | import con_pass_hash 2 | 3 | my_hash = Hash() 4 | 5 | @export 6 | def store(k: Any, v: Any): 7 | con_pass_hash.store_on_behalf(my_hash, k, v) 8 | 9 | @export 10 | def get(k: Any): 11 | return my_hash[k] 12 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/modules/module5.s.py: -------------------------------------------------------------------------------- 1 | import module8 2 | 3 | @export 4 | def get_context(): 5 | return { 6 | 'owner': ctx.owner, 7 | 'this': ctx.this, 8 | 'signer': ctx.signer, 9 | 'caller': ctx.caller 10 | } 11 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/orm_foreign_hash_contract.s.py: -------------------------------------------------------------------------------- 1 | fh = ForeignHash(foreign_contract='con_orm_hash_contract', foreign_name='h') 2 | 3 | @export 4 | def set_fh(k: str, v: int): 5 | fh[k] = v 6 | 7 | @export 8 | def get_fh(k: str): 9 | return fh[k] 10 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/orm_foreign_key_contract.s.py: -------------------------------------------------------------------------------- 1 | fv = ForeignVariable(foreign_contract='con_orm_variable_contract', foreign_name='v') 2 | 3 | @export 4 | def set_fv(i: int): 5 | fv.set(i) 6 | 7 | @export 8 | def get_fv(): 9 | return fv.get() 10 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/modules/module5.s.py: -------------------------------------------------------------------------------- 1 | import module8 2 | 3 | @export 4 | def get_context(): 5 | return { 6 | 'owner': ctx.owner, 7 | 'this': ctx.this, 8 | 'signer': ctx.signer, 9 | 'caller': ctx.caller 10 | } 11 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/time.s.py: -------------------------------------------------------------------------------- 1 | old_time = datetime.datetime(2019, 1, 1) 2 | 3 | @export 4 | def gt(): 5 | return now > old_time 6 | 7 | @export 8 | def lt(): 9 | return now < old_time 10 | 11 | @export 12 | def eq(): 13 | return now == old_time 14 | -------------------------------------------------------------------------------- /tests/security/contracts/get_set_driver.py: -------------------------------------------------------------------------------- 1 | import erc20 2 | driver = erc20.rt.env.get('__Driver') 3 | 4 | @construct 5 | def seed(): 6 | driver.set( 7 | key='erc20.balances:stu', 8 | value=1 9 | ) 10 | 11 | @export 12 | def dummy(): 13 | pass 14 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/constructor_args_contract.s.py: -------------------------------------------------------------------------------- 1 | var1 = Variable() 2 | var2 = Variable() 3 | 4 | @construct 5 | def seed(a, b): 6 | var1.set(a) 7 | var2.set(b) 8 | 9 | @export 10 | def get(): 11 | a = var1.get() 12 | b = var2.get() 13 | return a, b -------------------------------------------------------------------------------- /tests/integration/test_contracts/modules/module2.s.py: -------------------------------------------------------------------------------- 1 | import module4 2 | import module5 3 | 4 | @export 5 | def get_context(): 6 | return { 7 | 'owner': ctx.owner, 8 | 'this': ctx.this, 9 | 'signer': ctx.signer, 10 | 'caller': ctx.caller 11 | } 12 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/modules/module3.s.py: -------------------------------------------------------------------------------- 1 | import module6 2 | import module7 3 | 4 | @export 5 | def get_context(): 6 | return { 7 | 'owner': ctx.owner, 8 | 'this': ctx.this, 9 | 'signer': ctx.signer, 10 | 'caller': ctx.caller 11 | } 12 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/modules/module2.s.py: -------------------------------------------------------------------------------- 1 | import module4 2 | import module5 3 | 4 | @export 5 | def get_context(): 6 | return { 7 | 'owner': ctx.owner, 8 | 'this': ctx.this, 9 | 'signer': ctx.signer, 10 | 'caller': ctx.caller 11 | } 12 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/modules/module3.s.py: -------------------------------------------------------------------------------- 1 | import module6 2 | import module7 3 | 4 | @export 5 | def get_context(): 6 | return { 7 | 'owner': ctx.owner, 8 | 'this': ctx.this, 9 | 'signer': ctx.signer, 10 | 'caller': ctx.caller 11 | } 12 | -------------------------------------------------------------------------------- /tests/security/contracts/get_set_driver_2.py: -------------------------------------------------------------------------------- 1 | import erc20 2 | z = erc20.rt 3 | driver = z.env.get('__Driver') 4 | 5 | @construct 6 | def seed(): 7 | driver.set( 8 | key='erc20.balances:stu', 9 | value=1 10 | ) 11 | 12 | @export 13 | def dummy(): 14 | pass 15 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/modules/dynamic_import.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def called_from_a_far(): 3 | m = importlib.import_module('all_in_one') 4 | return m.call_me_again_again() 5 | 6 | @export 7 | def called_from_a_far_stacked(): 8 | m = importlib.import_module('all_in_one') 9 | return m.call() -------------------------------------------------------------------------------- /tests/performance/test_contracts/modules/dynamic_import.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def called_from_a_far(): 3 | m = importlib.import_module('all_in_one') 4 | return m.call_me_again_again() 5 | 6 | @export 7 | def called_from_a_far_stacked(): 8 | m = importlib.import_module('all_in_one') 9 | return m.call() -------------------------------------------------------------------------------- /tests/security/contracts/con_inf_writes.s.py: -------------------------------------------------------------------------------- 1 | hhash = Hash(default_value=0) 2 | 3 | @construct 4 | def seed(): 5 | hashed_data = 'woohoo' 6 | while True: 7 | hashed_data = hashlib.sha3(hashed_data) 8 | hhash[hashed_data] = 'a' 9 | 10 | @export 11 | def dummy(): 12 | return 0 -------------------------------------------------------------------------------- /tests/integration/test_contracts/dater.py: -------------------------------------------------------------------------------- 1 | #import datetime 2 | 3 | v = Variable() 4 | 5 | @export 6 | def replicate(d: datetime.datetime): 7 | assert d > now, 'D IS NOT LARGER THAN NOW' 8 | v.set(d) 9 | 10 | @export 11 | def subtract(d1: datetime.datetime, d2: datetime.datetime): 12 | return d1 - d2 13 | -------------------------------------------------------------------------------- /tests/unit/contracts/proxythis.py: -------------------------------------------------------------------------------- 1 | @export 2 | def proxythis(con: str): 3 | return importlib.import_module(con).getthis() 4 | 5 | @export 6 | def nestedproxythis(con: str): 7 | return importlib.import_module(con).nested_exported() 8 | 9 | @export 10 | def noproxy(): 11 | return ctx.this, ctx.caller -------------------------------------------------------------------------------- /src/contracting/contracts/proxythis.py: -------------------------------------------------------------------------------- 1 | @export 2 | def proxythis(con: str): 3 | return importlib.import_module(con).getthis() 4 | 5 | @export 6 | def nestedproxythis(con: str): 7 | return importlib.import_module(con).nested_exported() 8 | 9 | @export 10 | def noproxy(): 11 | return ctx.this, ctx.caller -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" # Type of package ecosystem (e.g., "npm", "maven") 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" # How often to check for updates 7 | open-pull-requests-limit: 10 # Maximum number of open pull requests Dependabot should have -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution‑NonCommercial 4.0 International 2 | 3 | Copyright © 2025 XIAN.org 4 | 5 | This work is licensed under CC BY‑NC 4.0. 6 | You may copy, modify, and share it **for non‑commercial purposes only**, provided that you give appropriate credit. 7 | Full legal text: https://creativecommons.org/licenses/by-nc/4.0/legalcode 8 | -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/module1.py: -------------------------------------------------------------------------------- 1 | ''' 2 | how the modules import each other. this is to test ctx.caller etc 3 | 4 | 1 5 | | | 6 | 2 3 7 | | | | | 8 | 4 5 6 7 9 | | 10 | 8 11 | 12 | ''' 13 | 14 | import module2 15 | import module3 16 | 17 | 18 | print('{} called from {}, signed by {}'.format(ctx.this, ctx.caller, ctx.signer)) 19 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/private_methods.s.py: -------------------------------------------------------------------------------- 1 | test_hash = Hash() 2 | 3 | @export 4 | def call_private(): 5 | return private() 6 | 7 | def private(): 8 | return 'abc' 9 | 10 | @export 11 | def set(k: str, v: int): 12 | test_hash[k] = v 13 | 14 | @export 15 | def set_multi(k: str, k2: str, k3: str, v: int): 16 | test_hash[k, k2, k3] = v -------------------------------------------------------------------------------- /tests/unit/loop_client_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | count=0 3 | while : 4 | do 5 | output=$(python test_client.py TestSenecaClient.test_update_master_db_with_incomplete_sb 2>&1 >/dev/null) 6 | if [[ $output == *"FAIL:"* ]]; then 7 | echo "FAILURE!!" 8 | echo "$output" 9 | break 10 | else 11 | count=$((count+1)) 12 | echo "Test passed (succ #$count)" 13 | fi 14 | done 15 | 16 | -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/module_func.py: -------------------------------------------------------------------------------- 1 | supply = Variable() 2 | balances = Hash(default_value=0) 3 | 4 | @construct 5 | def seed(): 6 | balances['test'] = 100 7 | supply.set(balances['test']) 8 | 9 | @export 10 | def test_func(status=None): 11 | return status 12 | 13 | @export 14 | def test_keymod(deduct): 15 | balances['test'] -= deduct 16 | return balances['test'] 17 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/modules/all_in_one.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def call_me(): 3 | return call_me_again() 4 | 5 | @export 6 | def call_me_again(): 7 | return call_me_again_again() 8 | 9 | @export 10 | def call_me_again_again(): 11 | return { 12 | 'owner': ctx.owner, 13 | 'this': ctx.this, 14 | 'signer': ctx.signer, 15 | 'caller': ctx.caller 16 | } 17 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/modules/all_in_one.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def call_me(): 3 | return call_me_again() 4 | 5 | @export 6 | def call_me_again(): 7 | return call_me_again_again() 8 | 9 | @export 10 | def call_me_again_again(): 11 | return { 12 | 'owner': ctx.owner, 13 | 'this': ctx.this, 14 | 'signer': ctx.signer, 15 | 'caller': ctx.caller 16 | } 17 | -------------------------------------------------------------------------------- /tests/security/contracts/double_spend_gas_attack.s.py: -------------------------------------------------------------------------------- 1 | import con_erc20 2 | 3 | @construct 4 | def seed(): 5 | pass 6 | 7 | @export 8 | def double_spend(receiver: str): 9 | allowance = con_erc20.allowance(owner=ctx.caller, spender=ctx.this) 10 | con_erc20.transfer_from(amount=allowance, to=receiver, main_account=ctx.caller) 11 | 12 | i = 0 13 | while True: 14 | i += 1 15 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/dynamic_import.py: -------------------------------------------------------------------------------- 1 | @export 2 | def called_from_a_far(): 3 | m = importlib.import_module('con_all_in_one') 4 | res = m.call_me_again_again() 5 | 6 | return [res, { 7 | 'name': 'called_from_a_far', 8 | 'owner': ctx.owner, 9 | 'this': ctx.this, 10 | 'signer': ctx.signer, 11 | 'caller': ctx.caller, 12 | 'entry': ctx.entry, 13 | 'submission_name': ctx.submission_name 14 | }] -------------------------------------------------------------------------------- /tests/integration/test_contracts/modules/module1.s.py: -------------------------------------------------------------------------------- 1 | ''' 2 | how the modules import each other. this is to test ctx.caller etc 3 | 4 | 1 5 | | | 6 | 2 3 7 | | | | | 8 | 4 5 6 7 9 | | 10 | 8 11 | 12 | ''' 13 | 14 | import module2 15 | import module3 16 | 17 | 18 | @export 19 | def get_context(): 20 | return { 21 | 'owner': ctx.owner, 22 | 'this': ctx.this, 23 | 'signer': ctx.signer, 24 | 'caller': ctx.caller 25 | } 26 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/modules/module1.s.py: -------------------------------------------------------------------------------- 1 | ''' 2 | how the modules import each other. this is to test ctx.caller etc 3 | 4 | 1 5 | | | 6 | 2 3 7 | | | | | 8 | 4 5 6 7 9 | | 10 | 8 11 | 12 | ''' 13 | 14 | import module2 15 | import module3 16 | 17 | 18 | @export 19 | def get_context(): 20 | return { 21 | 'owner': ctx.owner, 22 | 'this': ctx.this, 23 | 'signer': ctx.signer, 24 | 'caller': ctx.caller 25 | } 26 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/bastardcoin.s.py: -------------------------------------------------------------------------------- 1 | balances = Hash(default_value=0) 2 | 3 | @construct 4 | def seed(): 5 | balances['stu'] = 999 6 | balances['colin'] = 555 7 | 8 | @export 9 | def transfer(amount: int, to: str): 10 | sender = ctx.caller 11 | assert balances[sender] >= amount, 'Not enough coins to send!' 12 | 13 | balances[sender] -= amount 14 | balances[to] += amount 15 | 16 | @export 17 | def balance_of(account: str): 18 | return balances[account] -------------------------------------------------------------------------------- /tests/unit/contracts/exception.s.py: -------------------------------------------------------------------------------- 1 | balances = Hash(default_value=0) 2 | 3 | @construct 4 | def seed(): 5 | balances['stu'] = 999 6 | balances['colin'] = 555 7 | 8 | @export 9 | def transfer(amount: int, to: str): 10 | sender = ctx.caller 11 | assert balances[sender] >= amount, 'Not enough coins to send!' 12 | 13 | balances[sender] -= amount 14 | balances[to] += amount 15 | 16 | raise Exception('This is an exception') 17 | 18 | @export 19 | def balance_of(account: str): 20 | return balances[account] -------------------------------------------------------------------------------- /tests/integration/test_contracts/exception.py: -------------------------------------------------------------------------------- 1 | balances = Hash(default_value=0) 2 | 3 | @construct 4 | def seed(): 5 | balances['stu'] = 999 6 | balances['colin'] = 555 7 | 8 | @export 9 | def transfer(amount: int, to: str): 10 | sender = ctx.caller 11 | assert balances[sender] >= amount, 'Not enough coins to send!' 12 | 13 | balances[sender] -= amount 14 | balances[to] += amount 15 | 16 | raise Exception('This is an exception') 17 | 18 | @export 19 | def balance_of(account: str): 20 | return balances[account] -------------------------------------------------------------------------------- /tests/integration/test_contracts/foreign_thing.s.py: -------------------------------------------------------------------------------- 1 | thing_H = ForeignHash(foreign_contract='con_thing', foreign_name='H') 2 | thing_V = ForeignVariable(foreign_contract='con_thing', foreign_name='V') 3 | 4 | @export 5 | def read_H_hello(): 6 | return thing_H['hello'] 7 | 8 | @export 9 | def read_H_something(): 10 | return thing_H['something'] 11 | 12 | @export 13 | def read_V(): 14 | return thing_V.get() 15 | 16 | @export 17 | def set_H(k: str, v: Any): 18 | thing_H[k] = v 19 | 20 | @export 21 | def set_V(v: Any): 22 | thing_V.set(v) -------------------------------------------------------------------------------- /tests/unit/contracts/currency.s.py: -------------------------------------------------------------------------------- 1 | balances = Hash() 2 | 3 | @construct 4 | def seed(): 5 | balances['stu'] = 1000000 6 | balances['colin'] = 100 7 | 8 | @export 9 | def transfer(amount: int, to: str): 10 | sender = ctx.signer 11 | assert balances[sender] >= amount, 'Not enough coins to send!' 12 | 13 | balances[sender] -= amount 14 | 15 | if balances[to] is None: 16 | balances[to] = amount 17 | else: 18 | balances[to] += amount 19 | 20 | @export 21 | def balance(account: str): 22 | return balances[account] 23 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/currency.s.py: -------------------------------------------------------------------------------- 1 | balances = Hash() 2 | 3 | @construct 4 | def seed(): 5 | balances['stu'] = 1000000 6 | balances['colin'] = 100 7 | 8 | @export 9 | def transfer(amount: int, to: str): 10 | sender = ctx.signer 11 | assert balances[sender] >= amount, 'Not enough coins to send!' 12 | 13 | balances[sender] -= amount 14 | 15 | if balances[to] is None: 16 | balances[to] = amount 17 | else: 18 | balances[to] += amount 19 | 20 | @export 21 | def balance(account: str): 22 | return balances[account] 23 | -------------------------------------------------------------------------------- /tests/unit/test_stdlib_hashing.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.stdlib.bridge.hashing import sha256, sha3 3 | 4 | 5 | class TestHashing(TestCase): 6 | def test_sha3(self): 7 | secret = '1a54390942257a70bb843c1bd94eb996' 8 | _hash = '6c839446b4d4fa2582af5011730c680b3ee39929f041b7bee6f376211cc710f7' 9 | 10 | self.assertEqual(_hash, sha3(secret)) 11 | 12 | def test_sha256(self): 13 | secret = '842b65a7d48e3a3c3f0e9d37eaced0b2' 14 | _hash = 'eaf48a02d3a4bb3aeb0ecb337f6efb026ee0bbc460652510cff929de78935514' 15 | 16 | self.assertEqual(_hash, sha256(secret)) -------------------------------------------------------------------------------- /tests/integration/test_contracts/leaky.s.py: -------------------------------------------------------------------------------- 1 | supply = Variable() 2 | balances = Hash(default_value=0) 3 | 4 | @construct 5 | def seed(): 6 | balances['stu'] = 1000000 7 | balances['colin'] = 100 8 | supply.set(balances['stu'] + balances['colin']) 9 | 10 | @export 11 | def transfer(amount: int, to: str): 12 | sender = ctx.signer 13 | 14 | balances[sender] -= amount 15 | balances[to] += amount 16 | 17 | # putting the assert down here shouldn't matter to the execution and data environment 18 | assert balances[sender] >= amount, 'Not enough coins to send!' 19 | 20 | @export 21 | def balance_of(account: str): 22 | return balances[account] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | .env 25 | .venv 26 | env/ 27 | venv/ 28 | ENV/ 29 | 30 | # IDE 31 | .idea/ 32 | .vscode/ 33 | *.swp 34 | *.swo 35 | 36 | # Testing 37 | .coverage 38 | .pytest_cache/ 39 | htmlcov/ 40 | 41 | # Poetry 42 | poetry.lock__pycache__/ 43 | dist 44 | xian_contracting.egg-info 45 | contracting.egg-info 46 | build 47 | .idea 48 | logs 49 | *.so 50 | .ccls 51 | *.pyc 52 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please add a brief description of the feature / change / bug-fix you want to merge. 4 | 5 | ## Type of change 6 | 7 | Please delete options that are not relevant. 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would require a resync of blockchain state) 12 | 13 | ## Checklist 14 | 15 | - [ ] I have performed a self-review of my own code 16 | - [ ] I have tested this change in my development environment. 17 | - [ ] I have added tests to prove that this change works 18 | - [ ] All existing tests pass after this change 19 | - [ ] I have added / updated documentation related to this change -------------------------------------------------------------------------------- /src/contracting/constants.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | RECURSION_LIMIT = 1024 4 | 5 | DELIMITER = ':' 6 | INDEX_SEPARATOR = '.' 7 | HDF5_GROUP_SEPARATOR = '/' 8 | 9 | SUBMISSION_CONTRACT_NAME = 'submission' 10 | PRIVATE_METHOD_PREFIX = '__' 11 | EXPORT_DECORATOR_STRING = 'export' 12 | INIT_DECORATOR_STRING = 'construct' 13 | INIT_FUNC_NAME = '__{}'.format(PRIVATE_METHOD_PREFIX) 14 | VALID_DECORATORS = {EXPORT_DECORATOR_STRING, INIT_DECORATOR_STRING} 15 | 16 | ORM_CLASS_NAMES = {'Variable', 'Hash', 'ForeignVariable', 'ForeignHash', 'LogEvent'} 17 | 18 | MAX_HASH_DIMENSIONS = 16 19 | MAX_KEY_SIZE = 1024 20 | 21 | READ_COST_PER_BYTE = 1 22 | WRITE_COST_PER_BYTE = 25 23 | 24 | STAMPS_PER_TAU = 20 25 | 26 | BLOCK_NUM_DEFAULT = -1 27 | FILENAME_LEN_MAX = 255 28 | 29 | DEFAULT_STAMPS = 1000000 30 | 31 | STORAGE_HOME = Path().home().joinpath(".cometbft/xian") 32 | -------------------------------------------------------------------------------- /src/contracting/stdlib/bridge/crypto.py: -------------------------------------------------------------------------------- 1 | from types import ModuleType 2 | import nacl 3 | 4 | 5 | def verify(vk: str, msg: str, signature: str): 6 | vk = bytes.fromhex(vk) 7 | msg = msg.encode() 8 | signature = bytes.fromhex(signature) 9 | 10 | vk = nacl.signing.VerifyKey(vk) 11 | try: 12 | vk.verify(msg, signature) 13 | except: 14 | return False 15 | return True 16 | 17 | 18 | def key_is_valid(key: str): 19 | """ Check if the given address is valid. 20 | Can be used with public and private keys """ 21 | if not len(key) == 64: 22 | return False 23 | try: 24 | int(key, 16) 25 | except: 26 | return False 27 | return True 28 | 29 | 30 | crypto_module = ModuleType('crypto') 31 | crypto_module.verify = verify 32 | crypto_module.key_is_valid = key_is_valid 33 | 34 | exports = { 35 | 'crypto': crypto_module 36 | } 37 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "xian-contracting" 3 | version = "1.0.1" 4 | description = "Xian Network Python Contracting Engine" 5 | authors = ["Xian Network "] 6 | readme = "README.md" 7 | packages = [{include = "contracting", from = "src"}] 8 | license = "GPL-3.0-only" 9 | repository = "https://github.com/xian-network/xian-contracting" 10 | keywords = ["blockchain", "xian", "contracting", "python"] 11 | classifiers = [ 12 | "Programming Language :: Python :: 3.11", 13 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 14 | "Development Status :: 5 - Production/Stable", 15 | ] 16 | 17 | [tool.poetry.dependencies] 18 | python = "~=3.11.0" 19 | astor = "0.8.1" 20 | pycodestyle = "2.10.0" 21 | autopep8 = "1.5.7" 22 | iso8601 = "*" 23 | h5py = "*" 24 | cachetools = "*" 25 | loguru = "*" 26 | pynacl = "*" 27 | psutil = "*" 28 | 29 | [build-system] 30 | requires = ["poetry-core"] 31 | build-backend = "poetry.core.masonry.api" 32 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI and GitHub Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Trigger on version tags 7 | 8 | jobs: 9 | build-and-publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Set up Python 3.11 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: '3.11' 18 | 19 | - name: Install Poetry 20 | run: | 21 | curl -sSL https://install.python-poetry.org | python3 - 22 | 23 | - name: Build and Publish to PyPI 24 | env: 25 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 26 | run: | 27 | poetry config pypi-token.pypi $PYPI_TOKEN 28 | poetry build 29 | poetry publish 30 | 31 | - name: Create GitHub Release 32 | uses: softprops/action-gh-release@v1 33 | with: 34 | files: dist/* 35 | generate_release_notes: true 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/contracting/stdlib/bridge/hashing.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from types import ModuleType 4 | 5 | ''' 6 | Bytes can't be stored in JSON so we use hex-strings converted into bytes and back. 7 | ''' 8 | 9 | 10 | def sha3(hex_str: str): 11 | try: 12 | byte_str = bytes.fromhex(hex_str) 13 | except ValueError: 14 | byte_str = hex_str.encode() 15 | 16 | hasher = hashlib.sha3_256() 17 | hasher.update(byte_str) 18 | 19 | hashed_bytes = hasher.digest() 20 | 21 | return hashed_bytes.hex() 22 | 23 | 24 | def sha256(hex_str: str): 25 | try: 26 | byte_str = bytes.fromhex(hex_str) 27 | except ValueError: 28 | byte_str = hex_str.encode() 29 | 30 | hasher = hashlib.sha256() 31 | hasher.update(byte_str) 32 | 33 | hashed_bytes = hasher.digest() 34 | 35 | return hashed_bytes.hex() 36 | 37 | 38 | hashlib_module = ModuleType('hashlib') 39 | hashlib_module.sha3 = sha3 40 | hashlib_module.sha256 = sha256 41 | 42 | exports = { 43 | 'hashlib': hashlib_module, 44 | } 45 | -------------------------------------------------------------------------------- /tests/unit/test_driver_tombstones.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from contracting.storage.driver import Driver 3 | 4 | 5 | class TestDriverTombstones(unittest.TestCase): 6 | 7 | def setUp(self): 8 | self.driver = Driver(bypass_cache=False) 9 | self.driver.flush_full() 10 | 11 | def tearDown(self): 12 | self.driver.flush_full() 13 | 14 | def test_items_excludes_pending_deletes(self): 15 | key = 'con.hash:subkey' 16 | self.driver.set(key, 'v1') 17 | # Persist first so value exists on disk 18 | self.driver.commit() 19 | # Now set tombstone in the same transaction context 20 | self.driver.set(key, None) 21 | 22 | items = self.driver.items(prefix='con.hash:') 23 | keys = self.driver.keys(prefix='con.hash:') 24 | values = self.driver.values(prefix='con.hash:') 25 | 26 | self.assertNotIn(key, items) 27 | self.assertNotIn(key, keys) 28 | self.assertEqual(values, []) 29 | 30 | 31 | if __name__ == '__main__': 32 | unittest.main() 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/unit/contracts/submission.s.py: -------------------------------------------------------------------------------- 1 | @__export('submission') 2 | def submit_contract(name: str, code: str, owner: Any=None, constructor_args: dict={}): 3 | if ctx.caller != 'sys': 4 | assert name.startswith('con_'), 'Contract must start with con_!' 5 | 6 | assert ctx.caller == ctx.signer, 'Contract cannot be called from another contract!' 7 | assert len(name) <= 64, 'Contract name length exceeds 64 characters!' 8 | assert name.islower(), 'Contract name must be lowercase!' 9 | 10 | __Contract().submit( 11 | name=name, 12 | code=code, 13 | owner=owner, 14 | constructor_args=constructor_args, 15 | developer=ctx.caller 16 | ) 17 | 18 | 19 | @__export('submission') 20 | def change_developer(contract: str, new_developer: str): 21 | d = __Contract()._driver.get_var(contract=contract, variable='__developer__') 22 | assert ctx.caller == d, 'Sender is not current developer!' 23 | 24 | __Contract()._driver.set_var( 25 | contract=contract, 26 | variable='__developer__', 27 | value=new_developer 28 | ) 29 | -------------------------------------------------------------------------------- /src/contracting/contracts/submission.s.py: -------------------------------------------------------------------------------- 1 | @__export('submission') 2 | def submit_contract(name: str, code: str, owner: Any=None, constructor_args: dict={}): 3 | if ctx.caller != 'sys': 4 | assert name.startswith('con_'), 'Contract must start with con_!' 5 | 6 | assert ctx.caller == ctx.signer, 'Contract cannot be called from another contract!' 7 | assert len(name) <= 64, 'Contract name length exceeds 64 characters!' 8 | assert name.islower(), 'Contract name must be lowercase!' 9 | 10 | __Contract().submit( 11 | name=name, 12 | code=code, 13 | owner=owner, 14 | constructor_args=constructor_args, 15 | developer=ctx.caller 16 | ) 17 | 18 | 19 | @__export('submission') 20 | def change_developer(contract: str, new_developer: str): 21 | d = __Contract()._driver.get_var(contract=contract, variable='__developer__') 22 | assert ctx.caller == d, 'Sender is not current developer!' 23 | 24 | __Contract()._driver.set_var( 25 | contract=contract, 26 | variable='__developer__', 27 | value=new_developer 28 | ) 29 | -------------------------------------------------------------------------------- /tests/security/contracts/submission.s.py: -------------------------------------------------------------------------------- 1 | @__export('submission') 2 | def submit_contract(name: str, code: str, owner: Any=None, constructor_args: dict={}): 3 | if ctx.caller != 'sys': 4 | assert name.startswith('con_'), 'Contract must start with con_!' 5 | 6 | assert ctx.caller == ctx.signer, 'Contract cannot be called from another contract!' 7 | assert len(name) <= 64, 'Contract name length exceeds 64 characters!' 8 | assert name.islower(), 'Contract name must be lowercase!' 9 | 10 | __Contract().submit( 11 | name=name, 12 | code=code, 13 | owner=owner, 14 | constructor_args=constructor_args, 15 | developer=ctx.caller 16 | ) 17 | 18 | 19 | @__export('submission') 20 | def change_developer(contract: str, new_developer: str): 21 | d = __Contract()._driver.get_var(contract=contract, variable='__developer__') 22 | assert ctx.caller == d, 'Sender is not current developer!' 23 | 24 | __Contract()._driver.set_var( 25 | contract=contract, 26 | variable='__developer__', 27 | value=new_developer 28 | ) 29 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/submission.s.py: -------------------------------------------------------------------------------- 1 | @__export('submission') 2 | def submit_contract(name: str, code: str, owner: Any=None, constructor_args: dict={}): 3 | if ctx.caller != 'sys': 4 | assert name.startswith('con_'), 'Contract must start with con_!' 5 | 6 | assert ctx.caller == ctx.signer, 'Contract cannot be called from another contract!' 7 | assert len(name) <= 64, 'Contract name length exceeds 64 characters!' 8 | assert name.islower(), 'Contract name must be lowercase!' 9 | 10 | __Contract().submit( 11 | name=name, 12 | code=code, 13 | owner=owner, 14 | constructor_args=constructor_args, 15 | developer=ctx.caller 16 | ) 17 | 18 | 19 | @__export('submission') 20 | def change_developer(contract: str, new_developer: str): 21 | d = __Contract()._driver.get_var(contract=contract, variable='__developer__') 22 | assert ctx.caller == d, 'Sender is not current developer!' 23 | 24 | __Contract()._driver.set_var( 25 | contract=contract, 26 | variable='__developer__', 27 | value=new_developer 28 | ) 29 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/submission.s.py: -------------------------------------------------------------------------------- 1 | @__export('submission') 2 | def submit_contract(name: str, code: str, owner: Any=None, constructor_args: dict={}): 3 | if ctx.caller != 'sys': 4 | assert name.startswith('con_'), 'Contract must start with con_!' 5 | 6 | assert ctx.caller == ctx.signer, 'Contract cannot be called from another contract!' 7 | assert len(name) <= 64, 'Contract name length exceeds 64 characters!' 8 | assert name.islower(), 'Contract name must be lowercase!' 9 | 10 | __Contract().submit( 11 | name=name, 12 | code=code, 13 | owner=owner, 14 | constructor_args=constructor_args, 15 | developer=ctx.caller 16 | ) 17 | 18 | 19 | @__export('submission') 20 | def change_developer(contract: str, new_developer: str): 21 | d = __Contract()._driver.get_var(contract=contract, variable='__developer__') 22 | assert ctx.caller == d, 'Sender is not current developer!' 23 | 24 | __Contract()._driver.set_var( 25 | contract=contract, 26 | variable='__developer__', 27 | value=new_developer 28 | ) 29 | -------------------------------------------------------------------------------- /tests/unit/precompiled/updated_submission.py: -------------------------------------------------------------------------------- 1 | @__export('submission') 2 | def submit_contract(name: str, code: str, owner: Any=None, constructor_args: dict={}): 3 | if ctx.caller != 'sys': 4 | assert name.startswith('con_'), 'Contract must start with con_!' 5 | 6 | assert ctx.caller == ctx.signer, 'Contract cannot be called from another contract!' 7 | assert len(name) <= 64, 'Contract name length exceeds 64 characters!' 8 | assert name.islower(), 'Contract name must be lowercase!' 9 | 10 | __Contract().submit( 11 | name=name, 12 | code=code, 13 | owner=owner, 14 | constructor_args=constructor_args, 15 | developer=ctx.caller 16 | ) 17 | 18 | 19 | @__export('submission') 20 | def change_developer(contract: str, new_developer: str): 21 | d = __Contract()._driver.get_var(contract=contract, variable='__developer__') 22 | assert ctx.caller == d, 'Sender is not current developer !!!!!!!!' 23 | 24 | __Contract()._driver.set_var( 25 | contract=contract, 26 | variable='__developer__', 27 | value=new_developer 28 | ) 29 | -------------------------------------------------------------------------------- /src/contracting/stdlib/env.py: -------------------------------------------------------------------------------- 1 | from contracting.stdlib.bridge.orm import exports as orm_exports 2 | from contracting.stdlib.bridge.hashing import exports as hash_exports 3 | from contracting.stdlib.bridge.time import exports as time_exports 4 | from contracting.stdlib.bridge.random import exports as random_exports 5 | from contracting.stdlib.bridge.imports import exports as imports_exports 6 | from contracting.stdlib.bridge.access import exports as access_exports 7 | from contracting.stdlib.bridge.decimal import exports as decimal_exports 8 | from contracting.stdlib.bridge.crypto import exports as crypto_exports 9 | 10 | # TODO create a module instead and return it inside of a dictionary like: 11 | # { 12 | # 'stdlib': module 13 | # } 14 | # 15 | # Then stdlib.datetime becomes available, etc 16 | 17 | 18 | def gather(): 19 | env = {} 20 | 21 | env.update(orm_exports) 22 | env.update(hash_exports) 23 | env.update(time_exports) 24 | env.update(random_exports) 25 | env.update(imports_exports) 26 | env.update(access_exports) 27 | env.update(decimal_exports) 28 | env.update(crypto_exports) 29 | 30 | return env 31 | -------------------------------------------------------------------------------- /tests/integration/test_complex_object_setting.py: -------------------------------------------------------------------------------- 1 | from contracting.client import ContractingClient 2 | from unittest import TestCase 3 | import os 4 | 5 | def contract(): 6 | storage = Hash() 7 | 8 | @export 9 | def create(x: int, y: int, color: str): 10 | storage[x, y] = { 11 | 'color': color, 12 | 'owner': ctx.caller 13 | } 14 | 15 | @export 16 | def update(x: int, y: int, color: str): 17 | s = storage[x, y] 18 | 19 | s['color'] = color 20 | 21 | storage[x, y] = s 22 | 23 | 24 | class TestComplexStorage(TestCase): 25 | def setUp(self): 26 | self.c = ContractingClient(signer='stu') 27 | self.c.flush() 28 | 29 | self.c.submit(contract, name="con_contract") 30 | self.contract = self.c.get_contract('con_contract') 31 | 32 | def tearDown(self): 33 | self.c.flush() 34 | 35 | def test_storage(self): 36 | self.contract.create(x=1, y=2, color='howdy') 37 | self.assertEqual(self.contract.storage[1, 2]['color'], 'howdy') 38 | 39 | def test_modify(self): 40 | self.contract.create(x=1, y=2, color='howdy') 41 | self.contract.update(x=1, y=2, color='yoyoyo') 42 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/float_issue.s.py: -------------------------------------------------------------------------------- 1 | random.seed() 2 | gradients = Hash() 3 | 4 | def rand_vect(): 5 | theta = random.randint(0, 100) / 50 * 3.13 6 | return {'x': 5.124 / theta, 'y': 7.124 / theta} 7 | 8 | 9 | def dot_prod_grid(x, y, vx, vy): 10 | d_vect = {'x': x - vx, 'y': y - vy} 11 | key = str(vx) + ',' + str(vy) 12 | if gradients[key]: 13 | g_vect = gradients[key] 14 | else: 15 | g_vect = rand_vect() 16 | gradients[key] = g_vect 17 | 18 | return (d_vect['y']) * (g_vect['x']) + (d_vect['y']) * (g_vect['y']) 19 | 20 | 21 | def smootherstep(x): 22 | return (6.0 * x ** 5.0 - 15.0 * x ** 4.0 + 10.0 * x ** 3.0) 23 | 24 | 25 | def interp(x, a, b): 26 | return a + (b - a) * smootherstep(x) 27 | 28 | 29 | @export 30 | def seed(): 31 | gradients['0'] = 1 # test 32 | 33 | @export 34 | def get(x: float, y: float): 35 | xf = int(x) 36 | yf = int(y) 37 | tl = dot_prod_grid(x, y, xf, yf) 38 | tr = dot_prod_grid(x, y, xf + 1, yf) 39 | bl = dot_prod_grid(x, y, xf, yf + 1) 40 | br = dot_prod_grid(x, y, xf + 1, yf + 1) 41 | xt = interp(x - xf, tl, tr) 42 | xb = interp(x - xf, bl, br) 43 | return interp(y - yf, xt, xb) -------------------------------------------------------------------------------- /tests/integration/test_datetime_contracts.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.client import ContractingClient 3 | from contracting.stdlib.bridge.time import Datetime 4 | import os 5 | 6 | class TestSenecaClientReplacesExecutor(TestCase): 7 | def setUp(self): 8 | self.c = ContractingClient(signer='stu') 9 | self.c.flush() 10 | 11 | dater_path = os.path.join(os.path.dirname(__file__), "test_contracts", "dater.py") 12 | 13 | with open(dater_path) as f: 14 | self.c.submit(f=f.read(), name='con_dater') 15 | 16 | self.dater = self.c.get_contract('con_dater') 17 | 18 | def tearDown(self): 19 | self.c.flush() 20 | 21 | def test_datetime_passed_argument_and_now_are_correctly_compared(self): 22 | self.dater.replicate(d=Datetime(year=3000, month=1, day=1)) 23 | 24 | def test_datetime_passed_argument_and_now_are_correctly_compared_json(self): 25 | with self.assertRaises(TypeError): 26 | self.dater.replicate(d={'__time__':[3000, 12, 15, 12, 12, 12, 0]}) 27 | 28 | with self.assertRaises(TypeError): 29 | self.dater.replicate(d=[2025, 11, 15, 21, 47, 14, 0]) 30 | 31 | def test_datetime_subtracts(self): 32 | self.dater.subtract(d1=Datetime(year=2000, month=1, day=1), d2=Datetime(year=2001, month=1, day=1)) -------------------------------------------------------------------------------- /tests/unit/test_client_keys_prefix.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from contracting.client import ContractingClient 3 | 4 | 5 | class TestClientKeysPrefix(unittest.TestCase): 6 | 7 | def setUp(self): 8 | self.client = ContractingClient() 9 | self.client.flush() 10 | 11 | # Submit two dummy contracts with similar prefixes 12 | code_a = """ 13 | @export 14 | def f(): 15 | pass 16 | """ 17 | code_b = code_a 18 | self.client.submit(code_a, name='abc') 19 | self.client.submit(code_b, name='abc2') 20 | 21 | # Write distinct state under each contract to detect leakage 22 | self.client.set_var('abc', '__code__', value='X') 23 | self.client.set_var('abc2', '__code__', value='Y') 24 | 25 | # Also add a hash-like key for both 26 | self.client.set_var('abc', 'h', arguments=['k'], value=1) 27 | self.client.set_var('abc2', 'h', arguments=['k'], value=2) 28 | 29 | def tearDown(self): 30 | self.client.flush() 31 | 32 | def test_keys_scoped_to_exact_contract(self): 33 | abc = self.client.get_contract('abc') 34 | keys = abc.keys() 35 | # Ensure keys from abc2 are not present 36 | self.assertTrue(all(not k.startswith('abc2.') for k in keys)) 37 | 38 | 39 | if __name__ == '__main__': 40 | unittest.main() 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/contracting/stdlib/bridge/access.py: -------------------------------------------------------------------------------- 1 | from contracting.execution.runtime import rt 2 | from contextlib import ContextDecorator 3 | from contracting.storage.driver import Driver 4 | from typing import Any 5 | 6 | 7 | class __export(ContextDecorator): 8 | def __init__(self, contract): 9 | self.contract = contract 10 | 11 | def __enter__(self, *args, **kwargs): 12 | driver = rt.env.get('__Driver') or Driver() 13 | 14 | if rt.context._context_changed(self.contract): 15 | current_state = rt.context._get_state() 16 | 17 | state = { 18 | 'owner': driver.get_owner(self.contract), 19 | 'caller': current_state['this'], 20 | 'signer': current_state['signer'], 21 | 'this': self.contract, 22 | 'entry': current_state['entry'], 23 | 'submission_name': current_state['submission_name'] 24 | } 25 | 26 | rt.context._add_state(state) 27 | 28 | if state['owner'] is not None and state['owner'] != state['caller']: 29 | raise Exception('Caller is not the owner!') 30 | else: 31 | rt.context._ins_state() 32 | 33 | def __exit__(self, *args, **kwargs): 34 | rt.context._pop_state() 35 | 36 | 37 | exports = { 38 | '__export': __export, 39 | 'ctx': rt.context, 40 | 'rt': rt, 41 | 'Any': Any 42 | } 43 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/stubucks.s.py: -------------------------------------------------------------------------------- 1 | supply = Variable() 2 | balances = Hash(default_value=0) 3 | 4 | @construct 5 | def seed(): 6 | balances['stu'] = 123 7 | balances['colin'] = 321 8 | supply.set(balances['stu'] + balances['colin']) 9 | 10 | @export 11 | def transfer(amount: int, to: str): 12 | sender = ctx.caller 13 | assert balances[sender] >= amount, 'Not enough coins to send!' 14 | 15 | balances[sender] -= amount 16 | balances[to] += amount 17 | 18 | @export 19 | def balance_of(account: str): 20 | return balances[account] 21 | 22 | @export 23 | def total_supply(): 24 | return supply.get() 25 | 26 | @export 27 | def allowance(owner: str, spender: str): 28 | return balances[owner, spender] 29 | 30 | @export 31 | def approve(amount: int, to: str): 32 | sender = ctx.caller 33 | balances[sender, to] += amount 34 | return balances[sender, to] 35 | 36 | @export 37 | def transfer_from(amount: int, to: str, main_account: str): 38 | sender = ctx.caller 39 | 40 | assert balances[main_account, sender] >= amount, 'Not enough coins approved to send! You have {} and are trying to spend {}'\ 41 | .format(balances[main_account, sender], amount) 42 | assert balances[main_account] >= amount, 'Not enough coins to send!' 43 | 44 | balances[main_account, sender] -= amount 45 | balances[main_account] -= amount 46 | 47 | balances[to] += amount 48 | -------------------------------------------------------------------------------- /tests/security/contracts/erc20_clone.s.py: -------------------------------------------------------------------------------- 1 | supply = Variable() 2 | balances = Hash(default_value=0) 3 | 4 | @construct 5 | def seed(): 6 | balances['stu'] = 1000000 7 | balances['colin'] = 100 8 | supply.set(balances['stu'] + balances['colin']) 9 | 10 | @export 11 | def transfer(amount: int, to: str): 12 | sender = ctx.caller 13 | assert balances[sender] >= amount, 'Not enough coins to send!' 14 | 15 | balances[sender] -= amount 16 | balances[to] += amount 17 | 18 | @export 19 | def balance_of(account: str): 20 | return balances[account] 21 | 22 | @export 23 | def total_supply(): 24 | return supply.get() 25 | 26 | @export 27 | def allowance(owner: str, spender: str): 28 | return balances[owner, spender] 29 | 30 | @export 31 | def approve(amount: str, to: str): 32 | sender = ctx.caller 33 | balances[sender, to] += amount 34 | return balances[sender, to] 35 | 36 | @export 37 | def transfer_from(amount: int, to: str, main_account: str): 38 | sender = ctx.caller 39 | 40 | assert balances[main_account, sender] >= amount, 'Not enough coins approved to send! You have {} and are trying to spend {}'\ 41 | .format(balances[main_account, sender], amount) 42 | assert balances[main_account] >= amount, 'Not enough coins to send!' 43 | 44 | balances[main_account, sender] -= amount 45 | balances[main_account] -= amount 46 | 47 | balances[to] += amount 48 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/erc20_clone.s.py: -------------------------------------------------------------------------------- 1 | supply = Variable() 2 | balances = Hash(default_value=0) 3 | 4 | @construct 5 | def seed(): 6 | balances['stu'] = 1000000 7 | balances['colin'] = 100 8 | supply.set(balances['stu'] + balances['colin']) 9 | 10 | @export 11 | def transfer(amount: int, to: str): 12 | sender = ctx.caller 13 | assert balances[sender] >= amount, 'Not enough coins to send!' 14 | 15 | balances[sender] -= amount 16 | balances[to] += amount 17 | 18 | @export 19 | def balance_of(account: str): 20 | return balances[account] 21 | 22 | @export 23 | def total_supply(): 24 | return supply.get() 25 | 26 | @export 27 | def allowance(owner: str, spender: str): 28 | return balances[owner, spender] 29 | 30 | @export 31 | def approve(amount: str, to: str): 32 | sender = ctx.caller 33 | balances[sender, to] += amount 34 | return balances[sender, to] 35 | 36 | @export 37 | def transfer_from(amount: int, to: str, main_account: str): 38 | sender = ctx.caller 39 | 40 | assert balances[main_account, sender] >= amount, 'Not enough coins approved to send! You have {} and are trying to spend {}'\ 41 | .format(balances[main_account, sender], amount) 42 | assert balances[main_account] >= amount, 'Not enough coins to send!' 43 | 44 | balances[main_account, sender] -= amount 45 | balances[main_account] -= amount 46 | 47 | balances[to] += amount 48 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/tejastokens.s.py: -------------------------------------------------------------------------------- 1 | supply = Variable() 2 | balances = Hash(default_value=0) 3 | 4 | @construct 5 | def seed(): 6 | balances['stu'] = 321 7 | balances['colin'] = 123 8 | supply.set(balances['stu'] + balances['colin']) 9 | 10 | @export 11 | def transfer(amount: int, to: str): 12 | sender = ctx.caller 13 | assert balances[sender] >= amount, 'Not enough coins to send!' 14 | 15 | balances[sender] -= amount 16 | balances[to] += amount 17 | 18 | @export 19 | def balance_of(account: str): 20 | return balances[account] 21 | 22 | @export 23 | def total_supply(): 24 | return supply.get() 25 | 26 | @export 27 | def allowance(owner: str, spender: str): 28 | return balances[owner, spender] 29 | 30 | @export 31 | def approve(amount: int, to: str): 32 | sender = ctx.caller 33 | balances[sender, to] += amount 34 | return balances[sender, to] 35 | 36 | @export 37 | def transfer_from(amount: int, to: str, main_account: str): 38 | sender = ctx.caller 39 | 40 | assert balances[main_account, sender] >= amount, 'Not enough coins approved to send! You have {} and are trying to spend {}'\ 41 | .format(balances[main_account, sender], amount) 42 | assert balances[main_account] >= amount, 'Not enough coins to send!' 43 | 44 | balances[main_account, sender] -= amount 45 | balances[main_account] -= amount 46 | 47 | balances[to] += amount 48 | -------------------------------------------------------------------------------- /tests/performance/test_contracts/erc20_clone.s.py: -------------------------------------------------------------------------------- 1 | supply = Variable() 2 | balances = Hash(default_value=0) 3 | 4 | @construct 5 | def seed(): 6 | balances['stu'] = 1000000 7 | balances['colin'] = 100 8 | supply.set(balances['stu'] + balances['colin']) 9 | 10 | @export 11 | def transfer(amount: int, to: str): 12 | sender = ctx.caller 13 | assert balances[sender] >= amount, 'Not enough coins to send!' 14 | 15 | balances[sender] -= amount 16 | balances[to] += amount 17 | 18 | @export 19 | def balance_of(account: str): 20 | return balances[account] 21 | 22 | @export 23 | def total_supply(): 24 | return supply.get() 25 | 26 | @export 27 | def allowance(owner: str, spender: str): 28 | return balances[owner, spender] 29 | 30 | @export 31 | def approve(amount: str, to: str): 32 | sender = ctx.caller 33 | balances[sender, to] += amount 34 | return balances[sender, to] 35 | 36 | @export 37 | def transfer_from(amount: int, to: str, main_account: str): 38 | sender = ctx.caller 39 | 40 | assert balances[main_account, sender] >= amount, 'Not enough coins approved to send! You have {} and are trying to spend {}'\ 41 | .format(balances[main_account, sender], amount) 42 | assert balances[main_account] >= amount, 'Not enough coins to send!' 43 | 44 | balances[main_account, sender] -= amount 45 | balances[main_account] -= amount 46 | 47 | balances[to] += amount 48 | -------------------------------------------------------------------------------- /tests/unit/precompiled/compiled_token.py: -------------------------------------------------------------------------------- 1 | # Monkey patch for testing, as this is purely for 'interface enforcement' testing 2 | from contracting.storage.orm import Variable, Hash 3 | 4 | class ctx: 5 | caller = 1 6 | 7 | __supply = Variable(contract='__main__', name='supply') 8 | __balances = Hash(default_value=0, contract='__main__', name='balances') 9 | 10 | 11 | def ____(): 12 | __balances['stu'] = 1000000 13 | __balances['colin'] = 100 14 | __supply.set(__balances['stu'] + __balances['colin']) 15 | 16 | def transfer(amount, to): 17 | sender = ctx.caller 18 | assert __balances[sender] >= amount, 'Not enough coins to send!' 19 | __balances[sender] -= amount 20 | __balances[to] += amount 21 | 22 | def balance_of(account): 23 | return __balances[account] 24 | 25 | def total_supply(): 26 | return __supply.get() 27 | 28 | def allowance(owner, spender): 29 | return __balances[owner, spender] 30 | 31 | def approve(amount, to): 32 | sender = ctx.caller 33 | __balances[sender, to] += amount 34 | return __balances[sender, to] 35 | 36 | def transfer_from(amount, to, main_account): 37 | sender = ctx.caller 38 | assert __balances[main_account, sender 39 | ] >= amount, 'Not enough coins approved to send! You have {} and are trying to spend {}'.format( 40 | __balances[main_account, sender], amount) 41 | assert __balances[main_account] >= amount, 'Not enough coins to send!' 42 | __balances[main_account, sender] -= amount 43 | __balances[main_account] -= amount 44 | __balances[to] += amount 45 | 46 | def __private_func(): 47 | return 5 -------------------------------------------------------------------------------- /tests/integration/test_contracts/dynamic_importing.s.py: -------------------------------------------------------------------------------- 1 | @export 2 | def balance_for_token(tok: str, account: str): 3 | t = importlib.import_module(tok) 4 | return t.balance_of(account=account) 5 | 6 | @export 7 | def only_erc20(tok: str, account: str): 8 | t = importlib.import_module(tok) 9 | assert enforce_erc20(t), 'You cannot use a non-ERC20 standard token!!' 10 | 11 | return t.balance_of(account=account) 12 | 13 | @export 14 | def is_erc20_compatible(tok: str): 15 | interface = [ 16 | importlib.Func('transfer', args=('amount', 'to')), 17 | importlib.Func('balance_of', args=('account',)), 18 | importlib.Func('total_supply'), 19 | importlib.Func('allowance', args=('owner', 'spender')), 20 | importlib.Func('approve', args=('amount', 'to')), 21 | importlib.Func('transfer_from', args=('amount', 'to', 'main_account')), 22 | importlib.Var('supply', Variable), 23 | importlib.Var('balances', Hash) 24 | ] 25 | 26 | t = importlib.import_module(tok) 27 | 28 | return importlib.enforce_interface(t, interface) 29 | 30 | def enforce_erc20(m): 31 | interface = [ 32 | importlib.Func('transfer', args=('amount', 'to')), 33 | importlib.Func('balance_of', args=('account',)), 34 | importlib.Func('total_supply'), 35 | importlib.Func('allowance', args=('owner', 'spender')), 36 | importlib.Func('approve', args=('amount', 'to')), 37 | importlib.Func('transfer_from', args=('amount', 'to', 'main_account')), 38 | importlib.Var('supply', Variable), 39 | importlib.Var('balances', Hash) 40 | ] 41 | 42 | return importlib.enforce_interface(m, interface) -------------------------------------------------------------------------------- /src/contracting/compilation/parser.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | 4 | def methods_for_contract(contract_code: str): 5 | tree = ast.parse(contract_code) 6 | 7 | function_defs = [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)] 8 | 9 | funcs = [] 10 | for definition in function_defs: 11 | func_name = definition.name 12 | 13 | if func_name.startswith('__'): 14 | continue 15 | 16 | kwargs = [] 17 | 18 | for arg in definition.args.args: 19 | try: 20 | a = arg.annotation.id 21 | except AttributeError: 22 | a = arg.annotation.value.id + '.' + arg.annotation.attr 23 | 24 | kwargs.append({ 25 | 'name': arg.arg, 26 | 'type': a 27 | }) 28 | 29 | funcs.append({'name': func_name, 'arguments': kwargs}) 30 | 31 | return funcs 32 | 33 | 34 | def variables_for_contract(contract_code: str): 35 | tree = ast.parse(contract_code) 36 | 37 | assigns = [] 38 | 39 | for node in ast.walk(tree): 40 | if isinstance(node, ast.Assign): 41 | assigns.append(node) 42 | 43 | if isinstance(node, ast.FunctionDef): 44 | break 45 | 46 | variables = [] 47 | hashes = [] 48 | 49 | for assign in assigns: 50 | if type(assign.value) == ast.Call: 51 | if assign.value.func.id == 'Variable': 52 | variables.append(assign.targets[0].id.lstrip('__')) 53 | elif assign.value.func.id == 'Hash': 54 | hashes.append(assign.targets[0].id.lstrip('__')) 55 | 56 | return { 57 | 'variables': variables, 58 | 'hashes': hashes 59 | } 60 | -------------------------------------------------------------------------------- /tests/integration/test_contracts/atomic_swaps.s.py: -------------------------------------------------------------------------------- 1 | import con_erc20_clone 2 | 3 | swaps = Hash() 4 | 5 | @export 6 | def initiate(participant: str, expiration: datetime.datetime, hashlock: str, amount: float): 7 | allowance = con_erc20_clone.allowance(ctx.caller, ctx.this) 8 | 9 | assert allowance >= amount, \ 10 | "You cannot initiate an atomic swap without allowing '{}' " \ 11 | "at least {} coins. You have only allowed {} coins".format(ctx.this, amount, allowance) 12 | 13 | swaps[participant, hashlock] = [expiration, amount] 14 | 15 | con_erc20_clone.transfer_from(amount, ctx.this, ctx.caller) 16 | 17 | @export 18 | def redeem(secret: str): 19 | 20 | hashlock = hashlib.sha256(secret) 21 | 22 | result = swaps[ctx.caller, hashlock] 23 | 24 | assert result is not None, 'Incorrect sender or secret passed.' 25 | 26 | expiration, amount = result 27 | 28 | assert expiration >= now, 'Swap has expired.' 29 | 30 | con_erc20_clone.transfer(amount, ctx.caller) 31 | swaps[ctx.caller, hashlock] = None # change this to respond to the del keyword? 32 | 33 | @export 34 | def refund(participant: str, secret: str): 35 | 36 | assert participant != ctx.caller and participant != ctx.signer, \ 37 | 'Caller and signer cannot issue a refund.' 38 | 39 | hashlock = hashlib.sha256(secret) 40 | 41 | result = swaps[participant, hashlock] 42 | 43 | assert result is not None, 'No swap to refund found.' 44 | 45 | expiration, amount = result 46 | 47 | assert expiration < now, 'Swap has not expired.' 48 | 49 | con_erc20_clone.transfer(amount, ctx.caller) 50 | swaps[participant, hashlock] = None 51 | 52 | 53 | # Should fail if called 54 | def test(): 55 | return 123 -------------------------------------------------------------------------------- /tests/integration/test_constructor_args.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.stdlib.bridge.time import Datetime 3 | from contracting.client import ContractingClient 4 | import os 5 | 6 | class TestSenecaClientReplacesExecutor(TestCase): 7 | def setUp(self): 8 | self.c = ContractingClient(signer='stu') 9 | self.c.raw_driver.flush_full() 10 | 11 | submission_path = os.path.join(os.path.dirname(__file__), "test_contracts", "submission.s.py") 12 | 13 | with open(submission_path) as f: 14 | contract = f.read() 15 | 16 | self.c.raw_driver.set_contract(name='submission', code=contract) 17 | 18 | self.c.raw_driver.commit() 19 | 20 | # submit erc20 clone 21 | constructor_args_contract_path = os.path.join(os.path.dirname(__file__), "test_contracts", "constructor_args_contract.s.py") 22 | 23 | with open(constructor_args_contract_path) as f: 24 | self.code = f.read() 25 | 26 | def test_custom_args_works(self): 27 | self.c.submit(self.code, name='con_constructor_args_contract', constructor_args={'a': 123, 'b': 321}) 28 | 29 | contract = self.c.get_contract('con_constructor_args_contract') 30 | a, b = contract.get() 31 | 32 | self.assertEqual(a, 123) 33 | self.assertEqual(b, 321) 34 | 35 | def test_custom_args_overloading(self): 36 | with self.assertRaises(TypeError): 37 | self.c.submit(self.code, name='con_constructor_args_contract', constructor_args={'a': 123, 'x': 321}) 38 | 39 | def test_custom_args_not_enough_args(self): 40 | with self.assertRaises(TypeError): 41 | self.c.submit(self.code, name='con_constructor_args_contract', constructor_args={'a': 123}) 42 | -------------------------------------------------------------------------------- /src/contracting/storage/contract.py: -------------------------------------------------------------------------------- 1 | from contracting.compilation.compiler import ContractingCompiler 2 | from contracting.storage.driver import Driver 3 | from contracting.execution.runtime import rt 4 | from contracting.stdlib import env 5 | from contracting import constants 6 | 7 | _driver = rt.env.get('__Driver') or Driver() 8 | 9 | 10 | class Contract: 11 | def __init__(self, driver: Driver = _driver): 12 | self._driver = driver 13 | 14 | def submit(self, name, code, owner=None, constructor_args={}, developer=None): 15 | if self._driver.get_contract(name) is not None: 16 | raise Exception('Contract already exists.') 17 | 18 | c = ContractingCompiler(module_name=name) 19 | 20 | code_obj = c.parse_to_code(code, lint=True) 21 | 22 | scope = env.gather() 23 | scope.update({'__contract__': True}) 24 | scope.update(rt.env) 25 | 26 | exec(code_obj, scope) 27 | 28 | if scope.get(constants.INIT_FUNC_NAME) is not None: 29 | if constructor_args is None: 30 | constructor_args = {} 31 | scope[constants.INIT_FUNC_NAME](**constructor_args) 32 | 33 | now = scope.get('now') 34 | if now is not None: 35 | self._driver.set_contract( 36 | name=name, 37 | code=code_obj, 38 | owner=owner, 39 | overwrite=False, 40 | timestamp=now, 41 | developer=developer 42 | ) 43 | else: 44 | self._driver.set_contract( 45 | name=name, 46 | code=code_obj, 47 | owner=owner, 48 | overwrite=False, 49 | developer=developer 50 | ) 51 | -------------------------------------------------------------------------------- /tests/unit/test_sys_contracts/currency.s.py: -------------------------------------------------------------------------------- 1 | xrate = Variable() 2 | seed_amount = Variable() 3 | balances = Hash() 4 | allowed = Hash() 5 | 6 | @construct 7 | def seed(): 8 | xrate = 1.0 9 | seed_amount.set(1000000) 10 | balances['reserves'] = 0 11 | 12 | founder_wallets = [ 13 | '324ee2e3544a8853a3c5a0ef0946b929aa488cbe7e7ee31a0fef9585ce398502', 14 | 'a103715914a7aae8dd8fddba945ab63a169dfe6e37f79b4a58bcf85bfd681694', 15 | '20da05fdba92449732b3871cc542a058075446fedb41430ee882e99f9091cc4d', 16 | 'ed19061921c593a9d16875ca660b57aa5e45c811c8cf7af0cfcbd23faa52cbcd', 17 | 'cb9bfd4b57b243248796e9eb90bc4f0053d78f06ce68573e0fdca422f54bb0d2', 18 | 'c1f845ad8967b93092d59e4ef56aef3eba49c33079119b9c856a5354e9ccdf84' 19 | ] 20 | 21 | for w in founder_wallets: 22 | balances[w] = seed_amount.get() 23 | 24 | 25 | def assert_stamps(stamps): 26 | assert balances[ctx.signer] >= stamps, \ 27 | "Not enough funds to submit stamps" 28 | 29 | 30 | def submit_stamps(stamps): 31 | stamps *= xrate 32 | 33 | balances[ctx.signer] -= stamps 34 | balances['reserves'] += stamps 35 | 36 | @export 37 | def transfer(to, amount): 38 | assert balances[ctx.signer] - amount >= 0, \ 39 | 'Sender balance must be non-negative!!!' 40 | 41 | balances[ctx.sender] -= amount 42 | balances[to] += amount 43 | 44 | @export 45 | def approve(spender, amount): 46 | allowed[ctx.sender][spender] = amount 47 | 48 | @export 49 | def transfer_from(approver, spender, amount): 50 | assert allowed[approver][spender] >= amount 51 | assert balances[approver] >= amount 52 | 53 | allowed[approver][spender] -= amount 54 | 55 | balances[approver] -= amount 56 | balances[spender] += amount 57 | -------------------------------------------------------------------------------- /src/contracting/stdlib/bridge/orm.py: -------------------------------------------------------------------------------- 1 | from contracting.storage.orm import Variable, Hash, ForeignVariable, ForeignHash, LogEvent 2 | from contracting.storage.contract import Contract 3 | from contracting.execution.runtime import rt 4 | 5 | 6 | class V(Variable): 7 | def __init__(self, *args, **kwargs): 8 | if rt.env.get('__Driver') is not None: 9 | kwargs['driver'] = rt.env.get('__Driver') 10 | super().__init__(*args, **kwargs) 11 | 12 | 13 | class H(Hash): 14 | def __init__(self, *args, **kwargs): 15 | if rt.env.get('__Driver') is not None: 16 | kwargs['driver'] = rt.env.get('__Driver') 17 | super().__init__(*args, **kwargs) 18 | 19 | 20 | class FV(ForeignVariable): 21 | def __init__(self, *args, **kwargs): 22 | if rt.env.get('__Driver') is not None: 23 | kwargs['driver'] = rt.env.get('__Driver') 24 | super().__init__(*args, **kwargs) 25 | 26 | 27 | class FH(ForeignHash): 28 | def __init__(self, *args, **kwargs): 29 | if rt.env.get('__Driver') is not None: 30 | kwargs['driver'] = rt.env.get('__Driver') 31 | super().__init__(*args, **kwargs) 32 | 33 | 34 | class C(Contract): 35 | def __init__(self, *args, **kwargs): 36 | if rt.env.get('__Driver') is not None: 37 | kwargs['driver'] = rt.env.get('__Driver') 38 | super().__init__(*args, **kwargs) 39 | 40 | 41 | class LE(LogEvent): 42 | def __init__(self, *args, **kwargs): 43 | if rt.env.get('__Driver') is not None: 44 | kwargs['driver'] = rt.env.get('__Driver') 45 | super().__init__(*args, **kwargs) 46 | 47 | 48 | # Define the locals that will be available for smart contracts at runtime 49 | exports = { 50 | 'Variable': V, 51 | 'Hash': H, 52 | 'ForeignVariable': FV, 53 | 'ForeignHash': FH, 54 | 'LogEvent': LE, 55 | '__Contract': C 56 | } 57 | -------------------------------------------------------------------------------- /tests/performance/prof_transfer.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | from contracting.storage.driver import Driver 3 | from contracting.execution.executor import Executor 4 | 5 | def submission_kwargs_for_file(f): 6 | # Get the file name only by splitting off directories 7 | split = f.split('/') 8 | split = split[-1] 9 | 10 | # Now split off the .s 11 | split = split.split('.') 12 | contract_name = split[0] 13 | 14 | with open(f) as file: 15 | contract_code = file.read() 16 | 17 | return { 18 | 'name': contract_name, 19 | 'code': contract_code, 20 | } 21 | 22 | 23 | TEST_SUBMISSION_KWARGS = { 24 | 'sender': 'stu', 25 | 'contract_name': 'submission', 26 | 'function_name': 'submit_contract' 27 | } 28 | 29 | 30 | d = Driver() 31 | d.flush_full() 32 | 33 | with open('../../contracting/contracts/submission.s.py') as f: 34 | contract = f.read() 35 | 36 | d.set_contract(name='submission', 37 | code=contract) 38 | d.commit() 39 | 40 | recipients = [secrets.token_hex(16) for _ in range(1000)] 41 | 42 | 43 | e = Executor() 44 | 45 | e.execute(**TEST_SUBMISSION_KWARGS, 46 | kwargs=submission_kwargs_for_file('../integration/test_contracts/erc20_clone.s.py')) 47 | 48 | 49 | import datetime 50 | 51 | for i in range(20): 52 | now = datetime.datetime.now() 53 | 54 | # profiler = Profiler() 55 | # profiler.start() 56 | for r in recipients: 57 | res = e.execute(sender='stu', 58 | contract_name='con_erc20_clone', 59 | function_name='transfer', 60 | kwargs={ 61 | 'amount': 1, 62 | 'to': r 63 | }) 64 | print(res) 65 | 66 | # profiler.stop() 67 | 68 | print(datetime.datetime.now() - now) 69 | 70 | d.flush_full() 71 | 72 | # print(profiler.last_session.duration) 73 | # print(profiler.output_text(unicode=True, color=True, show_all=True)) 74 | 75 | -------------------------------------------------------------------------------- /tests/performance/test_transfer.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import secrets 3 | from contracting.storage.driver import Driver 4 | from contracting.execution.executor import Executor 5 | import os 6 | 7 | def submission_kwargs_for_file(f): 8 | # Get the file name only by splitting off directories 9 | split = f.split('/') 10 | split = split[-1] 11 | 12 | # Now split off the .s 13 | split = split.split('.') 14 | contract_name = split[0] 15 | 16 | with open(f) as file: 17 | contract_code = file.read() 18 | 19 | return { 20 | 'name': f'con_{contract_name}', 21 | 'code': contract_code, 22 | } 23 | 24 | 25 | TEST_SUBMISSION_KWARGS = { 26 | 'sender': 'stu', 27 | 'contract_name': 'submission', 28 | 'function_name': 'submit_contract' 29 | } 30 | 31 | 32 | class TestSandbox(TestCase): 33 | def setUp(self): 34 | self.d = Driver() 35 | self.d.flush_full() 36 | 37 | self.script_dir = os.path.dirname(os.path.abspath(__file__)) 38 | 39 | submission_path = os.path.join(self.script_dir, "test_contracts", "submission.s.py") 40 | with open(submission_path) as f: 41 | contract = f.read() 42 | 43 | self.d.set_contract(name='submission', 44 | code=contract) 45 | self.d.commit() 46 | 47 | self.recipients = [secrets.token_hex(16) for _ in range(10000)] 48 | 49 | def tearDown(self): 50 | self.d.flush_full() 51 | 52 | def test_transfer_performance(self): 53 | e = Executor() 54 | 55 | e.execute(**TEST_SUBMISSION_KWARGS, 56 | kwargs=submission_kwargs_for_file(os.path.join(self.script_dir, "test_contracts", "erc20_clone.s.py"))) 57 | 58 | for r in self.recipients: 59 | e.execute(sender='stu', 60 | contract_name='con_erc20_clone', 61 | function_name='transfer', 62 | kwargs={ 63 | 'amount': 1, 64 | 'to': r 65 | }) -------------------------------------------------------------------------------- /tests/integration/test_run_private_function.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.client import ContractingClient 3 | import os 4 | 5 | class TestRunPrivateFunction(TestCase): 6 | def setUp(self): 7 | self.client = ContractingClient() 8 | 9 | private_methods_path = os.path.join(os.path.dirname(__file__), "test_contracts", "private_methods.s.py") 10 | 11 | with open(private_methods_path) as f: 12 | code = f.read() 13 | 14 | self.client.submit(code, name='private_methods') 15 | self.private_methods = self.client.get_contract('private_methods') 16 | 17 | def tearDown(self): 18 | self.client.flush() 19 | 20 | def test_can_call_public_func(self): 21 | self.assertEqual(self.private_methods.call_private(), 'abc') 22 | 23 | def test_cannot_call_private_func(self): 24 | with self.assertRaises(Exception): 25 | self.private_methods.private() 26 | 27 | def test_cannot_execute_private_func(self): 28 | with self.assertRaises(AssertionError): 29 | self.private_methods.executor.execute( 30 | sender='sys', 31 | contract_name='private_methods', 32 | function_name='__private', 33 | kwargs={} 34 | ) 35 | 36 | def test_can_call_private_func_if_run_private_function_called(self): 37 | self.assertEqual(self.private_methods.run_private_function('__private'), 'abc') 38 | 39 | def test_can_call_private_func_if_run_private_function_called_and_no_prefix(self): 40 | self.assertEqual(self.private_methods.run_private_function('private'), 'abc') 41 | 42 | def test_can_call_private_but_then_not(self): 43 | self.assertEqual(self.private_methods.run_private_function('private'), 'abc') 44 | 45 | with self.assertRaises(AssertionError): 46 | self.private_methods.executor.execute( 47 | sender='sys', 48 | contract_name='private_methods', 49 | function_name='__private', 50 | kwargs={} 51 | ) 52 | -------------------------------------------------------------------------------- /tests/integration/test_executor_transaction_writes.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from unittest import TestCase 3 | from contracting.stdlib.bridge.time import Datetime 4 | from contracting.client import ContractingClient 5 | from contracting.storage.driver import Driver 6 | import os 7 | 8 | class TestTransactionWrites(TestCase): 9 | def setUp(self): 10 | self.c = ContractingClient() 11 | self.c.flush() 12 | 13 | currency_path = os.path.join(os.path.dirname(__file__), "test_contracts", "currency.s.py") 14 | 15 | with open(currency_path) as f: 16 | contract = f.read() 17 | 18 | self.c.submit(contract, name="currency") 19 | 20 | self.c.executor.driver.commit() 21 | 22 | def tearDown(self): 23 | self.c.raw_driver.flush_full() 24 | 25 | def test_transfers(self): 26 | self.c.set_var( 27 | contract="currency", variable="balances", arguments=["bill"], value=200 28 | ) 29 | res3 = self.c.executor.execute( 30 | contract_name="currency", 31 | function_name="transfer", 32 | kwargs={"to": "someone", "amount": 100}, 33 | stamps=1000, 34 | sender="bill", 35 | ) 36 | self.assertEquals(res3["writes"], self.c.executor.driver.pending_writes) 37 | res2 = self.c.executor.execute( 38 | contract_name="currency", 39 | function_name="transfer", 40 | kwargs={"to": "someone", "amount": 100}, 41 | stamps=1000, 42 | sender="bill", 43 | ) 44 | 45 | self.assertEquals(res2["writes"], self.c.executor.driver.pending_writes) 46 | # This operation will raise an exception, so will not make any writes. 47 | res3 = self.c.executor.execute( 48 | contract_name="currency", 49 | function_name="transfer", 50 | kwargs={"to": "someone", "amount": 100}, 51 | stamps=1000, 52 | sender="bill", 53 | ) 54 | self.assertEquals(res3["writes"], {}) 55 | 56 | 57 | if __name__ == "__main__": 58 | import unittest 59 | 60 | unittest.main() 61 | -------------------------------------------------------------------------------- /tests/unit/test_state_management.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from xian.processor import TxProcessor 3 | from contracting.client import ContractingClient 4 | from xian.services.simulator import Simulator 5 | from xian.constants import Constants 6 | import os 7 | import pathlib 8 | class MyTestCase(unittest.TestCase): 9 | 10 | def setUp(self): 11 | constants = Constants() 12 | self.c = ContractingClient(storage_home=constants.STORAGE_HOME) 13 | self.tx_processor = TxProcessor(client=self.c) 14 | self.stamp_calculator = Simulator() 15 | self.d = self.c.raw_driver 16 | self.d.flush_full() 17 | 18 | script_dir = os.path.dirname(os.path.abspath(__file__)) 19 | 20 | submission_contract_path = os.path.join(script_dir, "contracts", "submission.s.py") 21 | 22 | with open(submission_contract_path) as f: 23 | contract = f.read() 24 | self.d.set_contract(name="submission", code=contract) 25 | 26 | def deploy_broken_stuff(self): 27 | # Get the directory where the script is located 28 | script_dir = os.path.dirname(os.path.abspath(__file__)) 29 | 30 | proxythis_path = os.path.join(script_dir, "contracts", "proxythis.py") 31 | with open(proxythis_path) as f: 32 | contract = f.read() 33 | 34 | self.c.submit( 35 | contract, 36 | name="con_proxythis", 37 | ) 38 | 39 | self.proxythis = self.c.get_contract("con_proxythis") 40 | 41 | thistest2_path = os.path.join(script_dir, "contracts", "thistest2.py") 42 | 43 | with open(thistest2_path) as f: 44 | contract = f.read() 45 | 46 | self.c.submit( 47 | contract, 48 | name="con_thistest2", 49 | ) 50 | 51 | self.thistest2 = self.c.get_contract("con_thistest2") 52 | 53 | def test_submit(self): 54 | self.deploy_broken_stuff() 55 | self.assertEqual(self.proxythis.proxythis(con="con_thistest2", signer="address"), ("con_thistest2", "con_proxythis")) 56 | self.assertEqual(self.proxythis.noproxy(signer="address"), ("con_proxythis", "address")) 57 | self.assertEqual(self.proxythis.nestedproxythis(con="con_thistest2", signer="address"), ("con_thistest2", "con_proxythis")) 58 | 59 | if __name__ == '__main__': 60 | unittest.main() -------------------------------------------------------------------------------- /tests/integration/test_memory_clean_up_after_execution.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.storage.driver import Driver 3 | from contracting.execution.executor import Executor 4 | import os 5 | 6 | import contracting 7 | import psutil 8 | import gc 9 | 10 | 11 | def submission_kwargs_for_file(f): 12 | # Get the file name only by splitting off directories 13 | split = f.split('/') 14 | split = split[-1] 15 | 16 | # Now split off the .s 17 | split = split.split('.') 18 | contract_name = split[0] 19 | 20 | with open(f) as file: 21 | contract_code = file.read() 22 | 23 | return { 24 | 'name': f'con_{contract_name}', 25 | 'code': contract_code, 26 | } 27 | 28 | 29 | TEST_SUBMISSION_KWARGS = { 30 | 'sender': 'stu', 31 | 'contract_name': 'submission', 32 | 'function_name': 'submit_contract' 33 | } 34 | 35 | 36 | class TestMetering(TestCase): 37 | def setUp(self): 38 | # Hard load the submission contract 39 | self.d = Driver() 40 | self.d.flush_full() 41 | 42 | submission_path = os.path.join(os.path.dirname(__file__), "test_contracts", "submission.s.py") 43 | 44 | with open(submission_path) as f: 45 | contract = f.read() 46 | 47 | self.d.set_contract(name='submission', 48 | code=contract) 49 | self.d.commit() 50 | 51 | currency_path = os.path.join(os.path.dirname(__file__), "test_contracts", "currency.s.py") 52 | # Execute the currency contract with metering disabled 53 | self.e = Executor(driver=self.d) 54 | self.e.execute(**TEST_SUBMISSION_KWARGS, 55 | kwargs=submission_kwargs_for_file(currency_path), metering=False, auto_commit=True) 56 | 57 | def tearDown(self): 58 | self.d.flush_full() 59 | 60 | # def test_memory_clean_up_after_execution(self): 61 | # process = psutil.Process(os.getpid()) 62 | # before = process.memory_info().rss / 1024 / 1024 63 | # for i in range(500): 64 | # output = self.e.execute('stu', 'con_currency', 'transfer', kwargs={'amount': 100, 'to': 'colin'}, auto_commit=True,metering=True) 65 | # gc.collect() 66 | # after = process.memory_info().rss / 1024 / 1024 67 | # before_2 = process.memory_info().rss / 1024 / 1024 68 | # for i in range(500): 69 | # output = self.e.execute('stu', 'con_currency', 'transfer', kwargs={'amount': 100, 'to': 'colin'}, auto_commit=True,metering=False) 70 | # gc.collect() 71 | # after_2 = process.memory_info().rss / 1024 / 1024 72 | 73 | # print(f'RAM Difference with metering: {after - before} MB') 74 | # print(f'RAM Difference without metering: {after_2 - before_2} MB') 75 | 76 | 77 | 78 | if __name__ == '__main__': 79 | t = TestMetering() 80 | t.setUp() 81 | t.test_memory_clean_up_after_execution() 82 | t.tearDown() 83 | -------------------------------------------------------------------------------- /src/contracting/compilation/whitelists.py: -------------------------------------------------------------------------------- 1 | import ast, builtins 2 | 3 | ALLOWED_BUILTINS = {'Exception', 'False', 'None', 'True', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 4 | 'bytes', 'chr', 'dict', 'divmod', 'filter', 'format', 'frozenset', 'hex', 'int', 'isinstance', 5 | 'issubclass', 'import', 'len', 'list', 'map', 'max', 'min', 'oct', 'ord', 'pow', 'range', 'reversed', 6 | 'round', 'set', 'sorted', 'str', 'sum', 'tuple', 'zip'} 7 | 8 | ILLEGAL_BUILTINS = set(dir(builtins)) - ALLOWED_BUILTINS 9 | 10 | ALLOWED_AST_TYPES = {ast.Module, ast.Eq, ast.Call, ast.Dict, ast.Attribute, ast.Pow, ast.Index, ast.Not, ast.alias, 11 | ast.If, ast.FunctionDef, ast.Global, ast.GtE, ast.LtE, ast.Load, ast.arg, ast.Add, ast.Import, 12 | ast.ImportFrom, ast.Name, ast.Num, ast.BinOp, ast.Store, ast.Assert, ast.Assign, ast.AugAssign, 13 | ast.Subscript, ast.Compare, ast.Return, ast.NameConstant, ast.Expr, ast.keyword, ast.Sub, 14 | ast.arguments, ast.List, ast.Set, ast.Str, ast.UnaryOp, ast.Pass, ast.Tuple, ast.Div, ast.In, 15 | ast.NotIn, ast.Gt, ast.Lt, ast.Starred, ast.Mod, ast.NotEq, ast.For, ast.While, ast.ListComp, 16 | ast.comprehension, ast.Slice, ast.USub, ast.BoolOp, ast.And, ast.Or, ast.Mult, ast.IsNot, ast.Is, ast.Constant} 17 | 18 | ILLEGAL_AST_TYPES = { 19 | ast.AsyncFor, 20 | ast.AsyncFunctionDef, 21 | ast.AsyncWith, 22 | ast.Await, 23 | ast.ClassDef, 24 | ast.Ellipsis, 25 | ast.GeneratorExp, 26 | ast.Global, 27 | ast.ImportFrom, 28 | ast.Interactive, 29 | ast.Lambda, 30 | ast.MatMult, 31 | ast.Nonlocal, 32 | ast.Suite, 33 | ast.Try, 34 | ast.With, 35 | ast.Yield, 36 | ast.YieldFrom, 37 | } 38 | 39 | ALLOWED_ANNOTATION_TYPES = {'dict', 'list', 'str', 'int', 'float', 'bool', 'datetime.timedelta', 'datetime.datetime', 'Any'} 40 | 41 | VIOLATION_TRIGGERS = [ 42 | "S1- Illegal contracting syntax type used", 43 | "S2- Illicit use of '_' before variable", 44 | "S3- Illicit use of Nested imports", 45 | "S4- ImportFrom compilation nodes not yet supported", 46 | "S5- Contract not found in lib", 47 | "S6- Illicit use of classes", 48 | "S7- Illicit use of Async functions", 49 | "S8- Invalid decorator used", 50 | "S9- Multiple use of constructors detected", 51 | "S10- Illicit use of multiple decorators", 52 | "S11- Illicit keyword overloading for ORM assignments", 53 | "S12- Multiple targets to ORM definition detected", 54 | "S13- No valid contracting decorator found", 55 | "S14- Illegal use of a builtin", 56 | "S15- Reuse of ORM name definition in a function definition argument name", 57 | "S16- Illegal argument annotation used", 58 | "S17- No valid argument annotation found", 59 | "S18- Illegal use of return annotation", 60 | "S19- Illegal use of a nested function definition." 61 | ] 62 | -------------------------------------------------------------------------------- /src/contracting/stdlib/bridge/imports.py: -------------------------------------------------------------------------------- 1 | from types import FunctionType, ModuleType 2 | from contracting.constants import PRIVATE_METHOD_PREFIX 3 | from contracting.storage.orm import Datum 4 | from contracting.storage.driver import Driver, OWNER_KEY 5 | from contracting.execution.runtime import rt 6 | 7 | import importlib 8 | import sys 9 | 10 | 11 | def extract_closure(fn): 12 | closure = fn.__closure__[0] 13 | return closure.cell_contents 14 | 15 | 16 | class Func: 17 | def __init__(self, name, args=(), private=False): 18 | self.name = name 19 | 20 | if private: 21 | self.name = PRIVATE_METHOD_PREFIX + self.name 22 | 23 | self.args = args 24 | 25 | def is_of(self, f: FunctionType): 26 | 27 | if f.__closure__ is not None: 28 | f = extract_closure(f) 29 | 30 | num_args = f.__code__.co_argcount 31 | 32 | if f.__code__.co_name == self.name and f.__code__.co_varnames[:num_args] == self.args: 33 | return True 34 | 35 | return False 36 | 37 | 38 | class Var: 39 | def __init__(self, name, t): 40 | self.name = PRIVATE_METHOD_PREFIX + name 41 | assert issubclass(t, Datum), 'Cannot enforce a variable that is not a Variable, Hash, or Foreign type!' 42 | self.type = t 43 | 44 | def is_of(self, v): 45 | if isinstance(v, self.type): 46 | return True 47 | return False 48 | 49 | 50 | def import_module(name): 51 | assert not name.isdigit() and all(c.isalnum() or c == '_' for c in name), 'Invalid contract name!' 52 | assert name.islower(), 'Name must be lowercase!' 53 | 54 | _driver = rt.env.get('__Driver') or Driver() 55 | 56 | if name in set(list(sys.stdlib_module_names) + list(sys.builtin_module_names)): 57 | raise ImportError 58 | 59 | if name.startswith('_'): 60 | raise ImportError 61 | 62 | if _driver.get_contract(name) is None: 63 | raise ImportError 64 | 65 | return importlib.import_module(name, package=None) 66 | 67 | 68 | def enforce_interface(m: ModuleType, interface: list): 69 | implemented = vars(m) 70 | 71 | for i in interface: 72 | attribute = implemented.get(i.name) 73 | if attribute is None: 74 | return False 75 | 76 | # Branch for data types 77 | if isinstance(attribute, Datum): 78 | if not i.is_of(attribute): 79 | return False 80 | 81 | if isinstance(attribute, FunctionType): 82 | if not i.is_of(attribute): 83 | return False 84 | 85 | return True 86 | 87 | 88 | def owner_of(m: ModuleType): 89 | _driver = rt.env.get('__Driver') or Driver() 90 | owner = _driver.get_var(m.__name__, OWNER_KEY) 91 | return owner 92 | 93 | 94 | imports_module = ModuleType('importlib') 95 | imports_module.import_module = import_module 96 | imports_module.enforce_interface = enforce_interface 97 | imports_module.Func = Func 98 | imports_module.Var = Var 99 | imports_module.owner_of = owner_of 100 | 101 | exports = { 102 | 'importlib': imports_module, 103 | } 104 | -------------------------------------------------------------------------------- /src/contracting/stdlib/bridge/random.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module wraps and exposes the Python stdlib random functions that can be made deterministic with a random seed 3 | and return fixed point precision where possible. This allows some psuedorandom behavior when it is nice to have, but 4 | with the caveat that it's based on environmental constants such as the last block hash and public information such 5 | as the sender's address to seed the random state so it's not *really* random. 6 | 7 | It's most likely 'random enough' for most cases, but people can always theoretically reproduce the seed and try to 8 | front-run a smart contract by testing the seeded randoms for a preferable outcome and submitting a transaction 9 | before the next block is minted. While this is extremely unlikely and hard to pull off, it's a valid hole in the 10 | security and needs to be accepted as a flaw when using random numbers on a reproducible transaction log such as a 11 | blockchain. 12 | """ 13 | 14 | import random 15 | 16 | from types import ModuleType 17 | from contracting.execution.runtime import rt 18 | 19 | 20 | class Seeded: 21 | s = False 22 | 23 | 24 | def seed(aux_salt=None): 25 | block_height = '0' 26 | if rt.env.get('block_num') is not None: 27 | block_height = str(rt.env.get('block_num')) 28 | 29 | block_hash = rt.env.get('block_hash') or '0' 30 | __input_hash = rt.env.get('__input_hash') or '0' 31 | 32 | # Auxiliary salt is used to create completely unique random seeds based on some other properties (optional) 33 | auxiliary_salt = '' 34 | if aux_salt is not None and rt.env.get(aux_salt): 35 | auxiliary_salt = str(rt.env.get(aux_salt)) 36 | else: 37 | if rt.env.get("AUXILIARY_SALT"): 38 | auxiliary_salt = str(rt.env.get("AUXILIARY_SALT")) 39 | 40 | s = block_height + block_hash + __input_hash + auxiliary_salt 41 | 42 | random.seed(s) 43 | Seeded.s = True 44 | 45 | 46 | def getrandbits(k): 47 | assert Seeded.s, 'Random state not seeded. Call seed().' 48 | 49 | b_str = '' 50 | for i in range(k): 51 | if random.random() > 0.5: 52 | b_str += '1' 53 | else: 54 | b_str += '0' 55 | 56 | return int(b_str, 2) 57 | 58 | 59 | def shuffle(l): 60 | assert Seeded.s, 'Random state not seeded. Call seed().' 61 | random.shuffle(l) 62 | 63 | 64 | def randrange(k): 65 | assert Seeded.s, 'Random state not seeded. Call seed().' 66 | return random.randrange(k) 67 | 68 | 69 | def randint(a, b): 70 | assert Seeded.s, 'Random state not seeded. Call seed().' 71 | return random.randint(a, b) 72 | 73 | 74 | def choice(l): 75 | assert Seeded.s, 'Random state not seeded. Call seed().' 76 | return random.choice(l) 77 | 78 | 79 | def choices(l, k): 80 | assert Seeded.s, 'Random state not seeded. Call seed().' 81 | return random.choices(l, k=k) 82 | 83 | 84 | # Construct module for exposure in the contract runtime 85 | random_module = ModuleType('random') 86 | random_module.seed = seed 87 | random_module.shuffle = shuffle 88 | random_module.getrandbits = getrandbits 89 | random_module.randrange = randrange 90 | random_module.randint = randint 91 | random_module.choice = choice 92 | random_module.choices = choices 93 | 94 | # Add it to the export object and it's good to go 95 | exports = { 96 | 'random': random_module 97 | } 98 | -------------------------------------------------------------------------------- /tests/integration/test_builtins_locked_off.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.client import ContractingClient 3 | import os 4 | 5 | class TestBuiltinsLockedOff(TestCase): 6 | def setUp(self): 7 | self.c = ContractingClient(signer='stu') 8 | 9 | def tearDown(self): 10 | self.c.raw_driver.flush_full() 11 | 12 | 13 | def test_if_builtin_can_be_submitted(self): 14 | builtin_path = os.path.join(os.path.dirname(__file__), "test_contracts", "builtin_lib.s.py") 15 | 16 | with open(builtin_path) as f: 17 | contract = f.read() 18 | 19 | with self.assertRaises(Exception): 20 | self.c.submit(contract, name='con_builtin') 21 | 22 | def test_if_non_builtin_can_be_submitted(self): 23 | pass 24 | 25 | 26 | class TestMathBuiltinsLockedOff(TestCase): 27 | def setUp(self): 28 | self.c = ContractingClient(signer='stu') 29 | 30 | def tearDown(self): 31 | self.c.raw_driver.flush_full() 32 | 33 | def test_if_builtin_can_be_submitted(self): 34 | mathtime_path = os.path.join(os.path.dirname(__file__), "test_contracts", "mathtime.s.py") 35 | 36 | with open(mathtime_path) as f: 37 | contract = f.read() 38 | 39 | with self.assertRaises(Exception): 40 | self.c.submit(contract, name='con_mathtime') 41 | 42 | 43 | class TestDatabaseLoaderLoadsFirst(TestCase): 44 | def setUp(self): 45 | self.c = ContractingClient(signer='stu') 46 | 47 | def tearDown(self): 48 | self.c.raw_driver.flush_full() 49 | 50 | def test_if_builtin_can_be_submitted(self): 51 | contracting_path = os.path.join(os.path.dirname(__file__), "test_contracts", "contracting.s.py") 52 | 53 | with open(contracting_path) as f: 54 | contract = f.read() 55 | self.c.submit(contract, name='con_contracting') 56 | 57 | import_test_path = os.path.join(os.path.dirname(__file__), "test_contracts", "import_test.s.py") 58 | 59 | with open(import_test_path) as f: 60 | contract = f.read() 61 | with self.assertRaises(ImportError): 62 | self.c.submit(contract, name='con_import_test') 63 | 64 | 65 | class TestDynamicImport(TestCase): 66 | def setUp(self): 67 | self.c = ContractingClient(signer='stu') 68 | 69 | def tearDown(self): 70 | self.c.raw_driver.flush_full() 71 | 72 | def test_if_builtin_can_be_submitted(self): 73 | dynamic_import_path = os.path.join(os.path.dirname(__file__), "test_contracts", "dynamic_import.s.py") 74 | 75 | with open(dynamic_import_path) as f: 76 | contract = f.read() 77 | self.c.submit(contract, name='con_dynamic_import') 78 | 79 | dynamic_import = self.c.get_contract('con_dynamic_import') 80 | 81 | with self.assertRaises(ImportError): 82 | dynamic_import.import_thing(name='con_math') 83 | 84 | 85 | class TestFloatIssue(TestCase): 86 | def setUp(self): 87 | self.c = ContractingClient(signer='stu') 88 | 89 | def tearDown(self): 90 | self.c.raw_driver.flush_full() 91 | 92 | def test_if_builtin_can_be_submitted(self): 93 | float_issue_path = os.path.join(os.path.dirname(__file__), "test_contracts", "float_issue.s.py") 94 | 95 | with open(float_issue_path) as f: 96 | contract = f.read() 97 | self.c.submit(contract, name='con_float_issue') 98 | 99 | float_issue = self.c.get_contract('con_float_issue') 100 | 101 | float_issue.get(x=0.1, y=0.1) 102 | -------------------------------------------------------------------------------- /tests/unit/test_context_data_struct.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.execution.runtime import Context 3 | 4 | 5 | class TestContext(TestCase): 6 | def test_get_state(self): 7 | c = Context(base_state={ 8 | 'caller': 'stu', 9 | 'signer': 'stu', 10 | 'this': 'contract', 11 | 'owner': None 12 | }) 13 | 14 | self.assertEqual(c._get_state(), c._base_state) 15 | 16 | def test_get_state_after_added_state(self): 17 | c = Context(base_state={ 18 | 'caller': 'stu', 19 | 'signer': 'stu', 20 | 'this': 'contract', 21 | 'owner': None 22 | }) 23 | 24 | new_state = { 25 | 'caller': 'stuart', 26 | 'signer': 'stuart', 27 | 'this': 'contracts', 28 | 'owner': 123 29 | } 30 | 31 | c._add_state(new_state) 32 | 33 | self.assertEqual(c._get_state(), new_state) 34 | 35 | def test_pop_state_doesnt_fail_if_none_added(self): 36 | c = Context(base_state={ 37 | 'caller': 'stu', 38 | 'signer': 'stu', 39 | 'this': 'contract', 40 | 'owner': None 41 | }) 42 | 43 | c._pop_state() 44 | 45 | self.assertEqual(c._get_state(), c._base_state) 46 | 47 | def test_pop_state_removes_last_state(self): 48 | c = Context(base_state={ 49 | 'caller': 'stu', 50 | 'signer': 'stu', 51 | 'this': 'contract', 52 | 'owner': None 53 | }) 54 | 55 | new_state = { 56 | 'caller': 'stuart', 57 | 'signer': 'stuart', 58 | 'this': 'contracts', 59 | 'owner': 123 60 | } 61 | 62 | c._add_state(new_state) 63 | 64 | self.assertEqual(c._get_state(), new_state) 65 | 66 | c._pop_state() 67 | 68 | self.assertEqual(c._get_state(), c._base_state) 69 | 70 | def test_add_state_doesnt_work_if_this_is_same(self): 71 | c = Context(base_state={ 72 | 'caller': 'stu', 73 | 'signer': 'stu', 74 | 'this': 'contract', 75 | 'owner': None 76 | }) 77 | 78 | new_state = { 79 | 'caller': 'stuart', 80 | 'signer': 'stuart', 81 | 'this': 'contract', 82 | 'owner': 123 83 | } 84 | 85 | c._add_state(new_state) 86 | 87 | self.assertEqual(c._get_state(), c._base_state) 88 | 89 | def test_properties_read(self): 90 | c = Context(base_state={ 91 | 'caller': 'stu', 92 | 'signer': 'stu', 93 | 'this': 'contract', 94 | 'owner': None 95 | }) 96 | 97 | self.assertEqual(c._base_state['this'], c.this) 98 | self.assertEqual(c._base_state['caller'], c.caller) 99 | self.assertEqual(c._base_state['signer'], c.signer) 100 | self.assertEqual(c._base_state['owner'], c.owner) 101 | 102 | def test_properties_cant_be_written(self): 103 | c = Context(base_state={ 104 | 'caller': 'stu', 105 | 'signer': 'stu', 106 | 'this': 'contract', 107 | 'owner': None 108 | }) 109 | 110 | with self.assertRaises(Exception): 111 | c.this = 1 112 | 113 | with self.assertRaises(Exception): 114 | c.caller = 1 115 | 116 | with self.assertRaises(Exception): 117 | c.signer = 1 118 | 119 | with self.assertRaises(Exception): 120 | c.owner = 1 121 | -------------------------------------------------------------------------------- /tests/unit/test_module.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.execution.module import * 3 | from contracting.storage.driver import Driver 4 | import types 5 | import glob 6 | import os 7 | 8 | class TestDatabase(TestCase): 9 | def setUp(self): 10 | self.d = Driver() 11 | self.d.flush_full() 12 | 13 | def tearDown(self): 14 | self.d.flush_full() 15 | 16 | def test_push_and_get_contract(self): 17 | code = 'a = 123' 18 | name = 'test' 19 | 20 | self.d.set_contract(name, code) 21 | _code = self.d.get_contract(name) 22 | 23 | self.assertEqual(code, _code, 'Pushing and getting contracts is not working.') 24 | 25 | def test_flush(self): 26 | code = 'a = 123' 27 | name = 'test' 28 | 29 | self.d.set_contract(name, code) 30 | self.d.commit() 31 | self.d.flush_full() 32 | 33 | self.assertIsNone(self.d.get_contract(name)) 34 | 35 | 36 | class TestDatabaseLoader(TestCase): 37 | def setUp(self): 38 | self.dl = DatabaseLoader() 39 | 40 | def test_init(self): 41 | self.assertTrue(isinstance(self.dl.d, Driver), 'self.d is not a Database object.') 42 | 43 | def test_create_module(self): 44 | self.assertEqual(self.dl.create_module(None), None, 'self.create_module should return None') 45 | 46 | def test_exec_module(self): 47 | module = types.ModuleType('test') 48 | 49 | self.dl.d.set_contract('test', 'b = 1337') 50 | self.dl.exec_module(module) 51 | self.dl.d.flush_full() 52 | 53 | self.assertEqual(module.b, 1337) 54 | 55 | def test_exec_module_nonattribute(self): 56 | module = types.ModuleType('test') 57 | 58 | self.dl.d.set_contract('test', 'b = 1337') 59 | self.dl.exec_module(module) 60 | self.dl.d.flush_full() 61 | 62 | with self.assertRaises(AttributeError): 63 | module.a 64 | 65 | def test_module_representation(self): 66 | module = types.ModuleType('howdy') 67 | 68 | self.assertEqual(self.dl.module_repr(module), "") 69 | 70 | 71 | class TestInstallLoader(TestCase): 72 | def test_install_loader(self): 73 | uninstall_database_loader() 74 | 75 | self.assertNotIn(DatabaseFinder, sys.meta_path) 76 | 77 | install_database_loader() 78 | 79 | self.assertIn(DatabaseFinder, sys.meta_path) 80 | 81 | uninstall_database_loader() 82 | 83 | self.assertNotIn(DatabaseFinder, sys.meta_path) 84 | 85 | def test_integration_and_importing(self): 86 | dl = DatabaseLoader() 87 | dl.d.set_contract('testing', 'a = 1234567890') 88 | dl.d.commit() 89 | 90 | install_database_loader() 91 | 92 | import testing 93 | 94 | #dl.d.flush() 95 | 96 | self.assertEqual(testing.a, 1234567890) 97 | 98 | 99 | driver = Driver() 100 | 101 | 102 | class TestModuleLoadingIntegration(TestCase): 103 | def setUp(self): 104 | sys.meta_path.append(DatabaseFinder) 105 | driver.flush_full() 106 | 107 | self.script_dir = os.path.dirname(os.path.abspath(__file__)) 108 | contracts = glob.glob(os.path.join(self.script_dir, "test_sys_contracts", "*.py")) 109 | for contract in contracts: 110 | name = contract.split('/')[-1] 111 | name = name.split('.')[0] 112 | 113 | with open(contract) as f: 114 | code = f.read() 115 | 116 | driver.set_contract(name=name, code=code) 117 | driver.commit() 118 | 119 | def tearDown(self): 120 | sys.meta_path.remove(DatabaseFinder) 121 | driver.flush_full() 122 | 123 | def test_get_code_string(self): 124 | ctx = types.ModuleType('ctx') 125 | code = '''import module1 126 | 127 | print("now i can run my functions!") 128 | ''' 129 | 130 | exec(code, vars(ctx)) 131 | 132 | print('ok do it again') 133 | 134 | exec(code, vars(ctx)) 135 | -------------------------------------------------------------------------------- /src/contracting/execution/runtime.py: -------------------------------------------------------------------------------- 1 | from contracting import constants 2 | from contracting.execution.tracer import Tracer 3 | 4 | import contracting 5 | import sys 6 | import os 7 | import math 8 | 9 | 10 | class Context: 11 | def __init__(self, base_state, maxlen=constants.RECURSION_LIMIT): 12 | self._state = [] 13 | self._depth = [] 14 | self._base_state = base_state 15 | self._maxlen = maxlen 16 | 17 | def _context_changed(self, contract): 18 | if self._get_state()['this'] == contract: 19 | return False 20 | return True 21 | 22 | def _get_state(self): 23 | if len(self._state) == 0: 24 | return self._base_state 25 | return self._state[-1] 26 | 27 | def _add_state(self, state: dict): 28 | if self._context_changed(state['this']) and len(self._state) < self._maxlen: 29 | self._state.append(state) 30 | self._depth.append(1) 31 | 32 | def _ins_state(self): 33 | if len(self._depth) > 0: 34 | self._depth[-1] += 1 35 | 36 | def _pop_state(self): 37 | if len(self._state) > 0: #len(self._state) should equal len(self._depth) 38 | self._depth[-1] -= 1 39 | if self._depth[-1] == 0: 40 | self._state.pop(-1) 41 | self._depth.pop(-1) 42 | 43 | def _reset(self): 44 | self._state = [] 45 | self._depth = [] 46 | 47 | @property 48 | def this(self): 49 | return self._get_state()['this'] 50 | 51 | @property 52 | def caller(self): 53 | return self._get_state()['caller'] 54 | 55 | @property 56 | def signer(self): 57 | return self._get_state()['signer'] 58 | 59 | @property 60 | def owner(self): 61 | return self._get_state()['owner'] 62 | 63 | @property 64 | def entry(self): 65 | return self._get_state()['entry'] 66 | 67 | @property 68 | def submission_name(self): 69 | return self._get_state()['submission_name'] 70 | 71 | _context = Context({ 72 | 'this': None, 73 | 'caller': None, 74 | 'owner': None, 75 | 'signer': None, 76 | 'entry': None, 77 | 'submission_name': None 78 | }) 79 | 80 | WRITE_MAX = 1024 * 128 81 | 82 | 83 | class Runtime: 84 | cu_path = contracting.__path__[0] 85 | cu_path = os.path.join(cu_path, 'execution', 'metering', 'cu_costs.const') 86 | 87 | os.environ['CU_COST_FNAME'] = cu_path 88 | 89 | loaded_modules = [] 90 | 91 | env = {} 92 | stamps = 0 93 | 94 | writes = 0 95 | 96 | tracer = Tracer() 97 | 98 | signer = None 99 | 100 | context = _context 101 | 102 | @classmethod 103 | def set_up(cls, stmps, meter): 104 | if meter: 105 | cls.stamps = stmps 106 | cls.tracer.set_stamp(stmps) 107 | cls.tracer.start() 108 | 109 | cls.context._reset() 110 | 111 | @classmethod 112 | def clean_up(cls): 113 | cls.tracer.stop() 114 | cls.tracer.reset() 115 | cls.stamps = 0 116 | cls.writes = 0 117 | 118 | cls.signer = None 119 | 120 | for mod in cls.loaded_modules: 121 | if sys.modules.get(mod) is not None: 122 | del sys.modules[mod] 123 | 124 | cls.loaded_modules = [] 125 | cls.env = {} 126 | 127 | @classmethod 128 | def deduct_read(cls, key, value): 129 | if cls.tracer.is_started(): 130 | cost = len(key) + len(value) 131 | cost *= constants.READ_COST_PER_BYTE 132 | cls.tracer.add_cost(cost) 133 | 134 | @classmethod 135 | def deduct_write(cls, key, value): 136 | if key is not None and cls.tracer.is_started(): 137 | cost = len(key) + len(value) 138 | cls.writes += cost 139 | assert cls.writes < WRITE_MAX, 'You have exceeded the maximum write capacity per transaction!' 140 | 141 | stamp_cost = cost * constants.WRITE_COST_PER_BYTE 142 | cls.tracer.add_cost(stamp_cost) 143 | 144 | 145 | rt = Runtime() 146 | -------------------------------------------------------------------------------- /tests/unit/test_client.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.client import ContractingClient 3 | from contracting.storage.driver import Driver 4 | import os 5 | from pathlib import Path 6 | 7 | class TestClient(TestCase): 8 | def setUp(self): 9 | self.client = None 10 | 11 | self.driver = Driver() 12 | 13 | self.script_dir = os.path.dirname(os.path.abspath(__file__)) 14 | 15 | submission_file_path = os.path.join(self.script_dir, "contracts", "submission.s.py") 16 | with open(submission_file_path) as f: 17 | self.submission_contract_file = f.read() 18 | 19 | def tearDown(self): 20 | if self.client: 21 | self.client.flush() 22 | 23 | def test_set_submission_updates_contract_file(self): 24 | self.client = ContractingClient(driver=self.driver) 25 | self.client.flush() 26 | 27 | submission_1_code = self.client.raw_driver.get('submission.__code__') 28 | 29 | self.script_dir = os.path.dirname(os.path.abspath(__file__)) 30 | submission_file_path = os.path.join(self.script_dir, "precompiled", "updated_submission.py") 31 | 32 | self.driver.flush_full() 33 | self.client.set_submission_contract(filename=submission_file_path) 34 | 35 | submission_2_code = self.client.raw_driver.get('submission.__code__') 36 | 37 | self.assertNotEqual(submission_1_code, submission_2_code) 38 | 39 | def test_can_create_instance_without_submission_contract(self): 40 | self.client = ContractingClient(submission_filename=None, driver=self.driver) 41 | 42 | self.assertIsNotNone(self.client) 43 | 44 | 45 | def test_gets_submission_contract_from_state_if_no_filename_provided(self): 46 | self.driver.set_contract(name='submission', code=self.submission_contract_file) 47 | self.driver.commit() 48 | 49 | self.client = ContractingClient(submission_filename=None, driver=self.driver) 50 | 51 | self.assertIsNotNone(self.client.submission_contract) 52 | 53 | def test_set_submission_contract__sets_from_submission_filename_property(self): 54 | self.client = ContractingClient(driver=self.driver) 55 | 56 | self.client.raw_driver.flush_full() 57 | self.client.submission_contract = None 58 | 59 | contract = self.client.raw_driver.get_contract('submission') 60 | self.assertIsNone(contract) 61 | self.assertIsNone(self.client.submission_contract) 62 | 63 | self.client.set_submission_contract() 64 | 65 | contract = self.client.raw_driver.get_contract('submission') 66 | self.assertIsNotNone(contract) 67 | self.assertIsNotNone(self.client.submission_contract) 68 | 69 | def test_set_submission_contract__sets_from_submission_from_state(self): 70 | self.client = ContractingClient(driver=self.driver) 71 | 72 | self.client.raw_driver.flush_full() 73 | self.client.submission_contract = None 74 | 75 | contract = self.client.raw_driver.get_contract('submission') 76 | self.assertIsNone(contract) 77 | self.assertIsNone(self.client.submission_contract) 78 | 79 | self.driver.set_contract(name='submission', code=self.submission_contract_file) 80 | self.driver.commit() 81 | 82 | self.client.set_submission_contract() 83 | 84 | contract = self.client.raw_driver.get_contract('submission') 85 | self.assertIsNotNone(contract) 86 | self.assertIsNotNone(self.client.submission_contract) 87 | 88 | def test_set_submission_contract__no_contract_provided_or_found_raises_AssertionError(self): 89 | self.client = ContractingClient(driver=self.driver) 90 | 91 | self.client.raw_driver.flush_full() 92 | self.client.submission_filename = None 93 | 94 | with self.assertRaises(AssertionError): 95 | self.client.set_submission_contract() 96 | 97 | def test_submit__raises_AssertionError_if_no_submission_contract_set(self): 98 | self.client = ContractingClient(submission_filename=None, driver=self.driver) 99 | 100 | with self.assertRaises(AssertionError): 101 | self.client.submit(f="") 102 | 103 | 104 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit on any error 4 | 5 | # Colors for output 6 | RED='\033[0;31m' 7 | GREEN='\033[0;32m' 8 | YELLOW='\033[1;33m' 9 | NC='\033[0m' # No Color 10 | 11 | # Function to print with color 12 | print_status() { 13 | echo -e "${GREEN}==>${NC} $1" 14 | } 15 | 16 | print_warning() { 17 | echo -e "${YELLOW}WARNING:${NC} $1" 18 | } 19 | 20 | print_error() { 21 | echo -e "${RED}ERROR:${NC} $1" 22 | } 23 | 24 | # Check if a version bump type was provided 25 | if [ -z "$1" ]; then 26 | print_error "Please provide a version bump type: patch, minor, or major" 27 | echo "Usage: ./release.sh [patch|minor|major]" 28 | exit 1 29 | fi 30 | 31 | # Validate version bump type 32 | if [ "$1" != "patch" ] && [ "$1" != "minor" ] && [ "$1" != "major" ]; then 33 | print_error "Invalid version bump type. Please use: patch, minor, or major" 34 | exit 1 35 | fi 36 | 37 | # Make sure we're on the master branch 38 | BRANCH=$(git branch --show-current) 39 | if [ "$BRANCH" != "master" ]; then 40 | print_error "Please switch to the master branch before creating a release" 41 | exit 1 42 | fi 43 | 44 | # Make sure the working directory is clean 45 | if [ -n "$(git status --porcelain)" ]; then 46 | print_error "Working directory is not clean. Please commit or stash changes first." 47 | exit 1 48 | fi 49 | 50 | # Check if poetry is installed 51 | if ! command -v poetry &> /dev/null; then 52 | print_error "Poetry could not be found. Please install it first." 53 | exit 1 54 | fi 55 | 56 | # Check if pytest is installed 57 | if ! poetry run python -c "import pytest" 2>/dev/null; then 58 | print_warning "pytest is not installed. Skipping tests." 59 | RUN_TESTS=false 60 | else 61 | RUN_TESTS=true 62 | fi 63 | 64 | # Pull latest changes 65 | print_status "Pulling latest changes from master..." 66 | git pull origin master 67 | 68 | # Show what the new version will be and ask for confirmation 69 | CURRENT_VERSION=$(poetry version -s) 70 | NEW_VERSION=$(poetry version $1 --dry-run) 71 | print_status "Current version: $CURRENT_VERSION" 72 | print_status "New version will be: $NEW_VERSION" 73 | 74 | # Generate changelog 75 | LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "none") 76 | if [ "$LAST_TAG" != "none" ]; then 77 | print_status "Generating changelog since $LAST_TAG..." 78 | CHANGELOG=$(git log "$LAST_TAG"..HEAD --oneline --pretty=format:"- %s") 79 | else 80 | CHANGELOG=$(git log --oneline --pretty=format:"- %s") 81 | fi 82 | 83 | echo -e "\nChangelog:" 84 | echo "$CHANGELOG" 85 | echo 86 | 87 | # Check dependencies 88 | print_status "Checking for outdated dependencies..." 89 | poetry show --outdated || true 90 | 91 | # Run tests if available 92 | if [ "$RUN_TESTS" = true ]; then 93 | print_status "Running tests..." 94 | poetry run pytest || { 95 | print_error "Tests failed!" 96 | exit 1 97 | } 98 | fi 99 | 100 | # Final confirmation 101 | echo 102 | print_status "Ready to release version $NEW_VERSION" 103 | read -p "Continue? (y/n) " -n 1 -r 104 | echo 105 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 106 | print_status "Release cancelled." 107 | exit 1 108 | fi 109 | 110 | # Update version using Poetry 111 | print_status "Bumping version ($1)..." 112 | poetry version $1 113 | 114 | # Create release notes file 115 | RELEASE_NOTES="release_notes.md" 116 | echo "# Release Notes for v$NEW_VERSION" > $RELEASE_NOTES 117 | echo "" >> $RELEASE_NOTES 118 | echo "## Changes" >> $RELEASE_NOTES 119 | echo "$CHANGELOG" >> $RELEASE_NOTES 120 | 121 | # Stage and commit version bump 122 | print_status "Committing version bump..." 123 | git add pyproject.toml $RELEASE_NOTES 124 | git commit -m "Bump version to $NEW_VERSION 125 | 126 | Release Notes: 127 | $CHANGELOG" 128 | 129 | # Create and push tag 130 | print_status "Creating and pushing tag v$NEW_VERSION..." 131 | git tag -a "v$NEW_VERSION" -m "Version $NEW_VERSION 132 | 133 | $CHANGELOG" 134 | git push && git push --tags 135 | 136 | # Cleanup 137 | rm $RELEASE_NOTES 138 | 139 | print_status "Release process initiated!" 140 | print_status "Version $NEW_VERSION will be published to PyPI and GitHub releases automatically." 141 | print_status "You can monitor the progress at: https://github.com/xian-network/xian-contracting/actions" -------------------------------------------------------------------------------- /tests/unit/test_revert_on_exception.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from contracting.storage.driver import Driver 3 | from contracting.execution.executor import Executor 4 | from contracting.constants import STAMPS_PER_TAU 5 | from xian.processor import TxProcessor 6 | from contracting.client import ContractingClient 7 | import contracting 8 | import random 9 | import string 10 | import os 11 | import sys 12 | from loguru import logger 13 | 14 | # Get the directory where the script is located 15 | script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 16 | 17 | # Change the current working directory 18 | os.chdir(script_dir) 19 | 20 | def submission_kwargs_for_file(f): 21 | # Get the file name only by splitting off directories 22 | split = f.split('/') 23 | split = split[-1] 24 | 25 | # Now split off the .s 26 | split = split.split('.') 27 | contract_name = split[0] 28 | 29 | with open(f) as file: 30 | contract_code = file.read() 31 | 32 | return { 33 | 'name': f'con_{contract_name}', 34 | 'code': contract_code, 35 | } 36 | 37 | TEST_SUBMISSION_KWARGS = { 38 | 'sender': 'stu', 39 | 'contract_name': 'submission', 40 | 'function_name': 'submit_contract' 41 | } 42 | 43 | class MyTestCase(unittest.TestCase): 44 | 45 | def setUp(self): 46 | self.c = ContractingClient() 47 | self.tx_processor = TxProcessor(client=self.c) 48 | # Hard load the submission contract 49 | self.d = self.c.raw_driver 50 | self.d.flush_full() 51 | 52 | self.script_dir = os.path.dirname(os.path.abspath(__file__)) 53 | submission_file_path = os.path.join(self.script_dir, "contracts", "submission.s.py") 54 | 55 | with open(submission_file_path) as f: 56 | contract = f.read() 57 | 58 | self.d.set_contract(name='submission', code=contract) 59 | 60 | currency_file_path = os.path.join(self.script_dir, "contracts", "currency.s.py") 61 | 62 | with open(currency_file_path) as f: 63 | contract = f.read() 64 | self.d.set_contract(name='currency', code=contract) 65 | 66 | self.c.executor.execute(**TEST_SUBMISSION_KWARGS, kwargs=submission_kwargs_for_file(currency_file_path), metering=False, auto_commit=True) 67 | 68 | exception_file_path = os.path.join(self.script_dir, "contracts", "exception.s.py") 69 | 70 | with open(exception_file_path) as f: 71 | contract = f.read() 72 | self.d.set_contract(name='exception', code=contract) 73 | 74 | self.c.executor.execute(**TEST_SUBMISSION_KWARGS, 75 | kwargs=submission_kwargs_for_file(exception_file_path), 76 | metering=False, auto_commit=True) 77 | self.d.commit() 78 | 79 | def test_exception(self): 80 | prior_balance = self.d.get('con_exception.balances:stu') 81 | logger.debug(f"Prior balance (exception): {prior_balance}") 82 | 83 | 84 | output = self.tx_processor.process_tx({ 85 | "payload": 86 | {'sender': 'stu', 'contract': 'con_exception', 'function': 'transfer', 'kwargs': {'amount': 100, 'to': 'colin'},"stamps_supplied":1000}, 87 | "metadata": 88 | {"signature":"abc"},"b_meta":{"nanos":0, 89 | "hash":"0x0","height":0, "chain_id":"xian-1"}}) 90 | logger.debug(f"Output (exception): {output}") 91 | 92 | new_balance = self.d.get('con_exception.balances:stu') 93 | logger.debug(f"New balance (exception): {new_balance}") 94 | 95 | self.assertEqual(prior_balance, new_balance) 96 | 97 | def test_non_exception(self): 98 | prior_balance = self.d.get('con_currency.balances:stu') 99 | 100 | output = self.tx_processor.process_tx({ 101 | "payload": 102 | {'sender': 'stu', 'contract': 'con_currency', 'function': 'transfer', 'kwargs': {'amount': 100, 'to': 'colin'},"stamps_supplied":1000}, 103 | "metadata": 104 | {"signature":"abc"},"b_meta":{"nanos":0,"hash":"0x0","height":0, "chain_id":"xian-1"}}) 105 | 106 | new_balance = self.d.get('con_currency.balances:stu') 107 | logger.debug(f"New balance (non-exception): {new_balance}") 108 | 109 | self.assertEqual(prior_balance - 100, new_balance) 110 | 111 | if __name__ == '__main__': 112 | unittest.main() 113 | -------------------------------------------------------------------------------- /src/contracting/execution/module.py: -------------------------------------------------------------------------------- 1 | from importlib.abc import Loader 2 | from importlib import invalidate_caches, __import__ 3 | from importlib.machinery import ModuleSpec 4 | from contracting.storage.driver import Driver 5 | from contracting.stdlib import env 6 | from contracting.execution.runtime import rt 7 | 8 | import marshal 9 | import builtins 10 | import sys 11 | import importlib.util 12 | 13 | # This function overrides the __import__ function, which is the builtin function that is called whenever Python runs 14 | # an 'import' statement. If the globals dictionary contains {'__contract__': True}, then this function will make sure 15 | # that the module being imported comes from the database and not from builtins or site packages. 16 | # 17 | # For all exec statements, we add the {'__contract__': True} _key to the globals to protect against unwanted imports. 18 | # 19 | # Note: anything installed with pip or in site-packages will also not work, so contract package names *must* be unique. 20 | 21 | 22 | def is_valid_import(name): 23 | spec = importlib.util.find_spec(name) 24 | if not isinstance(spec.loader, DatabaseLoader): 25 | raise ImportError("module {} cannot be imported in a smart contract.".format(name)) 26 | 27 | 28 | def restricted_import(name, globals=None, locals=None, fromlist=(), level=0): 29 | if globals is not None and globals.get('__contract__') is True: 30 | spec = importlib.util.find_spec(name) 31 | if spec is None or not isinstance(spec.loader, DatabaseLoader): 32 | raise ImportError("module {} cannot be imported in a smart contract.".format(name)) 33 | 34 | return __import__(name, globals, locals, fromlist, level) 35 | 36 | 37 | def enable_restricted_imports(): 38 | builtins.__import__ = restricted_import 39 | # builtins.float = ContractingDecimal 40 | 41 | 42 | def disable_restricted_imports(): 43 | builtins.__import__ = __import__ 44 | 45 | 46 | def uninstall_builtins(): 47 | sys.meta_path.clear() 48 | sys.path_hooks.clear() 49 | sys.path.clear() 50 | sys.path_importer_cache.clear() 51 | invalidate_caches() 52 | 53 | 54 | def install_database_loader(driver=Driver()): 55 | DatabaseFinder.driver = driver 56 | if DatabaseFinder not in sys.meta_path: 57 | sys.meta_path.insert(0, DatabaseFinder) 58 | 59 | 60 | def uninstall_database_loader(): 61 | sys.meta_path = list(set(sys.meta_path)) 62 | if DatabaseFinder in sys.meta_path: 63 | sys.meta_path.remove(DatabaseFinder) 64 | 65 | 66 | def install_system_contracts(directory=''): 67 | pass 68 | 69 | 70 | ''' 71 | Is this where interaction with the database occurs with the interface of code strings, etc? 72 | IE: pushing a contract does sanity checks here? 73 | ''' 74 | 75 | 76 | class DatabaseFinder: 77 | driver = Driver() 78 | 79 | def find_spec(self, fullname, path=None, target=None): 80 | if DatabaseFinder.driver.get_contract(self) is None: 81 | return None 82 | return ModuleSpec(self, DatabaseLoader(DatabaseFinder.driver)) 83 | 84 | 85 | MODULE_CACHE = {} 86 | 87 | 88 | class DatabaseLoader(Loader): 89 | def __init__(self, d=Driver()): 90 | self.d = d 91 | 92 | def create_module(self, spec): 93 | return None 94 | 95 | def exec_module(self, module): 96 | # fetch the individual contract 97 | code = self.d.get_compiled(module.__name__) 98 | if code is None: 99 | raise ImportError("Module {} not found".format(module.__name__)) 100 | 101 | if type(code) != bytes: 102 | code = bytes.fromhex(code) 103 | 104 | code = marshal.loads(code) 105 | 106 | if code is None: 107 | raise ImportError("Module {} not found".format(module.__name__)) 108 | 109 | scope = env.gather() 110 | scope.update(rt.env) 111 | 112 | scope.update({'__contract__': True}) 113 | 114 | # execute the module with the std env and update the module to pass forward 115 | exec(code, scope) 116 | 117 | # Update the module's attributes with the new scope 118 | vars(module).update(scope) 119 | del vars(module)['__builtins__'] 120 | 121 | rt.loaded_modules.append(module.__name__) 122 | 123 | def module_repr(self, module): 124 | return ''.format(module.__name__) 125 | -------------------------------------------------------------------------------- /src/contracting/compilation/compiler.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import astor 3 | 4 | from contracting import constants 5 | from contracting.compilation.linter import Linter 6 | 7 | 8 | class ContractingCompiler(ast.NodeTransformer): 9 | def __init__(self, module_name='__main__', linter=Linter()): 10 | self.module_name = module_name 11 | self.linter = linter 12 | self.lint_alerts = None 13 | self.constructor_visited = False 14 | self.private_names = set() 15 | self.orm_names = set() 16 | self.visited_names = set() # store the method visits 17 | 18 | def parse(self, source: str, lint=True): 19 | self.constructor_visited = False 20 | 21 | tree = ast.parse(source) 22 | 23 | if lint: 24 | self.lint_alerts = self.linter.check(tree) 25 | # compilation.fix_missing_locations(tree) 26 | 27 | tree = self.visit(tree) 28 | 29 | if self.lint_alerts is not None: 30 | raise Exception(self.lint_alerts) 31 | 32 | # check all visited nodes and see if they are actually private 33 | 34 | # An Expr node can have a value func of compilation.Name, or compilation. 35 | # Attribute which you much access the value of. 36 | # TODO: This code branching is not ideal and should be investigated for simplicity. 37 | for node in self.visited_names: 38 | if node.id in self.private_names or node.id in self.orm_names: 39 | node.id = self.privatize(node.id) 40 | 41 | ast.fix_missing_locations(tree) 42 | 43 | # reset state 44 | self.private_names = set() 45 | self.orm_names = set() 46 | self.visited_names = set() 47 | 48 | return tree 49 | 50 | @staticmethod 51 | def privatize(s): 52 | return '{}{}'.format(constants.PRIVATE_METHOD_PREFIX, s) 53 | 54 | def compile(self, source: str, lint=True): 55 | tree = self.parse(source, lint=lint) 56 | 57 | compiled_code = compile(tree, '', 'exec') 58 | 59 | return compiled_code 60 | 61 | def parse_to_code(self, source, lint=True): 62 | tree = self.parse(source, lint=lint) 63 | code = astor.to_source(tree) 64 | return code 65 | 66 | def visit_FunctionDef(self, node): 67 | 68 | # Presumes all decorators are valid, as caught by linter. 69 | if node.decorator_list: 70 | # Presumes that a single decorator is passed. This is caught by the linter. 71 | decorator = node.decorator_list.pop() 72 | 73 | # change the name of the init function to '____' so it is uncallable except once 74 | if decorator.id == constants.INIT_DECORATOR_STRING: 75 | node.name = '____' 76 | 77 | elif decorator.id == constants.EXPORT_DECORATOR_STRING: 78 | # Transform @export decorators to @__export(contract_name) decorators 79 | decorator.id = '{}{}'.format('__', constants.EXPORT_DECORATOR_STRING) 80 | 81 | new_node = ast.Call( 82 | func=decorator, 83 | args=[ast.Str(s=self.module_name)], 84 | keywords=[] 85 | ) 86 | 87 | node.decorator_list.append(new_node) 88 | 89 | else: 90 | self.private_names.add(node.name) 91 | node.name = self.privatize(node.name) 92 | 93 | self.generic_visit(node) 94 | 95 | return node 96 | 97 | def visit_Assign(self, node): 98 | if (isinstance(node.value, ast.Call) and not 99 | isinstance(node.value.func, ast.Attribute) and 100 | node.value.func.id in constants.ORM_CLASS_NAMES): 101 | 102 | node.value.keywords.append(ast.keyword('contract', ast.Str(self.module_name))) 103 | node.value.keywords.append(ast.keyword('name', ast.Str(node.targets[0].id))) 104 | self.orm_names.add(node.targets[0].id) 105 | 106 | self.generic_visit(node) 107 | 108 | return node 109 | 110 | def visit_Name(self, node): 111 | self.visited_names.add(node) 112 | return node 113 | 114 | def visit_Expr(self, node): 115 | self.generic_visit(node) 116 | return node 117 | 118 | def visit_Num(self, node): 119 | if isinstance(node.n, float): 120 | return ast.Call(func=ast.Name(id='decimal', ctx=ast.Load()), 121 | args=[ast.Str(str(node.n))], keywords=[]) 122 | return node 123 | -------------------------------------------------------------------------------- /tests/unit/test_runtime.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.execution import runtime 3 | import sys 4 | import psutil 5 | import os 6 | 7 | 8 | class TestRuntime(TestCase): 9 | def tearDown(self): 10 | runtime.rt.tracer.stop() 11 | runtime.rt.clean_up() 12 | 13 | def test_tracer_works_roughly(self): 14 | stamps = 1000000 15 | # This doesnt work because we are only metering things with _contract_ in Globals 16 | runtime.rt.set_up(stmps=stamps, meter=True) 17 | globals()['__contract__'] = True 18 | x = max([i for i in range(2)]) 19 | globals()['__contract__'] = False 20 | runtime.rt.tracer.stop() 21 | used = runtime.rt.tracer.get_stamp_used() 22 | runtime.rt.clean_up() 23 | self.assertLess(stamps - used, stamps) 24 | 25 | def test_tracer_bypass_records_no_stamps(self): 26 | stamps = 1000 27 | runtime.rt.set_up(stmps=stamps, meter=False) 28 | a = 5 29 | b = 5 30 | runtime.rt.tracer.stop() 31 | used = runtime.rt.tracer.get_stamp_used() 32 | runtime.rt.clean_up() 33 | self.assertEqual(stamps - used, stamps) 34 | 35 | def test_arbitrary_modification_of_stamps_works(self): 36 | stamps = 1000 37 | sub = 500 38 | runtime.rt.set_up(stmps=stamps, meter=True) 39 | globals()['__contract__'] = True 40 | a = 5 41 | globals()['__contract__'] = False 42 | runtime.rt.tracer.stop() 43 | used_1 = runtime.rt.tracer.get_stamp_used() 44 | runtime.rt.tracer.set_stamp(stamps - sub) 45 | used_2 = runtime.rt.tracer.get_stamp_used() 46 | runtime.rt.clean_up() 47 | 48 | print(used_1, used_2) 49 | 50 | def test_starting_and_stopping_tracer_works_roughly(self): 51 | stamps = 1000000 52 | runtime.rt.set_up(stmps=stamps, meter=True) 53 | globals()['__contract__'] = True 54 | x = max([i for i in range(4)]) 55 | globals()['__contract__'] = False 56 | runtime.rt.tracer.stop() 57 | used_1 = runtime.rt.tracer.get_stamp_used() 58 | runtime.rt.clean_up() 59 | 60 | stamps = 10000000 61 | runtime.rt.set_up(stmps=stamps, meter=True) 62 | globals()['__contract__'] = True 63 | x = max([i for i in range(1)]) 64 | runtime.rt.tracer.stop() 65 | x = max([i for i in range(1)]) 66 | runtime.rt.tracer.stop() 67 | globals()['__contract__'] = False 68 | used_2 = runtime.rt.tracer.get_stamp_used() 69 | runtime.rt.clean_up() 70 | 71 | self.assertGreater(used_1, used_2) 72 | 73 | def test_modifying_stamps_during_tracing(self): 74 | stamps = 10000 75 | runtime.rt.set_up(stmps=stamps, meter=True) 76 | a = 5 77 | b = 5 78 | runtime.rt.tracer.stop() 79 | c = 5 80 | d = 5 81 | e = 5 82 | runtime.rt.clean_up() 83 | used_1 = runtime.rt.tracer.get_stamp_used() 84 | 85 | stamps = 5000 86 | runtime.rt.set_up(stmps=stamps, meter=True) 87 | a = 5 88 | b = 5 89 | runtime.rt.tracer.stop() 90 | used_1 = runtime.rt.tracer.get_stamp_used() 91 | runtime.rt.set_up(stmps=stamps - used_1, meter=True) 92 | c = 5 93 | d = 5 94 | e = 5 95 | runtime.rt.clean_up() 96 | used_2 = runtime.rt.tracer.get_stamp_used() 97 | 98 | print(used_1, used_2) 99 | 100 | def test_add_exists(self): 101 | stamps = 1000 102 | 103 | runtime.rt.set_up(stmps=stamps, meter=True) 104 | 105 | runtime.rt.tracer.add_cost(900) 106 | runtime.rt.tracer.stop() 107 | 108 | used_1 = runtime.rt.tracer.get_stamp_used() 109 | 110 | runtime.rt.clean_up() 111 | print(used_1) 112 | 113 | def test_deduct_write_adjusts_total_writes(self): 114 | stamps = 1000 115 | 116 | runtime.rt.set_up(stmps=stamps, meter=True) 117 | 118 | self.assertEqual(runtime.rt.writes, 0) 119 | 120 | runtime.rt.deduct_write('a', 'bad') 121 | 122 | self.assertEqual(runtime.rt.writes, 4) 123 | 124 | runtime.rt.clean_up() 125 | 126 | def test_deduct_write_fails_if_too_many_writes(self): 127 | stamps = 1000 128 | 129 | runtime.rt.set_up(stmps=stamps, meter=True) 130 | 131 | self.assertEqual(runtime.rt.writes, 0) 132 | 133 | runtime.rt.deduct_write('a', 'bad') 134 | 135 | self.assertEqual(runtime.rt.writes, 4) 136 | 137 | with self.assertRaises(AssertionError): 138 | runtime.rt.deduct_write('a', 'b' * 32 * 1024) 139 | 140 | runtime.rt.clean_up() -------------------------------------------------------------------------------- /examples/01 A very simple Counter contract.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# A very simple Counter contract\n", 8 | "\n", 9 | "Let's start writing a contract in python that stores a number which everyone can increment:" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "def counter_contract():\n", 19 | " # introduce a state called count which holds a single value\n", 20 | " count = Variable()\n", 21 | " \n", 22 | " # @construct means that this is the function that will be called when the smart contract is created\n", 23 | " @construct\n", 24 | " def constructor():\n", 25 | " count.set(0)\n", 26 | " \n", 27 | " # @export makes this function public, so it can be called by anyone on a deployed contract \n", 28 | " @export\n", 29 | " def increment():\n", 30 | " count.set(count.get() + 1)" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "To interact with smart contracts we need a client:" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "from contracting.client import ContractingClient\n", 47 | "client = ContractingClient(signer='ren')" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "Next we will submit the contract to the client:" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 3, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "client.submit(counter_contract)" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": {}, 69 | "source": [ 70 | "Now we can get the submitted contract to interact with it:" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": 4, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "contract = client.get_contract('counter_contract')" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "metadata": {}, 85 | "source": [ 86 | "Let's investigate the counter:" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 5, 92 | "metadata": {}, 93 | "outputs": [ 94 | { 95 | "data": { 96 | "text/plain": [ 97 | "0" 98 | ] 99 | }, 100 | "execution_count": 5, 101 | "metadata": {}, 102 | "output_type": "execute_result" 103 | } 104 | ], 105 | "source": [ 106 | "contract.count.get()" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "It is 0 as expected because we initialized it to 0 in the constructor function.\n", 114 | "\n", 115 | "Everyone can increment the counter by calling the public increment function:" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 6, 121 | "metadata": {}, 122 | "outputs": [], 123 | "source": [ 124 | "contract.increment()" 125 | ] 126 | }, 127 | { 128 | "cell_type": "markdown", 129 | "metadata": {}, 130 | "source": [ 131 | "Let's investigate the counter again:" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": 7, 137 | "metadata": {}, 138 | "outputs": [ 139 | { 140 | "data": { 141 | "text/plain": [ 142 | "1" 143 | ] 144 | }, 145 | "execution_count": 7, 146 | "metadata": {}, 147 | "output_type": "execute_result" 148 | } 149 | ], 150 | "source": [ 151 | "contract.count.get()" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "Seems like our increment function works.\n", 159 | "\n", 160 | "This concludes our first look into writing smart contracts in Python on Lamden. Dive deeper by looking at the next example. " 161 | ] 162 | } 163 | ], 164 | "metadata": { 165 | "kernelspec": { 166 | "display_name": "Python 3", 167 | "language": "python", 168 | "name": "python3" 169 | }, 170 | "language_info": { 171 | "codemirror_mode": { 172 | "name": "ipython", 173 | "version": 3 174 | }, 175 | "file_extension": ".py", 176 | "mimetype": "text/x-python", 177 | "name": "python", 178 | "nbconvert_exporter": "python", 179 | "pygments_lexer": "ipython3", 180 | "version": "3.6.5" 181 | } 182 | }, 183 | "nbformat": 4, 184 | "nbformat_minor": 2 185 | } 186 | -------------------------------------------------------------------------------- /tests/unit/test_new_driver.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from shutil import rmtree 4 | from datetime import datetime 5 | from contracting.storage.driver import Driver 6 | 7 | class TestDriver(unittest.TestCase): 8 | 9 | def setUp(self): 10 | # Setup a fresh instance of Driver and ensure a clean storage environment 11 | self.driver = Driver(bypass_cache=False) 12 | self.driver.flush_full() 13 | 14 | def tearDown(self): 15 | # Clean up any state that might affect other tests 16 | self.driver.flush_full() 17 | 18 | def test_set_and_get(self): 19 | key = 'test_key' 20 | value = 'test_value' 21 | self.driver.set(key, value) 22 | self.driver.commit() 23 | retrieved_value = self.driver.get(key) 24 | self.assertEqual(retrieved_value, value) 25 | 26 | def test_find(self): 27 | key = 'test_key' 28 | value = 'test_value' 29 | self.driver.set(key, value) 30 | self.driver.commit() 31 | retrieved_value = self.driver.find(key) 32 | self.assertEqual(retrieved_value, value) 33 | 34 | def test_keys_from_disk(self): 35 | key1 = 'test_key1' 36 | key2 = 'test_key2' 37 | value = 'test_value' 38 | self.driver.set(key1, value) 39 | self.driver.set(key2, value) 40 | self.driver.commit() 41 | keys = self.driver.keys_from_disk() 42 | self.assertIn(key1, keys) 43 | self.assertIn(key2, keys) 44 | 45 | def test_iter_from_disk(self): 46 | key1 = 'test_key1' 47 | key2 = 'test_key2' 48 | prefix_key = 'prefix_key' 49 | value = 'test_value' 50 | self.driver.set(key1, value) 51 | self.driver.set(key2, value) 52 | self.driver.set(prefix_key, value) 53 | self.driver.commit() 54 | keys = self.driver.iter_from_disk(prefix=prefix_key) 55 | self.assertIn(prefix_key, keys) 56 | self.assertNotIn(key1, keys) 57 | self.assertNotIn(key2, keys) 58 | 59 | def test_items(self): 60 | prefix_key = 'prefix_key' 61 | value = 'test_value' 62 | self.driver.set(prefix_key, value) 63 | self.driver.commit() 64 | items = self.driver.items(prefix=prefix_key) 65 | self.assertIn(prefix_key, items) 66 | self.assertEqual(items[prefix_key], value) 67 | 68 | def test_delete_key_from_disk(self): 69 | key = 'test_key' 70 | value = 'test_value' 71 | self.driver.set(key, value) 72 | self.driver.commit() 73 | self.driver.delete_key_from_disk(key) 74 | retrieved_value = self.driver.value_from_disk(key) 75 | self.assertIsNone(retrieved_value) 76 | 77 | def test_flush_cache(self): 78 | key = 'test_key' 79 | value = 'test_value' 80 | self.driver.set(key, value) 81 | self.driver.flush_cache() 82 | self.assertFalse(self.driver.pending_writes) 83 | 84 | def test_flush_disk(self): 85 | key = 'test_key' 86 | value = 'test_value' 87 | self.driver.set(key, value) 88 | self.driver.commit() 89 | self.driver.flush_disk() 90 | self.assertFalse(self.driver.get(key)) 91 | 92 | def test_commit(self): 93 | key = 'test_key' 94 | value = 'test_value' 95 | self.driver.set(key, value) 96 | self.driver.commit() 97 | retrieved_value = self.driver.get(key) 98 | self.assertEqual(retrieved_value, value) 99 | 100 | def test_get_all_contract_state(self): 101 | key = 'contract.key' 102 | value = 'contract_value' 103 | self.driver.set(key, value) 104 | self.driver.commit() 105 | contract_state = self.driver.get_all_contract_state() 106 | self.assertIn(key, contract_state) 107 | self.assertEqual(contract_state[key], value) 108 | 109 | def test_transaction_writes(self): 110 | key = 'test_key' 111 | value = 'test_value' 112 | self.driver.set(key, value, is_txn_write=True) 113 | # self.driver.commit() 114 | transaction_writes = self.driver.transaction_writes 115 | self.assertIn(key, transaction_writes) 116 | self.assertEqual(transaction_writes[key], value) 117 | 118 | def test_clear_transaction_writes(self): 119 | key = 'test_key' 120 | value = 'test_value' 121 | self.driver.set(key, value) 122 | # self.driver.commit() 123 | self.driver.clear_transaction_writes() 124 | transaction_writes = self.driver.transaction_writes 125 | self.assertNotIn(key, transaction_writes) 126 | 127 | def test_get_run_state(self): 128 | # We can't test this function here since we are not running a real blockchain. 129 | pass 130 | 131 | if __name__ == '__main__': 132 | unittest.main() 133 | -------------------------------------------------------------------------------- /tests/integration/test_senecaCompiler_integration.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.compilation.compiler import ContractingCompiler 3 | from contracting.stdlib import env 4 | from contracting import constants 5 | 6 | import re 7 | import astor 8 | import os 9 | 10 | class TestSenecaCompiler(TestCase): 11 | def test_visit_assign_variable(self): 12 | code = ''' 13 | v = Variable() 14 | ''' 15 | c = ContractingCompiler() 16 | comp = c.parse(code, lint=False) 17 | code_str = astor.to_source(comp) 18 | 19 | scope = env.gather() 20 | 21 | exec(code_str, scope) 22 | 23 | v = scope['__v'] 24 | 25 | self.assertEqual(v._key, '__main__.v') 26 | 27 | def test_visit_assign_foreign_variable(self): 28 | code = ''' 29 | fv = ForeignVariable(foreign_contract='scoob', foreign_name='kumbucha') 30 | ''' 31 | c = ContractingCompiler() 32 | comp = c.parse(code, lint=False) 33 | code_str = astor.to_source(comp) 34 | 35 | scope = env.gather() 36 | 37 | exec(code_str, scope) 38 | 39 | fv = scope['__fv'] 40 | 41 | self.assertEqual(fv._key, 'scoob.kumbucha') 42 | 43 | def test_assign_hash_variable(self): 44 | code = ''' 45 | h = Hash() 46 | ''' 47 | c = ContractingCompiler() 48 | comp = c.parse(code, lint=False) 49 | code_str = astor.to_source(comp) 50 | 51 | scope = env.gather() 52 | 53 | exec(code_str, scope) 54 | 55 | h = scope['__h'] 56 | 57 | self.assertEqual(h._key, '__main__.h') 58 | 59 | def test_assign_foreign_hash(self): 60 | code = ''' 61 | fv = ForeignHash(foreign_contract='scoob', foreign_name='kumbucha') 62 | ''' 63 | 64 | c = ContractingCompiler() 65 | comp = c.parse(code, lint=False) 66 | code_str = astor.to_source(comp) 67 | 68 | scope = env.gather() 69 | 70 | exec(code_str, scope) 71 | 72 | fv = scope['__fv'] 73 | 74 | self.assertEqual(fv._key, 'scoob.kumbucha') 75 | # self.assertEqual(fv.foreign_key, 'scoob.kumbucha') 76 | # 77 | # def test_export_decorator_pops(self): 78 | # code = ''' 79 | # @export 80 | # def funtimes(): 81 | # print('cool') 82 | # ''' 83 | # 84 | # c = ContractingCompiler() 85 | # comp = c.parse(code, lint=False) 86 | # code_str = astor.to_source(comp) 87 | # 88 | # self.assertNotIn('@export', code_str) 89 | 90 | def test_private_function_prefixes_properly(self): 91 | code = ''' 92 | def private(): 93 | print('cool') 94 | ''' 95 | 96 | c = ContractingCompiler() 97 | comp = c.parse(code, lint=False) 98 | code_str = astor.to_source(comp) 99 | 100 | self.assertIn('__private', code_str) 101 | 102 | def test_private_func_call_in_public_func_properly_renamed(self): 103 | code = ''' 104 | @export 105 | def public(): 106 | private('hello') 107 | 108 | def private(message): 109 | print(message) 110 | ''' 111 | 112 | c = ContractingCompiler() 113 | comp = c.parse(code, lint=False) 114 | code_str = astor.to_source(comp) 115 | 116 | # there should be two private occurances of the method call 117 | self.assertEqual(len([m.start() for m in re.finditer('__private', code_str)]), 2) 118 | 119 | def test_private_func_call_in_other_private_functions(self): 120 | code = ''' 121 | def a(): 122 | b() 123 | 124 | def b(): 125 | c() 126 | 127 | def c(): 128 | e() 129 | 130 | def d(): 131 | print('hello') 132 | 133 | def e(): 134 | d() 135 | ''' 136 | c = ContractingCompiler() 137 | comp = c.parse(code, lint=False) 138 | code_str = astor.to_source(comp) 139 | 140 | self.assertEqual(len([m.start() for m in re.finditer(constants.PRIVATE_METHOD_PREFIX, code_str)]), 9) 141 | 142 | def test_construct_renames_properly(self): 143 | code = ''' 144 | @construct 145 | def seed(): 146 | print('yes') 147 | 148 | @export 149 | def hello(): 150 | print('no') 151 | 152 | def goodbye(): 153 | print('idk') 154 | ''' 155 | 156 | c = ContractingCompiler() 157 | comp = c.parse(code, lint=False) 158 | code_str = astor.to_source(comp) 159 | 160 | def test_token_contract_parses_correctly(self): 161 | currency_path = os.path.join(os.path.dirname(__file__), "test_contracts", "currency.s.py") 162 | 163 | with open(currency_path) as f: 164 | code = f.read() 165 | 166 | 167 | c = ContractingCompiler() 168 | comp = c.parse(code, lint=False) 169 | code_str = astor.to_source(comp) 170 | 171 | def test_export_decorator_argument_is_added(self): 172 | code = ''' 173 | @export 174 | def test(): 175 | pass 176 | ''' 177 | c = ContractingCompiler() 178 | comp = c.parse(code, lint=False) 179 | code_str = astor.to_source(comp) 180 | print(code_str) 181 | -------------------------------------------------------------------------------- /tests/integration/test_seneca_client_randoms.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.client import ContractingClient 3 | import random 4 | 5 | def con_random_contract(): 6 | random.seed() 7 | 8 | cards = [1, 2, 3, 4, 5, 6, 7, 8] 9 | 10 | cardinal_values = ['A', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'] 11 | suits = ['S', 'C', 'H', 'D'] 12 | 13 | cities = ['Cleveland', 'Detroit', 'Chicago', 'New York', 'San Francisco'] 14 | 15 | @export 16 | def shuffle_cards(**kwargs: dict): 17 | random.shuffle(cards) 18 | return cards 19 | 20 | @export 21 | def random_number(k: int): 22 | return random.randrange(k) 23 | 24 | @export 25 | def random_number_2(k: int): 26 | # adjust the random state by calling another random function 27 | shuffle_cards() 28 | return random.randrange(k) 29 | 30 | @export 31 | def random_bits(k: int): 32 | shuffle_cards() 33 | shuffle_cards() 34 | shuffle_cards() 35 | return random.getrandbits(k) 36 | 37 | @export 38 | def int_in_range(a: int, b: int): 39 | shuffle_cards() 40 | shuffle_cards() 41 | return random.randint(a, b) 42 | 43 | @export 44 | def deal_card(): 45 | random.shuffle(cardinal_values) 46 | random.shuffle(cardinal_values) 47 | random.shuffle(cardinal_values) 48 | 49 | random.shuffle(suits) 50 | random.shuffle(suits) 51 | random.shuffle(suits) 52 | 53 | c = '' 54 | c += random.choice(cardinal_values) 55 | c += random.choice(suits) 56 | 57 | return c 58 | 59 | @export 60 | def pick_cities(k: int): 61 | return random.choices(cities, k) 62 | 63 | 64 | class TestRandomsContract(TestCase): 65 | def setUp(self): 66 | self.c = ContractingClient(signer='stu') 67 | self.c.flush() 68 | self.c.submit(con_random_contract) 69 | 70 | self.random_contract = self.c.get_contract('con_random_contract') 71 | 72 | def tearDown(self): 73 | self.c.flush() 74 | 75 | def test_basic_shuffle(self): 76 | cards_1 = self.random_contract.shuffle_cards() 77 | cards_2 = self.random_contract.shuffle_cards() 78 | 79 | self.assertEqual(cards_1, cards_2) 80 | 81 | def test_basic_shuffle_different_with_different_seeds(self): 82 | cards_1 = self.random_contract.shuffle_cards(environment={'block_num': 999}) 83 | cards_2 = self.random_contract.shuffle_cards(environment={'block_num': 998}) 84 | 85 | self.assertNotEqual(cards_1, cards_2) 86 | 87 | def test_random_num_one_vs_two(self): 88 | k = self.random_contract.random_number(k=1000) 89 | k2 = self.random_contract.random_number_2(k=1000) 90 | 91 | self.assertNotEqual(k, k2) 92 | 93 | random.seed('000') 94 | 95 | self.assertEqual(k, random.randrange(1000)) 96 | 97 | random.seed('000') 98 | cards = [1, 2, 3, 4, 5, 6, 7, 8] 99 | random.shuffle(cards) 100 | 101 | self.assertEqual(k2, random.randrange(1000)) 102 | '''' TEST CASE IS IRRELEVANT as getrandbits will never sync with system random. 103 | def test_random_getrandbits(self): 104 | b = self.random_contract.random_bits(k=20) 105 | 106 | random.seed('000') 107 | 108 | cards = [1, 2, 3, 4, 5, 6, 7, 8] 109 | random.shuffle(cards) 110 | random.shuffle(cards) 111 | random.shuffle(cards) 112 | 113 | self.assertEqual(b, random.getrandbits(20)) 114 | ''' 115 | 116 | def test_random_range_int(self): 117 | a = self.random_contract.int_in_range(a=100, b=50000) 118 | 119 | random.seed('000') 120 | 121 | cards = [1, 2, 3, 4, 5, 6, 7, 8] 122 | random.shuffle(cards) 123 | random.shuffle(cards) 124 | 125 | self.assertEqual(a, random.randint(a=100, b=50000)) 126 | 127 | def test_random_choice(self): 128 | cities = self.random_contract.pick_cities(k=2) 129 | 130 | random.seed('000') 131 | c = ['Cleveland', 'Detroit', 'Chicago', 'New York', 'San Francisco'] 132 | cc = random.choices(c, k=2) 133 | 134 | self.assertListEqual(cities, cc) 135 | 136 | def test_auxiliary_salt(self): 137 | cards_1 = self.random_contract.shuffle_cards(environment={ 138 | 'AUXILIARY_SALT': 'ffd8ded9ced929a41dae83b1f22a6a31b52f79bbf4cdabe6a27d9646dd2bd725fc29c8bc122cb9e37a2904da00e34df499ee7a897505d1de3f0511f9f9c1150c'}) 139 | cards_2 = self.random_contract.shuffle_cards(environment={ 140 | 'AUXILIARY_SALT': 'ffd8ded9ced929a41dae83b1f22a6a31b52f79bbf4cdabe6a27d9646dd2bd725fc29c8bc122cb9e37a2904da00e34df499ee7a897505d1de3f0511f9f9c1150c'}) 141 | cards_3 = self.random_contract.shuffle_cards(environment={ 142 | 'AUXILIARY_SALT': 'f79bbded9ced929a41dae83b1f22a6a31b52f79bbf4cdabe6a27d9646dd2bd725fc29c8bc122cb9e37a2904da00e34df499ee7a897505d1de3f0511f9f9c1150c'}) 143 | 144 | self.assertEqual(cards_1, cards_2) 145 | self.assertNotEqual(cards_1, cards_3) -------------------------------------------------------------------------------- /src/contracting/storage/hdf5.py: -------------------------------------------------------------------------------- 1 | import h5py 2 | 3 | from threading import Lock 4 | from collections import defaultdict 5 | from contracting.storage.encoder import encode, decode 6 | from contracting import constants 7 | 8 | # A dictionary to maintain file-specific locks 9 | file_locks = defaultdict(Lock) 10 | 11 | # Constants 12 | ATTR_LEN_MAX = 64000 13 | ATTR_VALUE = "value" 14 | ATTR_BLOCK = "block" 15 | 16 | 17 | def get_file_lock(file_path): 18 | """Retrieve a lock for a specific file path.""" 19 | return file_locks[file_path] 20 | 21 | 22 | def get_value(file_path, group_name): 23 | return get_attr(file_path, group_name, ATTR_VALUE) 24 | 25 | 26 | def get_block(file_path, group_name): 27 | return get_attr(file_path, group_name, ATTR_BLOCK) 28 | 29 | 30 | def get_attr(file_path, group_name, attr_name): 31 | try: 32 | with h5py.File(file_path, 'r') as f: 33 | try: 34 | value = f[group_name].attrs[attr_name] 35 | return value.decode() if isinstance(value, bytes) else value 36 | except KeyError: 37 | return None 38 | except OSError: 39 | # File doesn't exist 40 | return None 41 | 42 | 43 | 44 | def get_groups(file_path): 45 | try: 46 | with h5py.File(file_path, 'r') as f: 47 | return list(f.keys()) 48 | except OSError: 49 | # File doesn't exist 50 | return [] 51 | 52 | 53 | 54 | def set(file_path, group_name, value, blocknum, timeout=20): 55 | """ 56 | Set the value and blocknum attributes in the HDF5 file for the given group. 57 | """ 58 | # Acquire a file lock to prevent concurrent writes 59 | lock = get_file_lock(file_path if isinstance(file_path, str) else file_path.filename) 60 | if lock.acquire(timeout=timeout): 61 | try: 62 | with h5py.File(file_path, 'a') as f: 63 | 64 | # Write value and blocknum to the group attributes 65 | write_attr(f, group_name, ATTR_VALUE, value, timeout) 66 | write_attr(f, group_name, ATTR_BLOCK, blocknum, timeout) 67 | finally: 68 | # Always release the lock after operation 69 | lock.release() 70 | else: 71 | raise TimeoutError("Lock acquisition timed out") 72 | 73 | 74 | def write_attr(file_or_path, group_name, attr_name, value, timeout=20): 75 | """ 76 | Write an attribute to a group inside an HDF5 file. 77 | """ 78 | 79 | # Open the file and ensure group exists, then write the attribute 80 | if isinstance(file_or_path, str): 81 | with h5py.File(file_or_path, 'a') as f: 82 | _write_attr_to_file(f, group_name, attr_name, value, timeout) 83 | else: 84 | _write_attr_to_file(file_or_path, group_name, attr_name, value, timeout) 85 | 86 | 87 | def _write_attr_to_file(file, group_name, attr_name, value, timeout): 88 | """ 89 | Internal method to write the attribute to the group. 90 | """ 91 | # Ensure the group exists, or create it if necessary 92 | grp = file.require_group(group_name) 93 | 94 | # Write or update the attribute in the group 95 | if attr_name in grp.attrs: 96 | del grp.attrs[attr_name] 97 | if value is not None: 98 | grp.attrs[attr_name] = value 99 | 100 | 101 | 102 | def delete(file_path, group_name, timeout=20): 103 | lock = get_file_lock(file_path if isinstance(file_path, str) else file_path.filename) 104 | if lock.acquire(timeout=timeout): 105 | try: 106 | with h5py.File(file_path, 'a') as f: 107 | try: 108 | del f[group_name].attrs[ATTR_VALUE] 109 | del f[group_name].attrs[ATTR_BLOCK] 110 | except KeyError: 111 | pass 112 | finally: 113 | lock.release() 114 | else: 115 | raise TimeoutError("Lock acquisition timed out") 116 | 117 | 118 | def set_value_to_disk(file_path, group_name, value, block_num=None, timeout=20): 119 | """ 120 | Save value to disk with optional block number. 121 | """ 122 | encoded_value = encode(value) if value is not None else None 123 | 124 | set(file_path, group_name, encoded_value, block_num if block_num is not None else -1, timeout) 125 | 126 | 127 | def delete_key_from_disk(file_path, group_name, timeout=20): 128 | delete(file_path, group_name, timeout) 129 | 130 | 131 | def get_value_from_disk(file_path, group_name): 132 | return decode(get_value(file_path, group_name)) 133 | 134 | 135 | 136 | def get_all_keys_from_file(file_path): 137 | """ 138 | Retrieve all keys (datasets and groups) from an HDF5 file and replace '/' with a specified character. 139 | 140 | :param file_path: Path to the HDF5 file. 141 | :param replace_char: Character to replace '/' with in the keys. 142 | :return: List of all keys in the HDF5 file with '/' replaced by replace_char. 143 | """ 144 | keys = [] 145 | 146 | def visit_func(name, node): 147 | keys.append(name.replace(constants.HDF5_GROUP_SEPARATOR, constants.DELIMITER)) 148 | 149 | with h5py.File(file_path, 'r') as f: 150 | f.visititems(visit_func) 151 | 152 | return keys 153 | -------------------------------------------------------------------------------- /src/contracting/stdlib/bridge/decimal.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal, Context, ROUND_FLOOR 2 | import decimal 3 | 4 | # Define precision constants 5 | MAX_UPPER_PRECISION = 30 6 | MAX_LOWER_PRECISION = 30 7 | 8 | # Set the decimal context for precision and rounding 9 | CONTEXT = Context( 10 | prec=MAX_UPPER_PRECISION + MAX_LOWER_PRECISION, 11 | rounding=ROUND_FLOOR, 12 | Emin=-100, 13 | Emax=100 14 | ) 15 | decimal.setcontext(CONTEXT) 16 | 17 | # Create min and max decimal strings for precision boundaries 18 | def make_min_decimal_str(prec): 19 | return '0.' + '0' * (prec - 1) + '1' 20 | 21 | def make_max_decimal_str(prec): 22 | return '1' + '0' * (prec - 1) 23 | 24 | # Convert scientific notation to non-exponential format if needed 25 | def neg_sci_not(s: str): 26 | try: 27 | base, exp = s.split('e-') 28 | if float(base) > 9: 29 | return s 30 | 31 | base = base.replace('.', '') 32 | numbers = ('0' * (int(exp) - 1)) + base 33 | 34 | if int(exp) > 0: 35 | numbers = '0.' + numbers 36 | 37 | return numbers 38 | except ValueError: 39 | return s 40 | 41 | # Define maximum and minimum decimal constants 42 | MAX_DECIMAL = Decimal(make_max_decimal_str(MAX_UPPER_PRECISION)) 43 | MIN_DECIMAL = Decimal(make_min_decimal_str(MAX_LOWER_PRECISION)) 44 | 45 | # Ensure the value is within bounds and quantized 46 | def fix_precision(x: Decimal): 47 | if x > MAX_DECIMAL: 48 | return MAX_DECIMAL 49 | return x.quantize(MIN_DECIMAL, rounding=ROUND_FLOOR).normalize() 50 | 51 | # Main ContractingDecimal class 52 | class ContractingDecimal: 53 | def _get_other(self, other): 54 | if isinstance(other, ContractingDecimal): 55 | return other._d 56 | elif isinstance(other, (float, int)): 57 | return fix_precision(Decimal(neg_sci_not(str(other)))) 58 | return other 59 | 60 | def __init__(self, a): 61 | if isinstance(a, (float, int)): 62 | self._d = Decimal(neg_sci_not(str(a))) 63 | elif isinstance(a, str): 64 | self._d = Decimal(neg_sci_not(a)) 65 | elif isinstance(a, Decimal): 66 | self._d = a 67 | else: 68 | self._d = Decimal(a) 69 | 70 | # Clamp and quantize during initialization 71 | self._d = fix_precision(self._d) 72 | 73 | def __bool__(self): 74 | return self._d > 0 75 | 76 | def __eq__(self, other): 77 | return self._d == self._get_other(other) 78 | 79 | def __lt__(self, other): 80 | return self._d < self._get_other(other) 81 | 82 | def __le__(self, other): 83 | return self._d <= self._get_other(other) 84 | 85 | def __gt__(self, other): 86 | return self._d > self._get_other(other) 87 | 88 | def __ge__(self, other): 89 | return self._d >= self._get_other(other) 90 | 91 | def __str__(self): 92 | return self._d.to_eng_string() 93 | 94 | def __repr__(self): 95 | return self._d.to_eng_string() 96 | 97 | def __neg__(self): 98 | return ContractingDecimal(-self._d) 99 | 100 | def __pos__(self): 101 | return self 102 | 103 | def __abs__(self): 104 | return ContractingDecimal(abs(self._d)) 105 | 106 | def __add__(self, other): 107 | return ContractingDecimal(fix_precision(self._d + self._get_other(other))) 108 | 109 | def __radd__(self, other): 110 | return ContractingDecimal(fix_precision(self._get_other(other) + self._d)) 111 | 112 | def __sub__(self, other): 113 | return ContractingDecimal(fix_precision(self._d - self._get_other(other))) 114 | 115 | def __rsub__(self, other): 116 | return ContractingDecimal(fix_precision(self._get_other(other) - self._d)) 117 | 118 | def __mul__(self, other): 119 | return ContractingDecimal(fix_precision(self._d * self._get_other(other))) 120 | 121 | def __rmul__(self, other): 122 | return ContractingDecimal(fix_precision(self._get_other(other) * self._d)) 123 | 124 | def __truediv__(self, other): 125 | return ContractingDecimal(fix_precision(self._d / self._get_other(other))) 126 | 127 | def __rtruediv__(self, other): 128 | return ContractingDecimal(fix_precision(self._get_other(other) / self._d)) 129 | 130 | def __mod__(self, other): 131 | return ContractingDecimal(fix_precision(self._d % self._get_other(other))) 132 | 133 | def __rmod__(self, other): 134 | return ContractingDecimal(fix_precision(self._get_other(other) % self._d)) 135 | 136 | def __floordiv__(self, other): 137 | return ContractingDecimal(fix_precision(self._d // self._get_other(other))) 138 | 139 | def __rfloordiv__(self, other): 140 | return ContractingDecimal(fix_precision(self._get_other(other) // self._d)) 141 | 142 | def __pow__(self, other): 143 | return ContractingDecimal(fix_precision(self._d ** self._get_other(other))) 144 | 145 | def __rpow__(self, other): 146 | return ContractingDecimal(fix_precision(self._get_other(other) ** self._d)) 147 | 148 | def __int__(self): 149 | return int(self._d) 150 | 151 | def __float__(self): 152 | return float(self._d) 153 | 154 | def __round__(self, n=None): 155 | return round(self._d, n) 156 | 157 | # Export ContractingDecimal for external use 158 | exports = { 159 | 'decimal': ContractingDecimal 160 | } 161 | -------------------------------------------------------------------------------- /tests/integration/test_rich_ctx_calling.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.client import ContractingClient 3 | 4 | 5 | def con_module1(): 6 | @export 7 | def get_context2(): 8 | return { 9 | 'name': 'get_context2', 10 | 'owner': ctx.owner, 11 | 'this': ctx.this, 12 | 'signer': ctx.signer, 13 | 'caller': ctx.caller, 14 | 'entry': ctx.entry, 15 | 'submission_name': ctx.submission_name 16 | } 17 | 18 | 19 | def con_all_in_one(): 20 | @export 21 | def call_me(): 22 | return call_me_again() 23 | 24 | @export 25 | def call_me_again(): 26 | return call_me_again_again() 27 | 28 | @export 29 | def call_me_again_again(): 30 | return({ 31 | 'name': 'call_me_again_again', 32 | 'owner': ctx.owner, 33 | 'this': ctx.this, 34 | 'signer': ctx.signer, 35 | 'caller': ctx.caller, 36 | 'entry': ctx.entry, 37 | 'submission_name': ctx.submission_name 38 | }) 39 | 40 | 41 | def con_dynamic_import(): 42 | 43 | @export 44 | def called_from_a_far(): 45 | m = importlib.import_module('con_all_in_one') 46 | res = m.call_me_again_again() 47 | 48 | return [res, { 49 | 'name': 'called_from_a_far', 50 | 'owner': ctx.owner, 51 | 'this': ctx.this, 52 | 'signer': ctx.signer, 53 | 'caller': ctx.caller, 54 | 'entry': ctx.entry, 55 | 'submission_name': ctx.submission_name 56 | }] 57 | 58 | @export 59 | def con_called_from_a_far_stacked(): 60 | m = importlib.import_module('con_all_in_one') 61 | return m.call() 62 | 63 | def con_submission_name_test(): 64 | submission_name = Variable() 65 | 66 | @construct 67 | def seed(): 68 | submission_name.set(ctx.submission_name) 69 | 70 | @export 71 | def get_submission_context(): 72 | return submission_name.get() 73 | 74 | @export 75 | def get_entry_context(): 76 | con_name, func_name = ctx.entry 77 | 78 | return { 79 | 'entry_contract': con_name, 80 | 'entry_function': func_name 81 | } 82 | 83 | 84 | 85 | class TestRandomsContract(TestCase): 86 | def setUp(self): 87 | self.c = ContractingClient(signer='stu') 88 | self.c.flush() 89 | 90 | # Submit contracts 91 | self.c.submit(con_module1) 92 | self.c.submit(con_all_in_one) 93 | self.c.submit(con_dynamic_import) 94 | self.c.submit(con_submission_name_test) 95 | 96 | def tearDown(self): 97 | self.c.flush() 98 | 99 | def test_ctx2(self): 100 | module = self.c.get_contract('con_module1') 101 | res = module.get_context2() 102 | expected = { 103 | 'name': 'get_context2', 104 | 'entry': ('con_module1', 'get_context2'), 105 | 'owner': None, 106 | 'this': 'con_module1', 107 | 'signer': 'stu', 108 | 'caller': 'stu', 109 | 'submission_name': None 110 | } 111 | self.assertDictEqual(res, expected) 112 | 113 | def test_multi_call_doesnt_affect_parameters(self): 114 | aio = self.c.get_contract('con_all_in_one') 115 | res = aio.call_me() 116 | 117 | expected = { 118 | 'name': 'call_me_again_again', 119 | 'entry': ('con_all_in_one', 'call_me'), 120 | 'owner': None, 121 | 'this': 'con_all_in_one', 122 | 'signer': 'stu', 123 | 'caller': 'stu', 124 | 'submission_name': None 125 | } 126 | 127 | self.assertDictEqual(res, expected) 128 | 129 | # To-Do: Figure out why this test does not work when using pytest tests/integration, but works when running the test directly 130 | # def test_dynamic_call(self): 131 | # dy = self.c.get_contract('con_dynamic_import') 132 | # res1, res2 = dy.called_from_a_far() 133 | 134 | # expected1 = { 135 | # 'name': 'call_me_again_again', 136 | # 'entry': ('con_dynamic_import', 'called_from_a_far'), 137 | # 'owner': None, 138 | # 'this': 'con_all_in_one', 139 | # 'signer': 'stu', 140 | # 'caller': 'con_dynamic_import', 141 | # 'submission_name': None 142 | # } 143 | 144 | # expected2 = { 145 | # 'name': 'called_from_a_far', 146 | # 'entry': ('con_dynamic_import', 'called_from_a_far'), 147 | # 'owner': None, 148 | # 'this': 'con_dynamic_import', 149 | # 'signer': 'stu', 150 | # 'caller': 'stu', 151 | # 'submission_name': None 152 | # } 153 | 154 | # self.assertDictEqual(res1, expected1) 155 | # self.assertDictEqual(res2, expected2) 156 | 157 | 158 | def test_submission_name_in_construct_function(self): 159 | contract = self.c.get_contract('con_submission_name_test') 160 | submission_name = contract.get_submission_context() 161 | 162 | self.assertEqual("con_submission_name_test", submission_name) 163 | 164 | def test_entry_context(self): 165 | contract = self.c.get_contract('con_submission_name_test') 166 | details = contract.get_entry_context() 167 | 168 | self.assertEqual("con_submission_name_test", details.get('entry_contract')) 169 | self.assertEqual("get_entry_context", details.get('entry_function')) -------------------------------------------------------------------------------- /tests/integration/test_stamp_deduction.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.storage.driver import Driver 3 | from contracting.execution.executor import Executor 4 | from contracting.constants import STAMPS_PER_TAU 5 | 6 | import contracting 7 | import os 8 | 9 | def submission_kwargs_for_file(f): 10 | # Get the file name only by splitting off directories 11 | split = f.split('/') 12 | split = split[-1] 13 | 14 | # Now split off the .s 15 | split = split.split('.') 16 | contract_name = split[0] 17 | 18 | with open(f) as file: 19 | contract_code = file.read() 20 | 21 | return { 22 | 'name': f'con_{contract_name}', 23 | 'code': contract_code, 24 | } 25 | 26 | 27 | TEST_SUBMISSION_KWARGS = { 28 | 'sender': 'stu', 29 | 'contract_name': 'submission', 30 | 'function_name': 'submit_contract' 31 | } 32 | 33 | 34 | class TestMetering(TestCase): 35 | def setUp(self): 36 | # Hard load the submission contract 37 | self.d = Driver() 38 | self.d.flush_full() 39 | 40 | submission_path = os.path.join(os.path.dirname(__file__), "test_contracts", "submission.s.py") 41 | 42 | with open(submission_path) as f: 43 | contract = f.read() 44 | 45 | self.d.set_contract(name='submission', 46 | code=contract) 47 | self.d.commit() 48 | 49 | # Execute the currency contract with metering disabled 50 | self.e = Executor(driver=self.d, currency_contract='con_currency') 51 | currency_path = os.path.join(os.path.dirname(__file__), "test_contracts", "currency.s.py") 52 | self.e.execute(**TEST_SUBMISSION_KWARGS, 53 | kwargs=submission_kwargs_for_file(currency_path), metering=False, auto_commit=True) 54 | 55 | def tearDown(self): 56 | self.d.flush_full() 57 | 58 | def test_simple_execution_deducts_stamps(self): 59 | prior_balance = self.d.get('con_currency.balances:stu') 60 | 61 | output = self.e.execute('stu', 'con_currency', 'transfer', kwargs={'amount': 100, 'to': 'colin'}, auto_commit=True) 62 | 63 | new_balance = self.d.get('con_currency.balances:stu') 64 | 65 | self.assertEqual(float(prior_balance - new_balance - 100), output['stamps_used'] / STAMPS_PER_TAU) 66 | 67 | def test_too_few_stamps_fails_and_deducts_properly(self): 68 | prior_balance = self.d.get('con_currency.balances:stu') 69 | 70 | print(prior_balance) 71 | 72 | output = self.e.execute('stu', 'con_currency', 'transfer', kwargs={'amount': 100, 'to': 'colin'}, 73 | stamps=1, auto_commit=True) 74 | 75 | print(output) 76 | 77 | new_balance = self.d.get('con_currency.balances:stu') 78 | 79 | self.assertEqual(float(prior_balance - new_balance), output['stamps_used'] / STAMPS_PER_TAU) 80 | 81 | def test_adding_too_many_stamps_throws_error(self): 82 | prior_balance = self.d.get('con_currency.balances:stu') 83 | too_many_stamps = (prior_balance + 1000) * STAMPS_PER_TAU 84 | 85 | output = self.e.execute('stu', 'con_currency', 'transfer', kwargs={'amount': 100, 'to': 'colin'}, 86 | stamps=too_many_stamps, auto_commit=True) 87 | 88 | self.assertEqual(output['status_code'], 1) 89 | 90 | def test_adding_all_stamps_with_infinate_loop_eats_all_balance(self): 91 | self.d.set('con_currency.balances:stu', 500) 92 | self.d.commit() 93 | 94 | prior_balance = self.d.get('con_currency.balances:stu') 95 | 96 | prior_balance *= STAMPS_PER_TAU 97 | 98 | inf_loop_path = os.path.join(os.path.dirname(__file__), "test_contracts", "inf_loop.s.py") 99 | 100 | res = self.e.execute( 101 | **TEST_SUBMISSION_KWARGS, 102 | kwargs=submission_kwargs_for_file(inf_loop_path), 103 | stamps=prior_balance, 104 | 105 | metering=True, auto_commit=True 106 | ) 107 | 108 | new_balance = self.d.get('con_currency.balances:stu') 109 | 110 | # Not all stamps will be deducted because it will blow up in the middle of execution 111 | 112 | self.assertTrue(new_balance < 500) 113 | 114 | def test_submitting_contract_succeeds_with_enough_stamps(self): 115 | prior_balance = self.d.get('con_currency.balances:stu') 116 | 117 | print(prior_balance) 118 | 119 | erc20_clone_path = os.path.join(os.path.dirname(__file__), "test_contracts", "erc20_clone.s.py") 120 | output = self.e.execute(**TEST_SUBMISSION_KWARGS, 121 | kwargs=submission_kwargs_for_file(erc20_clone_path), auto_commit=True 122 | ) 123 | print(output) 124 | 125 | new_balance = self.d.get('con_currency.balances:stu') 126 | 127 | print(new_balance) 128 | 129 | self.assertEqual(float(prior_balance - new_balance), output['stamps_used'] / STAMPS_PER_TAU) 130 | 131 | def test_pending_writes_has_deducted_stamp_amount_prior_to_auto_commit(self): 132 | prior_balance = self.d.get('con_currency.balances:stu') 133 | 134 | erc20_clone_path = os.path.join(os.path.dirname(__file__), "test_contracts", "erc20_clone.s.py") 135 | output = self.e.execute(**TEST_SUBMISSION_KWARGS, 136 | kwargs=submission_kwargs_for_file(erc20_clone_path), auto_commit=False 137 | ) 138 | self.assertNotEquals(self.e.driver.pending_writes['con_currency.balances:stu'], prior_balance) 139 | 140 | -------------------------------------------------------------------------------- /tests/unit/test_datetime.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.stdlib.bridge.time import Datetime, Timedelta 3 | from datetime import datetime as dt 4 | from datetime import timedelta 5 | 6 | 7 | class TestDatetime(TestCase): 8 | def test_datetime_variables_set(self): 9 | now = dt.now() 10 | 11 | d = Datetime(now.year, now.month, now.day) 12 | 13 | self.assertEqual(0, d.microsecond) 14 | self.assertEqual(0, d.second) 15 | self.assertEqual(0, d.minute) 16 | self.assertEqual(0, d.hour) 17 | self.assertEqual(now.day, d.day) 18 | self.assertEqual(now.month, d.month) 19 | self.assertEqual(now.year, d.year) 20 | 21 | ### 22 | # == 23 | ### 24 | def test_datetime_eq_true(self): 25 | now = dt.now() 26 | 27 | d = Datetime(now.year, now.month, now.day) 28 | e = Datetime(now.year, now.month, now.day) 29 | 30 | self.assertTrue(d == e) 31 | 32 | def test_datetime_eq_false(self): 33 | now = dt.now() 34 | d = Datetime(now.year, now.month, now.day) 35 | 36 | then = now + timedelta(days=1) 37 | e = Datetime(then.year, then.month, then.day) 38 | 39 | self.assertFalse(d == e) 40 | 41 | ### 42 | # != 43 | ### 44 | def test_datetime_ne_false(self): 45 | now = dt.now() 46 | 47 | d = Datetime(now.year, now.month, now.day) 48 | e = Datetime(now.year, now.month, now.day) 49 | 50 | self.assertFalse(d != e) 51 | 52 | def test_datetime_ne_true(self): 53 | now = dt.now() 54 | d = Datetime(now.year, now.month, now.day) 55 | 56 | then = now + timedelta(days=1) 57 | e = Datetime(then.year, then.month, then.day) 58 | 59 | self.assertTrue(d != e) 60 | 61 | ### 62 | # < 63 | ### 64 | def test_datetime_lt_true(self): 65 | now = dt.now() 66 | d = Datetime(now.year, now.month, now.day) 67 | 68 | then = now + timedelta(days=1) 69 | e = Datetime(then.year, then.month, then.day) 70 | 71 | self.assertTrue(d < e) 72 | 73 | def test_datetime_lt_false(self): 74 | now = dt.now() 75 | d = Datetime(now.year, now.month, now.day) 76 | 77 | then = now + timedelta(days=1) 78 | e = Datetime(then.year, then.month, then.day) 79 | 80 | self.assertFalse(e < d) 81 | 82 | ### 83 | # > 84 | ### 85 | def test_datetime_gt_true(self): 86 | now = dt.now() 87 | d = Datetime(now.year, now.month, now.day) 88 | 89 | then = now + timedelta(days=1) 90 | e = Datetime(then.year, then.month, then.day) 91 | 92 | self.assertTrue(e > d) 93 | 94 | def test_datetime_gt_false(self): 95 | now = dt.now() 96 | d = Datetime(now.year, now.month, now.day) 97 | 98 | then = now + timedelta(days=1) 99 | e = Datetime(then.year, then.month, then.day) 100 | 101 | self.assertFalse(d > e) 102 | 103 | ### 104 | # >= 105 | ### 106 | def test_datetime_ge_true_g(self): 107 | now = dt.now() 108 | d = Datetime(now.year, now.month, now.day) 109 | 110 | then = now + timedelta(days=1) 111 | e = Datetime(then.year, then.month, then.day) 112 | 113 | self.assertTrue(e >= d) 114 | 115 | def test_datetime_ge_true_eq(self): 116 | now = dt.now() 117 | 118 | d = Datetime(now.year, now.month, now.day) 119 | e = Datetime(now.year, now.month, now.day) 120 | 121 | self.assertTrue(d >= e) 122 | 123 | def test_datetime_ge_false_g(self): 124 | now = dt.now() 125 | d = Datetime(now.year, now.month, now.day) 126 | 127 | then = now + timedelta(days=1) 128 | e = Datetime(then.year, then.month, then.day) 129 | 130 | self.assertFalse(d >= e) 131 | 132 | ### 133 | # <= 134 | ### 135 | def test_datetime_le_true(self): 136 | now = dt.now() 137 | d = Datetime(now.year, now.month, now.day) 138 | 139 | then = now + timedelta(days=1) 140 | e = Datetime(then.year, then.month, then.day) 141 | 142 | self.assertTrue(d <= e) 143 | 144 | def test_datetime_le_true_eq(self): 145 | now = dt.now() 146 | 147 | d = Datetime(now.year, now.month, now.day) 148 | e = Datetime(now.year, now.month, now.day) 149 | 150 | self.assertTrue(d <= e) 151 | 152 | def test_datetime_le_false(self): 153 | now = dt.now() 154 | d = Datetime(now.year, now.month, now.day) 155 | 156 | then = now + timedelta(days=1) 157 | e = Datetime(then.year, then.month, then.day) 158 | 159 | self.assertFalse(e <= d) 160 | 161 | def test_datetime_subtraction_to_proper_timedelta(self): 162 | d = Datetime(2019, 1, 1) 163 | e = Datetime(2018, 1, 1) 164 | 165 | self.assertEqual((d - e), Timedelta(days=365)) 166 | 167 | 168 | def test_datetime_strptime(self): 169 | d = dt(2019, 1, 1) 170 | self.assertEqual(str(Datetime.strptime(str(d), '%Y-%m-%d %H:%M:%S')), str(d)) 171 | 172 | 173 | def test_datetime_strptime_invalid_format(self): 174 | d = dt(2019, 1, 1) 175 | with self.assertRaises(ValueError): 176 | Datetime.strptime(str(d), '%Y-%m-%d') 177 | 178 | def test_datetime_strptime_invalid_date(self): 179 | with self.assertRaises(ValueError): 180 | Datetime.strptime('2019-02-30 12:00:00', '%Y-%m-%d %H:%M:%S') 181 | 182 | 183 | def test_datetime_strptime_invalid_date_format(self): 184 | with self.assertRaises(ValueError): 185 | Datetime.strptime('2019-02-30 12:00:00', '%Y-%m-%d %H:%M:%S') 186 | 187 | 188 | def test_datetime_returns_correct_datetime_cls(self): 189 | d = dt(2019, 1, 1) 190 | self.assertEqual(Datetime.strptime(str(d), '%Y-%m-%d %H:%M:%S'), Datetime(2019, 1, 1)) 191 | 192 | -------------------------------------------------------------------------------- /src/contracting/execution/tracer.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import dis 3 | import threading 4 | import psutil 5 | import os 6 | 7 | # Define the opcode costs 8 | cu_costs = { 9 | 0: 2, 1: 2, 2: 4, 3: 4, 4: 4, 5: 4, 6: 4, 7: 4, 8: 4, 9: 2, 10: 2, 11: 4, 12: 2, 10 | 13: 4, 14: 4, 15: 4, 16: 4, 17: 4, 18: 4, 19: 4, 20: 2, 21: 4, 22: 8, 23: 6, 24: 6, 11 | 25: 4, 26: 4, 27: 4, 28: 4, 29: 4, 30: 4, 31: 6, 32: 6, 33: 6, 34: 2, 35: 6, 36: 6, 12 | 37: 6, 38: 2, 39: 4, 40: 4, 41: 4, 42: 4, 43: 4, 44: 2, 45: 2, 46: 2, 47: 4, 48: 2, 13 | 49: 6, 50: 6, 51: 6, 52: 6, 53: 4, 54: 6, 55: 4, 56: 4, 57: 4, 58: 4, 59: 4, 60: 4, 14 | 61: 4, 62: 4, 63: 4, 64: 6, 65: 6, 66: 8, 67: 8, 68: 8, 69: 12, 70: 2, 71: 1610, 72: 8, 15 | 73: 6, 74: 4, 75: 6, 76: 6, 77: 4, 78: 4, 79: 4, 80: 6, 81: 6, 82: 4, 83: 2, 84: 126, 16 | 85: 1000, 86: 4, 87: 8, 88: 6, 89: 4, 90: 2, 91: 2, 92: 2, 93: 512, 94: 8, 95: 6, 96: 6, 17 | 97: 4, 98: 4, 99: 2, 100: 2, 101: 2, 102: 2, 103: 6, 104: 8, 105: 8, 106: 4, 107: 4, 18 | 108: 38, 109: 126, 110: 4, 111: 4, 112: 4, 113: 6, 114: 4, 115: 4, 116: 4, 117: 4, 118: 6, 19 | 119: 6, 120: 4, 121: 4, 122: 4, 123: 6, 124: 32, 125: 2, 126: 2, 127: 4, 128: 4, 129: 4, 20 | 130: 6, 131: 10, 132: 8, 133: 12, 134: 4, 135: 4, 136: 8, 137: 2, 138: 2, 139: 2, 140: 4, 21 | 141: 6, 142: 12, 143: 6, 144: 2, 145: 8, 146: 8, 147: 6, 148: 2, 149: 6, 150: 6, 151: 6, 22 | 152: 6, 153: 4, 154: 4, 155: 4, 156: 6, 157: 4, 158: 4, 159: 4, 160: 4, 161: 2, 162: 4, 23 | 163: 6, 164: 6, 165: 6, 166: 6, 167: 2, 168: 4, 169: 4, 170: 2, 171: 8, 172: 2, 173: 4, 24 | 174: 4, 175: 4, 176: 4, 177: 4, 178: 4, 179: 4, 180: 4, 255: 8 25 | } 26 | 27 | # Define maximum stamps 28 | MAX_STAMPS = 6500000 29 | 30 | class Tracer: 31 | def __init__(self): 32 | self.cost = 0 33 | self.stamp_supplied = 0 34 | self.last_frame_mem_usage = 0 35 | self.total_mem_usage = 0 36 | self.started = False 37 | self.call_count = 0 38 | self.max_call_count = 800000 39 | self.instruction_cache = {} 40 | self.lock = threading.Lock() 41 | 42 | def start(self): 43 | sys.settrace(self.trace_func) 44 | self.cost = 0 45 | self.call_count = 0 46 | self.started = True 47 | 48 | def stop(self): 49 | if self.started: 50 | sys.settrace(None) 51 | self.started = False 52 | 53 | def reset(self): 54 | self.stop() 55 | self.cost = 0 56 | self.stamp_supplied = 0 57 | self.last_frame_mem_usage = 0 58 | self.total_mem_usage = 0 59 | self.call_count = 0 60 | 61 | def set_stamp(self, stamp): 62 | self.stamp_supplied = stamp 63 | 64 | def add_cost(self, new_cost): 65 | self.cost += new_cost 66 | if self.cost > self.stamp_supplied or self.cost > MAX_STAMPS: 67 | self.stop() 68 | raise AssertionError("The cost has exceeded the stamp supplied!") 69 | 70 | def get_stamp_used(self): 71 | return self.cost 72 | 73 | def get_last_frame_mem_usage(self): 74 | return self.last_frame_mem_usage 75 | 76 | def get_total_mem_usage(self): 77 | return self.total_mem_usage 78 | 79 | def is_started(self): 80 | return self.started 81 | 82 | def get_memory_usage(self): 83 | process = psutil.Process(os.getpid()) 84 | mem_info = process.memory_info() 85 | # Return the RSS (Resident Set Size) 86 | return mem_info.rss 87 | 88 | def trace_func(self, frame, event, arg): 89 | if event == 'line': 90 | self.call_count += 1 91 | if self.call_count > self.max_call_count: 92 | self.stop() 93 | raise AssertionError("Call count exceeded threshold! Infinite Loop?") 94 | 95 | # Check if the function matches the target module and function names 96 | code = frame.f_code 97 | current_function_name = code.co_name 98 | globals_dict = frame.f_globals 99 | module_name = globals_dict.get('__name__', '') 100 | 101 | # Only trace code within contracts (if '__contract__' in globals) 102 | if '__contract__' not in globals_dict: 103 | return 104 | 105 | # Get the opcode at the current instruction 106 | lasti = frame.f_lasti 107 | opcode = self.get_opcode(code, lasti) 108 | 109 | # Update memory usage 110 | if self.last_frame_mem_usage == 0: 111 | self.last_frame_mem_usage = self.get_memory_usage() 112 | 113 | new_memory_usage = self.get_memory_usage() 114 | if new_memory_usage > self.last_frame_mem_usage: 115 | self.total_mem_usage += (new_memory_usage - self.last_frame_mem_usage) 116 | self.last_frame_mem_usage = new_memory_usage 117 | 118 | # Check for memory usage limit (set an arbitrary limit, e.g., 500MB) 119 | if self.total_mem_usage > 500 * 1024 * 1024: 120 | self.stop() 121 | raise AssertionError(f"Transaction exceeded memory usage! Total usage: {self.total_mem_usage} bytes") 122 | 123 | # Add cost based on opcode 124 | opcode_cost = cu_costs.get(opcode, 1) # Default cost if opcode not found 125 | self.cost += opcode_cost 126 | 127 | if self.cost > self.stamp_supplied or self.cost > MAX_STAMPS: 128 | self.stop() 129 | raise AssertionError("The cost has exceeded the stamp supplied!") 130 | 131 | return self.trace_func 132 | 133 | def get_opcode(self, code, offset): 134 | # Cache the instruction map per code object 135 | with self.lock: 136 | instruction_map = self.instruction_cache.get(code) 137 | if instruction_map is None: 138 | instruction_map = {} 139 | for instr in dis.get_instructions(code): 140 | instruction_map[instr.offset] = instr.opcode 141 | self.instruction_cache[code] = instruction_map 142 | opcode = instruction_map.get(offset, None) 143 | if opcode is None: 144 | # Instruction not found; default to 0 145 | opcode = 0 146 | return opcode 147 | -------------------------------------------------------------------------------- /tests/unit/test_imports_stdlib.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from contracting.stdlib.bridge import imports 3 | from types import ModuleType 4 | from contracting.storage.orm import Hash, Variable 5 | import os 6 | 7 | class TestImports(TestCase): 8 | def setUp(self): 9 | scope = {} 10 | 11 | self.script_dir = os.path.dirname(os.path.abspath(__file__)) 12 | compiled_token_file_path = os.path.join(self.script_dir, "precompiled", "compiled_token.py") 13 | 14 | with open(compiled_token_file_path) as f: 15 | code = f.read() 16 | 17 | exec(code, scope) 18 | 19 | m = ModuleType('testing') 20 | 21 | vars(m).update(scope) 22 | del vars(m)['__builtins__'] 23 | 24 | self.module = m 25 | 26 | def test_func_correct_type(self): 27 | def sup(x, y): 28 | return x + y 29 | 30 | s = imports.Func(name='sup', args=('x', 'y')) 31 | 32 | self.assertTrue(s.is_of(sup)) 33 | 34 | def test_func_incorrect_name(self): 35 | def sup(x, y): 36 | return x + y 37 | 38 | s = imports.Func(name='not_much', args=('x', 'y')) 39 | 40 | self.assertFalse(s.is_of(sup)) 41 | 42 | def test_func_incorrect_args(self): 43 | def sup(a, b): 44 | return a + b 45 | 46 | s = imports.Func(name='sup', args=('x', 'y')) 47 | 48 | self.assertFalse(s.is_of(sup)) 49 | 50 | def test_func_correct_with_kwargs(self): 51 | def sup(x=100, y=200): 52 | return x + y 53 | 54 | s = imports.Func(name='sup', args=('x', 'y')) 55 | 56 | self.assertTrue(s.is_of(sup)) 57 | 58 | def test_func_correct_with_annotations(self): 59 | def sup(x: int, y: int): 60 | return x + y 61 | 62 | s = imports.Func(name='sup', args=('x', 'y')) 63 | 64 | self.assertTrue(s.is_of(sup)) 65 | 66 | def test_func_correct_with_kwargs_and_annotations(self): 67 | def sup(x: int=100, y: int=100): 68 | return x + y 69 | 70 | s = imports.Func(name='sup', args=('x', 'y')) 71 | 72 | self.assertTrue(s.is_of(sup)) 73 | 74 | def test_func_correct_private(self): 75 | def __sup(a, b): 76 | return a + b 77 | 78 | s = imports.Func(name='sup', args=('a', 'b'), private=True) 79 | 80 | self.assertTrue(s.is_of(__sup)) 81 | 82 | def test_func_false_private(self): 83 | def __sup(a, b): 84 | return a + b 85 | 86 | s = imports.Func(name='sup', args=('x', 'y'), private=True) 87 | 88 | self.assertFalse(s.is_of(__sup)) 89 | 90 | def test_var_fails_if_type_not_of_datum(self): 91 | with self.assertRaises(AssertionError): 92 | imports.Var('blah', str) 93 | 94 | def test_enforce_interface_works_all_public_funcs(self): 95 | interface = [ 96 | imports.Func('transfer', args=('amount', 'to')), 97 | imports.Func('balance_of', args=('account',)), 98 | imports.Func('total_supply'), 99 | imports.Func('allowance', args=('owner', 'spender')), 100 | imports.Func('approve', args=('amount', 'to')), 101 | imports.Func('transfer_from', args=('amount', 'to', 'main_account')) 102 | ] 103 | 104 | self.assertTrue(imports.enforce_interface(self.module, interface)) 105 | 106 | def test_enforce_interface_works_on_subset_funcs(self): 107 | interface = [ 108 | imports.Func('transfer', args=('amount', 'to')), 109 | imports.Func('balance_of', args=('account',)), 110 | imports.Func('total_supply'), 111 | imports.Func('allowance', args=('owner', 'spender')), 112 | imports.Func('transfer_from', args=('amount', 'to', 'main_account')) 113 | ] 114 | 115 | self.assertTrue(imports.enforce_interface(self.module, interface)) 116 | 117 | def test_enforce_interface_fails_on_wrong_funcs(self): 118 | interface = [ 119 | imports.Func('transfer', args=('amount', 'to')), 120 | imports.Func('balance_of', args=('account',)), 121 | imports.Func('spooky'), 122 | imports.Func('allowance', args=('owner', 'spender')), 123 | imports.Func('transfer_from', args=('amount', 'to', 'main_account')) 124 | ] 125 | 126 | self.assertFalse(imports.enforce_interface(self.module, interface)) 127 | 128 | def test_enforce_interface_on_resources(self): 129 | interface = [ 130 | imports.Var('supply', Variable), 131 | imports.Var('balances', Hash), 132 | ] 133 | 134 | self.assertTrue(imports.enforce_interface(self.module, interface)) 135 | 136 | def test_complete_enforcement(self): 137 | interface = [ 138 | imports.Func('transfer', args=('amount', 'to')), 139 | imports.Func('balance_of', args=('account',)), 140 | imports.Func('total_supply'), 141 | imports.Func('allowance', args=('owner', 'spender')), 142 | imports.Func('approve', args=('amount', 'to')), 143 | imports.Func('transfer_from', args=('amount', 'to', 'main_account')), 144 | imports.Var('supply', Variable), 145 | imports.Var('balances', Hash) 146 | ] 147 | 148 | self.assertTrue(imports.enforce_interface(self.module, interface)) 149 | 150 | def test_private_function_enforcement(self): 151 | interface = [ 152 | imports.Func('private_func', private=True), 153 | ] 154 | 155 | self.assertTrue(imports.enforce_interface(self.module, interface)) 156 | 157 | def test_complete_enforcement_with_private_func(self): 158 | interface = [ 159 | imports.Func('transfer', args=('amount', 'to')), 160 | imports.Func('balance_of', args=('account',)), 161 | imports.Func('total_supply'), 162 | imports.Func('allowance', args=('owner', 'spender')), 163 | imports.Func('approve', args=('amount', 'to')), 164 | imports.Func('private_func', private=True), 165 | imports.Func('transfer_from', args=('amount', 'to', 'main_account')), 166 | imports.Var('supply', Variable), 167 | imports.Var('balances', Hash) 168 | ] 169 | 170 | self.assertTrue(imports.enforce_interface(self.module, interface)) -------------------------------------------------------------------------------- /src/contracting/storage/encoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | import decimal 3 | 4 | from contracting.stdlib.bridge.time import Datetime, Timedelta 5 | from contracting.stdlib.bridge.decimal import ContractingDecimal, MAX_LOWER_PRECISION, fix_precision 6 | from contracting.constants import INDEX_SEPARATOR, DELIMITER 7 | 8 | MONGO_MIN_INT = -(2 ** 63) 9 | MONGO_MAX_INT = 2 ** 63 - 1 10 | 11 | ## 12 | # ENCODER CLASS 13 | # Add to this to encode Python types for storage. 14 | # Right now, this is only for datetime types. They are passed into the system as ISO strings, cast into Datetime objs 15 | # and stored as dicts. Is there a better way? I don't know, maybe. 16 | ## 17 | 18 | 19 | def safe_repr(obj, max_len=1024): 20 | try: 21 | r = obj.__repr__() 22 | rr = r.split(' at 0x') 23 | if len(rr) > 1: 24 | return rr[0] + '>' 25 | return rr[0][:max_len] 26 | except: 27 | return None 28 | 29 | 30 | class Encoder(json.JSONEncoder): 31 | def default(self, o, *args): 32 | if isinstance(o, Datetime) or o.__class__.__name__ == Datetime.__name__: 33 | return { 34 | '__time__': [o.year, o.month, o.day, o.hour, o.minute, o.second, o.microsecond] 35 | } 36 | elif isinstance(o, Timedelta) or o.__class__.__name__ == Timedelta.__name__: 37 | return { 38 | '__delta__': [o._timedelta.days, o._timedelta.seconds] 39 | } 40 | elif isinstance(o, bytes): 41 | return { 42 | '__bytes__': o.hex() 43 | } 44 | elif isinstance(o, decimal.Decimal) or o.__class__.__name__ == decimal.Decimal.__name__: 45 | #return format(o, f'.{MAX_LOWER_PRECISION}f') 46 | return { 47 | '__fixed__': str(fix_precision(o)) 48 | } 49 | 50 | elif isinstance(o, ContractingDecimal) or o.__class__.__name__ == ContractingDecimal.__name__: 51 | #return format(o._d, f'.{MAX_LOWER_PRECISION}f') 52 | return { 53 | '__fixed__': str(fix_precision(o._d)) 54 | } 55 | #else: 56 | # return safe_repr(o) 57 | return super().default(o) 58 | 59 | 60 | def encode_int(value: int): 61 | if MONGO_MIN_INT < value < MONGO_MAX_INT: 62 | return value 63 | 64 | return { 65 | '__big_int__': str(value) 66 | } 67 | 68 | 69 | def encode_ints_in_dict(data: dict): 70 | d = dict() 71 | for k, v in data.items(): 72 | if isinstance(v, int): 73 | d[k] = encode_int(v) 74 | elif isinstance(v, dict): 75 | d[k] = encode_ints_in_dict(v) 76 | elif isinstance(v, list): 77 | d[k] = [] 78 | for i in v: 79 | if isinstance(i, dict): 80 | d[k].append(encode_ints_in_dict(i)) 81 | elif isinstance(i, int): 82 | d[k].append(encode_int(i)) 83 | else: 84 | d[k].append(i) 85 | else: 86 | d[k] = v 87 | 88 | return d 89 | 90 | 91 | # JSON library from Python 3 doesn't let you instantiate your custom Encoder. You have to pass it as an obj to json 92 | def encode(data: str): 93 | """ NOTE: 94 | Normally encoding behavior is overriden in 'default' method inside 95 | a class derived from json.JSONEncoder. Unfortunately this can be done only 96 | for custom types. 97 | 98 | Due to MongoDB integer limitation (8 bytes), we need to preprocess 'big' integers. 99 | """ 100 | if isinstance(data, int): 101 | data = encode_int(data) 102 | elif isinstance(data, dict): 103 | data = encode_ints_in_dict(data) 104 | 105 | return json.dumps(data, cls=Encoder, separators=(',', ':')) 106 | 107 | 108 | def as_object(d): 109 | if '__time__' in d: 110 | return Datetime(*d['__time__']) 111 | elif '__delta__' in d: 112 | return Timedelta(days=d['__delta__'][0], seconds=d['__delta__'][1]) 113 | elif '__bytes__' in d: 114 | return bytes.fromhex(d['__bytes__']) 115 | elif '__fixed__' in d: 116 | return ContractingDecimal(d['__fixed__']) 117 | elif '__big_int__' in d: 118 | return int(d['__big_int__']) 119 | return dict(d) 120 | 121 | 122 | # Decode has a hook for JSON objects, which are just Python dictionaries. You have to specify the logic in this hook. 123 | # This is not uniform, but this is how Python made it. 124 | def decode(data): 125 | if data is None: 126 | return None 127 | 128 | if isinstance(data, bytes): 129 | data = data.decode() 130 | 131 | try: 132 | return json.loads(data, object_hook=as_object) 133 | except json.decoder.JSONDecodeError as e: 134 | return None 135 | 136 | 137 | def make_key(contract, variable, args=[]): 138 | contract_variable = INDEX_SEPARATOR.join((contract, variable)) 139 | if args: 140 | return DELIMITER.join((contract_variable, *args)) 141 | return contract_variable 142 | 143 | 144 | def encode_kv(key, value): 145 | # if key is None: 146 | # key = '' 147 | # 148 | # if value is None: 149 | # value = '' 150 | 151 | k = key.encode() 152 | v = encode(value).encode() 153 | return k, v 154 | 155 | 156 | def decode_kv(key, value): 157 | k = key.decode() 158 | v = decode(value) 159 | # if v == '': 160 | # v = None 161 | return k, v 162 | 163 | 164 | TYPES = {'__fixed__', '__delta__', '__bytes__', '__time__', '__big_int__'} 165 | 166 | 167 | def convert(k, v): 168 | if k == '__fixed__': 169 | return ContractingDecimal(v) 170 | elif k == '__delta__': 171 | return Timedelta(days=v[0], seconds=v[1]) 172 | elif k == '__bytes__': 173 | return bytes.fromhex(v) 174 | elif k == '__time__': 175 | return Datetime(*v) 176 | elif k == '__big_int__': 177 | return int(v) 178 | return v 179 | 180 | 181 | def convert_dict(d): 182 | if not isinstance(d, dict): 183 | return d 184 | 185 | d2 = dict() 186 | for k, v in d.items(): 187 | if k in TYPES: 188 | return convert(k, v) 189 | 190 | elif isinstance(v, dict): 191 | d2[k] = convert_dict(v) 192 | 193 | elif isinstance(v, list): 194 | d2[k] = [] 195 | for i in v: 196 | d2[k].append(convert_dict(i)) 197 | 198 | else: 199 | d2[k] = v 200 | 201 | return d2 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xian Contracting 2 | 3 | Xian Contracting is a Python-based smart contract development and execution framework. Unlike traditional blockchain platforms like Ethereum, Xian Contracting leverages Python's VM to create a more accessible and familiar environment for developers to write smart contracts. 4 | 5 | ## Features 6 | 7 | - **Python-Native**: Write smart contracts in standard Python with some additional decorators and constructs 8 | - **Storage System**: Built-in ORM-like system with `Variable` and `Hash` data structures 9 | - **Runtime Security**: Secure execution environment with memory and computation limitations 10 | - **Metering System**: Built-in computation metering to prevent infinite loops and resource abuse 11 | - **Event System**: Built-in logging and event system for contract state changes 12 | - **Import Controls**: Secure import system that prevents access to dangerous system modules 13 | 14 | ## Installation 15 | 16 | ```bash 17 | pip install xian-contracting 18 | ``` 19 | 20 | ## Quick Start 21 | 22 | Here's a complete token contract example with approval system: 23 | 24 | ```python 25 | def token_contract(): 26 | balances = Hash() 27 | owner = Variable() 28 | 29 | @construct 30 | def seed(): 31 | owner.set(ctx.caller) 32 | 33 | @export 34 | def approve(amount: float, to: str): 35 | assert amount > 0, 'Cannot send negative balances.' 36 | balances[ctx.caller, to] += amount 37 | 38 | @export 39 | def transfer_from(amount: float, to: str, main_account: str): 40 | approved = allowances[main_account, ctx.caller] 41 | 42 | assert amount > 0, 'Cannot send negative balances!' 43 | assert approved >= amount, f'You approved {approved} but need {amount}' 44 | assert balances[main_account] >= amount, 'Not enough tokens to send!' 45 | 46 | allowances[main_account, ctx.caller] -= amount 47 | balances[main_account] -= amount 48 | balances[to] += amount 49 | 50 | @export 51 | def transfer(amount: float, to: str): 52 | assert amount > 0, 'Cannot send negative balances.' 53 | assert balances[ctx.caller] >= amount, 'Not enough coins to send.' 54 | 55 | balances[ctx.caller] -= amount 56 | balances[to] += amount 57 | 58 | @export 59 | def mint(to, amount): 60 | assert ctx.caller == owner.get(), 'Only the original contract author can mint!' 61 | balances[to] += amount 62 | ``` 63 | 64 | ## Core Concepts 65 | 66 | ### Storage Types 67 | 68 | - **Variable**: Single-value storage 69 | ```python 70 | counter = Variable() 71 | counter.set(0) # Set value 72 | current = counter.get() # Get value 73 | ``` 74 | 75 | - **Hash**: Key-value storage with support for complex and multi-level keys 76 | ```python 77 | balances = Hash() 78 | # Single-level key 79 | balances['alice'] = 100 80 | alice_balance = balances['alice'] 81 | 82 | # Multi-level keys for complex relationships 83 | balances['alice', 'bob'] = 50 # e.g., alice approves bob to spend 50 tokens 84 | approved_amount = balances['alice', 'bob'] # Get the approved amount 85 | 86 | # You can use up to 16 dimensions in key tuples 87 | data['user', 'preferences', 'theme'] = 'dark' 88 | ``` 89 | 90 | ### Contract Decorators 91 | 92 | - **@construct**: Initializes contract state (can only be called once) 93 | ```python 94 | @construct 95 | def seed(): 96 | owner.set(ctx.caller) 97 | ``` 98 | 99 | - **@export**: Makes function callable from outside the contract 100 | ```python 101 | @export 102 | def increment(amount: int): 103 | counter.set(counter.get() + amount) 104 | ``` 105 | 106 | ### Contract Context 107 | 108 | The `ctx` object provides important runtime information: 109 | 110 | - `ctx.caller`: Address of the account calling the contract 111 | - `ctx.this`: Current contract's address 112 | - `ctx.signer`: Original transaction signer 113 | - `ctx.owner`: Contract owner's address 114 | 115 | ## Using the ContractingClient 116 | 117 | The `ContractingClient` class is your main interface for deploying and interacting with contracts: 118 | 119 | ```python 120 | from contracting.client import ContractingClient 121 | 122 | # Initialize the client 123 | client = ContractingClient() 124 | 125 | # Submit a contract 126 | with open('token.py', 'r') as f: 127 | contract = f.read() 128 | 129 | client.submit(name='con_token', code=contract) 130 | 131 | # Get contract instance 132 | token = client.get_contract('con_token') 133 | 134 | # Call contract methods 135 | token.transfer(amount=100, to='bob') 136 | ``` 137 | 138 | ## Storage Driver 139 | 140 | The framework includes a powerful storage system: 141 | 142 | ```python 143 | from contracting.storage.driver import Driver 144 | 145 | driver = Driver() 146 | 147 | # Direct storage operations 148 | driver.set('key', 'value') 149 | driver.get('key') 150 | 151 | # Contract storage 152 | driver.set_contract(name='contract_name', code=contract_code) 153 | driver.get_contract('contract_name') 154 | ``` 155 | 156 | ## Event System 157 | 158 | Contracts can emit events which can be tracked by external systems: 159 | 160 | ```python 161 | def token_contract(): 162 | transfer_event = LogEvent( 163 | 'transfer', 164 | { 165 | 'sender': {'type': str, 'idx': True}, 166 | 'receiver': {'type': str, 'idx': True}, 167 | 'amount': {'type': float} 168 | } 169 | ) 170 | 171 | @export 172 | def transfer(amount: float, to: str): 173 | # ... transfer logic ... 174 | 175 | # Emit event 176 | transfer_event({ 177 | 'sender': ctx.caller, 178 | 'receiver': to, 179 | 'amount': amount 180 | }) 181 | ``` 182 | 183 | ## Security Features 184 | 185 | - Restricted imports to prevent malicious code execution 186 | - Memory usage tracking and limitations 187 | - Computation metering to prevent infinite loops 188 | - Secure runtime environment 189 | - Type checking and validation 190 | - Private method protection 191 | 192 | ## Development and Testing 193 | 194 | When developing contracts, you can use the linter to check for common issues: 195 | 196 | ```python 197 | from contracting.client import ContractingClient 198 | 199 | client = ContractingClient() 200 | violations = client.lint(contract_code) 201 | ``` 202 | 203 | ## License 204 | 205 | This project is licensed under the Creative Commons Attribution‑NonCommercial 4.0 International - see the [LICENSE](LICENSE) file for details. 206 | Non‑commercial use only. See LICENSE for details. 207 | --------------------------------------------------------------------------------