├── bc4py ├── chain │ ├── __init__.py │ ├── checking │ │ ├── checkblock.py │ │ ├── __init__.py │ │ ├── tx_mintcoin.py │ │ ├── utils.py │ │ ├── tx_reward.py │ │ └── checktx.py │ ├── msgpack.py │ ├── utils.py │ ├── signature.py │ ├── genesisblock.py │ ├── workhash.py │ └── difficulty.py ├── database │ ├── __init__.py │ ├── obj.py │ └── tools.py ├── user │ ├── api │ │ ├── __init__.py │ │ ├── ep_others.py │ │ ├── jsonrpc │ │ │ ├── __init__.py │ │ │ ├── others.py │ │ │ └── account.py │ │ ├── ep_blockchain.py │ │ ├── utils.py │ │ ├── ep_websocket.py │ │ ├── ep_wallet.py │ │ ├── ep_system.py │ │ ├── server.py │ │ └── ep_account.py │ ├── txcreation │ │ ├── __init__.py │ │ ├── transfer.py │ │ ├── mintcoin.py │ │ └── utils.py │ ├── network │ │ ├── __init__.py │ │ ├── update.py │ │ ├── directcmd.py │ │ ├── sendnew.py │ │ ├── connection.py │ │ └── broadcast.py │ └── __init__.py ├── bip32 │ ├── __init__.py │ ├── utils.py │ └── base58.py ├── __init__.py ├── exit.py ├── gittool.py ├── for_debug │ └── __init__.py └── config.py ├── requirements-dev.txt ├── image ├── favicon.ico ├── python-min-x16.png ├── python-min-x32.png └── python-min-x64.png ├── MANIFEST.in ├── requirements-c.txt ├── .gitignore ├── .style.yapf ├── requirements.txt ├── doc ├── AboutPullrequest.md ├── InstallLevedb.md ├── WindowsBinary.md ├── GenesisBlock.md ├── WindowsPackage.md ├── Proxy.md ├── Mining.md ├── Development.md └── AboutPoC.md ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── LICENSE ├── setup.py ├── README.md ├── tests └── test_bip32.py ├── localnode.py └── publicnode.py /bc4py/chain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bc4py/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | aiomonitor>=0.4.2 2 | -------------------------------------------------------------------------------- /image/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kumacoinproject/bc4py/HEAD/image/favicon.ico -------------------------------------------------------------------------------- /image/python-min-x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kumacoinproject/bc4py/HEAD/image/python-min-x16.png -------------------------------------------------------------------------------- /image/python-min-x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kumacoinproject/bc4py/HEAD/image/python-min-x32.png -------------------------------------------------------------------------------- /image/python-min-x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kumacoinproject/bc4py/HEAD/image/python-min-x64.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENCE 3 | include requirements.txt 4 | include requirements-c.txt 5 | include requirements-dev.txt 6 | -------------------------------------------------------------------------------- /requirements-c.txt: -------------------------------------------------------------------------------- 1 | bell-yespower==1.0.3 2 | multi-party-schnorr==0.1.9 3 | bc4py_extension==0.1.7 4 | x11_hash==1.4 5 | shield-x16s-hash==1.0.1 6 | plyvel # leveldb wrapper 7 | -------------------------------------------------------------------------------- /bc4py/user/api/__init__.py: -------------------------------------------------------------------------------- 1 | from bc4py.user.api.server import setup_rest_server 2 | from bc4py.user.api.ep_system import __api_version__ 3 | 4 | __all__ = [ 5 | "setup_rest_server", 6 | "__api_version__", 7 | ] 8 | -------------------------------------------------------------------------------- /bc4py/user/txcreation/__init__.py: -------------------------------------------------------------------------------- 1 | from bc4py.user.txcreation.transfer import * 2 | from bc4py.user.txcreation.mintcoin import * 3 | 4 | 5 | __all__ = [ 6 | "send_from", 7 | "send_many", 8 | "issue_mint_coin", 9 | "change_mint_coin", 10 | ] 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.log 3 | bc4py/user/boot.json 4 | 5 | ### Vim ### 6 | # Swap 7 | [._]*.s[a-v][a-z] 8 | [._]*.sw[a-p] 9 | [._]s[a-rt-v][a-z] 10 | [._]ss[a-gi-z] 11 | [._]sw[a-p] 12 | 13 | # Session 14 | Session.vim 15 | 16 | # formatter 17 | yapf.patch 18 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = google 3 | ARITHMETIC_PRECEDENCE_INDICATION = true 4 | NO_SPACES_AROUND_SELECTED_BINARY_OPERATORS = true 5 | SPLIT_ARGUMENTS_WHEN_COMMA_TERMINATED = true 6 | COLUMN_LIMIT = 115 7 | INDENT_DICTIONARY_VALUE = true 8 | 9 | # create: yapf -d -r -p bc4py > yapf.patch 10 | # affect: patch -p0 < yapf.patch 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp>=3.0.0 # ver3 required 2 | fastapi>=0.38.0 3 | uvicorn # dependency of fastapi 4 | pycryptodomex>=3.9.0 5 | p2p_python==3.0.4 6 | psutil 7 | requests 8 | more_itertools 9 | expiringdict 10 | rx>=3.0.0 # ReactiveX 11 | msgpack<=0.6.2 12 | setuptools-rust 13 | mnemonic 14 | aiosqlite 15 | asyncio-contextmanager 16 | aioitertools 17 | ecdsa>=0.14 18 | -------------------------------------------------------------------------------- /bc4py/bip32/__init__.py: -------------------------------------------------------------------------------- 1 | from bc4py.bip32.base58 import * 2 | from bc4py.bip32.bip32 import * 3 | from bc4py.bip32.utils import * 4 | 5 | ADDR_SIZE = 1 + 20 # bytes: version + identifier 6 | 7 | __all__ = [ 8 | "check_encode", 9 | "check_decode", 10 | "Bip32", 11 | "BIP32_HARDEN", 12 | "parse_bip32_path", 13 | "struct_bip32_path", 14 | "ADDR_SIZE", 15 | "is_address", 16 | "get_address", 17 | "convert_address", 18 | "dummy_address", 19 | ] 20 | -------------------------------------------------------------------------------- /bc4py/database/obj.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | if TYPE_CHECKING: 4 | from bc4py.database.builder import * 5 | 6 | """ 7 | database object 8 | ==== 9 | warning: do not import bc4py.* on this file 10 | """ 11 | 12 | 13 | tables: 'Tables' = None 14 | chain_builder: 'ChainBuilder' = None 15 | tx_builder: 'TransactionBuilder' = None 16 | account_builder: 'AccountBuilder' = None 17 | 18 | 19 | __all__ = [ 20 | "tables", 21 | "chain_builder", 22 | "tx_builder", 23 | "account_builder", 24 | ] 25 | -------------------------------------------------------------------------------- /bc4py/user/network/__init__.py: -------------------------------------------------------------------------------- 1 | from bc4py.user.network.broadcast import BroadcastCmd, broadcast_check 2 | from bc4py.user.network.sendnew import mined_newblock 3 | from bc4py.user.network.directcmd import DirectCmd 4 | from bc4py.user.network.update import update_info_for_generate 5 | from bc4py.user.network.fastsync import sync_chain_loop 6 | 7 | __all__ = [ 8 | "BroadcastCmd", 9 | "broadcast_check", 10 | "mined_newblock", 11 | "DirectCmd", 12 | "update_info_for_generate", 13 | "sync_chain_loop", 14 | ] 15 | -------------------------------------------------------------------------------- /doc/AboutPullrequest.md: -------------------------------------------------------------------------------- 1 | New function pull request 2 | ==== 3 | Write summary 4 | 5 | overview 6 | ---- 7 | * Why you do this change? 8 | * What is problem? 9 | * How this will be solved? 10 | 11 | influence range / target user 12 | ---- 13 | * Benefits and disadvantages of program 14 | * Advantage / disadvantage to general users 15 | * Benefits and disadvantages to other users 16 | 17 | 18 | Technical changes overview 19 | ---- 20 | As reviewed by the reviewer, change over technical viewpoint Description. 21 | * What has been changed? 22 | * How the logic works? 23 | * What kind of query from the DB, what to process and what change? 24 | * Reproducing conditions (When/What/Where do the problem occur.) 25 | * etc.. 26 | -------------------------------------------------------------------------------- /.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 | ## 🐛 Bug Reports 11 | 12 | When reporting a bug, please provide the following information. If this is not a bug report you can just discard this template. 13 | 14 | ### 🌍 Environment 15 | 16 | - Your operating system and version: 17 | - Your python version: 18 | - How did you install python (e.g. apt or pyenv)? Did you use a virtualenv?: 19 | - Your start script (e.g. `python publicnode.py -h`) 20 | 21 | ### 💥 Reproducing 22 | 23 | Please provide a [minimal working example](https://stackoverflow.com/help/mcve). This means both error message and related code. 24 | 25 | Please also write what exact flags are required to reproduce your results. 26 | -------------------------------------------------------------------------------- /bc4py/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.31-alpha' 2 | __chain_version__ = 0 3 | __block_version__ = 1 4 | __message__ = 'This is alpha version - use at your own risk, do not use for merchant applications' 5 | __logo__ = r""" 6 | ____ _ _ _____ _ _ 7 | | _ \| | | | / ____| | (_) 8 | | |_) | | ___ ___| | __ | | | |__ __ _ _ _ __ 9 | | _ <| |/ _ \ / __| |/ / | | | '_ \ / _` | | '_ \ 10 | | |_) | | (_) | (__| < | |____| | | | (_| | | | | | 11 | |____/|_|\___/ \___|_|\_\ \_____|_| |_|\__,_|_|_| |_| 12 | 13 | ______ _____ _ _ 14 | | ____| | __ \ | | | | 15 | | |__ ___ _ __ | |__) | _| |_| |__ ___ _ __ 16 | | __/ _ \| '__| | ___/ | | | __| '_ \ / _ \| '_ \ 17 | | | | (_) | | | | | |_| | |_| | | | (_) | | | | 18 | |_| \___/|_| |_| \__, |\__|_| |_|\___/|_| |_| 19 | __/ | 20 | |___/ 21 | """ 22 | -------------------------------------------------------------------------------- /bc4py/bip32/utils.py: -------------------------------------------------------------------------------- 1 | from bc4py_extension import PyAddress 2 | import hashlib 3 | 4 | 5 | def is_address(ck: PyAddress, hrp, ver): 6 | """check bech32 format and version""" 7 | try: 8 | if ck.hrp != hrp: 9 | return False 10 | if ck.version != ver: 11 | return False 12 | except ValueError: 13 | return False 14 | return True 15 | 16 | 17 | def get_address(pk, hrp, ver) -> PyAddress: 18 | """get address from public key""" 19 | identifier = hashlib.new('ripemd160', hashlib.sha256(pk).digest()).digest() 20 | return PyAddress.from_param(hrp, ver, identifier) 21 | 22 | 23 | def convert_address(ck: PyAddress, hrp, ver) -> PyAddress: 24 | """convert address's version""" 25 | return PyAddress.from_param(hrp, ver, ck.identifier()) 26 | 27 | 28 | def dummy_address(dummy_identifier) -> PyAddress: 29 | assert len(dummy_identifier) == 20 30 | return PyAddress.from_param('dummy', 0, dummy_identifier) 31 | 32 | 33 | __all__ = [ 34 | "is_address", 35 | "get_address", 36 | "convert_address", 37 | "dummy_address", 38 | ] 39 | -------------------------------------------------------------------------------- /doc/InstallLevedb.md: -------------------------------------------------------------------------------- 1 | Install levelDB 2 | ==== 3 | You can use [plyvel](https://github.com/wbolster/plyvel for Linux, Winodws and arm. 4 | 5 | For windows 6 | ---- 7 | * Do yourself (recommend) [How to install plyvel on wondows](https://gist.github.com/namuyan/1a8aef3482fa17c6b206ff028efc9807) 8 | * Use binary at your own risk [1.1.0.build v0-win](https://github.com/ppolxda/plyvel/releases/tag/1.1.0.build-v0-win) 9 | 10 | For linux 11 | ---- 12 | `pip3 install --user plyvel` 13 | 14 | For ARMs 15 | ---- 16 | [leveldb-1.20-build](https://tangerina.jp/blog/leveldb-1.20-build/) 17 | 18 | ```text 19 | # compile 20 | wget https://github.com/google/leveldb/archive/v1.20.tar.gz 21 | zcat v1.20.tar.gz | tar xf - 22 | cd leveldb-1.20 23 | make 24 | make check 25 | # copy source 26 | sudo cp -r include/leveldb /usr/local/include/ 27 | sudo install -o root -m 644 -p out-shared/libleveldb.so.1.20 /usr/local/lib/ 28 | sudo cp -d out-shared/libleveldb.so out-shared/libleveldb.so.1 /usr/local/lib/ 29 | sudo install -o root -m 644 -p out-static/lib* /usr/local/lib/ 30 | # affect changes 31 | sudo ldconfig 32 | pip install plyvel 33 | ``` 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 namuyang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/user/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | import os 6 | 7 | try: 8 | with open('README.md') as f: 9 | readme = f.read() 10 | except IOError: 11 | readme = '' 12 | 13 | 14 | # version 15 | here = os.path.dirname(os.path.abspath(__file__)) 16 | init_path = os.path.join(here, 'bc4py', '__init__.py') 17 | version = next((line.split('=')[1].strip().replace("'", '') 18 | for line in open(init_path) 19 | if line.startswith('__version__ = ')), 20 | '0.0.dev0') 21 | 22 | # requirements 23 | with open(os.path.join(here, 'requirements.txt')) as fp: 24 | install_requires = fp.read().splitlines() 25 | with open(os.path.join(here, 'requirements-c.txt')) as fp: 26 | install_requires += fp.read().splitlines() 27 | 28 | setup( 29 | name="bc4py", 30 | version=version.replace('-', '').replace('alpha', 'a').replace('beta', 'b'), 31 | url='https://github.com/kumacoinproject/bc4py', 32 | author='namuyan', 33 | description='Simple blockchain library for python3.', 34 | long_description=readme, 35 | long_description_content_type='text/markdown', 36 | packages=find_packages(), 37 | install_requires=install_requires, 38 | include_package_data=True, 39 | license="MIT Licence", 40 | classifiers=[ 41 | 'Programming Language :: Python :: 3.6', 42 | 'License :: OSI Approved :: MIT License', 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /doc/WindowsBinary.md: -------------------------------------------------------------------------------- 1 | how to build windows binary 2 | ==== 3 | **this is old, use []() instead!** 4 | use [Nuitka](https://nuitka.net/) and [MSYS2 x86_64](https://www.msys2.org/) 5 | 6 | build by 7 | ---- 8 | * Windows10 64bit 9 | * Python 3.6.7 :: Anaconda custom (64-bit) 10 | * Nuitka 0.6.5 11 | * gcc (Rev2, Built by MSYS2 project) 8.3.0 12 | 13 | replace modified libs 14 | ---- 15 | Changes is simple, good to edit when new commits come. 16 | ```bash 17 | pip install -U git+https://github.com/namuyan/fastapi 18 | pip install -U git+https://github.com/namuyan/uvicorn 19 | ``` 20 | 21 | command 22 | ---- 23 | ```bash 24 | python -m nuitka --mingw64 -j 2 --show-progress --show-scons --recurse-all --standalone --windows-icon=favicon.ico \ 25 | --nofollow-import-to=cryptography \ 26 | --nofollow-import-to=numpy \ 27 | --nofollow-import-to=gevent \ 28 | --nofollow-import-to=matplotlib \ 29 | --nofollow-import-to=IPython \ 30 | --nofollow-import-to=Cython \ 31 | --nofollow-import-to=setuptools \ 32 | --nofollow-import-to=distutils \ 33 | publicnode.py 34 | rm -r publicnode.build 35 | ``` 36 | 37 | fix 38 | ---- 39 | * `OSError: Cannot load native module 'Cryptodome.Hash._RIPEMD160': Trying '_RIPEMD160.cp36-win_amd64.pyd'` 40 | copy *site-packages/Cryptodome/Hash/_RIPEMD160.cp36-win_amd64.pyd* to *Cryptodome/Hash/_RIPEMD160.pyd* 41 | 42 | * `ctypes.WinError` on `\lib\site-packages\nuitka\utils\WindowsResources.py` 43 | [just add two lines](https://github.com/Nuitka/Nuitka/issues/468#issuecomment-532633902) 44 | -------------------------------------------------------------------------------- /bc4py/exit.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import V, P, stream 2 | from bc4py.database import obj 3 | from logging import getLogger 4 | import asyncio 5 | 6 | 7 | log = getLogger('bc4py') 8 | loop = asyncio.get_event_loop() 9 | 10 | 11 | async def system_safe_exit(): 12 | """system safe exit method""" 13 | log.info("start system stop process") 14 | try: 15 | P.F_STOP = True 16 | 17 | # reactive stream close 18 | stream.dispose() 19 | 20 | await obj.chain_builder.close() 21 | 22 | from bc4py.user.generate import close_generate 23 | close_generate() 24 | 25 | if V.API_OBJ: 26 | V.API_OBJ.handle_exit(None, None) 27 | await V.API_OBJ.shutdown() 28 | 29 | if V.P2P_OBJ: 30 | V.P2P_OBJ.close() 31 | 32 | log.info("wait all tasks for max 60s..") 33 | all_task = asyncio.Task.all_tasks() 34 | all_task.remove(asyncio.Task.current_task()) 35 | await asyncio.wait(all_task, timeout=60.0) 36 | log.info("stop waiting tasks and close after 1s") 37 | loop.call_later(1.0, loop.stop) 38 | except Exception: 39 | log.warning("failed system stop process", exc_info=True) 40 | else: 41 | log.info("success system stop process") 42 | 43 | 44 | def blocking_run(): 45 | """block and exit with system_safe_exit""" 46 | try: 47 | loop.run_forever() 48 | except KeyboardInterrupt: 49 | log.info("stop blocking run!") 50 | loop.run_until_complete(system_safe_exit()) 51 | loop.close() 52 | 53 | 54 | __all__ = [ 55 | "system_safe_exit", 56 | "blocking_run", 57 | ] 58 | -------------------------------------------------------------------------------- /doc/GenesisBlock.md: -------------------------------------------------------------------------------- 1 | Create genesis block 2 | ==================== 3 | Open interactive console. 4 | 5 | ```python 6 | from bc4py.config import C 7 | from bc4py.utils import set_database_path, set_blockchain_params 8 | from bc4py.chain.genesisblock import create_genesis_block 9 | from bc4py.database.create import check_account_db 10 | from bc4py.user.boot import create_boot_file, import_keystone 11 | import asyncio 12 | 13 | loop = asyncio.get_event_loop() 14 | 15 | # setup database path and initialize database 16 | set_database_path() 17 | import_keystone(passphrase='hello python') 18 | loop.run_until_complete(check_account_db()) 19 | 20 | # consensus 21 | consensus = { 22 | C.BLOCK_COIN_POS: 6, # Coin staking 23 | C.BLOCK_CAP_POS: 6, # Capacity staking 24 | C.BLOCK_FLK_POS: 7, # fund-lock staking 25 | C.BLOCK_YES_POW: 27, # Yespower mining 26 | C.BLOCK_X11_POW: 27, # X11 mining 27 | C.BLOCK_X16S_POW: 27} # X16S mining 28 | 29 | # create first block 30 | genesis_block, genesis_params = create_genesis_block( 31 | hrp='test', 32 | mining_supply=100000000 * 100000000, # one hundred million mining supply 33 | block_span=120, # block time 34 | digit_number=8, # base currency digit 35 | minimum_price=100, # minimum gas price 36 | consensus=consensus, # mining consensus, key is algo value is ratio 37 | genesis_msg="for test params", # genesis message 38 | premine=None) # premine [(address, coin_id, amount), ...] 39 | 40 | # check genesis block 41 | set_blockchain_params(genesis_block, genesis_params) 42 | print(genesis_block.getinfo()) 43 | create_boot_file(genesis_block, genesis_params) 44 | ``` 45 | -------------------------------------------------------------------------------- /bc4py/gittool.py: -------------------------------------------------------------------------------- 1 | from os.path import join 2 | import os.path 3 | import hashlib 4 | 5 | 6 | def get_original_branch(): 7 | try: 8 | str_hash = read(join('.git', 'ORIG_HEAD')) 9 | for line in read(join('.git', 'packed-refs')).split("\n"): 10 | if line.startswith(str_hash): 11 | _, branch_path = line.split(" ") 12 | return branch_path.split("/")[-1] 13 | except Exception: 14 | return None 15 | 16 | 17 | def get_current_branch(): 18 | try: 19 | branch = read(join('.git', 'HEAD')) 20 | branch = branch.lstrip().rstrip() 21 | return branch.split('/')[-1] 22 | except Exception: 23 | return None 24 | 25 | 26 | def read(path): 27 | with open(path, mode='r', errors='ignore') as fp: 28 | return fp.read().lstrip().rstrip() 29 | 30 | 31 | def calc_python_source_hash(folder=None): 32 | """calculate sha1 of bc4py source""" 33 | h = hashlib.sha1() 34 | 35 | def calc(p): 36 | for path in sorted(os.listdir(p), key=lambda x: str(x)): 37 | full_path = join(p, path) 38 | if os.path.isdir(full_path): 39 | calc(full_path) 40 | elif full_path.endswith('.py'): 41 | with open(full_path, mode='br') as fp: 42 | h.update(fp.read()) 43 | else: 44 | pass 45 | 46 | try: 47 | if folder is None: 48 | folder = os.path.split(os.path.abspath(__file__))[0] 49 | calc(folder) 50 | return h.hexdigest() 51 | except Exception: 52 | return None 53 | 54 | 55 | __all__ = [ 56 | "get_original_branch", 57 | "get_current_branch", 58 | "calc_python_source_hash", 59 | ] 60 | -------------------------------------------------------------------------------- /doc/WindowsPackage.md: -------------------------------------------------------------------------------- 1 | how to setup windows package 2 | ==== 3 | Use embedded python because of sustainable work instead of Nuitka3. 4 | 5 | prepare 6 | ---- 7 | * [python-3.7.6-embed-amd64.zip](https://www.python.org/ftp/python/3.7.6/python-3.7.6-embed-amd64.zip) 8 | * [MSYS2 x86_64](https://www.msys2.org/) 9 | * requirements-cp37 10 | 11 | setup 12 | ---- 13 | open MSYS2 commandline 14 | 1. `cd python-3.X.X-embed-amd64` 15 | 2. `sed -i -e "s/\# import site/import site/" python37._pth` 16 | 3. install pip 17 | * `wget "https://bootstrap.pypa.io/get-pip.py" -O "get-pip.py"` 18 | * `./python.exe get-pip.py && rm get-pip.py` 19 | 4. setup bc4py 20 | * `git clone -b develop https://github.com/namuyan/bc4py` 21 | * `./python.exe -m pip install -r bc4py/requirements.txt` 22 | * `./python.exe -m pip install requirements-cp37/* && rm -r requirements-cp37` 23 | * `mv bc4py bc4py_old && mv "bc4py_old/bc4py" bc4py && mv "bc4py_old/publicnode.py" . && rm -rf bc4py_old` 24 | 5. check execute `./python.exe publicnode.py -h` 25 | 6. clear cache `find * \! -type f | grep "__pycache__" | xargs -d \\n rm -r` 26 | 27 | build *requirements-cp37* by yourself 28 | ---- 29 | embedded python don't have compile function, so you install another normal python. 30 | check compile required libs [requirements-c.txt](https://github.com/kumacoinproject/bc4py/blob/develop/requirements-c.txt) 31 | 32 | 1. `mkdir requirements-cp37 && cd requirements-cp37` 33 | 2. `python.exe -m pip wheel -r ../bc4py/requirements-c.txt` 34 | 35 | trouble shooting 36 | ---- 37 | * plyvel: https://github.com/ppolxda/plyvel/releases/tag/1.1.0.build-v0-win 38 | * -lmsvcr140 not found: https://teratail.com/questions/238135 39 | * fastecdsa cause coredump => https://pypi.org/project/fastecdsa-any/ 40 | -------------------------------------------------------------------------------- /doc/Proxy.md: -------------------------------------------------------------------------------- 1 | http/ws proxy introduction 2 | ==== 3 | We are often requested "always-on SSL" (HTTPS) compliant. 4 | A browser display a warning when we access HTTP sites. 5 | However, we need root privilege to user SSL port(443), is not recommended. 6 | That's why I wrote a method to setup local proxy. [JP](https://gist.github.com/namuyan/a94d2363cc363a5d8393c8716d8f5143) 7 | 8 | 9 | Init env 10 | ---- 11 | 1. `sudo apt-get install nodejs` 12 | 2. ```sudo ln -s `which nodejs` /usr/bin/node``` 13 | 3. check `nodejs -v` 14 | 4. `sudo apt-get install npm` 15 | 5. `mkdir proxy && cd proxy && mkdir ssl` 16 | 6. `npm init` 17 | 18 | edit package.json 19 | ---- 20 | Add `fs` and `http-proxy` to dependencies. 21 | ```json 22 | { 23 | "name": "proxy", 24 | "version": "1.0.0", 25 | "description": "Proxy server.", 26 | "main": "index.js", 27 | "scripts": { 28 | "start": "node index.js &", 29 | "stop": "./stop.sh" 30 | }, 31 | "author": "Robert Lie", 32 | "license": "ISC", 33 | "dependencies": { 34 | "fs": "0.0.1-security", 35 | "http-proxy": "^1.16.2" 36 | } 37 | } 38 | ``` 39 | 40 | start/stop proxy 41 | ---- 42 | 1. start by `sudo npm start` 43 | 2. stop by `sudo npm stop` 44 | 45 | index.js 46 | ---- 47 | start by index.js, very simple proxy server. 48 | ```javascript 49 | var httpProxy = require('http-proxy'), 50 | fs = require('fs'); 51 | 52 | var ssl = { 53 | key: fs.readFileSync('./ssl/priv.pem', 'utf8'), 54 | cert: fs.readFileSync('./ssl/cert.pem', 'utf8') 55 | }; 56 | 57 | var server = httpProxy.createProxyServer({ 58 | target: {host: 'localhost', port: 3000}, 59 | ssl: ssl, 60 | ws: true, 61 | xfwd: true 62 | }); 63 | 64 | server.on('error', function(err, req, res) { 65 | res.end(); 66 | }); 67 | 68 | server.listen(443); 69 | ``` 70 | -------------------------------------------------------------------------------- /bc4py/user/network/update.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C 2 | from bc4py.database import obj 3 | from bc4py.user.generate import * 4 | from logging import getLogger 5 | from time import time 6 | import asyncio 7 | 8 | 9 | loop = asyncio.get_event_loop() 10 | log = getLogger('bc4py') 11 | update_count = 0 12 | block_lock = asyncio.Lock() 13 | unspent_lock = asyncio.Lock() 14 | 15 | 16 | def update_info_for_generate(u_block=True, u_unspent=True): 17 | """update generating status, used only on network fnc""" 18 | 19 | async def updates(num): 20 | try: 21 | consensus = tuple(t.consensus for t in generating_threads) 22 | info = '' 23 | if u_block and not block_lock.locked(): 24 | info += await update_block_info() 25 | if u_unspent and (C.BLOCK_COIN_POS in consensus) and not unspent_lock.locked(): 26 | info += await update_unspent_info() 27 | if info: 28 | log.debug("{} update finish{}".format(num, info)) 29 | except Exception: 30 | log.debug("update_info_for_generate exception", exc_info=True) 31 | 32 | global update_count 33 | asyncio.ensure_future(updates(update_count)) 34 | update_count += 1 35 | 36 | 37 | async def update_block_info(): 38 | async with block_lock: 39 | while obj.chain_builder.best_block is None: 40 | await asyncio.sleep(0.2) 41 | update_previous_block(obj.chain_builder.best_block) 42 | return ', height={}'.format(obj.chain_builder.best_block.height + 1) 43 | 44 | 45 | async def update_unspent_info(): 46 | async with unspent_lock: 47 | s = time() 48 | all_num, next_num = await update_unspents_txs() 49 | return ', unspents={}/{} {}mS'.format(next_num, all_num, int((time() - s) * 1000)) 50 | 51 | 52 | __all__ = [ 53 | "update_info_for_generate", 54 | ] 55 | -------------------------------------------------------------------------------- /doc/Mining.md: -------------------------------------------------------------------------------- 1 | About Mining 2 | ==== 3 | We provide mining interface 4 | [getwork](https://en.bitcoin.it/wiki/Getwork), 5 | [getblocktemplete](https://en.bitcoin.it/wiki/Getblocktemplate) and 6 | [stratum](https://github.com/namuyan/bc4py-stratum-pool). 7 | You can mine by cpuminer, ccminer, sgminer, cgminer and stratum pool with no modification. 8 | 9 | important 10 | ---- 11 | * Miner notify server hash algorithm by `password` integer, please select by [config.py](/bc4py/config.py). 12 | * We mimic block header structure of Bitcoin, so you can use general mining tools with no modification. 13 | But it depends on program, because coinbase transaction is differ from Bitcoin's. 14 | * Please at your own risk about using a mining tool. 15 | 16 | yespower 17 | ---- 18 | Yespower1.0 is anti GPU/ASIC hash algorithm and next generation of yescrypt. 19 | * [cpuminer-opt](https://github.com/bellflower2015/cpuminer-opt) 20 | * [Binary](https://github.com/bellflower2015/cpuminer-opt/releases) 21 | 22 | ```commandline 23 | cpuminer-sse2 -a yespower -o http://127.0.0.1:3000 -u USERNAME -p 5 --no-getwork --no-longpoll --no-stratum 24 | ``` 25 | 26 | X11 27 | ---- 28 | You can mine by ASIC. 29 | please look [PinIdea X11 USB ASIC Miner DU-1 Coming Soon](https://cryptomining-blog.com/tag/x11-miner-du-1/). 30 | * [sgminer-nicehash for GPU](https://github.com/nicehash/sgminer) 31 | * [X11-Miner binaries](https://github.com/stellawxo/X11-Miner) 32 | 33 | ```commandline 34 | cgminer --x11 -o http://127.0.0.1:3000 -u user -p 6 --dr1-clk 300 --dr1-fan LV1 -S //./COM5 --du1 35 | ``` 36 | 37 | ```commandline 38 | sgminer -k x11 -o http://127.0.0.1:3000 -u user -p 6 39 | ``` 40 | 41 | X16S 42 | ---- 43 | * [avermore-miner](https://github.com/brian112358/avermore-miner) 44 | * [sgminer-kl](https://github.com/KL0nLutiy/sgminer-kl) 45 | ```commandline 46 | sgminer -k x16s -o http://127.0.0.1:3000 -u user -p 9 47 | ``` 48 | -------------------------------------------------------------------------------- /doc/Development.md: -------------------------------------------------------------------------------- 1 | Development Guideline 2 | ==== 3 | We write this document to declare how to develop this repository. 4 | We and contributors will follow this guideline when merging changes and adding new functions. 5 | We think specifications should be backed by technical curiosity. 6 | 7 | Development process (core contributors) 8 | ---- 9 | If we develop required functions, commit to `develop` branch, and push to `master` after a section complete. 10 | or if we develop experimental or can be abolished functions, 11 | we checkout new branch and commit to `develop` after complete. 12 | 13 | Development process (external contributors) 14 | ---- 15 | 1. fork from `develop` branch. 16 | 2. commit to your repository. 17 | 3. marge pull request to `develop`, please wait reply from core contributors. 18 | 19 | Check before pull request 20 | ---- 21 | * Functions, do you confirm the function is easy to understand? enough documents? 22 | * Migrations, do you confirm the migration has backward compatibility? 23 | * Tools, do you confirm the tool is backed by realistic scenarios? check dependency? 24 | 25 | Check before issue 26 | ---- 27 | * Duplication, already raised same issues? 28 | * Documents, already written in documents? 29 | * Question, easily solved if you search by Google or Github? 30 | 31 | Coding style 32 | ---- 33 | * please use for formatter [yapf](https://github.com/google/yapf) 34 | * create patch `yapf -d -r -p bc4py > yapf.patch` 35 | * affect patch `patch -p0 < yapf.patch` 36 | 37 | In Japanese 38 | ---- 39 | 日本人なせいか英語で書いていたらよくわからなくなったので要約。 40 | * コア開発者は、主に`develop`にCommit、採用するか不明な機能はBranchきって別にCommitする。 41 | * コア開発者は、`develop`での開発が一段落したらSubVer上げて`master`にMargeする。 42 | * コントリビュータは、`develop`よりForkして変更点を加えた後に`develop`へプルリクエストする。 43 | * プルリクエストへのMarge可否がコア開発者より返信されます。後方互換性に注意して下さい。 44 | * コントリビュータは、プルリクエスト/イシュー作成前にご確認よろしくお願いします。 45 | * Alpha版ではこの規則に従いません。 46 | * Coding styleをフォーマッターで統一しましょう。 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bc4py 2 | ============================= 3 | [bc4py](https://github.com/kumacoinproject/bc4py)\(blockchain-for-python) enables you to 4 | create blockchain application by Python3. 5 | 6 | Function 7 | ---- 8 | * UTXO base 9 | * PoW, PoS and PoC multi-consensus 10 | * Minting colored coin 11 | 12 | Requirement 13 | ---- 14 | * Windows/Linux 15 | * **Python3.6+** 16 | * **Rust nightly** 17 | * [p2p-python](https://github.com/namuyan/p2p-python) 18 | * LevelDB 19 | * hash function 20 | * [yespower-python](https://github.com/namuyan/yespower-python) For CPU 21 | * [x16s-hash](https://pypi.org/project/shield-x16s-hash/) for GPU 22 | * [x11_hash](https://pypi.org/project/x11_hash/) For ASIC 23 | * Python extension [bc4py-extension](https://github.com/namuyan/bc4py_extension) 24 | * plotting tool [bc4py-plotter](https://github.com/namuyan/bc4py_plotter) 25 | 26 | Install 27 | ---- 28 | ```commandline 29 | cd ~ 30 | git clone https://github.com/kumacoinproject/bc4py 31 | rm -r doc tests 32 | mv bc4py blockchain-py 33 | cd blockchain-py 34 | pip3 install --user -r requirements.txt 35 | pip3 install --user -r requirements-c.txt 36 | wget http://example.com/boot.json 37 | ``` 38 | 39 | Start node 40 | ---- 41 | * `python3 localnode.py` Node working on local env, for debug. 42 | * `python3 publicnode.py` Node with mining/staking. 43 | 44 | Documents 45 | ---- 46 | * [Create genesis block](doc/GenesisBlock.md) 47 | * [How to mining](doc/Mining.md) 48 | * [testnet API document](https://testnet.kumacoin.dev/docs) 49 | * [About development](doc/Development.md) 50 | * [About new offer about program](doc/AboutPullrequest.md) 51 | * [HTTPS proxy introduction](doc/Proxy.md) 52 | * [Proof of capacity](doc/AboutPoC.md) 53 | * [Install LevelDB](doc/InstallLevedb.md) 54 | * [how to setup windows package](doc/WindowsPackage.md) 55 | 56 | Licence 57 | ---- 58 | MIT 59 | 60 | Author 61 | ---- 62 | [@namuyan_mine](http://twitter.com/namuyan_mine/) 63 | -------------------------------------------------------------------------------- /bc4py/chain/checking/checkblock.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, V, BlockChainError 2 | from bc4py.chain.block import Block 3 | from bc4py.chain.difficulty import get_bits_by_hash 4 | from logging import getLogger 5 | from time import time 6 | 7 | log = getLogger('bc4py') 8 | 9 | 10 | def check_block(block: Block): 11 | # 挿入前にBlockの正当性チェック 12 | if len(block.txs) == 0: 13 | raise BlockChainError('Block don\'t have any txs') 14 | elif block.size > C.SIZE_BLOCK_LIMIT: 15 | raise BlockChainError('Block size is too large [{}b>{}b]'.format(block.size, C.SIZE_BLOCK_LIMIT)) 16 | bits = get_bits_by_hash(previous_hash=block.previous_hash, consensus=block.flag)[0] 17 | if block.bits != bits: 18 | raise BlockChainError('Block bits differ from calc. [{}!={}]'.format(block.bits, bits)) 19 | log.debug("check block success {}".format(block)) 20 | 21 | 22 | def check_block_time(block: Block, fix_delay): 23 | # 新規受け入れ時のみ検査 24 | delay = int(time() - fix_delay) - block.create_time 25 | create_time = block.create_time - V.BLOCK_GENESIS_TIME 26 | if C.ACCEPT_MARGIN_TIME < abs(block.time - create_time): 27 | raise BlockChainError('Block time is out of range [{}<{}-{}={},{}]'.format( 28 | C.ACCEPT_MARGIN_TIME, block.time, create_time, block.time - create_time, delay)) 29 | if C.ACCEPT_MARGIN_TIME < delay: 30 | log.warning("Long delay, for check new block. [{}<{}]".format(C.ACCEPT_MARGIN_TIME, delay)) 31 | # check time warp 32 | # if block.flag != C.BLOCK_GENESIS: 33 | # previous_block = chain_builder.get_block(blockhash=block.previous_hash) 34 | # if previous_block is None: 35 | # raise BlockChainError('cannot find previous block height={}'.format(block.height)) 36 | # if previous_block.time >= block.time: 37 | # raise BlockChainError('block time warp not allowed previous={} new={}' 38 | # .format(previous_block.time, block.time)) 39 | log.debug("check block time success {}".format(block)) 40 | -------------------------------------------------------------------------------- /bc4py/chain/msgpack.py: -------------------------------------------------------------------------------- 1 | from bc4py.chain.block import Block 2 | from bc4py.chain.tx import TX 3 | import msgpack 4 | 5 | 6 | def default_hook(obj): 7 | if isinstance(obj, Block): 8 | return { 9 | '_bc4py_class_': 'Block', 10 | 'binary': obj.b, 11 | 'height': obj.height, 12 | 'flag': obj.flag, 13 | 'txs': [default_hook(tx) for tx in obj.txs] 14 | } 15 | if isinstance(obj, TX): 16 | return { 17 | '_bc4py_class_': 'TX', 18 | 'binary': obj.b, 19 | 'height': obj.height, 20 | 'signature': obj.signature, 21 | 'R': obj.R 22 | } 23 | return obj 24 | 25 | 26 | def object_hook(dct): 27 | if isinstance(dct, dict) and '_bc4py_class_' in dct: 28 | if dct['_bc4py_class_'] == 'Block': 29 | block = Block.from_binary(binary=dct['binary']) 30 | block.height = dct['height'] 31 | block.flag = dct['flag'] 32 | block.txs.extend(object_hook(tx) for tx in dct['txs']) 33 | for tx in block.txs: 34 | tx.height = block.height 35 | return block 36 | elif dct['_bc4py_class_'] == 'TX': 37 | tx = TX.from_binary(binary=dct['binary']) 38 | tx.height = dct['height'] 39 | tx.signature.extend(tuple(sig) for sig in dct['signature']) 40 | tx.R = dct['R'] 41 | return tx 42 | else: 43 | raise Exception('Not found class name "{}"'.format(dct['_bc4py_class_'])) 44 | else: 45 | return dct 46 | 47 | 48 | def dump(obj, fp, **kwargs): 49 | msgpack.pack(obj, fp, use_bin_type=True, default=default_hook, **kwargs) 50 | 51 | 52 | def dumps(obj, **kwargs): 53 | return msgpack.packb(obj, use_bin_type=True, default=default_hook, **kwargs) 54 | 55 | 56 | def load(fp): 57 | return msgpack.unpack(fp, object_hook=object_hook, raw=True, encoding='utf8') 58 | 59 | 60 | def loads(b): 61 | return msgpack.unpackb(b, object_hook=object_hook, raw=True, encoding='utf8') 62 | 63 | 64 | def stream_unpacker(fp): 65 | return msgpack.Unpacker(fp, object_hook=object_hook, raw=True, encoding='utf8') 66 | 67 | 68 | __all__ = [ 69 | "default_hook", 70 | "object_hook", 71 | "dump", 72 | "dumps", 73 | "load", 74 | "loads", 75 | "stream_unpacker", 76 | ] 77 | -------------------------------------------------------------------------------- /bc4py/for_debug/__init__.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import P, stream 2 | from logging import * 3 | from time import time 4 | import socket 5 | import asyncio 6 | import os 7 | 8 | loop = asyncio.get_event_loop() 9 | log = getLogger('bc4py') 10 | 11 | 12 | def f_already_bind(port): 13 | """ check port already bind """ 14 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 15 | r = False 16 | try: 17 | s.bind(("127.0.0.1", port)) 18 | except socket.error: 19 | print("Port is already in use") 20 | r = True 21 | s.close() 22 | return r 23 | 24 | 25 | def set_logger(level=INFO, path=None, f_remove=False): 26 | """ 27 | Setup logger 28 | :param level: logging level. 29 | :param path: output log file path 30 | :param f_remove: remove log file when restart. 31 | """ 32 | logger = getLogger() 33 | for sh in logger.handlers: 34 | logger.removeHandler(sh) 35 | logger.propagate = False 36 | logger.setLevel(DEBUG) 37 | formatter = Formatter('[%(asctime)-23s %(levelname)-4s] %(message)s') 38 | if path: 39 | # recode if user sets path 40 | if f_remove and os.path.exists(path): 41 | os.remove(path) 42 | sh = FileHandler(path) 43 | sh.setLevel(level) 44 | sh.setFormatter(formatter) 45 | logger.addHandler(sh) 46 | sh = StreamHandler() 47 | sh.setLevel(level) 48 | sh.setFormatter(formatter) 49 | logger.addHandler(sh) 50 | log.info("\n\n\n\n\n\n") 51 | log.info("Start program") 52 | 53 | 54 | def stream_printer(): 55 | log.debug("register stream print") 56 | stream.subscribe(on_next=log.debug, on_error=log.error) 57 | 58 | 59 | async def slow_event_loop_detector(span=1.0, limit=0.1): 60 | """find event loop delay and detect blocking""" 61 | log.info(f"setup slow_event_loop_detector limit={limit}s") 62 | while not P.F_STOP: 63 | try: 64 | s = time() 65 | await asyncio.sleep(0.0) 66 | if limit < time() - s: 67 | log.debug(f"slow event loop {int((time()-s)*1000)}mS!") 68 | await asyncio.sleep(span) 69 | except Exception: 70 | log.error("slow_event_loop_detector exception", exc_info=True) 71 | 72 | 73 | __all__ = [ 74 | "f_already_bind", 75 | "set_logger", 76 | "stream_printer", 77 | "slow_event_loop_detector", 78 | ] 79 | -------------------------------------------------------------------------------- /bc4py/user/api/ep_others.py: -------------------------------------------------------------------------------- 1 | from bc4py import __chain_version__ 2 | from bc4py.config import V 3 | from bc4py.chain import msgpack 4 | from bc4py.database import obj 5 | from bc4py.user.api.utils import error_response 6 | from logging import getLogger 7 | from time import time 8 | import asyncio 9 | import gzip 10 | import os 11 | 12 | loop = asyncio.get_event_loop() 13 | log = getLogger('bc4py') 14 | 15 | 16 | async def create_bootstrap(): 17 | """ 18 | This end-point create bootstrap.tar.gz file. 19 | * About 20 | * It will take some minutes. 21 | """ 22 | try: 23 | boot_path = os.path.join(V.DB_HOME_DIR, 'bootstrap-ver{}.dat.gz'.format(__chain_version__)) 24 | if os.path.exists(boot_path): 25 | log.warning("remove old bootstrap.dat.gz file") 26 | os.remove(boot_path) 27 | if obj.chain_builder.root_block is None: 28 | Exception('root block is None?') 29 | 30 | s = time() 31 | block = None 32 | size = 0.0 # MB 33 | stop_height = obj.chain_builder.root_block.height 34 | log.info("start create bootstrap.dat.gz data to {}".format(stop_height)) 35 | with gzip.open(boot_path, mode='wb') as fp: 36 | for height, blockhash in obj.tables.read_block_hash_iter(start_height=1): 37 | if stop_height <= height: 38 | break 39 | block = obj.chain_builder.get_block(blockhash=blockhash) 40 | if block is None: 41 | break 42 | await loop.run_in_executor( 43 | None, fp.write, msgpack.dumps((block, block.work_hash, block.bias))) 44 | size += block.total_size / 1000000 45 | if block.height % 1000 == 0: 46 | log.info("create bootstrap.dat.gz height={} size={}mb {}s passed" 47 | .format(block.height, round(size, 2), round(time() - s))) 48 | 49 | log.info("create new bootstrap.dat.gz finished, last={} size={}gb time={}m" 50 | .format(block, round(size/1000, 3), (time() - s) // 60)) 51 | return { 52 | "height": stop_height, 53 | "total_size": size, 54 | "start_time": int(s), 55 | "finish_time": int(time()), 56 | } 57 | except Exception: 58 | return error_response() 59 | 60 | 61 | __all__ = [ 62 | "create_bootstrap", 63 | ] 64 | -------------------------------------------------------------------------------- /bc4py/user/api/jsonrpc/__init__.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from fastapi import Depends 3 | from fastapi.security import HTTPBasic, HTTPBasicCredentials 4 | from .account import * 5 | from .mining import * 6 | from .others import * 7 | from logging import getLogger 8 | import traceback 9 | 10 | log = getLogger('bc4py') 11 | security = HTTPBasic() 12 | 13 | # about "coinbasetxn" 14 | # https://bitcoin.stackexchange.com/questions/13438/difference-between-coinbaseaux-flags-vs-coinbasetxn-data 15 | # https://github.com/bitcoin/bips/blob/master/bip-0022.mediawiki 16 | 17 | """ 18 | JSON-RPC server 19 | It's designed for Yiimp pool program. 20 | """ 21 | 22 | 23 | class JsonRpcFormat(BaseModel): 24 | method: str 25 | params: list = None 26 | id: str = None 27 | 28 | 29 | async def json_rpc(data: JsonRpcFormat, credentials: HTTPBasicCredentials = Depends(security)): 30 | """ 31 | JSON-RPC for Stratum-pool-mining 32 | 33 | BasicAuth params 34 | * **user**: no meaning 35 | * **password**: mining consensus number from config.py 36 | """ 37 | user, password = credentials.username, credentials.password 38 | 39 | try: 40 | # post format => {"id": id, "method": method, "params": [params]} 41 | params = data.params or list() # sgminer don't have 42 | log.debug("RpcRequest: method={} params={}".format(data.method, params)) 43 | if not isinstance(params, list): 44 | return res_failed("Params is list. not {}".format(type(params)), data.id) 45 | 46 | # find method function and throw task 47 | fnc = globals().get(data.method) 48 | if fnc is None: 49 | return res_failed("not found method {}".format(data.method), data.id) 50 | result = await fnc(*params, user=user, password=password) 51 | log.debug("RpcResponse: {}".format(result)) 52 | return res_success(result, data.id) 53 | except Exception as e: 54 | tb = traceback.format_exc() 55 | log.debug("JsonRpcError:", exc_info=True) 56 | return res_failed(str(e), data.id) 57 | 58 | 59 | def res_failed(error, uuid): 60 | return { 61 | 'id': uuid, 62 | 'result': None, 63 | 'error': error, 64 | } 65 | 66 | 67 | def res_success(result, uuid): 68 | return { 69 | 'id': uuid, 70 | 'result': result, 71 | 'error': None, 72 | } 73 | 74 | 75 | __all__ = [ 76 | "json_rpc", 77 | ] 78 | -------------------------------------------------------------------------------- /tests/test_bip32.py: -------------------------------------------------------------------------------- 1 | from bc4py.bip32 import * 2 | from mnemonic import Mnemonic 3 | import unittest 4 | 5 | 6 | WORDS = 'news clever spot drama infant detail sword cover color throw foot primary when ' \ 7 | 'slender rhythm clog autumn ecology enough bronze math you modify excuse' 8 | ROOT_SECRET = 'xprv9s21ZrQH143K3EGRfjQYhZ6fA3HPPiw6rxopHKXfWTrB66evM4fDRiUScJy5RCCGz98' \ 9 | 'nBaCCtwpwFCTDiFG5tx3mdnyyL1MbHmQQ19BWemo' 10 | ROOT_PUBLIC = 'xpub661MyMwAqRbcFiLtmkwZ4h3Pi57soBexEBjR5hwH4oP9xtz4tbyTyWnvTb44oGpDbVa' \ 11 | 'cdJcga8g26sn7KBYLaerJ54LHqki34DwDq42XRfL' 12 | LANGUAGE = 'english' 13 | HARD_PATH = "m/44'/5'/0'/0/3" 14 | SOFT_PATH = "m/32/12/0/0/3" 15 | 16 | 17 | def test_parse_and_struct(): 18 | """test bip32 parse and struct path""" 19 | parsed_path = parse_bip32_path(HARD_PATH) 20 | struct_path = struct_bip32_path(parsed_path) 21 | assert HARD_PATH == struct_path 22 | 23 | 24 | def test_derive_from_secret_key(): 25 | """test derived from private key""" 26 | bip = Bip32.from_extended_key(ROOT_SECRET) 27 | assert bip.depth == 0 28 | assert bip.index == 0 29 | for i in parse_bip32_path(HARD_PATH): 30 | bip = bip.child_key(i) 31 | assert bip.depth == 5 32 | assert bip.index == 3 33 | derived_secret = 'xprvA2fdawh3JYy7Zv8oPxTiLhGeyhfC4Gjjq1K2tV2vftYppmma88mW6uUhEP77o4Wgmgr' \ 34 | 'hvJjonaZLRZMcPK11Mjii9N7BcANiBDzS5DN4YWy' 35 | derived_public = 'xpub6FeyzTDw8vXQnQDGVyzihqDPXjVgTjTbCEEdgsSYEE5oha6ifg5kehoB5fGV8VrWPzv' \ 36 | '4uJpudNKKfXsg9e4Aj3xYKAin5ChjBA7V3fHoS4z' 37 | assert bip.extended_key(is_private=True) == derived_secret 38 | assert bip.extended_key(is_private=False) == derived_public 39 | 40 | 41 | def test_derive_from_public_key(): 42 | """test derived from public key""" 43 | bip = Bip32.from_extended_key(ROOT_PUBLIC) 44 | for i in parse_bip32_path(SOFT_PATH): 45 | bip = bip.child_key(i) 46 | derived_public = 'xpub6GNM2dtsy4aM7veEXp7VAhg2n44rHGrqsbvZNz8xASk1qxzFsiocrySaVrJ33cabKCW' \ 47 | 'Ugk3kHBaneBDBFcoD7MPzxfw5oXoeNrFeuMTPZ44' 48 | assert bip.extended_key(is_private=False) == derived_public 49 | 50 | 51 | def test_mnemonic_words(): 52 | """test decode mnemonic words and get root secret""" 53 | m = Mnemonic(LANGUAGE) 54 | entropy = m.to_seed(WORDS) 55 | bip = Bip32.from_entropy(entropy) 56 | assert bip.extended_key(is_private=True) == ROOT_SECRET 57 | 58 | 59 | if __name__ == "__main__": 60 | unittest.main() 61 | -------------------------------------------------------------------------------- /bc4py/chain/checking/__init__.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import V, stream, BlockChainError 2 | from bc4py.chain.checking.checkblock import check_block, check_block_time 3 | from bc4py.chain.checking.checktx import check_tx, check_tx_time 4 | from bc4py.chain.signature import fill_verified_addr_single 5 | from bc4py.database import obj 6 | from bc4py.database.create import create_db 7 | from logging import getLogger 8 | from time import time 9 | import asyncio 10 | 11 | new_block_lock = asyncio.Lock() 12 | log = getLogger('bc4py') 13 | 14 | 15 | async def new_insert_block(block, f_time=True, f_sign=True): 16 | t = time() 17 | async with new_block_lock: 18 | fixed_delay = time() - t 19 | try: 20 | # Check 21 | if not block.pow_check(): 22 | block.work2diff() 23 | block.target2diff() 24 | log.debug('reject, work check is failed. [{}<{}]' 25 | .format(block.difficulty, block.work_difficulty)) 26 | return False 27 | if f_time: 28 | check_block_time(block, fixed_delay) 29 | check_block(block) 30 | if f_sign: 31 | await fill_verified_addr_single(block) 32 | for tx in block.txs: 33 | check_tx(tx=tx, include_block=block) 34 | if f_time: 35 | check_tx_time(tx) 36 | # Recode 37 | obj.chain_builder.new_block(block) 38 | async with create_db(V.DB_ACCOUNT_PATH) as db: 39 | cur = await db.cursor() 40 | for tx in block.txs: 41 | await obj.account_builder.affect_new_tx(cur=cur, tx=tx) 42 | await db.commit() 43 | # insert database 44 | await obj.chain_builder.batch_apply() 45 | # inner streaming 46 | if not stream.is_disposed: 47 | stream.on_next(block) 48 | log.info("check success {}Sec {}".format(round(time() - t, 3), block)) 49 | return True 50 | except BlockChainError as e: 51 | log.warning("Reject new block by \"{}\"".format(e), exc_info=True) 52 | log.debug("Reject block => {}".format(block.getinfo())) 53 | return False 54 | except Exception as e: 55 | message = "New insert block error, \"{}\"".format(e) 56 | log.warning(message, exc_info=True) 57 | return False 58 | 59 | 60 | __all__ = [ 61 | "new_insert_block", 62 | "check_block", 63 | "check_block_time", 64 | "check_tx", 65 | "check_tx_time", 66 | ] 67 | -------------------------------------------------------------------------------- /bc4py/bip32/base58.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2014 Corgan Labs 4 | # See LICENSE.txt for distribution terms 5 | # 6 | 7 | from bc4py_extension import sha256d_hash 8 | 9 | __base58_alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 10 | __base58_alphabet_bytes = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 11 | __base58_radix = len(__base58_alphabet) 12 | 13 | 14 | def __string_to_int(data): 15 | """Convert string of bytes Python integer, MSB""" 16 | val = 0 17 | 18 | # Python 2.x compatibility 19 | if type(data) == str: 20 | data = bytearray(data) 21 | 22 | for (i, c) in enumerate(data[::-1]): 23 | val += (256**i) * c 24 | return val 25 | 26 | 27 | def encode(data): 28 | """Encode bytes into Bitcoin base58 string""" 29 | enc = '' 30 | val = __string_to_int(data) 31 | while val >= __base58_radix: 32 | val, mod = divmod(val, __base58_radix) 33 | enc = __base58_alphabet[mod] + enc 34 | if val: 35 | enc = __base58_alphabet[val] + enc 36 | 37 | # Pad for leading zeroes 38 | n = len(data) - len(data.lstrip(b'\0')) 39 | return __base58_alphabet[0] * n + enc 40 | 41 | 42 | def check_encode(raw): 43 | """Encode raw bytes into Bitcoin base58 string with checksum""" 44 | chk = sha256d_hash(raw)[:4] 45 | return encode(raw + chk) 46 | 47 | 48 | def decode(data): 49 | """Decode Bitcoin base58 format string to bytes""" 50 | # Python 2.x compatability 51 | if bytes != str: 52 | data = bytes(data, 'ascii') 53 | 54 | val = 0 55 | for (i, c) in enumerate(data[::-1]): 56 | val += __base58_alphabet_bytes.find(c) * (__base58_radix**i) 57 | 58 | dec = bytearray() 59 | while val >= 256: 60 | val, mod = divmod(val, 256) 61 | dec.append(mod) 62 | if val: 63 | dec.append(val) 64 | 65 | return bytes(dec[::-1]) 66 | 67 | 68 | def check_decode(enc): 69 | """Decode bytes from Bitcoin base58 string and test checksum""" 70 | dec = decode(enc) 71 | raw, chk = dec[:-4], dec[-4:] 72 | if chk != sha256d_hash(raw)[:4]: 73 | raise ValueError("base58 decoding checksum error dec={}".format(dec)) 74 | else: 75 | return raw 76 | 77 | 78 | def test(): 79 | assert (__base58_radix == 58) 80 | data = b'now is the time for all good men to come to the aid of their country' 81 | enc = check_encode(data) 82 | assert (check_decode(enc) == data) 83 | 84 | 85 | if __name__ == '__main__': 86 | test() 87 | 88 | __all__ = [ 89 | "encode", 90 | "check_encode", 91 | "decode", 92 | "check_decode", 93 | ] 94 | -------------------------------------------------------------------------------- /bc4py/chain/utils.py: -------------------------------------------------------------------------------- 1 | import msgpack 2 | import math 3 | 4 | DEFAULT_TARGET = float(0x00000000ffff0000000000000000000000000000000000000000000000000000) 5 | 6 | 7 | class GompertzCurve(object): 8 | k = None # total block reward supply 9 | b = 0.4 10 | c = 3.6 11 | ybnum = float(365 * 24 * 60) 12 | x0 = -0.4 13 | 14 | @staticmethod 15 | def calc_block_reward(height): 16 | g = GompertzCurve 17 | x = g.x0 + height / g.ybnum / 10.0 18 | # print("{} = {} + {} / {} / 10.0".format(x, g.x0, height, g.ybnum)) 19 | e = math.exp(-g.c * x) 20 | # print("{} = math.exp(-{} * {})".format(e, g.c, x)) 21 | r = g.c * math.log(g.b) * e * pow(g.b, e) / g.ybnum / 10.0 22 | # print("{} = {} * math.log({}) * {} * pow({}, {}) / {} / 10.0".format(r, g.c, g.b, e, g.b, e, g.ybnum)) 23 | # print("round(-{} * {})".format(g.k, r)) 24 | return round(-g.k * r) 25 | 26 | @staticmethod 27 | def base_total_supply(): 28 | g = GompertzCurve 29 | e = math.exp(-g.c * g.x0) 30 | return round(g.k * (g.b**e)) - g.calc_block_reward(0) 31 | 32 | @staticmethod 33 | def calc_total_supply(height): 34 | g = GompertzCurve 35 | x = g.x0 + height / g.ybnum / 10.0 36 | e = math.exp(-g.c * x) 37 | return round(g.k * (g.b**e)) - g.base_total_supply() 38 | 39 | 40 | def bin2signature(b): 41 | # pk:33, r:32or33 s: 32 42 | # b = BytesIO(b) 43 | # d = list(msgpack.Unpacker(b, raw=True, use_list=False, encoding='utf8')) 44 | # return d 45 | return list(msgpack.unpackb(b, raw=True, use_list=False, encoding='utf8')) 46 | 47 | 48 | def signature2bin(s): 49 | # b = BytesIO() 50 | # for pk, r, s in s: 51 | # b.write(msgpack.packb((pk, r, s), use_bin_type=True)) 52 | # return b.getvalue() 53 | return msgpack.packb(s, use_bin_type=True) 54 | 55 | 56 | def bits2target(bits): 57 | """ Convert bits to target """ 58 | exponent = ((bits >> 24) & 0xff) 59 | assert 3 <= exponent, "[exponent>=3] but {}".format(exponent) 60 | mantissa = bits & 0x7fffff 61 | if (bits & 0x800000) > 0: 62 | mantissa *= -1 63 | return mantissa * pow(256, exponent - 3) 64 | 65 | 66 | def target2bits(target): 67 | s = ("%064x" % target)[2:] 68 | while s[:2] == '00' and len(s) > 6: 69 | s = s[2:] 70 | bitsN, bitsBase = len(s) // 2, int('0x' + s[:6], 16) 71 | if bitsBase >= 0x800000: 72 | bitsN += 1 73 | bitsBase >>= 8 74 | return bitsN << 24 | bitsBase 75 | 76 | 77 | __all__ = [ 78 | "DEFAULT_TARGET", 79 | "GompertzCurve", 80 | "bin2signature", 81 | "signature2bin", 82 | "bits2target", 83 | "target2bits", 84 | ] 85 | -------------------------------------------------------------------------------- /bc4py/chain/signature.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, V 2 | from bc4py.bip32 import get_address 3 | from multi_party_schnorr import verify_auto_multi 4 | from logging import getLogger 5 | from os import cpu_count 6 | from time import time 7 | import asyncio 8 | 9 | loop = asyncio.get_event_loop() 10 | log = getLogger('bc4py') 11 | n_workers = cpu_count() 12 | 13 | 14 | async def fill_verified_addr_single(block): 15 | # format check 16 | for tx in block.txs: 17 | for sign in tx.signature: 18 | assert isinstance(sign, tuple), tx.getinfo() 19 | # get data to verify 20 | tasks = get_verify_tasks(block) 21 | # throw task 22 | if len(tasks) == 0: 23 | return 24 | await throw_tasks(tasks, V.BECH32_HRP, C.ADDR_NORMAL_VER) 25 | 26 | 27 | async def fill_verified_addr_many(blocks): 28 | s = time() 29 | # format check 30 | tasks = dict() 31 | for block in blocks: 32 | for tx in block.txs: 33 | for sign in tx.signature: 34 | assert isinstance(sign, tuple), tx.getinfo() 35 | # get data to verify 36 | tasks.update(get_verify_tasks(block)) 37 | # throw task 38 | if len(tasks) == 0: 39 | return 40 | await throw_tasks(tasks, V.BECH32_HRP, C.ADDR_NORMAL_VER) 41 | log.debug("verify {} signs by {}sec".format(len(tasks), round(time() - s, 3))) 42 | 43 | 44 | async def fill_verified_addr_tx(tx): 45 | assert tx.type != C.TX_POS_REWARD 46 | # format check 47 | for sign in tx.signature: 48 | assert isinstance(sign, tuple), tx.getinfo() 49 | # get data to verify 50 | tasks = dict() 51 | for pk, r, s in tx.signature: 52 | tasks[(s, r, pk, tx.b)] = tx 53 | # throw task 54 | if len(tasks) == 0: 55 | return 56 | await throw_tasks(tasks, V.BECH32_HRP, C.ADDR_NORMAL_VER) 57 | 58 | 59 | def get_verify_tasks(block): 60 | tasks = dict() 61 | for tx in block.txs: 62 | if tx.type == C.TX_POS_REWARD: 63 | binary = block.b 64 | else: 65 | binary = tx.b 66 | if len(tx.verified_list) == len(tx.signature): 67 | continue 68 | for pk, r, s in tx.signature: 69 | tasks[(s, r, pk, binary)] = tx 70 | return tasks 71 | 72 | 73 | async def throw_tasks(tasks, hrp, ver): 74 | task_list = list(tasks.keys()) 75 | future: asyncio.Future = loop.run_in_executor( 76 | None, verify_auto_multi, task_list, n_workers, False) 77 | await asyncio.wait_for(future, 120.0) 78 | result_list = future.result() 79 | # fill result 80 | for is_verify, key in zip(result_list, task_list): 81 | if not is_verify: 82 | continue 83 | if key not in tasks: 84 | continue 85 | s, r, pk, binary = key 86 | address = get_address(pk=pk, hrp=hrp, ver=ver) 87 | verified_list = tasks[key].verified_list 88 | if address not in verified_list: 89 | verified_list.append(address) 90 | 91 | 92 | __all__ = [ 93 | "fill_verified_addr_single", 94 | "fill_verified_addr_many", 95 | "fill_verified_addr_tx", 96 | ] 97 | -------------------------------------------------------------------------------- /bc4py/user/__init__.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | 4 | class Balance(defaultdict): 5 | __slots__ = tuple() 6 | 7 | def __init__(self, coin_id=None, amount=None, balance=None): 8 | super().__init__(int) 9 | if coin_id is not None and amount is not None: 10 | self[coin_id] = amount 11 | elif balance and isinstance(balance, dict): 12 | for k, v in balance.items(): 13 | self[k] = v 14 | 15 | def __repr__(self): 16 | return "".format(dict(self)) 17 | 18 | def __iter__(self): 19 | yield from self.items() 20 | 21 | def copy(self): 22 | # don't remove zero balance pair, until copy 23 | # after copy, new obj don't include zero balance pair 24 | return Balance(balance=dict(self)) 25 | 26 | def is_all_plus_amount(self): 27 | for v in self.values(): 28 | if v < 0: 29 | return False 30 | return True 31 | 32 | def is_all_minus_amount(self): 33 | for v in self.values(): 34 | if v > 0: 35 | return False 36 | return True 37 | 38 | def is_empty(self): 39 | for v in self.values(): 40 | if v != 0: 41 | return False 42 | return True 43 | 44 | def cleanup(self): 45 | for k, v in list(self.items()): 46 | if v == 0: 47 | del self[k] 48 | 49 | def __add__(self, other): 50 | assert isinstance(other, Balance) 51 | b = self.copy() 52 | for k in other.keys(): 53 | b[k] += other[k] 54 | return b 55 | 56 | def __sub__(self, other): 57 | assert isinstance(other, Balance) 58 | b = self.copy() 59 | for k in other.keys(): 60 | b[k] -= other[k] 61 | return b 62 | 63 | 64 | class Accounting(defaultdict): 65 | __slots__ = ('txhash',) 66 | 67 | def __init__(self, users=None, txhash=None): 68 | super().__init__(Balance) 69 | if users and isinstance(users, dict): 70 | for k, v in users.items(): 71 | self[k] = v.copy() 72 | self.txhash = txhash 73 | 74 | def __repr__(self): 75 | return "".format(dict(self)) 76 | 77 | def __iter__(self): 78 | yield from self.items() 79 | 80 | def copy(self): 81 | return Accounting(users=dict(self)) 82 | 83 | def cleanup(self): 84 | for k, v in list(self.items()): 85 | v.cleanup() 86 | if v.is_empty(): 87 | del self[k] 88 | 89 | def __add__(self, other): 90 | assert isinstance(other, Accounting) 91 | users = self.copy() 92 | for k, v in other.items(): 93 | users[k] += v 94 | return users 95 | 96 | def __sub__(self, other): 97 | assert isinstance(other, Accounting) 98 | users = self.copy() 99 | for k, v in other.items(): 100 | users[k] -= v 101 | return users 102 | 103 | 104 | __all__ = [ 105 | "Balance", 106 | "Accounting", 107 | ] 108 | -------------------------------------------------------------------------------- /bc4py/user/api/ep_blockchain.py: -------------------------------------------------------------------------------- 1 | from bc4py.database import obj 2 | from bc4py.database.mintcoin import get_mintcoin_object 3 | from bc4py.user.api.utils import error_response 4 | from binascii import a2b_hex 5 | 6 | 7 | async def get_block_by_height(height: int, txinfo: bool = False): 8 | """ 9 | show block info from height 10 | * Arguments 11 | 1. **height** : block height 12 | 2. **txinfo** : show tx info if true, only txhash if false 13 | """ 14 | blockhash = obj.chain_builder.get_block_hash(height) 15 | if blockhash is None: 16 | return error_response("Not found height") 17 | block = obj.chain_builder.get_block(blockhash) 18 | data = block.getinfo(txinfo) 19 | data['hex'] = block.b.hex() 20 | return data 21 | 22 | 23 | async def get_block_by_hash(hash: str, txinfo: bool = False): 24 | """ 25 | show block info by hash 26 | * Arguments 27 | 1. **hash** : block hash 28 | 2. **txinfo** : show tx info if true, only txhash if false 29 | """ 30 | try: 31 | blockhash = a2b_hex(hash) 32 | block = obj.chain_builder.get_block(blockhash) 33 | if block is None: 34 | return error_response("Not found block") 35 | data = block.getinfo(txinfo) 36 | data['hex'] = block.b.hex() 37 | return data 38 | except Exception: 39 | return error_response() 40 | 41 | 42 | async def get_tx_by_hash(hash: str): 43 | """ 44 | show tx info by hash 45 | * Arguments 46 | 1. **hash** : txhash 47 | """ 48 | try: 49 | txhash = a2b_hex(hash) 50 | # if you cannot get TX, please check DB config `txindex` 51 | tx = obj.tx_builder.get_tx(txhash) 52 | if tx is None: 53 | if obj.tables.table_config['txindex']: 54 | return error_response("not found the tx in this chain") 55 | else: 56 | return error_response('not found the tx, please set `txindex` true if you want full indexed') 57 | data = tx.getinfo() 58 | data['hex'] = tx.b.hex() 59 | return data 60 | except Exception: 61 | return error_response() 62 | 63 | 64 | async def get_mintcoin_info(mint_id: int = 0): 65 | """ 66 | show mint coin info by coinId 67 | * Arguments 68 | 1. **mint_id** : mint coin id 69 | * About 70 | * id 0 is base currency 71 | """ 72 | try: 73 | m = get_mintcoin_object(coin_id=mint_id) 74 | return m.info 75 | except Exception: 76 | return error_response() 77 | 78 | 79 | async def get_mintcoin_history(mint_id: int = 0): 80 | """ 81 | show mint coin history by coinId 82 | * Arguments 83 | 1. **mint_id** : mint coin id 84 | * About 85 | * caution! this show only database stored data, not memory not unconfirmed status. 86 | """ 87 | try: 88 | data = list() 89 | # from only database 90 | for height, index, txhash, params, setting in obj.tables.read_coins_iter(coin_id=mint_id): 91 | data.append({ 92 | 'height': height, 93 | 'index': index, 94 | 'txhash': txhash.hex(), 95 | 'params': params, 96 | 'setting': setting, 97 | }) 98 | return data 99 | except Exception: 100 | return error_response() 101 | 102 | 103 | __all__ = [ 104 | "get_block_by_height", 105 | "get_block_by_hash", 106 | "get_tx_by_hash", 107 | "get_mintcoin_info", 108 | "get_mintcoin_history", 109 | ] 110 | -------------------------------------------------------------------------------- /bc4py/user/api/utils.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import P 2 | from starlette.status import ( 3 | HTTP_403_FORBIDDEN, HTTP_503_SERVICE_UNAVAILABLE, HTTP_301_MOVED_PERMANENTLY) 4 | from starlette.requests import Request 5 | from starlette.responses import Response, PlainTextResponse 6 | from starlette.middleware.base import BaseHTTPMiddleware 7 | from typing import Any 8 | from json import dumps 9 | from logging import getLogger 10 | 11 | 12 | log = getLogger('bc4py') 13 | 14 | local_address = { 15 | "127.0.0.1", 16 | "::1", 17 | } 18 | ALLOW_CROSS_ORIGIN_ACCESS = { 19 | 'Access-Control-Allow-Origin': '*' 20 | } 21 | 22 | 23 | class IndentResponse(Response): 24 | media_type = "application/json" 25 | 26 | def render(self, content: Any) -> bytes: 27 | return dumps( 28 | content, 29 | ensure_ascii=False, 30 | allow_nan=False, 31 | indent=4, 32 | separators=(",", ":"), 33 | ).encode("utf-8") 34 | 35 | 36 | class ConditionCheckMiddleware(BaseHTTPMiddleware): 37 | async def dispatch(self, request: Request, call_next): 38 | # check private method 39 | if request.url.path.startswith('/private'): 40 | if request.client.host in local_address: 41 | proxy_host = request.headers.get('X-Forwarded-For') 42 | if proxy_host is None or proxy_host in local_address: 43 | pass # success 44 | elif proxy_host.startswith('::ffff:') and proxy_host[7:] in local_address: 45 | pass # success (IPv6 address including IPv4 address) 46 | else: 47 | return PlainTextResponse( 48 | "1 private method only allow from locals ({})".format(proxy_host), 49 | status_code=HTTP_403_FORBIDDEN, 50 | headers=ALLOW_CROSS_ORIGIN_ACCESS, 51 | ) 52 | else: 53 | return PlainTextResponse( 54 | "2 private method only allow from locals ({})".format(request.client.host), 55 | status_code=HTTP_403_FORBIDDEN, 56 | headers=ALLOW_CROSS_ORIGIN_ACCESS, 57 | ) 58 | 59 | # redirect to doc page 60 | if request.url.path == '/' and request.method == 'GET': 61 | return PlainTextResponse( 62 | status_code=HTTP_301_MOVED_PERMANENTLY, 63 | headers={"Location": "./docs"}, 64 | ) 65 | 66 | # avoid API access until booting 67 | if P.F_NOW_BOOTING and request.url.path != '/public/getsysteminfo': 68 | return PlainTextResponse( 69 | "server is now on booting mode..", 70 | status_code=HTTP_503_SERVICE_UNAVAILABLE, 71 | headers=ALLOW_CROSS_ORIGIN_ACCESS, 72 | ) 73 | 74 | # success 75 | return await call_next(request) 76 | 77 | 78 | def error_response(errors=None): 79 | """simple error message response""" 80 | if errors is None: 81 | import traceback 82 | errors = str(traceback.format_exc()) 83 | log.debug(f"API error:\n{errors}") 84 | s = errors.split("\n") 85 | simple_msg = None 86 | while not simple_msg: 87 | simple_msg = s.pop(-1) 88 | return PlainTextResponse( 89 | simple_msg + '\n', 90 | status_code=400, 91 | headers=ALLOW_CROSS_ORIGIN_ACCESS, 92 | ) 93 | 94 | 95 | __all__ = [ 96 | "local_address", 97 | "IndentResponse", 98 | "ConditionCheckMiddleware", 99 | "error_response", 100 | ] 101 | -------------------------------------------------------------------------------- /bc4py/user/api/jsonrpc/others.py: -------------------------------------------------------------------------------- 1 | from bc4py import __version__, __chain_version__, __message__ 2 | from bc4py.config import C, V 3 | from bc4py.utils import GompertzCurve 4 | from bc4py.bip32 import is_address 5 | from bc4py.database import obj 6 | from bc4py.database.create import create_db 7 | from bc4py.database.account import read_address2userid, read_userid2name, read_address2keypair 8 | from bc4py.chain.difficulty import get_bits_by_hash 9 | from bc4py_extension import PyAddress 10 | from logging import getLogger 11 | 12 | 13 | log = getLogger('bc4py') 14 | 15 | 16 | async def getinfo(*args, **kwargs): 17 | """ 18 | method "getinfo" 19 | """ 20 | consensus = int(kwargs['password']) 21 | best_block = obj.chain_builder.best_block 22 | # difficulty 23 | bits, target = get_bits_by_hash(previous_hash=best_block.hash, consensus=consensus) 24 | difficulty = (0xffffffffffffffff // target) / 100000000 25 | # balance 26 | async with create_db(V.DB_ACCOUNT_PATH) as db: 27 | cur = await db.cursor() 28 | users = await obj.account_builder.get_balance(cur=cur, confirm=6) 29 | return { 30 | "version": __version__, 31 | "protocolversion": __chain_version__, 32 | "balance": round(users.get(0, 0) / pow(10, V.COIN_DIGIT), 8), 33 | "blocks": best_block.height, 34 | "moneysupply": GompertzCurve.calc_total_supply(best_block.height), 35 | "connections": len(V.P2P_OBJ.core.user), 36 | "testnet": V.BECH32_HRP == 'test', 37 | "difficulty": round(difficulty, 8), 38 | "paytxfee": round(V.COIN_MINIMUM_PRICE / pow(10, V.COIN_DIGIT), 8), 39 | "errors": __message__, 40 | } 41 | 42 | 43 | async def validateaddress(*args, **kwargs): 44 | """ 45 | Arguments: 46 | 1. "address" (string, required) The bitcoin address to validate 47 | 48 | Result: 49 | { 50 | "isvalid" : true|false, (boolean) If the address is valid or not. If not, this is the only property returned. 51 | "address" : "address", (string) The bitcoin address validated 52 | "ismine" : true|false, (boolean) If the address is yours or not 53 | "pubkey" : "publickeyhex", (string, optional) The hex value of the raw public key, for single-key addresses (possibly embedded in P2SH or P2WSH) 54 | "account" : "account" (string) DEPRECATED. The account associated with the address, "" is the default account 55 | "hdkeypath" : "keypath" (string, optional) The HD keypath if the key is HD and available 56 | } 57 | """ 58 | if len(args) == 0: 59 | raise ValueError('no argument found') 60 | addr: PyAddress = PyAddress.from_string(args[0]) 61 | async with create_db(V.DB_ACCOUNT_PATH) as db: 62 | cur = await db.cursor() 63 | user_id = await read_address2userid(address=addr, cur=cur) 64 | if user_id is None: 65 | return { 66 | "isvalid": is_address(addr, V.BECH32_HRP, 0), 67 | "address": addr.string, 68 | "ismine": False, 69 | "pubkey": None, 70 | "account": None, 71 | "hdkeypath": None, 72 | } 73 | else: 74 | account = await read_userid2name(user=user_id, cur=cur) 75 | account = "" if account == C.ANT_UNKNOWN else account 76 | _, keypair, path = await read_address2keypair(address=addr, cur=cur) 77 | return { 78 | "isvalid": is_address(addr, V.BECH32_HRP, 0), 79 | "address": addr.string, 80 | "ismine": True, 81 | "pubkey": keypair.get_public_key().hex(), 82 | "account": account, 83 | "hdkeypath": path, 84 | } 85 | 86 | 87 | __all__ = [ 88 | "getinfo", 89 | "validateaddress", 90 | ] 91 | -------------------------------------------------------------------------------- /bc4py/user/network/directcmd.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import P, BlockChainError 2 | from bc4py.database import obj 3 | 4 | 5 | """ 6 | : Peer2Peer DirectCmd definition 7 | return string => ERROR 8 | return others => SUCCESS 9 | """ 10 | 11 | 12 | class DirectCmd(object): 13 | 14 | @staticmethod 15 | def best_info(user, data): 16 | """return best info of chain""" 17 | try: 18 | if obj.chain_builder.best_block: 19 | return { 20 | 'hash': obj.chain_builder.best_block.hash, 21 | 'height': obj.chain_builder.best_block.height, 22 | 'booting': P.F_NOW_BOOTING, 23 | } 24 | else: 25 | return { 26 | 'hash': None, 27 | 'height': None, 28 | 'booting': True, 29 | } 30 | except BlockChainError as e: 31 | return str(e) 32 | 33 | @staticmethod 34 | def block_by_height(user, data): 35 | height = data.get('height') 36 | if height is None: 37 | return 'do not find key "height"' 38 | if not isinstance(height, int): 39 | return f"height is not int! {height}" 40 | try: 41 | block = obj.chain_builder.get_block(height=height) 42 | if block: 43 | return block 44 | else: 45 | return f"Not found block height {height}" 46 | except BlockChainError as e: 47 | return str(e) 48 | 49 | @staticmethod 50 | def block_by_hash(user, data): 51 | blockhash = data.get('blockhash') 52 | if blockhash is None: 53 | return 'do not find key "blockhash"' 54 | if not isinstance(blockhash, bytes): 55 | return f"blockhash is not bytes! {blockhash}" 56 | try: 57 | block = obj.chain_builder.get_block(blockhash=blockhash) 58 | if block is None: 59 | return f"Not found blockhash {blockhash.hex()}" 60 | return block 61 | except BlockChainError as e: 62 | return str(e) 63 | 64 | @staticmethod 65 | def tx_by_hash(user, data): 66 | txhash = data.get('txhash') 67 | if txhash is None: 68 | return 'do not find key "txhash"' 69 | elif not isinstance(txhash, bytes): 70 | return f"txhash is not bytes! {txhash}" 71 | try: 72 | tx = obj.tx_builder.get_memorized_tx(txhash) 73 | if tx is None: 74 | return f"Not found tx {txhash.hex()}" 75 | return tx 76 | except BlockChainError as e: 77 | return str(e) 78 | 79 | @staticmethod 80 | def unconfirmed_tx(user, data): 81 | try: 82 | return { 83 | 'txs': obj.tx_builder.memory_pool.list_all_hash(), 84 | } 85 | except BlockChainError as e: 86 | print("exception! unconfirmed", user, e) 87 | return str(e) 88 | 89 | @staticmethod 90 | def big_blocks(user, data): 91 | try: 92 | index_height = data.get('height') 93 | if index_height is None: 94 | return 'do not find key "height"' 95 | request_len = data.get('request_len', 20) 96 | request_len = max(0, min(100, request_len)) 97 | if not isinstance(request_len, int): 98 | return f"request_len is int! {request_len}" 99 | data = list() 100 | for height in range(index_height, index_height + max(0, request_len)): 101 | block = obj.chain_builder.get_block(height=height) 102 | if block is None: 103 | break 104 | data.append(block) 105 | return data 106 | except BlockChainError as e: 107 | return str(e) 108 | 109 | 110 | __all__ = [ 111 | "DirectCmd", 112 | ] 113 | -------------------------------------------------------------------------------- /bc4py/chain/checking/tx_mintcoin.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, BlockChainError 2 | from bc4py.database.mintcoin import * 3 | from bc4py.database.tools import get_output_from_input 4 | from bc4py.user import Balance 5 | from bc4py_extension import PyAddress 6 | 7 | 8 | def check_tx_mint_coin(tx, include_block): 9 | if not (0 < len(tx.inputs) and 0 < len(tx.outputs)): 10 | raise BlockChainError('Input and output is more than 1') 11 | elif tx.message_type != C.MSG_MSGPACK: 12 | raise BlockChainError('TX_MINT_COIN message is bytes') 13 | elif include_block and 0 == include_block.txs.index(tx): 14 | raise BlockChainError('tx index is not proof tx') 15 | elif tx.gas_amount < tx.size + len(tx.signature) * C.SIGNATURE_GAS + C.MINTCOIN_GAS: 16 | raise BlockChainError('Insufficient gas amount [{}<{}+{}+{}]'.format(tx.gas_amount, tx.size, 17 | len(tx.signature) * C.SIGNATURE_GAS, 18 | C.MINTCOIN_GAS)) 19 | # check new mintcoin format 20 | try: 21 | mint_id, params, setting = tx.encoded_message() 22 | except Exception as e: 23 | raise BlockChainError('BjsonDecodeError: {}'.format(e)) 24 | m_before = get_mintcoin_object(coin_id=mint_id, best_block=include_block, stop_txhash=tx.hash) 25 | result = check_mintcoin_new_format(m_before=m_before, new_params=params, new_setting=setting) 26 | if isinstance(result, str): 27 | raise BlockChainError('Failed check mintcoin block={}: {}'.format(include_block, result)) 28 | # signature check 29 | require_cks, coins = input_output_digest(tx=tx) 30 | if m_before.address: 31 | require_cks.add(PyAddress.from_string(m_before.address)) 32 | signed_cks = set(tx.verified_list) 33 | if signed_cks != require_cks: 34 | raise BlockChainError('Signature check failed. signed={} require={} lack={}'.format( 35 | signed_cks, require_cks, require_cks - signed_cks)) 36 | # amount check 37 | include_coin_ids = set(coins.keys()) # include zero balance pair 38 | if include_coin_ids == {0, mint_id}: 39 | # increase/decrease mintcoin amount (exchange) 40 | # don't care about params and setting on this section 41 | if not m_before.setting['additional_issue']: 42 | raise BlockChainError('additional_issue is False but change amount') 43 | if coins[0] + coins[mint_id] != 0: 44 | raise BlockChainError('46 Don\'t match input/output amount. {}'.format(coins)) 45 | if coins[mint_id] < 0: 46 | pass # increase 47 | if coins[mint_id] > 0: 48 | pass # decrease 49 | elif len(include_coin_ids) == 1: 50 | include_id = include_coin_ids.pop() 51 | include_amount = coins[include_id] 52 | if include_id == 0: 53 | # only id=0, just only change mintcoin status 54 | if params is None and setting is None: 55 | raise BlockChainError('No update found') 56 | if include_amount != 0: 57 | raise BlockChainError('59 Don\'t match input/output amount. {}'.format(coins)) 58 | elif include_id == mint_id: 59 | raise BlockChainError('Only include mint_id, coins={}'.format(coins)) 60 | else: 61 | raise BlockChainError('Unexpected include_id, {}'.format(include_id)) 62 | else: 63 | raise BlockChainError('Unexpected include_coin_ids, {}'.format(include_coin_ids)) 64 | 65 | 66 | def input_output_digest(tx): 67 | require_cks = set() 68 | coins = Balance() 69 | # inputs 70 | for txhash, txindex in tx.inputs: 71 | pair = get_output_from_input(txhash, txindex) 72 | if pair is None: 73 | raise BlockChainError('input tx is None. {}:{}'.format(txhash.hex(), txindex)) 74 | address, coin_id, amount = pair 75 | require_cks.add(address) 76 | coins[coin_id] += amount 77 | # outputs 78 | for address, coin_id, amount in tx.outputs: 79 | coins[coin_id] -= amount 80 | # fee 81 | coins[0] -= tx.gas_amount * tx.gas_price 82 | return require_cks, coins 83 | 84 | 85 | __all__ = [ 86 | "check_tx_mint_coin", 87 | ] 88 | -------------------------------------------------------------------------------- /bc4py/chain/genesisblock.py: -------------------------------------------------------------------------------- 1 | from bc4py.chain.difficulty import MAX_BITS 2 | from bc4py.config import C, V, BlockChainError 3 | from bc4py.chain.block import Block 4 | from bc4py.chain.tx import TX 5 | from time import time 6 | from more_itertools import chunked 7 | 8 | 9 | def create_genesis_block(mining_supply, 10 | block_span, 11 | hrp='pycon', 12 | digit_number=8, 13 | minimum_price=100, 14 | consensus=None, 15 | genesis_msg="blockchain for python", 16 | premine=None): 17 | """ 18 | Height0のGenesisBlockを作成する 19 | :param mining_supply: PoW/POS合わせた全採掘量、プリマインを除く 20 | :param block_span: Blockの採掘間隔(Sec) 21 | :param hrp: human readable part 22 | :param digit_number: コインの分解能 23 | :param minimum_price: 最小gas_price 24 | :param consensus: 採掘アルゴ {consensus: ratio(0~100), ..} 25 | :param genesis_msg: GenesisMessage 26 | :param premine: プリマイン [(address, coin_id, amount), ...] 27 | """ 28 | 29 | # default: Yescript9割, Stake1割の分配 30 | consensus = consensus or {C.BLOCK_X16S_POW: 100} 31 | if sum(consensus.values()) != 100: 32 | raise BlockChainError('sum of consensus values is 100 [!={}]'.format(sum(consensus.values()))) 33 | elif not isinstance(sum(consensus.values()), int): 34 | raise BlockChainError('value is int only') 35 | elif not (0 < min(consensus.values()) <= 100): 36 | raise BlockChainError('out of range {}'.format(min(consensus.values()))) 37 | elif not (0 < max(consensus.values()) <= 100): 38 | raise BlockChainError('out of range {}'.format(min(consensus.values()))) 39 | all_consensus = { 40 | C.BLOCK_COIN_POS, C.BLOCK_CAP_POS, C.BLOCK_FLK_POS, C.BLOCK_YES_POW, C.BLOCK_X11_POW, C.BLOCK_X16S_POW 41 | } 42 | if len(set(consensus.keys()) - all_consensus) > 0: 43 | raise BlockChainError('Not found all_consensus number {}'.format(set(consensus.keys()) - all_consensus)) 44 | elif len(set(consensus.keys()) & all_consensus) == 0: 45 | raise BlockChainError('No usable consensus found {}'.format(set(consensus.keys()) & all_consensus)) 46 | elif not (0 < len(hrp) < 5): 47 | raise BlockChainError('hrp is too long hrp={}'.format(hrp)) 48 | elif 'dummy' in hrp or '1' in hrp: 49 | raise BlockChainError('Not allowed include "dummy" and "1" str {}'.format(hrp)) 50 | 51 | # params 52 | assert isinstance(minimum_price, int), 'minimum_price is INT' 53 | genesis_time = int(time()) 54 | # BLockChainの設定TX 55 | params = { 56 | 'hrp': hrp, 57 | 'genesis_time': genesis_time, # GenesisBlockの採掘時間 58 | 'mining_supply': mining_supply, # 全採掘量 59 | 'block_span': block_span, # ブロックの採掘間隔 60 | 'digit_number': digit_number, # 小数点以下の桁数 61 | 'minimum_price': minimum_price, 62 | 'consensus': consensus, # Block承認のアルゴリズム 63 | } 64 | V.BLOCK_GENESIS_TIME = genesis_time 65 | # first tx 66 | first_tx = TX.from_dict( 67 | tx={ 68 | 'type': C.TX_GENESIS, 69 | 'time': 0, 70 | 'deadline': 10800, 71 | 'gas_price': 0, 72 | 'gas_amount': 0, 73 | 'message_type': C.MSG_PLAIN, 74 | 'message': genesis_msg.encode() 75 | }) 76 | first_tx.height = 0 77 | # premine 78 | premine_txs = list() 79 | for index, chunk in enumerate(chunked(premine or list(), 255)): 80 | tx = TX.from_dict(tx={ 81 | 'type': C.TX_TRANSFER, 82 | 'time': 0, 83 | 'deadline': 10800, 84 | 'outputs': chunk, 85 | 'gas_price': 0, 86 | 'gas_amount': 0 87 | }) 88 | tx.height = 0 89 | premine_txs.append(tx) 90 | # height0のBlock生成 91 | genesis_block = Block.from_dict(block={ 92 | 'merkleroot': b'\x00' * 32, 93 | 'time': 0, 94 | 'previous_hash': b'\xff' * 32, 95 | 'bits': MAX_BITS, 96 | 'nonce': b'\xff' * 4 97 | }) 98 | # block params 99 | genesis_block.height = 0 100 | genesis_block.flag = C.BLOCK_GENESIS 101 | # block body 102 | genesis_block.txs.append(first_tx) 103 | genesis_block.txs.extend(premine_txs) 104 | genesis_block.bits2target() 105 | genesis_block.target2diff() 106 | genesis_block.update_merkleroot() 107 | genesis_block.serialize() 108 | return genesis_block, params 109 | -------------------------------------------------------------------------------- /localnode.py: -------------------------------------------------------------------------------- 1 | #!/user/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from bc4py import __version__, __chain_version__, __message__, __logo__ 5 | from bc4py.config import C, V 6 | from bc4py.utils import set_database_path, set_blockchain_params, check_already_started 7 | from bc4py.exit import blocking_run 8 | from bc4py.user.generate import * 9 | from bc4py.user.boot import * 10 | from bc4py.user.network import * 11 | from bc4py.user.api import setup_rest_server 12 | from bc4py.database import obj 13 | from bc4py.database.create import check_account_db 14 | from bc4py.database.builder import setup_database_obj 15 | from bc4py.chain.msgpack import default_hook, object_hook 16 | from p2p_python.utils import setup_p2p_params 17 | from p2p_python.server import Peer2Peer 18 | from bc4py.for_debug import set_logger, f_already_bind 19 | import asyncio 20 | import logging 21 | import os 22 | 23 | 24 | loop = asyncio.get_event_loop() 25 | 26 | 27 | def copy_boot(port): 28 | if port == 2000: 29 | return 30 | else: 31 | original = os.path.join(os.path.split(V.DB_HOME_DIR)[0], '2000', 'boot.json') 32 | destination = os.path.join(V.DB_HOME_DIR, 'boot.json') 33 | if original == destination: 34 | return 35 | with open(original, mode='br') as ifp: 36 | with open(destination, mode='bw') as ofp: 37 | ofp.write(ifp.read()) 38 | 39 | 40 | def setup_client(port, sub_dir): 41 | # BlockChain setup 42 | set_database_path(sub_dir=sub_dir) 43 | check_already_started() 44 | setup_database_obj() 45 | copy_boot(port) 46 | import_keystone(passphrase='hello python') 47 | loop.run_until_complete(check_account_db()) 48 | genesis_block, genesis_params, network_ver, connections = load_boot_file() 49 | set_blockchain_params(genesis_block, genesis_params) 50 | logging.info("Start p2p network-ver{} .".format(network_ver)) 51 | 52 | # P2P network setup 53 | setup_p2p_params(network_ver=network_ver, p2p_port=port, 54 | p2p_accept=True, p2p_udp_accept=True, sub_dir=sub_dir) 55 | p2p = Peer2Peer(f_local=True, default_hook=default_hook, object_hook=object_hook) 56 | p2p.event.setup_events_from_class(DirectCmd) 57 | p2p.setup() 58 | V.P2P_OBJ = p2p 59 | return connections 60 | 61 | 62 | async def setup_chain(port, connections): 63 | p2p = V.P2P_OBJ 64 | # for debug node 65 | if port != 2000 and await p2p.core.create_connection('127.0.0.1', 2000): 66 | logging.info("Connect!") 67 | else: 68 | await p2p.core.create_connection('127.0.0.1', 2001) 69 | 70 | # BroadcastProcess setup 71 | p2p.broadcast_check = broadcast_check 72 | 73 | # Update to newest blockchain 74 | if await obj.chain_builder.init(V.GENESIS_BLOCK, batch_size=500): 75 | # only genesisBlock yoy have, try to import bootstrap.dat.gz 76 | await load_bootstrap_file() 77 | await sync_chain_loop() 78 | 79 | # Mining/Staking setup 80 | # Debug.F_CONSTANT_DIFF = True 81 | # Debug.F_SHOW_DIFFICULTY = True 82 | # Debug.F_STICKY_TX_REJECTION = False # for debug 83 | if port == 2000: 84 | Generate(consensus=C.BLOCK_CAP_POS, power_limit=0.6) 85 | elif port % 3 == 0: 86 | Generate(consensus=C.BLOCK_YES_POW, power_limit=0.03) 87 | elif port % 3 == 1: 88 | Generate(consensus=C.BLOCK_X16S_POW, power_limit=0.03) 89 | elif port % 3 == 2: 90 | Generate(consensus=C.BLOCK_X11_POW, power_limit=0.03) 91 | Generate(consensus=C.BLOCK_COIN_POS, power_limit=0.3) 92 | asyncio.ensure_future(mined_newblock(mined_block_que)) 93 | logging.info("finished all initialization") 94 | 95 | 96 | def main(): 97 | port = 2000 98 | while True: 99 | if f_already_bind(port): 100 | port += 1 101 | continue 102 | else: 103 | break 104 | connections = setup_client(port=port, sub_dir=str(port)) 105 | loop.run_until_complete(setup_rest_server(port=port + 1000)) 106 | loop.run_until_complete(setup_chain(port, connections)) 107 | 108 | path = 'debug.2000.log' if port == 2000 else None 109 | set_logger(level=logging.DEBUG, path=path, f_remove=True) 110 | logging.info("\n{}\n=====\n{}, chain-ver={}\n{}\n" 111 | .format(__logo__, __version__, __chain_version__, __message__)) 112 | 113 | import aiomonitor 114 | aiomonitor.start_monitor(loop, port=port+2000, console_port=port+3000) 115 | logging.warning(f"aiomonitor working! use by console `nc 127.0.0.1 {port+2000}`") 116 | blocking_run() 117 | 118 | 119 | if __name__ == '__main__': 120 | main() 121 | -------------------------------------------------------------------------------- /bc4py/user/network/sendnew.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import V, P, BlockChainError 2 | from bc4py.chain.checking import new_insert_block, check_tx, check_tx_time 3 | from bc4py.user.network import BroadcastCmd 4 | from p2p_python.server import Peer2PeerCmd 5 | from p2p_python.config import PeerToPeerError 6 | from bc4py.database import obj 7 | from bc4py.user.network.update import update_info_for_generate 8 | from aiosqlite import Cursor 9 | from logging import getLogger 10 | from time import time 11 | import asyncio 12 | 13 | loop = asyncio.get_event_loop() 14 | log = getLogger('bc4py') 15 | 16 | 17 | async def mined_newblock(mined_block_que): 18 | """new thread, broadcast mined block to network""" 19 | assert V.P2P_OBJ, "PeerClient is None" 20 | assert isinstance(mined_block_que, asyncio.Queue) 21 | while not P.F_STOP: 22 | result = None 23 | try: 24 | new_block, result = await asyncio.wait_for(mined_block_que.get(), 1.0) 25 | new_block.create_time = int(time()) 26 | if P.F_NOW_BOOTING: 27 | log.debug("self reject, mined but now booting") 28 | result.set_result(False) 29 | continue 30 | elif new_block.height != obj.chain_builder.best_block.height + 1: 31 | log.debug("self reject, mined but its old block") 32 | result.set_result(False) 33 | continue 34 | else: 35 | log.debug("Mined block check success") 36 | if await new_insert_block(new_block): 37 | log.info("Mined new block {}".format(new_block.getinfo())) 38 | else: 39 | log.debug("self reject, cannot new insert") 40 | update_info_for_generate() 41 | result.set_result(False) 42 | continue 43 | proof_tx = new_block.txs[0] 44 | txs_hash_list = [tx.hash for tx in new_block.txs] 45 | data = { 46 | 'cmd': BroadcastCmd.NEW_BLOCK, 47 | 'data': { 48 | 'binary': new_block.b, 49 | 'height': new_block.height, 50 | 'txs': txs_hash_list, 51 | 'proof': proof_tx, 52 | 'block_flag': new_block.flag, 53 | } 54 | } 55 | try: 56 | await V.P2P_OBJ.send_command(cmd=Peer2PeerCmd.BROADCAST, data=data) 57 | log.info("Success broadcast new block {}".format(new_block)) 58 | update_info_for_generate() 59 | result.set_result(True) 60 | except PeerToPeerError as e: 61 | log.debug(f"unstable network '{e}'") 62 | except asyncio.TimeoutError: 63 | log.warning("Failed broadcast new block, other nodes don\'t accept {}".format(new_block.getinfo())) 64 | except asyncio.TimeoutError: 65 | if V.P2P_OBJ.f_stop: 66 | log.debug("Mined new block closed") 67 | break 68 | except BlockChainError as e: 69 | log.error('Failed mined new block "{}"'.format(e)) 70 | except Exception: 71 | log.error("mined_newblock exception", exc_info=True) 72 | 73 | # set failed signal 74 | if asyncio.isfuture(result) and not result.done(): 75 | result.set_result(False) 76 | 77 | 78 | async def send_newtx(new_tx, cur: Cursor, exc_info=True): 79 | assert V.P2P_OBJ, "PeerClient is None" 80 | try: 81 | check_tx_time(new_tx) 82 | check_tx(new_tx, include_block=None) 83 | data = { 84 | 'cmd': BroadcastCmd.NEW_TX, 85 | 'data': { 86 | 'tx': new_tx 87 | } 88 | } 89 | await V.P2P_OBJ.send_command(cmd=Peer2PeerCmd.BROADCAST, data=data) 90 | await obj.tx_builder.put_unconfirmed(cur=cur, tx=new_tx) 91 | log.info("Success broadcast new tx {}".format(new_tx)) 92 | update_info_for_generate(u_block=False, u_unspent=True) 93 | return True 94 | except ConnectionError as e: 95 | log.warning(f"retry send_newtx after 1s '{e}'") 96 | await asyncio.sleep(1.0) 97 | return await send_newtx(new_tx=new_tx, cur=cur, exc_info=exc_info) 98 | except Exception as e: 99 | log.warning("Failed broadcast new tx, other nodes don\'t accept {}".format(new_tx.getinfo())) 100 | log.warning("Reason is \"{}\"".format(e)) 101 | log.debug("traceback,", exc_info=exc_info) 102 | return False 103 | 104 | 105 | __all__ = [ 106 | "mined_newblock", 107 | "send_newtx", 108 | ] 109 | -------------------------------------------------------------------------------- /publicnode.py: -------------------------------------------------------------------------------- 1 | #!/user/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from bc4py import __version__, __chain_version__, __block_version__, __message__, __logo__ 5 | from bc4py.config import C, V 6 | from bc4py.utils import * 7 | from bc4py.exit import blocking_run 8 | from bc4py.user.generate import * 9 | from bc4py.user.boot import * 10 | from bc4py.user.network import * 11 | from bc4py.user.api import setup_rest_server 12 | from bc4py.database import obj 13 | from bc4py.database.create import check_account_db 14 | from bc4py.database.builder import setup_database_obj 15 | from bc4py.chain.msgpack import default_hook, object_hook 16 | from p2p_python.utils import setup_p2p_params, setup_server_hostname 17 | from p2p_python.server import Peer2Peer 18 | from bc4py.for_debug import set_logger 19 | import asyncio 20 | import logging 21 | import os 22 | 23 | 24 | loop = asyncio.get_event_loop() 25 | 26 | 27 | async def setup_chain(connections): 28 | p2p = V.P2P_OBJ 29 | 30 | for host, port in connections: 31 | try: 32 | if await p2p.core.create_connection(host, port): 33 | logging.info("Connect to {}:{}".format(host, port)) 34 | except Exception: 35 | logging.error("first connection", exc_info=True) 36 | 37 | # BroadcastProcess setup 38 | p2p.broadcast_check = broadcast_check 39 | 40 | # Update to newest blockchain 41 | if await obj.chain_builder.init(V.GENESIS_BLOCK, batch_size=500): 42 | # only genesisBlock yoy have, try to import bootstrap.dat.gz 43 | await load_bootstrap_file() 44 | await sync_chain_loop() 45 | 46 | # Mining/Staking setup 47 | # Debug.F_CONSTANT_DIFF = True 48 | # Debug.F_SHOW_DIFFICULTY = True 49 | # Debug.F_STICKY_TX_REJECTION = False # for debug 50 | asyncio.ensure_future(mined_newblock(mined_block_que)) 51 | logging.info("finished all initialization") 52 | 53 | 54 | def main(): 55 | p = console_args_parser() 56 | check_process_status(f_daemon=p.daemon) 57 | 58 | # environment 59 | set_database_path(sub_dir=p.sub_dir) 60 | check_already_started() 61 | setup_database_obj(txindex=p.txindex, addrindex=p.addrindex) 62 | import_keystone(passphrase='hello python') 63 | loop.run_until_complete(check_account_db()) 64 | genesis_block, genesis_params, network_ver, connections = load_boot_file() 65 | set_blockchain_params(genesis_block, genesis_params) 66 | logging.info("Start p2p network-ver{} .".format(network_ver)) 67 | 68 | # P2P network setup 69 | setup_p2p_params(network_ver=network_ver, p2p_port=p.p2p, 70 | p2p_accept=p.server, p2p_udp_accept=p.server, sub_dir=p.sub_dir) 71 | setup_server_hostname(hostname=p.hostname) 72 | p2p = Peer2Peer(default_hook=default_hook, object_hook=object_hook) 73 | p2p.event.setup_events_from_class(DirectCmd) 74 | p2p.setup() 75 | V.P2P_OBJ = p2p 76 | 77 | # setup rest server 78 | loop.run_until_complete(setup_rest_server( 79 | port=p.rest, host=p.host, extra_locals=p.extra_locals)) 80 | 81 | # setup blockchain 82 | loop.run_until_complete(setup_chain(connections)) 83 | 84 | # original logger 85 | set_logger(level=logging.getLevelName(p.log_level), path=p.log_path, f_remove=p.remove_log) 86 | logging.info(f"\n{__logo__}\n====\nsystem (str) = {__version__}\nchain (int) = {__chain_version__}\n" 87 | f"block (int) = {__block_version__}\nmessage = {__message__}") 88 | 89 | # generate (option) 90 | if p.staking: 91 | Generate(consensus=C.BLOCK_COIN_POS, power_limit=0.3) 92 | if p.capping: 93 | if os.path.isdir(p.capping): 94 | Generate(consensus=C.BLOCK_CAP_POS, power_limit=0.6, path=p.capping) 95 | else: 96 | logging.error("setting of PoC mining is wrong! capping=`{}`".format(p.capping)) 97 | if p.solo_mining: 98 | Generate(consensus=C.BLOCK_YES_POW, power_limit=0.05) 99 | Generate(consensus=C.BLOCK_X16S_POW, power_limit=0.05) 100 | Generate(consensus=C.BLOCK_X11_POW, power_limit=0.05) 101 | 102 | # block notify (option) 103 | if p.block_notify: 104 | notify_when_new_block(p.block_notify) 105 | 106 | # setup monitor (option) 107 | if p.console: 108 | try: 109 | import aiomonitor 110 | aiomonitor.start_monitor(loop, port=10010, console_port=10011) 111 | logging.info("netcat console on 127.0.0.1:10010") 112 | except ImportError: 113 | pass 114 | 115 | # blocking loop 116 | blocking_run() 117 | # auto safe exit 118 | 119 | 120 | if __name__ == '__main__': 121 | main() 122 | -------------------------------------------------------------------------------- /bc4py/chain/workhash.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, BlockChainError 2 | from bc4py_extension import poc_hash, poc_work, scope_index 3 | from bc4py.database.tools import get_output_from_input 4 | from concurrent.futures import ProcessPoolExecutor 5 | from bell_yespower import getPoWHash as yespower_hash # for CPU 6 | from x11_hash import getPoWHash as x11_hash # for ASIC 7 | from shield_x16s_hash import getPoWHash as x16s_hash # for GPU 8 | from logging import getLogger 9 | from hashlib import sha256 10 | from os import urandom 11 | from time import time 12 | import asyncio 13 | import psutil 14 | import atexit 15 | 16 | 17 | loop = asyncio.get_event_loop() 18 | log = getLogger('bc4py') 19 | 20 | 21 | def get_workhash_fnc(flag): 22 | if flag == C.BLOCK_YES_POW: 23 | return yespower_hash 24 | elif flag == C.BLOCK_X11_POW: 25 | return x11_hash 26 | elif flag == C.BLOCK_X16S_POW: 27 | return x16s_hash 28 | elif flag in C.consensus2name: 29 | raise Exception('Not found block flag {}'.format(C.consensus2name[flag])) 30 | else: 31 | raise Exception('Not found block flag {}?'.format(flag)) 32 | 33 | 34 | def get_stake_coin_hash(tx, previous_hash): 35 | # stake_hash => sha256(txhash + previous_hash) / amount 36 | assert tx.pos_amount is not None 37 | pos_work_hash = sha256(tx.hash + previous_hash).digest() 38 | work = int.from_bytes(pos_work_hash, 'little') 39 | work //= (tx.pos_amount // 100000000) 40 | return work.to_bytes(32, 'little') 41 | 42 | 43 | def update_work_hash(block): 44 | if block.flag == C.BLOCK_GENESIS: 45 | block.work_hash = b'\xff' * 32 46 | elif block.flag == C.BLOCK_COIN_POS: 47 | proof_tx = block.txs[0] 48 | if proof_tx.pos_amount is None: 49 | txhash, txindex = proof_tx.inputs[0] 50 | pair = get_output_from_input(input_hash=txhash, input_index=txindex, best_block=block) 51 | if pair is None: 52 | raise BlockChainError('Not found output {} of {}'.format(proof_tx, block)) 53 | address, coin_id, amount = pair 54 | proof_tx.pos_amount = amount 55 | block.work_hash = get_stake_coin_hash(tx=proof_tx, previous_hash=block.previous_hash) 56 | elif block.flag == C.BLOCK_CAP_POS: 57 | proof_tx = block.txs[0] 58 | address, coin_id, amount = proof_tx.outputs[0] 59 | scope_hash = poc_hash(address=address.string, nonce=block.nonce) 60 | index = scope_index(block.previous_hash) 61 | block.work_hash = poc_work( 62 | time=block.time, scope_hash=scope_hash[index * 32:index*32 + 32], previous_hash=block.previous_hash) 63 | elif block.flag == C.BLOCK_FLK_POS: 64 | raise BlockChainError("unimplemented") 65 | else: 66 | # POW_??? 67 | hash_fnc = get_workhash_fnc(block.flag) 68 | block.work_hash = hash_fnc(block.b) 69 | 70 | 71 | async def generate_many_hash(executor: ProcessPoolExecutor, block, request_num): 72 | assert request_num > 0 73 | # hash generating with multi-core 74 | future: asyncio.Future = loop.run_in_executor( 75 | executor, pow_generator, block.b, block.flag, request_num) 76 | await asyncio.wait_for(future, 120.0) 77 | binary, hashed, start = future.result() 78 | if binary is None: 79 | raise Exception(hashed) 80 | block.b = binary 81 | block.work_hash = hashed 82 | block.deserialize() 83 | return time() - start 84 | 85 | 86 | def pow_generator(binary, block_flag, request_num): 87 | start = time() 88 | try: 89 | hash_fnc = get_workhash_fnc(block_flag) 90 | hashed = hash_fnc(binary) 91 | minimum_num = int.from_bytes(hashed, 'little') 92 | new_binary = binary 93 | for _ in range(request_num): 94 | new_binary = new_binary[:-4] + urandom(4) 95 | new_hash = hash_fnc(new_binary) 96 | new_num = int.from_bytes(new_hash, 'little') 97 | if minimum_num > new_num: 98 | binary = new_binary 99 | hashed = new_hash 100 | minimum_num = new_num 101 | return binary, hashed, start 102 | except Exception as e: 103 | error = "Hashing failed {} by \"{}\"".format(binary, e) 104 | return None, error, start 105 | 106 | 107 | def get_executor_object(max_workers=None): 108 | """PoW mining process generator""" 109 | if max_workers is None: 110 | max_process_num = 4 111 | logical_cpu_num = psutil.cpu_count(logical=True) or max_process_num 112 | physical_cpu_nam = psutil.cpu_count(logical=False) or max_process_num 113 | max_workers = min(logical_cpu_num, physical_cpu_nam) 114 | executor = ProcessPoolExecutor(max_workers) 115 | atexit.register(executor.shutdown, wait=True) 116 | return executor 117 | 118 | 119 | __all__ = [ 120 | "get_workhash_fnc", 121 | "update_work_hash", 122 | "generate_many_hash", 123 | "get_executor_object", 124 | ] 125 | -------------------------------------------------------------------------------- /bc4py/user/api/ep_websocket.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import P, stream 2 | from bc4py.chain.block import Block 3 | from bc4py.chain.tx import TX 4 | from bc4py.user import Accounting 5 | from bc4py.user.api.utils import error_response 6 | from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect 7 | from logging import getLogger, INFO 8 | from typing import List 9 | import asyncio 10 | import json 11 | 12 | loop = asyncio.get_event_loop() 13 | log = getLogger('bc4py') 14 | getLogger('websockets').setLevel(INFO) 15 | 16 | number = 0 17 | clients: List['WsClient'] = list() 18 | client_lock = asyncio.Lock() 19 | 20 | 21 | CMD_NEW_BLOCK = 'Block' 22 | CMD_NEW_TX = 'TX' 23 | CMD_NEW_ACCOUNTING = 'Accounting' 24 | CMD_ERROR = 'Error' 25 | 26 | 27 | async def websocket_route(ws: WebSocket = None, is_public=True): 28 | """ 29 | websocket public stream 30 | """ 31 | if ws is None: 32 | return error_response('This endpoint is for websocket. You do not send with upgrade header.') 33 | 34 | await ws.accept() 35 | async with client_lock: 36 | clients.append(WsClient(ws, is_public)) 37 | while not P.F_STOP: 38 | try: 39 | item = await asyncio.wait_for(ws.receive_json(), 0.2) 40 | # receive command 41 | # warning: not implemented, no function 42 | data = { 43 | 'connect': len(clients), 44 | 'is_public': is_public, 45 | 'echo': item, 46 | } 47 | await ws.send_text(get_json_format(cmd='debug', data=data)) 48 | except (asyncio.TimeoutError, TypeError): 49 | if ws.application_state == WebSocketState.DISCONNECTED: 50 | log.debug("websocket already closed") 51 | break 52 | except WebSocketDisconnect: 53 | log.debug("websocket disconnected") 54 | break 55 | except Exception: 56 | log.error('websocket_route exception', exc_info=True) 57 | break 58 | await ws.close() 59 | log.debug("close {}".format(ws)) 60 | 61 | 62 | async def private_websocket_route(ws: WebSocket = None): 63 | """ 64 | websocket private stream 65 | """ 66 | if ws is None: 67 | return error_response('This endpoint is for websocket. You do not send with upgrade header.') 68 | 69 | await websocket_route(ws, False) 70 | 71 | 72 | class WsClient(object): 73 | 74 | def __init__(self, ws: WebSocket, is_public: bool): 75 | global number 76 | number += 1 77 | self.number = number 78 | self.ws = ws 79 | self.is_public = is_public 80 | 81 | def __repr__(self): 82 | ws_type = 'Pub' if self.is_public else 'Pri' 83 | return f"" 84 | 85 | async def close(self): 86 | if self.ws.application_state != WebSocketState.DISCONNECTED: 87 | await self.ws.close() 88 | async with client_lock: 89 | if self in clients: 90 | clients.remove(self) 91 | 92 | async def send(self, data: str): 93 | assert client_lock.locked() 94 | if self.ws.application_state == WebSocketState.DISCONNECTED: 95 | clients.remove(self) 96 | else: 97 | await self.ws.send_text(data) 98 | 99 | 100 | def get_json_format(cmd, data, status=True): 101 | send_data = { 102 | 'cmd': cmd, 103 | 'data': data, 104 | 'status': status, 105 | } 106 | return json.dumps(send_data) 107 | 108 | 109 | async def broadcast_clients(cmd, data, status=True, is_public=False): 110 | """broadcast to all clients""" 111 | message = get_json_format(cmd=cmd, data=data, status=status) 112 | async with client_lock: 113 | for client in clients: 114 | if is_public or not client.is_public: 115 | try: 116 | await client.send(message) 117 | except Exception: 118 | log.error("broadcast_clients exception", exc_info=True) 119 | 120 | 121 | def websocket_reactive_stream(data): 122 | """receive Block/TX data from stream""" 123 | if P.F_STOP: 124 | pass 125 | elif P.F_NOW_BOOTING: 126 | pass 127 | elif isinstance(data, Block): 128 | asyncio.ensure_future(broadcast_clients(CMD_NEW_BLOCK, data.getinfo(), is_public=True)) 129 | elif isinstance(data, TX): 130 | asyncio.ensure_future(broadcast_clients(CMD_NEW_TX, data.getinfo(), is_public=True)) 131 | elif isinstance(data, Accounting): 132 | data = { 133 | 'txhash': None if getattr(data, 'txhash', None) is None else data.txhash.hex(), 134 | 'balance': dict(data), 135 | } 136 | asyncio.ensure_future(broadcast_clients(CMD_NEW_ACCOUNTING, data, is_public=False)) 137 | else: 138 | pass 139 | 140 | 141 | # get new Block/TX object from reactive stream 142 | stream.subscribe(on_next=websocket_reactive_stream, on_error=log.error) 143 | 144 | 145 | __all__ = [ 146 | "CMD_ERROR", 147 | "CMD_NEW_BLOCK", 148 | "CMD_NEW_TX", 149 | "CMD_NEW_ACCOUNTING", 150 | "websocket_route", 151 | "private_websocket_route", 152 | "broadcast_clients", 153 | ] 154 | -------------------------------------------------------------------------------- /bc4py/config.py: -------------------------------------------------------------------------------- 1 | from p2p_python.server import Peer2Peer 2 | from bc4py_extension import PyAddress 3 | from uvicorn import Server 4 | from rx.subject import Subject 5 | from typing import Optional 6 | 7 | 8 | # internal stream by ReactiveX 9 | # close by `stream.dispose()` 10 | # doc: https://github.com/ReactiveX/RxPY/blob/develop/notebooks/Getting%20Started.ipynb 11 | stream = Subject() 12 | 13 | 14 | class C: # Constant 15 | # base currency info 16 | BASE_CURRENCY = { 17 | 'name': 'PyCoin', 18 | 'unit': 'PC', 19 | 'digit': 8, 20 | 'address': 'NDUMMYADDRESSAAAAAAAAAAAACRSTTMF', 21 | 'description': 'Base currency', 22 | 'image': None 23 | } 24 | 25 | # consensus 26 | BLOCK_GENESIS = 0 27 | BLOCK_COIN_POS = 1 28 | BLOCK_CAP_POS = 2 # proof of capacity 29 | BLOCK_FLK_POS = 3 # proof of fund-lock 30 | BLOCK_YES_POW = 5 31 | BLOCK_X11_POW = 6 32 | BLOCK_X16S_POW = 9 33 | consensus2name = { 34 | BLOCK_GENESIS: 'GENESIS', 35 | BLOCK_COIN_POS: 'POS_COIN', 36 | BLOCK_CAP_POS: 'POS_CAP', 37 | BLOCK_FLK_POS: 'POS_FLK', 38 | BLOCK_YES_POW: 'POW_YES', 39 | BLOCK_X11_POW: 'POW_X11', 40 | BLOCK_X16S_POW: 'POW_X16S', 41 | } 42 | 43 | # tx type 44 | TX_GENESIS = 0 # Height0の初期設定TX 45 | TX_POW_REWARD = 1 # POWの報酬TX 46 | TX_POS_REWARD = 2 # POSの報酬TX 47 | TX_TRANSFER = 3 # 送受金 48 | TX_MINT_COIN = 4 # 新規貨幣を鋳造 49 | TX_INNER = 255 # 内部のみで扱うTX 50 | txtype2name = { 51 | TX_GENESIS: 'GENESIS', 52 | TX_POW_REWARD: 'POW_REWARD', 53 | TX_POS_REWARD: 'POS_REWARD', 54 | TX_TRANSFER: 'TRANSFER', 55 | TX_MINT_COIN: 'MINT_COIN', 56 | TX_INNER: 'TX_INNER' 57 | } 58 | 59 | # message format 60 | MSG_NONE = 0 # no message 61 | MSG_PLAIN = 1 # 明示的にunicode 62 | MSG_BYTE = 2 # 明示的にbinary 63 | MSG_MSGPACK = 3 # msgpack protocol 64 | MSG_HASHLOCKED = 4 # hash-locked transaction 65 | msg_type2name = { 66 | MSG_NONE: 'NONE', 67 | MSG_PLAIN: 'PLAIN', 68 | MSG_BYTE: 'BYTE', 69 | MSG_MSGPACK: 'MSGPACK', 70 | MSG_HASHLOCKED: 'HASHLOCKED' 71 | } 72 | 73 | # difficulty 74 | DIFF_RETARGET = 20 # difficultyの計算Block数 75 | 76 | # address params 77 | ADDR_NORMAL_VER = 0 78 | BIP44_COIN_TYPE = 0x800001f8 # 504 79 | 80 | # block params 81 | MATURE_HEIGHT = 20 # 採掘されたBlockのOutputsが成熟する期間 82 | 83 | # account 84 | ANT_UNKNOWN = 0 # Unknown user 85 | ANT_STAKED = 3 # Staked balance 86 | account2name = { 87 | ANT_UNKNOWN: '@Unknown', 88 | ANT_STAKED: '@Staked', 89 | } 90 | 91 | # hierarchical wallet 92 | GAP_ADDR_LIMIT = 20 # address gap limit 93 | GAP_USER_LIMIT = 5 # account gap limit 94 | 95 | # Block/TX/Fee limit 96 | SIZE_BLOCK_LIMIT = 300 * 1000 # 300kb block 97 | SIZE_TX_LIMIT = 100 * 1000 # 100kb tx 98 | MEMORY_FILE_REFRESH_SPAN = 101 # memory_file refresh span 99 | MEMORY_CACHE_LIMIT = 250 # max memorized block size, means re-org limit 100 | MEMORY_BATCH_SIZE = 30 101 | MINTCOIN_GAS = int(10 * pow(10, 6)) # 新規Mintcoin発行GasFee 102 | SIGNATURE_GAS = int(0.01 * pow(10, 6)) # gas per one signature 103 | EXTRA_OUTPUT_REWARD_FEE = int(0.0001 * pow(10, 8)) # subtract EXTRA_OUTPUT fee from reward 104 | 105 | # network params 106 | ACCEPT_MARGIN_TIME = 180 # New block acceptance time margin (sec) 107 | MAX_RECURSIVE_BLOCK_DEPTH = 30 # recursive accept block limit 108 | 109 | # sqlite params 110 | SQLITE_CACHE_SIZE = 2000 # default size: 2000 111 | SQLITE_JOURNAL_MODE = 'MEMORY' # default journal mode: MEMORY 112 | SQLITE_SYNC_MODE = 'NORMAL' # default sync mode: NORMAL 113 | 114 | 115 | class V: 116 | # Blockchain basic params 117 | GENESIS_BLOCK = None 118 | GENESIS_PARAMS = None 119 | BLOCK_GENESIS_TIME = None 120 | BLOCK_TIME_SPAN = None 121 | BLOCK_MINING_SUPPLY = None 122 | BLOCK_REWARD = None 123 | BLOCK_BASE_CONSENSUS = None 124 | BLOCK_CONSENSUSES = None 125 | 126 | # base coin 127 | COIN_DIGIT = None 128 | COIN_MINIMUM_PRICE = None # Gasの最小Price 129 | 130 | # database path 131 | DB_HOME_DIR = None 132 | DB_ACCOUNT_PATH = None 133 | 134 | # Wallet 135 | BECH32_HRP = None # human readable part 136 | EXTENDED_KEY_OBJ = None # object 137 | 138 | # mining 139 | MINING_ADDRESS: Optional['PyAddress'] = None 140 | P2P_OBJ: Optional['Peer2Peer'] = None # P2P peer client object 141 | API_OBJ: Optional['Server'] = None # REST API object 142 | 143 | # developer 144 | BRANCH_NAME: Optional[str] = None # Github branch name 145 | SOURCE_HASH: Optional[str] = None # bc4py source sha1 hash 146 | 147 | 148 | class P: # 起動中もダイナミックに変化 149 | F_STOP = False # Stop signal 150 | F_NOW_BOOTING = True # Booting mode flag 151 | 152 | 153 | class Debug: 154 | F_SHOW_DIFFICULTY = False 155 | F_CONSTANT_DIFF = False 156 | F_STICKY_TX_REJECTION = True 157 | 158 | 159 | class BlockChainError(Exception): 160 | pass 161 | 162 | 163 | __all__ = [ 164 | 'stream', 165 | 'C', 166 | 'V', 167 | 'P', 168 | 'Debug', 169 | 'BlockChainError', 170 | ] 171 | 172 | -------------------------------------------------------------------------------- /bc4py/user/api/ep_wallet.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, V 2 | from bc4py.bip32 import Bip32, BIP32_HARDEN, get_address 3 | from bc4py.database.create import create_db 4 | from bc4py.database.account import * 5 | from bc4py.user.api.utils import error_response 6 | from fastapi.utils import BaseModel 7 | from bc4py_extension import PyAddress 8 | from multi_party_schnorr import PyKeyPair 9 | from mnemonic import Mnemonic 10 | from binascii import a2b_hex 11 | from logging import getLogger 12 | import asyncio 13 | 14 | log = getLogger('bc4py') 15 | 16 | language = 'english' 17 | length_list = [128, 160, 192, 224, 256] 18 | loop = asyncio.get_event_loop() 19 | 20 | 21 | class WalletFormat(BaseModel): 22 | passphrase: str = '' 23 | length: int = 256 24 | 25 | 26 | class PrivateKeyFormat(BaseModel): 27 | private_key: str 28 | address: str 29 | account: str = C.account2name[C.ANT_UNKNOWN] 30 | 31 | 32 | async def new_address(account: str = C.account2name[C.ANT_UNKNOWN], newly: bool = False): 33 | """ 34 | This end-point create new address. 35 | * Arguments 36 | 1. **account** : account name default="@Unknown" 37 | 2. **newly** : create a new address that is not generated yet 38 | """ 39 | async with create_db(V.DB_ACCOUNT_PATH) as db: 40 | cur = await db.cursor() 41 | user_id = await read_name2userid(account, cur) 42 | addr = None 43 | 44 | if not newly: 45 | addr = await read_unused_address(user_id, False, cur) 46 | 47 | if addr is None: 48 | newly = True 49 | addr = await generate_new_address_by_userid(user_id, cur) 50 | 51 | await db.commit() 52 | return { 53 | 'account': account, 54 | 'user_id': user_id, 55 | 'address': addr.string, 56 | 'version': addr.version, 57 | 'identifier': addr.identifier().hex(), 58 | 'newly': newly 59 | } 60 | 61 | 62 | async def get_keypair(address: str): 63 | """ 64 | This end-point show keypair info of address. 65 | * Arguments 66 | 1. **address** : (string, required) 67 | """ 68 | try: 69 | async with create_db(V.DB_ACCOUNT_PATH) as db: 70 | cur = await db.cursor() 71 | uuid, keypair, path = await read_address2keypair(PyAddress.from_string(address), cur) 72 | return { 73 | 'uuid': uuid, 74 | 'address': address, 75 | 'private_key': keypair.get_secret_key().hex(), 76 | 'public_key': keypair.get_public_key().hex(), 77 | 'path': path 78 | } 79 | except Exception: 80 | return error_response() 81 | 82 | 83 | async def create_wallet(wallet: WalletFormat): 84 | """ 85 | This end-point generate new keystone.json data. 86 | * Arguments 87 | 1. **passphrase** : (string, optional, default="") encrypt passphrase 88 | 2. **strength** : (numeric, optional, default=256) entropy bit length (12, 15, 18, 21, 24) 89 | """ 90 | try: 91 | if wallet.length not in length_list: 92 | return error_response('length is {}'.format(length_list)) 93 | mnemonic = Mnemonic(language).generate(wallet.length) 94 | seed = Mnemonic.to_seed(mnemonic, wallet.passphrase) 95 | root = Bip32.from_entropy(seed) 96 | bip = root.child_key(44 + BIP32_HARDEN).child_key(C.BIP44_COIN_TYPE) 97 | # keystone.json format 98 | return { 99 | 'mnemonic': mnemonic, 100 | 'passphrase': wallet.passphrase, 101 | 'account_secret_key': bip.extended_key(True), 102 | 'account_public_key': bip.extended_key(False), 103 | 'path': bip.path, 104 | 'comment': 'You must recode "mnemonic" and "passphrase" and remove after. ' 105 | 'You can remove "account_secret_key" but you cannot sign and create new account', 106 | } 107 | except Exception: 108 | return error_response() 109 | 110 | 111 | async def import_private_key(key: PrivateKeyFormat): 112 | """ 113 | This end-point import privateKey by manual. 114 | * Arguments 115 | 1. **private_key** : (hex string, required) 116 | 2. **address** : (string, required) check by this 117 | 3. **account** : (string, optional, default="@Unknown") insert to account 118 | """ 119 | try: 120 | sk = a2b_hex(key.private_key) 121 | ck = PyAddress.from_string(key.address) 122 | keypair: PyKeyPair = PyKeyPair.from_secret_key(sk) 123 | check_ck = get_address(pk=keypair.get_public_key(), hrp=V.BECH32_HRP, ver=C.ADDR_NORMAL_VER) 124 | if ck != check_ck: 125 | return error_response('Don\'t match, {}!={}'.format(ck, check_ck)) 126 | async with create_db(V.DB_ACCOUNT_PATH) as db: 127 | cur = await db.cursor() 128 | user = await read_name2userid(name=key.account, cur=cur) 129 | await insert_keypair_from_outside(sk=sk, ck=ck, user=user, cur=cur) 130 | await db.commit() 131 | return {'status': True} 132 | except Exception: 133 | return error_response() 134 | 135 | 136 | __all__ = [ 137 | "new_address", 138 | "get_keypair", 139 | "create_wallet", 140 | "import_private_key", 141 | ] 142 | -------------------------------------------------------------------------------- /bc4py/user/txcreation/transfer.py: -------------------------------------------------------------------------------- 1 | from bc4py import __chain_version__ 2 | from bc4py.config import C, V, BlockChainError 3 | from bc4py.chain.tx import TX 4 | from bc4py.database.account import insert_movelog, read_address2userid 5 | from bc4py.user import Balance, Accounting 6 | from bc4py.user.txcreation.utils import * 7 | from bc4py_extension import PyAddress 8 | from time import time 9 | 10 | 11 | async def send_many(sender, 12 | send_pairs, 13 | cur, 14 | fee_coin_id=0, 15 | gas_price=None, 16 | msg_type=C.MSG_NONE, 17 | msg_body=b'', 18 | subtract_fee_from_amount=False, 19 | retention=10800): 20 | assert isinstance(sender, int), 'Sender is user id' 21 | assert 0 < len(send_pairs), 'Empty send_pairs' 22 | # send_pairs check 23 | movements = Accounting() 24 | send_coins = Balance() 25 | outputs = list() 26 | coins = Balance() 27 | for address, coin_id, amount in send_pairs: 28 | assert isinstance(address, PyAddress) 29 | assert isinstance(coin_id, int) and isinstance(amount, int), 'CoinID, amount is int' 30 | coins[coin_id] += amount 31 | outputs.append((address, coin_id, amount)) 32 | user = await read_address2userid(address=address, cur=cur) 33 | if user is not None: 34 | movements[user][coin_id] += amount # send to myself 35 | movements[sender] -= coins 36 | # movements[C.ANT_OUTSIDE] += coins 37 | # tx 38 | now = int(time() - V.BLOCK_GENESIS_TIME) 39 | tx = TX.from_dict( 40 | tx={ 41 | 'version': __chain_version__, 42 | 'type': C.TX_TRANSFER, 43 | 'time': now, 44 | 'deadline': now + retention, 45 | 'inputs': list(), 46 | 'outputs': outputs, 47 | 'gas_price': gas_price or V.COIN_MINIMUM_PRICE, 48 | 'gas_amount': 1, 49 | 'message_type': msg_type, 50 | 'message': msg_body 51 | }) 52 | tx.gas_amount = tx.size + C.SIGNATURE_GAS 53 | # fill unspents 54 | input_address = await fill_inputs_outputs(tx=tx, cur=cur, fee_coin_id=fee_coin_id) 55 | # subtract fee from amount 56 | if subtract_fee_from_amount: 57 | if fee_coin_id != 0: 58 | raise BlockChainError('subtract_fee option require fee_coin_id=0') 59 | subtract_fee = subtract_fee_from_user_balance(tx) 60 | # fee returns to sender's balance 61 | movements[sender][0] += subtract_fee 62 | send_coins[0] -= subtract_fee 63 | fee_coins = Balance(coin_id=fee_coin_id, amount=tx.gas_price * tx.gas_amount) 64 | # check enough balance account have 65 | for address, coin_id, amount in send_pairs: 66 | send_coins[coin_id] += amount 67 | await check_enough_amount(sender=sender, send_coins=send_coins, fee_coins=fee_coins, cur=cur) 68 | # replace dummy address 69 | await replace_redeem_dummy_address(tx, cur) 70 | # setup signature 71 | tx.serialize() 72 | await add_sign_by_address(tx, input_address, cur) 73 | movements[sender] -= fee_coins 74 | # movements[C.ANT_OUTSIDE] += fee_coins 75 | await insert_movelog(movements, cur, tx.type, tx.time, tx.hash) 76 | return tx 77 | 78 | 79 | async def send_from( 80 | sender, 81 | address, 82 | coins, 83 | cur, 84 | fee_coin_id=0, 85 | gas_price=None, 86 | msg_type=C.MSG_NONE, 87 | msg_body=b'', 88 | subtract_fee_amount=False, 89 | retention=10800): 90 | assert isinstance(coins, Balance) 91 | assert isinstance(address, PyAddress) 92 | send_pairs = list() 93 | for coin_id, amount in coins: 94 | send_pairs.append((address, coin_id, amount)) 95 | return await send_many(sender=sender, send_pairs=send_pairs, cur=cur, fee_coin_id=fee_coin_id, 96 | gas_price=gas_price, msg_type=msg_type, msg_body=msg_body, 97 | subtract_fee_from_amount=subtract_fee_amount, retention=retention) 98 | 99 | 100 | def subtract_fee_from_user_balance(tx: TX): 101 | """subtract fee from user's sending outputs""" 102 | subtract_fee = tx.gas_amount * tx.gas_price 103 | f_subtracted = False 104 | f_added = False 105 | for index, (address, coin_id, amount) in enumerate(tx.outputs): 106 | if coin_id != 0: 107 | continue 108 | elif amount < subtract_fee: 109 | continue 110 | elif not f_added and address == DUMMY_REDEEM_ADDRESS: 111 | # add used fee to redeem output 112 | tx.outputs[index] = (address, coin_id, amount + subtract_fee) 113 | f_added = True 114 | elif not f_subtracted and address != DUMMY_REDEEM_ADDRESS: 115 | # subtract used fee from sending output 116 | tx.outputs[index] = (address, coin_id, amount - subtract_fee) 117 | f_subtracted = True 118 | else: 119 | continue 120 | # check 121 | if f_subtracted is False or f_added is False: 122 | raise BlockChainError('failed to subtract fee sub={} add={} fee={}' 123 | .format(f_subtracted, f_added, subtract_fee)) 124 | return subtract_fee 125 | 126 | 127 | __all__ = [ 128 | "send_from", 129 | "send_many", 130 | ] 131 | -------------------------------------------------------------------------------- /bc4py/user/api/ep_system.py: -------------------------------------------------------------------------------- 1 | from bc4py import __version__, __chain_version__, __message__ 2 | from bc4py.config import C, V, P 3 | from bc4py.chain.utils import GompertzCurve, DEFAULT_TARGET 4 | from bc4py.chain.difficulty import get_bits_by_hash, get_bias_by_hash 5 | from bc4py.database import obj 6 | from bc4py.user.api.utils import error_response, local_address 7 | from bc4py.user.generate import generating_threads 8 | from time import time 9 | import p2p_python.config 10 | import p2p_python 11 | 12 | MAX_256_FLOAT = float(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) 13 | start_time = int(time()) 14 | F_ADD_CACHE_INFO = False # to adjust cache size 15 | 16 | __api_version__ = '0.0.2' 17 | 18 | 19 | async def chain_info(): 20 | """ 21 | This end-point show blockchain status of node. 22 | """ 23 | try: 24 | best_height = obj.chain_builder.best_block.height 25 | best_block = obj.chain_builder.best_block 26 | best_block_info = best_block.getinfo() 27 | best_block_info['hex'] = best_block.b.hex() 28 | old_block_height = obj.chain_builder.best_chain[0].height - 1 29 | old_block_hash = obj.chain_builder.get_block_hash(old_block_height).hex() 30 | data = {'best': best_block_info} 31 | difficulty = dict() 32 | for consensus, ratio in V.BLOCK_CONSENSUSES.items(): 33 | name = C.consensus2name[consensus] 34 | bits, target = get_bits_by_hash(previous_hash=best_block.hash, consensus=consensus) 35 | target = float(target) 36 | block_time = round(V.BLOCK_TIME_SPAN / ratio * 100) 37 | diff = round(DEFAULT_TARGET / target, 8) 38 | bias = get_bias_by_hash(previous_hash=best_block.previous_hash, consensus=consensus) 39 | difficulty[name] = { 40 | 'number': consensus, 41 | 'bits': bits.to_bytes(4, 'big').hex(), 42 | 'diff': round(diff, 8), 43 | 'bias': round(bias, 8), 44 | 'fixed_diff': round(diff / bias, 8), 45 | 'hashrate(kh/s)': round((MAX_256_FLOAT/target) / block_time / 1000, 3) 46 | } 47 | data['mining'] = difficulty 48 | data['size'] = best_block.size 49 | data['checkpoint'] = {'height': old_block_height, 'blockhash': old_block_hash} 50 | data['money_supply'] = GompertzCurve.calc_total_supply(best_height) 51 | data['total_supply'] = GompertzCurve.k 52 | if F_ADD_CACHE_INFO: 53 | data['cache'] = { 54 | 'get_bits_by_hash': str(get_bits_by_hash.cache_info()), 55 | 'get_bias_by_hash': str(get_bias_by_hash.cache_info()) 56 | } 57 | return data 58 | except Exception: 59 | return error_response() 60 | 61 | 62 | async def chain_fork_info(): 63 | """ 64 | This end-point show blockchain fork info. 65 | """ 66 | try: 67 | best_chain = obj.chain_builder.best_chain 68 | main_chain = [block.getinfo() for block in best_chain] 69 | orphan_chain = [block.getinfo() for block in obj.chain_builder.chain.values() if block not in best_chain] 70 | return { 71 | 'main': main_chain, 72 | 'orphan': sorted(orphan_chain, key=lambda x: x['height']), 73 | 'root': obj.chain_builder.root_block.getinfo(), 74 | } 75 | except Exception: 76 | return error_response() 77 | 78 | 79 | async def system_info(): 80 | """ 81 | This end-point show public system info. 82 | """ 83 | return { 84 | 'network_ver': p2p_python.config.V.NETWORK_VER, 85 | 'system_ver': __version__, 86 | 'api_ver': __api_version__, 87 | 'chain_ver': __chain_version__, 88 | 'p2p_ver': p2p_python.__version__, 89 | 'message': __message__, 90 | 'booting': P.F_NOW_BOOTING, 91 | 'connections': len(V.P2P_OBJ.core.user), 92 | 'access_time': int(time()), 93 | 'start_time': start_time, 94 | 'genesis_time': V.BLOCK_GENESIS_TIME, 95 | } 96 | 97 | 98 | async def system_private_info(): 99 | """ 100 | This end-point show private system info. 101 | """ 102 | try: 103 | return { 104 | 'branch': V.BRANCH_NAME, 105 | 'source_hash': V.SOURCE_HASH, 106 | 'directory': V.DB_HOME_DIR, 107 | 'unconfirmed': [txhash.hex() for txhash in obj.tx_builder.memory_pool.list_all_hash()], 108 | 'generate_threads': [str(s) for s in generating_threads], 109 | 'local_address': list(local_address), 110 | 'prefetch_address': len(obj.account_builder.pre_fetch_addr), 111 | 'extended_key': repr(V.EXTENDED_KEY_OBJ), 112 | } 113 | except Exception: 114 | return error_response() 115 | 116 | 117 | async def network_info(): 118 | """ 119 | This end-point show network connection info. 120 | """ 121 | try: 122 | peers = list() 123 | data = V.P2P_OBJ.core.get_my_user_header() 124 | for user in V.P2P_OBJ.core.user: 125 | peers.append(user.getinfo()) 126 | data['peers'] = peers 127 | return data 128 | except Exception: 129 | return error_response() 130 | 131 | 132 | __all__ = [ 133 | "__api_version__", 134 | "chain_info", 135 | "chain_fork_info", 136 | "system_info", 137 | "system_private_info", 138 | "network_info", 139 | ] 140 | -------------------------------------------------------------------------------- /doc/AboutPoC.md: -------------------------------------------------------------------------------- 1 | About proof of capacity 2 | ==== 3 | PoC is a mining algorithm using storage media such as HDD. 4 | [BurstCoin](https://burstwiki.org/wiki/Main_Page) is famous for a currency that implement PoC. 5 | I used [blake2b](https://blake2.net/) for a hash function. It's very fast and very optimized for multi-processor. 6 | 7 | Tools 8 | ---- 9 | plotting tool [bc4py_plotter](http://github.com/namuyan/bc4py_plotter/) work on Linux/Windows. 10 | 11 | Algorithm 12 | ---- 13 | 1. We generate seed and push hash0 to top. 14 | ```text 15 | +---------+----------------+----------------------+ 16 | | hash0 | ver+identifier | nonce | 17 | | 64bytes | 21 bytes | little endian 4bytes | 18 | +---------+----------------+----------------------+ 19 | ^ 20 | | | | 21 | | +-------------+ blake2b +--------------+ 22 | | | 23 | | v 24 | | +----------+ 25 | +------------------ | hash0 | 26 | +----------+ 27 | ``` 28 | 2. We generate hashN and push to top. source of hashN is only the last 1024 generated bytes. 29 | ```text 30 | +---------+---------+ +---------+----------------+----------------------+ 31 | | hashN | hashN-1 | | hash0 | ver+identifier | nonce | 32 | | 64bytes | 64bytes | XXX | 64bytes | 21 bytes | little endian 4bytes | 33 | +---------+---------+ +---------+----------------+----------------------+ 34 | ^ 35 | | | | 36 | | +----- max size 1024bytes ------+ blake2b +---//----+ 37 | | | 38 | | v 39 | | +----------+ 40 | +------------------------------------ | hashN | 41 | +----------+ 42 | ``` 43 | 3. Once we have created 8192 hashes, we are now going to make a final hash. 44 | ```text 45 | +----------+----------+ +----------+----------------+----------------------+ 46 | | hash8191 | hash8190 | | hash0 | ver+identifier | nonce | 47 | | 64bytes | 64bytes | XXX | 64bytes | 21 bytes | little endian 4bytes | 48 | +----------+----------+ +----------+----------------+----------------------+ 49 | 50 | | | 51 | +---------+ blake2b +--------------------------------------------------------+ 52 | | 53 | v 54 | +-----------+ 55 | | finalHash | 56 | +-----------+ 57 | ``` 58 | 4. All hashes XOR with the final hash and split to 2 scopes. 59 | ```text 60 | +-------------------------+ +-------------------------+ 61 | | hash0 | | hash8191 | 62 | | 64bytes | XXX | 64bytes | 63 | +-------------------------+ +-------------------------+ 64 | | | | 65 | XOR XOR XOR 66 | | | | 67 | v v v 68 | +-------------------------+ +-------------------------+ 69 | | hash'0 | | hash'8191 | 70 | | 64bytes | XXX | 64bytes | 71 | +-------------------------+ +-------------------------+ 72 | | | | | | 73 | | | | | | 74 | v v v v v 75 | +-------------------------+ +-------------------------+ 76 | | scope0 | scope1 | | scope16382 | scope16383 | 77 | | 32bytes | 32bytes | XXX | 32bytes | 32bytes | 78 | +-------------------------+ +-------------------------+ 79 | ``` 80 | 5. We get a scope index by `int(previousHash) % 16384` and define ths indexed hash a scopeHash. 81 | 6. We generate a pre-workHash from 3 params. Then, we define first 32 bytes a workHash. 82 | ```text 83 | +------------+ +------------+ 84 | | scope0 | | scope16383 | 85 | | 32bytes | X X X X X X | 32bytes | 86 | +------------+ | +------------+ 87 | | 88 | +-----------+ 89 | | 90 | v 91 | +----------------------+ +----------------------+ +----------------------+ 92 | | blocktime | | scopeHash | | previousHash | 93 | | little endian 4bytes | | 32bytes | | 32bytes | 94 | +----------------------+ +----------------------+ +----------------------+ 95 | 96 | | | 97 | +----------------------------+ blake2b +--------------------------------+ 98 | v 99 | +----------------------+ 100 | | pre workHash 64bytes | 101 | +---+------------------+ 102 | v 103 | +----------+ 104 | | workHash | 105 | | 32bytes | 106 | +----------+ 107 | ``` 108 | 109 | References 110 | ---- 111 | * [Technical information to create plot files](https://burstwiki.org/wiki/Technical_information_to_create_plot_files) 112 | * [Technical information about mining and block forging](https://burstwiki.org/wiki/Technical_information_about_mining_and_block_forging) 113 | -------------------------------------------------------------------------------- /bc4py/chain/difficulty.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, V, Debug 2 | from bc4py.database import obj 3 | from bc4py.chain.utils import bits2target, target2bits 4 | from functools import lru_cache 5 | 6 | 7 | # https://github.com/zawy12/difficulty-algorithms/issues/3 8 | 9 | # // LWMA-2 difficulty algorithm (commented version) 10 | # // Copyright (c) 2017-2018 Zawy, MIT License 11 | # // https://github.com/zawy12/difficulty-algorithms/issues/3 12 | # // Bitcoin clones must lower their FTL. 13 | # // Cryptonote et al coins must make the following changes: 14 | # // #define BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW 11 15 | # // #define CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT 3 * DIFFICULTY_TARGET 16 | # // #define DIFFICULTY_WINDOW 60 // 45, 60, & 90 for T=600, 120, & 60. 17 | # // Bytecoin / Karbo clones may not have the following 18 | # // #define DIFFICULTY_BLOCKS_COUNT DIFFICULTY_WINDOW+1 19 | # // The BLOCKS_COUNT is to make timestamps & cumulative_difficulty vectors size N+1 20 | # // Do not sort timestamps. 21 | # // CN coins (but not Monero >= 12.3) must deploy the Jagerman MTP Patch. See: 22 | # // https://github.com/loki-project/loki/pull/26 or 23 | # // https://github.com/graft-project/GraftNetwork/pull/118/files 24 | 25 | 26 | BASE_TARGET = 0x00000000ffff0000000000000000000000000000000000000000000000000000 27 | MAX_BITS = 0x1f0fffff 28 | MAX_TARGET = bits2target(MAX_BITS) 29 | GENESIS_PREVIOUS_HASH = b'\xff' * 32 30 | MAX_SEARCH_BLOCKS = 1000 31 | 32 | 33 | @lru_cache(maxsize=1024) 34 | def get_block_from_cache(blockhash): 35 | """return namedTuple block header with lru_cache""" 36 | return obj.chain_builder.get_block_header(blockhash) 37 | 38 | 39 | @lru_cache(maxsize=256) 40 | def get_bits_by_hash(previous_hash, consensus): 41 | if Debug.F_CONSTANT_DIFF: 42 | return MAX_BITS, MAX_TARGET 43 | elif previous_hash == GENESIS_PREVIOUS_HASH: 44 | return MAX_BITS, MAX_TARGET 45 | 46 | # T= 47 | T = round(V.BLOCK_TIME_SPAN / V.BLOCK_CONSENSUSES[consensus] * 100) 48 | 49 | # height -1 = most recently solved block number 50 | # target = 1/difficulty/2^x where x is leading zeros in coin's max_target, I believe 51 | # Recommended N: 52 | N = int(45 * (600 / T) ** 0.3) 53 | 54 | # To get a more accurate solvetime to within +/- ~0.2%, use an adjustment factor. 55 | # This technique has been shown to be accurate in 4 coins. 56 | # In a formula: 57 | # [edit by zawy: since he's using target method, adjust should be 0.998. This was my mistake. ] 58 | adjust = 0.9989 ** (500 / N) 59 | K = int((N + 1) / 2 * adjust * T) 60 | # Bitcoin_gold T=600, N=45, K=13632 61 | 62 | # Loop through N most recent blocks. "< height", not "<=". 63 | # height-1 = most recently solved rblock 64 | target_hash = previous_hash 65 | timestamp = list() 66 | target = list() 67 | j = 0 68 | for _ in range(MAX_SEARCH_BLOCKS): 69 | target_block = get_block_from_cache(target_hash) 70 | if target_block is None: 71 | return MAX_BITS, MAX_TARGET 72 | if target_block.flag != consensus: 73 | target_hash = target_block.previous_hash 74 | continue 75 | if j == N + 1: 76 | break 77 | j += 1 78 | timestamp.insert(0, target_block.time) 79 | target.insert(0, bits2target(target_block.bits)) 80 | target_hash = target_block.previous_hash 81 | if target_hash == GENESIS_PREVIOUS_HASH: 82 | return MAX_BITS, MAX_TARGET 83 | else: 84 | # search too many block 85 | if len(target) < 2: 86 | # not found any mined blocks 87 | return MAX_BITS, MAX_TARGET 88 | else: 89 | # May have been a sudden difficulty raise 90 | # overwrite N param 91 | N = len(timestamp) - 1 92 | 93 | sum_target = t = j = 0 94 | for i in range(N): 95 | solve_time = max(0, timestamp[i + 1] - timestamp[i]) 96 | j += 1 97 | t += solve_time * j 98 | sum_target += target[i + 1] 99 | 100 | # Keep t reasonable in case strange solvetimes occurred. 101 | if t < N * K // 3: 102 | t = N * K // 3 103 | 104 | new_target = t * sum_target // K // N // N 105 | if MAX_TARGET < new_target: 106 | return MAX_BITS, MAX_TARGET 107 | 108 | # convert new target to bits 109 | new_bits = target2bits(new_target) 110 | if Debug.F_SHOW_DIFFICULTY: 111 | print("ratio", C.consensus2name[consensus], new_bits, previous_hash.hex()) 112 | return new_bits, new_target 113 | 114 | 115 | @lru_cache(maxsize=256) 116 | def get_bias_by_hash(previous_hash, consensus) -> float: 117 | N = 30 # target blocks 118 | 119 | if consensus == C.BLOCK_GENESIS: 120 | return 1.0 121 | elif previous_hash == GENESIS_PREVIOUS_HASH: 122 | return 1.0 123 | 124 | target_sum = 0 125 | target_cnt = 0 126 | others_best = dict() 127 | target_hash = previous_hash 128 | for _ in range(MAX_SEARCH_BLOCKS): 129 | target_block = get_block_from_cache(target_hash) 130 | if target_block is None: 131 | return 1.0 132 | if target_block.flag not in others_best: 133 | others_best[target_block.flag] = bits2target(target_block.bits) 134 | target_hash = target_block.previous_hash 135 | if target_hash == GENESIS_PREVIOUS_HASH: 136 | return 1.0 137 | elif target_block.flag == consensus and N > target_cnt: 138 | target_sum += bits2target(target_block.bits) * (N - target_cnt) 139 | target_cnt += 1 140 | elif len(V.BLOCK_CONSENSUSES) <= len(others_best) + 1: 141 | break 142 | 143 | if target_cnt == 0: 144 | return 1.0 145 | elif len(others_best) == 0: 146 | return BASE_TARGET * target_cnt / target_sum 147 | else: 148 | average_target = sum(others_best.values()) // len(others_best) 149 | return average_target * target_cnt / target_sum 150 | 151 | 152 | __all__ = [ 153 | "MAX_BITS", 154 | "MAX_TARGET", 155 | "get_bits_by_hash", 156 | "get_bias_by_hash", 157 | ] 158 | -------------------------------------------------------------------------------- /bc4py/user/api/server.py: -------------------------------------------------------------------------------- 1 | from bc4py.user.api.ep_system import * 2 | from bc4py.user.api.ep_account import * 3 | from bc4py.user.api.ep_sending import * 4 | from bc4py.user.api.ep_blockchain import * 5 | from bc4py.user.api.ep_wallet import * 6 | from bc4py.user.api.ep_others import * 7 | from bc4py.user.api.ep_websocket import * 8 | from bc4py.user.api.jsonrpc import json_rpc 9 | from bc4py.user.api.utils import * 10 | from bc4py.config import V 11 | from fastapi import FastAPI 12 | from starlette.middleware.cors import CORSMiddleware 13 | from starlette.middleware.gzip import GZipMiddleware 14 | import uvicorn 15 | import asyncio 16 | from logging import getLogger 17 | 18 | # insight API-ref 19 | # https://blockexplorer.com/api-ref 20 | 21 | log = getLogger('bc4py') 22 | loop = asyncio.get_event_loop() 23 | 24 | 25 | async def setup_rest_server(port=3000, host='127.0.0.1', extra_locals=None, **kwargs): 26 | """ 27 | create REST server for API 28 | :param extra_locals: add local address ex."1.2.3.4+5.6.7.8+4.5.6.7" 29 | :param port: REST bind port 30 | :param host: REST bind host, "0.0.0.0" is not restriction 31 | """ 32 | app = FastAPI( 33 | version=__api_version__, 34 | title="bc4py API documents", 35 | description="OpenAPI/Swagger-generated API Reference Documentation, " 36 | "[Swagger-UI](./docs) and [React based](./redoc)", 37 | ) 38 | 39 | # System 40 | api_kwargs = dict(tags=['System'], response_class=IndentResponse) 41 | app.add_api_route('/public/getsysteminfo', system_info, **api_kwargs) 42 | app.add_api_route('/private/getsysteminfo', system_private_info, **api_kwargs) 43 | app.add_api_route('/public/getchaininfo', chain_info, **api_kwargs) 44 | app.add_api_route('/private/chainforkinfo', chain_fork_info, **api_kwargs) 45 | app.add_api_route('/public/getnetworkinfo', network_info, **api_kwargs) 46 | app.add_api_route('/private/resync', system_resync, **api_kwargs) 47 | app.add_api_route('/private/stop', system_close, **api_kwargs) 48 | # Account 49 | api_kwargs = dict(tags=['Account'], response_class=IndentResponse) 50 | app.add_api_route('/private/listbalance', list_balance, **api_kwargs) 51 | app.add_api_route('/private/listtransactions', list_transactions, **api_kwargs) 52 | app.add_api_route('/public/listunspents', list_unspents, **api_kwargs) 53 | app.add_api_route('/private/listunspents', list_private_unspents, **api_kwargs) 54 | app.add_api_route('/private/listaccountaddress', list_account_address, **api_kwargs) 55 | app.add_api_route('/private/move', move_one, methods=['POST'], **api_kwargs) 56 | app.add_api_route('/private/movemany', move_many, methods=['POST'], **api_kwargs) 57 | # Wallet 58 | api_kwargs = dict(tags=['Wallet'], response_class=IndentResponse) 59 | app.add_api_route('/private/newaddress', new_address, **api_kwargs) 60 | app.add_api_route('/private/getkeypair', get_keypair, **api_kwargs) 61 | app.add_api_route('/private/createwallet', create_wallet, methods=['POST'], **api_kwargs) 62 | app.add_api_route('/private/importprivatekey', import_private_key, methods=['POST'], **api_kwargs) 63 | # Sending 64 | api_kwargs = dict(tags=['Sending'], response_class=IndentResponse) 65 | app.add_api_route('/public/createrawtx', create_raw_tx, methods=['POST'], **api_kwargs) 66 | app.add_api_route('/public/broadcasttx', broadcast_tx, methods=['POST'], **api_kwargs) 67 | app.add_api_route('/private/sendfrom', send_from_user, methods=['POST'], **api_kwargs) 68 | app.add_api_route('/private/sendmany', send_many_user, methods=['POST'], **api_kwargs) 69 | app.add_api_route('/private/issueminttx', issue_mint_tx, methods=['POST'], **api_kwargs) 70 | app.add_api_route('/private/changeminttx', change_mint_tx, methods=['POST'], **api_kwargs) 71 | # Blockchain 72 | api_kwargs = dict(tags=['Blockchain'], response_class=IndentResponse) 73 | app.add_api_route('/public/getblockbyheight', get_block_by_height, **api_kwargs) 74 | app.add_api_route('/public/getblockbyhash', get_block_by_hash, **api_kwargs) 75 | app.add_api_route('/public/gettxbyhash', get_tx_by_hash, **api_kwargs) 76 | app.add_api_route('/public/getmintinfo', get_mintcoin_info, **api_kwargs) 77 | app.add_api_route('/public/getminthistory', get_mintcoin_history, **api_kwargs) 78 | # Others 79 | api_kwargs = dict(tags=['Others'], response_class=IndentResponse) 80 | app.add_api_route('/private/createbootstrap', create_bootstrap, **api_kwargs) 81 | app.add_api_websocket_route('/public/ws', websocket_route) 82 | app.add_api_route('/public/ws', websocket_route, **api_kwargs) 83 | app.add_api_websocket_route('/private/ws', private_websocket_route) 84 | app.add_api_route('/private/ws', private_websocket_route, **api_kwargs) 85 | app.add_api_route('/', json_rpc, methods=['POST'], **api_kwargs) 86 | 87 | # Cross-Origin Resource Sharing 88 | app.add_middleware( 89 | CORSMiddleware, allow_origins=['*'], allow_credentials=True, allow_methods=['*'], allow_headers=["*"]) 90 | 91 | # Gzip compression response 92 | app.add_middleware(GZipMiddleware, minimum_size=1000) 93 | 94 | # reject when node is booting and redirect / 95 | app.add_middleware(ConditionCheckMiddleware) 96 | 97 | # add extra local address 98 | if extra_locals: 99 | local_address.update(extra_locals.split('+')) 100 | 101 | # setup config 102 | config = uvicorn.Config(app, host=host, port=port, **kwargs) 103 | config.setup_event_loop() 104 | config.load() 105 | 106 | # setup server 107 | server = uvicorn.Server(config) 108 | server.logger = log 109 | server.lifespan = config.lifespan_class(config) 110 | # server.install_signal_handlers() # ignore Ctrl+C 111 | await server.startup() 112 | asyncio.run_coroutine_threadsafe(server.main_loop(), loop) 113 | log.info(f"API listen on {host}:{port}") 114 | V.API_OBJ = server 115 | 116 | 117 | async def system_resync(): 118 | """ 119 | This end-point make system resync. It will take many time. 120 | """ 121 | from bc4py.config import P 122 | log.warning("Manual set booting flag to go into resync mode") 123 | P.F_NOW_BOOTING = True 124 | return 'set booting mode now' 125 | 126 | 127 | async def system_close(): 128 | """ 129 | This end-point make system close. 130 | It take a few seconds. 131 | """ 132 | log.info("closing now") 133 | from bc4py.exit import system_safe_exit 134 | asyncio.run_coroutine_threadsafe(system_safe_exit(), loop) 135 | return 'closing now' 136 | 137 | 138 | __all__ = [ 139 | "setup_rest_server", 140 | ] 141 | -------------------------------------------------------------------------------- /bc4py/user/api/jsonrpc/account.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, V 2 | from bc4py.bip32 import is_address 3 | from bc4py.database.create import create_db 4 | from bc4py.database.account import read_name2userid, read_account_address 5 | from bc4py.user import Balance 6 | from bc4py.user.txcreation.transfer import send_from, send_many 7 | from bc4py.user.network.sendnew import send_newtx 8 | from bc4py_extension import PyAddress 9 | from logging import getLogger 10 | 11 | 12 | log = getLogger('bc4py') 13 | 14 | 15 | async def sendtoaddress(*args, **kwargs): 16 | """ 17 | Send an amount to a given address. 18 | 19 | Arguments: 20 | 1. "address" (string, required) The bitcoin address to send to. 21 | 2. "amount" (numeric or string, required) The amount in BTC to send. eg 0.1 22 | 3. "comment" (string, optional) A comment used to store what the transaction is for. 23 | This is not part of the transaction, just kept in your wallet. 24 | 4. "comment_to" (string, optional) A comment to store the name of the person or organization 25 | to which you're sending the transaction. This is not part of the 26 | transaction, just kept in your wallet. 27 | 5. subtractfeefromamount (boolean, optional, default=false) The fee will be deducted from the amount being sent. 28 | The recipient will receive less bitcoins than you enter in the amount field. 29 | 30 | Result: 31 | "txid" (string) The transaction id. 32 | """ 33 | if len(args) < 2: 34 | raise ValueError('too few arguments num={}'.format(len(args))) 35 | _address, amount, *options = args 36 | address: PyAddress = PyAddress.from_string(_address) 37 | if not is_address(address, V.BECH32_HRP, 0): 38 | raise ValueError('address is invalid') 39 | amount = int(amount * pow(10, V.COIN_DIGIT)) 40 | _comment = str(options[0]) if 0 < len(options) else None # do not use by Yiimp 41 | _comment_to = str(options[1]) if 1 < len(options) else None # do not use by Yiimp 42 | subtract_fee_amount = bool(options[2]) if 2 < len(options) else False 43 | 44 | # execute send 45 | error = None 46 | from_id = C.ANT_UNKNOWN 47 | coin_id = 0 48 | coins = Balance(coin_id, amount) 49 | async with create_db(V.DB_ACCOUNT_PATH) as db: 50 | cur = await db.cursor() 51 | try: 52 | new_tx = await send_from(from_id, address, coins, cur, 53 | subtract_fee_amount=subtract_fee_amount) 54 | if await send_newtx(new_tx=new_tx, cur=cur): 55 | await db.commit() 56 | else: 57 | error = 'Failed to send new tx' 58 | except Exception as e: 59 | error = str(e) 60 | log.debug("sendtoaddress", exc_info=True) 61 | 62 | # submit result 63 | if error: 64 | raise ValueError(error) 65 | return new_tx.hash.hex() 66 | 67 | 68 | async def sendmany(*args, **kwargs): 69 | """ 70 | Send multiple times. Amounts are double-precision floating point numbers. 71 | Requires wallet passphrase to be set with walletpassphrase call. 72 | 73 | Arguments: 74 | 1. "fromaccount" (string, required) DEPRECATED. The account to send the funds from. Should be "" for the default account 75 | 2. "amounts" (string, required) A json object with addresses and amounts 76 | { 77 | "address":amount (numeric or string) The monacoin address is the key, the numeric amount (can be string) in MONA is the value 78 | ,... 79 | } 80 | 3. minconf (numeric, optional, default=1) Only use the balance confirmed at least this many times. 81 | 4. "comment" (string, optional) A comment 82 | 83 | Result: 84 | "txid" (string) The transaction id for the send. Only 1 transaction is created regardless of 85 | the number of addresses. 86 | """ 87 | if len(args) < 2: 88 | raise ValueError('too few arguments num={}'.format(len(args))) 89 | from_account, pairs, *options = args 90 | _minconf = options[0] if 0 < len(options) else 1 # ignore 91 | _comment = options[1] if 1 < len(options) else None # ignore 92 | 93 | # replace account "" to "@Unknown" 94 | from_account = C.account2name[C.ANT_UNKNOWN] if from_account == '' else from_account 95 | 96 | error = None 97 | async with create_db(V.DB_ACCOUNT_PATH) as db: 98 | cur = await db.cursor() 99 | try: 100 | user_id = await read_name2userid(from_account, cur) 101 | send_pairs = list() 102 | multiple = pow(10, V.COIN_DIGIT) 103 | for address, amount in pairs.items(): 104 | send_pairs.append((PyAddress.from_string(address), 0, int(amount * multiple))) 105 | new_tx = await send_many(user_id, send_pairs, cur) 106 | if await send_newtx(new_tx=new_tx, cur=cur): 107 | await db.commit() 108 | else: 109 | error = 'Failed to send new tx' 110 | await db.rollback() 111 | except Exception as e: 112 | error = str(e) 113 | log.debug("sendmany", exc_info=True) 114 | await db.rollback() 115 | 116 | # submit result 117 | if error: 118 | raise ValueError(error) 119 | return new_tx.hash.hex() 120 | 121 | 122 | async def getaccountaddress(*args, **kwargs): 123 | """ 124 | DEPRECATED. Returns the current Bitcoin address for receiving payments to this account. 125 | 126 | Arguments: 127 | 1. "account" (string, required) The account name for the address. It can also be set to the empty string "" to represent the default account. The account does not need to exist, it will be created and a new address created if there is no account by the given name. 128 | 129 | Result: 130 | "address" (string) The account bitcoin address 131 | """ 132 | if len(args) == 0: 133 | raise ValueError('too few arguments num={}'.format(len(args))) 134 | user_name = args[0] 135 | # replace account "" to "@Unknown" 136 | user_name = C.account2name[C.ANT_UNKNOWN] if user_name == '' else user_name 137 | async with create_db(V.DB_ACCOUNT_PATH) as db: 138 | cur = await db.cursor() 139 | user_id = await read_name2userid(user_name, cur) 140 | address = await read_account_address(user_id, cur) 141 | await db.commit() 142 | return address.string 143 | 144 | 145 | __all__ = [ 146 | "sendtoaddress", 147 | "sendmany", 148 | "getaccountaddress", 149 | ] 150 | -------------------------------------------------------------------------------- /bc4py/chain/checking/utils.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, V, BlockChainError 2 | from bc4py.bip32 import is_address 3 | from bc4py.database import obj 4 | from bc4py.database.tools import is_unused_index_except_me, get_output_from_input 5 | from bc4py.user import Balance 6 | from hashlib import sha256 7 | from typing import TYPE_CHECKING, Optional 8 | 9 | 10 | # typing 11 | if TYPE_CHECKING: 12 | from bc4py.chain.tx import TX 13 | from bc4py.chain.block import Block 14 | 15 | 16 | def inputs_origin_check(tx: 'TX', include_block: Optional['Block']): 17 | """check the TX inputs for inconsistencies""" 18 | # check if the same input is used in same tx 19 | if len(tx.inputs) != len(set(tx.inputs)): 20 | raise BlockChainError(f"input has same origin {len(tx.inputs)}!={len(set(tx.inputs))}") 21 | 22 | limit_height = obj.chain_builder.best_block.height - C.MATURE_HEIGHT 23 | for txhash, txindex in tx.inputs: 24 | pair = get_output_from_input(input_hash=txhash, input_index=txindex, best_block=include_block) 25 | if pair is None: 26 | raise BlockChainError('Not found input tx. {}:{}'.format(txhash.hex(), txindex)) 27 | 28 | if obj.tx_builder.memory_pool.exist(txhash): 29 | # input of tx is not unconfirmed or include at former index 30 | if include_block is not None: 31 | for dep_index, dep_tx in enumerate(include_block.txs): 32 | if dep_tx.hash == txhash: 33 | break 34 | else: 35 | raise Exception('cannot find dep tx? {}'.format(txhash.hex())) 36 | if include_block.txs.index(tx) <= dep_index: 37 | raise BlockChainError('inputs depends later TX on block. {} {}'.format(tx, txhash.hex())) 38 | 39 | # mined output is must mature the height 40 | if not is_mature_input(base_hash=txhash, limit_height=limit_height): 41 | check_tx = obj.tx_builder.get_memorized_tx(txhash) 42 | if check_tx is None: 43 | raise Exception('cannot get tx, memory block number is too few') 44 | if check_tx.type in (C.TX_POS_REWARD, C.TX_POW_REWARD): 45 | raise BlockChainError('input origin is proof tx, {}>{}'.format(check_tx.height, limit_height)) 46 | 47 | # check unused input 48 | if not is_unused_index_except_me( 49 | input_hash=txhash, 50 | input_index=txindex, 51 | except_hash=tx.hash, 52 | best_block=include_block): 53 | raise BlockChainError('1 Input of {} is already used! {}:{}'.format(tx, txhash.hex(), txindex)) 54 | 55 | # check if the same input is used by another tx in block 56 | if include_block: 57 | for input_tx in include_block.txs: 58 | if input_tx is tx: 59 | continue 60 | for input_hash, input_index in input_tx.inputs: 61 | if input_hash == txhash and input_index == txindex: 62 | raise BlockChainError('2 Input of {} is already used by {}'.format(tx, input_tx)) 63 | 64 | 65 | def amount_check(tx, payfee_coin_id, include_block): 66 | """check tx sum of inputs and outputs amount""" 67 | # Inputs 68 | input_coins = Balance() 69 | for txhash, txindex in tx.inputs: 70 | pair = get_output_from_input(input_hash=txhash, input_index=txindex, best_block=include_block) 71 | if pair is None: 72 | raise BlockChainError('Not found input tx {}'.format(txhash.hex())) 73 | address, coin_id, amount = pair 74 | input_coins[coin_id] += amount 75 | 76 | # Outputs 77 | output_coins = Balance() 78 | for address, coin_id, amount in tx.outputs: 79 | if amount <= 0: 80 | raise BlockChainError('Input amount is more than 0') 81 | output_coins[coin_id] += amount 82 | 83 | # Fee 84 | fee_coins = Balance(coin_id=payfee_coin_id, amount=tx.gas_price * tx.gas_amount) 85 | 86 | # Check all plus amount 87 | remain_amount = input_coins - output_coins - fee_coins 88 | if not remain_amount.is_empty(): 89 | raise BlockChainError('77 Don\'t match input/output. {}={}-{}-{}'.format( 90 | remain_amount, input_coins, output_coins, fee_coins)) 91 | 92 | 93 | def signature_check(tx, include_block): 94 | require_cks = set() 95 | checked_cks = set() 96 | signed_cks = set(tx.verified_list) 97 | for txhash, txindex in tx.inputs: 98 | pair = get_output_from_input(txhash, txindex, best_block=include_block) 99 | if pair is None: 100 | raise BlockChainError('Not found input tx {}'.format(txhash.hex())) 101 | address, coin_id, amount = pair 102 | if address in checked_cks: 103 | continue 104 | elif is_address(ck=address, hrp=V.BECH32_HRP, ver=C.ADDR_NORMAL_VER): 105 | require_cks.add(address) 106 | else: 107 | raise BlockChainError('Not common address {} {}'.format(address, tx)) 108 | # success check 109 | checked_cks.add(address) 110 | 111 | if not (0 < len(require_cks) < 256): 112 | raise BlockChainError('require signature is over range num={}'.format(len(require_cks))) 113 | if require_cks != signed_cks: 114 | raise BlockChainError('Signature verification failed. [{}={}]'.format(require_cks, signed_cks)) 115 | 116 | 117 | def stake_coin_check(tx, previous_hash, target_hash): 118 | # staked => sha256(txhash + previous_hash) / amount < 256^32 / target 119 | assert tx.pos_amount is not None 120 | pos_work_hash = sha256(tx.hash + previous_hash).digest() 121 | work = int.from_bytes(pos_work_hash, 'little') 122 | work //= (tx.pos_amount // 100000000) 123 | return work < int.from_bytes(target_hash, 'little') 124 | 125 | 126 | def is_mature_input(base_hash, limit_height) -> bool: 127 | """proof of stake input must mature same height""" 128 | # from unconfirmed 129 | if obj.tx_builder.memory_pool.exist(base_hash): 130 | return False 131 | 132 | # from memory 133 | for block in obj.chain_builder.best_chain: 134 | if block.height < limit_height: 135 | return True 136 | for tx in block.txs: 137 | if tx.hash == base_hash: 138 | return False 139 | 140 | # from database 141 | height = obj.chain_builder.root_block.height 142 | while limit_height <= height: 143 | block = obj.chain_builder.get_block(height=height) 144 | for tx in block.txs: 145 | if tx.hash == base_hash: 146 | return False 147 | height -= 1 148 | 149 | # check passed 150 | return True 151 | 152 | 153 | __all__ = [ 154 | "inputs_origin_check", 155 | "amount_check", 156 | "signature_check", 157 | "stake_coin_check", 158 | "is_mature_input", 159 | ] 160 | -------------------------------------------------------------------------------- /bc4py/user/network/connection.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import V, P, BlockChainError 2 | from bc4py.user.network.directcmd import DirectCmd 3 | from p2p_python.config import PeerToPeerError 4 | from collections import Counter 5 | from logging import getLogger 6 | import random 7 | import asyncio 8 | 9 | 10 | loop = asyncio.get_event_loop() 11 | log = getLogger('bc4py') 12 | good_node = list() 13 | bad_node = list() 14 | best_hash_on_network = None 15 | best_height_on_network = None 16 | 17 | 18 | async def set_good_node(): 19 | node = list() 20 | pc = V.P2P_OBJ 21 | status_counter = Counter() 22 | f_all_booting = True # flag: there is no stable node 23 | for user in pc.core.user.copy(): 24 | try: 25 | dummy, r = await pc.send_direct_cmd(cmd=DirectCmd.best_info, data=None, user=user) 26 | if isinstance(r, str): 27 | continue 28 | except (asyncio.TimeoutError, PeerToPeerError): 29 | continue 30 | # success get best-info 31 | if not isinstance(r['height'], int): 32 | continue 33 | if not isinstance(r['hash'], bytes): 34 | continue 35 | status_counter[(r['height'], r['hash'])] += 1 36 | if r['booting'] is False: 37 | f_all_booting = False 38 | node.append((user, r['hash'], r['height'], r['booting'])) 39 | # check unstable? 40 | if f_all_booting and len(pc.core.user) < 3: 41 | raise UnstableNetworkError("unstable network: All connection booting") 42 | if len(status_counter) == 0: 43 | raise UnstableNetworkError("unstable network: No status count") 44 | # get best height and best hash 45 | global best_hash_on_network, best_height_on_network 46 | (best_height, best_hash), count = status_counter.most_common()[0] 47 | if count == 1: 48 | best_height, best_hash = sorted(status_counter, key=lambda x: x[0], reverse=True)[0] 49 | best_hash_on_network = best_hash 50 | best_height_on_network = best_height 51 | good_node.clear() 52 | bad_node.clear() 53 | for user, blockhash, height, f_booting in node: 54 | if blockhash == best_hash_on_network and height == best_height_on_network: 55 | good_node.append(user) 56 | else: 57 | bad_node.append(user) 58 | 59 | 60 | def reset_good_node(): 61 | good_node.clear() 62 | global best_hash_on_network, best_height_on_network 63 | best_hash_on_network = None 64 | best_height_on_network = None 65 | 66 | 67 | async def ask_node(cmd, data=None, f_continue_asking=False): 68 | await check_network_connection() 69 | failed = 0 70 | pc = V.P2P_OBJ 71 | user_list = pc.core.user.copy() 72 | random.shuffle(user_list) 73 | while failed < 10: 74 | try: 75 | if len(user_list) == 0: 76 | break 77 | if len(good_node) == 0: 78 | await set_good_node() 79 | user = user_list.pop() 80 | if user in good_node: 81 | dummy, r = await pc.send_direct_cmd(cmd=cmd, data=data, user=user) 82 | if isinstance(r, str): 83 | failed += 1 84 | if f_continue_asking: 85 | log.warning("Failed cmd={} to {} by \"{}\"".format(cmd, user.header.name, r)) 86 | continue 87 | return r 88 | elif user in bad_node: 89 | pass 90 | else: 91 | await set_good_node() 92 | except asyncio.TimeoutError: 93 | pass 94 | except (UnstableNetworkError, PeerToPeerError) as e: 95 | log.warning("{}, wait 30sec".format(e)) 96 | await asyncio.sleep(30) 97 | raise BlockChainError('Too many retry ask_node. good={} bad={} failed={} cmd={}'.format( 98 | len(good_node), len(bad_node), failed, cmd)) 99 | 100 | 101 | async def ask_all_nodes(cmd, data=None): 102 | await check_network_connection() 103 | pc = V.P2P_OBJ 104 | user_list = pc.core.user.copy() 105 | random.shuffle(user_list) 106 | result = list() 107 | for user in pc.core.user.copy(): 108 | try: 109 | if len(good_node) == 0: 110 | await set_good_node() 111 | # check both good and bad 112 | if user in good_node or user in bad_node: 113 | dummy, r = await pc.send_direct_cmd(cmd=cmd, data=data, user=user) 114 | if not isinstance(r, str): 115 | result.append(r) 116 | else: 117 | await set_good_node() 118 | except asyncio.TimeoutError: 119 | pass 120 | except (UnstableNetworkError, PeerToPeerError) as e: 121 | log.warning("{}, wait 30sec".format(e)) 122 | await asyncio.sleep(30) 123 | if len(result) > 0: 124 | return result 125 | raise BlockChainError('Cannot get any data. good={} bad={} cmd={}' 126 | .format(len(good_node), len(bad_node), cmd.__name__)) 127 | 128 | 129 | async def ask_random_node(cmd, data=None): 130 | await check_network_connection() 131 | pc = V.P2P_OBJ 132 | user_list = pc.core.user.copy() 133 | random.shuffle(user_list) 134 | for user in user_list: 135 | try: 136 | if len(good_node) == 0: 137 | await set_good_node() 138 | # check both good and bad 139 | if user in good_node or user in bad_node: 140 | dummy, r = await pc.send_direct_cmd(cmd=cmd, data=data, user=user) 141 | if not isinstance(r, str): 142 | return r 143 | else: 144 | await set_good_node() 145 | except asyncio.TimeoutError: 146 | pass 147 | except (UnstableNetworkError, PeerToPeerError) as e: 148 | log.warning("{}, wait 30sec".format(e)) 149 | await asyncio.sleep(30) 150 | raise BlockChainError('Full seeked but cannot get any data. good={} bad={} cmd={}' 151 | .format(len(good_node), len(bad_node), cmd.__name__)) 152 | 153 | 154 | async def get_best_conn_info(): 155 | while best_height_on_network is None: 156 | await set_good_node() 157 | await asyncio.sleep(0.1) 158 | return best_height_on_network, best_hash_on_network 159 | 160 | 161 | async def check_network_connection(minimum=None): 162 | count = 0 163 | need = minimum or 2 164 | while len(V.P2P_OBJ.core.user) <= need: 165 | count += 1 166 | if P.F_STOP: 167 | return # skip 168 | elif count % 30 == 0: 169 | log.debug("{} connections, waiting for new.. {}Sec".format(len(V.P2P_OBJ.core.user), count)) 170 | elif count % 123 == 1: 171 | log.info("connect {} nodes but unsatisfied required number {}".format(len(V.P2P_OBJ.core.user), need)) 172 | else: 173 | await asyncio.sleep(1) 174 | 175 | 176 | class UnstableNetworkError(Exception): 177 | pass 178 | 179 | 180 | __all__ = [ 181 | "set_good_node", 182 | "reset_good_node", 183 | "ask_node", 184 | "ask_all_nodes", 185 | "ask_random_node", 186 | "get_best_conn_info", 187 | "check_network_connection", 188 | ] 189 | -------------------------------------------------------------------------------- /bc4py/user/txcreation/mintcoin.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, V, BlockChainError 2 | from bc4py.bip32 import dummy_address 3 | from bc4py.chain.tx import TX 4 | from bc4py.database.mintcoin import * 5 | from bc4py.database.account import generate_new_address_by_userid, insert_movelog 6 | from bc4py.user import Balance, Accounting 7 | from bc4py.user.txcreation.utils import * 8 | from bc4py_extension import PyAddress 9 | import random 10 | import msgpack 11 | 12 | MINTCOIN_DUMMY_ADDRESS = dummy_address(b'MINTCOIN_DUMMY_ADDR_') 13 | 14 | 15 | async def issue_mint_coin( 16 | name, 17 | unit, 18 | digit, 19 | amount, 20 | cur, 21 | description=None, 22 | image=None, 23 | additional_issue=True, 24 | change_address=True, 25 | gas_price=None, 26 | sender=C.ANT_UNKNOWN, 27 | retention=10800): 28 | mint_id = get_new_coin_id() 29 | mint_address = await generate_new_address_by_userid(user=sender, cur=cur) 30 | params = { 31 | "name": name, 32 | "unit": unit, 33 | "digit": digit, 34 | "address": mint_address.string, 35 | "description": description, 36 | "image": image 37 | } 38 | setting = {"additional_issue": additional_issue, "change_address": change_address} 39 | m_before = get_mintcoin_object(coin_id=mint_id) 40 | result = check_mintcoin_new_format(m_before=m_before, new_params=params, new_setting=setting) 41 | if isinstance(result, str): 42 | raise BlockChainError('check_mintcoin_new_format(): {}'.format(result)) 43 | msg_body = msgpack.packb((mint_id, params, setting), use_bin_type=True) 44 | tx = TX.from_dict( 45 | tx={ 46 | 'type': C.TX_MINT_COIN, 47 | 'inputs': list(), 48 | 'outputs': [(MINTCOIN_DUMMY_ADDRESS, 0, amount)], 49 | 'gas_price': gas_price or V.COIN_MINIMUM_PRICE, 50 | 'gas_amount': 1, 51 | 'message_type': C.MSG_MSGPACK, 52 | 'message': msg_body 53 | }) 54 | tx.update_time(retention) 55 | additional_gas = C.MINTCOIN_GAS 56 | tx.gas_amount = tx.size + C.SIGNATURE_GAS + additional_gas 57 | tx.serialize() 58 | # fill unspents 59 | fee_coin_id = 0 60 | input_address = await fill_inputs_outputs(tx=tx, cur=cur, fee_coin_id=fee_coin_id, additional_gas=additional_gas) 61 | # input_address.add(mint_address) 62 | fee_coins = Balance(coin_id=fee_coin_id, amount=tx.gas_price * tx.gas_amount) 63 | # check amount 64 | await check_enough_amount(sender=sender, send_coins=Balance(0, amount), fee_coins=fee_coins, cur=cur) 65 | # replace dummy address 66 | await replace_redeem_dummy_address(tx=tx, cur=cur) 67 | # replace dummy mint_id 68 | replace_mint_dummy_address(tx=tx, mint_address=mint_address, mint_id=mint_id, f_raise=True) 69 | # setup signature 70 | tx.serialize() 71 | await add_sign_by_address(tx=tx, input_address=input_address, cur=cur) 72 | # movement 73 | movements = Accounting() 74 | minting_coins = Balance(mint_id, amount) 75 | movements[sender] += minting_coins 76 | # movements[C.ANT_OUTSIDE] -= minting_coins 77 | movements[sender] -= fee_coins 78 | # movements[C.ANT_OUTSIDE] += fee_coins 79 | await insert_movelog(movements, cur, tx.type, tx.time, tx.hash) 80 | return mint_id, tx 81 | 82 | 83 | async def change_mint_coin( 84 | mint_id, 85 | cur, 86 | amount=None, 87 | description=None, 88 | image=None, 89 | setting=None, 90 | new_address=None, 91 | gas_price=None, 92 | sender=C.ANT_UNKNOWN, 93 | retention=10800): 94 | assert amount or description or image or setting or new_address 95 | assert new_address is None or isinstance(new_address, str) 96 | params = dict() 97 | if description: 98 | params['description'] = description 99 | if image: 100 | params['image'] = image 101 | if new_address: 102 | params['address'] = new_address 103 | if len(params) == 0: 104 | params = None 105 | if not params and not setting and not amount: 106 | raise BlockChainError('No update found') 107 | m_before = get_mintcoin_object(coin_id=mint_id) 108 | if m_before.version == -1: 109 | raise BlockChainError('Not init mintcoin. {}'.format(m_before)) 110 | result = check_mintcoin_new_format(m_before=m_before, new_params=params, new_setting=setting) 111 | if isinstance(result, str): 112 | raise BlockChainError('check_mintcoin_new_format(): {}'.format(result)) 113 | msg_body = msgpack.packb((mint_id, params, setting), use_bin_type=True) 114 | tx = TX.from_dict( 115 | tx={ 116 | 'type': C.TX_MINT_COIN, 117 | 'gas_price': gas_price or V.COIN_MINIMUM_PRICE, 118 | 'gas_amount': 1, 119 | 'message_type': C.MSG_MSGPACK, 120 | 'message': msg_body 121 | }) 122 | if amount: 123 | tx.outputs.append((MINTCOIN_DUMMY_ADDRESS, 0, amount)) 124 | send_coins = Balance(0, amount) 125 | minting_coins = Balance(mint_id, amount) 126 | else: 127 | send_coins = Balance(0, 0) 128 | minting_coins = Balance(0, 0) 129 | tx.update_time(retention) 130 | additional_gas = C.MINTCOIN_GAS + C.SIGNATURE_GAS # for mint_coin user signature 131 | tx.gas_amount = tx.size + C.SIGNATURE_GAS + additional_gas 132 | tx.serialize() 133 | # fill unspents 134 | fee_coin_id = 0 135 | input_address = await fill_inputs_outputs(tx=tx, cur=cur, fee_coin_id=fee_coin_id, additional_gas=additional_gas) 136 | mint_address = PyAddress.from_string(m_before.address) 137 | input_address.add(mint_address) 138 | fee_coins = Balance(coin_id=fee_coin_id, amount=tx.gas_price * tx.gas_amount) 139 | # check amount 140 | await check_enough_amount(sender=sender, send_coins=send_coins, fee_coins=fee_coins, cur=cur) 141 | # replace dummy address 142 | await replace_redeem_dummy_address(tx=tx, cur=cur) 143 | # replace dummy mint_id 144 | replace_mint_dummy_address(tx=tx, mint_address=mint_address, mint_id=mint_id, f_raise=False) 145 | # setup signature 146 | tx.serialize() 147 | await add_sign_by_address(tx=tx, input_address=input_address, cur=cur) 148 | # movement 149 | movements = Accounting() 150 | movements[sender] += minting_coins 151 | # movements[C.ANT_OUTSIDE] -= minting_coins 152 | movements[sender] -= fee_coins 153 | # movements[C.ANT_OUTSIDE] += fee_coins 154 | await insert_movelog(movements, cur, tx.type, tx.time, tx.hash) 155 | return tx 156 | 157 | 158 | def get_new_coin_id(): 159 | while True: 160 | coin_id = random.randint(1, 0xffffffff) 161 | if get_mintcoin_object(coin_id).version == -1: 162 | return coin_id 163 | 164 | 165 | def replace_mint_dummy_address(tx, mint_address, mint_id, f_raise): 166 | assert isinstance(mint_address, PyAddress) 167 | for index, (address, coin_id, amount) in enumerate(tx.outputs): 168 | if address == MINTCOIN_DUMMY_ADDRESS: 169 | tx.outputs[index] = (mint_address, mint_id, amount) 170 | break 171 | else: 172 | if f_raise: 173 | raise BlockChainError('Cannot replace Mintcoin dummy address') 174 | 175 | 176 | __all__ = [ 177 | "issue_mint_coin", 178 | "change_mint_coin", 179 | ] 180 | -------------------------------------------------------------------------------- /bc4py/user/network/broadcast.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, V, P, BlockChainError 2 | from bc4py.chain.block import Block 3 | from bc4py.chain.tx import TX 4 | from bc4py.chain.checking import new_insert_block, check_tx, check_tx_time 5 | from bc4py.chain.signature import fill_verified_addr_tx 6 | from bc4py.chain.workhash import update_work_hash 7 | from bc4py.database import obj 8 | from bc4py.database.create import create_db 9 | from bc4py.user.network.update import update_info_for_generate 10 | from bc4py.user.network.directcmd import DirectCmd 11 | from bc4py.user.network.connection import ask_node 12 | from p2p_python.user import User 13 | from logging import getLogger 14 | 15 | log = getLogger('bc4py') 16 | 17 | 18 | class BroadcastCmd: 19 | NEW_BLOCK = 'cmd/new-block' 20 | NEW_TX = 'cmd/new-tx' 21 | fail = 0 22 | 23 | @staticmethod 24 | async def new_block(user, data): 25 | try: 26 | new_block = await fill_newblock_info(user, data) 27 | except BlockChainError as e: 28 | warning = 'Do not accept block "{}"'.format(e) 29 | log.warning(warning) 30 | return False 31 | except Exception: 32 | error = "error on accept new block" 33 | log.error(error, exc_info=True) 34 | return False 35 | try: 36 | if await new_insert_block(new_block): 37 | update_info_for_generate() 38 | log.info("Accept new block {}".format(new_block)) 39 | return True 40 | else: 41 | return False 42 | except BlockChainError as e: 43 | error = 'Failed accept new block "{}"'.format(e) 44 | log.error(error, exc_info=True) 45 | return False 46 | except Exception: 47 | error = "error on accept new block" 48 | log.error(error, exc_info=True) 49 | return False 50 | 51 | @staticmethod 52 | async def new_tx(user, data): 53 | try: 54 | new_tx: TX = data['tx'] 55 | if obj.tx_builder.get_memorized_tx(new_tx.hash) is not None: 56 | log.debug("high latency node? already memorized new tx") 57 | return False 58 | check_tx_time(new_tx) 59 | await fill_verified_addr_tx(new_tx) 60 | check_tx(tx=new_tx, include_block=None) 61 | async with create_db(V.DB_ACCOUNT_PATH) as db: 62 | cur = await db.cursor() 63 | await obj.tx_builder.put_unconfirmed(cur=cur, tx=new_tx) 64 | log.info("Accept new tx {}".format(new_tx)) 65 | update_info_for_generate(u_block=False, u_unspent=False) 66 | return True 67 | except BlockChainError as e: 68 | error = 'Failed accept new tx "{}"'.format(e) 69 | log.error(error, exc_info=True) 70 | return False 71 | except Exception: 72 | error = "Failed accept new tx" 73 | log.error(error, exc_info=True) 74 | return False 75 | 76 | 77 | async def fill_newblock_info(user, data): 78 | new_block: Block = Block.from_binary(binary=data['binary']) 79 | log.debug("fill newblock height={} newblock={}".format(data.get('height'), new_block.hash.hex())) 80 | proof: TX = data['proof'] 81 | new_block.txs.append(proof) 82 | new_block.flag = data['block_flag'] 83 | my_block = obj.chain_builder.get_block(new_block.hash) 84 | if my_block: 85 | raise BlockChainError('Already inserted block {}'.format(my_block)) 86 | before_block = obj.chain_builder.get_block(new_block.previous_hash) 87 | if before_block is None: 88 | log.debug("Cannot find beforeBlock, try to ask outside node") 89 | # not found beforeBlock, need to check other node have the the block 90 | new_block.inner_score *= 0.70 # unknown previousBlock, score down 91 | before_block = await make_block_by_node(user, new_block.previous_hash, 0) 92 | new_height = before_block.height + 1 93 | proof.height = new_height 94 | new_block.height = new_height 95 | # work check 96 | # TODO: correct position? 97 | update_work_hash(new_block) 98 | if not new_block.pow_check(): 99 | raise BlockChainError('Proof of work is not satisfied') 100 | # Append general txs 101 | async with create_db(V.DB_ACCOUNT_PATH) as db: 102 | cur = await db.cursor() 103 | for txhash in data['txs'][1:]: 104 | tx = obj.tx_builder.get_memorized_tx(txhash) 105 | if tx is None: 106 | new_block.inner_score *= 0.75 # unknown tx, score down 107 | log.debug("Unknown tx, try to download") 108 | r = await ask_node(cmd=DirectCmd.tx_by_hash, data={'txhash': txhash}, f_continue_asking=True) 109 | if isinstance(r, str): 110 | raise BlockChainError('Failed unknown tx download "{}"'.format(r)) 111 | tx: TX = r 112 | tx.height = None 113 | await fill_verified_addr_tx(tx) 114 | check_tx(tx, include_block=None) 115 | await obj.tx_builder.put_unconfirmed(cur=cur, tx=tx) 116 | log.debug("Success unknown tx download {}".format(tx)) 117 | tx.height = new_height 118 | new_block.txs.append(tx) 119 | await db.commit() 120 | return new_block 121 | 122 | 123 | async def broadcast_check(user: User, data: dict): 124 | if P.F_NOW_BOOTING: 125 | return False 126 | elif BroadcastCmd.NEW_BLOCK == data['cmd']: 127 | result = await BroadcastCmd.new_block(user, data['data']) 128 | elif BroadcastCmd.NEW_TX == data['cmd']: 129 | result = await BroadcastCmd.new_tx(user, data['data']) 130 | else: 131 | return False 132 | # check failed count over 133 | if result: 134 | BroadcastCmd.fail = 0 135 | else: 136 | BroadcastCmd.fail += 1 137 | return result 138 | 139 | 140 | async def make_block_by_node(user, blockhash, depth): 141 | """create parent block from broadcast node""" 142 | log.debug("make block by node depth={} hash={}".format(depth, blockhash.hex())) 143 | _, r = await V.P2P_OBJ.send_direct_cmd( 144 | cmd=DirectCmd.block_by_hash, data={'blockhash': blockhash}, user=user) 145 | if isinstance(r, str) or not isinstance(r, Block): 146 | raise BlockChainError( 147 | "failed get parent block '{}' hash={}".format(r, blockhash.hex())) 148 | # success 149 | block: Block = r 150 | before_block = obj.chain_builder.get_block(blockhash=block.previous_hash) 151 | if before_block is None: 152 | if depth < C.MAX_RECURSIVE_BLOCK_DEPTH: 153 | before_block = await make_block_by_node(user, block.previous_hash, depth+1) 154 | else: 155 | raise BlockChainError('Cannot recursive get block depth={} hash={}' 156 | .format(depth, block.previous_hash.hex())) 157 | height = before_block.height + 1 158 | block.height = height 159 | block.inner_score *= 0.70 160 | for tx in block.txs: 161 | tx.height = height 162 | update_work_hash(block) 163 | if not await new_insert_block(block=block, f_time=False, f_sign=True): 164 | raise BlockChainError('Failed insert beforeBlock {}'.format(before_block)) 165 | return block 166 | 167 | 168 | __all__ = [ 169 | "BroadcastCmd", 170 | "broadcast_check", 171 | ] 172 | -------------------------------------------------------------------------------- /bc4py/chain/checking/tx_reward.py: -------------------------------------------------------------------------------- 1 | from bc4py import __chain_version__ 2 | from bc4py.config import C, BlockChainError 3 | from bc4py_extension import poc_hash, poc_work, scope_index 4 | from bc4py.chain.utils import GompertzCurve 5 | from bc4py.chain.checking.utils import stake_coin_check, is_mature_input 6 | from bc4py.database.tools import get_output_from_input 7 | 8 | 9 | def check_tx_pow_reward(tx, include_block): 10 | if not (len(tx.inputs) == 0 and len(tx.outputs) > 0): 11 | raise BlockChainError('Inout is 0, output is more than 1') 12 | elif include_block.txs.index(tx) != 0: 13 | raise BlockChainError('Proof tx is index 0') 14 | elif not (tx.gas_price == 0 and tx.gas_amount == 0): 15 | raise BlockChainError('Pow gas info is wrong. [{}, {}]'.format(tx.gas_price, tx.gas_amount)) 16 | elif len(tx.message) > 96: 17 | raise BlockChainError('Pow msg is less than 96bytes. [{}b>96b]'.format(len(tx.message))) 18 | elif len(tx.signature) != 0: 19 | raise BlockChainError('signature is only zero not {}'.format(len(tx.signature))) 20 | 21 | total_output_amount = 0 22 | for address, coin_id, amount in tx.outputs: 23 | if coin_id != 0: 24 | raise BlockChainError('Output coin_id is zero not {}'.format(coin_id)) 25 | total_output_amount += amount 26 | # allow many outputs for PoW reward distribution 27 | extra_output_fee = (len(tx.outputs) - 1) * C.EXTRA_OUTPUT_REWARD_FEE 28 | reward = GompertzCurve.calc_block_reward(include_block.height) 29 | income_fee = sum(tx.gas_amount * tx.gas_price for tx in include_block.txs) 30 | 31 | if not (include_block.time == tx.time == tx.deadline - 10800): 32 | raise BlockChainError('TX time is wrong 3. [{}={}={}-10800]'.format(include_block.time, tx.time, 33 | tx.deadline)) 34 | elif total_output_amount > reward + income_fee - extra_output_fee: 35 | raise BlockChainError('Input and output is wrong [{}<{}+{}-{}]' 36 | .format(total_output_amount, reward, income_fee, extra_output_fee)) 37 | 38 | 39 | def check_tx_pos_reward(tx, include_block): 40 | # POS報酬TXの検査 41 | if not (len(tx.inputs) == len(tx.outputs) == 1): 42 | raise BlockChainError('Inputs and outputs is only 1 len') 43 | elif include_block.txs.index(tx) != 0: 44 | raise BlockChainError('Proof tx is index 0') 45 | elif include_block.version != 0: 46 | raise BlockChainError('pos block version is 0') 47 | elif not (tx.gas_price == 0 and tx.gas_amount == 0): 48 | raise BlockChainError('Pos gas info is wrong. [{}, {}]'.format(tx.gas_price, tx.gas_amount)) 49 | elif not (tx.message_type == C.MSG_NONE and tx.message == b''): 50 | raise BlockChainError('Pos msg is None type. [{},{}]'.format(tx.message_type, tx.message)) 51 | 52 | txhash, txindex = tx.inputs[0] 53 | if not is_mature_input(base_hash=txhash, limit_height=include_block.height - C.MATURE_HEIGHT): 54 | raise BlockChainError('Source is not mature, {} {}'.format(include_block.height, txhash.hex())) 55 | base_pair = get_output_from_input(txhash, txindex, best_block=include_block) 56 | if base_pair is None: 57 | raise BlockChainError('Not found PosBaseTX:{} of {}'.format(txhash.hex(), tx)) 58 | input_address, input_coin_id, input_amount = base_pair 59 | tx.pos_amount = input_amount 60 | output_address, output_coin_id, output_amount = tx.outputs[0] 61 | reward = GompertzCurve.calc_block_reward(include_block.height) 62 | include_block.bits2target() 63 | 64 | if input_address != output_address: 65 | raise BlockChainError('Input address differ from output address. [{}!={}]'.format( 66 | input_address, output_address)) 67 | elif not (input_coin_id == output_coin_id == 0): 68 | raise BlockChainError('Input and output coinID is zero') 69 | elif input_amount + reward != output_amount: 70 | raise BlockChainError('Inout amount wrong [{}+{}!={}]'.format(input_amount, reward, output_amount)) 71 | elif tx.version != __chain_version__ or tx.message_type != C.MSG_NONE: 72 | raise BlockChainError('Not correct tx version or msg_type') 73 | elif not (include_block.time == tx.time == tx.deadline - 10800): 74 | raise BlockChainError('TX time is wrong 1. [{}={}={}-10800]'.format(include_block.time, tx.time, 75 | tx.deadline)) 76 | elif not stake_coin_check( 77 | tx=tx, previous_hash=include_block.previous_hash, target_hash=include_block.target_hash): 78 | raise BlockChainError('Proof of stake check is failed') 79 | 80 | 81 | def check_tx_poc_reward(tx, include_block): 82 | if not (len(tx.inputs) == 0 and len(tx.outputs) == 1): 83 | raise BlockChainError('inputs is 0 and outputs is 1') 84 | elif include_block.txs.index(tx) != 0: 85 | raise BlockChainError('Proof tx is index 0') 86 | elif not (tx.gas_price == 0 and tx.gas_amount == 0): 87 | raise BlockChainError('PoC gas info is wrong. [{}, {}]'.format(tx.gas_price, tx.gas_amount)) 88 | elif not (tx.message_type == C.MSG_NONE and tx.message == b''): 89 | raise BlockChainError('PoC msg is None type. [{},{}]'.format(tx.message_type, tx.message)) 90 | elif len(tx.signature) != 1: 91 | raise BlockChainError('signature is only one not {}'.format(len(tx.signature))) 92 | 93 | o_address, o_coin_id, o_amount = tx.outputs[0] 94 | reward = GompertzCurve.calc_block_reward(include_block.height) 95 | total_fee = sum(tx.gas_price * tx.gas_amount for tx in include_block.txs) 96 | include_block.bits2target() 97 | 98 | if o_coin_id != 0: 99 | raise BlockChainError('output coinID is 0') 100 | if reward + total_fee != o_amount: 101 | raise BlockChainError('Inout amount wrong [{}+{}!={}]'.format(reward, total_fee, o_amount)) 102 | if tx.version != __chain_version__: 103 | raise BlockChainError('Not correct tx version') 104 | if not (include_block.time == tx.time == tx.deadline - 10800): 105 | raise BlockChainError('TX time is wrong 1. [{}={}={}-10800]'.format(include_block.time, tx.time, 106 | tx.deadline)) 107 | 108 | # work check 109 | scope_hash = poc_hash(address=o_address.string, nonce=include_block.nonce) 110 | index = scope_index(include_block.previous_hash) 111 | work_hash = poc_work( 112 | time=include_block.time, 113 | scope_hash=scope_hash[index * 32:index*32 + 32], 114 | previous_hash=include_block.previous_hash) 115 | if int.from_bytes(work_hash, 'little') > int.from_bytes(include_block.target_hash, 'little'): 116 | raise BlockChainError('PoC check is failed, work={} target={}'.format(work_hash.hex(), 117 | include_block.target_hash.hex())) 118 | 119 | # signature check 120 | signed_cks = set(tx.verified_list) 121 | if len(signed_cks) != 1: 122 | raise BlockChainError('PoC signature num is wrong num={}'.format(len(signed_cks))) 123 | ck = signed_cks.pop() 124 | if ck != o_address: 125 | raise BlockChainError('PoC signature ck is miss math {}!={}'.format(ck, o_address)) 126 | 127 | 128 | __all__ = [ 129 | "check_tx_pow_reward", 130 | "check_tx_pos_reward", 131 | "check_tx_poc_reward", 132 | ] 133 | -------------------------------------------------------------------------------- /bc4py/chain/checking/checktx.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, V, BlockChainError 2 | from bc4py.chain.tx import TX 3 | from bc4py.chain.block import Block 4 | from bc4py.chain.checking.tx_reward import * 5 | from bc4py.chain.checking.tx_mintcoin import * 6 | from bc4py.chain.checking.utils import * 7 | from logging import getLogger 8 | from time import time 9 | import hashlib 10 | 11 | 12 | log = getLogger('bc4py') 13 | 14 | 15 | def check_tx(tx, include_block): 16 | # TXの正当性チェック 17 | f_inputs_origin_check = True 18 | f_amount_check = True 19 | f_signature_check = True 20 | f_size_check = True 21 | f_minimum_fee_check = True 22 | payfee_coin_id = 0 23 | 24 | # 共通検査 25 | if include_block: 26 | # tx is included block 27 | if tx not in include_block.txs: 28 | raise BlockChainError('Block not include the tx') 29 | elif not (tx.time <= include_block.time <= tx.deadline): 30 | raise BlockChainError('block time isn\'t include in TX time-deadline. [{}<={}<={}]'.format( 31 | tx.time, include_block.time, tx.deadline)) 32 | if 0 == include_block.txs.index(tx): 33 | if tx.type not in (C.TX_POS_REWARD, C.TX_POW_REWARD): 34 | raise BlockChainError('tx index is zero, but not proof tx') 35 | elif tx.type in (C.TX_POS_REWARD, C.TX_POW_REWARD): 36 | raise BlockChainError('{} index is not 0 idx:{}'.format(tx, include_block.txs.index(tx))) 37 | 38 | # 各々のタイプで検査 39 | if tx.type == C.TX_GENESIS: 40 | return 41 | 42 | elif tx.type == C.TX_POS_REWARD: 43 | f_amount_check = False 44 | f_minimum_fee_check = False 45 | # TODO: POS tx need Multisig? f_signature_check 46 | if include_block.flag == C.BLOCK_COIN_POS: 47 | check_tx_pos_reward(tx=tx, include_block=include_block) 48 | elif include_block.flag == C.BLOCK_CAP_POS: 49 | f_signature_check = False 50 | f_inputs_origin_check = False 51 | check_tx_poc_reward(tx=tx, include_block=include_block) 52 | elif include_block.flag == C.BLOCK_FLK_POS: 53 | raise BlockChainError("unimplemented") 54 | else: 55 | raise BlockChainError('Unknown block type {}'.format(include_block.flag)) 56 | 57 | elif tx.type == C.TX_POW_REWARD: 58 | f_amount_check = False 59 | f_signature_check = False 60 | f_minimum_fee_check = False 61 | check_tx_pow_reward(tx=tx, include_block=include_block) 62 | 63 | elif tx.type == C.TX_TRANSFER: 64 | if not (0 < len(tx.inputs) < 256 and 0 < len(tx.outputs) < 256): 65 | raise BlockChainError('Input and output is 1~256') 66 | # payCoinFeeID is default 0, not only 0 67 | _address, payfee_coin_id, _amount = tx.outputs[0] 68 | 69 | elif tx.type == C.TX_MINT_COIN: 70 | f_amount_check = False 71 | f_minimum_fee_check = False 72 | f_signature_check = False 73 | check_tx_mint_coin(tx=tx, include_block=include_block) 74 | 75 | else: 76 | raise BlockChainError('Unknown tx type "{}"'.format(tx.type)) 77 | 78 | # Inputs origin チェック 79 | if f_inputs_origin_check: 80 | inputs_origin_check(tx=tx, include_block=include_block) 81 | 82 | # 残高移動チェック 83 | if f_amount_check: 84 | amount_check(tx=tx, payfee_coin_id=payfee_coin_id, include_block=include_block) 85 | 86 | # 署名チェック 87 | if f_signature_check: 88 | signature_check(tx=tx, include_block=include_block) 89 | 90 | # hash-locked check 91 | if tx.message_type == C.MSG_HASHLOCKED: 92 | check_hash_locked(tx=tx) 93 | else: 94 | if tx.R != b'': 95 | raise BlockChainError('Not hash-locked tx R={}'.format(tx.R)) 96 | 97 | # message type check 98 | if tx.message_type not in C.msg_type2name: 99 | raise BlockChainError('Not found message type {}'.format(tx.message_type)) 100 | 101 | # Feeチェック 102 | if f_minimum_fee_check: 103 | if tx.gas_amount < tx.size + C.SIGNATURE_GAS * len(tx.signature): 104 | raise BlockChainError('Too low fee [{}<{}+{}]'.format(tx.gas_amount, tx.size, 105 | C.SIGNATURE_GAS * len(tx.signature))) 106 | 107 | # TX size チェック 108 | if f_size_check: 109 | if tx.size > C.SIZE_TX_LIMIT: 110 | raise BlockChainError('TX size is too large. [{}>{}]'.format(tx.size, C.SIZE_TX_LIMIT)) 111 | 112 | if include_block: 113 | log.debug("check success {}".format(tx)) 114 | else: 115 | log.info("check unconfirmed tx hash={}".format(tx.hash.hex())) 116 | 117 | 118 | def check_tx_time(tx): 119 | # For unconfirmed tx 120 | now = int(time()) - V.BLOCK_GENESIS_TIME 121 | if tx.time > now + C.ACCEPT_MARGIN_TIME: 122 | raise BlockChainError('TX time too early. {}>{}+{}'.format(tx.time, now, C.ACCEPT_MARGIN_TIME)) 123 | if tx.deadline < now - C.ACCEPT_MARGIN_TIME: 124 | raise BlockChainError('TX time is too late. [{}<{}-{}]'.format(tx.deadline, now, C.ACCEPT_MARGIN_TIME)) 125 | # common check 126 | if tx.deadline - tx.time < 10800: 127 | raise BlockChainError('TX acceptable spam is too short. {}-{}<{}'.format(tx.deadline, tx.time, 10800)) 128 | if tx.deadline - tx.time > 3600 * 24 * 30: # 30days 129 | raise BlockChainError('TX acceptable spam is too long. {}-{}>{}'.format(tx.deadline, tx.time, 130 | 3600 * 24 * 30)) 131 | 132 | 133 | def check_hash_locked(tx): 134 | if len(tx.R) == 0: 135 | raise BlockChainError('R of Hash-locked is None type') 136 | if len(tx.R) > 64: 137 | raise BlockChainError('R is too large {}bytes'.format(len(tx.R))) 138 | size = len(tx.message) 139 | if size == 20: 140 | if hashlib.new('ripemd160', tx.R).digest() != tx.message: 141 | raise BlockChainError('Hash-locked check RIPEMD160 failed') 142 | elif size == 32: 143 | if hashlib.new('sha256', tx.R).digest() != tx.message: 144 | raise BlockChainError('Hash-locked check SHA256 failed') 145 | else: 146 | raise BlockChainError('H of Hash-locked is not correct size {}'.format(size)) 147 | 148 | 149 | def check_unconfirmed_order(best_block, ordered_unconfirmed_txs): 150 | if len(ordered_unconfirmed_txs) == 0: 151 | return None 152 | s = time() 153 | dummy_proof_tx = TX() 154 | dummy_proof_tx.type = C.TX_POW_REWARD, 155 | dummy_block = Block() 156 | dummy_block.height = best_block.height + 1 157 | dummy_block.previous_hash = best_block.hash 158 | dummy_block.txs.append(dummy_proof_tx) # dummy for proof tx 159 | dummy_block.txs.extend(ordered_unconfirmed_txs) 160 | tx = None 161 | try: 162 | for tx in ordered_unconfirmed_txs: 163 | if tx.type == C.TX_GENESIS: 164 | pass 165 | elif tx.type == C.TX_POS_REWARD: 166 | pass 167 | elif tx.type == C.TX_POW_REWARD: 168 | pass 169 | elif tx.type == C.TX_TRANSFER: 170 | pass 171 | elif tx.type == C.TX_MINT_COIN: 172 | check_tx_mint_coin(tx=tx, include_block=dummy_block) 173 | else: 174 | raise BlockChainError('Unknown tx type "{}"'.format(tx.type)) 175 | else: 176 | log.debug('Finish unconfirmed order check {}mSec'.format(int((time() - s) * 1000))) 177 | return None 178 | except Exception as e: 179 | log.warning(e, exc_info=True) 180 | # return errored tx 181 | return tx 182 | 183 | 184 | __all__ = [ 185 | "check_tx", 186 | "check_tx_time", 187 | "check_hash_locked", 188 | "check_unconfirmed_order", 189 | ] 190 | -------------------------------------------------------------------------------- /bc4py/user/txcreation/utils.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, BlockChainError 2 | from bc4py.bip32 import dummy_address 3 | from bc4py.database import obj 4 | from bc4py.database.account import sign_message_by_address, generate_new_address_by_userid 5 | from bc4py.database.tools import get_my_unspents_iter, get_unspents_iter 6 | from bc4py.user import Balance 7 | from logging import getLogger 8 | import asyncio 9 | 10 | loop = asyncio.get_event_loop() 11 | log = getLogger('bc4py') 12 | 13 | DUMMY_REDEEM_ADDRESS = dummy_address(b'_DUMMY_REDEEM_ADDR__') 14 | MAX_RECURSIVE_DEPTH = 20 15 | 16 | 17 | async def fill_inputs_outputs( 18 | tx, 19 | target_address=None, 20 | cur=None, 21 | signature_num=None, 22 | fee_coin_id=0, 23 | additional_gas=0, 24 | dust_percent=0.8, 25 | utxo_cache=None, 26 | depth=0): 27 | if MAX_RECURSIVE_DEPTH < depth: 28 | raise BlockChainError('over max recursive depth on filling inputs_outputs!') 29 | # outputsの合計を取得 30 | output_coins = Balance() 31 | for address, coin_id, amount in tx.outputs.copy(): 32 | if address == DUMMY_REDEEM_ADDRESS: 33 | # 償還Outputは再構築するので消す 34 | tx.outputs.remove((address, coin_id, amount)) 35 | continue 36 | output_coins[coin_id] += amount 37 | # 一時的にfeeの概算 38 | fee_coins = Balance(coin_id=fee_coin_id, amount=tx.gas_price * tx.gas_amount) 39 | # 必要なだけinputsを取得 40 | tx.inputs.clear() 41 | need_coins = output_coins + fee_coins 42 | input_coins = Balance() 43 | input_address = set() 44 | f_dust_skipped = False 45 | if utxo_cache is None: 46 | if target_address: 47 | utxo_iter = get_unspents_iter(target_address=target_address) 48 | elif cur: 49 | utxo_iter = await get_my_unspents_iter(cur=cur) 50 | else: 51 | raise Exception('target_address and cur is None?') 52 | cache = list() 53 | utxo_cache = [cache, utxo_iter] 54 | else: 55 | cache, utxo_iter = utxo_cache 56 | async for is_cache, (address, height, txhash, txindex, coin_id, amount) in sum_utxo_iter(cache, utxo_iter): 57 | if not is_cache: 58 | cache.append((address, height, txhash, txindex, coin_id, amount)) 59 | if coin_id not in need_coins: 60 | continue 61 | if need_coins[coin_id] * dust_percent > amount: 62 | f_dust_skipped = True 63 | continue 64 | need_coins[coin_id] -= amount 65 | input_coins[coin_id] += amount 66 | input_address.add(address) 67 | tx.inputs.append((txhash, txindex)) 68 | if need_coins.is_all_minus_amount(): 69 | break 70 | else: 71 | if f_dust_skipped and dust_percent > 0.00001: 72 | new_dust_percent = round(dust_percent * 0.7, 6) 73 | log.debug("Retry by lower dust percent. {}=>{}".format(dust_percent, new_dust_percent)) 74 | return await fill_inputs_outputs( 75 | tx=tx, 76 | target_address=target_address, 77 | cur=cur, 78 | signature_num=signature_num, 79 | fee_coin_id=fee_coin_id, 80 | additional_gas=additional_gas, 81 | dust_percent=new_dust_percent, 82 | utxo_cache=utxo_cache, 83 | depth=depth+1) 84 | elif len(tx.inputs) > 255: 85 | raise BlockChainError('TX inputs is too many num={}'.format(len(tx.inputs))) 86 | else: 87 | raise BlockChainError(f"Insufficient balance! " 88 | f"try to send {input_coins+need_coins}, " 89 | f"but you only have {input_coins}") 90 | # redeemを計算 91 | redeem_coins = input_coins - output_coins - fee_coins 92 | for coin_id, amount in redeem_coins: 93 | tx.outputs.append((DUMMY_REDEEM_ADDRESS, coin_id, amount)) 94 | # Feeをチェックし再計算するか決める 95 | tx.serialize() 96 | if signature_num is None: 97 | need_gas_amount = tx.size + additional_gas + len(input_address) * C.SIGNATURE_GAS 98 | else: 99 | need_gas_amount = tx.size + additional_gas + signature_num * C.SIGNATURE_GAS 100 | if tx.gas_amount > need_gas_amount: 101 | # swap overflowed gas, gas_amount => redeem_output 102 | swap_amount = (tx.gas_amount - need_gas_amount) * tx.gas_price 103 | for index, (address, coin_id, amount) in enumerate(tx.outputs): 104 | if address != DUMMY_REDEEM_ADDRESS: 105 | continue 106 | elif coin_id != fee_coin_id: 107 | continue 108 | else: 109 | tx.outputs[index] = (address, coin_id, amount + swap_amount) 110 | break 111 | else: 112 | raise BlockChainError('cannot swap overflowed gas amount={}'.format(swap_amount)) 113 | # success swap 114 | tx.gas_amount = need_gas_amount 115 | tx.serialize() 116 | return input_address 117 | elif tx.gas_amount < need_gas_amount: 118 | # retry insufficient gas 119 | log.info("retry calculate fee gasBefore={} gasNext={}".format(tx.gas_amount, need_gas_amount)) 120 | tx.gas_amount = need_gas_amount 121 | return await fill_inputs_outputs( 122 | tx=tx, 123 | target_address=target_address, 124 | cur=cur, 125 | signature_num=signature_num, 126 | fee_coin_id=fee_coin_id, 127 | additional_gas=additional_gas, 128 | dust_percent=dust_percent, 129 | utxo_cache=utxo_cache, 130 | depth=depth+1) 131 | else: 132 | # tx.gas_amount == need_gas_amount 133 | return input_address 134 | 135 | 136 | async def replace_redeem_dummy_address(tx, cur=None, replace_by=None): 137 | assert cur or replace_by 138 | new_redeem_address = set() 139 | for index, (address, coin_id, amount) in enumerate(tx.outputs): 140 | if address != DUMMY_REDEEM_ADDRESS: 141 | continue 142 | if replace_by is None: 143 | new_address = await generate_new_address_by_userid(user=C.ANT_UNKNOWN, cur=cur, is_inner=True) 144 | else: 145 | new_address = replace_by 146 | tx.outputs[index] = (new_address, coin_id, amount) 147 | new_redeem_address.add(new_address) 148 | tx.serialize() 149 | return new_redeem_address 150 | 151 | 152 | async def add_sign_by_address(tx, input_address, cur): 153 | """add addr related signatures to TX""" 154 | count = 0 155 | for address in input_address: 156 | sign_pairs = await sign_message_by_address(raw=tx.b, address=address, cur=cur) 157 | if sign_pairs not in tx.signature: 158 | tx.signature.append(sign_pairs) 159 | tx.verified_list.append(address) 160 | count += 1 161 | return count 162 | 163 | 164 | async def check_enough_amount(sender, send_coins, fee_coins, cur): 165 | assert isinstance(sender, int) 166 | from_coins = (await obj.account_builder.get_balance(cur=cur, confirm=6))[sender] 167 | remain_coins = from_coins - send_coins - fee_coins 168 | if not remain_coins.is_all_plus_amount(): 169 | raise BlockChainError('Not enough balance in id={} balance={} remains={}request_num' 170 | .format(sender, from_coins, remain_coins)) 171 | 172 | 173 | async def sum_utxo_iter(cache: list, utxo_iter): 174 | """return with flag is_cache""" 175 | for args in cache: 176 | yield True, args 177 | async for args in utxo_iter: 178 | yield False, args 179 | 180 | 181 | __all__ = [ 182 | "DUMMY_REDEEM_ADDRESS", 183 | "fill_inputs_outputs", 184 | "replace_redeem_dummy_address", 185 | "add_sign_by_address", 186 | "check_enough_amount", 187 | ] 188 | -------------------------------------------------------------------------------- /bc4py/database/tools.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, BlockChainError 2 | from bc4py.database import obj 3 | from bc4py.database.account import read_all_pooled_address_set 4 | from typing import TYPE_CHECKING, List, AsyncGenerator 5 | 6 | best_block_cache = None 7 | best_chain_cache = None 8 | target_address_cache = set() 9 | 10 | if TYPE_CHECKING: 11 | from bc4py.chain.block import Block 12 | 13 | 14 | def _get_best_chain_all(best_block): 15 | global best_block_cache, best_chain_cache 16 | # MemoryにおけるBestBlockまでのChainを返す 17 | if best_block is None: 18 | best_block_cache = best_chain_cache = None 19 | return obj.chain_builder.best_chain 20 | elif best_block_cache and best_block == best_block_cache: 21 | return best_chain_cache 22 | else: 23 | dummy, best_chain = obj.chain_builder.get_best_chain(best_block) 24 | # best_chain = [, ,.. ] 25 | if len(best_chain) == 0: 26 | raise BlockChainError('Ignore, New block inserted on "_get_best_chain_all"') 27 | best_block_cache = best_block 28 | best_chain_cache = best_chain 29 | return best_chain 30 | 31 | 32 | async def get_unspents_iter(target_address, best_block=None, best_chain=None) -> AsyncGenerator: 33 | """get unspents related by `target_address`""" 34 | if best_chain is None: 35 | best_chain = _get_best_chain_all(best_block) 36 | assert best_chain is not None, 'Cannot get best_chain by {}'.format(best_block) 37 | allow_mined_height = best_chain[0].height - C.MATURE_HEIGHT 38 | 39 | # database 40 | for address in target_address: 41 | for dummy, txhash, txindex, coin_id, amount, f_used in obj.tables.read_address_idx_iter(address): 42 | if f_used is False: 43 | if not is_unused_index(input_hash=txhash, input_index=txindex, best_block=best_block, best_chain=best_chain): 44 | continue # used 45 | tx = obj.tx_builder.get_account_tx(txhash) 46 | if tx.type in (C.TX_POW_REWARD, C.TX_POS_REWARD): 47 | if tx.height is not None and tx.height < allow_mined_height: 48 | yield address, tx.height, txhash, txindex, coin_id, amount 49 | else: 50 | yield address, tx.height, txhash, txindex, coin_id, amount 51 | 52 | # memory 53 | for block in reversed(best_chain): 54 | for tx in block.txs: 55 | for index, (address, coin_id, amount) in enumerate(tx.outputs): 56 | if not is_unused_index(input_hash=tx.hash, input_index=index, best_block=best_block, best_chain=best_chain): 57 | continue # used 58 | elif address in target_address: 59 | if tx.type in (C.TX_POW_REWARD, C.TX_POS_REWARD): 60 | if tx.height is not None and tx.height < allow_mined_height: 61 | yield address, tx.height, tx.hash, index, coin_id, amount 62 | else: 63 | yield address, tx.height, tx.hash, index, coin_id, amount 64 | 65 | # unconfirmed 66 | if best_block is None: 67 | for tx in obj.tx_builder.memory_pool.list_all_obj(False): 68 | for index, (address, coin_id, amount) in enumerate(tx.outputs): 69 | if not is_unused_index(input_hash=tx.hash, input_index=index, best_block=best_block, best_chain=best_chain): 70 | continue # used 71 | elif address in target_address: 72 | yield address, None, tx.hash, index, coin_id, amount 73 | # 返り値 74 | # address, height, txhash, index, coin_id, amount 75 | 76 | 77 | async def get_my_unspents_iter(cur, best_chain=None) -> AsyncGenerator: 78 | """get unspents of account control (for private)""" 79 | last_uuid = len(target_address_cache) 80 | target_address_cache.update(await read_all_pooled_address_set(cur=cur, last_uuid=last_uuid)) 81 | return get_unspents_iter(target_address=target_address_cache, best_block=None, best_chain=best_chain) 82 | 83 | 84 | def get_output_from_input(input_hash, input_index, best_block=None, best_chain=None): 85 | """get OutputType from InputType""" 86 | assert obj.chain_builder.best_block, 'Not Tables init' 87 | if best_chain is None: 88 | best_chain = _get_best_chain_all(best_block) 89 | 90 | # check database 91 | pair = obj.tables.read_unused_index(input_hash, input_index) 92 | if pair is not None: 93 | return pair 94 | 95 | # check memory 96 | for block in best_chain: 97 | for tx in block.txs: 98 | if tx.hash == input_hash: 99 | if input_index < len(tx.outputs): 100 | return tx.outputs[input_index] 101 | 102 | # check unconfirmed 103 | if best_block is None: 104 | for txhash in obj.tx_builder.memory_pool.list_all_hash(): 105 | if txhash == input_hash: 106 | tx = obj.tx_builder.memory_pool.get_obj(txhash) 107 | if input_index < len(tx.outputs): 108 | return tx.outputs[input_index] 109 | 110 | # not found 111 | return None 112 | 113 | 114 | def is_unused_index( 115 | input_hash: bytes, 116 | input_index: int, 117 | best_block: 'Block' = None, 118 | best_chain: List['Block'] = None 119 | ) -> bool: 120 | """check inputs is unused(True) or not(False) 121 | note: designed for first check on `get_unspents_iter()` 122 | """ 123 | assert obj.chain_builder.best_block, 'Not Tables init' 124 | if best_chain is None: 125 | best_chain = _get_best_chain_all(best_block) 126 | input_pair_tuple = (input_hash, input_index) 127 | 128 | is_unused = False 129 | 130 | # check database 131 | if obj.tables.read_unused_index(input_hash, input_index) is not None: 132 | is_unused = True 133 | 134 | # check memory 135 | for block in best_chain: 136 | for tx in block.txs: 137 | if tx.hash == input_hash: 138 | assert input_index < len(tx.outputs) 139 | is_unused = True 140 | if input_pair_tuple in tx.inputs: 141 | return False 142 | 143 | # check unconfirmed 144 | if best_block is None: 145 | for tx in obj.tx_builder.memory_pool.list_all_obj(False): 146 | if tx.hash == input_hash: 147 | assert input_index < len(tx.outputs) 148 | is_unused = True 149 | if input_pair_tuple in tx.inputs: 150 | return False 151 | 152 | # all check passed 153 | return is_unused 154 | 155 | 156 | def is_unused_index_except_me( 157 | input_hash: bytes, 158 | input_index: int, 159 | except_hash: bytes, 160 | best_block: 'Block' = None, 161 | best_chain: List['Block'] = None, 162 | ) -> bool: 163 | """check inputs is unused(True) or not(False) 164 | note: designed for duplication check on `check_tx()` 165 | """ 166 | assert obj.chain_builder.best_block, 'Not Tables init' 167 | if best_chain is None: 168 | best_chain = _get_best_chain_all(best_block) 169 | input_pair_tuple = (input_hash, input_index) 170 | 171 | is_unused = False 172 | 173 | # check database 174 | if obj.tables.read_unused_index(input_hash, input_index) is not None: 175 | is_unused = True 176 | 177 | # check memory 178 | for block in best_chain: 179 | for tx in block.txs: 180 | if tx.hash == except_hash: 181 | continue 182 | if tx.hash == input_hash: 183 | assert input_index < len(tx.outputs) 184 | is_unused = True 185 | if input_pair_tuple in tx.inputs: 186 | return False 187 | 188 | # check unconfirmed 189 | if best_block is None: 190 | for tx in obj.tx_builder.memory_pool.list_all_obj(False): 191 | if tx.hash == except_hash: 192 | continue 193 | if tx.hash == input_hash: 194 | assert input_index < len(tx.outputs) 195 | is_unused = True 196 | if input_pair_tuple in tx.inputs: 197 | return False 198 | 199 | # all check passed 200 | return is_unused 201 | 202 | 203 | __all__ = [ 204 | "get_unspents_iter", 205 | "get_my_unspents_iter", 206 | "get_output_from_input", 207 | "is_unused_index", 208 | "is_unused_index_except_me", 209 | ] 210 | -------------------------------------------------------------------------------- /bc4py/user/api/ep_account.py: -------------------------------------------------------------------------------- 1 | from bc4py.config import C, V 2 | from bc4py.user import Balance 3 | from bc4py.database import obj 4 | from bc4py.database.create import create_db 5 | from bc4py.database.account import * 6 | from bc4py.database.tools import get_unspents_iter, get_my_unspents_iter 7 | from bc4py.user.api.utils import error_response 8 | from pydantic import BaseModel 9 | from bc4py_extension import PyAddress 10 | from aioitertools import enumerate as aioenumerate 11 | from typing import Dict 12 | 13 | 14 | class MoveOne(BaseModel): 15 | amount: int 16 | sender: str = C.account2name[C.ANT_UNKNOWN] 17 | recipient: str 18 | coin_id: int = 0 19 | 20 | 21 | class MoveMany(BaseModel): 22 | sender: str 23 | recipient: str = C.account2name[C.ANT_UNKNOWN] 24 | coins: Dict[int, int] 25 | 26 | 27 | async def list_balance(confirm: int = 6): 28 | """ 29 | This end-point show all user's account balances. 30 | * minimum confirmation height, default 6 31 | * Arguments 32 | 1. **confirm** : confirmation height 33 | * About 34 | * Get all account balance. 35 | * Coin_id `0` is base currency. 36 | """ 37 | data = dict() 38 | async with create_db(V.DB_ACCOUNT_PATH) as db: 39 | cur = await db.cursor() 40 | users = await obj.account_builder.get_balance(cur=cur, confirm=confirm) 41 | for user, balance in users.items(): 42 | name = await read_userid2name(user, cur) 43 | data[name] = dict(balance) 44 | return data 45 | 46 | 47 | async def list_transactions(page: int = 0, limit: int = 25): 48 | """ 49 | This end-point show all account's recent transactions. 50 | * Arguments 51 | 1. **page** : page number. 52 | 2. **limit** : Number of TX included in Page. 53 | * About 54 | * `movement` inner account balance movement. 55 | * `next` next page exists. 56 | * If `height` is null, TX is on memory. 57 | * null height TX is older than recode limit or unconfirmed. 58 | """ 59 | data = list() 60 | f_next_page = False 61 | start = page * limit 62 | async for tx_dict in obj.account_builder.get_movement_iter(start=page, f_dict=True): 63 | if limit == 0: 64 | f_next_page = True 65 | break 66 | tx_dict['index'] = start 67 | data.append(tx_dict) 68 | start += 1 69 | limit -= 1 70 | return { 71 | 'txs': data, 72 | 'next': f_next_page, 73 | } 74 | 75 | 76 | async def list_unspents(address: str, page: int = 0, limit: int = 25): 77 | """ 78 | This end-point show address related unspents. 79 | * Arguments 80 | 1. **address** : some addresses joined with comma 81 | 2. **page** : page number. 82 | 3. **limit** : Number of TX included in Page. 83 | * About 84 | * display from Database -> Memory -> Unconfirmed 85 | """ 86 | if not obj.tables.table_config['addrindex']: 87 | return error_response('Cannot use this API, please set `addrindex` true if you want full indexed') 88 | try: 89 | best_height = obj.chain_builder.best_block.height 90 | start = page * limit 91 | finish = (page+1) * limit - 1 92 | f_next_page = False 93 | target_address = set(map(lambda x: PyAddress.from_string(x), address.split(','))) 94 | unspents_iter = get_unspents_iter(target_address=target_address) 95 | data = list() 96 | async for index, (address, height, txhash, txindex, coin_id, amount) in aioenumerate(unspents_iter): 97 | if finish < index: 98 | f_next_page = True 99 | break 100 | if index < start: 101 | continue 102 | data.append({ 103 | 'address': address.string, 104 | 'height': height, 105 | 'confirmed': None if height is None else best_height - height, 106 | 'txhash': txhash.hex(), 107 | 'txindex': txindex, 108 | 'coin_id': coin_id, 109 | 'amount': amount 110 | }) 111 | return { 112 | 'data': data, 113 | 'next': f_next_page, 114 | } 115 | except Exception: 116 | return error_response() 117 | 118 | 119 | async def list_private_unspents(): 120 | """ 121 | This end-point show all unspents of account have. 122 | * About 123 | * just looks same with /public/listunspents 124 | """ 125 | data = list() 126 | best_height = obj.chain_builder.best_block.height 127 | async with create_db(V.DB_ACCOUNT_PATH) as db: 128 | cur = await db.cursor() 129 | unspent_iter = await get_my_unspents_iter(cur) 130 | async for address, height, txhash, txindex, coin_id, amount in unspent_iter: 131 | data.append({ 132 | 'address': address.string, 133 | 'height': height, 134 | 'confirmed': None if height is None else best_height - height, 135 | 'txhash': txhash.hex(), 136 | 'txindex': txindex, 137 | 'coin_id': coin_id, 138 | 'amount': amount 139 | }) 140 | return data 141 | 142 | 143 | async def list_account_address(account: str = C.account2name[C.ANT_UNKNOWN]): 144 | """ 145 | This end-point show account all related addresses. 146 | * Arguments 147 | 1. **account** : default="@Unknown" Account name 148 | """ 149 | async with create_db(V.DB_ACCOUNT_PATH) as db: 150 | cur = await db.cursor() 151 | user_id = await read_name2userid(account, cur) 152 | address_list = await read_pooled_address_list(user_id, cur) 153 | return { 154 | 'account': account, 155 | 'user_id': user_id, 156 | 'address': [addr.string for addr in address_list], 157 | } 158 | 159 | 160 | async def move_one(movement: MoveOne): 161 | """ 162 | This end-point create inner transaction. 163 | * Arguments 164 | 1. **sender** : (string, optional, default="@Unknown") Account name 165 | 2. **recipient** : (string, required) Account name 166 | 3. **coin_id** : (numeric, optional, default=0) 167 | 4. **amount** : (numeric, required) 168 | * About 169 | * txhash = (zerofill 24bytes) + (time big endian 4bytes) + (random 4bytes) 170 | * caution! minus amount is allowed. 171 | """ 172 | try: 173 | coins = Balance(movement.coin_id, movement.amount) 174 | async with create_db(V.DB_ACCOUNT_PATH, strict=True) as db: 175 | cur = await db.cursor() 176 | from_user = await read_name2userid(movement.sender, cur) 177 | to_user = await read_name2userid(movement.recipient, cur) 178 | txhash = await obj.account_builder.move_balance(cur, from_user, to_user, coins) 179 | await db.commit() 180 | return { 181 | 'txhash': txhash.hex(), 182 | 'from_id': from_user, 183 | 'to_id': to_user, 184 | } 185 | except Exception: 186 | return error_response() 187 | 188 | 189 | async def move_many(movement: MoveMany): 190 | """ 191 | This end-point create inner transaction. 192 | * Arguments 193 | 1. **sender** : (string, optional, default="@Unknown") Account name. 194 | 2. **recipient** : (string, required) Account name. 195 | 3. **coins** : (object, required) {coinId: amount, ..} 196 | * About 197 | * coins is dictionary, key=coin_id, value=amount. 198 | * caution! minus amount is allowed, zero is not allowed. 199 | """ 200 | try: 201 | coins = Balance() 202 | for k, v in movement.coins.items(): 203 | assert 0 <= k and 0 < v 204 | coins[k] += v 205 | async with create_db(V.DB_ACCOUNT_PATH, strict=True) as db: 206 | cur = await db.cursor() 207 | from_user = await read_name2userid(movement.sender, cur) 208 | to_user = await read_name2userid(movement.recipient, cur) 209 | txhash = await obj.account_builder.move_balance(cur, from_user, to_user, coins) 210 | await db.commit() 211 | return { 212 | 'txhash': txhash.hex(), 213 | 'from_id': from_user, 214 | 'to_id': to_user, 215 | } 216 | except Exception: 217 | return error_response() 218 | 219 | 220 | __all__ = [ 221 | "list_balance", 222 | "list_transactions", 223 | "list_unspents", 224 | "list_private_unspents", 225 | "list_account_address", 226 | "move_one", 227 | "move_many", 228 | ] 229 | --------------------------------------------------------------------------------